From 2e2a1e478934f6f3ee4af91850414034cffcf3af Mon Sep 17 00:00:00 2001 From: Cassandra Heart <7929478+CassOnMars@users.noreply.github.com> Date: Wed, 3 Jan 2024 01:31:42 -0600 Subject: [PATCH] v1.2.0 (#31) --- .../pkg/core/curves/bls48581_curve.go | 317 +- .../pkg/core/curves/bls48581_curve_test.go | 16 +- nekryptology/pkg/core/curves/curve.go | 54 +- .../native/bls48581/{arch.go => arch_32.go} | 2 + .../core/curves/native/bls48581/arch_64.go | 28 + .../native/bls48581/{big.go => big_32.go} | 2 + .../pkg/core/curves/native/bls48581/big_64.go | 999 + .../pkg/core/curves/native/bls48581/bls256.go | 32 +- .../{config_big.go => config_big_32.go} | 2 + .../curves/native/bls48581/config_big_64.go | 36 + .../curves/native/bls48581/config_curve.go | 27 - .../{config_field.go => config_field_32.go} | 2 + .../curves/native/bls48581/config_field_64.go | 49 + .../pkg/core/curves/native/bls48581/dbig.go | 70 +- .../pkg/core/curves/native/bls48581/ecdh.go | 381 - .../pkg/core/curves/native/bls48581/fp.go | 485 +- .../pkg/core/curves/native/bls48581/fp16.go | 303 +- .../pkg/core/curves/native/bls48581/fp2.go | 414 +- .../pkg/core/curves/native/bls48581/fp4.go | 549 +- .../pkg/core/curves/native/bls48581/fp48.go | 1075 +- .../pkg/core/curves/native/bls48581/fp8.go | 413 +- .../pkg/core/curves/native/bls48581/g1.go | 1903 +- .../pkg/core/curves/native/bls48581/g2.go | 897 +- .../pkg/core/curves/native/bls48581/hpke.go | 328 - .../core/curves/native/bls48581/mpin256.go | 202 - .../pkg/core/curves/native/bls48581/pair8.go | 949 +- .../native/bls48581/{rom.go => rom_32.go} | 2 + .../pkg/core/curves/native/bls48581/rom_64.go | 77 + nekryptology/pkg/vdf/vdf.go | 21 +- nekryptology/pkg/vdf/wesolowski.go | 36 +- nekryptology/pkg/zkp/schnorr/schnorr_test.go | 4 +- node/.vscode/settings.json | 5 + node/app/db_console.go | 16 +- node/app/wire.go | 13 +- node/app/wire_gen.go | 26 +- node/config/engine.go | 4 + .../consensus/ceremony/broadcast_messaging.go | 344 +- .../ceremony_data_clock_consensus_engine.go | 95 +- node/consensus/ceremony/consensus_frames.go | 499 +- .../ceremony/execution_registration.go | 8 +- node/consensus/ceremony/peer_messaging.go | 139 +- node/consensus/consensus_engine.go | 31 +- node/consensus/master/broadcast_messaging.go | 8 +- node/consensus/master/consensus_frames.go | 17 +- .../master/execution_registration.go | 10 +- .../master/master_clock_consensus_engine.go | 218 +- node/consensus/master/peer_messaging.go | 49 +- node/crypto/kzg.go | 221 +- node/crypto/kzg_test.go | 54 +- .../application/ceremony_application.go | 66 +- .../ceremony_application_in_progress.go | 9 +- .../application/ceremony_application_open.go | 4 +- .../application/ceremony_application_test.go | 5 +- .../ceremony_application_validating.go | 140 +- .../ceremony_application_validating_test.go | 164 +- .../ceremony/ceremony_execution_engine.go | 176 +- node/go.mod | 10 +- node/go.sum | 173 +- node/keys/inmem.go | 197 + node/main.go | 4 +- node/p2p/bloom_utils.go | 55 +- node/p2p/bloom_utils_test.go | 91 + node/p2p/blossomsub.go | 14 +- node/poor_mans_cd.sh | 41 + node/protobufs/ceremony.pb.go | 359 +- node/protobufs/ceremony.proto | 4 +- node/protobufs/clock.go | 37 +- node/retroactive_peers.json | 1052 + node/store/clock.go | 145 +- node/store/data_proof.go | 20 +- node/store/inmem.go | 349 + node/store/inmem_test.go | 90 + node/store/iterator.go | 13 +- node/store/key.go | 47 +- node/store/kvdb.go | 16 + node/store/pebble.go | 52 +- pebble/.editorconfig | 10 + pebble/.github/workflows/ci.yaml | 160 + pebble/.github/workflows/code-cover-gen.yaml | 71 + .../.github/workflows/code-cover-publish.yaml | 55 + .../.github/workflows/nightly-code-cover.yaml | 48 + pebble/.github/workflows/sanitizers.yaml | 32 + pebble/.github/workflows/stale.yml | 34 + pebble/.gitignore | 9 + pebble/LICENSE | 27 + pebble/Makefile | 131 + pebble/README.md | 226 + pebble/batch.go | 2312 + pebble/batch_test.go | 1652 + pebble/bloom/bloom.go | 250 + pebble/bloom/bloom_test.go | 219 + pebble/cache.go | 23 + pebble/checkpoint.go | 428 + pebble/checkpoint_test.go | 415 + pebble/cleaner.go | 295 + pebble/cleaner_test.go | 137 + pebble/cmd/pebble/.gitignore | 1 + pebble/cmd/pebble/db.go | 168 + pebble/cmd/pebble/fsbench.go | 707 + pebble/cmd/pebble/fsbenchlist.go | 39 + pebble/cmd/pebble/main.go | 99 + pebble/cmd/pebble/mvcc.go | 223 + pebble/cmd/pebble/queue.go | 116 + pebble/cmd/pebble/random.go | 92 + pebble/cmd/pebble/replay.go | 448 + pebble/cmd/pebble/replay_test.go | 77 + pebble/cmd/pebble/scan.go | 160 + pebble/cmd/pebble/sync.go | 143 + pebble/cmd/pebble/test.go | 400 + pebble/cmd/pebble/tombstone.go | 134 + pebble/cmd/pebble/util.go | 15 + pebble/cmd/pebble/write_bench.go | 483 + pebble/cmd/pebble/ycsb.go | 609 + pebble/commit.go | 517 + pebble/commit_test.go | 355 + pebble/compaction.go | 3924 + pebble/compaction_iter.go | 1473 + pebble/compaction_iter_test.go | 382 + pebble/compaction_picker.go | 2068 + pebble/compaction_picker_test.go | 1593 + pebble/compaction_test.go | 3912 + pebble/comparer.go | 31 + pebble/data_test.go | 1426 + pebble/db.go | 3050 + pebble/db_test.go | 1969 + pebble/docs/RFCS/20211018_range_keys.md | 961 + ...20220112_pebble_sstable_format_versions.md | 290 + ...20311_pebble_flushable_ingested_sstable.md | 280 + pebble/docs/RFCS/20221122_virtual_sstable.md | 366 + pebble/docs/css/app.css | 117 + pebble/docs/index.html | 166 + pebble/docs/io_profiling.md | 231 + pebble/docs/js/app.js | 695 + pebble/docs/js/d3.v5.min.js | 2 + pebble/docs/js/write-throughput.js | 420 + pebble/docs/memory.md | 92 + pebble/docs/range_deletions.md | 471 + pebble/docs/rocksdb.md | 757 + pebble/error_iter.go | 86 + pebble/error_test.go | 429 + pebble/event.go | 767 + pebble/event_listener_test.go | 376 + pebble/example_test.go | 37 + pebble/external_iterator.go | 561 + pebble/external_iterator_test.go | 380 + pebble/filenames.go | 54 + pebble/filenames_test.go | 110 + pebble/flush_test.go | 117 + pebble/flushable.go | 254 + pebble/flushable_test.go | 168 + pebble/format_major_version.go | 678 + pebble/format_major_version_test.go | 580 + pebble/get_iter.go | 258 + pebble/get_iter_test.go | 576 + pebble/go.mod | 49 + pebble/go.sum | 666 + pebble/ingest.go | 2410 + pebble/ingest_test.go | 3516 + pebble/internal.go | 51 + pebble/internal/ackseq/ackseq.go | 83 + pebble/internal/arenaskl/LICENSE | 201 + pebble/internal/arenaskl/README.md | 93 + pebble/internal/arenaskl/arena.go | 125 + pebble/internal/arenaskl/arena_test.go | 53 + pebble/internal/arenaskl/flush_iterator.go | 88 + pebble/internal/arenaskl/iterator.go | 275 + pebble/internal/arenaskl/node.go | 133 + pebble/internal/arenaskl/race_test.go | 42 + pebble/internal/arenaskl/skl.go | 464 + pebble/internal/arenaskl/skl_test.go | 972 + pebble/internal/base/cleaner.go | 60 + pebble/internal/base/comparer.go | 260 + pebble/internal/base/comparer_test.go | 117 + pebble/internal/base/error.go | 28 + pebble/internal/base/filenames.go | 202 + pebble/internal/base/filenames_test.go | 114 + pebble/internal/base/internal.go | 502 + pebble/internal/base/internal_test.go | 226 + pebble/internal/base/iterator.go | 414 + pebble/internal/base/iterator_test.go | 89 + pebble/internal/base/lazy_value.go | 287 + pebble/internal/base/lazy_value_test.go | 74 + pebble/internal/base/logger.go | 158 + pebble/internal/base/merger.go | 133 + pebble/internal/base/metrics.go | 98 + pebble/internal/base/metrics_test.go | 79 + pebble/internal/base/options.go | 76 + pebble/internal/batchskl/README.md | 56 + pebble/internal/batchskl/iterator.go | 223 + pebble/internal/batchskl/skl.go | 442 + pebble/internal/batchskl/skl_test.go | 539 + pebble/internal/bytealloc/bytealloc.go | 69 + pebble/internal/cache/LICENSE | 21 + pebble/internal/cache/cgo_disabled.go | 10 + pebble/internal/cache/cgo_enabled.go | 10 + pebble/internal/cache/clockpro.go | 909 + pebble/internal/cache/clockpro_normal.go | 10 + pebble/internal/cache/clockpro_test.go | 279 + pebble/internal/cache/clockpro_tracing.go | 20 + pebble/internal/cache/entry.go | 155 + pebble/internal/cache/entry_invariants.go | 38 + pebble/internal/cache/entry_normal.go | 103 + pebble/internal/cache/refcnt_normal.go | 59 + pebble/internal/cache/refcnt_tracing.go | 66 + pebble/internal/cache/robin_hood.go | 320 + pebble/internal/cache/robin_hood_test.go | 241 + pebble/internal/cache/testdata/cache | 99991 ++++++++++++++++ pebble/internal/cache/value.go | 46 + pebble/internal/cache/value_invariants.go | 55 + pebble/internal/cache/value_normal.go | 57 + pebble/internal/constants/constants.go | 17 + pebble/internal/constants/constants_test.go | 20 + pebble/internal/crc/crc.go | 42 + pebble/internal/datatest/datatest.go | 140 + pebble/internal/dsl/dsl.go | 160 + pebble/internal/dsl/predicates.go | 136 + pebble/internal/fastrand/fastrand.go | 17 + pebble/internal/fastrand/fastrand_test.go | 86 + pebble/internal/humanize/humanize.go | 68 + pebble/internal/humanize/humanize_test.go | 38 + pebble/internal/humanize/testdata/humanize | 49 + pebble/internal/intern/intern.go | 27 + pebble/internal/intern/intern_test.go | 30 + pebble/internal/invalidating/iter.go | 168 + pebble/internal/invariants/finalizer_off.go | 14 + pebble/internal/invariants/finalizer_on.go | 17 + pebble/internal/invariants/off.go | 11 + pebble/internal/invariants/on.go | 11 + pebble/internal/invariants/race_off.go | 11 + pebble/internal/invariants/race_on.go | 11 + pebble/internal/itertest/datadriven.go | 196 + pebble/internal/keyspan/bounded.go | 268 + pebble/internal/keyspan/bounded_test.go | 69 + pebble/internal/keyspan/datadriven_test.go | 432 + pebble/internal/keyspan/defragment.go | 539 + pebble/internal/keyspan/defragment_test.go | 271 + pebble/internal/keyspan/doc.go | 13 + pebble/internal/keyspan/filter.go | 115 + pebble/internal/keyspan/filter_test.go | 79 + pebble/internal/keyspan/fragmenter.go | 483 + pebble/internal/keyspan/fragmenter_test.go | 320 + pebble/internal/keyspan/get.go | 53 + pebble/internal/keyspan/interleaving_iter.go | 1149 + .../keyspan/interleaving_iter_test.go | 291 + pebble/internal/keyspan/internal_iter_shim.go | 125 + pebble/internal/keyspan/iter.go | 220 + pebble/internal/keyspan/iter_test.go | 147 + pebble/internal/keyspan/level_iter.go | 521 + pebble/internal/keyspan/level_iter_test.go | 472 + pebble/internal/keyspan/merging_iter.go | 1209 + pebble/internal/keyspan/merging_iter_test.go | 252 + pebble/internal/keyspan/seek.go | 48 + pebble/internal/keyspan/seek_test.go | 63 + pebble/internal/keyspan/span.go | 467 + pebble/internal/keyspan/span_test.go | 98 + pebble/internal/keyspan/testdata/bounded_iter | 251 + pebble/internal/keyspan/testdata/covers_at | 91 + .../keyspan/testdata/defragmenting_iter | 395 + .../internal/keyspan/testdata/filtering_iter | 84 + pebble/internal/keyspan/testdata/fragmenter | 951 + .../keyspan/testdata/fragmenter_covers | 58 + .../keyspan/testdata/fragmenter_emit_order | 21 + .../testdata/fragmenter_truncate_and_flush_to | 113 + .../keyspan/testdata/fragmenter_values | 65 + .../keyspan/testdata/interleaving_iter | 998 + .../testdata/interleaving_iter_masking | 501 + pebble/internal/keyspan/testdata/iter | 55 + pebble/internal/keyspan/testdata/level_iter | 475 + pebble/internal/keyspan/testdata/merging_iter | 758 + pebble/internal/keyspan/testdata/seek | 309 + pebble/internal/keyspan/testdata/truncate | 318 + pebble/internal/keyspan/testdata/visible | 58 + pebble/internal/keyspan/testdata/visible_at | 58 + pebble/internal/keyspan/transformer.go | 50 + pebble/internal/keyspan/truncate.go | 73 + pebble/internal/keyspan/truncate_test.go | 94 + pebble/internal/lint/lint.go | 5 + pebble/internal/lint/lint_test.go | 301 + pebble/internal/manifest/btree.go | 1304 + pebble/internal/manifest/btree_test.go | 991 + pebble/internal/manifest/l0_sublevels.go | 2042 + pebble/internal/manifest/l0_sublevels_test.go | 620 + pebble/internal/manifest/level.go | 46 + pebble/internal/manifest/level_metadata.go | 748 + .../internal/manifest/level_metadata_test.go | 144 + pebble/internal/manifest/level_test.go | 64 + .../manifest/testdata/MANIFEST_import | Bin 0 -> 7457190 bytes .../manifest/testdata/file_metadata_bounds | 81 + .../internal/manifest/testdata/l0_sublevels | 1766 + .../internal/manifest/testdata/level_iterator | 128 + .../manifest/testdata/level_iterator_filtered | 537 + pebble/internal/manifest/testdata/overlaps | 566 + .../manifest/testdata/version_check_ordering | 302 + .../manifest/testdata/version_edit_apply | 191 + pebble/internal/manifest/version.go | 1561 + pebble/internal/manifest/version_edit.go | 1122 + pebble/internal/manifest/version_edit_test.go | 545 + pebble/internal/manifest/version_test.go | 429 + pebble/internal/manual/manual.go | 60 + pebble/internal/manual/manual_32bit.go | 13 + pebble/internal/manual/manual_64bit.go | 13 + pebble/internal/manual/manual_mips.go | 13 + pebble/internal/manual/manual_nocgo.go | 20 + pebble/internal/metamorphic/.gitignore | 2 + .../crossversion/crossversion_test.go | 409 + pebble/internal/metamorphic/doc.go | 7 + pebble/internal/metamorphic/meta_test.go | 93 + .../metamorphic/metaflags/meta_flags.go | 234 + .../internal/metamorphic/metarunner/main.go | 66 + pebble/internal/mkbench/main.go | 60 + pebble/internal/mkbench/split.go | 89 + pebble/internal/mkbench/split_test.go | 72 + pebble/internal/mkbench/testdata/README.md | 20 + pebble/internal/mkbench/testdata/data-symlink | 1 + pebble/internal/mkbench/testdata/data.js | 14 + .../pebble/write/size=1024/run_1/1.log.gz | Bin 0 -> 7215 bytes .../pebble/write/size=1024/run_1/2.log.gz | Bin 0 -> 7220 bytes .../ycsb/size=1024/run_1/1.ycsb_A.log.gz | Bin 0 -> 624 bytes .../ycsb/size=1024/run_1/1.ycsb_B.log.gz | Bin 0 -> 644 bytes .../ycsb/size=1024/run_1/1.ycsb_C.log.gz | Bin 0 -> 544 bytes .../ycsb/size=1024/run_1/1.ycsb_D.log.gz | Bin 0 -> 638 bytes .../ycsb/size=1024/run_1/1.ycsb_E.log.gz | Bin 0 -> 637 bytes .../ycsb/size=1024/run_1/1.ycsb_F.log.gz | Bin 0 -> 604 bytes .../ycsb/size=1024/run_1/2.ycsb_A.log.gz | Bin 0 -> 628 bytes .../ycsb/size=1024/run_1/2.ycsb_B.log.gz | Bin 0 -> 630 bytes .../ycsb/size=1024/run_1/2.ycsb_C.log.gz | Bin 0 -> 548 bytes .../ycsb/size=1024/run_1/2.ycsb_D.log.gz | Bin 0 -> 646 bytes .../ycsb/size=1024/run_1/2.ycsb_E.log.gz | Bin 0 -> 644 bytes .../ycsb/size=1024/run_1/2.ycsb_F.log.gz | Bin 0 -> 614 bytes .../ycsb/size=1024/run_1/3.ycsb_A.log.gz | Bin 0 -> 633 bytes .../ycsb/size=1024/run_1/3.ycsb_B.log.gz | Bin 0 -> 630 bytes .../ycsb/size=1024/run_1/3.ycsb_C.log.gz | Bin 0 -> 554 bytes .../ycsb/size=1024/run_1/3.ycsb_D.log.gz | Bin 0 -> 629 bytes .../ycsb/size=1024/run_1/3.ycsb_E.log.gz | Bin 0 -> 643 bytes .../ycsb/size=1024/run_1/3.ycsb_F.log.gz | Bin 0 -> 600 bytes .../pebble/ycsb/size=64/run_1/1.ycsb_A.log | 21 + .../pebble/ycsb/size=64/run_1/1.ycsb_B.log | 21 + .../pebble/ycsb/size=64/run_1/1.ycsb_C.log | 21 + .../pebble/ycsb/size=64/run_1/1.ycsb_D.log | 21 + .../pebble/ycsb/size=64/run_1/1.ycsb_E.log | 21 + .../pebble/ycsb/size=64/run_1/1.ycsb_F.log | 21 + .../pebble/ycsb/size=64/run_1/2.ycsb_A.log | 21 + .../pebble/ycsb/size=64/run_1/2.ycsb_B.log | 21 + .../pebble/ycsb/size=64/run_1/2.ycsb_C.log | 21 + .../pebble/ycsb/size=64/run_1/2.ycsb_D.log | 21 + .../pebble/ycsb/size=64/run_1/2.ycsb_E.log | 21 + .../pebble/ycsb/size=64/run_1/2.ycsb_F.log | 21 + .../pebble/ycsb/size=64/run_1/3.ycsb_A.log | 21 + .../pebble/ycsb/size=64/run_1/3.ycsb_B.log | 21 + .../pebble/ycsb/size=64/run_1/3.ycsb_C.log | 21 + .../pebble/ycsb/size=64/run_1/3.ycsb_D.log | 21 + .../pebble/ycsb/size=64/run_1/3.ycsb_E.log | 21 + .../pebble/ycsb/size=64/run_1/3.ycsb_F.log | 21 + .../pebble/write/size=1024/run_1/1.log.gz | Bin 0 -> 7138 bytes .../pebble/write/size=1024/run_1/2.log.gz | Bin 0 -> 7095 bytes .../ycsb/size=1024/run_1/1.ycsb_A.log.gz | Bin 0 -> 631 bytes .../ycsb/size=1024/run_1/1.ycsb_B.log.gz | Bin 0 -> 644 bytes .../ycsb/size=1024/run_1/1.ycsb_C.log.gz | Bin 0 -> 550 bytes .../ycsb/size=1024/run_1/1.ycsb_D.log.gz | Bin 0 -> 639 bytes .../ycsb/size=1024/run_1/1.ycsb_E.log.gz | Bin 0 -> 639 bytes .../ycsb/size=1024/run_1/1.ycsb_F.log.gz | Bin 0 -> 608 bytes .../ycsb/size=1024/run_1/2.ycsb_A.log.gz | Bin 0 -> 625 bytes .../ycsb/size=1024/run_1/2.ycsb_B.log.gz | Bin 0 -> 637 bytes .../ycsb/size=1024/run_1/2.ycsb_C.log.gz | Bin 0 -> 560 bytes .../ycsb/size=1024/run_1/2.ycsb_D.log.gz | Bin 0 -> 643 bytes .../ycsb/size=1024/run_1/2.ycsb_E.log.gz | Bin 0 -> 637 bytes .../ycsb/size=1024/run_1/2.ycsb_F.log.gz | Bin 0 -> 613 bytes .../ycsb/size=1024/run_1/3.ycsb_A.log.gz | Bin 0 -> 633 bytes .../ycsb/size=1024/run_1/3.ycsb_B.log.gz | Bin 0 -> 637 bytes .../ycsb/size=1024/run_1/3.ycsb_C.log.gz | Bin 0 -> 565 bytes .../ycsb/size=1024/run_1/3.ycsb_D.log.gz | Bin 0 -> 637 bytes .../ycsb/size=1024/run_1/3.ycsb_E.log.gz | Bin 0 -> 640 bytes .../ycsb/size=1024/run_1/3.ycsb_F.log.gz | Bin 0 -> 598 bytes .../ycsb/size=64/run_1/1.ycsb_A.log.bz2 | Bin 0 -> 603 bytes .../ycsb/size=64/run_1/1.ycsb_B.log.bz2 | Bin 0 -> 613 bytes .../ycsb/size=64/run_1/1.ycsb_C.log.bz2 | Bin 0 -> 560 bytes .../ycsb/size=64/run_1/1.ycsb_D.log.bz2 | Bin 0 -> 607 bytes .../ycsb/size=64/run_1/1.ycsb_E.log.bz2 | Bin 0 -> 615 bytes .../ycsb/size=64/run_1/1.ycsb_F.log.bz2 | Bin 0 -> 622 bytes .../ycsb/size=64/run_1/2.ycsb_A.log.bz2 | Bin 0 -> 599 bytes .../ycsb/size=64/run_1/2.ycsb_B.log.bz2 | Bin 0 -> 616 bytes .../ycsb/size=64/run_1/2.ycsb_C.log.bz2 | Bin 0 -> 553 bytes .../ycsb/size=64/run_1/2.ycsb_D.log.bz2 | Bin 0 -> 614 bytes .../ycsb/size=64/run_1/2.ycsb_E.log.bz2 | Bin 0 -> 610 bytes .../ycsb/size=64/run_1/2.ycsb_F.log.bz2 | Bin 0 -> 623 bytes .../ycsb/size=64/run_1/3.ycsb_A.log.bz2 | Bin 0 -> 603 bytes .../ycsb/size=64/run_1/3.ycsb_B.log.bz2 | Bin 0 -> 611 bytes .../ycsb/size=64/run_1/3.ycsb_C.log.bz2 | Bin 0 -> 553 bytes .../ycsb/size=64/run_1/3.ycsb_D.log.bz2 | Bin 0 -> 611 bytes .../ycsb/size=64/run_1/3.ycsb_E.log.bz2 | Bin 0 -> 610 bytes .../ycsb/size=64/run_1/3.ycsb_F.log.bz2 | Bin 0 -> 620 bytes ...-pebble-write-size=1024-run_1-summary.json | 10 + ...-pebble-write-size=1024-run_1-summary.json | 10 + .../testdata/write-throughput/summary.json | 18 + pebble/internal/mkbench/testutil.go | 84 + pebble/internal/mkbench/util.go | 49 + pebble/internal/mkbench/util_test.go | 32 + pebble/internal/mkbench/write.go | 608 + pebble/internal/mkbench/write_test.go | 113 + pebble/internal/mkbench/ycsb.go | 301 + pebble/internal/mkbench/ycsb_test.go | 78 + pebble/internal/pacertoy/pebble/main.go | 420 + pebble/internal/pacertoy/rocksdb/main.go | 383 + pebble/internal/private/batch.go | 19 + pebble/internal/private/sstable.go | 29 + pebble/internal/randvar/deck.go | 62 + pebble/internal/randvar/deck_test.go | 20 + pebble/internal/randvar/flag.go | 160 + pebble/internal/randvar/rand.go | 23 + pebble/internal/randvar/randvar.go | 25 + pebble/internal/randvar/skewed_latest.go | 77 + pebble/internal/randvar/skewed_latest_test.go | 68 + pebble/internal/randvar/uniform.go | 54 + pebble/internal/randvar/weighted.go | 41 + pebble/internal/randvar/weighted_test.go | 20 + pebble/internal/randvar/zipf.go | 147 + pebble/internal/randvar/zipf_test.go | 129 + pebble/internal/rangedel/rangedel.go | 43 + pebble/internal/rangekey/coalesce.go | 380 + pebble/internal/rangekey/coalesce_test.go | 416 + pebble/internal/rangekey/rangekey.go | 411 + pebble/internal/rangekey/rangekey_test.go | 239 + pebble/internal/rangekey/testdata/coalesce | 87 + .../rangekey/testdata/defragmenting_iter | 81 + pebble/internal/rangekey/testdata/iter | 201 + pebble/internal/rate/rate.go | 96 + pebble/internal/rawalloc/rawalloc.go | 24 + pebble/internal/rawalloc/rawalloc_32bit.go | 22 + pebble/internal/rawalloc/rawalloc_64bit.go | 22 + pebble/internal/rawalloc/rawalloc_gccgo.go | 23 + pebble/internal/rawalloc/rawalloc_go1.9.go | 28 + pebble/internal/rawalloc/rawalloc_mips.go | 22 + pebble/internal/rawalloc/rawalloc_test.go | 32 + pebble/internal/testkeys/strconv.go | 121 + pebble/internal/testkeys/testdata/divvy | 46 + pebble/internal/testkeys/testkeys.go | 512 + pebble/internal/testkeys/testkeys_test.go | 252 + pebble/internal_test.go | 88 + pebble/iterator.go | 3016 + pebble/iterator_example_test.go | 124 + pebble/iterator_histories_test.go | 392 + pebble/iterator_test.go | 2913 + pebble/level_checker.go | 769 + pebble/level_checker_test.go | 273 + pebble/level_iter.go | 1252 + pebble/level_iter_test.go | 717 + pebble/log_recycler.go | 108 + pebble/log_recycler_test.go | 135 + pebble/logger.go | 16 + pebble/mem_table.go | 421 + pebble/mem_table_test.go | 455 + pebble/merger.go | 37 + pebble/merging_iter.go | 1400 + pebble/merging_iter_heap.go | 92 + pebble/merging_iter_test.go | 729 + pebble/metamorphic/config.go | 189 + pebble/metamorphic/generator.go | 1588 + pebble/metamorphic/generator_test.go | 224 + pebble/metamorphic/history.go | 193 + pebble/metamorphic/history_test.go | 56 + pebble/metamorphic/key_manager.go | 580 + pebble/metamorphic/key_manager_test.go | 390 + pebble/metamorphic/meta.go | 621 + pebble/metamorphic/ops.go | 1557 + pebble/metamorphic/options.go | 676 + pebble/metamorphic/options_test.go | 234 + pebble/metamorphic/parser.go | 599 + pebble/metamorphic/parser_test.go | 66 + pebble/metamorphic/retryable.go | 181 + pebble/metamorphic/test.go | 443 + pebble/metamorphic/testdata/key_manager | 288 + pebble/metamorphic/testdata/parser | 40 + pebble/metamorphic/testdata/reorder_history | 33 + pebble/metamorphic/utils.go | 104 + pebble/metrics.go | 627 + pebble/metrics_test.go | 375 + pebble/objstorage/noop_readahead.go | 34 + pebble/objstorage/objstorage.go | 330 + .../objiotracing/obj_io_tracing.go | 68 + .../objiotracing/obj_io_tracing_off.go | 59 + .../objiotracing/obj_io_tracing_on.go | 410 + .../objiotracing/obj_io_tracing_test.go | 130 + .../objstorage/objstorageprovider/provider.go | 524 + .../objstorageprovider/provider_test.go | 601 + .../objstorageprovider/readahead.go | 201 + .../objstorageprovider/readahead_test.go | 59 + .../objstorage/objstorageprovider/remote.go | 377 + .../objstorageprovider/remote_backing.go | 304 + .../objstorageprovider/remote_backing_test.go | 158 + .../objstorageprovider/remote_obj_name.go | 91 + .../remote_obj_name_test.go | 64 + .../objstorageprovider/remote_readable.go | 162 + .../remoteobjcat/catalog.go | 388 + .../remoteobjcat/catalog_test.go | 183 + .../remoteobjcat/testdata/catalog | 275 + .../remoteobjcat/version_edit.go | 254 + .../remoteobjcat/version_edit_test.go | 92 + .../objstorageprovider/shared_writable.go | 65 + .../sharedcache/shared_cache.go | 900 + .../sharedcache/shared_cache_helpers_test.go | 7 + .../sharedcache/shared_cache_internal_test.go | 90 + .../sharedcache/shared_cache_test.go | 265 + .../testdata/cache/compaction_reads | 26 + .../sharedcache/testdata/cache/eof_handling | 14 + .../sharedcache/testdata/cache/lru | 33 + .../cache/read_larger_than_two_cache_shards | 16 + .../cache/read_that_hits_two_cache_blocks | 20 + ...cache_blocks_with_first_read_at_big_offset | 16 + ...two_cache_blocks_with_first_read_at_offset | 16 + .../cache/read_that_hits_two_cache_shards | 20 + ...two_cache_shards_with_first_read_at_offset | 16 + .../sharedcache/testdata/cache/small_read | 20 + .../small_read_with_first_read_at_offset | 20 + .../testdata/provider/local | 100 + .../testdata/provider/local_readahead | 51 + .../testdata/provider/shared_attach | 150 + .../provider/shared_attach_after_unref | 91 + .../testdata/provider/shared_attach_multi | 92 + .../testdata/provider/shared_basic | 132 + .../testdata/provider/shared_no_ref | 178 + .../testdata/provider/shared_readahead | 87 + .../testdata/provider/shared_remove | 105 + .../objstorageprovider/testdata/readahead | 221 + pebble/objstorage/objstorageprovider/vfs.go | 109 + .../objstorageprovider/vfs_readable.go | 216 + .../objstorageprovider/vfs_writable.go | 65 + pebble/objstorage/remote/factory.go | 25 + pebble/objstorage/remote/localfs.go | 118 + pebble/objstorage/remote/logging.go | 139 + pebble/objstorage/remote/mem.go | 161 + pebble/objstorage/remote/storage.go | 133 + pebble/open.go | 1191 + pebble/open_test.go | 1390 + pebble/options.go | 1737 + pebble/options_test.go | 325 + pebble/pacer.go | 190 + pebble/pacer_test.go | 198 + pebble/range_del_test.go | 653 + pebble/range_keys.go | 713 + pebble/rangekey/rangekey.go | 33 + pebble/read_compaction_queue.go | 94 + pebble/read_state.go | 106 + pebble/read_state_test.go | 46 + pebble/record/log_writer.go | 771 + pebble/record/log_writer_test.go | 593 + pebble/record/record.go | 644 + pebble/record/record_test.go | 1064 + pebble/record/rotation.go | 82 + pebble/record/rotation_test.go | 49 + pebble/record/testdata/rotation | 65 + pebble/replay/replay.go | 1145 + pebble/replay/replay_test.go | 585 + pebble/replay/sampled_metric.go | 135 + pebble/replay/sampled_metric_test.go | 75 + .../replay/testdata/collect/clean_before_copy | 40 + .../replay/testdata/collect/copy_before_clean | 51 + .../replay/testdata/collect/manifest_copying | 81 + pebble/replay/testdata/collect/start_stop | 98 + .../replay/testdata/corpus/findManifestStart | 128 + .../replay/testdata/corpus/findWorkloadFiles | 77 + pebble/replay/testdata/corpus/high_read_amp | 121 + pebble/replay/testdata/corpus/simple | 87 + pebble/replay/testdata/flushed_sstable_keys | 65 + pebble/replay/testdata/replay | 108 + pebble/replay/testdata/replay_paced | 80 + pebble/replay/testdata/sampled_metric | 70 + pebble/replay/workload_capture.go | 438 + pebble/replay/workload_capture_test.go | 200 + pebble/scan_internal.go | 1016 + pebble/scan_internal_test.go | 566 + pebble/scripts/changed-go-pkgs.sh | 14 + pebble/scripts/code-coverage-publish.sh | 47 + pebble/scripts/code-coverage.sh | 49 + pebble/scripts/pr-codecov-run-tests.sh | 44 + pebble/scripts/run-crossversion-meta.sh | 46 + pebble/shims/cmp/cmp.go | 74 + pebble/shims/slices/slices.go | 519 + pebble/shims/slices/sort.go | 202 + pebble/shims/slices/zsortanyfunc.go | 482 + pebble/shims/slices/zsortordered.go | 484 + pebble/snapshot.go | 560 + pebble/snapshot_test.go | 384 + pebble/sstable/block.go | 1863 + pebble/sstable/block_property.go | 820 + pebble/sstable/block_property_test.go | 1487 + pebble/sstable/block_property_test_utils.go | 117 + pebble/sstable/block_test.go | 513 + pebble/sstable/buffer_pool.go | 148 + pebble/sstable/buffer_pool_test.go | 78 + pebble/sstable/category_stats.go | 172 + pebble/sstable/comparer.go | 34 + pebble/sstable/compression.go | 99 + pebble/sstable/compression_cgo.go | 34 + pebble/sstable/compression_cgo_test.go | 19 + pebble/sstable/compression_nocgo.go | 30 + pebble/sstable/compression_nocgo_test.go | 19 + pebble/sstable/compression_test.go | 60 + pebble/sstable/data_test.go | 500 + pebble/sstable/filter.go | 122 + pebble/sstable/format.go | 257 + pebble/sstable/format_test.go | 96 + pebble/sstable/internal.go | 36 + pebble/sstable/layout.go | 307 + pebble/sstable/options.go | 305 + pebble/sstable/properties.go | 450 + pebble/sstable/properties_test.go | 146 + pebble/sstable/random_test.go | 395 + pebble/sstable/raw_block.go | 262 + pebble/sstable/reader.go | 1268 + pebble/sstable/reader_iter.go | 291 + pebble/sstable/reader_iter_single_lvl.go | 1413 + pebble/sstable/reader_iter_two_lvl.go | 1092 + pebble/sstable/reader_test.go | 2117 + pebble/sstable/reader_virtual.go | 213 + pebble/sstable/suffix_rewriter.go | 589 + pebble/sstable/suffix_rewriter_test.go | 278 + pebble/sstable/table.go | 455 + pebble/sstable/table_test.go | 867 + pebble/sstable/testdata/.gitignore | 1 + pebble/sstable/testdata/Makefile | 17 + pebble/sstable/testdata/block | 135 + pebble/sstable/testdata/block_properties | 671 + .../testdata/block_properties_boundlimited | 167 + pebble/sstable/testdata/buffer_pool | 84 + .../testdata/h.block-bloom.no-compression.sst | Bin 0 -> 30659 bytes pebble/sstable/testdata/h.ldb | Bin 0 -> 15381 bytes pebble/sstable/testdata/h.no-compression.sst | Bin 0 -> 28408 bytes .../h.no-compression.two_level_index.sst | Bin 0 -> 28539 bytes pebble/sstable/testdata/h.sst | Bin 0 -> 15432 bytes ...n.prefix_extractor.no_whole_key_filter.sst | Bin 0 -> 30716 bytes .../testdata/h.table-bloom.no-compression.sst | Bin 0 -> 30697 bytes pebble/sstable/testdata/h.table-bloom.sst | Bin 0 -> 17722 bytes pebble/sstable/testdata/h.txt | 1710 + .../sstable/testdata/h.zstd-compression.sst | Bin 0 -> 11826 bytes pebble/sstable/testdata/hamlet-act-1.txt | 1234 + .../sstable/testdata/hamletreader/hamlet_iter | 389 + pebble/sstable/testdata/make-table.cc | 245 + pebble/sstable/testdata/prefixreader/bloom | 22 + pebble/sstable/testdata/prefixreader/iter | 270 + pebble/sstable/testdata/reader/bloom | 22 + pebble/sstable/testdata/reader/iter | 720 + .../sstable/testdata/reader_bpf/Pebblev2/iter | 56 + .../sstable/testdata/reader_bpf/Pebblev3/iter | 146 + .../testdata/reader_hide_obsolete/iter | 318 + .../sstable/testdata/readerstats_LevelDB/iter | 59 + .../testdata/readerstats_Pebblev3/iter | 79 + pebble/sstable/testdata/rewriter | 237 + pebble/sstable/testdata/rewriter_v3 | 237 + pebble/sstable/testdata/size_estimate | 144 + pebble/sstable/testdata/virtual_reader | 692 + pebble/sstable/testdata/writer | 370 + pebble/sstable/testdata/writer_range_keys | 135 + pebble/sstable/testdata/writer_v3 | 343 + pebble/sstable/testdata/writer_value_blocks | 330 + pebble/sstable/unsafe.go | 35 + pebble/sstable/unsafe_test.go | 75 + pebble/sstable/value_block.go | 957 + pebble/sstable/value_block_test.go | 111 + pebble/sstable/write_queue.go | 140 + pebble/sstable/writer.go | 2450 + pebble/sstable/writer_fixture_test.go | 191 + pebble/sstable/writer_rangekey_test.go | 124 + pebble/sstable/writer_test.go | 1075 + pebble/table_cache.go | 1208 + pebble/table_cache_test.go | 1263 + pebble/table_stats.go | 1086 + pebble/table_stats_test.go | 277 + pebble/testdata/.gitignore | 1 + pebble/testdata/Makefile | 8 + pebble/testdata/batch_get | 114 + pebble/testdata/batch_range_ops | 140 + pebble/testdata/batch_reader | 98 + pebble/testdata/checkpoint | 811 + pebble/testdata/cleaner | 243 + pebble/testdata/compaction_allow_zero_seqnum | 72 + pebble/testdata/compaction_atomic_unit_bounds | 61 + pebble/testdata/compaction_check_ordering | 161 + pebble/testdata/compaction_delete_only_hints | 417 + pebble/testdata/compaction_elide_tombstone | 208 + .../compaction_error_on_user_key_overlap | 20 + pebble/testdata/compaction_expand_inputs | 78 + .../compaction_find_grandparent_limit | 100 + pebble/testdata/compaction_find_l0_limit | 88 + pebble/testdata/compaction_inuse_key_ranges | 409 + pebble/testdata/compaction_iter | 1218 + pebble/testdata/compaction_iter_delete_sized | 1897 + pebble/testdata/compaction_iter_set_with_del | 1417 + pebble/testdata/compaction_output_file_size | 55 + pebble/testdata/compaction_output_level | 199 + pebble/testdata/compaction_output_splitters | 112 + pebble/testdata/compaction_picker_L0 | 432 + pebble/testdata/compaction_picker_concurrency | 172 + .../testdata/compaction_picker_estimated_debt | 108 + .../compaction_picker_level_max_bytes | 148 + pebble/testdata/compaction_picker_pick_file | 106 + .../testdata/compaction_picker_read_triggered | 145 + pebble/testdata/compaction_picker_scores | 264 + .../testdata/compaction_picker_target_level | 1312 + pebble/testdata/compaction_read_triggered | 166 + pebble/testdata/compaction_setup_inputs | 153 + .../compaction_setup_inputs_multilevel_dummy | 18 + ...mpaction_setup_inputs_multilevel_write_amp | 339 + pebble/testdata/compaction_tombstones | 419 + pebble/testdata/compaction_transform | 116 + pebble/testdata/concurrent_excise | 176 + pebble/testdata/db-stage-1/000002.log | Bin 0 -> 11 bytes pebble/testdata/db-stage-1/CURRENT | 1 + pebble/testdata/db-stage-1/LOCK | 0 pebble/testdata/db-stage-1/MANIFEST-000001 | Bin 0 -> 52 bytes pebble/testdata/db-stage-1/OPTIONS-000003 | 48 + .../marker.format-version.000012.013 | 0 .../marker.manifest.000001.MANIFEST-000001 | 0 pebble/testdata/db-stage-2/000002.log | Bin 0 -> 170 bytes pebble/testdata/db-stage-2/CURRENT | 1 + pebble/testdata/db-stage-2/LOCK | 0 pebble/testdata/db-stage-2/MANIFEST-000001 | Bin 0 -> 52 bytes pebble/testdata/db-stage-2/OPTIONS-000003 | 48 + .../marker.format-version.000012.013 | 0 .../marker.manifest.000001.MANIFEST-000001 | 0 pebble/testdata/db-stage-3/000004.sst | Bin 0 -> 709 bytes pebble/testdata/db-stage-3/000005.log | Bin 0 -> 11 bytes pebble/testdata/db-stage-3/CURRENT | 1 + pebble/testdata/db-stage-3/LOCK | 0 pebble/testdata/db-stage-3/MANIFEST-000001 | Bin 0 -> 52 bytes pebble/testdata/db-stage-3/MANIFEST-000006 | Bin 0 -> 93 bytes pebble/testdata/db-stage-3/OPTIONS-000007 | 48 + .../marker.format-version.000012.013 | 0 .../marker.manifest.000002.MANIFEST-000006 | 0 pebble/testdata/db-stage-4/000004.sst | Bin 0 -> 709 bytes pebble/testdata/db-stage-4/000005.log | Bin 0 -> 105 bytes pebble/testdata/db-stage-4/CURRENT | 1 + pebble/testdata/db-stage-4/LOCK | 0 pebble/testdata/db-stage-4/MANIFEST-000001 | Bin 0 -> 52 bytes pebble/testdata/db-stage-4/MANIFEST-000006 | Bin 0 -> 93 bytes pebble/testdata/db-stage-4/OPTIONS-000007 | 48 + .../marker.format-version.000012.013 | 0 .../marker.manifest.000002.MANIFEST-000006 | 0 pebble/testdata/delete_range | 89 + pebble/testdata/event_listener | 462 + pebble/testdata/excise | 339 + pebble/testdata/external_iterator | 286 + pebble/testdata/flushable_batch | 156 + pebble/testdata/flushable_ingest | 627 + .../format_major_version_pebblev1_migration | 170 + ...mat_major_version_split_user_key_migration | 148 + pebble/testdata/frontiers | 71 + pebble/testdata/indexed_batch_mutation | 759 + pebble/testdata/ingest | 1180 + pebble/testdata/ingest_external | 122 + pebble/testdata/ingest_load | 152 + pebble/testdata/ingest_memtable_overlaps | 175 + pebble/testdata/ingest_shared | 1125 + pebble/testdata/ingest_shared_lower | 1108 + pebble/testdata/ingest_sort_and_verify | 180 + pebble/testdata/ingest_target_level | 324 + pebble/testdata/ingest_update_seqnums | 142 + pebble/testdata/ingested_flushable_api | 112 + pebble/testdata/internal_iter_bounds | 36 + pebble/testdata/internal_iter_next | 82 + pebble/testdata/iter_histories/clone | 151 + pebble/testdata/iter_histories/errors | 99 + pebble/testdata/iter_histories/internal_next | 374 + .../iter_histories/iter_optimizations | 723 + .../iter_histories/lazy_combined_iteration | 279 + pebble/testdata/iter_histories/merge | 37 + pebble/testdata/iter_histories/next_prefix | 292 + .../testdata/iter_histories/prefix_iteration | 335 + .../testdata/iter_histories/range_key_changed | 274 + .../testdata/iter_histories/range_key_masking | 243 + .../testdata/iter_histories/range_keys_simple | 508 + pebble/testdata/iter_histories/set_options | 56 + pebble/testdata/iter_histories/skip_point | 115 + .../iter_histories/stats_no_invariants | 159 + pebble/testdata/iter_histories/with_limit | 206 + pebble/testdata/iterator | 1706 + .../testdata/iterator_block_interval_filter | 306 + pebble/testdata/iterator_bounds_lifetimes | 83 + pebble/testdata/iterator_next_prev | 278 + pebble/testdata/iterator_read_sampling | 351 + pebble/testdata/iterator_seek_opt | 317 + pebble/testdata/iterator_seek_opt_errors | 87 + pebble/testdata/iterator_stats | 80 + pebble/testdata/iterator_table_filter | 66 + pebble/testdata/level_checker | 358 + pebble/testdata/level_iter | 500 + pebble/testdata/level_iter_boundaries | 382 + pebble/testdata/level_iter_seek | 391 + pebble/testdata/make-db.go | 96 + pebble/testdata/manual_compaction | 1236 + .../manual_compaction_file_boundaries | 518 + ...manual_compaction_file_boundaries_delsized | 520 + pebble/testdata/manual_compaction_multilevel | 121 + pebble/testdata/manual_compaction_range_keys | 48 + .../testdata/manual_compaction_set_with_del | 863 + ...l_compaction_set_with_del_sstable_Pebblev4 | 811 + pebble/testdata/manual_flush | 77 + pebble/testdata/marked_for_compaction | 28 + pebble/testdata/merging_iter | 592 + pebble/testdata/merging_iter_seek | 303 + pebble/testdata/metrics | 720 + pebble/testdata/point_collapsing_iter | 67 + pebble/testdata/range_del | 1543 + pebble/testdata/read_compaction_queue | 627 + .../testdata/rocksdb-ingest-only/000003.log | 0 .../testdata/rocksdb-ingest-only/000006.sst | Bin 0 -> 1187 bytes pebble/testdata/rocksdb-ingest-only/CURRENT | 1 + pebble/testdata/rocksdb-ingest-only/IDENTITY | 1 + pebble/testdata/rocksdb-ingest-only/LOCK | 0 .../rocksdb-ingest-only/MANIFEST-000001 | Bin 0 -> 13 bytes .../rocksdb-ingest-only/MANIFEST-000007 | Bin 0 -> 135 bytes .../rocksdb-ingest-only/OPTIONS-000005 | 158 + pebble/testdata/scan_internal | 464 + pebble/testdata/scan_statistics | 146 + pebble/testdata/simple_level_iter | 77 + pebble/testdata/singledel_manual_compaction | 77 + .../singledel_manual_compaction_set_with_del | 282 + pebble/testdata/snapshot | 180 + pebble/testdata/sstable_key_compare | 96 + pebble/testdata/table_stats | 911 + pebble/testdata/table_stats_deletion_iter | 199 + pebble/testdata/version_check_consistency | 70 + pebble/tool/data/d3.v5.min.js | 2 + pebble/tool/data/lsm.css | 100 + pebble/tool/data/lsm.js | 797 + pebble/tool/data_test.go | 136 + pebble/tool/db.go | 777 + pebble/tool/db_io_bench.go | 316 + pebble/tool/db_test.go | 11 + pebble/tool/find.go | 641 + pebble/tool/find_test.go | 11 + pebble/tool/logs/compaction.go | 1230 + pebble/tool/logs/compaction_test.go | 467 + pebble/tool/logs/testdata/compactions-23-1 | 499 + pebble/tool/logs/testdata/compactions-latest | 499 + pebble/tool/logs/tool.go | 32 + pebble/tool/lsm.go | 439 + pebble/tool/lsm_data.go | 906 + pebble/tool/make_incorrect_manifests.go | 61 + pebble/tool/make_lsm_data.sh | 28 + pebble/tool/make_test_find_db.go | 171 + pebble/tool/make_test_remotecat.go | 77 + pebble/tool/make_test_sstables.go | 46 + pebble/tool/manifest.go | 576 + pebble/tool/manifest_test.go | 11 + pebble/tool/remotecat.go | 132 + pebble/tool/remotecat_test.go | 11 + pebble/tool/sstable.go | 564 + pebble/tool/sstable_test.go | 11 + pebble/tool/testdata/MANIFEST-invalid | Bin 0 -> 130 bytes pebble/tool/testdata/REMOTE-OBJ-CATALOG | Bin 0 -> 82 bytes pebble/tool/testdata/bad-magic.sst | Bin 0 -> 986 bytes .../corrupt-options-db/OPTIONS-000002 | 1 + pebble/tool/testdata/corrupted.sst | Bin 0 -> 986 bytes pebble/tool/testdata/db_check | 44 + pebble/tool/testdata/db_checkpoint | 18 + pebble/tool/testdata/db_get_set | 58 + pebble/tool/testdata/db_lsm | 39 + pebble/tool/testdata/db_properties | 72 + pebble/tool/testdata/db_scan | 88 + pebble/tool/testdata/db_space | 38 + pebble/tool/testdata/find | 140 + pebble/tool/testdata/find-db/000009.log | Bin 0 -> 11 bytes pebble/tool/testdata/find-db/CURRENT | 1 + pebble/tool/testdata/find-db/LOCK | 0 pebble/tool/testdata/find-db/MANIFEST-000001 | Bin 0 -> 422 bytes pebble/tool/testdata/find-db/OPTIONS-000003 | 36 + .../tool/testdata/find-db/archive/000002.log | Bin 0 -> 161 bytes .../tool/testdata/find-db/archive/000004.log | Bin 0 -> 99 bytes .../tool/testdata/find-db/archive/000005.sst | Bin 0 -> 784 bytes .../tool/testdata/find-db/archive/000006.sst | Bin 0 -> 817 bytes .../tool/testdata/find-db/archive/000007.sst | Bin 0 -> 808 bytes .../tool/testdata/find-db/archive/000008.sst | Bin 0 -> 791 bytes .../tool/testdata/find-db/archive/000010.sst | Bin 0 -> 834 bytes .../tool/testdata/find-db/archive/000011.sst | Bin 0 -> 898 bytes pebble/tool/testdata/find-mixed/000001.sst | 1 + pebble/tool/testdata/find-mixed/000002.sst | Bin 0 -> 817 bytes pebble/tool/testdata/manifest_dump | 399 + pebble/tool/testdata/manifest_summarize | 45 + pebble/tool/testdata/mixed/000002.log | Bin 0 -> 214 bytes pebble/tool/testdata/mixed/000004.log | Bin 0 -> 53 bytes pebble/tool/testdata/mixed/000005.sst | Bin 0 -> 1166 bytes pebble/tool/testdata/mixed/CURRENT | 1 + pebble/tool/testdata/mixed/LOCK | 0 pebble/tool/testdata/mixed/MANIFEST-000001 | Bin 0 -> 121 bytes pebble/tool/testdata/mixed/OPTIONS-000003 | 44 + pebble/tool/testdata/mixed/main.go | 107 + .../mixed/marker.format-version.000010.011 | 0 .../marker.manifest.000001.MANIFEST-000001 | 0 pebble/tool/testdata/out-of-order.sst | Bin 0 -> 833 bytes pebble/tool/testdata/remotecat | 33 + pebble/tool/testdata/sstable_check | 59 + pebble/tool/testdata/sstable_layout | 3894 + pebble/tool/testdata/sstable_properties | 229 + pebble/tool/testdata/sstable_scan | 402 + pebble/tool/testdata/sstable_space | 68 + pebble/tool/testdata/wal_dump | 118 + pebble/tool/tool.go | 152 + pebble/tool/util.go | 325 + pebble/tool/wal.go | 171 + pebble/tool/wal_test.go | 11 + pebble/version_set.go | 1009 + pebble/version_set_test.go | 509 + pebble/vfs/atomicfs/marker.go | 241 + pebble/vfs/atomicfs/marker_test.go | 295 + pebble/vfs/atomicfs/testdata/marker | 136 + pebble/vfs/clone.go | 137 + pebble/vfs/default_linux.go | 132 + pebble/vfs/default_unix.go | 49 + pebble/vfs/default_windows.go | 61 + pebble/vfs/disk_full.go | 453 + pebble/vfs/disk_full_test.go | 254 + pebble/vfs/disk_health.go | 806 + pebble/vfs/disk_health_test.go | 571 + pebble/vfs/disk_usage_linux.go | 44 + pebble/vfs/disk_usage_netbsd.go | 26 + pebble/vfs/disk_usage_openbsd.go | 26 + pebble/vfs/disk_usage_unix.go | 26 + pebble/vfs/disk_usage_windows.go | 22 + pebble/vfs/errorfs/dsl.go | 300 + pebble/vfs/errorfs/errorfs.go | 518 + pebble/vfs/errorfs/errorfs_test.go | 34 + pebble/vfs/errorfs/testdata/errorfs | 75 + pebble/vfs/errors_unix.go | 21 + pebble/vfs/errors_unix_test.go | 21 + pebble/vfs/errors_windows.go | 22 + pebble/vfs/fadvise_generic.go | 16 + pebble/vfs/fadvise_linux.go | 20 + pebble/vfs/fd_test.go | 36 + pebble/vfs/file_lock_generic.go | 20 + pebble/vfs/file_lock_test.go | 106 + pebble/vfs/file_lock_unix.go | 70 + pebble/vfs/file_lock_windows.go | 42 + pebble/vfs/logging_fs.go | 157 + pebble/vfs/mem_fs.go | 832 + pebble/vfs/mem_fs_test.go | 460 + pebble/vfs/syncing_file.go | 215 + pebble/vfs/syncing_file_linux_test.go | 107 + pebble/vfs/syncing_file_test.go | 292 + pebble/vfs/testdata/memfs_lock | 55 + pebble/vfs/testdata/vfs | 188 + pebble/vfs/vfs.go | 419 + pebble/vfs/vfs_test.go | 361 + pebble/vfs/vfstest/vfstest.go | 32 + 943 files changed, 348194 insertions(+), 6964 deletions(-) rename nekryptology/pkg/core/curves/native/bls48581/{arch.go => arch_32.go} (97%) create mode 100644 nekryptology/pkg/core/curves/native/bls48581/arch_64.go rename nekryptology/pkg/core/curves/native/bls48581/{big.go => big_32.go} (99%) create mode 100644 nekryptology/pkg/core/curves/native/bls48581/big_64.go rename nekryptology/pkg/core/curves/native/bls48581/{config_big.go => config_big_32.go} (98%) create mode 100644 nekryptology/pkg/core/curves/native/bls48581/config_big_64.go rename nekryptology/pkg/core/curves/native/bls48581/{config_field.go => config_field_32.go} (98%) create mode 100644 nekryptology/pkg/core/curves/native/bls48581/config_field_64.go delete mode 100644 nekryptology/pkg/core/curves/native/bls48581/ecdh.go delete mode 100644 nekryptology/pkg/core/curves/native/bls48581/hpke.go delete mode 100644 nekryptology/pkg/core/curves/native/bls48581/mpin256.go rename nekryptology/pkg/core/curves/native/bls48581/{rom.go => rom_32.go} (99%) create mode 100644 nekryptology/pkg/core/curves/native/bls48581/rom_64.go create mode 100644 node/.vscode/settings.json create mode 100644 node/keys/inmem.go create mode 100644 node/p2p/bloom_utils_test.go create mode 100755 node/poor_mans_cd.sh create mode 100644 node/retroactive_peers.json create mode 100644 node/store/inmem.go create mode 100644 node/store/inmem_test.go create mode 100644 node/store/kvdb.go create mode 100644 pebble/.editorconfig create mode 100644 pebble/.github/workflows/ci.yaml create mode 100644 pebble/.github/workflows/code-cover-gen.yaml create mode 100644 pebble/.github/workflows/code-cover-publish.yaml create mode 100644 pebble/.github/workflows/nightly-code-cover.yaml create mode 100644 pebble/.github/workflows/sanitizers.yaml create mode 100644 pebble/.github/workflows/stale.yml create mode 100644 pebble/.gitignore create mode 100644 pebble/LICENSE create mode 100644 pebble/Makefile create mode 100644 pebble/README.md create mode 100644 pebble/batch.go create mode 100644 pebble/batch_test.go create mode 100644 pebble/bloom/bloom.go create mode 100644 pebble/bloom/bloom_test.go create mode 100644 pebble/cache.go create mode 100644 pebble/checkpoint.go create mode 100644 pebble/checkpoint_test.go create mode 100644 pebble/cleaner.go create mode 100644 pebble/cleaner_test.go create mode 100644 pebble/cmd/pebble/.gitignore create mode 100644 pebble/cmd/pebble/db.go create mode 100644 pebble/cmd/pebble/fsbench.go create mode 100644 pebble/cmd/pebble/fsbenchlist.go create mode 100644 pebble/cmd/pebble/main.go create mode 100644 pebble/cmd/pebble/mvcc.go create mode 100644 pebble/cmd/pebble/queue.go create mode 100644 pebble/cmd/pebble/random.go create mode 100644 pebble/cmd/pebble/replay.go create mode 100644 pebble/cmd/pebble/replay_test.go create mode 100644 pebble/cmd/pebble/scan.go create mode 100644 pebble/cmd/pebble/sync.go create mode 100644 pebble/cmd/pebble/test.go create mode 100644 pebble/cmd/pebble/tombstone.go create mode 100644 pebble/cmd/pebble/util.go create mode 100644 pebble/cmd/pebble/write_bench.go create mode 100644 pebble/cmd/pebble/ycsb.go create mode 100644 pebble/commit.go create mode 100644 pebble/commit_test.go create mode 100644 pebble/compaction.go create mode 100644 pebble/compaction_iter.go create mode 100644 pebble/compaction_iter_test.go create mode 100644 pebble/compaction_picker.go create mode 100644 pebble/compaction_picker_test.go create mode 100644 pebble/compaction_test.go create mode 100644 pebble/comparer.go create mode 100644 pebble/data_test.go create mode 100644 pebble/db.go create mode 100644 pebble/db_test.go create mode 100644 pebble/docs/RFCS/20211018_range_keys.md create mode 100644 pebble/docs/RFCS/20220112_pebble_sstable_format_versions.md create mode 100644 pebble/docs/RFCS/20220311_pebble_flushable_ingested_sstable.md create mode 100644 pebble/docs/RFCS/20221122_virtual_sstable.md create mode 100644 pebble/docs/css/app.css create mode 100644 pebble/docs/index.html create mode 100644 pebble/docs/io_profiling.md create mode 100644 pebble/docs/js/app.js create mode 100644 pebble/docs/js/d3.v5.min.js create mode 100644 pebble/docs/js/write-throughput.js create mode 100644 pebble/docs/memory.md create mode 100644 pebble/docs/range_deletions.md create mode 100644 pebble/docs/rocksdb.md create mode 100644 pebble/error_iter.go create mode 100644 pebble/error_test.go create mode 100644 pebble/event.go create mode 100644 pebble/event_listener_test.go create mode 100644 pebble/example_test.go create mode 100644 pebble/external_iterator.go create mode 100644 pebble/external_iterator_test.go create mode 100644 pebble/filenames.go create mode 100644 pebble/filenames_test.go create mode 100644 pebble/flush_test.go create mode 100644 pebble/flushable.go create mode 100644 pebble/flushable_test.go create mode 100644 pebble/format_major_version.go create mode 100644 pebble/format_major_version_test.go create mode 100644 pebble/get_iter.go create mode 100644 pebble/get_iter_test.go create mode 100644 pebble/go.mod create mode 100644 pebble/go.sum create mode 100644 pebble/ingest.go create mode 100644 pebble/ingest_test.go create mode 100644 pebble/internal.go create mode 100644 pebble/internal/ackseq/ackseq.go create mode 100644 pebble/internal/arenaskl/LICENSE create mode 100644 pebble/internal/arenaskl/README.md create mode 100644 pebble/internal/arenaskl/arena.go create mode 100644 pebble/internal/arenaskl/arena_test.go create mode 100644 pebble/internal/arenaskl/flush_iterator.go create mode 100644 pebble/internal/arenaskl/iterator.go create mode 100644 pebble/internal/arenaskl/node.go create mode 100644 pebble/internal/arenaskl/race_test.go create mode 100644 pebble/internal/arenaskl/skl.go create mode 100644 pebble/internal/arenaskl/skl_test.go create mode 100644 pebble/internal/base/cleaner.go create mode 100644 pebble/internal/base/comparer.go create mode 100644 pebble/internal/base/comparer_test.go create mode 100644 pebble/internal/base/error.go create mode 100644 pebble/internal/base/filenames.go create mode 100644 pebble/internal/base/filenames_test.go create mode 100644 pebble/internal/base/internal.go create mode 100644 pebble/internal/base/internal_test.go create mode 100644 pebble/internal/base/iterator.go create mode 100644 pebble/internal/base/iterator_test.go create mode 100644 pebble/internal/base/lazy_value.go create mode 100644 pebble/internal/base/lazy_value_test.go create mode 100644 pebble/internal/base/logger.go create mode 100644 pebble/internal/base/merger.go create mode 100644 pebble/internal/base/metrics.go create mode 100644 pebble/internal/base/metrics_test.go create mode 100644 pebble/internal/base/options.go create mode 100644 pebble/internal/batchskl/README.md create mode 100644 pebble/internal/batchskl/iterator.go create mode 100644 pebble/internal/batchskl/skl.go create mode 100644 pebble/internal/batchskl/skl_test.go create mode 100644 pebble/internal/bytealloc/bytealloc.go create mode 100644 pebble/internal/cache/LICENSE create mode 100644 pebble/internal/cache/cgo_disabled.go create mode 100644 pebble/internal/cache/cgo_enabled.go create mode 100644 pebble/internal/cache/clockpro.go create mode 100644 pebble/internal/cache/clockpro_normal.go create mode 100644 pebble/internal/cache/clockpro_test.go create mode 100644 pebble/internal/cache/clockpro_tracing.go create mode 100644 pebble/internal/cache/entry.go create mode 100644 pebble/internal/cache/entry_invariants.go create mode 100644 pebble/internal/cache/entry_normal.go create mode 100644 pebble/internal/cache/refcnt_normal.go create mode 100644 pebble/internal/cache/refcnt_tracing.go create mode 100644 pebble/internal/cache/robin_hood.go create mode 100644 pebble/internal/cache/robin_hood_test.go create mode 100644 pebble/internal/cache/testdata/cache create mode 100644 pebble/internal/cache/value.go create mode 100644 pebble/internal/cache/value_invariants.go create mode 100644 pebble/internal/cache/value_normal.go create mode 100644 pebble/internal/constants/constants.go create mode 100644 pebble/internal/constants/constants_test.go create mode 100644 pebble/internal/crc/crc.go create mode 100644 pebble/internal/datatest/datatest.go create mode 100644 pebble/internal/dsl/dsl.go create mode 100644 pebble/internal/dsl/predicates.go create mode 100644 pebble/internal/fastrand/fastrand.go create mode 100644 pebble/internal/fastrand/fastrand_test.go create mode 100644 pebble/internal/humanize/humanize.go create mode 100644 pebble/internal/humanize/humanize_test.go create mode 100644 pebble/internal/humanize/testdata/humanize create mode 100644 pebble/internal/intern/intern.go create mode 100644 pebble/internal/intern/intern_test.go create mode 100644 pebble/internal/invalidating/iter.go create mode 100644 pebble/internal/invariants/finalizer_off.go create mode 100644 pebble/internal/invariants/finalizer_on.go create mode 100644 pebble/internal/invariants/off.go create mode 100644 pebble/internal/invariants/on.go create mode 100644 pebble/internal/invariants/race_off.go create mode 100644 pebble/internal/invariants/race_on.go create mode 100644 pebble/internal/itertest/datadriven.go create mode 100644 pebble/internal/keyspan/bounded.go create mode 100644 pebble/internal/keyspan/bounded_test.go create mode 100644 pebble/internal/keyspan/datadriven_test.go create mode 100644 pebble/internal/keyspan/defragment.go create mode 100644 pebble/internal/keyspan/defragment_test.go create mode 100644 pebble/internal/keyspan/doc.go create mode 100644 pebble/internal/keyspan/filter.go create mode 100644 pebble/internal/keyspan/filter_test.go create mode 100644 pebble/internal/keyspan/fragmenter.go create mode 100644 pebble/internal/keyspan/fragmenter_test.go create mode 100644 pebble/internal/keyspan/get.go create mode 100644 pebble/internal/keyspan/interleaving_iter.go create mode 100644 pebble/internal/keyspan/interleaving_iter_test.go create mode 100644 pebble/internal/keyspan/internal_iter_shim.go create mode 100644 pebble/internal/keyspan/iter.go create mode 100644 pebble/internal/keyspan/iter_test.go create mode 100644 pebble/internal/keyspan/level_iter.go create mode 100644 pebble/internal/keyspan/level_iter_test.go create mode 100644 pebble/internal/keyspan/merging_iter.go create mode 100644 pebble/internal/keyspan/merging_iter_test.go create mode 100644 pebble/internal/keyspan/seek.go create mode 100644 pebble/internal/keyspan/seek_test.go create mode 100644 pebble/internal/keyspan/span.go create mode 100644 pebble/internal/keyspan/span_test.go create mode 100644 pebble/internal/keyspan/testdata/bounded_iter create mode 100644 pebble/internal/keyspan/testdata/covers_at create mode 100644 pebble/internal/keyspan/testdata/defragmenting_iter create mode 100644 pebble/internal/keyspan/testdata/filtering_iter create mode 100644 pebble/internal/keyspan/testdata/fragmenter create mode 100644 pebble/internal/keyspan/testdata/fragmenter_covers create mode 100644 pebble/internal/keyspan/testdata/fragmenter_emit_order create mode 100644 pebble/internal/keyspan/testdata/fragmenter_truncate_and_flush_to create mode 100644 pebble/internal/keyspan/testdata/fragmenter_values create mode 100644 pebble/internal/keyspan/testdata/interleaving_iter create mode 100644 pebble/internal/keyspan/testdata/interleaving_iter_masking create mode 100644 pebble/internal/keyspan/testdata/iter create mode 100644 pebble/internal/keyspan/testdata/level_iter create mode 100644 pebble/internal/keyspan/testdata/merging_iter create mode 100644 pebble/internal/keyspan/testdata/seek create mode 100644 pebble/internal/keyspan/testdata/truncate create mode 100644 pebble/internal/keyspan/testdata/visible create mode 100644 pebble/internal/keyspan/testdata/visible_at create mode 100644 pebble/internal/keyspan/transformer.go create mode 100644 pebble/internal/keyspan/truncate.go create mode 100644 pebble/internal/keyspan/truncate_test.go create mode 100644 pebble/internal/lint/lint.go create mode 100644 pebble/internal/lint/lint_test.go create mode 100644 pebble/internal/manifest/btree.go create mode 100644 pebble/internal/manifest/btree_test.go create mode 100644 pebble/internal/manifest/l0_sublevels.go create mode 100644 pebble/internal/manifest/l0_sublevels_test.go create mode 100644 pebble/internal/manifest/level.go create mode 100644 pebble/internal/manifest/level_metadata.go create mode 100644 pebble/internal/manifest/level_metadata_test.go create mode 100644 pebble/internal/manifest/level_test.go create mode 100644 pebble/internal/manifest/testdata/MANIFEST_import create mode 100644 pebble/internal/manifest/testdata/file_metadata_bounds create mode 100644 pebble/internal/manifest/testdata/l0_sublevels create mode 100644 pebble/internal/manifest/testdata/level_iterator create mode 100644 pebble/internal/manifest/testdata/level_iterator_filtered create mode 100644 pebble/internal/manifest/testdata/overlaps create mode 100644 pebble/internal/manifest/testdata/version_check_ordering create mode 100644 pebble/internal/manifest/testdata/version_edit_apply create mode 100644 pebble/internal/manifest/version.go create mode 100644 pebble/internal/manifest/version_edit.go create mode 100644 pebble/internal/manifest/version_edit_test.go create mode 100644 pebble/internal/manifest/version_test.go create mode 100644 pebble/internal/manual/manual.go create mode 100644 pebble/internal/manual/manual_32bit.go create mode 100644 pebble/internal/manual/manual_64bit.go create mode 100644 pebble/internal/manual/manual_mips.go create mode 100644 pebble/internal/manual/manual_nocgo.go create mode 100644 pebble/internal/metamorphic/.gitignore create mode 100644 pebble/internal/metamorphic/crossversion/crossversion_test.go create mode 100644 pebble/internal/metamorphic/doc.go create mode 100644 pebble/internal/metamorphic/meta_test.go create mode 100644 pebble/internal/metamorphic/metaflags/meta_flags.go create mode 100644 pebble/internal/metamorphic/metarunner/main.go create mode 100644 pebble/internal/mkbench/main.go create mode 100644 pebble/internal/mkbench/split.go create mode 100644 pebble/internal/mkbench/split_test.go create mode 100644 pebble/internal/mkbench/testdata/README.md create mode 120000 pebble/internal/mkbench/testdata/data-symlink create mode 100644 pebble/internal/mkbench/testdata/data.js create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/write/size=1024/run_1/1.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/write/size=1024/run_1/2.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/1.ycsb_A.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/1.ycsb_B.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/1.ycsb_C.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/1.ycsb_D.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/1.ycsb_E.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/1.ycsb_F.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/2.ycsb_A.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/2.ycsb_B.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/2.ycsb_C.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/2.ycsb_D.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/2.ycsb_E.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/2.ycsb_F.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/3.ycsb_A.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/3.ycsb_B.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/3.ycsb_C.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/3.ycsb_D.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/3.ycsb_E.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=1024/run_1/3.ycsb_F.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/1.ycsb_A.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/1.ycsb_B.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/1.ycsb_C.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/1.ycsb_D.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/1.ycsb_E.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/1.ycsb_F.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/2.ycsb_A.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/2.ycsb_B.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/2.ycsb_C.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/2.ycsb_D.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/2.ycsb_E.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/2.ycsb_F.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/3.ycsb_A.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/3.ycsb_B.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/3.ycsb_C.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/3.ycsb_D.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/3.ycsb_E.log create mode 100644 pebble/internal/mkbench/testdata/data/20211027/pebble/ycsb/size=64/run_1/3.ycsb_F.log create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/write/size=1024/run_1/1.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/write/size=1024/run_1/2.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/1.ycsb_A.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/1.ycsb_B.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/1.ycsb_C.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/1.ycsb_D.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/1.ycsb_E.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/1.ycsb_F.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/2.ycsb_A.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/2.ycsb_B.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/2.ycsb_C.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/2.ycsb_D.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/2.ycsb_E.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/2.ycsb_F.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/3.ycsb_A.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/3.ycsb_B.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/3.ycsb_C.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/3.ycsb_D.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/3.ycsb_E.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=1024/run_1/3.ycsb_F.log.gz create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/1.ycsb_A.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/1.ycsb_B.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/1.ycsb_C.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/1.ycsb_D.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/1.ycsb_E.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/1.ycsb_F.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/2.ycsb_A.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/2.ycsb_B.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/2.ycsb_C.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/2.ycsb_D.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/2.ycsb_E.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/2.ycsb_F.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/3.ycsb_A.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/3.ycsb_B.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/3.ycsb_C.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/3.ycsb_D.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/3.ycsb_E.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/data/20211028/pebble/ycsb/size=64/run_1/3.ycsb_F.log.bz2 create mode 100644 pebble/internal/mkbench/testdata/write-throughput/20211027-pebble-write-size=1024-run_1-summary.json create mode 100644 pebble/internal/mkbench/testdata/write-throughput/20211028-pebble-write-size=1024-run_1-summary.json create mode 100644 pebble/internal/mkbench/testdata/write-throughput/summary.json create mode 100644 pebble/internal/mkbench/testutil.go create mode 100644 pebble/internal/mkbench/util.go create mode 100644 pebble/internal/mkbench/util_test.go create mode 100644 pebble/internal/mkbench/write.go create mode 100644 pebble/internal/mkbench/write_test.go create mode 100644 pebble/internal/mkbench/ycsb.go create mode 100644 pebble/internal/mkbench/ycsb_test.go create mode 100644 pebble/internal/pacertoy/pebble/main.go create mode 100644 pebble/internal/pacertoy/rocksdb/main.go create mode 100644 pebble/internal/private/batch.go create mode 100644 pebble/internal/private/sstable.go create mode 100644 pebble/internal/randvar/deck.go create mode 100644 pebble/internal/randvar/deck_test.go create mode 100644 pebble/internal/randvar/flag.go create mode 100644 pebble/internal/randvar/rand.go create mode 100644 pebble/internal/randvar/randvar.go create mode 100644 pebble/internal/randvar/skewed_latest.go create mode 100644 pebble/internal/randvar/skewed_latest_test.go create mode 100644 pebble/internal/randvar/uniform.go create mode 100644 pebble/internal/randvar/weighted.go create mode 100644 pebble/internal/randvar/weighted_test.go create mode 100644 pebble/internal/randvar/zipf.go create mode 100644 pebble/internal/randvar/zipf_test.go create mode 100644 pebble/internal/rangedel/rangedel.go create mode 100644 pebble/internal/rangekey/coalesce.go create mode 100644 pebble/internal/rangekey/coalesce_test.go create mode 100644 pebble/internal/rangekey/rangekey.go create mode 100644 pebble/internal/rangekey/rangekey_test.go create mode 100644 pebble/internal/rangekey/testdata/coalesce create mode 100644 pebble/internal/rangekey/testdata/defragmenting_iter create mode 100644 pebble/internal/rangekey/testdata/iter create mode 100644 pebble/internal/rate/rate.go create mode 100644 pebble/internal/rawalloc/rawalloc.go create mode 100644 pebble/internal/rawalloc/rawalloc_32bit.go create mode 100644 pebble/internal/rawalloc/rawalloc_64bit.go create mode 100644 pebble/internal/rawalloc/rawalloc_gccgo.go create mode 100644 pebble/internal/rawalloc/rawalloc_go1.9.go create mode 100644 pebble/internal/rawalloc/rawalloc_mips.go create mode 100644 pebble/internal/rawalloc/rawalloc_test.go create mode 100644 pebble/internal/testkeys/strconv.go create mode 100644 pebble/internal/testkeys/testdata/divvy create mode 100644 pebble/internal/testkeys/testkeys.go create mode 100644 pebble/internal/testkeys/testkeys_test.go create mode 100644 pebble/internal_test.go create mode 100644 pebble/iterator.go create mode 100644 pebble/iterator_example_test.go create mode 100644 pebble/iterator_histories_test.go create mode 100644 pebble/iterator_test.go create mode 100644 pebble/level_checker.go create mode 100644 pebble/level_checker_test.go create mode 100644 pebble/level_iter.go create mode 100644 pebble/level_iter_test.go create mode 100644 pebble/log_recycler.go create mode 100644 pebble/log_recycler_test.go create mode 100644 pebble/logger.go create mode 100644 pebble/mem_table.go create mode 100644 pebble/mem_table_test.go create mode 100644 pebble/merger.go create mode 100644 pebble/merging_iter.go create mode 100644 pebble/merging_iter_heap.go create mode 100644 pebble/merging_iter_test.go create mode 100644 pebble/metamorphic/config.go create mode 100644 pebble/metamorphic/generator.go create mode 100644 pebble/metamorphic/generator_test.go create mode 100644 pebble/metamorphic/history.go create mode 100644 pebble/metamorphic/history_test.go create mode 100644 pebble/metamorphic/key_manager.go create mode 100644 pebble/metamorphic/key_manager_test.go create mode 100644 pebble/metamorphic/meta.go create mode 100644 pebble/metamorphic/ops.go create mode 100644 pebble/metamorphic/options.go create mode 100644 pebble/metamorphic/options_test.go create mode 100644 pebble/metamorphic/parser.go create mode 100644 pebble/metamorphic/parser_test.go create mode 100644 pebble/metamorphic/retryable.go create mode 100644 pebble/metamorphic/test.go create mode 100644 pebble/metamorphic/testdata/key_manager create mode 100644 pebble/metamorphic/testdata/parser create mode 100644 pebble/metamorphic/testdata/reorder_history create mode 100644 pebble/metamorphic/utils.go create mode 100644 pebble/metrics.go create mode 100644 pebble/metrics_test.go create mode 100644 pebble/objstorage/noop_readahead.go create mode 100644 pebble/objstorage/objstorage.go create mode 100644 pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing.go create mode 100644 pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_off.go create mode 100644 pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_on.go create mode 100644 pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_test.go create mode 100644 pebble/objstorage/objstorageprovider/provider.go create mode 100644 pebble/objstorage/objstorageprovider/provider_test.go create mode 100644 pebble/objstorage/objstorageprovider/readahead.go create mode 100644 pebble/objstorage/objstorageprovider/readahead_test.go create mode 100644 pebble/objstorage/objstorageprovider/remote.go create mode 100644 pebble/objstorage/objstorageprovider/remote_backing.go create mode 100644 pebble/objstorage/objstorageprovider/remote_backing_test.go create mode 100644 pebble/objstorage/objstorageprovider/remote_obj_name.go create mode 100644 pebble/objstorage/objstorageprovider/remote_obj_name_test.go create mode 100644 pebble/objstorage/objstorageprovider/remote_readable.go create mode 100644 pebble/objstorage/objstorageprovider/remoteobjcat/catalog.go create mode 100644 pebble/objstorage/objstorageprovider/remoteobjcat/catalog_test.go create mode 100644 pebble/objstorage/objstorageprovider/remoteobjcat/testdata/catalog create mode 100644 pebble/objstorage/objstorageprovider/remoteobjcat/version_edit.go create mode 100644 pebble/objstorage/objstorageprovider/remoteobjcat/version_edit_test.go create mode 100644 pebble/objstorage/objstorageprovider/shared_writable.go create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/shared_cache_helpers_test.go create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/shared_cache_internal_test.go create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/shared_cache_test.go create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/compaction_reads create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/eof_handling create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/lru create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_larger_than_two_cache_shards create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks_with_first_read_at_big_offset create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks_with_first_read_at_offset create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_shards create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_shards_with_first_read_at_offset create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/small_read create mode 100644 pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/small_read_with_first_read_at_offset create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/local create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/local_readahead create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/shared_attach create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/shared_attach_after_unref create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/shared_attach_multi create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/shared_basic create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/shared_no_ref create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/shared_readahead create mode 100644 pebble/objstorage/objstorageprovider/testdata/provider/shared_remove create mode 100644 pebble/objstorage/objstorageprovider/testdata/readahead create mode 100644 pebble/objstorage/objstorageprovider/vfs.go create mode 100644 pebble/objstorage/objstorageprovider/vfs_readable.go create mode 100644 pebble/objstorage/objstorageprovider/vfs_writable.go create mode 100644 pebble/objstorage/remote/factory.go create mode 100644 pebble/objstorage/remote/localfs.go create mode 100644 pebble/objstorage/remote/logging.go create mode 100644 pebble/objstorage/remote/mem.go create mode 100644 pebble/objstorage/remote/storage.go create mode 100644 pebble/open.go create mode 100644 pebble/open_test.go create mode 100644 pebble/options.go create mode 100644 pebble/options_test.go create mode 100644 pebble/pacer.go create mode 100644 pebble/pacer_test.go create mode 100644 pebble/range_del_test.go create mode 100644 pebble/range_keys.go create mode 100644 pebble/rangekey/rangekey.go create mode 100644 pebble/read_compaction_queue.go create mode 100644 pebble/read_state.go create mode 100644 pebble/read_state_test.go create mode 100644 pebble/record/log_writer.go create mode 100644 pebble/record/log_writer_test.go create mode 100644 pebble/record/record.go create mode 100644 pebble/record/record_test.go create mode 100644 pebble/record/rotation.go create mode 100644 pebble/record/rotation_test.go create mode 100644 pebble/record/testdata/rotation create mode 100644 pebble/replay/replay.go create mode 100644 pebble/replay/replay_test.go create mode 100644 pebble/replay/sampled_metric.go create mode 100644 pebble/replay/sampled_metric_test.go create mode 100644 pebble/replay/testdata/collect/clean_before_copy create mode 100644 pebble/replay/testdata/collect/copy_before_clean create mode 100644 pebble/replay/testdata/collect/manifest_copying create mode 100644 pebble/replay/testdata/collect/start_stop create mode 100644 pebble/replay/testdata/corpus/findManifestStart create mode 100644 pebble/replay/testdata/corpus/findWorkloadFiles create mode 100644 pebble/replay/testdata/corpus/high_read_amp create mode 100644 pebble/replay/testdata/corpus/simple create mode 100644 pebble/replay/testdata/flushed_sstable_keys create mode 100644 pebble/replay/testdata/replay create mode 100644 pebble/replay/testdata/replay_paced create mode 100644 pebble/replay/testdata/sampled_metric create mode 100644 pebble/replay/workload_capture.go create mode 100644 pebble/replay/workload_capture_test.go create mode 100644 pebble/scan_internal.go create mode 100644 pebble/scan_internal_test.go create mode 100755 pebble/scripts/changed-go-pkgs.sh create mode 100755 pebble/scripts/code-coverage-publish.sh create mode 100755 pebble/scripts/code-coverage.sh create mode 100755 pebble/scripts/pr-codecov-run-tests.sh create mode 100755 pebble/scripts/run-crossversion-meta.sh create mode 100644 pebble/shims/cmp/cmp.go create mode 100644 pebble/shims/slices/slices.go create mode 100644 pebble/shims/slices/sort.go create mode 100644 pebble/shims/slices/zsortanyfunc.go create mode 100644 pebble/shims/slices/zsortordered.go create mode 100644 pebble/snapshot.go create mode 100644 pebble/snapshot_test.go create mode 100644 pebble/sstable/block.go create mode 100644 pebble/sstable/block_property.go create mode 100644 pebble/sstable/block_property_test.go create mode 100644 pebble/sstable/block_property_test_utils.go create mode 100644 pebble/sstable/block_test.go create mode 100644 pebble/sstable/buffer_pool.go create mode 100644 pebble/sstable/buffer_pool_test.go create mode 100644 pebble/sstable/category_stats.go create mode 100644 pebble/sstable/comparer.go create mode 100644 pebble/sstable/compression.go create mode 100644 pebble/sstable/compression_cgo.go create mode 100644 pebble/sstable/compression_cgo_test.go create mode 100644 pebble/sstable/compression_nocgo.go create mode 100644 pebble/sstable/compression_nocgo_test.go create mode 100644 pebble/sstable/compression_test.go create mode 100644 pebble/sstable/data_test.go create mode 100644 pebble/sstable/filter.go create mode 100644 pebble/sstable/format.go create mode 100644 pebble/sstable/format_test.go create mode 100644 pebble/sstable/internal.go create mode 100644 pebble/sstable/layout.go create mode 100644 pebble/sstable/options.go create mode 100644 pebble/sstable/properties.go create mode 100644 pebble/sstable/properties_test.go create mode 100644 pebble/sstable/random_test.go create mode 100644 pebble/sstable/raw_block.go create mode 100644 pebble/sstable/reader.go create mode 100644 pebble/sstable/reader_iter.go create mode 100644 pebble/sstable/reader_iter_single_lvl.go create mode 100644 pebble/sstable/reader_iter_two_lvl.go create mode 100644 pebble/sstable/reader_test.go create mode 100644 pebble/sstable/reader_virtual.go create mode 100644 pebble/sstable/suffix_rewriter.go create mode 100644 pebble/sstable/suffix_rewriter_test.go create mode 100644 pebble/sstable/table.go create mode 100644 pebble/sstable/table_test.go create mode 100644 pebble/sstable/testdata/.gitignore create mode 100644 pebble/sstable/testdata/Makefile create mode 100644 pebble/sstable/testdata/block create mode 100644 pebble/sstable/testdata/block_properties create mode 100644 pebble/sstable/testdata/block_properties_boundlimited create mode 100644 pebble/sstable/testdata/buffer_pool create mode 100644 pebble/sstable/testdata/h.block-bloom.no-compression.sst create mode 100644 pebble/sstable/testdata/h.ldb create mode 100644 pebble/sstable/testdata/h.no-compression.sst create mode 100644 pebble/sstable/testdata/h.no-compression.two_level_index.sst create mode 100644 pebble/sstable/testdata/h.sst create mode 100644 pebble/sstable/testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst create mode 100644 pebble/sstable/testdata/h.table-bloom.no-compression.sst create mode 100644 pebble/sstable/testdata/h.table-bloom.sst create mode 100644 pebble/sstable/testdata/h.txt create mode 100644 pebble/sstable/testdata/h.zstd-compression.sst create mode 100644 pebble/sstable/testdata/hamlet-act-1.txt create mode 100644 pebble/sstable/testdata/hamletreader/hamlet_iter create mode 100644 pebble/sstable/testdata/make-table.cc create mode 100644 pebble/sstable/testdata/prefixreader/bloom create mode 100644 pebble/sstable/testdata/prefixreader/iter create mode 100644 pebble/sstable/testdata/reader/bloom create mode 100644 pebble/sstable/testdata/reader/iter create mode 100644 pebble/sstable/testdata/reader_bpf/Pebblev2/iter create mode 100644 pebble/sstable/testdata/reader_bpf/Pebblev3/iter create mode 100644 pebble/sstable/testdata/reader_hide_obsolete/iter create mode 100644 pebble/sstable/testdata/readerstats_LevelDB/iter create mode 100644 pebble/sstable/testdata/readerstats_Pebblev3/iter create mode 100644 pebble/sstable/testdata/rewriter create mode 100644 pebble/sstable/testdata/rewriter_v3 create mode 100644 pebble/sstable/testdata/size_estimate create mode 100644 pebble/sstable/testdata/virtual_reader create mode 100644 pebble/sstable/testdata/writer create mode 100644 pebble/sstable/testdata/writer_range_keys create mode 100644 pebble/sstable/testdata/writer_v3 create mode 100644 pebble/sstable/testdata/writer_value_blocks create mode 100644 pebble/sstable/unsafe.go create mode 100644 pebble/sstable/unsafe_test.go create mode 100644 pebble/sstable/value_block.go create mode 100644 pebble/sstable/value_block_test.go create mode 100644 pebble/sstable/write_queue.go create mode 100644 pebble/sstable/writer.go create mode 100644 pebble/sstable/writer_fixture_test.go create mode 100644 pebble/sstable/writer_rangekey_test.go create mode 100644 pebble/sstable/writer_test.go create mode 100644 pebble/table_cache.go create mode 100644 pebble/table_cache_test.go create mode 100644 pebble/table_stats.go create mode 100644 pebble/table_stats_test.go create mode 100644 pebble/testdata/.gitignore create mode 100644 pebble/testdata/Makefile create mode 100644 pebble/testdata/batch_get create mode 100644 pebble/testdata/batch_range_ops create mode 100644 pebble/testdata/batch_reader create mode 100644 pebble/testdata/checkpoint create mode 100644 pebble/testdata/cleaner create mode 100644 pebble/testdata/compaction_allow_zero_seqnum create mode 100644 pebble/testdata/compaction_atomic_unit_bounds create mode 100644 pebble/testdata/compaction_check_ordering create mode 100644 pebble/testdata/compaction_delete_only_hints create mode 100644 pebble/testdata/compaction_elide_tombstone create mode 100644 pebble/testdata/compaction_error_on_user_key_overlap create mode 100644 pebble/testdata/compaction_expand_inputs create mode 100644 pebble/testdata/compaction_find_grandparent_limit create mode 100644 pebble/testdata/compaction_find_l0_limit create mode 100644 pebble/testdata/compaction_inuse_key_ranges create mode 100644 pebble/testdata/compaction_iter create mode 100644 pebble/testdata/compaction_iter_delete_sized create mode 100644 pebble/testdata/compaction_iter_set_with_del create mode 100644 pebble/testdata/compaction_output_file_size create mode 100644 pebble/testdata/compaction_output_level create mode 100644 pebble/testdata/compaction_output_splitters create mode 100644 pebble/testdata/compaction_picker_L0 create mode 100644 pebble/testdata/compaction_picker_concurrency create mode 100644 pebble/testdata/compaction_picker_estimated_debt create mode 100644 pebble/testdata/compaction_picker_level_max_bytes create mode 100644 pebble/testdata/compaction_picker_pick_file create mode 100644 pebble/testdata/compaction_picker_read_triggered create mode 100644 pebble/testdata/compaction_picker_scores create mode 100644 pebble/testdata/compaction_picker_target_level create mode 100644 pebble/testdata/compaction_read_triggered create mode 100644 pebble/testdata/compaction_setup_inputs create mode 100644 pebble/testdata/compaction_setup_inputs_multilevel_dummy create mode 100644 pebble/testdata/compaction_setup_inputs_multilevel_write_amp create mode 100644 pebble/testdata/compaction_tombstones create mode 100644 pebble/testdata/compaction_transform create mode 100644 pebble/testdata/concurrent_excise create mode 100644 pebble/testdata/db-stage-1/000002.log create mode 100644 pebble/testdata/db-stage-1/CURRENT create mode 100644 pebble/testdata/db-stage-1/LOCK create mode 100644 pebble/testdata/db-stage-1/MANIFEST-000001 create mode 100644 pebble/testdata/db-stage-1/OPTIONS-000003 create mode 100644 pebble/testdata/db-stage-1/marker.format-version.000012.013 create mode 100644 pebble/testdata/db-stage-1/marker.manifest.000001.MANIFEST-000001 create mode 100644 pebble/testdata/db-stage-2/000002.log create mode 100644 pebble/testdata/db-stage-2/CURRENT create mode 100644 pebble/testdata/db-stage-2/LOCK create mode 100644 pebble/testdata/db-stage-2/MANIFEST-000001 create mode 100644 pebble/testdata/db-stage-2/OPTIONS-000003 create mode 100644 pebble/testdata/db-stage-2/marker.format-version.000012.013 create mode 100644 pebble/testdata/db-stage-2/marker.manifest.000001.MANIFEST-000001 create mode 100644 pebble/testdata/db-stage-3/000004.sst create mode 100644 pebble/testdata/db-stage-3/000005.log create mode 100644 pebble/testdata/db-stage-3/CURRENT create mode 100644 pebble/testdata/db-stage-3/LOCK create mode 100644 pebble/testdata/db-stage-3/MANIFEST-000001 create mode 100644 pebble/testdata/db-stage-3/MANIFEST-000006 create mode 100644 pebble/testdata/db-stage-3/OPTIONS-000007 create mode 100644 pebble/testdata/db-stage-3/marker.format-version.000012.013 create mode 100644 pebble/testdata/db-stage-3/marker.manifest.000002.MANIFEST-000006 create mode 100644 pebble/testdata/db-stage-4/000004.sst create mode 100644 pebble/testdata/db-stage-4/000005.log create mode 100644 pebble/testdata/db-stage-4/CURRENT create mode 100644 pebble/testdata/db-stage-4/LOCK create mode 100644 pebble/testdata/db-stage-4/MANIFEST-000001 create mode 100644 pebble/testdata/db-stage-4/MANIFEST-000006 create mode 100644 pebble/testdata/db-stage-4/OPTIONS-000007 create mode 100644 pebble/testdata/db-stage-4/marker.format-version.000012.013 create mode 100644 pebble/testdata/db-stage-4/marker.manifest.000002.MANIFEST-000006 create mode 100644 pebble/testdata/delete_range create mode 100644 pebble/testdata/event_listener create mode 100644 pebble/testdata/excise create mode 100644 pebble/testdata/external_iterator create mode 100644 pebble/testdata/flushable_batch create mode 100644 pebble/testdata/flushable_ingest create mode 100644 pebble/testdata/format_major_version_pebblev1_migration create mode 100644 pebble/testdata/format_major_version_split_user_key_migration create mode 100644 pebble/testdata/frontiers create mode 100644 pebble/testdata/indexed_batch_mutation create mode 100644 pebble/testdata/ingest create mode 100644 pebble/testdata/ingest_external create mode 100644 pebble/testdata/ingest_load create mode 100644 pebble/testdata/ingest_memtable_overlaps create mode 100644 pebble/testdata/ingest_shared create mode 100644 pebble/testdata/ingest_shared_lower create mode 100644 pebble/testdata/ingest_sort_and_verify create mode 100644 pebble/testdata/ingest_target_level create mode 100644 pebble/testdata/ingest_update_seqnums create mode 100644 pebble/testdata/ingested_flushable_api create mode 100644 pebble/testdata/internal_iter_bounds create mode 100644 pebble/testdata/internal_iter_next create mode 100644 pebble/testdata/iter_histories/clone create mode 100644 pebble/testdata/iter_histories/errors create mode 100644 pebble/testdata/iter_histories/internal_next create mode 100644 pebble/testdata/iter_histories/iter_optimizations create mode 100644 pebble/testdata/iter_histories/lazy_combined_iteration create mode 100644 pebble/testdata/iter_histories/merge create mode 100644 pebble/testdata/iter_histories/next_prefix create mode 100644 pebble/testdata/iter_histories/prefix_iteration create mode 100644 pebble/testdata/iter_histories/range_key_changed create mode 100644 pebble/testdata/iter_histories/range_key_masking create mode 100644 pebble/testdata/iter_histories/range_keys_simple create mode 100644 pebble/testdata/iter_histories/set_options create mode 100644 pebble/testdata/iter_histories/skip_point create mode 100644 pebble/testdata/iter_histories/stats_no_invariants create mode 100644 pebble/testdata/iter_histories/with_limit create mode 100644 pebble/testdata/iterator create mode 100644 pebble/testdata/iterator_block_interval_filter create mode 100644 pebble/testdata/iterator_bounds_lifetimes create mode 100644 pebble/testdata/iterator_next_prev create mode 100644 pebble/testdata/iterator_read_sampling create mode 100644 pebble/testdata/iterator_seek_opt create mode 100644 pebble/testdata/iterator_seek_opt_errors create mode 100644 pebble/testdata/iterator_stats create mode 100644 pebble/testdata/iterator_table_filter create mode 100644 pebble/testdata/level_checker create mode 100644 pebble/testdata/level_iter create mode 100644 pebble/testdata/level_iter_boundaries create mode 100644 pebble/testdata/level_iter_seek create mode 100644 pebble/testdata/make-db.go create mode 100644 pebble/testdata/manual_compaction create mode 100644 pebble/testdata/manual_compaction_file_boundaries create mode 100644 pebble/testdata/manual_compaction_file_boundaries_delsized create mode 100644 pebble/testdata/manual_compaction_multilevel create mode 100644 pebble/testdata/manual_compaction_range_keys create mode 100644 pebble/testdata/manual_compaction_set_with_del create mode 100644 pebble/testdata/manual_compaction_set_with_del_sstable_Pebblev4 create mode 100644 pebble/testdata/manual_flush create mode 100644 pebble/testdata/marked_for_compaction create mode 100644 pebble/testdata/merging_iter create mode 100644 pebble/testdata/merging_iter_seek create mode 100644 pebble/testdata/metrics create mode 100644 pebble/testdata/point_collapsing_iter create mode 100644 pebble/testdata/range_del create mode 100644 pebble/testdata/read_compaction_queue create mode 100644 pebble/testdata/rocksdb-ingest-only/000003.log create mode 100644 pebble/testdata/rocksdb-ingest-only/000006.sst create mode 100644 pebble/testdata/rocksdb-ingest-only/CURRENT create mode 100644 pebble/testdata/rocksdb-ingest-only/IDENTITY create mode 100644 pebble/testdata/rocksdb-ingest-only/LOCK create mode 100644 pebble/testdata/rocksdb-ingest-only/MANIFEST-000001 create mode 100644 pebble/testdata/rocksdb-ingest-only/MANIFEST-000007 create mode 100644 pebble/testdata/rocksdb-ingest-only/OPTIONS-000005 create mode 100644 pebble/testdata/scan_internal create mode 100644 pebble/testdata/scan_statistics create mode 100644 pebble/testdata/simple_level_iter create mode 100644 pebble/testdata/singledel_manual_compaction create mode 100644 pebble/testdata/singledel_manual_compaction_set_with_del create mode 100644 pebble/testdata/snapshot create mode 100644 pebble/testdata/sstable_key_compare create mode 100644 pebble/testdata/table_stats create mode 100644 pebble/testdata/table_stats_deletion_iter create mode 100644 pebble/testdata/version_check_consistency create mode 100644 pebble/tool/data/d3.v5.min.js create mode 100644 pebble/tool/data/lsm.css create mode 100644 pebble/tool/data/lsm.js create mode 100644 pebble/tool/data_test.go create mode 100644 pebble/tool/db.go create mode 100644 pebble/tool/db_io_bench.go create mode 100644 pebble/tool/db_test.go create mode 100644 pebble/tool/find.go create mode 100644 pebble/tool/find_test.go create mode 100644 pebble/tool/logs/compaction.go create mode 100644 pebble/tool/logs/compaction_test.go create mode 100644 pebble/tool/logs/testdata/compactions-23-1 create mode 100644 pebble/tool/logs/testdata/compactions-latest create mode 100644 pebble/tool/logs/tool.go create mode 100644 pebble/tool/lsm.go create mode 100644 pebble/tool/lsm_data.go create mode 100644 pebble/tool/make_incorrect_manifests.go create mode 100755 pebble/tool/make_lsm_data.sh create mode 100644 pebble/tool/make_test_find_db.go create mode 100644 pebble/tool/make_test_remotecat.go create mode 100644 pebble/tool/make_test_sstables.go create mode 100644 pebble/tool/manifest.go create mode 100644 pebble/tool/manifest_test.go create mode 100644 pebble/tool/remotecat.go create mode 100644 pebble/tool/remotecat_test.go create mode 100644 pebble/tool/sstable.go create mode 100644 pebble/tool/sstable_test.go create mode 100644 pebble/tool/testdata/MANIFEST-invalid create mode 100644 pebble/tool/testdata/REMOTE-OBJ-CATALOG create mode 100644 pebble/tool/testdata/bad-magic.sst create mode 100644 pebble/tool/testdata/corrupt-options-db/OPTIONS-000002 create mode 100644 pebble/tool/testdata/corrupted.sst create mode 100644 pebble/tool/testdata/db_check create mode 100644 pebble/tool/testdata/db_checkpoint create mode 100644 pebble/tool/testdata/db_get_set create mode 100644 pebble/tool/testdata/db_lsm create mode 100644 pebble/tool/testdata/db_properties create mode 100644 pebble/tool/testdata/db_scan create mode 100644 pebble/tool/testdata/db_space create mode 100644 pebble/tool/testdata/find create mode 100644 pebble/tool/testdata/find-db/000009.log create mode 100644 pebble/tool/testdata/find-db/CURRENT create mode 100644 pebble/tool/testdata/find-db/LOCK create mode 100644 pebble/tool/testdata/find-db/MANIFEST-000001 create mode 100644 pebble/tool/testdata/find-db/OPTIONS-000003 create mode 100644 pebble/tool/testdata/find-db/archive/000002.log create mode 100644 pebble/tool/testdata/find-db/archive/000004.log create mode 100644 pebble/tool/testdata/find-db/archive/000005.sst create mode 100644 pebble/tool/testdata/find-db/archive/000006.sst create mode 100644 pebble/tool/testdata/find-db/archive/000007.sst create mode 100644 pebble/tool/testdata/find-db/archive/000008.sst create mode 100644 pebble/tool/testdata/find-db/archive/000010.sst create mode 100644 pebble/tool/testdata/find-db/archive/000011.sst create mode 100644 pebble/tool/testdata/find-mixed/000001.sst create mode 100644 pebble/tool/testdata/find-mixed/000002.sst create mode 100644 pebble/tool/testdata/manifest_dump create mode 100644 pebble/tool/testdata/manifest_summarize create mode 100644 pebble/tool/testdata/mixed/000002.log create mode 100644 pebble/tool/testdata/mixed/000004.log create mode 100644 pebble/tool/testdata/mixed/000005.sst create mode 100644 pebble/tool/testdata/mixed/CURRENT create mode 100644 pebble/tool/testdata/mixed/LOCK create mode 100644 pebble/tool/testdata/mixed/MANIFEST-000001 create mode 100644 pebble/tool/testdata/mixed/OPTIONS-000003 create mode 100644 pebble/tool/testdata/mixed/main.go create mode 100644 pebble/tool/testdata/mixed/marker.format-version.000010.011 create mode 100644 pebble/tool/testdata/mixed/marker.manifest.000001.MANIFEST-000001 create mode 100644 pebble/tool/testdata/out-of-order.sst create mode 100644 pebble/tool/testdata/remotecat create mode 100644 pebble/tool/testdata/sstable_check create mode 100644 pebble/tool/testdata/sstable_layout create mode 100644 pebble/tool/testdata/sstable_properties create mode 100644 pebble/tool/testdata/sstable_scan create mode 100644 pebble/tool/testdata/sstable_space create mode 100644 pebble/tool/testdata/wal_dump create mode 100644 pebble/tool/tool.go create mode 100644 pebble/tool/util.go create mode 100644 pebble/tool/wal.go create mode 100644 pebble/tool/wal_test.go create mode 100644 pebble/version_set.go create mode 100644 pebble/version_set_test.go create mode 100644 pebble/vfs/atomicfs/marker.go create mode 100644 pebble/vfs/atomicfs/marker_test.go create mode 100644 pebble/vfs/atomicfs/testdata/marker create mode 100644 pebble/vfs/clone.go create mode 100644 pebble/vfs/default_linux.go create mode 100644 pebble/vfs/default_unix.go create mode 100644 pebble/vfs/default_windows.go create mode 100644 pebble/vfs/disk_full.go create mode 100644 pebble/vfs/disk_full_test.go create mode 100644 pebble/vfs/disk_health.go create mode 100644 pebble/vfs/disk_health_test.go create mode 100644 pebble/vfs/disk_usage_linux.go create mode 100644 pebble/vfs/disk_usage_netbsd.go create mode 100644 pebble/vfs/disk_usage_openbsd.go create mode 100644 pebble/vfs/disk_usage_unix.go create mode 100644 pebble/vfs/disk_usage_windows.go create mode 100644 pebble/vfs/errorfs/dsl.go create mode 100644 pebble/vfs/errorfs/errorfs.go create mode 100644 pebble/vfs/errorfs/errorfs_test.go create mode 100644 pebble/vfs/errorfs/testdata/errorfs create mode 100644 pebble/vfs/errors_unix.go create mode 100644 pebble/vfs/errors_unix_test.go create mode 100644 pebble/vfs/errors_windows.go create mode 100644 pebble/vfs/fadvise_generic.go create mode 100644 pebble/vfs/fadvise_linux.go create mode 100644 pebble/vfs/fd_test.go create mode 100644 pebble/vfs/file_lock_generic.go create mode 100644 pebble/vfs/file_lock_test.go create mode 100644 pebble/vfs/file_lock_unix.go create mode 100644 pebble/vfs/file_lock_windows.go create mode 100644 pebble/vfs/logging_fs.go create mode 100644 pebble/vfs/mem_fs.go create mode 100644 pebble/vfs/mem_fs_test.go create mode 100644 pebble/vfs/syncing_file.go create mode 100644 pebble/vfs/syncing_file_linux_test.go create mode 100644 pebble/vfs/syncing_file_test.go create mode 100644 pebble/vfs/testdata/memfs_lock create mode 100644 pebble/vfs/testdata/vfs create mode 100644 pebble/vfs/vfs.go create mode 100644 pebble/vfs/vfs_test.go create mode 100644 pebble/vfs/vfstest/vfstest.go diff --git a/nekryptology/pkg/core/curves/bls48581_curve.go b/nekryptology/pkg/core/curves/bls48581_curve.go index 6326408..bec397d 100644 --- a/nekryptology/pkg/core/curves/bls48581_curve.go +++ b/nekryptology/pkg/core/curves/bls48581_curve.go @@ -7,6 +7,7 @@ package curves import ( + "arena" "errors" "fmt" "io" @@ -47,9 +48,9 @@ func (s *ScalarBls48581) Random(reader io.Reader) Scalar { func (s *ScalarBls48581) Hash(bytes []byte) Scalar { DST := []byte("BLS_SIG_BLS48581G1_XMD:SHA-512_SVDW_RO_NUL_") u := bls48581.Hash_to_field(ext.MC_SHA2, bls48581.HASH_TYPE, DST, bytes, 2) - u[0].Add(u[1]) - b := u[0].Redc() - b.Mod(bls48581.NewBIGints(bls48581.CURVE_Order)) + u[0].Add(u[1], nil) + b := u[0].Redc(nil) + b.Mod(bls48581.NewBIGints(bls48581.CURVE_Order, nil), nil) return &ScalarBls48581{ Value: b, point: s.point, @@ -58,14 +59,14 @@ func (s *ScalarBls48581) Hash(bytes []byte) Scalar { func (s *ScalarBls48581) Zero() Scalar { return &ScalarBls48581{ - Value: bls48581.NewBIGint(0), + Value: bls48581.NewBIGint(0, nil), point: s.point, } } func (s *ScalarBls48581) One() Scalar { return &ScalarBls48581{ - Value: bls48581.NewBIGint(1), + Value: bls48581.NewBIGint(1, nil), point: s.point, } } @@ -75,7 +76,7 @@ func (s *ScalarBls48581) IsZero() bool { } func (s *ScalarBls48581) IsOne() bool { - t := bls48581.NewBIGint(1) + t := bls48581.NewBIGint(1, nil) t.Sub(s.Value) return t.IsZero() } @@ -94,15 +95,15 @@ func (s *ScalarBls48581) IsEven() bool { func (s *ScalarBls48581) New(value int) Scalar { if value > 0 { - t := bls48581.NewBIGint(value) - t.Mod(bls48581.NewBIGints(bls48581.CURVE_Order)) + t := bls48581.NewBIGint(value, nil) + t.Mod(bls48581.NewBIGints(bls48581.CURVE_Order, nil), nil) return &ScalarBls48581{ Value: t, point: s.point, } } else { - t := bls48581.NewBIGint(-value) - v := bls48581.NewBIGints(bls48581.CURVE_Order) + t := bls48581.NewBIGint(-value, nil) + v := bls48581.NewBIGints(bls48581.CURVE_Order, nil) v.Sub(t) return &ScalarBls48581{ Value: v, @@ -121,8 +122,8 @@ func (s *ScalarBls48581) Cmp(rhs Scalar) int { } func (s *ScalarBls48581) Square() Scalar { - sqr := bls48581.NewBIGcopy(s.Value) - sqr = bls48581.Modsqr(sqr, bls48581.NewBIGints(bls48581.CURVE_Order)) + sqr := bls48581.NewBIGcopy(s.Value, nil) + sqr = bls48581.Modsqr(sqr, bls48581.NewBIGints(bls48581.CURVE_Order, nil), nil) return &ScalarBls48581{ Value: sqr, point: s.point, @@ -130,8 +131,13 @@ func (s *ScalarBls48581) Square() Scalar { } func (s *ScalarBls48581) Double() Scalar { - dbl := bls48581.NewBIGcopy(s.Value) - dbl = bls48581.Modmul(dbl, bls48581.NewBIGint(2), bls48581.NewBIGints(bls48581.CURVE_Order)) + dbl := bls48581.NewBIGcopy(s.Value, nil) + dbl = bls48581.Modmul( + dbl, + bls48581.NewBIGint(2, nil), + bls48581.NewBIGints(bls48581.CURVE_Order, nil), + nil, + ) return &ScalarBls48581{ Value: dbl, point: s.point, @@ -139,8 +145,8 @@ func (s *ScalarBls48581) Double() Scalar { } func (s *ScalarBls48581) Invert() (Scalar, error) { - v := bls48581.NewBIGcopy(s.Value) - v.Invmodp(bls48581.NewBIGints(bls48581.CURVE_Order)) + v := bls48581.NewBIGcopy(s.Value, nil) + v.Invmodp(bls48581.NewBIGints(bls48581.CURVE_Order, nil)) if v == nil { return nil, fmt.Errorf("inverse doesn't exist") } @@ -155,9 +161,9 @@ func (s *ScalarBls48581) Sqrt() (Scalar, error) { } func (s *ScalarBls48581) Cube() Scalar { - value := bls48581.NewBIGcopy(s.Value) - value = bls48581.Modsqr(value, bls48581.NewBIGints(bls48581.CURVE_Order)) - value = bls48581.Modmul(value, s.Value, bls48581.NewBIGints(bls48581.CURVE_Order)) + value := bls48581.NewBIGcopy(s.Value, nil) + value = bls48581.Modsqr(value, bls48581.NewBIGints(bls48581.CURVE_Order, nil), nil) + value = bls48581.Modmul(value, s.Value, bls48581.NewBIGints(bls48581.CURVE_Order, nil), nil) return &ScalarBls48581{ Value: value, point: s.point, @@ -167,8 +173,11 @@ func (s *ScalarBls48581) Cube() Scalar { func (s *ScalarBls48581) Add(rhs Scalar) Scalar { r, ok := rhs.(*ScalarBls48581) if ok { - value := bls48581.NewBIGcopy(s.Value) - value = bls48581.ModAdd(value, r.Value, bls48581.NewBIGints(bls48581.CURVE_Order)) + mem := arena.NewArena() + defer mem.Free() + value := bls48581.NewBIGcopy(s.Value, mem) + value = bls48581.ModAdd(value, r.Value, bls48581.NewBIGints(bls48581.CURVE_Order, mem), mem) + value = bls48581.NewBIGcopy(value, nil) return &ScalarBls48581{ Value: value, point: s.point, @@ -181,9 +190,12 @@ func (s *ScalarBls48581) Add(rhs Scalar) Scalar { func (s *ScalarBls48581) Sub(rhs Scalar) Scalar { r, ok := rhs.(*ScalarBls48581) if ok { - value := bls48581.NewBIGcopy(r.Value) - value = bls48581.Modneg(value, bls48581.NewBIGints(bls48581.CURVE_Order)) - value = bls48581.ModAdd(value, s.Value, bls48581.NewBIGints(bls48581.CURVE_Order)) + mem := arena.NewArena() + defer mem.Free() + value := bls48581.NewBIGcopy(r.Value, mem) + value = bls48581.Modneg(value, bls48581.NewBIGints(bls48581.CURVE_Order, mem), mem) + value = bls48581.ModAdd(value, s.Value, bls48581.NewBIGints(bls48581.CURVE_Order, mem), mem) + value = bls48581.NewBIGcopy(value, nil) return &ScalarBls48581{ Value: value, point: s.point, @@ -196,8 +208,11 @@ func (s *ScalarBls48581) Sub(rhs Scalar) Scalar { func (s *ScalarBls48581) Mul(rhs Scalar) Scalar { r, ok := rhs.(*ScalarBls48581) if ok { - value := bls48581.NewBIGcopy(s.Value) - value = bls48581.Modmul(value, r.Value, bls48581.NewBIGints(bls48581.CURVE_Order)) + mem := arena.NewArena() + defer mem.Free() + value := bls48581.NewBIGcopy(s.Value, mem) + value = bls48581.Modmul(value, r.Value, bls48581.NewBIGints(bls48581.CURVE_Order, mem), mem) + value = bls48581.NewBIGcopy(value, nil) return &ScalarBls48581{ Value: value, point: s.point, @@ -214,9 +229,12 @@ func (s *ScalarBls48581) MulAdd(y, z Scalar) Scalar { func (s *ScalarBls48581) Div(rhs Scalar) Scalar { r, ok := rhs.(*ScalarBls48581) if ok { - value := bls48581.NewBIGcopy(r.Value) - value.Invmodp(bls48581.NewBIGints(bls48581.CURVE_Order)) - value = bls48581.Modmul(value, s.Value, bls48581.NewBIGints(bls48581.CURVE_Order)) + mem := arena.NewArena() + defer mem.Free() + value := bls48581.NewBIGcopy(r.Value, mem) + value.Invmodp(bls48581.NewBIGints(bls48581.CURVE_Order, mem)) + value = bls48581.Modmul(value, s.Value, bls48581.NewBIGints(bls48581.CURVE_Order, mem), mem) + value = bls48581.NewBIGcopy(value, nil) return &ScalarBls48581{ Value: value, point: s.point, @@ -227,8 +245,11 @@ func (s *ScalarBls48581) Div(rhs Scalar) Scalar { } func (s *ScalarBls48581) Neg() Scalar { - value := bls48581.NewBIGcopy(s.Value) - value = bls48581.Modneg(value, bls48581.NewBIGints(bls48581.CURVE_Order)) + mem := arena.NewArena() + defer mem.Free() + value := bls48581.NewBIGcopy(s.Value, mem) + value = bls48581.Modneg(value, bls48581.NewBIGints(bls48581.CURVE_Order, mem), mem) + value = bls48581.NewBIGcopy(value, nil) return &ScalarBls48581{ Value: value, point: s.point, @@ -244,7 +265,7 @@ func (s *ScalarBls48581) SetBigInt(v *big.Int) (Scalar, error) { copy(t[bls48581.MODBYTES-uint(len(b)):], b) i := bls48581.FromBytes(t) - i.Mod(bls48581.NewBIGints(bls48581.CURVE_Order)) + i.Mod(bls48581.NewBIGints(bls48581.CURVE_Order, nil), nil) return &ScalarBls48581{ Value: i, point: s.point, @@ -298,7 +319,7 @@ func (s *ScalarBls48581) Point() Point { } func (s *ScalarBls48581) Clone() Scalar { - value := bls48581.NewBIGcopy(s.Value) + value := bls48581.NewBIGcopy(s.Value, nil) return &ScalarBls48581{ Value: value, point: s.point, @@ -306,7 +327,7 @@ func (s *ScalarBls48581) Clone() Scalar { } func (s *ScalarBls48581) SetPoint(p Point) PairingScalar { - value := bls48581.NewBIGcopy(s.Value) + value := bls48581.NewBIGcopy(s.Value, nil) return &ScalarBls48581{ Value: value, point: p, @@ -314,7 +335,7 @@ func (s *ScalarBls48581) SetPoint(p Point) PairingScalar { } func (s *ScalarBls48581) Order() *big.Int { - b := bls48581.NewBIGints(bls48581.CURVE_Order) + b := bls48581.NewBIGints(bls48581.CURVE_Order, nil) bytes := make([]byte, bls48581.MODBYTES) b.ToBytes(bytes) return new(big.Int).SetBytes(bytes) @@ -369,7 +390,7 @@ func (p *PointBls48581G1) Hash(bytes []byte) Point { func (p *PointBls48581G1) Identity() Point { g1 := bls48581.ECP_generator() - g1 = g1.Mul(bls48581.NewBIGint(0)) + g1 = g1.Mul(bls48581.NewBIGint(0, nil), nil, nil) return &PointBls48581G1{ Value: g1, } @@ -384,7 +405,7 @@ func (p *PointBls48581G1) Generator() Point { } func (p *PointBls48581G1) IsIdentity() bool { - return p.Value.Is_infinity() + return p.Value.Is_infinity(nil) } func (p *PointBls48581G1) IsNegative() bool { @@ -395,18 +416,18 @@ func (p *PointBls48581G1) IsNegative() bool { } func (p *PointBls48581G1) IsOnCurve() bool { - return bls48581.G1member(p.Value) + return bls48581.G1member(p.Value, nil) } func (p *PointBls48581G1) Double() Point { - v := bls48581.NewECP() + v := bls48581.NewECP(nil) v.Copy(p.Value) - v.Dbl() + v.Dbl(nil) return &PointBls48581G1{v} } func (p *PointBls48581G1) Scalar() Scalar { - value := bls48581.NewBIG() + value := bls48581.NewBIG(nil) return &ScalarBls48581{ Value: value, point: new(PointBls48581G1), @@ -414,9 +435,9 @@ func (p *PointBls48581G1) Scalar() Scalar { } func (p *PointBls48581G1) Neg() Point { - v := bls48581.NewECP() + v := bls48581.NewECP(nil) v.Copy(p.Value) - v.Neg() + v.Neg(nil) return &PointBls48581G1{v} } @@ -426,9 +447,9 @@ func (p *PointBls48581G1) Add(rhs Point) Point { } r, ok := rhs.(*PointBls48581G1) if ok { - v := bls48581.NewECP() + v := bls48581.NewECP(nil) v.Copy(p.Value) - v.Add(r.Value) + v.Add(r.Value, nil) return &PointBls48581G1{v} } else { return nil @@ -441,9 +462,9 @@ func (p *PointBls48581G1) Sub(rhs Point) Point { } r, ok := rhs.(*PointBls48581G1) if ok { - v := bls48581.NewECP() + v := bls48581.NewECP(nil) v.Copy(p.Value) - v.Sub(r.Value) + v.Sub(r.Value, nil) return &PointBls48581G1{v} } else { return nil @@ -456,9 +477,11 @@ func (p *PointBls48581G1) Mul(rhs Scalar) Point { } r, ok := rhs.(*ScalarBls48581) if ok { - v := bls48581.NewECP() + mem := arena.NewArena() + defer mem.Free() + v := bls48581.NewECP(mem) v.Copy(p.Value) - v = v.Mul(r.Value) + v = v.Mul(r.Value, nil, mem) return &PointBls48581G1{v} } else { return nil @@ -481,7 +504,7 @@ func (p *PointBls48581G1) Set(x, y *big.Int) (Point, error) { y.FillBytes(yBytes) xBig := bls48581.FromBytes(xBytes) yBig := bls48581.FromBytes(yBytes) - v := bls48581.NewECPbigs(xBig, yBig) + v := bls48581.NewECPbigs(xBig, yBig, nil) if v == nil { return nil, fmt.Errorf("invalid coordinates") } @@ -504,7 +527,7 @@ func (p *PointBls48581G1) FromAffineCompressed(bytes []byte) (Point, error) { var b [bls48581.MODBYTES + 1]byte copy(b[:], bytes) value := bls48581.ECP_fromBytes(b[:]) - if value == nil || value.Is_infinity() { + if value == nil || value.Is_infinity(nil) { return nil, errors.New("could not decode") } return &PointBls48581G1{value}, nil @@ -514,7 +537,7 @@ func (p *PointBls48581G1) FromAffineUncompressed(bytes []byte) (Point, error) { var b [bls48581.MODBYTES*2 + 1]byte copy(b[:], bytes) value := bls48581.ECP_fromBytes(b[:]) - if value == nil || value.Is_infinity() { + if value == nil || value.Is_infinity(nil) { return nil, errors.New("could not decode") } return &PointBls48581G1{value}, nil @@ -541,8 +564,10 @@ func (p *PointBls48581G1) SumOfProducts(points []Point, scalars []Scalar) Point } nScalars[i] = s.Value } - value := bls48581.ECP_muln(len(points), nPoints, nScalars) - if value == nil || value.Is_infinity() { + mem := arena.NewArena() + defer mem.Free() + value := bls48581.ECP_muln(len(points), nPoints, nScalars, mem) + if value == nil || value.Is_infinity(mem) { return nil } return &PointBls48581G1{value} @@ -563,77 +588,60 @@ func (p *PointBls48581G1) Pairing(rhs PairingPoint) Scalar { return &ScalarBls48581Gt{pair} } +func (p *PointBls48581G1) Ate2Pairing( + rhs *PointBls48581G2, + lhs2 *PointBls48581G1, + rhs2 *PointBls48581G2, +) Scalar { + ate2 := bls48581.Ate2(rhs2.Value, p.Value, rhs2.Value, lhs2.Value) + + return &ScalarBls48581Gt{ate2} +} + func (p *PointBls48581G1) MultiPairing(points ...PairingPoint) Scalar { return bls48multiPairing(points...) } func (p *PointBls48581G1) X() *big.Int { bytes := make([]byte, bls48581.MODBYTES) - p.Value.GetX().ToBytes(bytes[:]) + p.Value.GetX(nil).ToBytes(bytes[:]) return new(big.Int).SetBytes(bytes) } func (p *PointBls48581G1) Y() *big.Int { bytes := make([]byte, bls48581.MODBYTES) - p.Value.GetY().ToBytes(bytes[:]) + p.Value.GetY(nil).ToBytes(bytes[:]) return new(big.Int).SetBytes(bytes) } func (p *PointBls48581G1) Modulus() *big.Int { - b := bls48581.NewBIGints(bls48581.Modulus) + b := bls48581.NewBIGints(bls48581.Modulus, nil) bytes := make([]byte, bls48581.MODBYTES) b.ToBytes(bytes) return new(big.Int).SetBytes(bytes) } func (p *PointBls48581G1) MarshalBinary() ([]byte, error) { - return pointMarshalBinary(p) + return nil, nil } func (p *PointBls48581G1) UnmarshalBinary(input []byte) error { - pt, err := pointUnmarshalBinary(input) - if err != nil { - return err - } - ppt, ok := pt.(*PointBls48581G1) - if !ok { - return fmt.Errorf("invalid point") - } - p.Value = ppt.Value return nil } func (p *PointBls48581G1) MarshalText() ([]byte, error) { - return pointMarshalText(p) + return nil, nil } func (p *PointBls48581G1) UnmarshalText(input []byte) error { - pt, err := pointUnmarshalText(input) - if err != nil { - return err - } - ppt, ok := pt.(*PointBls48581G1) - if !ok { - return fmt.Errorf("invalid point") - } - p.Value = ppt.Value return nil } func (p *PointBls48581G1) MarshalJSON() ([]byte, error) { - return pointMarshalJson(p) + return nil, nil } func (p *PointBls48581G1) UnmarshalJSON(input []byte) error { - pt, err := pointUnmarshalJson(input) - if err != nil { - return err - } - P, ok := pt.(*PointBls48581G1) - if !ok { - return fmt.Errorf("invalid type") - } - p.Value = P.Value return nil } @@ -646,15 +654,15 @@ func (p *PointBls48581G2) Random(reader io.Reader) Point { func (p *PointBls48581G2) Hash(bytes []byte) Point { DST := []byte("BLS_SIG_BLS48581G2_XMD:SHA-512_SVDW_RO_NUL_") u := bls48581.Hash_to_field(ext.MC_SHA2, bls48581.HASH_TYPE, DST, bytes, 2) - u[0].Add(u[1]) - fp8 := bls48581.NewFP8fp(u[0]) + u[0].Add(u[1], nil) + fp8 := bls48581.NewFP8fp(u[0], nil) v := bls48581.ECP8_map2point(fp8) return &PointBls48581G2{v} } func (p *PointBls48581G2) Identity() Point { g2 := bls48581.ECP8_generator() - g2 = g2.Mul(bls48581.NewBIGint(0)) + g2 = g2.Mul(bls48581.NewBIGint(0, nil), nil) return &PointBls48581G2{ Value: g2, } @@ -669,7 +677,7 @@ func (p *PointBls48581G2) Generator() Point { } func (p *PointBls48581G2) IsIdentity() bool { - return p.Value.Is_infinity() + return p.Value.Is_infinity(nil) } func (p *PointBls48581G2) IsNegative() bool { @@ -680,18 +688,18 @@ func (p *PointBls48581G2) IsNegative() bool { } func (p *PointBls48581G2) IsOnCurve() bool { - return bls48581.G2member(p.Value) + return bls48581.G2member(p.Value, nil) } func (p *PointBls48581G2) Double() Point { - v := bls48581.NewECP8() + v := bls48581.NewECP8(nil) v.Copy(p.Value) - v.Dbl() + v.Dbl(nil) return &PointBls48581G2{v} } func (p *PointBls48581G2) Scalar() Scalar { - value := bls48581.NewBIG() + value := bls48581.NewBIG(nil) return &ScalarBls48581{ Value: value, point: new(PointBls48581G2), @@ -699,9 +707,9 @@ func (p *PointBls48581G2) Scalar() Scalar { } func (p *PointBls48581G2) Neg() Point { - v := bls48581.NewECP8() + v := bls48581.NewECP8(nil) v.Copy(p.Value) - v.Neg() + v.Neg(nil) return &PointBls48581G2{v} } @@ -711,9 +719,9 @@ func (p *PointBls48581G2) Add(rhs Point) Point { } r, ok := rhs.(*PointBls48581G2) if ok { - v := bls48581.NewECP8() + v := bls48581.NewECP8(nil) v.Copy(p.Value) - v.Add(r.Value) + v.Add(r.Value, nil) return &PointBls48581G2{v} } else { return nil @@ -726,9 +734,9 @@ func (p *PointBls48581G2) Sub(rhs Point) Point { } r, ok := rhs.(*PointBls48581G2) if ok { - v := bls48581.NewECP8() + v := bls48581.NewECP8(nil) v.Copy(p.Value) - v.Sub(r.Value) + v.Sub(r.Value, nil) return &PointBls48581G2{v} } else { return nil @@ -741,11 +749,11 @@ func (p *PointBls48581G2) Mul(rhs Scalar) Point { } r, ok := rhs.(*ScalarBls48581) if ok { - v := bls48581.NewECP8() + mem := arena.NewArena() + defer mem.Free() + v := bls48581.NewECP8(nil) v.Copy(p.Value) - bytes := make([]byte, bls48581.MODBYTES) - r.Value.ToBytes(bytes) - v = v.Mul(bls48581.FromBytes(bytes)) + v = v.Mul(r.Value, mem) return &PointBls48581G2{v} } else { return nil @@ -768,8 +776,8 @@ func (p *PointBls48581G2) Set(x, y *big.Int) (Point, error) { y.FillBytes(yBytes) xBig := bls48581.FP8_fromBytes(xBytes) yBig := bls48581.FP8_fromBytes(yBytes) - v := bls48581.NewECP8fp8s(xBig, yBig) - if v == nil || v.Is_infinity() { + v := bls48581.NewECP8fp8s(xBig, yBig, nil) + if v == nil || v.Is_infinity(nil) { return nil, fmt.Errorf("invalid coordinates") } return &PointBls48581G2{v}, nil @@ -791,7 +799,7 @@ func (p *PointBls48581G2) FromAffineCompressed(bytes []byte) (Point, error) { var b [bls48581.MODBYTES*8 + 1]byte copy(b[:], bytes) value := bls48581.ECP8_fromBytes(b[:]) - if value == nil || value.Is_infinity() { + if value == nil || value.Is_infinity(nil) { return nil, errors.New("could not decode") } return &PointBls48581G2{value}, nil @@ -801,7 +809,7 @@ func (p *PointBls48581G2) FromAffineUncompressed(bytes []byte) (Point, error) { var b [bls48581.MODBYTES*16 + 1]byte copy(b[:], bytes) value := bls48581.ECP8_fromBytes(b[:]) - if value == nil || value.Is_infinity() { + if value == nil || value.Is_infinity(nil) { return nil, errors.New("could not decode") } return &PointBls48581G2{value}, nil @@ -828,8 +836,8 @@ func (p *PointBls48581G2) SumOfProducts(points []Point, scalars []Scalar) Point } nScalars[i] = s.Value } - value := bls48581.Mul16(nPoints, nScalars) - if value == nil || value.Is_infinity() { + value := bls48581.Mul16(nPoints, nScalars, nil) + if value == nil || value.Is_infinity(nil) { return nil } return &PointBls48581G2{value} @@ -855,74 +863,47 @@ func (p *PointBls48581G2) MultiPairing(points ...PairingPoint) Scalar { } func (p *PointBls48581G2) X() *big.Int { - x := p.Value.GetX() + x := p.Value.GetX(nil) bytes := make([]byte, 8*bls48581.MODBYTES) x.ToBytes(bytes) return new(big.Int).SetBytes(bytes) } func (p *PointBls48581G2) Y() *big.Int { - y := p.Value.GetY() + y := p.Value.GetY(nil) bytes := make([]byte, 8*bls48581.MODBYTES) y.ToBytes(bytes) return new(big.Int).SetBytes(bytes) } func (p *PointBls48581G2) Modulus() *big.Int { - b := bls48581.NewBIGints(bls48581.Modulus) + b := bls48581.NewBIGints(bls48581.Modulus, nil) bytes := make([]byte, bls48581.MODBYTES) b.ToBytes(bytes) return new(big.Int).SetBytes(bytes) } func (p *PointBls48581G2) MarshalBinary() ([]byte, error) { - return pointMarshalBinary(p) + return nil, nil } func (p *PointBls48581G2) UnmarshalBinary(input []byte) error { - pt, err := pointUnmarshalBinary(input) - if err != nil { - return err - } - ppt, ok := pt.(*PointBls48581G2) - if !ok { - return fmt.Errorf("invalid point") - } - p.Value = ppt.Value return nil } func (p *PointBls48581G2) MarshalText() ([]byte, error) { - return pointMarshalText(p) + return nil, nil } func (p *PointBls48581G2) UnmarshalText(input []byte) error { - pt, err := pointUnmarshalText(input) - if err != nil { - return err - } - ppt, ok := pt.(*PointBls48581G2) - if !ok { - return fmt.Errorf("invalid point") - } - p.Value = ppt.Value return nil } func (p *PointBls48581G2) MarshalJSON() ([]byte, error) { - return pointMarshalJson(p) + return nil, nil } func (p *PointBls48581G2) UnmarshalJSON(input []byte) error { - pt, err := pointUnmarshalJson(input) - if err != nil { - return err - } - P, ok := pt.(*PointBls48581G2) - if !ok { - return fmt.Errorf("invalid type") - } - p.Value = P.Value return nil } @@ -931,21 +912,25 @@ func bls48multiPairing(points ...PairingPoint) Scalar { return nil } valid := true - r := bls48581.Initmp() + mem := arena.NewArena() + defer mem.Free() + r := bls48581.Initmp(mem) for i := 0; i < len(points); i += 2 { pt1, ok := points[i].(*PointBls48581G1) valid = valid && ok pt2, ok := points[i+1].(*PointBls48581G2) valid = valid && ok if valid { - bls48581.Another(r, pt2.Value, pt1.Value) + inner := arena.NewArena() + bls48581.Another(r, pt2.Value, pt1.Value, inner) + inner.Free() } } if !valid { return nil } - v := bls48581.Miller(r) + v := bls48581.Miller(r, mem) v = bls48581.Fexp(v) return &ScalarBls48581Gt{v} } @@ -973,15 +958,15 @@ func (s *ScalarBls48581Gt) Hash(bytes []byte) Scalar { } func (s *ScalarBls48581Gt) Zero() Scalar { - return &ScalarBls48581Gt{bls48581.NewFP48int(0)} + return &ScalarBls48581Gt{bls48581.NewFP48int(0, nil)} } func (s *ScalarBls48581Gt) One() Scalar { - return &ScalarBls48581Gt{bls48581.NewFP48int(1)} + return &ScalarBls48581Gt{bls48581.NewFP48int(1, nil)} } func (s *ScalarBls48581Gt) IsZero() bool { - return s.Value.IsZero() + return s.Value.IsZero(nil) } func (s *ScalarBls48581Gt) IsOne() bool { @@ -1034,7 +1019,7 @@ func (s *ScalarBls48581Gt) IsEven() bool { } func (s *ScalarBls48581Gt) New(input int) Scalar { - fp := bls48581.NewFP48int(input) + fp := bls48581.NewFP48int(input, nil) return &ScalarBls48581Gt{fp} } @@ -1048,20 +1033,20 @@ func (s *ScalarBls48581Gt) Cmp(rhs Scalar) int { } func (s *ScalarBls48581Gt) Square() Scalar { - v := bls48581.NewFP48copy(s.Value) - v.Sqr() + v := bls48581.NewFP48copy(s.Value, nil) + v.Sqr(nil) return &ScalarBls48581Gt{v} } func (s *ScalarBls48581Gt) Double() Scalar { - v := bls48581.NewFP48copy(s.Value) - v.Mul(bls48581.NewFP48int(2)) + v := bls48581.NewFP48copy(s.Value, nil) + v.Mul(bls48581.NewFP48int(2, nil), nil) return &ScalarBls48581Gt{v} } func (s *ScalarBls48581Gt) Invert() (Scalar, error) { - v := bls48581.NewFP48copy(s.Value) - v.Invert() + v := bls48581.NewFP48copy(s.Value, nil) + v.Invert(nil) if v == nil { return nil, fmt.Errorf("not invertible") } @@ -1074,9 +1059,9 @@ func (s *ScalarBls48581Gt) Sqrt() (Scalar, error) { } func (s *ScalarBls48581Gt) Cube() Scalar { - v := bls48581.NewFP48copy(s.Value) - v.Sqr() - v.Mul(s.Value) + v := bls48581.NewFP48copy(s.Value, nil) + v.Sqr(nil) + v.Mul(s.Value, nil) return &ScalarBls48581Gt{v} } @@ -1093,8 +1078,8 @@ func (s *ScalarBls48581Gt) Sub(rhs Scalar) Scalar { func (s *ScalarBls48581Gt) Mul(rhs Scalar) Scalar { r, ok := rhs.(*ScalarBls48581Gt) if ok { - v := bls48581.NewFP48copy(s.Value) - v.Mul(r.Value) + v := bls48581.NewFP48copy(s.Value, nil) + v.Mul(r.Value, nil) return &ScalarBls48581Gt{v} } else { return nil @@ -1108,9 +1093,9 @@ func (s *ScalarBls48581Gt) MulAdd(y, z Scalar) Scalar { func (s *ScalarBls48581Gt) Div(rhs Scalar) Scalar { r, ok := rhs.(*ScalarBls48581Gt) if ok { - v := bls48581.NewFP48copy(r.Value) - v.Invert() - v.Mul(s.Value) + v := bls48581.NewFP48copy(r.Value, nil) + v.Invert(nil) + v.Mul(s.Value, nil) return &ScalarBls48581Gt{v} } else { return nil @@ -1160,7 +1145,7 @@ func (s *ScalarBls48581Gt) SetBytesWide(bytes []byte) (Scalar, error) { } func (s *ScalarBls48581Gt) Clone() Scalar { - fp := bls48581.NewFP48copy(s.Value) + fp := bls48581.NewFP48copy(s.Value, nil) return &ScalarBls48581Gt{ Value: fp, } diff --git a/nekryptology/pkg/core/curves/bls48581_curve_test.go b/nekryptology/pkg/core/curves/bls48581_curve_test.go index d7540c5..72e24f3 100644 --- a/nekryptology/pkg/core/curves/bls48581_curve_test.go +++ b/nekryptology/pkg/core/curves/bls48581_curve_test.go @@ -78,9 +78,9 @@ func TestScalarBls48581G1Invert(t *testing.T) { nine := bls48581G1.Scalar.New(9) actual, _ := nine.Invert() sa, _ := actual.(*ScalarBls48581) - expected, err := bls48581G1.Scalar.SetBigInt(bhex("ab22a52d6e7108e9eabb0e17e8139cf4b9392413a05486ec3dcef3b90bea3db988c1478b9ec2b4f1382ab890f18c0c9a0f85d504cc493f9b79f8c84e41d01ae5070000000000000000")) + expected, err := bls48581G1.Scalar.SetBigInt(bhex("000000000000000007e51ad0414ec8f8799b3f49cc04d5850f9a0c8cf190b82a38f1b4c29e8b47c188b93dea0bb9f3ce3dec8654a0132439b9f49c13e8170ebbeae908716e2da522ab")) require.NoError(t, err) - require.Equal(t, sa.Cmp(expected), 0) + require.Equal(t, sa.Value.ToString(), expected.(*ScalarBls48581).Value.ToString()) } func TestScalarBls48581G1Add(t *testing.T) { @@ -91,11 +91,11 @@ func TestScalarBls48581G1Add(t *testing.T) { require.NotNil(t, fifteen) expected := bls48581G1.Scalar.New(15) require.Equal(t, expected.Cmp(fifteen), 0) - qq := bls48581.NewBIGints(bls48581.CURVE_Order) - qq.Sub(bls48581.NewBIGint(3)) + qq := bls48581.NewBIGints(bls48581.CURVE_Order, nil) + qq.Sub(bls48581.NewBIGint(3, nil)) upper := &ScalarBls48581{ - Value: bls48581.NewBIGcopy(qq), + Value: bls48581.NewBIGcopy(qq, nil), } actual := upper.Add(nine) require.NotNil(t, actual) @@ -106,8 +106,8 @@ func TestScalarBls48581G1Sub(t *testing.T) { bls48581G1 := BLS48581G1() nine := bls48581G1.Scalar.New(9) six := bls48581G1.Scalar.New(6) - n := bls48581.NewFPbig(bls48581.NewBIGints(bls48581.CURVE_Order)) - n.Sub(bls48581.NewFPint(3)) + n := bls48581.NewFPbig(bls48581.NewBIGints(bls48581.CURVE_Order, nil), nil) + n.Sub(bls48581.NewFPint(3, nil), nil) expected := bls48581G1.Scalar.New(0).Sub(bls48581G1.Scalar.New(3)) actual := six.Sub(nine) @@ -138,7 +138,7 @@ func TestScalarBls48581G1Serialize(t *testing.T) { sc := bls48581G1.Scalar.New(255) sequence := sc.Bytes() require.Equal(t, len(sequence), 73) - require.Equal(t, sequence, []byte{0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}) + require.Equal(t, sequence, []byte{0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff}) ret, err := bls48581G1.Scalar.SetBytes(sequence) require.NoError(t, err) require.Equal(t, ret.Cmp(sc), 0) diff --git a/nekryptology/pkg/core/curves/curve.go b/nekryptology/pkg/core/curves/curve.go index 420c4a4..424203b 100644 --- a/nekryptology/pkg/core/curves/curve.go +++ b/nekryptology/pkg/core/curves/curve.go @@ -575,11 +575,11 @@ func BLS48581G1() *Curve { func bls48581g1Init() { bls48581g1 = Curve{ Scalar: &ScalarBls48581{ - Value: bls48581.NewBIGint(1), + Value: bls48581.NewBIGint(1, nil), point: new(PointBls48581G1), }, Point: new(PointBls48581G1).Identity(), - Name: BLS12381G1Name, + Name: BLS48581G1Name, } } @@ -592,7 +592,7 @@ func BLS48581G2() *Curve { func bls48581g2Init() { bls48581g2 = Curve{ Scalar: &ScalarBls48581{ - Value: bls48581.NewBIGint(1), + Value: bls48581.NewBIGint(1, nil), point: new(PointBls48581G2), }, Point: new(PointBls48581G2).Identity(), @@ -603,7 +603,7 @@ func bls48581g2Init() { func BLS48581(preferredPoint Point) *PairingCurve { return &PairingCurve{ Scalar: &ScalarBls48581{ - Value: bls48581.NewBIG(), + Value: bls48581.NewBIG(nil), point: preferredPoint, }, PointG1: &PointBls48581G1{ @@ -613,7 +613,7 @@ func BLS48581(preferredPoint Point) *PairingCurve { Value: bls48581.ECP8_generator(), }, GT: &ScalarBls48581Gt{ - Value: bls48581.NewFP48int(1), + Value: bls48581.NewFP48int(1, nil), }, Name: BLS48581Name, } @@ -863,38 +863,40 @@ type sswuParams struct { // Let `n` be a number of point-scalar pairs. // Let `w` be a window of bits (6..8, chosen based on `n`, see cost factor). // -// 1. Prepare `2^(w-1) - 1` buckets with indices `[1..2^(w-1))` initialized with identity points. -// Bucket 0 is not needed as it would contain points multiplied by 0. -// 2. Convert scalars to a radix-`2^w` representation with signed digits in `[-2^w/2, 2^w/2]`. -// Note: only the last digit may equal `2^w/2`. -// 3. Starting with the last window, for each point `i=[0..n)` add it to a a bucket indexed by -// the point's scalar's value in the window. -// 4. Once all points in a window are sorted into buckets, add buckets by multiplying each -// by their index. Efficient way of doing it is to start with the last bucket and compute two sums: -// intermediate sum from the last to the first, and the full sum made of all intermediate sums. -// 5. Shift the resulting sum of buckets by `w` bits by using `w` doublings. -// 6. Add to the return value. -// 7. Repeat the loop. +// 1. Prepare `2^(w-1) - 1` buckets with indices `[1..2^(w-1))` initialized with identity points. +// Bucket 0 is not needed as it would contain points multiplied by 0. +// 2. Convert scalars to a radix-`2^w` representation with signed digits in `[-2^w/2, 2^w/2]`. +// Note: only the last digit may equal `2^w/2`. +// 3. Starting with the last window, for each point `i=[0..n)` add it to a a bucket indexed by +// the point's scalar's value in the window. +// 4. Once all points in a window are sorted into buckets, add buckets by multiplying each +// by their index. Efficient way of doing it is to start with the last bucket and compute two sums: +// intermediate sum from the last to the first, and the full sum made of all intermediate sums. +// 5. Shift the resulting sum of buckets by `w` bits by using `w` doublings. +// 6. Add to the return value. +// 7. Repeat the loop. // // Approximate cost w/o wNAF optimizations (A = addition, D = doubling): // // ```ascii // cost = (n*A + 2*(2^w/2)*A + w*D + A)*256/w -// | | | | | -// | | | | looping over 256/w windows -// | | | adding to the result -// sorting points | shifting the sum by w bits (to the next window, starting from last window) -// one by one | -// into buckets adding/subtracting all buckets -// multiplied by their indexes -// using a sum of intermediate sums +// +// | | | | | +// | | | | looping over 256/w windows +// | | | adding to the result +// sorting points | shifting the sum by w bits (to the next window, starting from last window) +// one by one | +// into buckets adding/subtracting all buckets +// multiplied by their indexes +// using a sum of intermediate sums +// // ``` // // For large `n`, dominant factor is (n*256/w) additions. // However, if `w` is too big and `n` is not too big, then `(2^w/2)*A` could dominate. // Therefore, the optimal choice of `w` grows slowly as `n` grows. // -// For constant time we use a fixed window of 6 +// # For constant time we use a fixed window of 6 // // This algorithm is adapted from section 4 of . // and https://cacr.uwaterloo.ca/techreports/2010/cacr2010-26.pdf diff --git a/nekryptology/pkg/core/curves/native/bls48581/arch.go b/nekryptology/pkg/core/curves/native/bls48581/arch_32.go similarity index 97% rename from nekryptology/pkg/core/curves/native/bls48581/arch.go rename to nekryptology/pkg/core/curves/native/bls48581/arch_32.go index ac0c2e7..436f2a7 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/arch.go +++ b/nekryptology/pkg/core/curves/native/bls48581/arch_32.go @@ -1,3 +1,5 @@ +//go:build js && wasm + /* * Copyright (c) 2012-2020 MIRACL UK Ltd. * diff --git a/nekryptology/pkg/core/curves/native/bls48581/arch_64.go b/nekryptology/pkg/core/curves/native/bls48581/arch_64.go new file mode 100644 index 0000000..2736e68 --- /dev/null +++ b/nekryptology/pkg/core/curves/native/bls48581/arch_64.go @@ -0,0 +1,28 @@ +//go:build !js && !wasm + +/* + * Copyright (c) 2012-2020 MIRACL UK Ltd. + * + * This file is part of MIRACL Core + * (see https://github.com/miracl/core). + * + * 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. + */ + +/* core BIG number class */ + +package bls48581 + +type Chunk int64 + +const CHUNK int = 64 /* Set word size */ diff --git a/nekryptology/pkg/core/curves/native/bls48581/big.go b/nekryptology/pkg/core/curves/native/bls48581/big_32.go similarity index 99% rename from nekryptology/pkg/core/curves/native/bls48581/big.go rename to nekryptology/pkg/core/curves/native/bls48581/big_32.go index 2540cb9..c556db2 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/big.go +++ b/nekryptology/pkg/core/curves/native/bls48581/big_32.go @@ -1,3 +1,5 @@ +//go:build js && wasm + /* * Copyright (c) 2012-2020 MIRACL UK Ltd. * diff --git a/nekryptology/pkg/core/curves/native/bls48581/big_64.go b/nekryptology/pkg/core/curves/native/bls48581/big_64.go new file mode 100644 index 0000000..31d016c --- /dev/null +++ b/nekryptology/pkg/core/curves/native/bls48581/big_64.go @@ -0,0 +1,999 @@ +//go:build !js && !wasm + +/* + * Copyright (c) 2012-2020 MIRACL UK Ltd. + * + * This file is part of MIRACL Core + * (see https://github.com/miracl/core). + * + * 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. + */ + +/* core BIG number class */ + +package bls48581 + +import ( + "arena" + "math/bits" + "strconv" + + "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" +) + +//import "fmt" + +type BIG struct { + w [NLEN]Chunk +} + +type DBIG struct { + w [2 * NLEN]Chunk +} + +/***************** 64-bit specific code ****************/ + +/* First the 32/64-bit dependent BIG code */ +/* Note that because of the lack of a 128-bit integer, 32 and 64-bit code needs to be done differently */ + +/* return a*b as DBIG */ +func mul(a *BIG, b *BIG, mem *arena.Arena) *DBIG { + c := NewDBIG(mem) + carry := Chunk(0) + + for i := 0; i < NLEN; i++ { + carry = 0 + for j := 0; j < NLEN; j++ { + carry, c.w[i+j] = mulAdd(a.w[i], b.w[j], carry, c.w[i+j]) + } + c.w[NLEN+i] = carry + } + + return c +} + +/* return a^2 as DBIG */ +func sqr(a *BIG, mem *arena.Arena) *DBIG { + c := NewDBIG(mem) + carry := Chunk(0) + + for i := 0; i < NLEN; i++ { + carry = 0 + for j := i + 1; j < NLEN; j++ { + //if a.w[i]<0 {fmt.Printf("Negative m i in sqr\n")} + //if a.w[j]<0 {fmt.Printf("Negative m j in sqr\n")} + carry, c.w[i+j] = mulAdd(2*a.w[i], a.w[j], carry, c.w[i+j]) + } + c.w[NLEN+i] = carry + } + + for i := 0; i < NLEN; i++ { + //if a.w[i]<0 {fmt.Printf("Negative m s in sqr\n")} + top, bot := mulAdd(a.w[i], a.w[i], 0, c.w[2*i]) + + c.w[2*i] = bot + c.w[2*i+1] += top + } + c.norm() + return c +} + +func monty(md *BIG, mc Chunk, d *DBIG, mem *arena.Arena) *BIG { + carry := Chunk(0) + m := Chunk(0) + for i := 0; i < NLEN; i++ { + if mc == -1 { + m = (-d.w[i]) & BMASK + } else { + if mc == 1 { + m = d.w[i] + } else { + m = (mc * d.w[i]) & BMASK + } + } + + carry = 0 + for j := 0; j < NLEN; j++ { + carry, d.w[i+j] = mulAdd(m, md.w[j], carry, d.w[i+j]) + //if m<0 {fmt.Printf("Negative m in monty\n")} + //if md.w[j]<0 {fmt.Printf("Negative m in monty\n")} + } + d.w[NLEN+i] += carry + } + + b := NewBIG(mem) + for i := 0; i < NLEN; i++ { + b.w[i] = d.w[NLEN+i] + } + b.norm() + return b +} + +/* set this[i]+=x*y+c, and return high part */ +func mulAdd(a Chunk, b Chunk, c Chunk, r Chunk) (Chunk, Chunk) { + + tp, bt := bits.Mul64(uint64(a), uint64(b)) // use math/bits intrinsic + bot := Chunk(bt & uint64(BMASK)) + top := Chunk((tp << (64 - BASEBITS)) | (bt >> BASEBITS)) + bot += c + bot += r + carry := bot >> BASEBITS + bot &= BMASK + top += carry + return top, bot + +} + +/************************************************************/ + +func (r *BIG) get(i int) Chunk { + return r.w[i] +} + +func (r *BIG) set(i int, x Chunk) { + r.w[i] = x +} + +func (r *BIG) xortop(x Chunk) { + r.w[NLEN-1] ^= x +} + +/* normalise BIG - force all digits < 2^BASEBITS */ +func (r *BIG) norm() Chunk { + carry := Chunk(0) + for i := 0; i < NLEN-1; i++ { + d := r.w[i] + carry + r.w[i] = d & BMASK + carry = d >> BASEBITS + } + r.w[NLEN-1] = (r.w[NLEN-1] + carry) + return (r.w[NLEN-1] >> ((8 * MODBYTES) % BASEBITS)) +} + +/* Shift right by less than a word */ +func (r *BIG) fshr(k uint) int { + w := r.w[0] & ((Chunk(1) << k) - 1) /* shifted out part */ + for i := 0; i < NLEN-1; i++ { + r.w[i] = (r.w[i] >> k) | ((r.w[i+1] << (BASEBITS - k)) & BMASK) + } + r.w[NLEN-1] = r.w[NLEN-1] >> k + return int(w) +} + +/* Shift right by less than a word */ +func (r *BIG) fshl(k uint) int { + r.w[NLEN-1] = (r.w[NLEN-1] << k) | (r.w[NLEN-2] >> (BASEBITS - k)) + for i := NLEN - 2; i > 0; i-- { + r.w[i] = ((r.w[i] << k) & BMASK) | (r.w[i-1] >> (BASEBITS - k)) + } + r.w[0] = (r.w[0] << k) & BMASK + return int(r.w[NLEN-1] >> ((8 * MODBYTES) % BASEBITS)) /* return excess - only used in ff.c */ +} + +func NewBIG(mem *arena.Arena) *BIG { + var b *BIG + if mem != nil { + b = arena.New[BIG](mem) + } else { + b = new(BIG) + } + for i := 0; i < NLEN; i++ { + b.w[i] = 0 + } + return b +} + +func NewBIGints(x [NLEN]Chunk, mem *arena.Arena) *BIG { + var b *BIG + if mem != nil { + b = arena.New[BIG](mem) + } else { + b = new(BIG) + } + for i := 0; i < NLEN; i++ { + b.w[i] = x[i] + } + return b +} + +func NewBIGint(x int, mem *arena.Arena) *BIG { + var b *BIG + if mem != nil { + b = arena.New[BIG](mem) + } else { + b = new(BIG) + } + b.w[0] = Chunk(x) + for i := 1; i < NLEN; i++ { + b.w[i] = 0 + } + return b +} + +func NewBIGcopy(x *BIG, mem *arena.Arena) *BIG { + var b *BIG + if mem != nil { + b = arena.New[BIG](mem) + } else { + b = new(BIG) + } + for i := 0; i < NLEN; i++ { + b.w[i] = x.w[i] + } + return b +} + +func NewBIGdcopy(x *DBIG, mem *arena.Arena) *BIG { + var b *BIG + if mem != nil { + b = arena.New[BIG](mem) + } else { + b = new(BIG) + } + for i := 0; i < NLEN; i++ { + b.w[i] = x.w[i] + } + return b +} + +/* test for zero */ +func (r *BIG) IsZero() bool { + d := Chunk(0) + for i := 0; i < NLEN; i++ { + d |= r.w[i] + } + return (1 & ((d - 1) >> BASEBITS)) != 0 +} + +/* set to zero */ +func (r *BIG) zero() { + for i := 0; i < NLEN; i++ { + r.w[i] = 0 + } +} + +/* Test for equal to one */ +func (r *BIG) isunity() bool { + d := Chunk(0) + for i := 1; i < NLEN; i++ { + d |= r.w[i] + } + return (1 & ((d - 1) >> BASEBITS) & (((r.w[0] ^ 1) - 1) >> BASEBITS)) != 0 +} + +/* set to one */ +func (r *BIG) one() { + r.w[0] = 1 + for i := 1; i < NLEN; i++ { + r.w[i] = 0 + } +} + +/* Copy from another BIG */ +func (r *BIG) copy(x *BIG) { + for i := 0; i < NLEN; i++ { + r.w[i] = x.w[i] + } +} + +/* Copy from another DBIG */ +func (r *BIG) dcopy(x *DBIG) { + for i := 0; i < NLEN; i++ { + r.w[i] = x.w[i] + } +} + +/* Conditional swap of two bigs depending on d using XOR - no branches */ +func (r *BIG) cswap(b *BIG, d int) Chunk { + c := Chunk(-d) + s := Chunk(0) + v := r.w[0] ^ b.w[1] + va := v + v + va >>= 1 + for i := 0; i < NLEN; i++ { + t := c & (r.w[i] ^ b.w[i]) + t ^= v + e := r.w[i] ^ t + s ^= e // to force calculation of e + r.w[i] = e ^ va + e = b.w[i] ^ t + s ^= e + b.w[i] = e ^ va + } + return s +} + +func (r *BIG) cmove(g *BIG, d int) Chunk { + b := Chunk(-d) + s := Chunk(0) + v := r.w[0] ^ g.w[1] + va := v + v + va >>= 1 + for i := 0; i < NLEN; i++ { + t := (r.w[i] ^ g.w[i]) & b + t ^= v + e := r.w[i] ^ t + s ^= e + r.w[i] = e ^ va + } + return s +} + +/* general shift right */ +func (r *BIG) shr(k uint) { + n := (k % BASEBITS) + m := int(k / BASEBITS) + for i := 0; i < NLEN-m-1; i++ { + r.w[i] = (r.w[m+i] >> n) | ((r.w[m+i+1] << (BASEBITS - n)) & BMASK) + } + r.w[NLEN-m-1] = r.w[NLEN-1] >> n + for i := NLEN - m; i < NLEN; i++ { + r.w[i] = 0 + } +} + +/* general shift left */ +func (r *BIG) shl(k uint) { + n := k % BASEBITS + m := int(k / BASEBITS) + + r.w[NLEN-1] = (r.w[NLEN-1-m] << n) + if NLEN >= m+2 { + r.w[NLEN-1] |= (r.w[NLEN-m-2] >> (BASEBITS - n)) + } + for i := NLEN - 2; i > m; i-- { + r.w[i] = ((r.w[i-m] << n) & BMASK) | (r.w[i-m-1] >> (BASEBITS - n)) + } + r.w[m] = (r.w[0] << n) & BMASK + for i := 0; i < m; i++ { + r.w[i] = 0 + } +} + +/* return number of bits */ +func (r *BIG) nbits() int { + t := NewBIGcopy(r, nil) + k := NLEN - 1 + t.norm() + for k >= 0 && t.w[k] == 0 { + k-- + } + if k < 0 { + return 0 + } + bts := int(BASEBITS) * k + c := t.w[k] + for c != 0 { + c /= 2 + bts++ + } + return bts +} + +func (r *BIG) Nbits() int { + return r.nbits() +} + +/* Convert to Hex String */ +func (r *BIG) ToString() string { + s := "" + len := r.nbits() + + if len%4 == 0 { + len /= 4 + } else { + len /= 4 + len++ + + } + MB := int(MODBYTES * 2) + if len < MB { + len = MB + } + + for i := len - 1; i >= 0; i-- { + b := NewBIGcopy(r, nil) + + b.shr(uint(i * 4)) + s += strconv.FormatInt(int64(b.w[0]&15), 16) + } + return s +} + +func (r *BIG) Add(x *BIG) { + for i := 0; i < NLEN; i++ { + r.w[i] = r.w[i] + x.w[i] + } +} + +func (r *BIG) or(x *BIG) { + for i := 0; i < NLEN; i++ { + r.w[i] = r.w[i] | x.w[i] + } +} + +/* return this+x */ +func (r *BIG) Plus(x *BIG) *BIG { + s := new(BIG) + for i := 0; i < NLEN; i++ { + s.w[i] = r.w[i] + x.w[i] + } + s.norm() + return s +} + +/* this+=x, where x is int */ +func (r *BIG) inc(x int) { + r.norm() + r.w[0] += Chunk(x) +} + +/* this*=c and catch overflow in DBIG */ +func (r *BIG) pxmul(c int, mem *arena.Arena) *DBIG { + m := NewDBIG(mem) + carry := Chunk(0) + for j := 0; j < NLEN; j++ { + carry, m.w[j] = mulAdd(r.w[j], Chunk(c), carry, m.w[j]) + //if c<0 {fmt.Printf("Negative c in pxmul\n")} + //if r.w[j]<0 {fmt.Printf("Negative c in pxmul\n")} + } + m.w[NLEN] = carry + return m +} + +/* return this-x */ +func (r *BIG) Minus(x *BIG) *BIG { + d := new(BIG) + for i := 0; i < NLEN; i++ { + d.w[i] = r.w[i] - x.w[i] + } + return d +} + +/* this-=x */ +func (r *BIG) Sub(x *BIG) { + for i := 0; i < NLEN; i++ { + r.w[i] = r.w[i] - x.w[i] + } +} + +/* reverse subtract this=x-this */ +func (r *BIG) rsub(x *BIG) { + for i := 0; i < NLEN; i++ { + r.w[i] = x.w[i] - r.w[i] + } +} + +/* this-=x, where x is int */ +func (r *BIG) dec(x int) { + r.norm() + r.w[0] -= Chunk(x) +} + +/* this*=x, where x is small intNEXCESS */ +func (r *BIG) pmul(c int) Chunk { + carry := Chunk(0) + // r.norm(); + for i := 0; i < NLEN; i++ { + ak := r.w[i] + r.w[i] = 0 + carry, r.w[i] = mulAdd(ak, Chunk(c), carry, r.w[i]) + //if c<0 {fmt.Printf("Negative c in pmul\n")} + //if ak<0 {fmt.Printf("Negative c in pmul\n")} + } + return carry +} + +/* convert this BIG to byte array */ +func (r *BIG) tobytearray(b []byte, n int) { + //r.norm(); + c := NewBIGcopy(r, nil) + c.norm() + + for i := int(MODBYTES) - 1; i >= 0; i-- { + b[i+n] = byte(c.w[0]) + c.fshr(8) + } +} + +/* convert from byte array to BIG */ +func frombytearray(b []byte, n int) *BIG { + m := NewBIG(nil) + l := len(b) + for i := 0; i < int(MODBYTES); i++ { + m.fshl(8) + if i < l { + m.w[0] += Chunk(int(b[i+n] & 0xff)) + } else { + m.w[0] += Chunk(int(0 & 0xff)) + } + } + return m +} + +func (r *BIG) ToBytes(b []byte) { + r.tobytearray(b, 0) +} + +func FromBytes(b []byte) *BIG { + return frombytearray(b, 0) +} + +/* divide by 3 */ +func (r *BIG) div3() int { + carry := Chunk(0) + r.norm() + base := (Chunk(1) << BASEBITS) + for i := NLEN - 1; i >= 0; i-- { + ak := (carry*base + r.w[i]) + r.w[i] = ak / 3 + carry = ak % 3 + } + return int(carry) +} + +/* return a*b where result fits in a BIG */ +func smul(a *BIG, b *BIG) *BIG { + carry := Chunk(0) + c := NewBIG(nil) + for i := 0; i < NLEN; i++ { + carry = 0 + for j := 0; j < NLEN; j++ { + if i+j < NLEN { + carry, c.w[i+j] = mulAdd(a.w[i], b.w[j], carry, c.w[i+j]) + } + } + } + return c +} + +/* Compare a and b, return 0 if a==b, -1 if ab. Inputs must be normalised */ +func Comp(a *BIG, b *BIG) int { + gt := Chunk(0) + eq := Chunk(1) + for i := NLEN - 1; i >= 0; i-- { + gt |= ((b.w[i] - a.w[i]) >> BASEBITS) & eq + eq &= ((b.w[i] ^ a.w[i]) - 1) >> BASEBITS + } + return int(gt + gt + eq - 1) +} + +/* return parity */ +func (r *BIG) parity() int { + return int(r.w[0] % 2) +} + +/* return n-th bit */ +func (r *BIG) bit(n int) int { + return int((r.w[n/int(BASEBITS)] & (Chunk(1) << (uint(n) % BASEBITS))) >> (uint(n) % BASEBITS)) + // if (r.w[n/int(BASEBITS)] & (Chunk(1) << (uint(n) % BASEBITS))) > 0 { + // return 1 + // } + // return 0 +} + +/* return n last bits */ +func (r *BIG) lastbits(n int) int { + msk := (1 << uint(n)) - 1 + r.norm() + return (int(r.w[0])) & msk +} + +/* set x = x mod 2^m */ +func (r *BIG) mod2m(m uint) { + wd := int(m / BASEBITS) + bt := m % BASEBITS + msk := (Chunk(1) << bt) - 1 + r.w[wd] &= msk + for i := wd + 1; i < NLEN; i++ { + r.w[i] = 0 + } +} + +/* a=1/a mod 2^256. This is very fast! */ +func (r *BIG) invmod2m() { + U := NewBIG(nil) + b := NewBIG(nil) + c := NewBIG(nil) + + U.inc(invmod256(r.lastbits(8))) + + for i := 8; i < BIGBITS; i <<= 1 { + U.norm() + ui := uint(i) + b.copy(r) + b.mod2m(ui) + t1 := smul(U, b) + t1.shr(ui) + c.copy(r) + c.shr(ui) + c.mod2m(ui) + + t2 := smul(U, c) + t2.mod2m(ui) + t1.Add(t2) + t1.norm() + b = smul(t1, U) + t1.copy(b) + t1.mod2m(ui) + + t2.one() + t2.shl(ui) + t1.rsub(t2) + t1.norm() + t1.shl(ui) + U.Add(t1) + } + U.mod2m(8 * MODBYTES) + r.copy(U) + r.norm() +} + +func (r *BIG) ctmod(m *BIG, bd uint, mem *arena.Arena) { + k := bd + sr := NewBIG(mem) + c := NewBIGcopy(m, mem) + r.norm() + + c.shl(k) + + for { + sr.copy(r) + sr.Sub(c) + sr.norm() + r.cmove(sr, int(1-((sr.w[NLEN-1]>>uint(CHUNK-1))&1))) + if k == 0 { + break + } + c.fshr(1) + k -= 1 + } +} + +/* reduce this mod m */ +func (r *BIG) Mod(m *BIG, mem *arena.Arena) { + k := r.nbits() - m.nbits() + if k < 0 { + k = 0 + } + r.ctmod(m, uint(k), mem) +} + +func (r *BIG) ctdiv(m *BIG, bd uint, mem *arena.Arena) { + k := bd + e := NewBIGint(1, mem) + sr := NewBIG(mem) + a := NewBIGcopy(r, mem) + c := NewBIGcopy(m, mem) + r.norm() + r.zero() + + c.shl(k) + e.shl(k) + + for { + sr.copy(a) + sr.Sub(c) + sr.norm() + d := int(1 - ((sr.w[NLEN-1] >> uint(CHUNK-1)) & 1)) + a.cmove(sr, d) + sr.copy(r) + sr.Add(e) + sr.norm() + r.cmove(sr, d) + if k == 0 { + break + } + c.fshr(1) + e.fshr(1) + k -= 1 + } +} + +/* divide this by m */ +func (r *BIG) div(m *BIG, mem *arena.Arena) { + k := r.nbits() - m.nbits() + if k < 0 { + k = 0 + } + r.ctdiv(m, uint(k), mem) +} + +/* get 8*MODBYTES size random number */ +func Random(rng *ext.RAND) *BIG { + m := NewBIG(nil) + var j int = 0 + var r byte = 0 + /* generate random BIG */ + for i := 0; i < 8*int(MODBYTES); i++ { + if j == 0 { + r = rng.GetByte() + } else { + r >>= 1 + } + + b := Chunk(int(r & 1)) + m.shl(1) + m.w[0] += b + j++ + j &= 7 + } + return m +} + +/* Create random BIG in portable way, one bit at a time */ +func Randomnum(q *BIG, rng *ext.RAND) *BIG { + d := NewDBIG(nil) + var j int = 0 + var r byte = 0 + for i := 0; i < 2*q.nbits(); i++ { + if j == 0 { + r = rng.GetByte() + } else { + r >>= 1 + } + + b := Chunk(int(r & 1)) + d.shl(1) + d.w[0] += b + j++ + j &= 7 + } + m := d.Mod(q, nil) + return m +} + +func Randtrunc(q *BIG, trunc int, rng *ext.RAND) *BIG { + m := Randomnum(q, rng) + if q.nbits() > trunc { + m.mod2m(uint(trunc)) + } + return m +} + +/* return a*b mod m */ +func Modmul(a1, b1, m *BIG, mem *arena.Arena) *BIG { + a := NewBIGcopy(a1, mem) + b := NewBIGcopy(b1, mem) + a.Mod(m, mem) + b.Mod(m, mem) + d := mul(a, b, mem) + return d.ctmod(m, uint(m.nbits()), mem) +} + +/* return a^2 mod m */ +func Modsqr(a1, m *BIG, mem *arena.Arena) *BIG { + a := NewBIGcopy(a1, mem) + a.Mod(m, mem) + d := sqr(a, mem) + return d.ctmod(m, uint(m.nbits()), mem) +} + +/* return -a mod m */ +func Modneg(a1, m *BIG, mem *arena.Arena) *BIG { + a := NewBIGcopy(a1, mem) + a.Mod(m, mem) + a.rsub(m) + a.norm() + return a +} + +/* return a+b mod m */ +func ModAdd(a1, b1, m *BIG, mem *arena.Arena) *BIG { + a := NewBIGcopy(a1, mem) + b := NewBIGcopy(b1, mem) + a.Mod(m, mem) + b.Mod(m, mem) + a.Add(b) + a.norm() + a.ctmod(m, 1, mem) + return a +} + +/* Jacobi Symbol (this/p). Returns 0, 1 or -1 */ +func (r *BIG) Jacobi(p *BIG) int { + mem := arena.NewArena() + defer mem.Free() + m := 0 + t := NewBIGint(0, mem) + x := NewBIGint(0, mem) + n := NewBIGint(0, mem) + zilch := NewBIGint(0, mem) + one := NewBIGint(1, mem) + if p.parity() == 0 || Comp(r, zilch) == 0 || Comp(p, one) <= 0 { + return 0 + } + r.norm() + x.copy(r) + n.copy(p) + x.Mod(p, mem) + + for Comp(n, one) > 0 { + if Comp(x, zilch) == 0 { + return 0 + } + n8 := n.lastbits(3) + k := 0 + for x.parity() == 0 { + k++ + x.shr(1) + } + if k%2 == 1 { + m += (n8*n8 - 1) / 8 + } + m += (n8 - 1) * (x.lastbits(2) - 1) / 4 + t.copy(n) + t.Mod(x, mem) + n.copy(x) + x.copy(t) + m %= 2 + + } + if m == 0 { + return 1 + } + return -1 +} + +/* this=1/this mod p. Binary method */ +func (r *BIG) Invmodp(p *BIG) { + mem := arena.NewArena() + defer mem.Free() + r.Mod(p, mem) + if r.IsZero() { + return + } + u := NewBIGcopy(r, mem) + v := NewBIGcopy(p, mem) + x1 := NewBIGint(1, mem) + x2 := NewBIGint(0, mem) + t := NewBIGint(0, mem) + one := NewBIGint(1, mem) + for Comp(u, one) != 0 && Comp(v, one) != 0 { + for u.parity() == 0 { + u.fshr(1) + t.copy(x1) + t.Add(p) + x1.cmove(t, x1.parity()) + x1.norm() + x1.fshr(1) + } + for v.parity() == 0 { + v.fshr(1) + t.copy(x2) + t.Add(p) + x2.cmove(t, x2.parity()) + x2.norm() + x2.fshr(1) + } + if Comp(u, v) >= 0 { + u.Sub(v) + u.norm() + t.copy(x1) + t.Add(p) + x1.cmove(t, (Comp(x1, x2)>>1)&1) + x1.Sub(x2) + x1.norm() + } else { + v.Sub(u) + v.norm() + t.copy(x2) + t.Add(p) + x2.cmove(t, (Comp(x2, x1)>>1)&1) + x2.Sub(x1) + x2.norm() + } + } + r.copy(x1) + r.cmove(x2, Comp(u, one)&1) +} + +/* return this^e mod m */ +func (r *BIG) Powmod(e1 *BIG, m *BIG, mem *arena.Arena) *BIG { + e := NewBIGcopy(e1, mem) + r.norm() + e.norm() + a := NewBIGint(1, mem) + z := NewBIGcopy(e, mem) + s := NewBIGcopy(r, mem) + for true { + bt := z.parity() + z.fshr(1) + if bt == 1 { + a = Modmul(a, s, m, mem) + } + if z.IsZero() { + break + } + s = Modsqr(s, m, mem) + } + return a +} + +/* Arazi and Qi inversion mod 256 */ +func invmod256(a int) int { + var t1 int = 0 + c := (a >> 1) & 1 + t1 += c + t1 &= 1 + t1 = 2 - t1 + t1 <<= 1 + U := t1 + 1 + + // i=2 + b := a & 3 + t1 = U * b + t1 >>= 2 + c = (a >> 2) & 3 + t2 := (U * c) & 3 + t1 += t2 + t1 *= U + t1 &= 3 + t1 = 4 - t1 + t1 <<= 2 + U += t1 + + // i=4 + b = a & 15 + t1 = U * b + t1 >>= 4 + c = (a >> 4) & 15 + t2 = (U * c) & 15 + t1 += t2 + t1 *= U + t1 &= 15 + t1 = 16 - t1 + t1 <<= 4 + U += t1 + + return U +} + +func logb2(w uint32) uint { + v := w + v |= (v >> 1) + v |= (v >> 2) + v |= (v >> 4) + v |= (v >> 8) + v |= (v >> 16) + + v = v - ((v >> 1) & 0x55555555) + v = (v & 0x33333333) + ((v >> 2) & 0x33333333) + r := uint((((v + (v >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24) + return (r) +} + +// Optimized combined shift, subtract and norm +func ssn(r *BIG, a *BIG, m *BIG) int { + n := NLEN - 1 + m.w[0] = (m.w[0] >> 1) | ((m.w[1] << (BASEBITS - 1)) & BMASK) + r.w[0] = a.w[0] - m.w[0] + carry := r.w[0] >> BASEBITS + r.w[0] &= BMASK + for i := 1; i < n; i++ { + m.w[i] = (m.w[i] >> 1) | ((m.w[i+1] << (BASEBITS - 1)) & BMASK) + r.w[i] = a.w[i] - m.w[i] + carry + carry = r.w[i] >> BASEBITS + r.w[i] &= BMASK + } + m.w[n] >>= 1 + r.w[n] = a.w[n] - m.w[n] + carry + return int((r.w[n] >> uint(CHUNK-1)) & 1) +} diff --git a/nekryptology/pkg/core/curves/native/bls48581/bls256.go b/nekryptology/pkg/core/curves/native/bls48581/bls256.go index c397a91..8d30ea1 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/bls256.go +++ b/nekryptology/pkg/core/curves/native/bls48581/bls256.go @@ -42,7 +42,7 @@ func ceil(a int, b int) int { /* output u \in F_p */ func Hash_to_field(hash int, hlen int, DST []byte, M []byte, ctr int) []*FP { - q := NewBIGints(Modulus) + q := NewBIGints(Modulus, nil) nbq := q.nbits() L := ceil(nbq+AESKEY*8, 8) var u []*FP @@ -53,7 +53,7 @@ func Hash_to_field(hash int, hlen int, DST []byte, M []byte, ctr int) []*FP { for j := 0; j < L; j++ { fd[j] = OKM[i*L+j] } - u = append(u, NewFPbig(DBIG_fromBytes(fd).ctmod(q, uint(8*L-nbq)))) + u = append(u, NewFPbig(DBIG_fromBytes(fd).ctmod(q, uint(8*L-nbq), nil), nil)) } return u } @@ -65,15 +65,15 @@ func Bls256_hash_to_point(M []byte) *ECP { P := ECP_map2point(u[0]) P1 := ECP_map2point(u[1]) - P.Add(P1) + P.Add(P1, nil) P.Cfp() - P.Affine() + P.Affine(nil) return P } func Init() int { G := ECP8_generator() - if G.Is_infinity() { + if G.Is_infinity(nil) { return BLS_FAIL } G2_TAB = precomp(G) @@ -82,7 +82,7 @@ func Init() int { /* generate key pair, private key S, public key W */ func KeyPairGenerate(IKM []byte, S []byte, W []byte) int { - r := NewBIGints(CURVE_Order) + r := NewBIGints(CURVE_Order, nil) nbr := r.nbits() L := ceil(3*ceil(nbr, 8), 2) LEN := ext.InttoBytes(L, 2) @@ -93,7 +93,7 @@ func KeyPairGenerate(IKM []byte, S []byte, W []byte) int { AIKM[len(IKM)] = 0 G := ECP8_generator() - if G.Is_infinity() { + if G.Is_infinity(nil) { return BLS_FAIL } SALT := []byte("BLS-SIG-KEYGEN-SALT-") @@ -101,10 +101,10 @@ func KeyPairGenerate(IKM []byte, S []byte, W []byte) int { OKM := ext.HKDF_Expand(ext.MC_SHA2, HASH_TYPE, L, PRK, LEN) dx := DBIG_fromBytes(OKM[:]) - s := dx.ctmod(r, uint(8*L-nbr)) + s := dx.ctmod(r, uint(8*L-nbr), nil) s.ToBytes(S) // SkToPk - G = G2mul(G, s) + G = G2mul(G, s, nil) G.ToBytes(W, true) return BLS_OK } @@ -113,7 +113,7 @@ func KeyPairGenerate(IKM []byte, S []byte, W []byte) int { func Core_Sign(SIG []byte, M []byte, S []byte) int { D := Bls256_hash_to_point(M) s := FromBytes(S) - D = G1mul(D, s) + D = G1mul(D, s, nil) D.ToBytes(SIG, true) return BLS_OK } @@ -124,21 +124,21 @@ func Core_Verify(SIG []byte, M []byte, W []byte) int { HM := Bls256_hash_to_point(M) D := ECP_fromBytes(SIG) - if !G1member(D) { + if !G1member(D, nil) { return BLS_FAIL } - D.Neg() + D.Neg(nil) PK := ECP8_fromBytes(W) - if !G2member(PK) { + if !G2member(PK, nil) { return BLS_FAIL } // Use new multi-pairing mechanism - r := Initmp() + r := Initmp(nil) Another_pc(r, G2_TAB, D) - Another(r, PK, HM) - v := Miller(r) + Another(r, PK, HM, nil) + v := Miller(r, nil) //.. or alternatively // G := ECP8_generator() diff --git a/nekryptology/pkg/core/curves/native/bls48581/config_big.go b/nekryptology/pkg/core/curves/native/bls48581/config_big_32.go similarity index 98% rename from nekryptology/pkg/core/curves/native/bls48581/config_big.go rename to nekryptology/pkg/core/curves/native/bls48581/config_big_32.go index 699a6e2..b814453 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/config_big.go +++ b/nekryptology/pkg/core/curves/native/bls48581/config_big_32.go @@ -1,3 +1,5 @@ +//go:build js && wasm + /* * Copyright (c) 2012-2020 MIRACL UK Ltd. * diff --git a/nekryptology/pkg/core/curves/native/bls48581/config_big_64.go b/nekryptology/pkg/core/curves/native/bls48581/config_big_64.go new file mode 100644 index 0000000..d31bd4d --- /dev/null +++ b/nekryptology/pkg/core/curves/native/bls48581/config_big_64.go @@ -0,0 +1,36 @@ +//go:build !js && !wasm + +/* + * Copyright (c) 2012-2020 MIRACL UK Ltd. + * + * This file is part of MIRACL Core + * (see https://github.com/miracl/core). + * + * 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 bls48581 + +// BIG length in bytes and number base +const MODBYTES uint = 73 +const BASEBITS uint = 60 + +// BIG lengths and Masks +const NLEN int = int((1 + ((8*MODBYTES - 1) / BASEBITS))) +const DNLEN int = 2 * NLEN +const BMASK Chunk = ((Chunk(1) << BASEBITS) - 1) +const HBITS uint = (BASEBITS / 2) +const HMASK Chunk = ((Chunk(1) << HBITS) - 1) +const NEXCESS int = (1 << (uint(CHUNK) - BASEBITS - 1)) + +const BIGBITS int = int(MODBYTES * 8) diff --git a/nekryptology/pkg/core/curves/native/bls48581/config_curve.go b/nekryptology/pkg/core/curves/native/bls48581/config_curve.go index dc23507..7a39de1 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/config_curve.go +++ b/nekryptology/pkg/core/curves/native/bls48581/config_curve.go @@ -19,11 +19,6 @@ package bls48581 -// Curve types -const WEIERSTRASS int = 0 -const EDWARDS int = 1 -const MONTGOMERY int = 2 - // Pairing Friendly? const NOT int = 0 const BN int = 1 @@ -31,10 +26,6 @@ const BLS12 int = 2 const BLS24 int = 3 const BLS48 int = 4 -// Pairing Twist type -const D_TYPE int = 0 -const M_TYPE int = 1 - // Sparsity const FP_ZERO int = 0 const FP_ONE int = 1 @@ -43,34 +34,16 @@ const FP_SPARSER int = 3 const FP_SPARSE int = 4 const FP_DENSE int = 5 -// Pairing x parameter sign -const POSITIVEX int = 0 -const NEGATIVEX int = 1 - -// Curve type - -const CURVETYPE int = WEIERSTRASS const CURVE_A int = 0 -const CURVE_PAIRING_TYPE int = BLS48 -// Pairings only - -const SEXTIC_TWIST int = D_TYPE -const SIGN_OF_X int = NEGATIVEX const ATE_BITS int = 33 const G2_TABLE int = 36 const HTC_ISO int = 0 const HTC_ISO_G2 int = 0 -// associated hash function and AES key size - const HASH_TYPE int = 64 const AESKEY int = 32 -const ALLOW_ALT_COMPRESS bool = false - -// These are manually decided policy decisions. To block any potential patent issues set to false. - const USE_GLV bool = true const USE_GS_G2 bool = true const USE_GS_GT bool = true diff --git a/nekryptology/pkg/core/curves/native/bls48581/config_field.go b/nekryptology/pkg/core/curves/native/bls48581/config_field_32.go similarity index 98% rename from nekryptology/pkg/core/curves/native/bls48581/config_field.go rename to nekryptology/pkg/core/curves/native/bls48581/config_field_32.go index 1d47ce8..81d262c 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/config_field.go +++ b/nekryptology/pkg/core/curves/native/bls48581/config_field_32.go @@ -1,3 +1,5 @@ +//go:build js && wasm + /* * Copyright (c) 2012-2020 MIRACL UK Ltd. * diff --git a/nekryptology/pkg/core/curves/native/bls48581/config_field_64.go b/nekryptology/pkg/core/curves/native/bls48581/config_field_64.go new file mode 100644 index 0000000..186ddd5 --- /dev/null +++ b/nekryptology/pkg/core/curves/native/bls48581/config_field_64.go @@ -0,0 +1,49 @@ +//go:build !js && !wasm + +/* + * Copyright (c) 2012-2020 MIRACL UK Ltd. + * + * This file is part of MIRACL Core + * (see https://github.com/miracl/core). + * + * 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 bls48581 + +// Modulus types +const NOT_SPECIAL int = 0 +const PSEUDO_MERSENNE int = 1 +const MONTGOMERY_FRIENDLY int = 2 +const GENERALISED_MERSENNE int = 3 + +const NEGATOWER int = 0 +const POSITOWER int = 1 + +// Modulus details +const MODBITS uint = 581 /* Number of bits in Modulus */ +const PM1D2 uint = 1 /* Modulus mod 8 */ +const RIADZ int = 2 /* hash-to-point Z */ +const RIADZG2A int = 2 /* G2 hash-to-point Z */ +const RIADZG2B int = 0 /* G2 hash-to-point Z */ +const MODTYPE int = NOT_SPECIAL //NOT_SPECIAL +const QNRI int = 0 // Fp2 QNR +const TOWER int = POSITOWER // Tower type +const FEXCESS int32 = ((int32(1) << 19) - 1) + +// Modulus Masks +const OMASK Chunk = ((Chunk(-1)) << (MODBITS % BASEBITS)) +const TBITS uint = MODBITS % BASEBITS // Number of active bits in top word +const TMASK Chunk = (Chunk(1) << TBITS) - 1 + +const BIG_ENDIAN_SIGN bool = false diff --git a/nekryptology/pkg/core/curves/native/bls48581/dbig.go b/nekryptology/pkg/core/curves/native/bls48581/dbig.go index 2e9a2ae..755cdd2 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/dbig.go +++ b/nekryptology/pkg/core/curves/native/bls48581/dbig.go @@ -21,28 +21,46 @@ package bls48581 -import "strconv" +import ( + "arena" + "strconv" +) //import "fmt" -func NewDBIG() *DBIG { - b := new(DBIG) +func NewDBIG(mem *arena.Arena) *DBIG { + var b *DBIG + if mem != nil { + b = arena.New[DBIG](mem) + } else { + b = new(DBIG) + } for i := 0; i < DNLEN; i++ { b.w[i] = 0 } return b } -func NewDBIGcopy(x *DBIG) *DBIG { - b := new(DBIG) +func NewDBIGcopy(x *DBIG, mem *arena.Arena) *DBIG { + var b *DBIG + if mem != nil { + b = arena.New[DBIG](mem) + } else { + b = new(DBIG) + } for i := 0; i < DNLEN; i++ { b.w[i] = x.w[i] } return b } -func NewDBIGscopy(x *BIG) *DBIG { - b := new(DBIG) +func NewDBIGscopy(x *BIG, mem *arena.Arena) *DBIG { + var b *DBIG + if mem != nil { + b = arena.New[DBIG](mem) + } else { + b = new(DBIG) + } for i := 0; i < NLEN-1; i++ { b.w[i] = x.w[i] } @@ -67,8 +85,8 @@ func (r *DBIG) norm() { } /* split DBIG at position n, return higher half, keep lower half */ -func (r *DBIG) split(n uint) *BIG { - t := NewBIG() +func (r *DBIG) split(n uint, mem *arena.Arena) *BIG { + t := NewBIG(mem) m := n % BASEBITS carry := r.w[DNLEN-1] << (BASEBITS - m) @@ -173,11 +191,11 @@ func (r *DBIG) shr(k uint) { } } -func (r *DBIG) ctmod(m *BIG, bd uint) *BIG { +func (r *DBIG) ctmod(m *BIG, bd uint, mem *arena.Arena) *BIG { k := bd r.norm() - c := NewDBIGscopy(m) - dr := NewDBIG() + c := NewDBIGscopy(m, mem) + dr := NewDBIG(mem) c.shl(k) @@ -192,25 +210,25 @@ func (r *DBIG) ctmod(m *BIG, bd uint) *BIG { k -= 1 c.shr(1) } - return NewBIGdcopy(r) + return NewBIGdcopy(r, mem) } /* reduces this DBIG mod a BIG, and returns the BIG */ -func (r *DBIG) Mod(m *BIG) *BIG { +func (r *DBIG) Mod(m *BIG, mem *arena.Arena) *BIG { k := r.nbits() - m.nbits() if k < 0 { k = 0 } - return r.ctmod(m, uint(k)) + return r.ctmod(m, uint(k), mem) } -func (r *DBIG) ctdiv(m *BIG, bd uint) *BIG { +func (r *DBIG) ctdiv(m *BIG, bd uint, mem *arena.Arena) *BIG { k := bd - c := NewDBIGscopy(m) - a := NewBIGint(0) - e := NewBIGint(1) - sr := NewBIG() - dr := NewDBIG() + c := NewDBIGscopy(m, mem) + a := NewBIGint(0, mem) + e := NewBIGint(1, mem) + sr := NewBIG(mem) + dr := NewDBIG(mem) r.norm() c.shl(k) @@ -237,12 +255,12 @@ func (r *DBIG) ctdiv(m *BIG, bd uint) *BIG { } /* return this/c */ -func (r *DBIG) div(m *BIG) *BIG { +func (r *DBIG) div(m *BIG, mem *arena.Arena) *BIG { k := r.nbits() - m.nbits() if k < 0 { k = 0 } - return r.ctdiv(m, uint(k)) + return r.ctdiv(m, uint(k), mem) } /* Convert to Hex String */ @@ -259,7 +277,7 @@ func (r *DBIG) toString() string { } for i := len - 1; i >= 0; i-- { - b := NewDBIGcopy(r) + b := NewDBIGcopy(r, nil) b.shr(uint(i * 4)) s += strconv.FormatInt(int64(b.w[0]&15), 16) @@ -270,7 +288,7 @@ func (r *DBIG) toString() string { /* return number of bits */ func (r *DBIG) nbits() int { k := DNLEN - 1 - t := NewDBIGcopy(r) + t := NewDBIGcopy(r, nil) t.norm() for k >= 0 && t.w[k] == 0 { k-- @@ -289,7 +307,7 @@ func (r *DBIG) nbits() int { /* convert from byte array to BIG */ func DBIG_fromBytes(b []byte) *DBIG { - m := NewDBIG() + m := NewDBIG(nil) for i := 0; i < len(b); i++ { m.shl(8) m.w[0] += Chunk(int(b[i] & 0xff)) diff --git a/nekryptology/pkg/core/curves/native/bls48581/ecdh.go b/nekryptology/pkg/core/curves/native/bls48581/ecdh.go deleted file mode 100644 index 0480043..0000000 --- a/nekryptology/pkg/core/curves/native/bls48581/ecdh.go +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Copyright (c) 2012-2020 MIRACL UK Ltd. - * - * This file is part of MIRACL Core - * (see https://github.com/miracl/ext.. - * - * 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. - */ - -/* ECDH/ECIES/ECDSA API Functions */ - -package bls48581 - -//import "fmt" -import "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" - -const INVALID_PUBLIC_KEY int = -2 -const ERROR int = -3 - -//const INVALID int = -4 -const EFS int = int(MODBYTES) -const EGS int = int(MODBYTES) - -// Transform a point multiplier to RFC7748 form -func RFC7748(r *BIG) { - lg := 0 - t := NewBIGint(1) - c := CURVE_Cof_I - for c != 1 { - lg++ - c /= 2 - } - n := uint(8*EGS - lg + 1) - r.mod2m(n) - t.shl(n) - r.Add(t) - c = r.lastbits(lg) - r.dec(c) -} - -/* return true if S is in ranger 0 < S < order , else return false */ -func ECDH_IN_RANGE(S []byte) bool { - r := NewBIGints(CURVE_Order) - s := FromBytes(S) - if s.IsZero() { - return false - } - if Comp(s, r) >= 0 { - return false - } - return true -} - -/* Calculate a public/private EC GF(p) key pair W,S where W=S.G mod EC(p), - * where S is the secret key and W is the public key - * and G is fixed generator. - * If RNG is NULL then the private key is provided externally in S - * otherwise it is generated randomly internally */ -func ECDH_KEY_PAIR_GENERATE(RNG *ext.RAND, S []byte, W []byte) int { - res := 0 - var s *BIG - var G *ECP - - G = ECP_generator() - r := NewBIGints(CURVE_Order) - - if RNG == nil { - s = FromBytes(S) - } else { - if CURVETYPE != WEIERSTRASS { - s = Random(RNG) // from random bytes - } else { - s = Randomnum(r, RNG) // Removes biases - } - } - - if CURVETYPE != WEIERSTRASS { - RFC7748(s) // For Montgomery or Edwards, apply RFC7748 transformation - } - - s.ToBytes(S) - WP := G.clmul(s, r) - WP.ToBytes(W, false) // To use point compression on public keys, change to true - - return res -} - -/* validate public key */ -func ECDH_PUBLIC_KEY_VALIDATE(W []byte) int { - WP := ECP_fromBytes(W) - res := 0 - - r := NewBIGints(CURVE_Order) - - if WP.Is_infinity() { - res = INVALID_PUBLIC_KEY - } - if res == 0 { - - q := NewBIGints(Modulus) - nb := q.nbits() - k := NewBIGint(1) - k.shl(uint((nb + 4) / 2)) - k.Add(q) - k.div(r) - - for k.parity() == 0 { - k.shr(1) - WP.Dbl() - } - - if !k.isunity() { - WP = WP.lmul(k) - } - if WP.Is_infinity() { - res = INVALID_PUBLIC_KEY - } - - } - return res -} - -/* IEEE-1363 Diffie-Hellman online calculation Z=S.WD */ -// type = 0 is just x coordinate output -// type = 1 for standard compressed output -// type = 2 for standard uncompress output 04|x|y -func ECDH_ECPSVDP_DH(S []byte, WD []byte, Z []byte, typ int) int { - res := 0 - - s := FromBytes(S) - - W := ECP_fromBytes(WD) - if W.Is_infinity() { - res = ERROR - } - - if res == 0 { - r := NewBIGints(CURVE_Order) - W = W.clmul(s, r) - if W.Is_infinity() { - res = ERROR - } else { - if CURVETYPE != MONTGOMERY { - if typ > 0 { - if typ == 1 { - W.ToBytes(Z, true) - } else { - W.ToBytes(Z, false) - } - } else { - W.GetX().ToBytes(Z) - } - return res - } else { - W.GetX().ToBytes(Z) - } - } - } - return res -} - -/* IEEE ECDSA Signature, C and D are signature on F using private key S */ -func ECDH_ECPSP_DSA(sha int, RNG *ext.RAND, S []byte, F []byte, C []byte, D []byte) int { - var T [EGS]byte - - B := ext.GPhashit(ext.MC_SHA2, sha, EGS, 0, F, -1, nil) - G := ECP_generator() - - r := NewBIGints(CURVE_Order) - s := FromBytes(S) - f := FromBytes(B[:]) - - c := NewBIGint(0) - d := NewBIGint(0) - V := NewECP() - - for d.IsZero() { - u := Randomnum(r, RNG) - w := Randomnum(r, RNG) /* IMPORTANT - side channel masking to protect invmodp() */ - - V.Copy(G) - V = V.clmul(u, r) - vx := V.GetX() - c.copy(vx) - c.Mod(r) - if c.IsZero() { - continue - } - u.copy(Modmul(u, w, r)) - u.Invmodp(r) - d.copy(Modmul(s, c, r)) - d.copy(ModAdd(d, f, r)) - d.copy(Modmul(d, w, r)) - d.copy(Modmul(u, d, r)) - } - - c.ToBytes(T[:]) - for i := 0; i < EGS; i++ { - C[i] = T[i] - } - d.ToBytes(T[:]) - for i := 0; i < EGS; i++ { - D[i] = T[i] - } - return 0 -} - -/* IEEE1363 ECDSA Signature Verification. Signature C and D on F is verified using public key W */ -func ECDH_ECPVP_DSA(sha int, W []byte, F []byte, C []byte, D []byte) int { - res := 0 - - B := ext.GPhashit(ext.MC_SHA2, sha, EGS, 0, F, -1, nil) - - G := ECP_generator() - r := NewBIGints(CURVE_Order) - - c := FromBytes(C) - d := FromBytes(D) - f := FromBytes(B[:]) - - if c.IsZero() || Comp(c, r) >= 0 || d.IsZero() || Comp(d, r) >= 0 { - res = ERROR - } - - if res == 0 { - d.Invmodp(r) - f.copy(Modmul(f, d, r)) - h2 := Modmul(c, d, r) - - WP := ECP_fromBytes(W) - if WP.Is_infinity() { - res = ERROR - } else { - P := NewECP() - P.Copy(WP) - - P = P.Mul2(h2, G, f) - - if P.Is_infinity() { - res = ERROR - } else { - d = P.GetX() - d.Mod(r) - - if Comp(d, c) != 0 { - res = ERROR - } - } - } - } - - return res -} - -/* IEEE1363 ECIES encryption. Encryption of plaintext M uses public key W and produces ciphertext V,C,T */ -func ECDH_ECIES_ENCRYPT(sha int, P1 []byte, P2 []byte, RNG *ext.RAND, W []byte, M []byte, V []byte, T []byte) []byte { - var Z [EFS]byte - var VZ [3*EFS + 1]byte - var K1 [AESKEY]byte - var K2 [AESKEY]byte - var U [EGS]byte - - if ECDH_KEY_PAIR_GENERATE(RNG, U[:], V) != 0 { - return nil - } - if ECDH_ECPSVDP_DH(U[:], W, Z[:], 0) != 0 { - return nil - } - - for i := 0; i < 2*EFS+1; i++ { - VZ[i] = V[i] - } - for i := 0; i < EFS; i++ { - VZ[2*EFS+1+i] = Z[i] - } - - K := ext.KDF2(ext.MC_SHA2, sha, VZ[:], P1, 2*AESKEY) - - for i := 0; i < AESKEY; i++ { - K1[i] = K[i] - K2[i] = K[AESKEY+i] - } - - C := ext.AES_CBC_IV0_ENCRYPT(K1[:], M) - - L2 := ext.InttoBytes(len(P2), 8) - - var AC []byte - - for i := 0; i < len(C); i++ { - AC = append(AC, C[i]) - } - for i := 0; i < len(P2); i++ { - AC = append(AC, P2[i]) - } - for i := 0; i < 8; i++ { - AC = append(AC, L2[i]) - } - - ext.HMAC(ext.MC_SHA2, sha, T, len(T), K2[:], AC) - - return C -} - -/* constant time n-byte compare */ -func ncomp(T1 []byte, T2 []byte, n int) bool { - res := 0 - for i := 0; i < n; i++ { - res |= int(T1[i] ^ T2[i]) - } - if res == 0 { - return true - } - return false -} - -/* IEEE1363 ECIES decryption. Decryption of ciphertext V,C,T using private key U outputs plaintext M */ -func ECDH_ECIES_DECRYPT(sha int, P1 []byte, P2 []byte, V []byte, C []byte, T []byte, U []byte) []byte { - var Z [EFS]byte - var VZ [3*EFS + 1]byte - var K1 [AESKEY]byte - var K2 [AESKEY]byte - - var TAG []byte = T[:] - - if ECDH_ECPSVDP_DH(U, V, Z[:], 0) != 0 { - return nil - } - - for i := 0; i < 2*EFS+1; i++ { - VZ[i] = V[i] - } - for i := 0; i < EFS; i++ { - VZ[2*EFS+1+i] = Z[i] - } - - K := ext.KDF2(ext.MC_SHA2, sha, VZ[:], P1, 2*AESKEY) - - for i := 0; i < AESKEY; i++ { - K1[i] = K[i] - K2[i] = K[AESKEY+i] - } - - M := ext.AES_CBC_IV0_DECRYPT(K1[:], C) - - if M == nil { - return nil - } - - L2 := ext.InttoBytes(len(P2), 8) - - var AC []byte - - for i := 0; i < len(C); i++ { - AC = append(AC, C[i]) - } - for i := 0; i < len(P2); i++ { - AC = append(AC, P2[i]) - } - for i := 0; i < 8; i++ { - AC = append(AC, L2[i]) - } - - ext.HMAC(ext.MC_SHA2, sha, TAG, len(TAG), K2[:], AC) - - if !ncomp(T, TAG, len(T)) { - return nil - } - - return M -} diff --git a/nekryptology/pkg/core/curves/native/bls48581/fp.go b/nekryptology/pkg/core/curves/native/bls48581/fp.go index c42feca..2cab741 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/fp.go +++ b/nekryptology/pkg/core/curves/native/bls48581/fp.go @@ -22,7 +22,11 @@ package bls48581 -import "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" +import ( + "arena" + + "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" +) type FP struct { x *BIG @@ -31,84 +35,119 @@ type FP struct { /* Constructors */ -func NewFP() *FP { - F := new(FP) - F.x = NewBIG() - F.XES = 1 - return F -} - -func NewFPint(a int) *FP { - F := new(FP) - if a < 0 { - m := NewBIGints(Modulus) - m.inc(a) - m.norm() - F.x = NewBIGcopy(m) +func NewFP(mem *arena.Arena) *FP { + if mem != nil { + F := arena.New[FP](mem) + F.x = NewBIG(mem) + F.XES = 1 + return F } else { - F.x = NewBIGint(a) + F := new(FP) + F.x = NewBIG(nil) + F.XES = 1 + return F } - F.nres() - return F } -func NewFPbig(a *BIG) *FP { - F := new(FP) - F.x = NewBIGcopy(a) - F.nres() - return F +func NewFPint(a int, mem *arena.Arena) *FP { + if mem != nil { + F := arena.New[FP](mem) + if a < 0 { + m := NewBIGints(Modulus, mem) + m.inc(a) + m.norm() + F.x = NewBIGcopy(m, mem) + } else { + F.x = NewBIGint(a, mem) + } + F.nres(mem) + return F + } else { + F := new(FP) + if a < 0 { + m := NewBIGints(Modulus, nil) + m.inc(a) + m.norm() + F.x = NewBIGcopy(m, nil) + } else { + F.x = NewBIGint(a, nil) + } + F.nres(nil) + return F + } } -func NewFPcopy(a *FP) *FP { - F := new(FP) - F.x = NewBIGcopy(a.x) - F.XES = a.XES - return F +func NewFPbig(a *BIG, mem *arena.Arena) *FP { + if mem != nil { + F := arena.New[FP](mem) + F.x = NewBIGcopy(a, mem) + F.nres(mem) + return F + } else { + F := new(FP) + F.x = NewBIGcopy(a, nil) + F.nres(nil) + return F + } +} + +func NewFPcopy(a *FP, mem *arena.Arena) *FP { + if mem != nil { + F := arena.New[FP](mem) + F.x = NewBIGcopy(a.x, mem) + F.XES = a.XES + return F + } else { + F := new(FP) + F.x = NewBIGcopy(a.x, nil) + F.XES = a.XES + return F + } } func NewFPrand(rng *ext.RAND) *FP { - m := NewBIGints(Modulus) + m := NewBIGints(Modulus, nil) w := Randomnum(m, rng) - F := NewFPbig(w) + F := NewFPbig(w, nil) return F } func (F *FP) ToString() string { - F.reduce() - return F.Redc().ToString() + F.reduce(nil) + return F.Redc(nil).ToString() } /* convert to Montgomery n-residue form */ -func (F *FP) nres() { +func (F *FP) nres(mem *arena.Arena) { if MODTYPE != PSEUDO_MERSENNE && MODTYPE != GENERALISED_MERSENNE { - r := NewBIGints(R2modp) - d := mul(F.x, r) - F.x.copy(mod(d)) + r := NewBIGints(R2modp, mem) + d := mul(F.x, r, mem) + F.x.copy(mod(d, mem)) F.XES = 2 } else { - md := NewBIGints(Modulus) - F.x.Mod(md) + md := NewBIGints(Modulus, mem) + F.x.Mod(md, mem) F.XES = 1 } } /* convert back to regular form */ -func (F *FP) Redc() *BIG { +func (F *FP) Redc(mem *arena.Arena) *BIG { if MODTYPE != PSEUDO_MERSENNE && MODTYPE != GENERALISED_MERSENNE { - d := NewDBIGscopy(F.x) - return mod(d) + d := NewDBIGscopy(F.x, mem) + return mod(d, mem) } else { - r := NewBIGcopy(F.x) + r := NewBIGcopy(F.x, mem) return r } } /* reduce a DBIG to a BIG using the appropriate form of the modulus */ -func mod(d *DBIG) *BIG { +func mod(d *DBIG, mem *arena.Arena) *BIG { if MODTYPE == PSEUDO_MERSENNE { - t := d.split(MODBITS) - b := NewBIGdcopy(d) + t := d.split(MODBITS, mem) + b := NewBIGdcopy(d, mem) v := t.pmul(int(MConst)) @@ -128,7 +167,7 @@ func mod(d *DBIG) *BIG { d.w[NLEN+i-1] = bot d.w[NLEN+i] += top } - b := NewBIG() + b := NewBIG(mem) for i := 0; i < NLEN; i++ { b.w[i] = d.w[NLEN+i] @@ -138,14 +177,14 @@ func mod(d *DBIG) *BIG { } if MODTYPE == GENERALISED_MERSENNE { // GoldiLocks only - t := d.split(MODBITS) - b := NewBIGdcopy(d) + t := d.split(MODBITS, mem) + b := NewBIGdcopy(d, mem) b.Add(t) - dd := NewDBIGscopy(t) + dd := NewDBIGscopy(t, mem) dd.shl(MODBITS / 2) - tt := dd.split(MODBITS) - lo := NewBIGdcopy(dd) + tt := dd.split(MODBITS, mem) + lo := NewBIGdcopy(dd, mem) b.Add(tt) b.Add(lo) b.norm() @@ -163,10 +202,10 @@ func mod(d *DBIG) *BIG { } if MODTYPE == NOT_SPECIAL { - md := NewBIGints(Modulus) - return monty(md, MConst, d) + md := NewBIGints(Modulus, mem) + return monty(md, MConst, d, mem) } - return NewBIG() + return NewBIG(mem) } // find appoximation to quotient of a/m @@ -189,9 +228,9 @@ func quo(n *BIG, m *BIG) int { } /* reduce this mod Modulus */ -func (F *FP) reduce() { - m := NewBIGints(Modulus) - r := NewBIGints(Modulus) +func (F *FP) reduce(mem *arena.Arena) { + m := NewBIGints(Modulus, mem) + r := NewBIGints(Modulus, mem) var sb uint F.x.norm() @@ -217,43 +256,49 @@ func (F *FP) reduce() { } /* test this=0? */ -func (F *FP) IsZero() bool { - W := NewFPcopy(F) - W.reduce() +func (F *FP) IsZero(mem *arena.Arena) bool { + W := NewFPcopy(F, mem) + W.reduce(mem) return W.x.IsZero() } func (F *FP) IsOne() bool { - W := NewFPcopy(F) - W.reduce() - T := NewFPint(1) + mem := arena.NewArena() + defer mem.Free() + W := NewFPcopy(F, mem) + W.reduce(mem) + T := NewFPint(1, mem) return W.Equals(T) } func (F *FP) islarger() int { - if F.IsZero() { + mem := arena.NewArena() + defer mem.Free() + if F.IsZero(mem) { return 0 } - sx := NewBIGints(Modulus) - fx := F.Redc() + sx := NewBIGints(Modulus, mem) + fx := F.Redc(mem) sx.Sub(fx) sx.norm() return Comp(fx, sx) } func (F *FP) ToBytes(b []byte) { - F.Redc().ToBytes(b) + F.Redc(nil).ToBytes(b) } func FP_fromBytes(b []byte) *FP { t := FromBytes(b) - return NewFPbig(t) + return NewFPbig(t, nil) } func (F *FP) isunity() bool { - W := NewFPcopy(F) - W.reduce() - return W.Redc().isunity() + mem := arena.NewArena() + defer mem.Free() + W := NewFPcopy(F, mem) + W.reduce(mem) + return W.Redc(mem).isunity() } /* copy from FP b */ @@ -270,25 +315,27 @@ func (F *FP) zero() { /* set this=1 */ func (F *FP) one() { + mem := arena.NewArena() + defer mem.Free() F.x.one() - F.nres() + F.nres(mem) } /* return sign */ -func (F *FP) sign() int { +func (F *FP) sign(mem *arena.Arena) int { if BIG_ENDIAN_SIGN { - m := NewBIGints(Modulus) + m := NewBIGints(Modulus, mem) m.dec(1) m.fshr(1) - n := NewFPcopy(F) - n.reduce() - w := n.Redc() + n := NewFPcopy(F, mem) + n.reduce(mem) + w := n.Redc(mem) cp := Comp(w, m) return ((cp + 1) & 2) >> 1 } else { - W := NewFPcopy(F) - W.reduce() - return W.Redc().parity() + W := NewFPcopy(F, mem) + W.reduce(mem) + return W.Redc(mem).parity() } } @@ -315,20 +362,20 @@ func (F *FP) cmove(b *FP, d int) { } /* this*=b mod Modulus */ -func (F *FP) Mul(b *FP) { +func (F *FP) Mul(b *FP, mem *arena.Arena) { if int64(F.XES)*int64(b.XES) > int64(FEXCESS) { - F.reduce() + F.reduce(mem) } - d := mul(F.x, b.x) - F.x.copy(mod(d)) + d := mul(F.x, b.x, mem) + F.x.copy(mod(d, mem)) F.XES = 2 } /* this = -this mod Modulus */ -func (F *FP) Neg() { - m := NewBIGints(Modulus) +func (F *FP) Neg(mem *arena.Arena) { + m := NewBIGints(Modulus, mem) sb := logb2(uint32(F.XES - 1)) m.fshl(sb) @@ -336,12 +383,12 @@ func (F *FP) Neg() { F.XES = (1 << sb) + 1 if F.XES > FEXCESS { - F.reduce() + F.reduce(mem) } } /* this*=c mod Modulus, where c is a small int */ -func (F *FP) imul(c int) { +func (F *FP) imul(c int, mem *arena.Arena) { // F.norm() s := false if c < 0 { @@ -350,60 +397,60 @@ func (F *FP) imul(c int) { } if MODTYPE == PSEUDO_MERSENNE || MODTYPE == GENERALISED_MERSENNE { - d := F.x.pxmul(c) - F.x.copy(mod(d)) + d := F.x.pxmul(c, mem) + F.x.copy(mod(d, mem)) F.XES = 2 } else { if F.XES*int32(c) <= FEXCESS { F.x.pmul(c) F.XES *= int32(c) } else { - n := NewFPint(c) - F.Mul(n) + n := NewFPint(c, mem) + F.Mul(n, mem) } } if s { - F.Neg() + F.Neg(mem) F.norm() } } /* this*=this mod Modulus */ -func (F *FP) Sqr() { +func (F *FP) Sqr(mem *arena.Arena) { if int64(F.XES)*int64(F.XES) > int64(FEXCESS) { - F.reduce() + F.reduce(mem) } - d := sqr(F.x) - F.x.copy(mod(d)) + d := sqr(F.x, mem) + F.x.copy(mod(d, mem)) F.XES = 2 } /* this+=b */ -func (F *FP) Add(b *FP) { +func (F *FP) Add(b *FP, mem *arena.Arena) { F.x.Add(b.x) F.XES += b.XES if F.XES > FEXCESS { - F.reduce() + F.reduce(mem) } } /* this-=b */ -func (F *FP) Sub(b *FP) { - n := NewFPcopy(b) - n.Neg() - F.Add(n) +func (F *FP) Sub(b *FP, mem *arena.Arena) { + n := NewFPcopy(b, mem) + n.Neg(mem) + F.Add(n, mem) } -func (F *FP) rsub(b *FP) { - F.Neg() - F.Add(b) +func (F *FP) rsub(b *FP, mem *arena.Arena) { + F.Neg(mem) + F.Add(b, mem) } /* this/=2 mod Modulus */ -func (F *FP) div2() { - p := NewBIGints(Modulus) +func (F *FP) div2(mem *arena.Arena) { + p := NewBIGints(Modulus, mem) pr := F.x.parity() - w := NewBIGcopy(F.x) + w := NewBIGcopy(F.x, mem) F.x.fshr(1) w.Add(p) w.norm() @@ -413,18 +460,22 @@ func (F *FP) div2() { /* return jacobi symbol (this/Modulus) */ func (F *FP) jacobi() int { - w := F.Redc() - p := NewBIGints(Modulus) + mem := arena.NewArena() + defer mem.Free() + w := F.Redc(mem) + p := NewBIGints(Modulus, mem) return w.Jacobi(p) } /* return TRUE if this==a */ func (F *FP) Equals(a *FP) bool { - f := NewFPcopy(F) - s := NewFPcopy(a) + mem := arena.NewArena() + defer mem.Free() + f := NewFPcopy(F, mem) + s := NewFPcopy(a, mem) - s.reduce() - f.reduce() + s.reduce(mem) + f.reduce(mem) if Comp(s.x, f.x) == 0 { return true } @@ -432,20 +483,22 @@ func (F *FP) Equals(a *FP) bool { } func (F *FP) Comp(a *FP) int { - f := NewFPcopy(F) - s := NewFPcopy(a) + mem := arena.NewArena() + defer mem.Free() + f := NewFPcopy(F, mem) + s := NewFPcopy(a, mem) - s.reduce() - f.reduce() + s.reduce(mem) + f.reduce(mem) return Comp(s.x, f.x) } -func (F *FP) pow(e *BIG) *FP { +func (F *FP) pow(e *BIG, mem *arena.Arena) *FP { var tb []*FP var w [1 + (NLEN*int(BASEBITS)+3)/4]int8 F.norm() - t := NewBIGcopy(e) + t := NewBIGcopy(e, mem) t.norm() nb := 1 + (t.nbits()+3)/4 @@ -456,51 +509,51 @@ func (F *FP) pow(e *BIG) *FP { w[i] = int8(lsbs) t.fshr(4) } - tb = append(tb, NewFPint(1)) - tb = append(tb, NewFPcopy(F)) + tb = append(tb, NewFPint(1, mem)) + tb = append(tb, NewFPcopy(F, mem)) for i := 2; i < 16; i++ { - tb = append(tb, NewFPcopy(tb[i-1])) - tb[i].Mul(F) + tb = append(tb, NewFPcopy(tb[i-1], mem)) + tb[i].Mul(F, mem) } - r := NewFPcopy(tb[w[nb-1]]) + r := NewFPcopy(tb[w[nb-1]], mem) for i := nb - 2; i >= 0; i-- { - r.Sqr() - r.Sqr() - r.Sqr() - r.Sqr() - r.Mul(tb[w[i]]) + r.Sqr(mem) + r.Sqr(mem) + r.Sqr(mem) + r.Sqr(mem) + r.Mul(tb[w[i]], mem) } - r.reduce() + r.reduce(mem) return r } // See https://eprint.iacr.org/2018/1038 // return this^(p-3)/4 or this^(p-5)/8 -func (F *FP) fpow() *FP { +func (F *FP) fpow(mem *arena.Arena) *FP { ac := [11]int{1, 2, 3, 6, 12, 15, 30, 60, 120, 240, 255} - var xp []*FP + xp := arena.MakeSlice[*FP](mem, 11, 11) // phase 1 - xp = append(xp, NewFPcopy(F)) - xp = append(xp, NewFPcopy(F)) - xp[1].Sqr() - xp = append(xp, NewFPcopy(xp[1])) - xp[2].Mul(F) - xp = append(xp, NewFPcopy(xp[2])) - xp[3].Sqr() - xp = append(xp, NewFPcopy(xp[3])) - xp[4].Sqr() - xp = append(xp, NewFPcopy(xp[4])) - xp[5].Mul(xp[2]) - xp = append(xp, NewFPcopy(xp[5])) - xp[6].Sqr() - xp = append(xp, NewFPcopy(xp[6])) - xp[7].Sqr() - xp = append(xp, NewFPcopy(xp[7])) - xp[8].Sqr() - xp = append(xp, NewFPcopy(xp[8])) - xp[9].Sqr() - xp = append(xp, NewFPcopy(xp[9])) - xp[10].Mul(xp[5]) + xp[0] = NewFPcopy(F, mem) + xp[1] = NewFPcopy(F, mem) + xp[1].Sqr(mem) + xp[2] = NewFPcopy(xp[1], mem) + xp[2].Mul(F, mem) + xp[3] = NewFPcopy(xp[2], mem) + xp[3].Sqr(mem) + xp[4] = NewFPcopy(xp[3], mem) + xp[4].Sqr(mem) + xp[5] = NewFPcopy(xp[4], mem) + xp[5].Mul(xp[2], mem) + xp[6] = NewFPcopy(xp[5], mem) + xp[6].Sqr(mem) + xp[7] = NewFPcopy(xp[6], mem) + xp[7].Sqr(mem) + xp[8] = NewFPcopy(xp[7], mem) + xp[8].Sqr(mem) + xp[9] = NewFPcopy(xp[8], mem) + xp[9].Sqr(mem) + xp[10] = NewFPcopy(xp[9], mem) + xp[10].Mul(xp[5], mem) var n, c int e := int(PM1D2) @@ -529,7 +582,7 @@ func (F *FP) fpow() *FP { k := w - c i := 10 - key := NewFP() + key := NewFP(mem) if k != 0 { for ac[i] > k { @@ -544,7 +597,7 @@ func (F *FP) fpow() *FP { if ac[i] > k { continue } - key.Mul(xp[i]) + key.Mul(xp[i], mem) k -= ac[i] } // phase 2 @@ -555,19 +608,19 @@ func (F *FP) fpow() *FP { j := 3 m := 8 nw := n - bw - t := NewFP() + t := NewFP(mem) for 2*m < nw { t.copy(xp[j]) j++ for i = 0; i < m; i++ { - t.Sqr() + t.Sqr(mem) } xp[j].copy(xp[j-1]) - xp[j].Mul(t) + xp[j].Mul(t, mem) m *= 2 } lo := nw - m - r := NewFPcopy(xp[j]) + r := NewFPcopy(xp[j], mem) for lo != 0 { m /= 2 @@ -578,84 +631,86 @@ func (F *FP) fpow() *FP { lo -= m t.copy(r) for i = 0; i < m; i++ { - t.Sqr() + t.Sqr(mem) } r.copy(t) - r.Mul(xp[j]) + r.Mul(xp[j], mem) } // phase 3 if bw != 0 { for i = 0; i < bw; i++ { - r.Sqr() + r.Sqr(mem) } - r.Mul(key) + r.Mul(key, mem) } if MODTYPE == GENERALISED_MERSENNE { // Goldilocks ONLY key.copy(r) - r.Sqr() - r.Mul(F) + r.Sqr(mem) + r.Mul(F, mem) for i = 0; i < n+1; i++ { - r.Sqr() + r.Sqr(mem) } - r.Mul(key) + r.Mul(key, mem) } for nd > 0 { - r.Sqr() + r.Sqr(mem) nd-- } return r } // calculates r=x^(p-1-2^e)/2^{e+1) where 2^e|p-1 -func (F *FP) progen() { +func (F *FP) progen(mem *arena.Arena) { if MODTYPE == PSEUDO_MERSENNE || MODTYPE == GENERALISED_MERSENNE { - F.copy(F.fpow()) + F.copy(F.fpow(mem)) return } e := uint(PM1D2) - m := NewBIGints(Modulus) + m := NewBIGints(Modulus, mem) m.dec(1) m.shr(e) m.dec(1) m.fshr(1) - F.copy(F.pow(m)) + F.copy(F.pow(m, mem)) } /* this=1/this mod Modulus */ -func (F *FP) Invert(h *FP) { +func (F *FP) Invert(h *FP, mem *arena.Arena) { e := int(PM1D2) F.norm() - s := NewFPcopy(F) + s := NewFPcopy(F, mem) for i := 0; i < e-1; i++ { - s.Sqr() - s.Mul(F) + s.Sqr(mem) + s.Mul(F, mem) } if h == nil { - F.progen() + F.progen(mem) } else { F.copy(h) } for i := 0; i <= e; i++ { - F.Sqr() + F.Sqr(mem) } - F.Mul(s) - F.reduce() + F.Mul(s, mem) + F.reduce(mem) } /* test for Quadratic residue */ func (F *FP) qr(h *FP) int { - r := NewFPcopy(F) + mem := arena.NewArena() + defer mem.Free() + r := NewFPcopy(F, mem) e := int(PM1D2) - r.progen() + r.progen(mem) if h != nil { h.copy(r) } - r.Sqr() - r.Mul(F) + r.Sqr(mem) + r.Mul(F, mem) for i := 0; i < e-1; i++ { - r.Sqr() + r.Sqr(mem) } if r.isunity() { @@ -666,29 +721,29 @@ func (F *FP) qr(h *FP) int { } /* return sqrt(this) mod Modulus */ -func (F *FP) Sqrt(h *FP) *FP { +func (F *FP) Sqrt(h *FP, mem *arena.Arena) *FP { e := int(PM1D2) - g := NewFPcopy(F) + g := NewFPcopy(F, mem) if h == nil { - g.progen() + g.progen(mem) } else { g.copy(h) } - m := NewBIGints(ROI) - v := NewFPbig(m) + m := NewBIGints(ROI, mem) + v := NewFPbig(m, mem) - t := NewFPcopy(g) - t.Sqr() - t.Mul(F) + t := NewFPcopy(g, mem) + t.Sqr(mem) + t.Mul(F, mem) - r := NewFPcopy(F) - r.Mul(g) - b := NewFPcopy(t) + r := NewFPcopy(F, mem) + r.Mul(g, mem) + b := NewFPcopy(t, mem) for k := e; k > 1; k-- { for j := 1; j < k-1; j++ { - b.Sqr() + b.Sqr(mem) } var u int if b.isunity() { @@ -697,41 +752,43 @@ func (F *FP) Sqrt(h *FP) *FP { u = 1 } g.copy(r) - g.Mul(v) + g.Mul(v, mem) r.cmove(g, u) - v.Sqr() + v.Sqr(mem) g.copy(t) - g.Mul(v) + g.Mul(v, mem) t.cmove(g, u) b.copy(t) } - sgn := r.sign() - nr := NewFPcopy(r) - nr.Neg() + sgn := r.sign(mem) + nr := NewFPcopy(r, mem) + nr.Neg(mem) nr.norm() r.cmove(nr, sgn) return r } func (F *FP) invsqrt(i *FP, s *FP) int { - h := NewFP() + mem := arena.NewArena() + defer mem.Free() + h := NewFP(mem) qr := F.qr(h) - s.copy(F.Sqrt(h)) + s.copy(F.Sqrt(h, mem)) i.copy(F) - i.Invert(h) + i.Invert(h, mem) return qr } // Two for the price of one - See Hamburg https://eprint.iacr.org/2012/309.pdf // Calculate Invert of i and square root of s, return QR func FP_tpo(i *FP, s *FP) int { - w := NewFPcopy(s) - t := NewFPcopy(i) - w.Mul(i) - t.Mul(w) + w := NewFPcopy(s, nil) + t := NewFPcopy(i, nil) + w.Mul(i, nil) + t.Mul(w, nil) qr := t.invsqrt(i, s) - i.Mul(w) - s.Mul(i) + i.Mul(w, nil) + s.Mul(i, nil) return qr } diff --git a/nekryptology/pkg/core/curves/native/bls48581/fp16.go b/nekryptology/pkg/core/curves/native/bls48581/fp16.go index 5f21e49..197ff43 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/fp16.go +++ b/nekryptology/pkg/core/curves/native/bls48581/fp16.go @@ -23,6 +23,8 @@ package bls48581 +import "arena" + //import "fmt" type FP16 struct { @@ -30,46 +32,81 @@ type FP16 struct { b *FP8 } -func NewFP16() *FP16 { - F := new(FP16) - F.a = NewFP8() - F.b = NewFP8() - return F +func NewFP16(mem *arena.Arena) *FP16 { + if mem != nil { + F := arena.New[FP16](mem) + F.a = NewFP8(mem) + F.b = NewFP8(mem) + return F + } else { + F := new(FP16) + F.a = NewFP8(nil) + F.b = NewFP8(nil) + return F + } } /* Constructors */ -func NewFP16int(a int) *FP16 { - F := new(FP16) - F.a = NewFP8int(a) - F.b = NewFP8() - return F +func NewFP16int(a int, mem *arena.Arena) *FP16 { + if mem != nil { + F := arena.New[FP16](mem) + F.a = NewFP8int(a, mem) + F.b = NewFP8(mem) + return F + } else { + F := new(FP16) + F.a = NewFP8int(a, nil) + F.b = NewFP8(nil) + return F + } } -func NewFP16copy(x *FP16) *FP16 { - F := new(FP16) - F.a = NewFP8copy(x.a) - F.b = NewFP8copy(x.b) - return F +func NewFP16copy(x *FP16, mem *arena.Arena) *FP16 { + if mem != nil { + F := arena.New[FP16](mem) + F.a = NewFP8copy(x.a, mem) + F.b = NewFP8copy(x.b, mem) + return F + } else { + F := new(FP16) + F.a = NewFP8copy(x.a, nil) + F.b = NewFP8copy(x.b, nil) + return F + } } -func NewFP16fp8s(c *FP8, d *FP8) *FP16 { - F := new(FP16) - F.a = NewFP8copy(c) - F.b = NewFP8copy(d) - return F +func NewFP16fp8s(c *FP8, d *FP8, mem *arena.Arena) *FP16 { + if mem != nil { + F := arena.New[FP16](mem) + F.a = c + F.b = d + return F + } else { + F := new(FP16) + F.a = c + F.b = d + return F + } } -func NewFP16fp8(c *FP8) *FP16 { - F := new(FP16) - F.a = NewFP8copy(c) - F.b = NewFP8() - return F +func NewFP16fp8(c *FP8, mem *arena.Arena) *FP16 { + if mem != nil { + F := arena.New[FP16](mem) + F.a = c + F.b = NewFP8(mem) + return F + } else { + F := new(FP16) + F.a = c + F.b = NewFP8(nil) + return F + } } /* reduce all components of this mod Modulus */ -func (F *FP16) reduce() { - F.a.reduce() - F.b.reduce() +func (F *FP16) reduce(mem *arena.Arena) { + F.a.reduce(mem) + F.b.reduce(mem) } /* normalise all components of this mod Modulus */ @@ -79,8 +116,8 @@ func (F *FP16) norm() { } /* test this==0 ? */ -func (F *FP16) IsZero() bool { - return F.a.IsZero() && F.b.IsZero() +func (F *FP16) IsZero(mem *arena.Arena) bool { + return F.a.IsZero(mem) && F.b.IsZero(mem) } func (F *FP16) ToBytes(bf []byte) { @@ -107,7 +144,7 @@ func FP16_fromBytes(bf []byte) *FP16 { t[i] = bf[i+MB] } ta := FP8_fromBytes(t[:]) - return NewFP16fp8s(ta, tb) + return NewFP16fp8s(ta, tb, nil) } /* Conditional move */ @@ -118,13 +155,15 @@ func (F *FP16) cmove(g *FP16, d int) { /* test this==1 ? */ func (F *FP16) isunity() bool { - one := NewFP8int(1) - return F.a.Equals(one) && F.b.IsZero() + mem := arena.NewArena() + defer mem.Free() + one := NewFP8int(1, mem) + return F.a.Equals(one) && F.b.IsZero(mem) } /* test is w real? That is in a+ib test b is zero */ func (F *FP16) isreal() bool { - return F.b.IsZero() + return F.b.IsZero(nil) } /* extract real part a */ @@ -165,137 +204,137 @@ func (F *FP16) one() { } /* set this=-this */ -func (F *FP16) Neg() { +func (F *FP16) Neg(mem *arena.Arena) { F.norm() - m := NewFP8copy(F.a) - t := NewFP8() - m.Add(F.b) - m.Neg() + m := NewFP8copy(F.a, mem) + t := NewFP8(mem) + m.Add(F.b, mem) + m.Neg(mem) t.copy(m) - t.Add(F.b) + t.Add(F.b, mem) F.b.copy(m) - F.b.Add(F.a) + F.b.Add(F.a, mem) F.a.copy(t) F.norm() } /* this=conjugate(this) */ -func (F *FP16) conj() { - F.b.Neg() +func (F *FP16) conj(mem *arena.Arena) { + F.b.Neg(mem) F.norm() } /* this=-conjugate(this) */ -func (F *FP16) nconj() { - F.a.Neg() +func (F *FP16) nconj(mem *arena.Arena) { + F.a.Neg(mem) F.norm() } /* this+=x */ -func (F *FP16) Add(x *FP16) { - F.a.Add(x.a) - F.b.Add(x.b) +func (F *FP16) Add(x *FP16, mem *arena.Arena) { + F.a.Add(x.a, mem) + F.b.Add(x.b, mem) } /* this-=x */ -func (F *FP16) Sub(x *FP16) { - m := NewFP16copy(x) - m.Neg() - F.Add(m) +func (F *FP16) Sub(x *FP16, mem *arena.Arena) { + m := NewFP16copy(x, mem) + m.Neg(mem) + F.Add(m, mem) } /* this-=x */ -func (F *FP16) rsub(x *FP16) { - F.Neg() - F.Add(x) +func (F *FP16) rsub(x *FP16, mem *arena.Arena) { + F.Neg(mem) + F.Add(x, mem) } /* this*=s where s is FP8 */ -func (F *FP16) pmul(s *FP8) { - F.a.Mul(s) - F.b.Mul(s) +func (F *FP16) pmul(s *FP8, mem *arena.Arena) { + F.a.Mul(s, mem) + F.b.Mul(s, mem) } /* this*=s where s is FP2 */ -func (F *FP16) qmul(s *FP2) { - F.a.qmul(s) - F.b.qmul(s) +func (F *FP16) qmul(s *FP2, mem *arena.Arena) { + F.a.qmul(s, mem) + F.b.qmul(s, mem) } /* this*=s where s is FP */ -func (F *FP16) tmul(s *FP) { - F.a.tmul(s) - F.b.tmul(s) +func (F *FP16) tmul(s *FP, mem *arena.Arena) { + F.a.tmul(s, mem) + F.b.tmul(s, mem) } /* this*=c where c is int */ -func (F *FP16) imul(c int) { - F.a.imul(c) - F.b.imul(c) +func (F *FP16) imul(c int, mem *arena.Arena) { + F.a.imul(c, mem) + F.b.imul(c, mem) } /* this*=this */ -func (F *FP16) Sqr() { - t1 := NewFP8copy(F.a) - t2 := NewFP8copy(F.b) - t3 := NewFP8copy(F.a) +func (F *FP16) Sqr(mem *arena.Arena) { + t1 := NewFP8copy(F.a, mem) + t2 := NewFP8copy(F.b, mem) + t3 := NewFP8copy(F.a, mem) - t3.Mul(F.b) - t1.Add(F.b) - t2.times_i() + t3.Mul(F.b, mem) + t1.Add(F.b, mem) + t2.times_i(mem) - t2.Add(F.a) + t2.Add(F.a, mem) t1.norm() t2.norm() F.a.copy(t1) - F.a.Mul(t2) + F.a.Mul(t2, mem) t2.copy(t3) - t2.times_i() - t2.Add(t3) + t2.times_i(mem) + t2.Add(t3, mem) t2.norm() - t2.Neg() - F.a.Add(t2) + t2.Neg(mem) + F.a.Add(t2, mem) F.b.copy(t3) - F.b.Add(t3) + F.b.Add(t3, mem) F.norm() } /* this*=y */ -func (F *FP16) Mul(y *FP16) { - t1 := NewFP8copy(F.a) - t2 := NewFP8copy(F.b) - t3 := NewFP8() - t4 := NewFP8copy(F.b) +func (F *FP16) Mul(y *FP16, mem *arena.Arena) { + t1 := NewFP8copy(F.a, mem) + t2 := NewFP8copy(F.b, mem) + t3 := NewFP8(mem) + t4 := NewFP8copy(F.b, mem) - t1.Mul(y.a) - t2.Mul(y.b) + t1.Mul(y.a, mem) + t2.Mul(y.b, mem) t3.copy(y.b) - t3.Add(y.a) - t4.Add(F.a) + t3.Add(y.a, mem) + t4.Add(F.a, mem) t3.norm() t4.norm() - t4.Mul(t3) + t4.Mul(t3, mem) t3.copy(t1) - t3.Neg() - t4.Add(t3) + t3.Neg(mem) + t4.Add(t3, mem) t4.norm() t3.copy(t2) - t3.Neg() + t3.Neg(mem) F.b.copy(t4) - F.b.Add(t3) + F.b.Add(t3, mem) - t2.times_i() + t2.times_i(mem) F.a.copy(t2) - F.a.Add(t1) + F.a.Add(t1, mem) F.norm() } @@ -306,77 +345,77 @@ func (F *FP16) toString() string { } /* this=1/this */ -func (F *FP16) Invert() { - t1 := NewFP8copy(F.a) - t2 := NewFP8copy(F.b) +func (F *FP16) Invert(mem *arena.Arena) { + t1 := NewFP8copy(F.a, mem) + t2 := NewFP8copy(F.b, mem) - t1.Sqr() - t2.Sqr() - t2.times_i() + t1.Sqr(mem) + t2.Sqr(mem) + t2.times_i(mem) t2.norm() - t1.Sub(t2) + t1.Sub(t2, mem) t1.norm() - t1.Invert(nil) + t1.Invert(nil, mem) - F.a.Mul(t1) - t1.Neg() + F.a.Mul(t1, mem) + t1.Neg(mem) t1.norm() - F.b.Mul(t1) + F.b.Mul(t1, mem) } /* this*=i where i = sqrt(sqrt(-1+sqrt(-1))) */ -func (F *FP16) times_i() { - s := NewFP8copy(F.b) - t := NewFP8copy(F.a) - s.times_i() +func (F *FP16) times_i(mem *arena.Arena) { + s := NewFP8copy(F.b, mem) + t := NewFP8copy(F.a, mem) + s.times_i(mem) F.a.copy(s) F.b.copy(t) F.norm() } -func (F *FP16) times_i2() { - F.a.times_i() - F.b.times_i() +func (F *FP16) times_i2(mem *arena.Arena) { + F.a.times_i(mem) + F.b.times_i(mem) } -func (F *FP16) times_i4() { - F.a.times_i2() - F.b.times_i2() +func (F *FP16) times_i4(mem *arena.Arena) { + F.a.times_i2(mem) + F.b.times_i2(mem) } /* this=this^p using Frobenius */ -func (F *FP16) frob(f *FP2) { - ff := NewFP2copy(f) - ff.Sqr() +func (F *FP16) frob(f *FP2, mem *arena.Arena) { + ff := NewFP2copy(f, mem) + ff.Sqr(mem) ff.norm() - F.a.frob(ff) - F.b.frob(ff) - F.b.qmul(f) - F.b.times_i() + F.a.frob(ff, mem) + F.b.frob(ff, mem) + F.b.qmul(f, mem) + F.b.times_i(mem) } /* this=this^e */ -func (F *FP16) pow(e *BIG) *FP16 { - w := NewFP16copy(F) +func (F *FP16) pow(e *BIG, mem *arena.Arena) *FP16 { + w := NewFP16copy(F, mem) w.norm() - z := NewBIGcopy(e) - r := NewFP16int(1) + z := NewBIGcopy(e, mem) + r := NewFP16int(1, mem) z.norm() for true { bt := z.parity() z.fshr(1) if bt == 1 { - r.Mul(w) + r.Mul(w, mem) } if z.IsZero() { break } - w.Sqr() + w.Sqr(mem) } - r.reduce() + r.reduce(mem) return r } diff --git a/nekryptology/pkg/core/curves/native/bls48581/fp2.go b/nekryptology/pkg/core/curves/native/bls48581/fp2.go index 861445d..1824f4b 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/fp2.go +++ b/nekryptology/pkg/core/curves/native/bls48581/fp2.go @@ -23,7 +23,11 @@ package bls48581 -import "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" +import ( + "arena" + + "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" +) //import "fmt" @@ -32,72 +36,128 @@ type FP2 struct { b *FP } -func NewFP2() *FP2 { - F := new(FP2) - F.a = NewFP() - F.b = NewFP() - return F +func NewFP2(mem *arena.Arena) *FP2 { + if mem != nil { + F := arena.New[FP2](mem) + F.a = NewFP(mem) + F.b = NewFP(mem) + return F + } else { + F := new(FP2) + F.a = NewFP(nil) + F.b = NewFP(nil) + return F + } } /* Constructors */ -func NewFP2int(a int) *FP2 { - F := new(FP2) - F.a = NewFPint(a) - F.b = NewFP() - return F +func NewFP2int(a int, mem *arena.Arena) *FP2 { + if mem != nil { + F := arena.New[FP2](mem) + F.a = NewFPint(a, mem) + F.b = NewFP(mem) + return F + } else { + F := new(FP2) + F.a = NewFPint(a, nil) + F.b = NewFP(nil) + return F + } } -func NewFP2ints(a int, b int) *FP2 { - F := new(FP2) - F.a = NewFPint(a) - F.b = NewFPint(b) - return F +func NewFP2ints(a int, b int, mem *arena.Arena) *FP2 { + if mem != nil { + F := arena.New[FP2](mem) + F.a = NewFPint(a, mem) + F.b = NewFPint(b, mem) + return F + } else { + F := new(FP2) + F.a = NewFPint(a, nil) + F.b = NewFPint(b, nil) + return F + } } -func NewFP2copy(x *FP2) *FP2 { - F := new(FP2) - F.a = NewFPcopy(x.a) - F.b = NewFPcopy(x.b) - return F +func NewFP2copy(x *FP2, mem *arena.Arena) *FP2 { + if mem != nil { + F := arena.New[FP2](mem) + F.a = NewFPcopy(x.a, mem) + F.b = NewFPcopy(x.b, mem) + return F + } else { + F := new(FP2) + F.a = NewFPcopy(x.a, nil) + F.b = NewFPcopy(x.b, nil) + return F + } } -func NewFP2fps(c *FP, d *FP) *FP2 { - F := new(FP2) - F.a = NewFPcopy(c) - F.b = NewFPcopy(d) - return F +func NewFP2fps(c *FP, d *FP, mem *arena.Arena) *FP2 { + if mem != nil { + F := arena.New[FP2](mem) + F.a = NewFPcopy(c, mem) + F.b = NewFPcopy(d, mem) + return F + } else { + F := new(FP2) + F.a = NewFPcopy(c, nil) + F.b = NewFPcopy(d, nil) + return F + } } -func NewFP2bigs(c *BIG, d *BIG) *FP2 { - F := new(FP2) - F.a = NewFPbig(c) - F.b = NewFPbig(d) - return F +func NewFP2bigs(c *BIG, d *BIG, mem *arena.Arena) *FP2 { + if mem != nil { + F := arena.New[FP2](mem) + F.a = NewFPbig(c, mem) + F.b = NewFPbig(d, mem) + return F + } else { + F := new(FP2) + F.a = NewFPbig(c, nil) + F.b = NewFPbig(d, nil) + return F + } } -func NewFP2fp(c *FP) *FP2 { - F := new(FP2) - F.a = NewFPcopy(c) - F.b = NewFP() - return F +func NewFP2fp(c *FP, mem *arena.Arena) *FP2 { + if mem != nil { + F := arena.New[FP2](mem) + F.a = NewFPcopy(c, mem) + F.b = NewFP(mem) + return F + } else { + F := new(FP2) + F.a = NewFPcopy(c, nil) + F.b = NewFP(nil) + return F + } } -func NewFP2big(c *BIG) *FP2 { - F := new(FP2) - F.a = NewFPbig(c) - F.b = NewFP() - return F +func NewFP2big(c *BIG, mem *arena.Arena) *FP2 { + if mem != nil { + F := arena.New[FP2](mem) + F.a = NewFPbig(c, mem) + F.b = NewFP(mem) + return F + } else { + F := new(FP2) + F.a = NewFPbig(c, nil) + F.b = NewFP(nil) + return F + } } func NewFP2rand(rng *ext.RAND) *FP2 { - F := NewFP2fps(NewFPrand(rng), NewFPrand(rng)) + F := NewFP2fps(NewFPrand(rng), NewFPrand(rng), nil) return F } /* reduce components mod Modulus */ -func (F *FP2) reduce() { - F.a.reduce() - F.b.reduce() +func (F *FP2) reduce(mem *arena.Arena) { + F.a.reduce(mem) + F.b.reduce(mem) } /* normalise components of w */ @@ -107,12 +167,12 @@ func (F *FP2) norm() { } /* test this=0 ? */ -func (F *FP2) IsZero() bool { - return (F.a.IsZero() && F.b.IsZero()) +func (F *FP2) IsZero(mem *arena.Arena) bool { + return (F.a.IsZero(mem) && F.b.IsZero(mem)) } func (F *FP2) islarger() int { - if F.IsZero() { + if F.IsZero(nil) { return 0 } cmp := F.b.islarger() @@ -146,7 +206,7 @@ func FP2_fromBytes(bf []byte) *FP2 { t[i] = bf[i+MB] } ta := FP_fromBytes(t[:]) - return NewFP2fps(ta, tb) + return NewFP2fps(ta, tb, nil) } func (F *FP2) cmove(g *FP2, d int) { @@ -156,8 +216,10 @@ func (F *FP2) cmove(g *FP2, d int) { /* test this=1 ? */ func (F *FP2) isunity() bool { - one := NewFPint(1) - return (F.a.Equals(one) && F.b.IsZero()) + mem := arena.NewArena() + defer mem.Free() + one := NewFPint(1, mem) + return (F.a.Equals(one) && F.b.IsZero(mem)) } /* test this=x */ @@ -166,13 +228,13 @@ func (F *FP2) Equals(x *FP2) bool { } /* extract a */ -func (F *FP2) GetA() *BIG { - return F.a.Redc() +func (F *FP2) GetA(mem *arena.Arena) *BIG { + return F.a.Redc(mem) } /* extract b */ -func (F *FP2) GetB() *BIG { - return F.b.Redc() +func (F *FP2) GetB(mem *arena.Arena) *BIG { + return F.b.Redc(mem) } /* copy this=x */ @@ -194,12 +256,12 @@ func (F *FP2) one() { } /* Return sign */ -func (F *FP2) sign() int { - p1 := F.a.sign() - p2 := F.b.sign() +func (F *FP2) sign(mem *arena.Arena) int { + p1 := F.a.sign(mem) + p2 := F.b.sign(mem) var u int if BIG_ENDIAN_SIGN { - if F.b.IsZero() { + if F.b.IsZero(mem) { u = 1 } else { u = 0 @@ -207,7 +269,7 @@ func (F *FP2) sign() int { p2 ^= (p1 ^ p2) & u return p2 } else { - if F.a.IsZero() { + if F.a.IsZero(mem) { u = 1 } else { u = 0 @@ -218,106 +280,106 @@ func (F *FP2) sign() int { } /* negate this mod Modulus */ -func (F *FP2) Neg() { - m := NewFPcopy(F.a) - t := NewFP() +func (F *FP2) Neg(mem *arena.Arena) { + m := NewFPcopy(F.a, mem) + t := NewFP(mem) - m.Add(F.b) - m.Neg() + m.Add(F.b, mem) + m.Neg(mem) t.copy(m) - t.Add(F.b) + t.Add(F.b, mem) F.b.copy(m) - F.b.Add(F.a) + F.b.Add(F.a, mem) F.a.copy(t) } /* set to a-ib */ -func (F *FP2) conj() { - F.b.Neg() +func (F *FP2) conj(mem *arena.Arena) { + F.b.Neg(mem) F.b.norm() } /* this+=a */ -func (F *FP2) Add(x *FP2) { - F.a.Add(x.a) - F.b.Add(x.b) +func (F *FP2) Add(x *FP2, mem *arena.Arena) { + F.a.Add(x.a, mem) + F.b.Add(x.b, mem) } /* this-=a */ -func (F *FP2) Sub(x *FP2) { - m := NewFP2copy(x) - m.Neg() - F.Add(m) +func (F *FP2) Sub(x *FP2, mem *arena.Arena) { + m := NewFP2copy(x, mem) + m.Neg(mem) + F.Add(m, mem) } /* this-=a */ -func (F *FP2) rsub(x *FP2) { - F.Neg() - F.Add(x) +func (F *FP2) rsub(x *FP2, mem *arena.Arena) { + F.Neg(mem) + F.Add(x, mem) } /* this*=s, where s is an FP */ -func (F *FP2) pmul(s *FP) { - F.a.Mul(s) - F.b.Mul(s) +func (F *FP2) pmul(s *FP, mem *arena.Arena) { + F.a.Mul(s, mem) + F.b.Mul(s, mem) } /* this*=i, where i is an int */ -func (F *FP2) imul(c int) { - F.a.imul(c) - F.b.imul(c) +func (F *FP2) imul(c int, mem *arena.Arena) { + F.a.imul(c, mem) + F.b.imul(c, mem) } /* this*=this */ -func (F *FP2) Sqr() { - w1 := NewFPcopy(F.a) - w3 := NewFPcopy(F.a) - mb := NewFPcopy(F.b) - w1.Add(F.b) +func (F *FP2) Sqr(mem *arena.Arena) { + w1 := NewFPcopy(F.a, mem) + w3 := NewFPcopy(F.a, mem) + mb := NewFPcopy(F.b, mem) + w1.Add(F.b, mem) - w3.Add(F.a) + w3.Add(F.a, mem) w3.norm() - F.b.Mul(w3) + F.b.Mul(w3, mem) - mb.Neg() - F.a.Add(mb) + mb.Neg(mem) + F.a.Add(mb, mem) w1.norm() F.a.norm() - F.a.Mul(w1) + F.a.Mul(w1, mem) } /* this*=y */ /* Now using Lazy reduction */ -func (F *FP2) Mul(y *FP2) { +func (F *FP2) Mul(y *FP2, mem *arena.Arena) { if int64(F.a.XES+F.b.XES)*int64(y.a.XES+y.b.XES) > int64(FEXCESS) { if F.a.XES > 1 { - F.a.reduce() + F.a.reduce(mem) } if F.b.XES > 1 { - F.b.reduce() + F.b.reduce(mem) } } - pR := NewDBIG() - C := NewBIGcopy(F.a.x) - D := NewBIGcopy(y.a.x) - p := NewBIGints(Modulus) + pR := NewDBIG(mem) + C := NewBIGcopy(F.a.x, mem) + D := NewBIGcopy(y.a.x, mem) + p := NewBIGints(Modulus, mem) pR.ucopy(p) - A := mul(F.a.x, y.a.x) - B := mul(F.b.x, y.b.x) + A := mul(F.a.x, y.a.x, mem) + B := mul(F.b.x, y.b.x, mem) C.Add(F.b.x) C.norm() D.Add(y.b.x) D.norm() - E := mul(C, D) - FF := NewDBIGcopy(A) + E := mul(C, D, mem) + FF := NewDBIGcopy(A, mem) FF.Add(B) B.rsub(pR) @@ -326,82 +388,84 @@ func (F *FP2) Mul(y *FP2) { E.Sub(FF) E.norm() - F.a.x.copy(mod(A)) + F.a.x.copy(mod(A, mem)) F.a.XES = 3 - F.b.x.copy(mod(E)) + F.b.x.copy(mod(E, mem)) F.b.XES = 2 } /* -func (F *FP2) pow(b *BIG) { - w := NewFP2copy(F); - r := NewFP2int(1) - z := NewBIGcopy(b) - for true { - bt := z.parity() - z.shr(1) - if bt==1 { - r.Mul(w) + func (F *FP2) pow(b *BIG) { + w := NewFP2copy(F); + r := NewFP2int(1) + z := NewBIGcopy(b) + for true { + bt := z.parity() + z.shr(1) + if bt==1 { + r.Mul(w) + } + if z.IsZero() {break} + w.Sqr() } - if z.IsZero() {break} - w.Sqr() + r.reduce() + F.copy(r) } - r.reduce() - F.copy(r) -} */ func (F *FP2) qr(h *FP) int { - c := NewFP2copy(F) - c.conj() - c.Mul(F) + mem := arena.NewArena() + defer mem.Free() + c := NewFP2copy(F, mem) + c.conj(mem) + c.Mul(F, mem) return c.a.qr(h) } /* sqrt(a+ib) = sqrt(a+sqrt(a*a-n*b*b)/2)+ib/(2*sqrt(a+sqrt(a*a-n*b*b)/2)) */ -func (F *FP2) Sqrt(h *FP) { - if F.IsZero() { +func (F *FP2) Sqrt(h *FP, mem *arena.Arena) { + if F.IsZero(mem) { return } - w1 := NewFPcopy(F.b) - w2 := NewFPcopy(F.a) - w3 := NewFP() - w4 := NewFP() - hint := NewFP() - w1.Sqr() - w2.Sqr() - w1.Add(w2) + w1 := NewFPcopy(F.b, mem) + w2 := NewFPcopy(F.a, mem) + w3 := NewFP(mem) + w4 := NewFP(mem) + hint := NewFP(mem) + w1.Sqr(mem) + w2.Sqr(mem) + w1.Add(w2, mem) w1.norm() - w1 = w1.Sqrt(h) + w1 = w1.Sqrt(h, mem) w2.copy(F.a) w3.copy(F.a) - w2.Add(w1) + w2.Add(w1, mem) w2.norm() - w2.div2() + w2.div2(mem) w1.copy(F.b) - w1.div2() + w1.div2(mem) qr := w2.qr(hint) // tweak hint w3.copy(hint) - w3.Neg() + w3.Neg(mem) w3.norm() w4.copy(w2) - w4.Neg() + w4.Neg(mem) w4.norm() w2.cmove(w4, 1-qr) hint.cmove(w3, 1-qr) - F.a.copy(w2.Sqrt(hint)) + F.a.copy(w2.Sqrt(hint, mem)) w3.copy(w2) - w3.Invert(hint) - w3.Mul(F.a) + w3.Invert(hint, mem) + w3.Mul(F.a, mem) F.b.copy(w3) - F.b.Mul(w1) + F.b.Mul(w1, mem) w4.copy(F.a) F.a.cmove(F.b, 1-qr) @@ -425,9 +489,9 @@ func (F *FP2) Sqrt(h *FP) { F.b.cmove(w4,1-qr) */ - sgn := F.sign() - nr := NewFP2copy(F) - nr.Neg() + sgn := F.sign(mem) + nr := NewFP2copy(F, mem) + nr.Neg(mem) nr.norm() F.cmove(nr, sgn) } @@ -443,63 +507,63 @@ func (F *FP2) toString() string { } /* this=1/this */ -func (F *FP2) Invert(h *FP) { +func (F *FP2) Invert(h *FP, mem *arena.Arena) { F.norm() - w1 := NewFPcopy(F.a) - w2 := NewFPcopy(F.b) + w1 := NewFPcopy(F.a, mem) + w2 := NewFPcopy(F.b, mem) - w1.Sqr() - w2.Sqr() - w1.Add(w2) - w1.Invert(h) - F.a.Mul(w1) - w1.Neg() + w1.Sqr(mem) + w2.Sqr(mem) + w1.Add(w2, mem) + w1.Invert(h, mem) + F.a.Mul(w1, mem) + w1.Neg(mem) w1.norm() - F.b.Mul(w1) + F.b.Mul(w1, mem) } /* this/=2 */ -func (F *FP2) div2() { - F.a.div2() - F.b.div2() +func (F *FP2) div2(mem *arena.Arena) { + F.a.div2(mem) + F.b.div2(mem) } /* this*=sqrt(-1) */ -func (F *FP2) times_i() { - z := NewFPcopy(F.a) +func (F *FP2) times_i(mem *arena.Arena) { + z := NewFPcopy(F.a, mem) F.a.copy(F.b) - F.a.Neg() + F.a.Neg(mem) F.b.copy(z) } /* w*=(1+sqrt(-1)) */ /* where X*2-(2^i+sqrt(-1)) is irreducible for FP4 */ -func (F *FP2) Mul_ip() { - t := NewFP2copy(F) +func (F *FP2) Mul_ip(mem *arena.Arena) { + t := NewFP2copy(F, mem) i := QNRI - F.times_i() + F.times_i(mem) for i > 0 { - t.Add(t) + t.Add(t, mem) t.norm() i-- } - F.Add(t) + F.Add(t, mem) if TOWER == POSITOWER { F.norm() - F.Neg() + F.Neg(mem) } } /* w/=(2^i+sqrt(-1)) */ -func (F *FP2) div_ip() { - z := NewFP2ints(1<= 0; i-- { if v.bit(i) != 1 { t.copy(b) - sf.conj() - c.conj() - b.xtr_A(a, sf, c) - sf.conj() + sf.conj(mem) + c.conj(mem) + b.xtr_A(a, sf, c, mem) + sf.conj(mem) c.copy(t) - c.xtr_D() - a.xtr_D() + c.xtr_D(mem) + a.xtr_D(mem) } else { t.copy(a) - t.conj() + t.conj(mem) a.copy(b) - a.xtr_D() - b.xtr_A(c, sf, t) - c.xtr_D() + a.xtr_D(mem) + b.xtr_A(c, sf, t, mem) + c.xtr_D(mem) } } if par == 0 { @@ -492,25 +549,25 @@ func (F *FP4) xtr_pow(n *BIG) *FP4 { } else { r.copy(b) } - r.reduce() + r.reduce(mem) return r } /* r=ck^a.cl^n using XTR double exponentiation method on traces of FP12s. See Stam thesis. */ -func (F *FP4) xtr_pow2(ck *FP4, ckml *FP4, ckm2l *FP4, a *BIG, b *BIG) *FP4 { +func (F *FP4) xtr_pow2(ck *FP4, ckml *FP4, ckm2l *FP4, a *BIG, b *BIG, mem *arena.Arena) *FP4 { - e := NewBIGcopy(a) - d := NewBIGcopy(b) - w := NewBIGint(0) + e := NewBIGcopy(a, mem) + d := NewBIGcopy(b, mem) + w := NewBIGint(0, mem) e.norm() d.norm() - cu := NewFP4copy(ck) // can probably be passed in w/o copying - cv := NewFP4copy(F) - cumv := NewFP4copy(ckml) - cum2v := NewFP4copy(ckm2l) - r := NewFP4() - t := NewFP4() + cu := NewFP4copy(ck, mem) // can probably be passed in w/o copying + cv := NewFP4copy(F, mem) + cumv := NewFP4copy(ckml, mem) + cum2v := NewFP4copy(ckm2l, mem) + r := NewFP4(mem) + t := NewFP4(mem) f2 := 0 for d.parity() == 0 && e.parity() == 0 { @@ -531,9 +588,9 @@ func (F *FP4) xtr_pow2(ck *FP4, ckml *FP4, ckm2l *FP4, a *BIG, b *BIG) *FP4 { e.norm() t.copy(cv) - t.xtr_A(cu, cumv, cum2v) + t.xtr_A(cu, cumv, cum2v, mem) cum2v.copy(cumv) - cum2v.conj() + cum2v.conj(mem) cumv.copy(cv) cv.copy(cu) cu.copy(t) @@ -541,24 +598,24 @@ func (F *FP4) xtr_pow2(ck *FP4, ckml *FP4, ckm2l *FP4, a *BIG, b *BIG) *FP4 { if d.parity() == 0 { d.fshr(1) r.copy(cum2v) - r.conj() + r.conj(mem) t.copy(cumv) - t.xtr_A(cu, cv, r) + t.xtr_A(cu, cv, r, mem) cum2v.copy(cumv) - cum2v.xtr_D() + cum2v.xtr_D(mem) cumv.copy(t) - cu.xtr_D() + cu.xtr_D(mem) } else { if e.parity() == 1 { d.Sub(e) d.norm() d.fshr(1) t.copy(cv) - t.xtr_A(cu, cumv, cum2v) - cu.xtr_D() + t.xtr_A(cu, cumv, cum2v, mem) + cu.xtr_D(mem) cum2v.copy(cv) - cum2v.xtr_D() - cum2v.conj() + cum2v.xtr_D(mem) + cum2v.conj(mem) cv.copy(t) } else { w.copy(d) @@ -566,13 +623,13 @@ func (F *FP4) xtr_pow2(ck *FP4, ckml *FP4, ckm2l *FP4, a *BIG, b *BIG) *FP4 { d.fshr(1) e.copy(w) t.copy(cumv) - t.xtr_D() + t.xtr_D(mem) cumv.copy(cum2v) - cumv.conj() + cumv.conj(mem) cum2v.copy(t) - cum2v.conj() + cum2v.conj(mem) t.copy(cv) - t.xtr_D() + t.xtr_D(mem) cv.copy(cu) cu.copy(t) } @@ -587,7 +644,7 @@ func (F *FP4) xtr_pow2(ck *FP4, ckml *FP4, ckm2l *FP4, a *BIG, b *BIG) *FP4 { e.Sub(d) e.norm() t.copy(cv) - t.xtr_A(cu, cumv, cum2v) + t.xtr_A(cu, cumv, cum2v, mem) cum2v.copy(cumv) cumv.copy(cu) cu.copy(t) @@ -598,13 +655,13 @@ func (F *FP4) xtr_pow2(ck *FP4, ckml *FP4, ckm2l *FP4, a *BIG, b *BIG) *FP4 { d.fshr(1) e.copy(w) t.copy(cumv) - t.xtr_D() + t.xtr_D(mem) cumv.copy(cum2v) - cumv.conj() + cumv.conj(mem) cum2v.copy(t) - cum2v.conj() + cum2v.conj(mem) t.copy(cv) - t.xtr_D() + t.xtr_D(mem) cv.copy(cu) cu.copy(t) } else { @@ -616,52 +673,52 @@ func (F *FP4) xtr_pow2(ck *FP4, ckml *FP4, ckm2l *FP4, a *BIG, b *BIG) *FP4 { d.copy(w) d.fshr(1) t.copy(cv) - t.xtr_A(cu, cumv, cum2v) - cumv.conj() + t.xtr_A(cu, cumv, cum2v, mem) + cumv.conj(mem) cum2v.copy(cu) - cum2v.xtr_D() - cum2v.conj() + cum2v.xtr_D(mem) + cum2v.conj(mem) cu.copy(cv) - cu.xtr_D() + cu.xtr_D(mem) cv.copy(t) } else { d.fshr(1) r.copy(cum2v) - r.conj() + r.conj(mem) t.copy(cumv) - t.xtr_A(cu, cv, r) + t.xtr_A(cu, cv, r, mem) cum2v.copy(cumv) - cum2v.xtr_D() + cum2v.xtr_D(mem) cumv.copy(t) - cu.xtr_D() + cu.xtr_D(mem) } } } } } r.copy(cv) - r.xtr_A(cu, cumv, cum2v) + r.xtr_A(cu, cumv, cum2v, mem) for i := 0; i < f2; i++ { - r.xtr_D() + r.xtr_D(mem) } - r = r.xtr_pow(d) + r = r.xtr_pow(d, mem) return r } /* this/=2 */ -func (F *FP4) div2() { - F.a.div2() - F.b.div2() +func (F *FP4) div2(mem *arena.Arena) { + F.a.div2(mem) + F.b.div2(mem) } -func (F *FP4) div_i() { - u := NewFP2copy(F.a) - v := NewFP2copy(F.b) - u.div_ip() +func (F *FP4) div_i(mem *arena.Arena) { + u := NewFP2copy(F.a, mem) + v := NewFP2copy(F.b, mem) + u.div_ip(mem) F.a.copy(v) F.b.copy(u) if TOWER == POSITOWER { - F.Neg() + F.Neg(mem) F.norm() } } @@ -688,70 +745,72 @@ func (F *FP4) pow(b *BIG) { /* */ // Test for Quadratic Residue func (F *FP4) qr(h *FP) int { - c := NewFP4copy(F) - c.conj() - c.Mul(F) + mem := arena.NewArena() + defer mem.Free() + c := NewFP4copy(F, mem) + c.conj(mem) + c.Mul(F, mem) return c.a.qr(h) } // sqrt(a+ib) = sqrt(a+sqrt(a*a-n*b*b)/2)+ib/(2*sqrt(a+sqrt(a*a-n*b*b)/2)) -func (F *FP4) Sqrt(h *FP) { - if F.IsZero() { +func (F *FP4) Sqrt(h *FP, mem *arena.Arena) { + if F.IsZero(mem) { return } - a := NewFP2copy(F.a) - b := NewFP2() - s := NewFP2copy(F.b) - t := NewFP2copy(F.a) - hint := NewFP() + a := NewFP2copy(F.a, mem) + b := NewFP2(mem) + s := NewFP2copy(F.b, mem) + t := NewFP2copy(F.a, mem) + hint := NewFP(mem) - s.Sqr() - a.Sqr() - s.Mul_ip() + s.Sqr(mem) + a.Sqr(mem) + s.Mul_ip(mem) s.norm() - a.Sub(s) + a.Sub(s, mem) s.copy(a) s.norm() - s.Sqrt(h) + s.Sqrt(h, mem) a.copy(t) b.copy(t) - a.Add(s) + a.Add(s, mem) a.norm() - a.div2() + a.div2(mem) b.copy(F.b) - b.div2() + b.div2(mem) qr := a.qr(hint) // tweak hint - multiply old hint by Norm(1/Beta)^e where Beta is irreducible polynomial s.copy(a) - twk := NewFPbig(NewBIGints(TWK)) - twk.Mul(hint) - s.div_ip() + twk := NewFPbig(NewBIGints(TWK, mem), mem) + twk.Mul(hint, mem) + s.div_ip(mem) s.norm() a.cmove(s, 1-qr) hint.cmove(twk, 1-qr) F.a.copy(a) - F.a.Sqrt(hint) + F.a.Sqrt(hint, mem) s.copy(a) - s.Invert(hint) - s.Mul(F.a) + s.Invert(hint, mem) + s.Mul(F.a, mem) F.b.copy(s) - F.b.Mul(b) + F.b.Mul(b, mem) t.copy(F.a) F.a.cmove(F.b, 1-qr) F.b.cmove(t, 1-qr) - sgn := F.sign() - nr := NewFP4copy(F) - nr.Neg() + sgn := F.sign(mem) + nr := NewFP4copy(F, mem) + nr.Neg(mem) nr.norm() F.cmove(nr, sgn) } diff --git a/nekryptology/pkg/core/curves/native/bls48581/fp48.go b/nekryptology/pkg/core/curves/native/bls48581/fp48.go index fd8df7f..50e9d85 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/fp48.go +++ b/nekryptology/pkg/core/curves/native/bls48581/fp48.go @@ -22,6 +22,8 @@ package bls48581 +import "arena" + //import "fmt" type FP48 struct { @@ -32,29 +34,52 @@ type FP48 struct { } /* Constructors */ -func NewFP48fp16(d *FP16) *FP48 { - F := new(FP48) - F.a = NewFP16copy(d) - F.b = NewFP16() - F.c = NewFP16() - F.stype = FP_SPARSEST - return F +func NewFP48fp16(d *FP16, mem *arena.Arena) *FP48 { + if mem != nil { + F := arena.New[FP48](mem) + F.a = NewFP16copy(d, mem) + F.b = NewFP16(mem) + F.c = NewFP16(mem) + F.stype = FP_SPARSEST + return F + } else { + F := new(FP48) + F.a = NewFP16copy(d, nil) + F.b = NewFP16(nil) + F.c = NewFP16(nil) + F.stype = FP_SPARSEST + return F + } } -func NewFP48() *FP48 { - F := new(FP48) - F.a = NewFP16() - F.b = NewFP16() - F.c = NewFP16() - F.stype = FP_ZERO - return F +func NewFP48(mem *arena.Arena) *FP48 { + if mem != nil { + F := arena.New[FP48](mem) + F.a = NewFP16(mem) + F.b = NewFP16(mem) + F.c = NewFP16(mem) + F.stype = FP_ZERO + return F + } else { + F := new(FP48) + F.a = NewFP16(nil) + F.b = NewFP16(nil) + F.c = NewFP16(nil) + F.stype = FP_ZERO + return F + } } -func NewFP48int(d int) *FP48 { - F := new(FP48) - F.a = NewFP16int(d) - F.b = NewFP16() - F.c = NewFP16() +func NewFP48int(d int, mem *arena.Arena) *FP48 { + var F *FP48 + if mem != nil { + F = arena.New[FP48](mem) + } else { + F = new(FP48) + } + F.a = NewFP16int(d, mem) + F.b = NewFP16(mem) + F.c = NewFP16(mem) if d == 1 { F.stype = FP_ONE } else { @@ -63,29 +88,39 @@ func NewFP48int(d int) *FP48 { return F } -func NewFP48fp16s(d *FP16, e *FP16, f *FP16) *FP48 { - F := new(FP48) - F.a = NewFP16copy(d) - F.b = NewFP16copy(e) - F.c = NewFP16copy(f) +func NewFP48fp16s(d *FP16, e *FP16, f *FP16, mem *arena.Arena) *FP48 { + var F *FP48 + if mem != nil { + F = arena.New[FP48](mem) + } else { + F = new(FP48) + } + F.a = d + F.b = e + F.c = f F.stype = FP_DENSE return F } -func NewFP48copy(x *FP48) *FP48 { - F := new(FP48) - F.a = NewFP16copy(x.a) - F.b = NewFP16copy(x.b) - F.c = NewFP16copy(x.c) +func NewFP48copy(x *FP48, mem *arena.Arena) *FP48 { + var F *FP48 + if mem != nil { + F = arena.New[FP48](mem) + } else { + F = new(FP48) + } + F.a = NewFP16copy(x.a, mem) + F.b = NewFP16copy(x.b, mem) + F.c = NewFP16copy(x.c, mem) F.stype = x.stype return F } /* reduce all components of this mod Modulus */ -func (F *FP48) reduce() { - F.a.reduce() - F.b.reduce() - F.c.reduce() +func (F *FP48) reduce(mem *arena.Arena) { + F.a.reduce(mem) + F.b.reduce(mem) + F.c.reduce(mem) } /* normalise all components of this */ @@ -96,8 +131,8 @@ func (F *FP48) norm() { } /* test x==0 ? */ -func (F *FP48) IsZero() bool { - return (F.a.IsZero() && F.b.IsZero() && F.c.IsZero()) +func (F *FP48) IsZero(mem *arena.Arena) bool { + return (F.a.IsZero(mem) && F.b.IsZero(mem) && F.c.IsZero(mem)) } /* Conditional move */ @@ -126,15 +161,17 @@ func (F *FP48) selector(g []*FP48, b int32) { F.cmove(g[6], teq(babs, 6)) F.cmove(g[7], teq(babs, 7)) - invF := NewFP48copy(F) - invF.conj() + invF := NewFP48copy(F, nil) + invF.conj(nil) F.cmove(invF, int(m&1)) } /* test x==1 ? */ func (F *FP48) Isunity() bool { - one := NewFP16int(1) - return (F.a.Equals(one) && F.b.IsZero() && F.c.IsZero()) + mem := arena.NewArena() + defer mem.Free() + one := NewFP16int(1, mem) + return (F.a.Equals(one) && F.b.IsZero(mem) && F.c.IsZero(mem)) } /* return 1 if x==y, else 0 */ @@ -182,94 +219,94 @@ func (F *FP48) zero() { } /* this=conj(this) */ -func (F *FP48) conj() { - F.a.conj() - F.b.nconj() - F.c.conj() +func (F *FP48) conj(mem *arena.Arena) { + F.a.conj(mem) + F.b.nconj(mem) + F.c.conj(mem) } /* Granger-Scott Unitary Squaring */ -func (F *FP48) uSqr() { - A := NewFP16copy(F.a) - B := NewFP16copy(F.c) - C := NewFP16copy(F.b) - D := NewFP16() +func (F *FP48) uSqr(mem *arena.Arena) { + A := NewFP16copy(F.a, mem) + B := NewFP16copy(F.c, mem) + C := NewFP16copy(F.b, mem) + D := NewFP16(mem) - F.a.Sqr() + F.a.Sqr(mem) D.copy(F.a) - D.Add(F.a) - F.a.Add(D) + D.Add(F.a, mem) + F.a.Add(D, mem) F.a.norm() - A.nconj() + A.nconj(mem) - A.Add(A) - F.a.Add(A) - B.Sqr() - B.times_i() + A.Add(A, mem) + F.a.Add(A, mem) + B.Sqr(mem) + B.times_i(mem) D.copy(B) - D.Add(B) - B.Add(D) + D.Add(B, mem) + B.Add(D, mem) B.norm() - C.Sqr() + C.Sqr(mem) D.copy(C) - D.Add(C) - C.Add(D) + D.Add(C, mem) + C.Add(D, mem) C.norm() - F.b.conj() - F.b.Add(F.b) - F.c.nconj() + F.b.conj(mem) + F.b.Add(F.b, mem) + F.c.nconj(mem) - F.c.Add(F.c) - F.b.Add(B) - F.c.Add(C) - F.reduce() + F.c.Add(F.c, mem) + F.b.Add(B, mem) + F.c.Add(C, mem) + F.reduce(mem) F.stype = FP_DENSE } /* Chung-Hasan SQR2 method from http://cacr.uwaterloo.ca/techreports/2006/cacr2006-24.pdf */ -func (F *FP48) Sqr() { +func (F *FP48) Sqr(mem *arena.Arena) { if F.stype == FP_ONE { return } - A := NewFP16copy(F.a) - B := NewFP16copy(F.b) - C := NewFP16copy(F.c) - D := NewFP16copy(F.a) + A := NewFP16copy(F.a, mem) + B := NewFP16copy(F.b, mem) + C := NewFP16copy(F.c, mem) + D := NewFP16copy(F.a, mem) - A.Sqr() - B.Mul(F.c) - B.Add(B) + A.Sqr(mem) + B.Mul(F.c, mem) + B.Add(B, mem) B.norm() - C.Sqr() - D.Mul(F.b) - D.Add(D) + C.Sqr(mem) + D.Mul(F.b, mem) + D.Add(D, mem) - F.c.Add(F.a) - F.c.Add(F.b) + F.c.Add(F.a, mem) + F.c.Add(F.b, mem) F.c.norm() - F.c.Sqr() + F.c.Sqr(mem) F.a.copy(A) - A.Add(B) + A.Add(B, mem) A.norm() - A.Add(C) - A.Add(D) + A.Add(C, mem) + A.Add(D, mem) A.norm() - A.Neg() - B.times_i() - C.times_i() + A.Neg(mem) + B.times_i(mem) + C.times_i(mem) - F.a.Add(B) + F.a.Add(B, mem) F.b.copy(C) - F.b.Add(D) - F.c.Add(A) + F.b.Add(D, mem) + F.c.Add(A, mem) if F.stype == FP_SPARSER || F.stype == FP_SPARSEST { F.stype = FP_SPARSE } else { @@ -279,70 +316,70 @@ func (F *FP48) Sqr() { } /* FP48 full multiplication this=this*y */ -func (F *FP48) Mul(y *FP48) { - z0 := NewFP16copy(F.a) - z1 := NewFP16() - z2 := NewFP16copy(F.b) - z3 := NewFP16() - t0 := NewFP16copy(F.a) - t1 := NewFP16copy(y.a) +func (F *FP48) Mul(y *FP48, mem *arena.Arena) { + z0 := NewFP16copy(F.a, mem) + z1 := NewFP16(mem) + z2 := NewFP16copy(F.b, mem) + z3 := NewFP16(mem) + t0 := NewFP16copy(F.a, mem) + t1 := NewFP16copy(y.a, mem) - z0.Mul(y.a) - z2.Mul(y.b) + z0.Mul(y.a, mem) + z2.Mul(y.b, mem) - t0.Add(F.b) + t0.Add(F.b, mem) t0.norm() - t1.Add(y.b) + t1.Add(y.b, mem) t1.norm() z1.copy(t0) - z1.Mul(t1) + z1.Mul(t1, mem) t0.copy(F.b) - t0.Add(F.c) + t0.Add(F.c, mem) t0.norm() t1.copy(y.b) - t1.Add(y.c) + t1.Add(y.c, mem) t1.norm() z3.copy(t0) - z3.Mul(t1) + z3.Mul(t1, mem) t0.copy(z0) - t0.Neg() + t0.Neg(mem) t1.copy(z2) - t1.Neg() + t1.Neg(mem) - z1.Add(t0) + z1.Add(t0, mem) //z1.norm(); F.b.copy(z1) - F.b.Add(t1) + F.b.Add(t1, mem) - z3.Add(t1) - z2.Add(t0) + z3.Add(t1, mem) + z2.Add(t0, mem) t0.copy(F.a) - t0.Add(F.c) + t0.Add(F.c, mem) t0.norm() t1.copy(y.a) - t1.Add(y.c) + t1.Add(y.c, mem) t1.norm() - t0.Mul(t1) - z2.Add(t0) + t0.Mul(t1, mem) + z2.Add(t0, mem) t0.copy(F.c) - t0.Mul(y.c) + t0.Mul(y.c, mem) t1.copy(t0) - t1.Neg() + t1.Neg(mem) F.c.copy(z2) - F.c.Add(t1) - z3.Add(t1) - t0.times_i() - F.b.Add(t0) + F.c.Add(t1, mem) + z3.Add(t1, mem) + t0.times_i(mem) + F.b.Add(t0, mem) z3.norm() - z3.times_i() + z3.times_i(mem) F.a.copy(z0) - F.a.Add(z3) + F.a.Add(z3, mem) F.stype = FP_DENSE F.norm() } @@ -350,7 +387,7 @@ func (F *FP48) Mul(y *FP48) { /* FP48 full multiplication w=w*y */ /* Supports sparse multiplicands */ /* Usually w is denser than y */ -func (F *FP48) ssmul(y *FP48) { +func (F *FP48) ssmul(y *FP48, mem *arena.Arena) { if F.stype == FP_ONE { F.Copy(y) return @@ -359,483 +396,307 @@ func (F *FP48) ssmul(y *FP48) { return } if y.stype >= FP_SPARSE { - z0 := NewFP16copy(F.a) - z1 := NewFP16() - z2 := NewFP16() - z3 := NewFP16() - z0.Mul(y.a) + z0 := NewFP16copy(F.a, mem) + z1 := NewFP16(mem) + z2 := NewFP16(mem) + z3 := NewFP16(mem) + z0.Mul(y.a, mem) - if SEXTIC_TWIST == M_TYPE { - if y.stype == FP_SPARSE || F.stype == FP_SPARSE { - z2.getb().copy(F.b.getb()) - z2.getb().Mul(y.b.getb()) - z2.geta().zero() - if y.stype != FP_SPARSE { - z2.geta().copy(F.b.getb()) - z2.geta().Mul(y.b.geta()) - } - if F.stype != FP_SPARSE { - z2.geta().copy(F.b.geta()) - z2.geta().Mul(y.b.getb()) - } - z2.times_i() - } else { - z2.copy(F.b) - z2.Mul(y.b) - } - } else { - z2.copy(F.b) - z2.Mul(y.b) - } - t0 := NewFP16copy(F.a) - t1 := NewFP16copy(y.a) - t0.Add(F.b) + z2.copy(F.b) + z2.Mul(y.b, mem) + t0 := NewFP16copy(F.a, mem) + t1 := NewFP16copy(y.a, mem) + t0.Add(F.b, mem) t0.norm() - t1.Add(y.b) + t1.Add(y.b, mem) t1.norm() z1.copy(t0) - z1.Mul(t1) + z1.Mul(t1, mem) t0.copy(F.b) - t0.Add(F.c) + t0.Add(F.c, mem) t0.norm() t1.copy(y.b) - t1.Add(y.c) + t1.Add(y.c, mem) t1.norm() z3.copy(t0) - z3.Mul(t1) + z3.Mul(t1, mem) t0.copy(z0) - t0.Neg() + t0.Neg(mem) t1.copy(z2) - t1.Neg() + t1.Neg(mem) - z1.Add(t0) + z1.Add(t0, mem) F.b.copy(z1) - F.b.Add(t1) + F.b.Add(t1, mem) - z3.Add(t1) - z2.Add(t0) + z3.Add(t1, mem) + z2.Add(t0, mem) t0.copy(F.a) - t0.Add(F.c) + t0.Add(F.c, mem) t0.norm() t1.copy(y.a) - t1.Add(y.c) + t1.Add(y.c, mem) t1.norm() - t0.Mul(t1) - z2.Add(t0) + t0.Mul(t1, mem) + z2.Add(t0, mem) - if SEXTIC_TWIST == D_TYPE { - if y.stype == FP_SPARSE || F.stype == FP_SPARSE { - t0.geta().copy(F.c.geta()) - t0.geta().Mul(y.c.geta()) - t0.getb().zero() - if y.stype != FP_SPARSE { - t0.getb().copy(F.c.geta()) - t0.getb().Mul(y.c.getb()) - } - if F.stype != FP_SPARSE { - t0.getb().copy(F.c.getb()) - t0.getb().Mul(y.c.geta()) - } - } else { - t0.copy(F.c) - t0.Mul(y.c) + if y.stype == FP_SPARSE || F.stype == FP_SPARSE { + t0.geta().copy(F.c.geta()) + t0.geta().Mul(y.c.geta(), mem) + t0.getb().zero() + if y.stype != FP_SPARSE { + t0.getb().copy(F.c.geta()) + t0.getb().Mul(y.c.getb(), mem) + } + if F.stype != FP_SPARSE { + t0.getb().copy(F.c.getb()) + t0.getb().Mul(y.c.geta(), mem) } } else { t0.copy(F.c) - t0.Mul(y.c) + t0.Mul(y.c, mem) } t1.copy(t0) - t1.Neg() + t1.Neg(mem) F.c.copy(z2) - F.c.Add(t1) - z3.Add(t1) - t0.times_i() - F.b.Add(t0) + F.c.Add(t1, mem) + z3.Add(t1, mem) + t0.times_i(mem) + F.b.Add(t0, mem) z3.norm() - z3.times_i() + z3.times_i(mem) F.a.copy(z0) - F.a.Add(z3) + F.a.Add(z3, mem) } else { if F.stype == FP_SPARSER || F.stype == FP_SPARSEST { - F.smul(y) + F.smul(y, mem) return } - if SEXTIC_TWIST == D_TYPE { // dense by sparser - 13m - z0 := NewFP16copy(F.a) - z2 := NewFP16copy(F.b) - z3 := NewFP16copy(F.b) - t0 := NewFP16() - t1 := NewFP16copy(y.a) - z0.Mul(y.a) + z0 := NewFP16copy(F.a, mem) + z2 := NewFP16copy(F.b, mem) + z3 := NewFP16copy(F.b, mem) + t0 := NewFP16(mem) + t1 := NewFP16copy(y.a, mem) + z0.Mul(y.a, mem) - if y.stype == FP_SPARSEST { - z2.tmul(y.b.a.a.a.a) - } else { - z2.pmul(y.b.geta()) - } - F.b.Add(F.a) - t1.geta().Add(y.b.geta()) - - t1.norm() - F.b.norm() - F.b.Mul(t1) - z3.Add(F.c) - z3.norm() - - if y.stype == FP_SPARSEST { - z3.tmul(y.b.a.a.a.a) - } else { - z3.pmul(y.b.geta()) - } - - t0.copy(z0) - t0.Neg() - t1.copy(z2) - t1.Neg() - - F.b.Add(t0) - - F.b.Add(t1) - z3.Add(t1) - z2.Add(t0) - - t0.copy(F.a) - t0.Add(F.c) - t0.norm() - z3.norm() - t0.Mul(y.a) - F.c.copy(z2) - F.c.Add(t0) - - z3.times_i() - F.a.copy(z0) - F.a.Add(z3) + if y.stype == FP_SPARSEST { + z2.tmul(y.b.a.a.a.a, mem) + } else { + z2.pmul(y.b.geta(), mem) } - if SEXTIC_TWIST == M_TYPE { - z0 := NewFP16copy(F.a) - z1 := NewFP16() - z2 := NewFP16() - z3 := NewFP16() - t0 := NewFP16copy(F.a) - t1 := NewFP16() + F.b.Add(F.a, mem) + t1.geta().Add(y.b.geta(), mem) - z0.Mul(y.a) - t0.Add(F.b) - t0.norm() + t1.norm() + F.b.norm() + F.b.Mul(t1, mem) + z3.Add(F.c, mem) + z3.norm() - z1.copy(t0) - z1.Mul(y.a) - t0.copy(F.b) - t0.Add(F.c) - t0.norm() - - z3.copy(t0) - - if y.stype == FP_SPARSEST { - z3.tmul(y.c.b.a.a.a) - } else { - z3.pmul(y.c.getb()) - } - z3.times_i() - - t0.copy(z0) - t0.Neg() - z1.Add(t0) - F.b.copy(z1) - z2.copy(t0) - - t0.copy(F.a) - t0.Add(F.c) - t0.norm() - t1.copy(y.a) - t1.Add(y.c) - t1.norm() - - t0.Mul(t1) - z2.Add(t0) - t0.copy(F.c) - - if y.stype == FP_SPARSEST { - t0.tmul(y.c.b.a.a.a) - } else { - t0.pmul(y.c.getb()) - } - t0.times_i() - t1.copy(t0) - t1.Neg() - - F.c.copy(z2) - F.c.Add(t1) - z3.Add(t1) - t0.times_i() - F.b.Add(t0) - z3.norm() - z3.times_i() - F.a.copy(z0) - F.a.Add(z3) + if y.stype == FP_SPARSEST { + z3.tmul(y.b.a.a.a.a, mem) + } else { + z3.pmul(y.b.geta(), mem) } + + t0.copy(z0) + t0.Neg(mem) + t1.copy(z2) + t1.Neg(mem) + + F.b.Add(t0, mem) + + F.b.Add(t1, mem) + z3.Add(t1, mem) + z2.Add(t0, mem) + + t0.copy(F.a) + t0.Add(F.c, mem) + t0.norm() + z3.norm() + t0.Mul(y.a, mem) + F.c.copy(z2) + F.c.Add(t0, mem) + + z3.times_i(mem) + F.a.copy(z0) + F.a.Add(z3, mem) } F.stype = FP_DENSE F.norm() } /* Special case of multiplication arises from special form of ATE pairing line function */ -func (F *FP48) smul(y *FP48) { - if SEXTIC_TWIST == D_TYPE { - w1 := NewFP8copy(F.a.geta()) - w2 := NewFP8copy(F.a.getb()) - var w3 *FP8 +func (F *FP48) smul(y *FP48, mem *arena.Arena) { + w1 := NewFP8copy(F.a.geta(), mem) + w2 := NewFP8copy(F.a.getb(), mem) + var w3 *FP8 - w1.Mul(y.a.geta()) - w2.Mul(y.a.getb()) + w1.Mul(y.a.geta(), mem) + w2.Mul(y.a.getb(), mem) - if y.stype == FP_SPARSEST || F.stype == FP_SPARSEST { - if y.stype == FP_SPARSEST && F.stype == FP_SPARSEST { - t := NewFPcopy(F.b.a.a.a.a) - t.Mul(y.b.a.a.a.a) - w3 = NewFP8fp(t) - } else { - if y.stype != FP_SPARSEST { - w3 = NewFP8copy(y.b.geta()) - w3.tmul(F.b.a.a.a.a) - } else { - w3 = NewFP8copy(F.b.geta()) - w3.tmul(y.b.a.a.a.a) - } - } + if y.stype == FP_SPARSEST || F.stype == FP_SPARSEST { + if y.stype == FP_SPARSEST && F.stype == FP_SPARSEST { + t := NewFPcopy(F.b.a.a.a.a, mem) + t.Mul(y.b.a.a.a.a, mem) + w3 = NewFP8fp(t, mem) } else { - w3 = NewFP8copy(F.b.geta()) - w3.Mul(y.b.geta()) + if y.stype != FP_SPARSEST { + w3 = NewFP8copy(y.b.geta(), mem) + w3.tmul(F.b.a.a.a.a, mem) + } else { + w3 = NewFP8copy(F.b.geta(), mem) + w3.tmul(y.b.a.a.a.a, mem) + } } - ta := NewFP8copy(F.a.geta()) - tb := NewFP8copy(y.a.geta()) - ta.Add(F.a.getb()) - ta.norm() - tb.Add(y.a.getb()) - tb.norm() - tc := NewFP8copy(ta) - tc.Mul(tb) - t := NewFP8copy(w1) - t.Add(w2) - t.Neg() - tc.Add(t) - - ta.copy(F.a.geta()) - ta.Add(F.b.geta()) - ta.norm() - tb.copy(y.a.geta()) - tb.Add(y.b.geta()) - tb.norm() - td := NewFP8copy(ta) - td.Mul(tb) - t.copy(w1) - t.Add(w3) - t.Neg() - td.Add(t) - - ta.copy(F.a.getb()) - ta.Add(F.b.geta()) - ta.norm() - tb.copy(y.a.getb()) - tb.Add(y.b.geta()) - tb.norm() - te := NewFP8copy(ta) - te.Mul(tb) - t.copy(w2) - t.Add(w3) - t.Neg() - te.Add(t) - - w2.times_i() - w1.Add(w2) - - F.a.geta().copy(w1) - F.a.getb().copy(tc) - F.b.geta().copy(td) - F.b.getb().copy(te) - F.c.geta().copy(w3) - F.c.getb().zero() - - F.a.norm() - F.b.norm() } else { - w1 := NewFP8copy(F.a.geta()) - w2 := NewFP8copy(F.a.getb()) - var w3 *FP8 - - w1.Mul(y.a.geta()) - w2.Mul(y.a.getb()) - - if y.stype == FP_SPARSEST || F.stype == FP_SPARSEST { - if y.stype == FP_SPARSEST && F.stype == FP_SPARSEST { - t := NewFPcopy(F.c.b.a.a.a) - t.Mul(y.c.b.a.a.a) - w3 = NewFP8fp(t) - } else { - if y.stype != FP_SPARSEST { - w3 = NewFP8copy(y.c.getb()) - w3.tmul(F.c.b.a.a.a) - } else { - w3 = NewFP8copy(F.c.getb()) - w3.tmul(y.c.b.a.a.a) - } - } - } else { - w3 = NewFP8copy(F.c.getb()) - w3.Mul(y.c.getb()) - } - - ta := NewFP8copy(F.a.geta()) - tb := NewFP8copy(y.a.geta()) - ta.Add(F.a.getb()) - ta.norm() - tb.Add(y.a.getb()) - tb.norm() - tc := NewFP8copy(ta) - tc.Mul(tb) - t := NewFP8copy(w1) - t.Add(w2) - t.Neg() - tc.Add(t) - - ta.copy(F.a.geta()) - ta.Add(F.c.getb()) - ta.norm() - tb.copy(y.a.geta()) - tb.Add(y.c.getb()) - tb.norm() - td := NewFP8copy(ta) - td.Mul(tb) - t.copy(w1) - t.Add(w3) - t.Neg() - td.Add(t) - - ta.copy(F.a.getb()) - ta.Add(F.c.getb()) - ta.norm() - tb.copy(y.a.getb()) - tb.Add(y.c.getb()) - tb.norm() - te := NewFP8copy(ta) - te.Mul(tb) - t.copy(w2) - t.Add(w3) - t.Neg() - te.Add(t) - - w2.times_i() - w1.Add(w2) - F.a.geta().copy(w1) - F.a.getb().copy(tc) - - w3.times_i() - w3.norm() - F.b.geta().zero() - F.b.getb().copy(w3) - - te.norm() - te.times_i() - F.c.geta().copy(te) - F.c.getb().copy(td) - - F.a.norm() - F.c.norm() - + w3 = NewFP8copy(F.b.geta(), mem) + w3.Mul(y.b.geta(), mem) } + ta := NewFP8copy(F.a.geta(), mem) + tb := NewFP8copy(y.a.geta(), mem) + ta.Add(F.a.getb(), mem) + ta.norm() + tb.Add(y.a.getb(), mem) + tb.norm() + tc := NewFP8copy(ta, mem) + tc.Mul(tb, mem) + t := NewFP8copy(w1, mem) + t.Add(w2, mem) + t.Neg(mem) + tc.Add(t, mem) + + ta.copy(F.a.geta()) + ta.Add(F.b.geta(), mem) + ta.norm() + tb.copy(y.a.geta()) + tb.Add(y.b.geta(), mem) + tb.norm() + td := NewFP8copy(ta, mem) + td.Mul(tb, mem) + t.copy(w1) + t.Add(w3, mem) + t.Neg(mem) + td.Add(t, mem) + + ta.copy(F.a.getb()) + ta.Add(F.b.geta(), mem) + ta.norm() + tb.copy(y.a.getb()) + tb.Add(y.b.geta(), mem) + tb.norm() + te := NewFP8copy(ta, mem) + te.Mul(tb, mem) + t.copy(w2) + t.Add(w3, mem) + t.Neg(mem) + te.Add(t, mem) + + w2.times_i(mem) + w1.Add(w2, mem) + + F.a.geta().copy(w1) + F.a.getb().copy(tc) + F.b.geta().copy(td) + F.b.getb().copy(te) + F.c.geta().copy(w3) + F.c.getb().zero() + + F.a.norm() + F.b.norm() F.stype = FP_SPARSE } /* this=1/this */ -func (F *FP48) Invert() { - f0 := NewFP16copy(F.a) - f1 := NewFP16copy(F.b) - f2 := NewFP16copy(F.a) - f3 := NewFP16() +func (F *FP48) Invert(mem *arena.Arena) { + f0 := NewFP16copy(F.a, mem) + f1 := NewFP16copy(F.b, mem) + f2 := NewFP16copy(F.a, mem) + f3 := NewFP16(mem) //F.norm() - f0.Sqr() - f1.Mul(F.c) - f1.times_i() - f0.Sub(f1) + f0.Sqr(mem) + f1.Mul(F.c, mem) + f1.times_i(mem) + f0.Sub(f1, mem) f0.norm() f1.copy(F.c) - f1.Sqr() - f1.times_i() - f2.Mul(F.b) - f1.Sub(f2) + f1.Sqr(mem) + f1.times_i(mem) + f2.Mul(F.b, mem) + f1.Sub(f2, mem) f1.norm() f2.copy(F.b) - f2.Sqr() + f2.Sqr(mem) f3.copy(F.a) - f3.Mul(F.c) - f2.Sub(f3) + f3.Mul(F.c, mem) + f2.Sub(f3, mem) f2.norm() f3.copy(F.b) - f3.Mul(f2) - f3.times_i() - F.a.Mul(f0) - f3.Add(F.a) - F.c.Mul(f1) - F.c.times_i() + f3.Mul(f2, mem) + f3.times_i(mem) + F.a.Mul(f0, mem) + f3.Add(F.a, mem) + F.c.Mul(f1, mem) + F.c.times_i(mem) - f3.Add(F.c) + f3.Add(F.c, mem) f3.norm() - f3.Invert() + f3.Invert(mem) F.a.copy(f0) - F.a.Mul(f3) + F.a.Mul(f3, mem) F.b.copy(f1) - F.b.Mul(f3) + F.b.Mul(f3, mem) F.c.copy(f2) - F.c.Mul(f3) + F.c.Mul(f3, mem) F.stype = FP_DENSE } /* this=this^p using Frobenius */ -func (F *FP48) frob(f *FP2, n int) { - f2 := NewFP2copy(f) - f3 := NewFP2copy(f) +func (F *FP48) frob(f *FP2, n int, mem *arena.Arena) { + f2 := NewFP2copy(f, mem) + f3 := NewFP2copy(f, mem) - f2.Sqr() - f3.Mul(f2) + f2.Sqr(mem) + f3.Mul(f2, mem) - f3.Mul_ip() + f3.Mul_ip(mem) f3.norm() - f3.Mul_ip() + f3.Mul_ip(mem) f3.norm() for i := 0; i < n; i++ { - F.a.frob(f3) - F.b.frob(f3) - F.c.frob(f3) + F.a.frob(f3, mem) + F.b.frob(f3, mem) + F.c.frob(f3, mem) - F.b.qmul(f) - F.b.times_i4() - F.b.times_i2() - F.c.qmul(f2) - F.c.times_i4() - F.c.times_i4() - F.c.times_i4() + F.b.qmul(f, mem) + F.b.times_i4(mem) + F.b.times_i2(mem) + F.c.qmul(f2, mem) + F.c.times_i4(mem) + F.c.times_i4(mem) + F.c.times_i4(mem) } F.stype = FP_DENSE } /* trace function */ -func (F *FP48) trace() *FP16 { - t := NewFP16() +func (F *FP48) trace(mem *arena.Arena) *FP16 { + t := NewFP16(mem) t.copy(F.a) - t.imul(3) - t.reduce() + t.imul(3, mem) + t.reduce(mem) return t } @@ -856,7 +717,7 @@ func FP48_fromBytes(w []byte) *FP48 { t[i] = w[i+2*MB] } a := FP16_fromBytes(t[:]) - return NewFP48fp16s(a, b, c) + return NewFP48fp16s(a, b, c, nil) } /* convert this to byte array */ @@ -883,48 +744,48 @@ func (F *FP48) ToString() string { } /* this=this^e */ -func (F *FP48) Pow(e *BIG) *FP48 { - sf := NewFP48copy(F) +func (F *FP48) Pow(e *BIG, mem *arena.Arena) *FP48 { + sf := NewFP48copy(F, mem) sf.norm() - e1 := NewBIGcopy(e) + e1 := NewBIGcopy(e, mem) e1.norm() - e3 := NewBIGcopy(e1) + e3 := NewBIGcopy(e1, mem) e3.pmul(3) e3.norm() - w := NewFP48copy(sf) + w := NewFP48copy(sf, mem) if e3.IsZero() { w.one() return w } nb := e3.nbits() for i := nb - 2; i >= 1; i-- { - w.uSqr() + w.uSqr(mem) bt := e3.bit(i) - e1.bit(i) if bt == 1 { - w.Mul(sf) + w.Mul(sf, mem) } if bt == -1 { - sf.conj() - w.Mul(sf) - sf.conj() + sf.conj(mem) + w.Mul(sf, mem) + sf.conj(mem) } } - w.reduce() + w.reduce(mem) return w } /* constant time powering by small integer of max length bts */ -func (F *FP48) pinpow(e int, bts int) { +func (F *FP48) pinpow(e int, bts int, mem *arena.Arena) { var R []*FP48 - R = append(R, NewFP48int(1)) - R = append(R, NewFP48copy(F)) + R = append(R, NewFP48int(1, mem)) + R = append(R, NewFP48copy(F, mem)) for i := bts - 1; i >= 0; i-- { b := (e >> uint(i)) & 1 - R[1-b].Mul(R[b]) - R[b].uSqr() + R[1-b].Mul(R[b], mem) + R[b].uSqr(mem) } F.Copy(R[0]) } @@ -985,79 +846,79 @@ func pow16(q []*FP48, u []*BIG) *FP48 { var w4 [NLEN*int(BASEBITS) + 1]int8 var s4 [NLEN*int(BASEBITS) + 1]int8 var t []*BIG - r := NewFP48() - p := NewFP48() - mt := NewBIGint(0) + r := NewFP48(nil) + p := NewFP48(nil) + mt := NewBIGint(0, nil) var bt int8 var k int for i := 0; i < 16; i++ { - t = append(t, NewBIGcopy(u[i])) + t = append(t, NewBIGcopy(u[i], nil)) } - g1 = append(g1, NewFP48copy(q[0])) // q[0] - g1 = append(g1, NewFP48copy(g1[0])) - g1[1].Mul(q[1]) // q[0].q[1] - g1 = append(g1, NewFP48copy(g1[0])) - g1[2].Mul(q[2]) // q[0].q[2] - g1 = append(g1, NewFP48copy(g1[1])) - g1[3].Mul(q[2]) // q[0].q[1].q[2] - g1 = append(g1, NewFP48copy(g1[0])) - g1[4].Mul(q[3]) // q[0].q[3] - g1 = append(g1, NewFP48copy(g1[1])) - g1[5].Mul(q[3]) // q[0].q[1].q[3] - g1 = append(g1, NewFP48copy(g1[2])) - g1[6].Mul(q[3]) // q[0].q[2].q[3] - g1 = append(g1, NewFP48copy(g1[3])) - g1[7].Mul(q[3]) // q[0].q[1].q[2].q[3] + g1 = append(g1, NewFP48copy(q[0], nil)) // q[0] + g1 = append(g1, NewFP48copy(g1[0], nil)) + g1[1].Mul(q[1], nil) // q[0].q[1] + g1 = append(g1, NewFP48copy(g1[0], nil)) + g1[2].Mul(q[2], nil) // q[0].q[2] + g1 = append(g1, NewFP48copy(g1[1], nil)) + g1[3].Mul(q[2], nil) // q[0].q[1].q[2] + g1 = append(g1, NewFP48copy(g1[0], nil)) + g1[4].Mul(q[3], nil) // q[0].q[3] + g1 = append(g1, NewFP48copy(g1[1], nil)) + g1[5].Mul(q[3], nil) // q[0].q[1].q[3] + g1 = append(g1, NewFP48copy(g1[2], nil)) + g1[6].Mul(q[3], nil) // q[0].q[2].q[3] + g1 = append(g1, NewFP48copy(g1[3], nil)) + g1[7].Mul(q[3], nil) // q[0].q[1].q[2].q[3] - g2 = append(g2, NewFP48copy(q[4])) // q[0] - g2 = append(g2, NewFP48copy(g2[0])) - g2[1].Mul(q[5]) // q[0].q[1] - g2 = append(g2, NewFP48copy(g2[0])) - g2[2].Mul(q[6]) // q[0].q[2] - g2 = append(g2, NewFP48copy(g2[1])) - g2[3].Mul(q[6]) // q[0].q[1].q[2] - g2 = append(g2, NewFP48copy(g2[0])) - g2[4].Mul(q[7]) // q[0].q[3] - g2 = append(g2, NewFP48copy(g2[1])) - g2[5].Mul(q[7]) // q[0].q[1].q[3] - g2 = append(g2, NewFP48copy(g2[2])) - g2[6].Mul(q[7]) // q[0].q[2].q[3] - g2 = append(g2, NewFP48copy(g2[3])) - g2[7].Mul(q[7]) // q[0].q[1].q[2].q[3] + g2 = append(g2, NewFP48copy(q[4], nil)) // q[0] + g2 = append(g2, NewFP48copy(g2[0], nil)) + g2[1].Mul(q[5], nil) // q[0].q[1] + g2 = append(g2, NewFP48copy(g2[0], nil)) + g2[2].Mul(q[6], nil) // q[0].q[2] + g2 = append(g2, NewFP48copy(g2[1], nil)) + g2[3].Mul(q[6], nil) // q[0].q[1].q[2] + g2 = append(g2, NewFP48copy(g2[0], nil)) + g2[4].Mul(q[7], nil) // q[0].q[3] + g2 = append(g2, NewFP48copy(g2[1], nil)) + g2[5].Mul(q[7], nil) // q[0].q[1].q[3] + g2 = append(g2, NewFP48copy(g2[2], nil)) + g2[6].Mul(q[7], nil) // q[0].q[2].q[3] + g2 = append(g2, NewFP48copy(g2[3], nil)) + g2[7].Mul(q[7], nil) // q[0].q[1].q[2].q[3] - g3 = append(g3, NewFP48copy(q[8])) // q[0] - g3 = append(g3, NewFP48copy(g3[0])) - g3[1].Mul(q[9]) // q[0].q[1] - g3 = append(g3, NewFP48copy(g3[0])) - g3[2].Mul(q[10]) // q[0].q[2] - g3 = append(g3, NewFP48copy(g3[1])) - g3[3].Mul(q[10]) // q[0].q[1].q[2] - g3 = append(g3, NewFP48copy(g3[0])) - g3[4].Mul(q[11]) // q[0].q[3] - g3 = append(g3, NewFP48copy(g3[1])) - g3[5].Mul(q[11]) // q[0].q[1].q[3] - g3 = append(g3, NewFP48copy(g3[2])) - g3[6].Mul(q[11]) // q[0].q[2].q[3] - g3 = append(g3, NewFP48copy(g3[3])) - g3[7].Mul(q[11]) // q[0].q[1].q[2].q[3] + g3 = append(g3, NewFP48copy(q[8], nil)) // q[0] + g3 = append(g3, NewFP48copy(g3[0], nil)) + g3[1].Mul(q[9], nil) // q[0].q[1] + g3 = append(g3, NewFP48copy(g3[0], nil)) + g3[2].Mul(q[10], nil) // q[0].q[2] + g3 = append(g3, NewFP48copy(g3[1], nil)) + g3[3].Mul(q[10], nil) // q[0].q[1].q[2] + g3 = append(g3, NewFP48copy(g3[0], nil)) + g3[4].Mul(q[11], nil) // q[0].q[3] + g3 = append(g3, NewFP48copy(g3[1], nil)) + g3[5].Mul(q[11], nil) // q[0].q[1].q[3] + g3 = append(g3, NewFP48copy(g3[2], nil)) + g3[6].Mul(q[11], nil) // q[0].q[2].q[3] + g3 = append(g3, NewFP48copy(g3[3], nil)) + g3[7].Mul(q[11], nil) // q[0].q[1].q[2].q[3] - g4 = append(g4, NewFP48copy(q[12])) // q[0] - g4 = append(g4, NewFP48copy(g4[0])) - g4[1].Mul(q[13]) // q[0].q[1] - g4 = append(g4, NewFP48copy(g4[0])) - g4[2].Mul(q[14]) // q[0].q[2] - g4 = append(g4, NewFP48copy(g4[1])) - g4[3].Mul(q[14]) // q[0].q[1].q[2] - g4 = append(g4, NewFP48copy(g4[0])) - g4[4].Mul(q[15]) // q[0].q[3] - g4 = append(g4, NewFP48copy(g4[1])) - g4[5].Mul(q[15]) // q[0].q[1].q[3] - g4 = append(g4, NewFP48copy(g4[2])) - g4[6].Mul(q[15]) // q[0].q[2].q[3] - g4 = append(g4, NewFP48copy(g4[3])) - g4[7].Mul(q[15]) // q[0].q[1].q[2].q[3] + g4 = append(g4, NewFP48copy(q[12], nil)) // q[0] + g4 = append(g4, NewFP48copy(g4[0], nil)) + g4[1].Mul(q[13], nil) // q[0].q[1] + g4 = append(g4, NewFP48copy(g4[0], nil)) + g4[2].Mul(q[14], nil) // q[0].q[2] + g4 = append(g4, NewFP48copy(g4[1], nil)) + g4[3].Mul(q[14], nil) // q[0].q[1].q[2] + g4 = append(g4, NewFP48copy(g4[0], nil)) + g4[4].Mul(q[15], nil) // q[0].q[3] + g4 = append(g4, NewFP48copy(g4[1], nil)) + g4[5].Mul(q[15], nil) // q[0].q[1].q[3] + g4 = append(g4, NewFP48copy(g4[2], nil)) + g4[6].Mul(q[15], nil) // q[0].q[2].q[3] + g4 = append(g4, NewFP48copy(g4[3], nil)) + g4[7].Mul(q[15], nil) // q[0].q[1].q[2].q[3] // Make them odd pb1 := 1 - t[0].parity() @@ -1149,41 +1010,41 @@ func pow16(q []*FP48, u []*BIG) *FP48 { // Main loop p.selector(g1, int32(2*w1[nb-1]+1)) r.selector(g2, int32(2*w2[nb-1]+1)) - p.Mul(r) + p.Mul(r, nil) r.selector(g3, int32(2*w3[nb-1]+1)) - p.Mul(r) + p.Mul(r, nil) r.selector(g4, int32(2*w4[nb-1]+1)) - p.Mul(r) + p.Mul(r, nil) for i := nb - 2; i >= 0; i-- { - p.uSqr() + p.uSqr(nil) r.selector(g1, int32(2*w1[i]+s1[i])) - p.Mul(r) + p.Mul(r, nil) r.selector(g2, int32(2*w2[i]+s2[i])) - p.Mul(r) + p.Mul(r, nil) r.selector(g3, int32(2*w3[i]+s3[i])) - p.Mul(r) + p.Mul(r, nil) r.selector(g4, int32(2*w4[i]+s4[i])) - p.Mul(r) + p.Mul(r, nil) } // apply correction r.Copy(q[0]) - r.conj() - r.Mul(p) + r.conj(nil) + r.Mul(p, nil) p.cmove(r, pb1) r.Copy(q[4]) - r.conj() - r.Mul(p) + r.conj(nil) + r.Mul(p, nil) p.cmove(r, pb2) r.Copy(q[8]) - r.conj() - r.Mul(p) + r.conj(nil) + r.Mul(p, nil) p.cmove(r, pb3) r.Copy(q[12]) - r.conj() - r.Mul(p) + r.conj(nil) + r.Mul(p, nil) p.cmove(r, pb4) - p.reduce() + p.reduce(nil) return p } diff --git a/nekryptology/pkg/core/curves/native/bls48581/fp8.go b/nekryptology/pkg/core/curves/native/bls48581/fp8.go index 4b94ff8..eed3355 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/fp8.go +++ b/nekryptology/pkg/core/curves/native/bls48581/fp8.go @@ -23,7 +23,11 @@ package bls48581 -import "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" +import ( + "arena" + + "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" +) //import "fmt" @@ -32,66 +36,115 @@ type FP8 struct { b *FP4 } -func NewFP8() *FP8 { - F := new(FP8) - F.a = NewFP4() - F.b = NewFP4() - return F +func NewFP8(mem *arena.Arena) *FP8 { + if mem != nil { + F := arena.New[FP8](mem) + F.a = NewFP4(mem) + F.b = NewFP4(mem) + return F + } else { + F := new(FP8) + F.a = NewFP4(nil) + F.b = NewFP4(nil) + return F + } } /* Constructors */ -func NewFP8int(a int) *FP8 { - F := new(FP8) - F.a = NewFP4int(a) - F.b = NewFP4() - return F +func NewFP8int(a int, mem *arena.Arena) *FP8 { + if mem != nil { + F := arena.New[FP8](mem) + F.a = NewFP4int(a, mem) + F.b = NewFP4(mem) + return F + } else { + F := new(FP8) + F.a = NewFP4int(a, nil) + F.b = NewFP4(nil) + return F + } } /* Constructors */ -func NewFP8ints(a int, b int) *FP8 { - F := new(FP8) - F.a = NewFP4int(a) - F.b = NewFP4int(b) - return F +func NewFP8ints(a int, b int, mem *arena.Arena) *FP8 { + if mem != nil { + F := arena.New[FP8](mem) + F.a = NewFP4int(a, mem) + F.b = NewFP4int(b, mem) + return F + } else { + F := new(FP8) + F.a = NewFP4int(a, nil) + F.b = NewFP4int(b, nil) + return F + } } -func NewFP8copy(x *FP8) *FP8 { - F := new(FP8) - F.a = NewFP4copy(x.a) - F.b = NewFP4copy(x.b) - return F +func NewFP8copy(x *FP8, mem *arena.Arena) *FP8 { + if mem != nil { + F := arena.New[FP8](mem) + F.a = NewFP4copy(x.a, mem) + F.b = NewFP4copy(x.b, mem) + return F + } else { + F := new(FP8) + F.a = NewFP4copy(x.a, nil) + F.b = NewFP4copy(x.b, nil) + return F + } } -func NewFP8fp4s(c *FP4, d *FP4) *FP8 { - F := new(FP8) - F.a = NewFP4copy(c) - F.b = NewFP4copy(d) - return F +func NewFP8fp4s(c *FP4, d *FP4, mem *arena.Arena) *FP8 { + if mem != nil { + F := arena.New[FP8](mem) + F.a = NewFP4copy(c, mem) + F.b = NewFP4copy(d, mem) + return F + } else { + F := new(FP8) + F.a = NewFP4copy(c, nil) + F.b = NewFP4copy(d, nil) + return F + } } -func NewFP8fp4(c *FP4) *FP8 { - F := new(FP8) - F.a = NewFP4copy(c) - F.b = NewFP4() - return F +func NewFP8fp4(c *FP4, mem *arena.Arena) *FP8 { + if mem != nil { + F := arena.New[FP8](mem) + F.a = NewFP4copy(c, mem) + F.b = NewFP4(mem) + return F + } else { + F := new(FP8) + F.a = NewFP4copy(c, nil) + F.b = NewFP4(nil) + return F + } } -func NewFP8fp(c *FP) *FP8 { - F := new(FP8) - F.a = NewFP4fp(c) - F.b = NewFP4() - return F +func NewFP8fp(c *FP, mem *arena.Arena) *FP8 { + if mem != nil { + F := arena.New[FP8](mem) + F.a = NewFP4fp(c, mem) + F.b = NewFP4(mem) + return F + } else { + F := new(FP8) + F.a = NewFP4fp(c, nil) + F.b = NewFP4(nil) + return F + } } func NewFP8rand(rng *ext.RAND) *FP8 { - F := NewFP8fp4s(NewFP4rand(rng), NewFP4rand(rng)) + F := NewFP8fp4s(NewFP4rand(rng), NewFP4rand(rng), nil) return F } /* reduce all components of this mod Modulus */ -func (F *FP8) reduce() { - F.a.reduce() - F.b.reduce() +func (F *FP8) reduce(mem *arena.Arena) { + F.a.reduce(mem) + F.b.reduce(mem) } /* normalise all components of this mod Modulus */ @@ -101,12 +154,12 @@ func (F *FP8) norm() { } /* test this==0 ? */ -func (F *FP8) IsZero() bool { - return F.a.IsZero() && F.b.IsZero() +func (F *FP8) IsZero(mem *arena.Arena) bool { + return F.a.IsZero(mem) && F.b.IsZero(mem) } func (F *FP8) islarger() int { - if F.IsZero() { + if F.IsZero(nil) { return 0 } cmp := F.b.islarger() @@ -140,7 +193,7 @@ func FP8_fromBytes(bf []byte) *FP8 { t[i] = bf[i+MB] } ta := FP4_fromBytes(t[:]) - return NewFP8fp4s(ta, tb) + return NewFP8fp4s(ta, tb, nil) } /* Conditional move */ @@ -151,13 +204,15 @@ func (F *FP8) cmove(g *FP8, d int) { /* test this==1 ? */ func (F *FP8) isunity() bool { - one := NewFP4int(1) - return F.a.Equals(one) && F.b.IsZero() + mem := arena.NewArena() + defer mem.Free() + one := NewFP4int(1, mem) + return F.a.Equals(one) && F.b.IsZero(mem) } /* test is w real? That is in a+ib test b is zero */ func (F *FP8) isreal() bool { - return F.b.IsZero() + return F.b.IsZero(nil) } /* extract real part a */ @@ -198,12 +253,12 @@ func (F *FP8) one() { } /* Return sign */ -func (F *FP8) sign() int { - p1 := F.a.sign() - p2 := F.b.sign() +func (F *FP8) sign(mem *arena.Arena) int { + p1 := F.a.sign(mem) + p2 := F.b.sign(mem) var u int if BIG_ENDIAN_SIGN { - if F.b.IsZero() { + if F.b.IsZero(mem) { u = 1 } else { u = 0 @@ -211,7 +266,7 @@ func (F *FP8) sign() int { p2 ^= (p1 ^ p2) & u return p2 } else { - if F.a.IsZero() { + if F.a.IsZero(mem) { u = 1 } else { u = 0 @@ -222,137 +277,137 @@ func (F *FP8) sign() int { } /* set this=-this */ -func (F *FP8) Neg() { +func (F *FP8) Neg(mem *arena.Arena) { F.norm() - m := NewFP4copy(F.a) - t := NewFP4() - m.Add(F.b) - m.Neg() + m := NewFP4copy(F.a, mem) + t := NewFP4(mem) + m.Add(F.b, mem) + m.Neg(mem) t.copy(m) - t.Add(F.b) + t.Add(F.b, mem) F.b.copy(m) - F.b.Add(F.a) + F.b.Add(F.a, mem) F.a.copy(t) F.norm() } /* this=conjugate(this) */ -func (F *FP8) conj() { - F.b.Neg() +func (F *FP8) conj(mem *arena.Arena) { + F.b.Neg(mem) F.norm() } /* this=-conjugate(this) */ -func (F *FP8) nconj() { - F.a.Neg() +func (F *FP8) nconj(mem *arena.Arena) { + F.a.Neg(mem) F.norm() } /* this+=x */ -func (F *FP8) Add(x *FP8) { - F.a.Add(x.a) - F.b.Add(x.b) +func (F *FP8) Add(x *FP8, mem *arena.Arena) { + F.a.Add(x.a, mem) + F.b.Add(x.b, mem) } /* this-=x */ -func (F *FP8) Sub(x *FP8) { - m := NewFP8copy(x) - m.Neg() - F.Add(m) +func (F *FP8) Sub(x *FP8, mem *arena.Arena) { + m := NewFP8copy(x, mem) + m.Neg(mem) + F.Add(m, mem) } /* this-=x */ -func (F *FP8) rsub(x *FP8) { - F.Neg() - F.Add(x) +func (F *FP8) rsub(x *FP8, mem *arena.Arena) { + F.Neg(mem) + F.Add(x, mem) } /* this*=s where s is FP4 */ -func (F *FP8) pmul(s *FP4) { - F.a.Mul(s) - F.b.Mul(s) +func (F *FP8) pmul(s *FP4, mem *arena.Arena) { + F.a.Mul(s, mem) + F.b.Mul(s, mem) } /* this*=s where s is FP2 */ -func (F *FP8) qmul(s *FP2) { - F.a.pmul(s) - F.b.pmul(s) +func (F *FP8) qmul(s *FP2, mem *arena.Arena) { + F.a.pmul(s, mem) + F.b.pmul(s, mem) } /* this*=s where s is FP */ -func (F *FP8) tmul(s *FP) { - F.a.qmul(s) - F.b.qmul(s) +func (F *FP8) tmul(s *FP, mem *arena.Arena) { + F.a.qmul(s, mem) + F.b.qmul(s, mem) } /* this*=c where c is int */ -func (F *FP8) imul(c int) { - F.a.imul(c) - F.b.imul(c) +func (F *FP8) imul(c int, mem *arena.Arena) { + F.a.imul(c, mem) + F.b.imul(c, mem) } /* this*=this */ -func (F *FP8) Sqr() { - t1 := NewFP4copy(F.a) - t2 := NewFP4copy(F.b) - t3 := NewFP4copy(F.a) +func (F *FP8) Sqr(mem *arena.Arena) { + t1 := NewFP4copy(F.a, mem) + t2 := NewFP4copy(F.b, mem) + t3 := NewFP4copy(F.a, mem) - t3.Mul(F.b) - t1.Add(F.b) - t2.times_i() + t3.Mul(F.b, mem) + t1.Add(F.b, mem) + t2.times_i(mem) - t2.Add(F.a) + t2.Add(F.a, mem) t1.norm() t2.norm() F.a.copy(t1) - F.a.Mul(t2) + F.a.Mul(t2, mem) t2.copy(t3) - t2.times_i() - t2.Add(t3) + t2.times_i(mem) + t2.Add(t3, mem) t2.norm() - t2.Neg() - F.a.Add(t2) + t2.Neg(mem) + F.a.Add(t2, mem) F.b.copy(t3) - F.b.Add(t3) + F.b.Add(t3, mem) F.norm() } /* this*=y */ -func (F *FP8) Mul(y *FP8) { - t1 := NewFP4copy(F.a) - t2 := NewFP4copy(F.b) - t3 := NewFP4() - t4 := NewFP4copy(F.b) +func (F *FP8) Mul(y *FP8, mem *arena.Arena) { + t1 := NewFP4copy(F.a, mem) + t2 := NewFP4copy(F.b, mem) + t3 := NewFP4(mem) + t4 := NewFP4copy(F.b, mem) - t1.Mul(y.a) - t2.Mul(y.b) + t1.Mul(y.a, mem) + t2.Mul(y.b, mem) t3.copy(y.b) - t3.Add(y.a) - t4.Add(F.a) + t3.Add(y.a, mem) + t4.Add(F.a, mem) t3.norm() t4.norm() - t4.Mul(t3) + t4.Mul(t3, mem) t3.copy(t1) - t3.Neg() - t4.Add(t3) + t3.Neg(mem) + t4.Add(t3, mem) t4.norm() t3.copy(t2) - t3.Neg() + t3.Neg(mem) F.b.copy(t4) - F.b.Add(t3) + F.b.Add(t3, mem) - t2.times_i() + t2.times_i(mem) F.a.copy(t2) - F.a.Add(t1) + F.a.Add(t1, mem) F.norm() } @@ -363,55 +418,55 @@ func (F *FP8) toString() string { } /* this=1/this */ -func (F *FP8) Invert(h *FP) { - t1 := NewFP4copy(F.a) - t2 := NewFP4copy(F.b) +func (F *FP8) Invert(h *FP, mem *arena.Arena) { + t1 := NewFP4copy(F.a, mem) + t2 := NewFP4copy(F.b, mem) - t1.Sqr() - t2.Sqr() - t2.times_i() + t1.Sqr(mem) + t2.Sqr(mem) + t2.times_i(mem) t2.norm() - t1.Sub(t2) + t1.Sub(t2, mem) t1.norm() - t1.Invert(h) + t1.Invert(h, mem) - F.a.Mul(t1) - t1.Neg() + F.a.Mul(t1, mem) + t1.Neg(mem) t1.norm() - F.b.Mul(t1) + F.b.Mul(t1, mem) } /* this*=i where i = sqrt(sqrt(-1+sqrt(-1))) */ -func (F *FP8) times_i() { - s := NewFP4copy(F.b) - t := NewFP4copy(F.a) - s.times_i() +func (F *FP8) times_i(mem *arena.Arena) { + s := NewFP4copy(F.b, mem) + t := NewFP4copy(F.a, mem) + s.times_i(mem) F.a.copy(s) F.b.copy(t) F.norm() if TOWER == POSITOWER { - F.Neg() + F.Neg(mem) F.norm() } } -func (F *FP8) times_i2() { - F.a.times_i() - F.b.times_i() +func (F *FP8) times_i2(mem *arena.Arena) { + F.a.times_i(mem) + F.b.times_i(mem) } /* this=this^p using Frobenius */ -func (F *FP8) frob(f *FP2) { - ff := NewFP2copy(f) - ff.Sqr() - ff.Mul_ip() +func (F *FP8) frob(f *FP2, mem *arena.Arena) { + ff := NewFP2copy(f, mem) + ff.Sqr(mem) + ff.Mul_ip(mem) ff.norm() - F.a.frob(ff) - F.b.frob(ff) - F.b.pmul(f) - F.b.times_i() + F.a.frob(ff, mem) + F.b.frob(ff, mem) + F.b.pmul(f, mem) + F.b.times_i(mem) } /* this=this^e @@ -671,19 +726,19 @@ func (F *FP8) xtr_pow2(ck *FP8, ckml *FP8, ckm2l *FP8, a *BIG, b *BIG) *FP8 { } */ /* this/=2 */ -func (F *FP8) div2() { - F.a.div2() - F.b.div2() +func (F *FP8) div2(mem *arena.Arena) { + F.a.div2(mem) + F.b.div2(mem) } -func (F *FP8) div_i() { - u := NewFP4copy(F.a) - v := NewFP4copy(F.b) - u.div_i() +func (F *FP8) div_i(mem *arena.Arena) { + u := NewFP4copy(F.a, mem) + v := NewFP4copy(F.b, mem) + u.div_i(mem) F.a.copy(v) F.b.copy(u) if TOWER == POSITOWER { - F.Neg() + F.Neg(mem) F.norm() } } @@ -710,70 +765,72 @@ func (F *FP8) pow(b *BIG) { /* */ // Test for Quadratic Residue func (F *FP8) qr(h *FP) int { - c := NewFP8copy(F) - c.conj() - c.Mul(F) + mem := arena.NewArena() + defer mem.Free() + c := NewFP8copy(F, mem) + c.conj(mem) + c.Mul(F, mem) return c.a.qr(h) } // sqrt(a+ib) = sqrt(a+sqrt(a*a-n*b*b)/2)+ib/(2*sqrt(a+sqrt(a*a-n*b*b)/2)) -func (F *FP8) Sqrt(h *FP) { - if F.IsZero() { +func (F *FP8) Sqrt(h *FP, mem *arena.Arena) { + if F.IsZero(mem) { return } - a := NewFP4copy(F.a) - b := NewFP4() - s := NewFP4copy(F.b) - t := NewFP4copy(F.a) - hint := NewFP() + a := NewFP4copy(F.a, mem) + b := NewFP4(mem) + s := NewFP4copy(F.b, mem) + t := NewFP4copy(F.a, mem) + hint := NewFP(mem) - s.Sqr() - a.Sqr() - s.times_i() + s.Sqr(mem) + a.Sqr(mem) + s.times_i(mem) s.norm() - a.Sub(s) + a.Sub(s, mem) s.copy(a) s.norm() - s.Sqrt(h) + s.Sqrt(h, mem) a.copy(t) b.copy(t) - a.Add(s) + a.Add(s, mem) a.norm() - a.div2() + a.div2(mem) b.copy(F.b) - b.div2() + b.div2(mem) qr := a.qr(hint) // tweak hint - multiply old hint by Norm(1/Beta)^e where Beta is irreducible polynomial s.copy(a) - twk := NewFPbig(NewBIGints(TWK)) - twk.Mul(hint) - s.div_i() + twk := NewFPbig(NewBIGints(TWK, mem), mem) + twk.Mul(hint, mem) + s.div_i(mem) s.norm() a.cmove(s, 1-qr) hint.cmove(twk, 1-qr) F.a.copy(a) - F.a.Sqrt(hint) + F.a.Sqrt(hint, mem) s.copy(a) - s.Invert(hint) - s.Mul(F.a) + s.Invert(hint, mem) + s.Mul(F.a, mem) F.b.copy(s) - F.b.Mul(b) + F.b.Mul(b, mem) t.copy(F.a) F.a.cmove(F.b, 1-qr) F.b.cmove(t, 1-qr) - sgn := F.sign() - nr := NewFP8copy(F) - nr.Neg() + sgn := F.sign(mem) + nr := NewFP8copy(F, mem) + nr.Neg(mem) nr.norm() F.cmove(nr, sgn) } diff --git a/nekryptology/pkg/core/curves/native/bls48581/g1.go b/nekryptology/pkg/core/curves/native/bls48581/g1.go index 99227dc..9959b11 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/g1.go +++ b/nekryptology/pkg/core/curves/native/bls48581/g1.go @@ -19,6 +19,8 @@ package bls48581 +import "arena" + //import "fmt" /* Elliptic Curve Point Structure */ @@ -29,54 +31,59 @@ type ECP struct { } /* Constructors */ -func NewECP() *ECP { - E := new(ECP) - E.x = NewFP() - E.y = NewFPint(1) - if CURVETYPE == EDWARDS { - E.z = NewFPint(1) +func NewECP(mem *arena.Arena) *ECP { + var E *ECP + if mem != nil { + E = arena.New[ECP](mem) } else { - E.z = NewFP() + E = new(ECP) } + E.x = NewFP(mem) + E.y = NewFPint(1, mem) + E.z = NewFP(mem) return E } /* set (x,y) from two BIGs */ -func NewECPbigs(ix *BIG, iy *BIG) *ECP { - E := new(ECP) - E.x = NewFPbig(ix) - E.y = NewFPbig(iy) - E.z = NewFPint(1) - E.x.norm() - rhs := RHS(E.x) - - if CURVETYPE == MONTGOMERY { - if rhs.qr(nil) != 1 { - E.inf() - } +func NewECPbigs(ix *BIG, iy *BIG, mem *arena.Arena) *ECP { + var E *ECP + if mem != nil { + E = arena.New[ECP](mem) } else { - y2 := NewFPcopy(E.y) - y2.Sqr() - if !y2.Equals(rhs) { - E.inf() - } + E = new(ECP) + } + E.x = NewFPbig(ix, mem) + E.y = NewFPbig(iy, mem) + E.z = NewFPint(1, mem) + E.x.norm() + rhs := RHS(E.x, mem) + + y2 := NewFPcopy(E.y, mem) + y2.Sqr(mem) + if !y2.Equals(rhs) { + E.inf() } return E } /* set (x,y) from BIG and a bit */ -func NewECPbigint(ix *BIG, s int) *ECP { - E := new(ECP) - E.x = NewFPbig(ix) - E.y = NewFP() +func NewECPbigint(ix *BIG, s int, mem *arena.Arena) *ECP { + var E *ECP + if mem != nil { + E = arena.New[ECP](mem) + } else { + E = new(ECP) + } + E.x = NewFPbig(ix, mem) + E.y = NewFP(mem) E.x.norm() - rhs := RHS(E.x) - E.z = NewFPint(1) - hint := NewFP() + rhs := RHS(E.x, mem) + E.z = NewFPint(1, mem) + hint := NewFP(mem) if rhs.qr(hint) == 1 { - ny := rhs.Sqrt(hint) - if ny.sign() != s { - ny.Neg() + ny := rhs.Sqrt(hint, mem) + if ny.sign(mem) != s { + ny.Neg(mem) ny.norm() } E.y.copy(ny) @@ -87,18 +94,21 @@ func NewECPbigint(ix *BIG, s int) *ECP { } /* set from x - calculate y from curve equation */ -func NewECPbig(ix *BIG) *ECP { - E := new(ECP) - E.x = NewFPbig(ix) - E.y = NewFP() +func NewECPbig(ix *BIG, mem *arena.Arena) *ECP { + var E *ECP + if mem != nil { + E = arena.New[ECP](mem) + } else { + E = new(ECP) + } + E.x = NewFPbig(ix, mem) + E.y = NewFP(mem) E.x.norm() - rhs := RHS(E.x) - E.z = NewFPint(1) - hint := NewFP() + rhs := RHS(E.x, mem) + E.z = NewFPint(1, mem) + hint := NewFP(mem) if rhs.qr(hint) == 1 { - if CURVETYPE != MONTGOMERY { - E.y.copy(rhs.Sqrt(hint)) - } + E.y.copy(rhs.Sqrt(hint, mem)) } else { E.inf() } @@ -106,36 +116,23 @@ func NewECPbig(ix *BIG) *ECP { } /* test for O point-at-infinity */ -func (E *ECP) Is_infinity() bool { +func (E *ECP) Is_infinity(mem *arena.Arena) bool { // if E.INF {return true} - if CURVETYPE == EDWARDS { - return (E.x.IsZero() && E.y.Equals(E.z)) - } - if CURVETYPE == WEIERSTRASS { - return (E.x.IsZero() && E.z.IsZero()) - } - if CURVETYPE == MONTGOMERY { - return E.z.IsZero() - } - return true + return (E.x.IsZero(mem) && E.z.IsZero(mem)) } /* Conditional swap of P and Q dependant on d */ func (E *ECP) cswap(Q *ECP, d int) { E.x.cswap(Q.x, d) - if CURVETYPE != MONTGOMERY { - E.y.cswap(Q.y, d) - } + E.y.cswap(Q.y, d) E.z.cswap(Q.z, d) } /* Conditional move of Q to P dependant on d */ func (E *ECP) cmove(Q *ECP, d int) { E.x.cmove(Q.x, d) - if CURVETYPE != MONTGOMERY { - E.y.cmove(Q.y, d) - } + E.y.cmove(Q.y, d) E.z.cmove(Q.z, d) } @@ -149,28 +146,20 @@ func teq(b int32, c int32) int { /* this=P */ func (E *ECP) Copy(P *ECP) { E.x.copy(P.x) - if CURVETYPE != MONTGOMERY { - E.y.copy(P.y) - } + E.y.copy(P.y) E.z.copy(P.z) } /* this=-this */ -func (E *ECP) Neg() { - if CURVETYPE == WEIERSTRASS { - E.y.Neg() - E.y.norm() - } - if CURVETYPE == EDWARDS { - E.x.Neg() - E.x.norm() - } +func (E *ECP) Neg(mem *arena.Arena) { + E.y.Neg(mem) + E.y.norm() return } /* Constant time select from pre-computed table */ func (E *ECP) selector(W []*ECP, b int32) { - MP := NewECP() + MP := NewECP(nil) m := b >> 31 babs := (b ^ m) - m @@ -186,137 +175,106 @@ func (E *ECP) selector(W []*ECP, b int32) { E.cmove(W[7], teq(babs, 7)) MP.Copy(E) - MP.Neg() + MP.Neg(nil) E.cmove(MP, int(m&1)) } /* set this=O */ func (E *ECP) inf() { E.x.zero() - if CURVETYPE != MONTGOMERY { - E.y.one() - } - if CURVETYPE != EDWARDS { - E.z.zero() - } else { - E.z.one() - } + E.y.one() + E.z.zero() } /* Test P == Q */ func (E *ECP) Equals(Q *ECP) bool { - a := NewFP() - b := NewFP() + mem := arena.NewArena() + defer mem.Free() + a := NewFP(mem) + b := NewFP(mem) a.copy(E.x) - a.Mul(Q.z) - a.reduce() + a.Mul(Q.z, mem) + a.reduce(mem) b.copy(Q.x) - b.Mul(E.z) - b.reduce() + b.Mul(E.z, mem) + b.reduce(mem) if !a.Equals(b) { return false } - if CURVETYPE != MONTGOMERY { - a.copy(E.y) - a.Mul(Q.z) - a.reduce() - b.copy(Q.y) - b.Mul(E.z) - b.reduce() - if !a.Equals(b) { - return false - } + a.copy(E.y) + a.Mul(Q.z, mem) + a.reduce(mem) + b.copy(Q.y) + b.Mul(E.z, mem) + b.reduce(mem) + if !a.Equals(b) { + return false } return true } /* Calculate RHS of curve equation */ -func RHS(x *FP) *FP { - r := NewFPcopy(x) - r.Sqr() +func RHS(x *FP, mem *arena.Arena) *FP { + r := NewFPcopy(x, mem) + r.Sqr(mem) - if CURVETYPE == WEIERSTRASS { // x^3+Ax+B - b := NewFPbig(NewBIGints(CURVE_B)) - r.Mul(x) - if CURVE_A == -3 { - cx := NewFPcopy(x) - cx.imul(3) - cx.Neg() - cx.norm() - r.Add(cx) - } - r.Add(b) + // x^3+Ax+B + b := NewFPbig(NewBIGints(CURVE_B, mem), mem) + r.Mul(x, mem) + if CURVE_A == -3 { + cx := NewFPcopy(x, mem) + cx.imul(3, mem) + cx.Neg(mem) + cx.norm() + r.Add(cx, mem) } - if CURVETYPE == EDWARDS { // (Ax^2-1)/(Bx^2-1) - b := NewFPbig(NewBIGints(CURVE_B)) + r.Add(b, mem) - one := NewFPint(1) - b.Mul(r) - b.Sub(one) - b.norm() - if CURVE_A == -1 { - r.Neg() - } - r.Sub(one) - r.norm() - b.Invert(nil) - r.Mul(b) - } - if CURVETYPE == MONTGOMERY { // x^3+Ax^2+x - x3 := NewFP() - x3.copy(r) - x3.Mul(x) - r.imul(CURVE_A) - r.Add(x3) - r.Add(x) - } - r.reduce() + r.reduce(mem) return r } /* set to affine - from (x,y,z) to (x,y) */ -func (E *ECP) Affine() { - if E.Is_infinity() { +func (E *ECP) Affine(mem *arena.Arena) { + if E.Is_infinity(mem) { return } - one := NewFPint(1) + one := NewFPint(1, mem) if E.z.Equals(one) { return } - E.z.Invert(nil) - E.x.Mul(E.z) - E.x.reduce() + E.z.Invert(nil, mem) + E.x.Mul(E.z, mem) + E.x.reduce(mem) - if CURVETYPE != MONTGOMERY { - E.y.Mul(E.z) - E.y.reduce() - } + E.y.Mul(E.z, mem) + E.y.reduce(mem) E.z.copy(one) } /* extract x as a BIG */ -func (E *ECP) GetX() *BIG { - W := NewECP() +func (E *ECP) GetX(mem *arena.Arena) *BIG { + W := NewECP(mem) W.Copy(E) - W.Affine() - return W.x.Redc() + W.Affine(mem) + return W.x.Redc(mem) } /* extract y as a BIG */ -func (E *ECP) GetY() *BIG { - W := NewECP() +func (E *ECP) GetY(mem *arena.Arena) *BIG { + W := NewECP(mem) W.Copy(E) - W.Affine() - return W.y.Redc() + W.Affine(mem) + return W.y.Redc(mem) } /* get sign of Y */ -func (E *ECP) GetS() int { - W := NewECP() +func (E *ECP) GetS(mem *arena.Arena) int { + W := NewECP(mem) W.Copy(E) - W.Affine() - return W.y.sign() + W.Affine(mem) + return W.y.sign(mem) } /* extract x as an FP */ @@ -338,55 +296,25 @@ func (E *ECP) getz() *FP { func (E *ECP) ToBytes(b []byte, compress bool) { var t [int(MODBYTES)]byte MB := int(MODBYTES) - alt := false - W := NewECP() + W := NewECP(nil) W.Copy(E) - W.Affine() - W.x.Redc().ToBytes(t[:]) + W.Affine(nil) + W.x.Redc(nil).ToBytes(t[:]) - if CURVETYPE == MONTGOMERY { - for i := 0; i < MB; i++ { - b[i] = t[i] + for i := 0; i < MB; i++ { + b[i+1] = t[i] + } + if compress { + b[0] = 0x02 + if W.y.sign(nil) == 1 { + b[0] = 0x03 } - //b[0] = 0x06 return } - - if (MODBITS-1)%8 <= 4 && ALLOW_ALT_COMPRESS { - alt = true - } - - if alt { - for i := 0; i < MB; i++ { - b[i] = t[i] - } - if compress { - b[0] |= 0x80 - if W.y.islarger() == 1 { - b[0] |= 0x20 - } - } else { - W.y.Redc().ToBytes(t[:]) - for i := 0; i < MB; i++ { - b[i+MB] = t[i] - } - } - } else { - for i := 0; i < MB; i++ { - b[i+1] = t[i] - } - if compress { - b[0] = 0x02 - if W.y.sign() == 1 { - b[0] = 0x03 - } - return - } - b[0] = 0x04 - W.y.Redc().ToBytes(t[:]) - for i := 0; i < MB; i++ { - b[i+MB+1] = t[i] - } + b[0] = 0x04 + W.y.Redc(nil).ToBytes(t[:]) + for i := 0; i < MB; i++ { + b[i+MB+1] = t[i] } } @@ -394,616 +322,194 @@ func (E *ECP) ToBytes(b []byte, compress bool) { func ECP_fromBytes(b []byte) *ECP { var t [int(MODBYTES)]byte MB := int(MODBYTES) - p := NewBIGints(Modulus) - alt := false + p := NewBIGints(Modulus, nil) - if CURVETYPE == MONTGOMERY { - for i := 0; i < MB; i++ { - t[i] = b[i] - } - px := FromBytes(t[:]) - if Comp(px, p) >= 0 { - return NewECP() - } - return NewECPbig(px) + for i := 0; i < MB; i++ { + t[i] = b[i+1] + } + px := FromBytes(t[:]) + if Comp(px, p) >= 0 { + return NewECP(nil) } - if (MODBITS-1)%8 <= 4 && ALLOW_ALT_COMPRESS { - alt = true + if b[0] == 0x04 { + for i := 0; i < MB; i++ { + t[i] = b[i+MB+1] + } + py := FromBytes(t[:]) + if Comp(py, p) >= 0 { + return NewECP(nil) + } + return NewECPbigs(px, py, nil) } - if alt { - for i := 0; i < MB; i++ { - t[i] = b[i] - } - t[0] &= 0x1f - px := FromBytes(t[:]) - if (b[0] & 0x80) == 0 { - for i := 0; i < MB; i++ { - t[i] = b[i+MB] - } - py := FromBytes(t[:]) - return NewECPbigs(px, py) - } else { - sgn := (b[0] & 0x20) >> 5 - P := NewECPbigint(px, 0) - cmp := P.y.islarger() - if (sgn == 1 && cmp != 1) || (sgn == 0 && cmp == 1) { - P.Neg() - } - return P - } - } else { - for i := 0; i < MB; i++ { - t[i] = b[i+1] - } - px := FromBytes(t[:]) - if Comp(px, p) >= 0 { - return NewECP() - } - - if b[0] == 0x04 { - for i := 0; i < MB; i++ { - t[i] = b[i+MB+1] - } - py := FromBytes(t[:]) - if Comp(py, p) >= 0 { - return NewECP() - } - return NewECPbigs(px, py) - } - - if b[0] == 0x02 || b[0] == 0x03 { - return NewECPbigint(px, int(b[0]&1)) - } + if b[0] == 0x02 || b[0] == 0x03 { + return NewECPbigint(px, int(b[0]&1), nil) } - return NewECP() + return NewECP(nil) } /* convert to hex string */ func (E *ECP) ToString() string { - W := NewECP() + W := NewECP(nil) W.Copy(E) - W.Affine() - if W.Is_infinity() { + W.Affine(nil) + if W.Is_infinity(nil) { return "infinity" } - if CURVETYPE == MONTGOMERY { - return "(" + W.x.Redc().ToString() + ")" - } else { - return "(" + W.x.Redc().ToString() + "," + W.y.Redc().ToString() + ")" - } + return "(" + W.x.Redc(nil).ToString() + "," + W.y.Redc(nil).ToString() + ")" } /* this*=2 */ -func (E *ECP) Dbl() { +func (E *ECP) Dbl(mem *arena.Arena) { + t0 := NewFPcopy(E.y, mem) + t0.Sqr(mem) + t1 := NewFPcopy(E.y, mem) + t1.Mul(E.z, mem) + t2 := NewFPcopy(E.z, mem) + t2.Sqr(mem) - if CURVETYPE == WEIERSTRASS { - if CURVE_A == 0 { - t0 := NewFPcopy(E.y) - t0.Sqr() - t1 := NewFPcopy(E.y) - t1.Mul(E.z) - t2 := NewFPcopy(E.z) - t2.Sqr() + E.z.copy(t0) + E.z.Add(t0, mem) + E.z.norm() + E.z.Add(E.z, mem) + E.z.Add(E.z, mem) + E.z.norm() + t2.imul(3*CURVE_B_I, mem) - E.z.copy(t0) - E.z.Add(t0) - E.z.norm() - E.z.Add(E.z) - E.z.Add(E.z) - E.z.norm() - t2.imul(3 * CURVE_B_I) + x3 := NewFPcopy(t2, mem) + x3.Mul(E.z, mem) - x3 := NewFPcopy(t2) - x3.Mul(E.z) + y3 := NewFPcopy(t0, mem) + y3.Add(t2, mem) + y3.norm() + E.z.Mul(t1, mem) + t1.copy(t2) + t1.Add(t2, mem) + t2.Add(t1, mem) + t0.Sub(t2, mem) + t0.norm() + y3.Mul(t0, mem) + y3.Add(x3, mem) + t1.copy(E.x) + t1.Mul(E.y, mem) + E.x.copy(t0) + E.x.norm() + E.x.Mul(t1, mem) + E.x.Add(E.x, mem) + E.x.norm() + E.y.copy(y3) + E.y.norm() - y3 := NewFPcopy(t0) - y3.Add(t2) - y3.norm() - E.z.Mul(t1) - t1.copy(t2) - t1.Add(t2) - t2.Add(t1) - t0.Sub(t2) - t0.norm() - y3.Mul(t0) - y3.Add(x3) - t1.copy(E.x) - t1.Mul(E.y) - E.x.copy(t0) - E.x.norm() - E.x.Mul(t1) - E.x.Add(E.x) - E.x.norm() - E.y.copy(y3) - E.y.norm() - } else { - t0 := NewFPcopy(E.x) - t1 := NewFPcopy(E.y) - t2 := NewFPcopy(E.z) - t3 := NewFPcopy(E.x) - z3 := NewFPcopy(E.z) - y3 := NewFP() - x3 := NewFP() - b := NewFP() - - if CURVE_B_I == 0 { - b.copy(NewFPbig(NewBIGints(CURVE_B))) - } - - t0.Sqr() //1 x^2 - t1.Sqr() //2 y^2 - t2.Sqr() //3 - - t3.Mul(E.y) //4 - t3.Add(t3) - t3.norm() //5 - z3.Mul(E.x) //6 - z3.Add(z3) - z3.norm() //7 - y3.copy(t2) - - if CURVE_B_I == 0 { - y3.Mul(b) - } else { - y3.imul(CURVE_B_I) - } - - y3.Sub(z3) //9 *** - x3.copy(y3) - x3.Add(y3) - x3.norm() //10 - - y3.Add(x3) //11 - x3.copy(t1) - x3.Sub(y3) - x3.norm() //12 - y3.Add(t1) - y3.norm() //13 - y3.Mul(x3) //14 - x3.Mul(t3) //15 - t3.copy(t2) - t3.Add(t2) //16 - t2.Add(t3) //17 - - if CURVE_B_I == 0 { - z3.Mul(b) - } else { - z3.imul(CURVE_B_I) - } - - z3.Sub(t2) //19 - z3.Sub(t0) - z3.norm() //20 *** - t3.copy(z3) - t3.Add(z3) //21 - - z3.Add(t3) - z3.norm() //22 - t3.copy(t0) - t3.Add(t0) //23 - t0.Add(t3) //24 - t0.Sub(t2) - t0.norm() //25 - - t0.Mul(z3) //26 - y3.Add(t0) //27 - t0.copy(E.y) - t0.Mul(E.z) //28 - t0.Add(t0) - t0.norm() //29 - z3.Mul(t0) //30 - x3.Sub(z3) //x3.norm();//31 - t0.Add(t0) - t0.norm() //32 - t1.Add(t1) - t1.norm() //33 - z3.copy(t0) - z3.Mul(t1) //34 - - E.x.copy(x3) - E.x.norm() - E.y.copy(y3) - E.y.norm() - E.z.copy(z3) - E.z.norm() - } - } - - if CURVETYPE == EDWARDS { - C := NewFPcopy(E.x) - D := NewFPcopy(E.y) - H := NewFPcopy(E.z) - J := NewFP() - - E.x.Mul(E.y) - E.x.Add(E.x) - E.x.norm() - C.Sqr() - D.Sqr() - if CURVE_A == -1 { - C.Neg() - } - E.y.copy(C) - E.y.Add(D) - E.y.norm() - - H.Sqr() - H.Add(H) - E.z.copy(E.y) - J.copy(E.y) - J.Sub(H) - J.norm() - E.x.Mul(J) - C.Sub(D) - C.norm() - E.y.Mul(C) - E.z.Mul(J) - - } - if CURVETYPE == MONTGOMERY { - A := NewFPcopy(E.x) - B := NewFPcopy(E.x) - AA := NewFP() - BB := NewFP() - C := NewFP() - - A.Add(E.z) - A.norm() - AA.copy(A) - AA.Sqr() - B.Sub(E.z) - B.norm() - BB.copy(B) - BB.Sqr() - C.copy(AA) - C.Sub(BB) - C.norm() - - E.x.copy(AA) - E.x.Mul(BB) - - A.copy(C) - A.imul((CURVE_A + 2) / 4) - - BB.Add(A) - BB.norm() - E.z.copy(BB) - E.z.Mul(C) - } return } /* this+=Q */ -func (E *ECP) Add(Q *ECP) { +func (E *ECP) Add(Q *ECP, mem *arena.Arena) { + b := 3 * CURVE_B_I + t0 := NewFPcopy(E.x, mem) + t0.Mul(Q.x, mem) + t1 := NewFPcopy(E.y, mem) + t1.Mul(Q.y, mem) + t2 := NewFPcopy(E.z, mem) + t2.Mul(Q.z, mem) + t3 := NewFPcopy(E.x, mem) + t3.Add(E.y, mem) + t3.norm() + t4 := NewFPcopy(Q.x, mem) + t4.Add(Q.y, mem) + t4.norm() + t3.Mul(t4, mem) + t4.copy(t0) + t4.Add(t1, mem) - if CURVETYPE == WEIERSTRASS { - if CURVE_A == 0 { - b := 3 * CURVE_B_I - t0 := NewFPcopy(E.x) - t0.Mul(Q.x) - t1 := NewFPcopy(E.y) - t1.Mul(Q.y) - t2 := NewFPcopy(E.z) - t2.Mul(Q.z) - t3 := NewFPcopy(E.x) - t3.Add(E.y) - t3.norm() - t4 := NewFPcopy(Q.x) - t4.Add(Q.y) - t4.norm() - t3.Mul(t4) - t4.copy(t0) - t4.Add(t1) + t3.Sub(t4, mem) + t3.norm() + t4.copy(E.y) + t4.Add(E.z, mem) + t4.norm() + x3 := NewFPcopy(Q.y, mem) + x3.Add(Q.z, mem) + x3.norm() - t3.Sub(t4) - t3.norm() - t4.copy(E.y) - t4.Add(E.z) - t4.norm() - x3 := NewFPcopy(Q.y) - x3.Add(Q.z) - x3.norm() + t4.Mul(x3, mem) + x3.copy(t1) + x3.Add(t2, mem) - t4.Mul(x3) - x3.copy(t1) - x3.Add(t2) + t4.Sub(x3, mem) + t4.norm() + x3.copy(E.x) + x3.Add(E.z, mem) + x3.norm() + y3 := NewFPcopy(Q.x, mem) + y3.Add(Q.z, mem) + y3.norm() + x3.Mul(y3, mem) + y3.copy(t0) + y3.Add(t2, mem) + y3.rsub(x3, mem) + y3.norm() + x3.copy(t0) + x3.Add(t0, mem) + t0.Add(x3, mem) + t0.norm() + t2.imul(b, mem) - t4.Sub(x3) - t4.norm() - x3.copy(E.x) - x3.Add(E.z) - x3.norm() - y3 := NewFPcopy(Q.x) - y3.Add(Q.z) - y3.norm() - x3.Mul(y3) - y3.copy(t0) - y3.Add(t2) - y3.rsub(x3) - y3.norm() - x3.copy(t0) - x3.Add(t0) - t0.Add(x3) - t0.norm() - t2.imul(b) + z3 := NewFPcopy(t1, mem) + z3.Add(t2, mem) + z3.norm() + t1.Sub(t2, mem) + t1.norm() + y3.imul(b, mem) - z3 := NewFPcopy(t1) - z3.Add(t2) - z3.norm() - t1.Sub(t2) - t1.norm() - y3.imul(b) + x3.copy(y3) + x3.Mul(t4, mem) + t2.copy(t3) + t2.Mul(t1, mem) + x3.rsub(t2, mem) + y3.Mul(t0, mem) + t1.Mul(z3, mem) + y3.Add(t1, mem) + t0.Mul(t3, mem) + z3.Mul(t4, mem) + z3.Add(t0, mem) - x3.copy(y3) - x3.Mul(t4) - t2.copy(t3) - t2.Mul(t1) - x3.rsub(t2) - y3.Mul(t0) - t1.Mul(z3) - y3.Add(t1) - t0.Mul(t3) - z3.Mul(t4) - z3.Add(t0) + E.x.copy(x3) + E.x.norm() + E.y.copy(y3) + E.y.norm() + E.z.copy(z3) + E.z.norm() - E.x.copy(x3) - E.x.norm() - E.y.copy(y3) - E.y.norm() - E.z.copy(z3) - E.z.norm() - } else { - - t0 := NewFPcopy(E.x) - t1 := NewFPcopy(E.y) - t2 := NewFPcopy(E.z) - t3 := NewFPcopy(E.x) - t4 := NewFPcopy(Q.x) - z3 := NewFP() - y3 := NewFPcopy(Q.x) - x3 := NewFPcopy(Q.y) - b := NewFP() - - if CURVE_B_I == 0 { - b.copy(NewFPbig(NewBIGints(CURVE_B))) - } - - t0.Mul(Q.x) //1 - t1.Mul(Q.y) //2 - t2.Mul(Q.z) //3 - - t3.Add(E.y) - t3.norm() //4 - t4.Add(Q.y) - t4.norm() //5 - t3.Mul(t4) //6 - t4.copy(t0) - t4.Add(t1) //7 - t3.Sub(t4) - t3.norm() //8 - t4.copy(E.y) - t4.Add(E.z) - t4.norm() //9 - x3.Add(Q.z) - x3.norm() //10 - t4.Mul(x3) //11 - x3.copy(t1) - x3.Add(t2) //12 - - t4.Sub(x3) - t4.norm() //13 - x3.copy(E.x) - x3.Add(E.z) - x3.norm() //14 - y3.Add(Q.z) - y3.norm() //15 - - x3.Mul(y3) //16 - y3.copy(t0) - y3.Add(t2) //17 - - y3.rsub(x3) - y3.norm() //18 - z3.copy(t2) - - if CURVE_B_I == 0 { - z3.Mul(b) - } else { - z3.imul(CURVE_B_I) - } - - x3.copy(y3) - x3.Sub(z3) - x3.norm() //20 - z3.copy(x3) - z3.Add(x3) //21 - - x3.Add(z3) //22 - z3.copy(t1) - z3.Sub(x3) - z3.norm() //23 - x3.Add(t1) - x3.norm() //24 - - if CURVE_B_I == 0 { - y3.Mul(b) - } else { - y3.imul(CURVE_B_I) - } - - t1.copy(t2) - t1.Add(t2) //26 - t2.Add(t1) //27 - - y3.Sub(t2) //28 - - y3.Sub(t0) - y3.norm() //29 - t1.copy(y3) - t1.Add(y3) //30 - y3.Add(t1) - y3.norm() //31 - - t1.copy(t0) - t1.Add(t0) //32 - t0.Add(t1) //33 - t0.Sub(t2) - t0.norm() //34 - t1.copy(t4) - t1.Mul(y3) //35 - t2.copy(t0) - t2.Mul(y3) //36 - y3.copy(x3) - y3.Mul(z3) //37 - y3.Add(t2) //38 - x3.Mul(t3) //39 - x3.Sub(t1) //40 - z3.Mul(t4) //41 - t1.copy(t3) - t1.Mul(t0) //42 - z3.Add(t1) - E.x.copy(x3) - E.x.norm() - E.y.copy(y3) - E.y.norm() - E.z.copy(z3) - E.z.norm() - - } - } - if CURVETYPE == EDWARDS { - b := NewFPbig(NewBIGints(CURVE_B)) - A := NewFPcopy(E.z) - B := NewFP() - C := NewFPcopy(E.x) - D := NewFPcopy(E.y) - EE := NewFP() - F := NewFP() - G := NewFP() - - A.Mul(Q.z) - B.copy(A) - B.Sqr() - C.Mul(Q.x) - D.Mul(Q.y) - - EE.copy(C) - EE.Mul(D) - EE.Mul(b) - F.copy(B) - F.Sub(EE) - G.copy(B) - G.Add(EE) - - if CURVE_A == 1 { - EE.copy(D) - EE.Sub(C) - } - C.Add(D) - - B.copy(E.x) - B.Add(E.y) - D.copy(Q.x) - D.Add(Q.y) - B.norm() - D.norm() - B.Mul(D) - B.Sub(C) - B.norm() - F.norm() - B.Mul(F) - E.x.copy(A) - E.x.Mul(B) - G.norm() - if CURVE_A == 1 { - EE.norm() - C.copy(EE) - C.Mul(G) - } - if CURVE_A == -1 { - C.norm() - C.Mul(G) - } - E.y.copy(A) - E.y.Mul(C) - E.z.copy(F) - E.z.Mul(G) - } return } -/* Differential Add for Montgomery curves. this+=Q where W is this-Q and is affine. */ -func (E *ECP) dAdd(Q *ECP, W *ECP) { - A := NewFPcopy(E.x) - B := NewFPcopy(E.x) - C := NewFPcopy(Q.x) - D := NewFPcopy(Q.x) - DA := NewFP() - CB := NewFP() - - A.Add(E.z) - B.Sub(E.z) - - C.Add(Q.z) - D.Sub(Q.z) - A.norm() - D.norm() - - DA.copy(D) - DA.Mul(A) - C.norm() - B.norm() - - CB.copy(C) - CB.Mul(B) - - A.copy(DA) - A.Add(CB) - A.norm() - A.Sqr() - B.copy(DA) - B.Sub(CB) - B.norm() - B.Sqr() - - E.x.copy(A) - E.z.copy(W.x) - E.z.Mul(B) - -} - /* this-=Q */ -func (E *ECP) Sub(Q *ECP) { - NQ := NewECP() +func (E *ECP) Sub(Q *ECP, mem *arena.Arena) { + NQ := NewECP(mem) NQ.Copy(Q) - NQ.Neg() - E.Add(NQ) + NQ.Neg(mem) + E.Add(NQ, mem) } /* constant time multiply by small integer of length bts - use lAdder */ -func (E *ECP) pinmul(e int32, bts int32) *ECP { - if CURVETYPE == MONTGOMERY { - return E.lmul(NewBIGint(int(e))) - } else { - P := NewECP() - R0 := NewECP() - R1 := NewECP() - R1.Copy(E) +func (E *ECP) pinmul(e int32, bts int32, mem *arena.Arena) *ECP { + P := NewECP(mem) + R0 := NewECP(mem) + R1 := NewECP(mem) + R1.Copy(E) - for i := bts - 1; i >= 0; i-- { - b := int((e >> uint32(i)) & 1) - P.Copy(R1) - P.Add(R0) - R0.cswap(R1, b) - R1.Copy(P) - R0.Dbl() - R0.cswap(R1, b) - } - P.Copy(R0) - return P + for i := bts - 1; i >= 0; i-- { + b := int((e >> uint32(i)) & 1) + P.Copy(R1) + P.Add(R0, mem) + R0.cswap(R1, b) + R1.Copy(P) + R0.Dbl(mem) + R0.cswap(R1, b) } + P.Copy(R0) + return P } // Point multiplication, multiplies a point P by a scalar e @@ -1016,120 +522,97 @@ func (E *ECP) pinmul(e int32, bts int32) *ECP { // The point multiplication methods used will process leading zeros correctly. // So this function leaks information about the length of e... -func (E *ECP) lmul(e *BIG) *ECP { - return E.clmul(e, e) +func (E *ECP) lmul(e *BIG, outer, mem *arena.Arena) *ECP { + return E.clmul(e, e, outer, mem) } // .. but this one does not (typically set maxe=r) // Set P=e*P /* return e.this */ -func (E *ECP) clmul(e *BIG, maxe *BIG) *ECP { - if e.IsZero() || E.Is_infinity() { - return NewECP() +func (E *ECP) clmul(e *BIG, maxe *BIG, outer, mem *arena.Arena) *ECP { + if e.IsZero() || E.Is_infinity(mem) { + return NewECP(outer) } - P := NewECP() - cm := NewBIGcopy(e) + P := NewECP(outer) + cm := NewBIGcopy(e, mem) cm.or(maxe) max := cm.nbits() - if CURVETYPE == MONTGOMERY { - /* use LAdder */ - D := NewECP() - R0 := NewECP() - R0.Copy(E) - R1 := NewECP() - R1.Copy(E) - R1.Dbl() - D.Copy(E) - D.Affine() - nb := max - for i := nb - 2; i >= 0; i-- { - b := int(e.bit(i)) - P.Copy(R1) - P.dAdd(R0, D) - R0.cswap(R1, b) - R1.Copy(P) - R0.Dbl() - R0.cswap(R1, b) - } - P.Copy(R0) - } else { - // fixed size windows - mt := NewBIG() - t := NewBIG() - Q := NewECP() - C := NewECP() + // fixed size windows + mt := NewBIG(mem) + t := NewBIG(mem) + Q := NewECP(mem) + C := NewECP(mem) - var W []*ECP - var w [1 + (NLEN*int(BASEBITS)+3)/4]int8 + var W []*ECP + var w [1 + (NLEN*int(BASEBITS)+3)/4]int8 - Q.Copy(E) - Q.Dbl() + Q.Copy(E) + Q.Dbl(mem) - W = append(W, NewECP()) - W[0].Copy(E) + W = append(W, NewECP(mem)) + W[0].Copy(E) - for i := 1; i < 8; i++ { - W = append(W, NewECP()) - W[i].Copy(W[i-1]) - W[i].Add(Q) - } - - // make exponent odd - Add 2P if even, P if odd - t.copy(e) - s := int(t.parity()) - t.inc(1) - t.norm() - ns := int(t.parity()) - mt.copy(t) - mt.inc(1) - mt.norm() - t.cmove(mt, s) - Q.cmove(E, ns) - C.Copy(Q) - - nb := 1 + (max+3)/4 - - // convert exponent to signed 4-bit window - for i := 0; i < nb; i++ { - w[i] = int8(t.lastbits(5) - 16) - t.dec(int(w[i])) - t.norm() - t.fshr(4) - } - w[nb] = int8(t.lastbits(5)) - - //P.Copy(W[(int(w[nb])-1)/2]) - P.selector(W, int32(w[nb])) - for i := nb - 1; i >= 0; i-- { - Q.selector(W, int32(w[i])) - P.Dbl() - P.Dbl() - P.Dbl() - P.Dbl() - P.Add(Q) - } - P.Sub(C) /* apply correction */ + for i := 1; i < 8; i++ { + W = append(W, NewECP(mem)) + W[i].Copy(W[i-1]) + W[i].Add(Q, mem) } + + // make exponent odd - Add 2P if even, P if odd + t.copy(e) + s := int(t.parity()) + t.inc(1) + t.norm() + ns := int(t.parity()) + mt.copy(t) + mt.inc(1) + mt.norm() + t.cmove(mt, s) + Q.cmove(E, ns) + C.Copy(Q) + + nb := 1 + (max+3)/4 + + // convert exponent to signed 4-bit window + for i := 0; i < nb; i++ { + w[i] = int8(t.lastbits(5) - 16) + t.dec(int(w[i])) + t.norm() + t.fshr(4) + } + w[nb] = int8(t.lastbits(5)) + + //P.Copy(W[(int(w[nb])-1)/2]) + P.selector(W, int32(w[nb])) + for i := nb - 1; i >= 0; i-- { + Q.selector(W, int32(w[i])) + P.Dbl(mem) + P.Dbl(mem) + P.Dbl(mem) + P.Dbl(mem) + P.Add(Q, mem) + } + P.Sub(C, mem) /* apply correction */ return P } /* Public version */ -func (E *ECP) Mul(e *BIG) *ECP { - return E.lmul(e) +func (E *ECP) Mul(e *BIG, outer, mem *arena.Arena) *ECP { + return E.lmul(e, outer, mem) } // Generic multi-multiplication, fixed 4-bit window, P=Sigma e_i*X_i -func ECP_muln(n int, X []*ECP, e []*BIG) *ECP { - P := NewECP() - R := NewECP() - S := NewECP() +func ECP_muln(n int, X []*ECP, e []*BIG, mem *arena.Arena) *ECP { + P := NewECP(nil) + R := NewECP(mem) + S := NewECP(mem) var B []*ECP - t := NewBIG() + t := NewBIG(mem) for i := 0; i < 16; i++ { - B = append(B, NewECP()) + B = append(B, NewECP(mem)) } - mt := NewBIGcopy(e[0]) + mt := NewBIGcopy(e[0], mem) mt.norm() for i := 1; i < n; i++ { // find biggest t.copy(e[i]) @@ -1142,36 +625,42 @@ func ECP_muln(n int, X []*ECP, e []*BIG) *ECP { for j := 0; j < 16; j++ { B[j].inf() } + + inner := arena.NewArena() for j := 0; j < n; j++ { mt.copy(e[j]) mt.norm() mt.shr(uint(i * 4)) k := mt.lastbits(4) - B[k].Add(X[j]) + B[k].Add(X[j], inner) + if j%32 == 0 || j == n-1 { + inner.Free() + inner = arena.NewArena() + } } R.inf() S.inf() for j := 15; j >= 1; j-- { - R.Add(B[j]) - S.Add(R) + R.Add(B[j], mem) + S.Add(R, mem) } for j := 0; j < 4; j++ { - P.Dbl() + P.Dbl(mem) } - P.Add(S) + P.Add(S, mem) } return P } /* Return e.this+f.Q */ -func (E *ECP) Mul2(e *BIG, Q *ECP, f *BIG) *ECP { - te := NewBIG() - tf := NewBIG() - mt := NewBIG() - S := NewECP() - T := NewECP() - C := NewECP() +func (E *ECP) Mul2(e *BIG, Q *ECP, f *BIG, mem *arena.Arena) *ECP { + te := NewBIG(mem) + tf := NewBIG(mem) + mt := NewBIG(mem) + S := NewECP(mem) + T := NewECP(mem) + C := NewECP(mem) var W []*ECP var w [1 + (NLEN*int(BASEBITS)+1)/2]int8 @@ -1180,28 +669,28 @@ func (E *ECP) Mul2(e *BIG, Q *ECP, f *BIG) *ECP { // precompute table for i := 0; i < 8; i++ { - W = append(W, NewECP()) + W = append(W, NewECP(mem)) } W[1].Copy(E) - W[1].Sub(Q) + W[1].Sub(Q, mem) W[2].Copy(E) - W[2].Add(Q) + W[2].Add(Q, mem) S.Copy(Q) - S.Dbl() + S.Dbl(mem) W[0].Copy(W[1]) - W[0].Sub(S) + W[0].Sub(S, mem) W[3].Copy(W[2]) - W[3].Add(S) + W[3].Add(S, mem) T.Copy(E) - T.Dbl() + T.Dbl(mem) W[5].Copy(W[1]) - W[5].Add(T) + W[5].Add(T, mem) W[6].Copy(W[2]) - W[6].Add(T) + W[6].Add(T, mem) W[4].Copy(W[5]) - W[4].Sub(S) + W[4].Sub(S, mem) W[7].Copy(W[6]) - W[7].Add(S) + W[7].Add(S, mem) // if multiplier is odd, Add 2, else Add 1 to multiplier, and Add 2P or P to correction @@ -1225,7 +714,7 @@ func (E *ECP) Mul2(e *BIG, Q *ECP, f *BIG) *ECP { mt.norm() tf.cmove(mt, s) S.cmove(Q, ns) - C.Add(S) + C.Add(S, mem) mt.copy(te) mt.Add(tf) @@ -1249,48 +738,31 @@ func (E *ECP) Mul2(e *BIG, Q *ECP, f *BIG) *ECP { S.selector(W, int32(w[nb])) for i := nb - 1; i >= 0; i-- { T.selector(W, int32(w[i])) - S.Dbl() - S.Dbl() - S.Add(T) + S.Dbl(mem) + S.Dbl(mem) + S.Add(T, mem) } - S.Sub(C) /* apply correction */ + S.Sub(C, mem) /* apply correction */ return S } func (E *ECP) Cfp() { - cf := CURVE_Cof_I - if cf == 1 { - return - } - if cf == 4 { - E.Dbl() - E.Dbl() - return - } - if cf == 8 { - E.Dbl() - E.Dbl() - E.Dbl() - return - } - c := NewBIGints(CURVE_Cof) - E.Copy(E.lmul(c)) + mem := arena.NewArena() + defer mem.Free() + c := NewBIGints(CURVE_Cof, mem) + E.Copy(E.lmul(c, nil, mem)) } /* Hunt and Peck a BIG to a curve point */ -func ECP_hap2point(h *BIG) *ECP { +func ECP_hap2point(h *BIG, mem *arena.Arena) *ECP { var P *ECP - x := NewBIGcopy(h) + x := NewBIGcopy(h, mem) for true { - if CURVETYPE != MONTGOMERY { - P = NewECPbigint(x, 0) - } else { - P = NewECPbig(x) - } + P = NewECPbigint(x, 0, mem) x.inc(1) x.norm() - if !P.Is_infinity() { + if !P.Is_infinity(mem) { break } } @@ -1299,539 +771,102 @@ func ECP_hap2point(h *BIG) *ECP { /* Constant time Map to Point */ func ECP_map2point(h *FP) *ECP { - P := NewECP() + P := NewECP(nil) - if CURVETYPE == MONTGOMERY { - // Elligator 2 - X1 := NewFP() - X2 := NewFP() - w := NewFP() - one := NewFPint(1) - A := NewFPint(CURVE_A) - t := NewFPcopy(h) - N := NewFP() - D := NewFP() - hint := NewFP() + // swu method + A := NewFP(nil) + B := NewFP(nil) + X1 := NewFP(nil) + X2 := NewFP(nil) + X3 := NewFP(nil) + one := NewFPint(1, nil) + Y := NewFP(nil) + D := NewFP(nil) + t := NewFPcopy(h, nil) + w := NewFP(nil) + //Y3:=NewFP() + sgn := t.sign(nil) - t.Sqr() + // Shallue and van de Woestijne + // SQRTm3 not available, so preprocess this out + /* */ + Z := RIADZ + X1.copy(NewFPint(Z, nil)) + X3.copy(X1) + A.copy(RHS(X1, nil)) + B.copy(NewFPbig(NewBIGints(SQRTm3, nil), nil)) + B.imul(Z, nil) - if PM1D2 == 2 { - t.Add(t) - } - if PM1D2 == 1 { - t.Neg() - } - if PM1D2 > 2 { - t.imul(QNRI) - } + t.Sqr(nil) + Y.copy(A) + Y.Mul(t, nil) + t.copy(one) + t.Add(Y, nil) + t.norm() + Y.rsub(one, nil) + Y.norm() + D.copy(t) + D.Mul(Y, nil) + D.Mul(B, nil) - t.norm() - D.copy(t) - D.Add(one) - D.norm() + w.copy(A) + FP_tpo(D, w) - X1.copy(A) - X1.Neg() - X1.norm() - X2.copy(X1) - X2.Mul(t) - - w.copy(X1) - w.Sqr() - N.copy(w) - N.Mul(X1) - w.Mul(A) - w.Mul(D) - N.Add(w) - t.copy(D) - t.Sqr() - t.Mul(X1) - N.Add(t) - N.norm() - - t.copy(N) - t.Mul(D) - qres := t.qr(hint) - w.copy(t) - w.Invert(hint) - D.copy(w) - D.Mul(N) - X1.Mul(D) - X2.Mul(D) - X1.cmove(X2, 1-qres) - - a := X1.Redc() - P.Copy(NewECPbig(a)) - } - if CURVETYPE == EDWARDS { - // Elligator 2 - map to Montgomery, place point, map back - X1 := NewFP() - X2 := NewFP() - t := NewFPcopy(h) - w := NewFP() - one := NewFPint(1) - A := NewFP() - w1 := NewFP() - w2 := NewFP() - B := NewFPbig(NewBIGints(CURVE_B)) - Y := NewFP() - K := NewFP() - D := NewFP() - hint := NewFP() - //Y3:=NewFP() - rfc := 0 - - if MODTYPE != GENERALISED_MERSENNE { - A.copy(B) - - if CURVE_A == 1 { - A.Add(one) - B.Sub(one) - } else { - A.Sub(one) - B.Add(one) - } - A.norm() - B.norm() - - A.div2() - B.div2() - B.div2() - - K.copy(B) - K.Neg() - K.norm() - //K.Invert(nil) - K.invsqrt(K, w1) - - rfc = RIADZ - if rfc == 1 { // RFC7748 - A.Mul(K) - K.Mul(w1) - //K=K.Sqrt(nil) - } else { - B.Sqr() - } - } else { - rfc = 1 - A.copy(NewFPint(156326)) - } - - t.Sqr() - qnr := 0 - if PM1D2 == 2 { - t.Add(t) - qnr = 2 - } - if PM1D2 == 1 { - t.Neg() - qnr = -1 - } - if PM1D2 > 2 { - t.imul(QNRI) - qnr = QNRI - } - t.norm() - - D.copy(t) - D.Add(one) - D.norm() - X1.copy(A) - X1.Neg() - X1.norm() - X2.copy(X1) - X2.Mul(t) - - // Figure out RHS of Montgomery curve in rational form gx1/d^3 - - w.copy(X1) - w.Sqr() - w1.copy(w) - w1.Mul(X1) - w.Mul(A) - w.Mul(D) - w1.Add(w) - w2.copy(D) - w2.Sqr() - - if rfc == 0 { - w.copy(X1) - w.Mul(B) - w2.Mul(w) - w1.Add(w2) - } else { - w2.Mul(X1) - w1.Add(w2) - } - w1.norm() - - B.copy(w1) - B.Mul(D) - qres := B.qr(hint) - w.copy(B) - w.Invert(hint) - D.copy(w) - D.Mul(w1) - X1.Mul(D) - X2.Mul(D) - D.Sqr() - - w1.copy(B) - w1.imul(qnr) - w.copy(NewFPbig(NewBIGints(CURVE_HTPC))) - w.Mul(hint) - w2.copy(D) - w2.Mul(h) - - X1.cmove(X2, 1-qres) - B.cmove(w1, 1-qres) - hint.cmove(w, 1-qres) - D.cmove(w2, 1-qres) - - Y.copy(B.Sqrt(hint)) - Y.Mul(D) - - /* - Y.copy(B.Sqrt(hint)) - Y.Mul(D) - - B.imul(qnr) - w.copy(NewFPbig(NewBIGints(CURVE_HTPC))) - hint.Mul(w) - - Y3.copy(B.Sqrt(hint)) - D.Mul(h) - Y3.Mul(D) - - X1.cmove(X2,1-qres) - Y.cmove(Y3,1-qres) - */ - w.copy(Y) - w.Neg() + w.Mul(B, nil) + if w.sign(nil) == 1 { + w.Neg(nil) w.norm() - Y.cmove(w, qres^Y.sign()) - - if rfc == 0 { - X1.Mul(K) - Y.Mul(K) - } - - if MODTYPE == GENERALISED_MERSENNE { - t.copy(X1) - t.Sqr() - w.copy(t) - w.Add(one) - w.norm() - t.Sub(one) - t.norm() - w1.copy(t) - w1.Mul(Y) - w1.Add(w1) - X2.copy(w1) - X2.Add(w1) - X2.norm() - t.Sqr() - Y.Sqr() - Y.Add(Y) - Y.Add(Y) - Y.norm() - B.copy(t) - B.Add(Y) - B.norm() - - w2.copy(Y) - w2.Sub(t) - w2.norm() - w2.Mul(X1) - t.Mul(X1) - Y.div2() - w1.copy(Y) - w1.Mul(w) - w1.rsub(t) - w1.norm() - - t.copy(X2) - t.Mul(w1) - P.x.copy(t) - t.copy(w2) - t.Mul(B) - P.y.copy(t) - t.copy(w1) - t.Mul(B) - P.z.copy(t) - - return P - } else { - w1.copy(X1) - w1.Add(one) - w1.norm() - w2.copy(X1) - w2.Sub(one) - w2.norm() - t.copy(w1) - t.Mul(Y) - X1.Mul(w1) - - if rfc == 1 { - X1.Mul(K) - } - Y.Mul(w2) - P.x.copy(X1) - P.y.copy(Y) - P.z.copy(t) - - return P - } } - if CURVETYPE == WEIERSTRASS { - // swu method - A := NewFP() - B := NewFP() - X1 := NewFP() - X2 := NewFP() - X3 := NewFP() - one := NewFPint(1) - Y := NewFP() - D := NewFP() - t := NewFPcopy(h) - w := NewFP() - D2 := NewFP() - hint := NewFP() - GX1 := NewFP() - //Y3:=NewFP() - sgn := t.sign() - if CURVE_A != 0 || HTC_ISO != 0 { - if HTC_ISO != 0 { - /* CAHCZS - A.copy(NewFPbig(NewBIGints(CURVE_Ad))) - B.copy(NewFPbig(NewBIGints(CURVE_Bd))) - CAHCZF */ - } else { - A.copy(NewFPint(CURVE_A)) - B.copy(NewFPbig(NewBIGints(CURVE_B))) - } - // SSWU method - t.Sqr() - t.imul(RIADZ) - w.copy(t) - w.Add(one) - w.norm() + w.Mul(B, nil) + w.Mul(h, nil) + w.Mul(Y, nil) + w.Mul(D, nil) - w.Mul(t) - D.copy(A) - D.Mul(w) + X1.Neg(nil) + X1.norm() + X1.div2(nil) + X2.copy(X1) + X1.Sub(w, nil) + X1.norm() + X2.Add(w, nil) + X2.norm() + A.Add(A, nil) + A.Add(A, nil) + A.norm() + t.Sqr(nil) + t.Mul(D, nil) + t.Sqr(nil) + A.Mul(t, nil) + X3.Add(A, nil) + X3.norm() - w.Add(one) - w.norm() - w.Mul(B) - w.Neg() - w.norm() + rhs := RHS(X2, nil) + X3.cmove(X2, rhs.qr(nil)) + rhs.copy(RHS(X1, nil)) + X3.cmove(X1, rhs.qr(nil)) + rhs.copy(RHS(X3, nil)) + Y.copy(rhs.Sqrt(nil, nil)) - X2.copy(w) - X3.copy(t) - X3.Mul(X2) + ne := Y.sign(nil) ^ sgn + w.copy(Y) + w.Neg(nil) + w.norm() + Y.cmove(w, ne) - // x^3+Ad^2x+Bd^3 - GX1.copy(X2) - GX1.Sqr() - D2.copy(D) - D2.Sqr() - w.copy(A) - w.Mul(D2) - GX1.Add(w) - GX1.norm() - GX1.Mul(X2) - D2.Mul(D) - w.copy(B) - w.Mul(D2) - GX1.Add(w) - GX1.norm() - - w.copy(GX1) - w.Mul(D) - qr := w.qr(hint) - D.copy(w) - D.Invert(hint) - D.Mul(GX1) - X2.Mul(D) - X3.Mul(D) - t.Mul(h) - D2.copy(D) - D2.Sqr() - - D.copy(D2) - D.Mul(t) - t.copy(w) - t.imul(RIADZ) - X1.copy(NewFPbig(NewBIGints(CURVE_HTPC))) - X1.Mul(hint) - - X2.cmove(X3, 1-qr) - D2.cmove(D, 1-qr) - w.cmove(t, 1-qr) - hint.cmove(X1, 1-qr) - - Y.copy(w.Sqrt(hint)) - Y.Mul(D2) - /* - Y.copy(w.Sqrt(hint)) - Y.Mul(D2) - - D2.Mul(t) - w.imul(RIADZ) - - X1.copy(NewFPbig(NewBIGints(CURVE_HTPC))) - hint.Mul(X1) - - Y3.copy(w.Sqrt(hint)) - Y3.Mul(D2) - - X2.cmove(X3,1-qr) - Y.cmove(Y3,1-qr) - */ - ne := Y.sign() ^ sgn - w.copy(Y) - w.Neg() - w.norm() - Y.cmove(w, ne) - - if HTC_ISO != 0 { - /* CAHCZS - k:=0 - isox:=HTC_ISO - isoy:=3*(isox-1)/2 - - //xnum - xnum:=NewFPbig(NewBIGints(PC[k])); k+=1 - for i:=0;i> 31 babs := (b ^ m) - m @@ -88,25 +99,26 @@ func (E *ECP8) selector(W []*ECP8, b int32) { E.cmove(W[7], teq(babs, 7)) MP.Copy(E) - MP.Neg() + MP.Neg(nil) E.cmove(MP, int(m&1)) } /* Test if P == Q */ func (E *ECP8) Equals(Q *ECP8) bool { - - a := NewFP8copy(E.x) - b := NewFP8copy(Q.x) - a.Mul(Q.z) - b.Mul(E.z) + mem := arena.NewArena() + defer mem.Free() + a := NewFP8copy(E.x, mem) + b := NewFP8copy(Q.x, mem) + a.Mul(Q.z, mem) + b.Mul(E.z, mem) if !a.Equals(b) { return false } a.copy(E.y) b.copy(Q.y) - a.Mul(Q.z) - b.Mul(E.z) + a.Mul(Q.z, mem) + b.Mul(E.z, mem) if !a.Equals(b) { return false } @@ -115,38 +127,38 @@ func (E *ECP8) Equals(Q *ECP8) bool { } /* set to Affine - (x,y,z) to (x,y) */ -func (E *ECP8) Affine() { - if E.Is_infinity() { +func (E *ECP8) Affine(mem *arena.Arena) { + if E.Is_infinity(mem) { return } - one := NewFP8int(1) + one := NewFP8int(1, mem) if E.z.Equals(one) { - E.x.reduce() - E.y.reduce() + E.x.reduce(mem) + E.y.reduce(mem) return } - E.z.Invert(nil) + E.z.Invert(nil, mem) - E.x.Mul(E.z) - E.x.reduce() - E.y.Mul(E.z) - E.y.reduce() + E.x.Mul(E.z, mem) + E.x.reduce(mem) + E.y.Mul(E.z, mem) + E.y.reduce(mem) E.z.copy(one) } /* extract affine x as FP2 */ -func (E *ECP8) GetX() *FP8 { - W := NewECP8() +func (E *ECP8) GetX(mem *arena.Arena) *FP8 { + W := NewECP8(mem) W.Copy(E) - W.Affine() + W.Affine(mem) return W.x } /* extract affine y as FP2 */ -func (E *ECP8) GetY() *FP8 { - W := NewECP8() +func (E *ECP8) GetY(mem *arena.Arena) *FP8 { + W := NewECP8(mem) W.Copy(E) - W.Affine() + W.Affine(mem) return W.y } @@ -169,47 +181,24 @@ func (E *ECP8) getz() *FP8 { func (E *ECP8) ToBytes(b []byte, compress bool) { var t [8 * int(MODBYTES)]byte MB := 8 * int(MODBYTES) - alt := false - W := NewECP8() + W := NewECP8(nil) W.Copy(E) - W.Affine() + W.Affine(nil) W.x.ToBytes(t[:]) - if (MODBITS-1)%8 <= 4 && ALLOW_ALT_COMPRESS { - alt = true + for i := 0; i < MB; i++ { + b[i+1] = t[i] } - - if alt { + if !compress { + b[0] = 0x04 + W.y.ToBytes(t[:]) for i := 0; i < MB; i++ { - b[i] = t[i] + b[i+MB+1] = t[i] } - if !compress { - W.y.ToBytes(t[:]) - for i := 0; i < MB; i++ { - b[i+MB] = t[i] - } - } else { - b[0] |= 0x80 - if W.y.islarger() == 1 { - b[0] |= 0x20 - } - } - } else { - for i := 0; i < MB; i++ { - b[i+1] = t[i] - } - if !compress { - b[0] = 0x04 - W.y.ToBytes(t[:]) - for i := 0; i < MB; i++ { - b[i+MB+1] = t[i] - } - } else { - b[0] = 0x02 - if W.y.sign() == 1 { - b[0] = 0x03 - } + b[0] = 0x02 + if W.y.sign(nil) == 1 { + b[0] = 0x03 } } } @@ -219,92 +208,64 @@ func ECP8_fromBytes(b []byte) *ECP8 { var t [8 * int(MODBYTES)]byte MB := 8 * int(MODBYTES) typ := int(b[0]) - alt := false - if (MODBITS-1)%8 <= 4 && ALLOW_ALT_COMPRESS { - alt = true + for i := 0; i < MB; i++ { + t[i] = b[i+1] } - - if alt { + rx := FP8_fromBytes(t[:]) + if typ == 0x04 { for i := 0; i < MB; i++ { - t[i] = b[i] - } - t[0] &= 0x1f - rx := FP8_fromBytes(t[:]) - if (b[0] & 0x80) == 0 { - for i := 0; i < MB; i++ { - t[i] = b[i+MB] - } - ry := FP8_fromBytes(t[:]) - return NewECP8fp8s(rx, ry) - } else { - sgn := (b[0] & 0x20) >> 5 - P := NewECP8fp8(rx, 0) - cmp := P.y.islarger() - if (sgn == 1 && cmp != 1) || (sgn == 0 && cmp == 1) { - P.Neg() - } - return P + t[i] = b[i+MB+1] } + ry := FP8_fromBytes(t[:]) + return NewECP8fp8s(rx, ry, nil) } else { - for i := 0; i < MB; i++ { - t[i] = b[i+1] - } - rx := FP8_fromBytes(t[:]) - if typ == 0x04 { - for i := 0; i < MB; i++ { - t[i] = b[i+MB+1] - } - ry := FP8_fromBytes(t[:]) - return NewECP8fp8s(rx, ry) - } else { - return NewECP8fp8(rx, typ&1) - } + return NewECP8fp8(rx, typ&1, nil) } } /* convert this to hex string */ func (E *ECP8) ToString() string { - W := NewECP8() + W := NewECP8(nil) W.Copy(E) - W.Affine() - if W.Is_infinity() { + W.Affine(nil) + if W.Is_infinity(nil) { return "infinity" } return "(" + W.x.toString() + "," + W.y.toString() + ")" } /* Calculate RHS of twisted curve equation x^3+B/i */ -func RHS8(x *FP8) *FP8 { - r := NewFP8copy(x) - r.Sqr() - b2 := NewFP2big(NewBIGints(CURVE_B)) - b4 := NewFP4fp2(b2) - b := NewFP8fp4(b4) +func RHS8(x *FP8, mem *arena.Arena) *FP8 { + r := NewFP8copy(x, mem) + r.Sqr(mem) + b2 := NewFP2big(NewBIGints(CURVE_B, mem), mem) + b4 := NewFP4fp2(b2, mem) + b := NewFP8fp4(b4, mem) - if SEXTIC_TWIST == D_TYPE { - b.div_i() - } - if SEXTIC_TWIST == M_TYPE { - b.times_i() - } - r.Mul(x) - r.Add(b) + b.div_i(mem) + r.Mul(x, mem) + r.Add(b, mem) - r.reduce() + r.reduce(mem) return r } /* construct this from (x,y) - but set to O if not on curve */ -func NewECP8fp8s(ix *FP8, iy *FP8) *ECP8 { - E := new(ECP8) - E.x = NewFP8copy(ix) - E.y = NewFP8copy(iy) - E.z = NewFP8int(1) +func NewECP8fp8s(ix *FP8, iy *FP8, mem *arena.Arena) *ECP8 { + var E *ECP8 + if mem != nil { + E = arena.New[ECP8](mem) + } else { + E = new(ECP8) + } + E.x = NewFP8copy(ix, mem) + E.y = NewFP8copy(iy, mem) + E.z = NewFP8int(1, mem) E.x.norm() - rhs := RHS8(E.x) - y2 := NewFP8copy(E.y) - y2.Sqr() + rhs := RHS8(E.x, mem) + y2 := NewFP8copy(E.y, mem) + y2.Sqr(mem) if !y2.Equals(rhs) { E.inf() } @@ -312,20 +273,25 @@ func NewECP8fp8s(ix *FP8, iy *FP8) *ECP8 { } /* construct this from x - but set to O if not on curve */ -func NewECP8fp8(ix *FP8, s int) *ECP8 { - E := new(ECP8) - h := NewFP() - E.x = NewFP8copy(ix) - E.y = NewFP8int(1) - E.z = NewFP8int(1) +func NewECP8fp8(ix *FP8, s int, mem *arena.Arena) *ECP8 { + var E *ECP8 + if mem != nil { + E = arena.New[ECP8](mem) + } else { + E = new(ECP8) + } + h := NewFP(mem) + E.x = NewFP8copy(ix, mem) + E.y = NewFP8int(1, mem) + E.z = NewFP8int(1, mem) E.x.norm() - rhs := RHS8(E.x) + rhs := RHS8(E.x, mem) if rhs.qr(h) == 1 { - rhs.Sqrt(h) - if rhs.sign() != s { - rhs.Neg() + rhs.Sqrt(h, mem) + if rhs.sign(mem) != s { + rhs.Neg(mem) } - rhs.reduce() + rhs.reduce(mem) E.y.copy(rhs) } else { @@ -335,55 +301,48 @@ func NewECP8fp8(ix *FP8, s int) *ECP8 { } /* this+=this */ -func (E *ECP8) Dbl() int { - iy := NewFP8copy(E.y) - if SEXTIC_TWIST == D_TYPE { - iy.times_i() - } +func (E *ECP8) Dbl(mem *arena.Arena) int { + iy := NewFP8copy(E.y, mem) + iy.times_i(mem) - t0 := NewFP8copy(E.y) - t0.Sqr() - if SEXTIC_TWIST == D_TYPE { - t0.times_i() - } - t1 := NewFP8copy(iy) - t1.Mul(E.z) - t2 := NewFP8copy(E.z) - t2.Sqr() + t0 := NewFP8copy(E.y, mem) + t0.Sqr(mem) + t0.times_i(mem) + t1 := NewFP8copy(iy, mem) + t1.Mul(E.z, mem) + t2 := NewFP8copy(E.z, mem) + t2.Sqr(mem) E.z.copy(t0) - E.z.Add(t0) + E.z.Add(t0, mem) E.z.norm() - E.z.Add(E.z) - E.z.Add(E.z) + E.z.Add(E.z, mem) + E.z.Add(E.z, mem) E.z.norm() - t2.imul(3 * CURVE_B_I) - if SEXTIC_TWIST == M_TYPE { - t2.times_i() - } - x3 := NewFP8copy(t2) - x3.Mul(E.z) + t2.imul(3*CURVE_B_I, mem) + x3 := NewFP8copy(t2, mem) + x3.Mul(E.z, mem) - y3 := NewFP8copy(t0) + y3 := NewFP8copy(t0, mem) - y3.Add(t2) + y3.Add(t2, mem) y3.norm() - E.z.Mul(t1) + E.z.Mul(t1, mem) t1.copy(t2) - t1.Add(t2) - t2.Add(t1) + t1.Add(t2, mem) + t2.Add(t1, mem) t2.norm() - t0.Sub(t2) + t0.Sub(t2, mem) t0.norm() //y^2-9bz^2 - y3.Mul(t0) - y3.Add(x3) //(y^2+3z*2)(y^2-9z^2)+3b.z^2.8y^2 + y3.Mul(t0, mem) + y3.Add(x3, mem) //(y^2+3z*2)(y^2-9z^2)+3b.z^2.8y^2 t1.copy(E.x) - t1.Mul(iy) // + t1.Mul(iy, mem) // E.x.copy(t0) E.x.norm() - E.x.Mul(t1) - E.x.Add(E.x) //(y^2-9bz^2)xy2 + E.x.Mul(t1, mem) + E.x.Add(E.x, mem) //(y^2-9bz^2)xy2 E.x.norm() E.y.copy(y3) @@ -393,90 +352,78 @@ func (E *ECP8) Dbl() int { } /* this+=Q - return 0 for Add, 1 for double, -1 for O */ -func (E *ECP8) Add(Q *ECP8) int { +func (E *ECP8) Add(Q *ECP8, mem *arena.Arena) int { b := 3 * CURVE_B_I - t0 := NewFP8copy(E.x) - t0.Mul(Q.x) // x.Q.x - t1 := NewFP8copy(E.y) - t1.Mul(Q.y) // y.Q.y + t0 := NewFP8copy(E.x, mem) + t0.Mul(Q.x, mem) // x.Q.x + t1 := NewFP8copy(E.y, mem) + t1.Mul(Q.y, mem) // y.Q.y - t2 := NewFP8copy(E.z) - t2.Mul(Q.z) - t3 := NewFP8copy(E.x) - t3.Add(E.y) + t2 := NewFP8copy(E.z, mem) + t2.Mul(Q.z, mem) + t3 := NewFP8copy(E.x, mem) + t3.Add(E.y, mem) t3.norm() //t3=X1+Y1 - t4 := NewFP8copy(Q.x) - t4.Add(Q.y) - t4.norm() //t4=X2+Y2 - t3.Mul(t4) //t3=(X1+Y1)(X2+Y2) + t4 := NewFP8copy(Q.x, mem) + t4.Add(Q.y, mem) + t4.norm() //t4=X2+Y2 + t3.Mul(t4, mem) //t3=(X1+Y1)(X2+Y2) t4.copy(t0) - t4.Add(t1) //t4=X1.X2+Y1.Y2 + t4.Add(t1, mem) //t4=X1.X2+Y1.Y2 - t3.Sub(t4) + t3.Sub(t4, mem) t3.norm() - if SEXTIC_TWIST == D_TYPE { - t3.times_i() //t3=(X1+Y1)(X2+Y2)-(X1.X2+Y1.Y2) = X1.Y2+X2.Y1 - } + t3.times_i(mem) //t3=(X1+Y1)(X2+Y2)-(X1.X2+Y1.Y2) = X1.Y2+X2.Y1 t4.copy(E.y) - t4.Add(E.z) + t4.Add(E.z, mem) t4.norm() //t4=Y1+Z1 - x3 := NewFP8copy(Q.y) - x3.Add(Q.z) + x3 := NewFP8copy(Q.y, mem) + x3.Add(Q.z, mem) x3.norm() //x3=Y2+Z2 - t4.Mul(x3) //t4=(Y1+Z1)(Y2+Z2) - x3.copy(t1) // - x3.Add(t2) //X3=Y1.Y2+Z1.Z2 + t4.Mul(x3, mem) //t4=(Y1+Z1)(Y2+Z2) + x3.copy(t1) // + x3.Add(t2, mem) //X3=Y1.Y2+Z1.Z2 - t4.Sub(x3) + t4.Sub(x3, mem) t4.norm() - if SEXTIC_TWIST == D_TYPE { - t4.times_i() //t4=(Y1+Z1)(Y2+Z2) - (Y1.Y2+Z1.Z2) = Y1.Z2+Y2.Z1 - } + t4.times_i(mem) //t4=(Y1+Z1)(Y2+Z2) - (Y1.Y2+Z1.Z2) = Y1.Z2+Y2.Z1 x3.copy(E.x) - x3.Add(E.z) + x3.Add(E.z, mem) x3.norm() // x3=X1+Z1 - y3 := NewFP8copy(Q.x) - y3.Add(Q.z) - y3.norm() // y3=X2+Z2 - x3.Mul(y3) // x3=(X1+Z1)(X2+Z2) + y3 := NewFP8copy(Q.x, mem) + y3.Add(Q.z, mem) + y3.norm() // y3=X2+Z2 + x3.Mul(y3, mem) // x3=(X1+Z1)(X2+Z2) y3.copy(t0) - y3.Add(t2) // y3=X1.X2+Z1+Z2 - y3.rsub(x3) + y3.Add(t2, mem) // y3=X1.X2+Z1+Z2 + y3.rsub(x3, mem) y3.norm() // y3=(X1+Z1)(X2+Z2) - (X1.X2+Z1.Z2) = X1.Z2+X2.Z1 - if SEXTIC_TWIST == D_TYPE { - t0.times_i() // x.Q.x - t1.times_i() // y.Q.y - } + t0.times_i(mem) // x.Q.x + t1.times_i(mem) // y.Q.y x3.copy(t0) - x3.Add(t0) - t0.Add(x3) + x3.Add(t0, mem) + t0.Add(x3, mem) t0.norm() - t2.imul(b) - if SEXTIC_TWIST == M_TYPE { - t2.times_i() - } - z3 := NewFP8copy(t1) - z3.Add(t2) + t2.imul(b, mem) + z3 := NewFP8copy(t1, mem) + z3.Add(t2, mem) z3.norm() - t1.Sub(t2) + t1.Sub(t2, mem) t1.norm() - y3.imul(b) - if SEXTIC_TWIST == M_TYPE { - y3.times_i() - } + y3.imul(b, mem) x3.copy(y3) - x3.Mul(t4) + x3.Mul(t4, mem) t2.copy(t3) - t2.Mul(t1) - x3.rsub(t2) - y3.Mul(t0) - t1.Mul(z3) - y3.Add(t1) - t0.Mul(t3) - z3.Mul(t4) - z3.Add(t0) + t2.Mul(t1, mem) + x3.rsub(t2, mem) + y3.Mul(t0, mem) + t1.Mul(z3, mem) + y3.Add(t1, mem) + t0.Mul(t3, mem) + z3.Mul(t4, mem) + z3.Add(t0, mem) E.x.copy(x3) E.x.norm() @@ -489,51 +436,42 @@ func (E *ECP8) Add(Q *ECP8) int { } /* set this-=Q */ -func (E *ECP8) Sub(Q *ECP8) int { - NQ := NewECP8() +func (E *ECP8) Sub(Q *ECP8, mem *arena.Arena) int { + NQ := NewECP8(mem) NQ.Copy(Q) - NQ.Neg() - D := E.Add(NQ) + NQ.Neg(mem) + D := E.Add(NQ, mem) return D } func ECP8_frob_constants() [3]*FP2 { - Fra := NewBIGints(Fra) - Frb := NewBIGints(Frb) - X := NewFP2bigs(Fra, Frb) + Fra := NewBIGints(Fra, nil) + Frb := NewBIGints(Frb, nil) + X := NewFP2bigs(Fra, Frb, nil) - F0 := NewFP2copy(X) - F0.Sqr() - F2 := NewFP2copy(F0) - F2.Mul_ip() + F0 := NewFP2copy(X, nil) + F0.Sqr(nil) + F2 := NewFP2copy(F0, nil) + F2.Mul_ip(nil) F2.norm() - F1 := NewFP2copy(F2) - F1.Sqr() - F2.Mul(F1) + F1 := NewFP2copy(F2, nil) + F1.Sqr(nil) + F2.Mul(F1, nil) - F2.Mul_ip() + F2.Mul_ip(nil) F2.norm() F1.copy(X) - if SEXTIC_TWIST == M_TYPE { - F1.Mul_ip() - F1.norm() - F1.Invert(nil) - F0.copy(F1) - F0.Sqr() - F1.Mul(F0) - } - if SEXTIC_TWIST == D_TYPE { - F0.copy(F1) - F0.Sqr() - F1.Mul(F0) - F0.Mul_ip() - F0.norm() - F1.Mul_ip() - F1.norm() - F1.Mul_ip() - F1.norm() - } + + F0.copy(F1) + F0.Sqr(nil) + F1.Mul(F0, nil) + F0.Mul_ip(nil) + F0.norm() + F1.Mul_ip(nil) + F1.norm() + F1.Mul_ip(nil) + F1.norm() F := [3]*FP2{F0, F1, F2} return F @@ -542,41 +480,27 @@ func ECP8_frob_constants() [3]*FP2 { /* set this*=q, where q is Modulus, using Frobenius */ func (E *ECP8) frob(F [3]*FP2, n int) { for i := 0; i < n; i++ { - E.x.frob(F[2]) - if SEXTIC_TWIST == M_TYPE { - E.x.qmul(F[0]) - E.x.times_i2() - } - if SEXTIC_TWIST == D_TYPE { - E.x.qmul(F[0]) - E.x.times_i2() - } - E.y.frob(F[2]) - if SEXTIC_TWIST == M_TYPE { - E.y.qmul(F[1]) - E.y.times_i2() - E.y.times_i() - } - if SEXTIC_TWIST == D_TYPE { - E.y.qmul(F[1]) - E.y.times_i() - } - - E.z.frob(F[2]) + E.x.frob(F[2], nil) + E.x.qmul(F[0], nil) + E.x.times_i2(nil) + E.y.frob(F[2], nil) + E.y.qmul(F[1], nil) + E.y.times_i(nil) + E.z.frob(F[2], nil) } } /* P*=e */ -func (E *ECP8) mul(e *BIG) *ECP8 { +func (E *ECP8) mul(e *BIG, mem *arena.Arena) *ECP8 { /* fixed size windows */ - mt := NewBIG() - t := NewBIG() - P := NewECP8() - Q := NewECP8() - C := NewECP8() + mt := NewBIG(mem) + t := NewBIG(mem) + P := NewECP8(nil) + Q := NewECP8(mem) + C := NewECP8(mem) - if E.Is_infinity() { - return NewECP8() + if E.Is_infinity(mem) { + return NewECP8(mem) } var W []*ECP8 @@ -584,15 +508,15 @@ func (E *ECP8) mul(e *BIG) *ECP8 { /* precompute table */ Q.Copy(E) - Q.Dbl() + Q.Dbl(mem) - W = append(W, NewECP8()) + W = append(W, NewECP8(mem)) W[0].Copy(E) for i := 1; i < 8; i++ { - W = append(W, NewECP8()) + W = append(W, NewECP8(mem)) W[i].Copy(W[i-1]) - W[i].Add(Q) + W[i].Add(Q, mem) } /* make exponent odd - Add 2P if even, P if odd */ @@ -622,81 +546,80 @@ func (E *ECP8) mul(e *BIG) *ECP8 { P.selector(W, int32(w[nb])) for i := nb - 1; i >= 0; i-- { Q.selector(W, int32(w[i])) - P.Dbl() - P.Dbl() - P.Dbl() - P.Dbl() - P.Add(Q) + P.Dbl(mem) + P.Dbl(mem) + P.Dbl(mem) + P.Dbl(mem) + P.Add(Q, mem) } - P.Sub(C) - P.Affine() + P.Sub(C, mem) + P.Affine(mem) return P } /* Public version */ -func (E *ECP8) Mul(e *BIG) *ECP8 { - return E.mul(e) +func (E *ECP8) Mul(e *BIG, mem *arena.Arena) *ECP8 { + return E.mul(e, mem) } /* needed for SOK */ func (E *ECP8) Cfp() { F := ECP8_frob_constants() - x := NewBIGints(CURVE_Bnx) + x := NewBIGints(CURVE_Bnx, nil) - xQ := E.Mul(x) - x2Q := xQ.Mul(x) - x3Q := x2Q.Mul(x) - x4Q := x3Q.Mul(x) - x5Q := x4Q.Mul(x) - x6Q := x5Q.Mul(x) - x7Q := x6Q.Mul(x) - x8Q := x7Q.Mul(x) + xQ := E.Mul(x, nil) + x2Q := xQ.Mul(x, nil) + x3Q := x2Q.Mul(x, nil) + x4Q := x3Q.Mul(x, nil) + x5Q := x4Q.Mul(x, nil) + x6Q := x5Q.Mul(x, nil) + x7Q := x6Q.Mul(x, nil) + x8Q := x7Q.Mul(x, nil) - if SIGN_OF_X == NEGATIVEX { - xQ.Neg() - x3Q.Neg() - x5Q.Neg() - x7Q.Neg() - } - x8Q.Sub(x7Q) - x8Q.Sub(E) + xQ.Neg(nil) + x3Q.Neg(nil) + x5Q.Neg(nil) + x7Q.Neg(nil) - x7Q.Sub(x6Q) + x8Q.Sub(x7Q, nil) + x8Q.Sub(E, nil) + + x7Q.Sub(x6Q, nil) x7Q.frob(F, 1) - x6Q.Sub(x5Q) + x6Q.Sub(x5Q, nil) x6Q.frob(F, 2) - x5Q.Sub(x4Q) + x5Q.Sub(x4Q, nil) x5Q.frob(F, 3) - x4Q.Sub(x3Q) + x4Q.Sub(x3Q, nil) x4Q.frob(F, 4) - x3Q.Sub(x2Q) + x3Q.Sub(x2Q, nil) x3Q.frob(F, 5) - x2Q.Sub(xQ) + x2Q.Sub(xQ, nil) x2Q.frob(F, 6) - xQ.Sub(E) + xQ.Sub(E, nil) xQ.frob(F, 7) - E.Dbl() + E.Dbl(nil) E.frob(F, 8) - E.Add(x8Q) - E.Add(x7Q) - E.Add(x6Q) - E.Add(x5Q) + E.Add(x8Q, nil) + E.Add(x7Q, nil) + E.Add(x6Q, nil) + E.Add(x5Q, nil) - E.Add(x4Q) - E.Add(x3Q) - E.Add(x2Q) - E.Add(xQ) + E.Add(x4Q, nil) + E.Add(x3Q, nil) + E.Add(x2Q, nil) + E.Add(xQ, nil) - E.Affine() + E.Affine(nil) } func ECP8_generator() *ECP8 { @@ -704,34 +627,34 @@ func ECP8_generator() *ECP8 { G = NewECP8fp8s( NewFP8fp4s( NewFP4fp2s( - NewFP2bigs(NewBIGints(CURVE_Pxaaa), NewBIGints(CURVE_Pxaab)), - NewFP2bigs(NewBIGints(CURVE_Pxaba), NewBIGints(CURVE_Pxabb))), + NewFP2bigs(NewBIGints(CURVE_Pxaaa, nil), NewBIGints(CURVE_Pxaab, nil), nil), + NewFP2bigs(NewBIGints(CURVE_Pxaba, nil), NewBIGints(CURVE_Pxabb, nil), nil), nil), NewFP4fp2s( - NewFP2bigs(NewBIGints(CURVE_Pxbaa), NewBIGints(CURVE_Pxbab)), - NewFP2bigs(NewBIGints(CURVE_Pxbba), NewBIGints(CURVE_Pxbbb)))), + NewFP2bigs(NewBIGints(CURVE_Pxbaa, nil), NewBIGints(CURVE_Pxbab, nil), nil), + NewFP2bigs(NewBIGints(CURVE_Pxbba, nil), NewBIGints(CURVE_Pxbbb, nil), nil), nil), nil), NewFP8fp4s( NewFP4fp2s( - NewFP2bigs(NewBIGints(CURVE_Pyaaa), NewBIGints(CURVE_Pyaab)), - NewFP2bigs(NewBIGints(CURVE_Pyaba), NewBIGints(CURVE_Pyabb))), + NewFP2bigs(NewBIGints(CURVE_Pyaaa, nil), NewBIGints(CURVE_Pyaab, nil), nil), + NewFP2bigs(NewBIGints(CURVE_Pyaba, nil), NewBIGints(CURVE_Pyabb, nil), nil), nil), NewFP4fp2s( - NewFP2bigs(NewBIGints(CURVE_Pybaa), NewBIGints(CURVE_Pybab)), - NewFP2bigs(NewBIGints(CURVE_Pybba), NewBIGints(CURVE_Pybbb))))) + NewFP2bigs(NewBIGints(CURVE_Pybaa, nil), NewBIGints(CURVE_Pybab, nil), nil), + NewFP2bigs(NewBIGints(CURVE_Pybba, nil), NewBIGints(CURVE_Pybbb, nil), nil), nil), nil), nil) return G } func ECP8_hap2point(h *BIG) *ECP8 { - one := NewBIGint(1) - x := NewBIGcopy(h) + one := NewBIGint(1, nil) + x := NewBIGcopy(h, nil) var X2 *FP2 var X4 *FP4 var X8 *FP8 var Q *ECP8 for true { - X2 = NewFP2bigs(one, x) - X4 = NewFP4fp2(X2) - X8 = NewFP8fp4(X4) - Q = NewECP8fp8(X8, 0) - if !Q.Is_infinity() { + X2 = NewFP2bigs(one, x, nil) + X4 = NewFP4fp2(X2, nil) + X8 = NewFP8fp4(X4, nil) + Q = NewECP8fp8(X8, 0, nil) + if !Q.Is_infinity(nil) { break } x.inc(1) @@ -743,83 +666,83 @@ func ECP8_hap2point(h *BIG) *ECP8 { /* Deterministic mapping of Fp to point on curve */ func ECP8_map2point(H *FP8) *ECP8 { // Shallue and van de Woestijne - NY := NewFP8int(1) - T := NewFP8copy(H) - sgn := T.sign() + NY := NewFP8int(1, nil) + T := NewFP8copy(H, nil) + sgn := T.sign(nil) - Z := NewFPint(RIADZG2A) - X1 := NewFP8fp(Z) - X3 := NewFP8copy(X1) - A := RHS8(X1) - W := NewFP8copy(A) - W.Sqrt(nil) + Z := NewFPint(RIADZG2A, nil) + X1 := NewFP8fp(Z, nil) + X3 := NewFP8copy(X1, nil) + A := RHS8(X1, nil) + W := NewFP8copy(A, nil) + W.Sqrt(nil, nil) - s := NewFPbig(NewBIGints(SQRTm3)) - Z.Mul(s) + s := NewFPbig(NewBIGints(SQRTm3, nil), nil) + Z.Mul(s, nil) - T.Sqr() - Y := NewFP8copy(A) - Y.Mul(T) + T.Sqr(nil) + Y := NewFP8copy(A, nil) + Y.Mul(T, nil) T.copy(NY) - T.Add(Y) + T.Add(Y, nil) T.norm() - Y.rsub(NY) + Y.rsub(NY, nil) Y.norm() NY.copy(T) - NY.Mul(Y) + NY.Mul(Y, nil) - NY.tmul(Z) - NY.Invert(nil) + NY.tmul(Z, nil) + NY.Invert(nil, nil) - W.tmul(Z) - if W.sign() == 1 { - W.Neg() + W.tmul(Z, nil) + if W.sign(nil) == 1 { + W.Neg(nil) W.norm() } - W.tmul(Z) - W.Mul(H) - W.Mul(Y) - W.Mul(NY) + W.tmul(Z, nil) + W.Mul(H, nil) + W.Mul(Y, nil) + W.Mul(NY, nil) - X1.Neg() + X1.Neg(nil) X1.norm() - X1.div2() - X2 := NewFP8copy(X1) - X1.Sub(W) + X1.div2(nil) + X2 := NewFP8copy(X1, nil) + X1.Sub(W, nil) X1.norm() - X2.Add(W) + X2.Add(W, nil) X2.norm() - A.Add(A) - A.Add(A) + A.Add(A, nil) + A.Add(A, nil) A.norm() - T.Sqr() - T.Mul(NY) - T.Sqr() - A.Mul(T) - X3.Add(A) + T.Sqr(nil) + T.Mul(NY, nil) + T.Sqr(nil) + A.Mul(T, nil) + X3.Add(A, nil) X3.norm() - Y.copy(RHS8(X2)) + Y.copy(RHS8(X2, nil)) X3.cmove(X2, Y.qr(nil)) - Y.copy(RHS8(X1)) + Y.copy(RHS8(X1, nil)) X3.cmove(X1, Y.qr(nil)) - Y.copy(RHS8(X3)) - Y.Sqrt(nil) + Y.copy(RHS8(X3, nil)) + Y.Sqrt(nil, nil) - ne := Y.sign() ^ sgn + ne := Y.sign(nil) ^ sgn W.copy(Y) - W.Neg() + W.Neg(nil) W.norm() Y.cmove(W, ne) - return NewECP8fp8s(X3, Y) + return NewECP8fp8s(X3, Y, nil) } /* Map octet string to curve point */ func ECP8_mapit(h []byte) *ECP8 { - q := NewBIGints(Modulus) + q := NewBIGints(Modulus, nil) dx := DBIG_fromBytes(h) - x := dx.Mod(q) + x := dx.Mod(q, nil) Q := ECP8_hap2point(x) Q.Cfp() @@ -830,14 +753,14 @@ func ECP8_mapit(h []byte) *ECP8 { // Bos & Costello https://eprint.iacr.org/2013/458.pdf // Faz-Hernandez & Longa & Sanchez https://eprint.iacr.org/2013/158.pdf // Side channel attack secure -func Mul16(Q []*ECP8, u []*BIG) *ECP8 { - W := NewECP8() - P := NewECP8() +func Mul16(Q []*ECP8, u []*BIG, mem *arena.Arena) *ECP8 { + W := NewECP8(mem) + P := NewECP8(mem) var T1 []*ECP8 var T2 []*ECP8 var T3 []*ECP8 var T4 []*ECP8 - mt := NewBIG() + mt := NewBIG(mem) var t []*BIG var bt int8 var k int @@ -852,104 +775,104 @@ func Mul16(Q []*ECP8, u []*BIG) *ECP8 { var s4 [NLEN*int(BASEBITS) + 1]int8 for i := 0; i < 16; i++ { - t = append(t, NewBIGcopy(u[i])) + t = append(t, NewBIGcopy(u[i], mem)) } - T1 = append(T1, NewECP8()) + T1 = append(T1, NewECP8(mem)) T1[0].Copy(Q[0]) // Q[0] - T1 = append(T1, NewECP8()) + T1 = append(T1, NewECP8(mem)) T1[1].Copy(T1[0]) - T1[1].Add(Q[1]) // Q[0]+Q[1] - T1 = append(T1, NewECP8()) + T1[1].Add(Q[1], mem) // Q[0]+Q[1] + T1 = append(T1, NewECP8(mem)) T1[2].Copy(T1[0]) - T1[2].Add(Q[2]) // Q[0]+Q[2] - T1 = append(T1, NewECP8()) + T1[2].Add(Q[2], mem) // Q[0]+Q[2] + T1 = append(T1, NewECP8(mem)) T1[3].Copy(T1[1]) - T1[3].Add(Q[2]) // Q[0]+Q[1]+Q[2] - T1 = append(T1, NewECP8()) + T1[3].Add(Q[2], mem) // Q[0]+Q[1]+Q[2] + T1 = append(T1, NewECP8(mem)) T1[4].Copy(T1[0]) - T1[4].Add(Q[3]) // Q[0]+Q[3] - T1 = append(T1, NewECP8()) + T1[4].Add(Q[3], mem) // Q[0]+Q[3] + T1 = append(T1, NewECP8(mem)) T1[5].Copy(T1[1]) - T1[5].Add(Q[3]) // Q[0]+Q[1]+Q[3] - T1 = append(T1, NewECP8()) + T1[5].Add(Q[3], mem) // Q[0]+Q[1]+Q[3] + T1 = append(T1, NewECP8(mem)) T1[6].Copy(T1[2]) - T1[6].Add(Q[3]) // Q[0]+Q[2]+Q[3] - T1 = append(T1, NewECP8()) + T1[6].Add(Q[3], mem) // Q[0]+Q[2]+Q[3] + T1 = append(T1, NewECP8(mem)) T1[7].Copy(T1[3]) - T1[7].Add(Q[3]) // Q[0]+Q[1]+Q[2]+Q[3] + T1[7].Add(Q[3], mem) // Q[0]+Q[1]+Q[2]+Q[3] - T2 = append(T2, NewECP8()) + T2 = append(T2, NewECP8(mem)) T2[0].Copy(Q[4]) // Q[0] - T2 = append(T2, NewECP8()) + T2 = append(T2, NewECP8(mem)) T2[1].Copy(T2[0]) - T2[1].Add(Q[5]) // Q[0]+Q[1] - T2 = append(T2, NewECP8()) + T2[1].Add(Q[5], mem) // Q[0]+Q[1] + T2 = append(T2, NewECP8(mem)) T2[2].Copy(T2[0]) - T2[2].Add(Q[6]) // Q[0]+Q[2] - T2 = append(T2, NewECP8()) + T2[2].Add(Q[6], mem) // Q[0]+Q[2] + T2 = append(T2, NewECP8(mem)) T2[3].Copy(T2[1]) - T2[3].Add(Q[6]) // Q[0]+Q[1]+Q[2] - T2 = append(T2, NewECP8()) + T2[3].Add(Q[6], mem) // Q[0]+Q[1]+Q[2] + T2 = append(T2, NewECP8(mem)) T2[4].Copy(T2[0]) - T2[4].Add(Q[7]) // Q[0]+Q[3] - T2 = append(T2, NewECP8()) + T2[4].Add(Q[7], mem) // Q[0]+Q[3] + T2 = append(T2, NewECP8(mem)) T2[5].Copy(T2[1]) - T2[5].Add(Q[7]) // Q[0]+Q[1]+Q[3] - T2 = append(T2, NewECP8()) + T2[5].Add(Q[7], mem) // Q[0]+Q[1]+Q[3] + T2 = append(T2, NewECP8(mem)) T2[6].Copy(T2[2]) - T2[6].Add(Q[7]) // Q[0]+Q[2]+Q[3] - T2 = append(T2, NewECP8()) + T2[6].Add(Q[7], mem) // Q[0]+Q[2]+Q[3] + T2 = append(T2, NewECP8(mem)) T2[7].Copy(T2[3]) - T2[7].Add(Q[7]) // Q[0]+Q[1]+Q[2]+Q[3] + T2[7].Add(Q[7], mem) // Q[0]+Q[1]+Q[2]+Q[3] - T3 = append(T3, NewECP8()) + T3 = append(T3, NewECP8(mem)) T3[0].Copy(Q[8]) // Q[0] - T3 = append(T3, NewECP8()) + T3 = append(T3, NewECP8(mem)) T3[1].Copy(T3[0]) - T3[1].Add(Q[9]) // Q[0]+Q[1] - T3 = append(T3, NewECP8()) + T3[1].Add(Q[9], mem) // Q[0]+Q[1] + T3 = append(T3, NewECP8(mem)) T3[2].Copy(T3[0]) - T3[2].Add(Q[10]) // Q[0]+Q[2] - T3 = append(T3, NewECP8()) + T3[2].Add(Q[10], mem) // Q[0]+Q[2] + T3 = append(T3, NewECP8(mem)) T3[3].Copy(T3[1]) - T3[3].Add(Q[10]) // Q[0]+Q[1]+Q[2] - T3 = append(T3, NewECP8()) + T3[3].Add(Q[10], mem) // Q[0]+Q[1]+Q[2] + T3 = append(T3, NewECP8(mem)) T3[4].Copy(T3[0]) - T3[4].Add(Q[11]) // Q[0]+Q[3] - T3 = append(T3, NewECP8()) + T3[4].Add(Q[11], mem) // Q[0]+Q[3] + T3 = append(T3, NewECP8(mem)) T3[5].Copy(T3[1]) - T3[5].Add(Q[11]) // Q[0]+Q[1]+Q[3] - T3 = append(T3, NewECP8()) + T3[5].Add(Q[11], mem) // Q[0]+Q[1]+Q[3] + T3 = append(T3, NewECP8(mem)) T3[6].Copy(T3[2]) - T3[6].Add(Q[11]) // Q[0]+Q[2]+Q[3] - T3 = append(T3, NewECP8()) + T3[6].Add(Q[11], mem) // Q[0]+Q[2]+Q[3] + T3 = append(T3, NewECP8(mem)) T3[7].Copy(T3[3]) - T3[7].Add(Q[11]) // Q[0]+Q[1]+Q[2]+Q[3] + T3[7].Add(Q[11], mem) // Q[0]+Q[1]+Q[2]+Q[3] - T4 = append(T4, NewECP8()) + T4 = append(T4, NewECP8(mem)) T4[0].Copy(Q[12]) // Q[0] - T4 = append(T4, NewECP8()) + T4 = append(T4, NewECP8(mem)) T4[1].Copy(T4[0]) - T4[1].Add(Q[13]) // Q[0]+Q[1] - T4 = append(T4, NewECP8()) + T4[1].Add(Q[13], mem) // Q[0]+Q[1] + T4 = append(T4, NewECP8(mem)) T4[2].Copy(T4[0]) - T4[2].Add(Q[14]) // Q[0]+Q[2] - T4 = append(T4, NewECP8()) + T4[2].Add(Q[14], mem) // Q[0]+Q[2] + T4 = append(T4, NewECP8(mem)) T4[3].Copy(T4[1]) - T4[3].Add(Q[14]) // Q[0]+Q[1]+Q[2] - T4 = append(T4, NewECP8()) + T4[3].Add(Q[14], mem) // Q[0]+Q[1]+Q[2] + T4 = append(T4, NewECP8(mem)) T4[4].Copy(T4[0]) - T4[4].Add(Q[15]) // Q[0]+Q[3] - T4 = append(T4, NewECP8()) + T4[4].Add(Q[15], mem) // Q[0]+Q[3] + T4 = append(T4, NewECP8(mem)) T4[5].Copy(T4[1]) - T4[5].Add(Q[15]) // Q[0]+Q[1]+Q[3] - T4 = append(T4, NewECP8()) + T4[5].Add(Q[15], mem) // Q[0]+Q[1]+Q[3] + T4 = append(T4, NewECP8(mem)) T4[6].Copy(T4[2]) - T4[6].Add(Q[15]) // Q[0]+Q[2]+Q[3] - T4 = append(T4, NewECP8()) + T4[6].Add(Q[15], mem) // Q[0]+Q[2]+Q[3] + T4 = append(T4, NewECP8(mem)) T4[7].Copy(T4[3]) - T4[7].Add(Q[15]) // Q[0]+Q[1]+Q[2]+Q[3] + T4[7].Add(Q[15], mem) // Q[0]+Q[1]+Q[2]+Q[3] // Make them odd pb1 := 1 - t[0].parity() @@ -1037,38 +960,38 @@ func Mul16(Q []*ECP8, u []*BIG) *ECP8 { // Main loop P.selector(T1, int32(2*w1[nb-1]+1)) W.selector(T2, int32(2*w2[nb-1]+1)) - P.Add(W) + P.Add(W, mem) W.selector(T3, int32(2*w3[nb-1]+1)) - P.Add(W) + P.Add(W, mem) W.selector(T4, int32(2*w4[nb-1]+1)) - P.Add(W) + P.Add(W, mem) for i := nb - 2; i >= 0; i-- { - P.Dbl() + P.Dbl(mem) W.selector(T1, int32(2*w1[i]+s1[i])) - P.Add(W) + P.Add(W, mem) W.selector(T2, int32(2*w2[i]+s2[i])) - P.Add(W) + P.Add(W, mem) W.selector(T3, int32(2*w3[i]+s3[i])) - P.Add(W) + P.Add(W, mem) W.selector(T4, int32(2*w4[i]+s4[i])) - P.Add(W) + P.Add(W, mem) } // apply correction W.Copy(P) - W.Sub(Q[0]) + W.Sub(Q[0], mem) P.cmove(W, pb1) W.Copy(P) - W.Sub(Q[4]) + W.Sub(Q[4], mem) P.cmove(W, pb2) W.Copy(P) - W.Sub(Q[8]) + W.Sub(Q[8], mem) P.cmove(W, pb3) W.Copy(P) - W.Sub(Q[12]) + W.Sub(Q[12], mem) P.cmove(W, pb4) - P.Affine() + P.Affine(mem) return P } diff --git a/nekryptology/pkg/core/curves/native/bls48581/hpke.go b/nekryptology/pkg/core/curves/native/bls48581/hpke.go deleted file mode 100644 index eb80eb7..0000000 --- a/nekryptology/pkg/core/curves/native/bls48581/hpke.go +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright (c) 2012-2020 MIRACL UK Ltd. - * - * This file is part of MIRACL Core - * (see https://github.com/miracl/ext.. - * - * 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. - */ - -/* Hybrid Public Key Encryption */ - -/* Following https://datatracker.ietf.org/doc/draft-irtf-cfrg-hpke/?include_text=1 */ - -package bls48581 - -import "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" - -//import "fmt" - -func reverse(X []byte) { - lx := len(X) - for i := 0; i < lx/2; i++ { - ch := X[i] - X[i] = X[lx-i-1] - X[lx-i-1] = ch - } -} - -func labeledExtract(SALT []byte, SUITE_ID []byte, label string, IKM []byte) []byte { - rfc := "HPKE-v1" - RFC := []byte(rfc) - LABEL := []byte(label) - var LIKM []byte - for i := 0; i < len(RFC); i++ { - LIKM = append(LIKM, RFC[i]) - } - for i := 0; i < len(SUITE_ID); i++ { - LIKM = append(LIKM, SUITE_ID[i]) - } - for i := 0; i < len(LABEL); i++ { - LIKM = append(LIKM, LABEL[i]) - } - if IKM != nil { - for i := 0; i < len(IKM); i++ { - LIKM = append(LIKM, IKM[i]) - } - } - return ext.HKDF_Extract(ext.MC_SHA2, HASH_TYPE, SALT, LIKM) -} - -func labeledExpand(PRK []byte, SUITE_ID []byte, label string, INFO []byte, L int) []byte { - rfc := "HPKE-v1" - RFC := []byte(rfc) - LABEL := []byte(label) - AR := ext.InttoBytes(L, 2) - var LINFO []byte - for i := 0; i < len(AR); i++ { - LINFO = append(LINFO, AR[i]) - } - for i := 0; i < len(RFC); i++ { - LINFO = append(LINFO, RFC[i]) - } - for i := 0; i < len(SUITE_ID); i++ { - LINFO = append(LINFO, SUITE_ID[i]) - } - for i := 0; i < len(LABEL); i++ { - LINFO = append(LINFO, LABEL[i]) - } - if INFO != nil { - for i := 0; i < len(INFO); i++ { - LINFO = append(LINFO, INFO[i]) - } - } - - return ext.HKDF_Expand(ext.MC_SHA2, HASH_TYPE, L, PRK, LINFO) -} - -func extractAndExpand(config_id int, DH []byte, context []byte) []byte { - kem := config_id & 255 - txt := "KEM" - KEM_ID := ext.InttoBytes(kem, 2) - KEM := []byte(txt) - var SUITE_ID []byte - for i := 0; i < len(KEM); i++ { - SUITE_ID = append(SUITE_ID, KEM[i]) - } - SUITE_ID = append(SUITE_ID, KEM_ID[0]) - SUITE_ID = append(SUITE_ID, KEM_ID[1]) - - PRK := labeledExtract(nil, SUITE_ID, "eae_prk", DH) - return labeledExpand(PRK, SUITE_ID, "shared_secret", context, HASH_TYPE) -} - -func DeriveKeyPair(config_id int, SK []byte, PK []byte, SEED []byte) bool { - counter := 0 - kem := config_id & 255 - - txt := "KEM" - KEM_ID := ext.InttoBytes(kem, 2) - KEM := []byte(txt) - var SUITE_ID []byte - for i := 0; i < len(KEM); i++ { - SUITE_ID = append(SUITE_ID, KEM[i]) - } - SUITE_ID = append(SUITE_ID, KEM_ID[0]) - SUITE_ID = append(SUITE_ID, KEM_ID[1]) - - PRK := labeledExtract(nil, SUITE_ID, "dkp_prk", SEED) - var S []byte - if kem == 32 || kem == 33 { // RFC7748 - S = labeledExpand(PRK, SUITE_ID, "sk", nil, EGS) - reverse(S) - if kem == 32 { - S[EGS-1] &= 248 - S[0] &= 127 - S[0] |= 64 - } else { - S[EGS-1] &= 252 - S[0] |= 128 - } - } else { - bit_mask := 0xff - if kem == 18 { - bit_mask = 1 - } - for i := 0; i < EGS; i++ { - S = append(S, 0) - } - for !ECDH_IN_RANGE(S) && counter < 256 { - var INFO [1]byte - INFO[0] = byte(counter) - S = labeledExpand(PRK, SUITE_ID, "candidate", INFO[:], EGS) - S[0] &= byte(bit_mask) - counter++ - } - } - for i := 0; i < EGS; i++ { - SK[i] = S[i] - } - ECDH_KEY_PAIR_GENERATE(nil, SK, PK) - if kem == 32 || kem == 33 { - reverse(PK) - } - if counter < 256 { - return true - } - return false -} - -func Encap(config_id int, skE []byte, pkE []byte, pkR []byte) []byte { - DH := make([]byte, EFS) - var kemcontext []byte - kem := config_id & 255 - - if kem == 32 || kem == 33 { - reverse(pkR) - ECDH_ECPSVDP_DH(skE, pkR, DH[:], 0) - reverse(pkR) - reverse(DH[:]) - } else { - ECDH_ECPSVDP_DH(skE, pkR, DH[:], 0) - } - for i := 0; i < len(pkE); i++ { - kemcontext = append(kemcontext, pkE[i]) - } - for i := 0; i < len(pkR); i++ { - kemcontext = append(kemcontext, pkR[i]) - } - return extractAndExpand(config_id, DH[:], kemcontext) -} - -func Decap(config_id int, skR []byte, pkE []byte, pkR []byte) []byte { - DH := make([]byte, EFS) - var kemcontext []byte - kem := config_id & 255 - - if kem == 32 || kem == 33 { - reverse(pkE) - ECDH_ECPSVDP_DH(skR, pkE, DH[:], 0) - reverse(pkE) - reverse(DH[:]) - } else { - ECDH_ECPSVDP_DH(skR, pkE, DH[:], 0) - } - - for i := 0; i < len(pkE); i++ { - kemcontext = append(kemcontext, pkE[i]) - } - for i := 0; i < len(pkR); i++ { - kemcontext = append(kemcontext, pkR[i]) - } - return extractAndExpand(config_id, DH[:], kemcontext) -} - -func AuthEncap(config_id int, skE []byte, skS []byte, pkE []byte, pkR []byte, pkS []byte) []byte { - pklen := len(pkE) - DH := make([]byte, EFS) - DH1 := make([]byte, EFS) - - kemcontext := make([]byte, 3*pklen) - kem := config_id & 255 - - if kem == 32 || kem == 33 { - reverse(pkR) - ECDH_ECPSVDP_DH(skE, pkR, DH[:], 0) - ECDH_ECPSVDP_DH(skS, pkR, DH1[:], 0) - reverse(pkR) - reverse(DH[:]) - reverse(DH1[:]) - } else { - ECDH_ECPSVDP_DH(skE, pkR, DH[:], 0) - ECDH_ECPSVDP_DH(skS, pkR, DH1[:], 0) - } - ZZ := make([]byte, 2*EFS) - for i := 0; i < EFS; i++ { - ZZ[i] = DH[i] - ZZ[EFS+i] = DH1[i] - } - - for i := 0; i < pklen; i++ { - kemcontext[i] = pkE[i] - kemcontext[pklen+i] = pkR[i] - kemcontext[2*pklen+i] = pkS[i] - } - return extractAndExpand(config_id, ZZ[:], kemcontext) -} - -func AuthDecap(config_id int, skR []byte, pkE []byte, pkR []byte, pkS []byte) []byte { - pklen := len(pkE) - DH := make([]byte, EFS) - DH1 := make([]byte, EFS) - kemcontext := make([]byte, 3*pklen) - - kem := config_id & 255 - - if kem == 32 || kem == 33 { - reverse(pkE) - reverse(pkS) - ECDH_ECPSVDP_DH(skR[:], pkE, DH[:], 0) - ECDH_ECPSVDP_DH(skR[:], pkS, DH1[:], 0) - reverse(pkE) - reverse(pkS) - reverse(DH[:]) - reverse(DH1[:]) - } else { - ECDH_ECPSVDP_DH(skR[:], pkE, DH[:], 0) - ECDH_ECPSVDP_DH(skR[:], pkS, DH1[:], 0) - } - ZZ := make([]byte, 2*EFS) - for i := 0; i < EFS; i++ { - ZZ[i] = DH[i] - ZZ[EFS+i] = DH1[i] - } - - for i := 0; i < pklen; i++ { - kemcontext[i] = pkE[i] - kemcontext[pklen+i] = pkR[i] - kemcontext[2*pklen+i] = pkS[i] - } - return extractAndExpand(config_id, ZZ[:], kemcontext) -} - -/* -func printBinary(array []byte) { - for i := 0; i < len(array); i++ { - fmt.Printf("%02x", array[i]) - } - fmt.Printf("\n") -} -*/ - -func KeySchedule(config_id int, mode int, Z []byte, info []byte, psk []byte, pskID []byte) ([]byte, []byte, []byte) { - var context []byte - - kem := config_id & 255 - kdf := (config_id >> 8) & 3 - aead := (config_id >> 10) & 3 - - txt := "HPKE" - KEM := []byte(txt) - var SUITE_ID []byte - for i := 0; i < len(KEM); i++ { - SUITE_ID = append(SUITE_ID, KEM[i]) - } - num := ext.InttoBytes(kem, 2) - SUITE_ID = append(SUITE_ID, num[0]) - SUITE_ID = append(SUITE_ID, num[1]) - num = ext.InttoBytes(kdf, 2) - SUITE_ID = append(SUITE_ID, num[0]) - SUITE_ID = append(SUITE_ID, num[1]) - num = ext.InttoBytes(aead, 2) - SUITE_ID = append(SUITE_ID, num[0]) - SUITE_ID = append(SUITE_ID, num[1]) - - ar := ext.InttoBytes(mode, 1) - for i := 0; i < len(ar); i++ { - context = append(context, ar[i]) - } - - H := labeledExtract(nil, SUITE_ID, "psk_id_hash", pskID) - for i := 0; i < HASH_TYPE; i++ { - context = append(context, H[i]) - } - H = labeledExtract(nil, SUITE_ID, "info_hash", info) - for i := 0; i < HASH_TYPE; i++ { - context = append(context, H[i]) - } - //H=labeledExtract(nil,SUITE_ID,"psk_hash",psk) - //secret:=labeledExtract(H,SUITE_ID,"secret",Z) - - secret := labeledExtract(Z, SUITE_ID, "secret", psk) - - key := labeledExpand(secret, SUITE_ID, "key", context, AESKEY) - nonce := labeledExpand(secret, SUITE_ID, "base_nonce", context, 12) - exp_secret := labeledExpand(secret, SUITE_ID, "exp", context, HASH_TYPE) - - return key, nonce, exp_secret -} diff --git a/nekryptology/pkg/core/curves/native/bls48581/mpin256.go b/nekryptology/pkg/core/curves/native/bls48581/mpin256.go deleted file mode 100644 index dd1d970..0000000 --- a/nekryptology/pkg/core/curves/native/bls48581/mpin256.go +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (c) 2012-2020 MIRACL UK Ltd. - * - * This file is part of MIRACL Core - * (see https://github.com/miracl/ext.. - * - * 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. - */ - -/* MPIN 256-bit API Functions */ - -package bls48581 - -import "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves/native/bls48581/ext" - -//import "fmt" - -const MFS int = int(MODBYTES) -const MGS int = int(MODBYTES) -const BAD_PARAMS int = -11 -const INVALID_POINT int = -14 -const WRONG_ORDER int = -18 -const BAD_PIN int = -19 - -/* Configure your PIN here */ - -const MAXPIN int32 = 10000 /* PIN less than this */ -const PBLEN int32 = 14 /* Number of bits in PIN */ - -func MPIN_HASH_ID(sha int, ID []byte) []byte { - return ext.GPhashit(ext.MC_SHA2, sha, int(MODBYTES), 0, nil, -1, ID) - //return mhashit(sha, 0, ID) -} - -func roundup(a int, b int) int { - return (((a)-1)/(b) + 1) -} - -func MPIN_ENCODE_TO_CURVE(DST []byte, ID []byte, HCID []byte) { - q := NewBIGints(Modulus) - k := q.Nbits() - r := NewBIGints(CURVE_Order) - m := r.Nbits() - L := roundup(k+roundup(m, 2), 8) - var fd = make([]byte, L) - OKM := ext.XMD_Expand(ext.MC_SHA2, HASH_TYPE, L, DST, ID) - - for j := 0; j < L; j++ { - fd[j] = OKM[j] - } - dx := DBIG_fromBytes(fd) - u := NewFPbig(dx.Mod(q)) - P := ECP_map2point(u) - - P.Cfp() - P.Affine() - P.ToBytes(HCID, false) -} - -/* create random secret S */ -func MPIN_RANDOM_GENERATE(rng *ext.RAND, S []byte) int { - r := NewBIGints(CURVE_Order) - s := Randtrunc(r, 16*AESKEY, rng) - s.ToBytes(S) - return 0 -} - -func MPIN_EXTRACT_PIN(CID []byte, pin int, TOKEN []byte) int { - P := ECP_fromBytes(TOKEN) - if P.Is_infinity() { - return INVALID_POINT - } - R := ECP_fromBytes(CID) - if R.Is_infinity() { - return INVALID_POINT - } - R = R.pinmul(int32(pin)%MAXPIN, PBLEN) - P.Sub(R) - P.ToBytes(TOKEN, false) - return 0 -} - -/* Implement step 2 on client side of MPin protocol */ -func MPIN_CLIENT_2(X []byte, Y []byte, SEC []byte) int { - r := NewBIGints(CURVE_Order) - P := ECP_fromBytes(SEC) - if P.Is_infinity() { - return INVALID_POINT - } - - px := FromBytes(X) - py := FromBytes(Y) - px.Add(py) - px.Mod(r) - - P = G1mul(P, px) - P.Neg() - P.ToBytes(SEC, false) - return 0 -} - -func MPIN_GET_CLIENT_SECRET(S []byte, IDHTC []byte, CST []byte) int { - s := FromBytes(S) - P := ECP_fromBytes(IDHTC) - if P.Is_infinity() { - return INVALID_POINT - } - G1mul(P, s).ToBytes(CST, false) - return 0 -} - -/* Implement step 1 on client side of MPin protocol */ -func MPIN_CLIENT_1(CID []byte, rng *ext.RAND, X []byte, pin int, TOKEN []byte, SEC []byte, xID []byte) int { - r := NewBIGints(CURVE_Order) - var x *BIG - if rng != nil { - x = Randtrunc(r, 16*AESKEY, rng) - x.ToBytes(X) - } else { - x = FromBytes(X) - } - - P := ECP_fromBytes(CID) - if P.Is_infinity() { - return INVALID_POINT - } - - T := ECP_fromBytes(TOKEN) - if T.Is_infinity() { - return INVALID_POINT - } - - W := P.pinmul(int32(pin)%MAXPIN, PBLEN) - T.Add(W) - - P = G1mul(P, x) - P.ToBytes(xID, false) - - T.ToBytes(SEC, false) - return 0 -} - -/* Extract Server Secret SST=S*Q where Q is fixed generator in G2 and S is master secret */ -func MPIN_GET_SERVER_SECRET(S []byte, SST []byte) int { - Q := ECP8_generator() - s := FromBytes(S) - Q = G2mul(Q, s) - Q.ToBytes(SST, false) - return 0 -} - -/* Implement step 2 of MPin protocol on server side */ -func MPIN_SERVER(HID []byte, Y []byte, SST []byte, xID []byte, mSEC []byte) int { - Q := ECP8_generator() - - sQ := ECP8_fromBytes(SST) - if sQ.Is_infinity() { - return INVALID_POINT - } - - if xID == nil { - return BAD_PARAMS - } - R := ECP_fromBytes(xID) - if R.Is_infinity() { - return INVALID_POINT - } - y := FromBytes(Y) - if HID == nil { - return BAD_PARAMS - } - P := ECP_fromBytes(HID) - if P.Is_infinity() { - return INVALID_POINT - } - - P = G1mul(P, y) - P.Add(R) - R = ECP_fromBytes(mSEC) - if R.Is_infinity() { - return INVALID_POINT - } - - var g *FP48 - g = Ate2(Q, R, sQ, P) - g = Fexp(g) - - if !g.Isunity() { - return BAD_PIN - } - return 0 -} diff --git a/nekryptology/pkg/core/curves/native/bls48581/pair8.go b/nekryptology/pkg/core/curves/native/bls48581/pair8.go index 123f253..39f988f 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/pair8.go +++ b/nekryptology/pkg/core/curves/native/bls48581/pair8.go @@ -21,111 +21,98 @@ package bls48581 +import ( + "arena" +) + //import "fmt" // Point doubling for pairings -func dbl(A *ECP8, AA *FP8, BB *FP8, CC *FP8) { - CC.copy(A.getx()) //X - YY := NewFP8copy(A.gety()) //Y - BB.copy(A.getz()) //Z - AA.copy(YY) //Y - AA.Mul(BB) //YZ - CC.Sqr() //X^2 - YY.Sqr() //Y^2 - BB.Sqr() //Z^2 +func dbl(A *ECP8, AA *FP8, BB *FP8, CC *FP8, mem *arena.Arena) { + CC.copy(A.getx()) //X + YY := NewFP8copy(A.gety(), mem) //Y + BB.copy(A.getz()) //Z + AA.copy(YY) //Y + AA.Mul(BB, mem) //YZ + CC.Sqr(mem) //X^2 + YY.Sqr(mem) //Y^2 + BB.Sqr(mem) //Z^2 - AA.Add(AA) - AA.Neg() + AA.Add(AA, mem) + AA.Neg(mem) AA.norm() //-2AA - AA.times_i() + AA.times_i(mem) sb := 3 * CURVE_B_I - BB.imul(sb) - CC.imul(3) - if SEXTIC_TWIST == D_TYPE { - YY.times_i() - CC.times_i() - } - if SEXTIC_TWIST == M_TYPE { - BB.times_i() - } - BB.Sub(YY) + BB.imul(sb, mem) + CC.imul(3, mem) + YY.times_i(mem) + CC.times_i(mem) + BB.Sub(YY, mem) BB.norm() - A.Dbl() + A.Dbl(mem) } // Point addition for pairings -func add(A *ECP8, B *ECP8, AA *FP8, BB *FP8, CC *FP8) { - AA.copy(A.getx()) // X1 - CC.copy(A.gety()) // Y1 - T1 := NewFP8copy(A.getz()) // Z1 - BB.copy(A.getz()) // Z1 +func add(A *ECP8, B *ECP8, AA *FP8, BB *FP8, CC *FP8, mem *arena.Arena) { + AA.copy(A.getx()) // X1 + CC.copy(A.gety()) // Y1 + T1 := NewFP8copy(A.getz(), mem) // Z1 + BB.copy(A.getz()) // Z1 - T1.Mul(B.gety()) // T1=Z1.Y2 - BB.Mul(B.getx()) // T2=Z1.X2 + T1.Mul(B.gety(), mem) // T1=Z1.Y2 + BB.Mul(B.getx(), mem) // T2=Z1.X2 - AA.Sub(BB) + AA.Sub(BB, mem) AA.norm() // X1=X1-Z1.X2 - CC.Sub(T1) + CC.Sub(T1, mem) CC.norm() // Y1=Y1-Z1.Y2 T1.copy(AA) // T1=X1-Z1.X2 - if SEXTIC_TWIST == M_TYPE { - AA.times_i() - AA.norm() - } + T1.Mul(B.gety(), mem) // T1=(X1-Z1.X2).Y2 - T1.Mul(B.gety()) // T1=(X1-Z1.X2).Y2 - - BB.copy(CC) // T2=Y1-Z1.Y2 - BB.Mul(B.getx()) // T2=(Y1-Z1.Y2).X2 - BB.Sub(T1) + BB.copy(CC) // T2=Y1-Z1.Y2 + BB.Mul(B.getx(), mem) // T2=(Y1-Z1.Y2).X2 + BB.Sub(T1, mem) BB.norm() // T2=(Y1-Z1.Y2).X2 - (X1-Z1.X2).Y2 - CC.Neg() + CC.Neg(mem) CC.norm() // Y1=-(Y1-Z1.Y2).Xs - A.Add(B) + A.Add(B, mem) } -func line(A *ECP8, B *ECP8, Qx *FP, Qy *FP) *FP48 { - AA := NewFP8() - BB := NewFP8() - CC := NewFP8() +func line(A *ECP8, B *ECP8, Qx *FP, Qy *FP, mem *arena.Arena) *FP48 { + AA := NewFP8(mem) + BB := NewFP8(mem) + CC := NewFP8(mem) var a *FP16 var b *FP16 var c *FP16 if A == B { - dbl(A, AA, BB, CC) + dbl(A, AA, BB, CC, mem) } else { - add(A, B, AA, BB, CC) + add(A, B, AA, BB, CC, mem) } - CC.tmul(Qx) - AA.tmul(Qy) + CC.tmul(Qx, mem) + AA.tmul(Qy, mem) - a = NewFP16fp8s(AA, BB) + a = NewFP16fp8s(AA, BB, mem) - if SEXTIC_TWIST == D_TYPE { - b = NewFP16fp8(CC) // L(0,1) | L(0,0) | L(1,0) - c = NewFP16() - } - if SEXTIC_TWIST == M_TYPE { - b = NewFP16() - c = NewFP16fp8(CC) - c.times_i() - } + b = NewFP16fp8(CC, mem) // L(0,1) | L(0,0) | L(1,0) + c = NewFP16(mem) - r := NewFP48fp16s(a, b, c) + r := NewFP48fp16s(a, b, c, mem) r.stype = FP_SPARSER return r } /* prepare ate parameter, n=6u+2 (BN) or n=u (BLS), n3=3*n */ -func lbits(n3 *BIG, n *BIG) int { - n.copy(NewBIGints(CURVE_Bnx)) +func lbits(n3 *BIG, n *BIG, mem *arena.Arena) int { + n.copy(NewBIGints(CURVE_Bnx, mem)) n3.copy(n) n3.pmul(3) n3.norm() @@ -133,40 +120,38 @@ func lbits(n3 *BIG, n *BIG) int { } /* prepare for multi-pairing */ -func Initmp() []*FP48 { +func Initmp(mem *arena.Arena) []*FP48 { var r []*FP48 for i := ATE_BITS - 1; i >= 0; i-- { - r = append(r, NewFP48int(1)) + r = append(r, NewFP48int(1, mem)) } return r } /* basic Miller loop */ -func Miller(r []*FP48) *FP48 { - res := NewFP48int(1) +func Miller(r []*FP48, mem *arena.Arena) *FP48 { + res := NewFP48int(1, mem) for i := ATE_BITS - 1; i >= 1; i-- { - res.Sqr() - res.ssmul(r[i]) + res.Sqr(mem) + res.ssmul(r[i], mem) r[i].zero() } - if SIGN_OF_X == NEGATIVEX { - res.conj() - } - res.ssmul(r[0]) + res.conj(mem) + res.ssmul(r[0], mem) r[0].zero() return res } // Store precomputed line details in an FP8 func pack(AA *FP8, BB *FP8, CC *FP8) *FP16 { - i := NewFP8copy(CC) - i.Invert(nil) - a := NewFP8copy(AA) - a.Mul(i) - b := NewFP8copy(BB) - b.Mul(i) - return NewFP16fp8s(a, b) + i := NewFP8copy(CC, nil) + i.Invert(nil, nil) + a := NewFP8copy(AA, nil) + a.Mul(i, nil) + b := NewFP8copy(BB, nil) + b.Mul(i, nil) + return NewFP16fp8s(a, b, nil) } // Unpack G2 line function details and include G1 @@ -175,52 +160,45 @@ func unpack(T *FP16, Qx *FP, Qy *FP) *FP48 { var b *FP16 var c *FP16 - a = NewFP16copy(T) - a.geta().tmul(Qy) - t := NewFP8fp(Qx) - if SEXTIC_TWIST == D_TYPE { - b = NewFP16fp8(t) - c = NewFP16() - } - if SEXTIC_TWIST == M_TYPE { - b = NewFP16() - c = NewFP16fp8(t) - c.times_i() - } - v := NewFP48fp16s(a, b, c) + a = NewFP16copy(T, nil) + a.geta().tmul(Qy, nil) + t := NewFP8fp(Qx, nil) + b = NewFP16fp8(t, nil) + c = NewFP16(nil) + v := NewFP48fp16s(a, b, c, nil) v.stype = FP_SPARSEST return v } func precomp(GV *ECP8) []*FP16 { - n := NewBIG() - n3 := NewBIG() - AA := NewFP8() - BB := NewFP8() - CC := NewFP8() + n := NewBIG(nil) + n3 := NewBIG(nil) + AA := NewFP8(nil) + BB := NewFP8(nil) + CC := NewFP8(nil) var bt int - P := NewECP8() + P := NewECP8(nil) P.Copy(GV) - A := NewECP8() + A := NewECP8(nil) A.Copy(P) - MP := NewECP8() + MP := NewECP8(nil) MP.Copy(P) - MP.Neg() + MP.Neg(nil) - nb := lbits(n3, n) + nb := lbits(n3, n, nil) var T []*FP16 for i := nb - 2; i >= 1; i-- { - dbl(A, AA, BB, CC) + dbl(A, AA, BB, CC, nil) T = append(T, pack(AA, BB, CC)) bt = n3.bit(i) - n.bit(i) if bt == 1 { - add(A, P, AA, BB, CC) + add(A, P, AA, BB, CC, nil) T = append(T, pack(AA, BB, CC)) } if bt == -1 { - add(A, MP, AA, BB, CC) + add(A, MP, AA, BB, CC, nil) T = append(T, pack(AA, BB, CC)) } } @@ -228,22 +206,22 @@ func precomp(GV *ECP8) []*FP16 { } func Another_pc(r []*FP48, T []*FP16, QV *ECP) { - n := NewBIG() - n3 := NewBIG() + n := NewBIG(nil) + n3 := NewBIG(nil) var lv, lv2 *FP48 var bt, j int - if QV.Is_infinity() { + if QV.Is_infinity(nil) { return } - Q := NewECP() + Q := NewECP(nil) Q.Copy(QV) - Q.Affine() - Qx := NewFPcopy(Q.getx()) - Qy := NewFPcopy(Q.gety()) + Q.Affine(nil) + Qx := NewFPcopy(Q.getx(), nil) + Qy := NewFPcopy(Q.gety(), nil) - nb := lbits(n3, n) + nb := lbits(n3, n, nil) j = 0 for i := nb - 2; i >= 1; i-- { lv = unpack(T[j], Qx, Qy) @@ -252,625 +230,452 @@ func Another_pc(r []*FP48, T []*FP16, QV *ECP) { if bt == 1 { lv2 = unpack(T[j], Qx, Qy) j += 1 - lv.smul(lv2) + lv.smul(lv2, nil) } if bt == -1 { lv2 = unpack(T[j], Qx, Qy) j += 1 - lv.smul(lv2) + lv.smul(lv2, nil) } - r[i].ssmul(lv) + r[i].ssmul(lv, nil) } } /* Accumulate another set of line functions for n-pairing */ -func Another(r []*FP48, P1 *ECP8, Q1 *ECP) { - n := NewBIG() - n3 := NewBIG() +func Another(r []*FP48, P1 *ECP8, Q1 *ECP, mem *arena.Arena) { + n := NewBIG(mem) + n3 := NewBIG(mem) var lv, lv2 *FP48 - if Q1.Is_infinity() { + if Q1.Is_infinity(mem) { return } // P is needed in affine form for line function, Q for (Qx,Qy) extraction - P := NewECP8() + P := NewECP8(mem) P.Copy(P1) - Q := NewECP() + Q := NewECP(mem) Q.Copy(Q1) - P.Affine() - Q.Affine() + P.Affine(mem) + Q.Affine(mem) - Qx := NewFPcopy(Q.getx()) - Qy := NewFPcopy(Q.gety()) + Qx := NewFPcopy(Q.getx(), mem) + Qy := NewFPcopy(Q.gety(), mem) - A := NewECP8() + A := NewECP8(mem) A.Copy(P) - MP := NewECP8() + MP := NewECP8(mem) MP.Copy(P) - MP.Neg() + MP.Neg(mem) - nb := lbits(n3, n) + nb := lbits(n3, n, mem) for i := nb - 2; i >= 1; i-- { - lv = line(A, A, Qx, Qy) + lv = line(A, A, Qx, Qy, mem) bt := n3.bit(i) - n.bit(i) if bt == 1 { - lv2 = line(A, P, Qx, Qy) - lv.smul(lv2) + lv2 = line(A, P, Qx, Qy, mem) + lv.smul(lv2, mem) } if bt == -1 { - lv2 = line(A, MP, Qx, Qy) - lv.smul(lv2) + lv2 = line(A, MP, Qx, Qy, mem) + lv.smul(lv2, mem) } - r[i].ssmul(lv) + r[i].ssmul(lv, mem) } } /* Optimal R-ate pairing */ func Ate(P1 *ECP8, Q1 *ECP) *FP48 { - n := NewBIG() - n3 := NewBIG() + n := NewBIG(nil) + n3 := NewBIG(nil) var lv, lv2 *FP48 - if Q1.Is_infinity() { - return NewFP48int(1) + if Q1.Is_infinity(nil) { + return NewFP48int(1, nil) } - P := NewECP8() + P := NewECP8(nil) P.Copy(P1) - P.Affine() - Q := NewECP() + P.Affine(nil) + Q := NewECP(nil) Q.Copy(Q1) - Q.Affine() + Q.Affine(nil) - Qx := NewFPcopy(Q.getx()) - Qy := NewFPcopy(Q.gety()) + Qx := NewFPcopy(Q.getx(), nil) + Qy := NewFPcopy(Q.gety(), nil) - A := NewECP8() - r := NewFP48int(1) + A := NewECP8(nil) + r := NewFP48int(1, nil) A.Copy(P) - NP := NewECP8() + NP := NewECP8(nil) NP.Copy(P) - NP.Neg() + NP.Neg(nil) - nb := lbits(n3, n) + nb := lbits(n3, n, nil) for i := nb - 2; i >= 1; i-- { - r.Sqr() - lv = line(A, A, Qx, Qy) + r.Sqr(nil) + lv = line(A, A, Qx, Qy, nil) bt := n3.bit(i) - n.bit(i) if bt == 1 { - lv2 = line(A, P, Qx, Qy) - lv.smul(lv2) + lv2 = line(A, P, Qx, Qy, nil) + lv.smul(lv2, nil) } if bt == -1 { - lv2 = line(A, NP, Qx, Qy) - lv.smul(lv2) + lv2 = line(A, NP, Qx, Qy, nil) + lv.smul(lv2, nil) } - r.ssmul(lv) + r.ssmul(lv, nil) } - if SIGN_OF_X == NEGATIVEX { - r.conj() - } + r.conj(nil) return r } /* Optimal R-ate double pairing e(P,Q).e(R,S) */ func Ate2(P1 *ECP8, Q1 *ECP, R1 *ECP8, S1 *ECP) *FP48 { - n := NewBIG() - n3 := NewBIG() + n := NewBIG(nil) + n3 := NewBIG(nil) var lv, lv2 *FP48 - if Q1.Is_infinity() { + if Q1.Is_infinity(nil) { return Ate(R1, S1) } - if S1.Is_infinity() { + if S1.Is_infinity(nil) { return Ate(P1, Q1) } - P := NewECP8() + P := NewECP8(nil) P.Copy(P1) - P.Affine() - Q := NewECP() + P.Affine(nil) + Q := NewECP(nil) Q.Copy(Q1) - Q.Affine() - R := NewECP8() + Q.Affine(nil) + R := NewECP8(nil) R.Copy(R1) - R.Affine() - S := NewECP() + R.Affine(nil) + S := NewECP(nil) S.Copy(S1) - S.Affine() + S.Affine(nil) - Qx := NewFPcopy(Q.getx()) - Qy := NewFPcopy(Q.gety()) - Sx := NewFPcopy(S.getx()) - Sy := NewFPcopy(S.gety()) + Qx := NewFPcopy(Q.getx(), nil) + Qy := NewFPcopy(Q.gety(), nil) + Sx := NewFPcopy(S.getx(), nil) + Sy := NewFPcopy(S.gety(), nil) - A := NewECP8() - B := NewECP8() - r := NewFP48int(1) + A := NewECP8(nil) + B := NewECP8(nil) + r := NewFP48int(1, nil) A.Copy(P) B.Copy(R) - NP := NewECP8() + NP := NewECP8(nil) NP.Copy(P) - NP.Neg() - NR := NewECP8() + NP.Neg(nil) + NR := NewECP8(nil) NR.Copy(R) - NR.Neg() + NR.Neg(nil) - nb := lbits(n3, n) + nb := lbits(n3, n, nil) for i := nb - 2; i >= 1; i-- { - r.Sqr() - lv = line(A, A, Qx, Qy) - lv2 = line(B, B, Sx, Sy) - lv.smul(lv2) - r.ssmul(lv) + r.Sqr(nil) + lv = line(A, A, Qx, Qy, nil) + lv2 = line(B, B, Sx, Sy, nil) + lv.smul(lv2, nil) + r.ssmul(lv, nil) bt := n3.bit(i) - n.bit(i) if bt == 1 { - lv = line(A, P, Qx, Qy) - lv2 = line(B, R, Sx, Sy) - lv.smul(lv2) - r.ssmul(lv) + lv = line(A, P, Qx, Qy, nil) + lv2 = line(B, R, Sx, Sy, nil) + lv.smul(lv2, nil) + r.ssmul(lv, nil) } if bt == -1 { - lv = line(A, NP, Qx, Qy) - lv2 = line(B, NR, Sx, Sy) - lv.smul(lv2) - r.ssmul(lv) + lv = line(A, NP, Qx, Qy, nil) + lv2 = line(B, NR, Sx, Sy, nil) + lv.smul(lv2, nil) + r.ssmul(lv, nil) } } - if SIGN_OF_X == NEGATIVEX { - r.conj() - } + r.conj(nil) return r } /* final exponentiation - keep separate for multi-pairings and to avoid thrashing stack */ func Fexp(m *FP48) *FP48 { - f := NewFP2bigs(NewBIGints(Fra), NewBIGints(Frb)) - x := NewBIGints(CURVE_Bnx) - r := NewFP48copy(m) + mem := arena.NewArena() + f := NewFP2bigs(NewBIGints(Fra, mem), NewBIGints(Frb, mem), mem) + x := NewBIGints(CURVE_Bnx, mem) + r := NewFP48copy(m, nil) // var t1, t2 *FP48 /* Easy part of final exp */ - lv := NewFP48copy(r) + lv := NewFP48copy(r, mem) - lv.Invert() - r.conj() + lv.Invert(mem) + r.conj(mem) - r.Mul(lv) + r.Mul(lv, mem) lv.Copy(r) - r.frob(f, 8) - r.Mul(lv) + r.frob(f, 8, mem) + r.Mul(lv, mem) /* Hard part of final exp */ // See https://eprint.iacr.org/2020/875.pdf - y1 := NewFP48copy(r) - y1.uSqr() - y1.Mul(r) // y1=r^3 + y1 := NewFP48copy(r, mem) + y1.uSqr(mem) + y1.Mul(r, mem) // y1=r^3 - y0 := NewFP48copy(r.Pow(x)) - if SIGN_OF_X == NEGATIVEX { - y0.conj() - } - t0 := NewFP48copy(r) - t0.conj() + y0 := NewFP48copy(r.Pow(x, mem), mem) + y0.conj(mem) + t0 := NewFP48copy(r, mem) + t0.conj(mem) r.Copy(y0) - r.Mul(t0) + r.Mul(t0, mem) - y0.Copy(r.Pow(x)) - if SIGN_OF_X == NEGATIVEX { - y0.conj() - } + y0.Copy(r.Pow(x, mem)) + y0.conj(mem) t0.Copy(r) - t0.conj() + t0.conj(mem) r.Copy(y0) - r.Mul(t0) + r.Mul(t0, mem) // ^(x+p) - y0.Copy(r.Pow(x)) - if SIGN_OF_X == NEGATIVEX { - y0.conj() - } + y0.Copy(r.Pow(x, mem)) + y0.conj(mem) t0.Copy(r) - t0.frob(f, 1) + t0.frob(f, 1, mem) r.Copy(y0) - r.Mul(t0) + r.Mul(t0, mem) // ^(x^2+p^2) - y0.Copy(r.Pow(x)) - y0.Copy(y0.Pow(x)) + y0.Copy(r.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) t0.Copy(r) - t0.frob(f, 2) + t0.frob(f, 2, mem) r.Copy(y0) - r.Mul(t0) + r.Mul(t0, mem) // ^(x^4+p^4) - y0.Copy(r.Pow(x)) - y0.Copy(y0.Pow(x)) - y0.Copy(y0.Pow(x)) - y0.Copy(y0.Pow(x)) + y0.Copy(r.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) t0.Copy(r) - t0.frob(f, 4) + t0.frob(f, 4, mem) r.Copy(y0) - r.Mul(t0) + r.Mul(t0, mem) // ^(x^8+p^8-1) - y0.Copy(r.Pow(x)) - y0.Copy(y0.Pow(x)) - y0.Copy(y0.Pow(x)) - y0.Copy(y0.Pow(x)) - y0.Copy(y0.Pow(x)) - y0.Copy(y0.Pow(x)) - y0.Copy(y0.Pow(x)) - y0.Copy(y0.Pow(x)) + y0.Copy(r.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) + y0.Copy(y0.Pow(x, mem)) t0.Copy(r) - t0.frob(f, 8) - y0.Mul(t0) + t0.frob(f, 8, mem) + y0.Mul(t0, mem) t0.Copy(r) - t0.conj() + t0.conj(mem) r.Copy(y0) - r.Mul(t0) + r.Mul(t0, mem) - r.Mul(y1) - r.reduce() + r.Mul(y1, mem) + r.reduce(mem) + mem.Free() - /* - // Ghamman & Fouotsa Method - - t7 := NewFP48copy(r) - t7.usqr() - - if x.parity() == 1 { - t2 = r.Pow(x) - t1 = NewFP48copy(t2) - t1.usqr() - t2 = t2.Pow(x) - } else { - t1 = t7.Pow(x) - x.fshr(1) - t2 = t1.Pow(x) - x.fshl(1) - } - - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3 := NewFP48copy(t1) - t3.conj() - t2.Mul(t3) - t2.Mul(r) - - r.Mul(t7) - - t1 = t2.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - t3.Copy(t1) - t3.frob(f, 14) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 13) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 12) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 11) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 10) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 9) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 8) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t2) - t3.conj() - t1.Mul(t3) - t3.Copy(t1) - t3.frob(f, 7) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 6) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 5) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 4) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 3) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 2) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - t3.Copy(t1) - t3.frob(f, 1) - r.Mul(t3) - t1 = t1.Pow(x) - if SIGN_OF_X == NEGATIVEX { - t1.conj() - } - - r.Mul(t1) - t2.frob(f, 15) - r.Mul(t2) - - r.reduce() - */ return r } /* GLV method */ -func glv(ee *BIG) []*BIG { +func glv(ee *BIG, mem *arena.Arena) []*BIG { var u []*BIG - q := NewBIGints(CURVE_Order) - x := NewBIGints(CURVE_Bnx) + q := NewBIGints(CURVE_Order, mem) + x := NewBIGints(CURVE_Bnx, mem) x2 := smul(x, x) x = smul(x2, x2) x2 = smul(x, x) bd := uint(q.nbits() - x2.nbits()) - u = append(u, NewBIGcopy(ee)) - u[0].ctmod(x2, bd) - u = append(u, NewBIGcopy(ee)) - u[1].ctdiv(x2, bd) + u = append(u, NewBIGcopy(ee, mem)) + u[0].ctmod(x2, bd, mem) + u = append(u, NewBIGcopy(ee, mem)) + u[1].ctdiv(x2, bd, mem) u[1].rsub(q) return u } /* Galbraith & Scott Method */ -func gs(ee *BIG) []*BIG { +func gs(ee *BIG, mem *arena.Arena) []*BIG { var u []*BIG - q := NewBIGints(CURVE_Order) - x := NewBIGints(CURVE_Bnx) + q := NewBIGints(CURVE_Order, mem) + x := NewBIGints(CURVE_Bnx, mem) bd := uint(q.nbits() - x.nbits()) - w := NewBIGcopy(ee) + w := NewBIGcopy(ee, mem) for i := 0; i < 15; i++ { - u = append(u, NewBIGcopy(w)) - u[i].ctmod(x, bd) - w.ctdiv(x, bd) - } - u = append(u, NewBIGcopy(w)) - if SIGN_OF_X == NEGATIVEX { - u[1].copy(Modneg(u[1], q)) - u[3].copy(Modneg(u[3], q)) - u[5].copy(Modneg(u[5], q)) - u[7].copy(Modneg(u[7], q)) - u[9].copy(Modneg(u[9], q)) - u[11].copy(Modneg(u[11], q)) - u[13].copy(Modneg(u[13], q)) - u[15].copy(Modneg(u[15], q)) + u = append(u, NewBIGcopy(w, mem)) + u[i].ctmod(x, bd, mem) + w.ctdiv(x, bd, mem) } + u = append(u, NewBIGcopy(w, mem)) + u[1].copy(Modneg(u[1], q, mem)) + u[3].copy(Modneg(u[3], q, mem)) + u[5].copy(Modneg(u[5], q, mem)) + u[7].copy(Modneg(u[7], q, mem)) + u[9].copy(Modneg(u[9], q, mem)) + u[11].copy(Modneg(u[11], q, mem)) + u[13].copy(Modneg(u[13], q, mem)) + u[15].copy(Modneg(u[15], q, mem)) return u } /* Multiply P by e in group G1 */ -func G1mul(P *ECP, e *BIG) *ECP { +func G1mul(P *ECP, e *BIG, mem *arena.Arena) *ECP { var R *ECP - q := NewBIGints(CURVE_Order) - ee := NewBIGcopy(e) - ee.Mod(q) - if USE_GLV { - R = NewECP() - R.Copy(P) - Q := NewECP() - Q.Copy(P) - Q.Affine() + q := NewBIGints(CURVE_Order, mem) + ee := NewBIGcopy(e, mem) + ee.Mod(q, mem) + R = NewECP(mem) + R.Copy(P) + Q := NewECP(mem) + Q.Copy(P) + Q.Affine(mem) - cru := NewFPbig(NewBIGints(CRu)) - t := NewBIGint(0) - u := glv(ee) - Q.getx().Mul(cru) + cru := NewFPbig(NewBIGints(CRu, mem), mem) + t := NewBIGint(0, mem) + u := glv(ee, mem) + Q.getx().Mul(cru, mem) - np := u[0].nbits() - t.copy(Modneg(u[0], q)) - nn := t.nbits() - if nn < np { - u[0].copy(t) - R.Neg() - } - - np = u[1].nbits() - t.copy(Modneg(u[1], q)) - nn = t.nbits() - if nn < np { - u[1].copy(t) - Q.Neg() - } - u[0].norm() - u[1].norm() - R = R.Mul2(u[0], Q, u[1]) - - } else { - R = P.clmul(e, q) + np := u[0].nbits() + t.copy(Modneg(u[0], q, mem)) + nn := t.nbits() + if nn < np { + u[0].copy(t) + R.Neg(mem) } + + np = u[1].nbits() + t.copy(Modneg(u[1], q, mem)) + nn = t.nbits() + if nn < np { + u[1].copy(t) + Q.Neg(mem) + } + u[0].norm() + u[1].norm() + R = R.Mul2(u[0], Q, u[1], mem) + return R } /* Multiply P by e in group G2 */ -func G2mul(P *ECP8, e *BIG) *ECP8 { +func G2mul(P *ECP8, e *BIG, mem *arena.Arena) *ECP8 { var R *ECP8 - q := NewBIGints(CURVE_Order) - ee := NewBIGcopy(e) - ee.Mod(q) - if USE_GS_G2 { - var Q []*ECP8 + q := NewBIGints(CURVE_Order, mem) + ee := NewBIGcopy(e, mem) + ee.Mod(q, mem) + var Q []*ECP8 - F := ECP8_frob_constants() - u := gs(ee) + F := ECP8_frob_constants() + u := gs(ee, mem) - t := NewBIGint(0) + t := NewBIGint(0, mem) - Q = append(Q, NewECP8()) - Q[0].Copy(P) - for i := 1; i < 16; i++ { - Q = append(Q, NewECP8()) - Q[i].Copy(Q[i-1]) - Q[i].frob(F, 1) - } - for i := 0; i < 16; i++ { - np := u[i].nbits() - t.copy(Modneg(u[i], q)) - nn := t.nbits() - if nn < np { - u[i].copy(t) - Q[i].Neg() - } - u[i].norm() - } - - R = Mul16(Q, u) - - } else { - R = P.Mul(e) + Q = append(Q, NewECP8(mem)) + Q[0].Copy(P) + for i := 1; i < 16; i++ { + Q = append(Q, NewECP8(mem)) + Q[i].Copy(Q[i-1]) + Q[i].frob(F, 1) } + for i := 0; i < 16; i++ { + np := u[i].nbits() + t.copy(Modneg(u[i], q, mem)) + nn := t.nbits() + if nn < np { + u[i].copy(t) + Q[i].Neg(mem) + } + u[i].norm() + } + + R = Mul16(Q, u, mem) return R } /* f=f^e */ /* Note that this method requires a lot of RAM! */ -func GTpow(d *FP48, e *BIG) *FP48 { - var r *FP48 - q := NewBIGints(CURVE_Order) - ee := NewBIGcopy(e) - ee.Mod(q) - if USE_GS_GT { - var g []*FP48 - f := NewFP2bigs(NewBIGints(Fra), NewBIGints(Frb)) - t := NewBIGint(0) +// func GTpow(d *FP48, e *BIG) *FP48 { +// var r *FP48 +// q := NewBIGints(CURVE_Order) +// ee := NewBIGcopy(e) +// ee.Mod(q) +// if USE_GS_GT { +// var g []*FP48 +// f := NewFP2bigs(NewBIGints(Fra), NewBIGints(Frb)) +// t := NewBIGint(0) - u := gs(ee) +// u := gs(ee) - g = append(g, NewFP48copy(d)) - for i := 1; i < 16; i++ { - g = append(g, NewFP48()) - g[i].Copy(g[i-1]) - g[i].frob(f, 1) - } - for i := 0; i < 16; i++ { - np := u[i].nbits() - t.copy(Modneg(u[i], q)) - nn := t.nbits() - if nn < np { - u[i].copy(t) - g[i].conj() - } - u[i].norm() - } - r = pow16(g, u) - } else { - r = d.Pow(ee) - } - return r -} +// g = append(g, NewFP48copy(d)) +// for i := 1; i < 16; i++ { +// g = append(g, NewFP48()) +// g[i].Copy(g[i-1]) +// g[i].frob(f, 1) +// } +// for i := 0; i < 16; i++ { +// np := u[i].nbits() +// t.copy(Modneg(u[i], q)) +// nn := t.nbits() +// if nn < np { +// u[i].copy(t) +// g[i].conj() +// } +// u[i].norm() +// } +// r = pow16(g, u) +// } else { +// r = d.Pow(ee) +// } +// return r +// } /* test G1 group membership */ -func G1member(P *ECP) bool { - if P.Is_infinity() { +func G1member(P *ECP, mem *arena.Arena) bool { + if P.Is_infinity(mem) { return false } - x := NewBIGints(CURVE_Bnx) - cru := NewFPbig(NewBIGints(CRu)) - W := NewECP() + x := NewBIGints(CURVE_Bnx, mem) + cru := NewFPbig(NewBIGints(CRu, mem), mem) + W := NewECP(mem) W.Copy(P) - W.getx().Mul(cru) - T := P.lmul(x) + W.getx().Mul(cru, mem) + T := P.lmul(x, mem, mem) if P.Equals(T) { return false } // P is of low order - T = T.Mul(x) - T = T.Mul(x) - T = T.Mul(x) - T = T.Mul(x) - T = T.Mul(x) - T = T.Mul(x) - T = T.Mul(x) - T.Neg() + T = T.Mul(x, mem, mem) + T = T.Mul(x, mem, mem) + T = T.Mul(x, mem, mem) + T = T.Mul(x, mem, mem) + T = T.Mul(x, mem, mem) + T = T.Mul(x, mem, mem) + T = T.Mul(x, mem, mem) + T.Neg(mem) if !W.Equals(T) { return false } @@ -889,19 +694,17 @@ func G1member(P *ECP) bool { } /* test G2 group membership */ -func G2member(P *ECP8) bool { - if P.Is_infinity() { +func G2member(P *ECP8, mem *arena.Arena) bool { + if P.Is_infinity(mem) { return false } F := ECP8_frob_constants() - x := NewBIGints(CURVE_Bnx) - W := NewECP8() + x := NewBIGints(CURVE_Bnx, mem) + W := NewECP8(mem) W.Copy(P) W.frob(F, 1) - T := P.Mul(x) - if SIGN_OF_X == NEGATIVEX { - T.Neg() - } + T := P.Mul(x, mem) + T.Neg(mem) /* R:=NewECP8(); R.Copy(W) R.frob(F,1) @@ -928,20 +731,20 @@ func GTcyclotomic(m *FP48) bool { if m.Isunity() { return false } - r := NewFP48copy(m) - r.conj() - r.Mul(m) + r := NewFP48copy(m, nil) + r.conj(nil) + r.Mul(m, nil) if !r.Isunity() { return false } - f := NewFP2bigs(NewBIGints(Fra), NewBIGints(Frb)) + f := NewFP2bigs(NewBIGints(Fra, nil), NewBIGints(Frb, nil), nil) r.Copy(m) - r.frob(f, 8) - w := NewFP48copy(r) - w.frob(f, 8) - w.Mul(m) + r.frob(f, 8, nil) + w := NewFP48copy(r, nil) + w.frob(f, 8, nil) + w.Mul(m, nil) if !w.Equals(r) { return false } @@ -953,16 +756,14 @@ func GTmember(m *FP48) bool { if !GTcyclotomic(m) { return false } - f := NewFP2bigs(NewBIGints(Fra), NewBIGints(Frb)) - x := NewBIGints(CURVE_Bnx) + f := NewFP2bigs(NewBIGints(Fra, nil), NewBIGints(Frb, nil), nil) + x := NewBIGints(CURVE_Bnx, nil) - r := NewFP48copy(m) - r.frob(f, 1) - t := m.Pow(x) + r := NewFP48copy(m, nil) + r.frob(f, 1, nil) + t := m.Pow(x, nil) - if SIGN_OF_X == NEGATIVEX { - t.conj() - } + t.conj(nil) if !r.Equals(t) { return false } diff --git a/nekryptology/pkg/core/curves/native/bls48581/rom.go b/nekryptology/pkg/core/curves/native/bls48581/rom_32.go similarity index 99% rename from nekryptology/pkg/core/curves/native/bls48581/rom.go rename to nekryptology/pkg/core/curves/native/bls48581/rom_32.go index 7e0400a..c6f7069 100644 --- a/nekryptology/pkg/core/curves/native/bls48581/rom.go +++ b/nekryptology/pkg/core/curves/native/bls48581/rom_32.go @@ -1,3 +1,5 @@ +//go:build js && wasm + /* * Copyright (c) 2012-2020 MIRACL UK Ltd. * diff --git a/nekryptology/pkg/core/curves/native/bls48581/rom_64.go b/nekryptology/pkg/core/curves/native/bls48581/rom_64.go new file mode 100644 index 0000000..e2adcbe --- /dev/null +++ b/nekryptology/pkg/core/curves/native/bls48581/rom_64.go @@ -0,0 +1,77 @@ +//go:build !js && !wasm + +/* + * Copyright (c) 2012-2020 MIRACL UK Ltd. + * + * This file is part of MIRACL Core + * (see https://github.com/miracl/core). + * + * 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. + */ + +/* Fixed Data in ROM - Field and Curve parameters */ + +package bls48581 + +// Base Bits= 60 +var Modulus = [...]Chunk{0xEDC154E6565912B, 0x8FDF721A4A48AC3, 0x7A5513170EE0A57, 0x394F4736DAF6836, 0xAF6E082ACD9CD30, 0xF3975444A48AE43, 0x22131BB3BE6C0F1, 0x12A0056E84F8D1, 0x76F313824E31D47, 0x1280F73FF34} +var ROI = [...]Chunk{0xEDC154E6565912A, 0x8FDF721A4A48AC3, 0x7A5513170EE0A57, 0x394F4736DAF6836, 0xAF6E082ACD9CD30, 0xF3975444A48AE43, 0x22131BB3BE6C0F1, 0x12A0056E84F8D1, 0x76F313824E31D47, 0x1280F73FF34} +var R2modp = [...]Chunk{0x79868479F1B5833, 0xFB6EBA8FCB82D07, 0x9CC8A7F1FD84C7F, 0x402C51CF5CC3CBB, 0x3F3114F078502C, 0xFC90829BDC8336E, 0xC7BE91DE9CA8EED, 0xD4D273BB17BFADB, 0x6EC7C9A81E792CA, 0x1DC317A6E4} +var SQRTm3 = [...]Chunk{0x51EDFC2A1D65A0A, 0xD62DAA292D8CDBF, 0x24112478269D616, 0x6C25D3CABF8AD71, 0xC8E9B16B5D3E4CD, 0xF50A03B738960EE, 0x1A664376FED4343, 0xBFFD8FB8925AE06, 0x600908C6A28DEAA, 0x1280F73F9A7} + +const MConst Chunk = 0x148B81FC39D5A7D + +var Fra = [...]Chunk{0x62EB6CFE42AEB25, 0xDB41942760AD3F9, 0xA7DF2570715ECE4, 0x90377B51208AC0F, 0x6848493E1C8C418, 0xF496307E298187E, 0x58740E3CAFD6B62, 0xF6067D047983E78, 0x49FA75CD7E73E55, 0xFD30DB501} +var Frb = [...]Chunk{0x62EB6CFE42AEB25, 0xDB41942760AD3F9, 0xA7DF2570715ECE4, 0x90377B51208AC0F, 0x6848493E1C8C418, 0xF496307E298187E, 0x58740E3CAFD6B62, 0xF6067D047983E78, 0x49FA75CD7E73E55, 0xFD30DB501} +var TWK = [...]Chunk{0x7B433D25F426953, 0xACE45923B9863D, 0xC28BBDFA2D37E16, 0x62FFCC8AFB4BC18, 0x661B4392F002C4F, 0x2ED27E951A14781, 0x670A6683B853246, 0xAEB8C9BA138A075, 0xC10075769CDDD9E, 0x3A65A537B} + +//*** rom curve parameters ***** +// Base Bits= 60 +// Ate Bits= 33 +// G2 Table size= 36 + +const CURVE_Cof_I int = 0 + +var CURVE_Cof = [...]Chunk{0x140000382, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} + +const CURVE_B_I int = 1 + +var CURVE_B = [...]Chunk{0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +var CURVE_Order = [...]Chunk{0x8A5FE6FCD671C01, 0xBE599467C24DA11, 0xC7CD0562303C4CC, 0x9D34C4C92016A85, 0xBC972C2E6E74196, 0x3F0B3CBE003FAD6, 0x615C0D6C635387A, 0xE2885E233A9CCC1, 0x2386F8A925, 0x0} +var CURVE_Gx = [...]Chunk{0xBCE8732315AF640, 0x74DA5D3A1E6D8C3, 0x57DB368B11786CB, 0x665D859236EBDBC, 0x46A9DF6F9645847, 0xEDFFB9F75445505, 0xE86868CF61ABDBA, 0x93F860DE3F257E0, 0x40F2BAF2B73DF1E, 0x2AF59B7AC3} +var CURVE_Gy = [...]Chunk{0xDBB5DE3E2587A70, 0xF37AEF7B926B576, 0xF77C2876D1B2E35, 0x78584C3EF22F487, 0xFFB98AEE53E80F6, 0xD41B720EF7BB7BE, 0xFEB8A52E991279D, 0xB398A488A553C9E, 0x31F91F86B3A2D1F, 0xCEFDA44F65} +var CURVE_HTPC = [...]Chunk{0x393F0BE031193EC, 0xC28896440758243, 0xDBE4AA8E70D4620, 0x6B27BD55EFD560E, 0x24A9624BEECD070, 0xE2626AD7C53B361, 0xDD845A98030C755, 0x29389B4E6A62C2D, 0x5AF94F05D8A9FD4, 0x92348CD5DC} + +var CURVE_Bnx = [...]Chunk{0x140000381, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} +var CRu = [...]Chunk{0x4DE9AC5E1C79B90, 0x5CD8E3F88E5DE82, 0xAB21F74F7421A20, 0x6694B9B60DB5D62, 0x73422B5FB82F431, 0xFF46A846B5FA6AA, 0x83D66C1E5FCBED6, 0x2096384F2AFA565, 0x8B75055DD5D1F4E, 0x2C6} +var CURVE_Pxaaa = [...]Chunk{0x34FD0B4ACE8BFAB, 0xB79766322154DEC, 0x4D80491F510317, 0x3CA0612F4005030, 0xBAAD1A8C42281A6, 0x3A2EF156C46FF79, 0x344DBCCB7DE64DB, 0x2775DEBABBEFC70, 0x71E4A38237FA45A, 0x5D615D9A78} +var CURVE_Pxaab = [...]Chunk{0x669B36676B47C57, 0x5556A01AFA143F1, 0x7630D979630FFD7, 0x6AFFA62504F0C3C, 0xABFEDF16214A7, 0x12307F4E1C3943A, 0xE1623E9526F6DA, 0xBC07E8B22BB6D98, 0x258512069B0E86A, 0x7C4973ECE2} +var CURVE_Pxaba = [...]Chunk{0x488156CA55A3E6A, 0xEF4CDED6B3F0B46, 0xCBDFBB879D5FEA8, 0x66F0D2A6D55F028, 0xC1DBD19242FFAE7, 0xCCBAB5AB6860161, 0xAE237CA7A6D6957, 0xAD83BC73A8A6CA9, 0xF1334E1B2EA1853, 0x1FCCC70198} +var CURVE_Pxabb = [...]Chunk{0x9A7033CBB7FEAFE, 0x10B8CB4E80BC3F0, 0x1C5257C200CA523, 0x43B1B279B9468C3, 0x5F63E1C776E6EC1, 0x393F8BE0CC218A9, 0x62F3E5821B7B92A, 0x54D4BFE8F5985AC, 0xEB6185C78D80129, 0xBE2218C25C} +var CURVE_Pxbaa = [...]Chunk{0x39C3A1C53F8CCE5, 0x5B5F746C9D4CBB7, 0xD55FC1889AA80C6, 0xEF492AE589274FA, 0x9E48199D5AC10B2, 0xC5805386699981F, 0xB1642B5675FF0E7, 0xA9DD63007C675D0, 0x35913A3C598E4CA, 0x38B91C600B} +var CURVE_Pxbab = [...]Chunk{0x2004D914A3C093A, 0x7960910FCE3370F, 0xA9F177612F097FC, 0x40B9C0B15DD7595, 0x3835D28997EB57B, 0x7BB037418181DF6, 0xEF0977A3D1A5867, 0xCDA088F7B8F35DC, 0x738603F1311E4E, 0xC96C7797EB} +var CURVE_Pxbba = [...]Chunk{0x41607E60750E057, 0x4B5B0E205C3354E, 0xCBE4324C22D6333, 0xAA5EFCF3432AAD1, 0xF293B13CED0FD0C, 0xA2C0B7A449CEF11, 0x9D13852B6DB908B, 0x8AEE660DEA41B3, 0x61EE3F0197A4989, 0xB9B7951C60} +var CURVE_Pxbbb = [...]Chunk{0xE19DA00FBC6AE34, 0x6AF2FC9E97C3F84, 0x9BD6AEBF9FC44E5, 0x90B7E2B0D458547, 0xA93F29CFF364A71, 0x719728A7F9F8CFC, 0xFAF47B5211CF741, 0x4AAA2B1E5D7A9DE, 0x2BDEC5282624C4F, 0x827D5C22FB} +var CURVE_Pyaaa = [...]Chunk{0x3EDD3FE4D2D7971, 0x45012AB12C0FF32, 0x9ABF77EEA6D6590, 0x336D8AE5163C159, 0x35AFA27748D90F7, 0xBFC435FAAB09062, 0x59A577E6F3B39E, 0x2F3024B918B4238, 0x75B5DFA49721645, 0xEB53356C3} +var CURVE_Pyaab = [...]Chunk{0x1471DB936CD5665, 0x8B423525FFC7B11, 0x2FA097D760E2E58, 0xD1892AB24E1DD21, 0x6B243B1F192C5C3, 0x64732FCBF3AFB09, 0xA325E6FBA01D729, 0x5FCADC2B75A422B, 0xE0FF144DA653181, 0x284DC75979} +var CURVE_Pyaba = [...]Chunk{0x8332A526A2A8474, 0xBC7C46FC3B8FDE6, 0x1D35D51A652269C, 0x36CA3295E5E2F0C, 0xC99D0E904115155, 0xD370514475F7D5, 0x216D5B119D3A48, 0x67669EF2C2FC503, 0x8523E421EFB703, 0xB36A201DD0} +var CURVE_Pyabb = [...]Chunk{0x6213DA92841589D, 0xB3D8B8A1E533731, 0x7BDA503EE5E578F, 0x817742770BA10D6, 0x224333FA40DCED2, 0x10E122D2742C89B, 0x60DCEE23DD8B0E7, 0x78762B1C2CDED33, 0xEDC0688223FBBD4, 0xAEC25A4621} +var CURVE_Pybaa = [...]Chunk{0x47831F982E50137, 0x857FDDDFCF7A43F, 0x30135945D137B08, 0xCA4E512B64F59F4, 0x7FA238CDCE8A1E2, 0x5F1129857ED85C7, 0xB43DD93B5A95980, 0x88325A2554DC541, 0xA9C46916503FA5A, 0xD209D5A223} +var CURVE_Pybab = [...]Chunk{0x4EEDC58CF90BEE4, 0xA59ED8226CF3A59, 0xFC198CAA72B679D, 0xF47C180D139E3AA, 0xE8C270841F6824, 0x55AB7504FA8342, 0xB16722B589D82E2, 0xD537B90421AD66E, 0x36B7A513D339D5A, 0x7D0D037457} +var CURVE_Pybba = [...]Chunk{0xD41FAEAFEB23986, 0xE884017D9AA62B3, 0x40FA639F53DCCC9, 0xAB8C74B2618B5BB, 0x5AE3A2864F22C1F, 0xE4C819A6DF98F42, 0xC0841B064155F14, 0xD17AF8A006F364F, 0xE65EA25C2D05DFD, 0x896767811B} +var CURVE_Pybbb = [...]Chunk{0x667FFCB732718B6, 0x5AC66E84069C55D, 0xD8C4AB33F748E, 0x333EC7192054173, 0x8E69C31E97E1AD0, 0xEF8ECA9A9533A3F, 0x6BE8E50C87549B6, 0x4F981B5E068F140, 0x9029D393A5C07E8, 0x35E2524FF8} + +//var CURVE_W=[2][10]Chunk {{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}} +//var CURVE_SB=[2][2][10]Chunk {{{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}},{{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}}} +//var CURVE_WB=[4][10]Chunk {{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}} +//var CURVE_BB=[4][4][10]Chunk {{{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}},{{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}},{{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}},{{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0},{0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}}} diff --git a/nekryptology/pkg/vdf/vdf.go b/nekryptology/pkg/vdf/vdf.go index 286d563..a5536b4 100644 --- a/nekryptology/pkg/vdf/vdf.go +++ b/nekryptology/pkg/vdf/vdf.go @@ -18,7 +18,7 @@ type VDF struct { finished bool } -//size of long integers in quadratic function group +// size of long integers in quadratic function group const sizeInBits = 2048 // New create a new instance of VDF. @@ -53,12 +53,31 @@ func (vdf *VDF) Execute() { vdf.finished = true } +func (vdf *VDF) ExecuteIteration(x_blob []byte) { + vdf.finished = false + + yBuf, proofBuf := GenerateVDFIteration(vdf.input[:], x_blob, vdf.difficulty, sizeInBits) + + copy(vdf.output[:], yBuf) + copy(vdf.output[258:], proofBuf) + + go func() { + vdf.outputChan <- vdf.output + }() + + vdf.finished = true +} + // Verify runs the verification of generated proof // currently on i7-6700K, verification takes about 350 ms func (vdf *VDF) Verify(proof [516]byte) bool { return VerifyVDF(vdf.input[:], proof[:], vdf.difficulty, sizeInBits) } +func (vdf *VDF) VerifyIteration(x_blob [258]byte, proof [516]byte, iterations uint32) bool { + return VerifyVDFIteration(vdf.input[:], x_blob[:], proof[:], vdf.difficulty, sizeInBits) +} + // IsFinished returns whether the vdf execution is finished or not. func (vdf *VDF) IsFinished() bool { return vdf.finished diff --git a/nekryptology/pkg/vdf/wesolowski.go b/nekryptology/pkg/vdf/wesolowski.go index 139b233..2051cc3 100644 --- a/nekryptology/pkg/vdf/wesolowski.go +++ b/nekryptology/pkg/vdf/wesolowski.go @@ -16,8 +16,8 @@ import ( "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/iqc" ) -//Creates L and k parameters from papers, based on how many iterations need to be -//performed, and how much memory should be used. +// Creates L and k parameters from papers, based on how many iterations need to be +// performed, and how much memory should be used. func approximateParameters(T uint32) (int, int, int) { //log_memory = math.log(10000000, 2) log_memory := math.Log(10000000) / math.Log(2) @@ -86,6 +86,20 @@ func GenerateVDFWithStopChan(seed []byte, iterations, int_size_bits uint32, stop } } +func GenerateVDFIteration(seed, x_blob []byte, iterations, int_size_bits uint32) ([]byte, []byte) { + int_size := (int_size_bits + 16) >> 4 + D := iqc.CreateDiscriminant(seed, int_size_bits) + x, _ := iqc.NewClassGroupFromBytesDiscriminant(x_blob[:(2*int_size)], D) + + y, proof := calculateVDF(D, x, iterations, int_size_bits, nil) + + if (y == nil) || (proof == nil) { + return nil, nil + } else { + return y.Serialize(), proof.Serialize() + } +} + func VerifyVDF(seed, proof_blob []byte, iterations, int_size_bits uint32) bool { int_size := (int_size_bits + 16) >> 4 @@ -97,6 +111,16 @@ func VerifyVDF(seed, proof_blob []byte, iterations, int_size_bits uint32) bool { return verifyProof(x, y, proof, iterations) } +func VerifyVDFIteration(seed, x_blob, proof_blob []byte, iterations, int_size_bits uint32) bool { + int_size := (int_size_bits + 16) >> 4 + D := iqc.CreateDiscriminant(seed, int_size_bits) + x, _ := iqc.NewClassGroupFromBytesDiscriminant(x_blob[:(2*int_size)], D) + y, _ := iqc.NewClassGroupFromBytesDiscriminant(proof_blob[:(2*int_size)], D) + proof, _ := iqc.NewClassGroupFromBytesDiscriminant(proof_blob[2*int_size:], D) + + return verifyProof(x, y, proof, iterations) +} + // Creates a random prime based on input x, y, T // Note – this differs from harmony-one's implementation, as the Fiat-Shamir // transform requires _all_ public parameters be input, or else there is the @@ -133,7 +157,7 @@ func getBlock(i, k, T int, B *big.Int) *big.Int { return iqc.FloorDivision(new(big.Int).Mul(p1, p2), B) } -//Optimized evalutation of h ^ (2^T // B) +// Optimized evalutation of h ^ (2^T // B) func evalOptimized(identity, h *iqc.ClassGroup, B *big.Int, T uint32, k, l int, C map[int]*iqc.ClassGroup) *iqc.ClassGroup { //k1 = k//2 var k1 int = k / 2 @@ -219,7 +243,7 @@ func evalOptimized(identity, h *iqc.ClassGroup, B *big.Int, T uint32, k, l int, return x } -//generate y = x ^ (2 ^T) and pi +// generate y = x ^ (2 ^T) and pi func generateProof(identity, x, y *iqc.ClassGroup, T uint32, k, l int, powers map[int]*iqc.ClassGroup) *iqc.ClassGroup { //x_s = x.serialize() x_s := x.Serialize() @@ -236,10 +260,12 @@ func generateProof(identity, x, y *iqc.ClassGroup, T uint32, k, l int, powers ma func calculateVDF(discriminant *big.Int, x *iqc.ClassGroup, iterations, int_size_bits uint32, stop <-chan struct{}) (y, proof *iqc.ClassGroup) { L, k, _ := approximateParameters(iterations) - loopCount := int(math.Ceil(float64(iterations) / float64(k*L))) + // NB: Dusk needs to do the disjoint set arithmetic, marking this spot down + // as the insertion point powers_to_calculate := make([]int, loopCount+2) + // link into next for i := 0; i < loopCount+1; i++ { powers_to_calculate[i] = i * k * L } diff --git a/nekryptology/pkg/zkp/schnorr/schnorr_test.go b/nekryptology/pkg/zkp/schnorr/schnorr_test.go index 47de66c..d897126 100644 --- a/nekryptology/pkg/zkp/schnorr/schnorr_test.go +++ b/nekryptology/pkg/zkp/schnorr/schnorr_test.go @@ -25,13 +25,13 @@ func TestZKPOverMultipleCurves(t *testing.T) { } for i, curve := range curveInstances { uniqueSessionId := sha3.New256().Sum([]byte("random seed")) - prover := NewProver(curve, nil, uniqueSessionId) + prover := NewProver(curve, nil, sha3.New256(), uniqueSessionId) secret := curve.Scalar.Random(rand.Reader) proof, err := prover.Prove(secret) require.NoError(t, err, fmt.Sprintf("failed in curve %d", i)) - err = Verify(proof, curve, nil, uniqueSessionId) + err = Verify(proof, curve, nil, sha3.New256(), uniqueSessionId) require.NoError(t, err, fmt.Sprintf("failed in curve %d", i)) } } diff --git a/node/.vscode/settings.json b/node/.vscode/settings.json new file mode 100644 index 0000000..aee3509 --- /dev/null +++ b/node/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "go.testEnvVars": { + "GOEXPERIMENT": "arenas" + } +} \ No newline at end of file diff --git a/node/app/db_console.go b/node/app/db_console.go index ed93749..bc8b354 100644 --- a/node/app/db_console.go +++ b/node/app/db_console.go @@ -23,6 +23,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "source.quilibrium.com/quilibrium/monorepo/node/config" "source.quilibrium.com/quilibrium/monorepo/node/execution/ceremony/application" + "source.quilibrium.com/quilibrium/monorepo/node/p2p" "source.quilibrium.com/quilibrium/monorepo/node/protobufs" "source.quilibrium.com/quilibrium/monorepo/node/tries" ) @@ -431,7 +432,7 @@ func (m model) View() string { list := []string{} for i, item := range m.filters { - str := item[0:12] + ".." + item[52:] + str := item[0:12] + ".." + item[len(item)-12:] if m.selectedFilter == item { list = append(list, selectedListStyle.Render(str)) } else if i == m.cursor { @@ -584,7 +585,7 @@ func (m model) View() string { for _, active := range app.ActiveParticipants { explorerContent += "\t" + base64.StdEncoding.EncodeToString( - active.KeyValue, + active.PublicKeySignatureEd448.PublicKey.KeyValue, ) + "\n" } @@ -624,7 +625,7 @@ func (m model) View() string { for _, active := range app.ActiveParticipants { explorerContent += "\t" + base64.StdEncoding.EncodeToString( - active.KeyValue, + active.PublicKeySignatureEd448.PublicKey.KeyValue, ) + "\n" } @@ -656,8 +657,10 @@ func (m model) View() string { ) + "\n" } case application.CEREMONY_APPLICATION_STATE_VALIDATING: + explorerContent += fmt.Sprintf( + "G1 Powers: %d\n", len(app.UpdatedTranscript.G1Powers), + ) explorerContent += "Preferred Next Round Participants: \n" - for _, next := range app.NextRoundPreferredParticipants { explorerContent += "\t" + base64.StdEncoding.EncodeToString( next.KeyValue, @@ -727,7 +730,10 @@ func consoleModel( 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }), - hex.EncodeToString(application.CEREMONY_ADDRESS), + hex.EncodeToString(append( + p2p.GetBloomFilter(application.CEREMONY_ADDRESS, 256, 3), + p2p.GetBloomFilterIndices(application.CEREMONY_ADDRESS, 65536, 24)..., + )), }, cursor: 0, conn: conn, diff --git a/node/app/wire.go b/node/app/wire.go index a3e09bb..588381f 100644 --- a/node/app/wire.go +++ b/node/app/wire.go @@ -8,7 +8,6 @@ import ( "go.uber.org/zap" "source.quilibrium.com/quilibrium/monorepo/node/config" "source.quilibrium.com/quilibrium/monorepo/node/consensus" - ceremonyConsensus "source.quilibrium.com/quilibrium/monorepo/node/consensus/ceremony" "source.quilibrium.com/quilibrium/monorepo/node/consensus/master" "source.quilibrium.com/quilibrium/monorepo/node/execution/ceremony" "source.quilibrium.com/quilibrium/monorepo/node/keys" @@ -38,6 +37,7 @@ var keyManagerSet = wire.NewSet( var storeSet = wire.NewSet( wire.FieldsOf(new(*config.Config), "DB"), store.NewPebbleDB, + wire.Bind(new(store.KVDB), new(*store.PebbleDB)), store.NewPebbleClockStore, store.NewPebbleKeyStore, store.NewPebbleDataProofStore, @@ -52,16 +52,8 @@ var pubSubSet = wire.NewSet( wire.Bind(new(p2p.PubSub), new(*p2p.BlossomSub)), ) -var dataConsensusSet = wire.NewSet( - wire.FieldsOf(new(*config.Config), "Engine"), - ceremonyConsensus.NewCeremonyDataClockConsensusEngine, - wire.Bind( - new(consensus.DataConsensusEngine), - new(*ceremonyConsensus.CeremonyDataClockConsensusEngine), - ), -) - var engineSet = wire.NewSet( + wire.FieldsOf(new(*config.Config), "Engine"), ceremony.NewCeremonyExecutionEngine, ) @@ -80,7 +72,6 @@ func NewNode(*config.Config) (*Node, error) { storeSet, pubSubSet, engineSet, - dataConsensusSet, consensusSet, newNode, )) diff --git a/node/app/wire_gen.go b/node/app/wire_gen.go index d391163..40fb5dc 100644 --- a/node/app/wire_gen.go +++ b/node/app/wire_gen.go @@ -11,9 +11,8 @@ import ( "go.uber.org/zap" "source.quilibrium.com/quilibrium/monorepo/node/config" "source.quilibrium.com/quilibrium/monorepo/node/consensus" - "source.quilibrium.com/quilibrium/monorepo/node/consensus/ceremony" "source.quilibrium.com/quilibrium/monorepo/node/consensus/master" - ceremony2 "source.quilibrium.com/quilibrium/monorepo/node/execution/ceremony" + "source.quilibrium.com/quilibrium/monorepo/node/execution/ceremony" "source.quilibrium.com/quilibrium/monorepo/node/keys" "source.quilibrium.com/quilibrium/monorepo/node/p2p" "source.quilibrium.com/quilibrium/monorepo/node/store" @@ -24,16 +23,15 @@ import ( func NewNode(configConfig *config.Config) (*Node, error) { zapLogger := logger() dbConfig := configConfig.DB - db := store.NewPebbleDB(dbConfig) - pebbleClockStore := store.NewPebbleClockStore(db, zapLogger) + pebbleDB := store.NewPebbleDB(dbConfig) + pebbleClockStore := store.NewPebbleClockStore(pebbleDB, zapLogger) keyConfig := configConfig.Key fileKeyManager := keys.NewFileKeyManager(keyConfig, zapLogger) p2PConfig := configConfig.P2P blossomSub := p2p.NewBlossomSub(p2PConfig, zapLogger) engineConfig := configConfig.Engine - pebbleKeyStore := store.NewPebbleKeyStore(db, zapLogger) - ceremonyDataClockConsensusEngine := ceremony.NewCeremonyDataClockConsensusEngine(engineConfig, zapLogger, fileKeyManager, pebbleClockStore, pebbleKeyStore, blossomSub) - ceremonyExecutionEngine := ceremony2.NewCeremonyExecutionEngine(zapLogger, ceremonyDataClockConsensusEngine, engineConfig, fileKeyManager, blossomSub, pebbleClockStore, pebbleKeyStore) + pebbleKeyStore := store.NewPebbleKeyStore(pebbleDB, zapLogger) + ceremonyExecutionEngine := ceremony.NewCeremonyExecutionEngine(zapLogger, engineConfig, fileKeyManager, blossomSub, pebbleClockStore, pebbleKeyStore) masterClockConsensusEngine := master.NewMasterClockConsensusEngine(engineConfig, zapLogger, pebbleClockStore, fileKeyManager, blossomSub) node, err := newNode(zapLogger, pebbleClockStore, fileKeyManager, blossomSub, ceremonyExecutionEngine, masterClockConsensusEngine) if err != nil { @@ -52,9 +50,9 @@ func NewDBConsole(configConfig *config.Config) (*DBConsole, error) { func NewClockStore(configConfig *config.Config) (store.ClockStore, error) { dbConfig := configConfig.DB - db := store.NewPebbleDB(dbConfig) + pebbleDB := store.NewPebbleDB(dbConfig) zapLogger := logger() - pebbleClockStore := store.NewPebbleClockStore(db, zapLogger) + pebbleClockStore := store.NewPebbleClockStore(pebbleDB, zapLogger) return pebbleClockStore, nil } @@ -75,17 +73,11 @@ var loggerSet = wire.NewSet( var keyManagerSet = wire.NewSet(wire.FieldsOf(new(*config.Config), "Key"), keys.NewFileKeyManager, wire.Bind(new(keys.KeyManager), new(*keys.FileKeyManager))) -var storeSet = wire.NewSet(wire.FieldsOf(new(*config.Config), "DB"), store.NewPebbleDB, store.NewPebbleClockStore, store.NewPebbleKeyStore, store.NewPebbleDataProofStore, wire.Bind(new(store.ClockStore), new(*store.PebbleClockStore)), wire.Bind(new(store.KeyStore), new(*store.PebbleKeyStore)), wire.Bind(new(store.DataProofStore), new(*store.PebbleDataProofStore))) +var storeSet = wire.NewSet(wire.FieldsOf(new(*config.Config), "DB"), store.NewPebbleDB, wire.Bind(new(store.KVDB), new(*store.PebbleDB)), store.NewPebbleClockStore, store.NewPebbleKeyStore, store.NewPebbleDataProofStore, wire.Bind(new(store.ClockStore), new(*store.PebbleClockStore)), wire.Bind(new(store.KeyStore), new(*store.PebbleKeyStore)), wire.Bind(new(store.DataProofStore), new(*store.PebbleDataProofStore))) var pubSubSet = wire.NewSet(wire.FieldsOf(new(*config.Config), "P2P"), p2p.NewBlossomSub, wire.Bind(new(p2p.PubSub), new(*p2p.BlossomSub))) -var dataConsensusSet = wire.NewSet(wire.FieldsOf(new(*config.Config), "Engine"), ceremony.NewCeremonyDataClockConsensusEngine, wire.Bind( - new(consensus.DataConsensusEngine), - new(*ceremony.CeremonyDataClockConsensusEngine), -), -) - -var engineSet = wire.NewSet(ceremony2.NewCeremonyExecutionEngine) +var engineSet = wire.NewSet(wire.FieldsOf(new(*config.Config), "Engine"), ceremony.NewCeremonyExecutionEngine) var consensusSet = wire.NewSet(master.NewMasterClockConsensusEngine, wire.Bind( new(consensus.ConsensusEngine), diff --git a/node/config/engine.go b/node/config/engine.go index 7fce2ac..b2f5f22 100644 --- a/node/config/engine.go +++ b/node/config/engine.go @@ -7,4 +7,8 @@ type EngineConfig struct { MaxFrames int64 `yaml:"maxFrames"` PendingCommitWorkers int64 `yaml:"pendingCommitWorkers"` MinimumPeersRequired int `yaml:"minimumPeersRequired"` + + // Values used only for testing – do not override these in production, your + // node will get kicked out + Difficulty uint32 `yaml:"difficulty"` } diff --git a/node/consensus/ceremony/broadcast_messaging.go b/node/consensus/ceremony/broadcast_messaging.go index b79c4a0..b9014db 100644 --- a/node/consensus/ceremony/broadcast_messaging.go +++ b/node/consensus/ceremony/broadcast_messaging.go @@ -2,8 +2,6 @@ package ceremony import ( "bytes" - "crypto" - "crypto/rand" "encoding/binary" "strings" "time" @@ -19,7 +17,6 @@ import ( "google.golang.org/protobuf/types/known/anypb" "source.quilibrium.com/quilibrium/monorepo/go-libp2p-blossomsub/pb" "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves" - "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/zkp/schnorr" "source.quilibrium.com/quilibrium/monorepo/node/consensus" qcrypto "source.quilibrium.com/quilibrium/monorepo/node/crypto" "source.quilibrium.com/quilibrium/monorepo/node/keys" @@ -111,22 +108,6 @@ func (e *CeremonyDataClockConsensusEngine) handleMessage( ); err != nil { return errors.Wrap(err, "handle message") } - case protobufs.ProvingKeyRequestType: - if err := e.handleProvingKeyRequest( - message.From, - msg.Address, - any, - ); err != nil { - return errors.Wrap(err, "handle message") - } - case protobufs.ProvingKeyAnnouncementType: - if err := e.handleProvingKey(message.From, msg.Address, any); err != nil { - return errors.Wrap(err, "handle message") - } - case protobufs.KeyBundleAnnouncementType: - if err := e.handleKeyBundle(message.From, msg.Address, any); err != nil { - return errors.Wrap(err, "handle message") - } case protobufs.CeremonyPeerListAnnounceType: if err := e.handleCeremonyPeerListAnnounce( message.From, @@ -304,177 +285,6 @@ func (e *CeremonyDataClockConsensusEngine) handleCeremonyLobbyStateTransition( return nil } -func (e *CeremonyDataClockConsensusEngine) handleKeyBundle( - peerID []byte, - address []byte, - any *anypb.Any, -) error { - e.logger.Debug("received key bundle") - keyBundleAnnouncement := &protobufs.KeyBundleAnnouncement{} - if err := any.UnmarshalTo(keyBundleAnnouncement); err != nil { - return errors.Wrap(err, "handle key bundle") - } - - if len(keyBundleAnnouncement.ProvingKeyBytes) == 0 { - return errors.Wrap(errors.New("proving key is nil"), "handle key bundle") - } - - k, err := e.keyStore.GetLatestKeyBundle(keyBundleAnnouncement.ProvingKeyBytes) - if err != nil && !errors.Is(err, store.ErrNotFound) { - return errors.Wrap(err, "handle key bundle") - } - - if k != nil { - latestAnnouncement := &protobufs.KeyBundleAnnouncement{} - err := proto.Unmarshal(k.Data, latestAnnouncement) - if err != nil { - return errors.Wrap(err, "handle key bundle") - } - - if bytes.Equal( - latestAnnouncement.IdentityKey.Challenge, - keyBundleAnnouncement.IdentityKey.Challenge, - ) && bytes.Equal( - latestAnnouncement.IdentityKey.Response, - keyBundleAnnouncement.IdentityKey.Response, - ) && bytes.Equal( - latestAnnouncement.IdentityKey.Statement, - keyBundleAnnouncement.IdentityKey.Statement, - ) && bytes.Equal( - latestAnnouncement.SignedPreKey.Challenge, - keyBundleAnnouncement.SignedPreKey.Challenge, - ) && bytes.Equal( - latestAnnouncement.SignedPreKey.Response, - keyBundleAnnouncement.SignedPreKey.Response, - ) && bytes.Equal( - latestAnnouncement.SignedPreKey.Statement, - keyBundleAnnouncement.SignedPreKey.Statement, - ) { - // This has already been proven, ignore - return nil - } - } - - var provingKey *protobufs.ProvingKeyAnnouncement - inclusion, err := e.keyStore.GetProvingKey( - keyBundleAnnouncement.ProvingKeyBytes, - ) - if err != nil { - if !errors.Is(err, store.ErrNotFound) { - return errors.Wrap(err, "handle key bundle") - } - - provingKey, err = e.keyStore.GetStagedProvingKey( - keyBundleAnnouncement.ProvingKeyBytes, - ) - if err != nil && !errors.Is(err, store.ErrNotFound) { - return errors.Wrap(err, "handle key bundle") - } - } else { - err := proto.Unmarshal(inclusion.Data, provingKey) - if err != nil { - return errors.Wrap(err, "handle key bundle") - } - } - - // We have a matching proving key, we can set this up to be committed. - if provingKey != nil { - e.logger.Debug("verifying key bundle announcement") - if err := keyBundleAnnouncement.Verify(provingKey); err != nil { - e.logger.Debug( - "could not verify key bundle announcement", - zap.Error(err), - ) - return nil - } - - go func() { - e.logger.Debug("adding key bundle announcement to pending commits") - - e.pendingCommits <- any - }() - - return nil - } else { - e.logger.Debug("proving key not found, requesting from peers") - - if err = e.publishMessage(e.filter, &protobufs.ProvingKeyRequest{ - ProvingKeyBytes: keyBundleAnnouncement.ProvingKeyBytes, - }); err != nil { - return errors.Wrap(err, "handle key bundle") - } - - e.dependencyMapMx.Lock() - e.dependencyMap[string(keyBundleAnnouncement.ProvingKeyBytes)] = any - e.dependencyMapMx.Unlock() - } - - return nil -} - -func (e *CeremonyDataClockConsensusEngine) handleProvingKey( - peerID []byte, - address []byte, - any *anypb.Any, -) error { - e.logger.Debug("received proving key") - - provingKeyAnnouncement := &protobufs.ProvingKeyAnnouncement{} - if err := any.UnmarshalTo(provingKeyAnnouncement); err != nil { - return errors.Wrap(err, "handle proving key") - } - - if err := provingKeyAnnouncement.Verify(); err != nil { - return errors.Wrap(err, "handle proving key") - } - - if err := e.keyStore.StageProvingKey(provingKeyAnnouncement); err != nil { - return errors.Wrap(err, "handle proving key") - } - - provingKey := provingKeyAnnouncement.PublicKey() - - e.logger.Debug( - "proving key staged", - zap.Binary("proving_key", provingKey), - ) - - go func() { - e.dependencyMapMx.Lock() - if e.dependencyMap[string(provingKey)] != nil { - keyBundleAnnouncement := &protobufs.KeyBundleAnnouncement{} - if err := proto.Unmarshal( - e.dependencyMap[string(provingKey)].Value, - keyBundleAnnouncement, - ); err != nil { - e.logger.Error( - "could not unmarshal key bundle announcement", - zap.Error(err), - ) - e.dependencyMapMx.Unlock() - return - } - if err := keyBundleAnnouncement.Verify( - provingKeyAnnouncement, - ); err != nil { - e.logger.Error( - "could not verify key bundle announcement", - zap.Error(err), - ) - e.dependencyMapMx.Unlock() - return - } - - e.pendingCommits <- e.dependencyMap[string(provingKey)] - - delete(e.dependencyMap, string(provingKey)) - } - e.dependencyMapMx.Unlock() - }() - - return nil -} - func (e *CeremonyDataClockConsensusEngine) handleClockFrameData( peerID []byte, address []byte, @@ -694,16 +504,30 @@ func (e *CeremonyDataClockConsensusEngine) handleClockFrameData( zap.Binary("filter", frame.Filter), zap.Uint64("frame_number", frame.FrameNumber), ) + masterFrame, err := e.clockStore.GetMasterClockFrame( + []byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }, + frame.FrameNumber-1, + ) + if err != nil { + e.logger.Info("received frame with no known master, needs sync") + return nil + } - parentSelector, selector, distance, err := - frame.GetParentSelectorAndDistance() + discriminator, err := masterFrame.GetSelector() + if err != nil { + return errors.Wrap(err, "handle clock frame data") + } + + parentSelector, distance, selector, err := + frame.GetParentSelectorAndDistance(discriminator) if err != nil { return errors.Wrap(err, "handle clock frame data") } - e.logger.Debug( - "difference between selector/discriminator", - zap.Binary("difference", distance.Bytes()), - ) if _, err := e.clockStore.GetParentDataClockFrame( frame.Filter, @@ -713,7 +537,7 @@ func (e *CeremonyDataClockConsensusEngine) handleClockFrameData( // If this is a frame number higher than what we're already caught up to, // push a request to fill the gap, unless we're syncing or it's in step, // then just lazily seek. - from := e.frame + from := e.frame.FrameNumber if from >= frame.FrameNumber-1 { from = frame.FrameNumber - 1 } @@ -737,9 +561,9 @@ func (e *CeremonyDataClockConsensusEngine) handleClockFrameData( } if err := e.clockStore.PutCandidateDataClockFrame( - parentSelector.Bytes(), - distance.Bytes(), - selector.Bytes(), + parentSelector.FillBytes(make([]byte, 32)), + distance.FillBytes(make([]byte, 32)), + selector.FillBytes(make([]byte, 32)), frame, txn, ); err != nil { @@ -752,7 +576,7 @@ func (e *CeremonyDataClockConsensusEngine) handleClockFrameData( return errors.Wrap(err, "handle clock frame data") } - if e.frame < frame.FrameNumber { + if e.frame.FrameNumber < frame.FrameNumber { e.latestFrameReceived = frame.FrameNumber e.lastFrameReceivedAt = time.Now().UTC() } @@ -819,12 +643,11 @@ func (e *CeremonyDataClockConsensusEngine) publishMessage( return e.pubSub.PublishToBitmask(filter, data) } -func (e *CeremonyDataClockConsensusEngine) announceKeyBundle() error { - e.logger.Debug("announcing key bundle") - idk, err := e.keyManager.GetAgreementKey("q-ratchet-idk") +func (e *CeremonyDataClockConsensusEngine) createCommunicationKeys() error { + _, err := e.keyManager.GetAgreementKey("q-ratchet-idk") if err != nil { if errors.Is(err, keys.KeyNotFoundErr) { - idk, err = e.keyManager.CreateAgreementKey( + _, err = e.keyManager.CreateAgreementKey( "q-ratchet-idk", keys.KeyTypeX448, ) @@ -836,10 +659,10 @@ func (e *CeremonyDataClockConsensusEngine) announceKeyBundle() error { } } - spk, err := e.keyManager.GetAgreementKey("q-ratchet-spk") + _, err = e.keyManager.GetAgreementKey("q-ratchet-spk") if err != nil { if errors.Is(err, keys.KeyNotFoundErr) { - spk, err = e.keyManager.CreateAgreementKey( + _, err = e.keyManager.CreateAgreementKey( "q-ratchet-spk", keys.KeyTypeX448, ) @@ -851,110 +674,5 @@ func (e *CeremonyDataClockConsensusEngine) announceKeyBundle() error { } } - idkPoint := curves.ED448().NewGeneratorPoint().Mul(idk) - idkProver := schnorr.NewProver( - curves.ED448(), - curves.ED448().NewGeneratorPoint(), - sha3.New256(), - []byte{}, - ) - - spkPoint := curves.ED448().NewGeneratorPoint().Mul(spk) - spkProver := schnorr.NewProver( - curves.ED448(), - curves.ED448().NewGeneratorPoint(), - sha3.New256(), - []byte{}, - ) - - idkProof, idkCommitment, err := idkProver.ProveCommit(idk) - if err != nil { - return errors.Wrap(err, "announce key bundle") - } - - spkProof, spkCommitment, err := spkProver.ProveCommit(spk) - if err != nil { - return errors.Wrap(err, "announce key bundle") - } - - msg := append( - append([]byte{}, idkCommitment...), - spkCommitment..., - ) - - signature, err := e.provingKey.Sign(rand.Reader, msg, crypto.Hash(0)) - if err != nil { - return errors.Wrap(err, "announce key bundle") - } - - signatureProto := &protobufs.ProvingKeyAnnouncement_ProvingKeySignatureEd448{ - ProvingKeySignatureEd448: &protobufs.Ed448Signature{ - PublicKey: &protobufs.Ed448PublicKey{ - KeyValue: e.provingKeyBytes, - }, - Signature: signature, - }, - } - provingKeyAnnouncement := &protobufs.ProvingKeyAnnouncement{ - IdentityCommitment: idkCommitment, - PrekeyCommitment: spkCommitment, - ProvingKeySignature: signatureProto, - } - - if err := e.publishMessage(e.filter, provingKeyAnnouncement); err != nil { - return errors.Wrap(err, "announce key bundle") - } - - idkSignature, err := e.provingKey.Sign( - rand.Reader, - idkPoint.ToAffineCompressed(), - crypto.Hash(0), - ) - if err != nil { - return errors.Wrap(err, "announce key bundle") - } - - spkSignature, err := e.provingKey.Sign( - rand.Reader, - spkPoint.ToAffineCompressed(), - crypto.Hash(0), - ) - if err != nil { - return errors.Wrap(err, "announce key bundle") - } - - keyBundleAnnouncement := &protobufs.KeyBundleAnnouncement{ - ProvingKeyBytes: e.provingKeyBytes, - IdentityKey: &protobufs.IdentityKey{ - Challenge: idkProof.C.Bytes(), - Response: idkProof.S.Bytes(), - Statement: idkProof.Statement.ToAffineCompressed(), - IdentityKeySignature: &protobufs.IdentityKey_PublicKeySignatureEd448{ - PublicKeySignatureEd448: &protobufs.Ed448Signature{ - PublicKey: &protobufs.Ed448PublicKey{ - KeyValue: idkPoint.ToAffineCompressed(), - }, - Signature: idkSignature, - }, - }, - }, - SignedPreKey: &protobufs.SignedPreKey{ - Challenge: spkProof.C.Bytes(), - Response: spkProof.S.Bytes(), - Statement: spkProof.Statement.ToAffineCompressed(), - SignedPreKeySignature: &protobufs.SignedPreKey_PublicKeySignatureEd448{ - PublicKeySignatureEd448: &protobufs.Ed448Signature{ - PublicKey: &protobufs.Ed448PublicKey{ - KeyValue: spkPoint.ToAffineCompressed(), - }, - Signature: spkSignature, - }, - }, - }, - } - - return errors.Wrap( - e.publishMessage(e.filter, keyBundleAnnouncement), - "announce key bundle", - ) + return nil } diff --git a/node/consensus/ceremony/ceremony_data_clock_consensus_engine.go b/node/consensus/ceremony/ceremony_data_clock_consensus_engine.go index 5ce1f30..fa40990 100644 --- a/node/consensus/ceremony/ceremony_data_clock_consensus_engine.go +++ b/node/consensus/ceremony/ceremony_data_clock_consensus_engine.go @@ -53,8 +53,7 @@ type ChannelServer = protobufs.CeremonyService_GetPublicChannelServer type CeremonyDataClockConsensusEngine struct { protobufs.UnimplementedCeremonyServiceServer - frame uint64 - activeFrame *protobufs.ClockFrame + frame *protobufs.ClockFrame difficulty uint32 logger *zap.Logger state consensus.EngineState @@ -113,6 +112,8 @@ func NewCeremonyDataClockConsensusEngine( clockStore store.ClockStore, keyStore store.KeyStore, pubSub p2p.PubSub, + filter []byte, + seed []byte, ) *CeremonyDataClockConsensusEngine { if logger == nil { panic(errors.New("logger is nil")) @@ -143,9 +144,14 @@ func NewCeremonyDataClockConsensusEngine( minimumPeersRequired = 3 } + difficulty := engineConfig.Difficulty + if difficulty == 0 { + difficulty = 10000 + } + e := &CeremonyDataClockConsensusEngine{ - frame: 0, - difficulty: 10000, + frame: nil, + difficulty: difficulty, logger: logger, state: consensus.EngineStateStopped, clockStore: clockStore, @@ -182,6 +188,8 @@ func NewCeremonyDataClockConsensusEngine( engineConfig, ) + e.filter = filter + e.input = seed e.provingKey = signer e.provingKeyType = keyType e.provingKeyBytes = bytes @@ -190,16 +198,10 @@ func NewCeremonyDataClockConsensusEngine( return e } -func (e *CeremonyDataClockConsensusEngine) Start( - filter []byte, - seed []byte, -) <-chan error { +func (e *CeremonyDataClockConsensusEngine) Start() <-chan error { e.logger.Info("starting ceremony consensus engine") e.state = consensus.EngineStateStarting errChan := make(chan error) - - e.filter = filter - e.input = seed e.state = consensus.EngineStateLoading e.logger.Info("loading last seen state") @@ -214,16 +216,16 @@ func (e *CeremonyDataClockConsensusEngine) Start( if latestFrame != nil { e.setFrame(latestFrame) } else { - latestFrame = e.createGenesisFrame() + latestFrame = e.CreateGenesisFrame(nil) + } + + err = e.createCommunicationKeys() + if err != nil { + panic(err) } e.logger.Info("subscribing to pubsub messages") e.pubSub.Subscribe(e.filter, e.handleMessage, true) - e.pubSub.Subscribe( - append(append([]byte{}, e.filter...), e.pubSub.GetPeerID()...), - e.handleSync, - true, - ) go func() { server := grpc.NewServer( @@ -240,8 +242,6 @@ func (e *CeremonyDataClockConsensusEngine) Start( } }() - latestFrame = e.performSanityCheck(latestFrame) - e.state = consensus.EngineStateCollecting for i := int64(0); i < e.pendingCommitWorkers; i++ { @@ -257,7 +257,7 @@ func (e *CeremonyDataClockConsensusEngine) Start( } timestamp := time.Now().UnixMilli() - msg := binary.BigEndian.AppendUint64([]byte{}, e.frame) + msg := binary.BigEndian.AppendUint64([]byte{}, e.frame.FrameNumber) msg = append(msg, consensus.GetVersion()...) msg = binary.BigEndian.AppendUint64(msg, uint64(timestamp)) sig, err := e.pubSub.SignMessage(msg) @@ -269,7 +269,7 @@ func (e *CeremonyDataClockConsensusEngine) Start( e.peerMap[string(e.pubSub.GetPeerID())] = &peerInfo{ peerId: e.pubSub.GetPeerID(), multiaddr: "", - maxFrame: e.frame, + maxFrame: e.frame.FrameNumber, version: consensus.GetVersion(), signature: sig, publicKey: e.pubSub.GetPublicKey(), @@ -307,38 +307,8 @@ func (e *CeremonyDataClockConsensusEngine) Start( }() go func() { - latest := latestFrame - for { - time.Sleep(30 * time.Second) - peerCount := e.pubSub.GetNetworkPeersCount() - if peerCount >= e.minimumPeersRequired { - e.logger.Info("selecting leader") - if e.frame > latest.FrameNumber && e.frame-latest.FrameNumber > 16 && - e.syncingTarget == nil { - e.logger.Info("rewinding sync head due to large delta") - latest, _, err = e.clockStore.GetDataClockFrame( - e.filter, - 0, - ) - if err != nil { - panic(err) - } - } - latest, err = e.commitLongestPath(latest) - if err != nil { - e.logger.Error("could not collect longest path", zap.Error(err)) - latest, _, err = e.clockStore.GetDataClockFrame(e.filter, 0) - if err != nil { - panic(err) - } - } - - latest = e.performSanityCheck(latest) - } - } - }() - - go func() { + e.logger.Info("waiting for peer list mappings") + time.Sleep(30 * time.Second) for e.state < consensus.EngineStateStopping { peerCount := e.pubSub.GetNetworkPeersCount() if peerCount < e.minimumPeersRequired { @@ -350,22 +320,23 @@ func (e *CeremonyDataClockConsensusEngine) Start( } else { switch e.state { case consensus.EngineStateCollecting: + currentFrame := latestFrame if latestFrame, err = e.collect(latestFrame); err != nil { e.logger.Error("could not collect", zap.Error(err)) e.state = consensus.EngineStateCollecting - errChan <- err + latestFrame = currentFrame } case consensus.EngineStateProving: + currentFrame := latestFrame if latestFrame, err = e.prove(latestFrame); err != nil { e.logger.Error("could not prove", zap.Error(err)) e.state = consensus.EngineStateCollecting - errChan <- err + latestFrame = currentFrame } case consensus.EngineStatePublishing: if err = e.publishProof(latestFrame); err != nil { e.logger.Error("could not publish", zap.Error(err)) e.state = consensus.EngineStateCollecting - errChan <- err } } } @@ -389,7 +360,7 @@ func (e *CeremonyDataClockConsensusEngine) Stop(force bool) <-chan error { for name := range e.executionEngines { name := name go func(name string) { - err := <-e.UnregisterExecutor(name, e.frame, force) + err := <-e.UnregisterExecutor(name, e.frame.FrameNumber, force) if err != nil { errChan <- err } @@ -463,7 +434,7 @@ func (e *CeremonyDataClockConsensusEngine) performSanityCheck( panic(err) } - parentSelector, _, _, err := disc.GetParentSelectorAndDistance() + parentSelector, _, _, err := disc.GetParentSelectorAndDistance(nil) if err != nil { panic(err) } @@ -536,7 +507,7 @@ func (e *CeremonyDataClockConsensusEngine) GetDifficulty() uint32 { return e.difficulty } -func (e *CeremonyDataClockConsensusEngine) GetFrame() uint64 { +func (e *CeremonyDataClockConsensusEngine) GetFrame() *protobufs.ClockFrame { return e.frame } @@ -550,12 +521,6 @@ func ( return e.frameChan } -func ( - e *CeremonyDataClockConsensusEngine, -) GetActiveFrame() *protobufs.ClockFrame { - return e.activeFrame -} - func ( e *CeremonyDataClockConsensusEngine, ) GetPeerInfo() *protobufs.PeerInfoResponse { diff --git a/node/consensus/ceremony/consensus_frames.go b/node/consensus/ceremony/consensus_frames.go index db8aabc..a1f29b3 100644 --- a/node/consensus/ceremony/consensus_frames.go +++ b/node/consensus/ceremony/consensus_frames.go @@ -3,16 +3,18 @@ package ceremony import ( "bytes" "context" + "encoding/base64" "encoding/binary" "encoding/hex" + "encoding/json" "fmt" "io" "math/big" + "os" "strings" "github.com/iden3/go-iden3-crypto/ff" "github.com/iden3/go-iden3-crypto/poseidon" - "github.com/libp2p/go-libp2p/core/peer" "github.com/pkg/errors" "go.uber.org/zap" "golang.org/x/crypto/sha3" @@ -25,7 +27,6 @@ import ( "source.quilibrium.com/quilibrium/monorepo/node/execution/ceremony/application" "source.quilibrium.com/quilibrium/monorepo/node/p2p" "source.quilibrium.com/quilibrium/monorepo/node/protobufs" - "source.quilibrium.com/quilibrium/monorepo/node/store" "source.quilibrium.com/quilibrium/monorepo/node/tries" ) @@ -322,9 +323,8 @@ func (e *CeremonyDataClockConsensusEngine) setFrame( } e.logger.Debug("set frame", zap.Uint64("frame_number", frame.FrameNumber)) e.currentDistance = distance - e.frame = frame.FrameNumber + e.frame = frame e.parentSelector = parent.Bytes() - e.activeFrame = frame go func() { e.frameChan <- frame }() @@ -332,7 +332,7 @@ func (e *CeremonyDataClockConsensusEngine) setFrame( func ( e *CeremonyDataClockConsensusEngine, -) createGenesisFrame() *protobufs.ClockFrame { +) CreateGenesisFrame(testProverKeys [][]byte) *protobufs.ClockFrame { e.logger.Info("creating genesis frame") for _, l := range strings.Split(string(e.input), "\n") { e.logger.Info(l) @@ -376,7 +376,7 @@ func ( transcript.RunningG2_256Powers = append( transcript.RunningG2_256Powers, &protobufs.BLS48581G2PublicKey{ - KeyValue: qcrypto.CeremonyPotPubKeys[len(qcrypto.CeremonyPotPubKeys)-1]. + KeyValue: qcrypto.CeremonyBLS48581G2[len(qcrypto.CeremonyBLS48581G2)-1]. ToAffineCompressed(), }, ) @@ -408,6 +408,44 @@ func ( rewardTrie.Add(addrBytes, 0, 50) } + // 2024-01-03: 1.2.0 + d, err := os.ReadFile("./retroactive_peers.json") + if err != nil { + panic(err) + } + + type peerData struct { + PeerId string `json:"peer_id"` + TokenBalance uint64 `json:"token_balance"` + } + type rewards struct { + Rewards []peerData `json:"rewards"` + } + + retroEntries := &rewards{} + err = json.Unmarshal(d, retroEntries) + if err != nil { + panic(err) + } + + e.logger.Info("adding retroactive peer reward info") + for _, s := range retroEntries.Rewards { + peerId := s.PeerId + peerBytes, err := base64.StdEncoding.DecodeString(peerId) + if err != nil { + panic(err) + } + + addr, err := poseidon.HashBytes(peerBytes) + if err != nil { + panic(err) + } + + addrBytes := addr.Bytes() + addrBytes = append(make([]byte, 32-len(addrBytes)), addrBytes...) + rewardTrie.Add(addrBytes, 0, s.TokenBalance) + } + trieBytes, err := rewardTrie.Serialize() if err != nil { panic(err) @@ -521,25 +559,42 @@ func ( // first phase: e.logger.Info("encoding signatories to prover trie") - for _, s := range qcrypto.CeremonySignatories { - pubkey := s.ToAffineCompressed() - e.logger.Info("0x" + hex.EncodeToString(pubkey)) + if len(testProverKeys) != 0 { + e.logger.Warn( + "TEST PROVER ENTRIES BEING ADDED, YOUR NODE WILL BE KICKED IF IN" + + " PRODUCTION", + ) + for _, s := range testProverKeys { + addr, err := poseidon.HashBytes(s) + if err != nil { + panic(err) + } - addr, err := poseidon.HashBytes(pubkey) - if err != nil { - panic(err) + addrBytes := addr.Bytes() + addrBytes = append(make([]byte, 32-len(addrBytes)), addrBytes...) + e.frameProverTrie.Add(addrBytes, 0) } + } else { + for _, s := range qcrypto.CeremonySignatories { + pubkey := s.ToAffineCompressed() + e.logger.Info("0x" + hex.EncodeToString(pubkey)) - addrBytes := addr.Bytes() - addrBytes = append(make([]byte, 32-len(addrBytes)), addrBytes...) - e.frameProverTrie.Add(addrBytes, 0) + addr, err := poseidon.HashBytes(pubkey) + if err != nil { + panic(err) + } + + addrBytes := addr.Bytes() + addrBytes = append(make([]byte, 32-len(addrBytes)), addrBytes...) + e.frameProverTrie.Add(addrBytes, 0) + } } e.logger.Info("proving genesis frame") input := []byte{} input = append(input, e.filter...) - input = binary.BigEndian.AppendUint64(input, e.frame) - input = binary.BigEndian.AppendUint64(input, uint64(0)) + input = binary.BigEndian.AppendUint64(input, 0) + input = binary.BigEndian.AppendUint64(input, 0) input = binary.BigEndian.AppendUint32(input, e.difficulty) input = append(input, e.input...) @@ -551,7 +606,7 @@ func ( frame := &protobufs.ClockFrame{ Filter: e.filter, - FrameNumber: e.frame, + FrameNumber: 0, Timestamp: 0, Difficulty: e.difficulty, Input: inputMessage, @@ -563,7 +618,7 @@ func ( PublicKeySignature: nil, } - parent, distance, selector, err := frame.GetParentSelectorAndDistance() + parent, _, selector, err := frame.GetParentSelectorAndDistance(nil) if err != nil { panic(err) } @@ -574,9 +629,9 @@ func ( } if err := e.clockStore.PutCandidateDataClockFrame( - parent.Bytes(), - distance.Bytes(), - selector.Bytes(), + parent.FillBytes(make([]byte, 32)), + big.NewInt(0).FillBytes(make([]byte, 32)), + selector.FillBytes(make([]byte, 32)), frame, txn, ); err != nil { @@ -643,13 +698,23 @@ func (e *CeremonyDataClockConsensusEngine) commitLongestPath( return nil, errors.Wrap(err, "commit longest path") } - selectorBytes := selector.Bytes() - selectorBytes = append( - make([]byte, 32-len(selectorBytes)), - selectorBytes..., - ) + masterFrame, err := e.clockStore.GetMasterClockFrame([]byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }, s[currentDepth].GetFrameNumber()) + if err != nil { + return nil, errors.Wrap(err, "commit longest path") + } + + proverSelector, err := masterFrame.GetSelector() + if err != nil { + return nil, errors.Wrap(err, "commit longest path") + } + nearest := e.frameProverTrie.FindNearest( - selectorBytes, + proverSelector.FillBytes(make([]byte, 32)), ) addr, err := value.GetAddress() @@ -786,37 +851,6 @@ func (e *CeremonyDataClockConsensusEngine) commitLongestPath( ) return nil, errors.Wrap(err, "commit longest path") } - case protobufs.KeyBundleAnnouncementType: - bundle := &protobufs.KeyBundleAnnouncement{} - if err := proto.Unmarshal(c.Data, bundle); err != nil { - e.logger.Error( - "could not commit candidate", - zap.Error(err), - zap.Uint64("frame_number", s.FrameNumber), - zap.Binary("commitment", c.Commitment), - ) - return nil, errors.Wrap(err, "commit longest path") - } - - e.logger.Debug( - "committing key bundle", - zap.Uint64("frame_number", s.FrameNumber), - zap.Binary("commitment", c.Commitment), - ) - - if err := e.keyStore.PutKeyBundle( - bundle.ProvingKeyBytes, - c, - txn, - ); err != nil { - e.logger.Error( - "could not commit candidate", - zap.Error(err), - zap.Uint64("frame_number", s.FrameNumber), - zap.Binary("output", s.Output), - ) - return nil, errors.Wrap(err, "commit longest path") - } } } } @@ -851,6 +885,22 @@ func (e *CeremonyDataClockConsensusEngine) commitLongestPath( } } + if current.FrameNumber != latest.FrameNumber { + to := current.FrameNumber + if to-16 > to { // underflow + to = 1 + } else { + to = to - 16 + } + + if 1 < to { + err := e.clockStore.DeleteCandidateDataClockFrameRange(e.filter, 1, to) + if err != nil { + e.logger.Error("error while purging candidate frames", zap.Error(err)) + } + } + } + return current, nil } @@ -860,7 +910,7 @@ func (e *CeremonyDataClockConsensusEngine) GetMostAheadPeer() ( error, ) { e.peerMapMx.Lock() - max := e.frame + max := e.frame.FrameNumber var peer []byte = nil for _, v := range e.peerMap { if v.maxFrame > max { @@ -882,190 +932,6 @@ func (e *CeremonyDataClockConsensusEngine) GetMostAheadPeer() ( return peer, max, nil } -func (e *CeremonyDataClockConsensusEngine) reverseOptimisticSync( - currentLatest *protobufs.ClockFrame, - maxFrame uint64, - peerId []byte, -) (*protobufs.ClockFrame, error) { - latest := currentLatest - cc, err := e.pubSub.GetDirectChannel(peerId) - if err != nil { - e.logger.Error( - "could not establish direct channel", - zap.Error(err), - ) - e.peerMapMx.Lock() - if _, ok := e.peerMap[string(peerId)]; ok { - e.uncooperativePeersMap[string(peerId)] = e.peerMap[string(peerId)] - delete(e.peerMap, string(peerId)) - } - e.peerMapMx.Unlock() - e.syncingTarget = nil - return latest, errors.Wrap(err, "reverse optimistic sync") - } - - client := protobufs.NewCeremonyServiceClient(cc) - - from := latest.FrameNumber - if from <= 1 { - from = 2 - } - - if maxFrame-from > 32 { - // divergence is high, ask them for the latest frame and if they - // respond with a valid answer, optimistically continue from this - // frame, if we hit a fault we'll mark them as uncooperative and move - // on - from = 2 - s, err := client.GetCompressedSyncFrames( - context.Background(), - &protobufs.ClockFramesRequest{ - Filter: e.filter, - FromFrameNumber: maxFrame - 32, - }, - grpc.MaxCallRecvMsgSize(600*1024*1024), - ) - if err != nil { - e.logger.Error( - "received error from peer", - zap.Error(err), - ) - e.peerMapMx.Lock() - if _, ok := e.peerMap[string(peerId)]; ok { - e.uncooperativePeersMap[string(peerId)] = e.peerMap[string(peerId)] - delete(e.peerMap, string(peerId)) - } - e.peerMapMx.Unlock() - e.syncingTarget = nil - return latest, errors.Wrap(err, "reverse optimistic sync") - } - var syncMsg *protobufs.CeremonyCompressedSync - for syncMsg, err = s.Recv(); err == nil; syncMsg, err = s.Recv() { - e.logger.Info( - "received compressed sync frame", - zap.Uint64("from", syncMsg.FromFrameNumber), - zap.Uint64("to", syncMsg.ToFrameNumber), - zap.Int("frames", len(syncMsg.TruncatedClockFrames)), - zap.Int("proofs", len(syncMsg.Proofs)), - ) - var next *protobufs.ClockFrame - if next, err = e.decompressAndStoreCandidates( - peerId, - syncMsg, - e.logger.Info, - ); err != nil && !errors.Is(err, ErrNoNewFrames) { - e.logger.Error( - "could not decompress and store candidate", - zap.Error(err), - ) - e.peerMapMx.Lock() - if _, ok := e.peerMap[string(peerId)]; ok { - e.uncooperativePeersMap[string(peerId)] = e.peerMap[string(peerId)] - delete(e.peerMap, string(peerId)) - } - e.peerMapMx.Unlock() - - if err := cc.Close(); err != nil { - e.logger.Error("error while closing connection", zap.Error(err)) - } - - e.syncingTarget = nil - e.syncingStatus = SyncStatusFailed - return currentLatest, errors.Wrap(err, "reverse optimistic sync") - } - if next != nil { - latest = next - } - } - if err != nil && err != io.EOF && !errors.Is(err, ErrNoNewFrames) { - if err := cc.Close(); err != nil { - e.logger.Error("error while closing connection", zap.Error(err)) - } - e.logger.Error("error while receiving sync", zap.Error(err)) - e.syncingTarget = nil - e.syncingStatus = SyncStatusFailed - return latest, errors.Wrap(err, "reverse optimistic sync") - } - } - - go func() { - defer func() { e.syncingTarget = nil }() - e.logger.Info("continuing sync in background") - s, err := client.GetCompressedSyncFrames( - context.Background(), - &protobufs.ClockFramesRequest{ - Filter: e.filter, - FromFrameNumber: from - 1, - ToFrameNumber: maxFrame, - }, - grpc.MaxCallRecvMsgSize(600*1024*1024), - ) - if err != nil { - e.logger.Error( - "error while retrieving sync", - zap.Error(err), - ) - e.peerMapMx.Lock() - if _, ok := e.peerMap[string(peerId)]; ok { - e.uncooperativePeersMap[string(peerId)] = e.peerMap[string(peerId)] - delete(e.peerMap, string(peerId)) - } - e.peerMapMx.Unlock() - e.syncingStatus = SyncStatusFailed - - if err := cc.Close(); err != nil { - e.logger.Error("error while closing connection", zap.Error(err)) - } - return - } else { - var syncMsg *protobufs.CeremonyCompressedSync - for syncMsg, err = s.Recv(); err == nil; syncMsg, err = s.Recv() { - e.logger.Debug( - "received compressed sync frame", - zap.Uint64("from", syncMsg.FromFrameNumber), - zap.Uint64("to", syncMsg.ToFrameNumber), - zap.Int("frames", len(syncMsg.TruncatedClockFrames)), - zap.Int("proofs", len(syncMsg.Proofs)), - ) - if _, err = e.decompressAndStoreCandidates( - peerId, - syncMsg, - e.logger.Debug, - ); err != nil && !errors.Is(err, ErrNoNewFrames) { - e.logger.Error( - "could not decompress and store candidate", - zap.Error(err), - ) - e.syncingTarget = nil - e.syncingStatus = SyncStatusFailed - if err := cc.Close(); err != nil { - e.logger.Error("error while closing connection", zap.Error(err)) - } - return - } - } - if err != nil && err != io.EOF && !errors.Is(err, ErrNoNewFrames) { - e.syncingTarget = nil - e.syncingStatus = SyncStatusFailed - e.logger.Error("error while receiving sync", zap.Error(err)) - if err := cc.Close(); err != nil { - e.logger.Error("error while closing connection", zap.Error(err)) - } - return - } - } - - if err := cc.Close(); err != nil { - e.logger.Error("error while closing connection", zap.Error(err)) - } - - e.syncingTarget = nil - e.syncingStatus = SyncStatusNotSyncing - }() - - return latest, nil -} - func (e *CeremonyDataClockConsensusEngine) sync( currentLatest *protobufs.ClockFrame, maxFrame uint64, @@ -1095,18 +961,48 @@ func (e *CeremonyDataClockConsensusEngine) sync( from = 1 } - if maxFrame > from { - s, err := client.GetCompressedSyncFrames( - context.Background(), - &protobufs.ClockFramesRequest{ - Filter: e.filter, - FromFrameNumber: maxFrame - 16, - }, - grpc.MaxCallRecvMsgSize(600*1024*1024), + if maxFrame > from && maxFrame > 3 { + from = maxFrame - 2 + } + + s, err := client.GetCompressedSyncFrames( + context.Background(), + &protobufs.ClockFramesRequest{ + Filter: e.filter, + FromFrameNumber: from, + }, + grpc.MaxCallRecvMsgSize(600*1024*1024), + ) + if err != nil { + e.logger.Error( + "received error from peer", + zap.Error(err), ) - if err != nil { + e.peerMapMx.Lock() + if _, ok := e.peerMap[string(peerId)]; ok { + e.uncooperativePeersMap[string(peerId)] = e.peerMap[string(peerId)] + delete(e.peerMap, string(peerId)) + } + e.peerMapMx.Unlock() + return latest, errors.Wrap(err, "reverse optimistic sync") + } + var syncMsg *protobufs.CeremonyCompressedSync + for syncMsg, err = s.Recv(); err == nil; syncMsg, err = s.Recv() { + e.logger.Info( + "received compressed sync frame", + zap.Uint64("from", syncMsg.FromFrameNumber), + zap.Uint64("to", syncMsg.ToFrameNumber), + zap.Int("frames", len(syncMsg.TruncatedClockFrames)), + zap.Int("proofs", len(syncMsg.Proofs)), + ) + var next *protobufs.ClockFrame + if next, err = e.decompressAndStoreCandidates( + peerId, + syncMsg, + e.logger.Info, + ); err != nil && !errors.Is(err, ErrNoNewFrames) { e.logger.Error( - "received error from peer", + "could not decompress and store candidate", zap.Error(err), ) e.peerMapMx.Lock() @@ -1115,56 +1011,31 @@ func (e *CeremonyDataClockConsensusEngine) sync( delete(e.peerMap, string(peerId)) } e.peerMapMx.Unlock() - return latest, errors.Wrap(err, "reverse optimistic sync") - } - var syncMsg *protobufs.CeremonyCompressedSync - for syncMsg, err = s.Recv(); err == nil; syncMsg, err = s.Recv() { - e.logger.Info( - "received compressed sync frame", - zap.Uint64("from", syncMsg.FromFrameNumber), - zap.Uint64("to", syncMsg.ToFrameNumber), - zap.Int("frames", len(syncMsg.TruncatedClockFrames)), - zap.Int("proofs", len(syncMsg.Proofs)), - ) - var next *protobufs.ClockFrame - if next, err = e.decompressAndStoreCandidates( - peerId, - syncMsg, - e.logger.Info, - ); err != nil && !errors.Is(err, ErrNoNewFrames) { - e.logger.Error( - "could not decompress and store candidate", - zap.Error(err), - ) - e.peerMapMx.Lock() - if _, ok := e.peerMap[string(peerId)]; ok { - e.uncooperativePeersMap[string(peerId)] = e.peerMap[string(peerId)] - delete(e.peerMap, string(peerId)) - } - e.peerMapMx.Unlock() - if err := cc.Close(); err != nil { - e.logger.Error("error while closing connection", zap.Error(err)) - } - - return currentLatest, errors.Wrap(err, "reverse optimistic sync") - } - if next != nil { - latest = next - } - } - if err != nil && err != io.EOF && !errors.Is(err, ErrNoNewFrames) { if err := cc.Close(); err != nil { e.logger.Error("error while closing connection", zap.Error(err)) } - e.logger.Error("error while receiving sync", zap.Error(err)) - return latest, errors.Wrap(err, "reverse optimistic sync") - } - e.logger.Info("received new leading frame", zap.Uint64("frame_number", latest.FrameNumber)) + return currentLatest, errors.Wrap(err, "reverse optimistic sync") + } + if next != nil { + latest = next + } + } + if err != nil && err != io.EOF && !errors.Is(err, ErrNoNewFrames) { if err := cc.Close(); err != nil { e.logger.Error("error while closing connection", zap.Error(err)) } + e.logger.Error("error while receiving sync", zap.Error(err)) + return latest, errors.Wrap(err, "reverse optimistic sync") + } + + e.logger.Info( + "received new leading frame", + zap.Uint64("frame_number", latest.FrameNumber), + ) + if err := cc.Close(); err != nil { + e.logger.Error("error while closing connection", zap.Error(err)) } return latest, nil @@ -1181,43 +1052,31 @@ func (e *CeremonyDataClockConsensusEngine) collect( latest = e.previousHead e.syncingStatus = SyncStatusNotSyncing } - maxFrame := uint64(0) - var peerId []byte peerId, maxFrame, err := e.GetMostAheadPeer() if err != nil { e.logger.Warn("no peers available, skipping sync") } else if peerId == nil { e.logger.Info("currently up to date, skipping sync") - } else if e.syncingTarget == nil { - e.syncingStatus = SyncStatusAwaitingResponse - e.logger.Info( - "setting syncing target", - zap.String("peer_id", peer.ID(peerId).String()), - ) - - e.syncingTarget = peerId - e.previousHead = latest - latest, err = e.reverseOptimisticSync(latest, maxFrame, peerId) - } else if maxFrame > latest.FrameNumber { + } else if maxFrame-2 > latest.FrameNumber { latest, err = e.sync(latest, maxFrame, peerId) } - go func() { - _, err = e.keyStore.GetProvingKey(e.provingKeyBytes) - if errors.Is(err, store.ErrNotFound) && - latest.FrameNumber-e.lastKeyBundleAnnouncementFrame > 6 { - if err = e.announceKeyBundle(); err != nil { - panic(err) - } - e.lastKeyBundleAnnouncementFrame = latest.FrameNumber - } - }() - e.logger.Info( "returning leader frame", zap.Uint64("frame_number", latest.FrameNumber), ) + e.logger.Info("selecting leader") + + latest, err = e.commitLongestPath(latest) + if err != nil { + e.logger.Error("could not collect longest path", zap.Error(err)) + latest, _, err = e.clockStore.GetDataClockFrame(e.filter, 0) + if err != nil { + panic(err) + } + } + e.setFrame(latest) e.state = consensus.EngineStateProving return latest, nil diff --git a/node/consensus/ceremony/execution_registration.go b/node/consensus/ceremony/execution_registration.go index 7e89a08..b781d1a 100644 --- a/node/consensus/ceremony/execution_registration.go +++ b/node/consensus/ceremony/execution_registration.go @@ -17,11 +17,11 @@ func (e *CeremonyDataClockConsensusEngine) RegisterExecutor( for { logger.Info( "awaiting frame", - zap.Uint64("current_frame", e.frame), + zap.Uint64("current_frame", e.frame.FrameNumber), zap.Uint64("target_frame", frame), ) - newFrame := e.frame + newFrame := e.frame.FrameNumber if newFrame >= frame { logger.Info( "injecting execution engine at frame", @@ -54,11 +54,11 @@ func (e *CeremonyDataClockConsensusEngine) UnregisterExecutor( for { logger.Info( "awaiting frame", - zap.Uint64("current_frame", e.frame), + zap.Uint64("current_frame", e.frame.FrameNumber), zap.Uint64("target_frame", frame), ) - newFrame := e.frame + newFrame := e.frame.FrameNumber if newFrame >= frame { logger.Info( "removing execution engine at frame", diff --git a/node/consensus/ceremony/peer_messaging.go b/node/consensus/ceremony/peer_messaging.go index 8f4c41b..07f1a5a 100644 --- a/node/consensus/ceremony/peer_messaging.go +++ b/node/consensus/ceremony/peer_messaging.go @@ -11,7 +11,6 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" - "source.quilibrium.com/quilibrium/monorepo/go-libp2p-blossomsub/pb" "source.quilibrium.com/quilibrium/monorepo/node/execution/ceremony/application" "source.quilibrium.com/quilibrium/monorepo/node/p2p" "source.quilibrium.com/quilibrium/monorepo/node/protobufs" @@ -20,52 +19,6 @@ import ( var ErrNoNewFrames = errors.New("peer reported no frames") -func (e *CeremonyDataClockConsensusEngine) handleSync( - message *pb.Message, -) error { - e.logger.Debug( - "received message", - zap.Binary("data", message.Data), - zap.Binary("from", message.From), - zap.Binary("signature", message.Signature), - ) - if bytes.Equal(message.From, e.pubSub.GetPeerID()) { - return nil - } - - msg := &protobufs.Message{} - - if err := proto.Unmarshal(message.Data, msg); err != nil { - return errors.Wrap(err, "handle sync") - } - - any := &anypb.Any{} - if err := proto.Unmarshal(msg.Payload, any); err != nil { - return errors.Wrap(err, "handle sync") - } - - switch any.TypeUrl { - case protobufs.ProvingKeyAnnouncementType: - if err := e.handleProvingKey( - message.From, - msg.Address, - any, - ); err != nil { - return errors.Wrap(err, "handle sync") - } - case protobufs.KeyBundleAnnouncementType: - if err := e.handleKeyBundle( - message.From, - msg.Address, - any, - ); err != nil { - return errors.Wrap(err, "handle sync") - } - } - - return nil -} - // GetCompressedSyncFrames implements protobufs.CeremonyServiceServer. func (e *CeremonyDataClockConsensusEngine) GetCompressedSyncFrames( request *protobufs.ClockFramesRequest, @@ -153,7 +106,7 @@ func (e *CeremonyDataClockConsensusEngine) GetCompressedSyncFrames( } } - max := e.frame + max := e.frame.FrameNumber to := request.ToFrameNumber // We need to slightly rewind, to compensate for unconfirmed frame heads on a @@ -469,93 +422,3 @@ func (e *CeremonyDataClockConsensusEngine) GetPublicChannel( ) error { return errors.New("not supported") } - -func (e *CeremonyDataClockConsensusEngine) handleProvingKeyRequest( - peerID []byte, - address []byte, - any *anypb.Any, -) error { - if bytes.Equal(peerID, e.pubSub.GetPeerID()) { - return nil - } - - request := &protobufs.ProvingKeyRequest{} - if err := any.UnmarshalTo(request); err != nil { - return nil - } - - if len(request.ProvingKeyBytes) == 0 { - e.logger.Debug( - "received proving key request for empty key", - zap.Binary("peer_id", peerID), - zap.Binary("address", address), - ) - return nil - } - - e.pubSub.Subscribe( - append(append([]byte{}, e.filter...), peerID...), - e.handleSync, - true, - ) - - e.logger.Debug( - "received proving key request", - zap.Binary("peer_id", peerID), - zap.Binary("address", address), - zap.Binary("proving_key", request.ProvingKeyBytes), - ) - - var provingKey *protobufs.ProvingKeyAnnouncement - inclusion, err := e.keyStore.GetProvingKey(request.ProvingKeyBytes) - if err != nil { - if !errors.Is(err, store.ErrNotFound) { - e.logger.Debug( - "peer asked for proving key that returned error", - zap.Binary("peer_id", peerID), - zap.Binary("address", address), - zap.Binary("proving_key", request.ProvingKeyBytes), - ) - return nil - } - - provingKey, err = e.keyStore.GetStagedProvingKey(request.ProvingKeyBytes) - if !errors.Is(err, store.ErrNotFound) { - e.logger.Debug( - "peer asked for proving key that returned error", - zap.Binary("peer_id", peerID), - zap.Binary("address", address), - zap.Binary("proving_key", request.ProvingKeyBytes), - ) - return nil - } else if err != nil { - e.logger.Debug( - "peer asked for unknown proving key", - zap.Binary("peer_id", peerID), - zap.Binary("address", address), - zap.Binary("proving_key", request.ProvingKeyBytes), - ) - return nil - } - } else { - err := proto.Unmarshal(inclusion.Data, provingKey) - if err != nil { - e.logger.Debug( - "inclusion commitment could not be deserialized", - zap.Binary("peer_id", peerID), - zap.Binary("address", address), - zap.Binary("proving_key", request.ProvingKeyBytes), - ) - return nil - } - } - - if err := e.publishMessage( - append(append([]byte{}, e.filter...), peerID...), - provingKey, - ); err != nil { - return nil - } - - return nil -} diff --git a/node/consensus/consensus_engine.go b/node/consensus/consensus_engine.go index d9f0fb4..d3b8eca 100644 --- a/node/consensus/consensus_engine.go +++ b/node/consensus/consensus_engine.go @@ -28,22 +28,21 @@ type ConsensusEngine interface { Stop(force bool) <-chan error RegisterExecutor(exec execution.ExecutionEngine, frame uint64) <-chan error UnregisterExecutor(name string, frame uint64, force bool) <-chan error - GetFrame() uint64 - GetDifficulty() uint32 - GetState() EngineState - GetFrameChannel() <-chan uint64 -} - -type DataConsensusEngine interface { - Start(filter []byte, seed []byte) <-chan error - Stop(force bool) <-chan error - RegisterExecutor(exec execution.ExecutionEngine, frame uint64) <-chan error - UnregisterExecutor(name string, frame uint64, force bool) <-chan error - GetFrame() uint64 + GetFrame() *protobufs.ClockFrame + GetDifficulty() uint32 + GetState() EngineState + GetFrameChannel() <-chan *protobufs.ClockFrame +} + +type DataConsensusEngine interface { + Start() <-chan error + Stop(force bool) <-chan error + RegisterExecutor(exec execution.ExecutionEngine, frame uint64) <-chan error + UnregisterExecutor(name string, frame uint64, force bool) <-chan error + GetFrame() *protobufs.ClockFrame GetDifficulty() uint32 GetState() EngineState GetFrameChannel() <-chan *protobufs.ClockFrame - GetActiveFrame() *protobufs.ClockFrame GetProvingKey( engineConfig *config.EngineConfig, ) (crypto.Signer, keys.KeyType, []byte, []byte) @@ -52,13 +51,13 @@ type DataConsensusEngine interface { } func GetMinimumVersionCutoff() time.Time { - return time.Date(2023, time.December, 2, 7, 0, 0, 0, time.UTC) + return time.Date(2024, time.January, 3, 7, 0, 0, 0, time.UTC) } func GetMinimumVersion() []byte { - return []byte{0x01, 0x01, 0x08} + return []byte{0x01, 0x02, 0x00} } func GetVersion() []byte { - return []byte{0x01, 0x01, 0x08} + return []byte{0x01, 0x02, 0x00} } diff --git a/node/consensus/master/broadcast_messaging.go b/node/consensus/master/broadcast_messaging.go index ba59f07..a956a3e 100644 --- a/node/consensus/master/broadcast_messaging.go +++ b/node/consensus/master/broadcast_messaging.go @@ -36,7 +36,6 @@ func (e *MasterClockConsensusEngine) handleMessage(message *pb.Message) error { eg := errgroup.Group{} eg.SetLimit(len(e.executionEngines)) - for name := range e.executionEngines { name := name eg.Go(func() error { @@ -52,7 +51,6 @@ func (e *MasterClockConsensusEngine) handleMessage(message *pb.Message) error { ) return errors.Wrap(err, "handle message") } - for _, m := range messages { m := m if err := e.publishMessage(m.Address, m); err != nil { @@ -64,11 +62,9 @@ func (e *MasterClockConsensusEngine) handleMessage(message *pb.Message) error { return errors.Wrap(err, "handle message") } } - return nil }) } - if err := eg.Wait(); err != nil { e.logger.Error("rejecting invalid message", zap.Error(err)) return errors.Wrap(err, "execution failed") @@ -96,7 +92,7 @@ func (e *MasterClockConsensusEngine) handleClockFrameData( return errors.Wrap(err, "handle clock frame data") } - if e.frame > frame.FrameNumber { + if e.frame.FrameNumber > frame.FrameNumber { e.logger.Debug( "received anachronistic frame", zap.Binary("sender", peerID), @@ -131,7 +127,7 @@ func (e *MasterClockConsensusEngine) handleClockFrameData( return errors.Wrap(err, "handle clock frame data") } - if e.frame < frame.FrameNumber { + if e.frame.FrameNumber < frame.FrameNumber { if err := e.enqueueSeenFrame(frame); err != nil { e.logger.Error("could not enqueue seen clock frame", zap.Error(err)) return errors.Wrap(err, "handle clock frame data") diff --git a/node/consensus/master/consensus_frames.go b/node/consensus/master/consensus_frames.go index 53693a2..04bd699 100644 --- a/node/consensus/master/consensus_frames.go +++ b/node/consensus/master/consensus_frames.go @@ -43,8 +43,7 @@ func (e *MasterClockConsensusEngine) setFrame(frame *protobufs.ClockFrame) { copy(previousSelectorBytes[:], frame.Output[:516]) e.logger.Debug("set frame", zap.Uint64("frame_number", frame.FrameNumber)) - e.frame = frame.FrameNumber - e.latestFrame = frame + e.frame = frame go func() { e.frameChan <- e.frame @@ -53,7 +52,7 @@ func (e *MasterClockConsensusEngine) setFrame(frame *protobufs.ClockFrame) { func ( e *MasterClockConsensusEngine, -) createGenesisFrame() *protobufs.ClockFrame { +) CreateGenesisFrame() *protobufs.ClockFrame { e.logger.Debug("creating genesis frame") b := sha3.Sum256(e.input) v := vdf.New(e.difficulty, b) @@ -65,7 +64,7 @@ func ( e.logger.Debug("proving genesis frame") input := []byte{} input = append(input, e.filter...) - input = binary.BigEndian.AppendUint64(input, e.frame) + input = binary.BigEndian.AppendUint64(input, 0) input = binary.BigEndian.AppendUint32(input, e.difficulty) if bytes.Equal(e.input, []byte{0x00}) { value := [516]byte{} @@ -82,7 +81,7 @@ func ( frame := &protobufs.ClockFrame{ Filter: e.filter, - FrameNumber: e.frame, + FrameNumber: 0, Timestamp: 0, Difficulty: e.difficulty, Input: inputMessage, @@ -107,13 +106,13 @@ func (e *MasterClockConsensusEngine) collect( if e.state == consensus.EngineStateCollecting { e.logger.Debug("collecting vdf proofs") - latest := e.latestFrame + latest := e.frame if e.syncingStatus == SyncStatusNotSyncing { peer, err := e.pubSub.GetRandomPeer(e.filter) if err != nil { if errors.Is(err, p2p.ErrNoPeersAvailable) { - e.logger.Warn("no peers available, skipping sync") + e.logger.Debug("no peers available, skipping sync") } else { e.logger.Error("error while fetching random peer", zap.Error(err)) } @@ -200,10 +199,10 @@ func ( }) if len(e.seenFrames) == 0 { - return e.latestFrame, nil + return e.frame, nil } - prev := e.latestFrame + prev := e.frame committedSet := []*protobufs.ClockFrame{} for len(e.seenFrames) > 0 { diff --git a/node/consensus/master/execution_registration.go b/node/consensus/master/execution_registration.go index 69657cb..e5dde0c 100644 --- a/node/consensus/master/execution_registration.go +++ b/node/consensus/master/execution_registration.go @@ -17,7 +17,7 @@ func (e *MasterClockConsensusEngine) RegisterExecutor( go func() { logger.Info( "starting execution engine at frame", - zap.Uint64("current_frame", e.frame), + zap.Uint64("current_frame", e.frame.FrameNumber), ) err := <-exec.Start() if err != nil { @@ -29,11 +29,11 @@ func (e *MasterClockConsensusEngine) RegisterExecutor( for { logger.Info( "awaiting frame", - zap.Uint64("current_frame", e.frame), + zap.Uint64("current_frame", e.frame.FrameNumber), zap.Uint64("target_frame", frame), ) - newFrame := e.frame + newFrame := e.frame.FrameNumber if newFrame >= frame { logger.Info( "injecting execution engine at frame", @@ -76,11 +76,11 @@ func (e *MasterClockConsensusEngine) UnregisterExecutor( for { logger.Info( "awaiting frame", - zap.Uint64("current_frame", e.frame), + zap.Uint64("current_frame", e.frame.FrameNumber), zap.Uint64("target_frame", frame), ) - newFrame := e.frame + newFrame := e.frame.FrameNumber if newFrame >= frame { logger.Info( "removing execution engine at frame", diff --git a/node/consensus/master/master_clock_consensus_engine.go b/node/consensus/master/master_clock_consensus_engine.go index 130149a..e8f6c22 100644 --- a/node/consensus/master/master_clock_consensus_engine.go +++ b/node/consensus/master/master_clock_consensus_engine.go @@ -25,16 +25,15 @@ const ( ) type MasterClockConsensusEngine struct { - frame uint64 + frame *protobufs.ClockFrame difficulty uint32 logger *zap.Logger state consensus.EngineState pubSub p2p.PubSub keyManager keys.KeyManager lastFrameReceivedAt time.Time - latestFrame *protobufs.ClockFrame - frameChan chan uint64 + frameChan chan *protobufs.ClockFrame executionEngines map[string]execution.ExecutionEngine filter []byte input []byte @@ -79,20 +78,29 @@ func NewMasterClockConsensusEngine( } e := &MasterClockConsensusEngine{ - frame: 0, + frame: nil, difficulty: 10000, logger: logger, state: consensus.EngineStateStopped, keyManager: keyManager, pubSub: pubSub, - frameChan: make(chan uint64), executionEngines: map[string]execution.ExecutionEngine{}, + frameChan: make(chan *protobufs.ClockFrame), input: seed, lastFrameReceivedAt: time.Time{}, syncingStatus: SyncStatusNotSyncing, clockStore: clockStore, } + latestFrame, err := e.clockStore.GetLatestMasterClockFrame(e.filter) + if err != nil && !errors.Is(err, store.ErrNotFound) { + panic(err) + } + + if latestFrame != nil { + e.frame = latestFrame + } + if e.filter, err = hex.DecodeString(engineConfig.Filter); err != nil { panic(errors.Wrap(err, "could not parse filter value")) } @@ -103,7 +111,7 @@ func NewMasterClockConsensusEngine( } func (e *MasterClockConsensusEngine) Start() <-chan error { - e.logger.Info("starting consensus engine") + e.logger.Info("starting master consensus engine") e.state = consensus.EngineStateStarting errChan := make(chan error) @@ -112,7 +120,7 @@ func (e *MasterClockConsensusEngine) Start() <-chan error { latestFrame, err := e.clockStore.GetLatestMasterClockFrame(e.filter) if err != nil && errors.Is(err, store.ErrNotFound) { - latestFrame = e.createGenesisFrame() + latestFrame = e.CreateGenesisFrame() txn, err := e.clockStore.NewTransaction() if err != nil { panic(err) @@ -131,11 +139,111 @@ func (e *MasterClockConsensusEngine) Start() <-chan error { e.setFrame(latestFrame) } + e.buildHistoricFrameCache(latestFrame) + + e.logger.Info("subscribing to pubsub messages") + e.pubSub.Subscribe(e.filter, e.handleMessage, true) + e.pubSub.Subscribe(e.pubSub.GetPeerID(), e.handleSync, true) + + e.state = consensus.EngineStateCollecting + + go func() { + for { + e.logger.Info( + "peers in store", + zap.Int("peer_store_count", e.pubSub.GetPeerstoreCount()), + zap.Int("network_peer_count", e.pubSub.GetNetworkPeersCount()), + ) + time.Sleep(10 * time.Second) + } + }() + + go func() { + for e.state < consensus.EngineStateStopping { + var err error + switch e.state { + case consensus.EngineStateCollecting: + currentFrame := latestFrame + if latestFrame, err = e.collect(latestFrame); err != nil { + e.logger.Error("could not collect", zap.Error(err)) + latestFrame = currentFrame + } + case consensus.EngineStateProving: + currentFrame := latestFrame + if latestFrame, err = e.prove(latestFrame); err != nil { + e.logger.Error("could not prove", zap.Error(err)) + latestFrame = currentFrame + } + case consensus.EngineStatePublishing: + if err = e.publishProof(latestFrame); err != nil { + e.logger.Error("could not publish", zap.Error(err)) + } + } + } + }() + + go func() { + errChan <- nil + }() + + return errChan +} + +func (e *MasterClockConsensusEngine) Stop(force bool) <-chan error { + e.logger.Info("stopping consensus engine") + e.state = consensus.EngineStateStopping + errChan := make(chan error) + + wg := sync.WaitGroup{} + wg.Add(len(e.executionEngines)) + for name := range e.executionEngines { + name := name + go func(name string) { + err := <-e.UnregisterExecutor(name, e.frame.FrameNumber, force) + if err != nil { + errChan <- err + } + wg.Done() + }(name) + } + + e.logger.Info("waiting for execution engines to stop") + wg.Wait() + e.logger.Info("execution engines stopped") + + e.state = consensus.EngineStateStopped + go func() { + errChan <- nil + }() + return errChan +} + +func (e *MasterClockConsensusEngine) GetDifficulty() uint32 { + return e.difficulty +} + +func (e *MasterClockConsensusEngine) GetFrame() *protobufs.ClockFrame { + return e.frame +} + +func (e *MasterClockConsensusEngine) GetState() consensus.EngineState { + return e.state +} + +func ( + e *MasterClockConsensusEngine, +) GetFrameChannel() <-chan *protobufs.ClockFrame { + return e.frameChan +} + +func (e *MasterClockConsensusEngine) buildHistoricFrameCache( + latestFrame *protobufs.ClockFrame, +) { e.historicFrames = []*protobufs.ClockFrame{} if latestFrame.FrameNumber != 0 { min := uint64(0) - if latestFrame.FrameNumber-255 > min { + if latestFrame.FrameNumber-255 > min && latestFrame.FrameNumber > 255 { min = latestFrame.FrameNumber - 255 } @@ -163,98 +271,4 @@ func (e *MasterClockConsensusEngine) Start() <-chan error { } e.historicFrames = append(e.historicFrames, latestFrame) - - e.logger.Info("subscribing to pubsub messages") - e.pubSub.Subscribe(e.filter, e.handleMessage, true) - e.pubSub.Subscribe(e.pubSub.GetPeerID(), e.handleSync, true) - - e.state = consensus.EngineStateCollecting - - go func() { - for { - e.logger.Info( - "peers in store", - zap.Int("peer_store_count", e.pubSub.GetPeerstoreCount()), - zap.Int("network_peer_count", e.pubSub.GetNetworkPeersCount()), - ) - time.Sleep(10 * time.Second) - } - }() - - go func() { - for e.state < consensus.EngineStateStopping { - var err error - switch e.state { - case consensus.EngineStateCollecting: - if latestFrame, err = e.collect(latestFrame); err != nil { - e.logger.Error("could not collect", zap.Error(err)) - errChan <- err - } - case consensus.EngineStateProving: - if latestFrame, err = e.prove(latestFrame); err != nil { - e.logger.Error("could not prove", zap.Error(err)) - errChan <- err - } - case consensus.EngineStatePublishing: - if err = e.publishProof(latestFrame); err != nil { - e.logger.Error("could not publish", zap.Error(err)) - errChan <- err - } - } - } - }() - - go func() { - errChan <- nil - }() - - return errChan -} - -func (e *MasterClockConsensusEngine) Stop(force bool) <-chan error { - e.logger.Info("stopping consensus engine") - e.state = consensus.EngineStateStopping - errChan := make(chan error) - - wg := sync.WaitGroup{} - wg.Add(len(e.executionEngines)) - for name := range e.executionEngines { - name := name - go func(name string) { - err := <-e.UnregisterExecutor(name, e.frame, force) - if err != nil { - errChan <- err - } - wg.Done() - }(name) - } - - e.logger.Info("waiting for execution engines to stop") - wg.Wait() - e.logger.Info("execution engines stopped") - - e.state = consensus.EngineStateStopped - - e.engineMx.Lock() - defer e.engineMx.Unlock() - go func() { - errChan <- nil - }() - return errChan -} - -func (e *MasterClockConsensusEngine) GetDifficulty() uint32 { - return e.difficulty -} - -func (e *MasterClockConsensusEngine) GetFrame() uint64 { - return e.frame -} - -func (e *MasterClockConsensusEngine) GetState() consensus.EngineState { - return e.state -} - -func (e *MasterClockConsensusEngine) GetFrameChannel() <-chan uint64 { - return e.frameChan } diff --git a/node/consensus/master/peer_messaging.go b/node/consensus/master/peer_messaging.go index 5afedbb..9d64c6b 100644 --- a/node/consensus/master/peer_messaging.go +++ b/node/consensus/master/peer_messaging.go @@ -5,7 +5,6 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" - "golang.org/x/sync/errgroup" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "source.quilibrium.com/quilibrium/monorepo/go-libp2p-blossomsub/pb" @@ -30,46 +29,6 @@ func (e *MasterClockConsensusEngine) handleSync(message *pb.Message) error { return errors.Wrap(err, "handle sync") } - eg := errgroup.Group{} - eg.SetLimit(len(e.executionEngines)) - - for name := range e.executionEngines { - name := name - eg.Go(func() error { - messages, err := e.executionEngines[name].ProcessMessage( - msg.Address, - msg, - ) - if err != nil { - e.logger.Error( - "could not process message for engine", - zap.Error(err), - zap.String("engine_name", name), - ) - return errors.Wrap(err, "handle message") - } - - for _, m := range messages { - m := m - if err := e.publishMessage(e.filter, m); err != nil { - e.logger.Error( - "could not publish message for engine", - zap.Error(err), - zap.String("engine_name", name), - ) - return errors.Wrap(err, "handle message") - } - } - - return nil - }) - } - - if err := eg.Wait(); err != nil { - e.logger.Error("rejecting invalid message", zap.Error(err)) - return errors.Wrap(err, "handle sync") - } - switch any.TypeUrl { case protobufs.ClockFramesResponseType: if err := e.handleClockFramesResponse( @@ -149,7 +108,7 @@ func (e *MasterClockConsensusEngine) handleClockFramesResponse( zap.Uint64("frame_number", frame.FrameNumber), ) - if e.frame < frame.FrameNumber { + if e.frame.FrameNumber < frame.FrameNumber { if err := e.enqueueSeenFrame(frame); err != nil { e.logger.Error("could not enqueue seen clock frame", zap.Error(err)) return errors.Wrap(err, "handle clock frame response") @@ -186,7 +145,7 @@ func (e *MasterClockConsensusEngine) handleClockFramesRequest( from := request.FromFrameNumber - if e.frame < from || len(e.historicFrames) == 0 { + if e.frame.FrameNumber < from || len(e.historicFrames) == 0 { e.logger.Debug( "peer asked for undiscovered frame", zap.Binary("peer_id", peerID), @@ -210,8 +169,8 @@ func (e *MasterClockConsensusEngine) handleClockFramesRequest( to = request.FromFrameNumber + 127 } - if int(to) > int(e.latestFrame.FrameNumber) { - to = e.latestFrame.FrameNumber + if int(to) > int(e.frame.FrameNumber) { + to = e.frame.FrameNumber } e.logger.Debug( diff --git a/node/crypto/kzg.go b/node/crypto/kzg.go index ffbf9a6..1c0488a 100644 --- a/node/crypto/kzg.go +++ b/node/crypto/kzg.go @@ -75,6 +75,221 @@ var CeremonyPotPubKeys []curves.PairingPoint var CeremonySignatories []curves.Point var FFTBLS48581 map[uint64][]curves.PairingPoint = make(map[uint64][]curves.PairingPoint) +func TestInit(file string) { + // start with phase 1 ceremony: + csBytes, err := os.ReadFile(file) + if err != nil { + panic(err) + } + + bls48581.Init() + + cs := &CeremonyState{} + if err := json.Unmarshal(csBytes, cs); err != nil { + panic(err) + } + + g1s := make([]curves.PairingPoint, 1024) + g2s := make([]curves.PairingPoint, 257) + g1ffts := make([]curves.PairingPoint, 1024) + wg := sync.WaitGroup{} + wg.Add(1024) + + for i := 0; i < 1024; i++ { + i := i + go func() { + b, err := hex.DecodeString(cs.PowersOfTau.G1Affines[i][2:]) + if err != nil { + panic(err) + } + g1, err := curves.BLS48581G1().NewGeneratorPoint().FromAffineCompressed(b) + if err != nil { + panic(err) + } + g1s[i] = g1.(curves.PairingPoint) + + f, err := hex.DecodeString(cs.PowersOfTau.G1FFT[i][2:]) + if err != nil { + panic(err) + } + g1fft, err := curves.BLS48581G1().NewGeneratorPoint().FromAffineCompressed(f) + if err != nil { + panic(err) + } + g1ffts[i] = g1fft.(curves.PairingPoint) + + if i < 257 { + b, err := hex.DecodeString(cs.PowersOfTau.G2Affines[i][2:]) + if err != nil { + panic(err) + } + g2, err := curves.BLS48581G2().NewGeneratorPoint().FromAffineCompressed( + b, + ) + if err != nil { + panic(err) + } + g2s[i] = g2.(curves.PairingPoint) + } + wg.Done() + }() + } + + wg.Wait() + + wg.Add(len(cs.Witness.RunningProducts)) + CeremonyRunningProducts = make([]curves.PairingPoint, len(cs.Witness.RunningProducts)) + for i, s := range cs.Witness.RunningProducts { + i, s := i, s + go func() { + b, err := hex.DecodeString(s[2:]) + if err != nil { + panic(err) + } + + g1, err := curves.BLS48581G1().NewGeneratorPoint().FromAffineCompressed(b) + if err != nil { + panic(err) + } + CeremonyRunningProducts[i] = g1.(curves.PairingPoint) + wg.Done() + }() + } + wg.Wait() + + wg.Add(len(cs.Witness.PotPubKeys)) + CeremonyPotPubKeys = make([]curves.PairingPoint, len(cs.Witness.PotPubKeys)) + for i, s := range cs.Witness.PotPubKeys { + i, s := i, s + go func() { + b, err := hex.DecodeString(s[2:]) + if err != nil { + panic(err) + } + + g2, err := curves.BLS48581G2().NewGeneratorPoint().FromAffineCompressed(b) + if err != nil { + panic(err) + } + CeremonyPotPubKeys[i] = g2.(curves.PairingPoint) + wg.Done() + }() + } + wg.Wait() + + wg.Add(len(cs.VoucherPubKeys)) + CeremonySignatories = make([]curves.Point, len(cs.VoucherPubKeys)) + for i, s := range cs.VoucherPubKeys { + i, s := i, s + go func() { + b, err := hex.DecodeString(s[2:]) + if err != nil { + panic(err) + } + + CeremonySignatories[i], err = curves.ED448().Point.FromAffineCompressed(b) + if err != nil { + panic(err) + } + wg.Done() + }() + } + wg.Wait() + + CeremonyBLS48581G1 = g1s + CeremonyBLS48581G2 = g2s + + // Post-ceremony, precompute everything and put it in the finalized ceremony + // state + modulus := make([]byte, 73) + bls48581.NewBIGints(bls48581.CURVE_Order, nil).ToBytes(modulus) + q := new(big.Int).SetBytes(modulus) + sizes := []int64{16, 128, 1024} + + wg.Add(len(sizes)) + root := make([]curves.PairingScalar, 3) + roots := make([][]curves.PairingScalar, 3) + reverseRoots := make([][]curves.PairingScalar, 3) + ffts := make([][]curves.PairingPoint, 3) + + for idx, i := range sizes { + i := i + idx := idx + go func() { + exp := new(big.Int).Quo( + new(big.Int).Sub(q, big.NewInt(1)), + big.NewInt(i), + ) + rootOfUnity := new(big.Int).Exp(big.NewInt(int64(37)), exp, q) + roots[idx] = make([]curves.PairingScalar, i+1) + reverseRoots[idx] = make([]curves.PairingScalar, i+1) + wg2 := sync.WaitGroup{} + wg2.Add(int(i)) + for j := int64(0); j < i; j++ { + j := j + go func() { + rev := big.NewInt(int64(j)) + r := new(big.Int).Exp( + rootOfUnity, + rev, + q, + ) + scalar, _ := (&curves.ScalarBls48581{}).SetBigInt(r) + + if rev.Cmp(big.NewInt(1)) == 0 { + root[idx] = scalar.(curves.PairingScalar) + } + + roots[idx][j] = scalar.(curves.PairingScalar) + reverseRoots[idx][i-j] = roots[idx][j] + wg2.Done() + }() + } + wg2.Wait() + roots[idx][i] = roots[idx][0] + reverseRoots[idx][0] = reverseRoots[idx][i] + wg.Done() + }() + } + wg.Wait() + + wg.Add(len(sizes)) + for i := range root { + i := i + RootOfUnityBLS48581[uint64(sizes[i])] = root[i] + RootsOfUnityBLS48581[uint64(sizes[i])] = roots[i] + ReverseRootsOfUnityBLS48581[uint64(sizes[i])] = reverseRoots[i] + + go func() { + // We precomputed 65536, others are cheap and will be fully precomputed + // post-ceremony + if sizes[i] < 65536 { + fftG1, err := FFTG1( + CeremonyBLS48581G1[:sizes[i]], + *curves.BLS48581( + curves.BLS48581G1().NewGeneratorPoint(), + ), + uint64(sizes[i]), + true, + ) + if err != nil { + panic(err) + } + + ffts[i] = fftG1 + } else { + ffts[i] = g1ffts + } + wg.Done() + }() + } + wg.Wait() + + for i := range root { + FFTBLS48581[uint64(sizes[i])] = ffts[i] + } +} + func Init() { // start with phase 1 ceremony: csBytes, err := os.ReadFile("./ceremony.json") @@ -202,7 +417,7 @@ func Init() { // Post-ceremony, precompute everything and put it in the finalized ceremony // state modulus := make([]byte, 73) - bls48581.NewBIGints(bls48581.CURVE_Order).ToBytes(modulus) + bls48581.NewBIGints(bls48581.CURVE_Order, nil).ToBytes(modulus) q := new(big.Int).SetBytes(modulus) sizes := []int64{16, 128, 1024, 65536} @@ -310,7 +525,7 @@ func NewKZGProver( func DefaultKZGProver() *KZGProver { modulus := make([]byte, 73) - bls48581.NewBIGints(bls48581.CURVE_Order).ToBytes(modulus) + bls48581.NewBIGints(bls48581.CURVE_Order, nil).ToBytes(modulus) q := new(big.Int).SetBytes(modulus) return NewKZGProver( curves.BLS48581(curves.BLS48581G1().Point), @@ -426,7 +641,7 @@ func (p *KZGProver) EvaluateLagrangeForm( xBI := x.BigInt() modulus := make([]byte, 73) - bls48581.NewBIGints(bls48581.CURVE_Order).ToBytes(modulus) + bls48581.NewBIGints(bls48581.CURVE_Order, nil).ToBytes(modulus) q := new(big.Int).SetBytes(modulus) xBI.Exp(xBI, width.BigInt(), q) xBI.Sub(xBI, big.NewInt(1)) diff --git a/node/crypto/kzg_test.go b/node/crypto/kzg_test.go index 87c4668..7f684af 100644 --- a/node/crypto/kzg_test.go +++ b/node/crypto/kzg_test.go @@ -81,7 +81,7 @@ func TestMain(m *testing.M) { // Post-ceremony, precompute everything and put it in the finalized ceremony // state modulus := make([]byte, 73) - bls48581.NewBIGints(bls48581.CURVE_Order).ToBytes(modulus) + bls48581.NewBIGints(bls48581.CURVE_Order, nil).ToBytes(modulus) q := new(big.Int).SetBytes(modulus) sizes := []int64{16} @@ -173,7 +173,7 @@ func TestMain(m *testing.M) { func TestKzgBytesToPoly(t *testing.T) { modulus := make([]byte, 73) - bls48581.NewBIGints(bls48581.CURVE_Order).ToBytes(modulus) + bls48581.NewBIGints(bls48581.CURVE_Order, nil).ToBytes(modulus) q := new(big.Int).SetBytes(modulus) p := crypto.NewKZGProver(curves.BLS48581(curves.BLS48581G1().Point), sha3.New256, q) @@ -215,7 +215,7 @@ func TestKzgBytesToPoly(t *testing.T) { func TestPolynomialCommitment(t *testing.T) { modulus := make([]byte, 73) - bls48581.NewBIGints(bls48581.CURVE_Order).ToBytes(modulus) + bls48581.NewBIGints(bls48581.CURVE_Order, nil).ToBytes(modulus) q := new(big.Int).SetBytes(modulus) p := crypto.NewKZGProver(curves.BLS48581(curves.BLS48581G1().Point), sha3.New256, q) @@ -263,7 +263,7 @@ func TestPolynomialCommitment(t *testing.T) { func TestKZGProof(t *testing.T) { modulus := make([]byte, 73) - bls48581.NewBIGints(bls48581.CURVE_Order).ToBytes(modulus) + bls48581.NewBIGints(bls48581.CURVE_Order, nil).ToBytes(modulus) q := new(big.Int).SetBytes(modulus) p := crypto.NewKZGProver(curves.BLS48581(curves.BLS48581G1().Point), sha3.New256, q) @@ -290,27 +290,51 @@ func TestKZGProof(t *testing.T) { curves.BLS48581G1().NewGeneratorPoint(), ), 16, - false, + true, ) require.NoError(t, err) - commit, err := p.Commit(evalPoly) + commit, err := p.Commit(poly) require.NoError(t, err) - z, err := (&curves.ScalarBls48581{}).SetBigInt(big.NewInt(2)) + z := crypto.RootsOfUnityBLS48581[16][2] require.NoError(t, err) - checky := poly[len(poly)-1] - for i := len(poly) - 2; i >= 0; i-- { - checky = checky.Mul(z).Add(poly[i]).(curves.PairingScalar) + checky := evalPoly[len(poly)-1] + for i := len(evalPoly) - 2; i >= 0; i-- { + checky = checky.Mul(z).Add(evalPoly[i]).(curves.PairingScalar) } - y, err := p.EvaluateLagrangeForm(evalPoly, z.(curves.PairingScalar), 16, 0) - require.NoError(t, err) - require.Equal(t, y.Cmp(checky), 0) + fmt.Printf("%+x\n", checky.Bytes()) - proof, err := p.Prove(evalPoly, commit, z.(curves.PairingScalar)) + divisors := make([]curves.PairingScalar, 2) + divisors[0] = (&curves.ScalarBls48581{}).Zero().Sub(z).(*curves.ScalarBls48581) + divisors[1] = (&curves.ScalarBls48581{}).One().(*curves.ScalarBls48581) + + a := make([]curves.PairingScalar, len(evalPoly)) + for i := 0; i < len(a); i++ { + a[i] = evalPoly[i].Clone().(*curves.ScalarBls48581) + } + + // Adapted from Feist's amortized proofs: + aPos := len(a) - 1 + bPos := len(divisors) - 1 + diff := aPos - bPos + out := make([]curves.PairingScalar, diff+1, diff+1) + for diff >= 0 { + out[diff] = a[aPos].Div(divisors[bPos]).(*curves.ScalarBls48581) + for i := bPos; i >= 0; i-- { + a[diff+i] = a[diff+i].Sub( + out[diff].Mul(divisors[i]), + ).(*curves.ScalarBls48581) + } + aPos -= 1 + diff -= 1 + } + + proof, err := p.PointLinearCombination(crypto.CeremonyBLS48581G1[:15], out) + // proof, err := p.Prove(evalPoly, commit, z.(curves.PairingScalar)) require.NoError(t, err) - require.True(t, p.Verify(commit, z.(curves.PairingScalar), y, proof)) + require.True(t, p.Verify(commit, z, checky, proof)) commitments, err := p.CommitAggregate( [][]curves.PairingScalar{evalPoly}, diff --git a/node/execution/ceremony/application/ceremony_application.go b/node/execution/ceremony/application/ceremony_application.go index 0819a39..85a09ea 100644 --- a/node/execution/ceremony/application/ceremony_application.go +++ b/node/execution/ceremony/application/ceremony_application.go @@ -14,8 +14,6 @@ var ErrInvalidStateTransition = errors.New("invalid state transition") type CeremonyApplicationState int -const V118_CUTOFF = uint64(45000) - var CEREMONY_ADDRESS = []byte{ // SHA3-256("q_kzg_ceremony") 0x34, 0x00, 0x1b, 0xe7, 0x43, 0x2c, 0x2e, 0x66, @@ -50,7 +48,7 @@ type CeremonyApplication struct { StateCount uint64 RoundCount uint64 LobbyState CeremonyApplicationState - ActiveParticipants []*protobufs.Ed448PublicKey + ActiveParticipants []*protobufs.CeremonyLobbyJoin NextRoundPreferredParticipants []*protobufs.Ed448PublicKey LatestSeenProverAttestations []*protobufs.CeremonySeenProverAttestation DroppedParticipantAttestations []*protobufs.CeremonyDroppedProverAttestation @@ -82,8 +80,22 @@ func (a *CeremonyApplication) Equals(b *CeremonyApplication) bool { for i := range a.ActiveParticipants { if !bytes.Equal( - a.ActiveParticipants[i].KeyValue, - b.ActiveParticipants[i].KeyValue, + a.ActiveParticipants[i].PublicKeySignatureEd448.PublicKey.KeyValue, + b.ActiveParticipants[i].PublicKeySignatureEd448.PublicKey.KeyValue, + ) { + return false + } + + if !bytes.Equal( + a.ActiveParticipants[i].IdentityKey.KeyValue, + b.ActiveParticipants[i].IdentityKey.KeyValue, + ) { + return false + } + + if !bytes.Equal( + a.ActiveParticipants[i].SignedPreKey.KeyValue, + b.ActiveParticipants[i].SignedPreKey.KeyValue, ) { return false } @@ -856,7 +868,7 @@ func (a *CeremonyApplication) ApplyTransition( } } - if currentFrameNumber > V118_CUTOFF && a.StateCount > 100 { + if a.StateCount > 10 { shouldReset = true } @@ -866,17 +878,19 @@ func (a *CeremonyApplication) ApplyTransition( a.RoundCount = 0 for _, p := range a.ActiveParticipants { p := p - if _, ok := droppedProversMap[string(p.KeyValue)]; !ok { + if _, ok := droppedProversMap[string( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + )]; !ok { a.NextRoundPreferredParticipants = append( append( []*protobufs.Ed448PublicKey{}, - p, + p.PublicKeySignatureEd448.PublicKey, ), a.NextRoundPreferredParticipants..., ) } } - a.ActiveParticipants = []*protobufs.Ed448PublicKey{} + a.ActiveParticipants = []*protobufs.CeremonyLobbyJoin{} a.DroppedParticipantAttestations = []*protobufs.CeremonyDroppedProverAttestation{} a.LatestSeenProverAttestations = @@ -958,7 +972,7 @@ func (a *CeremonyApplication) ApplyTransition( } a.LobbyState = CEREMONY_APPLICATION_STATE_VALIDATING - a.ActiveParticipants = []*protobufs.Ed448PublicKey{} + a.ActiveParticipants = []*protobufs.CeremonyLobbyJoin{} a.DroppedParticipantAttestations = []*protobufs.CeremonyDroppedProverAttestation{} a.LatestSeenProverAttestations = @@ -984,7 +998,7 @@ func (a *CeremonyApplication) ApplyTransition( } } - if currentFrameNumber > V118_CUTOFF && a.StateCount > 100 { + if a.StateCount > 10 { shouldReset = true } @@ -994,17 +1008,19 @@ func (a *CeremonyApplication) ApplyTransition( a.RoundCount = 0 for _, p := range a.ActiveParticipants { p := p - if _, ok := droppedProversMap[string(p.KeyValue)]; !ok { + if _, ok := droppedProversMap[string( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + )]; !ok { a.NextRoundPreferredParticipants = append( append( []*protobufs.Ed448PublicKey{}, - p, + p.PublicKeySignatureEd448.PublicKey, ), a.NextRoundPreferredParticipants..., ) } } - a.ActiveParticipants = []*protobufs.Ed448PublicKey{} + a.ActiveParticipants = []*protobufs.CeremonyLobbyJoin{} a.DroppedParticipantAttestations = []*protobufs.CeremonyDroppedProverAttestation{} a.LatestSeenProverAttestations = @@ -1036,7 +1052,25 @@ func (a *CeremonyApplication) ApplyTransition( } } - if a.UpdatedTranscript == nil { + shouldReset := false + if a.StateCount > 100 { + shouldReset = true + } + + if shouldReset { + a.LobbyState = CEREMONY_APPLICATION_STATE_OPEN + a.StateCount = 0 + a.RoundCount = 0 + a.ActiveParticipants = []*protobufs.CeremonyLobbyJoin{} + a.DroppedParticipantAttestations = + []*protobufs.CeremonyDroppedProverAttestation{} + a.LatestSeenProverAttestations = + []*protobufs.CeremonySeenProverAttestation{} + a.TranscriptRoundAdvanceCommits = + []*protobufs.CeremonyAdvanceRound{} + a.TranscriptShares = + []*protobufs.CeremonyTranscriptShare{} + } else if a.UpdatedTranscript == nil { rewardMultiplier := uint64(1) for i := 0; i < len(a.FinalCommits)-1; i++ { rewardMultiplier = rewardMultiplier << 1 @@ -1064,7 +1098,7 @@ func (a *CeremonyApplication) ApplyTransition( a.LobbyState = CEREMONY_APPLICATION_STATE_OPEN a.StateCount = 0 a.RoundCount = 0 - a.ActiveParticipants = []*protobufs.Ed448PublicKey{} + a.ActiveParticipants = []*protobufs.CeremonyLobbyJoin{} a.DroppedParticipantAttestations = []*protobufs.CeremonyDroppedProverAttestation{} a.LatestSeenProverAttestations = diff --git a/node/execution/ceremony/application/ceremony_application_in_progress.go b/node/execution/ceremony/application/ceremony_application_in_progress.go index abcc6d7..b61e5f3 100644 --- a/node/execution/ceremony/application/ceremony_application_in_progress.go +++ b/node/execution/ceremony/application/ceremony_application_in_progress.go @@ -22,7 +22,10 @@ func (a *CeremonyApplication) applySeenProverAttestation( inParticipantList := false for _, p := range a.ActiveParticipants { - if bytes.Equal(p.KeyValue, seenProverAttestation.SeenProverKey.KeyValue) { + if bytes.Equal( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + seenProverAttestation.SeenProverKey.KeyValue, + ) { inParticipantList = true break } @@ -93,7 +96,7 @@ func (a *CeremonyApplication) applyDroppedProverAttestation( inParticipantList := false for _, p := range a.ActiveParticipants { if bytes.Equal( - p.KeyValue, + p.PublicKeySignatureEd448.PublicKey.KeyValue, droppedProverAttestation.DroppedProverKey.KeyValue, ) { inParticipantList = true @@ -189,7 +192,7 @@ func (a *CeremonyApplication) applyTranscriptCommit( inParticipantList := false for _, p := range a.ActiveParticipants { if bytes.Equal( - p.KeyValue, + p.PublicKeySignatureEd448.PublicKey.KeyValue, transcriptCommit.ProverSignature.PublicKey.KeyValue, ) { inParticipantList = true diff --git a/node/execution/ceremony/application/ceremony_application_open.go b/node/execution/ceremony/application/ceremony_application_open.go index ab13d50..6f5b02d 100644 --- a/node/execution/ceremony/application/ceremony_application_open.go +++ b/node/execution/ceremony/application/ceremony_application_open.go @@ -89,11 +89,11 @@ func (a *CeremonyApplication) finalizeParticipantSet() error { power = power >> 1 } - a.ActiveParticipants = []*protobufs.Ed448PublicKey{} + a.ActiveParticipants = []*protobufs.CeremonyLobbyJoin{} for i := 0; i < int(power); i++ { a.ActiveParticipants = append( a.ActiveParticipants, - a.LobbyJoins[i].PublicKeySignatureEd448.PublicKey, + a.LobbyJoins[i], ) } diff --git a/node/execution/ceremony/application/ceremony_application_test.go b/node/execution/ceremony/application/ceremony_application_test.go index be3f033..cfcec43 100644 --- a/node/execution/ceremony/application/ceremony_application_test.go +++ b/node/execution/ceremony/application/ceremony_application_test.go @@ -122,7 +122,10 @@ func TestCeremonyTransitions(t *testing.T) { }) require.NoError(t, err) require.Equal(t, a.LobbyState, CEREMONY_APPLICATION_STATE_IN_PROGRESS) - require.True(t, bytes.Equal(a.ActiveParticipants[0].KeyValue, proverPubKey)) + require.True(t, bytes.Equal( + a.ActiveParticipants[0].PublicKeySignatureEd448.PublicKey.KeyValue, + proverPubKey, + )) tau := curves.BLS48581G1().Scalar.Random(rand.Reader) tau2 := tau.Mul(tau) diff --git a/node/execution/ceremony/application/ceremony_application_validating.go b/node/execution/ceremony/application/ceremony_application_validating.go index 4b50fac..d1d36e1 100644 --- a/node/execution/ceremony/application/ceremony_application_validating.go +++ b/node/execution/ceremony/application/ceremony_application_validating.go @@ -2,9 +2,9 @@ package application import ( "bytes" + "crypto/rand" "github.com/pkg/errors" - "golang.org/x/sync/errgroup" "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves" "source.quilibrium.com/quilibrium/monorepo/node/protobufs" ) @@ -37,59 +37,47 @@ func (a *CeremonyApplication) applyTranscript( ) } - g1s := make([]*curves.PointBls48581G1, len(a.UpdatedTranscript.G1Powers)) - eg := errgroup.Group{} - eg.SetLimit(100) + g1s := make([]curves.Point, len(a.UpdatedTranscript.G1Powers)) for i := range a.UpdatedTranscript.G1Powers { i := i - eg.Go(func() error { - if !bytes.Equal( - a.UpdatedTranscript.G1Powers[i].KeyValue, - transcript.G1Powers[i].KeyValue, - ) { - return errors.Wrap(errors.New("invalid g1s"), "apply transcript") - } + if !bytes.Equal( + a.UpdatedTranscript.G1Powers[i].KeyValue, + transcript.G1Powers[i].KeyValue, + ) { + return errors.Wrap(errors.New("invalid g1s"), "apply transcript") + } - g1 := &curves.PointBls48581G1{} - x, err := g1.FromAffineCompressed(a.UpdatedTranscript.G1Powers[i].KeyValue) - if err != nil { - return errors.Wrap(err, "apply transcript") - } - g1, _ = x.(*curves.PointBls48581G1) + g1 := &curves.PointBls48581G1{} + x, err := g1.FromAffineCompressed( + a.UpdatedTranscript.G1Powers[i].KeyValue, + ) + if err != nil { + return errors.Wrap(err, "apply transcript") + } - g1s[i] = g1 - - return nil - }) + g1s[i] = x } - g2s := make([]*curves.PointBls48581G2, len(a.UpdatedTranscript.G2Powers)) + g2s := make([]curves.Point, len(a.UpdatedTranscript.G2Powers)) for i := range a.UpdatedTranscript.G2Powers { i := i - eg.Go(func() error { - if !bytes.Equal( - a.UpdatedTranscript.G2Powers[i].KeyValue, - transcript.G2Powers[i].KeyValue, - ) { - return errors.Wrap(errors.New("invalid g2s"), "apply transcript") - } + if !bytes.Equal( + a.UpdatedTranscript.G2Powers[i].KeyValue, + transcript.G2Powers[i].KeyValue, + ) { + return errors.Wrap(errors.New("invalid g2s"), "apply transcript") + } - g2 := &curves.PointBls48581G2{} - x, err := g2.FromAffineCompressed(a.UpdatedTranscript.G2Powers[i].KeyValue) - if err != nil { - return errors.Wrap(err, "apply transcript") - } - g2, _ = x.(*curves.PointBls48581G2) + g2 := &curves.PointBls48581G2{} + x, err := g2.FromAffineCompressed( + a.UpdatedTranscript.G2Powers[i].KeyValue, + ) + if err != nil { + return errors.Wrap(err, "apply transcript") + } - g2s[i] = g2 - - return nil - }) - } - - if err := eg.Wait(); err != nil { - return err + g2s[i] = x } g1Witnesses := []*curves.PointBls48581G1{} @@ -168,52 +156,70 @@ func (a *CeremonyApplication) applyTranscript( } } - mp := []curves.PairingPoint{} mpg2 := curves.BLS48581G2().Point.Generator().(curves.PairingPoint) mpg2n := g2s[1].Neg().(curves.PairingPoint) - for i := 0; i < len(g1s)-1; i++ { - mp = append(mp, g1s[i]) - mp = append(mp, mpg2n) - mp = append(mp, g1s[i+1]) - mp = append(mp, mpg2) - } - - mp2 := []curves.PairingPoint{} mpg1 := curves.BLS48581G1().Point.Generator().(curves.PairingPoint) mpg1n := g1s[1].Neg().(curves.PairingPoint) - for i := 0; i < len(g2s)-1; i++ { - mp2 = append(mp2, mpg1n) - mp2 = append(mp2, g2s[i]) - mp2 = append(mp2, mpg1) - mp2 = append(mp2, g2s[i+1]) + + randoms := []curves.Scalar{} + sum := curves.BLS48581G1().Scalar.Zero() + + for i := 0; i < len(g1s)-1; i++ { + randoms = append(randoms, curves.BLS48581G1().Scalar.Random(rand.Reader)) + sum = sum.Add(randoms[i]) } - l := g1s[0].MultiPairing(mp...) - if !l.IsOne() { + g1CheckR := g1s[0].SumOfProducts(g1s[1:], randoms) + g1CheckL := g1s[0].SumOfProducts(g1s[:len(g1s)-1], randoms) + + if !mpg2.MultiPairing( + g1CheckL.(curves.PairingPoint), + mpg2n.Mul(sum).(curves.PairingPoint), + g1CheckR.(curves.PairingPoint), + mpg2.Mul(sum).(curves.PairingPoint), + ).IsOne() { return errors.Wrap( errors.New("pairing check failed for g1s"), "apply transcript", ) } - l = g1s[0].MultiPairing(mp2...) - if !l.IsOne() { + var g2CheckL, g2CheckR curves.Point + g2Sum := curves.BLS48581G1().Scalar.Zero() + for i := 0; i < len(g2s)-1; i++ { + g2Sum = g2Sum.Add(randoms[i]) + if g2CheckL == nil { + g2CheckL = g2s[0].Mul(randoms[0]) + g2CheckR = g2s[1].Mul(randoms[0]) + } else { + g2CheckL = g2CheckL.Add(g2s[i].Mul(randoms[i])) + g2CheckR = g2CheckR.Add(g2s[i+1].Mul(randoms[i])) + } + } + + if !mpg2.MultiPairing( + mpg1n.Mul(g2Sum).(curves.PairingPoint), + g2CheckL.(curves.PairingPoint), + mpg1.Mul(g2Sum).(curves.PairingPoint), + g2CheckR.(curves.PairingPoint), + ).IsOne() { return errors.Wrap( errors.New("pairing check failed for g2s"), "apply transcript", ) } - mp3 := []curves.PairingPoint{} + mp3 := make([]curves.PairingPoint, (len(g2Powers)-1)*4) for i := 0; i < len(g2Powers)-1; i++ { - mp3 = append(mp3, g1Witnesses[i+1].Neg().(curves.PairingPoint)) - mp3 = append(mp3, g2Powers[i]) - mp3 = append(mp3, mpg1) - mp3 = append(mp3, g2Powers[i+1]) + i := i + mp3[i*4+0] = g1Witnesses[i+1].Neg().(curves.PairingPoint) + mp3[i*4+1] = g2Powers[i] + mp3[i*4+2] = mpg1 + mp3[i*4+3] = g2Powers[i+1] } - l = g1s[0].MultiPairing(mp3...) + l := mp3[0].MultiPairing(mp3...) if !l.IsOne() { return errors.Wrap( errors.New("pairing check failed for witnesses"), diff --git a/node/execution/ceremony/application/ceremony_application_validating_test.go b/node/execution/ceremony/application/ceremony_application_validating_test.go index 7f6c196..3840e32 100644 --- a/node/execution/ceremony/application/ceremony_application_validating_test.go +++ b/node/execution/ceremony/application/ceremony_application_validating_test.go @@ -3,7 +3,9 @@ package application import ( "crypto" "crypto/rand" + "fmt" "testing" + "time" "github.com/cloudflare/circl/sign/ed448" "github.com/stretchr/testify/require" @@ -12,6 +14,166 @@ import ( "source.quilibrium.com/quilibrium/monorepo/node/protobufs" ) +// This does a full test of the 65536 powers, run this if you want to wait a +// long time +func TestApplyTranscript_Slow(t *testing.T) { + old := curves.BLS48581G1().Scalar.Random(rand.Reader) + olds := []*curves.ScalarBls48581{ + curves.BLS48581G1().Scalar.One().(*curves.ScalarBls48581), + } + tau := curves.BLS48581G1().Scalar.Random(rand.Reader) + taus := []*curves.ScalarBls48581{ + curves.BLS48581G1().Scalar.One().(*curves.ScalarBls48581), + } + fmt.Println(time.Now().Unix()) + fmt.Println("generate taus") + for i := 0; i < 65536; i++ { + olds = append(olds, olds[i].Mul(old).(*curves.ScalarBls48581)) + taus = append(taus, taus[i].Mul(tau).(*curves.ScalarBls48581)) + } + tauPubG2 := curves.BLS48581G2().Point.Generator().Mul(tau) + + fmt.Println(time.Now().Unix()) + fmt.Println("taus generated") + proverPubKey, proverKey, err := ed448.GenerateKey(rand.Reader) + require.NoError(t, err) + proverSig, err := proverKey.Sign( + rand.Reader, + tauPubG2.ToAffineCompressed(), + crypto.Hash(0), + ) + require.NoError(t, err) + + fmt.Println(time.Now().Unix()) + fmt.Println("prover signature generated") + blsSignature := make([]byte, int(bls48581.MODBYTES)+1) + key := tau.Bytes() + + for i, j := 0, len(key)-1; i < j; i, j = i+1, j-1 { + key[i], key[j] = key[j], key[i] + } + + if bls48581.Core_Sign(blsSignature, proverKey, key) != bls48581.BLS_OK { + require.Fail(t, "could not sign") + } + + fmt.Println(time.Now().Unix()) + fmt.Println("bls signature generated") + + blsSig := blsSignature[:] + oldTranscript := &protobufs.CeremonyTranscript{ + G1Powers: []*protobufs.BLS48581G1PublicKey{}, + G2Powers: []*protobufs.BLS48581G2PublicKey{}, + RunningG1_256Witnesses: []*protobufs.BLS48581G1PublicKey{ + { + KeyValue: curves.BLS48581G1().Point.Generator().ToAffineCompressed(), + }, + }, + RunningG2_256Powers: []*protobufs.BLS48581G2PublicKey{ + { + KeyValue: curves.BLS48581G2().Point.Generator().Mul( + olds[256], + ).ToAffineCompressed(), + }, + }, + } + updatedTranscript := &protobufs.CeremonyTranscript{ + G1Powers: []*protobufs.BLS48581G1PublicKey{}, + G2Powers: []*protobufs.BLS48581G2PublicKey{}, + RunningG1_256Witnesses: []*protobufs.BLS48581G1PublicKey{ + { + KeyValue: curves.BLS48581G1().Point.Generator().ToAffineCompressed(), + }, + { + KeyValue: curves.BLS48581G1().Point.Generator().Mul( + taus[256], + ).ToAffineCompressed(), + }, + }, + RunningG2_256Powers: []*protobufs.BLS48581G2PublicKey{ + { + KeyValue: curves.BLS48581G2().Point.Generator().Mul( + olds[256], + ).ToAffineCompressed(), + }, + { + KeyValue: curves.BLS48581G2().Point.Generator().Mul( + olds[256], + ).Mul(taus[256]).ToAffineCompressed(), + }, + }, + } + + for i, o := range olds { + oldTranscript.G1Powers = append( + oldTranscript.G1Powers, + &protobufs.BLS48581G1PublicKey{ + KeyValue: curves.BLS48581G1().Point.Generator().Mul( + o, + ).ToAffineCompressed(), + }, + ) + + updatedTranscript.G1Powers = append( + updatedTranscript.G1Powers, + &protobufs.BLS48581G1PublicKey{ + KeyValue: curves.BLS48581G1().Point.Generator().Mul( + o, + ).Mul(taus[i]).ToAffineCompressed(), + }, + ) + + if i < 257 { + oldTranscript.G2Powers = append( + oldTranscript.G2Powers, + &protobufs.BLS48581G2PublicKey{ + KeyValue: curves.BLS48581G2().Point.Generator().Mul( + o, + ).ToAffineCompressed(), + }, + ) + + updatedTranscript.G2Powers = append( + updatedTranscript.G2Powers, + &protobufs.BLS48581G2PublicKey{ + KeyValue: curves.BLS48581G2().Point.Generator().Mul( + o, + ).Mul(taus[i]).ToAffineCompressed(), + }, + ) + } + } + + fmt.Println(time.Now().Unix()) + fmt.Println("transcripts generated") + a := &CeremonyApplication{ + StateCount: 0, + RoundCount: 0, + LobbyState: CEREMONY_APPLICATION_STATE_VALIDATING, + FinalCommits: []*protobufs.CeremonyTranscriptCommit{ + { + ProverSignature: &protobufs.Ed448Signature{ + Signature: proverSig, + PublicKey: &protobufs.Ed448PublicKey{ + KeyValue: proverPubKey, + }, + }, + ContributionSignature: &protobufs.BLS48581Signature{ + Signature: blsSig, + PublicKey: &protobufs.BLS48581G2PublicKey{ + KeyValue: tauPubG2.ToAffineCompressed(), + }, + }, + }, + }, + LatestTranscript: oldTranscript, + UpdatedTranscript: updatedTranscript, + } + + err = a.applyTranscript(updatedTranscript) + require.NoError(t, err) +} + func TestApplyTranscript(t *testing.T) { old := curves.BLS48581G1().Scalar.Random(rand.Reader) old2 := old.Mul(old) @@ -322,5 +484,5 @@ func TestApplyRewritingTranscriptFails(t *testing.T) { } err = a.applyTranscript(updatedTranscript) - require.NoError(t, err) + require.Error(t, err) } diff --git a/node/execution/ceremony/ceremony_execution_engine.go b/node/execution/ceremony/ceremony_execution_engine.go index d122a09..8855ac8 100644 --- a/node/execution/ceremony/ceremony_execution_engine.go +++ b/node/execution/ceremony/ceremony_execution_engine.go @@ -37,6 +37,7 @@ type CeremonyExecutionEngine struct { keyManager keys.KeyManager engineConfig *config.EngineConfig pubSub p2p.PubSub + peerIdHash []byte provingKey crypto.Signer proverPublicKey []byte provingKeyAddress []byte @@ -48,11 +49,11 @@ type CeremonyExecutionEngine struct { alreadyPublishedTranscript bool seenMessageMap map[string]bool seenMessageMx sync.Mutex + intrinsicFilter []byte } func NewCeremonyExecutionEngine( logger *zap.Logger, - clock *ceremony.CeremonyDataClockConsensusEngine, engineConfig *config.EngineConfig, keyManager keys.KeyManager, pubSub p2p.PubSub, @@ -63,6 +64,27 @@ func NewCeremonyExecutionEngine( panic(errors.New("logger is nil")) } + seed, err := hex.DecodeString(engineConfig.GenesisSeed) + if err != nil { + panic(err) + } + + intrinsicFilter := append( + p2p.GetBloomFilter(application.CEREMONY_ADDRESS, 256, 3), + p2p.GetBloomFilterIndices(application.CEREMONY_ADDRESS, 65536, 24)..., + ) + + clock := ceremony.NewCeremonyDataClockConsensusEngine( + engineConfig, + logger, + keyManager, + clockStore, + keyStore, + pubSub, + intrinsicFilter, + seed, + ) + e := &CeremonyExecutionEngine{ logger: logger, clock: clock, @@ -76,8 +98,18 @@ func NewCeremonyExecutionEngine( alreadyPublishedShare: false, seenMessageMx: sync.Mutex{}, seenMessageMap: map[string]bool{}, + intrinsicFilter: intrinsicFilter, } + peerId := e.pubSub.GetPeerID() + addr, err := poseidon.HashBytes(peerId) + if err != nil { + panic(err) + } + + addrBytes := addr.Bytes() + addrBytes = append(make([]byte, 32-len(addrBytes)), addrBytes...) + e.peerIdHash = addrBytes provingKey, _, publicKeyBytes, provingKeyAddress := e.clock.GetProvingKey( engineConfig, ) @@ -117,15 +149,7 @@ func (e *CeremonyExecutionEngine) Start() <-chan error { )) go func() { - seed, err := hex.DecodeString(e.engineConfig.GenesisSeed) - if err != nil { - panic(err) - } - - err = <-e.clock.Start( - application.CEREMONY_ADDRESS, - seed, - ) + err := <-e.clock.Start() if err != nil { panic(err) } @@ -175,7 +199,7 @@ func (e *CeremonyExecutionEngine) ProcessMessage( return nil, errors.Wrap(err, "process message") } - if frame.FrameNumber < e.clock.GetFrame() { + if frame.FrameNumber < e.clock.GetFrame().FrameNumber { return nil, nil } @@ -270,7 +294,7 @@ func (e *CeremonyExecutionEngine) RunWorker() { frameChan := e.clock.GetFrameChannel() for { frameFromBuffer := <-frameChan - frame := e.clock.GetActiveFrame() + frame := e.clock.GetFrame() e.activeClockFrame = frame e.logger.Info( "evaluating next frame", @@ -289,9 +313,10 @@ func (e *CeremonyExecutionEngine) RunWorker() { } _, _, reward := app.RewardTrie.Get(e.provingKeyAddress) + _, _, retro := app.RewardTrie.Get(e.peerIdHash) e.logger.Info( "current application state", - zap.Uint64("my_balance", reward), + zap.Uint64("my_balance", reward+retro), zap.String("lobby_state", app.LobbyState.String()), ) @@ -313,7 +338,10 @@ func (e *CeremonyExecutionEngine) RunWorker() { e.logger.Info( "lobby open for joins", zap.Int("joined_participants", len(app.LobbyJoins)), - zap.Int("preferred_participants", len(app.NextRoundPreferredParticipants)), + zap.Int( + "preferred_participants", + len(app.NextRoundPreferredParticipants), + ), zap.Bool("in_lobby", alreadyJoined), zap.Uint64("state_count", app.StateCount), ) @@ -337,7 +365,10 @@ func (e *CeremonyExecutionEngine) RunWorker() { case application.CEREMONY_APPLICATION_STATE_IN_PROGRESS: inRound := false for _, p := range app.ActiveParticipants { - if bytes.Equal(p.KeyValue, e.proverPublicKey) { + if bytes.Equal( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + e.proverPublicKey, + ) { inRound = true break } @@ -353,7 +384,10 @@ func (e *CeremonyExecutionEngine) RunWorker() { e.logger.Info( "round in progress", zap.Any("participants", app.ActiveParticipants), - zap.Any("current_seen_attestations", len(app.LatestSeenProverAttestations)), + zap.Any( + "current_seen_attestations", + len(app.LatestSeenProverAttestations), + ), zap.Any( "current_dropped_attestations", len(app.DroppedParticipantAttestations), @@ -371,7 +405,10 @@ func (e *CeremonyExecutionEngine) RunWorker() { if len(e.peerChannels) == 0 && app.RoundCount == 1 && len(app.ActiveParticipants) > 1 { for i, p := range app.ActiveParticipants { - if bytes.Equal(p.KeyValue, e.proverPublicKey) { + if bytes.Equal( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + e.proverPublicKey, + ) { shouldConnect = true position = i break @@ -418,7 +455,10 @@ func (e *CeremonyExecutionEngine) RunWorker() { } } } else if len(app.ActiveParticipants) == 1 && - bytes.Equal(app.ActiveParticipants[0].KeyValue, e.proverPublicKey) { + bytes.Equal( + app.ActiveParticipants[0].PublicKeySignatureEd448.PublicKey.KeyValue, + e.proverPublicKey, + ) { if err = e.commitRound(e.activeSecrets); err != nil { e.logger.Error("error while participating in round", zap.Error(err)) } @@ -427,7 +467,10 @@ func (e *CeremonyExecutionEngine) RunWorker() { e.logger.Info( "round contribution finalizing", zap.Any("participants", len(app.ActiveParticipants)), - zap.Any("current_seen_attestations", len(app.LatestSeenProverAttestations)), + zap.Any( + "current_seen_attestations", + len(app.LatestSeenProverAttestations), + ), zap.Any( "current_dropped_attestations", len(app.DroppedParticipantAttestations), @@ -450,7 +493,10 @@ func (e *CeremonyExecutionEngine) RunWorker() { shouldPublish := false for _, p := range app.ActiveParticipants { - if bytes.Equal(p.KeyValue, e.proverPublicKey) { + if bytes.Equal( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + e.proverPublicKey, + ) { shouldPublish = true break } @@ -587,7 +633,7 @@ func (e *CeremonyExecutionEngine) announceJoin( return errors.Wrap( e.publishMessage( - application.CEREMONY_ADDRESS, + e.intrinsicFilter, join, ), "announce join", @@ -607,34 +653,20 @@ func (e *CeremonyExecutionEngine) connectToActivePeers( return errors.Wrap(err, "connect to active peers") } - for i, p := range app.ActiveParticipants { - if !bytes.Equal(p.KeyValue, e.proverPublicKey) { - ic, err := e.keyStore.GetLatestKeyBundle(p.KeyValue) - if err != nil { - return errors.Wrap(err, "connect to active peers") - } - - var kba *protobufs.KeyBundleAnnouncement - switch ic.TypeUrl { - case protobufs.KeyBundleAnnouncementType: - kba = &protobufs.KeyBundleAnnouncement{} - if err := proto.Unmarshal( - ic.Data, - kba, - ); err != nil { - return errors.Wrap(err, "connect to active peers") - } - } - + for i, p := range app.LobbyJoins { + if !bytes.Equal( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + e.proverPublicKey, + ) { receiverIdk, err := curves.ED448().Point.FromAffineCompressed( - kba.IdentityKey.GetPublicKeySignatureEd448().PublicKey.KeyValue, + p.IdentityKey.KeyValue, ) if err != nil { return errors.Wrap(err, "connect to active peers") } receiverSpk, err := curves.ED448().Point.FromAffineCompressed( - kba.SignedPreKey.GetPublicKeySignatureEd448().PublicKey.KeyValue, + p.SignedPreKey.KeyValue, ) if err != nil { return errors.Wrap(err, "connect to active peers") @@ -642,19 +674,24 @@ func (e *CeremonyExecutionEngine) connectToActivePeers( client, err := e.clock.GetPublicChannelForProvingKey( i > position, - p.KeyValue, + p.PublicKeySignatureEd448.PublicKey.KeyValue, ) if err != nil { e.logger.Error( "peer does not support direct public channels", - zap.Binary("proving_key", p.KeyValue), + zap.Binary( + "proving_key", + p.PublicKeySignatureEd448.PublicKey.KeyValue, + ), zap.Error(err), ) } - e.peerChannels[string(p.KeyValue)], err = p2p.NewPublicP2PChannel( + e.peerChannels[string( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + )], err = p2p.NewPublicP2PChannel( client, e.proverPublicKey, - p.KeyValue, + p.PublicKeySignatureEd448.PublicKey.KeyValue, i > position, idk, spk, @@ -690,8 +727,13 @@ func (e *CeremonyExecutionEngine) participateRound( idks := []curves.Point{} initiator := false for _, p := range app.ActiveParticipants { - if !bytes.Equal(p.KeyValue, e.proverPublicKey) { - ic, err := e.keyStore.GetLatestKeyBundle(p.KeyValue) + if !bytes.Equal( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + e.proverPublicKey, + ) { + ic, err := e.keyStore.GetLatestKeyBundle( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + ) if err != nil { return errors.Wrap(err, "participate round") } @@ -722,22 +764,29 @@ func (e *CeremonyExecutionEngine) participateRound( return errors.Wrap(err, "participate round") } - if _, ok := e.peerChannels[string(p.KeyValue)]; !ok { + if _, ok := e.peerChannels[string( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + )]; !ok { client, err := e.clock.GetPublicChannelForProvingKey( initiator, - p.KeyValue, + p.PublicKeySignatureEd448.PublicKey.KeyValue, ) if err != nil { e.logger.Error( "peer does not support direct public channels", - zap.Binary("proving_key", p.KeyValue), + zap.Binary( + "proving_key", + p.PublicKeySignatureEd448.PublicKey.KeyValue, + ), zap.Error(err), ) } - e.peerChannels[string(p.KeyValue)], err = p2p.NewPublicP2PChannel( + e.peerChannels[string( + p.PublicKeySignatureEd448.PublicKey.KeyValue, + )], err = p2p.NewPublicP2PChannel( client, e.proverPublicKey, - p.KeyValue, + p.PublicKeySignatureEd448.PublicKey.KeyValue, initiator, idk, spk, @@ -761,7 +810,10 @@ func (e *CeremonyExecutionEngine) participateRound( pubKeys := [][]byte{} for _, p := range app.ActiveParticipants { - pubKeys = append(pubKeys, p.KeyValue) + pubKeys = append( + pubKeys, + p.PublicKeySignatureEd448.PublicKey.KeyValue, + ) } newSecrets, err := application.ProcessRound( @@ -834,7 +886,7 @@ func (e *CeremonyExecutionEngine) commitRound(secrets []curves.Scalar) error { } if err := e.publishMessage( - application.CEREMONY_ADDRESS, + e.intrinsicFilter, advance, ); err != nil { return errors.Wrap(err, "commit round") @@ -849,7 +901,7 @@ func (e *CeremonyExecutionEngine) commitRound(secrets []curves.Scalar) error { func (e *CeremonyExecutionEngine) publishDroppedParticipant( participant []byte, ) { - frameNumber := e.clock.GetFrame() + frameNumber := e.clock.GetFrame().FrameNumber b := binary.BigEndian.AppendUint64([]byte("dropped"), frameNumber) b = append(b, participant...) @@ -876,7 +928,7 @@ func (e *CeremonyExecutionEngine) publishDroppedParticipant( } err = e.publishMessage( - application.CEREMONY_ADDRESS, + e.intrinsicFilter, dropped, ) if err != nil { @@ -893,7 +945,7 @@ func (e *CeremonyExecutionEngine) publishDroppedParticipant( func (e *CeremonyExecutionEngine) publishLastSeenParticipant( participant []byte, ) { - frameNumber := e.clock.GetFrame() + frameNumber := e.clock.GetFrame().FrameNumber b := binary.BigEndian.AppendUint64([]byte("lastseen"), frameNumber) b = append(b, participant...) @@ -919,7 +971,7 @@ func (e *CeremonyExecutionEngine) publishLastSeenParticipant( }, } err = e.publishMessage( - application.CEREMONY_ADDRESS, + e.intrinsicFilter, seen, ) if err != nil { @@ -1019,7 +1071,7 @@ func (e *CeremonyExecutionEngine) publishTranscriptShare( err = errors.Wrap( e.publishMessage( - application.CEREMONY_ADDRESS, + e.intrinsicFilter, transcriptShare, ), "publish transcript share", @@ -1035,7 +1087,7 @@ func (e *CeremonyExecutionEngine) publishTranscriptShare( func (e *CeremonyExecutionEngine) VerifyExecution( frame *protobufs.ClockFrame, ) error { - if e.clock.GetFrame() != frame.FrameNumber-1 { + if e.clock.GetFrame().FrameNumber != frame.FrameNumber-1 { return nil } @@ -1102,7 +1154,7 @@ func (e *CeremonyExecutionEngine) publishTranscript( e.alreadyPublishedTranscript = true err := errors.Wrap( e.publishMessage( - application.CEREMONY_ADDRESS, + e.intrinsicFilter, app.UpdatedTranscript, ), "publish transcript share", diff --git a/node/go.mod b/node/go.mod index ffaeba4..3cd7cfc 100644 --- a/node/go.mod +++ b/node/go.mod @@ -11,9 +11,11 @@ replace github.com/libp2p/go-libp2p-gostream => ../go-libp2p-gostream replace source.quilibrium.com/quilibrium/monorepo/go-libp2p-blossomsub => ../go-libp2p-blossomsub +replace github.com/cockroachdb/pebble => ../pebble + require ( filippo.io/edwards25519 v1.0.0-rc.1 - github.com/cockroachdb/pebble v0.0.0-20231025190044-422dce910055 + github.com/cockroachdb/pebble v0.0.0-20231210175920-b4d301aeb46a github.com/libp2p/go-libp2p v0.31.0 github.com/libp2p/go-libp2p-gostream v0.6.0 github.com/libp2p/go-libp2p-kad-dht v0.23.0 @@ -57,11 +59,9 @@ require ( github.com/quic-go/qtls-go1-19 v0.3.3 // indirect github.com/quic-go/qtls-go1-20 v0.2.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/term v0.14.0 // indirect - google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect + golang.org/x/term v0.14.0 google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -126,7 +126,7 @@ require ( github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mr-tron/base58 v1.2.0 github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect diff --git a/node/go.sum b/node/go.sum index f1e8d1a..72df8bd 100644 --- a/node/go.sum +++ b/node/go.sum @@ -9,22 +9,13 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= -github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= -github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= -github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= @@ -61,30 +52,16 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= -github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= -github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y= -github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= 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-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= 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 v0.0.0-20230527012508-ac69476c46ff h1:/F1VgP7wxZCRj8PzresPo2NbAdgPwmU7pi+CgZ8sBZw= -github.com/cockroachdb/pebble v0.0.0-20230527012508-ac69476c46ff/go.mod h1:TkdVsGYRqtULUppt2RbC+YaKtTHnHoWa2apfFrSKABw= -github.com/cockroachdb/pebble v0.0.0-20231025190044-422dce910055 h1:EigfnVX/iY/WTi3F+f4ezhAxJO+BePglQkEAKycNhqo= -github.com/cockroachdb/pebble v0.0.0-20231025190044-422dce910055/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= -github.com/cockroachdb/redact v1.0.8 h1:8QG/764wK+vmEYoOlfobpe12EQcS81ukx/a4hdVMxNw= -github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895 h1:XANOgPYtvELQ/h4IrmPAohXqe2pWA8Bwhejr3VQoZsA= 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/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM= -github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= 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/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/consensys/bavard v0.1.8-0.20210915155054-088da2f7f54a/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.5.3 h1:4xLFGZR3NWEH2zy+YzvzHicpToQR8FXFbfLNvpGB+rE= github.com/consensys/gnark-crypto v0.5.3/go.mod h1:hOdPlWQV1gDLp7faZVeg8Y0iEPFaOUnCc4XeCCk96p0= @@ -93,14 +70,10 @@ github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaD github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -114,14 +87,10 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5il github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= @@ -129,10 +98,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= -github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= @@ -140,43 +105,30 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -187,21 +139,18 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -231,7 +180,6 @@ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -245,20 +193,15 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/iden3/go-iden3-crypto v0.0.15 h1:4MJYlrot1l31Fzlo2sF56u7EVFeHHJkxGXXZCtESgK4= github.com/iden3/go-iden3-crypto v0.0.15/go.mod h1:dLpM4vEPJ3nDHzhWFXDjzkn1qHoBeOT/3UEhXsEsP3E= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/ipfs/boxo v0.8.0 h1:UdjAJmHzQHo/j3g3b1bAcAXCj/GM6iTwvSlBDvPBNBs= github.com/ipfs/boxo v0.8.0/go.mod h1:RIsi4CnTyQ7AUsNn5gXljJYZlQrHBMnJp94p73liFiA= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= @@ -276,10 +219,6 @@ github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g= github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M= -github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= -github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= -github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= -github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= @@ -295,23 +234,12 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= -github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= -github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= -github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= -github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= -github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 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/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= @@ -325,8 +253,6 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= -github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= @@ -357,14 +283,9 @@ github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -373,14 +294,10 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= -github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= @@ -396,11 +313,8 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -439,22 +353,14 @@ github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXS github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= -github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= @@ -464,9 +370,7 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -494,8 +398,6 @@ github.com/quic-go/qtls-go1-19 v0.3.3 h1:wznEHvJwd+2X3PqftRha0SUKmGsnb6dfArMhy9P github.com/quic-go/qtls-go1-19 v0.3.3/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= github.com/quic-go/qtls-go1-20 v0.2.3 h1:m575dovXn1y2ATOb1XrRFcrv0F+EQmlowTkoraNkDPI= github.com/quic-go/qtls-go1-20 v0.2.3/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI= -github.com/quic-go/qtls-go1-20 v0.3.2/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/quic-go/quic-go v0.36.3 h1:f+yOqeGhMoRX7/M3wmEw/djhzKWr15FtQysox85/834= github.com/quic-go/quic-go v0.36.3/go.mod h1:qxQumdeKw5GmWs1OsTZZnOxzSI+RJWuhf1O8FN35L2o= github.com/quic-go/webtransport-go v0.5.3 h1:5XMlzemqB4qmOlgIus5zB45AcZ2kCgCy2EptUrfOPWU= @@ -511,10 +413,7 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= @@ -539,22 +438,14 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -569,29 +460,14 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -624,20 +500,16 @@ go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1 golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -662,25 +534,18 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -702,20 +567,11 @@ golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -727,23 +583,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -752,11 +599,9 @@ golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -784,7 +629,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -793,14 +637,10 @@ google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= -google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -808,12 +648,9 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -829,21 +666,15 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/node/keys/inmem.go b/node/keys/inmem.go new file mode 100644 index 0000000..a59a201 --- /dev/null +++ b/node/keys/inmem.go @@ -0,0 +1,197 @@ +package keys + +import ( + "crypto" + "crypto/rand" + + "github.com/cloudflare/circl/sign/ed448" + "github.com/pkg/errors" + "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves" +) + +type InMemoryKeyManager struct { + key ByteString + store map[string]Key +} + +func NewInMemoryKeyManager() *InMemoryKeyManager { + store := make(map[string]Key) + + return &InMemoryKeyManager{ + store: store, + } +} + +// CreateSigningKey implements KeyManager +func (f *InMemoryKeyManager) CreateSigningKey( + id string, + keyType KeyType, +) (crypto.Signer, error) { + switch keyType { + case KeyTypeEd448: + pubkey, privkey, err := ed448.GenerateKey(rand.Reader) + if err != nil { + return nil, errors.Wrap(err, "could not generate key") + } + + if err = f.save( + id, + Key{ + Id: id, + Type: keyType, + PublicKey: ByteString(pubkey), + PrivateKey: ByteString(privkey), + }, + ); err != nil { + return nil, errors.Wrap(err, "could not save") + } + + return privkey, nil + // case KeyTypePCAS: + // _, privkey, err := addressing.GenerateKey(rand.Reader) + // if err != nil { + // return nil, errors.Wrap(err, "could not generate key") + // } + + // if err = f.save(id, privkey); err != nil { + // return nil, errors.Wrap(err, "could not save") + // } + + // return privkey, nil + } + + return nil, UnsupportedKeyTypeErr +} + +// CreateAgreementKey implements KeyManager +func (f *InMemoryKeyManager) CreateAgreementKey( + id string, + keyType KeyType, +) (curves.Scalar, error) { + switch keyType { + case KeyTypeX448: + privkey := curves.ED448().Scalar.Random(rand.Reader) + pubkey := curves.ED448().NewGeneratorPoint().Mul(privkey) + + if err := f.save( + id, + Key{ + Id: id, + Type: KeyTypeX448, + PublicKey: pubkey.ToAffineCompressed(), + PrivateKey: privkey.Bytes(), + }, + ); err != nil { + return nil, errors.Wrap(err, "could not save") + } + + return privkey, nil + } + + return nil, UnsupportedKeyTypeErr +} + +// GetAgreementKey implements KeyManager +func (f *InMemoryKeyManager) GetAgreementKey(id string) (curves.Scalar, error) { + key, err := f.read(id) + if err != nil { + return nil, err + } + + switch key.Type { + case KeyTypeX448: + privkey, err := curves.ED448().NewScalar().SetBytes(key.PrivateKey) + return privkey, err + } + + return nil, UnsupportedKeyTypeErr +} + +// GetRawKey implements KeyManager +func (f *InMemoryKeyManager) GetRawKey(id string) (*Key, error) { + key, err := f.read(id) + return &key, err +} + +// GetSigningKey implements KeyManager +func (f *InMemoryKeyManager) GetSigningKey(id string) (crypto.Signer, error) { + key, err := f.read(id) + if err != nil { + return nil, err + } + + switch key.Type { + case KeyTypeEd448: + privkey := (ed448.PrivateKey)(key.PrivateKey) + return privkey, err + // case KeyTypePCAS: + // privkey := (addressing.PCAS)(key.PrivateKey) + // return privkey, err + } + + return nil, UnsupportedKeyTypeErr +} + +// PutRawKey implements KeyManager +func (f *InMemoryKeyManager) PutRawKey(key *Key) error { + return f.save(key.Id, *key) +} + +// DeleteKey implements KeyManager +func (f *InMemoryKeyManager) DeleteKey(id string) error { + delete(f.store, id) + + return nil +} + +// GetKey implements KeyManager +func (f *InMemoryKeyManager) GetKey(id string) (key *Key, err error) { + storeKey, err := f.read(id) + if err != nil { + return nil, err + } + + return &storeKey, nil +} + +// ListKeys implements KeyManager +func (f *InMemoryKeyManager) ListKeys() ([]*Key, error) { + keys := []*Key{} + + for k := range f.store { + storeKey, err := f.read(k) + if err != nil { + return nil, err + } + keys = append(keys, &storeKey) + } + + return keys, nil +} + +var _ KeyManager = (*InMemoryKeyManager)(nil) + +func (f *InMemoryKeyManager) save(id string, key Key) error { + f.store[id] = Key{ + Id: key.Id, + Type: key.Type, + PublicKey: key.PublicKey, + PrivateKey: key.PrivateKey, + } + + return nil +} + +func (f *InMemoryKeyManager) read(id string) (Key, error) { + k, ok := f.store[id] + if !ok { + return Key{}, KeyNotFoundErr + } + + return Key{ + Id: k.Id, + Type: k.Type, + PublicKey: k.PublicKey, + PrivateKey: k.PrivateKey, + }, nil +} diff --git a/node/main.go b/node/main.go index 7e7769e..81a3101 100644 --- a/node/main.go +++ b/node/main.go @@ -25,7 +25,7 @@ import ( var ( configDirectory = flag.String( "config", - "./.config/", + filepath.Join(".", ".config"), "the configuration directory", ) importPrivKey = flag.String( @@ -233,5 +233,5 @@ func printLogo() { func printVersion() { fmt.Println(" ") - fmt.Println(" Quilibrium Node - v1.1.8 – Dawn") + fmt.Println(" Quilibrium Node - v1.2.0 – Dawn") } diff --git a/node/p2p/bloom_utils.go b/node/p2p/bloom_utils.go index aa2963a..101e9c9 100644 --- a/node/p2p/bloom_utils.go +++ b/node/p2p/bloom_utils.go @@ -3,6 +3,7 @@ package p2p import ( "fmt" "math/big" + "sort" "golang.org/x/crypto/sha3" ) @@ -64,10 +65,10 @@ func generateBitSlices( return nil } -// getBloomFilterIndices returns a bloom filter index based on the data, however +// GetBloomFilter returns a bloom filter based on the data, however // it assumes bitLength is a multiple of 32. If the filter size is not // conformant, this will generate biased indices. -func getBloomFilterIndices(data []byte, bitLength int, k int) []byte { +func GetBloomFilter(data []byte, bitLength int, k int) []byte { size := big.NewInt(int64(bitLength)).BitLen() - 1 digest := sha3.Sum256(data) output := make([]byte, bitLength/8) @@ -75,7 +76,7 @@ func getBloomFilterIndices(data []byte, bitLength int, k int) []byte { digestBI := new(big.Int).SetBytes(digest[:]) for i := 0; i < k; i++ { position := uint(0) - for j := size*(i+1) - 1; j >= size*i; j-- { + for j := size * i; j < size*(i+1); j++ { position = position<<1 | (digestBI.Bit(j)) } if outputBI.Bit(int(position)) != 1 { @@ -96,3 +97,51 @@ func getBloomFilterIndices(data []byte, bitLength int, k int) []byte { outputBI.FillBytes(output) return output } + +// GetBloomFilterIndices returns the indices of a bloom filter, in increasing +// order, assuming bitLength is a multiple of 32 as in GetBloomFilter. +func GetBloomFilterIndices(data []byte, bitLength int, k int) []byte { + size := big.NewInt(int64(bitLength)).BitLen() - 1 + h := sha3.NewShake256() + _, err := h.Write(data) + if err != nil { + panic(err) + } + + digest := make([]byte, size*k/8) + _, err = h.Read(digest) + if err != nil { + panic(err) + } + + indices := []string{} + for i := 0; i < k; i++ { + position := make([]byte, size/8) + for j := (size / 8) * i; j < (size/8)*(i+1); j++ { + position[j%(size/8)] = digest[j] + } + found := false + for _, ext := range indices { + if ext == string(position) { + k++ + found = true + break + } + } + if !found { + p := sort.SearchStrings(indices, string(position)) + if len(indices) > p { + indices = append(indices[:p+1], indices[p:]...) + indices[p] = string(position) + } else { + indices = append(indices, string(position)) + } + } + } + + output := "" + for _, idx := range indices { + output += idx + } + return []byte(output) +} diff --git a/node/p2p/bloom_utils_test.go b/node/p2p/bloom_utils_test.go new file mode 100644 index 0000000..2fa7698 --- /dev/null +++ b/node/p2p/bloom_utils_test.go @@ -0,0 +1,91 @@ +package p2p_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "source.quilibrium.com/quilibrium/monorepo/node/p2p" +) + +func TestGetBloomFilter(t *testing.T) { + fourByteThreeKTest := p2p.GetBloomFilter( + []byte{0x00, 0x00, 0x00, 0x00}, + 256, + 3, + ) + assert.ElementsMatch(t, fourByteThreeKTest, []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }) + + sixtyByteThreeKTest := p2p.GetBloomFilter( + bytes.Repeat([]byte{0x00}, 60), + 256, + 3, + ) + assert.ElementsMatch(t, sixtyByteThreeKTest, []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }) + + fourByteSixteenKTest := p2p.GetBloomFilter( + []byte{0x00, 0x00, 0x00, 0x00}, + 65536, + 16, + ) + assert.ElementsMatch(t, fourByteSixteenKTest, []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }) + + sixtyByteSixteenKTest := p2p.GetBloomFilter( + bytes.Repeat([]byte{0x00}, 60), + 65536, + 16, + ) + assert.ElementsMatch(t, sixtyByteSixteenKTest, []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }) +} + +func TestGetBloomFilterIndices(t *testing.T) { + fourByteThreeKTest := p2p.GetBloomFilterIndices( + []byte{0x00, 0x00, 0x00, 0x00}, + 256, + 3, + ) + assert.ElementsMatch(t, fourByteThreeKTest, []byte{0x1e, 0xa2, 0xb4}) + + sixtyByteThreeKTest := p2p.GetBloomFilterIndices( + bytes.Repeat([]byte{0x00}, 60), + 256, + 3, + ) + assert.ElementsMatch(t, sixtyByteThreeKTest, []byte{0x0a, 0x72, 0x80}) + + fourByteSixteenKTest := p2p.GetBloomFilterIndices( + []byte{0x00, 0x00, 0x00, 0x00}, + 65536, + 16, + ) + assert.ElementsMatch(t, fourByteSixteenKTest, []byte{ + 0x10, 0x23, 0x1e, 0x79, 0x39, 0xbe, 0x50, 0xe9, 0x64, 0x68, 0x73, 0x4f, + 0x7e, 0xd5, 0x8b, 0x4d, 0x8d, 0x15, 0x95, 0xd6, 0xb1, 0x25, 0xb3, 0x1a, + 0xb4, 0xa2, 0xbd, 0x3c, 0xea, 0x31, 0xee, 0x7e, + }) + + sixtyByteSixteenKTest := p2p.GetBloomFilterIndices( + bytes.Repeat([]byte{0x00}, 60), + 65536, + 16, + ) + assert.ElementsMatch(t, sixtyByteSixteenKTest, []byte{ + 0x10, 0x34, 0x16, 0x18, 0x27, 0xe7, 0x4b, 0xfc, 0x72, 0x0a, 0x80, 0x38, + 0x81, 0x12, 0x93, 0xec, 0xa1, 0xf8, 0xa2, 0x37, 0xa9, 0x1a, 0xc1, 0x55, + 0xc4, 0x16, 0xd1, 0x7e, 0xd5, 0xcd, 0xf0, 0x6c, + }) +} diff --git a/node/p2p/blossomsub.go b/node/p2p/blossomsub.go index f5dc4c4..58e8526 100644 --- a/node/p2p/blossomsub.go +++ b/node/p2p/blossomsub.go @@ -17,6 +17,7 @@ import ( libp2pconfig "github.com/libp2p/go-libp2p/config" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/p2p/discovery/routing" @@ -47,8 +48,6 @@ type BlossomSub struct { var _ PubSub = (*BlossomSub)(nil) var ErrNoPeersAvailable = errors.New("no peers available") -// Crucial note, bitmask lengths should always be a power of two so as to reduce -// index bias with hash functions var BITMASK_ALL = []byte{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, @@ -180,7 +179,8 @@ func (b *BlossomSub) PublishToBitmask(bitmask []byte, data []byte) error { } func (b *BlossomSub) Publish(data []byte) error { - bitmask := getBloomFilterIndices(data, 256, 3) + bitmask := GetBloomFilter(data, 256, 3) + bitmask = append(bitmask, GetBloomFilterIndices(data, 65536, 24)...) return b.PublishToBitmask(bitmask, data) } @@ -509,7 +509,8 @@ func discoverPeers( for peer := range peerChan { peer := peer - if peer.ID == h.ID() { + if peer.ID == h.ID() || + h.Network().Connectedness(peer.ID) == network.Connected { continue } @@ -535,10 +536,7 @@ func discoverPeers( go func() { for { time.Sleep(30 * time.Second) - if len(h.Network().Peers()) == 0 { - logger.Info("reinitiating discovery") - discover() - } + discover() } }() diff --git a/node/poor_mans_cd.sh b/node/poor_mans_cd.sh new file mode 100755 index 0000000..386cbc4 --- /dev/null +++ b/node/poor_mans_cd.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +start_process() { + go run ./... & + process_pid=$! + child_process_pid=$(pgrep -P $process_pid) +} + +is_process_running() { + ps -p $process_pid > /dev/null 2>&1 + return $? +} + +kill_process() { + kill $process_pid + kill $child_process_pid +} + +start_process + +while true; do + if ! is_process_running; then + echo "Process crashed or stopped. Restarting..." + start_process + fi + + git fetch + + local_head=$(git rev-parse HEAD) + remote_head=$(git rev-parse @{u}) + + if [ "$local_head" != "$remote_head" ]; then + kill_process + + git pull + + start_process + fi + + sleep 60 +done diff --git a/node/protobufs/ceremony.pb.go b/node/protobufs/ceremony.pb.go index 4e8398b..0c2af75 100644 --- a/node/protobufs/ceremony.pb.go +++ b/node/protobufs/ceremony.pb.go @@ -748,7 +748,7 @@ type CeremonyInProgressState struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ActiveParticipants []*Ed448PublicKey `protobuf:"bytes,1,rep,name=active_participants,json=activeParticipants,proto3" json:"active_participants,omitempty"` + ActiveParticipants []*CeremonyLobbyJoin `protobuf:"bytes,1,rep,name=active_participants,json=activeParticipants,proto3" json:"active_participants,omitempty"` LatestSeenProverAttestations []*CeremonySeenProverAttestation `protobuf:"bytes,2,rep,name=latest_seen_prover_attestations,json=latestSeenProverAttestations,proto3" json:"latest_seen_prover_attestations,omitempty"` DroppedParticipantAttestations []*CeremonyDroppedProverAttestation `protobuf:"bytes,3,rep,name=dropped_participant_attestations,json=droppedParticipantAttestations,proto3" json:"dropped_participant_attestations,omitempty"` TranscriptRoundAdvanceCommits []*CeremonyAdvanceRound `protobuf:"bytes,4,rep,name=transcript_round_advance_commits,json=transcriptRoundAdvanceCommits,proto3" json:"transcript_round_advance_commits,omitempty"` @@ -787,7 +787,7 @@ func (*CeremonyInProgressState) Descriptor() ([]byte, []int) { return file_ceremony_proto_rawDescGZIP(), []int{10} } -func (x *CeremonyInProgressState) GetActiveParticipants() []*Ed448PublicKey { +func (x *CeremonyInProgressState) GetActiveParticipants() []*CeremonyLobbyJoin { if x != nil { return x.ActiveParticipants } @@ -827,7 +827,7 @@ type CeremonyFinalizingState struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ActiveParticipants []*Ed448PublicKey `protobuf:"bytes,1,rep,name=active_participants,json=activeParticipants,proto3" json:"active_participants,omitempty"` + ActiveParticipants []*CeremonyLobbyJoin `protobuf:"bytes,1,rep,name=active_participants,json=activeParticipants,proto3" json:"active_participants,omitempty"` LatestSeenProverAttestations []*CeremonySeenProverAttestation `protobuf:"bytes,2,rep,name=latest_seen_prover_attestations,json=latestSeenProverAttestations,proto3" json:"latest_seen_prover_attestations,omitempty"` DroppedParticipantAttestations []*CeremonyDroppedProverAttestation `protobuf:"bytes,3,rep,name=dropped_participant_attestations,json=droppedParticipantAttestations,proto3" json:"dropped_participant_attestations,omitempty"` Commits []*CeremonyTranscriptCommit `protobuf:"bytes,4,rep,name=commits,proto3" json:"commits,omitempty"` @@ -867,7 +867,7 @@ func (*CeremonyFinalizingState) Descriptor() ([]byte, []int) { return file_ceremony_proto_rawDescGZIP(), []int{11} } -func (x *CeremonyFinalizingState) GetActiveParticipants() []*Ed448PublicKey { +func (x *CeremonyFinalizingState) GetActiveParticipants() []*CeremonyLobbyJoin { if x != nil { return x.ActiveParticipants } @@ -1567,189 +1567,190 @@ var file_ceremony_proto_rawDesc = []byte{ 0x6f, 0x64, 0x65, 0x2e, 0x6b, 0x65, 0x79, 0x73, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x64, 0x34, 0x34, 0x38, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x15, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, - 0x73, 0x22, 0xde, 0x04, 0x0a, 0x17, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x49, 0x6e, - 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x58, 0x0a, + 0x73, 0x22, 0xe5, 0x04, 0x0a, 0x17, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x49, 0x6e, + 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x5f, 0x0a, 0x13, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, - 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x71, 0x75, 0x69, - 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x6b, 0x65, 0x79, - 0x73, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x64, 0x34, 0x34, 0x38, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x4b, 0x65, 0x79, 0x52, 0x12, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x72, 0x74, 0x69, - 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x81, 0x01, 0x0a, 0x1f, 0x6c, 0x61, 0x74, 0x65, - 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x61, - 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x3a, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, - 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, - 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x53, 0x65, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, - 0x65, 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1c, 0x6c, - 0x61, 0x74, 0x65, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x41, - 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x87, 0x01, 0x0a, 0x20, - 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, - 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, + 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x71, 0x75, 0x69, + 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, + 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, + 0x79, 0x4c, 0x6f, 0x62, 0x62, 0x79, 0x4a, 0x6f, 0x69, 0x6e, 0x52, 0x12, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x81, + 0x01, 0x0a, 0x1f, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x70, + 0x72, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, + 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, + 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x53, + 0x65, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1c, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, + 0x50, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x87, 0x01, 0x0a, 0x20, 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x70, + 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, + 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, + 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, + 0x6d, 0x6f, 0x6e, 0x79, 0x44, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x65, + 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1e, 0x64, 0x72, + 0x6f, 0x70, 0x70, 0x65, 0x64, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, + 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x7a, 0x0a, 0x20, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, + 0x5f, 0x61, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, - 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x44, 0x72, 0x6f, - 0x70, 0x70, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1e, 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x50, 0x61, - 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x7a, 0x0a, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x61, 0x64, 0x76, 0x61, 0x6e, 0x63, - 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x31, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, - 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, - 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x6f, 0x75, - 0x6e, 0x64, 0x52, 0x1d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x6f, - 0x75, 0x6e, 0x64, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x73, 0x12, 0x5f, 0x0a, 0x17, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, - 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, - 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x6b, 0x65, 0x79, 0x73, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x64, 0x34, - 0x34, 0x38, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x15, 0x6e, 0x65, 0x78, - 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, - 0x74, 0x73, 0x22, 0x81, 0x05, 0x0a, 0x17, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x46, - 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x58, - 0x0a, 0x13, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, - 0x70, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x71, 0x75, - 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x6b, 0x65, - 0x79, 0x73, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x64, 0x34, 0x34, 0x38, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x52, 0x12, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x72, 0x74, - 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x81, 0x01, 0x0a, 0x1f, 0x6c, 0x61, 0x74, - 0x65, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x5f, - 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, + 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x41, 0x64, 0x76, + 0x61, 0x6e, 0x63, 0x65, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x1d, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, + 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x17, 0x6e, 0x65, 0x78, 0x74, + 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, + 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x71, 0x75, 0x69, 0x6c, + 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x6b, 0x65, 0x79, 0x73, + 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x64, 0x34, 0x34, 0x38, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x52, 0x15, 0x6e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x61, 0x72, + 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x22, 0x88, 0x05, 0x0a, 0x17, 0x43, 0x65, + 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x69, 0x6e, 0x67, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x5f, 0x0a, 0x13, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, + 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, - 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x53, 0x65, 0x65, 0x6e, 0x50, 0x72, 0x6f, - 0x76, 0x65, 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1c, - 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x65, 0x72, - 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x87, 0x01, 0x0a, - 0x20, 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, - 0x70, 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, - 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, - 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x44, 0x72, - 0x6f, 0x70, 0x70, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1e, 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x50, - 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4f, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, - 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, - 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, - 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x4c, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, - 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, - 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x53, 0x68, 0x61, 0x72, 0x65, 0x52, 0x06, 0x73, - 0x68, 0x61, 0x72, 0x65, 0x73, 0x12, 0x5f, 0x0a, 0x17, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, 0x6f, - 0x75, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, - 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, - 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x6b, 0x65, 0x79, 0x73, 0x2e, 0x70, 0x62, - 0x2e, 0x45, 0x64, 0x34, 0x34, 0x38, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, - 0x15, 0x6e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, - 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x22, 0xab, 0x02, 0x0a, 0x17, 0x43, 0x65, 0x72, 0x65, 0x6d, - 0x6f, 0x6e, 0x79, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x4f, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, - 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, - 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x73, 0x12, 0x5e, 0x0a, 0x12, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x2f, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, - 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, - 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x52, 0x11, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x12, 0x5f, 0x0a, 0x17, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x6e, - 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x03, + 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x4c, 0x6f, 0x62, 0x62, 0x79, 0x4a, 0x6f, + 0x69, 0x6e, 0x52, 0x12, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, + 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x81, 0x01, 0x0a, 0x1f, 0x6c, 0x61, 0x74, 0x65, 0x73, + 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x74, + 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x3a, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, + 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x53, 0x65, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x65, + 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1c, 0x6c, 0x61, + 0x74, 0x65, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x41, 0x74, + 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x87, 0x01, 0x0a, 0x20, 0x64, + 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, + 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, + 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, + 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x44, 0x72, 0x6f, 0x70, + 0x70, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x65, 0x72, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x1e, 0x64, 0x72, 0x6f, 0x70, 0x70, 0x65, 0x64, 0x50, 0x61, 0x72, + 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4f, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, + 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, + 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x4c, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, + 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, + 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x53, 0x68, 0x61, 0x72, 0x65, 0x52, 0x06, 0x73, 0x68, 0x61, + 0x72, 0x65, 0x73, 0x12, 0x5f, 0x0a, 0x17, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x6e, + 0x64, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x6b, 0x65, 0x79, 0x73, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x64, 0x34, 0x34, 0x38, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x15, 0x6e, 0x65, 0x78, 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, - 0x61, 0x6e, 0x74, 0x73, 0x22, 0x62, 0x0a, 0x18, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, - 0x50, 0x65, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, - 0x12, 0x46, 0x0a, 0x09, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, - 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, - 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x50, 0x65, 0x65, 0x72, 0x52, 0x08, - 0x70, 0x65, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x22, 0xd7, 0x01, 0x0a, 0x0c, 0x43, 0x65, 0x72, - 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x50, 0x65, 0x65, 0x72, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x61, 0x64, 0x64, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x61, 0x64, 0x64, 0x72, - 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, - 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x22, 0xe0, 0x02, 0x0a, 0x16, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x43, - 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x2a, 0x0a, - 0x11, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x66, 0x72, 0x6f, 0x6d, 0x46, 0x72, - 0x61, 0x6d, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x6f, 0x5f, - 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x0d, 0x74, 0x6f, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x12, 0x5a, 0x0a, 0x16, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x63, - 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x24, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, - 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6c, 0x6f, - 0x63, 0x6b, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x52, 0x14, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, - 0x65, 0x64, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x47, 0x0a, - 0x06, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, + 0x61, 0x6e, 0x74, 0x73, 0x22, 0xab, 0x02, 0x0a, 0x17, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, + 0x79, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x4f, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x35, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, + 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x73, 0x12, 0x5e, 0x0a, 0x12, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, - 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x63, 0x6c, - 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x06, - 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x12, 0x4d, 0x0a, 0x08, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, - 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, + 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, + 0x6d, 0x6f, 0x6e, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x11, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x12, 0x5f, 0x0a, 0x17, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, + 0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, + 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x6b, 0x65, 0x79, 0x73, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x64, 0x34, + 0x34, 0x38, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x15, 0x6e, 0x65, 0x78, + 0x74, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x50, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, + 0x74, 0x73, 0x22, 0x62, 0x0a, 0x18, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x50, 0x65, + 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x12, 0x46, + 0x0a, 0x09, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, + 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x50, 0x65, 0x65, 0x72, 0x52, 0x08, 0x70, 0x65, + 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x22, 0xd7, 0x01, 0x0a, 0x0c, 0x43, 0x65, 0x72, 0x65, 0x6d, + 0x6f, 0x6e, 0x79, 0x50, 0x65, 0x65, 0x72, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x61, 0x64, 0x64, 0x72, 0x12, 0x1b, + 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x22, 0xe0, 0x02, 0x0a, 0x16, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x43, 0x6f, 0x6d, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x2a, 0x0a, 0x11, 0x66, + 0x72, 0x6f, 0x6d, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x66, 0x72, 0x6f, 0x6d, 0x46, 0x72, 0x61, 0x6d, + 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x6f, 0x5f, 0x66, 0x72, + 0x61, 0x6d, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0d, 0x74, 0x6f, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, + 0x5a, 0x0a, 0x16, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x6c, 0x6f, + 0x63, 0x6b, 0x5f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x24, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, + 0x65, 0x2e, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6c, 0x6f, 0x63, 0x6b, + 0x46, 0x72, 0x61, 0x6d, 0x65, 0x52, 0x14, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, + 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x47, 0x0a, 0x06, 0x70, + 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x71, 0x75, + 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, + 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, + 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x06, 0x70, 0x72, + 0x6f, 0x6f, 0x66, 0x73, 0x12, 0x4d, 0x0a, 0x08, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, + 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, + 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x08, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x22, 0xa5, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, + 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x4d, 0x61, 0x70, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, + 0x61, 0x6d, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0b, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x70, 0x72, + 0x6f, 0x6f, 0x66, 0x12, 0x56, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x08, 0x73, 0x65, 0x67, - 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xa5, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, - 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x4d, 0x61, 0x70, 0x12, 0x21, 0x0a, 0x0c, - 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, - 0x14, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, - 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x56, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x71, 0x75, 0x69, - 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, - 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, - 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x4d, 0x61, 0x70, - 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x3e, 0x0a, - 0x14, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, - 0x74, 0x73, 0x4d, 0x61, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x7b, 0x0a, - 0x17, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x4d, 0x61, 0x70, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x79, 0x70, 0x65, - 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x79, 0x70, 0x65, - 0x55, 0x72, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, - 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x65, 0x67, - 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x32, 0x89, 0x02, 0x0a, 0x0f, 0x43, - 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, - 0x0a, 0x17, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, - 0x79, 0x6e, 0x63, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x2c, 0x2e, 0x71, 0x75, 0x69, 0x6c, - 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x6c, 0x6f, 0x63, - 0x6b, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, - 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, - 0x6e, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x43, 0x6f, - 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x79, 0x6e, 0x63, 0x30, 0x01, 0x12, 0x76, - 0x0a, 0x10, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x12, 0x2e, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, - 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x2e, 0x70, 0x62, 0x2e, - 0x50, 0x32, 0x50, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, - 0x70, 0x65, 0x1a, 0x2e, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, - 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x2e, 0x70, 0x62, 0x2e, - 0x50, 0x32, 0x50, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, - 0x70, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x3a, 0x5a, 0x38, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2f, 0x6d, 0x6f, 0x6e, 0x6f, 0x72, - 0x65, 0x70, 0x6f, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x0b, + 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x3e, 0x0a, 0x14, 0x49, + 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x4d, 0x61, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x7b, 0x0a, 0x17, 0x49, + 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x4d, 0x61, 0x70, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x79, 0x70, 0x65, 0x55, 0x72, + 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, + 0x68, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x65, 0x67, 0x6d, 0x65, + 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x32, 0x89, 0x02, 0x0a, 0x0f, 0x43, 0x65, 0x72, + 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, 0x0a, 0x17, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x79, 0x6e, + 0x63, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x2c, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, + 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, + 0x70, 0x62, 0x2e, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, + 0x75, 0x6d, 0x2e, 0x6e, 0x6f, 0x64, 0x65, 0x2e, 0x63, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, + 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x65, 0x72, 0x65, 0x6d, 0x6f, 0x6e, 0x79, 0x43, 0x6f, 0x6d, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x53, 0x79, 0x6e, 0x63, 0x30, 0x01, 0x12, 0x76, 0x0a, 0x10, + 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x12, 0x2e, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, + 0x64, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x32, + 0x50, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, + 0x1a, 0x2e, 0x2e, 0x71, 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x6e, 0x6f, + 0x64, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x32, + 0x50, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, + 0x28, 0x01, 0x30, 0x01, 0x42, 0x3a, 0x5a, 0x38, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x71, + 0x75, 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x71, 0x75, + 0x69, 0x6c, 0x69, 0x62, 0x72, 0x69, 0x75, 0x6d, 0x2f, 0x6d, 0x6f, 0x6e, 0x6f, 0x72, 0x65, 0x70, + 0x6f, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1822,12 +1823,12 @@ var file_ceremony_proto_depIdxs = []int32{ 22, // 23: quilibrium.node.ceremony.pb.CeremonyLobbyJoin.public_key_signature_ed448:type_name -> quilibrium.node.keys.pb.Ed448Signature 7, // 24: quilibrium.node.ceremony.pb.CeremonyOpenState.joined_participants:type_name -> quilibrium.node.ceremony.pb.CeremonyLobbyJoin 21, // 25: quilibrium.node.ceremony.pb.CeremonyOpenState.preferred_participants:type_name -> quilibrium.node.keys.pb.Ed448PublicKey - 21, // 26: quilibrium.node.ceremony.pb.CeremonyInProgressState.active_participants:type_name -> quilibrium.node.keys.pb.Ed448PublicKey + 7, // 26: quilibrium.node.ceremony.pb.CeremonyInProgressState.active_participants:type_name -> quilibrium.node.ceremony.pb.CeremonyLobbyJoin 2, // 27: quilibrium.node.ceremony.pb.CeremonyInProgressState.latest_seen_prover_attestations:type_name -> quilibrium.node.ceremony.pb.CeremonySeenProverAttestation 3, // 28: quilibrium.node.ceremony.pb.CeremonyInProgressState.dropped_participant_attestations:type_name -> quilibrium.node.ceremony.pb.CeremonyDroppedProverAttestation 6, // 29: quilibrium.node.ceremony.pb.CeremonyInProgressState.transcript_round_advance_commits:type_name -> quilibrium.node.ceremony.pb.CeremonyAdvanceRound 21, // 30: quilibrium.node.ceremony.pb.CeremonyInProgressState.next_round_participants:type_name -> quilibrium.node.keys.pb.Ed448PublicKey - 21, // 31: quilibrium.node.ceremony.pb.CeremonyFinalizingState.active_participants:type_name -> quilibrium.node.keys.pb.Ed448PublicKey + 7, // 31: quilibrium.node.ceremony.pb.CeremonyFinalizingState.active_participants:type_name -> quilibrium.node.ceremony.pb.CeremonyLobbyJoin 2, // 32: quilibrium.node.ceremony.pb.CeremonyFinalizingState.latest_seen_prover_attestations:type_name -> quilibrium.node.ceremony.pb.CeremonySeenProverAttestation 3, // 33: quilibrium.node.ceremony.pb.CeremonyFinalizingState.dropped_participant_attestations:type_name -> quilibrium.node.ceremony.pb.CeremonyDroppedProverAttestation 5, // 34: quilibrium.node.ceremony.pb.CeremonyFinalizingState.commits:type_name -> quilibrium.node.ceremony.pb.CeremonyTranscriptCommit diff --git a/node/protobufs/ceremony.proto b/node/protobufs/ceremony.proto index b4ce8dd..3014ce9 100644 --- a/node/protobufs/ceremony.proto +++ b/node/protobufs/ceremony.proto @@ -105,7 +105,7 @@ message CeremonyOpenState { } message CeremonyInProgressState { - repeated quilibrium.node.keys.pb.Ed448PublicKey active_participants = 1; + repeated CeremonyLobbyJoin active_participants = 1; repeated CeremonySeenProverAttestation latest_seen_prover_attestations = 2; repeated CeremonyDroppedProverAttestation dropped_participant_attestations = 3; repeated CeremonyAdvanceRound transcript_round_advance_commits = 4; @@ -113,7 +113,7 @@ message CeremonyInProgressState { } message CeremonyFinalizingState { - repeated quilibrium.node.keys.pb.Ed448PublicKey active_participants = 1; + repeated CeremonyLobbyJoin active_participants = 1; repeated CeremonySeenProverAttestation latest_seen_prover_attestations = 2; repeated CeremonyDroppedProverAttestation dropped_participant_attestations = 3; repeated CeremonyTranscriptCommit commits = 4; diff --git a/node/protobufs/clock.go b/node/protobufs/clock.go index 1bf62d9..e1c5077 100644 --- a/node/protobufs/clock.go +++ b/node/protobufs/clock.go @@ -121,7 +121,9 @@ func (frame *ClockFrame) VerifyMasterClockFrame() error { return nil } -func (frame *ClockFrame) GetParentSelectorAndDistance() ( +func (frame *ClockFrame) GetParentSelectorAndDistance( + discriminator *big.Int, +) ( *big.Int, *big.Int, *big.Int, @@ -141,27 +143,20 @@ func (frame *ClockFrame) GetParentSelectorAndDistance() ( parentSelector := new(big.Int).SetBytes(frame.ParentSelector) - var pubkey []byte - ed448PublicKey := frame.GetPublicKeySignatureEd448() - if ed448PublicKey != nil { - pubkey = ed448PublicKey.PublicKey.KeyValue - } else { - return nil, nil, nil, errors.Wrap( - errors.New("no valid signature provided"), - "get parent selector and distance", + var distance *big.Int + if discriminator != nil { + l := new(big.Int).Mod( + new(big.Int).Sub(selector, discriminator), + ff.Modulus(), ) - } - - discriminator, err := poseidon.HashBytes(pubkey) - if err != nil { - return nil, nil, nil, errors.Wrap(err, "get parent selector and distance") - } - - l := new(big.Int).Mod(new(big.Int).Sub(selector, discriminator), ff.Modulus()) - r := new(big.Int).Mod(new(big.Int).Sub(discriminator, selector), ff.Modulus()) - distance := r - if l.Cmp(r) == -1 { - distance = l + r := new(big.Int).Mod( + new(big.Int).Sub(discriminator, selector), + ff.Modulus(), + ) + distance = r + if l.Cmp(r) == 1 { + distance = l + } } return parentSelector, distance, selector, nil diff --git a/node/retroactive_peers.json b/node/retroactive_peers.json new file mode 100644 index 0000000..2a8d5fe --- /dev/null +++ b/node/retroactive_peers.json @@ -0,0 +1,1052 @@ +{ + "rewards": [ + { "peer_id": "EiDt/I7irgZJvxHTKVYBWFC84aZt6t+jH44pTtBwDps2Mw==", "token_balance": 137558 }, + { "peer_id": "EiCFw9CwNODrkiOIVFcyLpwLDVbzw+gJEk9Up36FOcAkYA==", "token_balance": 137558 }, + { "peer_id": "EiA/zYamLhLM7WvEJw76qCsu5BECV7HuHnyXqijSDCj/LA==", "token_balance": 137558 }, + { "peer_id": "EiDN04yQGVtsc8h1kiuZ9/hsp0N8YnWmuD/H4sLqBo4cMQ==", "token_balance": 137558 }, + { "peer_id": "EiBEDZeMlawFcSNZbhMdpp81oeABrkoys9FW5Gpuo+Vx1w==", "token_balance": 137558 }, + { "peer_id": "EiCpNbD4duH1dBHPSKerruhytoJXS4yBzmxtll4/+uTdbw==", "token_balance": 157208 }, + { "peer_id": "EiDW4pG0FjdnCCn4Fl3WaFtsuOjewUPyYy1hgxYGcHJU4A==", "token_balance": 137558 }, + { "peer_id": "EiDjVqM33jPfGp3G6hmXGnY/xT9+jJQaNqzaEc5YPh7nrA==", "token_balance": 137558 }, + { "peer_id": "EiDi4THsckwtdtcsVftxOXE5ECGQVt/PlUR5z3CHfdaOvg==", "token_balance": 137558 }, + { "peer_id": "EiA3/AHA4LVWEJBJC7Vj3DwN96vPIFrq7sUMIriesskU+A==", "token_balance": 157208 }, + { "peer_id": "EiB40xAnxyscpEqR+HI4sqtEHX2L5TpOglTr8wgR1Rf8XQ==", "token_balance": 137558 }, + { "peer_id": "EiBDjyTCf4m7wrvkAq795q3/9GTROLo3dQRpJDbpkA7TlA==", "token_balance": 137558 }, + { "peer_id": "EiDnRZq4L5VNWDvT6lj3Lx8hV6uLISnIw5/WbWxPlHJ/YQ==", "token_balance": 137558 }, + { "peer_id": "EiDcwwi2Y/d29+Za4AKneG8WVGzn0B8IGasVAluMJX8kNQ==", "token_balance": 137558 }, + { "peer_id": "EiC/2ozdDeYRHkBa7eHblE0B7iHotFyuutG1OoxiCF/Iuw==", "token_balance": 137558 }, + { "peer_id": "EiCsl0NhFsQfmWeqmGFnAHJO3EvcInss6hwpzs4yCGbYcA==", "token_balance": 137558 }, + { "peer_id": "EiBacHPWZ9GDbROC3ir0iBFWLSRlYiJ+31IrNoJd8IVcew==", "token_balance": 137558 }, + { "peer_id": "EiBQ82JQDutS5d301d7ZwTebXyQaw8boOLxi3eKMxDjMiA==", "token_balance": 137558 }, + { "peer_id": "EiC79invPH4VCatNnehIsRFpI32LHVISg8T6gmBI03L+iw==", "token_balance": 157208 }, + { "peer_id": "EiBqHhBPaZ40hnQkisOf5i72uii3Ft99g/WHSZeABmN+gQ==", "token_balance": 137558 }, + { "peer_id": "EiDLg4I2+SAV7f0dUfT/qwDJWAstv1CAYmbhvJG3LxrgZw==", "token_balance": 157208 }, + { "peer_id": "EiCPSIISA21MOeMW7/jaoEm1X8S0JZtUy49uqxPi/FGbTg==", "token_balance": 137558 }, + { "peer_id": "EiC+fzSzlytoB6Ip0cRcDGmEk12Ogw58Sc3hNdsg/SHkOw==", "token_balance": 137558 }, + { "peer_id": "EiBziJ/ddhCWFhS0qAIyvBqdgFzVxeww8UfIHyR1LLFJJg==", "token_balance": 137558 }, + { "peer_id": "EiDSp9LSre4xNdlcUQUeWZ27Le3z2ux+yXivh+CQ9i+f4A==", "token_balance": 137558 }, + { "peer_id": "EiB7PV8KSHxzLWMdhtKyJetVAwG1MmFAw+Aq2scndWw5ag==", "token_balance": 137558 }, + { "peer_id": "EiCOMYZsW3yEELpS24n3pBVjAiztvHT5PgCPtX2Tlw1OFQ==", "token_balance": 137558 }, + { "peer_id": "EiB7RtBwX+XKHvZ60TAmYFwUc8wf/gjr/oxM85oRovHeYg==", "token_balance": 137558 }, + { "peer_id": "EiCGalaov/tWEJ9kefQb3tT2t8Iqhw8I77Hy9LTiqkTFCA==", "token_balance": 137558 }, + { "peer_id": "EiDVxRV2koJRehT22MRKhCUAwVjWoOs7jG3YHkH+H4n/Kg==", "token_balance": 137558 }, + { "peer_id": "EiDw3Ywh65HO9XDhjHrCh9O58F4by8QJYG/BIAj/0k/MCg==", "token_balance": 137558 }, + { "peer_id": "EiDegv3AFdSkzSBIMEa66/4zF4dAy9VWX+PfsgyWbrjMMw==", "token_balance": 137558 }, + { "peer_id": "EiBYvhRhcArCadflEwdD5dCjxnpBR+GTqbzTOeB6mftNBQ==", "token_balance": 137558 }, + { "peer_id": "EiAMbrjjLm8tnSBsGYC5aAB8W+bTgLLfZRZ/vNevcfe1UQ==", "token_balance": 137558 }, + { "peer_id": "EiBK+wXpiV7GnfCYqkUBCQm8+G0YxGKlKwAYIEYdNSPKqg==", "token_balance": 137558 }, + { "peer_id": "EiBh7HnX3m2LJoXl0cFrHYC0wRkeMIu86iXuD3ITfOcTQA==", "token_balance": 137558 }, + { "peer_id": "EiDyQ/H+unN8ABzy3jMO8AXqqRWMkdpp9S5Qo5KxsRk0yA==", "token_balance": 157208 }, + { "peer_id": "EiCtbAIknJCzX3zMa7XKdL2C/Vk5SOAuQvN1QQ7g/NINuA==", "token_balance": 137558 }, + { "peer_id": "EiBPYBYzDA1opc87Pxn1+0W3T1m7r5LLDGKXORuyqb/NrA==", "token_balance": 137558 }, + { "peer_id": "EiCFRWMLrI4Ep1tM/Ypek+bBdRjFNs8VSTYUOmvAmbY++Q==", "token_balance": 137558 }, + { "peer_id": "EiDq5lIy++a/uU3dGPbUo727N4pUfjY4l5aESU0ri91igg==", "token_balance": 137558 }, + { "peer_id": "EiCir42Ak2MRRlHduW0N6EYW/TvZ4iiaCjbrvip2k0YZeg==", "token_balance": 157208 }, + { "peer_id": "EiAMqyEN+tXoJQXA3/JSsaUSEDmAh9BL9ZOd1i9+r8uIzw==", "token_balance": 137558 }, + { "peer_id": "EiBIcjkr470meG42q/bwhhTsKvsWL/SjRjhRwYlWCzmgnQ==", "token_balance": 137558 }, + { "peer_id": "EiDACZWOuYJyI5Zp/z+P4rRshPO81fv9v7w/D0Vb9YOjkw==", "token_balance": 137558 }, + { "peer_id": "EiCqZLOl92jTRBmkq4aVHo08RWPBQuGuieg53X/gfbJX5Q==", "token_balance": 157208 }, + { "peer_id": "EiBb6Z0DMUQ7je1as86/l+cqGYx0lbI/j2ZOMf7Twr3qiQ==", "token_balance": 137558 }, + { "peer_id": "EiDiXM2XUbY95oH/DxoP7zIzTkiBTqIq2Q6oG4/Lzf3KnA==", "token_balance": 137558 }, + { "peer_id": "EiAhgQeJqx2PJwubnlXxw/VNRyoUQdSrFjTuGr3ZoNesPQ==", "token_balance": 137558 }, + { "peer_id": "EiAXJMczXLr3cPMAhaEcbgskqDNJR0AYc4h925HYASTp2g==", "token_balance": 137558 }, + { "peer_id": "EiDSsJ9Cz15k1adu13K8cnWpaBfYZQia5D4HNf19HpkMbg==", "token_balance": 137558 }, + { "peer_id": "EiBhpttrR0jqq1LcunsuvGf/UvmpkLE9K3XsmbsKpMmKaw==", "token_balance": 137558 }, + { "peer_id": "EiCIOXvLFLqVeaZ9cbNh+l0su0ZrLcQ+8fYSp8EDtzzpaw==", "token_balance": 137558 }, + { "peer_id": "EiB/w8e9nsOFtyA2SYPeeGqiMgwwPFivXPEscYNWzR+MPA==", "token_balance": 137558 }, + { "peer_id": "EiDiCoY7zYQxXDN0WuNX+gT0bTvpriicypSUX2NULZb1Yw==", "token_balance": 137558 }, + { "peer_id": "EiBri4toaKjBVT+c2ttG082uNPjN2YntOGUz74YUnA5S8g==", "token_balance": 137558 }, + { "peer_id": "EiCKW+WVd/yseDDV2jCI+y7yekbimA4EOjB4NpaQDUzAXw==", "token_balance": 137558 }, + { "peer_id": "EiD90pwUVRhsyBqleRB6+fCFlmyzPEuSShGUBa5TxIIfEw==", "token_balance": 137558 }, + { "peer_id": "EiCx5rf/JCPlaaSu2vfAdf+YpYNp6vAr+CoiV9J/dtfayQ==", "token_balance": 137558 }, + { "peer_id": "EiDOu8gLzNR5ZWmg8a6JjNaFQS6LUp+0JMjJDrrGlwfvlw==", "token_balance": 137558 }, + { "peer_id": "EiA6mZroS+TJ0PvEI3sszwFCfWOMfE8rxfTebdSCjpXB7w==", "token_balance": 137558 }, + { "peer_id": "EiCvx9ZgMW2yiOdLGLrFSm0O8M99wnuVAAwFJotqjpQTNA==", "token_balance": 137558 }, + { "peer_id": "EiBBNOiQksmnZ68ePdBabTngUR97UXxkJYXRPEl2agvdGw==", "token_balance": 137558 }, + { "peer_id": "EiAo3nz97JlE158qfzDRyzBAjpWBOEx/faUrVQePy+vxSg==", "token_balance": 137558 }, + { "peer_id": "EiBkXrxRgcgoZdI1KbsVhQAHDrmOWc55aspWx0MImrRcvA==", "token_balance": 157208 }, + { "peer_id": "EiCbTU+5tg7y6uBqHEISeCGu1R9v0CiIjXxcr4yHngKClw==", "token_balance": 137558 }, + { "peer_id": "EiBJXIAwAW2SRFAQrdhnglMRBhOp5m4SxYF2yJTTW9lF4g==", "token_balance": 137558 }, + { "peer_id": "EiDU0CsEo5ClYdBuO3JPoxMBRx8tEAam5cdYHaBAcPH8kg==", "token_balance": 137558 }, + { "peer_id": "EiBtaK2pOIVjGqj9/f7lTSljT39JWd41YbjLuNX0NWo3eQ==", "token_balance": 157208 }, + { "peer_id": "EiBQnAqhRGVsBfSz4+YdbzV05rHDKh++b1vSEcAXvKauoQ==", "token_balance": 137558 }, + { "peer_id": "EiATlGphiTRVCj4/CD1sRTqolV53dk4NA2kG22bIK9D/4Q==", "token_balance": 137558 }, + { "peer_id": "EiAXDYLdsQlcFCcBArHnVbhoq1E2YBQhGLsVhqaV2pjI1g==", "token_balance": 137558 }, + { "peer_id": "EiDiSU/zw55buN1xoWp+EnVEfunIINTN/pNkWiSFx4oA5g==", "token_balance": 137558 }, + { "peer_id": "EiB3i1+vGFtWlC8Ei/8AfpypQexK7qx6F/R5REdPL3NcKA==", "token_balance": 137558 }, + { "peer_id": "EiBHbU8R0nMCaNXqO2g0ewk+4vakrS1f8EL/VCytPqmTFA==", "token_balance": 137558 }, + { "peer_id": "EiDwwS45tB4GWVX11CcliPTQejbRmS+lcTJClEpQbquF2Q==", "token_balance": 137558 }, + { "peer_id": "EiD0CCDAOEx5kecW5b4ICbyg8BhhEhApmhgASK9Bqt8UDg==", "token_balance": 137558 }, + { "peer_id": "EiCZ60/rAs6kL3pE9Qnw3y/bv2GdUMnBBIXNN3VRqpYq2Q==", "token_balance": 137558 }, + { "peer_id": "EiBpvlWQu1uluUEyD266UVTyn22s+GNv9MSdKsLfsyt7Ag==", "token_balance": 137558 }, + { "peer_id": "EiDTu1XyDtM1N43pEjkMzj7ai/Q4X2XnEaFgz31D/7G73w==", "token_balance": 137558 }, + { "peer_id": "EiCM/tgnkDCqFGaULwGhlGmxsH3VKVIEOVHMd6RyTyEPSA==", "token_balance": 137558 }, + { "peer_id": "EiCcaGUxOHidGuLwj3QDnVR5BA3BelUb5GA6UOFWVAcY6g==", "token_balance": 137558 }, + { "peer_id": "EiBVaTZNWB4SCvlWOmwK6d0C5Yzl33hW8q7CAxjgb5zlww==", "token_balance": 137558 }, + { "peer_id": "EiDjNhYf9f1XFjPNjHqat5KFyQGzTJadpcpom20xt0F56A==", "token_balance": 137558 }, + { "peer_id": "EiBoMVXwviMRtKyjX9dtXnBUiErng+hbl4hLPa1LBsDQLA==", "token_balance": 137558 }, + { "peer_id": "EiBoIc1nPO5+W5RqtFwqCo7kxSDFdckdPVwxpkW6UyEiNw==", "token_balance": 137558 }, + { "peer_id": "EiBDwCF8pdxlB93eGtnRSo23g651J/aygpWQEtOnAYUbYA==", "token_balance": 137558 }, + { "peer_id": "EiD24ZVIuHTeXxVpGf6b1azMHq67iyj1LZNLwlFP6eZDrg==", "token_balance": 157208 }, + { "peer_id": "EiB8EGifgNwKMxlBc8o71qYg++hs1FLIXQEq08s2/69ktg==", "token_balance": 137558 }, + { "peer_id": "EiDCPV7yxYd7F9zfXvF/nl02PR3zMZkQkm4KAERVBCFk3Q==", "token_balance": 137558 }, + { "peer_id": "EiCp/4Ozp78dzwtKPvqucyDPmAWvKQI0Fc9rf1SX6/CARQ==", "token_balance": 137558 }, + { "peer_id": "EiBT8XJHEtogwc/6GEAkLesKHzbDonM068Mg/mCLYPfUlA==", "token_balance": 137558 }, + { "peer_id": "EiC39fG4ecRzuvl4qpqkSJ3eIyVDr/iEfLviwASR/YtfsQ==", "token_balance": 157208 }, + { "peer_id": "EiDj5Tv5rYO3GX5P/qp14nwaEbDMHtgMkIaDYTAPCnJOwg==", "token_balance": 137558 }, + { "peer_id": "EiBZtWRH2M3P/YGeVcGzeH1WyPAT1EAufTd6yX1bRSR+vA==", "token_balance": 137558 }, + { "peer_id": "EiCsWqPPPT+BhctANRqQHFD+Dj/GdKsTmFHj6Xq84yg3XQ==", "token_balance": 137558 }, + { "peer_id": "EiDOauPSTm/mG6Elcd+EBBlpMXamMY4eCFXkRCGciKE6KQ==", "token_balance": 137558 }, + { "peer_id": "EiBw0l6b5gwe0O0Qr8WnIMx8a2Y2L5Vjziq74x/WfRg1HA==", "token_balance": 137558 }, + { "peer_id": "EiDKtgwh/YwV5hLImqNffl6K2cVyMYKbt243FB6YV4Re5w==", "token_balance": 137558 }, + { "peer_id": "EiBe1BsrDW/oLARa5LVx4d4o7ktYfxf/yoS+CEW1C4PcyQ==", "token_balance": 137558 }, + { "peer_id": "EiCVNzMEqHzb9TsgWV7AlzdT5lFAtVbA0b3g2VbZj7pUWA==", "token_balance": 157208 }, + { "peer_id": "EiBU1uPpjaSSbD+hqspV0xg9ms/a/u1SdZQPKPUwwp95WQ==", "token_balance": 157208 }, + { "peer_id": "EiBcAUabPqHI+YSNuzRpTFm5PxDeRVk3vb/4HYlLVkbxnw==", "token_balance": 137558 }, + { "peer_id": "EiCkMEu6lXBun9op8QbDcJJqwg+y531zJCdBcpKzyyzBVg==", "token_balance": 137558 }, + { "peer_id": "EiC57WWhYCOd1+qqmxW9XnjkhG82xuBDv/hZFT4wbJAbUA==", "token_balance": 157208 }, + { "peer_id": "EiAjhP9B5faB5+2IZI2cSm+FPfw9pfB7SL7AYDGlK4h4AQ==", "token_balance": 137558 }, + { "peer_id": "EiCOGSBY4ly4j3GZ9yCw1vVyr7rd5S9yWjViKuLuzTETsQ==", "token_balance": 137558 }, + { "peer_id": "EiAIIeJwouU3PVWLvUhPtm+MiQntIEKnpaKlsyIMzdgSnQ==", "token_balance": 137558 }, + { "peer_id": "EiBo1PqBDIQRzE3hNL0DttKpjuaj2yWG2tPL01UKY+31LA==", "token_balance": 137558 }, + { "peer_id": "EiD65Eh0LKGj6AgMlsVmEBGnpisJgTcdilvmlqAqBCVBTA==", "token_balance": 137558 }, + { "peer_id": "EiDxhfqRg6RzeaZrJl9FbDrEWc0Aq1iph+xWBamS6FcgTw==", "token_balance": 137558 }, + { "peer_id": "EiDrYab6PMYmhZ4e3ry0Qw/+3sbvVfG6M2vJfrUsZcn4hg==", "token_balance": 137558 }, + { "peer_id": "EiDeAUv0KvqElzgVULgPe7J3bZFV1Vi0qNREG7uCR4QOrA==", "token_balance": 137558 }, + { "peer_id": "EiBBqBROgbME6/f9OmDnI+p6gr/XfRWdhCVbaXWIe/It4g==", "token_balance": 137558 }, + { "peer_id": "EiByqJCfrbGJK6SAZJkdaM5LCIk1jsegmD9vQhscSWtFhw==", "token_balance": 137558 }, + { "peer_id": "EiCqM/Pjb5beBN9WYrlsg7jOYpT6qxLxIeflKY3dXXJRUw==", "token_balance": 137558 }, + { "peer_id": "EiDZopN+RnHBtk2ocwTFPuNe7JWxw22Q24q/vfvkcSW7KQ==", "token_balance": 157208 }, + { "peer_id": "EiAWko6omZov8Q02glLaXuFTcYcP2Em7cJ2EUdawyq1ZKQ==", "token_balance": 137558 }, + { "peer_id": "EiC79QU6FfCx3fXWTypurSbaa7MHNawbXlsRGRwD9kIuvw==", "token_balance": 137558 }, + { "peer_id": "EiBPAuih1CryAgv7qNreo/NH6lrJWAT8QYwt71XGMnprkQ==", "token_balance": 137558 }, + { "peer_id": "EiCNsbzzelVdxxhlJSvkwjt7xbZi40tpcojNJAaJlcxWTA==", "token_balance": 137558 }, + { "peer_id": "EiC842UyoMHux38m67Ij7jJz4aL7Mg1IB8CCZd2IF9ZlGA==", "token_balance": 137558 }, + { "peer_id": "EiB5TBHNSRvK9MwnbtW85pM9eaJGBULDivUFlCQauLUcig==", "token_balance": 137558 }, + { "peer_id": "EiB8UECS82ItUDV1t7C0UN4WyNu6nVBfdaM37lzwnXDcJg==", "token_balance": 137558 }, + { "peer_id": "EiDhS3+6Vc3SaMdJpYtrX5fnRS5ZRDEfF8vx+iOwWIO6Bw==", "token_balance": 137558 }, + { "peer_id": "EiAjlO4GFVrt2V+pQikLXmHfBmqcvTmJOQD3+eJXnOJl+w==", "token_balance": 137558 }, + { "peer_id": "EiCNte5+wR+8p342FHVoaR2OGq6Vb3NnXW2AhG4W+dFqLg==", "token_balance": 137558 }, + { "peer_id": "EiBjJ7ZhiNV9aHcjEtTyLANsX2lsrRbE7DUJvn5rHpW+vw==", "token_balance": 137558 }, + { "peer_id": "EiAcdEkxOVC3LS7goYpEO5du4LttGgg9NVM35jurSs99qw==", "token_balance": 137558 }, + { "peer_id": "EiDjXPG7MmNt/AKYQDDziB6rkUVFAQBPEL8rd2fhlnG08A==", "token_balance": 137558 }, + { "peer_id": "EiDrhsEnPMADvxpuN3kj1n18kvvWKXA3oWDHi1WZXFgnvw==", "token_balance": 137558 }, + { "peer_id": "EiCnI2JWuAmhP313X0ywIwgK0BugQ7NgA6ttp7PbBjPB3w==", "token_balance": 137558 }, + { "peer_id": "EiDa3uT5jYEMOq55A5jQStBwijgR0KF2GSifgZbGGRTZiA==", "token_balance": 157208 }, + { "peer_id": "EiCVUzWKGlK815nhPRldaii2fU72oiDebdMtOHC1NjxVBQ==", "token_balance": 137558 }, + { "peer_id": "EiAZ46m+gjk1acOEZWaYBE3Mzt/r2U9CKm9fZzkjwuP6pA==", "token_balance": 137558 }, + { "peer_id": "EiBNf+29/yg2TqUgcm5mVAKq2Awd4cIE0JXnehyh8GUVhA==", "token_balance": 137558 }, + { "peer_id": "EiD9ZRy93btwIV4ucAefUMYpuliAjG7dhP/8XV4jTOIgxQ==", "token_balance": 137558 }, + { "peer_id": "EiAk7UZ2QrfW3w7fW3aOnQGTeIx4BkodlcVoqoDj79dCUA==", "token_balance": 137558 }, + { "peer_id": "EiCWy8qgkdwORzG0b0o7x3sFHGMzGR29ZeCM0uUUqCJ3eQ==", "token_balance": 137558 }, + { "peer_id": "EiDz3WxEff1Sa8ByeiwpvFvIX7Nf9mBIGlSmzjW4yVtZPw==", "token_balance": 137558 }, + { "peer_id": "EiBbt7dZ0/SdrpyHEGRAVCNV5tpZOf8iGkf2hxGjGdgIWg==", "token_balance": 137558 }, + { "peer_id": "EiDOQHb6qPJjw4S9iqfiTbRyCpANCGzJ1wjk+DCMbZCqRA==", "token_balance": 137558 }, + { "peer_id": "EiB6VG4YGT/FOpsMlw70GtuxLwRWLtm82on6h8Tu0fZtNA==", "token_balance": 137558 }, + { "peer_id": "EiAk0lqV/eHg9YPtN4ChUQoPctuHRLtfi8X5i8cyP8aYCA==", "token_balance": 137558 }, + { "peer_id": "EiAkLM53kaSeuM8qcBRnWIBWtZbMtoYeTHrr2gOLQA7Y0Q==", "token_balance": 157208 }, + { "peer_id": "EiCA3PAFkKLgOXOPTOPWUvHRtDo4kqaP2A++jWq4FVnEeA==", "token_balance": 137558 }, + { "peer_id": "EiCb9Jvw2ydm4pzukNeYd7uQADWMG06yblmV+DBPlvvTGw==", "token_balance": 137558 }, + { "peer_id": "EiAD/2QUvbHyV9Z/I2YUFwGccdA7tyLJL3gN+78xoHvcJA==", "token_balance": 137558 }, + { "peer_id": "EiCZaMGDkCoAEyJgRozTxZtEkjmJmJK+7yGyiseoeFDsrQ==", "token_balance": 137558 }, + { "peer_id": "EiAr7ITg8LupDC6Ofj1F42es67j4IHuGDLXAbHBkcpX9iA==", "token_balance": 137558 }, + { "peer_id": "EiAuXy5mmGl5HjFoya9gddTfBQI9ltryDq5Jo8kTKrGmnA==", "token_balance": 157208 }, + { "peer_id": "EiA3D9eFBoUeMCLKvEiYi7mYE3pbmgedRj7MAdRWHC6iXA==", "token_balance": 137558 }, + { "peer_id": "EiCCwj7cKZuz7wtrOiw6GhNGg7j/pm1oupCJ5oq/8BR5Tg==", "token_balance": 137558 }, + { "peer_id": "EiDu8Y42LEgOfvdNSuX6le505kdL5UPQwfvXrndiVwUCXA==", "token_balance": 137558 }, + { "peer_id": "EiC/fh676OHCz8Vup8mvGJ2BRQuV4w/M2mPQVrjUy9418w==", "token_balance": 137558 }, + { "peer_id": "EiDisvDDXH6qzTtoiBfEo1UAQ/no5de1lOs5mbMvNuzGyw==", "token_balance": 137558 }, + { "peer_id": "EiDftaeCgl6Fl6VuRt7kH03+p83iY4fLCvVBO0awZbSkTg==", "token_balance": 137558 }, + { "peer_id": "EiCskqQddnjW6dsG0EkpjDRQtB0T/2i0HmF95FXq/oc/fw==", "token_balance": 137558 }, + { "peer_id": "EiDfYvHCkMwHQo1SGIrExl+xFo0Rk7WVyLzb3R6Jtdye4w==", "token_balance": 137558 }, + { "peer_id": "EiDPjISeAgCnpggVNcAFeTWP5T69QWbGZr9RK2D6SRoX6A==", "token_balance": 137558 }, + { "peer_id": "EiDjEtnQ3bj7NzDbaS1frrVfbGrjRNs4nXelBRgpdvtbhA==", "token_balance": 137558 }, + { "peer_id": "EiBmrBWcyAr89jZe8YB+hDvj/jl2ozI5fcN+UZU20RYmNA==", "token_balance": 137558 }, + { "peer_id": "EiAQjU11AvNccFg+G2P1xiXo3eAwdjKHDE+njKV+Gplo9A==", "token_balance": 137558 }, + { "peer_id": "EiCH1eO9ezx9Q5ICnHu0qUkWljIXs5sDKIgJ9toSbrzicw==", "token_balance": 137558 }, + { "peer_id": "EiBvj8iR5QnqtyCLMlgwLp17LRkCnPvjggfb+/8b00sQTg==", "token_balance": 137558 }, + { "peer_id": "EiCPkDUg+eAEAx4eMdctzsBJ2W6Q8KkCPz5Ed2WkCMDymg==", "token_balance": 137558 }, + { "peer_id": "EiD433yqmHk/ISwbVapwg4RFNvfOaw9OWtJTzHcB3JbmdQ==", "token_balance": 137558 }, + { "peer_id": "EiCUU9k9AvybFsitkfFweis1z8XdzfjSnFI7VwHxS4+K3w==", "token_balance": 137558 }, + { "peer_id": "EiCgvtp8KntElYqHPcMOqnNGaMRSMQrhiA75JzfpEjI5Kg==", "token_balance": 137558 }, + { "peer_id": "EiAkYt7QV7Fj37K5uPaaFzqSvm1Y6vQpHtYVkX2TXm6vZA==", "token_balance": 137558 }, + { "peer_id": "EiBqkYtumWWv0YqAikqLyskPL2XGvz984JUJao76py2Ftw==", "token_balance": 137558 }, + { "peer_id": "EiB48ZoOAYpAPbh+y1R/NV2KEStNmMtfluDCX9GEqLvrhg==", "token_balance": 137558 }, + { "peer_id": "EiB/LQgpH8BXKxXeT/1I04xa7BSK3Rpqf4gV7mgwecDV6g==", "token_balance": 137558 }, + { "peer_id": "EiBLTzHUsmdyJSU0K6IJ+3nD7deb3j/w4YBe2d9WNq5REA==", "token_balance": 157208 }, + { "peer_id": "EiDVngikPhoWPFGQDaukzbPry2+VnuJMNl+4qtNH7qLhQg==", "token_balance": 157208 }, + { "peer_id": "EiAeUgH7prW7wVAXhoMVNqfvN3epBJ75X0dftHiYx8/+kQ==", "token_balance": 137558 }, + { "peer_id": "EiDqQJ9ndp28N7MtMynRou6vRds9J30evUiR2sbzOSQwEA==", "token_balance": 137558 }, + { "peer_id": "EiBFJlOHZZIVEEOUYzN+7m9z8Dyi2JAJ7eMKzXhyxOxGwA==", "token_balance": 137558 }, + { "peer_id": "EiCgtRXQ+69xr3xSexZjmmBo9as5fmdXfeMAmee0LSRhHA==", "token_balance": 137558 }, + { "peer_id": "EiB+SDiUu5zYWg0XrjFABF+v72aDGylM/Xbvh99bLgC/rA==", "token_balance": 137558 }, + { "peer_id": "EiBra5zVAnmYZYfzXu6lB2/gS6BouXV7DcIPkA4qNdxIEA==", "token_balance": 137558 }, + { "peer_id": "EiDaGoc7jYl3OEYhffHVFzIa7lh2T/rwUSM8Lo8wt0wLIg==", "token_balance": 157208 }, + { "peer_id": "EiCc1y5qnoyYev03GMklk9mqPI4vzkRZA8NImSTq3pbHDQ==", "token_balance": 137558 }, + { "peer_id": "EiCreC2KQ/VUXAkh7B7vl/b+amIxVAK8agO4AakNwRhFqg==", "token_balance": 137558 }, + { "peer_id": "EiA+KiRdIaZ0l8JnM7p7b4ixAXsgnetDVV0b92i/0mKkfQ==", "token_balance": 157208 }, + { "peer_id": "EiCntm/fnimBfZVLzMo5FWyCLAecPdfBUjxsNA1ywux6uw==", "token_balance": 137558 }, + { "peer_id": "EiCpGHsztbHwtuhNQwCLjJ3A5mOuUBUSjZz0pOticztOSw==", "token_balance": 137558 }, + { "peer_id": "EiBpztNN0Xqf1t38b2EbhwdiV0uPnh7JouqPAQHf9k4eTA==", "token_balance": 137558 }, + { "peer_id": "EiDNgDbbc8h7d168za3+7OkxsPWfbF2/Wj0C0TqQ6wso8A==", "token_balance": 137558 }, + { "peer_id": "EiD2ODrPmC9doWMq/WCiymEsdeUBSHVdaQahQn8X1JSKGQ==", "token_balance": 137558 }, + { "peer_id": "EiCW3hg2/wfrbRsqXVAlRmNz6yhLJ/euBp5XT1WK0+RBTg==", "token_balance": 137558 }, + { "peer_id": "EiDcHmZCqlve6TljT1uNA4sf9nDkwoitbODaczsrrwDuyQ==", "token_balance": 137558 }, + { "peer_id": "EiCtKApm4Z9at1keNK+D9G+qtcZjcmOYorFsIdgBP4jxMQ==", "token_balance": 137558 }, + { "peer_id": "EiD3E5ybLfrVuIlTNC/PtLT0bxbJ73IkSm6V31WD7f/CgQ==", "token_balance": 137558 }, + { "peer_id": "EiABQZ4CCgA/dROFPMznV8OHlYJwvB7YVpE2QG8q/lYz4w==", "token_balance": 137558 }, + { "peer_id": "EiAv0sXFIFO95FO1i6pcm3Cv/etwK3hgnVNtAjIirIi79Q==", "token_balance": 137558 }, + { "peer_id": "EiA9CUVhMXrDwoey5zZ3UDk/9vv5NmVRSrI2E1VbfbwChQ==", "token_balance": 137558 }, + { "peer_id": "EiBF4OtXBF62jVOAd9/aGSmi4yMqJGgiGdlWuHoXXeQbgw==", "token_balance": 137558 }, + { "peer_id": "EiBVjczAYfWR+Cnr7Yr7wAWYVNDRvQB8MK7kimQxvddy9Q==", "token_balance": 157208 }, + { "peer_id": "EiCnqObOi5JPBt+wlcrfrtTOLvtzXcTH+bz8Ho9c0kBPlw==", "token_balance": 137558 }, + { "peer_id": "EiBf+/8AtcguK8Jg7G8uhSk2RrE4zeLyHtKvOq5ivGVG+g==", "token_balance": 137558 }, + { "peer_id": "EiCvbY06avzEq6Z5Fl9dVGpSYxgxvGXYBt2t/pYB5MeJUw==", "token_balance": 137558 }, + { "peer_id": "EiB5Anrm1+fR82PvpuxxA90ZxVjYytw1VNO2YofrhgHJ1w==", "token_balance": 137558 }, + { "peer_id": "EiDDxh5G0ceG81L8ZM5OImUHZCa5FRlBJQ517wpVK2d5pQ==", "token_balance": 157208 }, + { "peer_id": "EiB+lNO1WleKyaoqwVjpkYSBuq5YglwYMtokLaIJool4Mw==", "token_balance": 157208 }, + { "peer_id": "EiAqIczZ3El5VDLORlRmlL7P2PDJymLTOYbqVLVbOCxo+Q==", "token_balance": 137558 }, + { "peer_id": "EiABSmVVj6o/k35ZhWz+XngR//XA/WNXt9dnsOwuWqJomw==", "token_balance": 137558 }, + { "peer_id": "EiBC2kF9sAvD2S2/j95S1equyXP1djna0trNfv+/i7IWMg==", "token_balance": 137558 }, + { "peer_id": "EiB7pQTMuJPWqN4Ov9R4fA01GPo76W7U8wJnvyQ9mMwXyw==", "token_balance": 137558 }, + { "peer_id": "EiD7mDCGtv4wQl7lc9SKxdXWDGnhtCCG4xlnS/eHB6ZO5g==", "token_balance": 137558 }, + { "peer_id": "EiBK42dQx6Omt1qfftHiTirf/LjYcehnhshoLBNRxaNZ1Q==", "token_balance": 137558 }, + { "peer_id": "EiDI9PlqCQ44ItfmAv8res4WcjFBy/qoaLVUz3GgJ0l6tQ==", "token_balance": 137558 }, + { "peer_id": "EiDr82Ty9BU5Des3YkZWk+7yO0h6LwJKXzfkHbUipidHbw==", "token_balance": 137558 }, + { "peer_id": "EiCB0vEE0C3b88qllK92qAlVxjvAU6TZOUsVs0/q6O4BhQ==", "token_balance": 137558 }, + { "peer_id": "EiDeTDldMcJfdupjyie3xCfD6nqokoXvQWtrn6hpqY0x+g==", "token_balance": 137558 }, + { "peer_id": "EiB6e4Y3e7dHwg8qZkpt9uZ02Hopn3kNUkyxRcjFXabhpg==", "token_balance": 157208 }, + { "peer_id": "EiAN2SYlXsybAQ9XkdLJWBIYN3rPJJVyBODvA30r0crMgA==", "token_balance": 137558 }, + { "peer_id": "EiBtqGRGMEzBNhbkLDFeBqD0mtxI58LBRNV3wus3bKD18w==", "token_balance": 157208 }, + { "peer_id": "EiCQPlVMmVo2eTY+iZzPtKPtpSFpCkUrODV5bFDNoXu4Yg==", "token_balance": 137558 }, + { "peer_id": "EiBIKOtXIlcuDEBueu1XrNyswYrzSf1ujX8Kg5NWLZzc/g==", "token_balance": 137558 }, + { "peer_id": "EiAA7wfcxZux/SjXo2voSj09na2CREo1UGsWkf15UGZTDA==", "token_balance": 157208 }, + { "peer_id": "EiA4VMDy4uv9XWIgAi6ZhJDM35uaEuTwS3e/VZwecfKmKw==", "token_balance": 137558 }, + { "peer_id": "EiBN6EJOM2NQG4JSB17CIDX4Q1wer6oGycjXt7E2YzqfOQ==", "token_balance": 137558 }, + { "peer_id": "EiCGGLalG9WE6bozbLdGaK5guXTfZ23RBlGs9Uv9uPQVLQ==", "token_balance": 137558 }, + { "peer_id": "EiAvZSddPIdV7D3ljwhRZ+5b2OxVlC0Y8fa4zkILgPDSLA==", "token_balance": 137558 }, + { "peer_id": "EiAtFvzYP8Q3yVI6s0NMyUkjybEmDobuF//x+A3f5BJBmA==", "token_balance": 137558 }, + { "peer_id": "EiAZSsgD67QhAU3SE/6poRBrO5zPOR2VGOTxMM84TY/VDw==", "token_balance": 137558 }, + { "peer_id": "EiAgpQDCV3cnKbhkv16nt/pF8AxH4Zi9KLpkbU3L1j9G9A==", "token_balance": 137558 }, + { "peer_id": "EiCLn0btOlih5XI+OepuUGTh/cfAJ33w5ynGlpp83zyW4w==", "token_balance": 137558 }, + { "peer_id": "EiCDeai2KHQs6kQbLH89DmKkJGNB+bqz5Sf+8HMFRrtRnA==", "token_balance": 137558 }, + { "peer_id": "EiAu5k+odJOG2jRk2ikvsKBxdk1i2kSwXMUhmDCnipmc4A==", "token_balance": 137558 }, + { "peer_id": "EiCs4jsoS8acBYI/EEI3EWkF/LT++aAmJxwGL34NJ8fGaA==", "token_balance": 137558 }, + { "peer_id": "EiBI+4rfaF+ZqC40CLlZ+Rph6EBFLTtmipFLBf+0HIrurA==", "token_balance": 137558 }, + { "peer_id": "EiC5AOkAl2pVyR9LstzYM/NmYWgVfnIs3Icxz+mPmEPG4A==", "token_balance": 137558 }, + { "peer_id": "EiD/R1kMZFs/Uphk6hAObL8T+5tdW+bCUgZ9NVSdHcACyw==", "token_balance": 137558 }, + { "peer_id": "EiCuopZao+OzB/jbBkx4kBygbkbmdDfwGToQ9efHfvhFbA==", "token_balance": 137558 }, + { "peer_id": "EiC50zJitcCJXzRJvHvps2z6JPRhSDFmhibISk7ttGFoTQ==", "token_balance": 137558 }, + { "peer_id": "EiAT63M6Px7ZxHiYxiv7ZxZd42brelsauhHsRBt9Cax/uQ==", "token_balance": 137558 }, + { "peer_id": "EiCkrMhJuCmV2zhJIGGPciNuGc2lC6U4L0RxnbA5By4D+Q==", "token_balance": 137558 }, + { "peer_id": "EiB+GN7cVgllTzW8oYTbjHDYxV2p/OeuFkpCwaHZFUIlEA==", "token_balance": 137558 }, + { "peer_id": "EiBLrG3wRVZM++De9v3+ccZzDgMJ6HEAplPh61B4S5YY6w==", "token_balance": 137558 }, + { "peer_id": "EiBqwyHIPiwSaWxXdHRzK2iGUm4okH1Hv1KUjshZTmEZoQ==", "token_balance": 157208 }, + { "peer_id": "EiA0KQCUtI0PEsiNUZGyW+AaJouiK4FI9+FrLtQJxE+aDw==", "token_balance": 137558 }, + { "peer_id": "EiC4tbGUqzjdaz+IxkebyB3R3Pphzup4FvYFsxv4b3i+DQ==", "token_balance": 137558 }, + { "peer_id": "EiCGUQXfBRuokJRXVjxE5MTsmFW1PwJztBOqZ70C3PURdg==", "token_balance": 137558 }, + { "peer_id": "EiDe8g43WqEUHuI/FS1MOjIaBrvLDjiufR83YbwszrVqig==", "token_balance": 137558 }, + { "peer_id": "EiDPGdvmHON/A0ldwFf+R7z8PF49SLqEiAhyTfgqqTwDIw==", "token_balance": 157208 }, + { "peer_id": "EiDcMK4WU6KBZL4zXYvYOMz70aEWGY/npzh+LDzGlP+BKw==", "token_balance": 157208 }, + { "peer_id": "EiB8x4GuPKiBHbL5du5wlBYC5C5oppLajOjIfmLQJ6OTcA==", "token_balance": 137558 }, + { "peer_id": "EiB9s3cB6R2ef6szn78Buh39wNJTT6Zlzr8tOYyizQEGxg==", "token_balance": 157208 }, + { "peer_id": "EiDuLsv0RyrofD3QyQpsR5+1AWqGoxcSDWthqhmR4KlHUQ==", "token_balance": 137558 }, + { "peer_id": "EiAoWM0By5swyHg7oo8rX/+1dCoL4eUloJxAtl1YftCqrQ==", "token_balance": 137558 }, + { "peer_id": "EiBbNKpta8Qsn19wiIWnKTD6Z3Zq8NdZe7yW1rEyXdQwOg==", "token_balance": 137558 }, + { "peer_id": "EiBVxhk+ShOhyzwlS1dzxiBqvmh8zTfU7TBPa6H41Xu1Pg==", "token_balance": 137558 }, + { "peer_id": "EiCRoYv4VQBN7iwhi/yblGxEkTb163UCdcwbwS/VWEjfAg==", "token_balance": 137558 }, + { "peer_id": "EiCZ+OJHLfeVkclYarjJ6U9vVRq2yulDqBK7aPZEY5+oyQ==", "token_balance": 137558 }, + { "peer_id": "EiDhgDJHnu/4e+AIgtohdNveAD9vQN7bRAm89DSqSzP6sw==", "token_balance": 137558 }, + { "peer_id": "EiB9gEMg1+aoU0glO9ykSOOUPfCXNqt1qYUo9SOymO3bAg==", "token_balance": 137558 }, + { "peer_id": "EiA100t3PRxiPpxt1rKgVxYO5DLh7BAnZkkeLvveEXuT3Q==", "token_balance": 137558 }, + { "peer_id": "EiB97HDI8LQC0GV6Duon7y2OxJ6sYMhVrGFCwfmLLM3eRw==", "token_balance": 137558 }, + { "peer_id": "EiB3RYnKKJqN8plhodTS744tIZV42fQI2PDR3qi7pVXwoQ==", "token_balance": 137558 }, + { "peer_id": "EiDQAtKJ5IOwyWyJcLfMgQZUXagbv5T5bWJROotr7yhngw==", "token_balance": 137558 }, + { "peer_id": "EiDbQABcKvZHuMaoBOSILSvZ+WT1+HXz9uuh3iB4iCRmFg==", "token_balance": 137558 }, + { "peer_id": "EiDgA09flDCpYk2RZJLhkPNmYIaDPxFC9bc6HFK8t4y8nQ==", "token_balance": 137558 }, + { "peer_id": "EiBDoMkiC6QClOVuQbgQ8fNgF4+9UoYJEu/ofbY+Q2FZJA==", "token_balance": 137558 }, + { "peer_id": "EiDh6xqGmTtqaAcT8fUy42+9uOwSyuaiWu2H4BCemCT5Ig==", "token_balance": 157208 }, + { "peer_id": "EiAAY6pE5i4Va9G24V1wf3xlWAlff4lmAgGLxx5YRMDm+w==", "token_balance": 157208 }, + { "peer_id": "EiAUHyDXncXwDNwXam488KI/ZcHcEeQ3Y1UepIUVa0jZpw==", "token_balance": 137558 }, + { "peer_id": "EiDpDm6ULT0zFLyFt9FoEyDST0UUWENG/tvWVcfrPEEykA==", "token_balance": 137558 }, + { "peer_id": "EiDAgucJmDeH71VTlBr6Tds9Xlgp4wfCXYtPiEZ6EQl54w==", "token_balance": 137558 }, + { "peer_id": "EiCZW+B1BC+/XIBHqPp+TlQetFZtTONViTENNKX9gym+pw==", "token_balance": 137558 }, + { "peer_id": "EiB7jT12NBT0LAhuQgTe9W2ZbZPjh1r7zTw3gqeILXdW8A==", "token_balance": 137558 }, + { "peer_id": "EiAnaYLNqkkD+KsaiEmb3jjMxV5d3SwXZLRWZRWtyaolFA==", "token_balance": 137558 }, + { "peer_id": "EiAUXQsFmfuuGg2K6YXc/PIOcGoV7easZ4FLCZ2sLPubaQ==", "token_balance": 137558 }, + { "peer_id": "EiCKPjtVwy95EHMWusmjDKbuM7y5c2hQMARWuA2lWvozqw==", "token_balance": 137558 }, + { "peer_id": "EiAC36AjoCsacpU3+EDSsSmwn0k+h1kCRU0xDqUKDK1PBg==", "token_balance": 137558 }, + { "peer_id": "EiA9FLFmQFfiVgv42zPf00A1hiwsQ6dP3Ff4VYKhefPLPA==", "token_balance": 157208 }, + { "peer_id": "EiAAGmFUPKTUM+oerZ9OBng/gG5zKv7bNRSjmv/AC+evQA==", "token_balance": 137558 }, + { "peer_id": "EiAN5DlwxKeEIjn5coZTEUi4N464ny+WPYOWu6dzlTPS+g==", "token_balance": 137558 }, + { "peer_id": "EiCgtlpVM12OZRU3x1VCjvjsWNkKdjer5Xfy0VkMiZOhvQ==", "token_balance": 157208 }, + { "peer_id": "EiBgwEn7JglAV+D3hYPqQcmipxopjPiqc/nAzmYCngdQHw==", "token_balance": 137558 }, + { "peer_id": "EiAfLa3D9ekAB6XZ1E/P71iTJITjOJs3EKRNDBYtDiexmg==", "token_balance": 137558 }, + { "peer_id": "EiAgLM2GAnbCvpsZKlRfJwJ0eEKl3n4DPsIxE4jvX92l/Q==", "token_balance": 137558 }, + { "peer_id": "EiBZGTYzIzVy5pBAVaSbY9cGqw5xKfUvP0BQDZs5BR09mA==", "token_balance": 137558 }, + { "peer_id": "EiAqO53EIRsj17SqXZWWJECTyUaM6cOf3gW9PSEJKoQbZA==", "token_balance": 137558 }, + { "peer_id": "EiD706mU4PTftS7516daxMQoMj53ixhFrC83jtM62AK9aQ==", "token_balance": 137558 }, + { "peer_id": "EiDWgzeIvL6EEZM/EwjD0Q54TLd2+m2yZpIehC/tsJKOHg==", "token_balance": 137558 }, + { "peer_id": "EiAEogUfqBLPI1O3mUr/b/0AVNZLm7DuM1NT2RA+h4CS7w==", "token_balance": 137558 }, + { "peer_id": "EiCEg/y+xLhzoOykP3oen+G7aFEhcLgm5YNyiIrSlxEFlw==", "token_balance": 137558 }, + { "peer_id": "EiD1ClZn/lr+n/gnS96Q4gKwBDk3yl33kNIhx9wUxJiyEA==", "token_balance": 157208 }, + { "peer_id": "EiCS+UGbICfohPwlkkBArU+suk5ocOvPhGS5rDBavrSysw==", "token_balance": 137558 }, + { "peer_id": "EiBejpof3a17bbGGegjU85qeUIXYefAlXHLPAo1FIuQTYg==", "token_balance": 157208 }, + { "peer_id": "EiAGEpNUss0oz/eSsRLBtcNtd8TyiMVfr7xy48pIG8VawA==", "token_balance": 137558 }, + { "peer_id": "EiDyVGg9sAlZvuJh6sJn38vCxfReQGoSKupyZQT+PxZrBg==", "token_balance": 157208 }, + { "peer_id": "EiAdEYEQJQqGEmJmqnodIqypxuiRKOpULxCzOGk/Drc1Jw==", "token_balance": 137558 }, + { "peer_id": "EiD9Llqctpvwv0RRiL/zTbjoHZ9UxRaxw+rrZe1As8V61Q==", "token_balance": 137558 }, + { "peer_id": "EiA/aIPYU7aZvO8ZaEOf6aWuAo724Zh4wCoH0d0N0Pnn3w==", "token_balance": 157208 }, + { "peer_id": "EiBksN5pbnr4zLiMWshG5SrxhqxJUMBkZW5qvkpDauAq3Q==", "token_balance": 137558 }, + { "peer_id": "EiBE3N94casP7FFAAol1HDV++Nah7QCNKHHjZtRuk+z4QQ==", "token_balance": 137558 }, + { "peer_id": "EiBjrd5gSlAfML4iUDdknXTWrwZ9lfGViPSv7vIjyys99A==", "token_balance": 137558 }, + { "peer_id": "EiBLAMpUbItxRg+Q5FvtlvaGi2rYiwjtKzXe1DmqKUi6/g==", "token_balance": 137558 }, + { "peer_id": "EiApVYQygAgEh2IFOXEHou8avd8Qw3FjlwsYqbPVUhK0fg==", "token_balance": 137558 }, + { "peer_id": "EiCD+wJGK3BpFfByuU70A+iO/MhWoZZqkdxIkZqVtzq5Ag==", "token_balance": 157208 }, + { "peer_id": "EiDYS4Kmy1C3HrT/t5UcxpJhY9s274z/+QXmW+rh4qu4yg==", "token_balance": 157208 }, + { "peer_id": "EiBPrs9PE47ZceqK5Fp+jtpInHjfbvpuasWUDKgWx5JTLQ==", "token_balance": 137558 }, + { "peer_id": "EiDq+NOd3OhtBtqWme38fOnyjQ2DHYGp/d/15zk1ynCvEw==", "token_balance": 137558 }, + { "peer_id": "EiBdvGYTySnnmJC+7HY6hJ8mtgNtgEesmVGjM/bAZJO+sA==", "token_balance": 137558 }, + { "peer_id": "EiDHhNSCuO2rII/ptgl/bDU0Sp9M8gRdM4a5eOTk8JGBbA==", "token_balance": 137558 }, + { "peer_id": "EiBk/8MT5SauzL+k1yv6LdJ4IB1ANi8xG/VWP2oStpvVMg==", "token_balance": 137558 }, + { "peer_id": "EiDpJyIFhx17Wfw5i9Ae06RcoPE0h5or+uX7LO71Y+19Pg==", "token_balance": 137558 }, + { "peer_id": "EiCxYrVu5Bln87tUlUlItdaQuEFyNqj0FL9yOENzGRN94g==", "token_balance": 137558 }, + { "peer_id": "EiAV0e0LXPlk6QrL1ZQYD3yHC/As94dBNa5SnXC+8wduGw==", "token_balance": 137558 }, + { "peer_id": "EiBpRLhlsP/UL7lbpmG990Cx1Fs07gLbJn0qT/755fGGWQ==", "token_balance": 137558 }, + { "peer_id": "EiBfXibLxmGFrvVix+zDTo0f43GjXEKt0rYqMZfQyV7MJA==", "token_balance": 137558 }, + { "peer_id": "EiCqaQhJzVnVhpI/JPydNHCpQcc5YtpQ7KtljcByIGaezg==", "token_balance": 137558 }, + { "peer_id": "EiA9+jMngCwreiwZ4BQOkp4MRlEBG9Oopg2EfpSgWQ7zhg==", "token_balance": 137558 }, + { "peer_id": "EiDkWIDnoEvf8QAbWPplSJhlW365RSB92O3nt2Ur7wwtLA==", "token_balance": 137558 }, + { "peer_id": "EiDsoq1sGqFS27UHg5VPJZu/mi4xLVkOsO7aKfaPIiw9Sg==", "token_balance": 137558 }, + { "peer_id": "EiAmKQgu42zcqNBYuGM5Jy2f2KVhIvsrl4dLpJRbdoV3bQ==", "token_balance": 137558 }, + { "peer_id": "EiBRVawJJCCix8NamzTnuJ/eR1y+2AA53Arz29Wux9G4ZA==", "token_balance": 137558 }, + { "peer_id": "EiCKRjGjDdrs7V7YaYuR8whEioPfSdffIw27Hnz6WjOI0g==", "token_balance": 137558 }, + { "peer_id": "EiADQPc/+VKrP6PExxIIM29NtWczuEsQkIbFOuYOFK2RrQ==", "token_balance": 137558 }, + { "peer_id": "EiDgNrp4Fppegk+QsosN0y/ABGYGNPVL/bBI+W5+DxBodg==", "token_balance": 137558 }, + { "peer_id": "EiCoEn06vol7BFgli2k6joQ/X8oO35BJ8JqCJJTo8hhv+Q==", "token_balance": 137558 }, + { "peer_id": "EiCVHBFKcgVZvY+MrdeOb31tgLqsM+RFYHAbYBiZkrhMMQ==", "token_balance": 137558 }, + { "peer_id": "EiDysFOhmL573OSnuvgQCeI0uc/pWCyCjyQq2hL/IC2ARw==", "token_balance": 137558 }, + { "peer_id": "EiDZazhdtuv9vjvvHeJBjS51oNvrOwr9cZ+jMltVVbBcNw==", "token_balance": 137558 }, + { "peer_id": "EiCjDzoPmvwLkQKR/ClUHymrezQPixf6nTuTM7YiYkdE/Q==", "token_balance": 137558 }, + { "peer_id": "EiDmZTWRIgsNjFhaPEped34QUt1uBAJYSVvy7X0Hw+fJCg==", "token_balance": 137558 }, + { "peer_id": "EiALJndJOaLZYu9GaJ53HQ+OHqAtOU1s4/+iWOK/8iI42g==", "token_balance": 137558 }, + { "peer_id": "EiAQ1qkmtAgk9qEEJhGNFLaXprDMF+WNWkcZvGDKFVmOng==", "token_balance": 137558 }, + { "peer_id": "EiC2bTcJ8YY4Zz7VoSvfUlqYbRfwx6TPhntmH3PLETHXkQ==", "token_balance": 137558 }, + { "peer_id": "EiAlHUUXZnRgi49XoRmzEvLTBuf3J2WkCidjp9BA/HzslA==", "token_balance": 137558 }, + { "peer_id": "EiC4ftRiGqwMolMTJoV/gHGOykBApNz5E/3HwfvIkubIMA==", "token_balance": 137558 }, + { "peer_id": "EiAINRTHs8IfaTgVX/RztGWaf6XjMcWa9oHsWu9AqPQirQ==", "token_balance": 137558 }, + { "peer_id": "EiDKTuzZGvDyq4nrSLG1dG5EjSaBsTnI9Dhb5u2QR3w2IQ==", "token_balance": 157208 }, + { "peer_id": "EiA8mcKvhNnXtOzDkjarKpL5JPoRAaZIxd+Bxm5epWsh6A==", "token_balance": 137558 }, + { "peer_id": "EiBhB6ThVIxME1Z1BPBfxuKB94rSFUt2RDmc2+r/HUBlYA==", "token_balance": 137558 }, + { "peer_id": "EiD+dsBrCy/3DNorb9cecwT9FpR2MNNHc2OBeSA1Iu3HkQ==", "token_balance": 137558 }, + { "peer_id": "EiBGgDkjoanLHTyAKw8IR8wSnwPgnWOLcOsUQ2U+1oua2g==", "token_balance": 137558 }, + { "peer_id": "EiDk1kU9NOtQR8nMytl0QhxeH3Ssuw52OxqVvOZrslMr5w==", "token_balance": 137558 }, + { "peer_id": "EiCN+oyx7su4px0triKa6EYxl7AB0DbSpi1KJzbO86VTug==", "token_balance": 137558 }, + { "peer_id": "EiDcab9PStZfKELc20vvRFHE+fhzqmukDlJOR3+gAl+UmA==", "token_balance": 157208 }, + { "peer_id": "EiDp0kEdQErFqOg07FU/GnjeVB8XEpma44Vyc3mb1Ef9Eg==", "token_balance": 157208 }, + { "peer_id": "EiB89GnA0DEEes4RXAyLBJesXJSYjplUSkgpTNaoWBLsEw==", "token_balance": 157208 }, + { "peer_id": "EiA1VfdGkyx+tJG4N6UGu1hKjodgoAGTEorzWfVs+5Xw8Q==", "token_balance": 137558 }, + { "peer_id": "EiBU0+sJZJH+7Bp3a9j2LVluKd9t7UBFH5rahzHP7SpYWg==", "token_balance": 137558 }, + { "peer_id": "EiDA3iHr5GCI3fIw6p1sqS9Ut5PjuMqUdP+SitnCMvOPWg==", "token_balance": 137558 }, + { "peer_id": "EiCoQxedMtpJfB7t/35aaau4uWesK5ln4KykmSdgHG29AQ==", "token_balance": 137558 }, + { "peer_id": "EiATco/nzGcWFf8ahZJyIgppqW68b7uTDj92dh0QGrUzyA==", "token_balance": 137558 }, + { "peer_id": "EiBR2OQ10cuOu4nKdKeHaQy4Uz7IIyw3X9qCtsaNuH5Uow==", "token_balance": 137558 }, + { "peer_id": "EiCKZX/CNm+h9RK07sfStkvaxK6dHoP4/B1QCyOwQBmtug==", "token_balance": 137558 }, + { "peer_id": "EiABcdEEJJfBZJpdCbWjUh5uSRC4y59UkCp9GoM4biwT1g==", "token_balance": 137558 }, + { "peer_id": "EiCe0aTgx2Cu1zsxaVfDR7cxNu65D9v5I3T7MTIfosNG5A==", "token_balance": 137558 }, + { "peer_id": "EiDHCldHkMNoz266D1tgOti+SEToDtSjujcypt8nNL1G+w==", "token_balance": 137558 }, + { "peer_id": "EiB3vZK8ixEftjLqgkvldnn9yHTxe7mWNk6ba8mBmt9ksQ==", "token_balance": 137558 }, + { "peer_id": "EiDCSLA2xAN0e679iRafJHnACM1bKfnrLY7PIpFMxQmEFg==", "token_balance": 137558 }, + { "peer_id": "EiCLd4Vmux1VYcg6WvsfoOKy5vfL3uGtBnSCd7vE/fylWw==", "token_balance": 137558 }, + { "peer_id": "EiDPsj1bcWNbAKm+HHYmShX0kEZVA5UAcI7NyY14/CPpXQ==", "token_balance": 157208 }, + { "peer_id": "EiDqX4/5Ou8vKMRnayi0O3ltMkdvGkMRCb8G/PL87vQfFA==", "token_balance": 137558 }, + { "peer_id": "EiC68pzDnhtLXMM7nyATOZwbd+DQzQPse1J+ta5jeaOYFA==", "token_balance": 137558 }, + { "peer_id": "EiA/bxjlK17UdlZl5q//iaI+y9OYfz/sDiq/0+SkU6hT7A==", "token_balance": 137558 }, + { "peer_id": "EiCKfybjaoAGq11A2bZMx+W7NQCpEGYPDmj6IhM9TUh3tg==", "token_balance": 157208 }, + { "peer_id": "EiB4y3z/324s/Slp/j2Wv8hEP6RZPpL5pfkfNTI//d15/w==", "token_balance": 137558 }, + { "peer_id": "EiCtdbaksp9dOd9DDa72bWUokeG++DGYzI8tESCbN5U6TQ==", "token_balance": 137558 }, + { "peer_id": "EiBNmWmujZOthMkVX4hRXuxhT2UkGQxH3ZQSbRTYG3HkKQ==", "token_balance": 137558 }, + { "peer_id": "EiBHrClYSwP3MZZlFqPz/hycq7ujDlPGzNoHn1a2x7Bwhw==", "token_balance": 137558 }, + { "peer_id": "EiA+1+tnDCb0nAf41vlKbuXmB2NcY0CCe1FopFTwbULbxg==", "token_balance": 137558 }, + { "peer_id": "EiDmZtyzkPJQFaRPNlRXA9xqzFGZCqRI4dMxTI4xLlg5ug==", "token_balance": 137558 }, + { "peer_id": "EiAPdwVGIS5gdcR47qEmaQgYHTLGLDzbo8l+fqjFYA2qPw==", "token_balance": 137558 }, + { "peer_id": "EiDc3b5ykipRZwn2s9JD6GYWgCjjYNkDfEUH5ILBHnNPmw==", "token_balance": 137558 }, + { "peer_id": "EiAL2se2LGR/0tPFRZV1REcHjU2VzFgNfoE6BC07jkGw6w==", "token_balance": 137558 }, + { "peer_id": "EiAWxMdDkXEdvH+UyYTJ7zA/HopO7PdAeWzaFAlAPHxrAg==", "token_balance": 157208 }, + { "peer_id": "EiCUq9h/rTEx1kSSB4/LMVgsmregP6lUTomkgYrBnbjNFw==", "token_balance": 137558 }, + { "peer_id": "EiCHOyTO9Q4IfMaMUnUEiURByDFrt0eIGpgHrfMTH/qy0w==", "token_balance": 137558 }, + { "peer_id": "EiAQu5dD9vuiOMg6HzcsEMBivS/d4IQXN3h77mZjMNhibw==", "token_balance": 137558 }, + { "peer_id": "EiDXPzhsqJ/bjDAWkTTuIqlznKlSvcePJ8Mk4mhZnlCAfA==", "token_balance": 137558 }, + { "peer_id": "EiCkvJZWVRP7e4sH+GHrdXWqE7T5ADwTXCQ9SwZ6y1016g==", "token_balance": 137558 }, + { "peer_id": "EiBP9FJgyNS06x+T78clbNMSvf80nIQ+V7k+9aQhmwD/5g==", "token_balance": 137558 }, + { "peer_id": "EiBV6xdHFCqsbh/V2GHzykdmnpUaqFxNE92w4dlqokV0Xw==", "token_balance": 137558 }, + { "peer_id": "EiCCFD7uHIKJxtKbPL3LgG0wif4b8Vi8tWFd9l+1JqVDLw==", "token_balance": 137558 }, + { "peer_id": "EiDjWJ08Y7NayDPit22SnL/9lh2mdnBgejn0yULZYTkwHw==", "token_balance": 137558 }, + { "peer_id": "EiDMehCt7VlQilNOEWMDrqDTrRNgFQwB445sKjJtrTNb/A==", "token_balance": 137558 }, + { "peer_id": "EiDCwiJOFNOqormuFUv4gY9Fx3AALf2+zCGdINYXVwAU0w==", "token_balance": 137558 }, + { "peer_id": "EiB24XtmOig1e2COvo0PNam1LkAdVSqMaCVcoFuWfwPFjQ==", "token_balance": 137558 }, + { "peer_id": "EiCejOSxGqS/PE/DHRDdWqqtZsVg4dE3H/grq4kXA0lDFQ==", "token_balance": 137558 }, + { "peer_id": "EiA/6XxrFKmKK30gWpul7ST18nMrjCmE51M6SgRHEn0YCQ==", "token_balance": 137558 }, + { "peer_id": "EiArCutLI9X+cwDwPqe+ozv0OrjHovrvjPXA3vH5XbnY7w==", "token_balance": 137558 }, + { "peer_id": "EiCvMIjvDRwc9nKfoZGzf4PME3S7FKIQIwzXQndg5pNmWw==", "token_balance": 137558 }, + { "peer_id": "EiBpleJCfpISjLL2Qlpu0CBx0YtT/aitwblYnlbrEQhp0g==", "token_balance": 137558 }, + { "peer_id": "EiB3dJrzQj57hGlL77MICeb+TFDqzVu3ErL+bfFCKhkhtw==", "token_balance": 137558 }, + { "peer_id": "EiB1PX+8OIQEeBJn8aAbbIbhgCW2AeYXoO98ddxyGmiKMQ==", "token_balance": 137558 }, + { "peer_id": "EiB4d8LJCTTQDgqk54x0AJkhgb+WtGOaZIGJ4km8uUodMQ==", "token_balance": 137558 }, + { "peer_id": "EiAAA1mzRx2CKmcp/tmH3JZpmuJ11qFiWIz4iJKEFy5HWQ==", "token_balance": 137558 }, + { "peer_id": "EiB36hBV4anjov+eJuhz3Wu74TX5XEl0Def2x6sipX+/8Q==", "token_balance": 157208 }, + { "peer_id": "EiA/YbtP4FdCmHecgeScK+JyCgGsQXzT86F58sV6c2mWXA==", "token_balance": 137558 }, + { "peer_id": "EiD7OeDPJfZYGHp0Zhad50Zd7Rhx2YiLi/o8oUTwUQQRcw==", "token_balance": 137558 }, + { "peer_id": "EiC9YMdYZazIpQgj49BlcE31ISygEBkSDeYZJE3htKz/3g==", "token_balance": 137558 }, + { "peer_id": "EiD3zWIZoY6PXXeF7y4+a49EMNiviocb6Q7gHYu4wIpKsw==", "token_balance": 137558 }, + { "peer_id": "EiDiRmVhssP3nFyfci/Zh/EOBTzDlH6iht0UBNaaW9xLsQ==", "token_balance": 137558 }, + { "peer_id": "EiDiq1QVXeZvZoRP5lmcavDqMLk8oXKtSz+QJ7ok2zqQOw==", "token_balance": 137558 }, + { "peer_id": "EiAtM9sDIaGs9cvPhotiJDN0eX7TJPdAJ1c5lC1KR5px7Q==", "token_balance": 137558 }, + { "peer_id": "EiC0d2NFu9t8wJTBqSLCIbWLI5XVENcFHtnsMkUYUGcghg==", "token_balance": 137558 }, + { "peer_id": "EiBczdVAatCiWaVsgA4I6Qo9c8VDAndub3F1BYP6aFva1w==", "token_balance": 137558 }, + { "peer_id": "EiCQJsIDaNWniKt7oIH9PVMS4sAjqw9M1h9eI5tJOfcvbg==", "token_balance": 137558 }, + { "peer_id": "EiBAoCcTJStOSZN+ae9cxG/GJqJrFBPdAE9LkKxNzp0lOw==", "token_balance": 137558 }, + { "peer_id": "EiDDuejVIWjWB2EkgmzmTMXS0sWKWASp2xbiZ/w048hgLg==", "token_balance": 137558 }, + { "peer_id": "EiBTwCmsrLo9nNe+2v7aI/GfCAgvaVBx+JJlheMboLqmMw==", "token_balance": 137558 }, + { "peer_id": "EiDTBA//SALTW0B78xLh2mpJP/azixsk6pfPd9GE3VKd8g==", "token_balance": 157208 }, + { "peer_id": "EiD8xUedCdFDxKpvVHS6XxHuEon6fuXWIR5d27nWWOneQw==", "token_balance": 157208 }, + { "peer_id": "EiCkA9myYy7GEN28iFlSiunkX2hRWG5no8JKPRBnrGlBCA==", "token_balance": 137558 }, + { "peer_id": "EiC9u/D1Kc7QRXQQvYbtfdGyPORzFeyHAHlfjISFz8hlfw==", "token_balance": 137558 }, + { "peer_id": "EiBtQ+eLb0VB4yW1nXVa3iJiJ0qTYPNu/2gcyOEoV2zHbA==", "token_balance": 137558 }, + { "peer_id": "EiCwOekX3cjvUyWJAPdi1yKUW1QkpWF02joo7SKO75L+Mw==", "token_balance": 137558 }, + { "peer_id": "EiCJ0IkusUB9C9G1uS61LL1LGTKnybWemVgI6EIXnDpJhw==", "token_balance": 137558 }, + { "peer_id": "EiD0EzPqhxDeqVXu9L2f7ejpy1uyi7fMDTseHSicDS/uyQ==", "token_balance": 137558 }, + { "peer_id": "EiDYVPNxD3RkVLAn+NQRXw+Hpo9MndkCpZ9RRc4s56vkKQ==", "token_balance": 137558 }, + { "peer_id": "EiAhwM51CcGxWJu/btMQXLWcHvspYe9qmwOV71kr1saqmg==", "token_balance": 137558 }, + { "peer_id": "EiC3Kzc7YMFvjTQJNRzg3epEfqa2pf0HBUi7mO6/r4g0aw==", "token_balance": 137558 }, + { "peer_id": "EiBudCwHp8+4szbbCSoU/ub7EsqR4Ml2p6bkuKP8oqfB4w==", "token_balance": 128947 }, + { "peer_id": "EiDZobodlnDZ/nNPhlDlw29XUQlevuKLopzM4rsMC7keCA==", "token_balance": 128947 }, + { "peer_id": "EiCIdkcjKNCexeiA79DIPTsN7V6pEuZv9Jf+PIDAbXhT8g==", "token_balance": 128947 }, + { "peer_id": "EiBNcJ4MFi2zU9uRYHZecmx6mh2vnQtdtV42F7KpluNpeg==", "token_balance": 128947 }, + { "peer_id": "EiDFttf6R3TjZyW2/R9XODCUEbn3gSLruOSQclIanTGw3A==", "token_balance": 128947 }, + { "peer_id": "EiCYvXZFY0o48MA5kJL5aP+lX3jVIOyfwuxHE8QsqNsWrA==", "token_balance": 128947 }, + { "peer_id": "EiD1nq5d0976wfxEhbJqXeWO7lZgX+z9HyGuqkr03kj6wQ==", "token_balance": 128947 }, + { "peer_id": "EiDBqssLUs550bRROG5rT7Gh2ZSUD3yJt1sG+cR+KfEJbw==", "token_balance": 128947 }, + { "peer_id": "EiBHfY3x3OGPrj5NNv+Sv/v91uRzLunt5xDzfcjye47PMg==", "token_balance": 128947 }, + { "peer_id": "EiBVpYyEsSVHYip42IG4LbYO3R57T89Nckk7Gv0eDUPOAw==", "token_balance": 128947 }, + { "peer_id": "EiCKhTBAZRFHY3zim2MwgIg0ZHP4PEmyp0NJBqrHbouJ/w==", "token_balance": 128947 }, + { "peer_id": "EiDAlZyPyVkSoT396S5/4gqiV+T0xyFIUzfK3cu2/9YCQA==", "token_balance": 128947 }, + { "peer_id": "EiBenIgcHBcpfYF3glMJfMFFzR8vlTvEYKwut2EinyndIQ==", "token_balance": 128947 }, + { "peer_id": "EiBiCbWG5pLCORbJvDHn5WUs++MW3bJV5B18t1uk2x8vrg==", "token_balance": 128947 }, + { "peer_id": "EiB2HzTaxx/MVv/cnCzctCRJlt3dzGHS9GU90DW1acAmgw==", "token_balance": 128947 }, + { "peer_id": "EiB0ySUp4IB2umQ624skEgqsVQj0lY/LXieaABpuDV9+FA==", "token_balance": 128947 }, + { "peer_id": "EiAmVUd8apr3OsmKXyWwhJbMmmWA6BRTyZ+KI4IwAfFyCg==", "token_balance": 128947 }, + { "peer_id": "EiD83N5on3ALN7c5Yrukiz4ZvmtcVDF76I4/zRtLgc0wAA==", "token_balance": 128947 }, + { "peer_id": "EiD4bfZuFjOXKZLTxm71N6HZjzAIUM/8Sqv/cU998qo4mg==", "token_balance": 128947 }, + { "peer_id": "EiCe0FmkQzbuYrL9A0t3mwFeQdiIky2xHB7JMxjBYIeRAw==", "token_balance": 128947 }, + { "peer_id": "EiC8QR2AYCt9T7D/+f7uvyFWOKvcmUo4X0+aanBGzaVrEw==", "token_balance": 128947 }, + { "peer_id": "EiCPHbDqSZGymQQDigwc6p0jw4A13JlDF+BKvzh3O+P8fw==", "token_balance": 128947 }, + { "peer_id": "EiC9hCUmVbouUu0FP38f9zhg6x2AlhuBcQQxnIkau5X9oA==", "token_balance": 128947 }, + { "peer_id": "EiBKkvHpfQkotrQ+ZOcrHSr6Be8+Wr5JqXu9BurDWNvlQw==", "token_balance": 128947 }, + { "peer_id": "EiA/LKqaSLse/YfL2nBR0F3Vd7vrvmekkSMV2QUY8psjVA==", "token_balance": 128947 }, + { "peer_id": "EiA38eVX9pSSs+INUgLi/t3Zihv+LNAKjmW2Qt7+BwCx1Q==", "token_balance": 128947 }, + { "peer_id": "EiAlX1Mw91D9yPSEL479b5zRTfbUb+BSQ5QuGikmkgL3eA==", "token_balance": 128947 }, + { "peer_id": "EiAj8vNP8zEmMcEQTlsucUe82e7oBk8QVtvZDOmsje7Aew==", "token_balance": 128947 }, + { "peer_id": "EiAXmwRH8HdwKPDz7APhYqzVTxhoPUpdKmwSM5tPPkRm7Q==", "token_balance": 128947 }, + { "peer_id": "EiCrTZ/hTLYMdmQsS5mdScLKoGJTMoZ3F/U4GRxfB9gguA==", "token_balance": 128947 }, + { "peer_id": "EiAkWA+OKVn1Ajrp0evPyr/0rZD3doU4WmBkY4CtxzVYVw==", "token_balance": 128947 }, + { "peer_id": "EiCXvAsfWvjDuhFloqV/WQitXhHabIRBZqOHR3d5rVSiLA==", "token_balance": 128947 }, + { "peer_id": "EiDugvOk+eWUfMCo6LaEZbCdu6an+96p63qlPEHULHUTCA==", "token_balance": 128947 }, + { "peer_id": "EiDr0x0Yvn1qTyqfzwKne/hAnosvKjk9OWfsnbFI8tB2Aw==", "token_balance": 120115 }, + { "peer_id": "EiD5JmPqKCdDs3RIhBgapI1Ie4S6lddJ4O/iiMPwYoyoGg==", "token_balance": 120115 }, + { "peer_id": "EiCoPeQ+leGveEM5P/TX0fdNuWTg7Dp6T7lQiXzDLhbw7w==", "token_balance": 120115 }, + { "peer_id": "EiDf9LzrOWpezMQjR9rOjU+ZabucZu/yCLkzpvTJAj3dDw==", "token_balance": 120115 }, + { "peer_id": "EiCefkAcM4ut0sZiT2f6aEaNtIZI2hhjzT/itbe6PIqkuw==", "token_balance": 107750 }, + { "peer_id": "EiA24CKWMo0F6Am5cHrzVooLkX5sO0eFb3L3+jq74qY3hQ==", "token_balance": 107750 }, + { "peer_id": "EiAwlQCRHAtudM+JuqRo7h6Spgcp689hQeYufdGOVx9QPg==", "token_balance": 107750 }, + { "peer_id": "EiBfXdvIsVFwKz5WVDS4v4DCp7RY7WqiIL2ZDTWDVEz+yw==", "token_balance": 107750 }, + { "peer_id": "EiB06ZbA3xXEqWhuZGTHPUL1v0LOC9XmTjGLZQSiECcJLg==", "token_balance": 107750 }, + { "peer_id": "EiBa9ClQNKr1YNREaaZn5e7IrA0XkoCur/YTFlCPufsWkQ==", "token_balance": 107750 }, + { "peer_id": "EiCuH4VWAQn9FbvJem/dmswYYfRW8A7H+5Q3URK5gNv+NA==", "token_balance": 107750 }, + { "peer_id": "EiAX3pvxWa621huB5lEUELwY5RYFCrRd1KToebTN3TAXMA==", "token_balance": 107750 }, + { "peer_id": "EiB2EikvzsEXIL0Ybe2ssRjnmiATWfw9jXpWP9cL90dydw==", "token_balance": 107750 }, + { "peer_id": "EiCTzrmo7Gqk6p9gpJJvgyIb/rc9JT1OdjojH6TMKSMf8w==", "token_balance": 107750 }, + { "peer_id": "EiB+FoAYEbJ0RVI9L1rjxelLzUrbaHP1aOn/SQBbuVTFrA==", "token_balance": 107750 }, + { "peer_id": "EiDDwaJAiOcjRggi9W/DS01gwitZ4plOthL+sV/F2WydQQ==", "token_balance": 107750 }, + { "peer_id": "EiBk3y1Uw8WXmUHDRxc9ouse00KC9Dr3czFOG4MTZnUhsQ==", "token_balance": 107750 }, + { "peer_id": "EiD9PRQM5Kjk4lA5SV0wvdDJfgZjFrHyN5sZxXVySLuu7w==", "token_balance": 107750 }, + { "peer_id": "EiDmQlALdT6m6Q8wZM16eZGLVB9pjTsZEWivEIn4pdkczw==", "token_balance": 107750 }, + { "peer_id": "EiDp29FXbAscbKZNcH99kEDP0PcGwW22TsMxnn1HK1D1Rw==", "token_balance": 107750 }, + { "peer_id": "EiDj1Bddcb+fXnm4W8HLMHjNqjtlbhWmfED/1Oha+w33Hw==", "token_balance": 107750 }, + { "peer_id": "EiAuMprA+CZlzE1ehX9O3M7Y+ootz05eCVaFVyD8Udi47Q==", "token_balance": 107750 }, + { "peer_id": "EiDoyelvLUjWjyL343FNLw4b1v+6Loyo3YIJuG6Agb+TfA==", "token_balance": 107750 }, + { "peer_id": "EiAm0kOxey9yhwCN/J5WsfIm8fXRZuAX7gx4dWz5OgguKg==", "token_balance": 107750 }, + { "peer_id": "EiDVWI/eL4QK9rtZIRyqa36nRNaCroGY/8yX2Ex3q+4SQw==", "token_balance": 107750 }, + { "peer_id": "EiCUnLSqUJpeIkKltKHECTUl84MDnZtko0WTRh5OqCmO6A==", "token_balance": 107750 }, + { "peer_id": "EiDSP7Mw4TRSBmuF9OOEAXtcK//RTzJLWw3xwuVLQijRVQ==", "token_balance": 107750 }, + { "peer_id": "EiB5FLxFycijk+kbjUCLVN+u/LMdwvEHbiA8UmseaRJUDA==", "token_balance": 107750 }, + { "peer_id": "EiALqqXPhdT+LWlx2cbx3vYiFGxeUhv+KgyhE+MT8g7fLg==", "token_balance": 107750 }, + { "peer_id": "EiAQctJosQjmEukddnIjsML9I8IVPEG9AXnwIgSOwS1tag==", "token_balance": 107750 }, + { "peer_id": "EiB1uhvNx1vVURYaDgyjtKh/kAvKDTA+pizs9Ki1WK/nGg==", "token_balance": 107750 }, + { "peer_id": "EiBe+uGO+zfmX3+O+o4ZMG2FJYj8WNpF/3x8WIG/lpvr2Q==", "token_balance": 61824 }, + { "peer_id": "EiCB9chm8KaN8pZymFIdkbNZo0mi92Td9zwzZ3AoKrO79A==", "token_balance": 61824 }, + { "peer_id": "EiAPtMZC5vXTmvJp3tHtDWN//mufykfif5ytS+lM1xxMwA==", "token_balance": 61824 }, + { "peer_id": "EiD+FBGriVFqbWsGufY4M/7fljQ6Qczaa+ukYopboNnw0Q==", "token_balance": 61824 }, + { "peer_id": "EiAuD4e+ZRXsFJGh05UNVsqLQJQaaJ/E1PQZN6ElMB4Nnw==", "token_balance": 61824 }, + { "peer_id": "EiDTB6eObT+GA+cXvO/5j98NVdqQcU9wyBJ+/vSKIumj4A==", "token_balance": 61824 }, + { "peer_id": "EiDca75Qx2Lr6s/Lrbl9O6Z72bivN/mhpX/ckh8rwAK+fA==", "token_balance": 61824 }, + { "peer_id": "EiBvZAiuQHUUkZGpqjuOpwriYtQZwGArCN5E9NsoEEWu2Q==", "token_balance": 61824 }, + { "peer_id": "EiDRpjDjicKK6CO8udUAjJgAGqyCUNXVzAUa9PV/lnkenA==", "token_balance": 61824 }, + { "peer_id": "EiCwOvw+44dQw+xdnOoPehvrg3b/PNFBStXpRzj3t9+J8g==", "token_balance": 61824 }, + { "peer_id": "EiA3l0p54keWgYXyLjbyM6OIstASS0qMdA/8q/CCnvqIZA==", "token_balance": 61824 }, + { "peer_id": "EiCSeatYJDXNTeO5WPM7M8B8ts5SG+KPpda8MntmQAhVAQ==", "token_balance": 61824 }, + { "peer_id": "EiDs483tDwR8UI6GN9TlFNh2/BsrGKRIUJxXLJLKf9Sx2Q==", "token_balance": 61824 }, + { "peer_id": "EiDNkWoJFRRDalKXKC04bPXXg1UxEkGpRj/Dx7+02Z+ptA==", "token_balance": 61824 }, + { "peer_id": "EiCqTs19rOr56QlWFibxPrsgUwl8cWXzDxTmyE83dm+p/Q==", "token_balance": 61824 }, + { "peer_id": "EiB0lN+j8cxNve80vwVj+UnzYC5lkn5AzQxb1AitNkoiaA==", "token_balance": 61824 }, + { "peer_id": "EiCYihN/GnRlHD7rTEie7A5yPyYgFbLgY9mAvxz8gB2lrg==", "token_balance": 61824 }, + { "peer_id": "EiDY6YKI85tRvI+P3PlJ0YZovmCcAVQQHQOMZKZ/Y7hbsA==", "token_balance": 61824 }, + { "peer_id": "EiAk1nTaG+sv0sTHLdoB9v11oH1BmyGIZjMYOLQTLhF8KA==", "token_balance": 61824 }, + { "peer_id": "EiD24dCVyfK2SQFtCr0v+cb7m/R8fz0aSqi3MFdkG0heiw==", "token_balance": 61824 }, + { "peer_id": "EiB7yaEqAc4bQdJIxKNh7xpk86m3P6xgFWua7Y5mRBIaPg==", "token_balance": 61824 }, + { "peer_id": "EiC53Ud7eV3p75zwuoDzkkXdmZPbyz127GYJkNptKauLfw==", "token_balance": 61824 }, + { "peer_id": "EiCb/z7SCCYAb6ZRnj9pmDH/quyo9jJgUg22NNEYa/FgZQ==", "token_balance": 61824 }, + { "peer_id": "EiACcSxsFxdFF5tYlWtEJzu4phV0xY9nvT04UWxuzgaDJQ==", "token_balance": 61824 }, + { "peer_id": "EiCLgtRnsVkmApnbIuS4rKWuhHO/TNKb8RuMtMK/+RE4ow==", "token_balance": 61824 }, + { "peer_id": "EiAa5nZen9aFIi24iHtZ/Ex3lmQ2EPCmkeCPDFMwiOcNMQ==", "token_balance": 61824 }, + { "peer_id": "EiCujlXgMAztpSmJQXfEie+4VsQQ5ycaeyp1BWk4zkkfvA==", "token_balance": 61824 }, + { "peer_id": "EiAFPSGu47UX5mK1WZub/cyz+Eqzz/kHojG+EObqOT+Tig==", "token_balance": 61824 }, + { "peer_id": "EiCS2kZI7/gne4GKvMrZ2pZ2M2dvdOQT1SonV/VcHTL6rw==", "token_balance": 61824 }, + { "peer_id": "EiA+C/aDS3W3hp7Q8NU9KQrTk+1JSdVgOQUuZhBU1iQEvQ==", "token_balance": 61824 }, + { "peer_id": "EiCeW9mbI6XPL/+u3SCSBP/vnh3Y+04Nqa5j2aQu/es6Bw==", "token_balance": 61824 }, + { "peer_id": "EiDfDAJkh33TLXipy070sZFs6gkawKYOktAiMJzAlY3dHQ==", "token_balance": 61824 }, + { "peer_id": "EiB8E50DyoPLYs31IOyU5TVjEAHi9fQaLFKpkc6/ybGycg==", "token_balance": 61824 }, + { "peer_id": "EiDbjh0elVYk86eG92sl594syNz+T4kmRUOfGg5OT6jIBw==", "token_balance": 61824 }, + { "peer_id": "EiASb36QWr/w2bfUVBAdeyvJdae6VG6ymGHn1F6ThMqOXg==", "token_balance": 61824 }, + { "peer_id": "EiAOMF0CzwdP71hBZX85HrI/7EL3B0V5R1cQSt6P9tWVHQ==", "token_balance": 61824 }, + { "peer_id": "EiC1km3lus0lSHxQHZBITkn27ALaloZ+tm5wm4eZUQp05w==", "token_balance": 61824 }, + { "peer_id": "EiBn1Q9Yyf/9wN0DNHqi1BXtFnr8MO1rx/GKCBP9SIcD5w==", "token_balance": 61824 }, + { "peer_id": "EiD/aXrgArqCjMprLcySsatTjG3lmN3Vpm21ERGBapnbCQ==", "token_balance": 61824 }, + { "peer_id": "EiBGo3UaRFtHxngQqLNRhkDOEaWs5mNe1ZsliC5H9N0+FA==", "token_balance": 61824 }, + { "peer_id": "EiD4SkT81EXr+ZYU8aEtP5IAbLYRb4hfN4CWIbgoMa15Ew==", "token_balance": 61824 }, + { "peer_id": "EiDgOMRPpkwnokWMhfIulVCx5iRI/ZtVtkvGifd7oplQqA==", "token_balance": 61824 }, + { "peer_id": "EiAOU1Vo85qO+op2b8nHbDxEoQB1PiugGmU6oM1xIxGfIA==", "token_balance": 61824 }, + { "peer_id": "EiDUwdzfroP/KA3GUUhaS+FAUnN7St87xdg2aRbCxPYgBA==", "token_balance": 61824 }, + { "peer_id": "EiBH4XPQlGcXC4Tp64lv/dXntJwHAiwfiyd5KbiCALchpg==", "token_balance": 61824 }, + { "peer_id": "EiDuLC/SyTv5wbp/jsRjHXQz3ME2r65RMBoULwq8GOj8gA==", "token_balance": 61824 }, + { "peer_id": "EiC7FJ83dVoH+KhxBsRiWCOvvmOmgcbOykqjIJZGuMWtQw==", "token_balance": 61824 }, + { "peer_id": "EiCrnRmoeqytLLo2t7gFXN4DoXgDcW5gGxsdfx1NxiDPfQ==", "token_balance": 61824 }, + { "peer_id": "EiAUPveUbHBELb34KirMKvDmYFpM6gu7Y+7RFWGqt5G4TQ==", "token_balance": 61824 }, + { "peer_id": "EiBLmdy419du6Cr3FJ2JJ+pJknek6vvVBquFr6Wof24aqg==", "token_balance": 61824 }, + { "peer_id": "EiDet0IKnZSHTrc5runAi0s4fPU66sCrfVnC3YrKxl6FvQ==", "token_balance": 61824 }, + { "peer_id": "EiB8YNNT+f32fQyo5Yb2AZlj5djjuqohs2rS16847nfo7w==", "token_balance": 61824 }, + { "peer_id": "EiBfRtaMHINkOraGjpoveo0xGmXVIiTkcLPY3+s5zCSTBg==", "token_balance": 61824 }, + { "peer_id": "EiBzobVhRUY6NmHFQphTXzSVwGec4Dh6RO5hExcLzrurJA==", "token_balance": 61824 }, + { "peer_id": "EiCgn1324zpvTbmxl+4IkM7QsODIaApQ7szZowWWowBd+Q==", "token_balance": 61824 }, + { "peer_id": "EiBmytP4g2gix8OkjbEFrvPl9lr1KyW6SSjuo1duWg8mcw==", "token_balance": 61824 }, + { "peer_id": "EiBw6ZcCRqk6jnsin0txNjOXU33pycAv0dfvno2slS1orQ==", "token_balance": 61824 }, + { "peer_id": "EiB+pcJzbDvkZt9wFeDouWA0A+uWg1EoiNdkRCjTUshjvg==", "token_balance": 61824 }, + { "peer_id": "EiC0Z7QzQ7+LJMQDShs/t50tSSpgdBLHEVnIi37TCrcI/Q==", "token_balance": 61824 }, + { "peer_id": "EiDIhtoFJIXlDLxa30nAlL1xfVEsPer1PZPtL+OfImXoTQ==", "token_balance": 61824 }, + { "peer_id": "EiDqCjFGTEahSbLOKlHJhBWegH0Qp+ulT6OId6U9UlN82g==", "token_balance": 61824 }, + { "peer_id": "EiCnoPVKUzy4ckmKh29PTYa5YNLEFk7dSJp8jkjDfldZlA==", "token_balance": 61824 }, + { "peer_id": "EiAeijjugEKMsWJK2S4k9ZsdxL2AtNoo3yiu9yzgCUYljw==", "token_balance": 61824 }, + { "peer_id": "EiCgBkybsxAJev8LDuGaHFo+Y/0oXalNGwyNU8bn3dZ2EA==", "token_balance": 61824 }, + { "peer_id": "EiDI04gf7fFIdMgy48woEvhL37qjCLBoXO4FRx9SYjchkw==", "token_balance": 61824 }, + { "peer_id": "EiDXKrbqTGUCD+Sc+vK6dffF2hPyqrb24ZNYdnuGWlZJ0A==", "token_balance": 61824 }, + { "peer_id": "EiAuW/4ekqiqvUQ9uAuW8MmlFab0hGUzed7XYA547uNFVQ==", "token_balance": 61824 }, + { "peer_id": "EiCAisdo4y1a8hhfbTW57cf1mO1B0ozIjWFWBl/Rpuc8NQ==", "token_balance": 61824 }, + { "peer_id": "EiAD7hV/sysaahbEZvB1wgQ0qSZ7TJeCQpTEKJQp8e7gRA==", "token_balance": 61824 }, + { "peer_id": "EiAe5RkC1xrO2dLHNKRDfVMPnfQdqkuoFiSUEwq7/kqt7w==", "token_balance": 61824 }, + { "peer_id": "EiBf6AAXBRJNIlo4EVlCowR+GrkS1xtOZqfFDwZrbdj76A==", "token_balance": 61824 }, + { "peer_id": "EiAf6JNp6EJ+slqf6gFNccTE5Weaje/7mH9AC65B/l0XDA==", "token_balance": 61824 }, + { "peer_id": "EiDVQtE9e+nVS3KNnlT+K6I+8yyUbWgOCQ+i7q7ygQQd3A==", "token_balance": 61824 }, + { "peer_id": "EiB6VIK2jYp6H3TaNNvfekSteOtOtDTBR0ouuw08cz8goQ==", "token_balance": 61824 }, + { "peer_id": "EiBsKhzoSZX3OfW7JkaQ4SMhG4OFLQC/dLVS8UdqKRl8iw==", "token_balance": 61824 }, + { "peer_id": "EiBbyy7XmY+K1a7KZ3B1Rhqv8N5QycrRBi+SIMoTd1gCAw==", "token_balance": 61824 }, + { "peer_id": "EiDxKveI0vntWs5k2Gbkp7Jz1gsOWmM965JIkPIfybYM0g==", "token_balance": 61824 }, + { "peer_id": "EiCanoHsHQfIYBX2m61Lx8sQIJgfQR5kwisnqIzqjgoKJA==", "token_balance": 61824 }, + { "peer_id": "EiAm6wr+C8ONNrhJ/C03Urtr6ZymsqwUrQWcFO2sqehWFQ==", "token_balance": 61824 }, + { "peer_id": "EiBKuUHD5fLHHw0zbGDoSnudE0cK9MYqWgIlztDit86Cfw==", "token_balance": 61824 }, + { "peer_id": "EiD4l58piCN43cC082ods0+ZU14ztKJA3oJFYDDdxbZUmw==", "token_balance": 61824 }, + { "peer_id": "EiAv5VroWTdm+bfqpZP/GJNFBsZ4w7yX9MwjzNuk8VrDow==", "token_balance": 61824 }, + { "peer_id": "EiBpnRvySi87aP7LRpnZzTUORl+Xd3hXS+xm7HUHqWMPNQ==", "token_balance": 61824 }, + { "peer_id": "EiCvMFaIEmqxfeWqEIc8wupEbN9/UkgKzGpJp8Y7DSX1Pg==", "token_balance": 61824 }, + { "peer_id": "EiATuJlAk0T59YjAfRW/QU/r2TWsQ25HQs6ufKaKnbjsXg==", "token_balance": 61824 }, + { "peer_id": "EiCq8997oUfbEIBjahCrhxh7H31zVfdNkFkj+BDhhxaYQQ==", "token_balance": 61824 }, + { "peer_id": "EiAuGeUeyMApmQHbnLJNJsAKPshU1DGMZTVYEyVYNgoZyg==", "token_balance": 61824 }, + { "peer_id": "EiADsA5N8dze0U/Uo3KcuIlE4jYpVWNlfb9MkilY3kq7tw==", "token_balance": 61824 }, + { "peer_id": "EiB540GDYq60zLwqV7CKQn0+wN/Pdg67xnwZzFOhoh/2FA==", "token_balance": 61824 }, + { "peer_id": "EiAFs3PzeiYao4UnwO0+fBRE1sQfm3WJu3K0KD9wQyU4gA==", "token_balance": 61824 }, + { "peer_id": "EiBxWRcB+i8yYk6JsfjgWsiumDlx1d0rirWObGy+WrgSQw==", "token_balance": 61824 }, + { "peer_id": "EiC6Qc4m9Av+L6T3p2vtwzKhi9IQphqMxdjfjBjzeZAfNA==", "token_balance": 61824 }, + { "peer_id": "EiBWHq49B6I2twbr4ojxEMZ1zsPL3KWFVPz4zAs9pcNCMw==", "token_balance": 61824 }, + { "peer_id": "EiB++1x6xUHvHycpWOBnSyLQJoZI2phig50z+5M/pKAR4A==", "token_balance": 61824 }, + { "peer_id": "EiCtob6JlVawBXqHImckijCy8f3Ge7WNpRDAXCOlTRGuDg==", "token_balance": 61824 }, + { "peer_id": "EiB/Jdh3vbXBDgM3WppnUuct25thmE+eZDpv3yX491D2eg==", "token_balance": 61824 }, + { "peer_id": "EiBac8yDlhETCRXuEjqOBNwaR6VdAsN8Ysz3wd+Iw4WdPQ==", "token_balance": 61824 }, + { "peer_id": "EiDo/GPoHDZ30pnJ59vjSE0+SzcYIeR8lKknAQhzpuytJQ==", "token_balance": 61824 }, + { "peer_id": "EiDodcIFaqlsODm+b2HhJAFL/mAxmixJOVVbWWb+zbetSg==", "token_balance": 61824 }, + { "peer_id": "EiAxlPx8NRUo/Sz/EFfVkIhfa8W906wIz5KuDdpwT7lT+g==", "token_balance": 61824 }, + { "peer_id": "EiB8ev89g0spull/cBised4VxH7ny3cF6cDy9kqnLCvU2Q==", "token_balance": 61824 }, + { "peer_id": "EiC/nVmFcGmGT1CYY93X+PSTki2bLYhpG9y1U/8xxsz6tQ==", "token_balance": 61824 }, + { "peer_id": "EiCsdBs8fE2TDzQBj7hiTo8bJMmHyD3yt5H6D4CN0V99hg==", "token_balance": 61824 }, + { "peer_id": "EiA4yAXe96tr27C0bnyRu2nkBThd6f8s+5OH+WNbVx1Wjg==", "token_balance": 61824 }, + { "peer_id": "EiAcF3iqG1YDBQskroraFGPmgPkQeEToC0PeS0+/4Fxg8g==", "token_balance": 61824 }, + { "peer_id": "EiA8TCBqYy/w8Bkis0Lc9VEgcxntOIoxKFgoEEI9obruZg==", "token_balance": 61824 }, + { "peer_id": "EiCafNjTZ6ikwsNN6IHbT0d8V86sHGPnRsFAXe36xCwUUg==", "token_balance": 61824 }, + { "peer_id": "EiDQ9b0Td2Q61QD5PpXP5oa16wxJc9UDIlPJ4w64ZNvqYw==", "token_balance": 61824 }, + { "peer_id": "EiCHFUr7q08y7RpdkeL8mXLsnGA8M06r3kd9nBUTprDVtQ==", "token_balance": 61824 }, + { "peer_id": "EiDAX0wjwdc/SVHLDZN/LxXHHEN9ErUP9wbmHCU7ptrINA==", "token_balance": 61824 }, + { "peer_id": "EiAB8GYGS0bYBxwtxHRjfk1+NjCjo0qCe8zgKcvrwpGYkw==", "token_balance": 61824 }, + { "peer_id": "EiBBaL8rpEK4BzwxcWfBEq2OrEAoHTPymv2uwDYHqbfmRQ==", "token_balance": 61824 }, + { "peer_id": "EiA47GEI33UVHQEetz4Kao7xelFsR5nUMwDIamPxqHvxVA==", "token_balance": 61824 }, + { "peer_id": "EiB0ONqncERGkOWujrOSliQzQXTZBtdbypJ+Pmo8+5SqBA==", "token_balance": 61824 }, + { "peer_id": "EiCIOoud54jxO9jx390end9EIQaPZuqtKsY/F8WnFSmJfA==", "token_balance": 61824 }, + { "peer_id": "EiDnSgQZSMRH0M/Ezf5gypb/QNrbLkdcWC8GyCnpLVZwzg==", "token_balance": 61824 }, + { "peer_id": "EiAptsYmq/a5Icp9tR1AeMP3hxlpFZkOEq/k0BxMSW0wsw==", "token_balance": 61824 }, + { "peer_id": "EiCupmS1ciQafdBAooHOmPSZYGRIqWX1BVGmIz+xhDKN7A==", "token_balance": 61824 }, + { "peer_id": "EiCvpniatiKdJDnEoQeCrBJ5/Ojru5/uOjQQN/Vz1iCmzA==", "token_balance": 61824 }, + { "peer_id": "EiB+qjdMyYs5OIO/Sh//4EHHbfWEizaEGFFLc+w6A/NsYQ==", "token_balance": 61824 }, + { "peer_id": "EiCAK916o+OcyYV/7hCUiFVSfX3MCletSrzSHEMAgAvstQ==", "token_balance": 61824 }, + { "peer_id": "EiC+M2tOkhL/A40NYkgLxmI0/aSzC6A3gLc3wFF1EwQamw==", "token_balance": 61824 }, + { "peer_id": "EiA/akLerobM52O/J17SIMN2BKis+CPamCHdEVrEnCtcGw==", "token_balance": 33561 }, + { "peer_id": "EiA/iZh3KK5VxH9cc3iPp9A8NVZf97E+nbBiiSnvrx++Vg==", "token_balance": 33561 }, + { "peer_id": "EiA0740dd4FBfTVQ5B8XvGYVlyRR0JO/5y0NPkYDTlhx+w==", "token_balance": 33561 }, + { "peer_id": "EiA0HZzbMv0NIfY3mhHxbkuiOybc9eTgZ205Rvmt0LqEtg==", "token_balance": 33561 }, + { "peer_id": "EiA0TrhWj0QVgt6KeqEKuji+TYYGpMD3IuAaKbiTVRHpww==", "token_balance": 33561 }, + { "peer_id": "EiA2baZcR3a8El5GY/I+UPfTqG60Gac55TMZLccPKzFmDw==", "token_balance": 33561 }, + { "peer_id": "EiA5AGZ8I4EIf1nMgc/FBkI/4LjQcXv73DlD+fNVtOkq0Q==", "token_balance": 33561 }, + { "peer_id": "EiA6l1cD3nprm93KF9Y8KfPkogh2bqs7bx8rOZy23TP2kg==", "token_balance": 33561 }, + { "peer_id": "EiA7Pz+t1mGSvtRFLWarCW/okdx1CIOIIFG9+D13uWX6Jw==", "token_balance": 33561 }, + { "peer_id": "EiA9btruVye7Cpf0WiIda67ZKqgPL4YLWC+ABDY0rpFrbg==", "token_balance": 33561 }, + { "peer_id": "EiA9kSxfO2PvQAqguGxEOHX5tyrMBOSwwJQgZ/g/h97Dpg==", "token_balance": 33561 }, + { "peer_id": "EiA9vOx1NGXZZXUP906t9G8u1fFV2xH+o29kbPXvSnA5RA==", "token_balance": 33561 }, + { "peer_id": "EiAa3HCAVFV973qGJZEEg3lmgys5cKXH6G7HVx0f9DmBIA==", "token_balance": 33561 }, + { "peer_id": "EiAAhlHx5OR6cLBYnnpX5nXs4Y8k9RjPJrhzf3cHQb2xkg==", "token_balance": 33561 }, + { "peer_id": "EiAAmnX4CodVLPBwcvIEs1Oqcj6ApANp2xYPHdjea50jRw==", "token_balance": 33561 }, + { "peer_id": "EiAAPW1G0+5S+z8nfgFHySiKbsX9CT2+13kcLfRhYalkww==", "token_balance": 33561 }, + { "peer_id": "EiAaxxb4RlJ74mvFyGacvhRjJreBEKTykeYDwBsGmrabOw==", "token_balance": 33561 }, + { "peer_id": "EiAb6kSbftKbXZ8qumYjHn+wxCNY6RtC2wDX0/e1fPDEYg==", "token_balance": 33561 }, + { "peer_id": "EiAbbDQeJ71MCaOfNpqQIF4hJEfxJOolOf3Halfg7pORaw==", "token_balance": 33561 }, + { "peer_id": "EiABQRGzduIk5aWq5hkALpsJdx3MJBDv8fRHoUyyOqjomQ==", "token_balance": 33561 }, + { "peer_id": "EiAbtdBmWtDs1u6iUBlIt+Y5v6Sj8b7fPxfQ0mqisU0lMQ==", "token_balance": 33561 }, + { "peer_id": "EiAbwwA4wpLCEukt6vg4EiRiP/HI4OS+cxRi+WpdIDtBag==", "token_balance": 33561 }, + { "peer_id": "EiAc2s3k+s+MEeaoQ8ANiH28dSxWbbdLic4W9mMJ9M3PeQ==", "token_balance": 33561 }, + { "peer_id": "EiACz7owFX2P2m1qMsODZuLXRLd9fMc83WWf7cHFJMmqHw==", "token_balance": 33561 }, + { "peer_id": "EiADAfzHMYcg5UUGt0sIwVmJDkiiSM7kK/r7n2a3vh1Guw==", "token_balance": 33561 }, + { "peer_id": "EiAdOK/lq4G7SqVxEHZE9Jv6CWfHoK5a1vwfbm4O+knyGA==", "token_balance": 33561 }, + { "peer_id": "EiAdS9WLuPm1c5BU+Irw/Lc31UhYmddscB80aExVsyu9ZQ==", "token_balance": 33561 }, + { "peer_id": "EiAF8Kmvn7JtUwHLtej153fVHzwOpzA9lbL/ShtMRISbjw==", "token_balance": 33561 }, + { "peer_id": "EiAFb9b1iasILaveYLBq+dudCxXZrZ5mYWpqMTDPLP5SjQ==", "token_balance": 33561 }, + { "peer_id": "EiAFLa9OqY3DgnYxvAyUjrcFw/vbnTYXxz1VB28kUhrSdQ==", "token_balance": 33561 }, + { "peer_id": "EiAfqrHIIo2+Dr1aaQVcmeQ/Z0W9PlnCZoZQgRCHi0ue6Q==", "token_balance": 33561 }, + { "peer_id": "EiAfvR+3gMSl1rRb06UjW3r4BnqBmKScDq0SwcWnaHFK4w==", "token_balance": 33561 }, + { "peer_id": "EiAGi/THEGK8Xnj+T8j5Ts/E1Tr/gde8GDR4YkU8T9OYRw==", "token_balance": 33561 }, + { "peer_id": "EiAhUaHSoj3BKUUnvVg9abdHuq63Tm0qWIN1nX2xmyCV3Q==", "token_balance": 33561 }, + { "peer_id": "EiAHwIbKCjbm+BmxOEarNRsTtbh1H8/I9lWC6PunWpFGhw==", "token_balance": 33561 }, + { "peer_id": "EiAISnKrs5ASNGb0s7xnZjrrCCWevPDOsd4lHv57J5gW2g==", "token_balance": 33561 }, + { "peer_id": "EiAJdvz9obduntJXgi8Q6bHHrn6Vzhqtb2Qq0n8aNgukUQ==", "token_balance": 33561 }, + { "peer_id": "EiAJS3i+shd9vHDEYbyeMVlZS0RQ7e8kGIGdZ+H1V1+7Wg==", "token_balance": 33561 }, + { "peer_id": "EiAl6DYRNqRMtEKbPs4RQR0IBo1zqfM6QqVsORU5hXCIHA==", "token_balance": 33561 }, + { "peer_id": "EiAlE+Y2efamrIMiM8ykoVS373b9JfRyIUYs9MepXryPyQ==", "token_balance": 33561 }, + { "peer_id": "EiAmbSKaIkxs4INgf+P28hu1U8ZllazNtkhbZzlua6kb7Q==", "token_balance": 33561 }, + { "peer_id": "EiAMjLq1fHta0P+DZ8Lj1qiR6mT1TyWf/OEdHltrViFlpA==", "token_balance": 33561 }, + { "peer_id": "EiAmX3zdnKnKmdFHWEgYVvABA6QqCBCovXoqIF7kFmN3dg==", "token_balance": 33561 }, + { "peer_id": "EiAnTKU212B/Yb64J7v14ZSIioBqbPQXog6PvSGLOE/msA==", "token_balance": 33561 }, + { "peer_id": "EiAO+aV6/Czp74D2m6SI+x0wwtL7lAwIULzf2z+ObV0ydg==", "token_balance": 33561 }, + { "peer_id": "EiAobcBgASGBqo7kpINLnNmwv+v383h/MA2x/V58c7tQPA==", "token_balance": 33561 }, + { "peer_id": "EiAoO4D+jjdejWYA160gIaeJNpowr724X6/4cRKMqOYCJQ==", "token_balance": 33561 }, + { "peer_id": "EiAoyXeMEGhqze7b7QVVS7Tx1xfOEqtfi8LoVbwgs/WHrQ==", "token_balance": 33561 }, + { "peer_id": "EiAp0+gEXAkzJoOuBJMgndElivLXDR3smEwUTl0lYhz1RQ==", "token_balance": 33561 }, + { "peer_id": "EiAp60bHplMpPtpLEndCFc86K+wdGmej8jNK7x6L3/Zn8Q==", "token_balance": 33561 }, + { "peer_id": "EiAp6WcyGNk8adu4zRBt9tsEIErFtxdLB95icnuay68UVw==", "token_balance": 33561 }, + { "peer_id": "EiAq+eHz1CKq+c6gkJdz1Q6bIutfH8uyVjq5PRelTGn1TA==", "token_balance": 33561 }, + { "peer_id": "EiAqIi/Ohox19hK3Q57dwCjTGBcUSpprh8JS5CBggd/OAA==", "token_balance": 33561 }, + { "peer_id": "EiAQZ+QgJ5m59tuoSE1l/ieEwNlz6Po7223pYwmnDXGKIA==", "token_balance": 33561 }, + { "peer_id": "EiAR+iLt2TonvFdwsPc+CnhggVMyLJgsUTDv3uP5ViTFww==", "token_balance": 33561 }, + { "peer_id": "EiArIQvipFwtI6XERgNx0pqGjoZ1MX56hYRu8CdpulsZ8w==", "token_balance": 33561 }, + { "peer_id": "EiArK2bArvm4MYl7py3z2Xp3fBxe1ma0tEkuQR8zzPUskQ==", "token_balance": 33561 }, + { "peer_id": "EiAsfjCQGZLXjDJche5we8SDpT7++fH/NTlb8CjSYkMrjQ==", "token_balance": 33561 }, + { "peer_id": "EiCwv4d6oHK+EYLz3vx5bQe75I7Ps0LYrqNpSdw076vHiw==", "token_balance": 33561 }, + { "peer_id": "EiASJpXnYpnbh9KqA1ejjNrx548nvzREhhggyl+jnqkpng==", "token_balance": 33561 }, + { "peer_id": "EiAsydKWNZRjiSX2NMERO0JKVO8SSr9GqhviqQQG+MbS1g==", "token_balance": 33561 }, + { "peer_id": "EiATfXiLdhvhFAm3qzjqrjQc8ayLjCs0hFE5rCWXkTDsUg==", "token_balance": 33561 }, + { "peer_id": "EiAtMEKlsUwrSbsube1d6cJllBLsNq1j+iH56W1OnV7lmQ==", "token_balance": 33561 }, + { "peer_id": "EiATulgo3nHfYPQp0djiFlyhL8c9ZirZ4NSGTbTwI6dR4A==", "token_balance": 33561 }, + { "peer_id": "EiAVMJPE3xobrvDBSYrWYyFIESnB3aedgb1xQNhdvuYZrQ==", "token_balance": 33561 }, + { "peer_id": "EiAWtOu8NzFPJesBacHDG2E3WISh2LWNjuZAHnXd6Cvg3Q==", "token_balance": 33561 }, + { "peer_id": "EiAXFu4gdiCHtG/A6VZdtCywJ5Lplh6APHZfMSrHdjix0Q==", "token_balance": 33561 }, + { "peer_id": "EiAYIsoULoY6bIekM+Yqa5hJYuKhPSVy5+2bGUIaUkx1AA==", "token_balance": 33561 }, + { "peer_id": "EiAyj7Dl3rlh2z6B6gACIJv5IjLCm7gJSk/2D9H5VFOTUw==", "token_balance": 33561 }, + { "peer_id": "EiAZB+lRay451UHeCVksj03K0Zk4rGJo4efSL7mULpeh4A==", "token_balance": 33561 }, + { "peer_id": "EiAzguJnszJo+cNDo40ainUoR0lvSoPG4LT9LCXmUyMGJQ==", "token_balance": 33561 }, + { "peer_id": "EiB+0U1jaQW/33m05dxQ/S0OYnItEB1OMG+NwUyAH0PVxQ==", "token_balance": 33561 }, + { "peer_id": "EiB/8kobVeqeFljr2nMP0n5vDfod0S9WRzrsSFv9nkUpTg==", "token_balance": 33561 }, + { "peer_id": "EiB0bwtUxtSyFavpehB/DKEZSzjpg2P+l52DyTCGF88cxg==", "token_balance": 33561 }, + { "peer_id": "EiB0qeqkS1VR2xWI83HMr+ey9ZnMz/M1hQfsalpvrOAZeA==", "token_balance": 33561 }, + { "peer_id": "EiB1a1SKljSPHmso4+EfG1lMSEdFS0Wl84xh2gUNC96Vgw==", "token_balance": 33561 }, + { "peer_id": "EiB3mW1+NHsFaEWr8BS2TBf3okZdEs7LTwEBGLOS9fJUuA==", "token_balance": 33561 }, + { "peer_id": "EiB4JHgXoUSx4r/REx5wHJFk1YNlUR4zkVthOrF2G3qaIg==", "token_balance": 33561 }, + { "peer_id": "EiB4Nxn4n9eYwxSTMRT3PmhUDVQclQwjx6WHpRd5OUB8nA==", "token_balance": 33561 }, + { "peer_id": "EiB5JUEuZV7aXoTuF3DK2h+Rl6qhuq/pgQwh9p3ivjc+Dw==", "token_balance": 33561 }, + { "peer_id": "EiB5lMV8eGdxAq+nl3afjNspbuIOz0DbVcA7JAcXJpip6Q==", "token_balance": 33561 }, + { "peer_id": "EiB7mWD3Zu2E10et6LjGwn/i10hR/QzL7SR5JZ4h7AP4YQ==", "token_balance": 33561 }, + { "peer_id": "EiB7n66XHzp5X16+DiLjrIIoeliYqFUUzT2Nf57Haf1WMg==", "token_balance": 33561 }, + { "peer_id": "EiB8zyMv06OTwZn0HJB0yQh8YMKmaYr9IoT1hZlmT7blsA==", "token_balance": 33561 }, + { "peer_id": "EiBadGrAfFftqRwM/tSofGzpHbF1UEfgxcKCdPwP2pWcaw==", "token_balance": 33561 }, + { "peer_id": "EiBBWJR7DYIW2Ur6/3Lo8PtWruF40mkZsBd9pjhwvRmKBw==", "token_balance": 33561 }, + { "peer_id": "EiBC5VKF5YQfVSwPwKl2Ka+BnEquAYGT6PVBHzm1pzO2kA==", "token_balance": 33561 }, + { "peer_id": "EiBcmsS6EmEBO/YqVA9EtYDhMvSXiwTCbegWaAz7Ilggbg==", "token_balance": 33561 }, + { "peer_id": "EiBdAngPCp4mMyeT3/dp/tU5K8gRLkJbNRLpVBZVDOzrxQ==", "token_balance": 33561 }, + { "peer_id": "EiBDmeSPVzcneJHL3Lg4IG3FOY3hhhGQQHw/DBBDCiWDeA==", "token_balance": 33561 }, + { "peer_id": "EiBdnU3CGuhKS4i3CCOoXWUgd1pbP6BzssQyJ9uyPpsjoA==", "token_balance": 33561 }, + { "peer_id": "EiBDpkY2IAoq2X15Z1N7mzFsvvrkGK5Oo4Zptf64vWI30g==", "token_balance": 33561 }, + { "peer_id": "EiBDUFthmcCe+jlPcCUiTpfYdeL4mTseblHp49KH+HmX3g==", "token_balance": 33561 }, + { "peer_id": "EiBeh2Q4YiInxE24QXd7OCZWIufaP8WiIBVWHiMCN3HHTQ==", "token_balance": 33561 }, + { "peer_id": "EiBeKcob5o+L+Lc4xQcPYmayt3nmbDRiaXb+hcWAqeKYPA==", "token_balance": 33561 }, + { "peer_id": "EiBevqQOOZ+75Z3tQGS5UG9qHkUpg7ELZf6fD2t9PyqE/g==", "token_balance": 33561 }, + { "peer_id": "EiBeXZ7YqqTq+QKJig4FyCS+ozHPFJJ1yStJ0s8qnECP1g==", "token_balance": 33561 }, + { "peer_id": "EiBfcXJwnRrubRaLaNMbFCPEF4mIB1aeiMpEojbKDDiL1w==", "token_balance": 33561 }, + { "peer_id": "EiBfuLXHHGhdOu8zGgQF7QTHKuQga+BIizrhKplKvAMvRQ==", "token_balance": 33561 }, + { "peer_id": "EiBFXUH2G2KfAlodMJbXj2ox7QPw1jz5l0mMwICRO5Jo9g==", "token_balance": 33561 }, + { "peer_id": "EiBG4ll7Xd5GnZwnyoRD0CPPHwKI0XaVbXaDuBCwbuxetw==", "token_balance": 33561 }, + { "peer_id": "EiBGJELZMMnglR3l4CQF71ItkOzH042NnJ3Ms51Y4AJsTQ==", "token_balance": 33561 }, + { "peer_id": "EiBgpfYoiSVKy1WqQA0LNsRhHA6F1mkp4QvHWI2MU/UeZA==", "token_balance": 33561 }, + { "peer_id": "EiBgY6eN0MxGT3zLN7KzYnbIL2Sr5piDLyxrkbuPimeGAg==", "token_balance": 33561 }, + { "peer_id": "EiBh52+5wTVp60gTVCmREPZAiThINbKMs1NuT7fIOr2JnQ==", "token_balance": 33561 }, + { "peer_id": "EiBHeY4bA3u2JYRmjMoVIHJLZvK9QaLwsw1pbuD/kn8mnA==", "token_balance": 33561 }, + { "peer_id": "EiBHF2D1YFAMI/hjJAJ4B/y+PBhNafGEAxjccfJPishm+g==", "token_balance": 33561 }, + { "peer_id": "EiBhgKCiuKGNnM2Y1vwRLuIxAPR52monvCuG+qm3pv7pSQ==", "token_balance": 33561 }, + { "peer_id": "EiBHM3LPhhIhXX8oj7T1hJN7fBaKw30cn5zUMitxb8U6ZQ==", "token_balance": 33561 }, + { "peer_id": "EiBhM9eraY/qGeTnW+ZajQWMoOu3gBQ2z9fT0vM41gZwHQ==", "token_balance": 33561 }, + { "peer_id": "EiBhzc7WxJXbPS1IqD2KrtyOGq55aA1vSlSFaang1YTudQ==", "token_balance": 33561 }, + { "peer_id": "EiBi6d1AB30oohOQKpxTPuQcsCf9jAP+8es/m8xql+fxkg==", "token_balance": 33561 }, + { "peer_id": "EiBigEs+ON2j/hMhXl9ctg1oCyZiMIx7wTtYylx0Ou8wKA==", "token_balance": 33561 }, + { "peer_id": "EiBiMcu3TQx9OFyseXMW7Kta5qU9d6j1kfKJJHShymAkTw==", "token_balance": 33561 }, + { "peer_id": "EiBjNoymiVeNXeK8blTJrrpQEv0Pow9wNc1T5jufTpZzbw==", "token_balance": 33561 }, + { "peer_id": "EiBKaK+NyybyOJ8/ElLl4f833O0LuI9M7TPBoomWMSwOfQ==", "token_balance": 33561 }, + { "peer_id": "EiBm0As/9wBQb/u9j6++M3P/5Dh52/px0cLLMzgacwHF1g==", "token_balance": 33561 }, + { "peer_id": "EiBmkzxsNd2njEnsB2C9ZF6hSnkiFWgBV8jkb6zrboKR7A==", "token_balance": 33561 }, + { "peer_id": "EiBMXMuMJgVK9Reb9WzXszjSWIxWVNPOR7/wjmLWQuqgPw==", "token_balance": 33561 }, + { "peer_id": "EiBNK/q5mjOOGQonNkaOC/dvUOLMdQuT1FrvahlBzLNBZA==", "token_balance": 33561 }, + { "peer_id": "EiBNqPROwDeTq/wdw/wG+p77gdRxpQxYznJfDyy4PuPZ0w==", "token_balance": 33561 }, + { "peer_id": "EiBoyrGstUmmW796+68m315oYWHzkySxw6KQZhw/2QxjoA==", "token_balance": 33561 }, + { "peer_id": "EiBpCiEwzXp/IwAqBAm0aXquk+r38gkeX2oSwExq5FIkQg==", "token_balance": 33561 }, + { "peer_id": "EiBPilMWfKdMaHK3QUFo8te7MsJ28a/J99BdAwxTKf0tgQ==", "token_balance": 33561 }, + { "peer_id": "EiBpj2rICDPCIRRQ4X67hSXq/z1XOyi+VCztwwexkS3jDw==", "token_balance": 33561 }, + { "peer_id": "EiBPTmUxdd/X1LidP2a5919j9sNTJwmT559heKGcWUX+EA==", "token_balance": 33561 }, + { "peer_id": "EiBQ2y/H0VkSU5OvomqL47R6I+TsHpWouU0Sw4sXiyIUXA==", "token_balance": 33561 }, + { "peer_id": "EiBR9t0JKdnTBtp3FfjIwOQkNuTzEOwrDGBR67ir6O2Drw==", "token_balance": 33561 }, + { "peer_id": "EiBr9VmoOm+4IkBb6w1/Cs5YiECgcKSIFg15mWO3PXtxmw==", "token_balance": 33561 }, + { "peer_id": "EiBrA2jFA/vMnltH2CGgXtTHh6jkIsHU6PeiCH8el2O4tg==", "token_balance": 33561 }, + { "peer_id": "EiBRrIgSQDM/6Z33kSuomTRnaI2bhV/e9TYIR02hrBpKaQ==", "token_balance": 33561 }, + { "peer_id": "EiBrsy7y859RkDavqTzB7xu7d+812SgziYZp310Slx5aCg==", "token_balance": 33561 }, + { "peer_id": "EiBsmsQAsfN1HrPI1gwat4lSiV7gY7wgVj9Bv7Z6s4RQLQ==", "token_balance": 33561 }, + { "peer_id": "EiBsOLRx8jfcINZK/ssxgcwyVTrA4Qblw2j3glZ13bnOtA==", "token_balance": 33561 }, + { "peer_id": "EiBT8DnUHZSGwPCkT61r7dIrX30RXJ3ESnDZBTaRwR50kg==", "token_balance": 33561 }, + { "peer_id": "EiBTBk/X27/DClwVSAWjAFQV5OvcBC+sH8slItb84a3Kjg==", "token_balance": 33561 }, + { "peer_id": "EiBTeMZAt5ic7noc06AEJY8l6qScDHGtdNKWxH33mqcpbQ==", "token_balance": 33561 }, + { "peer_id": "EiBtGIu0h+64wjQVG4kQoxclXt6OCnG9JIuvXutYsPFJFw==", "token_balance": 33561 }, + { "peer_id": "EiBtYfZnWJu/Ar1oDXp0FM68VQDvBalJ9Cbq0E6RX7HOXQ==", "token_balance": 33561 }, + { "peer_id": "EiBu/WWMQHxaLsX1XyqYy25uAIE27G6iSKvM5itJ0rs9Kw==", "token_balance": 33561 }, + { "peer_id": "EiBUBgV0XJHRn3PmWtkISus5mlE5rB9vcUi51ZoYBNUm6Q==", "token_balance": 33561 }, + { "peer_id": "EiBUrVQqgIkfw+8jibFTiIRNbH7mSTtsW8HzEZmIeoZrUQ==", "token_balance": 33561 }, + { "peer_id": "EiBus/DIfBSaJGdwgK5Ooue1+jZ0ZVd8/9LtK1HNCqgx7w==", "token_balance": 33561 }, + { "peer_id": "EiBUyrjgnJtVbvfHI5kZTGiKvgvXuXWIGasC/nHWTYy0HQ==", "token_balance": 33561 }, + { "peer_id": "EiBVCf8/gq2S9gGLAKDAJF81I7uYcG4PfT3opAfBviSIIA==", "token_balance": 33561 }, + { "peer_id": "EiBWVKy5Ft92suGdvj4uynD025iyK1bHQZ+S2fKGDMdLkw==", "token_balance": 33561 }, + { "peer_id": "EiBx3EdbtPNcNViV6MR6LOq3lPJvwoArGzbpzxYzAzMG1g==", "token_balance": 33561 }, + { "peer_id": "EiBXDIUOsIXCVQHS+sW7B+Yg+1OudVthocHU0LTcDxZwRw==", "token_balance": 33561 }, + { "peer_id": "EiBxELcqLhVBUY7ZZyW/JNyTxdXzgYkzh51S4C8FBSl7Sw==", "token_balance": 33561 }, + { "peer_id": "EiBxgKPONqQVlIk5VqM4KVDeM6KSrbegG66Ncn4ySAXW5g==", "token_balance": 33561 }, + { "peer_id": "EiBXHNMT5mdoPQY2JfrEsu4amVv6Eggx2AEXrwXx/UHQ6g==", "token_balance": 33561 }, + { "peer_id": "EiBxlyWYOKw3ZQgyOxbDVINASSXczl4lAvccN33rO43JmQ==", "token_balance": 33561 }, + { "peer_id": "EiBXqH9N2MPXhqVE/5T/eCC/XYog8kiO5swlKMB4iMwrtQ==", "token_balance": 33561 }, + { "peer_id": "EiBXUarTiitJ2PEwqzo+l6AEld0m/W5qKw6pfFGZDSHWdg==", "token_balance": 33561 }, + { "peer_id": "EiBXXLcXOI3lI7+Yz3YpiAkeErANUw3zeVkMOHUM0DQvEw==", "token_balance": 33561 }, + { "peer_id": "EiByHO6dP93FY3FsuPVJ98BgFlXs03SaFk3w16GDKjJyuw==", "token_balance": 33561 }, + { "peer_id": "EiBypVSidPouWbIlwiVEgWDYhl0mXf870eIP/kBiCQo8oQ==", "token_balance": 33561 }, + { "peer_id": "EiBZzUZGBxNnfPFELzIy//rJmU0FpRk7Mu2aZv60wDGFBQ==", "token_balance": 33561 }, + { "peer_id": "EiC0WRkyHHxUQML+030JsEKoKMUzf0JbXGtl3oxW6K1kdw==", "token_balance": 33561 }, + { "peer_id": "EiC1l7XzOaZul+EnpRlNqicYIEQ3cIwe5FvAert0qvOxUA==", "token_balance": 33561 }, + { "peer_id": "EiC1RVPdNtjwIEH3ZON3OrVrFvOY1acugPS0KKpGO5ZVmQ==", "token_balance": 33561 }, + { "peer_id": "EiC1ST/PIv2uheK9Zt6HWU+PByWA7PaV2SForEClUDHEeQ==", "token_balance": 33561 }, + { "peer_id": "EiC2obFK5cPgQe/IuttjHqHYni8GsYsblYGTkQ4HvY6LZA==", "token_balance": 33561 }, + { "peer_id": "EiC484zYPCYAOQTc7I7BrpO+5plVVeW7HTjvmVqhQw6BHA==", "token_balance": 33561 }, + { "peer_id": "EiC4LgRgN6I8jvTES08ggVznv6CDpYQF7quw4TYVnsml4Q==", "token_balance": 33561 }, + { "peer_id": "EiC68ul1+gDOyG93CVMf3+TIxIRKodKmc/e5Yp4wZqJ+Mg==", "token_balance": 33561 }, + { "peer_id": "EiC7m7Q+geHtVjVqRKYTEqXcyru2IvSNyQXiLbwIVWVw0w==", "token_balance": 33561 }, + { "peer_id": "EiC8ZP9nLpSPeT7nTBYazd9V3RaDWoA4Ekziz0v+gHYRuQ==", "token_balance": 33561 }, + { "peer_id": "EiCagErmVTA8ytscM1QaWqFWRDC7mzQfBeUo3IqdCUnYsQ==", "token_balance": 33561 }, + { "peer_id": "EiCb34w0U9JqmsbKeYoJmhabt4FnNeydaq6Oqlvq1w5/fQ==", "token_balance": 33561 }, + { "peer_id": "EiCb9d8qxJ0iYDgNwO0gx8UIiJdikiIVUhcQrVsK1zGzCQ==", "token_balance": 33561 }, + { "peer_id": "EiCBHXr9Zls4Ov6VNntiy2gEKvb1mNjS2kO7zUhMRSmieQ==", "token_balance": 33561 }, + { "peer_id": "EiCE5oe7nQ7JpnNtSLXCMiHq/2SDuNY/FlIBdpFawDACCA==", "token_balance": 33561 }, + { "peer_id": "EiCEWTPV09cZrP87m0EdX2ZBNF3egeqEiwAMYz2ZYrgjhQ==", "token_balance": 33561 }, + { "peer_id": "EiCeXjEqt4hs/vmdpMsDVtlgnniNDzp9A/8z1O6PKDEukA==", "token_balance": 33561 }, + { "peer_id": "EiCfKRYgC0nEsbmFGY1nI3RGo9RCdzMZRLfQbMys9OAMsg==", "token_balance": 33561 }, + { "peer_id": "EiCFWus17WYdzOD7dcLAZlcPlfKDUy9QvRRYdg/YJBn48A==", "token_balance": 33561 }, + { "peer_id": "EiCg81scwxZyLkhMSgclSIVx6VKeoXMga4i3iMaKCMjUeg==", "token_balance": 33561 }, + { "peer_id": "EiCgOhJzjzGYHpwiqntXrfhuMnU1V6/MTZrAFjGpX+ydWA==", "token_balance": 33561 }, + { "peer_id": "EiCh0pcSGxXjRXf7OXcGg9wQGqQA/4DyrEh30XnEF6H4fg==", "token_balance": 33561 }, + { "peer_id": "EiCiC9dRImqGhqKABMUt8Hl81OpQwoxnJ7m0UASpH4lItg==", "token_balance": 33561 }, + { "peer_id": "EiCIm4dpeLlI+qLLHZ2Gsw6QKTM7JcxVobtsee/TTcWODw==", "token_balance": 33561 }, + { "peer_id": "EiCinhN5WURUo7iQv7zb7DYnzsMzij0sM15/shTTQ287DA==", "token_balance": 33561 }, + { "peer_id": "EiCiwC3yv3ePIt70lxe9PtWNhrwr7mXXBMJV+Xi6k/FKmA==", "token_balance": 33561 }, + { "peer_id": "EiCJ0rBmh6l4d2ygoaTxnbgqEX+CG6xdxKTa0MDnx2OC+g==", "token_balance": 33561 }, + { "peer_id": "EiCjCuorEu8LGrdJQyTzSsOPoKWbU1sGMTKENMDBZEkSWQ==", "token_balance": 33561 }, + { "peer_id": "EiCjL1Ek53nbh/WinVrH+kzmXRWOsthbobrTCzc29Jo7xQ==", "token_balance": 33561 }, + { "peer_id": "EiCjtvzkM8CqHvUHiQSzVjz/gSeg+2705f4/bZphUkf/NQ==", "token_balance": 33561 }, + { "peer_id": "EiCk5x+DdW2U38dgxiiQnSP/KB4rxn04lvdPJuubVrQdjw==", "token_balance": 33561 }, + { "peer_id": "EiCk7DYBnTkEz/UVj+X6AAZ66zrtDHPWQA8X2pah2hoccg==", "token_balance": 33561 }, + { "peer_id": "EiCl0cat1aSXx4nGYdOKNnvRFZQAYcqqDgtrh+g32j0uqw==", "token_balance": 33561 }, + { "peer_id": "EiClic3+AUmTDVqH8juQxGu1w9FEOf76bEBJW3l6J4S6cg==", "token_balance": 33561 }, + { "peer_id": "EiClV7TNuDYNywNzLpIxPTYsT9Gy2xlvNlBIIwgSMDYsbg==", "token_balance": 33561 }, + { "peer_id": "EiCNmcgNpLGN9Jwjyf/q7eCyDSA9U/Ba7RK7BX0kc/iOQw==", "token_balance": 33561 }, + { "peer_id": "EiCO9rhg1Rf8aFhOdBBxhXb4hOcXalPhFBJN1752/X+hdg==", "token_balance": 33561 }, + { "peer_id": "EiCOlhap9AJPpU8Bo2ZqhalrLyCtXfA9ta1kfPJ4QH4siw==", "token_balance": 33561 }, + { "peer_id": "EiCOnJABC9Nas3tONmczSklaxqkZDOaC+Pf4rEt4ufpu1A==", "token_balance": 33561 }, + { "peer_id": "EiCPE6wq/EFjgy8XntAioIOZQBtyBMuoJNdVmYRr25CIZQ==", "token_balance": 33561 }, + { "peer_id": "EiCq4Qqp06UxkbjKXHytu0yjUZLC1vXmYtNvydr3PMFj6w==", "token_balance": 33561 }, + { "peer_id": "EiCQDcloEnVyFTU04lxDTIP2XlJvlan0T8MCw/chwfZrSA==", "token_balance": 33561 }, + { "peer_id": "EiCqJwAtWrf4FJfj9GRsPSVaJ0p7OX5onfgrSURWeH8xIQ==", "token_balance": 33561 }, + { "peer_id": "EiCr1+4YvnxvquEKEVr8aZlkPdv9xPO1tvb5j/5EjjXnLQ==", "token_balance": 33561 }, + { "peer_id": "EiCRgIfoKLobbbSet+CIcoxpwmRGfZQmtexQGAL/fqParw==", "token_balance": 33561 }, + { "peer_id": "EiCSAdTRYWnxcQGQ9czd8RVql4W7unD+kCYz9/6WSjtmhw==", "token_balance": 33561 }, + { "peer_id": "EiCT+3Yt9UCVkwSbFxeJVvNUHgeqVVVO5QWeLnIGo9F7wA==", "token_balance": 33561 }, + { "peer_id": "EiCTa4639f/X5q+D74mSuC3QleLO5PMpCedo6bSnvhPveg==", "token_balance": 33561 }, + { "peer_id": "EiCTehwHrsswcYCnXB1PtjLhspaKVqU8rcCaD7crN0JKXQ==", "token_balance": 33561 }, + { "peer_id": "EiCtn8WO1aBlmjhXe+H1enGXJ4qq7owbchsgbClCRe6zEw==", "token_balance": 33561 }, + { "peer_id": "EiCV4gsPbqhe3dbMmrM9dY8rQRl0nSyzbD8bdFv7JVbB/Q==", "token_balance": 33561 }, + { "peer_id": "EiCvgCZr6YyWRGqiqEXv5ZABVwGyptZomlFkZINV68Uo5w==", "token_balance": 33561 }, + { "peer_id": "EiCw/Q8lTBPKAVWIoTdHzLTnngu/wZLlLnGONUAavDUdQQ==", "token_balance": 33561 }, + { "peer_id": "EiCWGm9hyulhgWc9FH9ejedN5kK0FlXCIYMV4JaNRh4o4g==", "token_balance": 33561 }, + { "peer_id": "EiCXGTygpITlerQe56QbBPQvH91IjalW5EM+iRKspD1NKA==", "token_balance": 33561 }, + { "peer_id": "EiCYE8yfBKAhsLg3c+nNsgZfuPkpqesC2MzEA7cGagZO0Q==", "token_balance": 33561 }, + { "peer_id": "EiCZ+QsNHYSeupXEwwpqZTcbmiVhs83YpXnqpAiZZN0QEg==", "token_balance": 33561 }, + { "peer_id": "EiD/WU32JObv7p8TukO4W1HTpbWFHPKMinRzt9XnHC3tJQ==", "token_balance": 33561 }, + { "peer_id": "EiD0PxxnNwV8tUqsqEL8u+tbFOO2/DgTam+E1iJyymECHA==", "token_balance": 33561 }, + { "peer_id": "EiD27CTm1hX5sfjJAp5aZQ3ZGQMJoKYjS6Ew08SLbWEroQ==", "token_balance": 33561 }, + { "peer_id": "EiD29GMnzfaDVuX0yzSkhdVKcUdklLReD0OO0J4rkA+ydA==", "token_balance": 33561 }, + { "peer_id": "EiD29YiZZBZnDL8O32tMrG7lfGmBEI1RJLVCposmEXrcNA==", "token_balance": 33561 }, + { "peer_id": "EiD3bLzoV0vdqAVD0B/lTSbKlV2qitlQuWf9TrFolf6kBg==", "token_balance": 33561 }, + { "peer_id": "EiD4EmSxter0Gv6sGLKSPgKQv3/e49vt9x5xgdOqyWFwWg==", "token_balance": 33561 }, + { "peer_id": "EiD5Bn9Ln6lI1zklAyIsM2+KR/fwyKL/rwV4YY3DuqjUew==", "token_balance": 33561 }, + { "peer_id": "EiD6/Q3HhVJWC9tNL+2kTroT/vRsNFv8nIKuAYRDZXStoA==", "token_balance": 33561 }, + { "peer_id": "EiD60ZIpO3Lzd+mmDUqxWOxD2aJCuENTdDudWcyrCehOzQ==", "token_balance": 33561 }, + { "peer_id": "EiD66rvM4SOZimHZwjosNyaL966f6109BLPuhkxGOYl1Lw==", "token_balance": 33561 }, + { "peer_id": "EiD66WAy4F6v0gXPFAZq1DIbejNT/PiHtO9cZmSgzAba6A==", "token_balance": 33561 }, + { "peer_id": "EiD7HhMlv0I+DO7LgaCXldw77czuyU3MWDX0E73e4DHQMw==", "token_balance": 33561 }, + { "peer_id": "EiD7KNmH7BJGvmXs5U2ULcLfEvjckhuzZyAgfQ4iLBsIOg==", "token_balance": 33561 }, + { "peer_id": "EiD7YoR7BoOlrDkV6SDbSZ5R1XxhAYpTXf9ZZkc0JywXNQ==", "token_balance": 33561 }, + { "peer_id": "EiD9W4Gx3C2Ir0lV2G22pbOX6w/pl5U5Xj8/QqwpmypuwQ==", "token_balance": 33561 }, + { "peer_id": "EiDA6nzZspLd9A2WXA86Wos45y3/6hauVJMJpWVVRhKMpQ==", "token_balance": 33561 }, + { "peer_id": "EiDAbSr47sbAy8IWtA7l43iYx8KcsXC/4UVmf9IYFYZXXA==", "token_balance": 33561 }, + { "peer_id": "EiDAKQjpZjFKd+e2xpv+zWxXlJtzRRJVIQln8Q/sJeV2FA==", "token_balance": 33561 }, + { "peer_id": "EiDB0azciKU6nhYyVmQnhdiESyemEKSBAyC+0v1Lxk6vXQ==", "token_balance": 33561 }, + { "peer_id": "EiDBeD7AHN9N8lDgqekN5xMCgu6AG26BkizF+3KlK8//SQ==", "token_balance": 33561 }, + { "peer_id": "EiDbPN8P4wl9UftHptd6nID1mHVaUn1pjH9RaUbULQfM1g==", "token_balance": 33561 }, + { "peer_id": "EiDC3Qg9wxNXBNXUg4etRoXcDZ77zs4sha2EjDFGjkBs3A==", "token_balance": 33561 }, + { "peer_id": "EiDc4eGssM/nIMWdlRQYFN+I+FKkTiqsyLeAmMI7u+DqGQ==", "token_balance": 33561 }, + { "peer_id": "EiDdhCrFlFXziz3Ks2EP4aGA+baZH+aSq88HFF/tHLegDg==", "token_balance": 33561 }, + { "peer_id": "EiDdJ2VBhCLCdxy5W0uRM7DQu1N+u34uS2mmzhx3Gi98JA==", "token_balance": 33561 }, + { "peer_id": "EiDe9W1f6gSofLBD292UzOL/a7y5ve9nro04d7P1TT+xmw==", "token_balance": 33561 }, + { "peer_id": "EiDeqYbTSJxcKTK8OVrTV5Zfh9hfEV6yzL+DJ35qyPUyNg==", "token_balance": 33561 }, + { "peer_id": "EiDfIbOJWhFFdAtd7Ypr2JbiN/Fdv0+7O9Yp1ceLezOLrw==", "token_balance": 33561 }, + { "peer_id": "EiDGdp6wRE2Vr3H6jMTc9hw7vsW0ekIjhySU+oxLHHIDcw==", "token_balance": 33561 }, + { "peer_id": "EiDGFcew84luMddwYdBRUrau8wUFqBfVmXXkeCa/yzT+jg==", "token_balance": 33561 }, + { "peer_id": "EiDgIQPTwxPNU+sxzBYH852WdNaWzCP8N8Q4HzY3sf88VA==", "token_balance": 33561 }, + { "peer_id": "EiDglUe1WsMkRs1bf1h8Ul+wEOnLUmih9S6FeE6aDWTGhw==", "token_balance": 33561 }, + { "peer_id": "EiDGu0ifK+mUbzKnHimQhDzO5V5xiF9pWry/gvIySPE8ow==", "token_balance": 33561 }, + { "peer_id": "EiDHRYbkf4lnWOKiySMj+Sz0bN0R7gckauP9rI277lYJNw==", "token_balance": 33561 }, + { "peer_id": "EiDJux9BM5SomowX1NTnYFBFSPOokE41C6DJ9Is864CvKw==", "token_balance": 33561 }, + { "peer_id": "EiDK1hOcfGrOh5UO4Pw+yYDv2MRs7aQoj+Znatddm1zN7Q==", "token_balance": 33561 }, + { "peer_id": "EiDK4CxNrEUn2moWMgEqIK/XqQozt4EJbS34ffrs7P/ItQ==", "token_balance": 33561 }, + { "peer_id": "EiDLAOrrGUnhrsjHAZMdqEmAlMdhyQw821xbs5c3t88EiA==", "token_balance": 33561 }, + { "peer_id": "EiDmgHCG4xZhd2Ks/t07nFDXARlzV/+N3yexBUlNTmqnrg==", "token_balance": 33561 }, + { "peer_id": "EiDMixUJXTtGcRyxwE+uM5UlfsxED4NuI4LVI95oxAm4BQ==", "token_balance": 33561 }, + { "peer_id": "EiDMY7IXiZB3issJiG8wX8Lb9Iku9i3fVV6dqqVFjBvz4Q==", "token_balance": 33561 }, + { "peer_id": "EiDn2Q10/Hym961SYwzfVmbp1uEfueMmiqZk4mlNOzuvbA==", "token_balance": 33561 }, + { "peer_id": "EiDnYFVX8Rr3ctyH9Nz0rlDXfwc+U8WQKeXoK7k43rP0UQ==", "token_balance": 33561 }, + { "peer_id": "EiDOn1yXt4t1bOLMBM+Lh6Pm4JBpKP6K3lV2oPMtEbmjkg==", "token_balance": 33561 }, + { "peer_id": "EiDOPuCY+XHn+deTf7FScuJSPK7uRN7bLekSMGSY0SXgiw==", "token_balance": 33561 }, + { "peer_id": "EiDoQskpaCTOEuP1QuawoVdewVHlc24Nxv1IHmXcjvbXgA==", "token_balance": 33561 }, + { "peer_id": "EiDOsXWYGeFSQCNs8jzysDXMpwhRCnfTiRdM7vFxw2tOPQ==", "token_balance": 33561 }, + { "peer_id": "EiDpbxb5eRcbqsvgLSLcTphknlUUmZg7E9sDz1wftKxCUw==", "token_balance": 33561 }, + { "peer_id": "EiDPiFiPckjEifgudwkmcfVueFX//ZS4t8LTq69N4kKIrQ==", "token_balance": 33561 }, + { "peer_id": "EiDpk+sQ42fIHEPCXjKswQVnq7XVQ/ySn3kfCg6tDUzBfA==", "token_balance": 33561 }, + { "peer_id": "EiDQKKqh6xsXAGdT9P29p+kcVobboAnwZt77bnLiuRal/Q==", "token_balance": 33561 }, + { "peer_id": "EiDRoqSJBW18n+qGu1RBMDsL2Sglkz3YGteYixIpRORvpw==", "token_balance": 33561 }, + { "peer_id": "EiDrVC3vX1ahb38Q5GJz9u7osC4EimbyhEFd6x23AijPqQ==", "token_balance": 33561 }, + { "peer_id": "EiDsI9lx0jiJ5L9Q6CWsnhnZfP0kCQ7Pvq6g7WdcQhyhew==", "token_balance": 33561 }, + { "peer_id": "EiDsSAzu5rDFHenTqj1GdXQw+mVUMDoMvWLeQKdK546F5A==", "token_balance": 33561 }, + { "peer_id": "EiDT/3ugdOvxgb4vg6x5CGFMf4js65U55aWKCMwK14AJGw==", "token_balance": 33561 }, + { "peer_id": "EiDtAtMfou/MhHrQNuCAKY/nExJGhUqPHy6q4s+kGzkTbA==", "token_balance": 33561 }, + { "peer_id": "EiDTxgd6YQr8q720CBjgBVHn8RH7cm8V4vp/TcPZznBR8g==", "token_balance": 33561 }, + { "peer_id": "EiDUUpBTQqf4O9avsTWT04PGDAwSjluLCXYrlZrmcnIAzg==", "token_balance": 33561 }, + { "peer_id": "EiDUxwzOEhSqfmr1I+gKAjOHOiRIzT5Lxi6JZdTEbn+9ZA==", "token_balance": 33561 }, + { "peer_id": "EiDvKwaMeYr3wtknCAbVV7S6lbEVbLKxKsdydZr7vAJLRw==", "token_balance": 33561 }, + { "peer_id": "EiDvlFO6IxfAJGQFHQQ1KNlFRFgGdnVCXf3QO42fz0O7NQ==", "token_balance": 33561 }, + { "peer_id": "EiDVRXtI2+zXpUC0tNe7h284vUwSN7xUde43hxUx5KiD1g==", "token_balance": 33561 }, + { "peer_id": "EiDwQWP9U75DFY1t1wRFbp/U+cj9l/UaTLY+MuGBYsLLDA==", "token_balance": 33561 }, + { "peer_id": "EiDX/W0a43KqHcx3jmbzO8SQxNHUi6+j9dq7F3Nzxy3vCA==", "token_balance": 33561 }, + { "peer_id": "EiDyedbXzqeXwwQyirQTKae/E5WQNhzKYf56KRP7xFyysA==", "token_balance": 33561 }, + { "peer_id": "EiDYOZL8lMNDKPiV3i86wbTjQitdNHW2VW0cy3gFJ0axgQ==", "token_balance": 33561 }, + { "peer_id": "EiDYt8fxGG/ULy6eX/t2kZxp8f16taumsvEX55JSgTJtwQ==", "token_balance": 33561 }, + { "peer_id": "EiDz/eTYn8bsQW0nD6iDSUAVd/ZQuUOy3CQewg2IQQx3vw==", "token_balance": 33561 }, + { "peer_id": "EiDZ06PQVTfdNqed8CeAK1Iv2u2UmlWqEo83OYfrblE/6w==", "token_balance": 33561 }, + { "peer_id": "EiDz0lmuI58BkBDLD744KQ04rbHFoG9mynSN8zzmQOjypA==", "token_balance": 33561 }, + { "peer_id": "EiDZGlv2zmXBgSz5r0a0GgUzyhcgfnkRHn3uj656cQL2KQ==", "token_balance": 33561 }, + { "peer_id": "EiDZnFHAENyupayig0aaBuEpD9LI/l/MarlPFvOXx1uYBA==", "token_balance": 33561 }, + { "peer_id": "EiCCL8IIBky56tp+bZIx0NHono36QgPVhoVFnbXXimZpjg==", "token_balance": 33561 }, + { "peer_id": "EiB3vW1YNxiqw2q6W0Qszof/Jv3BLvJvTNKRAzvlirfm0A==", "token_balance": 33561 }, + { "peer_id": "EiDiOz2nMrxqGBqsck8CcamwMsnZY8NlhXbl0IoqX0UZ4A==", "token_balance": 33561 }, + { "peer_id": "EiCJQgxkDhPQ7lBR9HYet5wKumBPl6YKCLThRcEo1BRPvg==", "token_balance": 33561 }, + { "peer_id": "EiAvwrixOsPLPWD06V7YvZbgHYFoCj8aTX/4V6RNIWSeFQ==", "token_balance": 33561 }, + { "peer_id": "EiA0uBCHhPvWzprp9sUCHiGBsR8cR6FGeiY2EP0w0yfKDg==", "token_balance": 33561 }, + { "peer_id": "EiBGXBHH7axfRHYhZIx03raLDe2kBgznSYRTNRbv0nrZqg==", "token_balance": 33561 }, + { "peer_id": "EiBZYHxq3m+Hmx6MxQD5r2NPjkHQ6IJXvEKEz6GSbsNsrw==", "token_balance": 33561 }, + { "peer_id": "EiAm45a2yURP5fS/qhslXzx/jbHyoJ+aYlh9JB4mZPKenA==", "token_balance": 33561 }, + { "peer_id": "EiC5wYjJ8IIGWXiNSRLJ55er/hUv++PoN6i9DgXq7aq+wg==", "token_balance": 33561 }, + { "peer_id": "EiA+p5KYXfEdSFVw9BhsgGQQVX9XyxOeRpmKI8guCf0Qwg==", "token_balance": 14131 }, + { "peer_id": "EiA2LTxEOq+aY3oXqW6II4rxMs++I3m6b1F/385DazbhAQ==", "token_balance": 14131 }, + { "peer_id": "EiA3iH1DhRv2Yq1foZJvJrSkBxnJpyBHgPWOKOzrSXlywA==", "token_balance": 14131 }, + { "peer_id": "EiA79Q7Re/ov7XBzR0oxeJgrPKzWlTt0/2deGNrvLSowbg==", "token_balance": 14131 }, + { "peer_id": "EiA7C6zF40t7xoAjwePQjrCfIdh1t+1hmU510igs0hV6kw==", "token_balance": 14131 }, + { "peer_id": "EiA8pL87yzOUyMDx1FzIM4muQv6Jpaa2CQ3PdGEFSdNpGw==", "token_balance": 14131 }, + { "peer_id": "EiAACXXXzU9DAQrN4gaGVgl4b216C/AH9hZxNv7eyhjazA==", "token_balance": 14131 }, + { "peer_id": "EiAahlQqT3/UlFvc1p+ww8S8S7gbK14Dyde50sK8hTINIw==", "token_balance": 14131 }, + { "peer_id": "EiAbO5mcT16RxhiZOozqX6rM3jVswTWBTOkq3QbQKgN8Yw==", "token_balance": 14131 }, + { "peer_id": "EiAcWp157pz7+CWI3VR2kWexFjndqceZkDJmaEnRYK2Eyg==", "token_balance": 14131 }, + { "peer_id": "EiAeCRrFuX5C/kdZYTVfriE1e3cQSQTKOEdnb62SCCKxag==", "token_balance": 14131 }, + { "peer_id": "EiAfa1YbG6suWnaDdeKePJDtcCdKn7sOotk+6evEaYGSVA==", "token_balance": 14131 }, + { "peer_id": "EiAgo0D0ewh2ozj2o4w6eifg47bAqg/aASPnODHq5DbJsw==", "token_balance": 14131 }, + { "peer_id": "EiAGpy+7f6G8KJO90sZhEwtms2pb/uV6y46FrGOAfLWciA==", "token_balance": 14131 }, + { "peer_id": "EiAh3ml2qwcOLuYmLNkCzdoqhKQubSGodc5c52TX7O5FIw==", "token_balance": 14131 }, + { "peer_id": "EiAIOvtUvZbLmyuACX8m3iVXBlWcbTeDe/qvT/Z/8WEoIQ==", "token_balance": 14131 }, + { "peer_id": "EiAJ05XAk9ozclScMhUY0JjMScIoltFIRnr9S2+FlFuU7Q==", "token_balance": 14131 }, + { "peer_id": "EiAJUiqPKU7Mtzv1uNUo2Eb1dLQH1AQXHCs9Y/mdE8LZFw==", "token_balance": 14131 }, + { "peer_id": "EiAKHUO4r8n420rLFpwpPGXvhnyjPmE/I6XU0UXLNkF8QA==", "token_balance": 14131 }, + { "peer_id": "EiAkmBx/IweHwGUFtS+wTJee30lN/IjD0WKaoezBzXTtYQ==", "token_balance": 14131 }, + { "peer_id": "EiAl1/Bp+0eIMrQQHz/Cq70g4LAqCPFm9pL8fXjynTDWIg==", "token_balance": 14131 }, + { "peer_id": "EiAlVBObQQZQlXJog9KHjcwCNn6My7QPDeUrQ5jhczFKJQ==", "token_balance": 14131 }, + { "peer_id": "EiAmclu9uZtgv3s7OKK1RgSZthUzSHfgKp30/RpB2sVsYA==", "token_balance": 14131 }, + { "peer_id": "EiAmwRgulhRQZ3k7ZfMzwfQu01svPWc0mRZUfzisyp2eXg==", "token_balance": 14131 }, + { "peer_id": "EiANWjxJzSc613LL8OuVeGuoCGsUAH1ffsyAzIXf/amqeg==", "token_balance": 14131 }, + { "peer_id": "EiAogWRRTBr2swYNDUDx8pkbW3fbSFV5XLzvyHbSP74mUA==", "token_balance": 14131 }, + { "peer_id": "EiAoW+AnTP/1jG2nGDcKEky5yLjj10rxOK/cS4VcHWHTgg==", "token_balance": 14131 }, + { "peer_id": "EiAP80CvojfPuRBG+VNJxSFonvhgrY8bflDWEsHuDlP3Jw==", "token_balance": 14131 }, + { "peer_id": "EiAPjOb8we+ocPGyciL5pgUFjLQEo4seVztvbI5qDowpIg==", "token_balance": 14131 }, + { "peer_id": "EiAq/9aanJWTgxULhlycv44eYd2sT/OSFkXYnQPVE70XKw==", "token_balance": 14131 }, + { "peer_id": "EiAq9OQhcZTIEVWNUMkAgAIdOyVyK9Pb0GoF6JzMtfz8ow==", "token_balance": 14131 }, + { "peer_id": "EiAr7LQ1goFdkwaYnLWZZnL7pnatm4rr5SDff8XU/k4fTQ==", "token_balance": 14131 }, + { "peer_id": "EiARCleCxEfq9N1oHMEGiEwm6M75QixANqXQGEIAsN8iyg==", "token_balance": 14131 }, + { "peer_id": "EiArYOBCuzHjKaOvc16KZWSaK02j+0z/ezXwS4I5tnSySA==", "token_balance": 14131 }, + { "peer_id": "EiATomioP0BdxT8l6xtqM1pidOs6XIkAFn3PnvnjjTgYlg==", "token_balance": 14131 }, + { "peer_id": "EiAv/HAyL9Flsw3UOBQaRiHbYvKn7u2SbZwfZz+Gwk88fA==", "token_balance": 14131 }, + { "peer_id": "EiAveS8oMO93ID3JT1eEgjUF3as/DpOZ2U/fY4TY3sClJg==", "token_balance": 14131 }, + { "peer_id": "EiAzALXcTZtECDgNMn7EbHYeGdFqumYukgPuvlEtY9Hszg==", "token_balance": 14131 }, + { "peer_id": "EiAZl/e92G7m7+MWa/q/CCliixw/LgSjRLMf+EoXJo2A2g==", "token_balance": 14131 }, + { "peer_id": "EiB104ofyZgg4SbNV1hbCxrupU2wBlA9RAyFL182vf7dpQ==", "token_balance": 14131 }, + { "peer_id": "EiB44PpKKYsov6QTsEL8qg4xjdfWmseav2cEq1wQF2q45w==", "token_balance": 14131 }, + { "peer_id": "EiB4CLe1iirwASrbNR2u1SIM53xaqJNpaaWnEpVLqFNf5A==", "token_balance": 14131 }, + { "peer_id": "EiB4SQlsGRD1xkWRy0zHa0wvrBlIUVmOGsownttpWHM8SA==", "token_balance": 14131 }, + { "peer_id": "EiB5Mzanmx9YS9nPK4c4FmMZULvJVydieWVRLTa2KbUmjA==", "token_balance": 14131 }, + { "peer_id": "EiB7dQs9O4zOkLS7TzjUNfxquzcoIezZ91YAg8DkzK4heA==", "token_balance": 14131 }, + { "peer_id": "EiB7DY3BGfeQ7My3lzZv2ZrGj2UFn5a7UC8n3dQdAguwcg==", "token_balance": 14131 }, + { "peer_id": "EiB7h+COLmrKzVuh6revYAeuGf0NuP62L7qTaTi3Pp4W2Q==", "token_balance": 14131 }, + { "peer_id": "EiB93kvIpMxjMQQNEWCmxoKeM5XD+2Lqqk8OJHYsZNCGMw==", "token_balance": 14131 }, + { "peer_id": "EiBABy5T0sFNCLuoLjNULkXJlLstpo13RsPvuAIKCiYQPA==", "token_balance": 14131 }, + { "peer_id": "EiBaFhAiwplT1pJf3uxKMcspM07osb3dOdwGSJk11rTR1Q==", "token_balance": 14131 }, + { "peer_id": "EiBaKSSnCi/ZfokmQApSYtAi4xvxbT5B263CkJqAt7YSAQ==", "token_balance": 14131 }, + { "peer_id": "EiBaKt+mklDecWvS1m17WSFlb1JjJDA6uXBcMRIr2NTz/w==", "token_balance": 14131 }, + { "peer_id": "EiBaUNq3wdeadsrrfv7UeOMCCnb+ilJ8b9rez8BxnXFDlQ==", "token_balance": 14131 }, + { "peer_id": "EiBavPp4YwTyr9/3pCeWOBVySprBSsr3n2U5u/0nmQ4bxg==", "token_balance": 14131 }, + { "peer_id": "EiBEi4r/vpOjt9Wp5obXZdHQoLTFoJNZutbtxdpEp6USPQ==", "token_balance": 14131 }, + { "peer_id": "EiBFJEoAMl9Y2wD1es0PIxJeo9zVVblq2yCrSAXEMs6ecw==", "token_balance": 14131 }, + { "peer_id": "EiBFNztvBF1fiJdeRZvqvwPykbvDJejXrak1Ex5+U+lu3g==", "token_balance": 14131 }, + { "peer_id": "EiBg0bACYD3Hqn6pi6K8lX7Q0z1d9BY7+qQuCgVao84yzg==", "token_balance": 14131 }, + { "peer_id": "EiBIQXASyYPnVMmSrQcchsWtsf/woOYQXfF6pYex/3IxkA==", "token_balance": 14131 }, + { "peer_id": "EiBiX3Di7eSOIV15fJn9MQa/2rk4t33vV5oP52Bz8qjzyA==", "token_balance": 14131 }, + { "peer_id": "EiBJQSjehIVAEQPADz1EPnq+/YtndF/gzfHhnAo6/Ytn3w==", "token_balance": 14131 }, + { "peer_id": "EiBkd2IgkaS1fd/3sHQc2XeM93gueoQPymSJnpy62PdhDQ==", "token_balance": 14131 }, + { "peer_id": "EiBlbDATmftg5CHxy9uQ95hOmBa+dQMEfrCT6ax6V55H3g==", "token_balance": 14131 }, + { "peer_id": "EiBNwIGR5eqXMXoFkg/Co0tiHXg/4G8i721v6yqFPynNLg==", "token_balance": 14131 }, + { "peer_id": "EiBo1ySjJJFfzv+E0OJTfG3Bc/vr03X7YU7bb6kIFsRTRA==", "token_balance": 14131 }, + { "peer_id": "EiBP+L50XDFFnSpAqLEpmOd78BNyXkBm3+1hMWRYWv5Tzg==", "token_balance": 14131 }, + { "peer_id": "EiBPSC7f1vRRRHH5oy1xCTKjSglLpRDDJxbFhBswHXKtNw==", "token_balance": 14131 }, + { "peer_id": "EiBR8FV0T0jpAuxBZCI6B+91USsfOb0l/3HVB0i+gnN3Wg==", "token_balance": 14131 }, + { "peer_id": "EiBRRGlEPmJifZ0MMoy1Utw8Q7o2UA8BKDRIYA6bx097GQ==", "token_balance": 14131 }, + { "peer_id": "EiBSMFZZ4TJffoXAwdUBGSnR+Um/G9ZCrrAiS8du2+ChKQ==", "token_balance": 14131 }, + { "peer_id": "EiBSRkj61j5iARkjIsvoRDtqbuiFXJAZei4TdQvLF7oEHA==", "token_balance": 14131 }, + { "peer_id": "EiBULRje4iGOFbRskmsV3hDFb0JxHIiAwqacthUkd+qJgA==", "token_balance": 14131 }, + { "peer_id": "EiBVLbCAfL1PI281LA2x+3zRtljiwMZomOA+uMuGRRvHnw==", "token_balance": 14131 }, + { "peer_id": "EiBwCm57KOKTW3KojF5hcYr5uny0gLOufZCQ9z29OXevgA==", "token_balance": 14131 }, + { "peer_id": "EiBWkUrOeYF9fBjgYjM1pL/d0F2ecDEUKJ3MwtK7nz2ihw==", "token_balance": 14131 }, + { "peer_id": "EiBX2BFQK4mDRNTJkQZi4t0AsAj0+t6YvVgSA62Cr1lLgA==", "token_balance": 14131 }, + { "peer_id": "EiBXXiIu7TEJZ8iubowxgHX2SXps2hvcoz7UOvU86yEZtw==", "token_balance": 14131 }, + { "peer_id": "EiBz/9XZy8GzqQAMSsh33WH7gSl9BjCe2CCosxuqL3kEyA==", "token_balance": 14131 }, + { "peer_id": "EiBZDzjcmWq411H6xhuYjPm2UinwNQPnjtqAcvacV6KTxg==", "token_balance": 14131 }, + { "peer_id": "EiBZrZpZEbVlmhzUnPUY/TmmgTXDFukYbxUe1fDFBobN2A==", "token_balance": 14131 }, + { "peer_id": "EiC/WW9wiPF6oV4P61KF+x63PP7bxUYKKn7KdJtduxpSOQ==", "token_balance": 14131 }, + { "peer_id": "EiC5PG8ELRDfFs57o/Lzo3wbl/0sUv+jfFysqgT+f4zF9g==", "token_balance": 14131 }, + { "peer_id": "EiC61EfGsE1TxPfXQIu8sIo8agpOIag5OWzuxy78So8p5g==", "token_balance": 14131 }, + { "peer_id": "EiC6Eqpi4B1w02f/sMTzCK6yza1FfrLcfajM1roAXwBW4g==", "token_balance": 14131 }, + { "peer_id": "EiC8PpB0i1w/qlTfAjSkF+71BpioCFJKx83PeRbj2dMGkA==", "token_balance": 14131 }, + { "peer_id": "EiCAKalEIOVZll5qTaJkoOORPvlvWy4dliRkvU4DGkTBPw==", "token_balance": 14131 }, + { "peer_id": "EiCAm8vtd0n4znyA2/wvOm0z6b1Np+Da0CILLlpKNDTVLQ==", "token_balance": 14131 }, + { "peer_id": "EiCbENzmjAe1NBaulfMVwu8WeIw4EsTZ1y9Wb9jM8aACvw==", "token_balance": 14131 }, + { "peer_id": "EiCBoUdPb0/A7SwR3PGVoS1B1NE+A3JschDYrrznjP/2/w==", "token_balance": 14131 }, + { "peer_id": "EiCEIc+lI/0h1nwaHM+jwyIwdNv9DuTwzsi9vXSYkvFGzQ==", "token_balance": 14131 }, + { "peer_id": "EiCEJTohv96gw6sui4NezA/5zS3DShpHcS3yrT0ifXBITg==", "token_balance": 14131 }, + { "peer_id": "EiCFAQ8TKGZxMN4SNekcYJRHGBIOXKbkVFRr2ScCd7msOg==", "token_balance": 14131 }, + { "peer_id": "EiCfOozM8WwgvzAOuhH4QTSqtzVjYYFolT9J3zBC4Vuo+A==", "token_balance": 14131 }, + { "peer_id": "EiCFTKONzz79nk8pq4Cx8w4Rj9lRLUQSUuqGJPKyR5eLQQ==", "token_balance": 14131 }, + { "peer_id": "EiCgA2vgV5TkNbB1BCDvfvCHwoN77QnBdLYVKbB+iucROQ==", "token_balance": 14131 }, + { "peer_id": "EiChBy5gzOvUBJGjlGwu8D0AOdyRTnZxkWf6xj3wtdfoLg==", "token_balance": 14131 }, + { "peer_id": "EiCHt1daeYFg3cqqGco15WbsDvK3/EMX7iSW0l3cG934Ig==", "token_balance": 14131 }, + { "peer_id": "EiChTVF5cYU31FtNQN4pi5TbcYZbl9Mm4vvgRX3WXkBRpg==", "token_balance": 14131 }, + { "peer_id": "EiChuk1RuNAGMf/jsHBaevQqu1Lc6CQgEnIUVc6+a9MfZw==", "token_balance": 14131 }, + { "peer_id": "EiCJIIdUQWygzHxU4N3eipgAYfA6oWME2+zeprOjHDuG+g==", "token_balance": 14131 }, + { "peer_id": "EiCKA61pUxmFcMaKmn/ykOD8UUPeun96ATttwdb96eP7iQ==", "token_balance": 14131 }, + { "peer_id": "EiCKFxs1XPwcYROzEWcBx3cm+6sAUXD4ytcHrvKfO1ecEA==", "token_balance": 14131 }, + { "peer_id": "EiCMU3UKFEm09ry84SM+cJWd6tQIVKp4a2FsDYv4KueM4w==", "token_balance": 14131 }, + { "peer_id": "EiCMUwONUM+HZjbSJogaFwryZUqEhVZfOOtsxfc15595dw==", "token_balance": 14131 }, + { "peer_id": "EiCP8Tvwyr0X13ErQ08wwJfQDQlIHJf36xcp36daZax84A==", "token_balance": 14131 }, + { "peer_id": "EiCPEA5+vd9bWr35axyuo+aGl8sPNzuWmuYqiDbLsKmq8A==", "token_balance": 14131 }, + { "peer_id": "EiCpXjiJFcyuQDSo9bHvZaN0lysbBqSyQdPL0ncRWjgqMg==", "token_balance": 14131 }, + { "peer_id": "EiCqm2uoj682Zm9wLC34QS6PZzT8MK+1VouN2uS7ZynLyA==", "token_balance": 14131 }, + { "peer_id": "EiCSiwM6vnVlcBve5sx5A31GJVpCW3LkTtgyLtcVZHSckg==", "token_balance": 14131 }, + { "peer_id": "EiCttygf7ST4sHqshAHusQazOBFVIAOPxQGv/dUwKmYDyQ==", "token_balance": 14131 }, + { "peer_id": "EiCU3/SNW2GUnF9j2m/AqEKvSlbj89RCIUe61hb+aaI4SA==", "token_balance": 14131 }, + { "peer_id": "EiCWe4C+eEuLmkui1FCgMHzw8dVCXG3zNRLm3COkdKxE0Q==", "token_balance": 14131 }, + { "peer_id": "EiCyDcq6dy4ZHHMo+S/zsxZbVF1J8qv/hkHjQBXtChj/pg==", "token_balance": 14131 }, + { "peer_id": "EiD/sK801+gJVhriWwPInV8K7yB0OVCqWQaShpZy+JMy4g==", "token_balance": 14131 }, + { "peer_id": "EiD2WIIwy5DcHuhy8jAG7xrvB00aB4dNJEYuv/6Fs4X7Fg==", "token_balance": 14131 }, + { "peer_id": "EiD38tGZ8118CmVi324OI6HjiOdiZfoKpQ0DM6QroSLGmA==", "token_balance": 14131 }, + { "peer_id": "EiD5C+It9Fi5ae81Bud8egyByPyYHJNMDBGLMKR8h8pCrA==", "token_balance": 14131 }, + { "peer_id": "EiD8qqXLTPxTS0tChKYkxtrYIiOs4YTxA0EcM3KfFYlCBg==", "token_balance": 14131 }, + { "peer_id": "EiDB7gTrbys+YCIGXqohCrJgSIKKhLkuIQ4Lt3XgcwUiYQ==", "token_balance": 14131 }, + { "peer_id": "EiDbn7CabET039d7TULd5bRWSW5a4FnuPyO7l3a7Qu6ULQ==", "token_balance": 14131 }, + { "peer_id": "EiDc3vxkQQxMDZh5/shT9asIXxRKADOmsIT/yUKm+lErsA==", "token_balance": 14131 }, + { "peer_id": "EiDDNED6DphDR/MfWD/LcmT1cF+myrhu8DqRDjpVG9kN1Q==", "token_balance": 14131 }, + { "peer_id": "EiDFZ3WZDDPjYO4+inRDTuyAQN0bNCCkDX7YcxVaGL5isQ==", "token_balance": 14131 }, + { "peer_id": "EiDghtmPnBgBl9aEfAEoX7zqGjRqyikcf8imBRSIx79SpQ==", "token_balance": 14131 }, + { "peer_id": "EiDI3IWtdE6+riV4Ag3pe32Ek0g7QETFsQD6IuCx7ZzkNQ==", "token_balance": 14131 }, + { "peer_id": "EiDLU5mqRefq3Lnleo4LXgk5+JG2Tq1LQ1daJErAkSmQkQ==", "token_balance": 14131 }, + { "peer_id": "EiDmlBVIHR+jqAZab1JomprMk4Zd60Kk9pfmvhxPRGwIzQ==", "token_balance": 14131 }, + { "peer_id": "EiDMLuuSCxqmmAZwl0fhAEB29T2NrYuxNX3eoVkoB+LgFw==", "token_balance": 14131 }, + { "peer_id": "EiDIXso0zaNVYv9o1oa6Xx4DbheVEht+mMcPJIvJWKSxuQ==", "token_balance": 14131 }, + { "peer_id": "EiDOt1mXOV0cA1q1yWHO7At79CFOSxBAFidPQhwMD4licg==", "token_balance": 14131 }, + { "peer_id": "EiDP21JvwRuBXadS9TWZ+AHvvsAOvucLV2GUf5CehuninA==", "token_balance": 14131 }, + { "peer_id": "EiDQYHxH3uptGi8btm3JlSllnuJv4psDL0mrgVmE4Tg0nA==", "token_balance": 14131 }, + { "peer_id": "EiDSJUeAmaspN5qvLSnOR/ku1cUtqA75i1nqgBND+X8VkA==", "token_balance": 14131 }, + { "peer_id": "EiDsx6A1ZJobkwHzI1L4x8MV/qSTnLo7B4dVwCOcmFntQw==", "token_balance": 14131 }, + { "peer_id": "EiDTUujybIA/oPuG8hSjPUioegcd1pOillvksY5zeOTUqw==", "token_balance": 14131 }, + { "peer_id": "EiDucIlcuEY1ohpR421WkGUOMimWFqAfMG3tp7lUtx534w==", "token_balance": 14131 }, + { "peer_id": "EiDueSVvXpj2B5IKYkjmjnGZtEeXuyqvCxgsNyClTeSZqA==", "token_balance": 14131 }, + { "peer_id": "EiDuXb8WGtb41p0fTfCECraBXI71y8TLCqgjyQP8/Cfw8A==", "token_balance": 14131 }, + { "peer_id": "EiDV19Jt3o+SI2JnbAub+zMddORpSZ+dfGO/j+P1lGDhPg==", "token_balance": 14131 }, + { "peer_id": "EiDWzbv+2wYUL9q5uDhQZq+j/OPxjzLZxCrwZCYQtPMAZQ==", "token_balance": 14131 }, + { "peer_id": "EiDx6BMj1AEzyv71+ZcJz8lpYZJo7TpFjj4aJ2jf8pXq3g==", "token_balance": 14131 }, + { "peer_id": "EiDXMvSSOv+nF6qZuBEU+cVCczPoclZgsWEzaDek8dIGjA==", "token_balance": 14131 }, + { "peer_id": "EiDyqp7F9IKBL5BGiAYdExLw+8ItsAYL2AVcS3XMDtPp+Q==", "token_balance": 14131 }, + { "peer_id": "EiDZGMJApfat9XujI1X19ulYZV9KdCjUP94kTp4ypv1JCw==", "token_balance": 14131 } + ] +} \ No newline at end of file diff --git a/node/store/clock.go b/node/store/clock.go index b688a58..073095b 100644 --- a/node/store/clock.go +++ b/node/store/clock.go @@ -97,29 +97,29 @@ type ClockStore interface { } type PebbleClockStore struct { - db *pebble.DB + db KVDB logger *zap.Logger } var _ ClockStore = (*PebbleClockStore)(nil) type PebbleMasterClockIterator struct { - i *pebble.Iterator + i Iterator } type PebbleClockIterator struct { - i *pebble.Iterator + i Iterator db *PebbleClockStore } type PebbleCandidateClockIterator struct { - i *pebble.Iterator + i Iterator db *PebbleClockStore } -var _ Iterator[*protobufs.ClockFrame] = (*PebbleMasterClockIterator)(nil) -var _ Iterator[*protobufs.ClockFrame] = (*PebbleClockIterator)(nil) -var _ Iterator[*protobufs.ClockFrame] = (*PebbleCandidateClockIterator)(nil) +var _ TypedIterator[*protobufs.ClockFrame] = (*PebbleMasterClockIterator)(nil) +var _ TypedIterator[*protobufs.ClockFrame] = (*PebbleClockIterator)(nil) +var _ TypedIterator[*protobufs.ClockFrame] = (*PebbleCandidateClockIterator)(nil) func (p *PebbleMasterClockIterator) First() bool { return p.i.First() @@ -173,7 +173,7 @@ func (p *PebbleMasterClockIterator) Value() (*protobufs.ClockFrame, error) { return nil, errors.Wrap(err, "get master clock frame iterator value") } - frame.ParentSelector = parent.Bytes() + frame.ParentSelector = parent.FillBytes(make([]byte, 32)) return frame, nil } @@ -306,7 +306,7 @@ func (p *PebbleCandidateClockIterator) Close() error { return errors.Wrap(p.i.Close(), "closing candidate clock frame iterator") } -func NewPebbleClockStore(db *pebble.DB, logger *zap.Logger) *PebbleClockStore { +func NewPebbleClockStore(db KVDB, logger *zap.Logger) *PebbleClockStore { return &PebbleClockStore{ db, logger, @@ -446,9 +446,7 @@ func clockProverTrieKey(filter []byte, frameNumber uint64) []byte { } func (p *PebbleClockStore) NewTransaction() (Transaction, error) { - return &PebbleTransaction{ - b: p.db.NewBatch(), - }, nil + return p.db.NewBatch(), nil } // GetEarliestMasterClockFrame implements ClockStore. @@ -530,7 +528,7 @@ func (p *PebbleClockStore) GetMasterClockFrame( return nil, errors.Wrap(err, "get master clock frame") } - frame.ParentSelector = parent.Bytes() + frame.ParentSelector = parent.FillBytes(make([]byte, 32)) return frame, nil } @@ -547,10 +545,10 @@ func (p *PebbleClockStore) RangeMasterClockFrames( startFrameNumber = temp } - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: clockMasterFrameKey(filter, startFrameNumber), - UpperBound: clockMasterFrameKey(filter, endFrameNumber), - }) + iter, err := p.db.NewIter( + clockMasterFrameKey(filter, startFrameNumber), + clockMasterFrameKey(filter, endFrameNumber), + ) if err != nil { return nil, errors.Wrap(err, "range master clock frames") } @@ -863,7 +861,7 @@ func (p *PebbleClockStore) PutCandidateDataClockFrame( frame *protobufs.ClockFrame, txn Transaction, ) error { - if err := p.saveAggregateProofs(nil, frame); err != nil { + if err := p.saveAggregateProofs(txn, frame); err != nil { return errors.Wrap( errors.Wrap(err, ErrInvalidData.Error()), "put candidate data clock frame", @@ -920,7 +918,7 @@ func (p *PebbleClockStore) PutDataClockFrame( backfill bool, ) error { if frame.FrameNumber != 0 { - if err := p.saveAggregateProofs(nil, frame); err != nil { + if err := p.saveAggregateProofs(txn, frame); err != nil { return errors.Wrap( errors.Wrap(err, ErrInvalidData.Error()), "put candidate data clock frame", @@ -1004,8 +1002,8 @@ func (p *PebbleClockStore) GetCandidateDataClockFrames( filter []byte, frameNumber uint64, ) ([]*protobufs.ClockFrame, error) { - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: clockDataCandidateFrameKey( + iter, err := p.db.NewIter( + clockDataCandidateFrameKey( filter, frameNumber, []byte{ @@ -1021,7 +1019,7 @@ func (p *PebbleClockStore) GetCandidateDataClockFrames( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, ), - UpperBound: clockDataCandidateFrameKey( + clockDataCandidateFrameKey( filter, frameNumber, []byte{ @@ -1037,7 +1035,7 @@ func (p *PebbleClockStore) GetCandidateDataClockFrames( 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }, ), - }) + ) if err != nil { return nil, errors.Wrap(err, "get candidate data clock frames") } @@ -1084,8 +1082,8 @@ func (p *PebbleClockStore) RangeCandidateDataClockFrames( 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, } } - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: clockDataCandidateFrameKey( + iter, err := p.db.NewIter( + clockDataCandidateFrameKey( filter, frameNumber, fromParent, @@ -1096,7 +1094,7 @@ func (p *PebbleClockStore) RangeCandidateDataClockFrames( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, ), - UpperBound: clockDataCandidateFrameKey( + clockDataCandidateFrameKey( filter, frameNumber, toParent, @@ -1107,7 +1105,7 @@ func (p *PebbleClockStore) RangeCandidateDataClockFrames( 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }, ), - }) + ) if err != nil { return nil, errors.Wrap(err, "range candidate data clock frames") } @@ -1127,10 +1125,10 @@ func (p *PebbleClockStore) RangeDataClockFrames( startFrameNumber = temp } - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: clockDataFrameKey(filter, startFrameNumber), - UpperBound: clockDataFrameKey(filter, endFrameNumber), - }) + iter, err := p.db.NewIter( + clockDataFrameKey(filter, startFrameNumber), + clockDataFrameKey(filter, endFrameNumber), + ) if err != nil { return nil, errors.Wrap(err, "get data clock frames") } @@ -1161,10 +1159,7 @@ func (p *PebbleClockStore) Deduplicate(filter []byte) error { }, ) - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: from, - UpperBound: to, - }) + iter, err := p.db.NewIter(from, to) if err != nil { return errors.Wrap(err, "deduplicate") } @@ -1187,7 +1182,7 @@ func (p *PebbleClockStore) Deduplicate(filter []byte) error { return err } - err = p.db.Set(iter.Key(), newValue, &pebble.WriteOptions{Sync: true}) + err = p.db.Set(iter.Key(), newValue) if err != nil { return err } @@ -1205,10 +1200,7 @@ func (p *PebbleClockStore) Deduplicate(filter []byte) error { from = clockDataFrameKey(filter, 1) to = clockDataFrameKey(filter, 20000) - iter, err = p.db.NewIter(&pebble.IterOptions{ - LowerBound: from, - UpperBound: to, - }) + iter, err = p.db.NewIter(from, to) if err != nil { return errors.Wrap(err, "deduplicate") } @@ -1231,7 +1223,7 @@ func (p *PebbleClockStore) Deduplicate(filter []byte) error { return err } - err = p.db.Set(iter.Key(), newValue, &pebble.WriteOptions{Sync: true}) + err = p.db.Set(iter.Key(), newValue) if err != nil { return err } @@ -1279,10 +1271,7 @@ func (p *PebbleClockStore) Deduplicate(filter []byte) error { }, ) - iter, err = p.db.NewIter(&pebble.IterOptions{ - LowerBound: from, - UpperBound: to, - }) + iter, err = p.db.NewIter(from, to) if err != nil { return errors.Wrap(err, "deduplicate") } @@ -1305,7 +1294,7 @@ func (p *PebbleClockStore) Deduplicate(filter []byte) error { return err } - err = p.db.Set(iter.Key(), newValue, &pebble.WriteOptions{Sync: true}) + err = p.db.Set(iter.Key(), newValue) if err != nil { return err } @@ -1334,10 +1323,7 @@ func (p *PebbleClockStore) GetCompressedDataClockFrames( from := clockDataFrameKey(filter, fromFrameNumber) to := clockDataFrameKey(filter, toFrameNumber+1) - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: from, - UpperBound: to, - }) + iter, err := p.db.NewIter(from, to) if err != nil { return nil, errors.Wrap(err, "get compressed data clock frames") } @@ -1418,10 +1404,7 @@ func (p *PebbleClockStore) GetCompressedDataClockFrames( }, ) - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: from, - UpperBound: to, - }) + iter, err := p.db.NewIter(from, to) if err != nil { return nil, errors.Wrap(err, "get compressed data clock frames") } @@ -1458,7 +1441,7 @@ func (p *PebbleClockStore) GetCompressedDataClockFrames( if err != nil { return nil, errors.Wrap(err, "get compressed data clock frames") } - parentSelector, _, _, err := frame.GetParentSelectorAndDistance() + parentSelector, _, _, err := frame.GetParentSelectorAndDistance(nil) if err != nil { return nil, errors.Wrap(err, "get compressed data clock frames") } @@ -1480,8 +1463,28 @@ func (p *PebbleClockStore) GetCompressedDataClockFrames( break } score := new(big.Int) - for _, p := range paths[i] { - _, distance, _, err := p.GetParentSelectorAndDistance() + for _, path := range paths[i] { + master, err := p.GetMasterClockFrame( + []byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }, + path.FrameNumber, + ) + if err != nil { + return nil, errors.Wrap(err, "get compressed data clock frames") + } + + discriminator, err := master.GetSelector() + if err != nil { + return nil, errors.Wrap(err, "get compressed data clock frames") + } + + _, distance, _, err := path.GetParentSelectorAndDistance( + discriminator, + ) if err != nil { return nil, errors.Wrap(err, "get compressed data clock frames") } @@ -1535,10 +1538,13 @@ func (p *PebbleClockStore) GetCompressedDataClockFrames( return nil, errors.Wrap(err, "get compressed data clock frames") } - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: dataProofInclusionKey(filter, []byte(k), 0), - UpperBound: dataProofInclusionKey(filter, []byte(k), limit+1), - }) + iter, err := p.db.NewIter( + dataProofInclusionKey(filter, []byte(k), 0), + dataProofInclusionKey(filter, []byte(k), limit+1), + ) + if err != nil { + return nil, errors.Wrap(err, "get compressed data clock frames") + } for iter.First(); iter.Valid(); iter.Next() { incCommit := iter.Value() @@ -1632,9 +1638,6 @@ func (p *PebbleClockStore) SetLatestDataClockFrameNumber( err := p.db.Set( clockDataLatestIndex(filter), binary.BigEndian.AppendUint64(nil, frameNumber), - &pebble.WriteOptions{ - Sync: true, - }, ) return errors.Wrap(err, "set latest data clock frame number") @@ -1678,9 +1681,6 @@ func (p *PebbleClockStore) DeleteCandidateDataClockFrameRange( 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }, ), - &pebble.WriteOptions{ - Sync: true, - }, ) return errors.Wrap(err, "delete candidate data clock frame range") } @@ -1727,10 +1727,13 @@ func (p *PebbleClockStore) GetHighestCandidateDataClockFrame( }, ) - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: from, - UpperBound: to, - }) + iter, err := p.db.NewIter(from, to) + if err != nil { + return nil, errors.Wrap( + errors.Wrap(err, ErrInvalidData.Error()), + "get highest candidate data clock frame", + ) + } found := iter.SeekLT(to) if found { diff --git a/node/store/data_proof.go b/node/store/data_proof.go index 6221447..0952fc2 100644 --- a/node/store/data_proof.go +++ b/node/store/data_proof.go @@ -30,12 +30,12 @@ type DataProofStore interface { } type PebbleDataProofStore struct { - db *pebble.DB + db KVDB logger *zap.Logger } func NewPebbleDataProofStore( - db *pebble.DB, + db KVDB, logger *zap.Logger, ) *PebbleDataProofStore { return &PebbleDataProofStore{ @@ -81,13 +81,11 @@ func dataProofSegmentKey( } func (p *PebbleDataProofStore) NewTransaction() (Transaction, error) { - return &PebbleTransaction{ - b: p.db.NewBatch(), - }, nil + return p.db.NewBatch(), nil } func internalGetAggregateProof( - db *pebble.DB, + db KVDB, filter []byte, commitment []byte, frameNumber uint64, @@ -114,10 +112,10 @@ func internalGetAggregateProof( Proof: copied, } - iter, err := db.NewIter(&pebble.IterOptions{ - LowerBound: dataProofInclusionKey(filter, commitment, 0), - UpperBound: dataProofInclusionKey(filter, commitment, limit+1), - }) + iter, err := db.NewIter( + dataProofInclusionKey(filter, commitment, 0), + dataProofInclusionKey(filter, commitment, limit+1), + ) if err != nil { return nil, errors.Wrap(err, "get aggregate proof") } @@ -206,7 +204,7 @@ func (p *PebbleDataProofStore) GetAggregateProof( } func internalPutAggregateProof( - db *pebble.DB, + db KVDB, txn Transaction, aggregateProof *protobufs.InclusionAggregateProof, commitment []byte, diff --git a/node/store/inmem.go b/node/store/inmem.go new file mode 100644 index 0000000..7ec5661 --- /dev/null +++ b/node/store/inmem.go @@ -0,0 +1,349 @@ +package store + +import ( + "errors" + "io" + "math/rand" + "sort" + "sync" + + "github.com/cockroachdb/pebble" +) + +type InMemKVDB struct { + open bool + sortedKeys []string + store map[string][]byte + storeMx sync.Mutex +} + +type Operation int + +const ( + SetOperation Operation = iota + DeleteOperation +) + +type InMemKVDBOperation struct { + op Operation + key []byte + value []byte +} + +type InMemKVDBTransaction struct { + id int + changes []InMemKVDBOperation + db *InMemKVDB +} + +type InMemKVDBIterator struct { + db *InMemKVDB + start []byte + end []byte + pos int + open bool +} + +func (i *InMemKVDBIterator) Key() []byte { + if !i.open { + return nil + } + i.db.storeMx.Lock() + if _, ok := i.db.store[i.db.sortedKeys[i.pos]]; !ok { + return nil + } + i.db.storeMx.Unlock() + + return []byte(i.db.sortedKeys[i.pos]) +} + +func (i *InMemKVDBIterator) First() bool { + if !i.open { + return false + } + i.db.storeMx.Lock() + found := false + idx := sort.SearchStrings(i.db.sortedKeys, string(i.start)) + final := sort.SearchStrings(i.db.sortedKeys, string(i.end)) + if idx < final { + i.pos = idx + found = true + } + i.db.storeMx.Unlock() + + return found +} + +func (i *InMemKVDBIterator) Next() bool { + if !i.open { + return false + } + i.db.storeMx.Lock() + found := false + if _, ok := i.db.store[i.db.sortedKeys[i.pos]]; ok { + final := sort.SearchStrings(i.db.sortedKeys, string(i.end)) + if i.pos < final { + i.pos = i.pos + 1 + found = true + } + } + i.db.storeMx.Unlock() + + return found +} + +func (i *InMemKVDBIterator) Prev() bool { + if !i.open { + return false + } + i.db.storeMx.Lock() + found := false + if _, ok := i.db.store[i.db.sortedKeys[i.pos]]; ok { + start := sort.SearchStrings(i.db.sortedKeys, string(i.start)) + if i.pos-1 > start { + i.pos = i.pos - 1 + found = true + } + } + i.db.storeMx.Unlock() + + return found +} + +func (i *InMemKVDBIterator) Valid() bool { + if !i.open { + return false + } + i.db.storeMx.Lock() + start := sort.SearchStrings(i.db.sortedKeys, string(i.start)) + final := sort.SearchStrings(i.db.sortedKeys, string(i.end)) + i.db.storeMx.Unlock() + + return i.pos < final && i.pos >= start +} + +func (i *InMemKVDBIterator) Value() []byte { + if !i.open { + return nil + } + + i.db.storeMx.Lock() + value := i.db.store[i.db.sortedKeys[i.pos]] + i.db.storeMx.Unlock() + + return value +} + +func (i *InMemKVDBIterator) Close() error { + if !i.open { + return errors.New("already closed iterator") + } + + i.open = false + return nil +} + +func (i *InMemKVDBIterator) SeekLT(lt []byte) bool { + if !i.open { + return false + } + i.db.storeMx.Lock() + found := false + if _, ok := i.db.store[i.db.sortedKeys[i.pos]]; ok { + idx := sort.SearchStrings(i.db.sortedKeys, string(lt)) + start := sort.SearchStrings(i.db.sortedKeys, string(i.start)) + if idx >= start { + i.pos = idx + 1 + found = true + } + } + i.db.storeMx.Unlock() + + return found +} + +func (t *InMemKVDBTransaction) Set(key []byte, value []byte) error { + if !t.db.open { + return errors.New("inmem db closed") + } + + t.changes = append(t.changes, InMemKVDBOperation{ + op: SetOperation, + key: key, + value: value, + }) + + return nil +} + +func (t *InMemKVDBTransaction) Commit() error { + if !t.db.open { + return errors.New("inmem db closed") + } + + var err error +loop: + for _, op := range t.changes { + switch op.op { + case SetOperation: + err = t.db.Set(op.key, op.value) + if err != nil { + break loop + } + case DeleteOperation: + err = t.db.Delete(op.key) + if err != nil { + break loop + } + } + } + + return err +} + +func (t *InMemKVDBTransaction) Delete(key []byte) error { + if !t.db.open { + return errors.New("inmem db closed") + } + + t.changes = append(t.changes, InMemKVDBOperation{ + op: DeleteOperation, + key: key, + }) + + return nil +} + +func (t *InMemKVDBTransaction) Abort() error { + return nil +} + +func NewInMemKVDB() *InMemKVDB { + return &InMemKVDB{ + open: true, + store: map[string][]byte{}, + sortedKeys: []string{}, + } +} + +func (d *InMemKVDB) Get(key []byte) ([]byte, io.Closer, error) { + if !d.open { + return nil, nil, errors.New("inmem db closed") + } + + d.storeMx.Lock() + b, ok := d.store[string(key)] + d.storeMx.Unlock() + if !ok { + return nil, nil, pebble.ErrNotFound + } + return b, io.NopCloser(nil), nil +} + +func (d *InMemKVDB) Set(key, value []byte) error { + if !d.open { + return errors.New("inmem db closed") + } + + d.storeMx.Lock() + _, ok := d.store[string(key)] + if !ok { + i := sort.SearchStrings(d.sortedKeys, string(key)) + if len(d.sortedKeys) > i { + d.sortedKeys = append(d.sortedKeys[:i+1], d.sortedKeys[i:]...) + d.sortedKeys[i] = string(key) + } else { + d.sortedKeys = append(d.sortedKeys, string(key)) + } + } + d.store[string(key)] = value + + d.storeMx.Unlock() + return nil +} + +func (d *InMemKVDB) Delete(key []byte) error { + if !d.open { + return errors.New("inmem db closed") + } + + d.storeMx.Lock() + _, ok := d.store[string(key)] + if ok { + i := sort.SearchStrings(d.sortedKeys, string(key)) + if len(d.sortedKeys)-1 > i { + d.sortedKeys = append(d.sortedKeys[:i], d.sortedKeys[i+1:]...) + } else { + d.sortedKeys = d.sortedKeys[:i] + } + } + delete(d.store, string(key)) + d.storeMx.Unlock() + return nil +} + +func (d *InMemKVDB) NewBatch() Transaction { + if !d.open { + return nil + } + + id := rand.Int() + return &InMemKVDBTransaction{ + id: id, + db: d, + changes: []InMemKVDBOperation{}, + } +} + +func (d *InMemKVDB) NewIter(lowerBound []byte, upperBound []byte) (Iterator, error) { + if !d.open { + return nil, errors.New("inmem db closed") + } + + return &InMemKVDBIterator{ + open: true, + db: d, + start: lowerBound, + end: upperBound, + pos: -1, + }, nil +} + +func (d *InMemKVDB) Compact(start, end []byte, parallelize bool) error { + if !d.open { + return errors.New("inmem db closed") + } + + return nil +} + +func (d *InMemKVDB) Close() error { + if !d.open { + return errors.New("inmem db closed") + } + + d.open = false + return nil +} + +func (d *InMemKVDB) DeleteRange(start, end []byte) error { + if !d.open { + return errors.New("inmem db closed") + } + + iter, err := d.NewIter(start, end) + if err != nil { + return err + } + + for iter.First(); iter.Valid(); iter.Next() { + err = d.Delete(iter.Key()) + if err != nil { + return err + } + } + + return nil +} + +var _ KVDB = (*InMemKVDB)(nil) diff --git a/node/store/inmem_test.go b/node/store/inmem_test.go new file mode 100644 index 0000000..4c5424a --- /dev/null +++ b/node/store/inmem_test.go @@ -0,0 +1,90 @@ +package store_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "source.quilibrium.com/quilibrium/monorepo/node/store" +) + +func TestIter(t *testing.T) { + db := store.NewInMemKVDB() + db.Set([]byte{0x01}, []byte{0x01}) + db.Set([]byte{0x02}, []byte{0x02}) + db.Set([]byte{0x03}, []byte{0x03}) + db.Set([]byte{0x04}, []byte{0x04}) + db.Set([]byte{0x06}, []byte{0x06}) + db.Set([]byte{0x07}, []byte{0x07}) + db.Set([]byte{0x08}, []byte{0x08}) + db.Set([]byte{0x010}, []byte{0x010}) + db.Set([]byte{0x012}, []byte{0x012}) + db.Set([]byte{0x014}, []byte{0x014}) + iter, err := db.NewIter([]byte{0x01}, []byte{0x04}) + assert.NoError(t, err) + assert.True(t, iter.First()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x01}) + assert.ElementsMatch(t, iter.Key(), []byte{0x01}) + assert.True(t, iter.Next()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x02}) + assert.ElementsMatch(t, iter.Key(), []byte{0x02}) + assert.True(t, iter.Next()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x03}) + assert.ElementsMatch(t, iter.Key(), []byte{0x03}) + assert.True(t, iter.Next()) + assert.False(t, iter.Valid()) + assert.NoError(t, iter.Close()) + + iter, err = db.NewIter([]byte{0x06}, []byte{0x09}) + assert.NoError(t, err) + assert.True(t, iter.First()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x06}) + assert.ElementsMatch(t, iter.Key(), []byte{0x06}) + assert.True(t, iter.Next()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x07}) + assert.ElementsMatch(t, iter.Key(), []byte{0x07}) + assert.True(t, iter.Next()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x08}) + assert.ElementsMatch(t, iter.Key(), []byte{0x08}) + assert.True(t, iter.Next()) + assert.False(t, iter.Valid()) + + iter, err = db.NewIter([]byte{0x05}, []byte{0x09}) + assert.NoError(t, err) + assert.True(t, iter.First()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x06}) + assert.ElementsMatch(t, iter.Key(), []byte{0x06}) + assert.True(t, iter.Next()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x07}) + assert.ElementsMatch(t, iter.Key(), []byte{0x07}) + assert.True(t, iter.Next()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x08}) + assert.ElementsMatch(t, iter.Key(), []byte{0x08}) + assert.True(t, iter.Next()) + assert.False(t, iter.Valid()) + + iter, err = db.NewIter([]byte{0x010}, []byte{0x015}) + assert.NoError(t, err) + assert.True(t, iter.First()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x10}) + assert.ElementsMatch(t, iter.Key(), []byte{0x10}) + assert.True(t, iter.Next()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x12}) + assert.ElementsMatch(t, iter.Key(), []byte{0x12}) + assert.True(t, iter.Next()) + assert.True(t, iter.Valid()) + assert.ElementsMatch(t, iter.Value(), []byte{0x14}) + assert.ElementsMatch(t, iter.Key(), []byte{0x14}) + assert.True(t, iter.Next()) + assert.False(t, iter.Valid()) +} diff --git a/node/store/iterator.go b/node/store/iterator.go index d239ee1..b19247d 100644 --- a/node/store/iterator.go +++ b/node/store/iterator.go @@ -2,7 +2,18 @@ package store import "google.golang.org/protobuf/proto" -type Iterator[T proto.Message] interface { +type Iterator interface { + Key() []byte + First() bool + Next() bool + Prev() bool + Valid() bool + Value() []byte + Close() error + SeekLT([]byte) bool +} + +type TypedIterator[T proto.Message] interface { First() bool Next() bool Valid() bool diff --git a/node/store/key.go b/node/store/key.go index 0adc8fc..0031e3c 100644 --- a/node/store/key.go +++ b/node/store/key.go @@ -37,28 +37,28 @@ type KeyStore interface { } type PebbleKeyStore struct { - db *pebble.DB + db KVDB logger *zap.Logger } type PebbleProvingKeyIterator struct { - i *pebble.Iterator + i Iterator } type PebbleStagedProvingKeyIterator struct { - i *pebble.Iterator + i Iterator } type PebbleKeyBundleIterator struct { - i *pebble.Iterator + i Iterator } var pki = (*PebbleProvingKeyIterator)(nil) var spki = (*PebbleStagedProvingKeyIterator)(nil) var kbi = (*PebbleKeyBundleIterator)(nil) -var _ Iterator[*protobufs.InclusionCommitment] = pki -var _ Iterator[*protobufs.ProvingKeyAnnouncement] = spki -var _ Iterator[*protobufs.InclusionCommitment] = kbi +var _ TypedIterator[*protobufs.InclusionCommitment] = pki +var _ TypedIterator[*protobufs.ProvingKeyAnnouncement] = spki +var _ TypedIterator[*protobufs.InclusionCommitment] = kbi var _ KeyStore = (*PebbleKeyStore)(nil) func (p *PebbleProvingKeyIterator) First() bool { @@ -169,7 +169,7 @@ func (p *PebbleKeyBundleIterator) Close() error { return errors.Wrap(p.i.Close(), "closing iterator") } -func NewPebbleKeyStore(db *pebble.DB, logger *zap.Logger) *PebbleKeyStore { +func NewPebbleKeyStore(db KVDB, logger *zap.Logger) *PebbleKeyStore { return &PebbleKeyStore{ db, logger, @@ -217,9 +217,7 @@ func keyBundleEarliestKey(provingKey []byte) []byte { } func (p *PebbleKeyStore) NewTransaction() (Transaction, error) { - return &PebbleTransaction{ - b: p.db.NewBatch(), - }, nil + return p.db.NewBatch(), nil } // Stages a proving key for later inclusion on proof of meaningful work. @@ -235,9 +233,6 @@ func (p *PebbleKeyStore) StageProvingKey( err = p.db.Set( stagedProvingKeyKey(provingKey.PublicKey()), data, - &pebble.WriteOptions{ - Sync: true, - }, ) if err != nil { return errors.Wrap(err, "stage proving key") @@ -462,8 +457,8 @@ func (p *PebbleKeyStore) PutKeyBundle( } func (p *PebbleKeyStore) RangeProvingKeys() (*PebbleProvingKeyIterator, error) { - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: provingKeyKey([]byte{ + iter, err := p.db.NewIter( + provingKeyKey([]byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -473,7 +468,7 @@ func (p *PebbleKeyStore) RangeProvingKeys() (*PebbleProvingKeyIterator, error) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }), - UpperBound: provingKeyKey([]byte{ + provingKeyKey([]byte{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, @@ -483,7 +478,7 @@ func (p *PebbleKeyStore) RangeProvingKeys() (*PebbleProvingKeyIterator, error) { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }), - }) + ) if err != nil { return nil, errors.Wrap(err, "range proving keys") } @@ -495,8 +490,8 @@ func (p *PebbleKeyStore) RangeStagedProvingKeys() ( *PebbleStagedProvingKeyIterator, error, ) { - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: stagedProvingKeyKey([]byte{ + iter, err := p.db.NewIter( + stagedProvingKeyKey([]byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -506,7 +501,7 @@ func (p *PebbleKeyStore) RangeStagedProvingKeys() ( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }), - UpperBound: stagedProvingKeyKey([]byte{ + stagedProvingKeyKey([]byte{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, @@ -516,7 +511,7 @@ func (p *PebbleKeyStore) RangeStagedProvingKeys() ( 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }), - }) + ) if err != nil { return nil, errors.Wrap(err, "range staged proving keys") } @@ -528,10 +523,10 @@ func (p *PebbleKeyStore) RangeKeyBundleKeys(provingKey []byte) ( *PebbleKeyBundleIterator, error, ) { - iter, err := p.db.NewIter(&pebble.IterOptions{ - LowerBound: keyBundleKey(provingKey, 0), - UpperBound: keyBundleKey(provingKey, 0xffffffffffffffff), - }) + iter, err := p.db.NewIter( + keyBundleKey(provingKey, 0), + keyBundleKey(provingKey, 0xffffffffffffffff), + ) if err != nil { return nil, errors.Wrap(err, "range key bundle keys") } diff --git a/node/store/kvdb.go b/node/store/kvdb.go new file mode 100644 index 0000000..01ebadf --- /dev/null +++ b/node/store/kvdb.go @@ -0,0 +1,16 @@ +package store + +import ( + "io" +) + +type KVDB interface { + Get(key []byte) ([]byte, io.Closer, error) + Set(key, value []byte) error + Delete(key []byte) error + NewBatch() Transaction + NewIter(lowerBound []byte, upperBound []byte) (Iterator, error) + Compact(start, end []byte, parallelize bool) error + Close() error + DeleteRange(start, end []byte) error +} diff --git a/node/store/pebble.go b/node/store/pebble.go index fc475b9..91b39fd 100644 --- a/node/store/pebble.go +++ b/node/store/pebble.go @@ -1,19 +1,67 @@ package store import ( + "io" + "github.com/cockroachdb/pebble" "source.quilibrium.com/quilibrium/monorepo/node/config" ) -func NewPebbleDB(config *config.DBConfig) *pebble.DB { +type PebbleDB struct { + db *pebble.DB +} + +func NewPebbleDB(config *config.DBConfig) *PebbleDB { db, err := pebble.Open(config.Path, &pebble.Options{}) if err != nil { panic(err) } - return db + return &PebbleDB{db} } +func (p *PebbleDB) Get(key []byte) ([]byte, io.Closer, error) { + return p.db.Get(key) +} + +func (p *PebbleDB) Set(key, value []byte) error { + return p.db.Set(key, value, &pebble.WriteOptions{Sync: true}) +} + +func (p *PebbleDB) Delete(key []byte) error { + return p.db.Delete(key, &pebble.WriteOptions{Sync: true}) +} + +func (p *PebbleDB) NewBatch() Transaction { + return &PebbleTransaction{ + b: p.db.NewBatch(), + } +} + +func (p *PebbleDB) NewIter(lowerBound []byte, upperBound []byte) ( + Iterator, + error, +) { + return p.db.NewIter(&pebble.IterOptions{ + LowerBound: lowerBound, + UpperBound: upperBound, + }) +} + +func (p *PebbleDB) Compact(start, end []byte, parallelize bool) error { + return p.db.Compact(start, end, parallelize) +} + +func (p *PebbleDB) Close() error { + return p.db.Close() +} + +func (p *PebbleDB) DeleteRange(start, end []byte) error { + return p.db.DeleteRange(start, end, &pebble.WriteOptions{Sync: true}) +} + +var _ KVDB = (*PebbleDB)(nil) + type Transaction interface { Set(key []byte, value []byte) error Commit() error diff --git a/pebble/.editorconfig b/pebble/.editorconfig new file mode 100644 index 0000000..0e4642a --- /dev/null +++ b/pebble/.editorconfig @@ -0,0 +1,10 @@ +# See http://editorconfig.org + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +# For non-go files, we indent with two spaces. In go files we indent +# with tabs but still set indent_size to control the github web viewer. +indent_size=2 diff --git a/pebble/.github/workflows/ci.yaml b/pebble/.github/workflows/ci.yaml new file mode 100644 index 0000000..a8fbb26 --- /dev/null +++ b/pebble/.github/workflows/ci.yaml @@ -0,0 +1,160 @@ +name: Test + +on: + push: + branches: + - master + - crl-release-* + pull_request: + branches: + - master + - crl-release-* + +jobs: + + linux: + name: go-linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - run: make test generate + + linux-32bit: + name: go-linux-32bit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - run: GOARCH=386 make test + + linux-crossversion: + name: go-linux-crossversion + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - run: make crossversion-meta + + linux-race: + name: go-linux-race + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - run: make testrace TAGS= + + linux-no-invariants: + name: go-linux-no-invariants + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - run: make test TAGS= + + linux-no-cgo: + name: go-linux-no-cgo + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - run: CGO_ENABLED=0 make test TAGS= + + darwin: + name: go-macos + runs-on: macos-12 + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - run: make test + + windows: + name: go-windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - run: go test -v ./... + + bsds: + name: go-bsds + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - name: FreeBSD build + env: + GOOS: freebsd + run: go build -v ./... + + - name: NetBSD build + env: + GOOS: netbsd + run: go build -v ./... + + - name: OpenBSD build + env: + GOOS: openbsd + run: go build -v ./... + + go-lint-checks: + name: go-lint-checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - name: mod-tidy-check + run: make mod-tidy-check + + - name: format-check + run: make format-check diff --git a/pebble/.github/workflows/code-cover-gen.yaml b/pebble/.github/workflows/code-cover-gen.yaml new file mode 100644 index 0000000..e6ec42a --- /dev/null +++ b/pebble/.github/workflows/code-cover-gen.yaml @@ -0,0 +1,71 @@ +name: PR code coverage (generate) + +on: + # This workflow does not have access to secrets because it runs on top of + # potentially unsafe changes. + pull_request: + types: [ opened, reopened, synchronize ] + branches: [ master ] + +jobs: + # The results of this job are uploaded as artifacts. A separate job will + # download the artifacts and upload them to a GCS bucket. + code-cover-gen: + runs-on: ubuntu-latest + env: + PR: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v3 + with: + # By default, checkout merges the PR into the current master. + # Instead, we want to check out the PR as-is. + ref: ${{ github.event.pull_request.head.sha }} + # Fetch all branches and history (we'll need the origin/master ref and + # the base commit). + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: "1.21" + + - name: Get list of changed packages + shell: bash + run: | + set -euxo pipefail + # To get the base commit, we get the number of commits in the PR. + # Note that github.event.pull_request.base.sha is not what we want, + # that is the tip of master and not necessarily the PR fork point. + NUM_COMMITS=$(gh pr view $PR --json commits --jq '.commits | length') + BASE_SHA=$(git rev-parse HEAD~${NUM_COMMITS}) + CHANGED_PKGS=$(scripts/changed-go-pkgs.sh ${BASE_SHA} ${HEAD_SHA}) + echo "BASE_SHA=${BASE_SHA}" >> "${GITHUB_ENV}" + echo "CHANGED_PKGS=${CHANGED_PKGS}" >> "${GITHUB_ENV}" + + - name: Generate "after" coverage + shell: bash + run: | + set -euxo pipefail + CHANGED_PKGS='${{ env.CHANGED_PKGS }}' + mkdir -p artifacts + # Make a copy of the script so that the "before" run below uses the + # same version. + cp scripts/pr-codecov-run-tests.sh ${RUNNER_TEMP}/ + ${RUNNER_TEMP}/pr-codecov-run-tests.sh artifacts/cover-${PR}-${HEAD_SHA}.json "${CHANGED_PKGS}" + + - name: Generate "before" coverage + shell: bash + run: | + set -euxo pipefail + BASE_SHA='${{ env.BASE_SHA }}' + CHANGED_PKGS='${{ env.CHANGED_PKGS }}' + git checkout -f ${BASE_SHA} + ${RUNNER_TEMP}/pr-codecov-run-tests.sh artifacts/cover-${PR}-${BASE_SHA}.json "${CHANGED_PKGS}" + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: cover + path: artifacts/cover-*.json diff --git a/pebble/.github/workflows/code-cover-publish.yaml b/pebble/.github/workflows/code-cover-publish.yaml new file mode 100644 index 0000000..ba5f63c --- /dev/null +++ b/pebble/.github/workflows/code-cover-publish.yaml @@ -0,0 +1,55 @@ +name: PR code coverage (publish) + +on: + workflow_run: + workflows: [ "PR code coverage (generate)" ] + types: [ "completed" ] + + +jobs: + # This job downloads the artifacts genearted by the code-cover-gen job and + # uploads them to a GCS bucket, from where Reviewable can access them. + code-cover-publish: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + - name: 'Download artifact' + uses: actions/github-script@v3.1.0 + with: + script: | + var artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "cover" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/cover.zip', Buffer.from(download.data)); + + - run: | + mkdir -p cover + unzip cover.zip -d cover + + - name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v1' + with: + credentials_json: '${{ secrets.CODECOVER_SERVICE_ACCOUNT_KEY }}' + + - name: 'Upload to GCS' + uses: 'google-github-actions/upload-cloud-storage@v1' + with: + path: 'cover' + glob: '**/cover-*.json' + parent: false + destination: 'crl-codecover-public/pr-pebble/' + process_gcloudignore: false diff --git a/pebble/.github/workflows/nightly-code-cover.yaml b/pebble/.github/workflows/nightly-code-cover.yaml new file mode 100644 index 0000000..5c444c3 --- /dev/null +++ b/pebble/.github/workflows/nightly-code-cover.yaml @@ -0,0 +1,48 @@ +name: Nightly code coverage + +on: + schedule: + - cron: '00 08 * * * ' + workflow_dispatch: + +jobs: + coverage-gen-and-publish: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + + steps: + - uses: actions/checkout@v3 + with: + # By default, checkout merges the PR into the current master. + # Instead, we want to check out the PR as-is. + ref: ${{ github.event.pull_request.head.sha }} + # Fetch all branches and history (we'll need the origin/master ref and + # the base commit). + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: "1.21" + + - name: Generate coverage + run: scripts/code-coverage.sh + + - name: Install lcov + run: | + sudo apt-get update + sudo apt-get install lcov + + - name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v1' + with: + credentials_json: '${{ secrets.CODECOVER_SERVICE_ACCOUNT_KEY }}' + + - name: 'Set up Cloud SDK' + uses: 'google-github-actions/setup-gcloud@v1' + with: + version: '>= 363.0.0' + + - name: Publish coverage + run: scripts/code-coverage-publish.sh diff --git a/pebble/.github/workflows/sanitizers.yaml b/pebble/.github/workflows/sanitizers.yaml new file mode 100644 index 0000000..a9da116 --- /dev/null +++ b/pebble/.github/workflows/sanitizers.yaml @@ -0,0 +1,32 @@ +name: Sanitizers + +on: + schedule: + - cron: "0 0 * * *" # Midnight UTC, daily. + +jobs: + linux-asan: + name: go-linux-asan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: "1.21" + + - run: make testasan + + linux-msan: + name: go-linux-msan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: "1.21" + + - run: make testmsan diff --git a/pebble/.github/workflows/stale.yml b/pebble/.github/workflows/stale.yml new file mode 100644 index 0000000..92d39cf --- /dev/null +++ b/pebble/.github/workflows/stale.yml @@ -0,0 +1,34 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "0 11 * * 1-4" + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v3 + with: + operations-per-run: 1000 + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: | + We have marked this issue as stale because it has been inactive for + 18 months. If this issue is still relevant, removing the stale label + or adding a comment will keep it active. Otherwise, we'll close it + in 10 days to keep the issue queue tidy. Thank you for your + contribution to Pebble! + stale-pr-message: 'Stale pull request message' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' + close-issue-label: 'X-stale' + close-pr-label: 'X-stale' + # Disable this for PR's, by setting a very high bar + days-before-pr-stale: 99999 + days-before-issue-stale: 540 + days-before-close: 10 + exempt-issue-labels: 'X-nostale' diff --git a/pebble/.gitignore b/pebble/.gitignore new file mode 100644 index 0000000..87ef192 --- /dev/null +++ b/pebble/.gitignore @@ -0,0 +1,9 @@ +# Github action artifacts. +artifacts +# Profiling artifacts. +cpu.*.prof +heap.prof +mutex.prof +coverprofile.out +# Testing artifacts +meta.*.test diff --git a/pebble/LICENSE b/pebble/LICENSE new file mode 100644 index 0000000..fec05ce --- /dev/null +++ b/pebble/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2011 The LevelDB-Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pebble/Makefile b/pebble/Makefile new file mode 100644 index 0000000..e430ff2 --- /dev/null +++ b/pebble/Makefile @@ -0,0 +1,131 @@ +GO := go +PKG := ./... +GOFLAGS := +STRESSFLAGS := +TAGS := invariants +TESTS := . +COVER_PROFILE := coverprofile.out + +.PHONY: all +all: + @echo usage: + @echo " make test" + @echo " make testrace" + @echo " make stress" + @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 := +.PHONY: test +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 + +testasan: testflags += -asan -timeout 20m +testasan: test + +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 + +.PHONY: stress stressrace +stressrace: testflags += -race +stress stressrace: testflags += -exec 'stress ${STRESSFLAGS}' -timeout 0 -test.v +stress stressrace: test + +.PHONY: stressmeta +stressmeta: override PKG = ./internal/metamorphic +stressmeta: override STRESSFLAGS += -p 1 +stressmeta: override TESTS = TestMeta$$ +stressmeta: stress + +.PHONY: crossversion-meta +crossversion-meta: + $(eval LATEST_RELEASE := $(shell git fetch origin && git branch -r --list '*/crl-release-*' | grep -o 'crl-release-.*$$' | sort | tail -1)) + git checkout ${LATEST_RELEASE}; \ + ${GO} test -c ./internal/metamorphic -o './internal/metamorphic/crossversion/${LATEST_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' + +.PHONY: stress-crossversion +stress-crossversion: + 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 + +.PHONY: clean +clean: + rm -f $(patsubst %,%.test,$(notdir $(shell go list ${PKG}))) + +git_dirty := $(shell git status -s) + +.PHONY: git-clean-check +git-clean-check: +ifneq ($(git_dirty),) + @echo "Git repository is dirty!" + @false +else + @echo "Git repository is clean." +endif + +.PHONY: mod-tidy-check +mod-tidy-check: +ifneq ($(git_dirty),) + $(error mod-tidy-check must be invoked on a clean repository) +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@44a36ec7 && crlfmt -w -tab 2 . + +.PHONY: format-check +format-check: +ifneq ($(git_dirty),) + $(error format-check must be invoked on a clean repository) +endif + $(MAKE) format + git diff + $(MAKE) git-clean-check diff --git a/pebble/README.md b/pebble/README.md new file mode 100644 index 0000000..c09e45d --- /dev/null +++ b/pebble/README.md @@ -0,0 +1,226 @@ +# 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/) + +Pebble is a LevelDB/RocksDB inspired key-value store focused on +performance and internal usage by CockroachDB. Pebble inherits the +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 +specifically targets the use case and feature set needed by CockroachDB: + +* Block-based tables +* Checkpoints +* Indexed batches +* Iterator options (lower/upper bound, table filter) +* Level-based compaction +* Manual compaction +* Merge operator +* Prefix bloom filters +* Prefix iteration +* Range deletion tombstones +* Reverse iteration +* SSTable ingestion +* Single delete +* Snapshots +* Table-level bloom filters + +RocksDB has a large number of features that are not implemented in +Pebble: + +* Backups +* Column families +* Delete files in range +* FIFO compaction style +* Forward iterator / tailing iterator +* Hash table format +* Memtable bloom filter +* Persistent cache +* Pin iterator key / value +* Plain table format +* SSTable ingest-behind +* Sub-compactions +* Transactions +* Universal compaction style + +***WARNING***: Pebble may silently corrupt data or behave incorrectly if +used with a RocksDB database that uses a feature Pebble doesn't +support. Caveat emptor! + +## Production Ready + +Pebble was introduced as an alternative storage engine to RocksDB in +CockroachDB v20.1 (released May 2020) and was used in production +successfully at that time. Pebble was made the default storage engine +in CockroachDB v20.2 (released Nov 2020). Pebble is being used in +production by users of CockroachDB at scale and is considered stable +and production ready. + +## Advantages + +Pebble offers several improvements over RocksDB: + +* Faster reverse iteration via backwards links in the memtable's + skiplist. +* 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 +Differences](docs/rocksdb.md) doc for more details on implementation +differences. + +## RocksDB Compatibility + +Pebble strives for forward compatibility with RocksDB 6.2.1 (the latest +version of RocksDB used by CockroachDB). Forward compatibility means +that a DB generated by RocksDB can be used by Pebble. Currently, Pebble +provides bidirectional compatibility with RocksDB (a Pebble generated DB +can be used by RocksDB) when using its FormatMostCompatible format. New +functionality that is backwards incompatible is gated behind new format +major versions. In general, Pebble only provides compatibility with the +subset of functionality and configuration used by CockroachDB. The scope +of RocksDB functionality and configuration is too large to adequately +test and document all the incompatibilities. The list below contains +known incompatibilities. + +* Pebble's use of WAL recycling is only compatible with RocksDB's + `kTolerateCorruptedTailRecords` WAL recovery mode. Older versions of + RocksDB would automatically map incompatible WAL recovery modes to + `kTolerateCorruptedTailRecords`. New versions of RocksDB will + disable WAL recycling. +* Column families. Pebble does not support column families, nor does + it attempt to detect their usage when opening a DB that may contain + them. +* Hash table format. Pebble does not support the hash table sstable + format. +* Plain table format. Pebble does not support the plain table sstable + format. +* 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 | +| 24.1 plan | FormatSSTableValueBlocks | | + +## Pedigree + +Pebble is based on the incomplete Go version of LevelDB: + +https://github.com/golang/leveldb + +The Go version of LevelDB is based on the C++ original: + +https://github.com/google/leveldb + +Optimizations and inspiration were drawn from RocksDB: + +https://github.com/facebook/rocksdb + +## Getting Started + +### Example Code + +```go +package main + +import ( + "fmt" + "log" + + "github.com/cockroachdb/pebble" +) + +func main() { + db, err := pebble.Open("demo", &pebble.Options{}) + if err != nil { + log.Fatal(err) + } + key := []byte("hello") + if err := db.Set(key, []byte("world"), pebble.Sync); err != nil { + log.Fatal(err) + } + value, closer, err := db.Get(key) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s %s\n", key, value) + if err := closer.Close(); err != nil { + log.Fatal(err) + } + if err := db.Close(); err != nil { + log.Fatal(err) + } +} +``` diff --git a/pebble/batch.go b/pebble/batch.go new file mode 100644 index 0000000..c3dbfcc --- /dev/null +++ b/pebble/batch.go @@ -0,0 +1,2312 @@ +// Copyright 2012 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" + "encoding/binary" + "fmt" + "io" + "math" + "sort" + "sync" + "sync/atomic" + "time" + "unsafe" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/batchskl" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/private" + "github.com/cockroachdb/pebble/internal/rangedel" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/internal/rawalloc" +) + +const ( + batchCountOffset = 8 + batchHeaderLen = 12 + batchInitialSize = 1 << 10 // 1 KB + batchMaxRetainedSize = 1 << 20 // 1 MB + invalidBatchCount = 1<<32 - 1 + maxVarintLen32 = 5 +) + +// ErrNotIndexed means that a read operation on a batch failed because the +// batch is not indexed and thus doesn't support reads. +var ErrNotIndexed = errors.New("pebble: batch not indexed") + +// ErrInvalidBatch indicates that a batch is invalid or otherwise corrupted. +var ErrInvalidBatch = base.MarkCorruptionError(errors.New("pebble: invalid batch")) + +// ErrBatchTooLarge indicates that a batch is invalid or otherwise corrupted. +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 +// until Finish is called, hence the name deferred. This struct lets the caller +// copy or encode keys/values directly into the batch representation instead of +// copying into an intermediary buffer then having pebble.Batch copy off of it. +type DeferredBatchOp struct { + index *batchskl.Skiplist + + // Key and Value point to parts of the binary batch representation where + // keys and values should be encoded/copied into. len(Key) and len(Value) + // bytes must be copied into these slices respectively before calling + // Finish(). Changing where these slices point to is not allowed. + Key, Value []byte + offset uint32 +} + +// Finish completes the addition of this batch operation, and adds it to the +// index if necessary. Must be called once (and exactly once) keys/values +// have been filled into Key and Value. Not calling Finish or not +// copying/encoding keys will result in an incomplete index, and calling Finish +// twice may result in a panic. +func (d DeferredBatchOp) Finish() error { + if d.index != nil { + if err := d.index.Add(d.offset); err != nil { + return err + } + } + return nil +} + +// A Batch is a sequence of Sets, Merges, Deletes, DeleteRanges, RangeKeySets, +// RangeKeyUnsets, and/or RangeKeyDeletes that are applied atomically. Batch +// implements the Reader interface, but only an indexed batch supports reading +// (without error) via Get or NewIter. A non-indexed batch will return +// ErrNotIndexed when read from. A batch is not safe for concurrent use, and +// consumers should use a batch per goroutine or provide their own +// synchronization. +// +// # Indexing +// +// Batches can be optionally indexed (see DB.NewIndexedBatch). An indexed batch +// allows iteration via an Iterator (see Batch.NewIter). The iterator provides +// a merged view of the operations in the batch and the underlying +// database. This is implemented by treating the batch as an additional layer +// in the LSM where every entry in the batch is considered newer than any entry +// in the underlying database (batch entries have the InternalKeySeqNumBatch +// bit set). By treating the batch as an additional layer in the LSM, iteration +// supports all batch operations (i.e. Set, Merge, Delete, DeleteRange, +// RangeKeySet, RangeKeyUnset, RangeKeyDelete) with minimal effort. +// +// The same key can be operated on multiple times in a batch, though only the +// latest operation will be visible. For example, Put("a", "b"), Delete("a") +// will cause the key "a" to not be visible in the batch. Put("a", "b"), +// Put("a", "c") will cause a read of "a" to return the value "c". +// +// The batch index is implemented via an skiplist (internal/batchskl). While +// the skiplist implementation is very fast, inserting into an indexed batch is +// significantly slower than inserting into a non-indexed batch. Only use an +// indexed batch if you require reading from it. +// +// # Atomic commit +// +// The operations in a batch are persisted by calling Batch.Commit which is +// equivalent to calling DB.Apply(batch). A batch is committed atomically by +// writing the internal batch representation to the WAL, adding all of the +// batch operations to the memtable associated with the WAL, and then +// incrementing the visible sequence number so that subsequent reads can see +// the effects of the batch operations. If WriteOptions.Sync is true, a call to +// Batch.Commit will guarantee that the batch is persisted to disk before +// returning. See commitPipeline for more on the implementation details. +// +// # Large batches +// +// The size of a batch is limited only by available memory (be aware that +// indexed batches require considerably additional memory for the skiplist +// structure). A given WAL file has a single memtable associated with it (this +// restriction could be removed, but doing so is onerous and complex). And a +// memtable has a fixed size due to the underlying fixed size arena. Note that +// this differs from RocksDB where a memtable can grow arbitrarily large using +// a list of arena chunks. In RocksDB this is accomplished by storing pointers +// in the arena memory, but that isn't possible in Go. +// +// During Batch.Commit, a batch which is larger than a threshold (> +// MemTableSize/2) is wrapped in a flushableBatch and inserted into the queue +// of memtables. A flushableBatch forces WAL to be rotated, but that happens +// anyways when the memtable becomes full so this does not cause significant +// WAL churn. Because the flushableBatch is readable as another layer in the +// LSM, Batch.Commit returns as soon as the flushableBatch has been added to +// the queue of memtables. +// +// Internally, a flushableBatch provides Iterator support by sorting the batch +// contents (the batch is sorted once, when it is added to the memtable +// queue). Sorting the batch contents and insertion of the contents into a +// memtable have the same big-O time, but the constant factor dominates +// here. Sorting is significantly faster and uses significantly less memory. +// +// # Internal representation +// +// The internal batch representation is a contiguous byte buffer with a fixed +// 12-byte header, followed by a series of records. +// +// +-------------+------------+--- ... ---+ +// | SeqNum (8B) | Count (4B) | Entries | +// +-------------+------------+--- ... ---+ +// +// Each record has a 1-byte kind tag prefix, followed by 1 or 2 length prefixed +// strings (varstring): +// +// +-----------+-----------------+-------------------+ +// | Kind (1B) | Key (varstring) | Value (varstring) | +// +-----------+-----------------+-------------------+ +// +// A varstring is a varint32 followed by N bytes of data. The Kind tags are +// exactly those specified by InternalKeyKind. The following table shows the +// format for records of each kind: +// +// InternalKeyKindDelete varstring +// InternalKeyKindLogData varstring +// InternalKeyKindIngestSST varstring +// InternalKeyKindSet varstring varstring +// InternalKeyKindMerge varstring varstring +// InternalKeyKindRangeDelete varstring varstring +// InternalKeyKindRangeKeySet varstring varstring +// InternalKeyKindRangeKeyUnset varstring varstring +// InternalKeyKindRangeKeyDelete varstring varstring +// +// The intuitive understanding here are that the arguments to Delete, Set, +// Merge, DeleteRange and RangeKeyDelete are encoded into the batch. The +// RangeKeySet and RangeKeyUnset operations are slightly more complicated, +// encoding their end key, suffix and value [in the case of RangeKeySet] within +// the Value varstring. For more information on the value encoding for +// RangeKeySet and RangeKeyUnset, see the internal/rangekey package. +// +// The internal batch representation is the on disk format for a batch in the +// 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, + // - 4 bytes for the count: the number of elements in the batch, + // or "\xff\xff\xff\xff" if the batch is invalid, + // - count elements, being: + // - one byte for the kind + // - the varint-string user key, + // - the varint-string value (if kind != delete). + // The sequence number and count are stored in little-endian order. + // + // The data field can be (but is not guaranteed to be) nil for new + // batches. Large batches will set the data field to nil when committed as + // the data has been moved to a flushableBatch and inserted into the queue of + // memtables. + data []byte + cmp Compare + formatKey base.FormatKey + abbreviatedKey AbbreviatedKey + + // An upper bound on required space to add this batch to a memtable. + // Note that although batches are limited to 4 GiB in size, that limit + // applies to len(data), not the memtable size. The upper bound on the + // size of a memtable node is larger than the overhead of the batch's log + // encoding, so memTableSize is larger than len(data) and may overflow a + // uint32. + memTableSize uint64 + + // The db to which the batch will be committed. Do not change this field + // after the batch has been created as it might invalidate internal state. + // Batch.memTableSize is only refreshed if Batch.db is set. Setting db to + // nil once it has been set implies that the Batch has encountered an error. + db *DB + + // The count of records in the batch. This count will be stored in the batch + // data whenever Repr() is called. + count uint64 + + // The count of range deletions in the batch. Updated every time a range + // deletion is added. + countRangeDels uint64 + + // The count of range key sets, unsets and deletes in the batch. Updated + // every time a RANGEKEYSET, RANGEKEYUNSET or RANGEKEYDEL key is added. + countRangeKeys uint64 + + // A deferredOp struct, stored in the Batch so that a pointer can be returned + // from the *Deferred() methods rather than a value. + deferredOp DeferredBatchOp + + // An optional skiplist keyed by offset into data of the entry. + index *batchskl.Skiplist + rangeDelIndex *batchskl.Skiplist + rangeKeyIndex *batchskl.Skiplist + + // Fragmented range deletion tombstones. Cached the first time a range + // deletion iterator is requested. The cache is invalidated whenever a new + // range deletion is added to the batch. This cache can only be used when + // opening an iterator to read at a batch sequence number >= + // tombstonesSeqNum. This is the case for all new iterators created over a + // batch but it's not the case for all cloned iterators. + tombstones []keyspan.Span + tombstonesSeqNum uint64 + + // Fragmented range key spans. Cached the first time a range key iterator is + // requested. The cache is invalidated whenever a new range key + // (RangeKey{Set,Unset,Del}) is added to the batch. This cache can only be + // used when opening an iterator to read at a batch sequence number >= + // tombstonesSeqNum. This is the case for all new iterators created over a + // batch but it's not the case for all cloned iterators. + rangeKeys []keyspan.Span + rangeKeysSeqNum uint64 + + // The flushableBatch wrapper if the batch is too large to fit in the + // memtable. + flushable *flushableBatch + + // 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 + // ApplyNoSyncWait, which implies WriteOptions.Sync is true, uses the commit + // WaitGroup for publishing the seqnum and the fsyncWait WaitGroup for + // waiting for the WAL fsync. + // + // TODO(sumeer): if we find that ApplyNoSyncWait in conjunction with + // SyncWait is causing higher memory usage because of the time duration + // between when the sync is already done, and a goroutine calls SyncWait + // (followed by Batch.Close), we could separate out {fsyncWait, commitErr} + // into a separate struct that is allocated separately (using another + // sync.Pool), and only that struct needs to outlive Batch.Close (which + // could then be called immediately after ApplyNoSyncWait). commitStats + // will also need to be in this separate struct. + commit sync.WaitGroup + fsyncWait sync.WaitGroup + + commitStats BatchCommitStats + + commitErr error + + // 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. +// +// NB: there is no Pebble internal tracing (using LoggerAndTracer) of slow +// batch commits. The caller can use these stats to do their own tracing as +// needed. +type BatchCommitStats struct { + // TotalDuration is the time spent in DB.{Apply,ApplyNoSyncWait} or + // Batch.Commit, plus the time waiting in Batch.SyncWait. If there is a gap + // between calling ApplyNoSyncWait and calling SyncWait, that gap could + // include some duration in which real work was being done for the commit + // and will not be included here. This missing time is considered acceptable + // since the goal of these stats is to understand user-facing latency. + // + // TotalDuration includes time spent in various queues both inside Pebble + // and outside Pebble (I/O queues, goroutine scheduler queue, mutex wait + // etc.). For some of these queues (which we consider important) the wait + // times are included below -- these expose low-level implementation detail + // and are meant for expert diagnosis and subject to change. There may be + // unaccounted time after subtracting those values from TotalDuration. + TotalDuration time.Duration + // SemaphoreWaitDuration is the wait time for semaphores in + // 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). 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). + MemTableWriteStallDuration time.Duration + // L0ReadAmpWriteStallDuration is the wait caused by a write stall due to + // high read amplification in L0 (due to not compacting fast enough out of + // L0). + L0ReadAmpWriteStallDuration time.Duration + // WALRotationDuration is the wait time for WAL rotation, which includes + // syncing and closing the old WAL and creating (or reusing) a new one. + WALRotationDuration time.Duration + // CommitWaitDuration is the wait for publishing the seqnum plus the + // duration for the WAL sync (if requested). The former should be tiny and + // one can assume that this is all due to the WAL sync. + CommitWaitDuration time.Duration +} + +var _ Reader = (*Batch)(nil) +var _ Writer = (*Batch)(nil) + +var batchPool = sync.Pool{ + New: func() interface{} { + return &Batch{} + }, +} + +type indexedBatch struct { + batch Batch + index batchskl.Skiplist +} + +var indexedBatchPool = sync.Pool{ + New: func() interface{} { + return &indexedBatch{} + }, +} + +func newBatch(db *DB) *Batch { + b := batchPool.Get().(*Batch) + b.db = db + 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 + i.batch.formatKey = comparer.FormatKey + i.batch.abbreviatedKey = comparer.AbbreviatedKey + i.batch.db = db + i.batch.index = &i.index + i.batch.index.Init(&i.batch.data, i.batch.cmp, i.batch.abbreviatedKey) + 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 +// with the base.InternalKeySeqNumBatch bit. These sequence numbers are only +// used during iteration, and the keys are assigned ordinary sequence numbers +// when the batch is committed. +func (b *Batch) nextSeqNum() uint64 { + return uint64(len(b.data)) | base.InternalKeySeqNumBatch +} + +func (b *Batch) release() { + if b.db == nil { + // The batch was not created using newBatch or newIndexedBatch, or an error + // was encountered. We don't try to reuse batches that encountered an error + // because they might be stuck somewhere in the system and attempting to + // reuse such batches is a recipe for onerous debugging sessions. Instead, + // let the GC do its job. + return + } + b.db = nil + + // NB: This is ugly (it would be cleaner if we could just assign a Batch{}), + // but necessary so that we can use atomic.StoreUint32 for the Batch.applied + // field. Without using an atomic to clear that field the Go race detector + // complains. + b.Reset() + b.cmp = nil + b.formatKey = nil + b.abbreviatedKey = nil + + if b.index == nil { + batchPool.Put(b) + } else { + b.index, b.rangeDelIndex, b.rangeKeyIndex = nil, nil, nil + indexedBatchPool.Put((*indexedBatch)(unsafe.Pointer(b))) + } +} + +func (b *Batch) refreshMemTableSize() error { + b.memTableSize = 0 + if len(b.data) < batchHeaderLen { + return nil + } + + b.countRangeDels = 0 + b.countRangeKeys = 0 + b.minimumFormatMajorVersion = 0 + for r := b.Reader(); ; { + kind, key, value, ok, err := r.Next() + if !ok { + if err != nil { + return err + } + break + } + switch kind { + case InternalKeyKindRangeDelete: + 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. +// +// It is safe to modify the contents of the arguments after Apply returns. +func (b *Batch) Apply(batch *Batch, _ *WriteOptions) error { + if b.ingestedSSTBatch { + panic("pebble: invalid batch application") + } + if len(batch.data) == 0 { + return nil + } + if len(batch.data) < batchHeaderLen { + return ErrInvalidBatch + } + + offset := len(b.data) + if offset == 0 { + b.init(offset) + offset = batchHeaderLen + } + b.data = append(b.data, batch.data[batchHeaderLen:]...) + + b.setCount(b.Count() + batch.Count()) + + if b.db != nil || b.index != nil { + // Only iterate over the new entries if we need to track memTableSize or in + // 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, err := iter.Next() + if !ok { + if err != nil { + return err + } + break + } + switch kind { + case InternalKeyKindRangeDelete: + b.countRangeDels++ + case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + b.countRangeKeys++ + case InternalKeyKindIngestSST: + panic("pebble: invalid key kind for batch") + } + if b.index != nil { + var err error + switch kind { + case InternalKeyKindRangeDelete: + b.tombstones = nil + b.tombstonesSeqNum = 0 + if b.rangeDelIndex == nil { + b.rangeDelIndex = batchskl.NewSkiplist(&b.data, b.cmp, b.abbreviatedKey) + } + err = b.rangeDelIndex.Add(uint32(offset)) + case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + b.rangeKeys = nil + b.rangeKeysSeqNum = 0 + if b.rangeKeyIndex == nil { + b.rangeKeyIndex = batchskl.NewSkiplist(&b.data, b.cmp, b.abbreviatedKey) + } + err = b.rangeKeyIndex.Add(uint32(offset)) + default: + err = b.index.Add(uint32(offset)) + } + if err != nil { + return err + } + } + b.memTableSize += memTableEntrySize(len(key), len(value)) + } + } + return nil +} + +// Get gets the value for the given key. It returns ErrNotFound if the Batch +// does not contain the key. +// +// The caller should not modify the contents of the returned slice, but it is +// safe to modify the contents of the argument after Get returns. The returned +// slice will remain valid until the returned Closer is closed. On success, the +// caller MUST call closer.Close() or a memory leak will occur. +func (b *Batch) Get(key []byte) ([]byte, io.Closer, error) { + if b.index == nil { + return nil, nil, ErrNotIndexed + } + return b.db.getInternal(key, b, nil /* snapshot */) +} + +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) + } + b.count++ + b.memTableSize += memTableEntrySize(keyLen, valueLen) + + pos := len(b.data) + b.deferredOp.offset = uint32(pos) + b.grow(1 + 2*maxVarintLen32 + keyLen + valueLen) + b.data[pos] = byte(kind) + pos++ + + { + // TODO(peter): Manually inlined version binary.PutUvarint(). This is 20% + // faster on BenchmarkBatchSet on go1.13. Remove if go1.14 or future + // versions show this to not be a performance win. + x := uint32(keyLen) + for x >= 0x80 { + b.data[pos] = byte(x) | 0x80 + x >>= 7 + pos++ + } + b.data[pos] = byte(x) + pos++ + } + + b.deferredOp.Key = b.data[pos : pos+keyLen] + pos += keyLen + + { + // TODO(peter): Manually inlined version binary.PutUvarint(). This is 20% + // faster on BenchmarkBatchSet on go1.13. Remove if go1.14 or future + // versions show this to not be a performance win. + x := uint32(valueLen) + for x >= 0x80 { + b.data[pos] = byte(x) | 0x80 + x >>= 7 + pos++ + } + b.data[pos] = byte(x) + pos++ + } + + b.deferredOp.Value = b.data[pos : pos+valueLen] + // Shrink data since varints may be shorter than the upper bound. + b.data = b.data[:pos+valueLen] +} + +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) + } + b.count++ + b.memTableSize += memTableEntrySize(keyLen, 0) + + pos := len(b.data) + b.deferredOp.offset = uint32(pos) + b.grow(1 + maxVarintLen32 + keyLen) + b.data[pos] = byte(kind) + pos++ + + { + // TODO(peter): Manually inlined version binary.PutUvarint(). Remove if + // go1.13 or future versions show this to not be a performance win. See + // BenchmarkBatchSet. + x := uint32(keyLen) + for x >= 0x80 { + b.data[pos] = byte(x) | 0x80 + x >>= 7 + pos++ + } + b.data[pos] = byte(x) + pos++ + } + + b.deferredOp.Key = b.data[pos : pos+keyLen] + b.deferredOp.Value = nil + + // Shrink data since varint may be shorter than the upper bound. + b.data = b.data[:pos+keyLen] +} + +// AddInternalKey allows the caller to add an internal key of point key or range +// key kinds (but not RangeDelete) to a batch. Passing in an internal key of +// kind 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 kind := key.Kind(); kind { + case InternalKeyKindRangeDelete: + panic("unexpected range delete in AddInternalKey") + case InternalKeyKindSingleDelete, InternalKeyKindDelete: + b.prepareDeferredKeyRecord(keyLen, kind) + b.deferredOp.index = b.index + case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + b.prepareDeferredKeyValueRecord(keyLen, len(value), kind) + hasValue = true + b.incrementRangeKeysCount() + default: + b.prepareDeferredKeyValueRecord(keyLen, len(value), 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. +func (b *Batch) Set(key, value []byte, _ *WriteOptions) error { + deferredOp := b.SetDeferred(len(key), len(value)) + copy(deferredOp.Key, key) + copy(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(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +// SetDeferred is similar to Set in that it adds a set operation to the batch, +// except it only takes in key/value lengths instead of complete slices, +// letting the caller encode into those objects and then call Finish() on the +// returned object. +func (b *Batch) SetDeferred(keyLen, valueLen int) *DeferredBatchOp { + b.prepareDeferredKeyValueRecord(keyLen, valueLen, InternalKeyKindSet) + b.deferredOp.index = b.index + return &b.deferredOp +} + +// Merge adds an action to the batch that merges the value at key with the new +// value. The details of the merge are dependent upon the configured merge +// operator. +// +// It is safe to modify the contents of the arguments after Merge returns. +func (b *Batch) Merge(key, value []byte, _ *WriteOptions) error { + deferredOp := b.MergeDeferred(len(key), len(value)) + copy(deferredOp.Key, key) + copy(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(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +// MergeDeferred is similar to Merge in that it adds a merge operation to the +// batch, except it only takes in key/value lengths instead of complete slices, +// letting the caller encode into those objects and then call Finish() on the +// returned object. +func (b *Batch) MergeDeferred(keyLen, valueLen int) *DeferredBatchOp { + b.prepareDeferredKeyValueRecord(keyLen, valueLen, InternalKeyKindMerge) + b.deferredOp.index = b.index + return &b.deferredOp +} + +// Delete adds an action to the batch that deletes the entry for key. +// +// It is safe to modify the contents of the arguments after Delete returns. +func (b *Batch) Delete(key []byte, _ *WriteOptions) error { + deferredOp := b.DeleteDeferred(len(key)) + copy(deferredOp.Key, key) + // 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(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +// DeleteDeferred is similar to Delete in that it adds a delete operation to +// the batch, except it only takes in key/value lengths instead of complete +// slices, letting the caller encode into those objects and then call Finish() +// on the returned object. +func (b *Batch) DeleteDeferred(keyLen int) *DeferredBatchOp { + b.prepareDeferredKeyRecord(keyLen, InternalKeyKindDelete) + b.deferredOp.index = b.index + 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. +// +// It is safe to modify the contents of the arguments after SingleDelete returns. +func (b *Batch) SingleDelete(key []byte, _ *WriteOptions) error { + deferredOp := b.SingleDeleteDeferred(len(key)) + copy(deferredOp.Key, key) + // 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(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +// SingleDeleteDeferred is similar to SingleDelete in that it adds a single delete +// operation to the batch, except it only takes in key/value lengths instead of +// complete slices, letting the caller encode into those objects and then call +// Finish() on the returned object. +func (b *Batch) SingleDeleteDeferred(keyLen int) *DeferredBatchOp { + b.prepareDeferredKeyRecord(keyLen, InternalKeyKindSingleDelete) + b.deferredOp.index = b.index + return &b.deferredOp +} + +// DeleteRange deletes all of the point keys (and values) in the range +// [start,end) (inclusive on start, exclusive on end). DeleteRange does NOT +// delete overlapping range keys (eg, keys set via RangeKeySet). +// +// It is safe to modify the contents of the arguments after DeleteRange +// returns. +func (b *Batch) DeleteRange(start, end []byte, _ *WriteOptions) error { + deferredOp := b.DeleteRangeDeferred(len(start), len(end)) + copy(deferredOp.Key, start) + copy(deferredOp.Value, end) + // TODO(peter): Manually inline DeferredBatchOp.Finish(). Mid-stack inlining + // in go1.13 will remove the need for this. + if deferredOp.index != nil { + if err := deferredOp.index.Add(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +// DeleteRangeDeferred is similar to DeleteRange in that it adds a delete range +// operation to the batch, except it only takes in key lengths instead of +// complete slices, letting the caller encode into those objects and then call +// Finish() on the returned object. Note that DeferredBatchOp.Key should be +// populated with the start key, and DeferredBatchOp.Value should be populated +// with the end key. +func (b *Batch) DeleteRangeDeferred(startLen, endLen int) *DeferredBatchOp { + b.prepareDeferredKeyValueRecord(startLen, endLen, InternalKeyKindRangeDelete) + b.countRangeDels++ + if b.index != nil { + b.tombstones = nil + b.tombstonesSeqNum = 0 + // Range deletions are rare, so we lazily allocate the index for them. + if b.rangeDelIndex == nil { + b.rangeDelIndex = batchskl.NewSkiplist(&b.data, b.cmp, b.abbreviatedKey) + } + b.deferredOp.index = b.rangeDelIndex + } + return &b.deferredOp +} + +// RangeKeySet sets a range key mapping the key range [start, end) at the MVCC +// timestamp suffix to value. The suffix is optional. If any portion of the key +// range [start, end) is already set by a range key with the same suffix value, +// RangeKeySet overrides it. +// +// It is safe to modify the contents of the arguments after RangeKeySet returns. +func (b *Batch) RangeKeySet(start, end, suffix, value []byte, _ *WriteOptions) error { + suffixValues := [1]rangekey.SuffixValue{{Suffix: suffix, Value: value}} + internalValueLen := rangekey.EncodedSetValueLen(end, suffixValues[:]) + + deferredOp := b.rangeKeySetDeferred(len(start), internalValueLen) + copy(deferredOp.Key, start) + n := rangekey.EncodeSetValue(deferredOp.Value, end, suffixValues[:]) + if n != internalValueLen { + panic("unexpected internal value length mismatch") + } + + // Manually inline DeferredBatchOp.Finish(). + if deferredOp.index != nil { + if err := deferredOp.index.Add(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +func (b *Batch) rangeKeySetDeferred(startLen, internalValueLen int) *DeferredBatchOp { + b.prepareDeferredKeyValueRecord(startLen, internalValueLen, InternalKeyKindRangeKeySet) + b.incrementRangeKeysCount() + return &b.deferredOp +} + +func (b *Batch) incrementRangeKeysCount() { + b.countRangeKeys++ + if b.minimumFormatMajorVersion < FormatRangeKeys { + b.minimumFormatMajorVersion = FormatRangeKeys + } + if b.index != nil { + b.rangeKeys = nil + b.rangeKeysSeqNum = 0 + // Range keys are rare, so we lazily allocate the index for them. + if b.rangeKeyIndex == nil { + b.rangeKeyIndex = batchskl.NewSkiplist(&b.data, b.cmp, b.abbreviatedKey) + } + b.deferredOp.index = b.rangeKeyIndex + } +} + +// RangeKeyUnset removes a range key mapping the key range [start, end) at the +// MVCC timestamp suffix. The suffix may be omitted to remove an unsuffixed +// range key. RangeKeyUnset only removes portions of range keys that fall within +// the [start, end) key span, and only range keys with suffixes that exactly +// match the unset suffix. +// +// It is safe to modify the contents of the arguments after RangeKeyUnset +// returns. +func (b *Batch) RangeKeyUnset(start, end, suffix []byte, _ *WriteOptions) error { + suffixes := [1][]byte{suffix} + internalValueLen := rangekey.EncodedUnsetValueLen(end, suffixes[:]) + + deferredOp := b.rangeKeyUnsetDeferred(len(start), internalValueLen) + copy(deferredOp.Key, start) + n := rangekey.EncodeUnsetValue(deferredOp.Value, end, suffixes[:]) + if n != internalValueLen { + panic("unexpected internal value length mismatch") + } + + // Manually inline DeferredBatchOp.Finish() + if deferredOp.index != nil { + if err := deferredOp.index.Add(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +func (b *Batch) rangeKeyUnsetDeferred(startLen, internalValueLen int) *DeferredBatchOp { + b.prepareDeferredKeyValueRecord(startLen, internalValueLen, InternalKeyKindRangeKeyUnset) + b.incrementRangeKeysCount() + return &b.deferredOp +} + +// RangeKeyDelete deletes all of the range keys in the range [start,end) +// (inclusive on start, exclusive on end). It does not delete point keys (for +// that use DeleteRange). RangeKeyDelete removes all range keys within the +// bounds, including those with or without suffixes. +// +// It is safe to modify the contents of the arguments after RangeKeyDelete +// returns. +func (b *Batch) RangeKeyDelete(start, end []byte, _ *WriteOptions) error { + deferredOp := b.RangeKeyDeleteDeferred(len(start), len(end)) + copy(deferredOp.Key, start) + copy(deferredOp.Value, end) + // Manually inline DeferredBatchOp.Finish(). + if deferredOp.index != nil { + if err := deferredOp.index.Add(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +// RangeKeyDeleteDeferred is similar to RangeKeyDelete in that it adds an +// operation to delete range keys to the batch, except it only takes in key +// lengths instead of complete slices, letting the caller encode into those +// objects and then call Finish() on the returned object. Note that +// DeferredBatchOp.Key should be populated with the start key, and +// DeferredBatchOp.Value should be populated with the end key. +func (b *Batch) RangeKeyDeleteDeferred(startLen, endLen int) *DeferredBatchOp { + b.prepareDeferredKeyValueRecord(startLen, endLen, InternalKeyKindRangeKeyDelete) + b.incrementRangeKeysCount() + return &b.deferredOp +} + +// LogData adds the specified to the batch. The data will be written to the +// WAL, but not added to memtables or sstables. Log data is never indexed, +// which makes it useful for testing WAL performance. +// +// It is safe to modify the contents of the argument after LogData returns. +func (b *Batch) LogData(data []byte, _ *WriteOptions) error { + origCount, origMemTableSize := b.count, b.memTableSize + b.prepareDeferredKeyRecord(len(data), InternalKeyKindLogData) + copy(b.deferredOp.Key, data) + // Since LogData only writes to the WAL and does not affect the memtable, we + // restore b.count and b.memTableSize to their origin values. Note that + // Batch.count only refers to records that are added to the memtable. + b.count, b.memTableSize = origCount, origMemTableSize + return nil +} + +// IngestSST adds the FileNum for an sstable to the batch. The data will only be +// written to the WAL (not added to memtables or sstables). +func (b *Batch) ingestSST(fileNum base.FileNum) { + if b.Empty() { + b.ingestedSSTBatch = true + } else if !b.ingestedSSTBatch { + // Batch contains other key kinds. + panic("pebble: invalid call to ingestSST") + } + + origMemTableSize := b.memTableSize + var buf [binary.MaxVarintLen64]byte + length := binary.PutUvarint(buf[:], uint64(fileNum)) + b.prepareDeferredKeyRecord(length, InternalKeyKindIngestSST) + copy(b.deferredOp.Key, buf[:length]) + // Since IngestSST writes only to the WAL and does not affect the memtable, + // we restore b.memTableSize to its original value. Note that Batch.count + // 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. +func (b *Batch) Empty() bool { + return len(b.data) <= batchHeaderLen +} + +// Len returns the current size of the batch in bytes. +func (b *Batch) Len() int { + if len(b.data) <= batchHeaderLen { + return batchHeaderLen + } + return len(b.data) +} + +// Repr returns the underlying batch representation. It is not safe to modify +// the contents. Reset() will not change the contents of the returned value, +// though any other mutation operation may do so. +func (b *Batch) Repr() []byte { + if len(b.data) == 0 { + b.init(batchHeaderLen) + } + binary.LittleEndian.PutUint32(b.countData(), b.Count()) + return b.data +} + +// SetRepr sets the underlying batch representation. The batch takes ownership +// of the supplied slice. It is not safe to modify it afterwards until the +// Batch is no longer in use. +func (b *Batch) SetRepr(data []byte) error { + if len(data) < batchHeaderLen { + return base.CorruptionErrorf("invalid batch") + } + 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. + err = b.refreshMemTableSize() + } + return err +} + +// NewIter returns an iterator that is unpositioned (Iterator.Valid() will +// return false). The iterator can be positioned via a call to SeekGE, +// SeekPrefixGE, SeekLT, First or Last. Only indexed batches support iterators. +// +// 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, error) { + return b.NewIterWithContext(context.Background(), o) +} + +// NewIterWithContext is like NewIter, and additionally accepts a context for +// tracing. +func (b *Batch) NewIterWithContext(ctx context.Context, o *IterOptions) (*Iterator, error) { + if b.index == nil { + return nil, ErrNotIndexed + } + return b.db.newIter(ctx, b, newIterOpts{}, o), nil +} + +// NewBatchOnlyIter constructs an iterator that only reads the contents of the +// batch, and does not overlay the batch mutations on top of the DB state. +// +// 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) NewBatchOnlyIter(ctx context.Context, o *IterOptions) (*Iterator, error) { + if b.index == nil { + return nil, ErrNotIndexed + } + return b.db.newIter(ctx, b, newIterOpts{batch: batchIterOpts{batchOnly: true}}, o), nil +} + +// newInternalIter creates a new internalIterator that iterates over the +// contents of the batch. +func (b *Batch) newInternalIter(o *IterOptions) *batchIter { + iter := &batchIter{} + b.initInternalIter(o, iter) + return iter +} + +func (b *Batch) initInternalIter(o *IterOptions, iter *batchIter) { + *iter = batchIter{ + cmp: b.cmp, + batch: b, + iter: b.index.NewIter(o.GetLowerBound(), o.GetUpperBound()), + // NB: We explicitly do not propagate the batch snapshot to the point + // key iterator. Filtering point keys within the batch iterator can + // cause pathological behavior where a batch iterator advances + // significantly farther than necessary filtering many batch keys that + // are not visible at the batch sequence number. Instead, the merging + // iterator enforces bounds. + // + // For example, consider an engine that contains the committed keys + // 'bar' and 'bax', with no keys between them. Consider a batch + // containing keys 1,000 keys within the range [a,z]. All of the + // batch keys were added to the batch after the iterator was + // constructed, so they are not visible to the iterator. A call to + // SeekGE('bax') would seek the LSM iterators and discover the key + // 'bax'. It would also seek the batch iterator, landing on the key + // 'baz' but discover it that it's not visible. The batch iterator would + // next through the rest of the batch's keys, only to discover there are + // no visible keys greater than or equal to 'bax'. + // + // Filtering these batch points within the merging iterator ensures that + // the batch iterator never needs to iterate beyond 'baz', because it + // already found a smaller, visible key 'bax'. + snapshot: base.InternalKeySeqNumMax, + } +} + +func (b *Batch) newRangeDelIter(o *IterOptions, batchSnapshot uint64) *keyspan.Iter { + // Construct an iterator even if rangeDelIndex is nil, because it is allowed + // to refresh later, so we need the container to exist. + iter := new(keyspan.Iter) + b.initRangeDelIter(o, iter, batchSnapshot) + return iter +} + +func (b *Batch) initRangeDelIter(_ *IterOptions, iter *keyspan.Iter, batchSnapshot uint64) { + if b.rangeDelIndex == nil { + iter.Init(b.cmp, nil) + return + } + + // Fragment the range tombstones the first time a range deletion iterator is + // requested. The cached tombstones are invalidated if another range + // deletion tombstone is added to the batch. This cache is only guaranteed + // to be correct if we're opening an iterator to read at a batch sequence + // number at least as high as tombstonesSeqNum. The cache is guaranteed to + // include all tombstones up to tombstonesSeqNum, and if any additional + // tombstones were added after that sequence number the cache would've been + // cleared. + nextSeqNum := b.nextSeqNum() + if b.tombstones != nil && b.tombstonesSeqNum <= batchSnapshot { + iter.Init(b.cmp, b.tombstones) + return + } + + tombstones := make([]keyspan.Span, 0, b.countRangeDels) + frag := &keyspan.Fragmenter{ + Cmp: b.cmp, + Format: b.formatKey, + Emit: func(s keyspan.Span) { + tombstones = append(tombstones, s) + }, + } + it := &batchIter{ + cmp: b.cmp, + batch: b, + iter: b.rangeDelIndex.NewIter(nil, nil), + snapshot: batchSnapshot, + } + fragmentRangeDels(frag, it, int(b.countRangeDels)) + iter.Init(b.cmp, tombstones) + + // If we just read all the tombstones in the batch (eg, batchSnapshot was + // set to b.nextSeqNum()), then cache the tombstones so that a subsequent + // call to initRangeDelIter may use them without refragmenting. + if nextSeqNum == batchSnapshot { + b.tombstones = tombstones + b.tombstonesSeqNum = nextSeqNum + } +} + +func fragmentRangeDels(frag *keyspan.Fragmenter, it internalIterator, count int) { + // The memory management here is a bit subtle. The keys and values returned + // by the iterator are slices in Batch.data. Thus the fragmented tombstones + // are slices within Batch.data. If additional entries are added to the + // Batch, Batch.data may be reallocated. The references in the fragmented + // tombstones will remain valid, pointing into the old Batch.data. GC for + // the win. + + // Use a single []keyspan.Key buffer to avoid allocating many + // individual []keyspan.Key slices with a single element each. + keyBuf := make([]keyspan.Key, 0, count) + for key, val := it.First(); key != nil; key, val = it.Next() { + s := rangedel.Decode(*key, val.InPlaceValue(), keyBuf) + keyBuf = s.Keys[len(s.Keys):] + + // Set a fixed capacity to avoid accidental overwriting. + s.Keys = s.Keys[:len(s.Keys):len(s.Keys)] + frag.Add(s) + } + frag.Finish() +} + +func (b *Batch) newRangeKeyIter(o *IterOptions, batchSnapshot uint64) *keyspan.Iter { + // Construct an iterator even if rangeKeyIndex is nil, because it is allowed + // to refresh later, so we need the container to exist. + iter := new(keyspan.Iter) + b.initRangeKeyIter(o, iter, batchSnapshot) + return iter +} + +func (b *Batch) initRangeKeyIter(_ *IterOptions, iter *keyspan.Iter, batchSnapshot uint64) { + if b.rangeKeyIndex == nil { + iter.Init(b.cmp, nil) + return + } + + // Fragment the range keys the first time a range key iterator is requested. + // The cached spans are invalidated if another range key is added to the + // batch. This cache is only guaranteed to be correct if we're opening an + // iterator to read at a batch sequence number at least as high as + // rangeKeysSeqNum. The cache is guaranteed to include all range keys up to + // rangeKeysSeqNum, and if any additional range keys were added after that + // sequence number the cache would've been cleared. + nextSeqNum := b.nextSeqNum() + if b.rangeKeys != nil && b.rangeKeysSeqNum <= batchSnapshot { + iter.Init(b.cmp, b.rangeKeys) + return + } + + rangeKeys := make([]keyspan.Span, 0, b.countRangeKeys) + frag := &keyspan.Fragmenter{ + Cmp: b.cmp, + Format: b.formatKey, + Emit: func(s keyspan.Span) { + rangeKeys = append(rangeKeys, s) + }, + } + it := &batchIter{ + cmp: b.cmp, + batch: b, + iter: b.rangeKeyIndex.NewIter(nil, nil), + snapshot: batchSnapshot, + } + fragmentRangeKeys(frag, it, int(b.countRangeKeys)) + iter.Init(b.cmp, rangeKeys) + + // If we just read all the range keys in the batch (eg, batchSnapshot was + // set to b.nextSeqNum()), then cache the range keys so that a subsequent + // call to initRangeKeyIter may use them without refragmenting. + if nextSeqNum == batchSnapshot { + b.rangeKeys = rangeKeys + b.rangeKeysSeqNum = nextSeqNum + } +} + +func fragmentRangeKeys(frag *keyspan.Fragmenter, it internalIterator, count int) error { + // The memory management here is a bit subtle. The keys and values + // returned by the iterator are slices in Batch.data. Thus the + // fragmented key spans are slices within Batch.data. If additional + // entries are added to the Batch, Batch.data may be reallocated. The + // references in the fragmented keys will remain valid, pointing into + // the old Batch.data. GC for the win. + + // Use a single []keyspan.Key buffer to avoid allocating many + // individual []keyspan.Key slices with a single element each. + keyBuf := make([]keyspan.Key, 0, count) + for ik, val := it.First(); ik != nil; ik, val = it.Next() { + s, err := rangekey.Decode(*ik, val.InPlaceValue(), keyBuf) + if err != nil { + return err + } + keyBuf = s.Keys[len(s.Keys):] + + // Set a fixed capacity to avoid accidental overwriting. + s.Keys = s.Keys[:len(s.Keys):len(s.Keys)] + frag.Add(s) + } + frag.Finish() + return nil +} + +// Commit applies the batch to its parent writer. +func (b *Batch) Commit(o *WriteOptions) error { + return b.db.Apply(b, o) +} + +// Close closes the batch without committing it. +func (b *Batch) Close() error { + b.release() + return nil +} + +// Indexed returns true if the batch is indexed (i.e. supports read +// operations). +func (b *Batch) Indexed() bool { + return b.index != nil +} + +// 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 < size { + n *= 2 + } + if cap(b.data) < n { + b.data = rawalloc.New(batchHeaderLen, n) + } + b.setCount(0) + b.setSeqNum(0) + b.data = b.data[:batchHeaderLen] +} + +// Reset resets the batch for reuse. The underlying byte slice (that is +// 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() { + // 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 + // retention size, don't re-use it. Let it be GC-ed instead. + // This prevents the memory from an unusually large batch from + // being held on to indefinitely. + b.data = nil + } else { + // Otherwise, reset the buffer for re-use. + b.data = b.data[:batchHeaderLen] + b.setSeqNum(0) + } + } + if b.index != nil { + b.index.Init(&b.data, b.cmp, b.abbreviatedKey) + } +} + +// seqNumData returns the 8 byte little-endian sequence number. Zero means that +// the batch has not yet been applied. +func (b *Batch) seqNumData() []byte { + return b.data[:8] +} + +// countData returns the 4 byte little-endian count data. "\xff\xff\xff\xff" +// means that the batch is invalid. +func (b *Batch) countData() []byte { + return b.data[8:12] +} + +func (b *Batch) grow(n int) { + newSize := len(b.data) + n + if uint64(newSize) >= maxBatchSize { + panic(ErrBatchTooLarge) + } + if newSize > cap(b.data) { + newCap := 2 * cap(b.data) + for newCap < newSize { + newCap *= 2 + } + newData := rawalloc.New(len(b.data), newCap) + copy(newData, b.data) + b.data = newData + } + b.data = b.data[:newSize] +} + +func (b *Batch) setSeqNum(seqNum uint64) { + binary.LittleEndian.PutUint64(b.seqNumData(), seqNum) +} + +// SeqNum returns the batch sequence number which is applied to the first +// record in the batch. The sequence number is incremented for each subsequent +// record. It returns zero if the batch is empty. +func (b *Batch) SeqNum() uint64 { + if len(b.data) == 0 { + b.init(batchHeaderLen) + } + return binary.LittleEndian.Uint64(b.seqNumData()) +} + +func (b *Batch) setCount(v uint32) { + b.count = uint64(v) +} + +// Count returns the count of memtable-modifying operations in this batch. All +// operations with the except of LogData increment this count. For IngestSSTs, +// count is only used to indicate the number of SSTs ingested in the record, the +// batch isn't applied to the memtable. +func (b *Batch) Count() uint32 { + if b.count > math.MaxUint32 { + panic(ErrInvalidBatch) + } + return uint32(b.count) +} + +// Reader returns a BatchReader for the current batch contents. If the batch is +// mutated, the new entries will not be visible to the reader. +func (b *Batch) Reader() BatchReader { + if len(b.data) == 0 { + b.init(batchHeaderLen) + } + return b.data[batchHeaderLen:] +} + +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]) + if a := *((*uint8)(ptr)); a < 128 { + v = uint32(a) + n = 1 + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + v = uint32(b)<<7 | uint32(a) + n = 2 + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + v = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + n = 3 + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + v = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + n = 4 + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + v = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + n = 5 + } + + data = data[n:] + if v > uint32(len(data)) { + return nil, nil, false + } + return data[v:], data[:v], true +} + +// SyncWait is to be used in conjunction with DB.ApplyNoSyncWait. +func (b *Batch) SyncWait() error { + now := time.Now() + b.fsyncWait.Wait() + if b.commitErr != nil { + b.db = nil // prevent batch reuse on error + } + waitDuration := time.Since(now) + b.commitStats.CommitWaitDuration += waitDuration + b.commitStats.TotalDuration += waitDuration + return b.commitErr +} + +// CommitStats returns stats related to committing the batch. Should be called +// after Batch.Commit, DB.Apply. If DB.ApplyNoSyncWait is used, should be +// called after Batch.SyncWait. +func (b *Batch) CommitStats() BatchCommitStats { + return b.commitStats +} + +// BatchReader iterates over the entries contained in a batch. +type BatchReader []byte + +// ReadBatch constructs a BatchReader from a batch representation. The +// header is not validated. ReadBatch returns a new batch reader and the +// count of entries contained within the batch. +func ReadBatch(repr []byte) (r BatchReader, count uint32) { + if len(repr) <= batchHeaderLen { + return nil, count + } + count = binary.LittleEndian.Uint32(repr[batchCountOffset:batchHeaderLen]) + return repr[batchHeaderLen:], count +} + +// 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, nil + } + kind = InternalKeyKind((*r)[0]) + if kind > InternalKeyKindMax { + 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, errors.Wrapf(ErrInvalidBatch, "decoding user key") + } + switch kind { + case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, + InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete, + InternalKeyKindDeleteSized: + *r, value, ok = batchDecodeStr(*r) + if !ok { + return 0, nil, nil, false, errors.Wrapf(ErrInvalidBatch, "decoding %s value", kind) + } + } + return kind, ukey, value, true, nil +} + +// Note: batchIter mirrors the implementation of flushableBatchIter. Keep the +// two in sync. +type batchIter struct { + cmp Compare + batch *Batch + iter batchskl.Iterator + err error + // snapshot holds a batch "sequence number" at which the batch is being + // read. This sequence number has the InternalKeySeqNumBatch bit set, so it + // encodes an offset within the batch. Only batch entries earlier than the + // offset are visible during iteration. + snapshot uint64 +} + +// batchIter implements the base.InternalIterator interface. +var _ base.InternalIterator = (*batchIter)(nil) + +func (i *batchIter) String() string { + return "batch" +} + +func (i *batchIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, base.LazyValue) { + // Ignore TrySeekUsingNext if the view of the batch changed. + if flags.TrySeekUsingNext() && flags.BatchJustRefreshed() { + flags = flags.DisableTrySeekUsingNext() + } + + i.err = nil // clear cached iteration error + ikey := i.iter.SeekGE(key, flags) + for ikey != nil && ikey.SeqNum() >= i.snapshot { + ikey = i.iter.Next() + } + if ikey == nil { + return nil, base.LazyValue{} + } + return ikey, base.MakeInPlaceValue(i.value()) +} + +func (i *batchIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + return i.SeekGE(key, flags) +} + +func (i *batchIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + ikey := i.iter.SeekLT(key) + for ikey != nil && ikey.SeqNum() >= i.snapshot { + ikey = i.iter.Prev() + } + if ikey == nil { + return nil, base.LazyValue{} + } + return ikey, base.MakeInPlaceValue(i.value()) +} + +func (i *batchIter) First() (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + ikey := i.iter.First() + for ikey != nil && ikey.SeqNum() >= i.snapshot { + ikey = i.iter.Next() + } + if ikey == nil { + return nil, base.LazyValue{} + } + return ikey, base.MakeInPlaceValue(i.value()) +} + +func (i *batchIter) Last() (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + ikey := i.iter.Last() + for ikey != nil && ikey.SeqNum() >= i.snapshot { + ikey = i.iter.Prev() + } + if ikey == nil { + return nil, base.LazyValue{} + } + return ikey, base.MakeInPlaceValue(i.value()) +} + +func (i *batchIter) Next() (*InternalKey, base.LazyValue) { + ikey := i.iter.Next() + for ikey != nil && ikey.SeqNum() >= i.snapshot { + ikey = i.iter.Next() + } + if ikey == nil { + return nil, base.LazyValue{} + } + return ikey, base.MakeInPlaceValue(i.value()) +} + +func (i *batchIter) NextPrefix(succKey []byte) (*InternalKey, LazyValue) { + // Because NextPrefix was invoked `succKey` must be ≥ the key at i's current + // position. Seek the arena iterator using TrySeekUsingNext. + ikey := i.iter.SeekGE(succKey, base.SeekGEFlagsNone.EnableTrySeekUsingNext()) + for ikey != nil && ikey.SeqNum() >= i.snapshot { + ikey = i.iter.Next() + } + if ikey == nil { + return nil, base.LazyValue{} + } + return ikey, base.MakeInPlaceValue(i.value()) +} + +func (i *batchIter) Prev() (*InternalKey, base.LazyValue) { + ikey := i.iter.Prev() + for ikey != nil && ikey.SeqNum() >= i.snapshot { + ikey = i.iter.Prev() + } + if ikey == nil { + return nil, base.LazyValue{} + } + return ikey, base.MakeInPlaceValue(i.value()) +} + +func (i *batchIter) value() []byte { + offset, _, keyEnd := i.iter.KeyInfo() + data := i.batch.data + if len(data[offset:]) == 0 { + i.err = base.CorruptionErrorf("corrupted batch") + return nil + } + + switch InternalKeyKind(data[offset]) { + case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, + InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete, + InternalKeyKindDeleteSized: + _, value, ok := batchDecodeStr(data[keyEnd:]) + if !ok { + return nil + } + return value + default: + return nil + } +} + +func (i *batchIter) Error() error { + return i.err +} + +func (i *batchIter) Close() error { + _ = i.iter.Close() + return i.err +} + +func (i *batchIter) SetBounds(lower, upper []byte) { + i.iter.SetBounds(lower, upper) +} + +func (i *batchIter) SetContext(_ context.Context) {} + +type flushableBatchEntry struct { + // offset is the byte offset of the record within the batch repr. + offset uint32 + // index is the 0-based ordinal number of the record within the batch. Used + // to compute the seqnum for the record. + index uint32 + // key{Start,End} are the start and end byte offsets of the key within the + // batch repr. Cached to avoid decoding the key length on every + // comparison. The value is stored starting at keyEnd. + keyStart uint32 + keyEnd uint32 +} + +// flushableBatch wraps an existing batch and provides the interfaces needed +// for making the batch flushable (i.e. able to mimic a memtable). +type flushableBatch struct { + cmp Compare + formatKey base.FormatKey + data []byte + + // The base sequence number for the entries in the batch. This is the same + // value as Batch.seqNum() and is cached here for performance. + seqNum uint64 + + // A slice of offsets and indices for the entries in the batch. Used to + // implement flushableBatchIter. Unlike the indexing on a normal batch, a + // flushable batch is indexed such that batch entry i will be given the + // sequence number flushableBatch.seqNum+i. + // + // Sorted in increasing order of key and decreasing order of offset (since + // higher offsets correspond to higher sequence numbers). + // + // Does not include range deletion entries or range key entries. + offsets []flushableBatchEntry + + // Fragmented range deletion tombstones. + tombstones []keyspan.Span + + // Fragmented range keys. + rangeKeys []keyspan.Span +} + +var _ flushable = (*flushableBatch)(nil) + +// newFlushableBatch creates a new batch that implements the flushable +// 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, error) { + b := &flushableBatch{ + data: batch.data, + cmp: comparer.Compare, + formatKey: comparer.FormatKey, + offsets: make([]flushableBatchEntry, 0, batch.Count()), + } + if b.data != nil { + // Note that this sequence number is not correct when this batch has not + // been applied since the sequence number has not been assigned yet. The + // correct sequence number will be set later. But it is correct when the + // batch is being replayed from the WAL. + b.seqNum = batch.SeqNum() + } + var rangeDelOffsets []flushableBatchEntry + var rangeKeyOffsets []flushableBatchEntry + if len(b.data) > batchHeaderLen { + // Non-empty batch. + 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, err := iter.Next() + if !ok { + if err != nil { + return nil, err + } + break + } + entry := flushableBatchEntry{ + offset: uint32(offset), + index: uint32(index), + } + if keySize := uint32(len(key)); keySize == 0 { + // Must add 2 to the offset. One byte encodes `kind` and the next + // byte encodes `0`, which is the length of the key. + entry.keyStart = uint32(offset) + 2 + entry.keyEnd = entry.keyStart + } else { + entry.keyStart = uint32(uintptr(unsafe.Pointer(&key[0])) - + uintptr(unsafe.Pointer(&b.data[0]))) + entry.keyEnd = entry.keyStart + keySize + } + switch kind { + case InternalKeyKindRangeDelete: + rangeDelOffsets = append(rangeDelOffsets, entry) + case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + rangeKeyOffsets = append(rangeKeyOffsets, entry) + default: + b.offsets = append(b.offsets, entry) + } + } + } + + // Sort all of offsets, rangeDelOffsets and rangeKeyOffsets, using *batch's + // sort.Interface implementation. + pointOffsets := b.offsets + sort.Sort(b) + b.offsets = rangeDelOffsets + sort.Sort(b) + b.offsets = rangeKeyOffsets + sort.Sort(b) + b.offsets = pointOffsets + + if len(rangeDelOffsets) > 0 { + frag := &keyspan.Fragmenter{ + Cmp: b.cmp, + Format: b.formatKey, + Emit: func(s keyspan.Span) { + b.tombstones = append(b.tombstones, s) + }, + } + it := &flushableBatchIter{ + batch: b, + data: b.data, + offsets: rangeDelOffsets, + cmp: b.cmp, + index: -1, + } + fragmentRangeDels(frag, it, len(rangeDelOffsets)) + } + if len(rangeKeyOffsets) > 0 { + frag := &keyspan.Fragmenter{ + Cmp: b.cmp, + Format: b.formatKey, + Emit: func(s keyspan.Span) { + b.rangeKeys = append(b.rangeKeys, s) + }, + } + it := &flushableBatchIter{ + batch: b, + data: b.data, + offsets: rangeKeyOffsets, + cmp: b.cmp, + index: -1, + } + fragmentRangeKeys(frag, it, len(rangeKeyOffsets)) + } + return b, nil +} + +func (b *flushableBatch) setSeqNum(seqNum uint64) { + if b.seqNum != 0 { + panic(fmt.Sprintf("pebble: flushableBatch.seqNum already set: %d", b.seqNum)) + } + b.seqNum = seqNum + for i := range b.tombstones { + for j := range b.tombstones[i].Keys { + b.tombstones[i].Keys[j].Trailer = base.MakeTrailer( + b.tombstones[i].Keys[j].SeqNum()+seqNum, + b.tombstones[i].Keys[j].Kind(), + ) + } + } + for i := range b.rangeKeys { + for j := range b.rangeKeys[i].Keys { + b.rangeKeys[i].Keys[j].Trailer = base.MakeTrailer( + b.rangeKeys[i].Keys[j].SeqNum()+seqNum, + b.rangeKeys[i].Keys[j].Kind(), + ) + } + } +} + +func (b *flushableBatch) Len() int { + return len(b.offsets) +} + +func (b *flushableBatch) Less(i, j int) bool { + ei := &b.offsets[i] + ej := &b.offsets[j] + ki := b.data[ei.keyStart:ei.keyEnd] + kj := b.data[ej.keyStart:ej.keyEnd] + switch c := b.cmp(ki, kj); { + case c < 0: + return true + case c > 0: + return false + default: + return ei.offset > ej.offset + } +} + +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, + data: b.data, + offsets: b.offsets, + cmp: b.cmp, + index: -1, + lower: o.GetLowerBound(), + upper: o.GetUpperBound(), + } +} + +// newFlushIter is part of the flushable interface. +func (b *flushableBatch) newFlushIter(o *IterOptions, bytesFlushed *uint64) internalIterator { + return &flushFlushableBatchIter{ + flushableBatchIter: flushableBatchIter{ + batch: b, + data: b.data, + offsets: b.offsets, + cmp: b.cmp, + index: -1, + }, + bytesIterated: bytesFlushed, + } +} + +// newRangeDelIter is part of the flushable interface. +func (b *flushableBatch) newRangeDelIter(o *IterOptions) keyspan.FragmentIterator { + if len(b.tombstones) == 0 { + return nil + } + 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 + } + 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 +} + +// Note: flushableBatchIter mirrors the implementation of batchIter. Keep the +// two in sync. +type flushableBatchIter struct { + // Members to be initialized by creator. + batch *flushableBatch + // The bytes backing the batch. Always the same as batch.data? + data []byte + // The sorted entries. This is not always equal to batch.offsets. + offsets []flushableBatchEntry + cmp Compare + // Must be initialized to -1. It is the index into offsets that represents + // the current iterator position. + index int + + // For internal use by the implementation. + key InternalKey + err error + + // Optionally initialize to bounds of iteration, if any. + lower []byte + upper []byte +} + +// flushableBatchIter implements the base.InternalIterator interface. +var _ base.InternalIterator = (*flushableBatchIter)(nil) + +func (i *flushableBatchIter) String() string { + return "flushable-batch" +} + +// SeekGE implements internalIterator.SeekGE, as documented in the pebble +// package. Ignore flags.TrySeekUsingNext() since we don't expect this +// optimization to provide much benefit here at the moment. +func (i *flushableBatchIter) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + ikey := base.MakeSearchKey(key) + i.index = sort.Search(len(i.offsets), func(j int) bool { + return base.InternalCompare(i.cmp, ikey, i.getKey(j)) <= 0 + }) + if i.index >= len(i.offsets) { + return nil, base.LazyValue{} + } + i.key = i.getKey(i.index) + if i.upper != nil && i.cmp(i.key.UserKey, i.upper) >= 0 { + i.index = len(i.offsets) + return nil, base.LazyValue{} + } + return &i.key, i.value() +} + +// SeekPrefixGE implements internalIterator.SeekPrefixGE, as documented in the +// pebble package. +func (i *flushableBatchIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + return i.SeekGE(key, flags) +} + +// SeekLT implements internalIterator.SeekLT, as documented in the pebble +// package. +func (i *flushableBatchIter) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + ikey := base.MakeSearchKey(key) + i.index = sort.Search(len(i.offsets), func(j int) bool { + return base.InternalCompare(i.cmp, ikey, i.getKey(j)) <= 0 + }) + i.index-- + if i.index < 0 { + return nil, base.LazyValue{} + } + i.key = i.getKey(i.index) + if i.lower != nil && i.cmp(i.key.UserKey, i.lower) < 0 { + i.index = -1 + return nil, base.LazyValue{} + } + return &i.key, i.value() +} + +// First implements internalIterator.First, as documented in the pebble +// package. +func (i *flushableBatchIter) First() (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + if len(i.offsets) == 0 { + return nil, base.LazyValue{} + } + i.index = 0 + i.key = i.getKey(i.index) + if i.upper != nil && i.cmp(i.key.UserKey, i.upper) >= 0 { + i.index = len(i.offsets) + return nil, base.LazyValue{} + } + return &i.key, i.value() +} + +// Last implements internalIterator.Last, as documented in the pebble +// package. +func (i *flushableBatchIter) Last() (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + if len(i.offsets) == 0 { + return nil, base.LazyValue{} + } + i.index = len(i.offsets) - 1 + i.key = i.getKey(i.index) + if i.lower != nil && i.cmp(i.key.UserKey, i.lower) < 0 { + i.index = -1 + return nil, base.LazyValue{} + } + return &i.key, i.value() +} + +// Note: flushFlushableBatchIter.Next mirrors the implementation of +// flushableBatchIter.Next due to performance. Keep the two in sync. +func (i *flushableBatchIter) Next() (*InternalKey, base.LazyValue) { + if i.index == len(i.offsets) { + return nil, base.LazyValue{} + } + i.index++ + if i.index == len(i.offsets) { + return nil, base.LazyValue{} + } + i.key = i.getKey(i.index) + if i.upper != nil && i.cmp(i.key.UserKey, i.upper) >= 0 { + i.index = len(i.offsets) + return nil, base.LazyValue{} + } + return &i.key, i.value() +} + +func (i *flushableBatchIter) Prev() (*InternalKey, base.LazyValue) { + if i.index < 0 { + return nil, base.LazyValue{} + } + i.index-- + if i.index < 0 { + return nil, base.LazyValue{} + } + i.key = i.getKey(i.index) + if i.lower != nil && i.cmp(i.key.UserKey, i.lower) < 0 { + i.index = -1 + return nil, base.LazyValue{} + } + return &i.key, i.value() +} + +// Note: flushFlushableBatchIter.NextPrefix mirrors the implementation of +// flushableBatchIter.NextPrefix due to performance. Keep the two in sync. +func (i *flushableBatchIter) NextPrefix(succKey []byte) (*InternalKey, LazyValue) { + return i.SeekGE(succKey, base.SeekGEFlagsNone.EnableTrySeekUsingNext()) +} + +func (i *flushableBatchIter) getKey(index int) InternalKey { + e := &i.offsets[index] + kind := InternalKeyKind(i.data[e.offset]) + key := i.data[e.keyStart:e.keyEnd] + return base.MakeInternalKey(key, i.batch.seqNum+uint64(e.index), kind) +} + +func (i *flushableBatchIter) value() base.LazyValue { + p := i.data[i.offsets[i.index].offset:] + if len(p) == 0 { + i.err = base.CorruptionErrorf("corrupted batch") + return base.LazyValue{} + } + kind := InternalKeyKind(p[0]) + if kind > InternalKeyKindMax { + i.err = base.CorruptionErrorf("corrupted batch") + return base.LazyValue{} + } + var value []byte + var ok bool + switch kind { + case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, + InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete, + InternalKeyKindDeleteSized: + keyEnd := i.offsets[i.index].keyEnd + _, value, ok = batchDecodeStr(i.data[keyEnd:]) + if !ok { + i.err = base.CorruptionErrorf("corrupted batch") + return base.LazyValue{} + } + } + return base.MakeInPlaceValue(value) +} + +func (i *flushableBatchIter) Valid() bool { + return i.index >= 0 && i.index < len(i.offsets) +} + +func (i *flushableBatchIter) Error() error { + return i.err +} + +func (i *flushableBatchIter) Close() error { + return i.err +} + +func (i *flushableBatchIter) SetBounds(lower, upper []byte) { + i.lower = lower + i.upper = upper +} + +func (i *flushableBatchIter) SetContext(_ context.Context) {} + +// flushFlushableBatchIter is similar to flushableBatchIter but it keeps track +// of number of bytes iterated. +type flushFlushableBatchIter struct { + flushableBatchIter + bytesIterated *uint64 +} + +// flushFlushableBatchIter implements the base.InternalIterator interface. +var _ base.InternalIterator = (*flushFlushableBatchIter)(nil) + +func (i *flushFlushableBatchIter) String() string { + return "flushable-batch" +} + +func (i *flushFlushableBatchIter) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + panic("pebble: SeekGE unimplemented") +} + +func (i *flushFlushableBatchIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("pebble: SeekPrefixGE unimplemented") +} + +func (i *flushFlushableBatchIter) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*InternalKey, base.LazyValue) { + panic("pebble: SeekLT unimplemented") +} + +func (i *flushFlushableBatchIter) First() (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + key, val := i.flushableBatchIter.First() + if key == nil { + return nil, base.LazyValue{} + } + entryBytes := i.offsets[i.index].keyEnd - i.offsets[i.index].offset + *i.bytesIterated += uint64(entryBytes) + i.valueSize() + return key, val +} + +func (i *flushFlushableBatchIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { + panic("pebble: Prev unimplemented") +} + +// Note: flushFlushableBatchIter.Next mirrors the implementation of +// flushableBatchIter.Next due to performance. Keep the two in sync. +func (i *flushFlushableBatchIter) Next() (*InternalKey, base.LazyValue) { + if i.index == len(i.offsets) { + return nil, base.LazyValue{} + } + i.index++ + if i.index == len(i.offsets) { + return nil, base.LazyValue{} + } + i.key = i.getKey(i.index) + entryBytes := i.offsets[i.index].keyEnd - i.offsets[i.index].offset + *i.bytesIterated += uint64(entryBytes) + i.valueSize() + return &i.key, i.value() +} + +func (i flushFlushableBatchIter) Prev() (*InternalKey, base.LazyValue) { + panic("pebble: Prev unimplemented") +} + +func (i flushFlushableBatchIter) valueSize() uint64 { + p := i.data[i.offsets[i.index].offset:] + if len(p) == 0 { + i.err = base.CorruptionErrorf("corrupted batch") + return 0 + } + kind := InternalKeyKind(p[0]) + if kind > InternalKeyKindMax { + i.err = base.CorruptionErrorf("corrupted batch") + return 0 + } + var length uint64 + switch kind { + case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete: + keyEnd := i.offsets[i.index].keyEnd + v, n := binary.Uvarint(i.data[keyEnd:]) + if n <= 0 { + i.err = base.CorruptionErrorf("corrupted batch") + return 0 + } + length = v + uint64(n) + } + return length +} + +// batchSort returns iterators for the sorted contents of the batch. It is +// intended for testing use only. The batch.Sort dance is done to prevent +// exposing this method in the public pebble interface. +func batchSort( + i interface{}, +) ( + points internalIterator, + rangeDels keyspan.FragmentIterator, + rangeKeys keyspan.FragmentIterator, +) { + b := i.(*Batch) + if b.Indexed() { + pointIter := b.newInternalIter(nil) + rangeDelIter := b.newRangeDelIter(nil, math.MaxUint64) + rangeKeyIter := b.newRangeKeyIter(nil, math.MaxUint64) + return pointIter, rangeDelIter, rangeKeyIter + } + f, err := newFlushableBatch(b, b.db.opts.Comparer) + if err != nil { + panic(err) + } + return f.newIter(nil), f.newRangeDelIter(nil), f.newRangeKeyIter(nil) +} + +func init() { + private.BatchSort = batchSort +} diff --git a/pebble/batch_test.go b/pebble/batch_test.go new file mode 100644 index 0000000..c977874 --- /dev/null +++ b/pebble/batch_test.go @@ -0,0 +1,1652 @@ +// Copyright 2012 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 ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "math" + "math/rand" + "strconv" + "strings" + "sync" + "testing" + "time" + "unicode" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/batchskl" + "github.com/cockroachdb/pebble/internal/itertest" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestBatch(t *testing.T) { + testBatch(t, 0) + testBatch(t, batchInitialSize) +} + +func testBatch(t *testing.T, size int) { + type testCase struct { + kind InternalKeyKind + key, value string + valueInt uint32 + } + + verifyTestCases := func(b *Batch, testCases []testCase, indexedPointKindsOnly bool) { + r := b.Reader() + + for _, tc := range testCases { + if indexedPointKindsOnly && (tc.kind == InternalKeyKindLogData || tc.kind == InternalKeyKindIngestSST || + tc.kind == InternalKeyKindRangeDelete) { + continue + } + kind, k, v, ok, err := r.Next() + if !ok { + if err != nil { + t.Fatal(err) + } + t.Fatalf("next returned !ok: test case = %v", tc) + } + key, value := string(k), string(v) + if kind != tc.kind || key != tc.key || value != tc.value { + t.Errorf("got (%d, %q, %q), want (%d, %q, %q)", + kind, key, value, tc.kind, tc.key, tc.value) + } + } + if len(r) != 0 { + t.Errorf("reader was not exhausted: remaining bytes = %q", r) + } + } + + encodeFileNum := func(n base.FileNum) string { + return string(binary.AppendUvarint(nil, uint64(n))) + } + decodeFileNum := func(d []byte) base.FileNum { + val, n := binary.Uvarint(d) + if n <= 0 { + t.Fatalf("invalid filenum encoding") + } + return base.FileNum(val) + } + + // RangeKeySet and RangeKeyUnset are untested here because they don't expose + // deferred variants. This is a consequence of these keys' more complex + // value encodings. + testCases := []testCase{ + {InternalKeyKindIngestSST, encodeFileNum(1), "", 0}, + {InternalKeyKindSet, "roses", "red", 0}, + {InternalKeyKindSet, "violets", "blue", 0}, + {InternalKeyKindDelete, "roses", "", 0}, + {InternalKeyKindSingleDelete, "roses", "", 0}, + {InternalKeyKindSet, "", "", 0}, + {InternalKeyKindSet, "", "non-empty", 0}, + {InternalKeyKindDelete, "", "", 0}, + {InternalKeyKindSingleDelete, "", "", 0}, + {InternalKeyKindSet, "grass", "green", 0}, + {InternalKeyKindSet, "grass", "greener", 0}, + {InternalKeyKindSet, "eleventy", strings.Repeat("!!11!", 100), 0}, + {InternalKeyKindDelete, "nosuchkey", "", 0}, + {InternalKeyKindDeleteSized, "eleventy", string(binary.AppendUvarint([]byte(nil), 508)), 500}, + {InternalKeyKindSingleDelete, "nosuchkey", "", 0}, + {InternalKeyKindSet, "binarydata", "\x00", 0}, + {InternalKeyKindSet, "binarydata", "\xff", 0}, + {InternalKeyKindMerge, "merge", "mergedata", 0}, + {InternalKeyKindMerge, "merge", "", 0}, + {InternalKeyKindMerge, "", "", 0}, + {InternalKeyKindRangeDelete, "a", "b", 0}, + {InternalKeyKindRangeDelete, "", "", 0}, + {InternalKeyKindLogData, "logdata", "", 0}, + {InternalKeyKindLogData, "", "", 0}, + {InternalKeyKindRangeKeyDelete, "grass", "green", 0}, + {InternalKeyKindRangeKeyDelete, "", "", 0}, + {InternalKeyKindDeleteSized, "nosuchkey", string(binary.AppendUvarint([]byte(nil), 11)), 2}, + } + b := newBatchWithSize(nil, size) + for _, tc := range testCases { + switch tc.kind { + case InternalKeyKindSet: + _ = b.Set([]byte(tc.key), []byte(tc.value), nil) + case InternalKeyKindMerge: + _ = b.Merge([]byte(tc.key), []byte(tc.value), nil) + case InternalKeyKindDelete: + _ = b.Delete([]byte(tc.key), nil) + case InternalKeyKindDeleteSized: + _ = b.DeleteSized([]byte(tc.key), tc.valueInt, nil) + case InternalKeyKindSingleDelete: + _ = b.SingleDelete([]byte(tc.key), nil) + case InternalKeyKindRangeDelete: + _ = b.DeleteRange([]byte(tc.key), []byte(tc.value), nil) + case InternalKeyKindLogData: + _ = b.LogData([]byte(tc.key), nil) + case InternalKeyKindRangeKeyDelete: + _ = b.RangeKeyDelete([]byte(tc.key), []byte(tc.value), nil) + case InternalKeyKindIngestSST: + b.ingestSST(decodeFileNum([]byte(tc.key))) + } + } + verifyTestCases(b, testCases, false /* indexedKindsOnly */) + + b.Reset() + // Run the same operations, this time using the Deferred variants of each + // operation (eg. SetDeferred). + for _, tc := range testCases { + key := []byte(tc.key) + value := []byte(tc.value) + switch tc.kind { + case InternalKeyKindSet: + d := b.SetDeferred(len(key), len(value)) + copy(d.Key, key) + copy(d.Value, value) + d.Finish() + case InternalKeyKindMerge: + d := b.MergeDeferred(len(key), len(value)) + copy(d.Key, key) + copy(d.Value, value) + d.Finish() + case InternalKeyKindDelete: + d := b.DeleteDeferred(len(key)) + copy(d.Key, key) + copy(d.Value, value) + d.Finish() + case InternalKeyKindDeleteSized: + d := b.DeleteSizedDeferred(len(tc.key), tc.valueInt) + copy(d.Key, key) + d.Finish() + case InternalKeyKindSingleDelete: + d := b.SingleDeleteDeferred(len(key)) + copy(d.Key, key) + copy(d.Value, value) + d.Finish() + case InternalKeyKindRangeDelete: + d := b.DeleteRangeDeferred(len(key), len(value)) + copy(d.Key, key) + copy(d.Value, value) + d.Finish() + case InternalKeyKindLogData: + _ = b.LogData([]byte(tc.key), nil) + case InternalKeyKindIngestSST: + b.ingestSST(decodeFileNum([]byte(tc.key))) + case InternalKeyKindRangeKeyDelete: + d := b.RangeKeyDeleteDeferred(len(key), len(value)) + copy(d.Key, key) + copy(d.Value, value) + d.Finish() + } + } + verifyTestCases(b, testCases, false /* indexedKindsOnly */) + + b.Reset() + // Run the same operations, this time using AddInternalKey instead of the + // Kind-specific methods. + for _, tc := range testCases { + if tc.kind == InternalKeyKindLogData || tc.kind == InternalKeyKindIngestSST || + tc.kind == InternalKeyKindRangeDelete { + continue + } + key := []byte(tc.key) + value := []byte(tc.value) + b.AddInternalKey(&InternalKey{UserKey: key, Trailer: base.MakeTrailer(0, tc.kind)}, value, nil) + } + verifyTestCases(b, testCases, true /* indexedKindsOnly */) +} + +func TestBatchPreAlloc(t *testing.T) { + var cases = []struct { + size int + exp int + }{ + {0, batchInitialSize}, + {batchInitialSize, batchInitialSize}, + {2 * batchInitialSize, 2 * batchInitialSize}, + } + for _, c := range cases { + b := newBatchWithSize(nil, c.size) + b.Set([]byte{0x1}, []byte{0x2}, nil) + if cap(b.data) != c.exp { + t.Errorf("Unexpected memory space, required: %d, got: %d", c.exp, cap(b.data)) + } + } +} + +func TestBatchIngestSST(t *testing.T) { + // Verify that Batch.IngestSST has the correct batch count and memtable + // size. + var b Batch + b.ingestSST(1) + require.Equal(t, int(b.Count()), 1) + b.ingestSST(2) + require.Equal(t, int(b.Count()), 2) + require.Equal(t, int(b.memTableSize), 0) + require.Equal(t, b.ingestedSSTBatch, true) +} + +func TestBatchLen(t *testing.T) { + var b Batch + + requireLenAndReprEq := func(size int) { + require.Equal(t, size, b.Len()) + require.Equal(t, size, len(b.Repr())) + } + + requireLenAndReprEq(batchHeaderLen) + + key := "test-key" + value := "test-value" + + err := b.Set([]byte(key), []byte(value), nil) + require.NoError(t, err) + + requireLenAndReprEq(33) + + err = b.Delete([]byte(key), nil) + require.NoError(t, err) + + requireLenAndReprEq(43) +} + +func TestBatchEmpty(t *testing.T) { + testBatchEmpty(t, 0) + testBatchEmpty(t, batchInitialSize) +} + +func testBatchEmpty(t *testing.T, size int) { + b := newBatchWithSize(nil, size) + require.True(t, b.Empty()) + + ops := []func(*Batch) error{ + func(b *Batch) error { return b.Set(nil, nil, nil) }, + func(b *Batch) error { return b.Merge(nil, nil, nil) }, + func(b *Batch) error { return b.Delete(nil, nil) }, + func(b *Batch) error { return b.DeleteRange(nil, nil, nil) }, + func(b *Batch) error { return b.LogData(nil, nil) }, + func(b *Batch) error { return b.RangeKeySet(nil, nil, nil, nil, nil) }, + func(b *Batch) error { return b.RangeKeyUnset(nil, nil, nil, nil) }, + func(b *Batch) error { return b.RangeKeyDelete(nil, nil, nil) }, + } + + for _, op := range ops { + require.NoError(t, op(b)) + require.False(t, b.Empty()) + b.Reset() + require.True(t, b.Empty()) + // Reset may choose to reuse b.data, so clear it to the zero value in + // order to test the lazy initialization of b.data. + b = newBatchWithSize(nil, size) + } + + _ = b.Reader() + require.True(t, b.Empty()) + b.Reset() + require.True(t, b.Empty()) + b = newBatchWithSize(nil, size) + + require.Equal(t, uint64(0), b.SeqNum()) + require.True(t, b.Empty()) + b.Reset() + require.True(t, b.Empty()) + b = &Batch{} + + d, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + defer d.Close() + ib := newIndexedBatch(d, DefaultComparer) + iter, _ := ib.NewIter(nil) + require.False(t, iter.First()) + iter2, err := iter.Clone(CloneOptions{}) + require.NoError(t, err) + require.NoError(t, iter.Close()) + _, err = iter.Clone(CloneOptions{}) + require.True(t, err != nil) + require.False(t, iter2.First()) + require.NoError(t, iter2.Close()) + iter3, err := ib.NewBatchOnlyIter(context.Background(), nil) + require.NoError(t, err) + require.False(t, iter3.First()) + _, err = iter3.Clone(CloneOptions{}) + require.Error(t, err) + require.NoError(t, iter3.Close()) +} + +func TestBatchApplyNoSyncWait(t *testing.T) { + db, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + defer db.Close() + var batches []*Batch + options := &WriteOptions{Sync: true} + for i := 0; i < 10000; i++ { + b := db.NewBatch() + str := fmt.Sprintf("a%d", i) + require.NoError(t, b.Set([]byte(str), []byte(str), nil)) + require.NoError(t, db.ApplyNoSyncWait(b, options)) + // k-v pair is visible even if not yet synced. + val, closer, err := db.Get([]byte(str)) + require.NoError(t, err) + require.Equal(t, str, string(val)) + closer.Close() + batches = append(batches, b) + } + for _, b := range batches { + require.NoError(t, b.SyncWait()) + b.Close() + } +} + +func TestBatchReset(t *testing.T) { + db, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + defer db.Close() + key := "test-key" + value := "test-value" + b := db.NewBatch() + require.NoError(t, b.Set([]byte(key), []byte(value), nil)) + dd := b.DeleteRangeDeferred(len(key), len(value)) + copy(dd.Key, key) + copy(dd.Value, value) + dd.Finish() + + require.NoError(t, b.RangeKeySet([]byte(key), []byte(value), []byte(value), []byte(value), nil)) + + b.setSeqNum(100) + b.applied.Store(true) + b.commitErr = errors.New("test-error") + b.commit.Add(1) + b.fsyncWait.Add(1) + require.Equal(t, uint32(3), b.Count()) + require.Equal(t, uint64(1), b.countRangeDels) + require.Equal(t, uint64(1), b.countRangeKeys) + require.True(t, len(b.data) > 0) + require.True(t, b.SeqNum() > 0) + require.True(t, b.memTableSize > 0) + require.NotEqual(t, b.deferredOp, DeferredBatchOp{}) + // At this point b.data has not been modified since the db.NewBatch() and is + // either nil or contains a byte slice of length batchHeaderLen, with a 0 + // seqnum encoded in data[0:8] and an arbitrary count encoded in data[8:12]. + // The following commented code will often fail. + // count := binary.LittleEndian.Uint32(b.countData()) + // if count != 0 && count != 3 { + // t.Fatalf("count: %d", count) + // } + // If we simply called b.Reset now and later used b.data to initialize + // expected, the count in expected will also be arbitrary. So we fix the + // count in b.data now by calling b.Repr(). This call isn't essential, since + // we will call b.Repr() again, and just shows that it fixes the count in + // b.data. + _ = b.Repr() + require.Equal(t, uint32(3), binary.LittleEndian.Uint32(b.countData())) + + b.Reset() + require.Equal(t, db, b.db) + require.Equal(t, false, b.applied.Load()) + require.Nil(t, b.commitErr) + require.Equal(t, uint32(0), b.Count()) + require.Equal(t, uint64(0), b.countRangeDels) + require.Equal(t, uint64(0), b.countRangeKeys) + require.Equal(t, batchHeaderLen, len(b.data)) + require.Equal(t, uint64(0), b.SeqNum()) + require.Equal(t, uint64(0), b.memTableSize) + require.Equal(t, FormatMajorVersion(0x00), b.minimumFormatMajorVersion) + require.Equal(t, b.deferredOp, DeferredBatchOp{}) + _ = b.Repr() + + var expected Batch + require.NoError(t, expected.SetRepr(b.data)) + expected.db = db + require.Equal(t, &expected, b) + + // Reset batch can be used to write and commit a new record. + b.Set([]byte(key), []byte(value), nil) + require.NoError(t, db.Apply(b, nil)) + v, closer, err := db.Get([]byte(key)) + require.NoError(t, err) + defer closer.Close() + require.Equal(t, v, []byte(value)) +} + +func TestIndexedBatchReset(t *testing.T) { + indexCount := func(sl *batchskl.Skiplist) int { + count := 0 + iter := sl.NewIter(nil, nil) + defer iter.Close() + for iter.First(); iter.Valid(); iter.Next() { + count++ + } + return count + } + db, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + defer db.Close() + b := newIndexedBatch(db, DefaultComparer) + start := "start-key" + end := "end-key" + key := "test-key" + value := "test-value" + b.DeleteRange([]byte(start), []byte(end), nil) + b.Set([]byte(key), []byte(value), nil) + require.NoError(t, b. + RangeKeySet([]byte(start), []byte(end), []byte("suffix"), []byte(value), nil)) + require.NotNil(t, b.rangeKeyIndex) + require.NotNil(t, b.rangeDelIndex) + require.NotNil(t, b.index) + require.Equal(t, 1, indexCount(b.index)) + + b.Reset() + require.NotNil(t, b.cmp) + require.NotNil(t, b.formatKey) + require.NotNil(t, b.abbreviatedKey) + require.NotNil(t, b.index) + require.Nil(t, b.rangeDelIndex) + require.Nil(t, b.rangeKeyIndex) + + count := func(ib *Batch) int { + iter, _ := ib.NewIter(nil) + defer iter.Close() + iter2, err := iter.Clone(CloneOptions{}) + require.NoError(t, err) + defer iter2.Close() + iter3, err := ib.NewBatchOnlyIter(context.Background(), nil) + require.NoError(t, err) + defer iter3.Close() + var count [3]int + for i, it := range []*Iterator{iter, iter2, iter3} { + for it.First(); it.Valid(); it.Next() { + count[i]++ + } + } + require.Equal(t, count[0], count[1]) + require.Equal(t, count[0], count[2]) + return count[0] + } + contains := func(ib *Batch, key, value string) bool { + iter, _ := ib.NewIter(nil) + defer iter.Close() + iter2, err := iter.Clone(CloneOptions{}) + require.NoError(t, err) + defer iter2.Close() + iter3, err := ib.NewBatchOnlyIter(context.Background(), nil) + require.NoError(t, err) + defer iter3.Close() + var found [3]bool + for i, it := range []*Iterator{iter, iter2, iter3} { + for it.First(); it.Valid(); it.Next() { + if string(it.Key()) == key && + string(it.Value()) == value { + found[i] = true + } + } + } + require.Equal(t, found[0], found[1]) + require.Equal(t, found[0], found[2]) + return found[0] + } + // Set a key and check whether the key-value pair is visible. + b.Set([]byte(key), []byte(value), nil) + require.Equal(t, 1, indexCount(b.index)) + require.Equal(t, 1, count(b)) + require.True(t, contains(b, key, value)) + + // Use range delete to delete the above inserted key-value pair. + b.DeleteRange([]byte(key), []byte(value), nil) + require.NotNil(t, b.rangeDelIndex) + require.Equal(t, 1, indexCount(b.rangeDelIndex)) + require.Equal(t, 0, count(b)) + require.False(t, contains(b, key, value)) +} + +// TestIndexedBatchMutation tests mutating an indexed batch with an open +// iterator. +func TestIndexedBatchMutation(t *testing.T) { + opts := &Options{ + Comparer: testkeys.Comparer, + FS: vfs.NewMem(), + FormatMajorVersion: internalFormatNewest, + } + d, err := Open("", opts) + require.NoError(t, err) + defer func() { d.Close() }() + + b := newIndexedBatch(d, DefaultComparer) + iters := map[string]*Iterator{} + defer func() { + for _, iter := range iters { + require.NoError(t, iter.Close()) + } + }() + + datadriven.RunTest(t, "testdata/indexed_batch_mutation", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "batch": + writeBatch := newBatch(d) + if err := runBatchDefineCmd(td, writeBatch); err != nil { + return err.Error() + } + if err := writeBatch.Commit(nil); err != nil { + return err.Error() + } + return "" + case "new-batch-iter": + name := td.CmdArgs[0].String() + iters[name], _ = b.NewIter(&IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + }) + return "" + case "new-batch-only-iter": + name := td.CmdArgs[0].String() + iters[name], _ = b.NewBatchOnlyIter(context.Background(), &IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + }) + return "" + case "new-db-iter": + name := td.CmdArgs[0].String() + iters[name], _ = d.NewIter(&IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + }) + return "" + case "new-batch": + if b != nil { + require.NoError(t, b.Close()) + } + b = newIndexedBatch(d, opts.Comparer) + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + return "" + case "flush": + require.NoError(t, d.Flush()) + return "" + case "iter": + var iter string + td.ScanArgs(t, "iter", &iter) + return runIterCmd(td, iters[iter], false /* closeIter */) + case "mutate": + mut := newBatch(d) + if err := runBatchDefineCmd(td, mut); err != nil { + return err.Error() + } + if err := b.Apply(mut, nil); err != nil { + return err.Error() + } + return "" + case "clone": + var from, to string + var refreshBatchView bool + td.ScanArgs(t, "from", &from) + td.ScanArgs(t, "to", &to) + td.ScanArgs(t, "refresh-batch", &refreshBatchView) + var err error + iters[to], err = iters[from].Clone(CloneOptions{RefreshBatchView: refreshBatchView}) + if err != nil { + return err.Error() + } + return "" + case "reset": + for key, iter := range iters { + if err := iter.Close(); err != nil { + return err.Error() + } + delete(iters, key) + } + if d != nil { + if err := d.Close(); err != nil { + return err.Error() + } + } + opts.FS = vfs.NewMem() + d, err = Open("", opts) + require.NoError(t, err) + return "" + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +func TestIndexedBatch_GlobalVisibility(t *testing.T) { + opts := &Options{ + FS: vfs.NewMem(), + FormatMajorVersion: internalFormatNewest, + Comparer: testkeys.Comparer, + } + d, err := Open("", opts) + require.NoError(t, err) + defer d.Close() + + require.NoError(t, d.Set([]byte("foo"), []byte("foo"), nil)) + + // Create an iterator over an empty indexed batch. + b := newIndexedBatch(d, DefaultComparer) + iterOpts := IterOptions{KeyTypes: IterKeyTypePointsAndRanges} + iter, _ := b.NewIter(&iterOpts) + defer iter.Close() + + // Mutate the database's committed state. + mut := newBatch(d) + require.NoError(t, mut.Set([]byte("bar"), []byte("bar"), nil)) + require.NoError(t, mut.DeleteRange([]byte("e"), []byte("g"), nil)) + require.NoError(t, mut.RangeKeySet([]byte("a"), []byte("c"), []byte("@1"), []byte("v"), nil)) + require.NoError(t, mut.Commit(nil)) + + scanIter := func() string { + var buf bytes.Buffer + for valid := iter.First(); valid; valid = iter.Next() { + fmt.Fprintf(&buf, "%s: (", iter.Key()) + hasPoint, hasRange := iter.HasPointAndRange() + if hasPoint { + fmt.Fprintf(&buf, "%s,", iter.Value()) + } else { + fmt.Fprintf(&buf, ".,") + } + if hasRange { + start, end := iter.RangeBounds() + fmt.Fprintf(&buf, "[%s-%s)", start, end) + writeRangeKeys(&buf, iter) + } else { + fmt.Fprintf(&buf, ".") + } + fmt.Fprintln(&buf, ")") + } + return strings.TrimSpace(buf.String()) + } + // Scanning the iterator should only see the point key written before the + // iterator was constructed. + require.Equal(t, `foo: (foo,.)`, scanIter()) + + // After calling SetOptions, the iterator should still only see the point + // key written before the iterator was constructed. SetOptions refreshes the + // iterator's view of its own indexed batch, but not committed state. + iter.SetOptions(&iterOpts) + require.Equal(t, `foo: (foo,.)`, scanIter()) +} + +func TestFlushableBatchReset(t *testing.T) { + var b Batch + var err error + b.flushable, err = newFlushableBatch(&b, DefaultComparer) + require.NoError(t, err) + + b.Reset() + require.Nil(t, b.flushable) +} + +func TestBatchIncrement(t *testing.T) { + testCases := []uint32{ + 0x00000000, + 0x00000001, + 0x00000002, + 0x0000007f, + 0x00000080, + 0x000000fe, + 0x000000ff, + 0x00000100, + 0x00000101, + 0x000001ff, + 0x00000200, + 0x00000fff, + 0x00001234, + 0x0000fffe, + 0x0000ffff, + 0x00010000, + 0x00010001, + 0x000100fe, + 0x000100ff, + 0x00020100, + 0x03fffffe, + 0x03ffffff, + 0x04000000, + 0x04000001, + 0x7fffffff, + 0xfffffffe, + } + for _, tc := range testCases { + var buf [batchHeaderLen]byte + binary.LittleEndian.PutUint32(buf[8:12], tc) + var b Batch + b.SetRepr(buf[:]) + b.count++ + got := binary.LittleEndian.Uint32(b.Repr()[8:12]) + want := tc + 1 + if got != want { + t.Errorf("input=%d: got %d, want %d", tc, got, want) + } + _, count := ReadBatch(b.Repr()) + if got != want { + t.Errorf("input=%d: got %d, want %d", tc, count, want) + } + } + + err := func() (err error) { + defer func() { + if v := recover(); v != nil { + if verr, ok := v.(error); ok { + err = verr + } + } + }() + var buf [batchHeaderLen]byte + binary.LittleEndian.PutUint32(buf[8:12], 0xffffffff) + var b Batch + b.SetRepr(buf[:]) + b.count++ + b.Repr() + return nil + }() + if err != ErrInvalidBatch { + t.Fatalf("expected %v, but found %v", ErrInvalidBatch, err) + } +} + +func TestBatchOpDoesIncrement(t *testing.T) { + var b Batch + key := []byte("foo") + value := []byte("bar") + + if b.Count() != 0 { + t.Fatalf("new batch has a nonzero count: %d", b.Count()) + } + + // Should increment count by 1 + _ = b.Set(key, value, nil) + if b.Count() != 1 { + t.Fatalf("expected count: %d, got %d", 1, b.Count()) + } + + var b2 Batch + // Should increment count by 1 each + _ = b2.Set(key, value, nil) + _ = b2.Delete(key, nil) + if b2.Count() != 2 { + t.Fatalf("expected count: %d, got %d", 2, b2.Count()) + } + + // Should increment count by b2.count() + _ = b.Apply(&b2, nil) + if b.Count() != 3 { + t.Fatalf("expected count: %d, got %d", 3, b.Count()) + } + + // Should increment count by 1 + _ = b.Merge(key, value, nil) + if b.Count() != 4 { + t.Fatalf("expected count: %d, got %d", 4, b.Count()) + } + + // Should NOT increment count. + _ = b.LogData([]byte("foobarbaz"), nil) + if b.Count() != 4 { + t.Fatalf("expected count: %d, got %d", 4, b.Count()) + } +} + +func TestBatchGet(t *testing.T) { + testCases := []struct { + method string + memTableSize uint64 + }{ + {"build", 64 << 20}, + {"build", 2 << 10}, + {"apply", 64 << 20}, + } + + for _, c := range testCases { + t.Run(fmt.Sprintf("%s,mem=%d", c.method, c.memTableSize), func(t *testing.T) { + d, err := Open("", &Options{ + FS: vfs.NewMem(), + MemTableSize: c.memTableSize, + }) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer d.Close() + var b *Batch + + datadriven.RunTest(t, "testdata/batch_get", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + switch c.method { + case "build": + b = d.NewIndexedBatch() + case "apply": + b = d.NewBatch() + } + + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + + switch c.method { + case "apply": + tmp := d.NewIndexedBatch() + tmp.Apply(b, nil) + b = tmp + } + return "" + + case "commit": + if err := b.Commit(nil); err != nil { + return err.Error() + } + b = nil + return "" + + case "get": + if len(td.CmdArgs) != 1 { + return fmt.Sprintf("%s expects 1 argument", td.Cmd) + } + v, closer, err := b.Get([]byte(td.CmdArgs[0].String())) + if err != nil { + return err.Error() + } + defer closer.Close() + return string(v) + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) + }) + } +} + +func TestBatchIter(t *testing.T) { + var b *Batch + + for _, method := range []string{"build", "apply"} { + for _, testdata := range []string{ + "testdata/internal_iter_next", "testdata/internal_iter_bounds"} { + t.Run(method, func(t *testing.T) { + datadriven.RunTest(t, testdata, func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + switch method { + case "build": + b = newIndexedBatch(nil, DefaultComparer) + case "apply": + b = newBatch(nil) + } + + for _, key := range strings.Split(d.Input, "\n") { + j := strings.Index(key, ":") + ikey := base.ParseInternalKey(key[:j]) + value := []byte(key[j+1:]) + b.Set(ikey.UserKey, value, nil) + } + + switch method { + case "apply": + tmp := newIndexedBatch(nil, DefaultComparer) + tmp.Apply(b, nil) + b = tmp + } + return "" + + case "iter": + var options IterOptions + for _, arg := range d.CmdArgs { + switch arg.Key { + case "lower": + if len(arg.Vals) != 1 { + return fmt.Sprintf( + "%s expects at most 1 value for lower", d.Cmd) + } + options.LowerBound = []byte(arg.Vals[0]) + case "upper": + if len(arg.Vals) != 1 { + return fmt.Sprintf( + "%s expects at most 1 value for upper", d.Cmd) + } + options.UpperBound = []byte(arg.Vals[0]) + default: + return fmt.Sprintf("unknown arg: %s", arg.Key) + } + } + iter := b.newInternalIter(&options) + defer iter.Close() + return itertest.RunInternalIterCmd(t, d, iter) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) + }) + } + } +} + +func TestBatchRangeOps(t *testing.T) { + var b *Batch + + datadriven.RunTest(t, "testdata/batch_range_ops", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "clear": + b = nil + return "" + + case "apply": + if b == nil { + b = newIndexedBatch(nil, DefaultComparer) + } + t := newBatch(nil) + if err := runBatchDefineCmd(td, t); err != nil { + return err.Error() + } + if err := b.Apply(t, nil); err != nil { + return err.Error() + } + return "" + + case "define": + if b == nil { + b = newIndexedBatch(nil, DefaultComparer) + } + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + return "" + + case "scan": + if len(td.CmdArgs) > 1 { + return fmt.Sprintf("%s expects at most 1 argument", td.Cmd) + } + var fragmentIter keyspan.FragmentIterator + var internalIter base.InternalIterator + switch { + case td.HasArg("range-del"): + fragmentIter = b.newRangeDelIter(nil, math.MaxUint64) + defer fragmentIter.Close() + case td.HasArg("range-key"): + fragmentIter = b.newRangeKeyIter(nil, math.MaxUint64) + defer fragmentIter.Close() + default: + internalIter = b.newInternalIter(nil) + defer internalIter.Close() + } + + var buf bytes.Buffer + if fragmentIter != nil { + for s := fragmentIter.First(); s != nil; s = fragmentIter.Next() { + for i := range s.Keys { + s.Keys[i].Trailer = base.MakeTrailer( + s.Keys[i].SeqNum()&^base.InternalKeySeqNumBatch, + s.Keys[i].Kind(), + ) + } + fmt.Fprintln(&buf, s) + } + } else { + for k, v := internalIter.First(); k != nil; k, v = internalIter.Next() { + k.SetSeqNum(k.SeqNum() &^ InternalKeySeqNumBatch) + fmt.Fprintf(&buf, "%s:%s\n", k, v.InPlaceValue()) + } + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestBatchTooLarge(t *testing.T) { + var b Batch + var result interface{} + func() { + defer func() { + if r := recover(); r != nil { + result = r + } + }() + b.grow(maxBatchSize) + }() + require.EqualValues(t, ErrBatchTooLarge, result) +} + +func TestFlushableBatchIter(t *testing.T) { + var b *flushableBatch + datadriven.RunTest(t, "testdata/internal_iter_next", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + batch := newBatch(nil) + for _, key := range strings.Split(d.Input, "\n") { + j := strings.Index(key, ":") + ikey := base.ParseInternalKey(key[:j]) + value := []byte(fmt.Sprint(ikey.SeqNum())) + batch.Set(ikey.UserKey, value, nil) + } + var err error + b, err = newFlushableBatch(batch, DefaultComparer) + require.NoError(t, err) + return "" + + case "iter": + iter := b.newIter(nil) + defer iter.Close() + return itertest.RunInternalIterCmd(t, d, iter) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestFlushableBatch(t *testing.T) { + var b *flushableBatch + datadriven.RunTest(t, "testdata/flushable_batch", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + batch := newBatch(nil) + for _, key := range strings.Split(d.Input, "\n") { + j := strings.Index(key, ":") + ikey := base.ParseInternalKey(key[:j]) + value := []byte(fmt.Sprint(ikey.SeqNum())) + switch ikey.Kind() { + case InternalKeyKindDelete: + require.NoError(t, batch.Delete(ikey.UserKey, nil)) + case InternalKeyKindSet: + require.NoError(t, batch.Set(ikey.UserKey, value, nil)) + case InternalKeyKindMerge: + require.NoError(t, batch.Merge(ikey.UserKey, value, nil)) + case InternalKeyKindRangeDelete: + require.NoError(t, batch.DeleteRange(ikey.UserKey, value, nil)) + case InternalKeyKindRangeKeyDelete: + require.NoError(t, batch.RangeKeyDelete(ikey.UserKey, value, nil)) + case InternalKeyKindRangeKeySet: + require.NoError(t, batch.RangeKeySet(ikey.UserKey, value, value, value, nil)) + case InternalKeyKindRangeKeyUnset: + require.NoError(t, batch.RangeKeyUnset(ikey.UserKey, value, value, nil)) + } + } + var err error + b, err = newFlushableBatch(batch, DefaultComparer) + require.NoError(t, err) + return "" + + case "iter": + var opts IterOptions + for _, arg := range d.CmdArgs { + if len(arg.Vals) != 1 { + return fmt.Sprintf("%s: %s=", d.Cmd, arg.Key) + } + switch arg.Key { + case "lower": + opts.LowerBound = []byte(arg.Vals[0]) + case "upper": + opts.UpperBound = []byte(arg.Vals[0]) + default: + return fmt.Sprintf("%s: unknown arg: %s", d.Cmd, arg.Key) + } + } + + iter := b.newIter(&opts) + defer iter.Close() + return itertest.RunInternalIterCmd(t, d, iter) + + case "dump": + if len(d.CmdArgs) != 1 || len(d.CmdArgs[0].Vals) != 1 || d.CmdArgs[0].Key != "seq" { + return "dump seq=\n" + } + seqNum, err := strconv.Atoi(d.CmdArgs[0].Vals[0]) + if err != nil { + return err.Error() + } + b.setSeqNum(uint64(seqNum)) + + var buf bytes.Buffer + + iter := newInternalIterAdapter(b.newIter(nil)) + for valid := iter.First(); valid; valid = iter.Next() { + fmt.Fprintf(&buf, "%s:%s\n", iter.Key(), iter.Value()) + } + iter.Close() + + if rangeDelIter := b.newRangeDelIter(nil); rangeDelIter != nil { + scanKeyspanIterator(&buf, rangeDelIter) + rangeDelIter.Close() + } + if rangeKeyIter := b.newRangeKeyIter(nil); rangeKeyIter != nil { + scanKeyspanIterator(&buf, rangeKeyIter) + rangeKeyIter.Close() + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestFlushableBatchDeleteRange(t *testing.T) { + var fb *flushableBatch + var input string + + datadriven.RunTest(t, "testdata/delete_range", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "clear": + input = "" + return "" + + case "define": + b := newBatch(nil) + // NB: We can't actually add to the flushable batch as we can to a + // memtable (which shares the "testdata/delete_range" data), so we fake + // it by concatenating the input and rebuilding the flushable batch from + // scratch. + input += "\n" + td.Input + td.Input = input + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + var err error + fb, err = newFlushableBatch(b, DefaultComparer) + require.NoError(t, err) + return "" + + case "scan": + var buf bytes.Buffer + if td.HasArg("range-del") { + fi := fb.newRangeDelIter(nil) + defer fi.Close() + scanKeyspanIterator(&buf, fi) + } else { + ii := fb.newIter(nil) + defer ii.Close() + scanInternalIter(&buf, ii) + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func scanInternalIter(w io.Writer, ii internalIterator) { + for k, v := ii.First(); k != nil; k, v = ii.Next() { + fmt.Fprintf(w, "%s:%s\n", k, v.InPlaceValue()) + } +} + +func scanKeyspanIterator(w io.Writer, ki keyspan.FragmentIterator) { + for s := ki.First(); s != nil; s = ki.Next() { + fmt.Fprintln(w, s) + } +} + +func TestFlushableBatchBytesIterated(t *testing.T) { + batch := newBatch(nil) + for j := 0; j < 1000; j++ { + key := make([]byte, 8+j%3) + value := make([]byte, 7+j%5) + batch.Set(key, value, nil) + + fb, err := newFlushableBatch(batch, DefaultComparer) + require.NoError(t, err) + + var bytesIterated uint64 + it := fb.newFlushIter(nil, &bytesIterated) + + var prevIterated uint64 + for key, _ := it.First(); key != nil; key, _ = it.Next() { + if bytesIterated < prevIterated { + t.Fatalf("bytesIterated moved backward: %d < %d", bytesIterated, prevIterated) + } + prevIterated = bytesIterated + } + + expected := fb.inuseBytes() + if bytesIterated != expected { + t.Fatalf("bytesIterated: got %d, want %d", bytesIterated, expected) + } + } +} + +func TestEmptyFlushableBatch(t *testing.T) { + // Verify that we can create a flushable batch on an empty batch. + fb, err := newFlushableBatch(newBatch(nil), DefaultComparer) + require.NoError(t, err) + it := newInternalIterAdapter(fb.newIter(nil)) + require.False(t, it.First()) +} + +func TestBatchCommitStats(t *testing.T) { + testFunc := func() error { + db, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + defer db.Close() + b := db.NewBatch() + defer b.Close() + stats := b.CommitStats() + require.Equal(t, BatchCommitStats{}, stats) + + // The stall code peers into the internals, instead of adding general + // purpose hooks, to avoid changing production code. We can revisit this + // choice if it becomes hard to maintain. + + // Commit semaphore stall funcs. + var unstallCommitSemaphore func() + stallCommitSemaphore := func() { + commitPipeline := db.commit + commitSemaphoreReserved := 0 + done := false + for !done { + select { + case commitPipeline.commitQueueSem <- struct{}{}: + commitSemaphoreReserved++ + default: + done = true + } + if done { + break + } + } + unstallCommitSemaphore = func() { + for i := 0; i < commitSemaphoreReserved; i++ { + <-commitPipeline.commitQueueSem + } + } + } + + // Memstable stall funcs. + var unstallMemtable func() + stallMemtable := func() { + db.mu.Lock() + defer db.mu.Unlock() + prev := db.opts.MemTableStopWritesThreshold + db.opts.MemTableStopWritesThreshold = 0 + unstallMemtable = func() { + db.mu.Lock() + defer db.mu.Unlock() + db.opts.MemTableStopWritesThreshold = prev + db.mu.compact.cond.Broadcast() + } + } + + // L0 read-amp stall funcs. + var unstallL0ReadAmp func() + stallL0ReadAmp := func() { + db.mu.Lock() + defer db.mu.Unlock() + prev := db.opts.L0StopWritesThreshold + db.opts.L0StopWritesThreshold = 0 + unstallL0ReadAmp = func() { + db.mu.Lock() + defer db.mu.Unlock() + db.opts.L0StopWritesThreshold = prev + db.mu.compact.cond.Broadcast() + } + } + + // Commit wait stall funcs. + var unstallCommitWait func() + stallCommitWait := func() { + b.commit.Add(1) + unstallCommitWait = func() { + b.commit.Done() + } + } + + // Stall everything. + stallCommitSemaphore() + stallMemtable() + stallL0ReadAmp() + stallCommitWait() + + // Exceed initialMemTableSize -- this is needed to make stallMemtable work. + require.NoError(t, b.Set(make([]byte, initialMemTableSize), nil, nil)) + + var commitWG sync.WaitGroup + commitWG.Add(1) + go func() { + require.NoError(t, db.Apply(b, &WriteOptions{Sync: true})) + commitWG.Done() + }() + // Unstall things in the order that the stalls will happen. + sleepDuration := 10 * time.Millisecond + time.Sleep(sleepDuration) + unstallCommitSemaphore() + time.Sleep(sleepDuration) + unstallMemtable() + time.Sleep(sleepDuration) + unstallL0ReadAmp() + time.Sleep(sleepDuration) + unstallCommitWait() + + // Wait for Apply to return. + commitWG.Wait() + stats = b.CommitStats() + expectedDuration := (2 * sleepDuration) / 3 + if expectedDuration > stats.SemaphoreWaitDuration { + return errors.Errorf("SemaphoreWaitDuration %s is too low", + stats.SemaphoreWaitDuration.String()) + } + if expectedDuration > stats.MemTableWriteStallDuration { + return errors.Errorf("MemTableWriteStallDuration %s is too low", + stats.MemTableWriteStallDuration.String()) + } + if expectedDuration > stats.L0ReadAmpWriteStallDuration { + return errors.Errorf("L0ReadAmpWriteStallDuration %s is too low", + stats.L0ReadAmpWriteStallDuration) + } + if expectedDuration > stats.CommitWaitDuration { + return errors.Errorf("CommitWaitDuration %s is too low", + stats.CommitWaitDuration) + } + if 5*expectedDuration > stats.TotalDuration { + return errors.Errorf("TotalDuration %s is too low", + stats.TotalDuration) + } + return nil + } + // Try a few times, and succeed if one of them succeeds. + var err error + for i := 0; i < 5; i++ { + err = testFunc() + if err == nil { + break + } + } + require.NoError(t, err) +} + +func TestBatchReader(t *testing.T) { + datadriven.RunTest(t, "testdata/batch_reader", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "scan": + var repr bytes.Buffer + for i, l := range strings.Split(td.Input, "\n") { + // Remove any trailing comments behind #. + if i := strings.IndexRune(l, '#'); i >= 0 { + l = l[:i] + } + // Strip all whitespace from the line. + l = strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r + }, l) + b, err := hex.DecodeString(l) + if err != nil { + return fmt.Sprintf("failed to decode hex; line %d", i) + } + repr.Write(b) + } + r, count := ReadBatch(repr.Bytes()) + var out strings.Builder + fmt.Fprintf(&out, "Count: %d\n", count) + for { + kind, ukey, value, ok, err := r.Next() + if !ok { + if err != nil { + fmt.Fprintf(&out, "err: %s\n", err) + } else { + fmt.Fprint(&out, "eof") + } + break + } + fmt.Fprintf(&out, "%s: %q: %q\n", kind, ukey, value) + } + return out.String() + + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +func BenchmarkBatchSet(b *testing.B) { + value := make([]byte, 10) + for i := range value { + value[i] = byte(i) + } + key := make([]byte, 8) + batch := newBatch(nil) + + b.ResetTimer() + + const batchSize = 1000 + for i := 0; i < b.N; i += batchSize { + end := i + batchSize + if end > b.N { + end = b.N + } + + for j := i; j < end; j++ { + binary.BigEndian.PutUint64(key, uint64(j)) + batch.Set(key, value, nil) + } + batch.Reset() + } + + b.StopTimer() +} + +func BenchmarkIndexedBatchSet(b *testing.B) { + value := make([]byte, 10) + for i := range value { + value[i] = byte(i) + } + key := make([]byte, 8) + batch := newIndexedBatch(nil, DefaultComparer) + + b.ResetTimer() + + const batchSize = 1000 + for i := 0; i < b.N; i += batchSize { + end := i + batchSize + if end > b.N { + end = b.N + } + + for j := i; j < end; j++ { + binary.BigEndian.PutUint64(key, uint64(j)) + batch.Set(key, value, nil) + } + batch.Reset() + } + + b.StopTimer() +} + +func BenchmarkBatchSetDeferred(b *testing.B) { + value := make([]byte, 10) + for i := range value { + value[i] = byte(i) + } + key := make([]byte, 8) + batch := newBatch(nil) + + b.ResetTimer() + + const batchSize = 1000 + for i := 0; i < b.N; i += batchSize { + end := i + batchSize + if end > b.N { + end = b.N + } + + for j := i; j < end; j++ { + binary.BigEndian.PutUint64(key, uint64(j)) + deferredOp := batch.SetDeferred(len(key), len(value)) + + copy(deferredOp.Key, key) + copy(deferredOp.Value, value) + + deferredOp.Finish() + } + batch.Reset() + } + + b.StopTimer() +} + +func BenchmarkIndexedBatchSetDeferred(b *testing.B) { + value := make([]byte, 10) + for i := range value { + value[i] = byte(i) + } + key := make([]byte, 8) + batch := newIndexedBatch(nil, DefaultComparer) + + b.ResetTimer() + + const batchSize = 1000 + for i := 0; i < b.N; i += batchSize { + end := i + batchSize + if end > b.N { + end = b.N + } + + for j := i; j < end; j++ { + binary.BigEndian.PutUint64(key, uint64(j)) + deferredOp := batch.SetDeferred(len(key), len(value)) + + copy(deferredOp.Key, key) + copy(deferredOp.Value, value) + + deferredOp.Finish() + } + batch.Reset() + } + + b.StopTimer() +} + +func TestBatchMemTableSizeOverflow(t *testing.T) { + opts := &Options{ + FS: vfs.NewMem(), + } + opts.EnsureDefaults() + d, err := Open("", opts) + require.NoError(t, err) + + bigValue := make([]byte, 1000) + b := d.NewBatch() + + // memTableSize can overflow as a uint32. + b.memTableSize = math.MaxUint32 - 50 + for i := 0; i < 10; i++ { + k := fmt.Sprintf("key-%05d", i) + require.NoError(t, b.Set([]byte(k), bigValue, nil)) + } + require.Greater(t, b.memTableSize, uint64(math.MaxUint32)) + require.NoError(t, b.Close()) + require.NoError(t, d.Close()) +} + +// TestBatchSpanCaching stress tests the caching of keyspan.Spans for range +// tombstones and range keys. +func TestBatchSpanCaching(t *testing.T) { + opts := &Options{ + Comparer: testkeys.Comparer, + FS: vfs.NewMem(), + FormatMajorVersion: internalFormatNewest, + } + d, err := Open("", opts) + require.NoError(t, err) + defer d.Close() + + ks := testkeys.Alpha(1) + b := d.NewIndexedBatch() + for i := int64(0); i < ks.Count(); i++ { + k := testkeys.Key(ks, i) + require.NoError(t, b.Set(k, k, nil)) + } + + seed := int64(time.Now().UnixNano()) + t.Logf("seed = %d", seed) + rng := rand.New(rand.NewSource(seed)) + iters := make([][]*Iterator, ks.Count()) + defer func() { + for _, keyIters := range iters { + for _, iter := range keyIters { + _ = iter.Close() + } + } + }() + + // This test begins with one point key for every letter of the alphabet. + // Over the course of the test, point keys are 'replaced' with range keys + // with narrow bounds from left to right. Iterators are created at random, + // sometimes from the batch and sometimes by cloning existing iterators. + + checkIter := func(iter *Iterator, nextKey int64) { + var i int64 + for valid := iter.First(); valid; valid = iter.Next() { + hasPoint, hasRange := iter.HasPointAndRange() + require.Equal(t, testkeys.Key(ks, i), iter.Key()) + if i < nextKey { + // This key should not exist as a point key, just a range key. + require.False(t, hasPoint) + require.True(t, hasRange) + } else { + require.True(t, hasPoint) + require.False(t, hasRange) + } + i++ + } + require.Equal(t, ks.Count(), i) + } + + // Each iteration of the below loop either reads or writes. + // + // A write iteration writes a new RANGEDEL and RANGEKEYSET into the batch, + // covering a single point key seeded above. Writing these two span keys + // together 'replaces' the point key with a range key. Each write iteration + // ratchets nextWriteKey so the next write iteration will write the next + // key. + // + // A read iteration creates a new iterator and ensures its state is + // expected: some prefix of only point keys, followed by a suffix of only + // range keys. Iterators created through Clone should observe the point keys + // that existed when the cloned iterator was created. + for nextWriteKey := int64(0); nextWriteKey < ks.Count(); { + p := rng.Float64() + switch { + case p < .10: /* 10 % */ + // Write a new range deletion and range key. + start := testkeys.Key(ks, nextWriteKey) + end := append(start, 0x00) + require.NoError(t, b.DeleteRange(start, end, nil)) + require.NoError(t, b.RangeKeySet(start, end, nil, []byte("foo"), nil)) + nextWriteKey++ + case p < .55: /* 45 % */ + // Create a new iterator directly from the batch and check that it + // observes the correct state. + iter, _ := b.NewIter(&IterOptions{KeyTypes: IterKeyTypePointsAndRanges}) + checkIter(iter, nextWriteKey) + iters[nextWriteKey] = append(iters[nextWriteKey], iter) + default: /* 45 % */ + // Create a new iterator through cloning a random existing iterator + // and check that it observes the right state. + readKey := rng.Int63n(nextWriteKey + 1) + itersForReadKey := iters[readKey] + if len(itersForReadKey) == 0 { + continue + } + iter, err := itersForReadKey[rng.Intn(len(itersForReadKey))].Clone(CloneOptions{}) + require.NoError(t, err) + checkIter(iter, readKey) + iters[readKey] = append(iters[readKey], iter) + } + } +} diff --git a/pebble/bloom/bloom.go b/pebble/bloom/bloom.go new file mode 100644 index 0000000..bf72e1d --- /dev/null +++ b/pebble/bloom/bloom.go @@ -0,0 +1,250 @@ +// Copyright 2013 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 bloom implements Bloom filters. +package bloom // import "github.com/cockroachdb/pebble/bloom" + +import ( + "encoding/binary" + "fmt" + "sync" + + "github.com/cockroachdb/pebble/internal/base" +) + +const ( + cacheLineSize = 64 + cacheLineBits = cacheLineSize * 8 +) + +type tableFilter []byte + +func (f tableFilter) MayContain(key []byte) bool { + if len(f) <= 5 { + return false + } + n := len(f) - 5 + nProbes := f[n] + nLines := binary.LittleEndian.Uint32(f[n+1:]) + cacheLineBits := 8 * (uint32(n) / nLines) + + h := hash(key) + delta := h>>17 | h<<15 + b := (h % nLines) * cacheLineBits + + for j := uint8(0); j < nProbes; j++ { + bitPos := b + (h % cacheLineBits) + if f[bitPos/8]&(1<<(bitPos%8)) == 0 { + return false + } + h += delta + } + return true +} + +func calculateProbes(bitsPerKey int) uint32 { + // We intentionally round down to reduce probing cost a little bit + n := uint32(float64(bitsPerKey) * 0.69) // 0.69 =~ ln(2) + if n < 1 { + n = 1 + } + if n > 30 { + n = 30 + } + return n +} + +// extend appends n zero bytes to b. It returns the overall slice (of length +// n+len(originalB)) and the slice of n trailing zeroes. +func extend(b []byte, n int) (overall, trailer []byte) { + want := n + len(b) + if want <= cap(b) { + overall = b[:want] + trailer = overall[len(b):] + for i := range trailer { + trailer[i] = 0 + } + } else { + // Grow the capacity exponentially, with a 1KiB minimum. + c := 1024 + for c < want { + c += c / 4 + } + overall = make([]byte, want, c) + trailer = overall[len(b):] + copy(overall, b) + } + return overall, trailer +} + +// hash implements a hashing algorithm similar to the Murmur hash. +func hash(b []byte) uint32 { + const ( + seed = 0xbc9f1d34 + m = 0xc6a4a793 + ) + h := uint32(seed) ^ uint32(uint64(uint32(len(b))*m)) + for ; len(b) >= 4; b = b[4:] { + h += uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 + h *= m + h ^= h >> 16 + } + + // The code below first casts each byte to a signed 8-bit integer. This is + // necessary to match RocksDB's behavior. Note that the `byte` type in Go is + // unsigned. What is the difference between casting a signed 8-bit value vs + // unsigned 8-bit value into an unsigned 32-bit value? + // Sign-extension. Consider the value 250 which has the bit pattern 11111010: + // + // uint32(250) = 00000000000000000000000011111010 + // uint32(int8(250)) = 11111111111111111111111111111010 + // + // Note that the original LevelDB code did not explicitly cast to a signed + // 8-bit value which left the behavior dependent on whether C characters were + // signed or unsigned which is a compiler flag for gcc (-funsigned-char). + switch len(b) { + case 3: + h += uint32(int8(b[2])) << 16 + fallthrough + case 2: + h += uint32(int8(b[1])) << 8 + fallthrough + case 1: + h += uint32(int8(b[0])) + h *= m + h ^= h >> 24 + } + return h +} + +const hashBlockLen = 16384 + +type hashBlock [hashBlockLen]uint32 + +var hashBlockPool = sync.Pool{ + New: func() interface{} { + return &hashBlock{} + }, +} + +type tableFilterWriter struct { + bitsPerKey int + + numHashes int + // We store the hashes in blocks. + blocks []*hashBlock + lastHash uint32 + + // Initial "in-line" storage for the blocks slice (to avoid some small + // allocations). + blocksBuf [16]*hashBlock +} + +func newTableFilterWriter(bitsPerKey int) *tableFilterWriter { + w := &tableFilterWriter{ + bitsPerKey: bitsPerKey, + } + w.blocks = w.blocksBuf[:0] + return w +} + +// AddKey implements the base.FilterWriter interface. +func (w *tableFilterWriter) AddKey(key []byte) { + h := hash(key) + if w.numHashes != 0 && h == w.lastHash { + return + } + ofs := w.numHashes % hashBlockLen + if ofs == 0 { + // Time for a new block. + w.blocks = append(w.blocks, hashBlockPool.Get().(*hashBlock)) + } + w.blocks[len(w.blocks)-1][ofs] = h + w.numHashes++ + w.lastHash = h +} + +// Finish implements the base.FilterWriter interface. +func (w *tableFilterWriter) Finish(buf []byte) []byte { + // The table filter format matches the RocksDB full-file filter format. + var nLines int + if w.numHashes != 0 { + nLines = (w.numHashes*w.bitsPerKey + cacheLineBits - 1) / (cacheLineBits) + // Make nLines an odd number to make sure more bits are involved when + // determining which block. + if nLines%2 == 0 { + nLines++ + } + } + + nBytes := nLines * cacheLineSize + // +5: 4 bytes for num-lines, 1 byte for num-probes + buf, filter := extend(buf, nBytes+5) + + if nLines != 0 { + nProbes := calculateProbes(w.bitsPerKey) + for bIdx, b := range w.blocks { + length := hashBlockLen + if bIdx == len(w.blocks)-1 && w.numHashes%hashBlockLen != 0 { + length = w.numHashes % hashBlockLen + } + for _, h := range b[:length] { + delta := h>>17 | h<<15 // rotate right 17 bits + b := (h % uint32(nLines)) * (cacheLineBits) + for i := uint32(0); i < nProbes; i++ { + bitPos := b + (h % cacheLineBits) + filter[bitPos/8] |= (1 << (bitPos % 8)) + h += delta + } + } + } + filter[nBytes] = byte(nProbes) + binary.LittleEndian.PutUint32(filter[nBytes+1:], uint32(nLines)) + } + + // Release the hash blocks. + for i, b := range w.blocks { + hashBlockPool.Put(b) + w.blocks[i] = nil + } + w.blocks = w.blocks[:0] + w.numHashes = 0 + return buf +} + +// FilterPolicy implements the FilterPolicy interface from the pebble package. +// +// The integer value is the approximate number of bits used per key. A good +// value is 10, which yields a filter with ~ 1% false positive rate. +type FilterPolicy int + +var _ base.FilterPolicy = FilterPolicy(0) + +// Name implements the pebble.FilterPolicy interface. +func (p FilterPolicy) Name() string { + // This string looks arbitrary, but its value is written to LevelDB .sst + // files, and should be this exact value to be compatible with those files + // and with the C++ LevelDB code. + return "rocksdb.BuiltinBloomFilter" +} + +// MayContain implements the pebble.FilterPolicy interface. +func (p FilterPolicy) MayContain(ftype base.FilterType, f, key []byte) bool { + switch ftype { + case base.TableFilter: + return tableFilter(f).MayContain(key) + default: + panic(fmt.Sprintf("unknown filter type: %v", ftype)) + } +} + +// NewWriter implements the pebble.FilterPolicy interface. +func (p FilterPolicy) NewWriter(ftype base.FilterType) base.FilterWriter { + switch ftype { + case base.TableFilter: + return newTableFilterWriter(int(p)) + default: + panic(fmt.Sprintf("unknown filter type: %v", ftype)) + } +} diff --git a/pebble/bloom/bloom_test.go b/pebble/bloom/bloom_test.go new file mode 100644 index 0000000..74a6f62 --- /dev/null +++ b/pebble/bloom/bloom_test.go @@ -0,0 +1,219 @@ +// Copyright 2013 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 bloom + +import ( + "crypto/rand" + "strings" + "testing" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" +) + +func (f tableFilter) String() string { + var buf strings.Builder + for i, x := range f { + if i > 0 { + if i%8 == 0 { + buf.WriteString("\n") + } else { + buf.WriteString(" ") + } + } + + for j := uint(0); j < 8; j++ { + if x&(1<<(7-j)) != 0 { + buf.WriteString("1") + } else { + buf.WriteString(".") + } + } + } + buf.WriteString("\n") + return buf.String() +} + +func newTableFilter(bitsPerKey int, keys ...[]byte) tableFilter { + w := FilterPolicy(bitsPerKey).NewWriter(base.TableFilter) + for _, key := range keys { + w.AddKey(key) + } + return tableFilter(w.Finish(nil)) +} + +func TestSmallBloomFilter(t *testing.T) { + f := newTableFilter(10, []byte("hello"), []byte("world")) + + // The magic expected string comes from running RocksDB's util/bloom_test.cc:FullBloomTest.FullSmall. + want := ` +........ ........ ........ .......1 ........ ........ ........ ........ +........ .1...... ........ .1...... ........ ........ ........ ........ +...1.... ........ ........ ........ ........ ........ ........ ........ +........ ........ ........ ........ ........ ........ ........ ...1.... +........ ........ ........ ........ .....1.. ........ ........ ........ +.......1 ........ ........ ........ ........ ........ .1...... ........ +........ ........ ........ ........ ........ ...1.... ........ ........ +.......1 ........ ........ ........ .1...1.. ........ ........ ........ +.....11. .......1 ........ ........ ........ +` + want = strings.TrimLeft(want, "\n") + require.EqualValues(t, want, f.String()) + + m := map[string]bool{ + "hello": true, + "world": true, + "x": false, + "foo": false, + } + for k, want := range m { + require.EqualValues(t, want, f.MayContain([]byte(k))) + } +} + +func TestBloomFilter(t *testing.T) { + nextLength := func(x int) int { + if x < 10 { + return x + 1 + } + if x < 100 { + return x + 10 + } + if x < 1000 { + return x + 100 + } + return x + 1000 + } + le32 := func(i int) []byte { + b := make([]byte, 4) + b[0] = uint8(uint32(i) >> 0) + b[1] = uint8(uint32(i) >> 8) + b[2] = uint8(uint32(i) >> 16) + b[3] = uint8(uint32(i) >> 24) + return b + } + + nMediocreFilters, nGoodFilters := 0, 0 +loop: + for length := 1; length <= 10000; length = nextLength(length) { + keys := make([][]byte, 0, length) + for i := 0; i < length; i++ { + keys = append(keys, le32(i)) + } + f := newTableFilter(10, keys...) + // The size of the table bloom filter is measured in multiples of the + // cache line size. The '+2' contribution captures the rounding up in the + // length division plus preferring an odd number of cache lines. As such, + // this formula isn't exact, but the exact formula is hard to read. + maxLen := 5 + ((length*10)/cacheLineBits+2)*cacheLineSize + if len(f) > maxLen { + t.Errorf("length=%d: len(f)=%d > max len %d", length, len(f), maxLen) + continue + } + + // All added keys must match. + for _, key := range keys { + if !f.MayContain(key) { + t.Errorf("length=%d: did not contain key %q", length, key) + continue loop + } + } + + // Check false positive rate. + nFalsePositive := 0 + for i := 0; i < 10000; i++ { + if f.MayContain(le32(1e9 + i)) { + nFalsePositive++ + } + } + if nFalsePositive > 0.02*10000 { + t.Errorf("length=%d: %d false positives in 10000", length, nFalsePositive) + continue + } + if nFalsePositive > 0.0125*10000 { + nMediocreFilters++ + } else { + nGoodFilters++ + } + } + + if nMediocreFilters > nGoodFilters/5 { + t.Errorf("%d mediocre filters but only %d good filters", nMediocreFilters, nGoodFilters) + } +} + +func TestHash(t *testing.T) { + testCases := []struct { + s string + expected uint32 + }{ + // The magic expected numbers come from RocksDB's util/hash_test.cc:TestHash. + {"", 3164544308}, + {"\x08", 422599524}, + {"\x17", 3168152998}, + {"\x9a", 3195034349}, + {"\x1c", 2651681383}, + {"\x4d\x76", 2447836956}, + {"\x52\xd5", 3854228105}, + {"\x91\xf7", 31066776}, + {"\xd6\x27", 1806091603}, + {"\x30\x46\x0b", 3808221797}, + {"\x56\xdc\xd6", 2157698265}, + {"\xd4\x52\x33", 1721992661}, + {"\x6a\xb5\xf4", 2469105222}, + {"\x67\x53\x81\x1c", 118283265}, + {"\x69\xb8\xc0\x88", 3416318611}, + {"\x1e\x84\xaf\x2d", 3315003572}, + {"\x46\xdc\x54\xbe", 447346355}, + {"\xd0\x7a\x6e\xea\x56", 4255445370}, + {"\x86\x83\xd5\xa4\xd8", 2390603402}, + {"\xb7\x46\xbb\x77\xce", 2048907743}, + {"\x6c\xa8\xbc\xe5\x99", 2177978500}, + {"\x5c\x5e\xe1\xa0\x73\x81", 1036846008}, + {"\x08\x5d\x73\x1c\xe5\x2e", 229980482}, + {"\x42\xfb\xf2\x52\xb4\x10", 3655585422}, + {"\x73\xe1\xff\x56\x9c\xce", 3502708029}, + {"\x5c\xbe\x97\x75\x54\x9a\x52", 815120748}, + {"\x16\x82\x39\x49\x88\x2b\x36", 3056033698}, + {"\x59\x77\xf0\xa7\x24\xf4\x78", 587205227}, + {"\xd3\xa5\x7c\x0e\xc0\x02\x07", 2030937252}, + {"\x31\x1b\x98\x75\x96\x22\xd3\x9a", 469635402}, + {"\x38\xd6\xf7\x28\x20\xb4\x8a\xe9", 3530274698}, + {"\xbb\x18\x5d\xf4\x12\x03\xf7\x99", 1974545809}, + {"\x80\xd4\x3b\x3b\xae\x22\xa2\x78", 3563570120}, + {"\x1a\xb5\xd0\xfe\xab\xc3\x61\xb2\x99", 2706087434}, + {"\x8e\x4a\xc3\x18\x20\x2f\x06\xe6\x3c", 1534654151}, + {"\xb6\xc0\xdd\x05\x3f\xc4\x86\x4c\xef", 2355554696}, + {"\x9a\x5f\x78\x0d\xaf\x50\xe1\x1f\x55", 1400800912}, + {"\x22\x6f\x39\x1f\xf8\xdd\x4f\x52\x17\x94", 3420325137}, + {"\x32\x89\x2a\x75\x48\x3a\x4a\x02\x69\xdd", 3427803584}, + {"\x06\x92\x5c\xf4\x88\x0e\x7e\x68\x38\x3e", 1152407945}, + {"\xbd\x2c\x63\x38\xbf\xe9\x78\xb7\xbf\x15", 3382479516}, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + require.EqualValues(t, tc.expected, hash([]byte(tc.s))) + }) + } +} + +func BenchmarkBloomFilter(b *testing.B) { + const keyLen = 128 + const numKeys = 1024 + keys := make([][]byte, numKeys) + for i := range keys { + keys[i] = make([]byte, keyLen) + _, _ = rand.Read(keys[i]) + } + b.ResetTimer() + policy := FilterPolicy(10) + for i := 0; i < b.N; i++ { + w := policy.NewWriter(base.TableFilter) + for _, key := range keys { + w.AddKey(key) + } + w.Finish(nil) + } +} diff --git a/pebble/cache.go b/pebble/cache.go new file mode 100644 index 0000000..91f5532 --- /dev/null +++ b/pebble/cache.go @@ -0,0 +1,23 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import "github.com/cockroachdb/pebble/internal/cache" + +// Cache exports the cache.Cache type. +type Cache = cache.Cache + +// NewCache creates a new cache of the specified size. Memory for the cache is +// allocated on demand, not during initialization. The cache is created with a +// reference count of 1. Each DB it is associated with adds a reference, so the +// creator of the cache should usually release their reference after the DB is +// created. +// +// c := pebble.NewCache(...) +// defer c.Unref() +// d, err := pebble.Open(pebble.Options{Cache: c}) +func NewCache(size int64) *cache.Cache { + return cache.New(size) +} diff --git a/pebble/checkpoint.go b/pebble/checkpoint.go new file mode 100644 index 0000000..f321c01 --- /dev/null +++ b/pebble/checkpoint.go @@ -0,0 +1,428 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "io" + "os" + + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/atomicfs" +) + +// checkpointOptions hold the optional parameters to construct checkpoint +// snapshots. +type checkpointOptions struct { + // flushWAL set to true will force a flush and sync of the WAL prior to + // checkpointing. + flushWAL bool + + // If set, any SSTs that don't overlap with these spans are excluded from a checkpoint. + restrictToSpans []CheckpointSpan +} + +// CheckpointOption set optional parameters used by `DB.Checkpoint`. +type CheckpointOption func(*checkpointOptions) + +// WithFlushedWAL enables flushing and syncing the WAL prior to constructing a +// checkpoint. This guarantees that any writes committed before calling +// DB.Checkpoint will be part of that checkpoint. +// +// Note that this setting can only be useful in cases when some writes are +// performed with Sync = false. Otherwise, the guarantee will already be met. +// +// Passing this option is functionally equivalent to calling +// DB.LogData(nil, Sync) right before DB.Checkpoint. +func WithFlushedWAL() CheckpointOption { + return func(opt *checkpointOptions) { + opt.flushWAL = true + } +} + +// WithRestrictToSpans specifies spans of interest for the checkpoint. Any SSTs +// that don't overlap with any of these spans are excluded from the checkpoint. +// +// Note that the checkpoint can still surface keys outside of these spans (from +// the WAL and from SSTs that partially overlap with these spans). Moreover, +// these surface keys aren't necessarily "valid" in that they could have been +// modified but the SST containing the modification is excluded. +func WithRestrictToSpans(spans []CheckpointSpan) CheckpointOption { + return func(opt *checkpointOptions) { + opt.restrictToSpans = spans + } +} + +// CheckpointSpan is a key range [Start, End) (inclusive on Start, exclusive on +// End) of interest for a checkpoint. +type CheckpointSpan struct { + Start []byte + End []byte +} + +// excludeFromCheckpoint returns true if an SST file should be excluded from the +// checkpoint because it does not overlap with the spans of interest +// (opt.restrictToSpans). +func excludeFromCheckpoint(f *fileMetadata, opt *checkpointOptions, cmp Compare) bool { + if len(opt.restrictToSpans) == 0 { + // Option not set; don't exclude anything. + return false + } + for _, s := range opt.restrictToSpans { + if f.Overlaps(cmp, s.Start, s.End, true /* exclusiveEnd */) { + return false + } + } + // None of the restrictToSpans overlapped; we can exclude this file. + return true +} + +// mkdirAllAndSyncParents creates destDir and any of its missing parents. +// Those missing parents, as well as the closest existing ancestor, are synced. +// Returns a handle to the directory created at destDir. +func mkdirAllAndSyncParents(fs vfs.FS, destDir string) (vfs.File, error) { + // Collect paths for all directories between destDir (excluded) and its + // closest existing ancestor (included). + var parentPaths []string + foundExistingAncestor := false + for parentPath := fs.PathDir(destDir); parentPath != "."; parentPath = fs.PathDir(parentPath) { + parentPaths = append(parentPaths, parentPath) + _, err := fs.Stat(parentPath) + if err == nil { + // Exit loop at the closest existing ancestor. + foundExistingAncestor = true + break + } + if !oserror.IsNotExist(err) { + return nil, err + } + } + // Handle empty filesystem edge case. + if !foundExistingAncestor { + parentPaths = append(parentPaths, "") + } + // Create destDir and any of its missing parents. + if err := fs.MkdirAll(destDir, 0755); err != nil { + return nil, err + } + // Sync all the parent directories up to the closest existing ancestor, + // included. + for _, parentPath := range parentPaths { + parentDir, err := fs.OpenDir(parentPath) + if err != nil { + return nil, err + } + err = parentDir.Sync() + if err != nil { + _ = parentDir.Close() + return nil, err + } + err = parentDir.Close() + if err != nil { + return nil, err + } + } + return fs.OpenDir(destDir) +} + +// Checkpoint constructs a snapshot of the DB instance in the specified +// directory. The WAL, MANIFEST, OPTIONS, and sstables will be copied into the +// snapshot. Hard links will be used when possible. Beware of the significant +// space overhead for a checkpoint if hard links are disabled. Also beware that +// even if hard links are used, the space overhead for the checkpoint will +// increase over time as the DB performs compactions. +func (d *DB) Checkpoint( + destDir string, opts ...CheckpointOption, +) ( + ckErr error, /* used in deferred cleanup */ +) { + opt := &checkpointOptions{} + for _, fn := range opts { + fn(opt) + } + + if _, err := d.opts.FS.Stat(destDir); !oserror.IsNotExist(err) { + if err == nil { + return &os.PathError{ + Op: "checkpoint", + Path: destDir, + Err: oserror.ErrExist, + } + } + return err + } + + if opt.flushWAL && !d.opts.DisableWAL { + // Write an empty log-data record to flush and sync the WAL. + if err := d.LogData(nil /* data */, Sync); err != nil { + return err + } + } + + // Disable file deletions. + d.mu.Lock() + d.disableFileDeletions() + defer func() { + d.mu.Lock() + defer d.mu.Unlock() + d.enableFileDeletions() + }() + + // TODO(peter): RocksDB provides the option to roll the manifest if the + // MANIFEST size is too large. Should we do this too? + + // Lock the manifest before getting the current version. We need the + // length of the manifest that we read to match the current version that + // we read, otherwise we might copy a versionEdit not reflected in the + // sstables we copy/link. + d.mu.versions.logLock() + // Get the unflushed log files, the current version, and the current manifest + // file number. + memQueue := d.mu.mem.queue + current := d.mu.versions.currentVersion() + formatVers := d.FormatMajorVersion() + 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() + d.mu.Unlock() + + // Wrap the normal filesystem with one which wraps newly created files with + // vfs.NewSyncingFile. + fs := vfs.NewSyncingFS(d.opts.FS, vfs.SyncingFileOptions{ + NoSyncOnClose: d.opts.NoSyncOnClose, + BytesPerSync: d.opts.BytesPerSync, + }) + + // Create the dir and its parents (if necessary), and sync them. + var dir vfs.File + defer func() { + if dir != nil { + _ = dir.Close() + } + if ckErr != nil { + // Attempt to cleanup on error. + _ = fs.RemoveAll(destDir) + } + }() + dir, ckErr = mkdirAllAndSyncParents(fs, destDir) + if ckErr != nil { + return ckErr + } + + { + // Link or copy the OPTIONS. + srcPath := base.MakeFilepath(fs, d.dirname, fileTypeOptions, optionsFileNum) + destPath := fs.PathJoin(destDir, fs.PathBase(srcPath)) + ckErr = vfs.LinkOrCopy(fs, srcPath, destPath) + if ckErr != nil { + return ckErr + } + } + + { + // Set the format major version in the destination directory. + var versionMarker *atomicfs.Marker + versionMarker, _, ckErr = atomicfs.LocateMarker(fs, destDir, formatVersionMarkerName) + if ckErr != nil { + return ckErr + } + + // We use the marker to encode the active format version in the + // marker filename. Unlike other uses of the atomic marker, + // there is no file with the filename `formatVers.String()` on + // the filesystem. + ckErr = versionMarker.Move(formatVers.String()) + if ckErr != nil { + return ckErr + } + ckErr = versionMarker.Close() + if ckErr != nil { + return ckErr + } + } + + 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() + for f := iter.First(); f != nil; f = iter.Next() { + if excludeFromCheckpoint(f, opt, d.cmp) { + if excludedFiles == nil { + excludedFiles = make(map[deletedFileEntry]*fileMetadata) + } + excludedFiles[deletedFileEntry{ + Level: l, + FileNum: f.FileNum, + }] = f + continue + } + + 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 { + return ckErr + } + } + } + + 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, manifestSize, + excludedFiles, removeBackingTables, + ) + if ckErr != nil { + return ckErr + } + + // Copy the WAL files. We copy rather than link because WAL file recycling + // will cause the WAL files to be reused which would invalidate the + // checkpoint. + for i := range memQueue { + logNum := memQueue[i].logNum + if logNum == 0 { + continue + } + srcPath := base.MakeFilepath(fs, d.walDirname, fileTypeLog, logNum) + destPath := fs.PathJoin(destDir, fs.PathBase(srcPath)) + ckErr = vfs.Copy(fs, srcPath, destPath) + if ckErr != nil { + return ckErr + } + } + + // Sync and close the checkpoint directory. + ckErr = dir.Sync() + if ckErr != nil { + return ckErr + } + ckErr = dir.Close() + dir = nil + return ckErr +} + +func (d *DB) writeCheckpointManifest( + fs vfs.FS, + formatVers FormatMajorVersion, + destDirPath string, + destDir vfs.File, + 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 + // MANIFEST after we took our snapshot of the sstables will + // reference sstables that aren't in our checkpoint. For a + // similar reason, we need to limit how much of the MANIFEST we + // copy. + // If some files are excluded from the checkpoint, also append a block that + // records those files as deleted. + if err := func() error { + srcPath := base.MakeFilepath(fs, d.dirname, fileTypeManifest, manifestFileNum) + destPath := fs.PathJoin(destDirPath, fs.PathBase(srcPath)) + src, err := fs.Open(srcPath, vfs.SequentialReadsOption) + if err != nil { + return err + } + defer src.Close() + + dst, err := fs.Create(destPath) + if err != nil { + return err + } + defer dst.Close() + + // Copy all existing records. We need to copy at the record level in case we + // 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) + w := record.NewWriter(dst) + for { + rr, err := r.Next() + if err != nil { + if err == io.EOF { + break + } + return err + } + + rw, err := w.Next() + if err != nil { + return err + } + if _, err := io.Copy(rw, rr); err != nil { + return err + } + } + + if len(excludedFiles) > 0 { + // Write out an additional VersionEdit that deletes the excluded SST files. + ve := versionEdit{ + DeletedFiles: excludedFiles, + RemovedBackingTables: removeBackingTables, + } + + rw, err := w.Next() + if err != nil { + return err + } + if err := ve.Encode(rw); err != nil { + return err + } + } + if err := w.Close(); err != nil { + return err + } + return dst.Sync() + }(); err != nil { + return err + } + + // Recent format versions use an atomic marker for setting the + // active manifest. Older versions use the CURRENT file. The + // setCurrentFunc function will return a closure that will + // take the appropriate action for the database's format + // version. + var manifestMarker *atomicfs.Marker + manifestMarker, _, err := atomicfs.LocateMarker(fs, destDirPath, manifestMarkerName) + if err != nil { + return err + } + if err := setCurrentFunc(formatVers, manifestMarker, fs, destDirPath, destDir)(manifestFileNum); err != nil { + return err + } + return manifestMarker.Close() +} diff --git a/pebble/checkpoint_test.go b/pebble/checkpoint_test.go new file mode 100644 index 0000000..e5e20a9 --- /dev/null +++ b/pebble/checkpoint_test.go @@ -0,0 +1,415 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "bytes" + "context" + "fmt" + "math/rand" + "slices" + "sort" + "strings" + "sync" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestCheckpoint(t *testing.T) { + dbs := make(map[string]*DB) + defer func() { + for _, db := range dbs { + if db.closed.Load() == nil { + require.NoError(t, db.Close()) + } + } + }() + + mem := vfs.NewMem() + var memLog base.InMemLogger + opts := &Options{ + FS: vfs.WithLogging(mem, memLog.Infof), + FormatMajorVersion: internalFormatNewest, + L0CompactionThreshold: 10, + DisableAutomaticCompactions: true, + } + opts.private.disableTableStats = true + opts.private.testingAlwaysWaitForCleanup = true + + datadriven.RunTest(t, "testdata/checkpoint", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "batch": + if len(td.CmdArgs) != 1 { + return "batch " + } + memLog.Reset() + d := dbs[td.CmdArgs[0].String()] + b := d.NewBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(Sync); err != nil { + return err.Error() + } + return memLog.String() + + case "checkpoint": + if !(len(td.CmdArgs) == 2 || (len(td.CmdArgs) == 3 && td.CmdArgs[2].Key == "restrict")) { + return "checkpoint [restrict=(start-end, ...)]" + } + var opts []CheckpointOption + if len(td.CmdArgs) == 3 { + var spans []CheckpointSpan + for _, v := range td.CmdArgs[2].Vals { + splits := strings.SplitN(v, "-", 2) + if len(splits) != 2 { + return fmt.Sprintf("invalid restrict range %q", v) + } + spans = append(spans, CheckpointSpan{ + Start: []byte(splits[0]), + End: []byte(splits[1]), + }) + } + opts = append(opts, WithRestrictToSpans(spans)) + } + memLog.Reset() + d := dbs[td.CmdArgs[0].String()] + if err := d.Checkpoint(td.CmdArgs[1].String(), opts...); err != nil { + return err.Error() + } + return memLog.String() + + case "ingest-and-excise": + d := dbs[td.CmdArgs[0].String()] + + // Hacky but the command doesn't expect a db string. Get rid of it. + td.CmdArgs = td.CmdArgs[1:] + if err := runIngestAndExciseCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + + case "build": + d := dbs[td.CmdArgs[0].String()] + + // Hacky but the command doesn't expect a db string. Get rid of it. + td.CmdArgs = td.CmdArgs[1:] + if err := runBuildCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + + case "lsm": + d := dbs[td.CmdArgs[0].String()] + + // Hacky but the command doesn't expect a db string. Get rid of it. + td.CmdArgs = td.CmdArgs[1:] + return runLSMCmd(td, d) + + case "compact": + if len(td.CmdArgs) != 1 { + return "compact " + } + memLog.Reset() + d := dbs[td.CmdArgs[0].String()] + if err := d.Compact(nil, []byte("\xff"), false); err != nil { + return err.Error() + } + d.TestOnlyWaitForCleaning() + return memLog.String() + + case "print-backing": + // prints contents of the file backing map in the version. Used to + // test whether the checkpoint removed the filebackings correctly. + if len(td.CmdArgs) != 1 { + return "print-backing " + } + d := dbs[td.CmdArgs[0].String()] + d.mu.Lock() + d.mu.versions.logLock() + var fileNums []base.DiskFileNum + for _, b := range d.mu.versions.backingState.fileBackingMap { + fileNums = append(fileNums, b.DiskFileNum) + } + d.mu.versions.logUnlock() + d.mu.Unlock() + + slices.Sort(fileNums) + var buf bytes.Buffer + for _, f := range fileNums { + buf.WriteString(fmt.Sprintf("%s\n", f.String())) + } + return buf.String() + + case "close": + if len(td.CmdArgs) != 1 { + return "close " + } + d := dbs[td.CmdArgs[0].String()] + require.NoError(t, d.Close()) + return "" + + case "flush": + if len(td.CmdArgs) != 1 { + return "flush " + } + memLog.Reset() + d := dbs[td.CmdArgs[0].String()] + if err := d.Flush(); err != nil { + return err.Error() + } + return memLog.String() + + case "list": + if len(td.CmdArgs) != 1 { + return "list " + } + paths, err := mem.List(td.CmdArgs[0].String()) + if err != nil { + return err.Error() + } + sort.Strings(paths) + return fmt.Sprintf("%s\n", strings.Join(paths, "\n")) + + case "open": + if len(td.CmdArgs) != 1 && len(td.CmdArgs) != 2 { + return "open [readonly]" + } + opts.ReadOnly = false + if len(td.CmdArgs) == 2 { + if td.CmdArgs[1].String() != "readonly" { + return "open [readonly]" + } + opts.ReadOnly = true + } + + memLog.Reset() + dir := td.CmdArgs[0].String() + d, err := Open(dir, opts) + if err != nil { + return err.Error() + } + dbs[dir] = d + return memLog.String() + + case "scan": + if len(td.CmdArgs) != 1 { + return "scan " + } + memLog.Reset() + d := dbs[td.CmdArgs[0].String()] + iter, _ := d.NewIter(nil) + for valid := iter.First(); valid; valid = iter.Next() { + memLog.Infof("%s %s", iter.Key(), iter.Value()) + } + memLog.Infof(".") + if err := iter.Close(); err != nil { + memLog.Infof("%v\n", err) + } + return memLog.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestCheckpointCompaction(t *testing.T) { + fs := vfs.NewMem() + d, err := Open("", &Options{FS: fs}) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + + var wg sync.WaitGroup + wg.Add(4) + go func() { + defer cancel() + defer wg.Done() + for i := 0; ctx.Err() == nil; i++ { + if err := d.Set([]byte(fmt.Sprintf("key%06d", i)), nil, nil); err != nil { + t.Error(err) + return + } + } + }() + go func() { + defer cancel() + defer wg.Done() + for ctx.Err() == nil { + if err := d.Compact([]byte("key"), []byte("key999999"), false); err != nil { + t.Error(err) + return + } + } + }() + check := make(chan string, 100) + go func() { + defer cancel() + defer close(check) + defer wg.Done() + for i := 0; ctx.Err() == nil && i < 200; i++ { + dir := fmt.Sprintf("checkpoint%06d", i) + if err := d.Checkpoint(dir); err != nil { + t.Error(err) + return + } + select { + case <-ctx.Done(): + return + case check <- dir: + } + } + }() + go func() { + opts := &Options{FS: fs} + defer cancel() + defer wg.Done() + for dir := range check { + d2, err := Open(dir, opts) + if err != nil { + t.Error(err) + return + } + // Check the checkpoint has all the sstables that the manifest + // claims it has. + tableInfos, _ := d2.SSTables() + for _, tables := range tableInfos { + for _, tbl := range tables { + if tbl.Virtual { + continue + } + if _, err := fs.Stat(base.MakeFilepath(fs, dir, base.FileTypeTable, tbl.FileNum.DiskFileNum())); err != nil { + t.Error(err) + return + } + } + } + if err := d2.Close(); err != nil { + t.Error(err) + return + } + } + }() + <-ctx.Done() + wg.Wait() + require.NoError(t, d.Close()) +} + +func TestCheckpointFlushWAL(t *testing.T) { + const checkpointPath = "checkpoints/checkpoint" + fs := vfs.NewStrictMem() + opts := &Options{FS: fs} + key, value := []byte("key"), []byte("value") + + // Create a checkpoint from an unsynced DB. + { + d, err := Open("", opts) + require.NoError(t, err) + { + wb := d.NewBatch() + err = wb.Set(key, value, nil) + require.NoError(t, err) + err = d.Apply(wb, NoSync) + require.NoError(t, err) + } + err = d.Checkpoint(checkpointPath, WithFlushedWAL()) + require.NoError(t, err) + require.NoError(t, d.Close()) + fs.ResetToSyncedState() + } + + // Check that the WAL has been flushed in the checkpoint. + { + files, err := fs.List(checkpointPath) + require.NoError(t, err) + hasLogFile := false + for _, f := range files { + info, err := fs.Stat(fs.PathJoin(checkpointPath, f)) + require.NoError(t, err) + if strings.HasSuffix(f, ".log") { + hasLogFile = true + require.NotZero(t, info.Size()) + } + } + require.True(t, hasLogFile) + } + + // Check that the checkpoint contains the expected data. + { + d, err := Open(checkpointPath, opts) + require.NoError(t, err) + iter, _ := d.NewIter(nil) + require.True(t, iter.First()) + require.Equal(t, key, iter.Key()) + require.Equal(t, value, iter.Value()) + require.False(t, iter.Next()) + require.NoError(t, iter.Close()) + require.NoError(t, d.Close()) + } +} + +func TestCheckpointManyFiles(t *testing.T) { + if testing.Short() { + t.Skip("skipping because of short flag") + } + const checkpointPath = "checkpoint" + opts := &Options{ + FS: vfs.NewMem(), + FormatMajorVersion: internalFormatNewest, + DisableAutomaticCompactions: true, + } + // Disable compression to speed up the test. + opts.EnsureDefaults() + for i := range opts.Levels { + opts.Levels[i].Compression = NoCompression + } + + d, err := Open("", opts) + require.NoError(t, err) + defer d.Close() + + mkKey := func(x int) []byte { + return []byte(fmt.Sprintf("key%06d", x)) + } + // We want to test the case where the appended record with the excluded files + // makes the manifest cross 32KB. This will happen for a range of values + // around 450. + n := 400 + rand.Intn(100) + for i := 0; i < n; i++ { + err := d.Set(mkKey(i), nil, nil) + require.NoError(t, err) + err = d.Flush() + require.NoError(t, err) + } + err = d.Checkpoint(checkpointPath, WithRestrictToSpans([]CheckpointSpan{ + { + Start: mkKey(0), + End: mkKey(10), + }, + })) + require.NoError(t, err) + + // Open the checkpoint and iterate through all the keys. + { + d, err := Open(checkpointPath, opts) + require.NoError(t, err) + iter, _ := d.NewIter(nil) + require.True(t, iter.First()) + require.NoError(t, iter.Error()) + n := 1 + for iter.Next() { + n++ + } + require.NoError(t, iter.Error()) + require.NoError(t, iter.Close()) + require.NoError(t, d.Close()) + require.Equal(t, 10, n) + } +} diff --git a/pebble/cleaner.go b/pebble/cleaner.go new file mode 100644 index 0000000..f9fa43b --- /dev/null +++ b/pebble/cleaner.go @@ -0,0 +1,295 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "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 + +// DeleteCleaner exports the base.DeleteCleaner type. +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/pebble/cleaner_test.go b/pebble/cleaner_test.go new file mode 100644 index 0000000..11d9ab9 --- /dev/null +++ b/pebble/cleaner_test.go @@ -0,0 +1,137 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "fmt" + "sort" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestCleaner(t *testing.T) { + dbs := make(map[string]*DB) + defer func() { + for _, db := range dbs { + require.NoError(t, db.Close()) + } + }() + + mem := vfs.NewMem() + var memLog base.InMemLogger + fs := vfs.WithLogging(mem, memLog.Infof) + datadriven.RunTest(t, "testdata/cleaner", func(t *testing.T, td *datadriven.TestData) string { + memLog.Reset() + switch td.Cmd { + case "batch": + if len(td.CmdArgs) != 1 { + return "batch " + } + d := dbs[td.CmdArgs[0].String()] + b := d.NewBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(Sync); err != nil { + return err.Error() + } + return memLog.String() + + case "compact": + if len(td.CmdArgs) != 1 { + return "compact " + } + d := dbs[td.CmdArgs[0].String()] + if err := d.Compact(nil, []byte("\xff"), false); err != nil { + return err.Error() + } + return memLog.String() + + case "flush": + if len(td.CmdArgs) != 1 { + return "flush " + } + d := dbs[td.CmdArgs[0].String()] + if err := d.Flush(); err != nil { + return err.Error() + } + return memLog.String() + + case "close": + if len(td.CmdArgs) != 1 { + return "close " + } + dbDir := td.CmdArgs[0].String() + d := dbs[dbDir] + if err := d.Close(); err != nil { + return err.Error() + } + delete(dbs, dbDir) + return memLog.String() + + case "list": + if len(td.CmdArgs) != 1 { + return "list " + } + paths, err := mem.List(td.CmdArgs[0].String()) + if err != nil { + return err.Error() + } + sort.Strings(paths) + return fmt.Sprintf("%s\n", strings.Join(paths, "\n")) + + case "open": + if len(td.CmdArgs) < 1 || len(td.CmdArgs) > 3 { + return "open [archive] [readonly]" + } + dir := td.CmdArgs[0].String() + opts := (&Options{ + FS: fs, + WALDir: dir + "_wal", + }).WithFSDefaults() + + for i := 1; i < len(td.CmdArgs); i++ { + switch td.CmdArgs[i].String() { + case "readonly": + opts.ReadOnly = true + case "archive": + opts.Cleaner = ArchiveCleaner{} + default: + return "open [archive] [readonly]" + } + } + // Asynchronous table stats retrieval makes the output flaky. + opts.private.disableTableStats = true + opts.private.testingAlwaysWaitForCleanup = true + d, err := Open(dir, opts) + if err != nil { + return err.Error() + } + d.TestOnlyWaitForCleaning() + dbs[dir] = d + return memLog.String() + + case "create-bogus-file": + if len(td.CmdArgs) != 1 { + return "create-bogus-file " + } + dst, err := fs.Create(td.CmdArgs[0].String()) + require.NoError(t, err) + _, err = dst.Write([]byte("bogus data")) + require.NoError(t, err) + require.NoError(t, dst.Sync()) + require.NoError(t, dst.Close()) + return memLog.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} diff --git a/pebble/cmd/pebble/.gitignore b/pebble/cmd/pebble/.gitignore new file mode 100644 index 0000000..812a2be --- /dev/null +++ b/pebble/cmd/pebble/.gitignore @@ -0,0 +1 @@ +pebble diff --git a/pebble/cmd/pebble/db.go b/pebble/cmd/pebble/db.go new file mode 100644 index 0000000..41c6e59 --- /dev/null +++ b/pebble/cmd/pebble/db.go @@ -0,0 +1,168 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package main + +import ( + "log" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/bytealloc" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/vfs" +) + +// DB specifies the minimal interfaces that need to be implemented to support +// the pebble command. +type DB interface { + NewIter(*pebble.IterOptions) iterator + NewBatch() batch + Scan(iter iterator, key []byte, count int64, reverse bool) error + Metrics() *pebble.Metrics + Flush() error +} + +type iterator interface { + SeekLT(key []byte) bool + SeekGE(key []byte) bool + Valid() bool + Key() []byte + Value() []byte + First() bool + Next() bool + Last() bool + Prev() bool + Close() error +} + +type batch interface { + Close() error + Commit(opts *pebble.WriteOptions) error + Set(key, value []byte, opts *pebble.WriteOptions) error + Delete(key []byte, opts *pebble.WriteOptions) error + LogData(data []byte, opts *pebble.WriteOptions) error +} + +// Adapters for Pebble. Since the interfaces above are based on Pebble's +// interfaces, it can simply forward calls for everything. +type pebbleDB struct { + d *pebble.DB + ballast []byte +} + +func newPebbleDB(dir string) DB { + cache := pebble.NewCache(cacheSize) + defer cache.Unref() + opts := &pebble.Options{ + Cache: cache, + Comparer: mvccComparer, + DisableWAL: disableWAL, + FormatMajorVersion: pebble.FormatNewest, + L0CompactionThreshold: 2, + L0StopWritesThreshold: 1000, + LBaseMaxBytes: 64 << 20, // 64 MB + Levels: make([]pebble.LevelOptions, 7), + MaxOpenFiles: 16384, + MemTableSize: 64 << 20, + MemTableStopWritesThreshold: 4, + Merger: &pebble.Merger{ + Name: "cockroach_merge_operator", + }, + MaxConcurrentCompactions: func() int { + return 3 + }, + } + + for i := 0; i < len(opts.Levels); i++ { + l := &opts.Levels[i] + l.BlockSize = 32 << 10 // 32 KB + l.IndexBlockSize = 256 << 10 // 256 KB + l.FilterPolicy = bloom.FilterPolicy(10) + l.FilterType = pebble.TableFilter + if i > 0 { + l.TargetFileSize = opts.Levels[i-1].TargetFileSize * 2 + } + l.EnsureDefaults() + } + opts.Levels[6].FilterPolicy = nil + opts.FlushSplitBytes = opts.Levels[0].TargetFileSize + + opts.EnsureDefaults() + + if verbose { + lel := pebble.MakeLoggingEventListener(nil) + opts.EventListener = &lel + opts.EventListener.TableDeleted = nil + opts.EventListener.TableIngested = nil + opts.EventListener.WALCreated = nil + opts.EventListener.WALDeleted = nil + } + + if pathToLocalSharedStorage != "" { + opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + // Store all shared objects on local disk, for convenience. + "": remote.NewLocalFS(pathToLocalSharedStorage, vfs.Default), + }) + opts.Experimental.CreateOnShared = remote.CreateOnSharedAll + if secondaryCacheSize != 0 { + opts.Experimental.SecondaryCacheSizeBytes = secondaryCacheSize + } + } + + p, err := pebble.Open(dir, opts) + if err != nil { + log.Fatal(err) + } + if pathToLocalSharedStorage != "" { + if err := p.SetCreatorID(1); err != nil { + log.Fatal(err) + } + } + return pebbleDB{ + d: p, + ballast: make([]byte, 1<<30), + } +} + +func (p pebbleDB) Flush() error { + return p.d.Flush() +} + +func (p pebbleDB) NewIter(opts *pebble.IterOptions) iterator { + iter, _ := p.d.NewIter(opts) + return iter +} + +func (p pebbleDB) NewBatch() batch { + return p.d.NewBatch() +} + +func (p pebbleDB) Scan(iter iterator, key []byte, count int64, reverse bool) error { + var data bytealloc.A + if reverse { + for i, valid := 0, iter.SeekLT(key); valid; valid = iter.Prev() { + data, _ = data.Copy(iter.Key()) + data, _ = data.Copy(iter.Value()) + i++ + if i >= int(count) { + break + } + } + } else { + for i, valid := 0, iter.SeekGE(key); valid; valid = iter.Next() { + data, _ = data.Copy(iter.Key()) + data, _ = data.Copy(iter.Value()) + i++ + if i >= int(count) { + break + } + } + } + return nil +} + +func (p pebbleDB) Metrics() *pebble.Metrics { + return p.d.Metrics() +} diff --git a/pebble/cmd/pebble/fsbench.go b/pebble/cmd/pebble/fsbench.go new file mode 100644 index 0000000..94d437d --- /dev/null +++ b/pebble/cmd/pebble/fsbench.go @@ -0,0 +1,707 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "os" + "path" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/vfs" + "github.com/spf13/cobra" +) + +var fsBenchCmd = &cobra.Command{ + Use: "fs ", + Short: "Run file system benchmarks.", + Long: ` +Run file system benchmarks. Each benchmark is predefined and can be +run using the command "bench fs --bench-name ". +Each possible which can be run is defined in the code. +Benchmarks may require the specification of a --duration or +--max-ops flag, to prevent the benchmark from running forever +or running out of memory. + +The --num-times flag can be used to run the entire benchmark, more than +once. If the flag isn't provided, then the benchmark is only run once. +`, + Args: cobra.ExactArgs(1), + RunE: runFsBench, +} + +const writeBatchSize = 1 << 10 + +var fsConfig struct { + // An upper limit on the number of ops which can be run. + maxOps int + + // Benchmark to run. + benchname string + + // Number of times each benchmark should be run. + numTimes int + + fs vfs.FS + + precomputedWriteBatch []byte +} + +func init() { + fsBenchCmd.Flags().IntVar( + &fsConfig.maxOps, "max-ops", 0, + "Maximum number of times the operation which is being benchmarked should be run.", + ) + + fsBenchCmd.Flags().StringVar( + &fsConfig.benchname, "bench-name", "", "The benchmark to run.") + fsBenchCmd.MarkFlagRequired("bench-name") + + fsBenchCmd.Flags().IntVar( + &fsConfig.numTimes, "num-times", 1, + "Number of times each benchmark should be run.") + + // Add subcommand to list + fsBenchCmd.AddCommand(listFsBench) + + // Just use the default vfs implementation for now. + fsConfig.fs = vfs.Default + + fsConfig.precomputedWriteBatch = bytes.Repeat([]byte("a"), writeBatchSize) +} + +// State relevant to a benchmark. +type fsBench struct { + // A short name for the benchmark. + name string + + // A one line description for the benchmark. + description string + + // numOps is the total number of ops which + // have been run for the benchmark. This is used + // to make sure that we don't benchmark the operation + // more than max-ops times. + numOps int + + // directory under which the benchmark is run. + dir vfs.File + dirName string + + // Stats associated with the benchmark. + reg *histogramRegistry + + // The operation which we're benchmarking. This + // will be called over and over again. + // Returns false if run should no longer be called. + run func(*namedHistogram) bool + + // Stop the benchmark from executing any further. + // Stop is safe to call concurrently with run. + stop func() + + // A cleanup func which must be called after + // the benchmark has finished running. + // Clean should be only called after making sure + // that the run function is no longer executing. + clean func() +} + +// createFile can be used to create an empty file. +// Invariant: File shouldn't already exist. +func createFile(filepath string) vfs.File { + fh, err := fsConfig.fs.Create(filepath) + if err != nil { + log.Fatalln(err) + } + return fh +} + +// Invariant: file with filepath should exist. +func deleteFile(filepath string) { + err := fsConfig.fs.Remove(filepath) + if err != nil { + log.Fatalln(err) + } +} + +// Write size bytes to the file in batches. +func writeToFile(fh vfs.File, size int64) { + for size > 0 { + var toWrite []byte + if size >= writeBatchSize { + toWrite = fsConfig.precomputedWriteBatch + } else { + toWrite = fsConfig.precomputedWriteBatch[:size] + } + written, err := fh.Write(toWrite) + if err != nil { + log.Fatalln(err) + } + if written != len(toWrite) { + log.Fatalf("Couldn't write %d bytes to file\n", size) + } + size -= int64(len(toWrite)) + } +} + +func syncFile(fh vfs.File) { + err := fh.Sync() + if err != nil { + log.Fatalln(err) + } +} + +func closeFile(fh vfs.File) { + err := fh.Close() + if err != nil { + log.Fatalln(err) + } +} + +func getDiskUsage(filepath string) { + _, err := fsConfig.fs.GetDiskUsage(filepath) + if err != nil { + log.Fatalln(err) + } +} + +func openDir(filepath string) vfs.File { + fh, err := fsConfig.fs.OpenDir(filepath) + if err != nil { + log.Fatalln(err) + } + return fh +} + +func mkDir(filepath string) { + err := fsConfig.fs.MkdirAll(filepath, 0755) + if err != nil { + log.Fatalln(err) + } +} + +func removeAllFiles(filepath string) { + err := fsConfig.fs.RemoveAll(filepath) + if err != nil { + log.Fatalln(err) + } +} + +// fileSize is in bytes. +func createBench(benchName string, benchDescription string) fsBenchmark { + createBench := func(dirpath string) *fsBench { + bench := &fsBench{} + mkDir(dirpath) + fh := openDir(dirpath) + + bench.dir = fh + bench.dirName = dirpath + bench.reg = newHistogramRegistry() + bench.numOps = 0 + bench.name = benchName + bench.description = benchDescription + + // setup the operation to benchmark, and the cleanup functions. + pref := "temp_" + var numFiles int + var done atomic.Bool + + bench.run = func(hist *namedHistogram) bool { + if done.Load() { + return false + } + + start := time.Now() + fh := createFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, numFiles))) + syncFile(bench.dir) + hist.Record(time.Since(start)) + + closeFile(fh) + numFiles++ + return true + } + + bench.stop = func() { + done.Store(true) + } + + bench.clean = func() { + removeAllFiles(dirpath) + closeFile(bench.dir) + } + + return bench + } + + return fsBenchmark{ + createBench, + benchName, + benchDescription, + } +} + +// This benchmark prepopulates a directory with some files of a given size. Then, it creates and deletes +// a file of some size, while measuring only the performance of the delete. +func deleteBench( + benchName string, benchDescription string, preNumFiles int, preFileSize int64, fileSize int64, +) fsBenchmark { + + createBench := func(dirpath string) *fsBench { + bench := &fsBench{} + mkDir(dirpath) + fh := openDir(dirpath) + + bench.dir = fh + bench.dirName = dirpath + bench.reg = newHistogramRegistry() + bench.numOps = 0 + bench.name = benchName + bench.description = benchDescription + + // prepopulate the directory + prePref := "pre_temp_" + for i := 0; i < preNumFiles; i++ { + fh := createFile(path.Join(dirpath, fmt.Sprintf("%s%d", prePref, i))) + if preFileSize > 0 { + writeToFile(fh, preFileSize) + syncFile(fh) + } + closeFile(fh) + } + syncFile(bench.dir) + + var done atomic.Bool + bench.run = func(hist *namedHistogram) bool { + if done.Load() { + return false + } + + filename := "newfile" + fh := createFile(path.Join(dirpath, filename)) + writeToFile(fh, fileSize) + syncFile(fh) + + start := time.Now() + deleteFile(path.Join(dirpath, filename)) + hist.Record(time.Since(start)) + + return true + } + + bench.stop = func() { + done.Store(true) + } + + bench.clean = func() { + removeAllFiles(dirpath) + closeFile(bench.dir) + } + + return bench + } + + return fsBenchmark{ + createBench, + benchName, + benchDescription, + } +} + +// This benchmark creates some files in a directory, and then measures the performance +// of the vfs.Remove function. +// fileSize is in bytes. +func deleteUniformBench( + benchName string, benchDescription string, numFiles int, fileSize int64, +) fsBenchmark { + createBench := func(dirpath string) *fsBench { + bench := &fsBench{} + mkDir(dirpath) + fh := openDir(dirpath) + + bench.dir = fh + bench.dirName = dirpath + bench.reg = newHistogramRegistry() + bench.numOps = 0 + bench.name = benchName + bench.description = benchDescription + + // setup the operation to benchmark, and the cleaup functions. + pref := "temp_" + for i := 0; i < numFiles; i++ { + fh := createFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, i))) + if fileSize > 0 { + writeToFile(fh, fileSize) + syncFile(fh) + } + closeFile(fh) + } + syncFile(bench.dir) + + var done atomic.Bool + bench.run = func(hist *namedHistogram) bool { + if done.Load() { + return false + } + + if numFiles == 0 { + return false + } + + start := time.Now() + deleteFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, numFiles-1))) + hist.Record(time.Since(start)) + + numFiles-- + return true + } + + bench.stop = func() { + done.Store(true) + } + + bench.clean = func() { + removeAll(dirpath) + closeFile(bench.dir) + } + + return bench + } + + return fsBenchmark{ + createBench, + benchName, + benchDescription, + } +} + +// Tests the performance of syncing data to disk. +// Only measures the sync performance. +// The writes will be synced after every writeSize bytes have been written. +func writeSyncBench( + benchName string, benchDescription string, maxFileSize int64, writeSize int64, +) fsBenchmark { + + if writeSize > maxFileSize { + log.Fatalln("File write threshold is greater than max file size.") + } + + createBench := func(dirpath string) *fsBench { + bench := &fsBench{} + mkDir(dirpath) + fh := openDir(dirpath) + + bench.dir = fh + bench.dirName = dirpath + bench.reg = newHistogramRegistry() + bench.numOps = 0 + bench.name = benchName + bench.description = benchDescription + + pref := "temp_" + var benchData struct { + done atomic.Bool + fh vfs.File + fileNum int + bytesWritten int64 + } + benchData.fh = createFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, benchData.fileNum))) + + bench.run = func(hist *namedHistogram) bool { + if benchData.done.Load() { + return false + } + + if benchData.bytesWritten+writeSize > maxFileSize { + closeFile(benchData.fh) + benchData.fileNum++ + benchData.bytesWritten = 0 + benchData.fh = createFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, benchData.fileNum))) + } + + benchData.bytesWritten += writeSize + writeToFile(benchData.fh, writeSize) + + start := time.Now() + syncFile(benchData.fh) + hist.Record(time.Since(start)) + + return true + } + + bench.stop = func() { + benchData.done.Store(true) + } + + bench.clean = func() { + closeFile(benchData.fh) + removeAllFiles(dirpath) + closeFile(bench.dir) + } + + return bench + } + + return fsBenchmark{ + createBench, + benchName, + benchDescription, + } +} + +// Tests the peformance of calling the vfs.GetDiskUsage call on a directory, +// as the number of files/total size of files in the directory grows. +func diskUsageBench( + benchName string, benchDescription string, maxFileSize int64, writeSize int64, +) fsBenchmark { + + if writeSize > maxFileSize { + log.Fatalln("File write threshold is greater than max file size.") + } + + createBench := func(dirpath string) *fsBench { + bench := &fsBench{} + mkDir(dirpath) + fh := openDir(dirpath) + + bench.dir = fh + bench.dirName = dirpath + bench.reg = newHistogramRegistry() + bench.numOps = 0 + bench.name = benchName + bench.description = benchDescription + + pref := "temp_" + var benchData struct { + done atomic.Bool + fh vfs.File + fileNum int + bytesWritten int64 + } + benchData.fh = createFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, benchData.fileNum))) + + bench.run = func(hist *namedHistogram) bool { + if benchData.done.Load() { + return false + } + + if benchData.bytesWritten+writeSize > maxFileSize { + closeFile(benchData.fh) + benchData.fileNum++ + benchData.bytesWritten = 0 + benchData.fh = createFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, benchData.fileNum))) + } + + benchData.bytesWritten += writeSize + writeToFile(benchData.fh, writeSize) + syncFile(benchData.fh) + + start := time.Now() + getDiskUsage(dirpath) + hist.Record(time.Since(start)) + + return true + } + + bench.stop = func() { + benchData.done.Store(true) + } + + bench.clean = func() { + closeFile(benchData.fh) + removeAllFiles(dirpath) + closeFile(bench.dir) + } + + return bench + } + + return fsBenchmark{ + createBench, + benchName, + benchDescription, + } +} + +// A benchmark is a function which takes a directory +// as input and returns the fsBench struct which has +// all the information required to run the benchmark. +type fsBenchmark struct { + createBench func(string) *fsBench + name string + description string +} + +// The various benchmarks which can be run. +var benchmarks = map[string]fsBenchmark{ + "create_empty": createBench("create_empty", "create empty file, sync par dir"), + "delete_10k_2MiB": deleteUniformBench( + "delete_10k_2MiB", "create 10k 2MiB size files, measure deletion times", 10_000, 2<<20, + ), + "delete_100k_2MiB": deleteUniformBench( + "delete_100k_2MiB", "create 100k 2MiB size files, measure deletion times", 100_000, 2<<20, + ), + "delete_200k_2MiB": deleteUniformBench( + "delete_200k_2MiB", "create 200k 2MiB size files, measure deletion times", 200_000, 2<<20, + ), + "write_sync_1MiB": writeSyncBench( + "write_sync_1MiB", "Write 1MiB to a file, then sync, while timing the sync.", 2<<30, 1<<20, + ), + "write_sync_16MiB": writeSyncBench( + "write_sync_16MiB", "Write 16MiB to a file, then sync, while timing the sync.", 2<<30, 16<<20, + ), + "write_sync_128MiB": writeSyncBench( + "write_sync_128MiB", "Write 128MiB to a file, then sync, while timing the sync.", 2<<30, 128<<20, + ), + "disk_usage_128MB": diskUsageBench( + "disk_usage_128MB", + "Write 128MiB to a file, measure GetDiskUsage call. Create a new file, when file size is 1GB.", + 1<<30, 128<<20, + ), + "disk_usage_many_files": diskUsageBench( + "disk_usage_many_files", + "Create new file, Write 128KiB to a file, measure GetDiskUsage call.", + 128<<10, 128<<10, + ), + "delete_large_dir_256MiB": deleteBench( + "delete_large_dir_256MiB", "Prepopulate directory with 100k 1MiB files, measure delete peformance of 256MiB files", + 1e5, 1<<20, 256<<20, + ), + "delete_large_dir_2MiB": deleteBench( + "delete_large_dir_2MiB", "Prepopulate directory with 100k 1MiB files, measure delete peformance of 2MiB files", + 1e5, 1<<20, 2<<20, + ), + "delete_small_dir_2GiB": deleteBench( + "delete_small_dir_2GiB", "Prepopulate directory with 1k 1MiB files, measure delete peformance of 2GiB files", + 1e3, 1<<20, 2<<30, + ), + "delete_small_dir_256MiB": deleteBench( + "delete_small_dir_256MiB", "Prepopulate directory with 1k 1MiB files, measure delete peformance of 256MiB files", + 1e3, 1<<20, 256<<20, + ), + "delete_small_dir_2MiB": deleteBench( + "delete_small_dir_2MiB", "Prepopulate directory with 1k 1MiB files, measure delete peformance of 2MiB files", + 1e3, 1<<20, 2<<20, + ), +} + +func runFsBench(_ *cobra.Command, args []string) error { + benchmark, ok := benchmarks[fsConfig.benchname] + if !ok { + return errors.Errorf("trying to run an unknown benchmark: %s", fsConfig.benchname) + } + + // Run the benchmark a comple of times. + fmt.Printf("The benchmark will be run %d time(s).\n", fsConfig.numTimes) + for i := 0; i < fsConfig.numTimes; i++ { + fmt.Println("Starting benchmark:", i) + benchStruct := benchmark.createBench(args[0]) + runTestWithoutDB(testWithoutDB{ + init: benchStruct.init, + tick: benchStruct.tick, + done: benchStruct.done, + }) + } + return nil +} + +func (bench *fsBench) init(wg *sync.WaitGroup) { + fmt.Println("Running benchmark:", bench.name) + fmt.Println("Description:", bench.description) + + wg.Add(1) + go bench.execute(wg) +} + +func (bench *fsBench) execute(wg *sync.WaitGroup) { + defer wg.Done() + + latencyHist := bench.reg.Register(bench.name) + + for { + // run the op which we're benchmarking. + bench.numOps++ + + // The running function will determine exactly what to latency + // it wants to measure. + continueBench := bench.run(latencyHist) + if !continueBench || (fsConfig.maxOps > 0 && bench.numOps >= fsConfig.maxOps) { + break + } + } +} + +func (bench *fsBench) tick(elapsed time.Duration, i int) { + if i%20 == 0 { + fmt.Println("____optype__elapsed__ops/sec(inst)___ops/sec(cum)__p50(ms)__p95(ms)__p99(ms)__pMax(ms)") + } + bench.reg.Tick(func(tick histogramTick) { + h := tick.Hist + + fmt.Printf("%10s %8s %14.1f %14.1f %5.6f %5.6f %5.6f %5.6f\n", + tick.Name[:10], + time.Duration(elapsed.Seconds()+0.5)*time.Second, + float64(h.TotalCount())/tick.Elapsed.Seconds(), + float64(tick.Cumulative.TotalCount())/elapsed.Seconds(), + time.Duration(h.ValueAtQuantile(50)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(95)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(99)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(100)).Seconds()*1000, + ) + }) +} + +func (bench *fsBench) done(wg *sync.WaitGroup, elapsed time.Duration) { + // Do the cleanup. + bench.stop() + wg.Wait() + defer bench.clean() + + fmt.Println("\n____optype__elapsed_____ops(total)___ops/sec(cum)__avg(ms)__p50(ms)__p95(ms)__p99(ms)__pMax(ms)") + + resultTick := histogramTick{} + bench.reg.Tick(func(tick histogramTick) { + h := tick.Cumulative + if resultTick.Cumulative == nil { + resultTick.Now = tick.Now + resultTick.Cumulative = h + } else { + resultTick.Cumulative.Merge(h) + } + + fmt.Printf("%10s %7.1fs %14d %14.1f %5.6f %5.6f %5.6f %5.6f %5.6f\n", + tick.Name[:10], elapsed.Seconds(), h.TotalCount(), + float64(h.TotalCount())/elapsed.Seconds(), + time.Duration(h.Mean()).Seconds()*1000, + time.Duration(h.ValueAtQuantile(50)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(95)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(99)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(100)).Seconds()*1000, + ) + }) + fmt.Println() + + resultHist := resultTick.Cumulative + + fmt.Printf("Benchmarkfsbench/%s %d %0.1f ops/sec\n\n", + bench.name, + resultHist.TotalCount(), + float64(resultHist.TotalCount())/elapsed.Seconds(), + ) +} + +func verbosef(fmtstr string, args ...interface{}) { + if verbose { + fmt.Printf(fmtstr, args...) + } +} + +func removeAll(dir string) { + verbosef("Removing %q.\n", dir) + if err := os.RemoveAll(dir); err != nil { + log.Fatal(err) + } +} diff --git a/pebble/cmd/pebble/fsbenchlist.go b/pebble/cmd/pebble/fsbenchlist.go new file mode 100644 index 0000000..467af81 --- /dev/null +++ b/pebble/cmd/pebble/fsbenchlist.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + + "github.com/cockroachdb/errors" + "github.com/spf13/cobra" +) + +var listFsBench = &cobra.Command{ + Use: "list [] [] ...", + Short: "List the available file system benchmarks.", + Long: ` +List the available file system benchmarks. If no is supplied +as an argument, then all the available benchmark names are printed. +If one or more s are supplied as arguments, then the benchmark +descriptions are printed out for those names. +`, + RunE: runListFsBench, +} + +func runListFsBench(_ *cobra.Command, args []string) error { + if len(args) == 0 { + fmt.Println("Available benchmarks:") + for name := range benchmarks { + fmt.Println(name) + } + } else { + for _, v := range args { + benchStruct, ok := benchmarks[v] + if !ok { + return errors.Errorf("trying to print out the description for unknown benchmark: %s", v) + } + fmt.Println("Name:", benchStruct.name) + fmt.Println("Description:", benchStruct.description) + } + } + return nil +} diff --git a/pebble/cmd/pebble/main.go b/pebble/cmd/pebble/main.go new file mode 100644 index 0000000..9417bfb --- /dev/null +++ b/pebble/cmd/pebble/main.go @@ -0,0 +1,99 @@ +// Copyright 2018 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 main + +import ( + "log" + "os" + "time" + + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/tool" + "github.com/spf13/cobra" +) + +var ( + cacheSize int64 + concurrency int + disableWAL bool + duration time.Duration + maxSize uint64 + maxOpsPerSec = newRateFlag("") + verbose bool + waitCompactions bool + wipe bool + pathToLocalSharedStorage string + // If zero, or if !sharedStorageEnabled, secondary cache is + // not used. + secondaryCacheSize int64 +) + +func main() { + log.SetFlags(0) + + cobra.EnableCommandSorting = false + + benchCmd := &cobra.Command{ + Use: "bench", + Short: "benchmarks", + } + + replayCmd := initReplayCmd() + benchCmd.AddCommand( + replayCmd, + scanCmd, + syncCmd, + tombstoneCmd, + ycsbCmd, + fsBenchCmd, + writeBenchCmd, + ) + + rootCmd := &cobra.Command{ + Use: "pebble [command] (flags)", + Short: "pebble benchmarking/introspection tool", + } + rootCmd.AddCommand(benchCmd) + + t := tool.New(tool.Comparers(mvccComparer, testkeys.Comparer), tool.Mergers(fauxMVCCMerger)) + rootCmd.AddCommand(t.Commands...) + + for _, cmd := range []*cobra.Command{replayCmd, scanCmd, syncCmd, tombstoneCmd, writeBenchCmd, ycsbCmd} { + cmd.Flags().BoolVarP( + &verbose, "verbose", "v", false, "enable verbose event logging") + cmd.Flags().StringVar( + &pathToLocalSharedStorage, "shared-storage", "", "path to local shared storage (empty for no shared storage)") + cmd.Flags().Int64Var( + &secondaryCacheSize, "secondary-cache", 0, "secondary cache size in bytes") + } + for _, cmd := range []*cobra.Command{scanCmd, syncCmd, tombstoneCmd, ycsbCmd} { + cmd.Flags().Int64Var( + &cacheSize, "cache", 1<<30, "cache size") + } + for _, cmd := range []*cobra.Command{scanCmd, syncCmd, tombstoneCmd, ycsbCmd, fsBenchCmd, writeBenchCmd} { + cmd.Flags().DurationVarP( + &duration, "duration", "d", 10*time.Second, "the duration to run (0, run forever)") + } + for _, cmd := range []*cobra.Command{scanCmd, syncCmd, tombstoneCmd, ycsbCmd} { + cmd.Flags().IntVarP( + &concurrency, "concurrency", "c", 1, "number of concurrent workers") + cmd.Flags().BoolVar( + &disableWAL, "disable-wal", false, "disable the WAL (voiding persistence guarantees)") + cmd.Flags().VarP( + maxOpsPerSec, "rate", "m", "max ops per second [{zipf,uniform}:]min[-max][/period (sec)]") + cmd.Flags().BoolVar( + &waitCompactions, "wait-compactions", false, + "wait for background compactions to complete after load stops") + cmd.Flags().BoolVarP( + &wipe, "wipe", "w", false, "wipe the database before starting") + cmd.Flags().Uint64Var( + &maxSize, "max-size", 0, "maximum disk size, in MB (0, run forever)") + } + + if err := rootCmd.Execute(); err != nil { + // Cobra has already printed the error message. + os.Exit(1) + } +} diff --git a/pebble/cmd/pebble/mvcc.go b/pebble/cmd/pebble/mvcc.go new file mode 100644 index 0000000..0e388de --- /dev/null +++ b/pebble/cmd/pebble/mvcc.go @@ -0,0 +1,223 @@ +// Copyright 2018 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 main + +import ( + "bytes" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/bytealloc" +) + +// MVCC encoding and decoding routines adapted from CockroachDB sources. Used +// to perform apples-to-apples benchmarking for CockroachDB's usage of RocksDB. + +var mvccComparer = &pebble.Comparer{ + Compare: mvccCompare, + + AbbreviatedKey: func(k []byte) uint64 { + key, _, ok := mvccSplitKey(k) + if !ok { + return 0 + } + return pebble.DefaultComparer.AbbreviatedKey(key) + }, + + Equal: func(a, b []byte) bool { + return mvccCompare(a, b) == 0 + }, + + Separator: func(dst, a, b []byte) []byte { + aKey, _, ok := mvccSplitKey(a) + if !ok { + return append(dst, a...) + } + bKey, _, ok := mvccSplitKey(b) + if !ok { + return append(dst, a...) + } + // If the keys are the same just return a. + if bytes.Equal(aKey, bKey) { + return append(dst, a...) + } + n := len(dst) + // MVCC key comparison uses bytes.Compare on the roachpb.Key, which is the same semantics as + // pebble.DefaultComparer, so reuse the latter's Separator implementation. + dst = pebble.DefaultComparer.Separator(dst, aKey, bKey) + // Did it pick a separator different than aKey -- if it did not we can't do better than a. + buf := dst[n:] + if bytes.Equal(aKey, buf) { + return append(dst[:n], a...) + } + // The separator is > aKey, so we only need to add the timestamp sentinel. + return append(dst, 0) + }, + + Successor: func(dst, a []byte) []byte { + aKey, _, ok := mvccSplitKey(a) + if !ok { + return append(dst, a...) + } + n := len(dst) + // MVCC key comparison uses bytes.Compare on the roachpb.Key, which is the same semantics as + // pebble.DefaultComparer, so reuse the latter's Successor implementation. + dst = pebble.DefaultComparer.Successor(dst, aKey) + // Did it pick a successor different than aKey -- if it did not we can't do better than a. + buf := dst[n:] + if bytes.Equal(aKey, buf) { + return append(dst[:n], a...) + } + // The successor is > aKey, so we only need to add the timestamp sentinel. + return append(dst, 0) + }, + + Split: func(k []byte) int { + key, _, ok := mvccSplitKey(k) + if !ok { + return len(k) + } + // This matches the behavior of libroach/KeyPrefix. RocksDB requires that + // keys generated via a SliceTransform be comparable with normal encoded + // MVCC keys. Encoded MVCC keys have a suffix indicating the number of + // bytes of timestamp data. MVCC keys without a timestamp have a suffix of + // 0. We're careful in EncodeKey to make sure that the user-key always has + // a trailing 0. If there is no timestamp this falls out naturally. If + // there is a timestamp we prepend a 0 to the encoded timestamp data. + return len(key) + 1 + }, + + Name: "cockroach_comparator", +} + +func mvccSplitKey(mvccKey []byte) (key []byte, ts []byte, ok bool) { + if len(mvccKey) == 0 { + return nil, nil, false + } + n := len(mvccKey) - 1 + tsLen := int(mvccKey[n]) + if n < tsLen { + return nil, nil, false + } + key = mvccKey[:n-tsLen] + if tsLen > 0 { + ts = mvccKey[n-tsLen+1 : len(mvccKey)-1] + } + return key, ts, true +} + +func mvccCompare(a, b []byte) int { + // NB: For performance, this routine manually splits the key into the + // user-key and timestamp components rather than using SplitMVCCKey. Don't + // try this at home kids: use SplitMVCCKey. + + aEnd := len(a) - 1 + bEnd := len(b) - 1 + if aEnd < 0 || bEnd < 0 { + // This should never happen unless there is some sort of corruption of + // the keys. This is a little bizarre, but the behavior exactly matches + // engine/db.cc:DBComparator. + return bytes.Compare(a, b) + } + + // Compute the index of the separator between the key and the timestamp. + aSep := aEnd - int(a[aEnd]) + bSep := bEnd - int(b[bEnd]) + if aSep < 0 || bSep < 0 { + // This should never happen unless there is some sort of corruption of + // the keys. This is a little bizarre, but the behavior exactly matches + // engine/db.cc:DBComparator. + return bytes.Compare(a, b) + } + + // Compare the "user key" part of the key. + if c := bytes.Compare(a[:aSep], b[:bSep]); c != 0 { + return c + } + + // Compare the timestamp part of the key. + aTS := a[aSep:aEnd] + bTS := b[bSep:bEnd] + if len(aTS) == 0 { + if len(bTS) == 0 { + return 0 + } + return -1 + } else if len(bTS) == 0 { + return 1 + } + return bytes.Compare(bTS, aTS) +} + +// \x00[[]]<#timestamp-bytes> +func mvccEncode(dst, key []byte, walltime uint64, logical uint32) []byte { + dst = append(dst, key...) + dst = append(dst, 0) + if walltime != 0 || logical != 0 { + extra := byte(1 + 8) + dst = encodeUint64Ascending(dst, walltime) + if logical != 0 { + dst = encodeUint32Ascending(dst, logical) + extra += 4 + } + dst = append(dst, extra) + } + return dst +} + +func mvccForwardScan(d DB, start, end, ts []byte) (int, int64) { + it := d.NewIter(&pebble.IterOptions{ + LowerBound: mvccEncode(nil, start, 0, 0), + UpperBound: mvccEncode(nil, end, 0, 0), + }) + defer it.Close() + + var data bytealloc.A + var count int + var nbytes int64 + + for valid := it.First(); valid; valid = it.Next() { + key, keyTS, _ := mvccSplitKey(it.Key()) + if bytes.Compare(keyTS, ts) <= 0 { + data, _ = data.Copy(key) + data, _ = data.Copy(it.Value()) + } + count++ + nbytes += int64(len(it.Key()) + len(it.Value())) + } + return count, nbytes +} + +func mvccReverseScan(d DB, start, end, ts []byte) (int, int64) { + it := d.NewIter(&pebble.IterOptions{ + LowerBound: mvccEncode(nil, start, 0, 0), + UpperBound: mvccEncode(nil, end, 0, 0), + }) + defer it.Close() + + var data bytealloc.A + var count int + var nbytes int64 + + for valid := it.Last(); valid; valid = it.Prev() { + key, keyTS, _ := mvccSplitKey(it.Key()) + if bytes.Compare(keyTS, ts) <= 0 { + data, _ = data.Copy(key) + data, _ = data.Copy(it.Value()) + } + count++ + nbytes += int64(len(it.Key()) + len(it.Value())) + } + return count, nbytes +} + +var fauxMVCCMerger = &pebble.Merger{ + Name: "cockroach_merge_operator", + Merge: func(key, value []byte) (pebble.ValueMerger, error) { + // This merger is used by the compact benchmark and use the + // pebble default value merger to concatenate values. + // It shouldn't materially affect the benchmarks. + return pebble.DefaultMerger.Merge(key, value) + }, +} diff --git a/pebble/cmd/pebble/queue.go b/pebble/cmd/pebble/queue.go new file mode 100644 index 0000000..7193741 --- /dev/null +++ b/pebble/cmd/pebble/queue.go @@ -0,0 +1,116 @@ +// Copyright 2020 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 main + +import ( + "fmt" + "log" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/randvar" + "github.com/spf13/cobra" + "golang.org/x/exp/rand" +) + +var queueConfig struct { + size int + values *randvar.BytesFlag +} + +func initQueue(cmd *cobra.Command) { + cmd.Flags().IntVar( + &queueConfig.size, "queue-size", 256, + "size of the queue to maintain") + queueConfig.values = randvar.NewBytesFlag("16384") + cmd.Flags().Var( + queueConfig.values, "queue-values", + "queue value size distribution [{zipf,uniform}:]min[-max][/]") +} + +func queueTest() (test, *atomic.Int64) { + ops := new(atomic.Int64) // atomic + var ( + lastOps int64 + lastElapsed time.Duration + ) + return test{ + init: func(d DB, wg *sync.WaitGroup) { + var ( + value []byte + rng = rand.New(rand.NewSource(1449168817)) + queue = make([][]byte, queueConfig.size) + ) + for i := 0; i < queueConfig.size; i++ { + b := d.NewBatch() + queue[i] = mvccEncode(nil, encodeUint32Ascending([]byte("queue-"), uint32(i)), uint64(i+1), 0) + value = queueConfig.values.Bytes(rng, value) + b.Set(queue[i], value, pebble.NoSync) + if err := b.Commit(pebble.NoSync); err != nil { + log.Fatal(err) + } + } + if err := d.Flush(); err != nil { + log.Fatal(err) + } + + limiter := maxOpsPerSec.newRateLimiter() + wg.Add(1) + go func() { + defer wg.Done() + + for i := queueConfig.size; ; i++ { + idx := i % queueConfig.size + + // Delete the head. + b := d.NewBatch() + if err := b.Delete(queue[idx], pebble.Sync); err != nil { + log.Fatal(err) + } + if err := b.Commit(pebble.Sync); err != nil { + log.Fatal(err) + } + _ = b.Close() + wait(limiter) + + // Append to the tail. + b = d.NewBatch() + queue[idx] = mvccEncode(queue[idx][:0], encodeUint32Ascending([]byte("queue-"), uint32(i)), uint64(i+1), 0) + value = queueConfig.values.Bytes(rng, value) + b.Set(queue[idx], value, nil) + if err := b.Commit(pebble.Sync); err != nil { + log.Fatal(err) + } + _ = b.Close() + wait(limiter) + ops.Add(1) + } + }() + }, + tick: func(elapsed time.Duration, i int) { + if i%20 == 0 { + fmt.Println("Queue___elapsed_______ops/sec") + } + + curOps := ops.Load() + dur := elapsed - lastElapsed + fmt.Printf("%15s %13.1f\n", + time.Duration(elapsed.Seconds()+0.5)*time.Second, + float64(curOps-lastOps)/dur.Seconds(), + ) + lastOps = curOps + lastElapsed = elapsed + }, + done: func(elapsed time.Duration) { + curOps := ops.Load() + fmt.Println("\nQueue___elapsed___ops/sec(cum)") + fmt.Printf("%13.1fs %14.1f\n\n", + elapsed.Seconds(), + float64(curOps)/elapsed.Seconds()) + }, + }, ops +} diff --git a/pebble/cmd/pebble/random.go b/pebble/cmd/pebble/random.go new file mode 100644 index 0000000..c098b74 --- /dev/null +++ b/pebble/cmd/pebble/random.go @@ -0,0 +1,92 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package main + +import ( + "strconv" + "strings" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/randvar" + "github.com/cockroachdb/pebble/internal/rate" +) + +type rateFlag struct { + randvar.Flag + fluctuateDuration time.Duration + spec string +} + +func newRateFlag(spec string) *rateFlag { + f := &rateFlag{} + if err := f.Set(spec); err != nil { + panic(err) + } + return f +} + +func (f *rateFlag) String() string { + return f.spec +} + +// Type implements the Flag.Value interface. +func (f *rateFlag) Type() string { + return "ratevar" +} + +// Set implements the Flag.Value interface. +func (f *rateFlag) Set(spec string) error { + if spec == "" { + if err := f.Flag.Set("0"); err != nil { + return err + } + f.fluctuateDuration = time.Duration(0) + f.spec = spec + return nil + } + + parts := strings.Split(spec, "/") + if len(parts) == 0 || len(parts) > 2 { + return errors.Errorf("invalid ratevar spec: %s", errors.Safe(spec)) + } + if err := f.Flag.Set(parts[0]); err != nil { + return err + } + // Don't fluctuate by default. + f.fluctuateDuration = time.Duration(0) + if len(parts) == 2 { + fluctuateDurationFloat, err := strconv.ParseFloat(parts[1], 64) + if err != nil { + return err + } + f.fluctuateDuration = time.Duration(fluctuateDurationFloat) * time.Second + } + f.spec = spec + return nil +} + +func (f *rateFlag) newRateLimiter() *rate.Limiter { + if f.spec == "" { + return nil + } + rng := randvar.NewRand() + limiter := rate.NewLimiter(float64(f.Uint64(rng)), 1) + if f.fluctuateDuration != 0 { + go func(limiter *rate.Limiter) { + ticker := time.NewTicker(f.fluctuateDuration) + for range ticker.C { + limiter.SetRate(float64(f.Uint64(rng))) + } + }(limiter) + } + return limiter +} + +func wait(l *rate.Limiter) { + if l != nil { + l.Wait(1) + } +} diff --git a/pebble/cmd/pebble/replay.go b/pebble/cmd/pebble/replay.go new file mode 100644 index 0000000..7479769 --- /dev/null +++ b/pebble/cmd/pebble/replay.go @@ -0,0 +1,448 @@ +// 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 main + +import ( + "bytes" + "context" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "syscall" + "time" + "unicode" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/replay" + "github.com/cockroachdb/pebble/vfs" + "github.com/spf13/cobra" +) + +func initReplayCmd() *cobra.Command { + c := replayConfig{ + pacer: pacerFlag{Pacer: replay.PaceByFixedReadAmp(10)}, + runDir: "", + count: 1, + streamLogs: false, + ignoreCheckpoint: false, + } + cmd := &cobra.Command{ + Use: "replay ", + Short: "run the provided captured write workload", + Args: cobra.ExactArgs(1), + RunE: c.runE, + } + cmd.Flags().IntVar( + &c.count, "count", 1, "the number of times to replay the workload") + cmd.Flags().StringVar( + &c.name, "name", "", "the name of the workload being replayed") + cmd.Flags().VarPF( + &c.pacer, "pacer", "p", "the pacer to use: unpaced, reference-ramp, or fixed-ramp=N") + cmd.Flags().Uint64Var( + &c.maxWritesMB, "max-writes", 0, "the maximum volume of writes (MB) to apply, with 0 denoting unlimited") + cmd.Flags().StringVar( + &c.optionsString, "options", "", "Pebble options to override, in the OPTIONS ini format but with any whitespace as field delimiters instead of newlines") + cmd.Flags().StringVar( + &c.runDir, "run-dir", c.runDir, "the directory to use for the replay data directory; defaults to a random dir in pwd") + cmd.Flags().Int64Var( + &c.maxCacheSize, "max-cache-size", c.maxCacheSize, "the max size of the block cache") + cmd.Flags().BoolVar( + &c.streamLogs, "stream-logs", c.streamLogs, "stream the Pebble logs to stdout during replay") + cmd.Flags().BoolVar( + &c.ignoreCheckpoint, "ignore-checkpoint", c.ignoreCheckpoint, "ignore the workload's initial checkpoint") + cmd.Flags().StringVar( + &c.checkpointDir, "checkpoint-dir", c.checkpointDir, "path to the checkpoint to use if not /checkpoint") + return cmd +} + +type replayConfig struct { + name string + pacer pacerFlag + runDir string + count int + maxWritesMB uint64 + streamLogs bool + checkpointDir string + ignoreCheckpoint bool + optionsString string + maxCacheSize int64 + + cleanUpFuncs []func() error +} + +func (c *replayConfig) args() (args []string) { + if c.name != "" { + args = append(args, "--name", c.name) + } + if c.pacer.spec != "" { + args = append(args, "--pacer", c.pacer.spec) + } + if c.runDir != "" { + args = append(args, "--run-dir", c.runDir) + } + if c.count != 0 { + args = append(args, "--count", fmt.Sprint(c.count)) + } + if c.maxWritesMB != 0 { + args = append(args, "--max-writes", fmt.Sprint(c.maxWritesMB)) + } + if c.maxCacheSize != 0 { + args = append(args, "--max-cache-size", fmt.Sprint(c.maxCacheSize)) + } + if c.streamLogs { + args = append(args, "--stream-logs") + } + if c.checkpointDir != "" { + args = append(args, "--checkpoint-dir", c.checkpointDir) + } + if c.ignoreCheckpoint { + args = append(args, "--ignore-checkpoint") + } + if c.optionsString != "" { + args = append(args, "--options", c.optionsString) + } + return args +} + +func (c *replayConfig) runE(cmd *cobra.Command, args []string) error { + if c.ignoreCheckpoint && c.checkpointDir != "" { + return errors.Newf("cannot provide both --checkpoint-dir and --ignore-checkpoint") + } + stdout := cmd.OutOrStdout() + + workloadPath := args[0] + if err := c.runOnce(stdout, workloadPath); err != nil { + return err + } + c.count-- + + // If necessary, run it again. We run again replacing our existing process + // with the next run so that we're truly starting over. This helps avoid the + // possibility of state within the Go runtime, the fragmentation of the + // heap, or global state within Pebble from interfering with the + // independence of individual runs. Previously we called runOnce multiple + // times without exec-ing, but we observed less variance between runs from + // within the same process. + if c.count > 0 { + fmt.Printf("%d runs remaining.", c.count) + executable, err := os.Executable() + if err != nil { + return err + } + execArgs := append(append([]string{executable, "bench", "replay"}, c.args()...), workloadPath) + syscall.Exec(executable, execArgs, os.Environ()) + } + return nil +} + +func (c *replayConfig) runOnce(stdout io.Writer, workloadPath string) error { + defer c.cleanUp() + if c.name == "" { + c.name = vfs.Default.PathBase(workloadPath) + } + + r := &replay.Runner{ + RunDir: c.runDir, + WorkloadFS: vfs.Default, + WorkloadPath: workloadPath, + Pacer: c.pacer, + Opts: &pebble.Options{}, + } + if c.maxWritesMB > 0 { + r.MaxWriteBytes = c.maxWritesMB * (1 << 20) + } + if err := c.initRunDir(r); err != nil { + return err + } + if err := c.initOptions(r); err != nil { + return err + } + if verbose { + fmt.Fprintln(stdout, "Options:") + fmt.Fprintln(stdout, r.Opts.String()) + } + + // Begin the workload. Run does not block. + ctx := context.Background() + if err := r.Run(ctx); err != nil { + return errors.Wrapf(err, "starting workload") + } + + // Wait blocks until the workload is complete. Once Wait returns, all of the + // workload's write operations have been replayed AND the database's + // compactions have quiesced. + m, err := r.Wait() + if err != nil { + return errors.Wrapf(err, "waiting for workload to complete") + } + if err := r.Close(); err != nil { + return errors.Wrapf(err, "cleaning up") + } + fmt.Fprintln(stdout, "Workload complete.") + if err := m.WriteBenchmarkString(c.name, stdout); err != nil { + return err + } + for _, plot := range m.Plots(120 /* width */, 30 /* height */) { + fmt.Fprintln(stdout, plot.Name) + fmt.Fprintln(stdout, plot.Plot) + fmt.Fprintln(stdout) + } + fmt.Fprintln(stdout, m.Final.String()) + return nil +} + +func (c *replayConfig) initRunDir(r *replay.Runner) error { + if r.RunDir == "" { + // Default to replaying in a new directory within the current working + // directory. + wd, err := os.Getwd() + if err != nil { + return err + } + r.RunDir, err = os.MkdirTemp(wd, "replay-") + if err != nil { + return err + } + c.cleanUpFuncs = append(c.cleanUpFuncs, func() error { + return os.RemoveAll(r.RunDir) + }) + } + if !c.ignoreCheckpoint { + checkpointDir := c.getCheckpointDir(r) + fmt.Printf("%s: Attempting to initialize with checkpoint %q.\n", time.Now().Format(time.RFC3339), checkpointDir) + ok, err := vfs.Clone( + r.WorkloadFS, + vfs.Default, + checkpointDir, + filepath.Join(r.RunDir), + vfs.CloneTryLink) + if err != nil { + return err + } + if !ok { + return errors.Newf("no checkpoint %q exists; you may re-run with --ignore-checkpoint", checkpointDir) + } + fmt.Printf("%s: Run directory initialized with checkpoint %q.\n", time.Now().Format(time.RFC3339), checkpointDir) + } + return nil +} + +func (c *replayConfig) initOptions(r *replay.Runner) error { + // If using a workload checkpoint, load the Options from it. + // TODO(jackson): Allow overriding the OPTIONS. + if !c.ignoreCheckpoint { + ls, err := r.WorkloadFS.List(c.getCheckpointDir(r)) + if err != nil { + return err + } + sort.Strings(ls) + var optionsFilepath string + for _, l := range ls { + path := r.WorkloadFS.PathJoin(r.WorkloadPath, "checkpoint", l) + typ, _, ok := base.ParseFilename(r.WorkloadFS, path) + if ok && typ == base.FileTypeOptions { + optionsFilepath = path + } + } + f, err := r.WorkloadFS.Open(optionsFilepath) + if err != nil { + return err + } + o, err := io.ReadAll(f) + if err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + if err := r.Opts.Parse(string(o), c.parseHooks()); err != nil { + return err + } + } + if err := c.parseCustomOptions(c.optionsString, r.Opts); err != nil { + return err + } + // TODO(jackson): If r.Opts.Comparer == nil, peek at the workload's + // manifests and pull the comparer out of them. + // + // r.Opts.Comparer can only be nil at this point if ignoreCheckpoint is + // set; otherwise we'll have already extracted the Comparer from the + // checkpoint's OPTIONS file. + + if c.streamLogs { + r.Opts.AddEventListener(pebble.MakeLoggingEventListener(pebble.DefaultLogger)) + } + r.Opts.EnsureDefaults() + return nil +} + +func (c *replayConfig) getCheckpointDir(r *replay.Runner) string { + if c.checkpointDir != "" { + return c.checkpointDir + } + return r.WorkloadFS.PathJoin(r.WorkloadPath, `checkpoint`) +} + +func (c *replayConfig) parseHooks() *pebble.ParseHooks { + return &pebble.ParseHooks{ + NewCache: func(size int64) *cache.Cache { + if c.maxCacheSize != 0 && size > c.maxCacheSize { + size = c.maxCacheSize + } + return cache.New(size) + }, + NewComparer: makeComparer, + NewFilterPolicy: func(name string) (pebble.FilterPolicy, error) { + switch name { + case "none": + return nil, nil + case "rocksdb.BuiltinBloomFilter": + return bloom.FilterPolicy(10), nil + default: + return nil, errors.Errorf("invalid filter policy name %q", name) + } + }, + NewMerger: makeMerger, + } +} + +// parseCustomOptions parses Pebble Options passed through a CLI flag. +// Ordinarily Pebble Options are specified through an INI file with newlines +// delimiting fields. That doesn't translate well to a CLI interface, so this +// function accepts fields are that delimited by any whitespace. This is the +// same format that CockroachDB accepts Pebble Options through the --store flag, +// and this code is copied from there. +func (c *replayConfig) parseCustomOptions(optsStr string, opts *pebble.Options) error { + if optsStr == "" { + return nil + } + // Pebble options are supplied in the Pebble OPTIONS ini-like + // format, but allowing any whitespace to delimit lines. Convert + // the options to a newline-delimited format. This isn't a trivial + // character replacement because whitespace may appear within a + // stanza, eg ["Level 0"]. + value := strings.TrimSpace(optsStr) + var buf bytes.Buffer + for len(value) > 0 { + i := strings.IndexFunc(value, func(r rune) bool { + return r == '[' || unicode.IsSpace(r) + }) + switch { + case i == -1: + buf.WriteString(value) + value = value[len(value):] + case value[i] == '[': + // If there's whitespace within [ ], we write it verbatim. + j := i + strings.IndexRune(value[i:], ']') + buf.WriteString(value[:j+1]) + value = value[j+1:] + case unicode.IsSpace(rune(value[i])): + // NB: This doesn't handle multibyte whitespace. + buf.WriteString(value[:i]) + buf.WriteRune('\n') + value = strings.TrimSpace(value[i+1:]) + } + } + return opts.Parse(buf.String(), c.parseHooks()) +} + +func (c *replayConfig) cleanUp() error { + for _, f := range c.cleanUpFuncs { + if err := f(); err != nil { + return err + } + } + return nil +} + +func makeComparer(name string) (*pebble.Comparer, error) { + switch name { + case base.DefaultComparer.Name: + return base.DefaultComparer, nil + case "cockroach_comparator": + return mvccComparer, nil + default: + return nil, errors.Newf("unrecognized comparer %q", name) + } +} + +func makeMerger(name string) (*pebble.Merger, error) { + switch name { + case base.DefaultMerger.Name: + return base.DefaultMerger, nil + case "cockroach_merge_operator": + // We don't want to reimplement the cockroach merger. Instead we + // implement this merger to return the newer of the two operands. This + // doesn't exactly model cockroach's true use but should be good enough. + // TODO(jackson): Consider lifting replay into a `cockroach debug` + // command so we can use the true merger and comparer. + merger := new(pebble.Merger) + merger.Merge = func(key, value []byte) (pebble.ValueMerger, error) { + return &overwriteValueMerger{value: append([]byte{}, value...)}, nil + } + merger.Name = name + return merger, nil + default: + return nil, errors.Newf("unrecognized comparer %q", name) + } +} + +// pacerFlag provides a command line flag interface for specifying the pacer to +// use. It implements the flag.Value interface. +type pacerFlag struct { + replay.Pacer + spec string +} + +var _ flag.Value = (*pacerFlag)(nil) + +func (f *pacerFlag) String() string { return f.spec } +func (f *pacerFlag) Type() string { return "pacer" } + +// Set implements the Flag.Value interface. +func (f *pacerFlag) Set(spec string) error { + f.spec = spec + switch { + case spec == "unpaced": + f.Pacer = replay.Unpaced{} + case spec == "reference-ramp": + f.Pacer = replay.PaceByReferenceReadAmp{} + case strings.HasPrefix(spec, "fixed-ramp="): + rAmp, err := strconv.Atoi(strings.TrimPrefix(spec, "fixed-ramp=")) + if err != nil { + return errors.Newf("unable to parse fixed r-amp: %s", err) + } + f.Pacer = replay.PaceByFixedReadAmp(rAmp) + default: + return errors.Newf("unrecognized pacer spec: %q", errors.Safe(spec)) + } + return nil +} + +type overwriteValueMerger struct { + value []byte +} + +func (o *overwriteValueMerger) MergeNewer(value []byte) error { + o.value = append(o.value[:0], value...) + return nil +} + +func (o *overwriteValueMerger) MergeOlder(value []byte) error { + return nil +} + +func (o *overwriteValueMerger) Finish(includesBase bool) ([]byte, io.Closer, error) { + return o.value, nil, nil +} diff --git a/pebble/cmd/pebble/replay_test.go b/pebble/cmd/pebble/replay_test.go new file mode 100644 index 0000000..b3f6225 --- /dev/null +++ b/pebble/cmd/pebble/replay_test.go @@ -0,0 +1,77 @@ +// 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 main + +import ( + "fmt" + "testing" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/stretchr/testify/require" +) + +func TestParseOptionsStr(t *testing.T) { + type testCase struct { + c replayConfig + options *pebble.Options + } + + testCases := []testCase{ + { + c: replayConfig{optionsString: `[Options] max_concurrent_compactions=9`}, + options: &pebble.Options{MaxConcurrentCompactions: func() int { return 9 }}, + }, + { + c: replayConfig{optionsString: `[Options] bytes_per_sync=90000`}, + options: &pebble.Options{BytesPerSync: 90000}, + }, + { + c: replayConfig{optionsString: fmt.Sprintf(`[Options] cache_size=%d`, 16<<20 /* 16MB */)}, + options: &pebble.Options{Cache: cache.New(16 << 20 /* 16 MB */)}, + }, + { + c: replayConfig{ + maxCacheSize: 16 << 20, /* 16 MB */ + optionsString: fmt.Sprintf(`[Options] cache_size=%d`, int64(10<<30 /* 10 GB */)), + }, + options: &pebble.Options{Cache: cache.New(16 << 20 /* 16 MB */)}, + }, + { + c: replayConfig{optionsString: `[Options] [Level "0"] target_file_size=222`}, + options: &pebble.Options{Levels: []pebble.LevelOptions{ + {TargetFileSize: 222}, + }}, + }, + { + c: replayConfig{optionsString: `[Options] lbase_max_bytes=10 max_open_files=20 [Level "0"] target_file_size=30 [Level "1"] index_block_size=40`}, + options: &pebble.Options{ + LBaseMaxBytes: 10, + MaxOpenFiles: 20, + Levels: []pebble.LevelOptions{ + {TargetFileSize: 30}, + {IndexBlockSize: 40}, + }, + }, + }, + } + + for _, tc := range testCases { + o := new(pebble.Options) + require.NoError(t, tc.c.parseCustomOptions(tc.c.optionsString, o)) + o.EnsureDefaults() + got := o.String() + + tc.options.EnsureDefaults() + want := tc.options.String() + require.Equal(t, want, got) + if o.Cache != nil { + o.Cache.Unref() + } + if tc.options.Cache != nil { + tc.options.Cache.Unref() + } + } +} diff --git a/pebble/cmd/pebble/scan.go b/pebble/cmd/pebble/scan.go new file mode 100644 index 0000000..0803501 --- /dev/null +++ b/pebble/cmd/pebble/scan.go @@ -0,0 +1,160 @@ +// Copyright 2018 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 main + +import ( + "fmt" + "log" + "math" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/randvar" + "github.com/spf13/cobra" + "golang.org/x/exp/rand" +) + +var scanConfig struct { + reverse bool + rows *randvar.Flag + values *randvar.BytesFlag +} + +var scanCmd = &cobra.Command{ + Use: "scan ", + Short: "run the scan benchmark", + Long: ``, + Args: cobra.ExactArgs(1), + Run: runScan, +} + +func init() { + scanCmd.Flags().BoolVarP( + &scanConfig.reverse, "reverse", "r", false, "reverse scan") + scanConfig.rows = randvar.NewFlag("100") + scanCmd.Flags().Var( + scanConfig.rows, "rows", "number of rows to scan in each operation") + scanConfig.values = randvar.NewBytesFlag("8") + scanCmd.Flags().Var( + scanConfig.values, "values", + "value size distribution [{zipf,uniform}:]min[-max][/]") +} + +func runScan(cmd *cobra.Command, args []string) { + var ( + bytes atomic.Int64 + scanned atomic.Int64 + lastBytes int64 + lastScanned int64 + lastElapsed time.Duration + ) + + opts := pebble.Sync + if disableWAL { + opts = pebble.NoSync + } + + rowDist := scanConfig.rows + + runTest(args[0], test{ + init: func(d DB, wg *sync.WaitGroup) { + const count = 100000 + const batch = 1000 + + rng := rand.New(rand.NewSource(1449168817)) + keys := make([][]byte, count) + + for i := 0; i < count; { + b := d.NewBatch() + var value []byte + for end := i + batch; i < end; i++ { + keys[i] = mvccEncode(nil, encodeUint32Ascending([]byte("key-"), uint32(i)), uint64(i+1), 0) + value = scanConfig.values.Bytes(rng, value) + if err := b.Set(keys[i], value, nil); err != nil { + log.Fatal(err) + } + } + if err := b.Commit(opts); err != nil { + log.Fatal(err) + } + } + + if err := d.Flush(); err != nil { + log.Fatal(err) + } + + limiter := maxOpsPerSec.newRateLimiter() + + wg.Add(concurrency) + for i := 0; i < concurrency; i++ { + go func(i int) { + defer wg.Done() + + rng := rand.New(rand.NewSource(uint64(i))) + startKeyBuf := append(make([]byte, 0, 64), []byte("key-")...) + endKeyBuf := append(make([]byte, 0, 64), []byte("key-")...) + minTS := encodeUint64Ascending(nil, math.MaxUint64) + + for { + wait(limiter) + + rows := int(rowDist.Uint64(rng)) + startIdx := rng.Int31n(int32(len(keys) - rows)) + startKey := encodeUint32Ascending(startKeyBuf[:4], uint32(startIdx)) + endKey := encodeUint32Ascending(endKeyBuf[:4], uint32(startIdx+int32(rows))) + + var count int + var nbytes int64 + if scanConfig.reverse { + count, nbytes = mvccReverseScan(d, startKey, endKey, minTS) + } else { + count, nbytes = mvccForwardScan(d, startKey, endKey, minTS) + } + + if count != rows { + log.Fatalf("scanned %d, expected %d\n", count, rows) + } + + bytes.Add(nbytes) + scanned.Add(int64(count)) + } + }(i) + } + }, + + tick: func(elapsed time.Duration, i int) { + if i%20 == 0 { + fmt.Println("_elapsed_______rows/sec_______MB/sec_______ns/row") + } + + curBytes := bytes.Load() + curScanned := scanned.Load() + dur := elapsed - lastElapsed + fmt.Printf("%8s %14.1f %12.1f %12.1f\n", + time.Duration(elapsed.Seconds()+0.5)*time.Second, + float64(curScanned-lastScanned)/dur.Seconds(), + float64(curBytes-lastBytes)/(dur.Seconds()*(1<<20)), + float64(dur)/float64(curScanned-lastScanned), + ) + lastBytes = curBytes + lastScanned = curScanned + lastElapsed = elapsed + }, + + done: func(elapsed time.Duration) { + curBytes := bytes.Load() + curScanned := scanned.Load() + fmt.Println("\n_elapsed___ops/sec(cum)__MB/sec(cum)__ns/row(avg)") + fmt.Printf("%7.1fs %14.1f %12.1f %12.1f\n\n", + elapsed.Seconds(), + float64(curScanned)/elapsed.Seconds(), + float64(curBytes)/(elapsed.Seconds()*(1<<20)), + float64(elapsed)/float64(curScanned), + ) + }, + }) +} diff --git a/pebble/cmd/pebble/sync.go b/pebble/cmd/pebble/sync.go new file mode 100644 index 0000000..e2add26 --- /dev/null +++ b/pebble/cmd/pebble/sync.go @@ -0,0 +1,143 @@ +// Copyright 2018 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 main + +import ( + "fmt" + "log" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/randvar" + "github.com/spf13/cobra" + "golang.org/x/exp/rand" +) + +var syncConfig struct { + batch *randvar.Flag + walOnly bool + values *randvar.BytesFlag +} + +var syncCmd = &cobra.Command{ + Use: "sync ", + Short: "run the sync benchmark", + Long: ``, + Args: cobra.ExactArgs(1), + Run: runSync, +} + +func init() { + syncConfig.batch = randvar.NewFlag("5") + syncCmd.Flags().Var( + syncConfig.batch, "batch", + "batch size distribution [{zipf,uniform}:]min[-max]") + syncCmd.Flags().BoolVar( + &syncConfig.walOnly, "wal-only", false, "write data only to the WAL") + syncConfig.values = randvar.NewBytesFlag("uniform:60-80/1.0") + syncCmd.Flags().Var( + syncConfig.values, "values", + "value size distribution [{zipf,uniform}:]min[-max][/]") +} + +func runSync(cmd *cobra.Command, args []string) { + reg := newHistogramRegistry() + var bytes atomic.Uint64 + var lastBytes uint64 + + opts := pebble.Sync + if disableWAL { + opts = pebble.NoSync + } + + batchDist := syncConfig.batch + + runTest(args[0], test{ + init: func(d DB, wg *sync.WaitGroup) { + limiter := maxOpsPerSec.newRateLimiter() + + wg.Add(concurrency) + for i := 0; i < concurrency; i++ { + latency := reg.Register("ops") + go func() { + defer wg.Done() + + rand := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + var raw []byte + var buf []byte + var block []byte + for { + wait(limiter) + + start := time.Now() + b := d.NewBatch() + var n uint64 + count := int(batchDist.Uint64(rand)) + for j := 0; j < count; j++ { + block = syncConfig.values.Bytes(rand, block) + + if syncConfig.walOnly { + if err := b.LogData(block, nil); err != nil { + log.Fatal(err) + } + } else { + raw = encodeUint32Ascending(raw[:0], rand.Uint32()) + key := mvccEncode(buf[:0], raw, 0, 0) + buf = key[:0] + if err := b.Set(key, block, nil); err != nil { + log.Fatal(err) + } + } + n += uint64(len(block)) + } + if err := b.Commit(opts); err != nil { + log.Fatal(err) + } + latency.Record(time.Since(start)) + bytes.Add(n) + } + }() + } + }, + + tick: func(elapsed time.Duration, i int) { + if i%20 == 0 { + fmt.Println("_elapsed____ops/sec___mb/sec__p50(ms)__p95(ms)__p99(ms)_pMax(ms)") + } + reg.Tick(func(tick histogramTick) { + h := tick.Hist + n := bytes.Load() + fmt.Printf("%8s %10.1f %8.1f %8.1f %8.1f %8.1f %8.1f\n", + time.Duration(elapsed.Seconds()+0.5)*time.Second, + float64(h.TotalCount())/tick.Elapsed.Seconds(), + float64(n-lastBytes)/(1024.0*1024.0)/tick.Elapsed.Seconds(), + time.Duration(h.ValueAtQuantile(50)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(95)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(99)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(100)).Seconds()*1000, + ) + lastBytes = n + }) + }, + + done: func(elapsed time.Duration) { + fmt.Println("\n_elapsed___ops(total)_ops/sec(cum)_mb/sec(cum)__avg(ms)__p50(ms)__p95(ms)__p99(ms)_pMax(ms)") + reg.Tick(func(tick histogramTick) { + h := tick.Cumulative + fmt.Printf("%7.1fs %12d %12.1f %11.1f %8.1f %8.1f %8.1f %8.1f %8.1f\n\n", + elapsed.Seconds(), h.TotalCount(), + float64(h.TotalCount())/elapsed.Seconds(), + float64(bytes.Load()/(1024.0*1024.0))/elapsed.Seconds(), + time.Duration(h.Mean()).Seconds()*1000, + time.Duration(h.ValueAtQuantile(50)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(95)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(99)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(100)).Seconds()*1000) + }) + }, + }) +} diff --git a/pebble/cmd/pebble/test.go b/pebble/cmd/pebble/test.go new file mode 100644 index 0000000..c8d707b --- /dev/null +++ b/pebble/cmd/pebble/test.go @@ -0,0 +1,400 @@ +// Copyright 2018 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 main + +import ( + "fmt" + "io" + "log" + "os" + "os/signal" + "runtime" + "runtime/pprof" + "sort" + "sync" + "syscall" + "time" + + "github.com/HdrHistogram/hdrhistogram-go" + "github.com/cockroachdb/pebble" +) + +const ( + minLatency = 10 * time.Microsecond + maxLatency = 10 * time.Second +) + +func startCPUProfile() func() { + runtime.SetMutexProfileFraction(1000) + + done := startRecording("cpu.%04d.prof", pprof.StartCPUProfile, pprof.StopCPUProfile) + return func() { + done() + if p := pprof.Lookup("heap"); p != nil { + f, err := os.Create("heap.prof") + if err != nil { + log.Fatal(err) + } + if err := p.WriteTo(f, 0); err != nil { + log.Fatal(err) + } + f.Close() + } + if p := pprof.Lookup("mutex"); p != nil { + f, err := os.Create("mutex.prof") + if err != nil { + log.Fatal(err) + } + if err := p.WriteTo(f, 0); err != nil { + log.Fatal(err) + } + f.Close() + } + } +} + +func startRecording(fmtStr string, startFunc func(io.Writer) error, stopFunc func()) func() { + doneCh := make(chan struct{}) + var doneWG sync.WaitGroup + doneWG.Add(1) + + go func() { + defer doneWG.Done() + + start := time.Now() + t := time.NewTicker(10 * time.Second) + defer t.Stop() + + var current *os.File + defer func() { + if current != nil { + stopFunc() + current.Close() + } + }() + + for { + if current != nil { + stopFunc() + current.Close() + current = nil + } + path := fmt.Sprintf(fmtStr, int(time.Since(start).Seconds()+0.5)) + f, err := os.Create(path) + if err != nil { + log.Fatalf("unable to create cpu profile: %s", err) + return + } + if err := startFunc(f); err != nil { + log.Fatalf("unable to start cpu profile: %v", err) + f.Close() + return + } + current = f + + select { + case <-doneCh: + return + case <-t.C: + } + } + }() + + return func() { + close(doneCh) + doneWG.Wait() + } +} + +func newHistogram() *hdrhistogram.Histogram { + return hdrhistogram.New(minLatency.Nanoseconds(), maxLatency.Nanoseconds(), 1) +} + +type namedHistogram struct { + name string + mu struct { + sync.Mutex + current *hdrhistogram.Histogram + } +} + +func newNamedHistogram(name string) *namedHistogram { + w := &namedHistogram{name: name} + w.mu.current = newHistogram() + return w +} + +func (w *namedHistogram) Record(elapsed time.Duration) { + if elapsed < minLatency { + elapsed = minLatency + } else if elapsed > maxLatency { + elapsed = maxLatency + } + + w.mu.Lock() + err := w.mu.current.RecordValue(elapsed.Nanoseconds()) + w.mu.Unlock() + + if err != nil { + // Note that a histogram only drops recorded values that are out of range, + // but we clamp the latency value to the configured range to prevent such + // drops. This code path should never happen. + panic(fmt.Sprintf(`%s: recording value: %s`, w.name, err)) + } +} + +func (w *namedHistogram) tick(fn func(h *hdrhistogram.Histogram)) { + w.mu.Lock() + defer w.mu.Unlock() + h := w.mu.current + w.mu.current = newHistogram() + fn(h) +} + +type histogramTick struct { + // Name is the name given to the histograms represented by this tick. + Name string + // Hist is the merged result of the represented histograms for this tick. + // Hist.TotalCount() is the number of operations that occurred for this tick. + Hist *hdrhistogram.Histogram + // Cumulative is the merged result of the represented histograms for all + // time. Cumulative.TotalCount() is the total number of operations that have + // occurred over all time. + Cumulative *hdrhistogram.Histogram + // Elapsed is the amount of time since the last tick. + Elapsed time.Duration + // Now is the time at which the tick was gathered. It covers the period + // [Now-Elapsed,Now). + Now time.Time +} + +type histogramRegistry struct { + mu struct { + sync.Mutex + registered []*namedHistogram + } + + start time.Time + cumulative map[string]*hdrhistogram.Histogram + prevTick map[string]time.Time +} + +func newHistogramRegistry() *histogramRegistry { + return &histogramRegistry{ + start: time.Now(), + cumulative: make(map[string]*hdrhistogram.Histogram), + prevTick: make(map[string]time.Time), + } +} + +func (w *histogramRegistry) Register(name string) *namedHistogram { + hist := newNamedHistogram(name) + + w.mu.Lock() + w.mu.registered = append(w.mu.registered, hist) + w.mu.Unlock() + + return hist +} + +func (w *histogramRegistry) Tick(fn func(histogramTick)) { + w.mu.Lock() + registered := append([]*namedHistogram(nil), w.mu.registered...) + w.mu.Unlock() + + merged := make(map[string]*hdrhistogram.Histogram) + var names []string + for _, hist := range registered { + hist.tick(func(h *hdrhistogram.Histogram) { + if p, ok := merged[hist.name]; ok { + p.Merge(h) + } else { + merged[hist.name] = h + names = append(names, hist.name) + } + }) + } + + now := time.Now() + sort.Strings(names) + for _, name := range names { + mergedHist := merged[name] + if _, ok := w.cumulative[name]; !ok { + w.cumulative[name] = newHistogram() + } + w.cumulative[name].Merge(mergedHist) + + prevTick, ok := w.prevTick[name] + if !ok { + prevTick = w.start + } + w.prevTick[name] = now + fn(histogramTick{ + Name: name, + Hist: merged[name], + Cumulative: w.cumulative[name], + Elapsed: now.Sub(prevTick), + Now: now, + }) + } +} + +type testWithoutDB struct { + init func(wg *sync.WaitGroup) + tick func(elapsed time.Duration, i int) + done func(wg *sync.WaitGroup, elapsed time.Duration) +} + +func runTestWithoutDB(t testWithoutDB) { + var wg sync.WaitGroup + t.init(&wg) + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + done := make(chan os.Signal, 3) + workersDone := make(chan struct{}) + signal.Notify(done, os.Interrupt) + + go func() { + wg.Wait() + close(workersDone) + }() + + if duration > 0 { + go func() { + time.Sleep(duration) + done <- syscall.Signal(0) + }() + } + + stopProf := startCPUProfile() + defer stopProf() + + start := time.Now() + for i := 0; ; i++ { + select { + case <-ticker.C: + if workersDone != nil { + t.tick(time.Since(start), i) + } + + case <-workersDone: + workersDone = nil + t.done(&wg, time.Since(start)) + return + + case sig := <-done: + fmt.Println("operating system is killing the op.", sig) + if workersDone != nil { + t.done(&wg, time.Since(start)) + } + return + } + } +} + +type test struct { + init func(db DB, wg *sync.WaitGroup) + tick func(elapsed time.Duration, i int) + done func(elapsed time.Duration) +} + +func runTest(dir string, t test) { + // Check if the directory exists. + if wipe { + fmt.Printf("wiping %s\n", dir) + if err := os.RemoveAll(dir); err != nil { + log.Fatal(err) + } + } + + fmt.Printf("dir %s\nconcurrency %d\n", dir, concurrency) + + db := newPebbleDB(dir) + var wg sync.WaitGroup + t.init(db, &wg) + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + done := make(chan os.Signal, 3) + workersDone := make(chan struct{}) + signal.Notify(done, os.Interrupt) + + go func() { + wg.Wait() + close(workersDone) + }() + + if maxSize > 0 { + go func() { + for { + time.Sleep(10 * time.Second) + if db.Metrics().DiskSpaceUsage() > maxSize*1e6 { + fmt.Println("max size reached") + done <- syscall.Signal(0) + } + } + }() + } + if duration > 0 { + go func() { + time.Sleep(duration) + done <- syscall.Signal(0) + }() + } + + stopProf := startCPUProfile() + defer stopProf() + + backgroundCompactions := func(p *pebble.Metrics) bool { + // The last level never gets selected as an input level for compaction, + // only as an output level, so ignore it for the purposes of determining if + // background compactions are still needed. + for i := range p.Levels[:len(p.Levels)-1] { + if p.Levels[i].Score > 1 { + return true + } + } + return false + } + + start := time.Now() + for i := 0; ; i++ { + select { + case <-ticker.C: + if workersDone != nil { + t.tick(time.Since(start), i) + if verbose && (i%10) == 9 { + fmt.Printf("%s", db.Metrics()) + } + } else if waitCompactions { + p := db.Metrics() + fmt.Printf("%s", p) + if !backgroundCompactions(p) { + return + } + } + + case <-workersDone: + workersDone = nil + t.done(time.Since(start)) + p := db.Metrics() + fmt.Printf("%s", p) + if !waitCompactions || !backgroundCompactions(p) { + return + } + fmt.Printf("waiting for background compactions\n") + + case <-done: + if workersDone != nil { + t.done(time.Since(start)) + } + fmt.Printf("%s", db.Metrics()) + return + } + } +} diff --git a/pebble/cmd/pebble/tombstone.go b/pebble/cmd/pebble/tombstone.go new file mode 100644 index 0000000..bbe0e3b --- /dev/null +++ b/pebble/cmd/pebble/tombstone.go @@ -0,0 +1,134 @@ +// Copyright 2020 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 main + +import ( + "fmt" + "log" + "sync" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/spf13/cobra" +) + +func init() { + // NB: the tombstone workload piggybacks off the existing flags and + // configs for the queue and ycsb workloads. + initQueue(tombstoneCmd) + initYCSB(tombstoneCmd) +} + +var tombstoneCmd = &cobra.Command{ + Use: "tombstone ", + Short: "run the mixed-workload point tombstone benchmark", + Long: ` +Run a customizable YCSB workload, alongside a single-writer, fixed-sized queue +workload. This command is intended for evaluating compaction heuristics +surrounding point tombstones. + +The queue workload writes a point tombstone with every operation. A compaction +strategy that does not account for point tombstones may accumulate many +uncompacted tombstones, causing steady growth of the disk space consumed by +the queue keyspace. + +The --queue-values flag controls the distribution of the queue value sizes. +Larger values are more likely to exhibit problematic point tombstone behavior +on a database using a min-overlapping ratio heuristic because the compact +point tombstones may overlap many tables in the next level. + +The --queue-size flag controls the fixed number of live keys in the queue. Low +queue sizes may not exercise problematic tombstone behavior if queue sets and +deletes get written to the same sstable. The large-valued sets can serve as a +counterweight to the point tombstones, narrowing the keyrange of the sstable +inflating its size relative to its overlap with the next level. + `, + Args: cobra.ExactArgs(1), + RunE: runTombstoneCmd, +} + +func runTombstoneCmd(cmd *cobra.Command, args []string) error { + if wipe && ycsbConfig.prepopulatedKeys > 0 { + return errors.New("--wipe and --prepopulated-keys both specified which is nonsensical") + } + + weights, err := ycsbParseWorkload(ycsbConfig.workload) + if err != nil { + return err + } + + keyDist, err := ycsbParseKeyDist(ycsbConfig.keys) + if err != nil { + return err + } + + batchDist := ycsbConfig.batch + scanDist := ycsbConfig.scans + if err != nil { + return err + } + + valueDist := ycsbConfig.values + y := newYcsb(weights, keyDist, batchDist, scanDist, valueDist) + q, queueOps := queueTest() + + queueStart := []byte("queue-") + queueEnd := append(append([]byte{}, queueStart...), 0xFF) + + var lastElapsed time.Duration + var lastQueueOps int64 + + var pdb pebbleDB + runTest(args[0], test{ + init: func(d DB, wg *sync.WaitGroup) { + pdb = d.(pebbleDB) + y.init(d, wg) + q.init(d, wg) + }, + tick: func(elapsed time.Duration, i int) { + if i%20 == 0 { + fmt.Println(" queue ycsb") + fmt.Println("________elapsed______queue_size__ops/sec(inst)___ops/sec(cum)__ops/sec(inst)___ops/sec(cum)") + } + + curQueueOps := queueOps.Load() + dur := elapsed - lastElapsed + queueOpsPerSec := float64(curQueueOps-lastQueueOps) / dur.Seconds() + queueCumOpsPerSec := float64(curQueueOps) / elapsed.Seconds() + + lastQueueOps = curQueueOps + lastElapsed = elapsed + + var ycsbOpsPerSec, ycsbCumOpsPerSec float64 + y.reg.Tick(func(tick histogramTick) { + h := tick.Hist + ycsbOpsPerSec = float64(h.TotalCount()) / tick.Elapsed.Seconds() + ycsbCumOpsPerSec = float64(tick.Cumulative.TotalCount()) / elapsed.Seconds() + }) + + queueSize, err := pdb.d.EstimateDiskUsage(queueStart, queueEnd) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%15s %15s %14.1f %14.1f %14.1f %14.1f\n", + time.Duration(elapsed.Seconds()+0.5)*time.Second, + humanize.Bytes.Uint64(queueSize), + queueOpsPerSec, + queueCumOpsPerSec, + ycsbOpsPerSec, + ycsbCumOpsPerSec) + }, + done: func(elapsed time.Duration) { + fmt.Println("________elapsed______queue_size") + queueSize, err := pdb.d.EstimateDiskUsage(queueStart, queueEnd) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%15s %15s\n", elapsed.Truncate(time.Second), humanize.Bytes.Uint64(queueSize)) + }, + }) + return nil +} diff --git a/pebble/cmd/pebble/util.go b/pebble/cmd/pebble/util.go new file mode 100644 index 0000000..2da4685 --- /dev/null +++ b/pebble/cmd/pebble/util.go @@ -0,0 +1,15 @@ +// Copyright 2018 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 main + +func encodeUint32Ascending(b []byte, v uint32) []byte { + return append(b, byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) +} + +func encodeUint64Ascending(b []byte, v uint64) []byte { + return append(b, + byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), + byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) +} diff --git a/pebble/cmd/pebble/write_bench.go b/pebble/cmd/pebble/write_bench.go new file mode 100644 index 0000000..397a536 --- /dev/null +++ b/pebble/cmd/pebble/write_bench.go @@ -0,0 +1,483 @@ +// Copyright 2021 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 main + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/cockroachdb/pebble/internal/ackseq" + "github.com/cockroachdb/pebble/internal/randvar" + "github.com/cockroachdb/pebble/internal/rate" + "github.com/spf13/cobra" +) + +// The following constants match the values that Cockroach uses in Admission +// Control at the time of writing. +// See: https://github.com/cockroachdb/cockroach/blob/cb5d5108a7705eac7be82bc7f0f8b6f4dc825b96/pkg/util/admission/granter.go#L1212-L1229 +const ( + defaultL0FileLimit = 1000 + defaultL0SubLevelLimit = 20 +) + +var writeBenchConfig struct { + batch *randvar.Flag + keys string + values *randvar.BytesFlag + concurrency int + rateStart int + incBase int + testPeriod time.Duration + cooloffPeriod time.Duration + targetL0Files int + targetL0SubLevels int + maxRateDipFraction float64 + debug bool +} + +var writeBenchCmd = &cobra.Command{ + Use: "write ", + Short: "Run YCSB F to find an a sustainable write throughput", + Long: ` +Run YCSB F (100% writes) at varying levels of sustained write load (ops/sec) to +determine an optimal value of write throughput. + +The benchmark works by maintaining a fixed amount of write load on the DB for a +fixed amount of time. If the database can handle the sustained load - determined +by a heuristic that takes into account the number of files in L0 sub-levels, the +number of L0 sub-levels, and whether the DB has encountered a write stall (i.e. +measured load on the DB drops to zero) - the load is increased on the DB. + +Load increases exponentially from an initial load. If the DB fails the heuristic +at the given write load, the load on the DB is paused for a period of time (the +cool-off period) before returning to the last value at which the DB could handle +the load. The exponent is then reset and the process repeats from this new +initial value. This allows the benchmark to converge on and oscillate around the +optimal write load. + +The values of load at which the DB passes and fails the heuristic are maintained +over the duration of the benchmark. On completion of the benchmark, an "optimal" +value is computed. The optimal value is computed as the value that minimizes the +mis-classification of the recorded "passes" and "fails"". This can be visualized +as a point on the x-axis that separates the passes and fails into the left and +right half-planes, minimizing the number of fails that fall to the left of this +point (i.e. mis-classified fails) and the number of passes that fall to the +right (i.e. mis-classified passes). + +The resultant "optimal sustained write load" value provides an estimate of the +write load that the DB can sustain without failing the target heuristic. + +A typical invocation of the benchmark is as follows: + + pebble bench write [PATH] --wipe -c 1024 -d 8h --rate-start 30000 --debug +`, + Args: cobra.ExactArgs(1), + RunE: runWriteBenchmark, +} + +func init() { + initWriteBench(writeBenchCmd) +} + +func initWriteBench(cmd *cobra.Command) { + // Default values for custom flags. + writeBenchConfig.batch = randvar.NewFlag("1") + writeBenchConfig.values = randvar.NewBytesFlag("1000") + + cmd.Flags().Var( + writeBenchConfig.batch, "batch", + "batch size distribution [{zipf,uniform}:]min[-max]") + cmd.Flags().StringVar( + &writeBenchConfig.keys, "keys", "zipf", "latest, uniform, or zipf") + cmd.Flags().Var( + writeBenchConfig.values, "values", + "value size distribution [{zipf,uniform}:]min[-max][/]") + cmd.Flags().IntVarP( + &writeBenchConfig.concurrency, "concurrency", "c", + 1, "number of concurrent workers") + cmd.Flags().IntVar( + &writeBenchConfig.rateStart, "rate-start", + 1000, "starting write load (ops/sec)") + cmd.Flags().IntVar( + &writeBenchConfig.incBase, "rate-inc-base", + 100, "increment / decrement base") + cmd.Flags().DurationVar( + &writeBenchConfig.testPeriod, "test-period", + 60*time.Second, "time to run at a given write load") + cmd.Flags().DurationVar( + &writeBenchConfig.cooloffPeriod, "cooloff-period", + 30*time.Second, "time to pause write load after a failure") + cmd.Flags().IntVar( + &writeBenchConfig.targetL0Files, "l0-files", + defaultL0FileLimit, "target L0 file count") + cmd.Flags().IntVar( + &writeBenchConfig.targetL0SubLevels, "l0-sublevels", + defaultL0SubLevelLimit, "target L0 sublevel count") + cmd.Flags().BoolVarP( + &wipe, "wipe", "w", false, "wipe the database before starting") + cmd.Flags().Float64Var( + &writeBenchConfig.maxRateDipFraction, "max-rate-dip-fraction", 0.1, + "fraction at which to mark a test-run as failed if the actual rate dips below (relative to the desired rate)") + cmd.Flags().BoolVar( + &writeBenchConfig.debug, "debug", false, "print benchmark debug information") +} + +// writeBenchResult contains the results of a test run at a given rate. The +// independent variable is the rate (in ops/sec) and the dependent variable is +// whether the test passed or failed. Additional metadata associated with the +// test run is also captured. +type writeBenchResult struct { + name string + rate int // The rate at which the test is currently running. + passed bool // Was the test successful at this rate. + elapsed time.Duration // The total elapsed time of the test. + bytes uint64 // The size of the LSM. + levels int // The number of levels occupied in the LSM. + writeAmp float64 // The write amplification. +} + +// String implements fmt.Stringer, printing a raw benchmark line. These lines +// are used when performing analysis on a given benchmark run. +func (r writeBenchResult) String() string { + return fmt.Sprintf("BenchmarkRaw%s %d ops/sec %v pass %s elapsed %d bytes %d levels %.2f writeAmp", + r.name, + r.rate, + r.passed, + r.elapsed, + r.bytes, + r.levels, + r.writeAmp, + ) +} + +func runWriteBenchmark(_ *cobra.Command, args []string) error { + const workload = "F" // 100% inserts. + var ( + writers []*pauseWriter + writersWg *sync.WaitGroup // Tracks completion of all pauseWriters. + cooloff bool // Is cool-off enabled. + streak int // The number of successive passes. + clockStart time.Time // Start time for current load. + cooloffStart time.Time // When cool-off was enabled. + stack []int // Stack of passing load values. + pass, fail []int // Values of load that pass and fail, respectively. + rateAcc float64 // Accumulator of measured rates for a single test run. + ) + + desiredRate := writeBenchConfig.rateStart + incBase := writeBenchConfig.incBase + weights, err := ycsbParseWorkload(workload) + + if err != nil { + return err + } + + keyDist, err := ycsbParseKeyDist(writeBenchConfig.keys) + if err != nil { + return err + } + batchDist := writeBenchConfig.batch + valueDist := writeBenchConfig.values + + // Construct a new YCSB F benchmark with the configured values. + y := newYcsb(weights, keyDist, batchDist, nil /* scans */, valueDist) + y.keyNum = ackseq.New(0) + + setLimit := func(l int) { + perWriterRate := float64(l) / float64(len(writers)) + for _, w := range writers { + w.setRate(perWriterRate) + } + } + + // Function closure to run on test-run failure. + onTestFail := func(r writeBenchResult, cancel func()) { + fail = append(fail, desiredRate) + + // Emit a benchmark raw datapoint. + fmt.Println(r) + + // We failed at the current load, we have two options: + + // a) No room to backtrack. We're done. + if len(stack) == 0 { + debugPrint("no room to backtrack; exiting ...\n") + cancel() + writersWg.Wait() + return + } + + // b) We still have room to backtrack. Reduce the load to the + // last known passing value. + desiredRate, stack = stack[len(stack)-1], stack[:len(stack)-1] + setLimit(desiredRate) + + // Enter the cool-off period. + cooloff = true + var wg sync.WaitGroup + for _, w := range writers { + // With a large number of writers, pausing synchronously can + // take a material amount of time. Instead, pause the + // writers in parallel in the background, and wait for all + // to complete before continuing. + wg.Add(1) + go func(writer *pauseWriter) { + writer.pause() + wg.Done() + }(w) + } + wg.Wait() + + // Reset the counters and clocks. + streak = 0 + rateAcc = 0 + cooloffStart = time.Now() + clockStart = time.Now() + debugPrint("Fail. Pausing writers for cool-off period.\n") + debugPrint(fmt.Sprintf("new rate=%d\npasses=%v\nfails=%v\nstack=%v\n", + desiredRate, pass, fail, stack)) + } + + // Function closure to run on test-run success. + onTestSuccess := func(r writeBenchResult) { + streak++ + pass = append(pass, desiredRate) + stack = append(stack, desiredRate) + + // Emit a benchmark raw datapoint. + r.passed = true + fmt.Println(r) + + // Increase the rate. + desiredRate = desiredRate + incBase*(1<<(streak-1)) + setLimit(desiredRate) + + // Restart the test. + rateAcc = 0 + clockStart = time.Now() + + debugPrint(fmt.Sprintf("Pass.\nnew rate=%d\npasses=%v\nfails=%v\nstreak=%d\nstack=%v\n", + desiredRate, pass, fail, streak, stack)) + } + + name := fmt.Sprintf("write/values=%s", writeBenchConfig.values) + ctx, cancel := context.WithCancel(context.Background()) + runTest(args[0], test{ + init: func(db DB, wg *sync.WaitGroup) { + y.db = db + writersWg = wg + + // Spawn the writers. + for i := 0; i < writeBenchConfig.concurrency; i++ { + writer := newPauseWriter(y, float64(desiredRate)) + writers = append(writers, writer) + writersWg.Add(1) + go writer.run(ctx, wg) + } + setLimit(desiredRate) + + // Start the clock on the current load. + clockStart = time.Now() + }, + tick: func(elapsed time.Duration, i int) { + m := y.db.Metrics() + if i%20 == 0 { + if writeBenchConfig.debug && i > 0 { + fmt.Printf("%s\n", m) + } + fmt.Println("___elapsed___clock___rate(desired)___rate(actual)___L0files___L0levels___levels______lsmBytes___writeAmp") + } + + // Print the current stats. + l0Files := m.Levels[0].NumFiles + l0Sublevels := m.Levels[0].Sublevels + nLevels := 0 + for _, l := range m.Levels { + if l.BytesIn > 0 { + nLevels++ + } + } + lsmBytes := m.DiskSpaceUsage() + total := m.Total() + writeAmp := (&total).WriteAmp() + + var currRate float64 + var stalled bool + y.reg.Tick(func(tick histogramTick) { + h := tick.Hist + currRate = float64(h.TotalCount()) / tick.Elapsed.Seconds() + stalled = !cooloff && currRate == 0 + }) + rateAcc += currRate + + // The heuristic by which the DB can sustain a given write load is + // determined by whether the DB, for the configured window of time: + // 1) did not encounter a write stall (i.e. write load fell to + // zero), + // 2) number of files in L0 was at or below the target, and + // 3) number of L0 sub-levels is at or below the target. + failed := stalled || + int(l0Files) > writeBenchConfig.targetL0Files || + int(l0Sublevels) > writeBenchConfig.targetL0SubLevels + + // Print the result for this tick. + fmt.Printf("%10s %7s %15d %14.1f %9d %10d %8d %13d %10.2f\n", + time.Duration(elapsed.Seconds()+0.5)*time.Second, + time.Duration(time.Since(clockStart).Seconds()+0.5)*time.Second, + desiredRate, + currRate, + l0Files, + l0Sublevels, + nLevels, + lsmBytes, + writeAmp, + ) + + // If we're in cool-off mode, allow it to complete before resuming + // writing. + if cooloff { + if time.Since(cooloffStart) < writeBenchConfig.cooloffPeriod { + return + } + debugPrint("ending cool-off") + + // Else, resume writing. + cooloff = false + for _, w := range writers { + w.unpause() + } + clockStart = time.Now() + + return + } + + r := writeBenchResult{ + name: name, + rate: desiredRate, + elapsed: time.Duration(elapsed.Seconds()+0.5) * time.Second, + bytes: lsmBytes, + levels: nLevels, + writeAmp: writeAmp, + } + + if failed { + onTestFail(r, cancel) + return + } + + // Else, the DB could handle the current load. We only increase + // after a fixed amount of time at this load as elapsed. + testElapsed := time.Since(clockStart) + if testElapsed < writeBenchConfig.testPeriod { + // This test-run still has time on the clock. + return + } + + // This test-run has completed. + + // If the average rate over the test is less than the desired rate, + // we mark this test-run as a failure. This handles cases where we + // encounter a bottleneck that limits write throughput but + // incorrectly mark the test as passed. + diff := 1 - rateAcc/(float64(desiredRate)*testElapsed.Seconds()) + if diff > writeBenchConfig.maxRateDipFraction { + if writeBenchConfig.debug { + debugPrint(fmt.Sprintf( + "difference in rates (%.2f) exceeded threshold (%.2f); marking test as failed\n", + diff, writeBenchConfig.maxRateDipFraction, + )) + } + onTestFail(r, cancel) + return + } + + // Mark this test-run as passed. + onTestSuccess(r) + }, + done: func(elapsed time.Duration) { + // Print final analysis. + var total int64 + y.reg.Tick(func(tick histogramTick) { + total = tick.Cumulative.TotalCount() + }) + fmt.Println("___elapsed___ops(total)") + fmt.Printf("%10s %12d\n", elapsed.Truncate(time.Second), total) + }, + }) + + return nil +} + +// debugPrint prints a debug line to stdout if debug logging is enabled via the +// --debug flag. +func debugPrint(s string) { + if !writeBenchConfig.debug { + return + } + fmt.Print("DEBUG: " + s) +} + +// pauseWriter issues load against a pebble instance, and can be paused on +// demand to allow the DB to recover. +type pauseWriter struct { + y *ycsb + limiter *rate.Limiter + pauseC chan struct{} + unpauseC chan struct{} +} + +// newPauseWriter returns a new pauseWriter. +func newPauseWriter(y *ycsb, initialRate float64) *pauseWriter { + // Set the burst rate for the limiter to the lowest sensible value to + // prevent excessive bursting. Note that a burst of zero effectively + // disables the rate limiter, as a wait time of +Inf is returned from all + // calls, and `wait(l *rate.Limiter)` will not sleep in this case. + const burst = 1 + return &pauseWriter{ + y: y, + limiter: rate.NewLimiter(float64(initialRate), burst), + pauseC: make(chan struct{}), + unpauseC: make(chan struct{}), + } +} + +// run starts the pauseWriter, issuing load against the DB. +func (w *pauseWriter) run(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + buf := &ycsbBuf{rng: randvar.NewRand()} + hist := w.y.reg.Register("insert") + for { + select { + case <-ctx.Done(): + return + case <-w.pauseC: + // Hold the goroutine here until we unpause. + <-w.unpauseC + default: + wait(w.limiter) + start := time.Now() + w.y.insert(w.y.db, buf) + hist.Record(time.Since(start)) + } + } +} + +// pause signals that the writer should pause after the current operation. +func (w *pauseWriter) pause() { + w.pauseC <- struct{}{} +} + +// unpause unpauses the writer. +func (w *pauseWriter) unpause() { + w.unpauseC <- struct{}{} +} + +// setRate sets the rate limit for this writer. +func (w *pauseWriter) setRate(r float64) { + w.limiter.SetRate(r) +} diff --git a/pebble/cmd/pebble/ycsb.go b/pebble/cmd/pebble/ycsb.go new file mode 100644 index 0000000..41de324 --- /dev/null +++ b/pebble/cmd/pebble/ycsb.go @@ -0,0 +1,609 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package main + +import ( + "fmt" + "log" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/ackseq" + "github.com/cockroachdb/pebble/internal/randvar" + "github.com/cockroachdb/pebble/internal/rate" + "github.com/spf13/cobra" + "golang.org/x/exp/rand" +) + +const ( + ycsbInsert = iota + ycsbRead + ycsbScan + ycsbReverseScan + ycsbUpdate + ycsbNumOps +) + +var ycsbConfig struct { + batch *randvar.Flag + keys string + initialKeys int + prepopulatedKeys int + numOps uint64 + scans *randvar.Flag + values *randvar.BytesFlag + workload string +} + +var ycsbCmd = &cobra.Command{ + Use: "ycsb ", + Short: "run customizable YCSB benchmark", + Long: ` +Run a customizable YCSB workload. The workload is specified by the --workload +flag which can take either one of the standard workload mixes (A-F), or +customizable workload fixes specified as a command separated list of op=weight +pairs. For example, --workload=read=50,update=50 performs a workload composed +of 50% reads and 50% updates. This is identical to the standard workload A. + +The --batch, --scans, and --values flags take the specification for a random +variable: [:][-]. The parameter must be one of "uniform" +or "zipf". If is omitted, a uniform distribution is used. If is +omitted it is set to the same value as . The specification "1000" results +in a constant 1000. The specification "10-100" results in a uniformly random +variable in the range [10,100). The specification "zipf(10,100)" results in a +zipf distribution with a minimum value of 10 and a maximum value of 100. + +The --batch flag controls the size of batches used for insert and update +operations. The --scans flag controls the number of iterations performed by a +scan operation. Read operations always read a single key. + +The --values flag provides for an optional "/" +suffix. The default target compression ratio is 1.0 (i.e. incompressible random +data). A value of 2 will cause random data to be generated that should compress +to 50% of its uncompressed size. + +Standard workloads: + + A: 50% reads / 50% updates + B: 95% reads / 5% updates + C: 100% reads + D: 95% reads / 5% inserts + E: 95% scans / 5% inserts + F: 100% inserts +`, + Args: cobra.ExactArgs(1), + RunE: runYcsb, +} + +func init() { + initYCSB(ycsbCmd) +} + +func initYCSB(cmd *cobra.Command) { + ycsbConfig.batch = randvar.NewFlag("1") + cmd.Flags().Var( + ycsbConfig.batch, "batch", + "batch size distribution [{zipf,uniform}:]min[-max]") + cmd.Flags().StringVar( + &ycsbConfig.keys, "keys", "zipf", "latest, uniform, or zipf") + cmd.Flags().IntVar( + &ycsbConfig.initialKeys, "initial-keys", 10000, + "initial number of keys to insert before beginning workload") + cmd.Flags().IntVar( + &ycsbConfig.prepopulatedKeys, "prepopulated-keys", 0, + "number of keys that were previously inserted into the database") + cmd.Flags().Uint64VarP( + &ycsbConfig.numOps, "num-ops", "n", 0, + "maximum number of operations (0 means unlimited)") + ycsbConfig.scans = randvar.NewFlag("zipf:1-1000") + cmd.Flags().Var( + ycsbConfig.scans, "scans", + "scan length distribution [{zipf,uniform}:]min[-max]") + cmd.Flags().StringVar( + &ycsbConfig.workload, "workload", "B", + "workload type (A-F) or spec (read=X,update=Y,...)") + ycsbConfig.values = randvar.NewBytesFlag("1000") + cmd.Flags().Var( + ycsbConfig.values, "values", + "value size distribution [{zipf,uniform}:]min[-max][/]") +} + +type ycsbWeights []float64 + +func (w ycsbWeights) get(i int) float64 { + if i >= len(w) { + return 0 + } + return w[i] +} + +var ycsbWorkloads = map[string]ycsbWeights{ + "A": { + ycsbRead: 0.5, + ycsbUpdate: 0.5, + }, + "B": { + ycsbRead: 0.95, + ycsbUpdate: 0.05, + }, + "C": { + ycsbRead: 1.0, + }, + "D": { + ycsbInsert: 0.05, + ycsbRead: 0.95, + // TODO(peter): default to skewed-latest distribution. + }, + "E": { + ycsbInsert: 0.05, + ycsbScan: 0.95, + }, + "F": { + ycsbInsert: 1.0, + // TODO(peter): the real workload is read-modify-write. + }, +} + +func ycsbParseWorkload(w string) (ycsbWeights, error) { + if weights := ycsbWorkloads[w]; weights != nil { + return weights, nil + } + iWeights := make([]int, ycsbNumOps) + for _, p := range strings.Split(w, ",") { + parts := strings.Split(p, "=") + if len(parts) != 2 { + return nil, errors.Errorf("malformed weights: %s", errors.Safe(w)) + } + weight, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + switch parts[0] { + case "insert": + iWeights[ycsbInsert] = weight + case "read": + iWeights[ycsbRead] = weight + case "scan": + iWeights[ycsbScan] = weight + case "rscan": + iWeights[ycsbReverseScan] = weight + case "update": + iWeights[ycsbUpdate] = weight + } + } + + var sum int + for _, w := range iWeights { + sum += w + } + if sum == 0 { + return nil, errors.Errorf("zero weight specified: %s", errors.Safe(w)) + } + + weights := make(ycsbWeights, ycsbNumOps) + for i := range weights { + weights[i] = float64(iWeights[i]) / float64(sum) + } + return weights, nil +} + +func ycsbParseKeyDist(d string) (randvar.Dynamic, error) { + totalKeys := uint64(ycsbConfig.initialKeys + ycsbConfig.prepopulatedKeys) + switch strings.ToLower(d) { + case "latest": + return randvar.NewDefaultSkewedLatest() + case "uniform": + return randvar.NewUniform(1, totalKeys), nil + case "zipf": + return randvar.NewZipf(1, totalKeys, 0.99) + default: + return nil, errors.Errorf("unknown distribution: %s", errors.Safe(d)) + } +} + +func runYcsb(cmd *cobra.Command, args []string) error { + if wipe && ycsbConfig.prepopulatedKeys > 0 { + return errors.New("--wipe and --prepopulated-keys both specified which is nonsensical") + } + + weights, err := ycsbParseWorkload(ycsbConfig.workload) + if err != nil { + return err + } + + keyDist, err := ycsbParseKeyDist(ycsbConfig.keys) + if err != nil { + return err + } + + batchDist := ycsbConfig.batch + scanDist := ycsbConfig.scans + if err != nil { + return err + } + + valueDist := ycsbConfig.values + y := newYcsb(weights, keyDist, batchDist, scanDist, valueDist) + runTest(args[0], test{ + init: y.init, + tick: y.tick, + done: y.done, + }) + return nil +} + +type ycsbBuf struct { + rng *rand.Rand + keyBuf []byte + valueBuf []byte + keyNums []uint64 +} + +type ycsb struct { + db DB + writeOpts *pebble.WriteOptions + weights ycsbWeights + reg *histogramRegistry + keyDist randvar.Dynamic + batchDist randvar.Static + scanDist randvar.Static + valueDist *randvar.BytesFlag + readAmpCount atomic.Uint64 + readAmpSum atomic.Uint64 + keyNum *ackseq.S + numOps atomic.Uint64 + limiter *rate.Limiter + opsMap map[string]int +} + +func newYcsb( + weights ycsbWeights, + keyDist randvar.Dynamic, + batchDist, scanDist randvar.Static, + valueDist *randvar.BytesFlag, +) *ycsb { + y := &ycsb{ + reg: newHistogramRegistry(), + weights: weights, + keyDist: keyDist, + batchDist: batchDist, + scanDist: scanDist, + valueDist: valueDist, + opsMap: make(map[string]int), + } + y.writeOpts = pebble.Sync + if disableWAL { + y.writeOpts = pebble.NoSync + } + + ops := map[string]int{ + "insert": ycsbInsert, + "read": ycsbRead, + "rscan": ycsbReverseScan, + "scan": ycsbScan, + "update": ycsbUpdate, + } + for name, op := range ops { + w := y.weights.get(op) + if w == 0 { + continue + } + wstr := fmt.Sprint(int(100 * w)) + fill := strings.Repeat("_", 3-len(wstr)) + if fill == "" { + fill = "_" + } + fullName := fmt.Sprintf("%s%s%s", name, fill, wstr) + y.opsMap[fullName] = op + } + return y +} + +func (y *ycsb) init(db DB, wg *sync.WaitGroup) { + y.db = db + + if ycsbConfig.initialKeys > 0 { + buf := &ycsbBuf{rng: randvar.NewRand()} + + b := db.NewBatch() + size := 0 + start := time.Now() + last := start + for i := 1; i <= ycsbConfig.initialKeys; i++ { + if now := time.Now(); now.Sub(last) >= time.Second { + fmt.Printf("%5s inserted %d keys (%0.1f%%)\n", + time.Duration(now.Sub(start).Seconds()+0.5)*time.Second, + i-1, 100*float64(i-1)/float64(ycsbConfig.initialKeys)) + last = now + } + if size >= 1<<20 { + if err := b.Commit(y.writeOpts); err != nil { + log.Fatal(err) + } + b = db.NewBatch() + size = 0 + } + key := y.makeKey(uint64(i+ycsbConfig.prepopulatedKeys), buf) + value := y.randBytes(buf) + if err := b.Set(key, value, nil); err != nil { + log.Fatal(err) + } + size += len(key) + len(value) + } + if err := b.Commit(y.writeOpts); err != nil { + log.Fatal(err) + } + _ = b.Close() + fmt.Printf("inserted keys [%d-%d)\n", + 1+ycsbConfig.prepopulatedKeys, + 1+ycsbConfig.prepopulatedKeys+ycsbConfig.initialKeys) + } + y.keyNum = ackseq.New(uint64(ycsbConfig.initialKeys + ycsbConfig.prepopulatedKeys)) + + y.limiter = maxOpsPerSec.newRateLimiter() + + wg.Add(concurrency) + + // If this workload doesn't produce reads, sample the worst case read-amp + // from Metrics() periodically. + if y.weights.get(ycsbRead) == 0 && y.weights.get(ycsbScan) == 0 && y.weights.get(ycsbReverseScan) == 0 { + wg.Add(1) + go y.sampleReadAmp(db, wg) + } + + for i := 0; i < concurrency; i++ { + go y.run(db, wg) + } +} + +func (y *ycsb) run(db DB, wg *sync.WaitGroup) { + defer wg.Done() + + var latency [ycsbNumOps]*namedHistogram + for name, op := range y.opsMap { + latency[op] = y.reg.Register(name) + } + + buf := &ycsbBuf{rng: randvar.NewRand()} + + ops := randvar.NewWeighted(nil, y.weights...) + for { + wait(y.limiter) + + start := time.Now() + + op := ops.Int() + switch op { + case ycsbInsert: + y.insert(db, buf) + case ycsbRead: + y.read(db, buf) + case ycsbScan: + y.scan(db, buf, false /* reverse */) + case ycsbReverseScan: + y.scan(db, buf, true /* reverse */) + case ycsbUpdate: + y.update(db, buf) + default: + panic("not reached") + } + + latency[op].Record(time.Since(start)) + if ycsbConfig.numOps > 0 && y.numOps.Add(1) >= ycsbConfig.numOps { + break + } + } +} + +func (y *ycsb) sampleReadAmp(db DB, wg *sync.WaitGroup) { + defer wg.Done() + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for range ticker.C { + m := db.Metrics() + y.readAmpCount.Add(1) + y.readAmpSum.Add(uint64(m.ReadAmp())) + if ycsbConfig.numOps > 0 && y.numOps.Load() >= ycsbConfig.numOps { + break + } + } +} + +func (y *ycsb) hashKey(key uint64) uint64 { + // Inlined version of fnv.New64 + Write. + const offset64 = 14695981039346656037 + const prime64 = 1099511628211 + + h := uint64(offset64) + for i := 0; i < 8; i++ { + h *= prime64 + h ^= uint64(key & 0xff) + key >>= 8 + } + return h +} + +func (y *ycsb) makeKey(keyNum uint64, buf *ycsbBuf) []byte { + const size = 24 + 10 + if cap(buf.keyBuf) < size { + buf.keyBuf = make([]byte, size) + } + key := buf.keyBuf[:4] + copy(key, "user") + key = strconv.AppendUint(key, y.hashKey(keyNum), 10) + // Use the MVCC encoding for keys. This appends a timestamp with + // walltime=1. That knowledge is utilized by rocksDB.Scan. + key = append(key, '\x00', '\x00', '\x00', '\x00', '\x00', + '\x00', '\x00', '\x00', '\x01', '\x09') + buf.keyBuf = key + return key +} + +func (y *ycsb) nextReadKey(buf *ycsbBuf) []byte { + // NB: the range of values returned by keyDist is tied to the range returned + // by keyNum.Base. See how these are both incremented by ycsb.insert(). + keyNum := y.keyDist.Uint64(buf.rng) + return y.makeKey(keyNum, buf) +} + +func (y *ycsb) randBytes(buf *ycsbBuf) []byte { + buf.valueBuf = y.valueDist.Bytes(buf.rng, buf.valueBuf) + return buf.valueBuf +} + +func (y *ycsb) insert(db DB, buf *ycsbBuf) { + count := y.batchDist.Uint64(buf.rng) + if cap(buf.keyNums) < int(count) { + buf.keyNums = make([]uint64, count) + } + keyNums := buf.keyNums[:count] + + b := db.NewBatch() + for i := range keyNums { + keyNums[i] = y.keyNum.Next() + _ = b.Set(y.makeKey(keyNums[i], buf), y.randBytes(buf), nil) + } + if err := b.Commit(y.writeOpts); err != nil { + log.Fatal(err) + } + _ = b.Close() + + for i := range keyNums { + delta, err := y.keyNum.Ack(keyNums[i]) + if err != nil { + log.Fatal(err) + } + if delta > 0 { + y.keyDist.IncMax(delta) + } + } +} + +func (y *ycsb) read(db DB, buf *ycsbBuf) { + key := y.nextReadKey(buf) + iter := db.NewIter(nil) + iter.SeekGE(key) + if iter.Valid() { + _ = iter.Key() + _ = iter.Value() + } + + type metrics interface { + Metrics() pebble.IteratorMetrics + } + if m, ok := iter.(metrics); ok { + y.readAmpCount.Add(1) + y.readAmpSum.Add(uint64(m.Metrics().ReadAmp)) + } + + if err := iter.Close(); err != nil { + log.Fatal(err) + } +} + +func (y *ycsb) scan(db DB, buf *ycsbBuf, reverse bool) { + count := y.scanDist.Uint64(buf.rng) + key := y.nextReadKey(buf) + iter := db.NewIter(nil) + if err := db.Scan(iter, key, int64(count), reverse); err != nil { + log.Fatal(err) + } + + type metrics interface { + Metrics() pebble.IteratorMetrics + } + if m, ok := iter.(metrics); ok { + y.readAmpCount.Add(1) + y.readAmpSum.Add(uint64(m.Metrics().ReadAmp)) + } + + if err := iter.Close(); err != nil { + log.Fatal(err) + } +} + +func (y *ycsb) update(db DB, buf *ycsbBuf) { + count := int(y.batchDist.Uint64(buf.rng)) + b := db.NewBatch() + for i := 0; i < count; i++ { + _ = b.Set(y.nextReadKey(buf), y.randBytes(buf), nil) + } + if err := b.Commit(y.writeOpts); err != nil { + log.Fatal(err) + } + _ = b.Close() +} + +func (y *ycsb) tick(elapsed time.Duration, i int) { + if i%20 == 0 { + fmt.Println("____optype__elapsed__ops/sec(inst)___ops/sec(cum)__p50(ms)__p95(ms)__p99(ms)_pMax(ms)") + } + y.reg.Tick(func(tick histogramTick) { + h := tick.Hist + + fmt.Printf("%10s %8s %14.1f %14.1f %8.1f %8.1f %8.1f %8.1f\n", + tick.Name, + time.Duration(elapsed.Seconds()+0.5)*time.Second, + float64(h.TotalCount())/tick.Elapsed.Seconds(), + float64(tick.Cumulative.TotalCount())/elapsed.Seconds(), + time.Duration(h.ValueAtQuantile(50)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(95)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(99)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(100)).Seconds()*1000, + ) + }) +} + +func (y *ycsb) done(elapsed time.Duration) { + fmt.Println("\n____optype__elapsed_____ops(total)___ops/sec(cum)__avg(ms)__p50(ms)__p95(ms)__p99(ms)_pMax(ms)") + + resultTick := histogramTick{} + y.reg.Tick(func(tick histogramTick) { + h := tick.Cumulative + if resultTick.Cumulative == nil { + resultTick.Now = tick.Now + resultTick.Cumulative = h + } else { + resultTick.Cumulative.Merge(h) + } + + fmt.Printf("%10s %7.1fs %14d %14.1f %8.1f %8.1f %8.1f %8.1f %8.1f\n", + tick.Name, elapsed.Seconds(), h.TotalCount(), + float64(h.TotalCount())/elapsed.Seconds(), + time.Duration(h.Mean()).Seconds()*1000, + time.Duration(h.ValueAtQuantile(50)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(95)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(99)).Seconds()*1000, + time.Duration(h.ValueAtQuantile(100)).Seconds()*1000) + }) + fmt.Println() + + resultHist := resultTick.Cumulative + m := y.db.Metrics() + total := m.Total() + + readAmpCount := y.readAmpCount.Load() + readAmpSum := y.readAmpSum.Load() + if readAmpCount == 0 { + readAmpSum = 0 + readAmpCount = 1 + } + + fmt.Printf("Benchmarkycsb/%s/values=%s %d %0.1f ops/sec %d read %d write %.2f r-amp %0.2f w-amp\n\n", + ycsbConfig.workload, ycsbConfig.values, + resultHist.TotalCount(), + float64(resultHist.TotalCount())/elapsed.Seconds(), + total.BytesRead, + total.BytesFlushed+total.BytesCompacted, + float64(readAmpSum)/float64(readAmpCount), + total.WriteAmp(), + ) +} diff --git a/pebble/commit.go b/pebble/commit.go new file mode 100644 index 0000000..38cdbb8 --- /dev/null +++ b/pebble/commit.go @@ -0,0 +1,517 @@ +// Copyright 2018 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 ( + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/pebble/record" +) + +// commitQueue is a lock-free fixed-size single-producer, multi-consumer +// queue. The single producer can enqueue (push) to the head, and consumers can +// dequeue (pop) from the tail. +// +// It has the added feature that it nils out unused slots to avoid unnecessary +// retention of objects. +type commitQueue struct { + // headTail packs together a 32-bit head index and a 32-bit tail index. Both + // are indexes into slots modulo len(slots)-1. + // + // tail = index of oldest data in queue + // head = index of next slot to fill + // + // Slots in the range [tail, head) are owned by consumers. A consumer + // continues to own a slot outside this range until it nils the slot, at + // which point ownership passes to the producer. + // + // The head index is stored in the most-significant bits so that we can + // atomically add to it and the overflow is harmless. + 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]atomic.Pointer[Batch] +} + +const dequeueBits = 32 + +func (q *commitQueue) unpack(ptrs uint64) (head, tail uint32) { + const mask = 1<> dequeueBits) & mask) + tail = uint32(ptrs & mask) + return +} + +func (q *commitQueue) pack(head, tail uint32) uint64 { + const mask = 1< syncWAL +func (p *commitPipeline) Commit(b *Batch, syncWAL bool, noSyncWait bool) error { + if b.Empty() { + return nil + } + + commitStartTime := time.Now() + // Acquire semaphores. + p.commitQueueSem <- struct{}{} + if syncWAL { + p.logSyncQSem <- struct{}{} + } + b.commitStats.SemaphoreWaitDuration = time.Since(commitStartTime) + + // Prepare the batch for committing: enqueuing the batch in the pending + // queue, determining the batch sequence number and writing the data to the + // WAL. + // + // NB: We set Batch.commitErr on error so that the batch won't be a candidate + // for reuse. See Batch.release(). + mem, err := p.prepare(b, syncWAL, noSyncWait) + if err != nil { + b.db = nil // prevent batch reuse on error + // NB: we are not doing <-p.commitQueueSem since the batch is still + // sitting in the pending queue. We should consider fixing this by also + // removing the batch from the pending queue. + return err + } + + // Apply the batch to the memtable. + if err := p.env.apply(b, mem); err != nil { + b.db = nil // prevent batch reuse on error + // NB: we are not doing <-p.commitQueueSem since the batch is still + // sitting in the pending queue. We should consider fixing this by also + // removing the batch from the pending queue. + return err + } + + // Publish the batch sequence number. + p.publish(b) + + <-p.commitQueueSem + + if !noSyncWait { + // Already waited for commit, so look at the error. + if b.commitErr != nil { + b.db = nil // prevent batch reuse on error + err = b.commitErr + } + } + // Else noSyncWait. The LogWriter can be concurrently writing to + // b.commitErr. We will read b.commitErr in Batch.SyncWait after the + // LogWriter is done writing. + + b.commitStats.TotalDuration = time.Since(commitStartTime) + + return err +} + +// AllocateSeqNum allocates count sequence numbers, invokes the prepare +// callback, then the apply callback, and then publishes the sequence +// numbers. AllocateSeqNum does not write to the WAL or add entries to the +// memtable. AllocateSeqNum can be used to sequence an operation such as +// sstable ingestion within the commit pipeline. The prepare callback is +// invoked with commitPipeline.mu held, but note that DB.mu is not held and +// must be locked if necessary. +func (p *commitPipeline) AllocateSeqNum( + count int, prepare func(seqNum uint64), apply func(seqNum uint64), +) { + // This method is similar to Commit and prepare. Be careful about trying to + // share additional code with those methods because Commit and prepare are + // performance critical code paths. + + b := newBatch(nil) + defer b.release() + + // Give the batch a count of 1 so that the log and visible sequence number + // are incremented correctly. + b.data = make([]byte, batchHeaderLen) + b.setCount(uint32(count)) + b.commit.Add(1) + + p.commitQueueSem <- struct{}{} + + p.mu.Lock() + + // Enqueue the batch in the pending queue. Note that while the pending queue + // is lock-free, we want the order of batches to be the same as the sequence + // number order. + p.pending.enqueue(b) + + // Assign the batch a sequence number. Note that we use atomic operations + // here to handle concurrent reads of logSeqNum. commitPipeline.mu provides + // mutual exclusion for other goroutines writing to logSeqNum. + logSeqNum := p.env.logSeqNum.Add(uint64(count)) - uint64(count) + seqNum := logSeqNum + if seqNum == 0 { + // We can't use the value 0 for the global seqnum during ingestion, because + // 0 indicates no global seqnum. So allocate one more seqnum. + p.env.logSeqNum.Add(1) + seqNum++ + } + b.setSeqNum(seqNum) + + // Wait for any outstanding writes to the memtable to complete. This is + // necessary for ingestion so that the check for memtable overlap can see any + // writes that were sequenced before the ingestion. The spin loop is + // unfortunate, but obviates the need for additional synchronization. + for { + visibleSeqNum := p.env.visibleSeqNum.Load() + if visibleSeqNum == logSeqNum { + break + } + runtime.Gosched() + } + + // Invoke the prepare callback. Note the lack of error reporting. Even if the + // callback internally fails, the sequence number needs to be published in + // order to allow the commit pipeline to proceed. + prepare(b.SeqNum()) + + p.mu.Unlock() + + // Invoke the apply callback. + apply(b.SeqNum()) + + // Publish the sequence number. + p.publish(b) + + <-p.commitQueueSem +} + +func (p *commitPipeline) prepare(b *Batch, syncWAL bool, noSyncWait bool) (*memTable, error) { + n := uint64(b.Count()) + if n == invalidBatchCount { + return nil, ErrInvalidBatch + } + var syncWG *sync.WaitGroup + var syncErr *error + switch { + case !syncWAL: + // Only need to wait for the publish. + b.commit.Add(1) + // Remaining cases represent syncWAL=true. + case noSyncWait: + syncErr = &b.commitErr + syncWG = &b.fsyncWait + // Only need to wait synchronously for the publish. The user will + // (asynchronously) wait on the batch's fsyncWait. + b.commit.Add(1) + b.fsyncWait.Add(1) + case !noSyncWait: + syncErr = &b.commitErr + syncWG = &b.commit + // Must wait for both the publish and the WAL fsync. + b.commit.Add(2) + } + + p.mu.Lock() + + // Enqueue the batch in the pending queue. Note that while the pending queue + // is lock-free, we want the order of batches to be the same as the sequence + // number order. + p.pending.enqueue(b) + + // Assign the batch a sequence number. Note that we use atomic operations + // here to handle concurrent reads of logSeqNum. commitPipeline.mu provides + // mutual exclusion for other goroutines writing to logSeqNum. + b.setSeqNum(p.env.logSeqNum.Add(n) - n) + + // Write the data to the WAL. + mem, err := p.env.write(b, syncWG, syncErr) + + p.mu.Unlock() + + return mem, err +} + +func (p *commitPipeline) publish(b *Batch) { + // Mark the batch as applied. + b.applied.Store(true) + + // Loop dequeuing applied batches from the pending queue. If our batch was + // the head of the pending queue we are guaranteed that either we'll publish + // it or someone else will dequeueApplied and publish it. If our batch is not the + // head of the queue then either we'll dequeueApplied applied batches and reach our + // batch or there is an unapplied batch blocking us. When that unapplied + // batch applies it will go through the same process and publish our batch + // for us. + for { + t := p.pending.dequeueApplied() + if t == nil { + // Wait for another goroutine to publish us. We might also be waiting for + // the WAL sync to finish. + now := time.Now() + b.commit.Wait() + b.commitStats.CommitWaitDuration += time.Since(now) + break + } + if !t.applied.Load() { + panic("not reached") + } + + // We're responsible for publishing the sequence number for batch t, but + // another concurrent goroutine might sneak in and publish the sequence + // number for a subsequent batch. That's ok as all we're guaranteeing is + // that the sequence number ratchets up. + for { + curSeqNum := p.env.visibleSeqNum.Load() + newSeqNum := t.SeqNum() + uint64(t.Count()) + if newSeqNum <= curSeqNum { + // t's sequence number has already been published. + break + } + if p.env.visibleSeqNum.CompareAndSwap(curSeqNum, newSeqNum) { + // We successfully published t's sequence number. + break + } + } + + t.commit.Done() + } +} diff --git a/pebble/commit_test.go b/pebble/commit_test.go new file mode 100644 index 0000000..51b618d --- /dev/null +++ b/pebble/commit_test.go @@ -0,0 +1,355 @@ +// Copyright 2018 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 ( + "encoding/binary" + "fmt" + "io" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/cockroachdb/pebble/internal/arenaskl" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/vfs" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +type testCommitEnv struct { + logSeqNum atomic.Uint64 + visibleSeqNum atomic.Uint64 + writeCount atomic.Uint64 + applyBuf struct { + sync.Mutex + buf []uint64 + } + queueSemChan chan struct{} +} + +func (e *testCommitEnv) env() commitEnv { + return commitEnv{ + logSeqNum: &e.logSeqNum, + visibleSeqNum: &e.visibleSeqNum, + apply: e.apply, + write: e.write, + } +} + +func (e *testCommitEnv) apply(b *Batch, mem *memTable) error { + e.applyBuf.Lock() + e.applyBuf.buf = append(e.applyBuf.buf, b.SeqNum()) + e.applyBuf.Unlock() + return nil +} + +func (e *testCommitEnv) write(b *Batch, wg *sync.WaitGroup, _ *error) (*memTable, error) { + e.writeCount.Add(1) + if wg != nil { + wg.Done() + <-e.queueSemChan + } + return nil, nil +} + +func TestCommitQueue(t *testing.T) { + var q commitQueue + var batches [16]Batch + for i := range batches { + q.enqueue(&batches[i]) + } + if b := q.dequeueApplied(); b != nil { + t.Fatalf("unexpectedly dequeued batch: %p", b) + } + batches[1].applied.Store(true) + if b := q.dequeueApplied(); b != nil { + t.Fatalf("unexpectedly dequeued batch: %p", b) + } + for i := range batches { + batches[i].applied.Store(true) + if b := q.dequeueApplied(); b != &batches[i] { + t.Fatalf("%d: expected batch %p, but found %p", i, &batches[i], b) + } + } + if b := q.dequeueApplied(); b != nil { + t.Fatalf("unexpectedly dequeued batch: %p", b) + } +} + +func TestCommitPipeline(t *testing.T) { + var e testCommitEnv + p := newCommitPipeline(e.env()) + + n := 10000 + if invariants.RaceEnabled { + // Under race builds we have to limit the concurrency or we hit the + // following error: + // + // race: limit on 8128 simultaneously alive goroutines is exceeded, dying + n = 1000 + } + + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go func(i int) { + defer wg.Done() + var b Batch + _ = b.Set([]byte(fmt.Sprint(i)), nil, nil) + _ = p.Commit(&b, false, false) + }(i) + } + wg.Wait() + + if s := e.writeCount.Load(); uint64(n) != s { + t.Fatalf("expected %d written batches, but found %d", n, s) + } + if n != len(e.applyBuf.buf) { + t.Fatalf("expected %d written batches, but found %d", + n, len(e.applyBuf.buf)) + } + if s := e.logSeqNum.Load(); uint64(n) != s { + t.Fatalf("expected %d, but found %d", n, s) + } + if s := e.visibleSeqNum.Load(); uint64(n) != s { + t.Fatalf("expected %d, but found %d", n, s) + } +} + +func TestCommitPipelineSync(t *testing.T) { + n := 10000 + if invariants.RaceEnabled { + // Under race builds we have to limit the concurrency or we hit the + // following error: + // + // race: limit on 8128 simultaneously alive goroutines is exceeded, dying + n = 1000 + } + + for _, noSyncWait := range []bool{false, true} { + t.Run(fmt.Sprintf("no-sync-wait=%t", noSyncWait), func(t *testing.T) { + var e testCommitEnv + p := newCommitPipeline(e.env()) + e.queueSemChan = p.logSyncQSem + + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go func(i int) { + defer wg.Done() + var b Batch + require.NoError(t, b.Set([]byte(fmt.Sprint(i)), nil, nil)) + require.NoError(t, p.Commit(&b, true, noSyncWait)) + if noSyncWait { + require.NoError(t, b.SyncWait()) + } + }(i) + } + wg.Wait() + if s := e.writeCount.Load(); uint64(n) != s { + t.Fatalf("expected %d written batches, but found %d", n, s) + } + if n != len(e.applyBuf.buf) { + t.Fatalf("expected %d written batches, but found %d", + n, len(e.applyBuf.buf)) + } + if s := e.logSeqNum.Load(); uint64(n) != s { + t.Fatalf("expected %d, but found %d", n, s) + } + if s := e.visibleSeqNum.Load(); uint64(n) != s { + t.Fatalf("expected %d, but found %d", n, s) + } + }) + } +} + +func TestCommitPipelineAllocateSeqNum(t *testing.T) { + var e testCommitEnv + p := newCommitPipeline(e.env()) + + const n = 10 + var wg sync.WaitGroup + wg.Add(n) + var prepareCount atomic.Uint64 + var applyCount atomic.Uint64 + for i := 1; i <= n; i++ { + go func(i int) { + defer wg.Done() + p.AllocateSeqNum(i, func(_ uint64) { + prepareCount.Add(1) + }, func(seqNum uint64) { + applyCount.Add(1) + }) + }(i) + } + wg.Wait() + + if s := prepareCount.Load(); n != s { + t.Fatalf("expected %d prepares, but found %d", n, s) + } + if s := applyCount.Load(); n != s { + t.Fatalf("expected %d applies, but found %d", n, s) + } + // AllocateSeqNum always returns a non-zero sequence number causing the + // values we see to be offset from 1. + const total = 1 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + if s := e.logSeqNum.Load(); total != s { + t.Fatalf("expected %d, but found %d", total, s) + } + if s := e.visibleSeqNum.Load(); total != s { + t.Fatalf("expected %d, but found %d", total, s) + } +} + +type syncDelayFile struct { + vfs.File + done chan struct{} +} + +func (f *syncDelayFile) Sync() error { + <-f.done + return nil +} + +func TestCommitPipelineWALClose(t *testing.T) { + // This test stresses the edge case of N goroutines blocked in the + // commitPipeline waiting for the log to sync when we concurrently decide to + // rotate and close the log. + + mem := vfs.NewMem() + f, err := mem.Create("test-wal") + require.NoError(t, err) + + // syncDelayFile will block on the done channel befor returning from Sync + // call. + sf := &syncDelayFile{ + File: f, + done: make(chan struct{}), + } + + // A basic commitEnv which writes to a WAL. + var wal *record.LogWriter + var walDone sync.WaitGroup + testEnv := commitEnv{ + logSeqNum: new(atomic.Uint64), + visibleSeqNum: new(atomic.Uint64), + apply: func(b *Batch, mem *memTable) error { + // At this point, we've called SyncRecord but the sync is blocked. + walDone.Done() + return nil + }, + write: func(b *Batch, syncWG *sync.WaitGroup, syncErr *error) (*memTable, error) { + _, err := wal.SyncRecord(b.data, syncWG, syncErr) + return nil, err + }, + } + p := newCommitPipeline(testEnv) + wal = record.NewLogWriter(sf, 0 /* logNum */, record.LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), + QueueSemChan: p.logSyncQSem, + }) + + // Launch N (commitConcurrency) goroutines which each create a batch and + // commit it with sync==true. Because of the syncDelayFile, none of these + // operations can complete until syncDelayFile.done is closed. + errCh := make(chan error, cap(p.commitQueueSem)) + walDone.Add(cap(errCh)) + for i := 0; i < cap(errCh); i++ { + go func(i int) { + b := &Batch{} + if err := b.LogData([]byte("foo"), nil); err != nil { + errCh <- err + return + } + errCh <- p.Commit(b, true /* sync */, false) + }(i) + } + + // Wait for all of the WAL writes to queue up. This ensures we don't violate + // the concurrency requirements of LogWriter, and also ensures all of the WAL + // writes are queued. + walDone.Wait() + close(sf.done) + + // Close the WAL. A "queue is full" panic means that something is broken. + require.NoError(t, wal.Close()) + for i := 0; i < cap(errCh); i++ { + require.NoError(t, <-errCh) + } +} + +func BenchmarkCommitPipeline(b *testing.B) { + for _, noSyncWait := range []bool{false, true} { + for _, parallelism := range []int{1, 2, 4, 8, 16, 32, 64, 128} { + b.Run(fmt.Sprintf("no-sync-wait=%t/parallel=%d", noSyncWait, parallelism), + func(b *testing.B) { + b.SetParallelism(parallelism) + mem := newMemTable(memTableOptions{}) + var wal *record.LogWriter + nullCommitEnv := commitEnv{ + logSeqNum: new(atomic.Uint64), + visibleSeqNum: new(atomic.Uint64), + apply: func(b *Batch, mem *memTable) error { + err := mem.apply(b, b.SeqNum()) + if err != nil { + return err + } + mem.writerUnref() + return nil + }, + write: func(b *Batch, syncWG *sync.WaitGroup, syncErr *error) (*memTable, error) { + for { + err := mem.prepare(b) + if err == arenaskl.ErrArenaFull { + mem = newMemTable(memTableOptions{}) + continue + } + if err != nil { + return nil, err + } + break + } + + _, err := wal.SyncRecord(b.data, syncWG, syncErr) + return mem, err + }, + } + p := newCommitPipeline(nullCommitEnv) + wal = record.NewLogWriter(io.Discard, 0, /* logNum */ + record.LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), + QueueSemChan: p.logSyncQSem, + }) + const keySize = 8 + b.SetBytes(2 * keySize) + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + buf := make([]byte, keySize) + + for pb.Next() { + batch := newBatch(nil) + binary.BigEndian.PutUint64(buf, rng.Uint64()) + batch.Set(buf, buf, nil) + if err := p.Commit(batch, true /* sync */, noSyncWait); err != nil { + b.Fatal(err) + } + if noSyncWait { + if err := batch.SyncWait(); err != nil { + b.Fatal(err) + } + } + batch.release() + } + }) + }) + } + } +} diff --git a/pebble/compaction.go b/pebble/compaction.go new file mode 100644 index 0000000..42a709f --- /dev/null +++ b/pebble/compaction.go @@ -0,0 +1,3924 @@ +// Copyright 2013 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 ( + "bytes" + "context" + "fmt" + "io" + "math" + "runtime/pprof" + "sort" + "sync/atomic" + "time" + + "github.com/cockroachdb/errors" + "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/private" + "github.com/cockroachdb/pebble/internal/rangedel" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" +) + +var errEmptyTable = errors.New("pebble: empty table") + +// ErrCancelledCompaction is returned if a compaction is cancelled by a +// concurrent excise or ingest-split operation. +var ErrCancelledCompaction = errors.New("pebble: compaction cancelled by a concurrent operation, will retry compaction") + +var compactLabels = pprof.Labels("pebble", "compact") +var flushLabels = pprof.Labels("pebble", "flush") +var gcLabels = pprof.Labels("pebble", "gc") + +// getInternalWriterProperties accesses a private variable (in the +// internal/private package) initialized by the sstable Writer. This indirection +// is necessary to ensure non-Pebble users constructing sstables for ingestion +// are unable to set internal-only properties. +var getInternalWriterProperties = private.SSTableInternalProperties.(func(*sstable.Writer) *sstable.Properties) + +// expandedCompactionByteSizeLimit is the maximum number of bytes in all +// compacted files. We avoid expanding the lower level file set of a compaction +// if it would make the total compaction cover more than this many bytes. +func expandedCompactionByteSizeLimit(opts *Options, level int, availBytes uint64) uint64 { + v := uint64(25 * opts.Level(level).TargetFileSize) + + // Never expand a compaction beyond half the available capacity, divided + // by the maximum number of concurrent compactions. Each of the concurrent + // compactions may expand up to this limit, so this attempts to limit + // compactions to half of available disk space. Note that this will not + // prevent compaction picking from pursuing compactions that are larger + // than this threshold before expansion. + diskMax := (availBytes / 2) / uint64(opts.MaxConcurrentCompactions()) + if v > diskMax { + v = diskMax + } + return v +} + +// maxGrandparentOverlapBytes is the maximum bytes of overlap with level+1 +// before we stop building a single file in a level-1 to level compaction. +func maxGrandparentOverlapBytes(opts *Options, level int) uint64 { + return uint64(10 * opts.Level(level).TargetFileSize) +} + +// maxReadCompactionBytes is used to prevent read compactions which +// are too wide. +func maxReadCompactionBytes(opts *Options, level int) uint64 { + return uint64(10 * opts.Level(level).TargetFileSize) +} + +// noCloseIter wraps around a FragmentIterator, intercepting and eliding +// calls to Close. It is used during compaction to ensure that rangeDelIters +// are not closed prematurely. +type noCloseIter struct { + keyspan.FragmentIterator +} + +func (i noCloseIter) Close() error { + return nil +} + +type compactionLevel struct { + level int + files manifest.LevelSlice + // l0SublevelInfo contains information about L0 sublevels being compacted. + // It's only set for the start level of a compaction starting out of L0 and + // is nil for all other compactions. + l0SublevelInfo []sublevelInfo +} + +func (cl compactionLevel) Clone() compactionLevel { + newCL := compactionLevel{ + level: cl.level, + files: cl.files.Reslice(func(start, end *manifest.LevelIterator) {}), + } + return newCL +} +func (cl compactionLevel) String() string { + return fmt.Sprintf(`Level %d, Files %s`, cl.level, cl.files) +} + +// Return output from compactionOutputSplitters. See comment on +// compactionOutputSplitter.shouldSplitBefore() on how this value is used. +type maybeSplit int + +const ( + noSplit maybeSplit = iota + splitNow +) + +// String implements the Stringer interface. +func (c maybeSplit) String() string { + if c == noSplit { + return "no-split" + } + return "split-now" +} + +// compactionOutputSplitter is an interface for encapsulating logic around +// switching the output of a compaction to a new output file. Additional +// constraints around switching compaction outputs that are specific to that +// compaction type (eg. flush splits) are implemented in +// compactionOutputSplitters that compose other child compactionOutputSplitters. +type compactionOutputSplitter interface { + // shouldSplitBefore returns whether we should split outputs before the + // specified "current key". The return value is splitNow or noSplit. + // splitNow means a split is advised before the specified key, and noSplit + // means no split is advised. If shouldSplitBefore(a) advises a split then + // shouldSplitBefore(b) should also advise a split given b >= a, until + // onNewOutput is called. + shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit + // onNewOutput updates internal splitter state when the compaction switches + // to a new sstable, and returns the next limit for the new output which + // would get used to truncate range tombstones if the compaction iterator + // runs out of keys. The limit returned MUST be > key according to the + // compaction's comparator. The specified key is the first key in the new + // output, or nil if this sstable will only contain range tombstones already + // in the fragmenter. + onNewOutput(key []byte) []byte +} + +// fileSizeSplitter is a compactionOutputSplitter that enforces target file +// sizes. This splitter splits to a new output file when the estimated file size +// is 0.5x-2x the target file size. If there are overlapping grandparent files, +// this splitter will attempt to split at a grandparent boundary. For example, +// consider the example where a compaction wrote 'd' to the current output file, +// and the next key has a user key 'g': +// +// previous key next key +// | | +// | | +// +---------------|----+ +--|----------+ +// grandparents: | 000006 | | | | 000007 | +// +---------------|----+ +--|----------+ +// a b d e f g i +// +// Splitting the output file F before 'g' will ensure that the current output +// file F does not overlap the grandparent file 000007. Aligning sstable +// boundaries like this can significantly reduce write amplification, since a +// subsequent compaction of F into the grandparent level will avoid needlessly +// rewriting any keys within 000007 that do not overlap F's bounds. Consider the +// following compaction: +// +// +----------------------+ +// input | | +// level +----------------------+ +// \/ +// +---------------+ +---------------+ +// output |XXXXXXX| | | |XXXXXXXX| +// level +---------------+ +---------------+ +// +// The input-level file overlaps two files in the output level, but only +// partially. The beginning of the first output-level file and the end of the +// second output-level file will be rewritten verbatim. This write I/O is +// "wasted" in the sense that no merging is being performed. +// +// To prevent the above waste, this splitter attempts to split output files +// before the start key of grandparent files. It still strives to write output +// files of approximately the target file size, by constraining this splitting +// at grandparent points to apply only if the current output's file size is +// about the right order of magnitude. +// +// Note that, unlike most other splitters, this splitter does not guarantee that +// it will advise splits only at user key change boundaries. +type fileSizeSplitter struct { + frontier frontier + targetFileSize uint64 + atGrandparentBoundary bool + boundariesObserved uint64 + nextGrandparent *fileMetadata + grandparents manifest.LevelIterator +} + +func newFileSizeSplitter( + f *frontiers, targetFileSize uint64, grandparents manifest.LevelIterator, +) *fileSizeSplitter { + s := &fileSizeSplitter{targetFileSize: targetFileSize} + s.nextGrandparent = grandparents.First() + s.grandparents = grandparents + if s.nextGrandparent != nil { + s.frontier.Init(f, s.nextGrandparent.Smallest.UserKey, s.reached) + } + return s +} + +func (f *fileSizeSplitter) reached(nextKey []byte) []byte { + f.atGrandparentBoundary = true + f.boundariesObserved++ + // NB: f.grandparents is a bounded iterator, constrained to the compaction + // key range. + f.nextGrandparent = f.grandparents.Next() + if f.nextGrandparent == nil { + return nil + } + // TODO(jackson): Should we also split before or immediately after + // grandparents' largest keys? Splitting before the start boundary prevents + // overlap with the grandparent. Also splitting after the end boundary may + // increase the probability of move compactions. + return f.nextGrandparent.Smallest.UserKey +} + +func (f *fileSizeSplitter) shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit { + atGrandparentBoundary := f.atGrandparentBoundary + + // Clear f.atGrandparentBoundary unconditionally. + // + // This is a bit subtle. Even if do decide to split, it's possible that a + // higher-level splitter will ignore our request (eg, because we're between + // two internal keys with the same user key). In this case, the next call to + // shouldSplitBefore will find atGrandparentBoundary=false. This is + // desirable, because in this case we would've already written the earlier + // key with the same user key to the output file. The current output file is + // already doomed to overlap the grandparent whose bound triggered + // atGrandparentBoundary=true. We should continue on, waiting for the next + // grandparent boundary. + f.atGrandparentBoundary = false + + // If the key is a range tombstone, the EstimatedSize may not grow right + // away when a range tombstone is added to the fragmenter: It's dependent on + // whether or not the this new range deletion will start a new fragment. + // Range deletions are rare, so we choose to simply not split yet. + // TODO(jackson): Reconsider this, and consider range keys too as a part of + // #2321. + if key.Kind() == InternalKeyKindRangeDelete || tw == nil { + return noSplit + } + + estSize := tw.EstimatedSize() + switch { + case estSize < f.targetFileSize/2: + // The estimated file size is less than half the target file size. Don't + // split it, even if currently aligned with a grandparent file because + // it's too small. + return noSplit + case estSize >= 2*f.targetFileSize: + // The estimated file size is double the target file size. Split it even + // if we were not aligned with a grandparent file boundary to avoid + // excessively exceeding the target file size. + return splitNow + case !atGrandparentBoundary: + // Don't split if we're not at a grandparent, except if we've exhausted + // all the grandparents overlapping this compaction's key range. Then we + // may want to split purely based on file size. + if f.nextGrandparent == nil { + // There are no more grandparents. Optimize for the target file size + // and split as soon as we hit the target file size. + if estSize >= f.targetFileSize { + return splitNow + } + } + return noSplit + default: + // INVARIANT: atGrandparentBoundary + // INVARIANT: targetSize/2 < estSize < 2*targetSize + // + // The estimated file size is close enough to the target file size that + // we should consider splitting. + // + // Determine whether to split now based on how many grandparent + // boundaries we have already observed while building this output file. + // The intuition here is that if the grandparent level is dense in this + // part of the keyspace, we're likely to continue to have more + // opportunities to split this file aligned with a grandparent. If this + // is the first grandparent boundary observed, we split immediately + // (we're already at ≥50% the target file size). Otherwise, each + // overlapping grandparent we've observed increases the minimum file + // size by 5% of the target file size, up to at most 90% of the target + // file size. + // + // TODO(jackson): The particular thresholds are somewhat unprincipled. + // This is the same heuristic as RocksDB implements. Is there are more + // principled formulation that can, further reduce w-amp, produce files + // closer to the target file size, or is more understandable? + + // NB: Subtract 1 from `boundariesObserved` to account for the current + // boundary we're considering splitting at. `reached` will have + // incremented it at the same time it set `atGrandparentBoundary`. + minBoundaries := f.boundariesObserved-1 + if minBoundaries > 8 { + minBoundaries = 8 + } + minimumPctOfTargetSize := 50 + 5*minBoundaries + if estSize < (minimumPctOfTargetSize*f.targetFileSize)/100 { + return noSplit + } + return splitNow + } +} + +func (f *fileSizeSplitter) onNewOutput(key []byte) []byte { + f.boundariesObserved = 0 + return nil +} + +func newLimitFuncSplitter(f *frontiers, limitFunc func(userKey []byte) []byte) *limitFuncSplitter { + s := &limitFuncSplitter{limitFunc: limitFunc} + s.frontier.Init(f, nil, s.reached) + return s +} + +type limitFuncSplitter struct { + frontier frontier + limitFunc func(userKey []byte) []byte + split maybeSplit +} + +func (lf *limitFuncSplitter) shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit { + return lf.split +} + +func (lf *limitFuncSplitter) reached(nextKey []byte) []byte { + lf.split = splitNow + return nil +} + +func (lf *limitFuncSplitter) onNewOutput(key []byte) []byte { + lf.split = noSplit + if key != nil { + // TODO(jackson): For some users, like L0 flush splits, there's no need + // to binary search over all the flush splits every time. The next split + // point must be ahead of the previous flush split point. + limit := lf.limitFunc(key) + lf.frontier.Update(limit) + return limit + } + lf.frontier.Update(nil) + return nil +} + +// splitterGroup is a compactionOutputSplitter that splits whenever one of its +// child splitters advises a compaction split. +type splitterGroup struct { + cmp Compare + splitters []compactionOutputSplitter +} + +func (a *splitterGroup) shouldSplitBefore( + key *InternalKey, tw *sstable.Writer, +) (suggestion maybeSplit) { + for _, splitter := range a.splitters { + if splitter.shouldSplitBefore(key, tw) == splitNow { + return splitNow + } + } + return noSplit +} + +func (a *splitterGroup) onNewOutput(key []byte) []byte { + var earliestLimit []byte + for _, splitter := range a.splitters { + limit := splitter.onNewOutput(key) + if limit == nil { + continue + } + if earliestLimit == nil || a.cmp(limit, earliestLimit) < 0 { + earliestLimit = limit + } + } + return earliestLimit +} + +// userKeyChangeSplitter is a compactionOutputSplitter that takes in a child +// splitter, and splits when 1) that child splitter has advised a split, and 2) +// the compaction output is at the boundary between two user keys (also +// the boundary between atomic compaction units). Use this splitter to wrap +// any splitters that don't guarantee user key splits (i.e. splitters that make +// their determination in ways other than comparing the current key against a +// limit key.) If a wrapped splitter advises a split, it must continue +// to advise a split until a new output. +type userKeyChangeSplitter struct { + cmp Compare + splitter compactionOutputSplitter + unsafePrevUserKey func() []byte +} + +func (u *userKeyChangeSplitter) shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit { + // NB: The userKeyChangeSplitter only needs to suffer a key comparison if + // the wrapped splitter requests a split. + // + // We could implement this splitter using frontiers: When the inner splitter + // requests a split before key `k`, we'd update a frontier to be + // ImmediateSuccessor(k). Then on the next key greater than >k, the + // frontier's `reached` func would be called and we'd return splitNow. + // This doesn't really save work since duplicate user keys are rare, and it + // requires us to materialize the ImmediateSuccessor key. It also prevents + // us from splitting on the same key that the inner splitter requested a + // split for—instead we need to wait until the next key. The current + // implementation uses `unsafePrevUserKey` to gain access to the previous + // key which allows it to immediately respect the inner splitter if + // possible. + if split := u.splitter.shouldSplitBefore(key, tw); split != splitNow { + return split + } + if u.cmp(key.UserKey, u.unsafePrevUserKey()) > 0 { + return splitNow + } + return noSplit +} + +func (u *userKeyChangeSplitter) onNewOutput(key []byte) []byte { + return u.splitter.onNewOutput(key) +} + +// compactionWritable is a objstorage.Writable wrapper that, on every write, +// updates a metric in `versions` on bytes written by in-progress compactions so +// far. It also increments a per-compaction `written` int. +type compactionWritable struct { + objstorage.Writable + + versions *versionSet + written *int64 +} + +// Write is part of the objstorage.Writable interface. +func (c *compactionWritable) Write(p []byte) error { + if err := c.Writable.Write(p); err != nil { + return err + } + + *c.written += int64(len(p)) + c.versions.incrementCompactionBytes(int64(len(p))) + return nil +} + +type compactionKind int + +const ( + compactionKindDefault compactionKind = iota + compactionKindFlush + // compactionKindMove denotes a move compaction where the input file is + // retained and linked in a new level without being obsoleted. + compactionKindMove + // compactionKindCopy denotes a copy compaction where the input file is + // copied byte-by-byte into a new file with a new FileNum in the output level. + compactionKindCopy + compactionKindDeleteOnly + compactionKindElisionOnly + compactionKindRead + compactionKindRewrite + compactionKindIngestedFlushable +) + +func (k compactionKind) String() string { + switch k { + case compactionKindDefault: + return "default" + case compactionKindFlush: + return "flush" + case compactionKindMove: + return "move" + case compactionKindDeleteOnly: + return "delete-only" + case compactionKindElisionOnly: + return "elision-only" + case compactionKindRead: + return "read" + case compactionKindRewrite: + return "rewrite" + case compactionKindIngestedFlushable: + return "ingested-flushable" + case compactionKindCopy: + return "copy" + } + return "?" +} + +// rangeKeyCompactionTransform is used to transform range key spans as part of the +// keyspan.MergingIter. As part of this transformation step, we can elide range +// keys in the last snapshot stripe, as well as coalesce range keys within +// snapshot stripes. +func rangeKeyCompactionTransform( + eq base.Equal, snapshots []uint64, elideRangeKey func(start, end []byte) bool, +) keyspan.Transformer { + return keyspan.TransformerFunc(func(cmp base.Compare, s keyspan.Span, dst *keyspan.Span) error { + elideInLastStripe := func(keys []keyspan.Key) []keyspan.Key { + // Unsets and deletes in the last snapshot stripe can be elided. + k := 0 + for j := range keys { + if elideRangeKey(s.Start, s.End) && + (keys[j].Kind() == InternalKeyKindRangeKeyUnset || keys[j].Kind() == InternalKeyKindRangeKeyDelete) { + continue + } + keys[k] = keys[j] + k++ + } + keys = keys[:k] + return keys + } + // snapshots are in ascending order, while s.keys are in descending seqnum + // order. Partition s.keys by snapshot stripes, and call rangekey.Coalesce + // on each partition. + dst.Start = s.Start + dst.End = s.End + dst.Keys = dst.Keys[:0] + i, j := len(snapshots)-1, 0 + usedLen := 0 + for i >= 0 { + start := j + for j < len(s.Keys) && !base.Visible(s.Keys[j].SeqNum(), snapshots[i], base.InternalKeySeqNumMax) { + // Include j in current partition. + j++ + } + if j > start { + keysDst := dst.Keys[usedLen:cap(dst.Keys)] + if err := rangekey.Coalesce(cmp, eq, s.Keys[start:j], &keysDst); err != nil { + return err + } + if j == len(s.Keys) { + // This is the last snapshot stripe. Unsets and deletes can be elided. + keysDst = elideInLastStripe(keysDst) + } + usedLen += len(keysDst) + dst.Keys = append(dst.Keys, keysDst...) + } + i-- + } + if j < len(s.Keys) { + keysDst := dst.Keys[usedLen:cap(dst.Keys)] + if err := rangekey.Coalesce(cmp, eq, s.Keys[j:], &keysDst); err != nil { + return err + } + keysDst = elideInLastStripe(keysDst) + usedLen += len(keysDst) + dst.Keys = append(dst.Keys, keysDst...) + } + return nil + }) +} + +// compaction is a table compaction from one level to the next, starting from a +// given version. +type compaction struct { + // cancel is a bool that can be used by other goroutines to signal a compaction + // to cancel, such as if a conflicting excise operation raced it to manifest + // application. Only holders of the manifest lock will write to this atomic. + cancel atomic.Bool + + kind compactionKind + cmp Compare + equal Equal + comparer *base.Comparer + formatKey base.FormatKey + logger Logger + version *version + stats base.InternalIteratorStats + beganAt time.Time + // versionEditApplied is set to true when a compaction has completed and the + // resulting version has been installed (if successful), but the compaction + // goroutine is still cleaning up (eg, deleting obsolete files). + versionEditApplied bool + bufferPool sstable.BufferPool + + // 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 + // A compaction's outputLevel is nil for delete-only compactions. + outputLevel *compactionLevel + + // extraLevels point to additional levels in between the input and output + // levels that get compacted in multilevel compactions + extraLevels []*compactionLevel + + inputs []compactionLevel + + // maxOutputFileSize is the maximum size of an individual table created + // during compaction. + maxOutputFileSize uint64 + // maxOverlapBytes is the maximum number of bytes of overlap allowed for a + // single output table with the tables in the grandparent level. + maxOverlapBytes uint64 + // disableSpanElision disables elision of range tombstones and range keys. Used + // by tests to allow range tombstones or range keys to be added to tables where + // they would otherwise be elided. + disableSpanElision bool + + // flushing contains the flushables (aka memtables) that are being flushed. + flushing flushableList + // bytesIterated contains the number of bytes that have been flushed/compacted. + bytesIterated uint64 + // bytesWritten contains the number of bytes that have been written to outputs. + bytesWritten int64 + + // The boundaries of the input data. + smallest InternalKey + largest InternalKey + + // The range deletion tombstone fragmenter. Adds range tombstones as they are + // returned from `compactionIter` and fragments them for output to files. + // Referenced by `compactionIter` which uses it to check whether keys are deleted. + rangeDelFrag keyspan.Fragmenter + // The range key fragmenter. Similar to rangeDelFrag in that it gets range + // keys from the compaction iter and fragments them for output to files. + rangeKeyFrag keyspan.Fragmenter + // The range deletion tombstone iterator, that merges and fragments + // tombstones across levels. This iterator is included within the compaction + // input iterator as a single level. + // TODO(jackson): Remove this when the refactor of FragmentIterator, + // InterleavingIterator, etc is complete. + rangeDelIter keyspan.InternalIteratorShim + // rangeKeyInterleaving is the interleaving iter for range keys. + rangeKeyInterleaving keyspan.InterleavingIter + + // A list of objects to close when the compaction finishes. Used by input + // iteration to keep rangeDelIters open for the lifetime of the compaction, + // and only close them when the compaction finishes. + closers []io.Closer + + // grandparents are the tables in level+2 that overlap with the files being + // compacted. Used to determine output table boundaries. Do not assume that the actual files + // in the grandparent when this compaction finishes will be the same. + grandparents manifest.LevelSlice + + // Boundaries at which flushes to L0 should be split. Determined by + // L0Sublevels. If nil, flushes aren't split. + l0Limits [][]byte + + // List of disjoint inuse key ranges the compaction overlaps with in + // grandparent and lower levels. See setupInuseKeyRanges() for the + // construction. Used by elideTombstone() and elideRangeTombstone() to + // determine if keys affected by a tombstone possibly exist at a lower level. + inuseKeyRanges []manifest.UserKeyRange + // inuseEntireRange is set if the above inuse key ranges wholly contain the + // compaction's key range. This allows compactions in higher levels to often + // elide key comparisons. + inuseEntireRange bool + elideTombstoneIndex int + + // allowedZeroSeqNum is true if seqnums can be zeroed if there are no + // snapshots requiring them to be kept. This determination is made by + // looking for an sstable which overlaps the bounds of the compaction at a + // lower level in the LSM during runCompaction. + allowedZeroSeqNum bool + + metrics map[int]*LevelMetrics + + pickerMetrics compactionPickerMetrics +} + +func (c *compaction) makeInfo(jobID int) CompactionInfo { + info := CompactionInfo{ + JobID: jobID, + Reason: c.kind.String(), + Input: make([]LevelInfo, 0, len(c.inputs)), + Annotations: []string{}, + } + for _, cl := range c.inputs { + inputInfo := LevelInfo{Level: cl.level, Tables: nil} + iter := cl.files.Iter() + for m := iter.First(); m != nil; m = iter.Next() { + inputInfo.Tables = append(inputInfo.Tables, m.TableInfo()) + } + info.Input = append(info.Input, inputInfo) + } + if c.outputLevel != nil { + info.Output.Level = c.outputLevel.level + + // If there are no inputs from the output level (eg, a move + // compaction), add an empty LevelInfo to info.Input. + if len(c.inputs) > 0 && c.inputs[len(c.inputs)-1].level != c.outputLevel.level { + info.Input = append(info.Input, LevelInfo{Level: c.outputLevel.level}) + } + } else { + // For a delete-only compaction, set the output level to L6. The + // output level is not meaningful here, but complicating the + // info.Output interface with a pointer doesn't seem worth the + // semantic distinction. + info.Output.Level = numLevels - 1 + } + + for i, score := range c.pickerMetrics.scores { + info.Input[i].Score = score + } + info.SingleLevelOverlappingRatio = c.pickerMetrics.singleLevelOverlappingRatio + info.MultiLevelOverlappingRatio = c.pickerMetrics.multiLevelOverlappingRatio + if len(info.Input) > 2 { + info.Annotations = append(info.Annotations, "multilevel") + } + return info +} + +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, + inputs: pc.inputs, + smallest: pc.smallest, + largest: pc.largest, + logger: opts.Logger, + version: pc.version, + beganAt: beganAt, + maxOutputFileSize: pc.maxOutputFileSize, + maxOverlapBytes: pc.maxOverlapBytes, + 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 { + c.extraLevels = pc.extraLevels + c.outputLevel = &c.inputs[len(c.inputs)-1] + } + // Compute the set of outputLevel+1 files that overlap this compaction (these + // are the grandparent sstables). + if c.outputLevel.level+1 < numLevels { + c.grandparents = c.version.Overlaps(c.outputLevel.level+1, c.cmp, + 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 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. + 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, beganAt time.Time, +) *compaction { + c := &compaction{ + kind: compactionKindDeleteOnly, + cmp: opts.Comparer.Compare, + equal: opts.equal(), + comparer: opts.Comparer, + formatKey: opts.Comparer.FormatKey, + logger: opts.Logger, + version: cur, + beganAt: beganAt, + inputs: inputs, + } + + // Set c.smallest, c.largest. + files := make([]manifest.LevelIterator, 0, len(inputs)) + for _, in := range inputs { + files = append(files, in.files.Iter()) + } + c.smallest, c.largest = manifest.KeyRange(opts.Comparer.Compare, files...) + return c +} + +func adjustGrandparentOverlapBytesForFlush(c *compaction, flushingBytes uint64) { + // Heuristic to place a lower bound on compaction output file size + // caused by Lbase. Prior to this heuristic we have observed an L0 in + // production with 310K files of which 290K files were < 10KB in size. + // Our hypothesis is that it was caused by L1 having 2600 files and + // ~10GB, such that each flush got split into many tiny files due to + // overlapping with most of the files in Lbase. + // + // The computation below is general in that it accounts + // for flushing different volumes of data (e.g. we may be flushing + // many memtables). For illustration, we consider the typical + // example of flushing a 64MB memtable. So 12.8MB output, + // based on the compression guess below. If the compressed bytes + // guess is an over-estimate we will end up with smaller files, + // and if an under-estimate we will end up with larger files. + // With a 2MB target file size, 7 files. We are willing to accept + // 4x the number of files, if it results in better write amplification + // when later compacting to Lbase, i.e., ~450KB files (target file + // size / 4). + // + // Note that this is a pessimistic heuristic in that + // fileCountUpperBoundDueToGrandparents could be far from the actual + // number of files produced due to the grandparent limits. For + // example, in the extreme, consider a flush that overlaps with 1000 + // files in Lbase f0...f999, and the initially calculated value of + // maxOverlapBytes will cause splits at f10, f20,..., f990, which + // means an upper bound file count of 100 files. Say the input bytes + // in the flush are such that acceptableFileCount=10. We will fatten + // up maxOverlapBytes by 10x to ensure that the upper bound file count + // drops to 10. However, it is possible that in practice, even without + // this change, we would have produced no more than 10 files, and that + // this change makes the files unnecessarily wide. Say the input bytes + // are distributed such that 10% are in f0...f9, 10% in f10...f19, ... + // 10% in f80...f89 and 10% in f990...f999. The original value of + // maxOverlapBytes would have actually produced only 10 sstables. But + // by increasing maxOverlapBytes by 10x, we may produce 1 sstable that + // spans f0...f89, i.e., a much wider sstable than necessary. + // + // We could produce a tighter estimate of + // fileCountUpperBoundDueToGrandparents if we had knowledge of the key + // distribution of the flush. The 4x multiplier mentioned earlier is + // a way to try to compensate for this pessimism. + // + // TODO(sumeer): we don't have compression info for the data being + // flushed, but it is likely that existing files that overlap with + // this flush in Lbase are representative wrt compression ratio. We + // could store the uncompressed size in FileMetadata and estimate + // the compression ratio. + const approxCompressionRatio = 0.2 + approxOutputBytes := approxCompressionRatio * float64(flushingBytes) + approxNumFilesBasedOnTargetSize := + int(math.Ceil(approxOutputBytes / float64(c.maxOutputFileSize))) + acceptableFileCount := float64(4 * approxNumFilesBasedOnTargetSize) + // The byte calculation is linear in numGrandparentFiles, but we will + // incur this linear cost in findGrandparentLimit too, so we are also + // willing to pay it now. We could approximate this cheaply by using + // the mean file size of Lbase. + grandparentFileBytes := c.grandparents.SizeSum() + fileCountUpperBoundDueToGrandparents := + float64(grandparentFileBytes) / float64(c.maxOverlapBytes) + if fileCountUpperBoundDueToGrandparents > acceptableFileCount { + c.maxOverlapBytes = uint64( + float64(c.maxOverlapBytes) * + (fileCountUpperBoundDueToGrandparents / acceptableFileCount)) + } +} + +func newFlush( + opts *Options, cur *version, baseLevel int, flushing flushableList, beganAt time.Time, +) *compaction { + c := &compaction{ + kind: compactionKindFlush, + cmp: opts.Comparer.Compare, + equal: opts.equal(), + comparer: opts.Comparer, + formatKey: opts.Comparer.FormatKey, + logger: opts.Logger, + version: cur, + beganAt: beganAt, + inputs: []compactionLevel{{level: -1}, {level: 0}}, + maxOutputFileSize: math.MaxUint64, + maxOverlapBytes: math.MaxUint64, + flushing: flushing, + } + c.startLevel = &c.inputs[0] + c.outputLevel = &c.inputs[1] + + if len(flushing) > 0 { + if _, ok := flushing[0].flushable.(*ingestedFlushable); ok { + if len(flushing) != 1 { + panic("pebble: ingestedFlushable must be flushed one at a time.") + } + c.kind = compactionKindIngestedFlushable + return c + } + } + + // Make sure there's no ingestedFlushable after the first flushable in the + // list. + for _, f := range flushing { + if _, ok := f.flushable.(*ingestedFlushable); ok { + panic("pebble: flushing shouldn't contain ingestedFlushable flushable") + } + } + + if cur.L0Sublevels != nil { + c.l0Limits = cur.L0Sublevels.FlushSplitKeys() + } + + smallestSet, largestSet := false, false + updatePointBounds := func(iter internalIterator) { + if key, _ := iter.First(); key != nil { + if !smallestSet || + base.InternalCompare(c.cmp, c.smallest, *key) > 0 { + smallestSet = true + c.smallest = key.Clone() + } + } + if key, _ := iter.Last(); key != nil { + if !largestSet || + base.InternalCompare(c.cmp, c.largest, *key) < 0 { + largestSet = true + c.largest = key.Clone() + } + } + } + + updateRangeBounds := func(iter keyspan.FragmentIterator) { + // File bounds require s != nil && !s.Empty(). We only need to check for + // s != nil here, as the memtable's FragmentIterator would never surface + // empty spans. + if s := iter.First(); s != nil { + if key := s.SmallestKey(); !smallestSet || + base.InternalCompare(c.cmp, c.smallest, key) > 0 { + smallestSet = true + c.smallest = key.Clone() + } + } + if s := iter.Last(); s != nil { + if key := s.LargestKey(); !largestSet || + base.InternalCompare(c.cmp, c.largest, key) < 0 { + largestSet = true + c.largest = key.Clone() + } + } + } + + var flushingBytes uint64 + for i := range flushing { + f := flushing[i] + updatePointBounds(f.newIter(nil)) + if rangeDelIter := f.newRangeDelIter(nil); rangeDelIter != nil { + updateRangeBounds(rangeDelIter) + } + if rangeKeyIter := f.newRangeKeyIter(nil); rangeKeyIter != nil { + updateRangeBounds(rangeKeyIter) + } + flushingBytes += f.inuseBytes() + } + + if opts.FlushSplitBytes > 0 { + c.maxOutputFileSize = uint64(opts.Level(0).TargetFileSize) + c.maxOverlapBytes = maxGrandparentOverlapBytes(opts, 0) + c.grandparents = c.version.Overlaps(baseLevel, c.cmp, c.smallest.UserKey, + c.largest.UserKey, c.largest.IsExclusiveSentinel()) + adjustGrandparentOverlapBytesForFlush(c, flushingBytes) + } + + c.setupInuseKeyRanges() + return c +} + +func (c *compaction) hasExtraLevelData() bool { + if len(c.extraLevels) == 0 { + // not a multi level compaction + return false + } else if c.extraLevels[0].files.Empty() { + // a multi level compaction without data in the intermediate input level; + // e.g. for a multi level compaction with levels 4,5, and 6, this could + // occur if there is no files to compact in 5, or in 5 and 6 (i.e. a move). + return false + } + return true +} + +func (c *compaction) setupInuseKeyRanges() { + level := c.outputLevel.level + 1 + if c.outputLevel.level == 0 { + level = 0 + } + // calculateInuseKeyRanges will return a series of sorted spans. Overlapping + // or abutting spans have already been merged. + c.inuseKeyRanges = calculateInuseKeyRanges( + c.version, c.cmp, level, numLevels-1, c.smallest.UserKey, c.largest.UserKey, + ) + // Check if there's a single in-use span that encompasses the entire key + // range of the compaction. This is an optimization to avoid key comparisons + // against inuseKeyRanges during the compaction when every key within the + // compaction overlaps with an in-use span. + if len(c.inuseKeyRanges) > 0 { + c.inuseEntireRange = c.cmp(c.inuseKeyRanges[0].Start, c.smallest.UserKey) <= 0 && + c.cmp(c.inuseKeyRanges[0].End, c.largest.UserKey) >= 0 + } +} + +func calculateInuseKeyRanges( + v *version, cmp base.Compare, level, maxLevel int, smallest, largest []byte, +) []manifest.UserKeyRange { + // Use two slices, alternating which one is input and which one is output + // as we descend the LSM. + var input, output []manifest.UserKeyRange + + // L0 requires special treatment, since sstables within L0 may overlap. + // We use the L0 Sublevels structure to efficiently calculate the merged + // in-use key ranges. + if level == 0 { + output = v.L0Sublevels.InUseKeyRanges(smallest, largest) + level++ + } + + for ; level <= maxLevel; level++ { + // NB: We always treat `largest` as inclusive for simplicity, because + // there's little consequence to calculating slightly broader in-use key + // ranges. + overlaps := v.Overlaps(level, cmp, smallest, largest, false /* exclusiveEnd */) + iter := overlaps.Iter() + + // We may already have in-use key ranges from higher levels. Iterate + // through both our accumulated in-use key ranges and this level's + // files, merging the two. + // + // Tables higher within the LSM have broader key spaces. We use this + // when possible to seek past a level's files that are contained by + // our current accumulated in-use key ranges. This helps avoid + // per-sstable work during flushes or compactions in high levels which + // overlap the majority of the LSM's sstables. + input, output = output, input + output = output[:0] + + var currFile *fileMetadata + var currAccum *manifest.UserKeyRange + if len(input) > 0 { + currAccum, input = &input[0], input[1:] + } + + // If we have an accumulated key range and its start is ≤ smallest, + // we can seek to the accumulated range's end. Otherwise, we need to + // start at the first overlapping file within the level. + if currAccum != nil && cmp(currAccum.Start, smallest) <= 0 { + currFile = seekGT(&iter, cmp, currAccum.End) + } else { + currFile = iter.First() + } + + for currFile != nil || currAccum != nil { + // If we've exhausted either the files in the level or the + // accumulated key ranges, we just need to append the one we have. + // If we have both a currFile and a currAccum, they either overlap + // or they're disjoint. If they're disjoint, we append whichever + // one sorts first and move on to the next file or range. If they + // overlap, we merge them into currAccum and proceed to the next + // file. + switch { + case currAccum == nil || (currFile != nil && cmp(currFile.Largest.UserKey, currAccum.Start) < 0): + // This file is strictly before the current accumulated range, + // or there are no more accumulated ranges. + output = append(output, manifest.UserKeyRange{ + Start: currFile.Smallest.UserKey, + End: currFile.Largest.UserKey, + }) + currFile = iter.Next() + case currFile == nil || (currAccum != nil && cmp(currAccum.End, currFile.Smallest.UserKey) < 0): + // The current accumulated key range is strictly before the + // current file, or there are no more files. + output = append(output, *currAccum) + currAccum = nil + if len(input) > 0 { + currAccum, input = &input[0], input[1:] + } + default: + // The current accumulated range and the current file overlap. + // Adjust the accumulated range to be the union. + if cmp(currFile.Smallest.UserKey, currAccum.Start) < 0 { + currAccum.Start = currFile.Smallest.UserKey + } + if cmp(currFile.Largest.UserKey, currAccum.End) > 0 { + currAccum.End = currFile.Largest.UserKey + } + + // Extending `currAccum`'s end boundary may have caused it to + // overlap with `input` key ranges that we haven't processed + // yet. Merge any such key ranges. + for len(input) > 0 && cmp(input[0].Start, currAccum.End) <= 0 { + if cmp(input[0].End, currAccum.End) > 0 { + currAccum.End = input[0].End + } + input = input[1:] + } + // Seek the level iterator past our current accumulated end. + currFile = seekGT(&iter, cmp, currAccum.End) + } + } + } + return output +} + +func seekGT(iter *manifest.LevelIterator, cmp base.Compare, key []byte) *manifest.FileMetadata { + f := iter.SeekGE(cmp, key) + for f != nil && cmp(f.Largest.UserKey, key) == 0 { + f = iter.Next() + } + return f +} + +// findGrandparentLimit takes the start user key for a table and returns the +// user key to which that table can extend without excessively overlapping +// the grandparent level. If no limit is needed considering the grandparent +// files, this function returns nil. This is done in order to prevent a table +// at level N from overlapping too much data at level N+1. We want to avoid +// such large overlaps because they translate into large compactions. The +// current heuristic stops output of a table if the addition of another key +// would cause the table to overlap more than 10x the target file size at +// level N. See maxGrandparentOverlapBytes. +func (c *compaction) findGrandparentLimit(start []byte) []byte { + iter := c.grandparents.Iter() + var overlappedBytes uint64 + var greater bool + for f := iter.SeekGE(c.cmp, start); f != nil; f = iter.Next() { + overlappedBytes += f.Size + // To ensure forward progress we always return a larger user + // key than where we started. See comments above clients of + // this function for how this is used. + greater = greater || c.cmp(f.Smallest.UserKey, start) > 0 + if !greater { + continue + } + + // We return the smallest bound of a sstable rather than the + // largest because the smallest is always inclusive, and limits + // are used exlusively when truncating range tombstones. If we + // truncated an output to the largest key while there's a + // pending tombstone, the next output file would also overlap + // the same grandparent f. + if overlappedBytes > c.maxOverlapBytes { + return f.Smallest.UserKey + } + } + return nil +} + +// findL0Limit takes the start key for a table and returns the user key to which +// that table can be extended without hitting the next l0Limit. Having flushed +// sstables "bridging across" an l0Limit could lead to increased L0 -> LBase +// compaction sizes as well as elevated read amplification. +func (c *compaction) findL0Limit(start []byte) []byte { + if c.startLevel.level > -1 || c.outputLevel.level != 0 || len(c.l0Limits) == 0 { + return nil + } + index := sort.Search(len(c.l0Limits), func(i int) bool { + return c.cmp(c.l0Limits[i], start) > 0 + }) + if index < len(c.l0Limits) { + return c.l0Limits[index] + } + return nil +} + +// errorOnUserKeyOverlap returns an error if the last two written sstables in +// this compaction have revisions of the same user key present in both sstables, +// when it shouldn't (eg. when splitting flushes). +func (c *compaction) errorOnUserKeyOverlap(ve *versionEdit) error { + if n := len(ve.NewFiles); n > 1 { + meta := ve.NewFiles[n-1].Meta + prevMeta := ve.NewFiles[n-2].Meta + if !prevMeta.Largest.IsExclusiveSentinel() && + c.cmp(prevMeta.Largest.UserKey, meta.Smallest.UserKey) >= 0 { + return errors.Errorf("pebble: compaction split user key across two sstables: %s in %s and %s", + prevMeta.Largest.Pretty(c.formatKey), + prevMeta.FileNum, + meta.FileNum) + } + } + return nil +} + +// allowZeroSeqNum returns true if seqnum's can be zeroed if there are no +// snapshots requiring them to be kept. It performs this determination by +// looking for an sstable which overlaps the bounds of the compaction at a +// lower level in the LSM. +func (c *compaction) allowZeroSeqNum() bool { + return c.elideRangeTombstone(c.smallest.UserKey, c.largest.UserKey) +} + +// elideTombstone returns true if it is ok to elide a tombstone for the +// specified key. A return value of true guarantees that there are no key/value +// pairs at c.level+2 or higher that possibly contain the specified user +// key. The keys in multiple invocations to elideTombstone must be supplied in +// order. +func (c *compaction) elideTombstone(key []byte) bool { + if c.inuseEntireRange || len(c.flushing) != 0 { + return false + } + + for ; c.elideTombstoneIndex < len(c.inuseKeyRanges); c.elideTombstoneIndex++ { + r := &c.inuseKeyRanges[c.elideTombstoneIndex] + if c.cmp(key, r.End) <= 0 { + if c.cmp(key, r.Start) >= 0 { + return false + } + break + } + } + return true +} + +// elideRangeTombstone returns true if it is ok to elide the specified range +// tombstone. A return value of true guarantees that there are no key/value +// pairs at c.outputLevel.level+1 or higher that possibly overlap the specified +// tombstone. +func (c *compaction) elideRangeTombstone(start, end []byte) bool { + // Disable range tombstone elision if the testing knob for that is enabled, + // or if we are flushing memtables. The latter requirement is due to + // inuseKeyRanges not accounting for key ranges in other memtables that are + // being flushed in the same compaction. It's possible for a range tombstone + // in one memtable to overlap keys in a preceding memtable in c.flushing. + // + // This function is also used in setting allowZeroSeqNum, so disabling + // elision of range tombstones also disables zeroing of SeqNums. + // + // TODO(peter): we disable zeroing of seqnums during flushing to match + // RocksDB behavior and to avoid generating overlapping sstables during + // DB.replayWAL. When replaying WAL files at startup, we flush after each + // WAL is replayed building up a single version edit that is + // applied. Because we don't apply the version edit after each flush, this + // code doesn't know that L0 contains files and zeroing of seqnums should + // be disabled. That is fixable, but it seems safer to just match the + // RocksDB behavior for now. + if c.disableSpanElision || len(c.flushing) != 0 { + return false + } + + lower := sort.Search(len(c.inuseKeyRanges), func(i int) bool { + return c.cmp(c.inuseKeyRanges[i].End, start) >= 0 + }) + upper := sort.Search(len(c.inuseKeyRanges), func(i int) bool { + return c.cmp(c.inuseKeyRanges[i].Start, end) > 0 + }) + return lower >= upper +} + +// elideRangeKey returns true if it is ok to elide the specified range key. A +// return value of true guarantees that there are no key/value pairs at +// c.outputLevel.level+1 or higher that possibly overlap the specified range key. +func (c *compaction) elideRangeKey(start, end []byte) bool { + // TODO(bilal): Track inuseKeyRanges separately for the range keyspace as + // opposed to the point keyspace. Once that is done, elideRangeTombstone + // can just check in the point keyspace, and this function can check for + // inuseKeyRanges in the range keyspace. + return c.elideRangeTombstone(start, end) +} + +// newInputIter returns an iterator over all the input tables in a compaction. +func (c *compaction) newInputIter( + newIters tableNewIters, newRangeKeyIter keyspan.TableNewSpanIter, snapshots []uint64, +) (_ internalIterator, retErr error) { + // 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 + } + } + err := manifest.CheckOrdering(c.cmp, c.formatKey, + manifest.Level(c.outputLevel.level), c.outputLevel.files.Iter(), + manifest.AllowSplitUserKeys) + if err != nil { + return nil, err + } + 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 + } + } + } + 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(), + manifest.AllowSplitUserKeys) + if err != nil { + return nil, err + } + } + } + + // 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 := len(c.flushing) + if numInputLevels < len(c.inputs) { + numInputLevels = 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 { + if iter != nil { + iter.Close() + } + } + for _, rangeDelIter := range rangeDelIters { + rangeDelIter.Close() + } + } + }() + iterOpts := IterOptions{ + CategoryAndQoS: sstable.CategoryAndQoS{ + Category: "pebble-compaction", + QoSLevel: sstable.NonLatencySensitiveQoSLevel, + }, + logger: c.logger, + } + + // 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) + } + if rangeKeyIter := f.newRangeKeyIter(nil); rangeKeyIter != nil { + rangeKeyIters = append(rangeKeyIters, rangeKeyIter) + } + } + } 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(context.Background(), + 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(), iterOpts, 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 + // lifetime of the compaction. + c.closers = append(c.closers, iter) + iter = noCloseIter{iter} + + // We do not need to truncate range keys to sstable boundaries, or + // only read within the file's atomic compaction units, unlike with + // 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 + } + li.Init(keyspan.SpanIterOptions{}, c.cmp, newRangeKeyIterWrapper, level.files.Iter(), l, manifest.KeyTypeRange) + rangeKeyIters = append(rangeKeyIters, li) + } + return 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 + } + } + } + + // 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. + // TODO(jackson): Replace the InternalIteratorShim with an interleaving + // iterator. + if len(rangeDelIters) > 0 { + c.rangeDelIter.Init(c.cmp, rangeDelIters...) + iters = append(iters, &c.rangeDelIter) + } + + // 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, iter, di, keyspan.InterleavingIterOpts{}) + iter = &c.rangeKeyInterleaving + } + return iter, nil +} + +func (c *compaction) newRangeDelIter( + newIters tableNewIters, + f manifest.LevelFile, + opts IterOptions, + l manifest.Level, + bytesIterated *uint64, +) (keyspan.FragmentIterator, io.Closer, error) { + opts.level = l + iter, rangeDelIter, err := newIters(context.Background(), f.FileMetadata, + &opts, 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 + } + + // 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 { + if len(c.flushing) != 0 { + return "flush\n" + } + + var buf bytes.Buffer + for level := c.startLevel.level; level <= c.outputLevel.level; level++ { + i := level - c.startLevel.level + fmt.Fprintf(&buf, "%d:", level) + iter := c.inputs[i].files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + fmt.Fprintf(&buf, " %s:%s-%s", f.FileNum, f.Smallest, f.Largest) + } + fmt.Fprintf(&buf, "\n") + } + return buf.String() +} + +type manualCompaction struct { + // Count of the retries either due to too many concurrent compactions, or a + // concurrent compaction to overlapping levels. + retries int + level int + outputLevel int + done chan error + start []byte + end []byte + split bool +} + +type readCompaction struct { + level int + // [start, end] key ranges are used for de-duping. + start []byte + end []byte + + // The file associated with the compaction. + // If the file no longer belongs in the same + // level, then we skip the compaction. + fileNum base.FileNum +} + +func (d *DB) addInProgressCompaction(c *compaction) { + d.mu.compact.inProgress[c] = struct{}{} + var isBase, isIntraL0 bool + for _, cl := range c.inputs { + iter := cl.files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if f.IsCompacting() { + d.opts.Logger.Fatalf("L%d->L%d: %s already being compacted", c.startLevel.level, c.outputLevel.level, f.FileNum) + } + f.SetCompactionState(manifest.CompactionStateCompacting) + if c.startLevel != nil && c.outputLevel != nil && c.startLevel.level == 0 { + if c.outputLevel.level == 0 { + f.IsIntraL0Compacting = true + isIntraL0 = true + } else { + isBase = true + } + } + } + } + + if (isIntraL0 || isBase) && c.version.L0Sublevels != nil { + l0Inputs := []manifest.LevelSlice{c.startLevel.files} + if isIntraL0 { + l0Inputs = append(l0Inputs, c.outputLevel.files) + } + if err := c.version.L0Sublevels.UpdateStateForStartedCompaction(l0Inputs, isBase); err != nil { + d.opts.Logger.Fatalf("could not update state for compaction: %s", err) + } + } +} + +// Removes compaction markers from files in a compaction. The rollback parameter +// indicates whether the compaction state should be rolled back to its original +// state in the case of an unsuccessful 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) 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() { + if !f.IsCompacting() { + d.opts.Logger.Fatalf("L%d->L%d: %s not being compacted", c.startLevel.level, c.outputLevel.level, f.FileNum) + } + if !rollback { + // On success all compactions other than move-compactions transition the + // file into the Compacted state. Move-compacted files become eligible + // for compaction again and transition back to NotCompacting. + if c.kind != compactionKindMove { + f.SetCompactionState(manifest.CompactionStateCompacted) + } else { + f.SetCompactionState(manifest.CompactionStateNotCompacting) + } + } else { + // Else, on rollback, all input files unconditionally transition back to + // NotCompacting. + f.SetCompactionState(manifest.CompactionStateNotCompacting) + } + f.IsIntraL0Compacting = false + } + } + l0InProgress := inProgressL0Compactions(d.getInProgressCompactionInfoLocked(c)) + func() { + // InitCompactingFileInfo requires that no other manifest writes be + // happening in parallel with it, i.e. we're not in the midst of installing + // another version. Otherwise, it's possible that we've created another + // L0Sublevels instance, but not added it to the versions list, causing + // all the indices in FileMetadata to be inaccurate. To ensure this, + // grab the manifest lock. + d.mu.versions.logLock() + defer d.mu.versions.logUnlock() + d.mu.versions.currentVersion().L0Sublevels.InitCompactingFileInfo(l0InProgress) + }() +} + +func (d *DB) calculateDiskAvailableBytes() uint64 { + if space, err := d.opts.FS.GetDiskUsage(d.dirname); err == nil { + d.diskAvailBytes.Store(space.AvailBytes) + return space.AvailBytes + } else if !errors.Is(err, vfs.ErrUnsupported) { + d.opts.EventListener.BackgroundError(err) + } + return d.diskAvailBytes.Load() +} + +func (d *DB) getDeletionPacerInfo() deletionPacerInfo { + var pacerInfo deletionPacerInfo + // Call GetDiskUsage after every file deletion. This may seem inefficient, + // but in practice this was observed to take constant time, regardless of + // volume size used, at least on linux with ext4 and zfs. All invocations + // take 10 microseconds or less. + pacerInfo.freeBytes = d.calculateDiskAvailableBytes() + d.mu.Lock() + pacerInfo.obsoleteBytes = d.mu.versions.metrics.Table.ObsoleteSize + pacerInfo.liveBytes = uint64(d.mu.versions.metrics.Total().Size) + d.mu.Unlock() + 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. +func (d *DB) maybeScheduleFlush() { + if d.mu.compact.flushing || d.closed.Load() != nil || d.opts.ReadOnly { + return + } + if len(d.mu.mem.queue) <= 1 { + return + } + + if !d.passedFlushThreshold() { + return + } + + d.mu.compact.flushing = true + go d.flush() +} + +func (d *DB) passedFlushThreshold() bool { + var n int + var size uint64 + for ; n < len(d.mu.mem.queue)-1; n++ { + if !d.mu.mem.queue[n].readyForFlush() { + break + } + if d.mu.mem.queue[n].flushForced { + // A flush was forced. Pretend the memtable size is the configured + // size. See minFlushSize below. + size += d.opts.MemTableSize + } else { + size += d.mu.mem.queue[n].totalBytes() + } + } + if n == 0 { + // None of the immutable memtables are ready for flushing. + return false + } + + // Only flush once the sum of the queued memtable sizes exceeds half the + // 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 := d.opts.MemTableSize / 2 + return size >= minFlushSize +} + +func (d *DB) maybeScheduleDelayedFlush(tbl *memTable, dur time.Duration) { + var mem *flushableEntry + for _, m := range d.mu.mem.queue { + if m.flushable == tbl { + mem = m + break + } + } + if mem == nil || mem.flushForced { + return + } + deadline := d.timeNow().Add(dur) + if !mem.delayedFlushForcedAt.IsZero() && deadline.After(mem.delayedFlushForcedAt) { + // Already scheduled to flush sooner than within `dur`. + return + } + mem.delayedFlushForcedAt = deadline + go func() { + timer := time.NewTimer(dur) + defer timer.Stop() + + select { + case <-d.closedCh: + return + case <-mem.flushed: + return + case <-timer.C: + d.commit.mu.Lock() + defer d.commit.mu.Unlock() + d.mu.Lock() + defer d.mu.Unlock() + + // NB: The timer may fire concurrently with a call to Close. If a + // Close call beat us to acquiring d.mu, d.closed holds ErrClosed, + // and it's too late to flush anything. Otherwise, the Close call + // will block on locking d.mu until we've finished scheduling the + // flush and set `d.mu.compact.flushing` to true. Close will wait + // for the current flush to complete. + if d.closed.Load() != nil { + return + } + + if d.mu.mem.mutable == tbl { + d.makeRoomForWrite(nil) + } else { + mem.flushForced = true + } + d.maybeScheduleFlush() + } + }() +} + +func (d *DB) flush() { + pprof.Do(context.Background(), flushLabels, func(context.Context) { + flushingWorkStart := time.Now() + d.mu.Lock() + defer d.mu.Unlock() + idleDuration := flushingWorkStart.Sub(d.mu.compact.noOngoingFlushStartTime) + var bytesFlushed uint64 + var err error + if bytesFlushed, err = d.flush1(); err != nil { + // TODO(peter): count consecutive flush errors and backoff. + d.opts.EventListener.BackgroundError(err) + } + d.mu.compact.flushing = false + d.mu.compact.noOngoingFlushStartTime = time.Now() + workDuration := d.mu.compact.noOngoingFlushStartTime.Sub(flushingWorkStart) + d.mu.compact.flushWriteThroughput.Bytes += int64(bytesFlushed) + d.mu.compact.flushWriteThroughput.WorkDuration += workDuration + d.mu.compact.flushWriteThroughput.IdleDuration += idleDuration + // More flush work may have arrived while we were flushing, so schedule + // another flush if needed. + d.maybeScheduleFlush() + // The flush may have produced too many files in a level, so schedule a + // compaction if needed. + d.maybeScheduleCompaction() + d.mu.compact.cond.Broadcast() + }) +} + +// runIngestFlush is used to generate a flush version edit for sstables which +// were ingested as flushables. Both DB.mu and the manifest lock must be held +// while runIngestFlush is called. +func (d *DB) runIngestFlush(c *compaction) (*manifest.VersionEdit, error) { + if len(c.flushing) != 1 { + panic("pebble: ingestedFlushable must be flushed one at a time.") + } + + // Construct the VersionEdit, levelMetrics etc. + c.metrics = make(map[int]*LevelMetrics, numLevels) + // Finding the target level for ingestion must use the latest version + // after the logLock has been acquired. + c.version = d.mu.versions.currentVersion() + + baseLevel := d.mu.versions.picker.getBaseLevel() + iterOpts := IterOptions{logger: d.opts.Logger} + ve := &versionEdit{} + var level int + var err error + var fileToSplit *fileMetadata + var ingestSplitFiles []ingestSplitFile + for _, file := range c.flushing[0].flushable.(*ingestedFlushable).files { + 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.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.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 +} + +// flush runs a compaction that copies the immutable memtables from memory to +// disk. +// +// 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) flush1() (bytesFlushed uint64, err error) { + // NB: The flushable queue can contain flushables of type ingestedFlushable. + // The sstables in ingestedFlushable.files must be placed into the appropriate + // level in the lsm. Let's say the flushable queue contains a prefix of + // regular immutable memtables, then an ingestedFlushable, and then the + // mutable memtable. When the flush of the ingestedFlushable is performed, + // it needs an updated view of the lsm. That is, the prefix of immutable + // memtables must have already been flushed. Similarly, if there are two + // contiguous ingestedFlushables in the queue, then the first flushable must + // be flushed, so that the second flushable can see an updated view of the + // lsm. + // + // Given the above, we restrict flushes to either some prefix of regular + // memtables, or a single flushable of type ingestedFlushable. The DB.flush + // function will call DB.maybeScheduleFlush again, so a new flush to finish + // the remaining flush work should be scheduled right away. + // + // NB: Large batches placed in the flushable queue share the WAL with the + // previous memtable in the queue. We must ensure the property that both the + // large batch and the memtable with which it shares a WAL are flushed + // together. The property ensures that the minimum unflushed log number + // isn't incremented incorrectly. Since a flushableBatch.readyToFlush always + // returns true, and since the large batch will always be placed right after + // the memtable with which it shares a WAL, the property is naturally + // ensured. The large batch will always be placed after the memtable with + // which it shares a WAL because we ensure it in DB.commitWrite by holding + // the commitPipeline.mu and then holding DB.mu. As an extra defensive + // measure, if we try to flush the memtable without also flushing the + // 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 { + if n == 0 { + // The first flushable is of type ingestedFlushable. Since these + // must be flushed individually, we perform a flush for just + // this. + if !f.readyForFlush() { + // This check is almost unnecessary, but we guard against it + // just in case this invariant changes in the future. + panic("pebble: ingestedFlushable should always be ready to flush.") + } + // By setting n = 1, we ensure that the first flushable(n == 0) + // is scheduled for a flush. The number of tables added is equal to the + // number of files in the ingest operation. + n = 1 + inputs = len(f.files) + ingest = true + break + } else { + // There was some prefix of flushables which weren't of type + // ingestedFlushable. So, perform a flush for those. + break + } + } + 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. + return 0, nil + } + if !ingest { + // Flushes of memtables add the prefix of n memtables from the flushable + // queue. + inputs = n + } + + // Require that every memtable being flushed has a log number less than the + // new minimum unflushed log number. + minUnflushedLogNum := d.mu.mem.queue[n].logNum + if !d.opts.DisableWAL { + for i := 0; i < n; i++ { + if logNum := d.mu.mem.queue[i].logNum; logNum >= minUnflushedLogNum { + panic(errors.AssertionFailedf("logNum invariant violated: flushing %d items; %d:type=%T,logNum=%d; %d:type=%T,logNum=%d", + n, + i, d.mu.mem.queue[i].flushable, logNum, + n, d.mu.mem.queue[n].flushable, minUnflushedLogNum)) + } + } + } + + c := newFlush(d.opts, d.mu.versions.currentVersion(), + 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, + InputBytes: inputBytes, + Ingest: ingest, + }) + startTime := d.timeNow() + + var ve *manifest.VersionEdit + 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, + // we need to acquire the logLock below to perform the logAndApply step + // anyway, we create the VersionEdit for ingestedFlushable outside of + // runCompaction. For all other flush cases, we construct the VersionEdit + // inside runCompaction. + if c.kind != compactionKindIngestedFlushable { + ve, pendingOutputs, stats, err = d.runCompaction(jobID, c) + } + + // Acquire logLock. This will be released either on an error, by way of + // logUnlock, or through a call to logAndApply if there is no error. + d.mu.versions.logLock() + + if c.kind == compactionKindIngestedFlushable { + ve, err = d.runIngestFlush(c) + } + + info := FlushInfo{ + 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 { + e := &ve.NewFiles[i] + info.Output = append(info.Output, e.Meta.TableInfo()) + // Ingested tables are not necessarily flushed to L0. Record the level of + // each ingested file explicitly. + if ingest { + info.IngestLevels = append(info.IngestLevels, e.Level) + } + } + if len(ve.NewFiles) == 0 { + info.Err = errEmptyTable + } + + // The flush succeeded or it produced an empty sstable. In either case we + // want to bump the minimum unflushed log number to the log number of the + // oldest unflushed memtable. + ve.MinUnflushedLogNum = minUnflushedLogNum + if c.kind != compactionKindIngestedFlushable { + metrics := c.metrics[0] + if d.opts.DisableWAL { + // If the WAL is disabled, every flushable has a zero [logSize], + // resulting in zero bytes in. Instead, use the number of bytes we + // flushed as the BytesIn. This ensures we get a reasonable w-amp + // calculation even when the WAL is disabled. + metrics.BytesIn = metrics.BytesFlushed + } else { + metrics := c.metrics[0] + for i := 0; i < n; i++ { + 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. + 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 { + // We won't be performing the logAndApply step because of the error, + // so logUnlock. + d.mu.versions.logUnlock() + } + + bytesFlushed = c.bytesIterated + + // 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 { + flushed = d.mu.mem.queue[:n] + d.mu.mem.queue = d.mu.mem.queue[n:] + d.updateReadStateLocked(d.opts.DebugCheck) + d.updateTableStatsLocked(ve.NewFiles) + if ingest { + d.mu.versions.metrics.Flush.AsIngestCount++ + for _, l := range c.metrics { + d.mu.versions.metrics.Flush.AsIngestBytes += l.BytesIngested + 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 + // IterOptions.OnlyReadGuaranteedDurable. + info.TotalDuration = d.timeNow().Sub(startTime) + d.opts.EventListener.FlushEnd(info) + + // The order of these operations matters here for ease of testing. + // Removing the reader reference first allows tests to be guaranteed that + // the memtable reservation has been released by the time a synchronous + // flush returns. readerUnrefLocked may also produce obsolete files so the + // call to deleteObsoleteFiles must happen after it. + for i := range flushed { + flushed[i].readerUnrefLocked(true) + } + + d.deleteObsoleteFiles(jobID) + + // Mark all the memtables we flushed as flushed. + for i := range flushed { + close(flushed[i].flushed) + } + + return bytesFlushed, err +} + +// maybeScheduleCompactionAsync should be used when +// we want to possibly schedule a compaction, but don't +// want to eat the cost of running maybeScheduleCompaction. +// This method should be launched in a separate goroutine. +// d.mu must not be held when this is called. +func (d *DB) maybeScheduleCompactionAsync() { + defer d.compactionSchedulers.Done() + + d.mu.Lock() + d.maybeScheduleCompaction() + d.mu.Unlock() +} + +// maybeScheduleCompaction schedules a compaction if necessary. +// +// d.mu must be held when calling this. +func (d *DB) maybeScheduleCompaction() { + d.maybeScheduleCompactionPicker(pickAuto) +} + +func pickAuto(picker compactionPicker, env compactionEnv) *pickedCompaction { + return picker.pickAuto(env) +} + +func pickElisionOnly(picker compactionPicker, env compactionEnv) *pickedCompaction { + return picker.pickElisionOnlyCompaction(env) +} + +// maybeScheduleCompactionPicker schedules a compaction if necessary, +// calling `pickFunc` to pick automatic compactions. +// +// d.mu must be held when calling this. +func (d *DB) maybeScheduleCompactionPicker( + pickFunc func(compactionPicker, compactionEnv) *pickedCompaction, +) { + if d.closed.Load() != nil || d.opts.ReadOnly { + return + } + maxConcurrentCompactions := d.opts.MaxConcurrentCompactions() + if d.mu.compact.compactingCount >= maxConcurrentCompactions { + if len(d.mu.compact.manual) > 0 { + // Inability to run head blocks later manual compactions. + d.mu.compact.manual[0].retries++ + } + return + } + + // Compaction picking needs a coherent view of a Version. In particular, we + // need to exlude concurrent ingestions from making a decision on which level + // to ingest into that conflicts with our compaction + // decision. versionSet.logLock provides the necessary mutual exclusion. + d.mu.versions.logLock() + defer d.mu.versions.logUnlock() + + // Check for the closed flag again, in case the DB was closed while we were + // waiting for logLock(). + if d.closed.Load() != nil { + return + } + + env := compactionEnv{ + diskAvailBytes: d.diskAvailBytes.Load(), + earliestSnapshotSeqNum: d.mu.snapshots.earliest(), + earliestUnflushedSeqNum: d.getEarliestUnflushedSeqNumLocked(), + } + + // Check for delete-only compactions first, because they're expected to be + // cheap and reduce future compaction work. + if !d.opts.private.disableDeleteOnlyCompactions && + len(d.mu.compact.deletionHints) > 0 && + !d.opts.DisableAutomaticCompactions { + v := d.mu.versions.currentVersion() + snapshots := d.mu.snapshots.toSlice() + inputs, unresolvedHints := checkDeleteCompactionHints(d.cmp, v, d.mu.compact.deletionHints, snapshots) + d.mu.compact.deletionHints = unresolvedHints + + if len(inputs) > 0 { + c := newDeleteOnlyCompaction(d.opts, v, inputs, d.timeNow()) + d.mu.compact.compactingCount++ + d.addInProgressCompaction(c) + go d.compact(c, nil) + } + } + + 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 := pickManualCompaction(v, d.opts, env, d.mu.versions.picker.getBaseLevel(), manual) + if pc != nil { + 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) + go d.compact(c, manual.done) + } else if !retryLater { + // Noop + d.mu.compact.manual = d.mu.compact.manual[1:] + manual.done <- nil + } else { + // Inability to run head blocks later manual compactions. + manual.retries++ + break + } + } + + for !d.opts.DisableAutomaticCompactions && d.mu.compact.compactingCount < maxConcurrentCompactions { + env.inProgressCompactions = d.getInProgressCompactionInfoLocked(nil) + env.readCompactionEnv = readCompactionEnv{ + readCompactions: &d.mu.compact.readCompactions, + flushing: d.mu.compact.flushing || d.passedFlushThreshold(), + rescheduleReadCompaction: &d.mu.compact.rescheduleReadCompaction, + } + pc := pickFunc(d.mu.versions.picker, env) + if pc == nil { + break + } + c := newCompaction(pc, d.opts, d.timeNow(), d.ObjProvider()) + d.mu.compact.compactingCount++ + d.addInProgressCompaction(c) + go d.compact(c, nil) + } +} + +// deleteCompactionHintType indicates whether the deleteCompactionHint was +// generated from a span containing a range del (point key only), a range key +// delete (range key only), or both a point and range key. +type deleteCompactionHintType uint8 + +const ( + // NOTE: While these are primarily used as enumeration types, they are also + // used for some bitwise operations. Care should be taken when updating. + deleteCompactionHintTypeUnknown deleteCompactionHintType = iota + deleteCompactionHintTypePointKeyOnly + deleteCompactionHintTypeRangeKeyOnly + deleteCompactionHintTypePointAndRangeKey +) + +// String implements fmt.Stringer. +func (h deleteCompactionHintType) String() string { + switch h { + case deleteCompactionHintTypeUnknown: + return "unknown" + case deleteCompactionHintTypePointKeyOnly: + return "point-key-only" + case deleteCompactionHintTypeRangeKeyOnly: + return "range-key-only" + case deleteCompactionHintTypePointAndRangeKey: + return "point-and-range-key" + default: + panic(fmt.Sprintf("unknown hint type: %d", h)) + } +} + +// compactionHintFromKeys returns a deleteCompactionHintType given a slice of +// keyspan.Keys. +func compactionHintFromKeys(keys []keyspan.Key) deleteCompactionHintType { + var hintType deleteCompactionHintType + for _, k := range keys { + switch k.Kind() { + case base.InternalKeyKindRangeDelete: + hintType |= deleteCompactionHintTypePointKeyOnly + case base.InternalKeyKindRangeKeyDelete: + hintType |= deleteCompactionHintTypeRangeKeyOnly + default: + panic(fmt.Sprintf("unsupported key kind: %s", k.Kind())) + } + } + return hintType +} + +// A deleteCompactionHint records a user key and sequence number span that has been +// deleted by a range tombstone. A hint is recorded if at least one sstable +// falls completely within both the user key and sequence number spans. +// Once the tombstones and the observed completely-contained sstables fall +// into the same snapshot stripe, a delete-only compaction may delete any +// sstables within the range. +type deleteCompactionHint struct { + // The type of key span that generated this hint (point key, range key, or + // both). + hintType deleteCompactionHintType + // start and end are user keys specifying a key range [start, end) of + // deleted keys. + start []byte + end []byte + // The level of the file containing the range tombstone(s) when the hint + // was created. Only lower levels need to be searched for files that may + // be deleted. + tombstoneLevel int + // The file containing the range tombstone(s) that created the hint. + tombstoneFile *fileMetadata + // The smallest and largest sequence numbers of the abutting tombstones + // merged to form this hint. All of a tables' keys must be less than the + // tombstone smallest sequence number to be deleted. All of a tables' + // sequence numbers must fall into the same snapshot stripe as the + // tombstone largest sequence number to be deleted. + tombstoneLargestSeqNum uint64 + tombstoneSmallestSeqNum uint64 + // The smallest sequence number of a sstable that was found to be covered + // by this hint. The hint cannot be resolved until this sequence number is + // in the same snapshot stripe as the largest tombstone sequence number. + // This is set when a hint is created, so the LSM may look different and + // notably no longer contain the sstable that contained the key at this + // sequence number. + fileSmallestSeqNum uint64 +} + +func (h deleteCompactionHint) String() string { + return fmt.Sprintf( + "L%d.%s %s-%s seqnums(tombstone=%d-%d, file-smallest=%d, type=%s)", + h.tombstoneLevel, h.tombstoneFile.FileNum, h.start, h.end, + h.tombstoneSmallestSeqNum, h.tombstoneLargestSeqNum, h.fileSmallestSeqNum, + h.hintType, + ) +} + +func (h *deleteCompactionHint) canDelete(cmp Compare, m *fileMetadata, snapshots []uint64) bool { + // The file can only be deleted if all of its keys are older than the + // earliest tombstone aggregated into the hint. + if m.LargestSeqNum >= h.tombstoneSmallestSeqNum || m.SmallestSeqNum < h.fileSmallestSeqNum { + return false + } + + // The file's oldest key must be in the same snapshot stripe as the + // newest tombstone. NB: We already checked the hint's sequence numbers, + // but this file's oldest sequence number might be lower than the hint's + // smallest sequence number despite the file falling within the key range + // if this file was constructed after the hint by a compaction. + ti, _ := snapshotIndex(h.tombstoneLargestSeqNum, snapshots) + fi, _ := snapshotIndex(m.SmallestSeqNum, snapshots) + if ti != fi { + return false + } + + switch h.hintType { + case deleteCompactionHintTypePointKeyOnly: + // A hint generated by a range del span cannot delete tables that contain + // range keys. + if m.HasRangeKeys { + return false + } + case deleteCompactionHintTypeRangeKeyOnly: + // A hint generated by a range key del span cannot delete tables that + // contain point keys. + if m.HasPointKeys { + return false + } + case deleteCompactionHintTypePointAndRangeKey: + // A hint from a span that contains both range dels *and* range keys can + // only be deleted if both bounds fall within the hint. The next check takes + // care of this. + default: + panic(fmt.Sprintf("pebble: unknown delete compaction hint type: %d", h.hintType)) + } + + // The file's keys must be completely contained within the hint range. + return cmp(h.start, m.Smallest.UserKey) <= 0 && cmp(m.Largest.UserKey, h.end) < 0 +} + +func (d *DB) maybeUpdateDeleteCompactionHints(c *compaction) { + // Compactions that zero sequence numbers can interfere with compaction + // deletion hints. Deletion hints apply to tables containing keys older + // than a threshold. If a key more recent than the threshold is zeroed in + // a compaction, a delete-only compaction may mistake it as meeting the + // threshold and drop a table containing live data. + // + // To avoid this scenario, compactions that zero sequence numbers remove + // any conflicting deletion hints. A deletion hint is conflicting if both + // of the following conditions apply: + // * its key space overlaps with the compaction + // * at least one of its inputs contains a key as recent as one of the + // hint's tombstones. + // + if !c.allowedZeroSeqNum { + return + } + + updatedHints := d.mu.compact.deletionHints[:0] + for _, h := range d.mu.compact.deletionHints { + // If the compaction's key space is disjoint from the hint's key + // space, the zeroing of sequence numbers won't affect the hint. Keep + // the hint. + keysDisjoint := d.cmp(h.end, c.smallest.UserKey) < 0 || d.cmp(h.start, c.largest.UserKey) > 0 + if keysDisjoint { + updatedHints = append(updatedHints, h) + continue + } + + // All of the compaction's inputs must be older than the hint's + // tombstones. + inputsOlder := true + for _, in := range c.inputs { + iter := in.files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + inputsOlder = inputsOlder && f.LargestSeqNum < h.tombstoneSmallestSeqNum + } + } + if inputsOlder { + updatedHints = append(updatedHints, h) + continue + } + + // Drop h, because the compaction c may have zeroed sequence numbers + // of keys more recent than some of h's tombstones. + } + d.mu.compact.deletionHints = updatedHints +} + +func checkDeleteCompactionHints( + cmp Compare, v *version, hints []deleteCompactionHint, snapshots []uint64, +) ([]compactionLevel, []deleteCompactionHint) { + var files map[*fileMetadata]bool + var byLevel [numLevels][]*fileMetadata + + unresolvedHints := hints[:0] + for _, h := range hints { + // Check each compaction hint to see if it's resolvable. Resolvable + // hints are removed and trigger a delete-only compaction if any files + // in the current LSM still meet their criteria. Unresolvable hints + // are saved and don't trigger a delete-only compaction. + // + // When a compaction hint is created, the sequence numbers of the + // range tombstones and the covered file with the oldest key are + // recorded. The largest tombstone sequence number and the smallest + // file sequence number must be in the same snapshot stripe for the + // hint to be resolved. The below graphic models a compaction hint + // covering the keyspace [b, r). The hint completely contains two + // files, 000002 and 000003. The file 000003 contains the lowest + // covered sequence number at #90. The tombstone b.RANGEDEL.230:h has + // the highest tombstone sequence number incorporated into the hint. + // The hint may be resolved only once the snapshots at #100, #180 and + // #210 are all closed. File 000001 is not included within the hint + // because it extends beyond the range tombstones in user key space. + // + // 250 + // + // |-b...230:h-| + // _____________________________________________________ snapshot #210 + // 200 |--h.RANGEDEL.200:r--| + // + // _____________________________________________________ snapshot #180 + // + // 150 +--------+ + // +---------+ | 000003 | + // | 000002 | | | + // +_________+ | | + // 100_____________________|________|___________________ snapshot #100 + // +--------+ + // _____________________________________________________ snapshot #70 + // +---------------+ + // 50 | 000001 | + // | | + // +---------------+ + // ______________________________________________________________ + // a b c d e f g h i j k l m n o p q r s t u v w x y z + + ti, _ := snapshotIndex(h.tombstoneLargestSeqNum, snapshots) + fi, _ := snapshotIndex(h.fileSmallestSeqNum, snapshots) + if ti != fi { + // Cannot resolve yet. + unresolvedHints = append(unresolvedHints, h) + continue + } + + // The hint h will be resolved and dropped, regardless of whether + // there are any tables that can be deleted. + for l := h.tombstoneLevel + 1; l < numLevels; l++ { + overlaps := v.Overlaps(l, cmp, h.start, h.end, true /* exclusiveEnd */) + iter := overlaps.Iter() + for m := iter.First(); m != nil; m = iter.Next() { + if m.IsCompacting() || !h.canDelete(cmp, m, snapshots) || files[m] { + continue + } + if files == nil { + // Construct files lazily, assuming most calls will not + // produce delete-only compactions. + files = make(map[*fileMetadata]bool) + } + files[m] = true + byLevel[l] = append(byLevel[l], m) + } + } + } + + var compactLevels []compactionLevel + for l, files := range byLevel { + if len(files) == 0 { + continue + } + compactLevels = append(compactLevels, compactionLevel{ + level: l, + files: manifest.NewLevelSliceKeySorted(cmp, files), + }) + } + return compactLevels, unresolvedHints +} + +// compact runs one compaction and maybe schedules another call to compact. +func (d *DB) compact(c *compaction, errChannel chan error) { + pprof.Do(context.Background(), compactLabels, func(context.Context) { + d.mu.Lock() + defer d.mu.Unlock() + if err := d.compact1(c, errChannel); err != nil { + // TODO(peter): count consecutive compaction errors and backoff. + 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() + d.mu.compact.cond.Broadcast() + }) +} + +// compact1 runs one compaction. +// +// 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) compact1(c *compaction, errChannel chan error) (err error) { + if errChannel != nil { + defer func() { + errChannel <- err + }() + } + + jobID := d.mu.nextJobID + d.mu.nextJobID++ + info := c.makeInfo(jobID) + d.opts.EventListener.CompactionBegin(info) + startTime := d.timeNow() + + ve, pendingOutputs, stats, err := d.runCompaction(jobID, c) + + info.Duration = d.timeNow().Sub(startTime) + if err == nil { + 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. + 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() + } + } + + info.Done = true + info.Err = err + if err == nil { + for i := range ve.NewFiles { + 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) + } + + // 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(c.beganAt) + d.opts.EventListener.CompactionEnd(info) + + // Update the read state before deleting obsolete files because the + // read-state update will cause the previous version to be unref'd and if + // there are no references obsolete tables will be added to the obsolete + // table list. + if err == nil { + d.updateReadStateLocked(d.opts.DebugCheck) + d.updateTableStatsLocked(ve.NewFiles) + } + d.deleteObsoleteFiles(jobID) + + return err +} + +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()) + // 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. + vers := d.mu.versions.currentVersion() + vers.Ref() + defer vers.UnrefLocked() + + 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 +// memtables or old on-disk tables. +// +// 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) runCompaction( + jobID int, c *compaction, +) (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. + defer func() { + // If we're handling a panic, don't expect the version edit to validate. + if r := recover(); r != nil { + panic(r) + } else if ve != nil { + err := validateVersionEdit(ve, d.opts.Experimental.KeyValidationFunc, d.opts.Comparer.FormatKey) + if err != nil { + d.opts.Logger.Fatalf("pebble: version edit validation failed: %s", err) + } + } + }() + + // Check for a delete-only compaction. This can occur when wide range + // tombstones completely contain sstables. + if c.kind == compactionKindDeleteOnly { + c.metrics = make(map[int]*LevelMetrics, len(c.inputs)) + ve := &versionEdit{ + DeletedFiles: map[deletedFileEntry]*fileMetadata{}, + } + for _, cl := range c.inputs { + levelMetrics := &LevelMetrics{} + iter := cl.files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + ve.DeletedFiles[deletedFileEntry{ + Level: cl.level, + FileNum: f.FileNum, + }] = f + } + c.metrics[cl.level] = levelMetrics + } + return ve, nil, stats, nil + } + + if c.kind == compactionKindIngestedFlushable { + panic("pebble: runCompaction cannot handle compactionKindIngestedFlushable.") + } + + // 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 || 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") + } + } + if c.cancel.Load() { + return ve, nil, stats, ErrCancelledCompaction + } + objMeta, err := d.objProvider.Lookup(fileTypeTable, meta.FileBacking.DiskFileNum) + if err != nil { + return ve, pendingOutputs, stats, err + } + c.metrics = map[int]*LevelMetrics{ + c.outputLevel.level: { + BytesMoved: meta.Size, + TablesMoved: 1, + }, + } + ve := &versionEdit{ + DeletedFiles: map[deletedFileEntry]*fileMetadata{ + {Level: c.startLevel.level, FileNum: meta.FileNum}: meta, + }, + NewFiles: []newFileEntry{ + {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 + } + + defer func() { + if retErr != nil { + pendingOutputs = nil + } + }() + + 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()) + + var ( + createdFiles []base.DiskFileNum + tw *sstable.Writer + pinnedKeySize uint64 + pinnedValueSize uint64 + pinnedCount uint64 + ) + defer func() { + if iter != nil { + retErr = firstError(retErr, iter.Close()) + } + if tw != nil { + retErr = firstError(retErr, tw.Close()) + } + if retErr != nil { + for _, fileNum := range createdFiles { + _ = d.objProvider.Remove(fileTypeTable, fileNum) + } + } + for _, closer := range c.closers { + retErr = firstError(retErr, closer.Close()) + } + }() + + ve = &versionEdit{ + DeletedFiles: map[deletedFileEntry]*fileMetadata{}, + } + + startLevelBytes := c.startLevel.files.SizeSum() + outputMetrics := &LevelMetrics{ + BytesIn: startLevelBytes, + BytesRead: c.outputLevel.files.SizeSum(), + } + if len(c.extraLevels) > 0 { + outputMetrics.BytesIn += c.extraLevels[0].files.SizeSum() + } + outputMetrics.BytesRead += outputMetrics.BytesIn + + c.metrics = map[int]*LevelMetrics{ + c.outputLevel.level: outputMetrics, + } + if len(c.flushing) == 0 && c.metrics[c.startLevel.level] == nil { + c.metrics[c.startLevel.level] = &LevelMetrics{} + } + 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() + + // 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. + writerOpts.BlockPropertyCollectors = nil + } + + // prevPointKey is a sstable.WriterOption that provides access to + // the last point key written to a writer's sstable. When a new + // output begins in newOutput, prevPointKey is updated to point to + // the new output's sstable.Writer. This allows the compaction loop + // to access the last written point key without requiring the + // compaction loop to make a copy of each key ahead of time. Users + // must be careful, because the byte slice returned by UnsafeKey + // points directly into the Writer's block buffer. + var prevPointKey sstable.PreviousPointKeyOpt + var cpuWorkHandle CPUWorkHandle + defer func() { + if cpuWorkHandle != nil { + d.opts.Experimental.CPUWorkPermissionGranter.CPUWorkDone(cpuWorkHandle) + } + }() + + 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.PhysicalMeta()) + d.mu.Unlock() + + 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 + } + + reason := "flushing" + if c.flushing == nil { + reason = "compacting" + } + d.opts.EventListener.TableCreated(TableCreateInfo{ + JobID: jobID, + Reason: reason, + Path: d.objProvider.Path(objMeta), + FileNum: fileNum, + }) + if c.kind != compactionKindFlush { + writable = &compactionWritable{ + Writable: writable, + versions: d.mu.versions, + written: &c.bytesWritten, + } + } + 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( + MaxFileWriteAdditionalCPUTime, + ) + writerOpts.Parallelism = + d.opts.Experimental.MaxWriterConcurrency > 0 && + (cpuWorkHandle.Permitted() || d.opts.Experimental.ForceWriterParallelism) + + tw = sstable.NewWriter(writable, writerOpts, cacheOpts, &prevPointKey) + + fileMeta.CreationTime = time.Now().Unix() + ve.NewFiles = append(ve.NewFiles, newFileEntry{ + Level: c.outputLevel.level, + Meta: fileMeta, + }) + return nil + } + + // splitL0Outputs is true during flushes and intra-L0 compactions with flush + // splits enabled. + splitL0Outputs := c.outputLevel.level == 0 && d.opts.FlushSplitBytes > 0 + + // finishOutput is called with the a user key up to which all tombstones + // should be flushed. Typically, this is the first key of the next + // sstable or an empty key if this output is the final sstable. + finishOutput := func(splitKey []byte) error { + // If we haven't output any point records to the sstable (tw == nil) then the + // sstable will only contain range tombstones and/or range keys. The smallest + // key in the sstable will be the start key of the first range tombstone or + // range key added. We need to ensure that this start key is distinct from + // the splitKey passed to finishOutput (if set), otherwise we would generate + // an sstable where the largest key is smaller than the smallest key due to + // how the largest key boundary is set below. NB: It is permissible for the + // range tombstone / range key start key to be the empty string. + // + // TODO: It is unfortunate that we have to do this check here rather than + // when we decide to finish the sstable in the runCompaction loop. A better + // structure currently eludes us. + if tw == nil { + startKey := c.rangeDelFrag.Start() + if len(iter.tombstones) > 0 { + startKey = iter.tombstones[0].Start + } + if startKey == nil { + startKey = c.rangeKeyFrag.Start() + if len(iter.rangeKeys) > 0 { + startKey = iter.rangeKeys[0].Start + } + } + if splitKey != nil && d.cmp(startKey, splitKey) == 0 { + return nil + } + } + + // NB: clone the key because the data can be held on to by the call to + // compactionIter.Tombstones via keyspan.Fragmenter.FlushTo, and by the + // WriterMetadata.LargestRangeDel.UserKey. + splitKey = append([]byte(nil), splitKey...) + for _, v := range iter.Tombstones(splitKey) { + if tw == nil { + if err := newOutput(); err != nil { + return err + } + } + // The tombstone being added could be completely outside the + // eventual bounds of the sstable. Consider this example (bounds + // in square brackets next to table filename): + // + // ./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 + // + // Note that both of the top two SSTables have range tombstones + // that start after the file's end keys. Since the file bound + // computation happens well after all range tombstones have been + // added to the writer, eliding out-of-file range tombstones based + // on sequence number at this stage is difficult, and necessitates + // read-time logic to ignore range tombstones outside file bounds. + if err := rangedel.Encode(&v, tw.Add); err != nil { + return err + } + } + for _, v := range iter.RangeKeys(splitKey) { + // Same logic as for range tombstones, except added using tw.AddRangeKey. + if tw == nil { + if err := newOutput(); err != nil { + return err + } + } + if err := rangekey.Encode(&v, tw.AddRangeKey); err != nil { + return err + } + } + + if tw == nil { + return nil + } + { + // Set internal sstable properties. + p := getInternalWriterProperties(tw) + // Set the external sst version to 0. This is what RocksDB expects for + // db-internal sstables; otherwise, it could apply a global sequence number. + p.ExternalFormatVersion = 0 + // Set the snapshot pinned totals. + p.SnapshotPinnedKeys = pinnedCount + p.SnapshotPinnedKeySize = pinnedKeySize + p.SnapshotPinnedValueSize = pinnedValueSize + stats.cumulativePinnedKeys += pinnedCount + stats.cumulativePinnedSize += pinnedKeySize + pinnedValueSize + pinnedCount = 0 + pinnedKeySize = 0 + pinnedValueSize = 0 + } + if err := tw.Close(); err != nil { + tw = nil + return err + } + d.opts.Experimental.CPUWorkPermissionGranter.CPUWorkDone(cpuWorkHandle) + cpuWorkHandle = nil + writerMeta, err := tw.Metadata() + if err != nil { + tw = nil + return err + } + tw = nil + meta := ve.NewFiles[len(ve.NewFiles)-1].Meta + 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.PhysicalMeta(), &writerMeta.Properties, + ) + + if c.flushing == nil { + outputMetrics.TablesCompacted++ + outputMetrics.BytesCompacted += meta.Size + } else { + outputMetrics.TablesFlushed++ + outputMetrics.BytesFlushed += meta.Size + } + outputMetrics.Size += int64(meta.Size) + outputMetrics.NumFiles++ + outputMetrics.Additional.BytesWrittenDataBlocks += writerMeta.Properties.DataSize + outputMetrics.Additional.BytesWrittenValueBlocks += writerMeta.Properties.ValueBlocksSize + + if n := len(ve.NewFiles); n > 1 { + // This is not the first output file. Ensure the sstable boundaries + // are nonoverlapping. + prevMeta := ve.NewFiles[n-2].Meta + if writerMeta.SmallestRangeDel.UserKey != nil { + c := d.cmp(writerMeta.SmallestRangeDel.UserKey, prevMeta.Largest.UserKey) + if c < 0 { + return errors.Errorf( + "pebble: smallest range tombstone start key is less than previous sstable largest key: %s < %s", + writerMeta.SmallestRangeDel.Pretty(d.opts.Comparer.FormatKey), + prevMeta.Largest.Pretty(d.opts.Comparer.FormatKey)) + } else if c == 0 && !prevMeta.Largest.IsExclusiveSentinel() { + // The user key portion of the range boundary start key is + // equal to the previous table's largest key user key, and + // the previous table's largest key is not exclusive. This + // violates the invariant that tables are key-space + // partitioned. + return errors.Errorf( + "pebble: invariant violation: previous sstable largest key %s, current sstable smallest rangedel: %s", + prevMeta.Largest.Pretty(d.opts.Comparer.FormatKey), + writerMeta.SmallestRangeDel.Pretty(d.opts.Comparer.FormatKey), + ) + } + } + } + + // Verify that all range deletions outputted to the sstable are + // truncated to split key. + if splitKey != nil && writerMeta.LargestRangeDel.UserKey != nil && + d.cmp(writerMeta.LargestRangeDel.UserKey, splitKey) > 0 { + return errors.Errorf( + "pebble: invariant violation: rangedel largest key %q extends beyond split key %q", + writerMeta.LargestRangeDel.Pretty(d.opts.Comparer.FormatKey), + d.opts.Comparer.FormatKey(splitKey), + ) + } + + if writerMeta.HasPointKeys { + meta.ExtendPointKeyBounds(d.cmp, writerMeta.SmallestPoint, writerMeta.LargestPoint) + } + if writerMeta.HasRangeDelKeys { + meta.ExtendPointKeyBounds(d.cmp, writerMeta.SmallestRangeDel, writerMeta.LargestRangeDel) + } + if writerMeta.HasRangeKeys { + meta.ExtendRangeKeyBounds(d.cmp, writerMeta.SmallestRangeKey, writerMeta.LargestRangeKey) + } + + // Verify that the sstable bounds fall within the compaction input + // bounds. This is a sanity check that we don't have a logic error + // elsewhere that causes the sstable bounds to accidentally expand past the + // compaction input bounds as doing so could lead to various badness such + // as keys being deleted by a range tombstone incorrectly. + if c.smallest.UserKey != nil { + switch v := d.cmp(meta.Smallest.UserKey, c.smallest.UserKey); { + case v >= 0: + // Nothing to do. + case v < 0: + return errors.Errorf("pebble: compaction output grew beyond bounds of input: %s < %s", + meta.Smallest.Pretty(d.opts.Comparer.FormatKey), + c.smallest.Pretty(d.opts.Comparer.FormatKey)) + } + } + if c.largest.UserKey != nil { + switch v := d.cmp(meta.Largest.UserKey, c.largest.UserKey); { + case v <= 0: + // Nothing to do. + case v > 0: + return errors.Errorf("pebble: compaction output grew beyond bounds of input: %s > %s", + meta.Largest.Pretty(d.opts.Comparer.FormatKey), + c.largest.Pretty(d.opts.Comparer.FormatKey)) + } + } + // Verify that we never split different revisions of the same user key + // across two different sstables. + if err := c.errorOnUserKeyOverlap(ve); err != nil { + return err + } + if err := meta.Validate(d.cmp, d.opts.Comparer.FormatKey); err != nil { + return err + } + return nil + } + + // Build a compactionOutputSplitter that contains all logic to determine + // whether the compaction loop should stop writing to one output sstable and + // switch to a new one. Some splitters can wrap other splitters, and the + // splitterGroup can be composed of multiple splitters. In this case, we + // start off with splitters for file sizes, grandparent limits, and (for L0 + // splits) L0 limits, before wrapping them in an splitterGroup. + sizeSplitter := newFileSizeSplitter(&iter.frontiers, c.maxOutputFileSize, c.grandparents.Iter()) + unsafePrevUserKey := func() []byte { + // Return the largest point key written to tw or the start of + // the current range deletion in the fragmenter, whichever is + // greater. + prevPoint := prevPointKey.UnsafeKey() + if c.cmp(prevPoint.UserKey, c.rangeDelFrag.Start()) > 0 { + return prevPoint.UserKey + } + return c.rangeDelFrag.Start() + } + outputSplitters := []compactionOutputSplitter{ + // We do not split the same user key across different sstables within + // one flush or compaction. The fileSizeSplitter may request a split in + // the middle of a user key, so the userKeyChangeSplitter ensures we are + // at a user key change boundary when doing a split. + &userKeyChangeSplitter{ + cmp: c.cmp, + splitter: sizeSplitter, + unsafePrevUserKey: unsafePrevUserKey, + }, + newLimitFuncSplitter(&iter.frontiers, c.findGrandparentLimit), + } + if splitL0Outputs { + outputSplitters = append(outputSplitters, newLimitFuncSplitter(&iter.frontiers, c.findL0Limit)) + } + splitter := &splitterGroup{cmp: c.cmp, splitters: outputSplitters} + + // Each outer loop iteration produces one output file. An iteration that + // produces a file containing point keys (and optionally range tombstones) + // guarantees that the input iterator advanced. An iteration that produces + // a file containing only range tombstones guarantees the limit passed to + // `finishOutput()` advanced to a strictly greater user key corresponding + // to a grandparent file largest key, or nil. Taken together, these + // progress guarantees ensure that eventually the input iterator will be + // exhausted and the range tombstone fragments will all be flushed. + for key, val := iter.First(); key != nil || !c.rangeDelFrag.Empty() || !c.rangeKeyFrag.Empty(); { + var firstKey []byte + if key != nil { + firstKey = key.UserKey + } else if startKey := c.rangeDelFrag.Start(); startKey != nil { + // Pass the start key of the first pending tombstone to find the + // next limit. All pending tombstones have the same start key. We + // use this as opposed to the end key of the last written sstable to + // effectively handle cases like these: + // + // a.SET.3 + // (lf.limit at b) + // d.RANGEDEL.4:f + // + // In this case, the partition after b has only range deletions, so + // if we were to find the limit after the last written key at the + // split point (key a), we'd get the limit b again, and + // finishOutput() would not advance any further because the next + // range tombstone to write does not start until after the L0 split + // point. + firstKey = startKey + } + splitterSuggestion := splitter.onNewOutput(firstKey) + + // Each inner loop iteration processes one key from the input iterator. + for ; key != nil; key, val = iter.Next() { + if split := splitter.shouldSplitBefore(key, tw); split == splitNow { + break + } + + switch key.Kind() { + case InternalKeyKindRangeDelete: + // Range tombstones are handled specially. They are fragmented, + // and they're not written until later during `finishOutput()`. + // We add them to the `Fragmenter` now to make them visible to + // `compactionIter` so covered keys in the same snapshot stripe + // can be elided. + + // The interleaved range deletion might only be one of many with + // these bounds. Some fragmenting is performed ahead of time by + // keyspan.MergingIter. + if s := c.rangeDelIter.Span(); !s.Empty() { + // The memory management here is subtle. Range deletions + // blocks do NOT use prefix compression, which ensures that + // range deletion spans' memory is available as long we keep + // the iterator open. However, the keyspan.MergingIter that + // merges spans across levels only guarantees the lifetime + // of the [start, end) bounds until the next positioning + // method is called. + // + // Additionally, the Span.Keys slice is owned by the the + // range deletion iterator stack, and it may be overwritten + // when we advance. + // + // Clone the Keys slice and the start and end keys. + // + // TODO(jackson): Avoid the clone by removing c.rangeDelFrag + // and performing explicit truncation of the pending + // rangedel span as necessary. + clone := keyspan.Span{ + Start: iter.cloneKey(s.Start), + End: iter.cloneKey(s.End), + Keys: make([]keyspan.Key, len(s.Keys)), + } + copy(clone.Keys, s.Keys) + c.rangeDelFrag.Add(clone) + } + continue + case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + // Range keys are handled in the same way as range tombstones, except + // with a dedicated fragmenter. + if s := c.rangeKeyInterleaving.Span(); !s.Empty() { + clone := keyspan.Span{ + Start: iter.cloneKey(s.Start), + End: iter.cloneKey(s.End), + Keys: make([]keyspan.Key, len(s.Keys)), + } + // Since the keys' Suffix and Value fields are not deep cloned, the + // underlying blockIter must be kept open for the lifetime of the + // compaction. + copy(clone.Keys, s.Keys) + c.rangeKeyFrag.Add(clone) + } + continue + } + if tw == nil { + if err := newOutput(); err != nil { + return nil, pendingOutputs, stats, err + } + } + if err := tw.AddWithForceObsolete(*key, val, iter.forceObsoleteDueToRangeDel); err != nil { + return nil, pendingOutputs, stats, err + } + if iter.snapshotPinned { + // The kv pair we just added to the sstable was only surfaced by + // the compaction iterator because an open snapshot prevented + // its elision. Increment the stats. + pinnedCount++ + pinnedKeySize += uint64(len(key.UserKey)) + base.InternalTrailerLen + pinnedValueSize += uint64(len(val)) + } + } + + // A splitter requested a split, and we're ready to finish the output. + // We need to choose the key at which to split any pending range + // tombstones. There are two options: + // 1. splitterSuggestion — The key suggested by the splitter. This key + // is guaranteed to be greater than the last key written to the + // current output. + // 2. key.UserKey — the first key of the next sstable output. This user + // key is also guaranteed to be greater than the last user key + // written to the current output (see userKeyChangeSplitter). + // + // Use whichever is smaller. Using the smaller of the two limits + // overlap with grandparents. Consider the case where the + // grandparent limit is calculated to be 'b', key is 'x', and + // there exist many sstables between 'b' and 'x'. If the range + // deletion fragmenter has a pending tombstone [a,x), splitting + // at 'x' would cause the output table to overlap many + // grandparents well beyond the calculated grandparent limit + // 'b'. Splitting at the smaller `splitterSuggestion` avoids + // this unbounded overlap with grandparent tables. + splitKey := splitterSuggestion + if key != nil && (splitKey == nil || c.cmp(splitKey, key.UserKey) > 0) { + splitKey = key.UserKey + } + if err := finishOutput(splitKey); err != nil { + return nil, pendingOutputs, stats, err + } + } + + for _, cl := range c.inputs { + iter := cl.files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + ve.DeletedFiles[deletedFileEntry{ + Level: cl.level, + FileNum: f.FileNum, + }] = f + } + } + + // 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 + } + + // Refresh the disk available statistic whenever a compaction/flush + // completes, before re-acquiring the mutex. + _ = d.calculateDiskAvailableBytes() + + return ve, pendingOutputs, stats, nil +} + +// validateVersionEdit validates that start and end keys across new and deleted +// files in a versionEdit pass the given validation function. +func validateVersionEdit( + ve *versionEdit, validateFn func([]byte) error, format base.FormatKey, +) error { + validateMetaFn := func(f *manifest.FileMetadata) error { + for _, key := range []InternalKey{f.Smallest, f.Largest} { + if err := validateFn(key.UserKey); err != nil { + return errors.Wrapf(err, "key=%q; file=%s", format(key.UserKey), f) + } + } + return nil + } + + // Validate both new and deleted files. + for _, f := range ve.NewFiles { + if err := validateMetaFn(f.Meta); err != nil { + return err + } + } + for _, m := range ve.DeletedFiles { + if err := validateMetaFn(m); err != nil { + return err + } + } + + return nil +} + +// scanObsoleteFiles scans the filesystem for files that are no longer needed +// and adds those to the internal lists of obsolete files. Note that the files +// are not actually deleted by this method. A subsequent call to +// deleteObsoleteFiles must be performed. Must be not be called concurrently +// with compactions and flushes. db.mu must be held when calling this function. +func (d *DB) scanObsoleteFiles(list []string) { + // Disable automatic compactions temporarily to avoid concurrent compactions / + // flushes from interfering. The original value is restored on completion. + disabledPrev := d.opts.DisableAutomaticCompactions + defer func() { + d.opts.DisableAutomaticCompactions = disabledPrev + }() + d.opts.DisableAutomaticCompactions = true + + // Wait for any ongoing compaction to complete before continuing. + for d.mu.compact.compactingCount > 0 || d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + + 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 + // during read only mode and aren't part of the Version. Note that if + // !d.opts.ReadOnly, then all flushables of type ingestedFlushable have + // already been flushed. + for _, fEntry := range d.mu.mem.queue { + if f, ok := fEntry.flushable.(*ingestedFlushable); ok { + for _, file := range f.files { + liveFileNums[file.FileBacking.DiskFileNum] = struct{}{} + } + } + } + + minUnflushedLogNum := d.mu.versions.minUnflushedLogNum + manifestFileNum := d.mu.versions.manifestFileNum + + var obsoleteLogs []fileInfo + var obsoleteTables []fileInfo + var obsoleteManifests []fileInfo + var obsoleteOptions []fileInfo + + for _, filename := range list { + fileType, diskFileNum, ok := base.ParseFilename(d.opts.FS, filename) + if !ok { + continue + } + switch fileType { + case fileTypeLog: + if diskFileNum >= minUnflushedLogNum { + continue + } + 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 diskFileNum >= manifestFileNum { + continue + } + 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 diskFileNum.FileNum() >= d.optionsFileNum.FileNum() { + continue + } + fi := fileInfo{fileNum: diskFileNum} + if stat, err := d.opts.FS.Stat(filename); err == nil { + fi.fileSize = uint64(stat.Size()) + } + obsoleteOptions = append(obsoleteOptions, fi) + case fileTypeTable: + // Objects are handled through the objstorage provider below. + default: + // Don't delete files we don't know about. + } + } + + objects := d.objProvider.List() + for _, obj := range objects { + switch obj.FileType { + case fileTypeTable: + if _, ok := liveFileNums[obj.DiskFileNum]; ok { + continue + } + fileInfo := fileInfo{ + fileNum: obj.DiskFileNum, + } + if size, err := d.objProvider.Size(obj); err == nil { + fileInfo.fileSize = uint64(size) + } + obsoleteTables = append(obsoleteTables, fileInfo) + + default: + // Ignore object types we don't know about. + } + } + + 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 = merge(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) +} + +// disableFileDeletions disables file deletions and then waits for any +// in-progress deletion to finish. The caller is required to call +// enableFileDeletions in order to enable file deletions again. It is ok for +// multiple callers to disable file deletions simultaneously, though they must +// all invoke enableFileDeletions in order for file deletions to be re-enabled +// (there is an internal reference count on file deletion disablement). +// +// d.mu must be held when calling this method. +func (d *DB) disableFileDeletions() { + d.mu.disableFileDeletions++ + d.mu.Unlock() + defer d.mu.Lock() + d.cleanupManager.Wait() +} + +// 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.disableFileDeletions <= 0 { + panic("pebble: file deletion disablement invariant violated") + } + d.mu.disableFileDeletions-- + if d.mu.disableFileDeletions > 0 { + return + } + jobID := d.mu.nextJobID + d.mu.nextJobID++ + d.deleteObsoleteFiles(jobID) +} + +type fileInfo struct { + fileNum base.DiskFileNum + fileSize uint64 +} + +// 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 { + // NB: d.mu.versions.minUnflushedLogNum is the log number of the earliest + // 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 { + obsoleteLogs = d.mu.log.queue[:i] + d.mu.log.queue = d.mu.log.queue[i:] + d.mu.versions.metrics.WAL.Files -= int64(len(obsoleteLogs)) + break + } + } + + 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. + slices.SortFunc(d.mu.versions.obsoleteManifests, func(a, b fileInfo) int { + return cmp.Compare(a.fileNum, b.fileNum) + }) + + var obsoleteManifests []fileInfo + manifestsToDelete := len(d.mu.versions.obsoleteManifests) - d.opts.NumPrevManifest + if manifestsToDelete > 0 { + obsoleteManifests = d.mu.versions.obsoleteManifests[:manifestsToDelete] + d.mu.versions.obsoleteManifests = d.mu.versions.obsoleteManifests[manifestsToDelete:] + if len(d.mu.versions.obsoleteManifests) == 0 { + d.mu.versions.obsoleteManifests = nil + } + } + + obsoleteOptions := d.mu.versions.obsoleteOptions + d.mu.versions.obsoleteOptions = nil + + // 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() + + files := [4]struct { + fileType fileType + obsolete []fileInfo + }{ + {fileTypeLog, obsoleteLogs}, + {fileTypeTable, obsoleteTables}, + {fileTypeManifest, obsoleteManifests}, + {fileTypeOptions, obsoleteOptions}, + } + _, noRecycle := d.opts.Cleaner.(base.NeedsFileContents) + 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. + slices.SortFunc(f.obsolete, func(a, b fileInfo) int { + return cmp.Compare(a.fileNum, b.fileNum) + }) + for _, fi := range f.obsolete { + dir := d.dirname + switch f.fileType { + case fileTypeLog: + if !noRecycle && d.logRecycler.add(fi) { + continue + } + dir = d.walDirname + case fileTypeTable: + d.tableCache.evict(fi.fileNum) + } + + filesToDelete = append(filesToDelete, obsoleteFile{ + dir: dir, + fileNum: fi.fileNum, + fileType: f.fileType, + fileSize: fi.fileSize, + }) + } + } + if len(filesToDelete) > 0 { + d.cleanupManager.EnqueueJob(jobID, filesToDelete) + } + if d.opts.private.testingAlwaysWaitForCleanup { + d.cleanupManager.Wait() + } +} + +func (d *DB) maybeScheduleObsoleteTableDeletion() { + d.mu.Lock() + defer d.mu.Unlock() + d.maybeScheduleObsoleteTableDeletionLocked() +} + +func (d *DB) maybeScheduleObsoleteTableDeletionLocked() { + if len(d.mu.versions.obsoleteTables) > 0 { + jobID := d.mu.nextJobID + d.mu.nextJobID++ + d.deleteObsoleteFiles(jobID) + } +} + +func merge(a, b []fileInfo) []fileInfo { + if len(b) == 0 { + return a + } + + a = append(a, b...) + slices.SortFunc(a, func(a, b fileInfo) int { + return cmp.Compare(a.fileNum, b.fileNum) + }) + return slices.CompactFunc(a, func(a, b fileInfo) bool { + return a.fileNum == b.fileNum + }) +} diff --git a/pebble/compaction_iter.go b/pebble/compaction_iter.go new file mode 100644 index 0000000..0fb9e45 --- /dev/null +++ b/pebble/compaction_iter.go @@ -0,0 +1,1473 @@ +// Copyright 2018 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 ( + "bytes" + "encoding/binary" + "fmt" + "io" + "sort" + "strconv" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/bytealloc" + "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 +// for collapsing entries during compaction. It wraps an internal iterator and +// collapses entries that are no longer necessary because they are shadowed by +// newer entries. The simplest example of this is when the internal iterator +// contains two keys: a.PUT.2 and a.PUT.1. Instead of returning both entries, +// compactionIter collapses the second entry because it is no longer +// necessary. The high-level structure for compactionIter is to iterate over +// its internal iterator and output 1 entry for every user-key. There are four +// complications to this story. +// +// 1. Eliding Deletion Tombstones +// +// Consider the entries a.DEL.2 and a.PUT.1. These entries collapse to +// a.DEL.2. Do we have to output the entry a.DEL.2? Only if a.DEL.2 possibly +// shadows an entry at a lower level. If we're compacting to the base-level in +// the LSM tree then a.DEL.2 is definitely not shadowing an entry at a lower +// level and can be elided. +// +// We can do slightly better than only eliding deletion tombstones at the base +// level by observing that we can elide a deletion tombstone if there are no +// sstables that contain the entry's key. This check is performed by +// elideTombstone. +// +// 2. Merges +// +// The MERGE operation merges the value for an entry with the existing value +// for an entry. The logical value of an entry can be composed of a series of +// merge operations. When compactionIter sees a MERGE, it scans forward in its +// internal iterator collapsing MERGE operations for the same key until it +// encounters a SET or DELETE operation. For example, the keys a.MERGE.4, +// a.MERGE.3, a.MERGE.2 will be collapsed to a.MERGE.4 and the values will be +// merged using the specified Merger. +// +// An interesting case here occurs when MERGE is combined with SET. Consider +// the entries a.MERGE.3 and a.SET.2. The collapsed key will be a.SET.3. The +// reason that the kind is changed to SET is because the SET operation acts as +// a barrier preventing further merging. This can be seen better in the +// scenario a.MERGE.3, a.SET.2, a.MERGE.1. The entry a.MERGE.1 may be at lower +// (older) level and not involved in the compaction. If the compaction of +// a.MERGE.3 and a.SET.2 produced a.MERGE.3, a subsequent compaction with +// a.MERGE.1 would merge the values together incorrectly. +// +// 3. Snapshots +// +// Snapshots are lightweight point-in-time views of the DB state. At its core, +// a snapshot is a sequence number along with a guarantee from Pebble that it +// will maintain the view of the database at that sequence number. Part of this +// guarantee is relatively straightforward to achieve. When reading from the +// database Pebble will ignore sequence numbers that are larger than the +// snapshot sequence number. The primary complexity with snapshots occurs +// during compaction: the collapsing of entries that are shadowed by newer +// entries is at odds with the guarantee that Pebble will maintain the view of +// the database at the snapshot sequence number. Rather than collapsing entries +// up to the next user key, compactionIter can only collapse entries up to the +// next snapshot boundary. That is, every snapshot boundary potentially causes +// another entry for the same user-key to be emitted. Another way to view this +// is that snapshots define stripes and entries are collapsed within stripes, +// but not across stripes. Consider the following scenario: +// +// a.PUT.9 +// a.DEL.8 +// a.PUT.7 +// a.DEL.6 +// a.PUT.5 +// +// In the absence of snapshots these entries would be collapsed to +// a.PUT.9. What if there is a snapshot at sequence number 7? The entries can +// be divided into two stripes and collapsed within the stripes: +// +// a.PUT.9 a.PUT.9 +// a.DEL.8 ---> +// a.PUT.7 +// -- -- +// a.DEL.6 ---> a.DEL.6 +// a.PUT.5 +// +// All of the rules described earlier still apply, but they are confined to +// operate within a snapshot stripe. Snapshots only affect compaction when the +// snapshot sequence number lies within the range of sequence numbers being +// compacted. In the above example, a snapshot at sequence number 10 or at +// sequence number 5 would not have any effect. +// +// 4. Range Deletions +// +// Range deletions provide the ability to delete all of the keys (and values) +// in a contiguous range. Range deletions are stored indexed by their start +// key. The end key of the range is stored in the value. In order to support +// lookup of the range deletions which overlap with a particular key, the range +// deletion tombstones need to be fragmented whenever they overlap. This +// fragmentation is performed by keyspan.Fragmenter. The fragments are then +// subject to the rules for snapshots. For example, consider the two range +// tombstones [a,e)#1 and [c,g)#2: +// +// 2: c-------g +// 1: a-------e +// +// These tombstones will be fragmented into: +// +// 2: c---e---g +// 1: a---c---e +// +// Do we output the fragment [c,e)#1? Since it is covered by [c-e]#2 the answer +// depends on whether it is in a new snapshot stripe. +// +// In addition to the fragmentation of range tombstones, compaction also needs +// to take the range tombstones into consideration when outputting normal +// keys. Just as with point deletions, a range deletion covering an entry can +// cause the entry to be elided. +// +// A note on the stability of keys and values. +// +// The stability guarantees of keys and values returned by the iterator tree +// that backs a compactionIter is nuanced and care must be taken when +// referencing any returned items. +// +// Keys and values returned by exported functions (i.e. First, Next, etc.) have +// lifetimes that fall into two categories: +// +// Lifetime valid for duration of compaction. Range deletion keys and values are +// stable for the duration of the compaction, due to way in which a +// compactionIter is typically constructed (i.e. via (*compaction).newInputIter, +// which wraps the iterator over the range deletion block in a noCloseIter, +// preventing the release of the backing memory until the compaction is +// finished). +// +// Lifetime limited to duration of sstable block liveness. Point keys (SET, DEL, +// etc.) and values must be cloned / copied following the return from the +// exported function, and before a subsequent call to Next advances the iterator +// and mutates the contents of the returned key and value. +type compactionIter struct { + equal Equal + merge Merge + iter internalIterator + err error + // `key.UserKey` is set to `keyBuf` caused by saving `i.iterKey.UserKey` + // and `key.Trailer` is set to `i.iterKey.Trailer`. This is the + // case on return from all public methods -- these methods return `key`. + // Additionally, it is the internal state when the code is moving to the + // next key so it can determine whether the user key has changed from + // the previous key. + key InternalKey + // keyTrailer is updated when `i.key` is updated and holds the key's + // original trailer (eg, before any sequence-number zeroing or changes to + // key kind). + keyTrailer uint64 + value []byte + valueCloser io.Closer + // Temporary buffer used for storing the previous user key in order to + // determine when iteration has advanced to a new user key and thus a new + // snapshot stripe. + keyBuf []byte + // Temporary buffer used for storing the previous value, which may be an + // unsafe, i.iter-owned slice that could be altered when the iterator is + // advanced. + valueBuf []byte + // Is the current entry valid? + valid bool + iterKey *InternalKey + iterValue []byte + iterStripeChange stripeChangeType + // `skip` indicates whether the remaining skippable entries in the current + // snapshot stripe should be skipped or processed. An example of a non- + // skippable entry is a range tombstone as we need to return it from the + // `compactionIter`, even if a key covering its start key has already been + // seen in the same stripe. `skip` has no effect when `pos == iterPosNext`. + // + // TODO(jackson): If we use keyspan.InterleavingIter for range deletions, + // like we do for range keys, the only remaining 'non-skippable' key is + // the invalid key. We should be able to simplify this logic and remove this + // field. + skip bool + // `pos` indicates the iterator position at the top of `Next()`. Its type's + // (`iterPos`) values take on the following meanings in the context of + // `compactionIter`. + // + // - `iterPosCur`: the iterator is at the last key returned. + // - `iterPosNext`: the iterator has already been advanced to the next + // candidate key. For example, this happens when processing merge operands, + // where we advance the iterator all the way into the next stripe or next + // user key to ensure we've seen all mergeable operands. + // - `iterPosPrev`: this is invalid as compactionIter is forward-only. + pos iterPos + // `snapshotPinned` indicates whether the last point key returned by the + // 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 + // The snapshot sequence numbers that need to be maintained. These sequence + // numbers define the snapshot stripes (see the Snapshots description + // above). The sequence numbers are in ascending order. + snapshots []uint64 + // frontiers holds a heap of user keys that affect compaction behavior when + // they're exceeded. Before a new key is returned, the compaction iterator + // advances the frontier, notifying any code that subscribed to be notified + // when a key was reached. The primary use today is within the + // implementation of compactionOutputSplitters in compaction.go. Many of + // these splitters wait for the compaction iterator to call Advance(k) when + // it's returning a new key. If the key that they're waiting for is + // surpassed, these splitters update internal state recording that they + // should request a compaction split next time they're asked in + // [shouldSplitBefore]. + frontiers frontiers + // Reference to the range deletion tombstone fragmenter (e.g., + // `compaction.rangeDelFrag`). + rangeDelFrag *keyspan.Fragmenter + rangeKeyFrag *keyspan.Fragmenter + // The fragmented tombstones. + tombstones []keyspan.Span + // 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 + // 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( + cmp Compare, + equal Equal, + formatKey base.FormatKey, + merge Merge, + iter internalIterator, + snapshots []uint64, + rangeDelFrag *keyspan.Fragmenter, + rangeKeyFrag *keyspan.Fragmenter, + allowZeroSeqNum bool, + elideTombstone func(key []byte) bool, + elideRangeTombstone func(start, end []byte) bool, + 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, + } + i.rangeDelFrag.Cmp = cmp + i.rangeDelFrag.Format = formatKey + i.rangeDelFrag.Emit = i.emitRangeDelChunk + i.rangeKeyFrag.Cmp = cmp + i.rangeKeyFrag.Format = formatKey + i.rangeKeyFrag.Emit = i.emitRangeKeyChunk + return i +} + +func (i *compactionIter) First() (*InternalKey, []byte) { + if i.err != nil { + return nil, nil + } + var iterValue LazyValue + i.iterKey, iterValue = i.iter.First() + i.iterValue, _, i.err = iterValue.Value(nil) + if i.err != nil { + return nil, nil + } + if i.iterKey != nil { + i.curSnapshotIdx, i.curSnapshotSeqNum = snapshotIndex(i.iterKey.SeqNum(), i.snapshots) + } + i.pos = iterPosNext + i.iterStripeChange = newStripeNewKey + return i.Next() +} + +func (i *compactionIter) Next() (*InternalKey, []byte) { + if i.err != nil { + return nil, nil + } + + // Close the closer for the current value if one was open. + if i.closeValueCloser() != nil { + return nil, nil + } + + // 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 == 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 == 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 + i.valid = false + + for i.iterKey != nil { + // If we entered a new snapshot stripe with the same key, any key we + // return on this iteration is only returned because the open snapshot + // prevented it from being elided or merged with the key returned for + // the previous stripe. Mark it as pinned so that the compaction loop + // can correctly populate output tables' pinned statistics. We might + // also set snapshotPinned=true down below if we observe that the key is + // deleted by a range deletion in a higher stripe or that this key is a + // tombstone that could be elided if only it were in the last snapshot + // stripe. + i.snapshotPinned = i.iterStripeChange == newStripeSameKey + + if i.iterKey.Kind() == InternalKeyKindRangeDelete || rangekey.IsRangeKey(i.iterKey.Kind()) { + // Return the span so the compaction can use it for file truncation and add + // it to the relevant fragmenter. We do not set `skip` to true before + // returning as there may be a forthcoming point key with the same user key + // and sequence number. Such a point key must be visible (i.e., not skipped + // over) since we promise point keys are not deleted by range tombstones at + // the same sequence number. + // + // Although, note that `skip` may already be true before reaching here + // due to an earlier key in the stripe. Then it is fine to leave it set + // to true, as the earlier key must have had a higher sequence number. + // + // NOTE: there is a subtle invariant violation here in that calling + // saveKey and returning a reference to the temporary slice violates + // the stability guarantee for range deletion keys. A potential + // mediation could return the original iterKey and iterValue + // directly, as the backing memory is guaranteed to be stable until + // the compaction completes. The violation here is only minor in + // that the caller immediately clones the range deletion InternalKey + // when passing the key to the deletion fragmenter (see the + // call-site in compaction.go). + // TODO(travers): address this violation by removing the call to + // saveKey and instead return the original iterKey and iterValue. + // This goes against the comment on i.key in the struct, and + // therefore warrants some investigation. + i.saveKey() + // TODO(jackson): Handle tracking pinned statistics for range keys + // and range deletions. This would require updating + // emitRangeDelChunk and rangeKeyCompactionTransform to update + // statistics when they apply their own snapshot striping logic. + i.snapshotPinned = false + i.value = i.iterValue + i.valid = true + return &i.key, i.value + } + + if cover := i.rangeDelFrag.Covers(*i.iterKey, i.curSnapshotSeqNum); cover == keyspan.CoversVisibly { + // A pending range deletion deletes this key. Skip it. + i.saveKey() + i.skipInStripe() + continue + } 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, 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.iterStripeChange == newStripeSameKey { + panic(errors.AssertionFailedf("pebble: skipInStripe in last stripe found a new stripe within the same key")) + } + if !i.skip && i.iterStripeChange != newStripeNewKey { + panic(errors.AssertionFailedf("pebble: skipInStripe in last stripe disabled skip without advancing to new key")) + } + continue + } else { + // We're not at the last snapshot stripe, so the tombstone + // can NOT yet be elided. Mark it as pinned, so that it's + // included in table statistics appropriately. + i.snapshotPinned = true + } + } + + switch i.iterKey.Kind() { + case InternalKeyKindDelete: + i.saveKey() + i.value = i.iterValue + i.valid = true + 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: + // The key we emit for this entry is a function of the current key + // kind, and whether this entry is followed by a DEL/SINGLEDEL + // entry. setNext() does the work to move the iterator forward, + // 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: + // Record the snapshot index before mergeNext as merging + // advances the iterator, adjusting curSnapshotIdx. + origSnapshotIdx := i.curSnapshotIdx + var valueMerger ValueMerger + valueMerger, i.err = i.merge(i.iterKey.UserKey, i.iterValue) + var change stripeChangeType + if i.err == nil { + change = i.mergeNext(valueMerger) + } + var needDelete bool + if i.err == nil { + // includesBase is true whenever we've transformed the MERGE record + // into a SET. + 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 { + if needDelete { + i.valid = false + if i.closeValueCloser() != nil { + return nil, nil + } + continue + } + // A non-skippable entry does not necessarily cover later merge + // operands, so we must not zero the current merge result's seqnum. + // + // For example, suppose the forthcoming two keys are a range + // tombstone, `[a, b)#3`, and a merge operand, `a#3`. Recall that + // range tombstones do not cover point keys at the same seqnum, so + // `a#3` is not deleted. The range tombstone will be seen first due + // to its larger value type. Since it is a non-skippable key, the + // current merge will not include `a#3`. If we zeroed the current + // merge result's seqnum, then it would conflict with the upcoming + // merge including `a#3`, whose seqnum will also be zeroed. + if change != sameStripeNonSkippable { + i.maybeZeroSeqnum(origSnapshotIdx) + } + return &i.key, i.value + } + 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 + + default: + i.err = base.CorruptionErrorf("invalid internal key kind: %d", errors.Safe(i.iterKey.Kind())) + i.valid = false + return nil, nil + } + } + + return nil, nil +} + +func (i *compactionIter) closeValueCloser() error { + if i.valueCloser == nil { + return nil + } + + i.err = i.valueCloser.Close() + i.valueCloser = nil + if i.err != nil { + i.valid = false + } + return i.err +} + +// snapshotIndex returns the index of the first sequence number in snapshots +// which is greater than or equal to seq. +func snapshotIndex(seq uint64, snapshots []uint64) (int, uint64) { + index := sort.Search(len(snapshots), func(i int) bool { + return snapshots[i] > seq + }) + if index >= len(snapshots) { + return index, InternalKeySeqNumMax + } + return index, snapshots[index] +} + +// 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 + 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 + // `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 + } +} + +func (i *compactionIter) iterNext() bool { + var iterValue LazyValue + i.iterKey, iterValue = i.iter.Next() + i.iterValue, _, i.err = iterValue.Value(nil) + if i.err != nil { + i.iterKey = nil + } + return i.iterKey != nil +} + +// stripeChangeType indicates how the snapshot stripe changed relative to the +// previous key. If no change, it also indicates whether the current entry is +// skippable. If the snapshot stripe changed, it also indicates whether the new +// stripe was entered because the iterator progressed onto an entirely new key +// or entered a new stripe within the same key. +type stripeChangeType int + +const ( + newStripeNewKey stripeChangeType = iota + newStripeSameKey + sameStripeSkippable + sameStripeNonSkippable +) + +// nextInStripe advances the iterator and returns one of the above const ints +// indicating how its state changed. +// +// 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 +} + +// 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 + + 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(errors.AssertionFailedf("pebble: invariant violation: %s and %s out of order", prevKey, key)) + } + + 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 + } + 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 + } + if i.curSnapshotIdx == origSnapshotIdx { + return sameStripeSkippable + } + return newStripeSameKey +} + +func (i *compactionIter) setNext() { + // Save the current key. + i.saveKey() + i.value = i.iterValue + i.valid = true + i.maybeZeroSeqnum(i.curSnapshotIdx) + + // There are two cases where we can early return and skip the remaining + // records in the stripe: + // - If the DB does not SETWITHDEL. + // - If this key is already a SETWITHDEL. + if i.formatVersion < FormatSetWithDelete || + i.iterKey.Kind() == InternalKeyKindSetWithDelete { + i.skip = true + return + } + + // We are iterating forward. Save the current value. + i.valueBuf = append(i.valueBuf[:0], i.iterValue...) + i.value = i.valueBuf + + // 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. + for { + switch i.nextInStripe() { + case newStripeNewKey, newStripeSameKey: + i.pos = iterPosNext + return + case sameStripeNonSkippable: + i.pos = iterPosNext + // We iterated onto a key that we cannot skip. We can + // conservatively transform the original SET into a SETWITHDEL + // as an indication that there *may* still be a DEL/SINGLEDEL + // under this SET, even if we did not actually encounter one. + // + // This is safe to do, as: + // + // - in the case that there *is not* actually a DEL/SINGLEDEL + // under this entry, any SINGLEDEL above this now-transformed + // SETWITHDEL will become a DEL when the two encounter in a + // compaction. The DEL will eventually be elided in a + // subsequent compaction. The cost for ensuring correctness is + // that this entry is kept around for an additional compaction + // cycle(s). + // + // - in the case there *is* indeed a DEL/SINGLEDEL under us + // (but in a different stripe or sstable), then we will have + // already done the work to transform the SET into a + // SETWITHDEL, and we will skip any additional iteration when + // this entry is encountered again in a subsequent compaction. + // + // Ideally, this codepath would be smart enough to handle the + // case of SET <- RANGEDEL <- ... <- DEL/SINGLEDEL <- .... + // This requires preserving any RANGEDEL entries we encounter + // along the way, then emitting the original (possibly + // transformed) key, followed by the RANGEDELs. This requires + // a sizable refactoring of the existing code, as nextInStripe + // currently returns a sameStripeNonSkippable when it + // encounters a RANGEDEL. + // TODO(travers): optimize to handle the RANGEDEL case if it + // turns out to be a performance problem. + i.key.SetKind(InternalKeyKindSetWithDelete) + + // By setting i.skip=true, we are saying that after the + // non-skippable key is emitted (which is likely a RANGEDEL), + // the remaining point keys that share the same user key as this + // saved key should be skipped. + i.skip = true + return + case sameStripeSkippable: + // 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))) + } + } +} + +func (i *compactionIter) mergeNext(valueMerger ValueMerger) stripeChangeType { + // Save the current key. + i.saveKey() + i.valid = true + + // Loop looking for older values in the current snapshot stripe and merge + // them. + for { + if i.nextInStripe() != sameStripeSkippable { + i.pos = iterPosNext + return i.iterStripeChange + } + if i.err != nil { + panic(i.err) + } + key := i.iterKey + switch key.Kind() { + 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 -> 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 + // older is acceptable to shadow. Note that this is slightly + // different from singleDeleteNext() which implements stricter + // semantics in terms of applying the SingleDelete to the single + // next Set. But those stricter semantics are not observable to + // the end-user since Iterator interprets SingleDelete as Delete. + // We could do something more complicated here and consume only a + // 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(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: + // MERGE + (SET*) -> SET. + i.err = valueMerger.MergeOlder(i.iterValue) + if i.err != nil { + i.valid = false + return sameStripeSkippable + } + i.key.SetKind(InternalKeyKindSet) + i.skip = true + 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) + if i.err != nil { + i.valid = false + return sameStripeSkippable + } + + default: + i.err = base.CorruptionErrorf("invalid internal key kind: %d", errors.Safe(i.iterKey.Kind())) + i.valid = false + return sameStripeSkippable + } + } +} + +// 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() + i.value = i.iterValue + i.valid = true + + // 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 { + i.pos = iterPosNext + return i.err == nil + } + if i.err != nil { + panic(i.err) + } + key := i.iterKey + switch key.Kind() { + case InternalKeyKindDelete, InternalKeyKindMerge, InternalKeyKindSetWithDelete, InternalKeyKindDeleteSized: + // We've hit a Delete, DeleteSized, Merge, SetWithDelete, transform + // the SingleDelete into a full Delete. + i.key.SetKind(InternalKeyKindDelete) + i.skip = true + return true + + case InternalKeyKindSet: + // This SingleDelete deletes the Set, 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. + i.nextInStripe() + // TODO(jackson): We could assert that nextInStripe either a) + // stepped onto a new key, or b) stepped on to a Delete, DeleteSized + // or SingleDel key. This would detect improper uses of SingleDel, + // but only when all three internal keys meet in the same compaction + // which is not likely. + i.valid = false + return false + + case InternalKeyKindSingleDelete: + // Two single deletes met in a compaction. With proper deterministic + // use of SingleDelete, this should never happen. The expectation is + // that there's exactly 1 set beneath a single delete. Currently, we + // opt to skip it. + // TODO(jackson): Should we make this an error? This would also + // allow us to simplify the code a bit by removing the for loop. + continue + + default: + i.err = base.CorruptionErrorf("invalid internal key kind: %d", errors.Safe(i.iterKey.Kind())) + i.valid = false + return false + } + } +} + +// 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 > 1, a DELSIZED that has not deleted a key must + // have a value with a length > 1. + // + // 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 + i.key.Trailer = i.iterKey.Trailer + i.keyTrailer = i.iterKey.Trailer + i.frontiers.Advance(i.key.UserKey) +} + +func (i *compactionIter) cloneKey(key []byte) []byte { + i.alloc, key = i.alloc.Copy(key) + return key +} + +func (i *compactionIter) Key() InternalKey { + return i.key +} + +func (i *compactionIter) Value() []byte { + return i.value +} + +func (i *compactionIter) Valid() bool { + return i.valid +} + +func (i *compactionIter) Error() error { + return i.err +} + +func (i *compactionIter) Close() error { + err := i.iter.Close() + if i.err == nil { + i.err = err + } + + // Close the closer for the current value if one was open. + if i.valueCloser != nil { + i.err = firstError(i.err, i.valueCloser.Close()) + i.valueCloser = nil + } + + return i.err +} + +// Tombstones returns a list of pending range tombstones in the fragmenter +// up to the specified key, or all pending range tombstones if key = nil. +func (i *compactionIter) Tombstones(key []byte) []keyspan.Span { + if key == nil { + i.rangeDelFrag.Finish() + } else { + // The specified end key is exclusive; no versions of the specified + // user key (including range tombstones covering that key) should + // be flushed yet. + i.rangeDelFrag.TruncateAndFlushTo(key) + } + tombstones := i.tombstones + i.tombstones = nil + return tombstones +} + +// RangeKeys returns a list of pending fragmented range keys up to the specified +// key, or all pending range keys if key = nil. +func (i *compactionIter) RangeKeys(key []byte) []keyspan.Span { + if key == nil { + i.rangeKeyFrag.Finish() + } else { + // The specified end key is exclusive; no versions of the specified + // user key (including range tombstones covering that key) should + // be flushed yet. + i.rangeKeyFrag.TruncateAndFlushTo(key) + } + rangeKeys := i.rangeKeys + i.rangeKeys = nil + return rangeKeys +} + +func (i *compactionIter) emitRangeDelChunk(fragmented keyspan.Span) { + // Apply the snapshot stripe rules, keeping only the latest tombstone for + // each snapshot stripe. + currentIdx := -1 + keys := fragmented.Keys[:0] + for _, k := range fragmented.Keys { + idx, _ := snapshotIndex(k.SeqNum(), i.snapshots) + if currentIdx == idx { + continue + } + if idx == 0 && i.elideRangeTombstone(fragmented.Start, fragmented.End) { + // This is the last snapshot stripe and the range tombstone + // can be elided. + break + } + + keys = append(keys, k) + if idx == 0 { + // This is the last snapshot stripe. + break + } + currentIdx = idx + } + if len(keys) > 0 { + i.tombstones = append(i.tombstones, keyspan.Span{ + Start: fragmented.Start, + End: fragmented.End, + Keys: keys, + }) + } +} + +func (i *compactionIter) emitRangeKeyChunk(fragmented keyspan.Span) { + // Elision of snapshot stripes happens in rangeKeyCompactionTransform, so no need to + // do that here. + if len(fragmented.Keys) > 0 { + i.rangeKeys = append(i.rangeKeys, fragmented) + } +} + +// maybeZeroSeqnum attempts to set the seqnum for the current key to 0. Doing +// so improves compression and enables an optimization during forward iteration +// to skip some key comparisons. The seqnum for an entry can be zeroed if the +// entry is on the bottom snapshot stripe and on the bottom level of the LSM. +func (i *compactionIter) maybeZeroSeqnum(snapshotIdx int) { + if !i.allowZeroSeqNum { + // TODO(peter): allowZeroSeqNum applies to the entire compaction. We could + // make the determination on a key by key basis, similar to what is done + // for elideTombstone. Need to add a benchmark for compactionIter to verify + // that isn't too expensive. + return + } + if snapshotIdx > 0 { + // This is not the last snapshot + return + } + i.key.SetSeqNum(base.SeqNumZero) +} + +// A frontier is used to monitor a compaction's progression across the user +// keyspace. +// +// A frontier hold a user key boundary that it's concerned with in its `key` +// field. If/when the compaction iterator returns an InternalKey with a user key +// _k_ such that k ≥ frontier.key, the compaction iterator invokes the +// frontier's `reached` function, passing _k_ as its argument. +// +// The `reached` function returns a new value to use as the key. If `reached` +// returns nil, the frontier is forgotten and its `reached` method will not be +// invoked again, unless the user calls [Update] to set a new key. +// +// A frontier's key may be updated outside the context of a `reached` +// invocation at any time, through its Update method. +type frontier struct { + // container points to the containing *frontiers that was passed to Init + // when the frontier was initialized. + container *frontiers + + // key holds the frontier's current key. If nil, this frontier is inactive + // and its reached func will not be invoked. The value of this key may only + // be updated by the `frontiers` type, or the Update method. + key []byte + + // reached is invoked to inform a frontier that its key has been reached. + // It's invoked with the user key that reached the limit. The `key` argument + // is guaranteed to be ≥ the frontier's key. + // + // After reached is invoked, the frontier's key is updated to the return + // value of `reached`. Note bene, the frontier is permitted to update its + // key to a user key ≤ the argument `key`. + // + // If a frontier is set to key k1, and reached(k2) is invoked (k2 ≥ k1), the + // frontier will receive reached(k2) calls until it returns nil or a key + // `k3` such that k2 < k3. This property is useful for frontiers that use + // `reached` invocations to drive iteration through collections of keys that + // may contain multiple keys that are both < k2 and ≥ k1. + reached func(key []byte) (next []byte) +} + +// Init initializes the frontier with the provided key and reached callback. +// The frontier is attached to the provided *frontiers and the provided reached +// func will be invoked when the *frontiers is advanced to a key ≥ this +// frontier's key. +func (f *frontier) Init( + frontiers *frontiers, initialKey []byte, reached func(key []byte) (next []byte), +) { + *f = frontier{ + container: frontiers, + key: initialKey, + reached: reached, + } + if initialKey != nil { + f.container.push(f) + } +} + +// String implements fmt.Stringer. +func (f *frontier) String() string { + return string(f.key) +} + +// Update replaces the existing frontier's key with the provided key. The +// frontier's reached func will be invoked when the new key is reached. +func (f *frontier) Update(key []byte) { + c := f.container + prevKeyIsNil := f.key == nil + f.key = key + if prevKeyIsNil { + if key != nil { + c.push(f) + } + return + } + + // Find the frontier within the heap (it must exist within the heap because + // f.key was != nil). If the frontier key is now nil, remove it from the + // heap. Otherwise, fix up its position. + for i := 0; i < len(c.items); i++ { + if c.items[i] == f { + if key != nil { + c.fix(i) + } else { + n := c.len() - 1 + c.swap(i, n) + c.down(i, n) + c.items = c.items[:n] + } + return + } + } + panic("unreachable") +} + +// frontiers is used to track progression of a task (eg, compaction) across the +// keyspace. Clients that want to be informed when the task advances to a key ≥ +// some frontier may register a frontier, providing a callback. The task calls +// `Advance(k)` with each user key encountered, which invokes the `reached` func +// on all tracked frontiers with `key`s ≤ k. +// +// Internally, frontiers is implemented as a simple heap. +type frontiers struct { + cmp Compare + items []*frontier +} + +// String implements fmt.Stringer. +func (f *frontiers) String() string { + var buf bytes.Buffer + for i := 0; i < len(f.items); i++ { + if i > 0 { + fmt.Fprint(&buf, ", ") + } + fmt.Fprintf(&buf, "%s: %q", f.items[i], f.items[i].key) + } + return buf.String() +} + +// Advance notifies all member frontiers with keys ≤ k. +func (f *frontiers) Advance(k []byte) { + for len(f.items) > 0 && f.cmp(k, f.items[0].key) >= 0 { + // This frontier has been reached. Invoke the closure and update with + // the next frontier. + f.items[0].key = f.items[0].reached(k) + if f.items[0].key == nil { + // This was the final frontier that this user was concerned with. + // Remove it from the heap. + f.pop() + } else { + // Fix up the heap root. + f.fix(0) + } + } +} + +func (f *frontiers) len() int { + return len(f.items) +} + +func (f *frontiers) less(i, j int) bool { + return f.cmp(f.items[i].key, f.items[j].key) < 0 +} + +func (f *frontiers) swap(i, j int) { + f.items[i], f.items[j] = f.items[j], f.items[i] +} + +// fix, up and down are copied from the go stdlib. + +func (f *frontiers) fix(i int) { + if !f.down(i, f.len()) { + f.up(i) + } +} + +func (f *frontiers) push(ff *frontier) { + n := len(f.items) + f.items = append(f.items, ff) + f.up(n) +} + +func (f *frontiers) pop() *frontier { + n := f.len() - 1 + f.swap(0, n) + f.down(0, n) + item := f.items[n] + f.items = f.items[:n] + return item +} + +func (f *frontiers) up(j int) { + for { + i := (j - 1) / 2 // parent + if i == j || !f.less(j, i) { + break + } + f.swap(i, j) + j = i + } +} + +func (f *frontiers) down(i0, n int) bool { + i := i0 + for { + j1 := 2*i + 1 + if j1 >= n || j1 < 0 { // j1 < 0 after int overflow + break + } + j := j1 // left child + if j2 := j1 + 1; j2 < n && f.less(j2, j1) { + j = j2 // = 2*i + 2 // right child + } + if !f.less(j, i) { + break + } + f.swap(i, j) + i = j + } + return i > i0 +} diff --git a/pebble/compaction_iter_test.go b/pebble/compaction_iter_test.go new file mode 100644 index 0000000..07c489c --- /dev/null +++ b/pebble/compaction_iter_test.go @@ -0,0 +1,382 @@ +// Copyright 2018 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 ( + "bytes" + "encoding/binary" + "fmt" + "io" + "slices" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invalidating" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/stretchr/testify/require" +) + +func TestSnapshotIndex(t *testing.T) { + testCases := []struct { + snapshots []uint64 + seq uint64 + expectedIndex int + expectedSeqNum uint64 + }{ + {[]uint64{}, 1, 0, InternalKeySeqNumMax}, + {[]uint64{1}, 0, 0, 1}, + {[]uint64{1}, 1, 1, InternalKeySeqNumMax}, + {[]uint64{1}, 2, 1, InternalKeySeqNumMax}, + {[]uint64{1, 3}, 1, 1, 3}, + {[]uint64{1, 3}, 2, 1, 3}, + {[]uint64{1, 3}, 3, 2, InternalKeySeqNumMax}, + {[]uint64{1, 3}, 4, 2, InternalKeySeqNumMax}, + {[]uint64{1, 3, 3}, 2, 1, 3}, + } + for _, c := range testCases { + t.Run("", func(t *testing.T) { + idx, seqNum := snapshotIndex(c.seq, c.snapshots) + if c.expectedIndex != idx { + t.Fatalf("expected %d, but got %d", c.expectedIndex, idx) + } + if c.expectedSeqNum != seqNum { + t.Fatalf("expected %d, but got %d", c.expectedSeqNum, seqNum) + } + }) + } +} + +type debugMerger struct { + buf []byte +} + +func (m *debugMerger) MergeNewer(value []byte) error { + m.buf = append(m.buf, value...) + return nil +} + +func (m *debugMerger) MergeOlder(value []byte) error { + buf := make([]byte, 0, len(m.buf)+len(value)) + buf = append(buf, value...) + buf = append(buf, m.buf...) + m.buf = buf + return nil +} + +func (m *debugMerger) Finish(includesBase bool) ([]byte, io.Closer, error) { + if includesBase { + m.buf = append(m.buf, []byte("[base]")...) + } + return m.buf, nil, nil +} + +func TestCompactionIter(t *testing.T) { + var merge Merge + var keys []InternalKey + var rangeKeys []keyspan.Span + var vals [][]byte + var snapshots []uint64 + var elideTombstones bool + var allowZeroSeqnum bool + var interleavingIter *keyspan.InterleavingIter + + // The input to the data-driven test is dependent on the format major + // version we are testing against. + fileFunc := func(formatVersion FormatMajorVersion) string { + if formatVersion < FormatSetWithDelete { + return "testdata/compaction_iter" + } + if formatVersion < FormatDeleteSizedAndObsolete { + return "testdata/compaction_iter_set_with_del" + } + return "testdata/compaction_iter_delete_sized" + } + + newIter := func(formatVersion FormatMajorVersion) *compactionIter { + // To adhere to the existing assumption that range deletion blocks in + // SSTables are not released while iterating, and therefore not + // susceptible to use-after-free bugs, we skip the zeroing of + // RangeDelete keys. + fi := &fakeIter{keys: keys, vals: vals} + interleavingIter = &keyspan.InterleavingIter{} + interleavingIter.Init( + base.DefaultComparer, + fi, + keyspan.NewIter(base.DefaultComparer.Compare, rangeKeys), + keyspan.InterleavingIterOpts{}) + iter := invalidating.NewIter(interleavingIter, invalidating.IgnoreKinds(InternalKeyKindRangeDelete)) + if merge == nil { + merge = func(key, value []byte) (base.ValueMerger, error) { + m := &debugMerger{} + m.buf = append(m.buf, value...) + return m, nil + } + } + + return newCompactionIter( + DefaultComparer.Compare, + DefaultComparer.Equal, + DefaultComparer.FormatKey, + merge, + iter, + snapshots, + &keyspan.Fragmenter{}, + &keyspan.Fragmenter{}, + allowZeroSeqnum, + func([]byte) bool { + return elideTombstones + }, + func(_, _ []byte) bool { + return elideTombstones + }, + formatVersion, + ) + } + + runTest := func(t *testing.T, formatVersion FormatMajorVersion) { + datadriven.RunTest(t, fileFunc(formatVersion), func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + merge = nil + if len(d.CmdArgs) > 0 && d.CmdArgs[0].Key == "merger" && + len(d.CmdArgs[0].Vals) > 0 && d.CmdArgs[0].Vals[0] == "deletable" { + merge = newDeletableSumValueMerger + } + keys = keys[:0] + vals = vals[:0] + rangeKeys = rangeKeys[:0] + for _, key := range strings.Split(d.Input, "\n") { + j := strings.Index(key, ":") + keys = append(keys, base.ParseInternalKey(key[:j])) + + if strings.HasPrefix(key[j+1:], "varint(") { + valueStr := strings.TrimSuffix(strings.TrimPrefix(key[j+1:], "varint("), ")") + v, err := strconv.ParseUint(valueStr, 10, 64) + require.NoError(t, err) + encodedValue := binary.AppendUvarint([]byte(nil), v) + vals = append(vals, encodedValue) + } else { + vals = append(vals, []byte(key[j+1:])) + } + } + return "" + + case "define-range-keys": + for _, key := range strings.Split(d.Input, "\n") { + s := keyspan.ParseSpan(strings.TrimSpace(key)) + rangeKeys = append(rangeKeys, s) + } + return "" + + case "iter": + snapshots = snapshots[:0] + elideTombstones = false + allowZeroSeqnum = false + printSnapshotPinned := false + printMissizedDels := false + printForceObsolete := false + for _, arg := range d.CmdArgs { + switch arg.Key { + case "snapshots": + for _, val := range arg.Vals { + seqNum, err := strconv.Atoi(val) + if err != nil { + return err.Error() + } + snapshots = append(snapshots, uint64(seqNum)) + } + case "elide-tombstones": + var err error + elideTombstones, err = strconv.ParseBool(arg.Vals[0]) + if err != nil { + return err.Error() + } + case "allow-zero-seqnum": + var err error + allowZeroSeqnum, err = strconv.ParseBool(arg.Vals[0]) + if err != nil { + return err.Error() + } + case "print-snapshot-pinned": + printSnapshotPinned = true + case "print-missized-dels": + printMissizedDels = true + case "print-force-obsolete": + printForceObsolete = true + default: + return fmt.Sprintf("%s: unknown arg: %s", d.Cmd, arg.Key) + } + } + slices.Sort(snapshots) + + iter := newIter(formatVersion) + var b bytes.Buffer + for _, line := range strings.Split(d.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + switch parts[0] { + case "first": + iter.First() + case "next": + iter.Next() + case "tombstones": + var key []byte + if len(parts) == 2 { + key = []byte(parts[1]) + } + for _, v := range iter.Tombstones(key) { + for _, k := range v.Keys { + fmt.Fprintf(&b, "%s-%s#%d\n", v.Start, v.End, k.SeqNum()) + } + } + fmt.Fprintf(&b, ".\n") + continue + case "range-keys": + var key []byte + if len(parts) == 2 { + key = []byte(parts[1]) + } + for _, v := range iter.RangeKeys(key) { + fmt.Fprintf(&b, "%s\n", v) + } + fmt.Fprintf(&b, ".\n") + continue + default: + return fmt.Sprintf("unknown op: %s", parts[0]) + } + if iter.Valid() { + snapshotPinned := "" + if printSnapshotPinned { + snapshotPinned = " (not pinned)" + if iter.snapshotPinned { + snapshotPinned = " (pinned)" + } + } + forceObsolete := "" + if printForceObsolete { + forceObsolete = " (not force obsolete)" + if iter.forceObsoleteDueToRangeDel { + forceObsolete = " (force obsolete)" + } + } + v := string(iter.Value()) + if iter.Key().Kind() == base.InternalKeyKindDeleteSized && len(iter.Value()) > 0 { + vn, n := binary.Uvarint(iter.Value()) + if n != len(iter.Value()) { + v = fmt.Sprintf("err: %0x value not a uvarint", iter.Value()) + } else { + v = fmt.Sprintf("varint(%d)", vn) + } + } + fmt.Fprintf(&b, "%s:%s%s%s\n", iter.Key(), v, snapshotPinned, forceObsolete) + if iter.Key().Kind() == InternalKeyKindRangeDelete { + iter.rangeDelFrag.Add(keyspan.Span{ + Start: append([]byte{}, iter.Key().UserKey...), + End: append([]byte{}, iter.Value()...), + Keys: []keyspan.Key{ + {Trailer: iter.Key().Trailer}, + }, + }) + } + if rangekey.IsRangeKey(iter.Key().Kind()) { + iter.rangeKeyFrag.Add(*interleavingIter.Span()) + } + } else if err := iter.Error(); err != nil { + fmt.Fprintf(&b, "err=%v\n", err) + } else { + fmt.Fprintf(&b, ".\n") + } + } + if printMissizedDels { + fmt.Fprintf(&b, "missized-dels=%d\n", iter.stats.countMissizedDels) + } + return b.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) + } + + // Rather than testing against all format version, we test against the + // significant boundaries. + formatVersions := []FormatMajorVersion{ + FormatMostCompatible, + FormatSetWithDelete - 1, + FormatSetWithDelete, + internalFormatNewest, + } + for _, formatVersion := range formatVersions { + t.Run(fmt.Sprintf("version-%s", formatVersion), func(t *testing.T) { + runTest(t, formatVersion) + }) + } +} + +func TestFrontiers(t *testing.T) { + cmp := testkeys.Comparer.Compare + var keySets [][][]byte + datadriven.RunTest(t, "testdata/frontiers", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "init": + // Init configures a frontier per line of input. Each line should + // contain a sorted whitespace-separated list of keys that the + // frontier will use. + // + // For example, the following input creates two separate monitored + // frontiers: one that sets its key successively to 'd', 'e', 'j' + // and one that sets its key to 'a', 'p', 'n', 'z': + // + // init + // b e j + // a p n z + + keySets = keySets[:0] + for _, line := range strings.Split(td.Input, "\n") { + keySets = append(keySets, bytes.Fields([]byte(line))) + } + return "" + case "scan": + f := &frontiers{cmp: cmp} + for _, keys := range keySets { + initTestFrontier(f, keys...) + } + var buf bytes.Buffer + for _, kStr := range strings.Fields(td.Input) { + k := []byte(kStr) + f.Advance(k) + fmt.Fprintf(&buf, "%s : { %s }\n", kStr, f.String()) + } + return buf.String() + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +// initTestFrontiers adds a new frontier to f that iterates through the provided +// keys. The keys slice must be sorted. +func initTestFrontier(f *frontiers, keys ...[]byte) *frontier { + ff := &frontier{} + var key []byte + if len(keys) > 0 { + key, keys = keys[0], keys[1:] + } + reached := func(k []byte) (nextKey []byte) { + if len(keys) > 0 { + nextKey, keys = keys[0], keys[1:] + } + return nextKey + } + ff.Init(f, key, reached) + return ff +} diff --git a/pebble/compaction_picker.go b/pebble/compaction_picker.go new file mode 100644 index 0000000..6567391 --- /dev/null +++ b/pebble/compaction_picker.go @@ -0,0 +1,2068 @@ +// Copyright 2018 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 ( + "bytes" + "fmt" + "math" + "sort" + "strings" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/manifest" +) + +// The minimum count for an intra-L0 compaction. This matches the RocksDB +// heuristic. +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 + readCompactionEnv readCompactionEnv +} + +type compactionPicker interface { + getScores([]compactionInfo) [numLevels]float64 + getBaseLevel() int + estimatedCompactionDebt(l0ExtraSize uint64) uint64 + pickAuto(env compactionEnv) (pc *pickedCompaction) + pickElisionOnlyCompaction(env compactionEnv) (pc *pickedCompaction) + pickRewriteCompaction(env compactionEnv) (pc *pickedCompaction) + pickReadTriggeredCompaction(env compactionEnv) (pc *pickedCompaction) + forceBaseLevel1() +} + +// readCompactionEnv is used to hold data required to perform read compactions +type readCompactionEnv struct { + rescheduleReadCompaction *bool + readCompactions *readCompactionQueue + flushing bool +} + +// 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 { + // 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 { + var buf bytes.Buffer + var largest int + for i, in := range info.inputs { + if i > 0 { + fmt.Fprintf(&buf, " -> ") + } + fmt.Fprintf(&buf, "L%d", in.level) + in.files.Each(func(m *fileMetadata) { + fmt.Fprintf(&buf, " %s", m.FileNum) + }) + if largest < in.level { + largest = in.level + } + } + if largest != info.outputLevel || len(info.inputs) == 1 { + fmt.Fprintf(&buf, " -> L%d", info.outputLevel) + } + return buf.String() +} + +type sortCompactionLevelsByPriority []candidateLevelInfo + +func (s sortCompactionLevelsByPriority) Len() int { + return len(s) +} + +// 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 sortCompactionLevelsByPriority) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// sublevelInfo is used to tag a LevelSlice for an L0 sublevel with the +// sublevel. +type sublevelInfo struct { + manifest.LevelSlice + sublevel manifest.Level +} + +func (cl sublevelInfo) Clone() sublevelInfo { + return sublevelInfo{ + sublevel: cl.sublevel, + LevelSlice: cl.LevelSlice.Reslice(func(start, end *manifest.LevelIterator) {}), + } +} +func (cl sublevelInfo) String() string { + return fmt.Sprintf(`Sublevel %s; Levels %s`, cl.sublevel, cl.LevelSlice) +} + +// generateSublevelInfo will generate the level slices for each of the sublevels +// from the level slice for all of L0. +func generateSublevelInfo(cmp base.Compare, levelFiles manifest.LevelSlice) []sublevelInfo { + sublevelMap := make(map[uint64][]*fileMetadata) + it := levelFiles.Iter() + for f := it.First(); f != nil; f = it.Next() { + sublevelMap[uint64(f.SubLevel)] = append(sublevelMap[uint64(f.SubLevel)], f) + } + + var sublevels []int + for level := range sublevelMap { + sublevels = append(sublevels, int(level)) + } + sort.Ints(sublevels) + + var levelSlices []sublevelInfo + for _, sublevel := range sublevels { + metas := sublevelMap[uint64(sublevel)] + levelSlices = append( + levelSlices, + sublevelInfo{ + manifest.NewLevelSliceKeySorted(cmp, metas), + manifest.L0Sublevel(sublevel), + }, + ) + } + 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. 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 + 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 + // maxOutputFileSize is the maximum size of an individual table created + // during compaction. + maxOutputFileSize uint64 + // maxOverlapBytes is the maximum number of bytes of overlap allowed for a + // single output table with the tables in the grandparent level. + maxOverlapBytes uint64 + // maxReadCompactionBytes is the maximum bytes a read compaction is allowed to + // 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 + pickerMetrics compactionPickerMetrics +} + +func defaultOutputLevel(startLevel, baseLevel int) int { + outputLevel := startLevel + 1 + if startLevel == 0 { + outputLevel = baseLevel + } + if outputLevel >= numLevels-1 { + outputLevel = numLevels - 1 + } + return outputLevel +} + +func newPickedCompaction( + opts *Options, cur *version, startLevel, outputLevel, baseLevel int, +) *pickedCompaction { + if startLevel > 0 && startLevel < baseLevel { + panic(fmt.Sprintf("invalid compaction: start level %d should not be empty (base level %d)", + startLevel, baseLevel)) + } + + adjustedLevel := adjustedOutputLevel(outputLevel, baseLevel) + pc := &pickedCompaction{ + cmp: opts.Comparer.Compare, + version: cur, + baseLevel: baseLevel, + inputs: []compactionLevel{{level: startLevel}, {level: outputLevel}}, + 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 { + outputLevel := baseLevel + if !isBase { + outputLevel = 0 // Intra L0 + } + + pc := newPickedCompaction(opts, vers, 0, outputLevel, baseLevel) + pc.lcf = lcf + pc.outputLevel.level = outputLevel + + // Manually build the compaction as opposed to calling + // pickAutoHelper. This is because L0Sublevels has already added + // any overlapping L0 SSTables that need to be added, and + // because compactions built by L0SSTables do not necessarily + // pick contiguous sequences of files in pc.version.Levels[0]. + files := make([]*manifest.FileMetadata, 0, len(lcf.Files)) + iter := vers.Levels[0].Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if lcf.FilesIncluded[f.L0Index] { + files = append(files, f) + } + } + pc.startLevel.files = manifest.NewLevelSliceSeqSorted(files) + return pc +} + +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, `, 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)) + builder.WriteString(fmt.Sprintf(`largest=%s, `, pc.largest)) + builder.WriteString(fmt.Sprintf(`version=%s, `, pc.version)) + builder.WriteString(fmt.Sprintf(`inputs=%s, `, pc.inputs)) + 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.startLevel.l0SublevelInfo)) + builder.WriteString(fmt.Sprintf(`lcf=%s`, pc.lcf)) + return builder.String() +} + +// Clone creates a deep copy of the pickedCompaction +func (pc *pickedCompaction) clone() *pickedCompaction { + + // Quickly copy over fields that do not require special deep copy care, and + // set all fields that will require a deep copy to nil. + newPC := &pickedCompaction{ + cmp: pc.cmp, + score: pc.score, + kind: pc.kind, + 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, + } + + newPC.inputs = make([]compactionLevel, len(pc.inputs)) + newPC.extraLevels = make([]*compactionLevel, 0, len(pc.extraLevels)) + for i := range pc.inputs { + newPC.inputs[i] = pc.inputs[i].Clone() + if i == 0 { + newPC.startLevel = &newPC.inputs[i] + } else if i == len(pc.inputs)-1 { + newPC.outputLevel = &newPC.inputs[i] + } else { + newPC.extraLevels = append(newPC.extraLevels, &newPC.inputs[i]) + } + } + + 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() + } + return newPC +} + +// maybeExpandedBounds is a helper function for setupInputs which ensures the +// pickedCompaction's smallest and largest internal keys are updated iff +// the candidate keys expand the key span. This avoids a bug for multi-level +// compactions: during the second call to setupInputs, the picked compaction's +// smallest and largest keys should not decrease the key span. +func (pc *pickedCompaction) maybeExpandBounds(smallest InternalKey, largest InternalKey) { + emptyKey := InternalKey{} + if base.InternalCompare(pc.cmp, smallest, emptyKey) == 0 { + if base.InternalCompare(pc.cmp, largest, emptyKey) != 0 { + panic("either both candidate keys are empty or neither are empty") + } + return + } + if base.InternalCompare(pc.cmp, pc.smallest, emptyKey) == 0 { + if base.InternalCompare(pc.cmp, pc.largest, emptyKey) != 0 { + panic("either both pc keys are empty or neither are empty") + } + pc.smallest = smallest + pc.largest = largest + return + } + if base.InternalCompare(pc.cmp, pc.smallest, smallest) >= 0 { + pc.smallest = smallest + } + if base.InternalCompare(pc.cmp, pc.largest, largest) <= 0 { + pc.largest = largest + } +} + +// setupInputs returns true if a compaction has been set up. It returns false if +// a concurrent compaction is occurring on the start or output level files. +func (pc *pickedCompaction) setupInputs( + opts *Options, diskAvailBytes uint64, startLevel *compactionLevel, +) bool { + // maxExpandedBytes is the maximum size of an expanded compaction. If + // growing a compaction results in a larger size, the original compaction + // is used instead. + maxExpandedBytes := expandedCompactionByteSizeLimit( + opts, adjustedOutputLevel(pc.outputLevel.level, pc.baseLevel), diskAvailBytes, + ) + + // Expand the initial inputs to a clean cut. + var isCompacting bool + startLevel.files, isCompacting = expandToAtomicUnit(pc.cmp, startLevel.files, false /* disableIsCompacting */) + if isCompacting { + return false + } + pc.maybeExpandBounds(manifest.KeyRange(pc.cmp, startLevel.files.Iter())) + + // Determine the sstables in the output level which overlap with the input + // sstables, and then expand those tables to a clean cut. No need to do + // this for intra-L0 compactions; outputLevel.files is left empty for those. + if startLevel.level != pc.outputLevel.level { + pc.outputLevel.files = pc.version.Overlaps(pc.outputLevel.level, pc.cmp, pc.smallest.UserKey, + pc.largest.UserKey, pc.largest.IsExclusiveSentinel()) + pc.outputLevel.files, isCompacting = expandToAtomicUnit(pc.cmp, pc.outputLevel.files, + false /* disableIsCompacting */) + if isCompacting { + return false + } + pc.maybeExpandBounds(manifest.KeyRange(pc.cmp, + startLevel.files.Iter(), pc.outputLevel.files.Iter())) + } + + // Grow the sstables in startLevel.level as long as it doesn't affect the number + // of sstables included from pc.outputLevel.level. + if pc.lcf != nil && startLevel.level == 0 && pc.outputLevel.level != 0 { + // Call the L0-specific compaction extension method. Similar logic as + // pc.grow. Additional L0 files are optionally added to the compaction at + // this step. Note that the bounds passed in are not the bounds of the + // compaction, but rather the smallest and largest internal keys that + // the compaction cannot include from L0 without pulling in more Lbase + // files. Consider this example: + // + // L0: c-d e+f g-h + // Lbase: a-b e+f i-j + // a b c d e f g h i j + // + // The e-f files have already been chosen in the compaction. As pulling + // in more LBase files is undesirable, the logic below will pass in + // smallest = b and largest = i to ExtendL0ForBaseCompactionTo, which + // will expand the compaction to include c-d and g-h from L0. The + // bounds passed in are exclusive; the compaction cannot be expanded + // to include files that "touch" it. + smallestBaseKey := base.InvalidInternalKey + largestBaseKey := base.InvalidInternalKey + if pc.outputLevel.files.Empty() { + baseIter := pc.version.Levels[pc.outputLevel.level].Iter() + if sm := baseIter.SeekLT(pc.cmp, pc.smallest.UserKey); sm != nil { + smallestBaseKey = sm.Largest + } + if la := baseIter.SeekGE(pc.cmp, pc.largest.UserKey); la != nil { + largestBaseKey = la.Smallest + } + } else { + // NB: We use Reslice to access the underlying level's files, but + // we discard the returned slice. The pc.outputLevel.files slice + // is not modified. + _ = pc.outputLevel.files.Reslice(func(start, end *manifest.LevelIterator) { + if sm := start.Prev(); sm != nil { + smallestBaseKey = sm.Largest + } + if la := end.Next(); la != nil { + largestBaseKey = la.Smallest + } + }) + } + oldLcf := pc.lcf.Clone() + if pc.version.L0Sublevels.ExtendL0ForBaseCompactionTo(smallestBaseKey, largestBaseKey, pc.lcf) { + var newStartLevelFiles []*fileMetadata + iter := pc.version.Levels[0].Iter() + var sizeSum uint64 + for j, f := 0, iter.First(); f != nil; j, f = j+1, iter.Next() { + if pc.lcf.FilesIncluded[f.L0Index] { + newStartLevelFiles = append(newStartLevelFiles, f) + sizeSum += f.Size + } + } + if sizeSum+pc.outputLevel.files.SizeSum() < maxExpandedBytes { + startLevel.files = manifest.NewLevelSliceSeqSorted(newStartLevelFiles) + pc.smallest, pc.largest = manifest.KeyRange(pc.cmp, + startLevel.files.Iter(), pc.outputLevel.files.Iter()) + } else { + *pc.lcf = *oldLcf + } + } + } else if pc.grow(pc.smallest, pc.largest, maxExpandedBytes, startLevel) { + pc.maybeExpandBounds(manifest.KeyRange(pc.cmp, + startLevel.files.Iter(), pc.outputLevel.files.Iter())) + } + + if pc.startLevel.level == 0 { + // We don't change the input files for the compaction beyond this point. + pc.startLevel.l0SublevelInfo = generateSublevelInfo(pc.cmp, pc.startLevel.files) + } + + return true +} + +// grow grows the number of inputs at c.level without changing the number of +// c.level+1 files in the compaction, and returns whether the inputs grew. sm +// and la are the smallest and largest InternalKeys in all of the inputs. +func (pc *pickedCompaction) grow( + sm, la InternalKey, maxExpandedBytes uint64, startLevel *compactionLevel, +) bool { + if pc.outputLevel.files.Empty() { + return false + } + grow0 := pc.version.Overlaps(startLevel.level, pc.cmp, sm.UserKey, + la.UserKey, la.IsExclusiveSentinel()) + grow0, isCompacting := expandToAtomicUnit(pc.cmp, grow0, false /* disableIsCompacting */) + if isCompacting { + return false + } + if grow0.Len() <= startLevel.files.Len() { + return false + } + if grow0.SizeSum()+pc.outputLevel.files.SizeSum() >= maxExpandedBytes { + return false + } + // We need to include the outputLevel iter because without it, in a multiLevel scenario, + // sm1 and la1 could shift the output level keyspace when pc.outputLevel.files is set to grow1. + sm1, la1 := manifest.KeyRange(pc.cmp, grow0.Iter(), pc.outputLevel.files.Iter()) + grow1 := pc.version.Overlaps(pc.outputLevel.level, pc.cmp, sm1.UserKey, + la1.UserKey, la1.IsExclusiveSentinel()) + grow1, isCompacting = expandToAtomicUnit(pc.cmp, grow1, false /* disableIsCompacting */) + if isCompacting { + return false + } + if grow1.Len() != pc.outputLevel.files.Len() { + return false + } + startLevel.files = grow0 + pc.outputLevel.files = grow1 + return true +} + +func (pc *pickedCompaction) compactionSize() uint64 { + var bytesToCompact uint64 + for i := range pc.inputs { + bytesToCompact += pc.inputs[i].files.SizeSum() + } + return bytesToCompact +} + +// setupMultiLevelCandidated returns true if it successfully added another level +// to the compaction. +func (pc *pickedCompaction) setupMultiLevelCandidate(opts *Options, diskAvailBytes uint64) bool { + pc.inputs = append(pc.inputs, compactionLevel{level: pc.outputLevel.level + 1}) + + // Recalibrate startLevel and outputLevel: + // - startLevel and outputLevel pointers may be obsolete after appending to pc.inputs. + // - push outputLevel to extraLevels and move the new level to outputLevel + pc.startLevel = &pc.inputs[0] + pc.extraLevels = []*compactionLevel{&pc.inputs[1]} + pc.outputLevel = &pc.inputs[2] + return pc.setupInputs(opts, diskAvailBytes, pc.extraLevels[len(pc.extraLevels)-1]) +} + +// expandToAtomicUnit expands the provided level slice within its level both +// forwards and backwards to its "atomic compaction unit" boundaries, if +// necessary. +// +// While picking compaction inputs, this is required to maintain the invariant +// that the versions of keys at level+1 are older than the versions of keys at +// level. Tables are added to the right of the current slice tables such that +// the rightmost table has a "clean cut". A clean cut is either a change in +// user keys, or when the largest key in the left sstable is a range tombstone +// sentinel key (InternalKeyRangeDeleteSentinel). +// +// In addition to maintaining the seqnum invariant, expandToAtomicUnit is used +// to provide clean boundaries for range tombstone truncation during +// compaction. In order to achieve these clean boundaries, expandToAtomicUnit +// needs to find a "clean cut" on the left edge of the compaction as well. +// This is necessary in order for "atomic compaction units" to always be +// compacted as a unit. Failure to do this leads to a subtle bug with +// truncation of range tombstones to atomic compaction unit boundaries. +// Consider the scenario: +// +// L3: +// 12:[a#2,15-b#1,1] +// 13:[b#0,15-d#72057594037927935,15] +// +// These sstables contain a range tombstone [a-d)#2 which spans the two +// sstables. The two sstables need to always be kept together. Compacting +// sstable 13 independently of sstable 12 would result in: +// +// L3: +// 12:[a#2,15-b#1,1] +// L4: +// 14:[b#0,15-d#72057594037927935,15] +// +// This state is still ok, but when sstable 12 is next compacted, its range +// tombstones will be truncated at "b" (the largest key in its atomic +// compaction unit). In the scenario here, that could result in b#1 becoming +// visible when it should be deleted. +// +// isCompacting is returned true for any atomic units that contain files that +// have in-progress compactions, i.e. FileMetadata.Compacting == true. If +// disableIsCompacting is true, isCompacting always returns false. This helps +// avoid spurious races from being detected when this method is used outside +// of compaction picking code. +// +// TODO(jackson): Compactions and flushes no longer split a user key between two +// sstables. We could perform a migration, re-compacting any sstables with split +// user keys, which would allow us to remove atomic compaction unit expansion +// code. +func expandToAtomicUnit( + cmp Compare, inputs manifest.LevelSlice, disableIsCompacting bool, +) (slice manifest.LevelSlice, isCompacting bool) { + // NB: Inputs for L0 can't be expanded and *version.Overlaps guarantees + // that we get a 'clean cut.' For L0, Overlaps will return a slice without + // access to the rest of the L0 files, so it's OK to try to reslice. + if inputs.Empty() { + // Nothing to expand. + return inputs, false + } + + // TODO(jackson): Update to avoid use of LevelIterator.Current(). The + // Reslice interface will require some tweaking, because we currently rely + // on Reslice having already positioned the LevelIterator appropriately. + + inputs = inputs.Reslice(func(start, end *manifest.LevelIterator) { + iter := start.Clone() + iter.Prev() + for cur, prev := start.Current(), iter.Current(); prev != nil; cur, prev = start.Prev(), iter.Prev() { + if cur.IsCompacting() { + isCompacting = true + } + if cmp(prev.Largest.UserKey, cur.Smallest.UserKey) < 0 { + break + } + if prev.Largest.IsExclusiveSentinel() { + // The table prev has a largest key indicating that the user key + // prev.largest.UserKey doesn't actually exist in the table. + break + } + // prev.Largest.UserKey == cur.Smallest.UserKey, so we need to + // include prev in the compaction. + } + + iter = end.Clone() + iter.Next() + for cur, next := end.Current(), iter.Current(); next != nil; cur, next = end.Next(), iter.Next() { + if cur.IsCompacting() { + isCompacting = true + } + if cmp(cur.Largest.UserKey, next.Smallest.UserKey) < 0 { + break + } + if cur.Largest.IsExclusiveSentinel() { + // The table cur has a largest key indicating that the user key + // cur.largest.UserKey doesn't actually exist in the table. + break + } + // cur.Largest.UserKey == next.Smallest.UserKey, so we need to + // include next in the compaction. + } + }) + inputIter := inputs.Iter() + isCompacting = !disableIsCompacting && + (isCompacting || inputIter.First().IsCompacting() || inputIter.Last().IsCompacting()) + return inputs, isCompacting +} + +func newCompactionPicker( + v *version, opts *Options, inProgressCompactions []compactionInfo, +) compactionPicker { + p := &compactionPickerByScore{ + opts: opts, + vers: v, + } + p.initLevelMaxBytes(inProgressCompactions) + return p +} + +// Information about a candidate compaction level that has been identified by +// the compaction picker. +type candidateLevelInfo struct { + // 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 + // picked by the compaction, and a pickedCompaction created for the + // compaction. + 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 +} + +// compensatedSize returns f's file size, inflated according to compaction +// priorities. +func compensatedSize(f *fileMetadata) uint64 { + // Add in the estimate of disk space that may be reclaimed by compacting the + // file's tombstones. + return f.Size + fileCompensation(f) +} + +// compensatedSizeAnnotator implements manifest.Annotator, annotating B-Tree +// nodes with the sum of the files' compensated sizes. Its annotation type is +// a *uint64. Compensated sizes may change once a table's stats are loaded +// asynchronously, so its values are marked as cacheable only if a file's +// stats have been loaded. +type compensatedSizeAnnotator struct { +} + +var _ manifest.Annotator = compensatedSizeAnnotator{} + +func (a compensatedSizeAnnotator) Zero(dst interface{}) interface{} { + if dst == nil { + return new(uint64) + } + v := dst.(*uint64) + *v = 0 + return v +} + +func (a compensatedSizeAnnotator) Accumulate( + f *fileMetadata, dst interface{}, +) (v interface{}, cacheOK bool) { + vptr := dst.(*uint64) + *vptr = *vptr + compensatedSize(f) + return vptr, f.StatsValid() +} + +func (a compensatedSizeAnnotator) Merge(src interface{}, dst interface{}) interface{} { + srcV := src.(*uint64) + dstV := dst.(*uint64) + *dstV = *dstV + *srcV + return dstV +} + +// totalCompensatedSize computes the compensated size over a file metadata +// iterator. Note that this function is linear in the files available to the +// iterator. Use the compensatedSizeAnnotator if querying the total +// compensated size of a level. +func totalCompensatedSize(iter manifest.LevelIterator) uint64 { + var sz uint64 + for f := iter.First(); f != nil; f = iter.Next() { + sz += compensatedSize(f) + } + return sz +} + +// compactionPickerByScore holds the state and logic for picking a compaction. A +// compaction picker is associated with a single version. A new compaction +// picker is created and initialized every time a new version is installed. +type compactionPickerByScore struct { + opts *Options + vers *version + // The level to target for L0 compactions. Levels L1 to baseLevel must be + // empty. + baseLevel int + // levelMaxBytes holds the dynamically adjusted max bytes setting for each + // level. + levelMaxBytes [numLevels]int64 +} + +var _ compactionPicker = &compactionPickerByScore{} + +func (p *compactionPickerByScore) getScores(inProgress []compactionInfo) [numLevels]float64 { + var scores [numLevels]float64 + for _, info := range p.calculateLevelScores(inProgress) { + scores[info.level] = info.compensatedScoreRatio + } + return scores +} + +func (p *compactionPickerByScore) getBaseLevel() int { + if p == nil { + return 1 + } + return p.baseLevel +} + +// estimatedCompactionDebt estimates the number of bytes which need to be +// compacted before the LSM tree becomes stable. +func (p *compactionPickerByScore) estimatedCompactionDebt(l0ExtraSize uint64) uint64 { + if p == nil { + return 0 + } + + // 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 + p.vers.Levels[0].Size() + lbaseSize := p.vers.Levels[p.baseLevel].Size() + + var compactionDebt uint64 + 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 + 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 := 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 { + // We only incur compaction debt if the next level contains data. If the + // next level is empty, a move-based compaction would be used. + levelRatio := float64(nextLevelSize) / float64(levelSize) + // The current level contributes bytesAddedToNextLevel to compactions. + // 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 +} + +func (p *compactionPickerByScore) initLevelMaxBytes(inProgressCompactions []compactionInfo) { + // The levelMaxBytes calculations here differ from RocksDB in two ways: + // + // 1. The use of dbSize vs maxLevelSize. RocksDB uses the size of the maximum + // level in L1-L6, rather than determining the size of the bottom level + // based on the total amount of data in the dB. The RocksDB calculation is + // problematic if L0 contains a significant fraction of data, or if the + // level sizes are roughly equal and thus there is a significant fraction + // of data outside of the largest level. + // + // 2. Not adjusting the size of Lbase based on L0. RocksDB computes + // baseBytesMax as the maximum of the configured LBaseMaxBytes and the + // size of L0. This is problematic because baseBytesMax is used to compute + // the max size of lower levels. A very large baseBytesMax will result in + // an overly large value for the size of lower levels which will caused + // those levels not to be compacted even when they should be + // compacted. This often results in "inverted" LSM shapes where Ln is + // larger than Ln+1. + + // Determine the first non-empty level and the total DB size. + firstNonEmptyLevel := -1 + var dbSize uint64 + for level := 1; level < numLevels; level++ { + if p.vers.Levels[level].Size() > 0 { + if firstNonEmptyLevel == -1 { + firstNonEmptyLevel = level + } + dbSize += p.vers.Levels[level].Size() + } + } + for _, c := range inProgressCompactions { + if c.outputLevel == 0 || c.outputLevel == -1 { + continue + } + if c.inputs[0].level == 0 && (firstNonEmptyLevel == -1 || c.outputLevel < firstNonEmptyLevel) { + firstNonEmptyLevel = c.outputLevel + } + } + + // Initialize the max-bytes setting for each level to "infinity" which will + // disallow compaction for that level. We'll fill in the actual value below + // for levels we want to allow compactions from. + for level := 0; level < numLevels; level++ { + p.levelMaxBytes[level] = math.MaxInt64 + } + + if dbSize == 0 { + // No levels for L1 and up contain any data. Target L0 compactions for the + // last level or to the level to which there is an ongoing L0 compaction. + p.baseLevel = numLevels - 1 + if firstNonEmptyLevel >= 0 { + p.baseLevel = firstNonEmptyLevel + } + return + } + + dbSize += p.vers.Levels[0].Size() + bottomLevelSize := dbSize - dbSize/uint64(p.opts.Experimental.LevelMultiplier) + + curLevelSize := bottomLevelSize + for level := numLevels - 2; level >= firstNonEmptyLevel; level-- { + curLevelSize = uint64(float64(curLevelSize) / float64(p.opts.Experimental.LevelMultiplier)) + } + + // Compute base level (where L0 data is compacted to). + baseBytesMax := uint64(p.opts.LBaseMaxBytes) + p.baseLevel = firstNonEmptyLevel + for p.baseLevel > 1 && curLevelSize > baseBytesMax { + p.baseLevel-- + curLevelSize = uint64(float64(curLevelSize) / float64(p.opts.Experimental.LevelMultiplier)) + } + + smoothedLevelMultiplier := 1.0 + if p.baseLevel < numLevels-1 { + smoothedLevelMultiplier = math.Pow( + float64(bottomLevelSize)/float64(baseBytesMax), + 1.0/float64(numLevels-p.baseLevel-1)) + } + + levelSize := float64(baseBytesMax) + for level := p.baseLevel; level < numLevels; level++ { + if level > p.baseLevel && levelSize > 0 { + levelSize *= smoothedLevelMultiplier + } + // Round the result since test cases use small target level sizes, which + // can be impacted by floating-point imprecision + integer truncation. + roundedLevelSize := math.Round(levelSize) + if roundedLevelSize > float64(math.MaxInt64) { + p.levelMaxBytes[level] = math.MaxInt64 + } else { + p.levelMaxBytes[level] = int64(roundedLevelSize) + } + } +} + +type levelSizeAdjust struct { + incomingActualBytes uint64 + outgoingActualBytes uint64 + outgoingCompensatedBytes uint64 +} + +func (a levelSizeAdjust) compensated() uint64 { + return a.incomingActualBytes - a.outgoingCompensatedBytes +} + +func (a levelSizeAdjust) actual() uint64 { + return a.incomingActualBytes - a.outgoingActualBytes +} + +func calculateSizeAdjust(inProgressCompactions []compactionInfo) [numLevels]levelSizeAdjust { + // Compute size adjustments for each level based on the in-progress + // compactions. We sum the file sizes of all files leaving and entering each + // level in in-progress compactions. For outgoing files, we also sum a + // separate sum of 'compensated file sizes', which are inflated according + // to deletion estimates. + // + // When we adjust a level's size according to these values during score + // calculation, we subtract the compensated size of start level inputs to + // account for the fact that score calculation uses compensated sizes. + // + // Since compensated file sizes may be compensated because they reclaim + // space from the output level's files, we only add the real file size to + // the output level. + // + // This is slightly different from RocksDB's behavior, which simply elides + // compacting files from the level size calculation. + 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 := input.files.SizeSum() + compensatedSize := totalCompensatedSize(input.files.Iter()) + + if input.level != c.outputLevel { + sizeAdjust[input.level].outgoingCompensatedBytes += compensatedSize + sizeAdjust[input.level].outgoingActualBytes += actualSize + if c.outputLevel != -1 { + sizeAdjust[c.outputLevel].incomingActualBytes += actualSize + } + } + } + } + return sizeAdjust +} + +func levelCompensatedSize(lm manifest.LevelMetadata) uint64 { + return *lm.Annotation(compensatedSizeAnnotator{}).(*uint64) +} + +func (p *compactionPickerByScore) calculateLevelScores( + inProgressCompactions []compactionInfo, +) [numLevels]candidateLevelInfo { + var scores [numLevels]candidateLevelInfo + for i := range scores { + scores[i].level = i + scores[i].outputLevel = i + 1 + } + 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 := 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 {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 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. + // + // 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++ { + // 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].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(sortCompactionLevelsByPriority(scores[:])) + return scores +} + +// 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 + // designed to accommodate cases where L0 is accumulating non-overlapping + // files in L0. Letting too many non-overlapping files accumulate in few + // sublevels is undesirable, because: + // 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 := vers.Levels[0].Len() + for _, c := range inProgressCompactions { + for _, cl := range c.inputs { + if cl.level == 0 { + noncompactingFiles -= cl.files.Len() + } + } + } + fileScore := float64(noncompactingFiles) / float64(opts.L0CompactionFileThreshold) + if score < fileScore { + score = fileScore + } + return score +} + +// 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 + // bottom level in a timely fashion so as to reclaim disk space. A table's + // smallest sequence number provides a measure of its age. The ratio of + // overlapping-bytes / table-size gives an indication of write + // amplification (a smaller ratio is preferrable). + // + // The current heuristic is based off the the RocksDB kMinOverlappingRatio + // heuristic. It chooses the file with the minimum overlapping ratio with + // the target level, which minimizes write amplification. + // + // It uses a "compensated size" for the denominator, which is the file + // size but artificially inflated by an estimate of the space that may be + // reclaimed through compaction. Currently, we only compensate for range + // deletions and only with a rough estimate of the reclaimable bytes. This + // differs from RocksDB which only compensates for point tombstones and + // only if they exceed the number of non-deletion entries in table. + // + // TODO(peter): For concurrent compactions, we may want to try harder to + // pick a seed file whose resulting compaction bounds do not overlap with + // an in-progress compaction. + + cmp := opts.Comparer.Compare + startIter := vers.Levels[level].Iter() + outputIter := vers.Levels[outputLevel].Iter() + + var file manifest.LevelFile + smallestRatio := uint64(math.MaxUint64) + + outputFile := outputIter.First() + + 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 && sstableKeyCompare(cmp, outputFile.Largest, f.Smallest) < 0 { + outputFile = outputIter.Next() + } + + for outputFile != nil && sstableKeyCompare(cmp, outputFile.Smallest, f.Largest) <= 0 && !compacting { + overlappingBytes += outputFile.Size + compacting = compacting || outputFile.IsCompacting() + + // For files in the bottommost level of the LSM, the + // Stats.RangeDeletionsBytesEstimate field is set to the estimate + // of bytes /within/ the file itself that may be dropped by + // recompacting the file. These bytes from obsolete keys would not + // need to be rewritten if we compacted `f` into `outputFile`, so + // they don't contribute to write amplification. Subtracting them + // out of the overlapping bytes helps prioritize these compactions + // that are cheaper than their file sizes suggest. + if outputLevel == numLevels-1 && outputFile.LargestSeqNum < earliestSnapshotSeqNum { + overlappingBytes -= outputFile.Stats.RangeDeletionsBytesEstimate + } + + // 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. + // + // 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() + } + + // If the input level file or one of the overlapping files is + // compacting, we're not going to be able to compact this file + // anyways, so skip it. + if compacting { + continue + } + + compSz := compensatedSize(f) + scaledRatio := overlappingBytes * 1024 / compSz + if scaledRatio < smallestRatio { + smallestRatio = scaledRatio + file = startIter.Take() + } + } + return file, file.FileMetadata != nil +} + +// pickAuto picks the best compaction, if any. +// +// On each call, pickAuto computes per-level size adjustments based on +// in-progress compactions, and computes a per-level score. The levels are +// iterated over in decreasing score order trying to find a valid compaction +// anchored at that level. +// +// If a score-based compaction cannot be found, pickAuto falls back to looking +// for an elision-only compaction to remove obsolete keys. +func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompaction) { + // Compaction concurrency is controlled by L0 read-amp. We allow one + // additional compaction per L0CompactionConcurrency sublevels, as well as + // one additional compaction per CompactionDebtConcurrency bytes of + // compaction debt. Compaction concurrency is tied to L0 sublevels as that + // signal is independent of the database size. We tack on the compaction + // debt as a second signal to prevent compaction concurrency from dropping + // significantly right after a base compaction finishes, and before those + // bytes have been compacted further down the LSM. + if n := len(env.inProgressCompactions); n > 0 { + l0ReadAmp := p.vers.L0Sublevels.MaxDepthAfterOngoingCompactions() + compactionDebt := p.estimatedCompactionDebt(0) + ccSignal1 := n * p.opts.Experimental.L0CompactionConcurrency + ccSignal2 := uint64(n) * p.opts.Experimental.CompactionDebtConcurrency + if l0ReadAmp < ccSignal1 && compactionDebt < ccSignal2 { + return nil + } + } + + scores := p.calculateLevelScores(env.inProgressCompactions) + + // TODO(bananabrick): Either remove, or change this into an event sent to the + // EventListener. + logCompaction := func(pc *pickedCompaction) { + var buf bytes.Buffer + for i := 0; i < numLevels; i++ { + if i != 0 && i < p.baseLevel { + continue + } + + var info *candidateLevelInfo + for j := range scores { + if scores[j].level == i { + info = &scores[j] + break + } + } + + marker := " " + if pc.startLevel.level == info.level { + marker = "*" + } + 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.Bytes.Int64(p.levelMaxBytes[info.level]), + ) + + count := 0 + for i := range env.inProgressCompactions { + c := &env.inProgressCompactions[i] + if c.inputs[0].level != info.level { + continue + } + count++ + if count == 1 { + fmt.Fprintf(&buf, " [") + } else { + fmt.Fprintf(&buf, " ") + } + fmt.Fprintf(&buf, "L%d->L%d", c.inputs[0].level, c.outputLevel) + } + if count > 0 { + fmt.Fprintf(&buf, "]") + } + fmt.Fprintf(&buf, "\n") + } + p.opts.Logger.Infof("pickAuto: L%d->L%d\n%s", + pc.startLevel.level, pc.outputLevel.level, buf.String()) + } + + // 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.shouldCompact() { + break + } + if info.level == numLevels-1 { + continue + } + + if info.level == 0 { + 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) { + p.addScoresToPickedCompactionMetrics(pc, scores) + pc.score = info.compensatedScoreRatio + // TODO(bananabrick): Create an EventListener for logCompaction. + if false { + logCompaction(pc) + } + return pc + } + continue + } + + // info.level > 0 + var ok bool + 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.levelMaxBytes) + // Fail-safe to protect against compacting the same sstable concurrently. + if pc != nil && !inputRangeAlreadyCompacting(env, pc) { + p.addScoresToPickedCompactionMetrics(pc, scores) + pc.score = info.compensatedScoreRatio + // TODO(bananabrick): Create an EventListener for logCompaction. + if false { + logCompaction(pc) + } + return pc + } + } + + // Check for L6 files with tombstones that may be elided. These files may + // exist if a snapshot prevented the elision of a tombstone or because of + // a move compaction. These are low-priority compactions because they + // don't help us keep up with writes, just reclaim disk space. + if pc := p.pickElisionOnlyCompaction(env); pc != nil { + return pc + } + + if pc := p.pickReadTriggeredCompaction(env); pc != nil { + return pc + } + + // NB: This should only be run if a read compaction wasn't + // scheduled. + // + // We won't be scheduling a read compaction right now, and in + // read heavy workloads, compactions won't be scheduled frequently + // because flushes aren't frequent. So we need to signal to the + // iterator to schedule a compaction when it adds compactions to + // the read compaction queue. + // + // We need the nil check here because without it, we have some + // tests which don't set that variable fail. Since there's a + // chance that one of those tests wouldn't want extra compactions + // to be scheduled, I added this check here, instead of + // setting rescheduleReadCompaction in those tests. + if env.readCompactionEnv.rescheduleReadCompaction != nil { + *env.readCompactionEnv.rescheduleReadCompaction = true + } + + // At the lowest possible compaction-picking priority, look for files marked + // for compaction. Pebble will mark files for compaction if they have atomic + // compaction units that span multiple files. While current Pebble code does + // not construct such sstables, RocksDB and earlier versions of Pebble may + // have created them. These split user keys form sets of files that must be + // compacted together for correctness (referred to as "atomic compaction + // units" within the code). Rewrite them in-place. + // + // It's also possible that a file may have been marked for compaction by + // even earlier versions of Pebble code, since FileMetadata's + // MarkedForCompaction field is persisted in the manifest. That's okay. We + // previously would've ignored the designation, whereas now we'll re-compact + // the file in place. + if p.vers.Stats.MarkedForCompaction > 0 { + if pc := p.pickRewriteCompaction(env); pc != nil { + return pc + } + } + + 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. +// If multiple files meet the criteria, it chooses whichever file has the +// lowest LargestSeqNum. The lowest LargestSeqNum file will be the first +// eligible for an elision-only compaction once snapshots less than or equal +// to its LargestSeqNum are closed. +type elisionOnlyAnnotator struct{} + +var _ manifest.Annotator = elisionOnlyAnnotator{} + +func (a elisionOnlyAnnotator) Zero(interface{}) interface{} { + return nil +} + +func (a elisionOnlyAnnotator) Accumulate(f *fileMetadata, dst interface{}) (interface{}, bool) { + if f.IsCompacting() { + return dst, true + } + if !f.StatsValid() { + return dst, false + } + // Bottommost files are large and not worthwhile to compact just + // to remove a few tombstones. Consider a file ineligible if its + // own range deletions delete less than 10% of its data and its + // deletion tombstones make up less than 10% of its entries. + // + // TODO(jackson): This does not account for duplicate user keys + // which may be collapsed. Ideally, we would have 'obsolete keys' + // statistics that would include tombstones, the keys that are + // dropped by tombstones and duplicated user keys. See #847. + // + // Note that tables that contain exclusively range keys (i.e. no point keys, + // `NumEntries` and `RangeDeletionsBytesEstimate` are both zero) are excluded + // from elision-only compactions. + // TODO(travers): Consider an alternative heuristic for elision of range-keys. + if f.Stats.RangeDeletionsBytesEstimate*10 < f.Size && + f.Stats.NumDeletions*10 <= f.Stats.NumEntries { + return dst, true + } + if dst == nil { + return f, true + } else if dstV := dst.(*fileMetadata); dstV.LargestSeqNum > f.LargestSeqNum { + return f, true + } + return dst, true +} + +func (a elisionOnlyAnnotator) Merge(v interface{}, accum interface{}) interface{} { + if v == nil { + return accum + } + // If we haven't accumulated an eligible file yet, or f's LargestSeqNum is + // less than the accumulated file's, use f. + if accum == nil { + return v + } + f := v.(*fileMetadata) + accumV := accum.(*fileMetadata) + if accumV == nil || accumV.LargestSeqNum > f.LargestSeqNum { + return f + } + return accumV +} + +// markedForCompactionAnnotator implements the manifest.Annotator interface, +// annotating B-Tree nodes with the *fileMetadata of a file that is marked for +// compaction within the subtree. If multiple files meet the criteria, it +// chooses whichever file has the lowest LargestSeqNum. +type markedForCompactionAnnotator struct{} + +var _ manifest.Annotator = markedForCompactionAnnotator{} + +func (a markedForCompactionAnnotator) Zero(interface{}) interface{} { + return nil +} + +func (a markedForCompactionAnnotator) Accumulate( + f *fileMetadata, dst interface{}, +) (interface{}, bool) { + if !f.MarkedForCompaction { + // Not marked for compaction; return dst. + return dst, true + } + return markedMergeHelper(f, dst) +} + +func (a markedForCompactionAnnotator) Merge(v interface{}, accum interface{}) interface{} { + if v == nil { + return accum + } + accum, _ = markedMergeHelper(v.(*fileMetadata), accum) + return accum +} + +// REQUIRES: f is non-nil, and f.MarkedForCompaction=true. +func markedMergeHelper(f *fileMetadata, dst interface{}) (interface{}, bool) { + if dst == nil { + return f, true + } else if dstV := dst.(*fileMetadata); dstV.LargestSeqNum > f.LargestSeqNum { + return f, true + } + return dst, true +} + +// pickElisionOnlyCompaction looks for compactions of sstables in the +// bottommost level containing obsolete records that may now be dropped. +func (p *compactionPickerByScore) pickElisionOnlyCompaction( + env compactionEnv, +) (pc *pickedCompaction) { + if p.opts.private.disableElisionOnlyCompactions { + return nil + } + v := p.vers.Levels[numLevels-1].Annotation(elisionOnlyAnnotator{}) + if v == nil { + return nil + } + candidate := v.(*fileMetadata) + if candidate.IsCompacting() || candidate.LargestSeqNum >= env.earliestSnapshotSeqNum { + return nil + } + lf := p.vers.Levels[numLevels-1].Find(p.opts.Comparer.Compare, candidate) + if lf == nil { + panic(fmt.Sprintf("file %s not found in level %d as expected", candidate.FileNum, numLevels-1)) + } + + // Construct a picked compaction of the elision candidate's atomic + // compaction unit. + pc = newPickedCompaction(p.opts, p.vers, numLevels-1, numLevels-1, p.baseLevel) + pc.kind = compactionKindElisionOnly + var isCompacting bool + pc.startLevel.files, isCompacting = expandToAtomicUnit(p.opts.Comparer.Compare, lf.Slice(), false /* disableIsCompacting */) + if isCompacting { + return nil + } + pc.smallest, pc.largest = manifest.KeyRange(pc.cmp, pc.startLevel.files.Iter()) + // Fail-safe to protect against compacting the same sstable concurrently. + if !inputRangeAlreadyCompacting(env, pc) { + return pc + } + return nil +} + +// pickRewriteCompaction attempts to construct a compaction that +// rewrites a file marked for compaction. pickRewriteCompaction will +// pull in adjacent files in the file's atomic compaction unit if +// necessary. A rewrite compaction outputs files to the same level as +// the input level. +func (p *compactionPickerByScore) pickRewriteCompaction(env compactionEnv) (pc *pickedCompaction) { + for l := numLevels - 1; l >= 0; l-- { + v := p.vers.Levels[l].Annotation(markedForCompactionAnnotator{}) + if v == nil { + // Try the next level. + continue + } + candidate := v.(*fileMetadata) + if candidate.IsCompacting() { + // Try the next level. + continue + } + lf := p.vers.Levels[l].Find(p.opts.Comparer.Compare, candidate) + if lf == nil { + panic(fmt.Sprintf("file %s not found in level %d as expected", candidate.FileNum, numLevels-1)) + } + + inputs := lf.Slice() + // L0 files generated by a flush have never been split such that + // adjacent files can contain the same user key. So we do not need to + // rewrite an atomic compaction unit for L0. Note that there is nothing + // preventing two different flushes from producing files that are + // non-overlapping from an InternalKey perspective, but span the same + // user key. However, such files cannot be in the same L0 sublevel, + // since each sublevel requires non-overlapping user keys (unlike other + // levels). + if l > 0 { + // Find this file's atomic compaction unit. This is only relevant + // for levels L1+. + var isCompacting bool + inputs, isCompacting = expandToAtomicUnit( + p.opts.Comparer.Compare, + inputs, + false, /* disableIsCompacting */ + ) + if isCompacting { + // Try the next level. + continue + } + } + + pc = newPickedCompaction(p.opts, p.vers, l, l, p.baseLevel) + pc.outputLevel.level = l + pc.kind = compactionKindRewrite + pc.startLevel.files = inputs + pc.smallest, pc.largest = manifest.KeyRange(pc.cmp, pc.startLevel.files.Iter()) + + // Fail-safe to protect against compacting the same sstable concurrently. + if !inputRangeAlreadyCompacting(env, pc) { + if pc.startLevel.level == 0 { + pc.startLevel.l0SublevelInfo = generateSublevelInfo(pc.cmp, pc.startLevel.files) + } + return pc + } + } + return nil +} + +// pickAutoLPositive picks an automatic compaction for the candidate +// file in a positive-numbered level. This function must not be used for +// L0. +func pickAutoLPositive( + env compactionEnv, + opts *Options, + vers *version, + cInfo candidateLevelInfo, + baseLevel int, + levelMaxBytes [numLevels]int64, +) (pc *pickedCompaction) { + if cInfo.level == 0 { + panic("pebble: pickAutoLPositive called for L0") + } + + pc = newPickedCompaction(opts, vers, cInfo.level, defaultOutputLevel(cInfo.level, baseLevel), baseLevel) + if pc.outputLevel.level != cInfo.outputLevel { + panic("pebble: compaction picked unexpected output level") + } + pc.startLevel.files = cInfo.file.Slice() + // Files in level 0 may overlap each other, so pick up all overlapping ones. + if pc.startLevel.level == 0 { + cmp := opts.Comparer.Compare + smallest, largest := manifest.KeyRange(cmp, pc.startLevel.files.Iter()) + pc.startLevel.files = vers.Overlaps(0, cmp, smallest.UserKey, + largest.UserKey, largest.IsExclusiveSentinel()) + if pc.startLevel.files.Empty() { + panic("pebble: empty compaction") + } + } + + if !pc.setupInputs(opts, env.diskAvailBytes, pc.startLevel) { + return nil + } + 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, 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.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. + 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) (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 + // compactions, and increasing read amplification in L0. + // + // TODO(bilal) Remove the minCompactionDepth parameter once fixing it at 1 + // has been shown to not cause a performance regression. + lcf, err := vers.L0Sublevels.PickBaseCompaction(1, vers.Levels[baseLevel].Slice()) + if err != nil { + opts.Logger.Errorf("error when picking base compaction: %s", err) + return + } + if lcf != nil { + pc = newPickedCompactionFromL0(lcf, opts, vers, baseLevel, true) + pc.setupInputs(opts, env.diskAvailBytes, pc.startLevel) + if pc.startLevel.files.Empty() { + opts.Logger.Fatalf("empty compaction chosen") + } + return pc.maybeAddLevel(opts, env.diskAvailBytes) + } + + // Couldn't choose a base compaction. Try choosing an intra-L0 + // compaction. Note that we pass in L0CompactionThreshold here as opposed to + // 1, since choosing a single sublevel intra-L0 compaction is + // counterproductive. + lcf, err = vers.L0Sublevels.PickIntraL0Compaction(env.earliestUnflushedSeqNum, minIntraL0Count) + if err != nil { + opts.Logger.Errorf("error when picking intra-L0 compaction: %s", err) + return + } + if lcf != nil { + pc = newPickedCompactionFromL0(lcf, opts, vers, 0, false) + if !pc.setupInputs(opts, env.diskAvailBytes, pc.startLevel) { + return nil + } + if pc.startLevel.files.Empty() { + opts.Logger.Fatalf("empty compaction chosen") + } + { + iter := pc.startLevel.files.Iter() + if iter.First() == nil || iter.Next() == nil { + // A single-file intra-L0 compaction is unproductive. + return nil + } + } + + pc.smallest, pc.largest = manifest.KeyRange(pc.cmp, pc.startLevel.files.Iter()) + } + return pc +} + +func pickManualCompaction( + vers *version, opts *Options, env compactionEnv, baseLevel int, manual *manualCompaction, +) (pc *pickedCompaction, retryLater bool) { + outputLevel := manual.level + 1 + if manual.level == 0 { + 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 + // sstables in Lbase to Lbase+1 which caused Lbase to change. Simply + // ignore this manual compaction as there is nothing to do (manual.level + // points to an empty level). + return nil, false + } + // This conflictsWithInProgress call is necessary for the manual compaction to + // be retried when it conflicts with an ongoing automatic compaction. Without + // 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, opts.Comparer.Compare) { + return nil, true + } + 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 { + if len(pc.extraLevels) > 0 { + // multilevel compactions relax this invariant + } else { + panic("pebble: compaction picked unexpected output level") + } + } + // Fail-safe to protect against compacting the same sstable concurrently. + if inputRangeAlreadyCompacting(env, pc) { + return nil, true + } + return pc, false +} + +func (p *compactionPickerByScore) pickReadTriggeredCompaction( + env compactionEnv, +) (pc *pickedCompaction) { + // If a flush is in-progress or expected to happen soon, it means more writes are taking place. We would + // soon be scheduling more write focussed compactions. In this case, skip read compactions as they are + // lower priority. + if env.readCompactionEnv.flushing || env.readCompactionEnv.readCompactions == nil { + return nil + } + for env.readCompactionEnv.readCompactions.size > 0 { + rc := env.readCompactionEnv.readCompactions.remove() + if pc = pickReadTriggeredCompactionHelper(p, rc, env); pc != nil { + break + } + } + return pc +} + +func pickReadTriggeredCompactionHelper( + p *compactionPickerByScore, rc *readCompaction, env compactionEnv, +) (pc *pickedCompaction) { + cmp := p.opts.Comparer.Compare + overlapSlice := p.vers.Overlaps(rc.level, cmp, rc.start, rc.end, false /* exclusiveEnd */) + if overlapSlice.Empty() { + // If there is no overlap, then the file with the key range + // must have been compacted away. So, we don't proceed to + // compact the same key range again. + return nil + } + + iter := overlapSlice.Iter() + var fileMatches bool + for f := iter.First(); f != nil; f = iter.Next() { + if f.FileNum == rc.fileNum { + fileMatches = true + break + } + } + if !fileMatches { + return nil + } + + pc = newPickedCompaction(p.opts, p.vers, rc.level, defaultOutputLevel(rc.level, p.baseLevel), p.baseLevel) + + pc.startLevel.files = overlapSlice + if !pc.setupInputs(p.opts, env.diskAvailBytes, pc.startLevel) { + return nil + } + if inputRangeAlreadyCompacting(env, pc) { + return nil + } + pc.kind = compactionKindRead + + // Prevent read compactions which are too wide. + outputOverlaps := pc.version.Overlaps( + pc.outputLevel.level, pc.cmp, pc.smallest.UserKey, + pc.largest.UserKey, pc.largest.IsExclusiveSentinel()) + if outputOverlaps.SizeSum() > pc.maxReadCompactionBytes { + return nil + } + + // Prevent compactions which start with a small seed file X, but overlap + // with over allowedCompactionWidth * X file sizes in the output layer. + const allowedCompactionWidth = 35 + if outputOverlaps.SizeSum() > overlapSlice.SizeSum()*allowedCompactionWidth { + return nil + } + + return pc +} + +func (p *compactionPickerByScore) forceBaseLevel1() { + p.baseLevel = 1 +} + +func inputRangeAlreadyCompacting(env compactionEnv, pc *pickedCompaction) bool { + for _, cl := range pc.inputs { + iter := cl.files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if f.IsCompacting() { + return true + } + } + } + + // Look for active compactions outputting to the same region of the key + // space in the same output level. Two potential compactions may conflict + // without sharing input files if there are no files in the output level + // that overlap with the intersection of the compactions' key spaces. + // + // Consider an active L0->Lbase compaction compacting two L0 files one + // [a-f] and the other [t-z] into Lbase. + // + // L0 + // ↦ 000100 ↤ ↦ 000101 ↤ + // L1 + // ↦ 000004 ↤ + // a b c d e f g h i j k l m n o p q r s t u v w x y z + // + // If a new file 000102 [j-p] is flushed while the existing compaction is + // still ongoing, new file would not be in any compacting sublevel + // intervals and would not overlap with any Lbase files that are also + // compacting. However, this compaction cannot be picked because the + // compaction's output key space [j-p] would overlap the existing + // compaction's output key space [a-z]. + // + // L0 + // ↦ 000100* ↤ ↦ 000102 ↤ ↦ 000101* ↤ + // L1 + // ↦ 000004* ↤ + // a b c d e f g h i j k l m n o p q r s t u v w x y z + // + // * - currently compacting + if pc.outputLevel != nil && pc.outputLevel.level != 0 { + for _, c := range env.inProgressCompactions { + if pc.outputLevel.level != c.outputLevel { + continue + } + if base.InternalCompare(pc.cmp, c.largest, pc.smallest) < 0 || + base.InternalCompare(pc.cmp, c.smallest, pc.largest) > 0 { + continue + } + + // The picked compaction and the in-progress compaction c are + // outputting to the same region of the key space of the same + // level. + return true + } + } + return false +} + +// conflictsWithInProgress checks if there are any in-progress compactions with overlapping keyspace. +func conflictsWithInProgress( + manual *manualCompaction, outputLevel int, inProgressCompactions []compactionInfo, cmp Compare, +) bool { + for _, c := range inProgressCompactions { + if (c.outputLevel == manual.level || c.outputLevel == outputLevel) && + isUserKeysOverlapping(manual.start, manual.end, c.smallest.UserKey, c.largest.UserKey, cmp) { + return true + } + for _, in := range c.inputs { + if in.files.Empty() { + continue + } + iter := in.files.Iter() + smallest := iter.First().Smallest.UserKey + largest := iter.Last().Largest.UserKey + if (in.level == manual.level || in.level == outputLevel) && + isUserKeysOverlapping(manual.start, manual.end, smallest, largest, cmp) { + return true + } + } + } + return false +} + +func isUserKeysOverlapping(x1, x2, y1, y2 []byte, cmp Compare) bool { + return cmp(x1, y2) <= 0 && cmp(y1, x2) <= 0 +} diff --git a/pebble/compaction_picker_test.go b/pebble/compaction_picker_test.go new file mode 100644 index 0000000..b0ace35 --- /dev/null +++ b/pebble/compaction_picker_test.go @@ -0,0 +1,1593 @@ +// Copyright 2018 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 ( + "bytes" + "fmt" + "math" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func loadVersion(t *testing.T, d *datadriven.TestData) (*version, *Options, string) { + var sizes [numLevels]int64 + opts := &Options{} + opts.testingRandomized(t) + opts.EnsureDefaults() + + if len(d.CmdArgs) != 1 { + return nil, nil, fmt.Sprintf("%s expects 1 argument", d.Cmd) + } + var err error + opts.LBaseMaxBytes, err = strconv.ParseInt(d.CmdArgs[0].Key, 10, 64) + if err != nil { + return nil, nil, err.Error() + } + + var files [numLevels][]*fileMetadata + if len(d.Input) > 0 { + // Parse each line as + // + // : [compensation] + // + // Creating sstables within the level whose file sizes total to `size` + // and whose compensated file sizes total to `size`+`compensation`. If + // size is sufficiently large, only one single file is created. See + // the TODO below. + for _, data := range strings.Split(d.Input, "\n") { + parts := strings.Split(data, " ") + parts[0] = strings.TrimSuffix(strings.TrimSpace(parts[0]), ":") + if len(parts) < 2 { + return nil, nil, fmt.Sprintf("malformed test:\n%s", d.Input) + } + level, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, nil, err.Error() + } + if files[level] != nil { + return nil, nil, fmt.Sprintf("level %d already filled", level) + } + size, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 64) + if err != nil { + return nil, nil, err.Error() + } + var compensation uint64 + if len(parts) == 3 { + compensation, err = strconv.ParseUint(strings.TrimSpace(parts[2]), 10, 64) + if err != nil { + return nil, nil, err.Error() + } + } + + var lastFile *fileMetadata + for i := uint64(1); sizes[level] < int64(size); i++ { + var key InternalKey + if level == 0 { + // For L0, make `size` overlapping files. + key = base.MakeInternalKey([]byte(fmt.Sprintf("%04d", 1)), i, InternalKeyKindSet) + } else { + key = base.MakeInternalKey([]byte(fmt.Sprintf("%04d", i)), i, InternalKeyKindSet) + } + m := (&fileMetadata{ + FileNum: base.FileNum(uint64(level)*100_000 + i), + SmallestSeqNum: key.SeqNum(), + LargestSeqNum: key.SeqNum(), + Size: 1, + Stats: manifest.TableStats{ + RangeDeletionsBytesEstimate: 0, + }, + }).ExtendPointKeyBounds(opts.Comparer.Compare, key, key) + m.InitPhysicalBacking() + m.StatsMarkValid() + lastFile = m + if size >= 100 { + // If the requested size of the level is very large only add a single + // file in order to avoid massive blow-up in the number of files in + // the Version. + // + // TODO(peter): There is tension between the testing in + // TestCompactionPickerLevelMaxBytes and + // TestCompactionPickerTargetLevel. Clean this up somehow. + m.Size = size + if level != 0 { + endKey := base.MakeInternalKey([]byte(fmt.Sprintf("%04d", size)), i, InternalKeyKindSet) + m.ExtendPointKeyBounds(opts.Comparer.Compare, key, endKey) + } + } + files[level] = append(files[level], m) + sizes[level] += int64(m.Size) + } + // Let all the compensation be due to the last file. + if lastFile != nil && compensation > 0 { + lastFile.Stats.RangeDeletionsBytesEstimate = compensation + } + } + } + + vers := newVersion(opts, files) + return vers, opts, "" +} + +func TestCompactionPickerByScoreLevelMaxBytes(t *testing.T) { + datadriven.RunTest(t, "testdata/compaction_picker_level_max_bytes", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "init": + vers, opts, errMsg := loadVersion(t, d) + if errMsg != "" { + return errMsg + } + + p, ok := newCompactionPicker(vers, opts, nil).(*compactionPickerByScore) + require.True(t, ok) + var buf bytes.Buffer + for level := p.getBaseLevel(); level < numLevels; level++ { + fmt.Fprintf(&buf, "%d: %d\n", level, p.levelMaxBytes[level]) + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestCompactionPickerTargetLevel(t *testing.T) { + var vers *version + var opts *Options + var pickerByScore *compactionPickerByScore + + parseInProgress := func(vals []string) ([]compactionInfo, error) { + var levels []int + for _, s := range vals { + l, err := strconv.ParseInt(s, 10, 8) + if err != nil { + return nil, err + } + levels = append(levels, int(l)) + } + if len(levels)%2 != 0 { + return nil, errors.New("odd number of levels with ongoing compactions") + } + var inProgress []compactionInfo + for i := 0; i < len(levels); i += 2 { + inProgress = append(inProgress, compactionInfo{ + inputs: []compactionLevel{ + {level: levels[i]}, + {level: levels[i+1]}, + }, + outputLevel: levels[i+1], + }) + } + return inProgress, nil + } + + resetCompacting := func() { + for _, files := range vers.Levels { + files.Slice().Each(func(f *fileMetadata) { + f.CompactionState = manifest.CompactionStateNotCompacting + }) + } + } + + datadriven.RunTest(t, "testdata/compaction_picker_target_level", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "init": + // loadVersion expects a single datadriven argument that it + // sets as Options.LBaseMaxBytes. It parses the input as + // newline-separated levels, specifying the level's file size + // and optionally additional compensation to be added during + // compensated file size calculations. Eg: + // + // init + // : [compensation] + // : [compensation] + var errMsg string + vers, opts, errMsg = loadVersion(t, d) + if errMsg != "" { + return errMsg + } + return runVersionFileSizes(vers) + case "init_cp": + resetCompacting() + + var inProgress []compactionInfo + if arg, ok := d.Arg("ongoing"); ok { + var err error + inProgress, err = parseInProgress(arg.Vals) + if err != nil { + return err.Error() + } + } + + p := newCompactionPicker(vers, opts, inProgress) + var ok bool + pickerByScore, ok = p.(*compactionPickerByScore) + require.True(t, ok) + return fmt.Sprintf("base: %d", pickerByScore.baseLevel) + case "queue": + var b strings.Builder + var inProgress []compactionInfo + for { + env := compactionEnv{ + diskAvailBytes: math.MaxUint64, + earliestUnflushedSeqNum: InternalKeySeqNumMax, + inProgressCompactions: inProgress, + } + pc := pickerByScore.pickAuto(env) + if pc == nil { + break + } + fmt.Fprintf(&b, "L%d->L%d: %.1f\n", pc.startLevel.level, pc.outputLevel.level, pc.score) + inProgress = append(inProgress, compactionInfo{ + inputs: pc.inputs, + outputLevel: pc.outputLevel.level, + smallest: pc.smallest, + largest: pc.largest, + }) + if pc.outputLevel.level == 0 { + // Once we pick one L0->L0 compaction, we'll keep on doing so + // because the test isn't marking files as Compacting. + break + } + for _, cl := range pc.inputs { + cl.files.Each(func(f *fileMetadata) { + f.CompactionState = manifest.CompactionStateCompacting + fmt.Fprintf(&b, " %s marked as compacting\n", f) + }) + } + } + + resetCompacting() + return b.String() + case "pick": + resetCompacting() + + var inProgress []compactionInfo + if len(d.CmdArgs) == 1 { + arg := d.CmdArgs[0] + if arg.Key != "ongoing" { + return "unknown arg: " + arg.Key + } + var err error + inProgress, err = parseInProgress(arg.Vals) + if err != nil { + return err.Error() + } + } + + // Mark files as compacting for each in-progress compaction. + for i := range inProgress { + c := &inProgress[i] + for j, cl := range c.inputs { + iter := vers.Levels[cl.level].Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if !f.IsCompacting() { + f.CompactionState = manifest.CompactionStateCompacting + c.inputs[j].files = iter.Take().Slice() + break + } + } + } + if c.inputs[0].level == 0 && c.outputLevel != 0 { + // L0->Lbase: mark all of Lbase as compacting. + c.inputs[1].files = vers.Levels[c.outputLevel].Slice() + for _, in := range c.inputs { + in.files.Each(func(f *fileMetadata) { + f.CompactionState = manifest.CompactionStateCompacting + }) + } + } + } + + var b strings.Builder + fmt.Fprintf(&b, "Initial state before pick:\n%s", runVersionFileSizes(vers)) + pc := pickerByScore.pickAuto(compactionEnv{ + earliestUnflushedSeqNum: InternalKeySeqNumMax, + inProgressCompactions: inProgress, + }) + if pc != nil { + fmt.Fprintf(&b, "Picked: L%d->L%d: %0.1f\n", pc.startLevel.level, pc.outputLevel.level, pc.score) + } + if pc == nil { + fmt.Fprintln(&b, "Picked: no compaction") + } + return b.String() + case "pick_manual": + var startLevel int + var start, end string + d.MaybeScanArgs(t, "level", &startLevel) + d.MaybeScanArgs(t, "start", &start) + d.MaybeScanArgs(t, "end", &end) + + iStart := base.MakeInternalKey([]byte(start), InternalKeySeqNumMax, InternalKeyKindMax) + iEnd := base.MakeInternalKey([]byte(end), 0, 0) + manual := &manualCompaction{ + done: make(chan error, 1), + level: startLevel, + start: iStart.UserKey, + end: iEnd.UserKey, + } + + pc, retryLater := pickManualCompaction( + pickerByScore.vers, + opts, + compactionEnv{ + earliestUnflushedSeqNum: InternalKeySeqNumMax, + }, + pickerByScore.getBaseLevel(), + manual) + if pc == nil { + return fmt.Sprintf("nil, retryLater = %v", retryLater) + } + + return fmt.Sprintf("L%d->L%d, retryLater = %v", pc.startLevel.level, pc.outputLevel.level, retryLater) + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestCompactionPickerEstimatedCompactionDebt(t *testing.T) { + datadriven.RunTest(t, "testdata/compaction_picker_estimated_debt", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "init": + vers, opts, errMsg := loadVersion(t, d) + if errMsg != "" { + return errMsg + } + opts.MemTableSize = 1000 + + p := newCompactionPicker(vers, opts, nil) + return fmt.Sprintf("%d\n", p.estimatedCompactionDebt(0)) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestCompactionPickerL0(t *testing.T) { + opts := (*Options)(nil).EnsureDefaults() + opts.Experimental.L0CompactionConcurrency = 1 + + parseMeta := func(s string) (*fileMetadata, error) { + parts := strings.Split(s, ":") + fileNum, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + fields := strings.Fields(parts[1]) + parts = strings.Split(fields[0], "-") + if len(parts) != 2 { + return nil, errors.Errorf("malformed table spec: %s", s) + } + m := (&fileMetadata{ + FileNum: base.FileNum(fileNum), + }).ExtendPointKeyBounds( + opts.Comparer.Compare, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + m.InitPhysicalBacking() + return m, nil + } + + var picker *compactionPickerByScore + var inProgressCompactions []compactionInfo + var pc *pickedCompaction + + datadriven.RunTest(t, "testdata/compaction_picker_L0", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + fileMetas := [manifest.NumLevels][]*fileMetadata{} + baseLevel := manifest.NumLevels - 1 + level := 0 + var err error + lines := strings.Split(td.Input, "\n") + var compactionLines []string + + for len(lines) > 0 { + data := strings.TrimSpace(lines[0]) + lines = lines[1:] + switch data { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err = strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + case "compactions": + compactionLines, lines = lines, nil + default: + meta, err := parseMeta(data) + if err != nil { + return err.Error() + } + if level != 0 && level < baseLevel { + baseLevel = level + } + fileMetas[level] = append(fileMetas[level], meta) + } + } + + // Parse in-progress compactions in the form of: + // L0 000001 -> L2 000005 + inProgressCompactions = nil + for len(compactionLines) > 0 { + parts := strings.Fields(compactionLines[0]) + compactionLines = compactionLines[1:] + + var level int + var info compactionInfo + first := true + compactionFiles := map[int][]*fileMetadata{} + for _, p := range parts { + switch p { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + var err error + level, err = strconv.Atoi(p[1:]) + if err != nil { + return err.Error() + } + if len(info.inputs) > 0 && info.inputs[len(info.inputs)-1].level == level { + // eg, L0 -> L0 compaction or L6 -> L6 compaction + continue + } + if info.outputLevel < level { + info.outputLevel = level + } + info.inputs = append(info.inputs, compactionLevel{level: level}) + case "->": + continue + default: + fileNum, err := strconv.Atoi(p) + if err != nil { + return err.Error() + } + var compactFile *fileMetadata + for _, m := range fileMetas[level] { + if m.FileNum == FileNum(fileNum) { + compactFile = m + } + } + if compactFile == nil { + return fmt.Sprintf("cannot find compaction file %s", FileNum(fileNum)) + } + compactFile.CompactionState = manifest.CompactionStateCompacting + if first || base.InternalCompare(DefaultComparer.Compare, info.largest, compactFile.Largest) < 0 { + info.largest = compactFile.Largest + } + if first || base.InternalCompare(DefaultComparer.Compare, info.smallest, compactFile.Smallest) > 0 { + info.smallest = compactFile.Smallest + } + first = false + compactionFiles[level] = append(compactionFiles[level], compactFile) + } + } + for i, cl := range info.inputs { + files := compactionFiles[cl.level] + info.inputs[i].files = manifest.NewLevelSliceSeqSorted(files) + // Mark as intra-L0 compacting if the compaction is + // L0 -> L0. + if info.outputLevel == 0 { + for _, f := range files { + f.IsIntraL0Compacting = true + } + } + } + inProgressCompactions = append(inProgressCompactions, info) + } + + version := newVersion(opts, fileMetas) + version.L0Sublevels.InitCompactingFileInfo(inProgressL0Compactions(inProgressCompactions)) + vs := &versionSet{ + opts: opts, + cmp: DefaultComparer.Compare, + cmpName: DefaultComparer.Name, + } + vs.versions.Init(nil) + vs.append(version) + picker = &compactionPickerByScore{ + opts: opts, + vers: version, + baseLevel: baseLevel, + } + vs.picker = picker + picker.initLevelMaxBytes(inProgressCompactions) + + var buf bytes.Buffer + fmt.Fprint(&buf, version.String()) + if len(inProgressCompactions) > 0 { + fmt.Fprintln(&buf, "compactions") + for _, c := range inProgressCompactions { + fmt.Fprintf(&buf, " %s\n", c.String()) + } + } + return buf.String() + case "pick-auto": + td.MaybeScanArgs(t, "l0_compaction_threshold", &opts.L0CompactionThreshold) + td.MaybeScanArgs(t, "l0_compaction_file_threshold", &opts.L0CompactionFileThreshold) + + pc = picker.pickAuto(compactionEnv{ + diskAvailBytes: math.MaxUint64, + earliestUnflushedSeqNum: math.MaxUint64, + inProgressCompactions: inProgressCompactions, + }) + var result strings.Builder + if pc != nil { + checkClone(t, pc) + c := newCompaction(pc, opts, time.Now(), nil /* provider */) + fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) + fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) + if !pc.outputLevel.files.Empty() { + fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) + } + if !c.grandparents.Empty() { + fmt.Fprintf(&result, "grandparents: %s\n", fileNums(c.grandparents)) + } + } else { + return "nil" + } + return result.String() + case "mark-for-compaction": + var fileNum uint64 + td.ScanArgs(t, "file", &fileNum) + for l, lm := range picker.vers.Levels { + iter := lm.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if f.FileNum != base.FileNum(fileNum) { + continue + } + f.MarkedForCompaction = true + picker.vers.Stats.MarkedForCompaction++ + picker.vers.Levels[l].InvalidateAnnotation(markedForCompactionAnnotator{}) + return fmt.Sprintf("marked L%d.%s", l, f.FileNum) + } + } + return "not-found" + case "max-output-file-size": + if pc == nil { + return "no compaction" + } + return fmt.Sprintf("%d", pc.maxOutputFileSize) + case "max-overlap-bytes": + if pc == nil { + return "no compaction" + } + return fmt.Sprintf("%d", pc.maxOverlapBytes) + } + return fmt.Sprintf("unrecognized command: %s", td.Cmd) + }) +} + +func TestCompactionPickerConcurrency(t *testing.T) { + opts := (*Options)(nil).EnsureDefaults() + opts.Experimental.L0CompactionConcurrency = 1 + + parseMeta := func(s string) (*fileMetadata, error) { + parts := strings.Split(s, ":") + fileNum, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + fields := strings.Fields(parts[1]) + parts = strings.Split(fields[0], "-") + if len(parts) != 2 { + return nil, errors.Errorf("malformed table spec: %s", s) + } + m := (&fileMetadata{ + FileNum: base.FileNum(fileNum), + Size: 1028, + }).ExtendPointKeyBounds( + opts.Comparer.Compare, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + m.InitPhysicalBacking() + for _, p := range fields[1:] { + if strings.HasPrefix(p, "size=") { + v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) + if err != nil { + return nil, err + } + m.Size = uint64(v) + } + } + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + return m, nil + } + + var picker *compactionPickerByScore + var inProgressCompactions []compactionInfo + + datadriven.RunTest(t, "testdata/compaction_picker_concurrency", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + fileMetas := [manifest.NumLevels][]*fileMetadata{} + level := 0 + var err error + lines := strings.Split(td.Input, "\n") + var compactionLines []string + + for len(lines) > 0 { + data := strings.TrimSpace(lines[0]) + lines = lines[1:] + switch data { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err = strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + case "compactions": + compactionLines, lines = lines, nil + default: + meta, err := parseMeta(data) + if err != nil { + return err.Error() + } + fileMetas[level] = append(fileMetas[level], meta) + } + } + + // Parse in-progress compactions in the form of: + // L0 000001 -> L2 000005 + inProgressCompactions = nil + for len(compactionLines) > 0 { + parts := strings.Fields(compactionLines[0]) + compactionLines = compactionLines[1:] + + var level int + var info compactionInfo + first := true + compactionFiles := map[int][]*fileMetadata{} + for _, p := range parts { + switch p { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + var err error + level, err = strconv.Atoi(p[1:]) + if err != nil { + return err.Error() + } + if len(info.inputs) > 0 && info.inputs[len(info.inputs)-1].level == level { + // eg, L0 -> L0 compaction or L6 -> L6 compaction + continue + } + if info.outputLevel < level { + info.outputLevel = level + } + info.inputs = append(info.inputs, compactionLevel{level: level}) + case "->": + continue + default: + fileNum, err := strconv.Atoi(p) + if err != nil { + return err.Error() + } + var compactFile *fileMetadata + for _, m := range fileMetas[level] { + if m.FileNum == FileNum(fileNum) { + compactFile = m + } + } + if compactFile == nil { + return fmt.Sprintf("cannot find compaction file %s", FileNum(fileNum)) + } + compactFile.CompactionState = manifest.CompactionStateCompacting + if first || base.InternalCompare(DefaultComparer.Compare, info.largest, compactFile.Largest) < 0 { + info.largest = compactFile.Largest + } + if first || base.InternalCompare(DefaultComparer.Compare, info.smallest, compactFile.Smallest) > 0 { + info.smallest = compactFile.Smallest + } + first = false + compactionFiles[level] = append(compactionFiles[level], compactFile) + } + } + for i, cl := range info.inputs { + files := compactionFiles[cl.level] + if cl.level == 0 { + info.inputs[i].files = manifest.NewLevelSliceSeqSorted(files) + } else { + info.inputs[i].files = manifest.NewLevelSliceKeySorted(DefaultComparer.Compare, files) + } + // Mark as intra-L0 compacting if the compaction is + // L0 -> L0. + if info.outputLevel == 0 { + for _, f := range files { + f.IsIntraL0Compacting = true + } + } + } + inProgressCompactions = append(inProgressCompactions, info) + } + + version := newVersion(opts, fileMetas) + version.L0Sublevels.InitCompactingFileInfo(inProgressL0Compactions(inProgressCompactions)) + vs := &versionSet{ + opts: opts, + cmp: DefaultComparer.Compare, + cmpName: DefaultComparer.Name, + } + vs.versions.Init(nil) + vs.append(version) + + picker = newCompactionPicker(version, opts, inProgressCompactions).(*compactionPickerByScore) + vs.picker = picker + + var buf bytes.Buffer + fmt.Fprint(&buf, version.String()) + if len(inProgressCompactions) > 0 { + fmt.Fprintln(&buf, "compactions") + for _, c := range inProgressCompactions { + fmt.Fprintf(&buf, " %s\n", c.String()) + } + } + return buf.String() + + case "pick-auto": + td.MaybeScanArgs(t, "l0_compaction_threshold", &opts.L0CompactionThreshold) + td.MaybeScanArgs(t, "l0_compaction_concurrency", &opts.Experimental.L0CompactionConcurrency) + td.MaybeScanArgs(t, "compaction_debt_concurrency", &opts.Experimental.CompactionDebtConcurrency) + + pc := picker.pickAuto(compactionEnv{ + earliestUnflushedSeqNum: math.MaxUint64, + inProgressCompactions: inProgressCompactions, + }) + var result strings.Builder + if pc != nil { + c := newCompaction(pc, opts, time.Now(), nil /* provider */) + fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) + fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) + if !pc.outputLevel.files.Empty() { + fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) + } + if !c.grandparents.Empty() { + fmt.Fprintf(&result, "grandparents: %s\n", fileNums(c.grandparents)) + } + } else { + return "nil" + } + return result.String() + } + return fmt.Sprintf("unrecognized command: %s", td.Cmd) + }) +} + +func TestCompactionPickerPickReadTriggered(t *testing.T) { + opts := (*Options)(nil).EnsureDefaults() + var picker *compactionPickerByScore + var rcList readCompactionQueue + var vers *version + + parseMeta := func(s string) (*fileMetadata, error) { + parts := strings.Split(s, ":") + fileNum, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + fields := strings.Fields(parts[1]) + parts = strings.Split(fields[0], "-") + if len(parts) != 2 { + return nil, errors.Errorf("malformed table spec: %s. usage: :start.SET.1-end.SET.2", s) + } + m := (&fileMetadata{ + FileNum: base.FileNum(fileNum), + Size: 1028, + }).ExtendPointKeyBounds( + opts.Comparer.Compare, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + m.InitPhysicalBacking() + for _, p := range fields[1:] { + if strings.HasPrefix(p, "size=") { + v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) + if err != nil { + return nil, err + } + m.Size = uint64(v) + } + } + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + return m, nil + } + + datadriven.RunTest(t, "testdata/compaction_picker_read_triggered", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + rcList = readCompactionQueue{} + fileMetas := [manifest.NumLevels][]*fileMetadata{} + level := 0 + var err error + lines := strings.Split(td.Input, "\n") + + for len(lines) > 0 { + data := strings.TrimSpace(lines[0]) + lines = lines[1:] + switch data { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err = strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + default: + meta, err := parseMeta(data) + if err != nil { + return err.Error() + } + fileMetas[level] = append(fileMetas[level], meta) + } + } + + vers = newVersion(opts, fileMetas) + vs := &versionSet{ + opts: opts, + cmp: DefaultComparer.Compare, + cmpName: DefaultComparer.Name, + } + vs.versions.Init(nil) + vs.append(vers) + var inProgressCompactions []compactionInfo + picker = newCompactionPicker(vers, opts, inProgressCompactions).(*compactionPickerByScore) + vs.picker = picker + + var buf bytes.Buffer + fmt.Fprint(&buf, vers.String()) + return buf.String() + + case "add-read-compaction": + for _, line := range strings.Split(td.Input, "\n") { + if line == "" { + continue + } + parts := strings.Split(line, " ") + if len(parts) != 3 { + return "error: malformed data for add-read-compaction. usage: : - " + } + if l, err := strconv.Atoi(parts[0][:1]); err == nil { + keys := strings.Split(parts[1], "-") + fileNum, _ := strconv.Atoi(parts[2]) + + rc := readCompaction{ + level: l, + start: []byte(keys[0]), + end: []byte(keys[1]), + fileNum: base.FileNum(fileNum), + } + rcList.add(&rc, DefaultComparer.Compare) + } else { + return err.Error() + } + } + return "" + + case "show-read-compactions": + var sb strings.Builder + if rcList.size == 0 { + sb.WriteString("(none)") + } + for i := 0; i < rcList.size; i++ { + rc := rcList.at(i) + sb.WriteString(fmt.Sprintf("(level: %d, start: %s, end: %s)\n", rc.level, string(rc.start), string(rc.end))) + } + return sb.String() + + case "pick-auto": + pc := picker.pickAuto(compactionEnv{ + earliestUnflushedSeqNum: math.MaxUint64, + readCompactionEnv: readCompactionEnv{ + readCompactions: &rcList, + flushing: false, + }, + }) + var result strings.Builder + if pc != nil { + fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) + fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) + if !pc.outputLevel.files.Empty() { + fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) + } + } else { + return "nil" + } + return result.String() + } + return fmt.Sprintf("unrecognized command: %s", td.Cmd) + }) +} + +type alwaysMultiLevel struct{} + +func (d alwaysMultiLevel) pick( + pcOrig *pickedCompaction, opts *Options, diskAvailBytes uint64, +) *pickedCompaction { + pcMulti := pcOrig.clone() + if !pcMulti.setupMultiLevelCandidate(opts, diskAvailBytes) { + return pcOrig + } + return pcMulti +} + +func (d alwaysMultiLevel) allowL0() bool { + return false +} + +func TestPickedCompactionSetupInputs(t *testing.T) { + opts := &Options{} + opts.EnsureDefaults() + + parseMeta := func(s string) *fileMetadata { + parts := strings.Split(strings.TrimSpace(s), " ") + var fileSize uint64 + var compacting bool + for _, part := range parts { + switch { + case part == "compacting": + compacting = true + case strings.HasPrefix(part, "size="): + v, err := strconv.ParseUint(strings.TrimPrefix(part, "size="), 10, 64) + require.NoError(t, err) + fileSize = v + } + } + tableParts := strings.Split(parts[0], "-") + if len(tableParts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + state := manifest.CompactionStateNotCompacting + if compacting { + state = manifest.CompactionStateCompacting + } + m := (&fileMetadata{ + CompactionState: state, + Size: fileSize, + }).ExtendPointKeyBounds( + opts.Comparer.Compare, + base.ParseInternalKey(strings.TrimSpace(tableParts[0])), + base.ParseInternalKey(strings.TrimSpace(tableParts[1])), + ) + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + m.InitPhysicalBacking() + return m + } + + setupInputTest := func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "setup-inputs": + var availBytes uint64 = math.MaxUint64 + var maxLevelBytes [7]int64 + args := d.CmdArgs + + if len(args) > 0 && args[0].Key == "avail-bytes" { + require.Equal(t, 1, len(args[0].Vals)) + var err error + availBytes, err = strconv.ParseUint(args[0].Vals[0], 10, 64) + require.NoError(t, err) + args = args[1:] + } + + if len(args) != 2 { + return "setup-inputs [avail-bytes=XXX] " + } + + pc := &pickedCompaction{ + cmp: DefaultComparer.Compare, + inputs: []compactionLevel{{level: -1}, {level: -1}}, + } + pc.startLevel, pc.outputLevel = &pc.inputs[0], &pc.inputs[1] + var currentLevel int + var files [numLevels][]*fileMetadata + fileNum := FileNum(1) + + for _, data := range strings.Split(d.Input, "\n") { + switch data[:2] { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + levelArgs := strings.Fields(data) + level, err := strconv.Atoi(levelArgs[0][1:]) + if err != nil { + return err.Error() + } + currentLevel = level + if len(levelArgs) > 1 { + maxSizeArg := strings.Replace(levelArgs[1], "max-size=", "", 1) + maxSize, err := strconv.ParseInt(maxSizeArg, 10, 64) + if err != nil { + return err.Error() + } + maxLevelBytes[level] = maxSize + } else { + maxLevelBytes[level] = math.MaxInt64 + } + if pc.startLevel.level == -1 { + pc.startLevel.level = level + + } else if pc.outputLevel.level == -1 { + if pc.startLevel.level >= level { + return fmt.Sprintf("startLevel=%d >= outputLevel=%d\n", pc.startLevel.level, level) + } + pc.outputLevel.level = level + } + default: + meta := parseMeta(data) + meta.FileNum = fileNum + fileNum++ + files[currentLevel] = append(files[currentLevel], meta) + } + } + + if pc.outputLevel.level == -1 { + pc.outputLevel.level = pc.startLevel.level + 1 + } + pc.version = newVersion(opts, files) + pc.startLevel.files = pc.version.Overlaps(pc.startLevel.level, pc.cmp, + []byte(args[0].String()), []byte(args[1].String()), false /* exclusiveEnd */) + + var isCompacting bool + if !pc.setupInputs(opts, availBytes, pc.startLevel) { + isCompacting = true + } + origPC := pc + pc = pc.maybeAddLevel(opts, availBytes) + // If pc points to a new pickedCompaction, a new multi level compaction + // was initialized. + initMultiLevel := pc != origPC + checkClone(t, pc) + var buf bytes.Buffer + for _, cl := range pc.inputs { + if cl.files.Empty() { + continue + } + + fmt.Fprintf(&buf, "L%d\n", cl.level) + cl.files.Each(func(f *fileMetadata) { + fmt.Fprintf(&buf, " %s\n", f) + }) + } + if isCompacting { + fmt.Fprintf(&buf, "is-compacting\n") + } + + if initMultiLevel { + extraLevel := pc.extraLevels[0].level + fmt.Fprintf(&buf, "init-multi-level(%d,%d,%d)\n", pc.startLevel.level, extraLevel, + pc.outputLevel.level) + fmt.Fprintf(&buf, "Original WriteAmp %.2f; ML WriteAmp %.2f\n", origPC.predictedWriteAmp(), pc.predictedWriteAmp()) + fmt.Fprintf(&buf, "Original OverlappingRatio %.2f; ML OverlappingRatio %.2f\n", origPC.overlappingRatio(), pc.overlappingRatio()) + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + } + + t.Logf("Test basic setup inputs behavior without multi level compactions") + opts.Experimental.MultiLevelCompactionHeuristic = NoMultiLevel{} + datadriven.RunTest(t, "testdata/compaction_setup_inputs", + setupInputTest) + + t.Logf("Turning multi level compaction on") + opts.Experimental.MultiLevelCompactionHeuristic = alwaysMultiLevel{} + datadriven.RunTest(t, "testdata/compaction_setup_inputs_multilevel_dummy", + setupInputTest) + + t.Logf("Try Write-Amp Heuristic") + opts.Experimental.MultiLevelCompactionHeuristic = WriteAmpHeuristic{} + datadriven.RunTest(t, "testdata/compaction_setup_inputs_multilevel_write_amp", + setupInputTest) +} + +func TestPickedCompactionExpandInputs(t *testing.T) { + opts := &Options{} + opts.EnsureDefaults() + cmp := DefaultComparer.Compare + var files []*fileMetadata + + parseMeta := func(s string) *fileMetadata { + parts := strings.Split(s, "-") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + m := (&fileMetadata{}).ExtendPointKeyBounds( + opts.Comparer.Compare, + base.ParseInternalKey(parts[0]), + base.ParseInternalKey(parts[1]), + ) + m.InitPhysicalBacking() + return m + } + + datadriven.RunTest(t, "testdata/compaction_expand_inputs", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + files = nil + if len(d.Input) == 0 { + return "" + } + for _, data := range strings.Split(d.Input, "\n") { + meta := parseMeta(data) + meta.FileNum = FileNum(len(files)) + files = append(files, meta) + } + manifest.SortBySmallest(files, cmp) + return "" + + case "expand-inputs": + pc := &pickedCompaction{ + cmp: cmp, + inputs: []compactionLevel{{level: 1}}, + } + pc.startLevel = &pc.inputs[0] + + var filesLevelled [numLevels][]*fileMetadata + filesLevelled[pc.startLevel.level] = files + pc.version = newVersion(opts, filesLevelled) + + if len(d.CmdArgs) != 1 { + return fmt.Sprintf("%s expects 1 argument", d.Cmd) + } + index, err := strconv.ParseInt(d.CmdArgs[0].String(), 10, 64) + if err != nil { + return err.Error() + } + + // Advance the iterator to position `index`. + iter := pc.version.Levels[pc.startLevel.level].Iter() + _ = iter.First() + for i := int64(0); i < index; i++ { + _ = iter.Next() + } + + inputs, _ := expandToAtomicUnit(cmp, iter.Take().Slice(), true /* disableIsCompacting */) + + var buf bytes.Buffer + inputs.Each(func(f *fileMetadata) { + fmt.Fprintf(&buf, "%d: %s-%s\n", f.FileNum, f.Smallest, f.Largest) + }) + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestCompactionOutputFileSize(t *testing.T) { + opts := (*Options)(nil).EnsureDefaults() + var picker *compactionPickerByScore + var vers *version + + parseMeta := func(s string) (*fileMetadata, error) { + parts := strings.Split(s, ":") + fileNum, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + fields := strings.Fields(parts[1]) + parts = strings.Split(fields[0], "-") + if len(parts) != 2 { + return nil, errors.Errorf("malformed table spec: %s. usage: :start.SET.1-end.SET.2", s) + } + m := (&fileMetadata{ + FileNum: base.FileNum(fileNum), + Size: 1028, + }).ExtendPointKeyBounds( + opts.Comparer.Compare, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + m.InitPhysicalBacking() + for _, p := range fields[1:] { + if strings.HasPrefix(p, "size=") { + v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) + if err != nil { + return nil, err + } + m.Size = uint64(v) + } + if strings.HasPrefix(p, "range-deletions-bytes-estimate=") { + v, err := strconv.Atoi(strings.TrimPrefix(p, "range-deletions-bytes-estimate=")) + if err != nil { + return nil, err + } + m.Stats.RangeDeletionsBytesEstimate = uint64(v) + m.Stats.NumDeletions = 1 // At least one range del responsible for the deletion bytes. + m.StatsMarkValid() + } + } + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + return m, nil + } + + datadriven.RunTest(t, "testdata/compaction_output_file_size", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + fileMetas := [manifest.NumLevels][]*fileMetadata{} + level := 0 + var err error + lines := strings.Split(td.Input, "\n") + + for len(lines) > 0 { + data := strings.TrimSpace(lines[0]) + lines = lines[1:] + switch data { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err = strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + default: + meta, err := parseMeta(data) + if err != nil { + return err.Error() + } + fileMetas[level] = append(fileMetas[level], meta) + } + } + + vers = newVersion(opts, fileMetas) + vs := &versionSet{ + opts: opts, + cmp: DefaultComparer.Compare, + cmpName: DefaultComparer.Name, + } + vs.versions.Init(nil) + vs.append(vers) + var inProgressCompactions []compactionInfo + picker = newCompactionPicker(vers, opts, inProgressCompactions).(*compactionPickerByScore) + vs.picker = picker + + var buf bytes.Buffer + fmt.Fprint(&buf, vers.String()) + return buf.String() + + case "pick-auto": + pc := picker.pickAuto(compactionEnv{ + earliestUnflushedSeqNum: math.MaxUint64, + earliestSnapshotSeqNum: math.MaxUint64, + }) + var buf bytes.Buffer + if pc != nil { + fmt.Fprintf(&buf, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) + fmt.Fprintf(&buf, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) + fmt.Fprintf(&buf, "maxOutputFileSize: %d\n", pc.maxOutputFileSize) + } else { + return "nil" + } + return buf.String() + + default: + return fmt.Sprintf("unrecognized command: %s", td.Cmd) + } + }) +} + +func TestCompactionPickerCompensatedSize(t *testing.T) { + testCases := []struct { + size uint64 + pointDelEstimateBytes uint64 + rangeDelEstimateBytes uint64 + wantBytes uint64 + }{ + { + size: 100, + pointDelEstimateBytes: 0, + rangeDelEstimateBytes: 0, + wantBytes: 100, + }, + { + size: 100, + pointDelEstimateBytes: 10, + rangeDelEstimateBytes: 0, + wantBytes: 100 + 10, + }, + { + size: 100, + pointDelEstimateBytes: 10, + rangeDelEstimateBytes: 5, + wantBytes: 100 + 10 + 5, + }, + } + + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + f := &fileMetadata{Size: tc.size} + f.InitPhysicalBacking() + f.Stats.PointDeletionsBytesEstimate = tc.pointDelEstimateBytes + f.Stats.RangeDeletionsBytesEstimate = tc.rangeDelEstimateBytes + gotBytes := compensatedSize(f) + require.Equal(t, tc.wantBytes, gotBytes) + }) + } +} + +func TestCompactionPickerPickFile(t *testing.T) { + fs := vfs.NewMem() + opts := &Options{ + Comparer: testkeys.Comparer, + FormatMajorVersion: FormatNewest, + FS: fs, + } + + d, err := Open("", opts) + require.NoError(t, err) + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + datadriven.RunTest(t, "testdata/compaction_picker_pick_file", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + require.NoError(t, d.Close()) + + d, err = runDBDefineCmd(td, opts) + if err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "file-sizes": + return runTableFileSizesCmd(td, d) + + case "pick-file": + s := strings.TrimPrefix(td.CmdArgs[0].String(), "L") + level, err := strconv.Atoi(s) + if err != nil { + return fmt.Sprintf("unable to parse arg %q as level", td.CmdArgs[0].String()) + } + if level == 0 { + panic("L0 picking unimplemented") + } + d.mu.Lock() + defer d.mu.Unlock() + + // Use maybeScheduleCompactionPicker to take care of all of the + // initialization of the compaction-picking environment, but never + // pick a compaction; just call pickFile using the user-provided + // level. + var lf manifest.LevelFile + var ok bool + d.maybeScheduleCompactionPicker(func(untypedPicker compactionPicker, env compactionEnv) *pickedCompaction { + p := untypedPicker.(*compactionPickerByScore) + lf, ok = pickCompactionSeedFile(p.vers, opts, level, level+1, env.earliestSnapshotSeqNum) + return nil + }) + if !ok { + return "(none)" + } + return lf.FileMetadata.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +type pausableCleaner struct { + mu sync.Mutex + cond sync.Cond + paused bool + cleaner Cleaner +} + +func (c *pausableCleaner) Clean(fs vfs.FS, fileType base.FileType, path string) error { + c.mu.Lock() + defer c.mu.Unlock() + for c.paused { + c.cond.Wait() + } + return c.cleaner.Clean(fs, fileType, path) +} + +func (c *pausableCleaner) pause() { + c.mu.Lock() + defer c.mu.Unlock() + c.paused = true +} + +func (c *pausableCleaner) resume() { + c.mu.Lock() + defer c.mu.Unlock() + c.paused = false + c.cond.Broadcast() +} + +func TestCompactionPickerScores(t *testing.T) { + fs := vfs.NewMem() + cleaner := pausableCleaner{cleaner: DeleteCleaner{}} + cleaner.cond.L = &cleaner.mu + opts := &Options{ + Cleaner: &cleaner, + Comparer: testkeys.Comparer, + DisableAutomaticCompactions: true, + FormatMajorVersion: FormatNewest, + FS: fs, + } + + d, err := Open("", opts) + require.NoError(t, err) + defer func() { + if d != nil { + cleaner.resume() + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + } + }() + + var buf bytes.Buffer + datadriven.RunTest(t, "testdata/compaction_picker_scores", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + + if td.HasArg("pause-cleaning") { + cleaner.pause() + } + + d, err = runDBDefineCmd(td, opts) + if err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "disable-table-stats": + d.mu.Lock() + d.opts.private.disableTableStats = true + d.mu.Unlock() + return "" + + case "enable-table-stats": + d.mu.Lock() + d.opts.private.disableTableStats = false + d.maybeCollectTableStatsLocked() + d.mu.Unlock() + return "" + + case "resume-cleaning": + cleaner.resume() + return "" + + case "ingest": + if err = runBuildCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + if err = runIngestCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "lsm": + return runLSMCmd(td, d) + + case "maybe-compact": + buf.Reset() + d.mu.Lock() + d.opts.DisableAutomaticCompactions = false + d.maybeScheduleCompaction() + fmt.Fprintf(&buf, "%d compactions in progress:", d.mu.compact.compactingCount) + for c := range d.mu.compact.inProgress { + fmt.Fprintf(&buf, "\n%s", c) + } + d.opts.DisableAutomaticCompactions = true + d.mu.Unlock() + return buf.String() + + case "scores": + waitFor := "completion" + td.MaybeScanArgs(t, "wait-for-compaction", &waitFor) + + // Wait for any running compactions to complete before calculating + // scores. Otherwise, the output of this command is + // nondeterministic. + switch waitFor { + case "completion": + d.mu.Lock() + for d.mu.compact.compactingCount > 0 { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + case "version-edit": + func() { + for { + d.mu.Lock() + wait := len(d.mu.compact.inProgress) > 0 + for c := range d.mu.compact.inProgress { + wait = wait && !c.versionEditApplied + } + d.mu.Unlock() + if !wait { + return + } + // d.mu.compact.cond isn't notified until the compaction + // is removed from inProgress, so we need to just sleep + // and check again soon. + time.Sleep(10 * time.Millisecond) + } + }() + default: + panic(fmt.Sprintf("unrecognized `wait-for-compaction` value: %q", waitFor)) + } + + buf.Reset() + fmt.Fprintf(&buf, "L Size Score\n") + for l, lm := range d.Metrics().Levels { + if l < numLevels-1 { + fmt.Fprintf(&buf, "L%-3d\t%-7s%.1f\n", l, humanize.Bytes.Int64(lm.Size), lm.Score) + } else { + fmt.Fprintf(&buf, "L%-3d\t%-7s-\n", l, humanize.Bytes.Int64(lm.Size)) + } + } + return buf.String() + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func fileNums(files manifest.LevelSlice) string { + var ss []string + files.Each(func(f *fileMetadata) { + ss = append(ss, f.FileNum.String()) + }) + sort.Strings(ss) + return strings.Join(ss, ",") +} + +func checkClone(t *testing.T, pc *pickedCompaction) { + pcClone := pc.clone() + require.Equal(t, pc.String(), pcClone.String()) + + // ensure all input files are in new address + for i := range pc.inputs { + // Len could be zero if setup inputs rejected a level + if pc.inputs[i].files.Len() > 0 { + require.NotEqual(t, &pc.inputs[i], &pcClone.inputs[i]) + } + } + for i := range pc.startLevel.l0SublevelInfo { + if pc.startLevel.l0SublevelInfo[i].Len() > 0 { + require.NotEqual(t, &pc.startLevel.l0SublevelInfo[i], &pcClone.startLevel.l0SublevelInfo[i]) + } + } +} diff --git a/pebble/compaction_test.go b/pebble/compaction_test.go new file mode 100644 index 0000000..ea1437a --- /dev/null +++ b/pebble/compaction_test.go @@ -0,0 +1,3912 @@ +// Copyright 2013 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 ( + "bytes" + "context" + crand "crypto/rand" + "fmt" + "math" + "math/rand" + "path/filepath" + "reflect" + "regexp" + "runtime" + "slices" + "sort" + "strconv" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/stretchr/testify/require" +) + +func newVersion(opts *Options, files [numLevels][]*fileMetadata) *version { + return manifest.NewVersion( + opts.Comparer.Compare, + opts.Comparer.FormatKey, + opts.FlushSplitBytes, + files) +} + +type compactionPickerForTesting struct { + score float64 + level int + baseLevel int + opts *Options + vers *manifest.Version + maxLevelBytes [7]int64 +} + +var _ compactionPicker = &compactionPickerForTesting{} + +func (p *compactionPickerForTesting) getScores([]compactionInfo) [numLevels]float64 { + return [numLevels]float64{} +} + +func (p *compactionPickerForTesting) getBaseLevel() int { + return p.baseLevel +} + +func (p *compactionPickerForTesting) estimatedCompactionDebt(l0ExtraSize uint64) uint64 { + return 0 +} + +func (p *compactionPickerForTesting) forceBaseLevel1() {} + +func (p *compactionPickerForTesting) pickAuto(env compactionEnv) (pc *pickedCompaction) { + if p.score < 1 { + return nil + } + outputLevel := p.level + 1 + if p.level == 0 { + outputLevel = p.baseLevel + } + iter := p.vers.Levels[p.level].Iter() + iter.First() + cInfo := candidateLevelInfo{ + level: p.level, + outputLevel: outputLevel, + file: iter.Take(), + } + if cInfo.level == 0 { + return pickL0(env, p.opts, p.vers, p.baseLevel) + } + return pickAutoLPositive(env, p.opts, p.vers, cInfo, p.baseLevel, p.maxLevelBytes) +} + +func (p *compactionPickerForTesting) pickElisionOnlyCompaction( + env compactionEnv, +) (pc *pickedCompaction) { + return nil +} + +func (p *compactionPickerForTesting) pickRewriteCompaction( + env compactionEnv, +) (pc *pickedCompaction) { + return nil +} + +func (p *compactionPickerForTesting) pickReadTriggeredCompaction( + env compactionEnv, +) (pc *pickedCompaction) { + return nil +} + +func TestPickCompaction(t *testing.T) { + fileNums := func(files manifest.LevelSlice) string { + var ss []string + files.Each(func(meta *fileMetadata) { + ss = append(ss, strconv.Itoa(int(meta.FileNum))) + }) + sort.Strings(ss) + return strings.Join(ss, ",") + } + + opts := (*Options)(nil).EnsureDefaults() + newFileMeta := func(fileNum FileNum, size uint64, smallest, largest base.InternalKey) *fileMetadata { + m := (&fileMetadata{ + FileNum: fileNum, + Size: size, + }).ExtendPointKeyBounds(opts.Comparer.Compare, smallest, largest) + m.InitPhysicalBacking() + return m + } + + testCases := []struct { + desc string + version *version + picker compactionPickerForTesting + want string + wantMulti bool + }{ + { + desc: "no compaction", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 0: { + newFileMeta( + 100, + 1, + base.ParseInternalKey("i.SET.101"), + base.ParseInternalKey("j.SET.102"), + ), + }, + }), + want: "", + }, + + { + desc: "1 L0 file", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 0: { + newFileMeta( + 100, + 1, + base.ParseInternalKey("i.SET.101"), + base.ParseInternalKey("j.SET.102"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 0, + baseLevel: 1, + }, + want: "100 ", + }, + + { + desc: "2 L0 files (0 overlaps)", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 0: { + newFileMeta( + 100, + 1, + base.ParseInternalKey("i.SET.101"), + base.ParseInternalKey("j.SET.102"), + ), + newFileMeta( + 110, + 1, + base.ParseInternalKey("k.SET.111"), + base.ParseInternalKey("l.SET.112"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 0, + baseLevel: 1, + }, + want: "100,110 ", + }, + + { + desc: "2 L0 files, with ikey overlap", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 0: { + newFileMeta( + 100, + 1, + base.ParseInternalKey("i.SET.101"), + base.ParseInternalKey("p.SET.102"), + ), + newFileMeta( + 110, + 1, + base.ParseInternalKey("j.SET.111"), + base.ParseInternalKey("q.SET.112"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 0, + baseLevel: 1, + }, + want: "100,110 ", + }, + + { + desc: "2 L0 files, with ukey overlap", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 0: { + newFileMeta( + 100, + 1, + base.ParseInternalKey("i.SET.101"), + base.ParseInternalKey("i.SET.102"), + ), + newFileMeta( + 110, + 1, + base.ParseInternalKey("i.SET.111"), + base.ParseInternalKey("i.SET.112"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 0, + baseLevel: 1, + }, + want: "100,110 ", + }, + + { + desc: "1 L0 file, 2 L1 files (0 overlaps)", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 0: { + newFileMeta( + 100, + 1, + base.ParseInternalKey("i.SET.101"), + base.ParseInternalKey("i.SET.102"), + ), + }, + 1: { + newFileMeta( + 200, + 1, + base.ParseInternalKey("a.SET.201"), + base.ParseInternalKey("b.SET.202"), + ), + newFileMeta( + 210, + 1, + base.ParseInternalKey("y.SET.211"), + base.ParseInternalKey("z.SET.212"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 0, + baseLevel: 1, + }, + want: "100 ", + }, + + { + desc: "1 L0 file, 2 L1 files (1 overlap), 4 L2 files (3 overlaps)", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 0: { + newFileMeta( + 100, + 1, + base.ParseInternalKey("i.SET.101"), + base.ParseInternalKey("t.SET.102"), + ), + }, + 1: { + newFileMeta( + 200, + 1, + base.ParseInternalKey("a.SET.201"), + base.ParseInternalKey("e.SET.202"), + ), + newFileMeta( + 210, + 1, + base.ParseInternalKey("f.SET.211"), + base.ParseInternalKey("j.SET.212"), + ), + }, + 2: { + newFileMeta( + 300, + 1, + base.ParseInternalKey("a.SET.301"), + base.ParseInternalKey("b.SET.302"), + ), + newFileMeta( + 310, + 1, + base.ParseInternalKey("c.SET.311"), + base.ParseInternalKey("g.SET.312"), + ), + newFileMeta( + 320, + 1, + base.ParseInternalKey("h.SET.321"), + base.ParseInternalKey("m.SET.322"), + ), + newFileMeta( + 330, + 1, + base.ParseInternalKey("n.SET.331"), + base.ParseInternalKey("z.SET.332"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 0, + baseLevel: 1, + }, + want: "100 210 310,320,330", + }, + + { + desc: "4 L1 files, 2 L2 files, can grow", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 1: { + newFileMeta( + 200, + 1, + base.ParseInternalKey("i1.SET.201"), + base.ParseInternalKey("i2.SET.202"), + ), + newFileMeta( + 210, + 1, + base.ParseInternalKey("j1.SET.211"), + base.ParseInternalKey("j2.SET.212"), + ), + newFileMeta( + 220, + 1, + base.ParseInternalKey("k1.SET.221"), + base.ParseInternalKey("k2.SET.222"), + ), + newFileMeta( + 230, + 1, + base.ParseInternalKey("l1.SET.231"), + base.ParseInternalKey("l2.SET.232"), + ), + }, + 2: { + newFileMeta( + 300, + 1, + base.ParseInternalKey("a0.SET.301"), + base.ParseInternalKey("l0.SET.302"), + ), + newFileMeta( + 310, + 1, + base.ParseInternalKey("l2.SET.311"), + base.ParseInternalKey("z2.SET.312"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 1, + baseLevel: 1, + }, + want: "200,210,220 300 ", + wantMulti: true, + }, + + { + desc: "4 L1 files, 2 L2 files, can't grow (range)", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 1: { + newFileMeta( + 200, + 1, + base.ParseInternalKey("i1.SET.201"), + base.ParseInternalKey("i2.SET.202"), + ), + newFileMeta( + 210, + 1, + base.ParseInternalKey("j1.SET.211"), + base.ParseInternalKey("j2.SET.212"), + ), + newFileMeta( + 220, + 1, + base.ParseInternalKey("k1.SET.221"), + base.ParseInternalKey("k2.SET.222"), + ), + newFileMeta( + 230, + 1, + base.ParseInternalKey("l1.SET.231"), + base.ParseInternalKey("l2.SET.232"), + ), + }, + 2: { + newFileMeta( + 300, + 1, + base.ParseInternalKey("a0.SET.301"), + base.ParseInternalKey("j0.SET.302"), + ), + newFileMeta( + 310, + 1, + base.ParseInternalKey("j2.SET.311"), + base.ParseInternalKey("z2.SET.312"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 1, + baseLevel: 1, + }, + want: "200 300 ", + wantMulti: true, + }, + + { + desc: "4 L1 files, 2 L2 files, can't grow (size)", + version: newVersion(opts, [numLevels][]*fileMetadata{ + 1: { + newFileMeta( + 200, + expandedCompactionByteSizeLimit(opts, 1, math.MaxUint64)-1, + base.ParseInternalKey("i1.SET.201"), + base.ParseInternalKey("i2.SET.202"), + ), + newFileMeta( + 210, + expandedCompactionByteSizeLimit(opts, 1, math.MaxUint64)-1, + base.ParseInternalKey("j1.SET.211"), + base.ParseInternalKey("j2.SET.212"), + ), + newFileMeta( + 220, + expandedCompactionByteSizeLimit(opts, 1, math.MaxUint64)-1, + base.ParseInternalKey("k1.SET.221"), + base.ParseInternalKey("k2.SET.222"), + ), + newFileMeta( + 230, + expandedCompactionByteSizeLimit(opts, 1, math.MaxUint64)-1, + base.ParseInternalKey("l1.SET.231"), + base.ParseInternalKey("l2.SET.232"), + ), + }, + 2: { + newFileMeta( + 300, + expandedCompactionByteSizeLimit(opts, 2, math.MaxUint64)-1, + base.ParseInternalKey("a0.SET.301"), + base.ParseInternalKey("l0.SET.302"), + ), + newFileMeta( + 310, + expandedCompactionByteSizeLimit(opts, 2, math.MaxUint64)-1, + base.ParseInternalKey("l2.SET.311"), + base.ParseInternalKey("z2.SET.312"), + ), + }, + }), + picker: compactionPickerForTesting{ + score: 99, + level: 1, + baseLevel: 1, + }, + want: "200 300 ", + }, + } + + for _, tc := range testCases { + vs := &versionSet{ + opts: opts, + cmp: DefaultComparer.Compare, + cmpName: DefaultComparer.Name, + } + vs.versions.Init(nil) + vs.append(tc.version) + tc.picker.opts = opts + tc.picker.vers = tc.version + vs.picker = &tc.picker + pc, got := vs.picker.pickAuto(compactionEnv{diskAvailBytes: math.MaxUint64}), "" + if pc != nil { + c := newCompaction(pc, opts, time.Now(), nil /* provider */) + + gotStart := fileNums(c.startLevel.files) + gotML := "" + observedMulti := len(c.extraLevels) > 0 + if observedMulti { + gotML = " " + fileNums(c.extraLevels[0].files) + } + gotOutput := " " + fileNums(c.outputLevel.files) + gotGrandparents := " " + fileNums(c.grandparents) + got = gotStart + gotML + gotOutput + gotGrandparents + if tc.wantMulti != observedMulti { + t.Fatalf("Expected Multi %t; Observed Multi %t, for %s", tc.wantMulti, observedMulti, got) + } + + } + if got != tc.want { + t.Fatalf("%s:\ngot %q\nwant %q", tc.desc, got, tc.want) + } + } +} + +func TestElideTombstone(t *testing.T) { + var d *DB + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + var buf bytes.Buffer + datadriven.RunTest(t, "testdata/compaction_elide_tombstone", + func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + if d != nil { + if err := d.Close(); err != nil { + return err.Error() + } + } + var err error + if d, err = runDBDefineCmd(td, (&Options{ + FS: vfs.NewMem(), + DebugCheck: DebugCheckLevels, + FormatMajorVersion: FormatNewest, + DisableAutomaticCompactions: true, + }).WithFSDefaults()); err != nil { + return err.Error() + } + if td.HasArg("verbose") { + return d.mu.versions.currentVersion().DebugString(base.DefaultFormatter) + } + return d.mu.versions.currentVersion().String() + case "elide": + buf.Reset() + var startLevel int + td.ScanArgs(t, "start-level", &startLevel) + c := compaction{ + cmp: testkeys.Comparer.Compare, + comparer: testkeys.Comparer, + version: d.mu.versions.currentVersion(), + inputs: []compactionLevel{{level: startLevel}, {level: startLevel + 1}}, + smallest: base.ParseInternalKey("a.SET.0"), + largest: base.ParseInternalKey("z.SET.0"), + } + c.startLevel, c.outputLevel = &c.inputs[0], &c.inputs[1] + c.setupInuseKeyRanges() + for _, ukey := range strings.Split(td.Input, "\n") { + fmt.Fprintf(&buf, "elideTombstone(%q) = %t\n", ukey, c.elideTombstone([]byte(ukey))) + } + return buf.String() + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestElideRangeTombstone(t *testing.T) { + opts := (*Options)(nil).EnsureDefaults() + + newFileMeta := func(smallest, largest base.InternalKey) *fileMetadata { + m := (&fileMetadata{}).ExtendPointKeyBounds( + opts.Comparer.Compare, smallest, largest, + ) + m.InitPhysicalBacking() + return m + } + + type want struct { + key string + endKey string + expected bool + } + + testCases := []struct { + desc string + level int + version *version + wants []want + flushing flushableList + }{ + { + desc: "empty", + level: 1, + version: newVersion(opts, [numLevels][]*fileMetadata{}), + wants: []want{ + {"x", "y", true}, + }, + }, + { + desc: "non-empty", + level: 1, + version: newVersion(opts, [numLevels][]*fileMetadata{ + 1: { + newFileMeta( + base.ParseInternalKey("c.SET.801"), + base.ParseInternalKey("g.SET.800"), + ), + newFileMeta( + base.ParseInternalKey("x.SET.701"), + base.ParseInternalKey("y.SET.700"), + ), + }, + 2: { + newFileMeta( + base.ParseInternalKey("d.SET.601"), + base.ParseInternalKey("h.SET.600"), + ), + newFileMeta( + base.ParseInternalKey("r.SET.501"), + base.ParseInternalKey("t.SET.500"), + ), + }, + 3: { + newFileMeta( + base.ParseInternalKey("f.SET.401"), + base.ParseInternalKey("g.SET.400"), + ), + newFileMeta( + base.ParseInternalKey("w.SET.301"), + base.ParseInternalKey("x.SET.300"), + ), + }, + 4: { + newFileMeta( + base.ParseInternalKey("f.SET.201"), + base.ParseInternalKey("m.SET.200"), + ), + newFileMeta( + base.ParseInternalKey("t.SET.101"), + base.ParseInternalKey("t.SET.100"), + ), + }, + }), + wants: []want{ + {"b", "c", true}, + {"c", "d", true}, + {"d", "e", true}, + {"e", "f", false}, + {"f", "g", false}, + {"g", "h", false}, + {"h", "i", false}, + {"l", "m", false}, + {"m", "n", false}, + {"n", "o", true}, + {"q", "r", true}, + {"r", "s", true}, + {"s", "t", false}, + {"t", "u", false}, + {"u", "v", true}, + {"v", "w", false}, + {"w", "x", false}, + {"x", "y", false}, + {"y", "z", true}, + }, + }, + { + desc: "flushing", + level: -1, + version: newVersion(opts, [numLevels][]*fileMetadata{ + 0: { + newFileMeta( + base.ParseInternalKey("h.SET.901"), + base.ParseInternalKey("j.SET.900"), + ), + }, + 1: { + newFileMeta( + base.ParseInternalKey("c.SET.801"), + base.ParseInternalKey("g.SET.800"), + ), + newFileMeta( + base.ParseInternalKey("x.SET.701"), + base.ParseInternalKey("y.SET.700"), + ), + }, + }), + wants: []want{ + {"m", "n", false}, + }, + // Pretend one memtable is being flushed + flushing: flushableList{nil}, + }, + } + + for _, tc := range testCases { + c := compaction{ + cmp: DefaultComparer.Compare, + comparer: DefaultComparer, + version: tc.version, + inputs: []compactionLevel{{level: tc.level}, {level: tc.level + 1}}, + smallest: base.ParseInternalKey("a.SET.0"), + largest: base.ParseInternalKey("z.SET.0"), + flushing: tc.flushing, + } + c.startLevel, c.outputLevel = &c.inputs[0], &c.inputs[1] + c.setupInuseKeyRanges() + for _, w := range tc.wants { + if got := c.elideRangeTombstone([]byte(w.key), []byte(w.endKey)); got != w.expected { + t.Errorf("%s: keys=%q-%q: got %v, want %v", tc.desc, w.key, w.endKey, got, w.expected) + } + } + } +} + +func TestCompactionTransform(t *testing.T) { + datadriven.RunTest(t, "testdata/compaction_transform", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "transform": + var snapshots []uint64 + var keyRanges []manifest.UserKeyRange + disableElision := td.HasArg("disable-elision") + td.MaybeScanArgs(t, "snapshots", &snapshots) + if arg, ok := td.Arg("in-use-key-ranges"); ok { + for _, keyRange := range arg.Vals { + parts := strings.SplitN(keyRange, "-", 2) + start := []byte(strings.TrimSpace(parts[0])) + end := []byte(strings.TrimSpace(parts[1])) + keyRanges = append(keyRanges, manifest.UserKeyRange{ + Start: start, + End: end, + }) + } + } + span := keyspan.ParseSpan(td.Input) + for i := range span.Keys { + if i > 0 { + if span.Keys[i-1].Trailer < span.Keys[i].Trailer { + return "span keys not sorted" + } + } + } + var outSpan keyspan.Span + c := compaction{ + cmp: base.DefaultComparer.Compare, + comparer: base.DefaultComparer, + disableSpanElision: disableElision, + inuseKeyRanges: keyRanges, + } + transformer := rangeKeyCompactionTransform(base.DefaultComparer.Equal, snapshots, c.elideRangeTombstone) + if err := transformer.Transform(base.DefaultComparer.Compare, span, &outSpan); err != nil { + return fmt.Sprintf("error: %s", err) + } + return outSpan.String() + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +type cpuPermissionGranter struct { + // requestCount is used to confirm that every GetPermission function call + // has a corresponding CPUWorkDone function call. + requestCount int + used bool + permit bool +} + +type cpuWorkHandle struct { + permit bool +} + +func (c cpuWorkHandle) Permitted() bool { + return c.permit +} + +func (t *cpuPermissionGranter) GetPermission(dur time.Duration) CPUWorkHandle { + t.requestCount++ + t.used = true + return cpuWorkHandle{t.permit} +} + +func (t *cpuPermissionGranter) CPUWorkDone(_ CPUWorkHandle) { + t.requestCount-- +} + +// Simple test to check if compactions are using the granter, and if exactly +// the acquired handles are returned. +func TestCompactionCPUGranter(t *testing.T) { + mem := vfs.NewMem() + opts := (&Options{FS: mem}).WithFSDefaults() + g := &cpuPermissionGranter{permit: true} + opts.Experimental.CPUWorkPermissionGranter = g + d, err := Open("", opts) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer d.Close() + + d.Set([]byte{'a'}, []byte{'a'}, nil) + err = d.Compact([]byte{'a'}, []byte{'b'}, true) + if err != nil { + t.Fatalf("Compact: %v", err) + } + require.True(t, g.used) + require.Equal(t, g.requestCount, 0) +} + +// Tests that there's no errors or panics when the default CPU granter is used. +func TestCompactionCPUGranterDefault(t *testing.T) { + mem := vfs.NewMem() + opts := (&Options{FS: mem}).WithFSDefaults() + d, err := Open("", opts) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer d.Close() + + d.Set([]byte{'a'}, []byte{'a'}, nil) + err = d.Compact([]byte{'a'}, []byte{'b'}, true) + if err != nil { + t.Fatalf("Compact: %v", err) + } +} + +func TestCompaction(t *testing.T) { + const memTableSize = 10000 + // Tuned so that 2 values can reside in the memtable before a flush, but a + // 3rd value will cause a flush. Needs to account for the max skiplist node + // size. + const valueSize = 3500 + + mem := vfs.NewMem() + opts := &Options{ + FS: mem, + MemTableSize: memTableSize, + DebugCheck: DebugCheckLevels, + L0CompactionThreshold: 8, + } + opts.testingRandomized(t).WithFSDefaults() + d, err := Open("", opts) + if err != nil { + t.Fatalf("Open: %v", err) + } + + get1 := func(iter internalIterator) (ret string) { + b := &bytes.Buffer{} + for key, _ := iter.First(); key != nil; key, _ = iter.Next() { + b.Write(key.UserKey) + } + if err := iter.Close(); err != nil { + t.Fatalf("iterator Close: %v", err) + } + return b.String() + } + getAll := func() (gotMem, gotDisk string, err error) { + d.mu.Lock() + defer d.mu.Unlock() + + if d.mu.mem.mutable != nil { + gotMem = get1(d.mu.mem.mutable.newIter(nil)) + } + ss := []string(nil) + v := d.mu.versions.currentVersion() + provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(mem, "" /* dirName */)) + if err != nil { + t.Fatalf("%v", err) + } + defer provider.Close() + for _, levelMetadata := range v.Levels { + iter := levelMetadata.Iter() + for meta := iter.First(); meta != nil; meta = iter.Next() { + if meta.Virtual { + continue + } + f, err := provider.OpenForReading(context.Background(), base.FileTypeTable, meta.FileBacking.DiskFileNum, objstorage.OpenOptions{}) + if err != nil { + return "", "", errors.WithStack(err) + } + r, err := sstable.NewReader(f, sstable.ReaderOptions{}) + if err != nil { + return "", "", errors.WithStack(err) + } + defer r.Close() + iter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return "", "", errors.WithStack(err) + } + ss = append(ss, get1(iter)+".") + } + } + sort.Strings(ss) + return gotMem, strings.Join(ss, ""), nil + } + + value := bytes.Repeat([]byte("x"), valueSize) + testCases := []struct { + key, wantMem, wantDisk string + }{ + {"+A", "A", ""}, + {"+a", "Aa", ""}, + {"+B", "B", "Aa."}, + {"+b", "Bb", "Aa."}, + // The next level-0 table overwrites the B key. + {"+C", "C", "Aa.Bb."}, + {"+B", "BC", "Aa.Bb."}, + // The next level-0 table deletes the a key. + {"+D", "D", "Aa.BC.Bb."}, + {"-a", "Da", "Aa.BC.Bb."}, + {"+d", "Dad", "Aa.BC.Bb."}, + {"+E", "E", "Aa.BC.Bb.Dad."}, + {"+e", "Ee", "Aa.BC.Bb.Dad."}, + // The next addition creates the fourth level-0 table, and l0CompactionTrigger == 8, + // but since the sublevel count is doubled when comparing with l0CompactionTrigger, + // the addition of the 4th sublevel triggers a non-trivial compaction into one level-1 table. + // Note that the keys in this one larger table are interleaved from the four smaller ones. + {"+F", "F", "ABCDEbde."}, + } + for _, tc := range testCases { + if key := tc.key[1:]; tc.key[0] == '+' { + if err := d.Set([]byte(key), value, nil); err != nil { + t.Errorf("%q: Set: %v", key, err) + break + } + } else { + if err := d.Delete([]byte(key), nil); err != nil { + t.Errorf("%q: Delete: %v", key, err) + break + } + } + + // try backs off to allow any writes to the memfs to complete. + err := try(100*time.Microsecond, 20*time.Second, func() error { + gotMem, gotDisk, err := getAll() + if err != nil { + return err + } + if testing.Verbose() { + fmt.Printf("mem=%s (%s) disk=%s (%s)\n", gotMem, tc.wantMem, gotDisk, tc.wantDisk) + } + + if gotMem != tc.wantMem { + return errors.Errorf("mem: got %q, want %q", gotMem, tc.wantMem) + } + if gotDisk != tc.wantDisk { + return errors.Errorf("ldb: got %q, want %q", gotDisk, tc.wantDisk) + } + return nil + }) + if err != nil { + t.Errorf("%q: %v", tc.key, err) + } + } + if err := d.Close(); err != nil { + t.Fatalf("db Close: %v", err) + } +} + +func TestValidateVersionEdit(t *testing.T) { + const badKey = "malformed-key" + + errValidationFailed := errors.New("validation failed") + validateFn := func(key []byte) error { + if string(key) == badKey { + return errValidationFailed + } + return nil + } + + cmp := DefaultComparer.Compare + newFileMeta := func(smallest, largest base.InternalKey) *fileMetadata { + m := (&fileMetadata{}).ExtendPointKeyBounds(cmp, smallest, largest) + m.InitPhysicalBacking() + return m + } + + testCases := []struct { + desc string + ve *versionEdit + vFunc func([]byte) error + wantErr error + }{ + { + desc: "single new file; start key", + ve: &versionEdit{ + NewFiles: []manifest.NewFileEntry{ + { + Meta: newFileMeta( + manifest.InternalKey{UserKey: []byte(badKey)}, + manifest.InternalKey{UserKey: []byte("z")}, + ), + }, + }, + }, + vFunc: validateFn, + wantErr: errValidationFailed, + }, + { + desc: "single new file; end key", + ve: &versionEdit{ + NewFiles: []manifest.NewFileEntry{ + { + Meta: newFileMeta( + manifest.InternalKey{UserKey: []byte("a")}, + manifest.InternalKey{UserKey: []byte(badKey)}, + ), + }, + }, + }, + vFunc: validateFn, + wantErr: errValidationFailed, + }, + { + desc: "multiple new files", + ve: &versionEdit{ + NewFiles: []manifest.NewFileEntry{ + { + Meta: newFileMeta( + manifest.InternalKey{UserKey: []byte("a")}, + manifest.InternalKey{UserKey: []byte("c")}, + ), + }, + { + Meta: newFileMeta( + manifest.InternalKey{UserKey: []byte(badKey)}, + manifest.InternalKey{UserKey: []byte("z")}, + ), + }, + }, + }, + vFunc: validateFn, + wantErr: errValidationFailed, + }, + { + desc: "single deleted file; start key", + ve: &versionEdit{ + DeletedFiles: map[manifest.DeletedFileEntry]*manifest.FileMetadata{ + deletedFileEntry{Level: 0, FileNum: 0}: newFileMeta( + manifest.InternalKey{UserKey: []byte(badKey)}, + manifest.InternalKey{UserKey: []byte("z")}, + ), + }, + }, + vFunc: validateFn, + wantErr: errValidationFailed, + }, + { + desc: "single deleted file; end key", + ve: &versionEdit{ + DeletedFiles: map[manifest.DeletedFileEntry]*manifest.FileMetadata{ + deletedFileEntry{Level: 0, FileNum: 0}: newFileMeta( + manifest.InternalKey{UserKey: []byte("a")}, + manifest.InternalKey{UserKey: []byte(badKey)}, + ), + }, + }, + vFunc: validateFn, + wantErr: errValidationFailed, + }, + { + desc: "multiple deleted files", + ve: &versionEdit{ + DeletedFiles: map[manifest.DeletedFileEntry]*manifest.FileMetadata{ + deletedFileEntry{Level: 0, FileNum: 0}: newFileMeta( + manifest.InternalKey{UserKey: []byte("a")}, + manifest.InternalKey{UserKey: []byte("c")}, + ), + deletedFileEntry{Level: 0, FileNum: 1}: newFileMeta( + manifest.InternalKey{UserKey: []byte(badKey)}, + manifest.InternalKey{UserKey: []byte("z")}, + ), + }, + }, + vFunc: validateFn, + wantErr: errValidationFailed, + }, + { + desc: "no errors", + ve: &versionEdit{ + NewFiles: []manifest.NewFileEntry{ + { + Level: 0, + Meta: newFileMeta( + manifest.InternalKey{UserKey: []byte("b")}, + manifest.InternalKey{UserKey: []byte("c")}, + ), + }, + { + Level: 0, + Meta: newFileMeta( + manifest.InternalKey{UserKey: []byte("d")}, + manifest.InternalKey{UserKey: []byte("g")}, + ), + }, + }, + DeletedFiles: map[manifest.DeletedFileEntry]*manifest.FileMetadata{ + deletedFileEntry{Level: 6, FileNum: 0}: newFileMeta( + manifest.InternalKey{UserKey: []byte("a")}, + manifest.InternalKey{UserKey: []byte("d")}, + ), + deletedFileEntry{Level: 6, FileNum: 1}: newFileMeta( + manifest.InternalKey{UserKey: []byte("x")}, + manifest.InternalKey{UserKey: []byte("z")}, + ), + }, + }, + vFunc: validateFn, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + err := validateVersionEdit(tc.ve, tc.vFunc, base.DefaultFormatter) + if tc.wantErr != nil { + if !errors.Is(err, tc.wantErr) { + t.Fatalf("got: %s; want: %s", err, tc.wantErr) + } + return + } + if err != nil { + t.Fatalf("got %s; wanted no error", err) + } + }) + } +} + +func TestManualCompaction(t *testing.T) { + var mem vfs.FS + var d *DB + defer func() { + if d != nil { + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + } + }() + + seed := time.Now().UnixNano() + rng := rand.New(rand.NewSource(seed)) + t.Logf("seed: %d", seed) + + randVersion := func(min, max FormatMajorVersion) FormatMajorVersion { + return FormatMajorVersion(int(min) + rng.Intn(int(max)-int(min)+1)) + } + + var compactionLog bytes.Buffer + compactionLogEventListener := &EventListener{ + CompactionEnd: func(info CompactionInfo) { + // Ensure determinism. + info.JobID = 1 + info.Duration = time.Second + info.TotalDuration = time.Second + fmt.Fprintln(&compactionLog, info.String()) + }, + } + reset := func(minVersion, maxVersion FormatMajorVersion) { + compactionLog.Reset() + if d != nil { + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + } + mem = vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + + opts := (&Options{ + FS: mem, + DebugCheck: DebugCheckLevels, + DisableAutomaticCompactions: true, + EventListener: compactionLogEventListener, + FormatMajorVersion: randVersion(minVersion, maxVersion), + }).WithFSDefaults() + + var err error + d, err = Open("", opts) + require.NoError(t, err) + } + + // d.mu must be held when calling. + createOngoingCompaction := func(start, end []byte, startLevel, outputLevel int) (ongoingCompaction *compaction) { + ongoingCompaction = &compaction{ + inputs: []compactionLevel{{level: startLevel}, {level: outputLevel}}, + smallest: InternalKey{UserKey: start}, + largest: InternalKey{UserKey: end}, + } + ongoingCompaction.startLevel = &ongoingCompaction.inputs[0] + ongoingCompaction.outputLevel = &ongoingCompaction.inputs[1] + // Mark files as compacting. + curr := d.mu.versions.currentVersion() + ongoingCompaction.startLevel.files = curr.Overlaps(startLevel, d.cmp, start, end, false) + ongoingCompaction.outputLevel.files = curr.Overlaps(outputLevel, d.cmp, start, end, false) + for _, cl := range ongoingCompaction.inputs { + iter := cl.files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + f.CompactionState = manifest.CompactionStateCompacting + } + } + d.mu.compact.inProgress[ongoingCompaction] = struct{}{} + d.mu.compact.compactingCount++ + return + } + + // d.mu must be held when calling. + deleteOngoingCompaction := func(ongoingCompaction *compaction) { + for _, cl := range ongoingCompaction.inputs { + iter := cl.files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + f.CompactionState = manifest.CompactionStateNotCompacting + } + } + delete(d.mu.compact.inProgress, ongoingCompaction) + d.mu.compact.compactingCount-- + } + + runTest := func(t *testing.T, testData string, minVersion, maxVersion FormatMajorVersion, verbose bool) { + reset(minVersion, maxVersion) + var ongoingCompaction *compaction + datadriven.RunTest(t, testData, func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + reset(minVersion, maxVersion) + return "" + + case "batch": + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + require.NoError(t, b.Commit(nil)) + return "" + + case "build": + if err := runBuildCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + + case "compact": + if err := runCompactCmd(td, d); err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + if verbose { + s = d.mu.versions.currentVersion().DebugString(base.DefaultFormatter) + } + d.mu.Unlock() + if td.HasArg("hide-file-num") { + re := regexp.MustCompile(`([0-9]*):\[`) + s = re.ReplaceAllString(s, "[") + } + return s + + case "define": + if d != nil { + if err := closeAllSnapshots(d); err != nil { + return err.Error() + } + if err := d.Close(); err != nil { + return err.Error() + } + } + + mem = vfs.NewMem() + opts := (&Options{ + FS: mem, + DebugCheck: DebugCheckLevels, + EventListener: compactionLogEventListener, + FormatMajorVersion: randVersion(minVersion, maxVersion), + DisableAutomaticCompactions: true, + }).WithFSDefaults() + + var err error + if d, err = runDBDefineCmd(td, opts); err != nil { + return err.Error() + } + + s := d.mu.versions.currentVersion().String() + if verbose { + s = d.mu.versions.currentVersion().DebugString(base.DefaultFormatter) + } + return s + + case "file-sizes": + return runTableFileSizesCmd(td, d) + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + if verbose { + s = d.mu.versions.currentVersion().DebugString(base.DefaultFormatter) + } + d.mu.Unlock() + return s + + case "ingest": + if err := runIngestCmd(td, d, mem); err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + if verbose { + s = d.mu.versions.currentVersion().DebugString(base.DefaultFormatter) + } + d.mu.Unlock() + return s + + case "iter": + // TODO(peter): runDBDefineCmd doesn't properly update the visible + // sequence number. So we have to use a snapshot with a very large + // sequence number, otherwise the DB appears empty. + snap := Snapshot{ + db: d, + seqNum: InternalKeySeqNumMax, + } + iter, _ := snap.NewIter(nil) + return runIterCmd(td, iter, true) + + case "lsm": + return runLSMCmd(td, d) + + case "populate": + b := d.NewBatch() + runPopulateCmd(t, td, b) + count := b.Count() + require.NoError(t, b.Commit(nil)) + return fmt.Sprintf("wrote %d keys\n", count) + + case "async-compact": + var s string + ch := make(chan error, 1) + go func() { + if err := runCompactCmd(td, d); err != nil { + ch <- err + close(ch) + return + } + d.mu.Lock() + s = d.mu.versions.currentVersion().String() + d.mu.Unlock() + close(ch) + }() + + manualDone := func() bool { + select { + case <-ch: + return true + default: + return false + } + } + + err := try(100*time.Microsecond, 20*time.Second, func() error { + if manualDone() { + return nil + } + + d.mu.Lock() + defer d.mu.Unlock() + if len(d.mu.compact.manual) == 0 { + return errors.New("no manual compaction queued") + } + manual := d.mu.compact.manual[0] + if manual.retries == 0 { + return errors.New("manual compaction has not been retried") + } + return nil + }) + if err != nil { + return err.Error() + } + + if manualDone() { + return "manual compaction did not block for ongoing\n" + s + } + + d.mu.Lock() + deleteOngoingCompaction(ongoingCompaction) + ongoingCompaction = nil + d.maybeScheduleCompaction() + d.mu.Unlock() + if err := <-ch; err != nil { + return err.Error() + } + return "manual compaction blocked until ongoing finished\n" + s + + case "add-ongoing-compaction": + var startLevel int + var outputLevel int + var start string + var end string + td.ScanArgs(t, "startLevel", &startLevel) + td.ScanArgs(t, "outputLevel", &outputLevel) + td.ScanArgs(t, "start", &start) + td.ScanArgs(t, "end", &end) + d.mu.Lock() + ongoingCompaction = createOngoingCompaction([]byte(start), []byte(end), startLevel, outputLevel) + d.mu.Unlock() + return "" + + case "remove-ongoing-compaction": + d.mu.Lock() + deleteOngoingCompaction(ongoingCompaction) + ongoingCompaction = nil + d.mu.Unlock() + return "" + + case "set-concurrent-compactions": + var concurrentCompactions int + td.ScanArgs(t, "num", &concurrentCompactions) + d.opts.MaxConcurrentCompactions = func() int { + return concurrentCompactions + } + return "" + + case "sstable-properties": + return runSSTablePropertiesCmd(t, td, d) + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "close-snapshots": + d.mu.Lock() + // Re-enable automatic compactions if they were disabled so that + // closing snapshots can trigger elision-only compactions if + // necessary. + d.opts.DisableAutomaticCompactions = false + + var ss []*Snapshot + l := &d.mu.snapshots + for i := l.root.next; i != &l.root; i = i.next { + ss = append(ss, i) + } + d.mu.Unlock() + for i := range ss { + if err := ss[i].Close(); err != nil { + return err.Error() + } + } + return "" + + case "compaction-log": + defer compactionLog.Reset() + return compactionLog.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) + } + + testCases := []struct { + testData string + minVersion FormatMajorVersion + maxVersion FormatMajorVersion // inclusive + verbose bool + }{ + { + testData: "testdata/manual_compaction", + minVersion: FormatMostCompatible, + maxVersion: FormatSetWithDelete - 1, + }, + { + testData: "testdata/manual_compaction_set_with_del", + minVersion: FormatBlockPropertyCollector, + // This test exercises split user keys. + maxVersion: FormatSplitUserKeysMarkedCompacted - 1, + }, + { + testData: "testdata/singledel_manual_compaction", + minVersion: FormatMostCompatible, + maxVersion: FormatSetWithDelete - 1, + }, + { + testData: "testdata/singledel_manual_compaction_set_with_del", + minVersion: FormatSetWithDelete, + maxVersion: internalFormatNewest, + }, + { + testData: "testdata/manual_compaction_range_keys", + minVersion: FormatRangeKeys, + maxVersion: internalFormatNewest, + verbose: true, + }, + { + testData: "testdata/manual_compaction_file_boundaries", + minVersion: FormatBlockPropertyCollector, + // This test exercises split user keys. + maxVersion: FormatSplitUserKeysMarkedCompacted - 1, + }, + { + testData: "testdata/manual_compaction_file_boundaries_delsized", + minVersion: FormatDeleteSizedAndObsolete, + maxVersion: internalFormatNewest, + }, + { + testData: "testdata/manual_compaction_set_with_del_sstable_Pebblev4", + minVersion: FormatDeleteSizedAndObsolete, + maxVersion: internalFormatNewest, + }, + { + testData: "testdata/manual_compaction_multilevel", + minVersion: FormatMostCompatible, + maxVersion: internalFormatNewest, + }, + } + + for _, tc := range testCases { + t.Run(tc.testData, func(t *testing.T) { + runTest(t, tc.testData, tc.minVersion, tc.maxVersion, tc.verbose) + }) + } +} + +func TestCompactionFindGrandparentLimit(t *testing.T) { + cmp := DefaultComparer.Compare + var grandparents []*fileMetadata + + var fileNum base.FileNum + parseMeta := func(s string) *fileMetadata { + parts := strings.Split(s, "-") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + fileNum++ + m := (&fileMetadata{ + FileNum: fileNum, + }).ExtendPointKeyBounds( + cmp, + InternalKey{UserKey: []byte(parts[0])}, + InternalKey{UserKey: []byte(parts[1])}, + ) + m.InitPhysicalBacking() + return m + } + + datadriven.RunTest(t, "testdata/compaction_find_grandparent_limit", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + grandparents = nil + if len(d.Input) == 0 { + return "" + } + for _, data := range strings.Split(d.Input, "\n") { + parts := strings.Fields(data) + if len(parts) != 2 { + return fmt.Sprintf("malformed test:\n%s", d.Input) + } + + meta := parseMeta(parts[0]) + var err error + meta.Size, err = strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return err.Error() + } + grandparents = append(grandparents, meta) + } + return "" + + case "compact": + c := &compaction{ + cmp: cmp, + equal: DefaultComparer.Equal, + comparer: DefaultComparer, + grandparents: manifest.NewLevelSliceKeySorted(cmp, grandparents), + } + if len(d.CmdArgs) != 1 { + return fmt.Sprintf("%s expects 1 argument", d.Cmd) + } + if len(d.CmdArgs[0].Vals) != 1 { + return fmt.Sprintf("%s expects 1 value", d.CmdArgs[0].Key) + } + var err error + c.maxOverlapBytes, err = strconv.ParseUint(d.CmdArgs[0].Vals[0], 10, 64) + if err != nil { + return err.Error() + } + + var buf bytes.Buffer + var smallest, largest string + var grandparentLimit []byte + for i, key := range strings.Fields(d.Input) { + if i == 0 { + smallest = key + grandparentLimit = c.findGrandparentLimit([]byte(key)) + } + if grandparentLimit != nil && c.cmp(grandparentLimit, []byte(key)) < 0 { + fmt.Fprintf(&buf, "%s-%s\n", smallest, largest) + smallest = key + grandparentLimit = c.findGrandparentLimit([]byte(key)) + } + largest = key + } + fmt.Fprintf(&buf, "%s-%s\n", smallest, largest) + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestCompactionFindL0Limit(t *testing.T) { + cmp := DefaultComparer.Compare + + fileNumCounter := 1 + parseMeta := func(s string) (*fileMetadata, error) { + fields := strings.Fields(s) + parts := strings.Split(fields[0], "-") + if len(parts) != 2 { + return nil, errors.Errorf("malformed table spec: %s", s) + } + m := (&fileMetadata{ + FileNum: base.FileNum(fileNumCounter), + }).ExtendPointKeyBounds( + cmp, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + fileNumCounter++ + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + + for _, field := range fields[1:] { + parts := strings.Split(field, "=") + switch parts[0] { + case "size": + size, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + t.Fatal(err) + } + m.Size = size + } + } + m.InitPhysicalBacking() + return m, nil + } + + var vers *version + flushSplitBytes := int64(0) + + datadriven.RunTest(t, "testdata/compaction_find_l0_limit", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + fileMetas := [manifest.NumLevels][]*fileMetadata{} + baseLevel := manifest.NumLevels - 1 + level := 0 + d.MaybeScanArgs(t, "flush_split_bytes", &flushSplitBytes) + + var err error + for _, data := range strings.Split(d.Input, "\n") { + data = strings.TrimSpace(data) + switch data { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err = strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + default: + meta, err := parseMeta(data) + if err != nil { + return err.Error() + } + if level != 0 && level < baseLevel { + baseLevel = level + } + fileMetas[level] = append(fileMetas[level], meta) + } + } + + vers = manifest.NewVersion(DefaultComparer.Compare, base.DefaultFormatter, flushSplitBytes, fileMetas) + flushSplitKeys := vers.L0Sublevels.FlushSplitKeys() + + var buf strings.Builder + buf.WriteString(vers.String()) + buf.WriteString("flush split keys:\n") + for _, key := range flushSplitKeys { + fmt.Fprintf(&buf, "\t%s\n", base.DefaultFormatter(key)) + } + + return buf.String() + + case "flush": + c := &compaction{ + cmp: cmp, + equal: DefaultComparer.Equal, + comparer: DefaultComparer, + version: vers, + l0Limits: vers.L0Sublevels.FlushSplitKeys(), + inputs: []compactionLevel{{level: -1}, {level: 0}}, + } + c.startLevel, c.outputLevel = &c.inputs[0], &c.inputs[1] + + var buf bytes.Buffer + var smallest, largest string + var l0Limit []byte + for i, key := range strings.Fields(d.Input) { + if i == 0 { + smallest = key + l0Limit = c.findL0Limit([]byte(key)) + } + if l0Limit != nil && c.cmp(l0Limit, []byte(key)) < 0 { + fmt.Fprintf(&buf, "%s-%s\n", smallest, largest) + smallest = key + l0Limit = c.findL0Limit([]byte(key)) + } + largest = key + } + fmt.Fprintf(&buf, "%s-%s\n", smallest, largest) + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestCompactionOutputLevel(t *testing.T) { + opts := (*Options)(nil).EnsureDefaults() + version := &version{} + + datadriven.RunTest(t, "testdata/compaction_output_level", + func(t *testing.T, d *datadriven.TestData) (res string) { + defer func() { + if r := recover(); r != nil { + res = fmt.Sprintln(r) + } + }() + + switch d.Cmd { + case "compact": + var start, base int + d.ScanArgs(t, "start", &start) + d.ScanArgs(t, "base", &base) + pc := newPickedCompaction(opts, version, start, defaultOutputLevel(start, base), base) + c := newCompaction(pc, opts, time.Now(), nil /* provider */) + return fmt.Sprintf("output=%d\nmax-output-file-size=%d\n", + c.outputLevel.level, c.maxOutputFileSize) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestCompactionAtomicUnitBounds(t *testing.T) { + cmp := DefaultComparer.Compare + var files manifest.LevelSlice + + parseMeta := func(s string) *fileMetadata { + parts := strings.Split(s, "-") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + m := (&fileMetadata{}).ExtendPointKeyBounds( + cmp, + base.ParseInternalKey(parts[0]), + base.ParseInternalKey(parts[1]), + ) + m.InitPhysicalBacking() + return m + } + + datadriven.RunTest(t, "testdata/compaction_atomic_unit_bounds", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + files = manifest.LevelSlice{} + if len(d.Input) == 0 { + return "" + } + var ff []*fileMetadata + for _, data := range strings.Split(d.Input, "\n") { + meta := parseMeta(data) + meta.FileNum = FileNum(len(ff)) + ff = append(ff, meta) + } + files = manifest.NewLevelSliceKeySorted(cmp, ff) + return "" + + case "atomic-unit-bounds": + c := &compaction{ + cmp: cmp, + equal: DefaultComparer.Equal, + comparer: DefaultComparer, + inputs: []compactionLevel{{files: files}, {}}, + } + c.startLevel, c.outputLevel = &c.inputs[0], &c.inputs[1] + if len(d.CmdArgs) != 1 { + return fmt.Sprintf("%s expects 1 argument", d.Cmd) + } + index, err := strconv.ParseInt(d.CmdArgs[0].String(), 10, 64) + if err != nil { + return err.Error() + } + iter := files.Iter() + // Advance iter to `index`. + _ = iter.First() + for i := int64(0); i < index; i++ { + _ = iter.Next() + } + atomicUnit, _ := expandToAtomicUnit(c.cmp, iter.Take().Slice(), true /* disableIsCompacting */) + lower, upper := manifest.KeyRange(c.cmp, atomicUnit.Iter()) + return fmt.Sprintf("%s-%s\n", lower.UserKey, upper.UserKey) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestCompactionDeleteOnlyHints(t *testing.T) { + parseUint64 := func(s string) uint64 { + v, err := strconv.ParseUint(s, 10, 64) + require.NoError(t, err) + return v + } + var d *DB + defer func() { + if d != nil { + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + } + }() + + var compactInfo *CompactionInfo // protected by d.mu + reset := func() (*Options, error) { + if d != nil { + compactInfo = nil + if err := closeAllSnapshots(d); err != nil { + return nil, err + } + if err := d.Close(); err != nil { + return nil, err + } + } + opts := (&Options{ + FS: vfs.NewMem(), + DebugCheck: DebugCheckLevels, + EventListener: &EventListener{ + CompactionEnd: func(info CompactionInfo) { + if compactInfo != nil { + return + } + compactInfo = &info + }, + }, + FormatMajorVersion: internalFormatNewest, + }).WithFSDefaults() + + // Collection of table stats can trigger compactions. As we want full + // control over when compactions are run, disable stats by default. + opts.private.disableTableStats = true + + return opts, nil + } + + compactionString := func() string { + for d.mu.compact.compactingCount > 0 { + d.mu.compact.cond.Wait() + } + + s := "(none)" + if compactInfo != nil { + // Fix the job ID and durations for determinism. + compactInfo.JobID = 100 + compactInfo.Duration = time.Second + compactInfo.TotalDuration = 2 * time.Second + s = compactInfo.String() + compactInfo = nil + } + return s + } + + var err error + var opts *Options + datadriven.RunTest(t, "testdata/compaction_delete_only_hints", + func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + opts, err = reset() + if err != nil { + return err.Error() + } + d, err = runDBDefineCmd(td, opts) + if err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "force-set-hints": + d.mu.Lock() + defer d.mu.Unlock() + d.mu.compact.deletionHints = d.mu.compact.deletionHints[:0] + var buf bytes.Buffer + for _, data := range strings.Split(td.Input, "\n") { + parts := strings.FieldsFunc(strings.TrimSpace(data), + func(r rune) bool { return r == '-' || r == ' ' || r == '.' }) + + start, end := []byte(parts[2]), []byte(parts[3]) + + var tombstoneFile *fileMetadata + tombstoneLevel := int(parseUint64(parts[0][1:])) + + // Set file number to the value provided in the input. + tombstoneFile = &fileMetadata{ + FileNum: base.FileNum(parseUint64(parts[1])), + } + + var hintType deleteCompactionHintType + switch typ := parts[7]; typ { + case "point_key_only": + hintType = deleteCompactionHintTypePointKeyOnly + case "range_key_only": + hintType = deleteCompactionHintTypeRangeKeyOnly + case "point_and_range_key": + hintType = deleteCompactionHintTypePointAndRangeKey + default: + return fmt.Sprintf("unknown hint type: %s", typ) + } + + h := deleteCompactionHint{ + hintType: hintType, + start: start, + end: end, + fileSmallestSeqNum: parseUint64(parts[4]), + tombstoneLevel: tombstoneLevel, + tombstoneFile: tombstoneFile, + tombstoneSmallestSeqNum: parseUint64(parts[5]), + tombstoneLargestSeqNum: parseUint64(parts[6]), + } + d.mu.compact.deletionHints = append(d.mu.compact.deletionHints, h) + fmt.Fprintln(&buf, h.String()) + } + return buf.String() + + case "get-hints": + d.mu.Lock() + defer d.mu.Unlock() + + // Force collection of table stats. This requires re-enabling the + // collection flag. We also do not want compactions to run as part of + // the stats collection job, so we disable it temporarily. + d.opts.private.disableTableStats = false + d.opts.DisableAutomaticCompactions = true + defer func() { + d.opts.private.disableTableStats = true + d.opts.DisableAutomaticCompactions = false + }() + + // NB: collectTableStats attempts to acquire the lock. Temporarily + // unlock here to avoid a deadlock. + d.mu.Unlock() + didRun := d.collectTableStats() + d.mu.Lock() + + if !didRun { + // If a job was already running, wait for the results. + d.waitTableStats() + } + + hints := d.mu.compact.deletionHints + if len(hints) == 0 { + return "(none)" + } + var buf bytes.Buffer + for _, h := range hints { + buf.WriteString(h.String() + "\n") + } + return buf.String() + + case "maybe-compact": + d.mu.Lock() + d.maybeScheduleCompaction() + + var buf bytes.Buffer + fmt.Fprintf(&buf, "Deletion hints:\n") + for _, h := range d.mu.compact.deletionHints { + fmt.Fprintf(&buf, " %s\n", h.String()) + } + if len(d.mu.compact.deletionHints) == 0 { + fmt.Fprintf(&buf, " (none)\n") + } + fmt.Fprintf(&buf, "Compactions:\n") + fmt.Fprintf(&buf, " %s", compactionString()) + d.mu.Unlock() + return buf.String() + + case "compact": + if err := runCompactCmd(td, d); err != nil { + return err.Error() + } + d.mu.Lock() + compactInfo = nil + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "close-snapshot": + seqNum, err := strconv.ParseUint(strings.TrimSpace(td.Input), 0, 64) + if err != nil { + return err.Error() + } + d.mu.Lock() + var s *Snapshot + l := &d.mu.snapshots + for i := l.root.next; i != &l.root; i = i.next { + if i.seqNum == seqNum { + s = i + } + } + d.mu.Unlock() + if s == nil { + return "(not found)" + } else if err := s.Close(); err != nil { + return err.Error() + } + + d.mu.Lock() + // Closing the snapshot may have triggered a compaction. + str := compactionString() + d.mu.Unlock() + return str + + case "iter": + snap := Snapshot{ + db: d, + seqNum: InternalKeySeqNumMax, + } + iter, _ := snap.NewIter(nil) + return runIterCmd(td, iter, true) + + case "reset": + opts, err = reset() + if err != nil { + return err.Error() + } + d, err = Open("", opts) + if err != nil { + return err.Error() + } + return "" + + case "ingest": + if err = runBuildCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + if err = runIngestCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + return "OK" + + case "describe-lsm": + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestCompactionTombstones(t *testing.T) { + var d *DB + defer func() { + if d != nil { + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + } + }() + + var compactInfo *CompactionInfo // protected by d.mu + + compactionString := func() string { + for d.mu.compact.compactingCount > 0 { + d.mu.compact.cond.Wait() + } + + s := "(none)" + if compactInfo != nil { + // Fix the job ID and durations for determinism. + compactInfo.JobID = 100 + compactInfo.Duration = time.Second + compactInfo.TotalDuration = 2 * time.Second + s = compactInfo.String() + compactInfo = nil + } + return s + } + + datadriven.RunTest(t, "testdata/compaction_tombstones", + func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + if d != nil { + compactInfo = nil + require.NoError(t, closeAllSnapshots(d)) + if err := d.Close(); err != nil { + return err.Error() + } + } + opts := (&Options{ + FS: vfs.NewMem(), + DebugCheck: DebugCheckLevels, + EventListener: &EventListener{ + CompactionEnd: func(info CompactionInfo) { + compactInfo = &info + }, + }, + FormatMajorVersion: internalFormatNewest, + }).WithFSDefaults() + var err error + d, err = runDBDefineCmd(td, opts) + if err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "maybe-compact": + d.mu.Lock() + d.opts.DisableAutomaticCompactions = false + d.maybeScheduleCompaction() + s := compactionString() + d.mu.Unlock() + return s + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "close-snapshot": + seqNum, err := strconv.ParseUint(strings.TrimSpace(td.Input), 0, 64) + if err != nil { + return err.Error() + } + d.mu.Lock() + var s *Snapshot + l := &d.mu.snapshots + for i := l.root.next; i != &l.root; i = i.next { + if i.seqNum == seqNum { + s = i + } + } + d.mu.Unlock() + if s == nil { + return "(not found)" + } else if err := s.Close(); err != nil { + return err.Error() + } + + d.mu.Lock() + // Closing the snapshot may have triggered a compaction. + str := compactionString() + d.mu.Unlock() + return str + + case "close": + if err := d.Close(); err != nil { + return err.Error() + } + d = nil + return "" + + case "version": + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func closeAllSnapshots(d *DB) error { + d.mu.Lock() + var ss []*Snapshot + l := &d.mu.snapshots + for i := l.root.next; i != &l.root; i = i.next { + ss = append(ss, i) + } + d.mu.Unlock() + for i := range ss { + if err := ss[i].Close(); err != nil { + return err + } + } + return nil +} + +func TestCompactionReadTriggeredQueue(t *testing.T) { + + // Convert a read compaction to a string which this test + // understands. + showRC := func(rc *readCompaction) string { + return fmt.Sprintf( + "L%d: %s-%s %d\n", rc.level, string(rc.start), string(rc.end), rc.fileNum, + ) + } + + var queue *readCompactionQueue + + datadriven.RunTest(t, "testdata/read_compaction_queue", + func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "create": + queue = &readCompactionQueue{} + return "(success)" + case "add-compaction": + for _, line := range strings.Split(td.Input, "\n") { + if line == "" { + continue + } + parts := strings.Split(line, " ") + + if len(parts) != 3 { + return "error: malformed data for add-compaction. usage: : - " + } + if l, err := strconv.Atoi(parts[0][1:2]); err == nil { + keys := strings.Split(parts[1], "-") + fileNum, _ := strconv.Atoi(parts[2]) + rc := readCompaction{ + level: l, + start: []byte(keys[0]), + end: []byte(keys[1]), + fileNum: base.FileNum(fileNum), + } + queue.add(&rc, DefaultComparer.Compare) + } else { + return err.Error() + } + } + return "" + case "remove-compaction": + rc := queue.remove() + if rc == nil { + return "(nil)" + } + return showRC(rc) + case "print-size": + // Print the size of the queue. + return fmt.Sprintf("%d", queue.size) + case "print-queue": + // Print each element of the queue on a separate line. + var sb strings.Builder + if queue.size == 0 { + sb.WriteString("(empty)") + } + + for i := 0; i < queue.size; i++ { + rc := queue.at(i) + sb.WriteString(showRC(rc)) + } + return sb.String() + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }, + ) +} + +func (qu *readCompactionQueue) at(i int) *readCompaction { + if i >= qu.size { + return nil + } + + return qu.queue[i] +} + +func TestCompactionReadTriggered(t *testing.T) { + var d *DB + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + var compactInfo *CompactionInfo // protected by d.mu + + compactionString := func() string { + for d.mu.compact.compactingCount > 0 { + d.mu.compact.cond.Wait() + } + + s := "(none)" + if compactInfo != nil { + // Fix the job ID and durations for determinism. + compactInfo.JobID = 100 + compactInfo.Duration = time.Second + compactInfo.TotalDuration = 2 * time.Second + s = compactInfo.String() + compactInfo = nil + } + return s + } + + datadriven.RunTest(t, "testdata/compaction_read_triggered", + func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + if d != nil { + compactInfo = nil + if err := d.Close(); err != nil { + return err.Error() + } + } + opts := (&Options{ + FS: vfs.NewMem(), + DebugCheck: DebugCheckLevels, + EventListener: &EventListener{ + CompactionEnd: func(info CompactionInfo) { + compactInfo = &info + }, + }, + }).WithFSDefaults() + var err error + d, err = runDBDefineCmd(td, opts) + if err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "add-read-compaction": + d.mu.Lock() + td.MaybeScanArgs(t, "flushing", &d.mu.compact.flushing) + for _, line := range strings.Split(td.Input, "\n") { + if line == "" { + continue + } + parts := strings.Split(line, " ") + if len(parts) != 3 { + return "error: malformed data for add-read-compaction. usage: : - " + } + if l, err := strconv.Atoi(parts[0][:1]); err == nil { + keys := strings.Split(parts[1], "-") + fileNum, _ := strconv.Atoi(parts[2]) + rc := readCompaction{ + level: l, + start: []byte(keys[0]), + end: []byte(keys[1]), + fileNum: base.FileNum(fileNum), + } + d.mu.compact.readCompactions.add(&rc, DefaultComparer.Compare) + } else { + return err.Error() + } + } + d.mu.Unlock() + return "" + + case "show-read-compactions": + d.mu.Lock() + var sb strings.Builder + if d.mu.compact.readCompactions.size == 0 { + sb.WriteString("(none)") + } + for i := 0; i < d.mu.compact.readCompactions.size; i++ { + rc := d.mu.compact.readCompactions.at(i) + sb.WriteString(fmt.Sprintf("(level: %d, start: %s, end: %s)\n", rc.level, string(rc.start), string(rc.end))) + } + d.mu.Unlock() + return sb.String() + + case "maybe-compact": + d.mu.Lock() + d.opts.DisableAutomaticCompactions = false + d.maybeScheduleCompaction() + s := compactionString() + d.mu.Unlock() + return s + + case "version": + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestCompactionInuseKeyRanges(t *testing.T) { + cmp := DefaultComparer.Compare + parseMeta := func(s string) *fileMetadata { + parts := strings.Split(s, "-") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + m := (&fileMetadata{}).ExtendRangeKeyBounds( + cmp, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + m.InitPhysicalBacking() + return m + } + + opts := (*Options)(nil).EnsureDefaults() + + var c *compaction + datadriven.RunTest(t, "testdata/compaction_inuse_key_ranges", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + c = &compaction{ + cmp: DefaultComparer.Compare, + equal: DefaultComparer.Equal, + comparer: DefaultComparer, + formatKey: DefaultComparer.FormatKey, + inputs: []compactionLevel{{}, {}}, + } + c.startLevel, c.outputLevel = &c.inputs[0], &c.inputs[1] + var files [numLevels][]*fileMetadata + var currentLevel int + fileNum := FileNum(1) + + for _, data := range strings.Split(td.Input, "\n") { + switch data { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err := strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + currentLevel = level + + default: + meta := parseMeta(data) + meta.FileNum = fileNum + fileNum++ + files[currentLevel] = append(files[currentLevel], meta) + } + } + c.version = newVersion(opts, files) + return c.version.String() + + case "inuse-key-ranges": + var buf bytes.Buffer + for _, line := range strings.Split(td.Input, "\n") { + parts := strings.Fields(line) + if len(parts) != 3 { + fmt.Fprintf(&buf, "expected : %q\n", line) + continue + } + level, err := strconv.Atoi(parts[0]) + if err != nil { + fmt.Fprintf(&buf, "expected : %q: %v\n", line, err) + continue + } + c.outputLevel.level = level + c.smallest.UserKey = []byte(parts[1]) + c.largest.UserKey = []byte(parts[2]) + + c.inuseKeyRanges = nil + c.setupInuseKeyRanges() + if len(c.inuseKeyRanges) == 0 { + fmt.Fprintf(&buf, ".\n") + } else { + for i, r := range c.inuseKeyRanges { + if i > 0 { + fmt.Fprintf(&buf, " ") + } + fmt.Fprintf(&buf, "%s-%s", r.Start, r.End) + } + fmt.Fprintf(&buf, "\n") + } + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestCompactionInuseKeyRangesRandomized(t *testing.T) { + var ( + fileNum = FileNum(0) + opts = (*Options)(nil).EnsureDefaults() + seed = int64(time.Now().UnixNano()) + rng = rand.New(rand.NewSource(seed)) + endKeyspace = 26 * 26 + ) + t.Logf("Using rng seed %d.", seed) + + for iter := 0; iter < 100; iter++ { + makeUserKey := func(i int) []byte { + if i >= endKeyspace { + i = endKeyspace - 1 + } + return []byte{byte(i/26 + 'a'), byte(i%26 + 'a')} + } + makeIK := func(level, i int) InternalKey { + return base.MakeInternalKey( + makeUserKey(i), + uint64(numLevels-level), + base.InternalKeyKindSet, + ) + } + makeFile := func(level, start, end int) *fileMetadata { + fileNum++ + m := (&fileMetadata{ + FileNum: fileNum, + }).ExtendPointKeyBounds( + opts.Comparer.Compare, + makeIK(level, start), + makeIK(level, end), + ) + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + m.InitPhysicalBacking() + return m + } + overlaps := func(startA, endA, startB, endB []byte) bool { + disjoint := opts.Comparer.Compare(endB, startA) < 0 || opts.Comparer.Compare(endA, startB) < 0 + return !disjoint + } + var files [numLevels][]*fileMetadata + for l := 0; l < numLevels; l++ { + for i := 0; i < rand.Intn(10); i++ { + s := rng.Intn(endKeyspace) + maxWidth := rng.Intn(endKeyspace-s) + 1 + e := rng.Intn(maxWidth) + s + sKey, eKey := makeUserKey(s), makeUserKey(e) + // Discard the key range if it overlaps any existing files + // within this level. + var o bool + for _, f := range files[l] { + o = o || overlaps(sKey, eKey, f.Smallest.UserKey, f.Largest.UserKey) + } + if o { + continue + } + files[l] = append(files[l], makeFile(l, s, e)) + } + slices.SortFunc(files[l], func(a, b *fileMetadata) int { + return opts.Comparer.Compare(a.Smallest.UserKey, b.Smallest.UserKey) + }) + } + v := newVersion(opts, files) + t.Log(v.DebugString(opts.Comparer.FormatKey)) + for i := 0; i < 1000; i++ { + l := rng.Intn(numLevels) + s := rng.Intn(endKeyspace) + maxWidth := rng.Intn(endKeyspace-s) + 1 + e := rng.Intn(maxWidth) + s + sKey, eKey := makeUserKey(s), makeUserKey(e) + keyRanges := calculateInuseKeyRanges(v, opts.Comparer.Compare, l, numLevels-1, sKey, eKey) + + for level := l; level < numLevels; level++ { + for _, f := range files[level] { + if !overlaps(sKey, eKey, f.Smallest.UserKey, f.Largest.UserKey) { + // This file doesn't overlap the queried range. Skip it. + continue + } + // This file does overlap the queried range. The key range + // [MAX(f.Smallest, sKey), MIN(f.Largest, eKey)] must be fully + // contained by a key range in keyRanges. + checkStart, checkEnd := f.Smallest.UserKey, f.Largest.UserKey + if opts.Comparer.Compare(checkStart, sKey) < 0 { + checkStart = sKey + } + if opts.Comparer.Compare(checkEnd, eKey) > 0 { + checkEnd = eKey + } + var contained bool + for _, kr := range keyRanges { + contained = contained || + (opts.Comparer.Compare(checkStart, kr.Start) >= 0 && + opts.Comparer.Compare(checkEnd, kr.End) <= 0) + } + if !contained { + t.Errorf("Seed %d, iter %d: File %s overlaps %q-%q, but is not fully contained in any of the key ranges.", + seed, iter, f, sKey, eKey) + } + } + } + } + } +} + +func TestCompactionAllowZeroSeqNum(t *testing.T) { + var d *DB + defer func() { + if d != nil { + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + } + }() + + metaRE := regexp.MustCompile(`^L([0-9]+):([^-]+)-(.+)$`) + var fileNum base.FileNum + parseMeta := func(s string) (level int, meta *fileMetadata) { + match := metaRE.FindStringSubmatch(s) + if match == nil { + t.Fatalf("malformed table spec: %s", s) + } + level, err := strconv.Atoi(match[1]) + if err != nil { + t.Fatalf("malformed table spec: %s: %s", s, err) + } + fileNum++ + meta = (&fileMetadata{ + FileNum: fileNum, + }).ExtendPointKeyBounds( + d.cmp, + InternalKey{UserKey: []byte(match[2])}, + InternalKey{UserKey: []byte(match[3])}, + ) + meta.InitPhysicalBacking() + return level, meta + } + + datadriven.RunTest(t, "testdata/compaction_allow_zero_seqnum", + func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + if d != nil { + require.NoError(t, closeAllSnapshots(d)) + if err := d.Close(); err != nil { + return err.Error() + } + } + + var err error + if d, err = runDBDefineCmd(td, nil /* options */); err != nil { + return err.Error() + } + + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "allow-zero-seqnum": + d.mu.Lock() + c := &compaction{ + cmp: d.cmp, + comparer: d.opts.Comparer, + version: d.mu.versions.currentVersion(), + inputs: []compactionLevel{{}, {}}, + } + c.startLevel, c.outputLevel = &c.inputs[0], &c.inputs[1] + d.mu.Unlock() + + var buf bytes.Buffer + for _, line := range strings.Split(td.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + c.flushing = nil + c.startLevel.level = -1 + + var startFiles, outputFiles []*fileMetadata + + switch { + case len(parts) == 1 && parts[0] == "flush": + c.outputLevel.level = 0 + d.mu.Lock() + c.flushing = d.mu.mem.queue + d.mu.Unlock() + + default: + for _, p := range parts { + level, meta := parseMeta(p) + if c.startLevel.level == -1 { + c.startLevel.level = level + } + + switch level { + case c.startLevel.level: + startFiles = append(startFiles, meta) + case c.startLevel.level + 1: + outputFiles = append(outputFiles, meta) + default: + return fmt.Sprintf("invalid level %d: expected %d or %d", + level, c.startLevel.level, c.startLevel.level+1) + } + } + c.outputLevel.level = c.startLevel.level + 1 + c.startLevel.files = manifest.NewLevelSliceSpecificOrder(startFiles) + c.outputLevel.files = manifest.NewLevelSliceKeySorted(c.cmp, outputFiles) + } + + c.smallest, c.largest = manifest.KeyRange(c.cmp, + c.startLevel.files.Iter(), + c.outputLevel.files.Iter()) + + c.inuseKeyRanges = nil + c.setupInuseKeyRanges() + fmt.Fprintf(&buf, "%t\n", c.allowZeroSeqNum()) + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestCompactionErrorOnUserKeyOverlap(t *testing.T) { + cmp := DefaultComparer.Compare + parseMeta := func(s string) *fileMetadata { + parts := strings.Split(s, "-") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + m := (&fileMetadata{}).ExtendPointKeyBounds( + cmp, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + m.InitPhysicalBacking() + return m + } + + datadriven.RunTest(t, "testdata/compaction_error_on_user_key_overlap", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "error-on-user-key-overlap": + c := &compaction{ + cmp: DefaultComparer.Compare, + comparer: DefaultComparer, + formatKey: DefaultComparer.FormatKey, + } + var files []manifest.NewFileEntry + fileNum := FileNum(1) + + for _, data := range strings.Split(d.Input, "\n") { + meta := parseMeta(data) + meta.FileNum = fileNum + fileNum++ + files = append(files, manifest.NewFileEntry{Level: 1, Meta: meta}) + } + + result := "OK" + ve := &versionEdit{ + NewFiles: files, + } + if err := c.errorOnUserKeyOverlap(ve); err != nil { + result = fmt.Sprint(err) + } + return result + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +// TestCompactionErrorCleanup tests an error encountered during a compaction +// after some output tables have been created. It ensures that the pending +// output tables are removed from the filesystem. +func TestCompactionErrorCleanup(t *testing.T) { + // protected by d.mu + var ( + initialSetupDone bool + tablesCreated []FileNum + ) + + mem := vfs.NewMem() + ii := errorfs.OnIndex(math.MaxInt32) // start disabled + opts := (&Options{ + FS: errorfs.Wrap(mem, errorfs.ErrInjected.If(ii)), + Levels: make([]LevelOptions, numLevels), + EventListener: &EventListener{ + TableCreated: func(info TableCreateInfo) { + t.Log(info) + + // If the initial setup is over, record tables created and + // inject an error immediately after the second table is + // created. + if initialSetupDone { + tablesCreated = append(tablesCreated, info.FileNum) + if len(tablesCreated) >= 2 { + ii.Store(0) + } + } + }, + }, + }).WithFSDefaults() + for i := range opts.Levels { + opts.Levels[i].TargetFileSize = 1 + } + opts.testingRandomized(t) + d, err := Open("", opts) + require.NoError(t, err) + + ingest := func(keys ...string) { + t.Helper() + f, err := mem.Create("ext") + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + for _, k := range keys { + require.NoError(t, w.Set([]byte(k), nil)) + } + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{"ext"})) + } + ingest("a", "c") + ingest("b") + + // Trigger a manual compaction, which will encounter an injected error + // after the second table is created. + d.mu.Lock() + initialSetupDone = true + d.mu.Unlock() + err = d.Compact([]byte("a"), []byte("d"), false) + require.Error(t, err, "injected error") + + d.mu.Lock() + if len(tablesCreated) < 2 { + t.Fatalf("expected 2 output tables created by compaction: found %d", len(tablesCreated)) + } + d.mu.Unlock() + + require.NoError(t, d.Close()) + for _, fileNum := range tablesCreated { + filename := fmt.Sprintf("%s.sst", fileNum) + if _, err = mem.Stat(filename); err == nil || !oserror.IsNotExist(err) { + t.Errorf("expected %q to not exist: %s", filename, err) + } + } +} + +func TestCompactionCheckOrdering(t *testing.T) { + cmp := DefaultComparer.Compare + parseMeta := func(s string) *fileMetadata { + parts := strings.Split(s, "-") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + m := (&fileMetadata{}).ExtendPointKeyBounds( + cmp, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + m.InitPhysicalBacking() + return m + } + + datadriven.RunTest(t, "testdata/compaction_check_ordering", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "check-ordering": + c := &compaction{ + cmp: DefaultComparer.Compare, + comparer: DefaultComparer, + formatKey: DefaultComparer.FormatKey, + logger: panicLogger{}, + inputs: []compactionLevel{{level: -1}, {level: -1}}, + } + c.startLevel, c.outputLevel = &c.inputs[0], &c.inputs[1] + var startFiles, outputFiles []*fileMetadata + var sublevels []manifest.LevelSlice + var files *[]*fileMetadata + var sublevel []*fileMetadata + var sublevelNum int + var parsingSublevel bool + fileNum := FileNum(1) + + switchSublevel := func() { + if sublevel != nil { + sublevels = append( + sublevels, manifest.NewLevelSliceSpecificOrder(sublevel), + ) + sublevel = nil + } + parsingSublevel = false + } + + for _, data := range strings.Split(d.Input, "\n") { + if data[0] == 'L' && len(data) == 4 { + // Format L0.{sublevel}. + switchSublevel() + level, err := strconv.Atoi(data[1:2]) + if err != nil { + return err.Error() + } + sublevelNum, err = strconv.Atoi(data[3:]) + if err != nil { + return err.Error() + } + if c.startLevel.level == -1 { + c.startLevel.level = level + files = &startFiles + } + parsingSublevel = true + } else if data[0] == 'L' { + switchSublevel() + level, err := strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + if c.startLevel.level == -1 { + c.startLevel.level = level + files = &startFiles + } else if c.outputLevel.level == -1 { + if c.startLevel.level >= level { + return fmt.Sprintf("startLevel=%d >= outputLevel=%d\n", c.startLevel.level, level) + } + c.outputLevel.level = level + files = &outputFiles + } else { + return "outputLevel already set\n" + } + } else { + meta := parseMeta(data) + meta.FileNum = fileNum + fileNum++ + *files = append(*files, meta) + if parsingSublevel { + meta.SubLevel = sublevelNum + sublevel = append(sublevel, meta) + } + } + } + + switchSublevel() + c.startLevel.files = manifest.NewLevelSliceSpecificOrder(startFiles) + c.outputLevel.files = manifest.NewLevelSliceSpecificOrder(outputFiles) + if c.outputLevel.level == -1 { + c.outputLevel.level = 0 + } + if c.startLevel.level == 0 { + // We don't change the input files for the compaction beyond this point. + c.startLevel.l0SublevelInfo = generateSublevelInfo(c.cmp, c.startLevel.files) + } + + newIters := func( + _ context.Context, _ *manifest.FileMetadata, _ *IterOptions, _ internalIterOpts, + ) (internalIterator, keyspan.FragmentIterator, error) { + return &errorIter{}, nil, nil + } + result := "OK" + _, err := c.newInputIter(newIters, nil, nil) + if err != nil { + result = fmt.Sprint(err) + } + return result + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +type mockSplitter struct { + shouldSplitVal maybeSplit +} + +func (m *mockSplitter) shouldSplitBefore(key *InternalKey, tw *sstable.Writer) maybeSplit { + return m.shouldSplitVal +} + +func (m *mockSplitter) onNewOutput(key []byte) []byte { + return nil +} + +func TestCompactionOutputSplitters(t *testing.T) { + var main, child0, child1 compactionOutputSplitter + var prevUserKey []byte + pickSplitter := func(input string) *compactionOutputSplitter { + switch input { + case "main": + return &main + case "child0": + return &child0 + case "child1": + return &child1 + default: + t.Fatalf("invalid splitter slot: %s", input) + return nil + } + } + + datadriven.RunTest(t, "testdata/compaction_output_splitters", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "reset": + main = nil + child0 = nil + child1 = nil + case "init": + if len(d.CmdArgs) < 2 { + return "expected at least 2 args" + } + splitterToInit := pickSplitter(d.CmdArgs[0].Key) + switch d.CmdArgs[1].Key { + case "array": + *splitterToInit = &splitterGroup{ + cmp: base.DefaultComparer.Compare, + splitters: []compactionOutputSplitter{child0, child1}, + } + case "mock": + *splitterToInit = &mockSplitter{} + case "userkey": + *splitterToInit = &userKeyChangeSplitter{ + cmp: base.DefaultComparer.Compare, + unsafePrevUserKey: func() []byte { + return prevUserKey + }, + splitter: child0, + } + } + (*splitterToInit).onNewOutput(nil) + case "set-should-split": + if len(d.CmdArgs) < 2 { + return "expected at least 2 args" + } + splitterToSet := (*pickSplitter(d.CmdArgs[0].Key)).(*mockSplitter) + var val maybeSplit + switch d.CmdArgs[1].Key { + case "split-now": + val = splitNow + case "no-split": + val = noSplit + default: + t.Fatalf("unexpected value for should-split: %s", d.CmdArgs[1].Key) + } + splitterToSet.shouldSplitVal = val + case "should-split-before": + if len(d.CmdArgs) < 1 { + return "expected at least 1 arg" + } + key := base.ParseInternalKey(d.CmdArgs[0].Key) + shouldSplit := main.shouldSplitBefore(&key, nil) + if shouldSplit == splitNow { + main.onNewOutput(key.UserKey) + prevUserKey = nil + } else { + prevUserKey = key.UserKey + } + return shouldSplit.String() + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + return "ok" + }) +} + +func TestCompactFlushQueuedMemTableAndFlushMetrics(t *testing.T) { + t.Run("", func(t *testing.T) { + // Verify that manual compaction forces a flush of a queued memtable. + + mem := vfs.NewMem() + d, err := Open("", testingRandomized(t, &Options{ + FS: mem, + }).WithFSDefaults()) + require.NoError(t, err) + + // Add the key "a" to the memtable, then fill up the memtable with the key + // prefix "b". The compaction will only overlap with the queued memtable, + // not the mutable memtable. + // NB: The initial memtable size is 256KB, which is filled up with random + // values which typically don't compress well. The test also appends the + // random value to the "b" key to limit overwriting of the same key, which + // would get collapsed at flush time since there are no open snapshots. + value := make([]byte, 50) + _, err = crand.Read(value) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("a"), value, nil)) + for { + _, err = crand.Read(value) + require.NoError(t, err) + require.NoError(t, d.Set(append([]byte("b"), value...), value, nil)) + d.mu.Lock() + done := len(d.mu.mem.queue) == 2 + d.mu.Unlock() + if done { + break + } + } + + require.NoError(t, d.Compact([]byte("a"), []byte("a\x00"), false)) + d.mu.Lock() + require.Equal(t, 1, len(d.mu.mem.queue)) + d.mu.Unlock() + // Flush metrics are updated after and non-atomically with the memtable + // being removed from the queue. + for begin := time.Now(); ; { + metrics := d.Metrics() + require.NotNil(t, metrics) + if metrics.Flush.WriteThroughput.Bytes >= 50*1024 { + // The writes (during which the flush is idle) and the flush work + // should not be so fast as to be unrealistic. If these turn out to be + // flaky we could instead inject a clock. + // + // Windows timer precision is bad (on the order of 1 millisecond) and + // can cause the duration to be 0. + if runtime.GOOS != "windows" { + tinyInterval := 50 * time.Microsecond + require.Less(t, tinyInterval, metrics.Flush.WriteThroughput.WorkDuration) + require.Less(t, tinyInterval, metrics.Flush.WriteThroughput.IdleDuration) + } + break + } + if time.Since(begin) > 2*time.Second { + t.Fatal("flush did not happen") + } + time.Sleep(time.Millisecond) + } + require.NoError(t, d.Close()) + }) +} + +func TestCompactFlushQueuedLargeBatch(t *testing.T) { + // Verify that compaction forces a flush of a queued large batch. + + mem := vfs.NewMem() + d, err := Open("", testingRandomized(t, &Options{ + FS: mem, + }).WithFSDefaults()) + require.NoError(t, err) + + // The default large batch threshold is slightly less than 1/2 of the + // memtable size which makes triggering a problem with flushing queued large + // batches irritating. Manually adjust the threshold to 1/8 of the memtable + // size in order to more easily create a situation where a large batch is + // queued but not automatically flushed. + d.mu.Lock() + d.largeBatchThreshold = d.opts.MemTableSize / 8 + require.Equal(t, 1, len(d.mu.mem.queue)) + d.mu.Unlock() + + // Set a record with a large value. This will be transformed into a large + // batch and placed in the flushable queue. + require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("v"), int(d.largeBatchThreshold)), nil)) + d.mu.Lock() + require.Greater(t, len(d.mu.mem.queue), 1) + d.mu.Unlock() + + require.NoError(t, d.Compact([]byte("a"), []byte("a\x00"), false)) + d.mu.Lock() + require.Equal(t, 1, len(d.mu.mem.queue)) + d.mu.Unlock() + + require.NoError(t, d.Close()) +} + +func TestFlushError(t *testing.T) { + // Error the first five times we try to write a sstable. + var errorOps atomic.Int32 + errorOps.Store(3) + fs := errorfs.Wrap(vfs.NewMem(), errorfs.InjectorFunc(func(op errorfs.Op) error { + if op.Kind == errorfs.OpCreate && filepath.Ext(op.Path) == ".sst" && errorOps.Add(-1) >= 0 { + return errorfs.ErrInjected + } + return nil + })) + d, err := Open("", testingRandomized(t, &Options{ + FS: fs, + EventListener: &EventListener{ + BackgroundError: func(err error) { + t.Log(err) + }, + }, + }).WithFSDefaults()) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("a"), []byte("foo"), NoSync)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Close()) +} + +func TestAdjustGrandparentOverlapBytesForFlush(t *testing.T) { + // 500MB in Lbase + var lbaseFiles []*manifest.FileMetadata + const lbaseSize = 5 << 20 + for i := 0; i < 100; i++ { + m := &manifest.FileMetadata{Size: lbaseSize, FileNum: FileNum(i)} + m.InitPhysicalBacking() + lbaseFiles = + append(lbaseFiles, m) + } + const maxOutputFileSize = 2 << 20 + // 20MB max overlap, so flush split into 25 files. + const maxOverlapBytes = 20 << 20 + ls := manifest.NewLevelSliceSpecificOrder(lbaseFiles) + testCases := []struct { + flushingBytes uint64 + adjustedOverlapBytes uint64 + }{ + // Flushes large enough that 25 files is acceptable. + {flushingBytes: 128 << 20, adjustedOverlapBytes: 20971520}, + {flushingBytes: 64 << 20, adjustedOverlapBytes: 20971520}, + // Small increase in adjustedOverlapBytes. + {flushingBytes: 32 << 20, adjustedOverlapBytes: 32768000}, + // Large increase in adjusterOverlapBytes, to limit to 4 files. + {flushingBytes: 1 << 20, adjustedOverlapBytes: 131072000}, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + c := compaction{ + grandparents: ls, + maxOverlapBytes: maxOverlapBytes, + maxOutputFileSize: maxOutputFileSize, + } + adjustGrandparentOverlapBytesForFlush(&c, tc.flushingBytes) + require.Equal(t, tc.adjustedOverlapBytes, c.maxOverlapBytes) + }) + } +} + +func TestCompactionInvalidBounds(t *testing.T) { + db, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + }).WithFSDefaults()) + require.NoError(t, err) + defer db.Close() + require.NoError(t, db.Compact([]byte("a"), []byte("b"), false)) + require.Error(t, db.Compact([]byte("a"), []byte("a"), false)) + require.Error(t, db.Compact([]byte("b"), []byte("a"), false)) +} + +func Test_calculateInuseKeyRanges(t *testing.T) { + opts := (*Options)(nil).EnsureDefaults() + cmp := base.DefaultComparer.Compare + newFileMeta := func(fileNum FileNum, size uint64, smallest, largest base.InternalKey) *fileMetadata { + m := (&fileMetadata{ + FileNum: fileNum, + Size: size, + }).ExtendPointKeyBounds(opts.Comparer.Compare, smallest, largest) + m.InitPhysicalBacking() + return m + } + tests := []struct { + name string + v *version + level int + depth int + smallest []byte + largest []byte + want []manifest.UserKeyRange + }{ + { + name: "No files in next level", + v: newVersion(opts, [numLevels][]*fileMetadata{ + 1: { + newFileMeta( + 1, + 1, + base.ParseInternalKey("a.SET.2"), + base.ParseInternalKey("c.SET.2"), + ), + newFileMeta( + 2, + 1, + base.ParseInternalKey("d.SET.2"), + base.ParseInternalKey("e.SET.2"), + ), + }, + }), + level: 1, + depth: 2, + smallest: []byte("a"), + largest: []byte("e"), + want: []manifest.UserKeyRange{ + { + Start: []byte("a"), + End: []byte("c"), + }, + { + Start: []byte("d"), + End: []byte("e"), + }, + }, + }, + { + name: "No overlapping key ranges", + v: newVersion(opts, [numLevels][]*fileMetadata{ + 1: { + newFileMeta( + 1, + 1, + base.ParseInternalKey("a.SET.1"), + base.ParseInternalKey("c.SET.1"), + ), + newFileMeta( + 2, + 1, + base.ParseInternalKey("l.SET.1"), + base.ParseInternalKey("p.SET.1"), + ), + }, + 2: { + newFileMeta( + 3, + 1, + base.ParseInternalKey("d.SET.1"), + base.ParseInternalKey("i.SET.1"), + ), + newFileMeta( + 4, + 1, + base.ParseInternalKey("s.SET.1"), + base.ParseInternalKey("w.SET.1"), + ), + }, + }), + level: 1, + depth: 2, + smallest: []byte("a"), + largest: []byte("z"), + want: []manifest.UserKeyRange{ + { + Start: []byte("a"), + End: []byte("c"), + }, + { + Start: []byte("d"), + End: []byte("i"), + }, + { + Start: []byte("l"), + End: []byte("p"), + }, + { + Start: []byte("s"), + End: []byte("w"), + }, + }, + }, + { + name: "First few non-overlapping, followed by overlapping", + v: newVersion(opts, [numLevels][]*fileMetadata{ + 1: { + newFileMeta( + 1, + 1, + base.ParseInternalKey("a.SET.1"), + base.ParseInternalKey("c.SET.1"), + ), + newFileMeta( + 2, + 1, + base.ParseInternalKey("d.SET.1"), + base.ParseInternalKey("e.SET.1"), + ), + newFileMeta( + 3, + 1, + base.ParseInternalKey("n.SET.1"), + base.ParseInternalKey("o.SET.1"), + ), + newFileMeta( + 4, + 1, + base.ParseInternalKey("p.SET.1"), + base.ParseInternalKey("q.SET.1"), + ), + }, + 2: { + newFileMeta( + 5, + 1, + base.ParseInternalKey("m.SET.1"), + base.ParseInternalKey("q.SET.1"), + ), + newFileMeta( + 6, + 1, + base.ParseInternalKey("s.SET.1"), + base.ParseInternalKey("w.SET.1"), + ), + }, + }), + level: 1, + depth: 2, + smallest: []byte("a"), + largest: []byte("z"), + want: []manifest.UserKeyRange{ + { + Start: []byte("a"), + End: []byte("c"), + }, + { + Start: []byte("d"), + End: []byte("e"), + }, + { + Start: []byte("m"), + End: []byte("q"), + }, + { + Start: []byte("s"), + End: []byte("w"), + }, + }, + }, + { + name: "All overlapping", + v: newVersion(opts, [numLevels][]*fileMetadata{ + 1: { + newFileMeta( + 1, + 1, + base.ParseInternalKey("d.SET.1"), + base.ParseInternalKey("e.SET.1"), + ), + newFileMeta( + 2, + 1, + base.ParseInternalKey("n.SET.1"), + base.ParseInternalKey("o.SET.1"), + ), + newFileMeta( + 3, + 1, + base.ParseInternalKey("p.SET.1"), + base.ParseInternalKey("q.SET.1"), + ), + }, + 2: { + newFileMeta( + 4, + 1, + base.ParseInternalKey("a.SET.1"), + base.ParseInternalKey("c.SET.1"), + ), + newFileMeta( + 5, + 1, + base.ParseInternalKey("d.SET.1"), + base.ParseInternalKey("w.SET.1"), + ), + }, + }), + level: 1, + depth: 2, + smallest: []byte("a"), + largest: []byte("z"), + want: []manifest.UserKeyRange{ + { + Start: []byte("a"), + End: []byte("c"), + }, + { + Start: []byte("d"), + End: []byte("w"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculateInuseKeyRanges(tt.v, cmp, tt.level, tt.depth, tt.smallest, tt.largest); !reflect.DeepEqual(got, tt.want) { + t.Errorf("calculateInuseKeyRanges() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMarkedForCompaction(t *testing.T) { + var mem vfs.FS = vfs.NewMem() + var d *DB + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + var buf bytes.Buffer + opts := (&Options{ + FS: mem, + DebugCheck: DebugCheckLevels, + DisableAutomaticCompactions: true, + FormatMajorVersion: internalFormatNewest, + EventListener: &EventListener{ + CompactionEnd: func(info CompactionInfo) { + // Fix the job ID and durations for determinism. + info.JobID = 100 + info.Duration = time.Second + info.TotalDuration = 2 * time.Second + fmt.Fprintln(&buf, info) + }, + }, + }).WithFSDefaults() + + reset := func() { + if d != nil { + require.NoError(t, d.Close()) + } + mem = vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + + var err error + d, err = Open("", opts) + require.NoError(t, err) + } + datadriven.RunTest(t, "testdata/marked_for_compaction", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + reset() + return "" + + case "define": + if d != nil { + if err := d.Close(); err != nil { + return err.Error() + } + } + var err error + if d, err = runDBDefineCmd(td, opts); err != nil { + return err.Error() + } + d.mu.Lock() + defer d.mu.Unlock() + t := time.Now() + d.timeNow = func() time.Time { + t = t.Add(time.Second) + return t + } + s := d.mu.versions.currentVersion().DebugString(base.DefaultFormatter) + return s + + case "mark-for-compaction": + d.mu.Lock() + defer d.mu.Unlock() + vers := d.mu.versions.currentVersion() + var fileNum uint64 + td.ScanArgs(t, "file", &fileNum) + for l, lm := range vers.Levels { + iter := lm.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if f.FileNum != base.FileNum(fileNum) { + continue + } + f.MarkedForCompaction = true + vers.Stats.MarkedForCompaction++ + vers.Levels[l].InvalidateAnnotation(markedForCompactionAnnotator{}) + return fmt.Sprintf("marked L%d.%s", l, f.FileNum) + } + } + return "not-found" + + case "maybe-compact": + d.mu.Lock() + defer d.mu.Unlock() + d.opts.DisableAutomaticCompactions = false + d.maybeScheduleCompaction() + for d.mu.compact.compactingCount > 0 { + d.mu.compact.cond.Wait() + } + + fmt.Fprintln(&buf, d.mu.versions.currentVersion().DebugString(base.DefaultFormatter)) + s := strings.TrimSpace(buf.String()) + buf.Reset() + opts.DisableAutomaticCompactions = true + return s + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +// createManifestErrorInjector injects errors (when enabled) into vfs.FS calls +// to create MANIFEST files. +type createManifestErrorInjector struct { + enabled atomic.Bool +} + +// TODO(jackson): Replace the createManifestErrorInjector with the composition +// of primitives defined in errorfs. This may require additional primitives. + +func (i *createManifestErrorInjector) String() string { return "MANIFEST-Creates" } + +// enable enables error injection for the vfs.FS. +func (i *createManifestErrorInjector) enable() { + i.enabled.Store(true) +} + +// MaybeError implements errorfs.Injector. +func (i *createManifestErrorInjector) MaybeError(op errorfs.Op) error { + if !i.enabled.Load() { + return nil + } + // This necessitates having a MaxManifestSize of 1, to reliably induce + // logAndApply errors. + if strings.Contains(op.Path, "MANIFEST") && op.Kind == errorfs.OpCreate { + return errorfs.ErrInjected + } + return nil +} + +var _ errorfs.Injector = &createManifestErrorInjector{} + +// TestCompaction_LogAndApplyFails exercises a flush or ingest encountering an +// unrecoverable error during logAndApply. +// +// Regression test for #1669. +func TestCompaction_LogAndApplyFails(t *testing.T) { + // flushKeys writes the given keys to the DB, flushing the resulting memtable. + var key = []byte("foo") + flushErrC := make(chan error) + flushKeys := func(db *DB) error { + b := db.NewBatch() + err := b.Set(key, nil, nil) + require.NoError(t, err) + err = b.Commit(nil) + require.NoError(t, err) + // An error from a failing flush is returned asynchronously. + go func() { _ = db.Flush() }() + return <-flushErrC + } + + // ingestKeys adds the given keys to the DB via an ingestion. + ingestKeys := func(db *DB) error { + // Create an SST for ingestion. + const fName = "ext" + f, err := db.opts.FS.Create(fName) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(t, w.Set(key, nil)) + require.NoError(t, w.Close()) + // Ingest the SST. + return db.Ingest([]string{fName}) + } + + testCases := []struct { + name string + addFn func(db *DB) error + backgroundErrorFn func(*DB, error) + }{ + { + name: "flush", + addFn: flushKeys, + backgroundErrorFn: func(db *DB, err error) { + require.True(t, errors.Is(err, errorfs.ErrInjected)) + flushErrC <- err + // A flush will attempt to retry in the background. For the purposes of + // testing this particular scenario, where we would have crashed anyway, + // drop the memtable on the floor to short circuit the retry loop. + // NB: we hold db.mu here. + var cur *flushableEntry + cur, db.mu.mem.queue = db.mu.mem.queue[0], db.mu.mem.queue[1:] + cur.readerUnrefLocked(true) + }, + }, + { + name: "ingest", + addFn: ingestKeys, + }, + } + + runTest := func(t *testing.T, addFn func(db *DB) error, bgFn func(*DB, error)) { + var db *DB + inj := &createManifestErrorInjector{} + logger := &fatalCapturingLogger{t: t} + opts := (&Options{ + FS: errorfs.Wrap(vfs.NewMem(), inj), + // Rotate the manifest after each write. This is required to trigger a + // file creation, into which errors can be injected. + MaxManifestFileSize: 1, + Logger: logger, + EventListener: &EventListener{ + BackgroundError: func(err error) { + if bgFn != nil { + bgFn(db, err) + } + }, + }, + DisableAutomaticCompactions: true, + }).WithFSDefaults() + + db, err := Open("", opts) + require.NoError(t, err) + defer func() { _ = db.Close() }() + + inj.enable() + err = addFn(db) + require.True(t, errors.Is(err, errorfs.ErrInjected)) + + // Under normal circumstances, such an error in logAndApply would panic and + // cause the DB to terminate here. Assert that we captured the fatal error. + require.True(t, errors.Is(logger.err, errorfs.ErrInjected)) + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + runTest(t, tc.addFn, tc.backgroundErrorFn) + }) + } +} + +// TestSharedObjectDeletePacing tests that we don't throttle shared object +// deletes (see the TargetBytesDeletionRate option). +func TestSharedObjectDeletePacing(t *testing.T) { + var opts Options + opts.FS = vfs.NewMem() + opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": remote.NewInMem(), + }) + opts.Experimental.CreateOnShared = remote.CreateOnSharedAll + opts.TargetByteDeletionRate = 1 + + d, err := Open("", &opts) + require.NoError(t, err) + require.NoError(t, d.SetCreatorID(1)) + + randVal := func() []byte { + res := make([]byte, 1024) + _, err := crand.Read(res) + require.NoError(t, err) + return res + } + + // We must set up things so that we will have more live bytes than obsolete + // bytes, otherwise delete pacing will be disabled anyway. + key := func(i int) string { + return fmt.Sprintf("k%02d", i) + } + const numKeys = 20 + for i := 1; i <= numKeys; i++ { + require.NoError(t, d.Set([]byte(key(i)), randVal(), nil)) + require.NoError(t, d.Compact([]byte(key(i)), []byte(key(i)+"1"), false)) + } + + done := make(chan struct{}) + go func() { + err = d.DeleteRange([]byte(key(5)), []byte(key(9)), nil) + if err == nil { + err = d.Compact([]byte(key(5)), []byte(key(9)), false) + } + // Wait for objects to be deleted. + for { + time.Sleep(10 * time.Millisecond) + if len(d.objProvider.List()) < numKeys-2 { + break + } + } + close(done) + }() + + select { + case <-time.After(60 * time.Second): + // Don't close the DB in this case (the goroutine above might panic). + t.Fatalf("compaction timed out, possibly due to incorrect deletion pacing") + case <-done: + } + require.NoError(t, err) + d.Close() +} + +type WriteErrorInjector struct { + enabled atomic.Bool +} + +// TODO(jackson): Replace WriteErrorInjector with use of primitives in errorfs, +// adding new primitives as necessary. + +func (i *WriteErrorInjector) String() string { return "FileWrites(ErrInjected)" } + +// enable enables error injection for the vfs.FS. +func (i *WriteErrorInjector) enable() { + i.enabled.Store(true) +} + +// disable disabled error injection for the vfs.FS. +func (i *WriteErrorInjector) disable() { + i.enabled.Store(false) +} + +// MaybeError implements errorfs.Injector. +func (i *WriteErrorInjector) MaybeError(op errorfs.Op) error { + if !i.enabled.Load() { + return nil + } + // Fail any future write. + if op.Kind == errorfs.OpFileWrite { + return errorfs.ErrInjected + } + return nil +} + +var _ errorfs.Injector = &WriteErrorInjector{} + +// Cumulative compaction stats shouldn't be updated on compaction error. +func TestCompactionErrorStats(t *testing.T) { + // protected by d.mu + var ( + useInjector bool + tablesCreated []FileNum + ) + + mem := vfs.NewMem() + injector := &WriteErrorInjector{} + opts := (&Options{ + FS: errorfs.Wrap(mem, injector), + Levels: make([]LevelOptions, numLevels), + EventListener: &EventListener{ + TableCreated: func(info TableCreateInfo) { + t.Log(info) + + if useInjector { + // We'll write 3 tables during compaction, and we only need + // the writes to error on the third file write, so only enable + // the injector after the first two files have been written to. + tablesCreated = append(tablesCreated, info.FileNum) + if len(tablesCreated) >= 2 { + injector.enable() + } + } + }, + }, + }).WithFSDefaults() + for i := range opts.Levels { + opts.Levels[i].TargetFileSize = 1 + } + opts.testingRandomized(t) + d, err := Open("", opts) + require.NoError(t, err) + + ingest := func(keys ...string) { + t.Helper() + f, err := mem.Create("ext") + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + for _, k := range keys { + require.NoError(t, w.Set([]byte(k), nil)) + } + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{"ext"})) + } + ingest("a", "c") + // Snapshot will preserve the older "a" key during compaction. + snap := d.NewSnapshot() + ingest("a", "b") + + // Trigger a manual compaction, which will encounter an injected error + // after the second table is created. + d.mu.Lock() + useInjector = true + d.mu.Unlock() + + err = d.Compact([]byte("a"), []byte("d"), false) + require.Error(t, err, "injected error") + + // Due to the error, stats shouldn't have been updated. + d.mu.Lock() + require.Equal(t, 0, int(d.mu.snapshots.cumulativePinnedCount)) + require.Equal(t, 0, int(d.mu.snapshots.cumulativePinnedSize)) + useInjector = false + d.mu.Unlock() + + injector.disable() + + // The following compaction won't error, but snapshot is open, so snapshot + // pinned stats should update. + require.NoError(t, d.Compact([]byte("a"), []byte("d"), false)) + require.NoError(t, snap.Close()) + + d.mu.Lock() + require.Equal(t, 1, int(d.mu.snapshots.cumulativePinnedCount)) + require.Equal(t, 9, int(d.mu.snapshots.cumulativePinnedSize)) + d.mu.Unlock() + require.NoError(t, d.Close()) +} diff --git a/pebble/comparer.go b/pebble/comparer.go new file mode 100644 index 0000000..c92cd79 --- /dev/null +++ b/pebble/comparer.go @@ -0,0 +1,31 @@ +// 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 pebble + +import "github.com/cockroachdb/pebble/internal/base" + +// Compare exports the base.Compare type. +type Compare = base.Compare + +// Equal exports the base.Equal type. +type Equal = base.Equal + +// AbbreviatedKey exports the base.AbbreviatedKey type. +type AbbreviatedKey = base.AbbreviatedKey + +// Separator exports the base.Separator type. +type Separator = base.Separator + +// Successor exports the base.Successor type. +type Successor = base.Successor + +// Split exports the base.Split type. +type Split = base.Split + +// Comparer exports the base.Comparer type. +type Comparer = base.Comparer + +// DefaultComparer exports the base.DefaultComparer variable. +var DefaultComparer = base.DefaultComparer diff --git a/pebble/data_test.go b/pebble/data_test.go new file mode 100644 index 0000000..9f6260f --- /dev/null +++ b/pebble/data_test.go @@ -0,0 +1,1426 @@ +// Copyright 2018 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 ( + "bytes" + crand "crypto/rand" + "fmt" + "io" + "math" + "math/rand" + "strconv" + "strings" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/private" + "github.com/cockroachdb/pebble/internal/rangedel" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/stretchr/testify/require" +) + +func runGetCmd(t testing.TB, td *datadriven.TestData, d *DB) string { + snap := Snapshot{ + db: d, + seqNum: InternalKeySeqNumMax, + } + td.MaybeScanArgs(t, "seq", &snap.seqNum) + + var buf bytes.Buffer + for _, data := range strings.Split(td.Input, "\n") { + v, closer, err := snap.Get([]byte(data)) + if err != nil { + fmt.Fprintf(&buf, "%s: %s\n", data, err) + } else { + fmt.Fprintf(&buf, "%s:%s\n", data, v) + closer.Close() + } + } + return buf.String() +} + +func runIterCmd(d *datadriven.TestData, iter *Iterator, closeIter bool) string { + if closeIter { + defer func() { + if iter != nil { + iter.Close() + } + }() + } + var b bytes.Buffer + for _, line := range strings.Split(d.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + printValidityState := false + var valid bool + var validityState IterValidityState + switch parts[0] { + case "seek-ge": + if len(parts) != 2 { + return "seek-ge \n" + } + valid = iter.SeekGE([]byte(parts[1])) + case "seek-prefix-ge": + if len(parts) != 2 { + return "seek-prefix-ge \n" + } + valid = iter.SeekPrefixGE([]byte(parts[1])) + case "seek-lt": + if len(parts) != 2 { + return "seek-lt \n" + } + valid = iter.SeekLT([]byte(parts[1])) + case "seek-ge-limit": + if len(parts) != 3 { + return "seek-ge-limit \n" + } + validityState = iter.SeekGEWithLimit( + []byte(parts[1]), []byte(parts[2])) + printValidityState = true + case "seek-lt-limit": + if len(parts) != 3 { + return "seek-lt-limit \n" + } + validityState = iter.SeekLTWithLimit( + []byte(parts[1]), []byte(parts[2])) + printValidityState = true + case "inspect": + if len(parts) != 2 { + return "inspect \n" + } + field := parts[1] + switch field { + case "lastPositioningOp": + op := "?" + switch iter.lastPositioningOp { + case unknownLastPositionOp: + op = "unknown" + case seekPrefixGELastPositioningOp: + op = "seekprefixge" + case seekGELastPositioningOp: + op = "seekge" + case seekLTLastPositioningOp: + op = "seeklt" + case invalidatedLastPositionOp: + op = "invalidate" + } + fmt.Fprintf(&b, "%s=%q\n", field, op) + default: + return fmt.Sprintf("unrecognized inspect field %q\n", field) + } + continue + case "next-limit": + if len(parts) != 2 { + return "next-limit \n" + } + validityState = iter.NextWithLimit([]byte(parts[1])) + printValidityState = true + case "internal-next": + validity, keyKind := iter.internalNext() + switch validity { + case internalNextError: + fmt.Fprintf(&b, "err: %s\n", iter.Error()) + case internalNextExhausted: + fmt.Fprint(&b, ".\n") + case internalNextValid: + fmt.Fprintf(&b, "%s\n", keyKind) + default: + panic("unreachable") + } + continue + case "can-deterministically-single-delete": + ok, err := CanDeterministicallySingleDelete(iter) + if err != nil { + fmt.Fprintf(&b, "err: %s\n", err) + } else { + fmt.Fprintf(&b, "%t\n", ok) + } + continue + case "prev-limit": + if len(parts) != 2 { + return "prev-limit \n" + } + validityState = iter.PrevWithLimit([]byte(parts[1])) + printValidityState = true + case "first": + valid = iter.First() + case "last": + valid = iter.Last() + case "next": + valid = iter.Next() + case "next-prefix": + valid = iter.NextPrefix() + case "prev": + valid = iter.Prev() + case "set-bounds": + if len(parts) <= 1 || len(parts) > 3 { + return "set-bounds lower= upper=\n" + } + var lower []byte + var upper []byte + for _, part := range parts[1:] { + arg := strings.Split(part, "=") + switch arg[0] { + case "lower": + lower = []byte(arg[1]) + case "upper": + upper = []byte(arg[1]) + default: + return fmt.Sprintf("set-bounds: unknown arg: %s", arg) + } + } + iter.SetBounds(lower, upper) + valid = iter.Valid() + case "set-options": + opts := iter.opts + if _, err := parseIterOptions(&opts, &iter.opts, parts[1:]); err != nil { + return fmt.Sprintf("set-options: %s", err.Error()) + } + iter.SetOptions(&opts) + valid = iter.Valid() + case "stats": + stats := iter.Stats() + // The timing is non-deterministic, so set to 0. + stats.InternalStats.BlockReadDuration = 0 + fmt.Fprintf(&b, "stats: %s\n", stats.String()) + continue + case "clone": + var opts CloneOptions + if len(parts) > 1 { + var iterOpts IterOptions + if foundAny, err := parseIterOptions(&iterOpts, &iter.opts, parts[1:]); err != nil { + return fmt.Sprintf("clone: %s", err.Error()) + } else if foundAny { + opts.IterOptions = &iterOpts + } + for _, part := range parts[1:] { + if arg := strings.Split(part, "="); len(arg) == 2 && arg[0] == "refresh-batch" { + var err error + opts.RefreshBatchView, err = strconv.ParseBool(arg[1]) + if err != nil { + return fmt.Sprintf("clone: refresh-batch: %s", err.Error()) + } + } + } + } + clonedIter, err := iter.Clone(opts) + if err != nil { + fmt.Fprintf(&b, "error in clone, skipping rest of input: err=%v\n", err) + return b.String() + } + if err = iter.Close(); err != nil { + fmt.Fprintf(&b, "err=%v\n", err) + } + iter = clonedIter + case "is-using-combined": + if iter.opts.KeyTypes != IterKeyTypePointsAndRanges { + fmt.Fprintln(&b, "not configured for combined iteration") + } else if iter.lazyCombinedIter.combinedIterState.initialized { + fmt.Fprintln(&b, "using combined (non-lazy) iterator") + } else { + fmt.Fprintln(&b, "using lazy iterator") + } + continue + default: + return fmt.Sprintf("unknown op: %s", parts[0]) + } + + valid = valid || validityState == IterValid + if valid != iter.Valid() { + fmt.Fprintf(&b, "mismatched valid states: %t vs %t\n", valid, iter.Valid()) + } + hasPoint, hasRange := iter.HasPointAndRange() + hasEither := hasPoint || hasRange + if hasEither != valid { + fmt.Fprintf(&b, "mismatched valid/HasPointAndRange states: valid=%t HasPointAndRange=(%t,%t)\n", valid, hasPoint, hasRange) + } + + if valid { + validityState = IterValid + } + printIterState(&b, iter, validityState, printValidityState) + } + return b.String() +} + +func parseIterOptions( + opts *IterOptions, ref *IterOptions, parts []string, +) (foundAny bool, err error) { + const usageString = "[lower=] [upper=] [key-types=point|range|both] [mask-suffix=] [mask-filter=] [only-durable=] [table-filter=reuse|none] [point-filters=reuse|none]\n" + for _, part := range parts { + arg := strings.SplitN(part, "=", 2) + if len(arg) != 2 { + return false, errors.Newf(usageString) + } + switch arg[0] { + case "point-filters": + switch arg[1] { + case "reuse": + opts.PointKeyFilters = ref.PointKeyFilters + case "none": + opts.PointKeyFilters = nil + default: + return false, errors.Newf("unknown arg point-filter=%q:\n%s", arg[1], usageString) + } + case "lower": + opts.LowerBound = []byte(arg[1]) + case "upper": + opts.UpperBound = []byte(arg[1]) + case "key-types": + switch arg[1] { + case "point": + opts.KeyTypes = IterKeyTypePointsOnly + case "range": + opts.KeyTypes = IterKeyTypeRangesOnly + case "both": + opts.KeyTypes = IterKeyTypePointsAndRanges + default: + return false, errors.Newf("unknown key-type %q:\n%s", arg[1], usageString) + } + case "mask-suffix": + opts.RangeKeyMasking.Suffix = []byte(arg[1]) + case "mask-filter": + opts.RangeKeyMasking.Filter = func() BlockPropertyFilterMask { + return sstable.NewTestKeysMaskingFilter() + } + case "table-filter": + switch arg[1] { + case "reuse": + opts.TableFilter = ref.TableFilter + case "none": + opts.TableFilter = nil + default: + return false, errors.Newf("unknown arg table-filter=%q:\n%s", arg[1], usageString) + } + case "only-durable": + var err error + opts.OnlyReadGuaranteedDurable, err = strconv.ParseBool(arg[1]) + if err != nil { + return false, errors.Newf("cannot parse only-durable=%q: %s", arg[1], err) + } + default: + continue + } + foundAny = true + } + return foundAny, nil +} + +func printIterState( + b io.Writer, iter *Iterator, validity IterValidityState, printValidityState bool, +) { + var validityStateStr string + if printValidityState { + switch validity { + case IterExhausted: + validityStateStr = " exhausted" + case IterValid: + validityStateStr = " valid" + case IterAtLimit: + validityStateStr = " at-limit" + } + } + if err := iter.Error(); err != nil { + fmt.Fprintf(b, "err=%v\n", err) + } else if validity == IterValid { + switch { + case iter.opts.pointKeys(): + hasPoint, hasRange := iter.HasPointAndRange() + fmt.Fprintf(b, "%s:%s (", iter.Key(), validityStateStr) + if hasPoint { + fmt.Fprintf(b, "%s, ", iter.Value()) + } else { + fmt.Fprint(b, "., ") + } + if hasRange { + start, end := iter.RangeBounds() + fmt.Fprintf(b, "[%s-%s)", formatASCIIKey(start), formatASCIIKey(end)) + writeRangeKeys(b, iter) + } else { + fmt.Fprint(b, ".") + } + if iter.RangeKeyChanged() { + fmt.Fprint(b, " UPDATED") + } + fmt.Fprint(b, ")") + default: + if iter.Valid() { + hasPoint, hasRange := iter.HasPointAndRange() + if hasPoint || !hasRange { + panic(fmt.Sprintf("pebble: unexpected HasPointAndRange (%t, %t)", hasPoint, hasRange)) + } + start, end := iter.RangeBounds() + fmt.Fprintf(b, "%s [%s-%s)", iter.Key(), formatASCIIKey(start), formatASCIIKey(end)) + writeRangeKeys(b, iter) + } else { + fmt.Fprint(b, ".") + } + if iter.RangeKeyChanged() { + fmt.Fprint(b, " UPDATED") + } + } + fmt.Fprintln(b) + } else { + fmt.Fprintf(b, ".%s\n", validityStateStr) + } +} + +func formatASCIIKey(b []byte) string { + if bytes.IndexFunc(b, func(r rune) bool { return r < 'A' || r > 'z' }) != -1 { + // This key is not just ASCII letters. Quote it. + return fmt.Sprintf("%q", b) + } + return string(b) +} + +func writeRangeKeys(b io.Writer, iter *Iterator) { + rangeKeys := iter.RangeKeys() + for j := 0; j < len(rangeKeys); j++ { + if j > 0 { + fmt.Fprint(b, ",") + } + fmt.Fprintf(b, " %s=%s", rangeKeys[j].Suffix, rangeKeys[j].Value) + } +} + +func runBatchDefineCmd(d *datadriven.TestData, b *Batch) error { + for _, line := range strings.Split(d.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + if parts[1] == `` { + parts[1] = "" + } + var err error + switch parts[0] { + case "set": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + err = b.Set([]byte(parts[1]), []byte(parts[2]), nil) + case "del": + if len(parts) != 2 { + return errors.Errorf("%s expects 1 argument", parts[0]) + } + err = b.Delete([]byte(parts[1]), nil) + case "del-sized": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + var valSize uint64 + valSize, err = strconv.ParseUint(parts[2], 10, 32) + if err != nil { + return err + } + err = b.DeleteSized([]byte(parts[1]), uint32(valSize), nil) + case "singledel": + if len(parts) != 2 { + return errors.Errorf("%s expects 1 argument", parts[0]) + } + err = b.SingleDelete([]byte(parts[1]), nil) + case "del-range": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + err = b.DeleteRange([]byte(parts[1]), []byte(parts[2]), nil) + case "merge": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + err = b.Merge([]byte(parts[1]), []byte(parts[2]), nil) + case "range-key-set": + if len(parts) < 4 || len(parts) > 5 { + return errors.Errorf("%s expects 3 or 4 arguments", parts[0]) + } + var val []byte + if len(parts) == 5 { + val = []byte(parts[4]) + } + err = b.RangeKeySet( + []byte(parts[1]), + []byte(parts[2]), + []byte(parts[3]), + val, + nil) + case "range-key-unset": + if len(parts) != 4 { + return errors.Errorf("%s expects 3 arguments", parts[0]) + } + err = b.RangeKeyUnset( + []byte(parts[1]), + []byte(parts[2]), + []byte(parts[3]), + nil) + case "range-key-del": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + err = b.RangeKeyDelete( + []byte(parts[1]), + []byte(parts[2]), + nil) + default: + return errors.Errorf("unknown op: %s", parts[0]) + } + if err != nil { + return err + } + } + return nil +} + +func runBuildRemoteCmd(td *datadriven.TestData, d *DB, storage remote.Storage) error { + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err + } + + if len(td.CmdArgs) < 1 { + return errors.New("build : argument missing") + } + path := td.CmdArgs[0].String() + + // Override table format, if provided. + tableFormat := d.opts.FormatMajorVersion.MaxTableFormat() + for _, cmdArg := range td.CmdArgs[1:] { + switch cmdArg.Key { + case "format": + switch cmdArg.Vals[0] { + case "leveldb": + tableFormat = sstable.TableFormatLevelDB + case "rocksdbv2": + tableFormat = sstable.TableFormatRocksDBv2 + case "pebblev1": + tableFormat = sstable.TableFormatPebblev1 + case "pebblev2": + tableFormat = sstable.TableFormatPebblev2 + case "pebblev3": + tableFormat = sstable.TableFormatPebblev3 + case "pebblev4": + tableFormat = sstable.TableFormatPebblev4 + default: + return errors.Errorf("unknown format string %s", cmdArg.Vals[0]) + } + } + } + + writeOpts := d.opts.MakeWriterOptions(0 /* level */, tableFormat) + + f, err := storage.CreateObject(path) + if err != nil { + return err + } + w := sstable.NewWriter(objstorageprovider.NewRemoteWritable(f), writeOpts) + iter := b.newInternalIter(nil) + for key, val := iter.First(); key != nil; key, val = iter.Next() { + tmp := *key + tmp.SetSeqNum(0) + if err := w.Add(tmp, val.InPlaceValue()); err != nil { + return err + } + } + if err := iter.Close(); err != nil { + return err + } + + if rdi := b.newRangeDelIter(nil, math.MaxUint64); rdi != nil { + for s := rdi.First(); s != nil; s = rdi.Next() { + err := rangedel.Encode(s, func(k base.InternalKey, v []byte) error { + k.SetSeqNum(0) + return w.Add(k, v) + }) + if err != nil { + return err + } + } + } + + if rki := b.newRangeKeyIter(nil, math.MaxUint64); rki != nil { + for s := rki.First(); s != nil; s = rki.Next() { + for _, k := range s.Keys { + var err error + switch k.Kind() { + case base.InternalKeyKindRangeKeySet: + err = w.RangeKeySet(s.Start, s.End, k.Suffix, k.Value) + case base.InternalKeyKindRangeKeyUnset: + err = w.RangeKeyUnset(s.Start, s.End, k.Suffix) + case base.InternalKeyKindRangeKeyDelete: + err = w.RangeKeyDelete(s.Start, s.End) + default: + panic("not a range key") + } + if err != nil { + return err + } + } + } + } + + return w.Close() +} + +func runBuildCmd(td *datadriven.TestData, d *DB, fs vfs.FS) error { + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err + } + + if len(td.CmdArgs) < 1 { + return errors.New("build : argument missing") + } + path := td.CmdArgs[0].String() + + // Override table format, if provided. + tableFormat := d.opts.FormatMajorVersion.MaxTableFormat() + for _, cmdArg := range td.CmdArgs[1:] { + switch cmdArg.Key { + case "format": + switch cmdArg.Vals[0] { + case "leveldb": + tableFormat = sstable.TableFormatLevelDB + case "rocksdbv2": + tableFormat = sstable.TableFormatRocksDBv2 + case "pebblev1": + tableFormat = sstable.TableFormatPebblev1 + case "pebblev2": + tableFormat = sstable.TableFormatPebblev2 + case "pebblev3": + tableFormat = sstable.TableFormatPebblev3 + case "pebblev4": + tableFormat = sstable.TableFormatPebblev4 + default: + return errors.Errorf("unknown format string %s", cmdArg.Vals[0]) + } + } + } + + writeOpts := d.opts.MakeWriterOptions(0 /* level */, tableFormat) + + f, err := fs.Create(path) + if err != nil { + return err + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), writeOpts) + iter := b.newInternalIter(nil) + for key, val := iter.First(); key != nil; key, val = iter.Next() { + tmp := *key + tmp.SetSeqNum(0) + if err := w.Add(tmp, val.InPlaceValue()); err != nil { + return err + } + } + if err := iter.Close(); err != nil { + return err + } + + if rdi := b.newRangeDelIter(nil, math.MaxUint64); rdi != nil { + for s := rdi.First(); s != nil; s = rdi.Next() { + err := rangedel.Encode(s, func(k base.InternalKey, v []byte) error { + k.SetSeqNum(0) + return w.Add(k, v) + }) + if err != nil { + return err + } + } + } + + if rki := b.newRangeKeyIter(nil, math.MaxUint64); rki != nil { + for s := rki.First(); s != nil; s = rki.Next() { + for _, k := range s.Keys { + var err error + switch k.Kind() { + case base.InternalKeyKindRangeKeySet: + err = w.RangeKeySet(s.Start, s.End, k.Suffix, k.Value) + case base.InternalKeyKindRangeKeyUnset: + err = w.RangeKeyUnset(s.Start, s.End, k.Suffix) + case base.InternalKeyKindRangeKeyDelete: + err = w.RangeKeyDelete(s.Start, s.End) + default: + panic("not a range key") + } + if err != nil { + return err + } + } + } + } + + return w.Close() +} + +func runCompactCmd(td *datadriven.TestData, d *DB) error { + if len(td.CmdArgs) > 4 { + return errors.Errorf("%s expects at most four arguments", td.Cmd) + } + parts := strings.Split(td.CmdArgs[0].Key, "-") + if len(parts) != 2 { + return errors.Errorf("expected -: %s", td.Input) + } + parallelize := td.HasArg("parallel") + if len(td.CmdArgs) >= 2 && strings.HasPrefix(td.CmdArgs[1].Key, "L") { + levelString := td.CmdArgs[1].String() + iStart := base.MakeInternalKey([]byte(parts[0]), InternalKeySeqNumMax, InternalKeyKindMax) + iEnd := base.MakeInternalKey([]byte(parts[1]), 0, 0) + if levelString[0] != 'L' { + return errors.Errorf("expected L: %s", levelString) + } + level, err := strconv.Atoi(levelString[1:]) + if err != nil { + return err + } + return d.manualCompact(iStart.UserKey, iEnd.UserKey, level, parallelize) + } + return d.Compact([]byte(parts[0]), []byte(parts[1]), parallelize) +} + +// runDBDefineCmd prepares a database state, returning the opened +// database with the initialized state. +// +// The command accepts input describing memtables and sstables to +// construct. Each new table is indicated by a line containing the +// level of the next table to build (eg, "L6"), or "mem" to build +// a memtable. Each subsequent line contains a new key-value pair. +// +// Point keys and range deletions should be encoded as the +// InternalKey's string representation, as understood by +// ParseInternalKey, followed a colon and the corresponding value. +// +// b.SET.50:foo +// c.DEL.20 +// +// Range keys may be encoded by prefixing the line with `rangekey:`, +// followed by the keyspan.Span string representation, as understood +// by keyspan.ParseSpan. +// +// rangekey:b-d:{(#5,RANGEKEYSET,@2,foo)} +// +// # Mechanics +// +// runDBDefineCmd works by simulating a flush for every file written. +// Keys are written to a memtable. When a file is complete, the table +// is flushed to physical files through manually invoking runCompaction. +// The resulting version edit is then manipulated to write the files +// to the indicated level. +// +// Because of it's low-level manipulation, runDBDefineCmd does allow the +// creation of invalid database states. If opts.DebugCheck is set, the +// level checker should detect the invalid state. +func runDBDefineCmd(td *datadriven.TestData, opts *Options) (*DB, error) { + opts = opts.EnsureDefaults() + opts.FS = vfs.NewMem() + + var snapshots []uint64 + var levelMaxBytes map[int]int64 + for _, arg := range td.CmdArgs { + switch arg.Key { + case "target-file-sizes": + opts.Levels = make([]LevelOptions, len(arg.Vals)) + for i := range arg.Vals { + size, err := strconv.ParseInt(arg.Vals[i], 10, 64) + if err != nil { + return nil, err + } + opts.Levels[i].TargetFileSize = size + } + case "snapshots": + snapshots = make([]uint64, len(arg.Vals)) + for i := range arg.Vals { + seqNum, err := strconv.ParseUint(arg.Vals[i], 10, 64) + if err != nil { + return nil, err + } + snapshots[i] = seqNum + if i > 0 && snapshots[i] < snapshots[i-1] { + return nil, errors.New("Snapshots must be in ascending order") + } + } + case "lbase-max-bytes": + lbaseMaxBytes, err := strconv.ParseInt(arg.Vals[0], 10, 64) + if err != nil { + return nil, err + } + opts.LBaseMaxBytes = lbaseMaxBytes + case "level-max-bytes": + levelMaxBytes = map[int]int64{} + for i := range arg.Vals { + j := strings.Index(arg.Vals[i], ":") + levelStr := strings.TrimSpace(arg.Vals[i][:j]) + level, err := strconv.Atoi(levelStr[1:]) + if err != nil { + return nil, err + } + size, err := strconv.ParseInt(strings.TrimSpace(arg.Vals[i][j+1:]), 10, 64) + if err != nil { + return nil, err + } + levelMaxBytes[level] = size + } + case "auto-compactions": + switch arg.Vals[0] { + case "off": + opts.DisableAutomaticCompactions = true + case "on": + opts.DisableAutomaticCompactions = false + default: + return nil, errors.Errorf("Unrecognized %q %q arg value: %q", td.Cmd, arg.Key, arg.Vals[0]) + } + case "enable-table-stats": + enable, err := strconv.ParseBool(arg.Vals[0]) + if err != nil { + return nil, errors.Errorf("%s: could not parse %q as bool: %s", td.Cmd, arg.Vals[0], err) + } + opts.private.disableTableStats = !enable + case "block-size": + size, err := strconv.Atoi(arg.Vals[0]) + if err != nil { + return nil, err + } + for _, levelOpts := range opts.Levels { + levelOpts.BlockSize = size + } + case "format-major-version": + fmv, err := strconv.Atoi(arg.Vals[0]) + if err != nil { + return nil, err + } + opts.FormatMajorVersion = FormatMajorVersion(fmv) + case "disable-multi-level": + opts.Experimental.MultiLevelCompactionHeuristic = NoMultiLevel{} + } + } + + // This is placed after the argument parsing above, because the arguments + // to define should be parsed even if td.Input is empty. + if td.Input == "" { + // Empty LSM. + d, err := Open("", opts) + if err != nil { + return nil, err + } + d.mu.Lock() + for i := range snapshots { + s := &Snapshot{db: d} + s.seqNum = snapshots[i] + d.mu.snapshots.pushBack(s) + } + for l, maxBytes := range levelMaxBytes { + d.mu.versions.picker.(*compactionPickerByScore).levelMaxBytes[l] = maxBytes + } + d.mu.Unlock() + return d, nil + } + + d, err := Open("", opts) + if err != nil { + return nil, err + } + d.mu.Lock() + d.mu.versions.dynamicBaseLevel = false + for i := range snapshots { + s := &Snapshot{db: d} + s.seqNum = snapshots[i] + d.mu.snapshots.pushBack(s) + } + defer d.mu.Unlock() + + var mem *memTable + var start, end *base.InternalKey + ve := &versionEdit{} + level := -1 + + maybeFlush := func() error { + if level < 0 { + return nil + } + + toFlush := flushableList{{ + flushable: mem, + flushed: make(chan struct{}), + }} + c := newFlush(d.opts, d.mu.versions.currentVersion(), + d.mu.versions.picker.getBaseLevel(), toFlush, time.Now()) + c.disableSpanElision = true + // NB: define allows the test to exactly specify which keys go + // into which sstables. If the test has a small target file + // size to test grandparent limits, etc, the maxOutputFileSize + // can cause splitting /within/ the bounds specified to the + // test. Ignore the target size here, and split only according + // to the user-defined boundaries. + c.maxOutputFileSize = math.MaxUint64 + + newVE, _, _, err := d.runCompaction(0, c) + if err != nil { + return err + } + largestSeqNum := d.mu.versions.logSeqNum.Load() + for _, f := range newVE.NewFiles { + if start != nil { + f.Meta.SmallestPointKey = *start + f.Meta.Smallest = *start + } + if end != nil { + f.Meta.LargestPointKey = *end + f.Meta.Largest = *end + } + if largestSeqNum <= f.Meta.LargestSeqNum { + largestSeqNum = f.Meta.LargestSeqNum + 1 + } + ve.NewFiles = append(ve.NewFiles, newFileEntry{ + Level: level, + Meta: f.Meta, + }) + } + // The committed keys were never written to the WAL, so neither + // the logSeqNum nor the commit pipeline's visibleSeqNum have + // been ratcheted. Manually ratchet them to the largest sequence + // number committed to ensure iterators opened from the database + // correctly observe the committed keys. + if d.mu.versions.logSeqNum.Load() < largestSeqNum { + d.mu.versions.logSeqNum.Store(largestSeqNum) + } + if d.mu.versions.visibleSeqNum.Load() < largestSeqNum { + d.mu.versions.visibleSeqNum.Store(largestSeqNum) + } + level = -1 + return nil + } + + // Example, a-c. + parseMeta := func(s string) (*fileMetadata, error) { + parts := strings.Split(s, "-") + if len(parts) != 2 { + return nil, errors.Errorf("malformed table spec: %s", s) + } + m := (&fileMetadata{}).ExtendPointKeyBounds( + opts.Comparer.Compare, + InternalKey{UserKey: []byte(parts[0])}, + InternalKey{UserKey: []byte(parts[1])}, + ) + m.InitPhysicalBacking() + return m, nil + } + + // Example, compact: a-c. + parseCompaction := func(outputLevel int, s string) (*compaction, error) { + m, err := parseMeta(s[len("compact:"):]) + if err != nil { + return nil, err + } + c := &compaction{ + inputs: []compactionLevel{{}, {level: outputLevel}}, + smallest: m.Smallest, + largest: m.Largest, + } + c.startLevel, c.outputLevel = &c.inputs[0], &c.inputs[1] + return c, nil + } + + for _, line := range strings.Split(td.Input, "\n") { + fields := strings.Fields(line) + if len(fields) > 0 { + switch fields[0] { + case "mem": + if err := maybeFlush(); err != nil { + return nil, err + } + // Add a memtable layer. + if !d.mu.mem.mutable.empty() { + d.mu.mem.mutable = newMemTable(memTableOptions{Options: d.opts}) + entry := d.newFlushableEntry(d.mu.mem.mutable, 0, 0) + entry.readerRefs.Add(1) + d.mu.mem.queue = append(d.mu.mem.queue, entry) + d.updateReadStateLocked(nil) + } + mem = d.mu.mem.mutable + start, end = nil, nil + fields = fields[1:] + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + if err := maybeFlush(); err != nil { + return nil, err + } + var err error + if level, err = strconv.Atoi(fields[0][1:]); err != nil { + return nil, err + } + fields = fields[1:] + start, end = nil, nil + boundFields := 0 + for _, field := range fields { + toBreak := false + switch { + case strings.HasPrefix(field, "start="): + ikey := base.ParseInternalKey(strings.TrimPrefix(field, "start=")) + start = &ikey + boundFields++ + case strings.HasPrefix(field, "end="): + ikey := base.ParseInternalKey(strings.TrimPrefix(field, "end=")) + end = &ikey + boundFields++ + default: + toBreak = true + } + if toBreak { + break + } + } + fields = fields[boundFields:] + mem = newMemTable(memTableOptions{Options: d.opts}) + } + } + + for _, data := range fields { + i := strings.Index(data, ":") + // Define in-progress compactions. + if data[:i] == "compact" { + c, err := parseCompaction(level, data) + if err != nil { + return nil, err + } + d.mu.compact.inProgress[c] = struct{}{} + continue + } + if data[:i] == "rangekey" { + span := keyspan.ParseSpan(data[i:]) + err := rangekey.Encode(&span, func(k base.InternalKey, v []byte) error { + return mem.set(k, v) + }) + if err != nil { + return nil, err + } + continue + } + key := base.ParseInternalKey(data[:i]) + valueStr := data[i+1:] + value := []byte(valueStr) + var randBytes int + if n, err := fmt.Sscanf(valueStr, "", &randBytes); err == nil && n == 1 { + value = make([]byte, randBytes) + rnd := rand.New(rand.NewSource(int64(key.SeqNum()))) + if _, err := rnd.Read(value[:]); err != nil { + return nil, err + } + } + if err := mem.set(key, value); err != nil { + return nil, err + } + } + } + + if err := maybeFlush(); err != nil { + return nil, err + } + + if len(ve.NewFiles) > 0 { + jobID := d.mu.nextJobID + d.mu.nextJobID++ + d.mu.versions.logLock() + if err := d.mu.versions.logAndApply(jobID, ve, newFileMetrics(ve.NewFiles), false, func() []compactionInfo { + return nil + }); err != nil { + return nil, err + } + d.updateReadStateLocked(nil) + d.updateTableStatsLocked(ve.NewFiles) + } + + for l, maxBytes := range levelMaxBytes { + d.mu.versions.picker.(*compactionPickerByScore).levelMaxBytes[l] = maxBytes + } + + return d, nil +} + +func runTableStatsCmd(td *datadriven.TestData, d *DB) string { + u, err := strconv.ParseUint(strings.TrimSpace(td.Input), 10, 64) + if err != nil { + return err.Error() + } + fileNum := base.FileNum(u) + + d.mu.Lock() + defer d.mu.Unlock() + v := d.mu.versions.currentVersion() + for _, levelMetadata := range v.Levels { + iter := levelMetadata.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if f.FileNum != fileNum { + continue + } + + if !f.StatsValid() { + d.waitTableStats() + } + + var b bytes.Buffer + fmt.Fprintf(&b, "num-entries: %d\n", f.Stats.NumEntries) + fmt.Fprintf(&b, "num-deletions: %d\n", f.Stats.NumDeletions) + fmt.Fprintf(&b, "num-range-key-sets: %d\n", f.Stats.NumRangeKeySets) + fmt.Fprintf(&b, "point-deletions-bytes-estimate: %d\n", f.Stats.PointDeletionsBytesEstimate) + fmt.Fprintf(&b, "range-deletions-bytes-estimate: %d\n", f.Stats.RangeDeletionsBytesEstimate) + return b.String() + } + } + return "(not found)" +} + +func runTableFileSizesCmd(td *datadriven.TestData, d *DB) string { + d.mu.Lock() + defer d.mu.Unlock() + return runVersionFileSizes(d.mu.versions.currentVersion()) +} + +func runVersionFileSizes(v *version) string { + var buf bytes.Buffer + for l, levelMetadata := range v.Levels { + if levelMetadata.Empty() { + continue + } + fmt.Fprintf(&buf, "L%d:\n", l) + iter := levelMetadata.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + fmt.Fprintf(&buf, " %s: %d bytes (%s)", f, f.Size, humanize.Bytes.Uint64(f.Size)) + if f.IsCompacting() { + fmt.Fprintf(&buf, " (IsCompacting)") + } + fmt.Fprintln(&buf) + } + } + return buf.String() +} + +// Prints some metadata about some sstable which is currently in the latest +// version. +func runMetadataCommand(t *testing.T, td *datadriven.TestData, d *DB) string { + var file int + td.ScanArgs(t, "file", &file) + var m *fileMetadata + d.mu.Lock() + currVersion := d.mu.versions.currentVersion() + for _, level := range currVersion.Levels { + lIter := level.Iter() + for f := lIter.First(); f != nil; f = lIter.Next() { + if f.FileNum == base.FileNum(uint64(file)) { + m = f + break + } + } + } + d.mu.Unlock() + var buf bytes.Buffer + // Add more metadata as needed. + fmt.Fprintf(&buf, "size: %d\n", m.Size) + return buf.String() +} + +func runSSTablePropertiesCmd(t *testing.T, td *datadriven.TestData, d *DB) string { + var file int + td.ScanArgs(t, "file", &file) + + // See if we can grab the FileMetadata associated with the file. This is needed + // to easily construct virtual sstable properties. + var m *fileMetadata + d.mu.Lock() + currVersion := d.mu.versions.currentVersion() + for _, level := range currVersion.Levels { + lIter := level.Iter() + for f := lIter.First(); f != nil; f = lIter.Next() { + if f.FileNum == base.FileNum(uint64(file)) { + m = f + break + } + } + } + d.mu.Unlock() + + // Note that m can be nil here if the sstable exists in the file system, but + // not in the lsm. If m is nil just assume that file is not virtual. + + backingFileNum := base.FileNum(uint64(file)).DiskFileNum() + if m != nil { + backingFileNum = m.FileBacking.DiskFileNum + } + fileName := base.MakeFilename(fileTypeTable, backingFileNum) + f, err := d.opts.FS.Open(fileName) + if err != nil { + return err.Error() + } + readable, err := sstable.NewSimpleReadable(f) + if err != nil { + return err.Error() + } + // TODO(bananabrick): cacheOpts is used to set the file number on a Reader, + // and virtual sstables expect this file number to be set. Split out the + // opts into fileNum opts, and cache opts. + cacheOpts := private.SSTableCacheOpts(0, backingFileNum).(sstable.ReaderOption) + r, err := sstable.NewReader(readable, d.opts.MakeReaderOptions(), cacheOpts) + if err != nil { + return err.Error() + } + defer r.Close() + + var v sstable.VirtualReader + props := r.Properties.String() + if m != nil && m.Virtual { + v = sstable.MakeVirtualReader(r, m.VirtualMeta(), false /* isForeign */) + props = v.Properties.String() + } + if len(td.Input) == 0 { + return props + } + var buf bytes.Buffer + propsSlice := strings.Split(props, "\n") + for _, requestedProp := range strings.Split(td.Input, "\n") { + fmt.Fprintf(&buf, "%s:\n", requestedProp) + for _, prop := range propsSlice { + if strings.Contains(prop, requestedProp) { + fmt.Fprintf(&buf, " %s\n", prop) + } + } + } + return buf.String() +} + +func runPopulateCmd(t *testing.T, td *datadriven.TestData, b *Batch) { + var maxKeyLength, valLength int + var timestamps []int + td.ScanArgs(t, "keylen", &maxKeyLength) + td.MaybeScanArgs(t, "timestamps", ×tamps) + td.MaybeScanArgs(t, "vallen", &valLength) + // Default to writing timestamps @1. + if len(timestamps) == 0 { + timestamps = append(timestamps, 1) + } + + ks := testkeys.Alpha(maxKeyLength) + buf := make([]byte, ks.MaxLen()+testkeys.MaxSuffixLen) + vbuf := make([]byte, valLength) + for i := int64(0); i < ks.Count(); i++ { + for _, ts := range timestamps { + n := testkeys.WriteKeyAt(buf, ks, i, int64(ts)) + + // Default to using the key as the value, but if the user provided + // the vallen argument, generate a random value of the specified + // length. + value := buf[:n] + if valLength > 0 { + _, err := crand.Read(vbuf) + require.NoError(t, err) + value = vbuf + } + require.NoError(t, b.Set(buf[:n], value, nil)) + } + } +} + +// waitTableStats waits until all new files' statistics have been loaded. It's +// used in tests. The d.mu mutex must be locked while calling this method. +func (d *DB) waitTableStats() { + for d.mu.tableStats.loading || len(d.mu.tableStats.pending) > 0 { + d.mu.tableStats.cond.Wait() + } +} + +func runIngestAndExciseCmd(td *datadriven.TestData, d *DB, fs vfs.FS) error { + var exciseSpan KeyRange + paths := make([]string, 0, len(td.CmdArgs)) + for i, arg := range td.CmdArgs { + switch td.CmdArgs[i].Key { + case "excise": + if len(td.CmdArgs[i].Vals) != 1 { + return errors.New("expected 2 values for excise separated by -, eg. ingest-and-excise foo1 excise=\"start-end\"") + } + fields := strings.Split(td.CmdArgs[i].Vals[0], "-") + if len(fields) != 2 { + return errors.New("expected 2 values for excise separated by -, eg. ingest-and-excise foo1 excise=\"start-end\"") + } + exciseSpan.Start = []byte(fields[0]) + exciseSpan.End = []byte(fields[1]) + default: + paths = append(paths, arg.String()) + } + } + + if _, err := d.IngestAndExcise(paths, nil /* shared */, exciseSpan); err != nil { + return err + } + return nil +} + +func runIngestCmd(td *datadriven.TestData, d *DB, fs vfs.FS) error { + paths := make([]string, 0, len(td.CmdArgs)) + for _, arg := range td.CmdArgs { + paths = append(paths, arg.String()) + } + + if err := d.Ingest(paths); err != nil { + return err + } + return nil +} + +func runIngestExternalCmd(td *datadriven.TestData, d *DB, locator string) error { + external := make([]ExternalFile, 0) + for _, arg := range strings.Split(td.Input, "\n") { + fields := strings.Split(arg, ",") + if len(fields) != 4 { + return errors.New("usage: path,size,smallest,largest") + } + ef := ExternalFile{} + ef.Locator = remote.Locator(locator) + ef.ObjName = fields[0] + sizeInt, err := strconv.Atoi(fields[1]) + if err != nil { + return err + } + ef.Size = uint64(sizeInt) + ef.SmallestUserKey = []byte(fields[2]) + ef.LargestUserKey = []byte(fields[3]) + ef.HasPointKey = true + external = append(external, ef) + } + + if _, err := d.IngestExternalFiles(external); err != nil { + return err + } + return nil +} + +func runForceIngestCmd(td *datadriven.TestData, d *DB) error { + var paths []string + var level int + for _, arg := range td.CmdArgs { + switch arg.Key { + case "paths": + paths = append(paths, arg.Vals...) + case "level": + var err error + level, err = strconv.Atoi(arg.Vals[0]) + if err != nil { + return err + } + } + } + _, err := d.ingest(paths, func( + tableNewIters, + keyspan.TableNewSpanIter, + IterOptions, + *Comparer, + *version, + int, + map[*compaction]struct{}, + *fileMetadata, + bool, + ) (int, *fileMetadata, error) { + return level, nil, nil + }, nil /* shared */, KeyRange{}, nil /* external */) + return err +} + +func runLSMCmd(td *datadriven.TestData, d *DB) string { + d.mu.Lock() + defer d.mu.Unlock() + if td.HasArg("verbose") { + return d.mu.versions.currentVersion().DebugString(d.opts.Comparer.FormatKey) + } + return d.mu.versions.currentVersion().String() +} + +func parseDBOptionsArgs(opts *Options, args []datadriven.CmdArg) error { + for _, cmdArg := range args { + switch cmdArg.Key { + case "auto-compactions": + switch cmdArg.Vals[0] { + case "off": + opts.DisableAutomaticCompactions = true + case "on": + opts.DisableAutomaticCompactions = false + default: + return errors.Errorf("Unrecognized %q arg value: %q", cmdArg.Key, cmdArg.Vals[0]) + } + case "inject-errors": + injs := make([]errorfs.Injector, len(cmdArg.Vals)) + for i := 0; i < len(cmdArg.Vals); i++ { + inj, err := errorfs.ParseDSL(cmdArg.Vals[i]) + if err != nil { + return err + } + injs[i] = inj + } + opts.FS = errorfs.Wrap(opts.FS, errorfs.Any(injs...)) + case "enable-table-stats": + enable, err := strconv.ParseBool(cmdArg.Vals[0]) + if err != nil { + return errors.Errorf("%s: could not parse %q as bool: %s", cmdArg.Key, cmdArg.Vals[0], err) + } + opts.private.disableTableStats = !enable + case "format-major-version": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return err + } + // Override the DB version. + opts.FormatMajorVersion = FormatMajorVersion(v) + case "block-size": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return err + } + for i := range opts.Levels { + opts.Levels[i].BlockSize = v + } + case "index-block-size": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return err + } + for i := range opts.Levels { + opts.Levels[i].IndexBlockSize = v + } + case "target-file-size": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return err + } + for i := range opts.Levels { + opts.Levels[i].TargetFileSize = int64(v) + } + case "bloom-bits-per-key": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return err + } + fp := bloom.FilterPolicy(v) + opts.Filters = map[string]FilterPolicy{fp.Name(): fp} + for i := range opts.Levels { + opts.Levels[i].FilterPolicy = fp + } + case "merger": + switch cmdArg.Vals[0] { + case "appender": + opts.Merger = base.DefaultMerger + default: + return errors.Newf("unrecognized Merger %q\n", cmdArg.Vals[0]) + } + } + } + return nil +} diff --git a/pebble/db.go b/pebble/db.go new file mode 100644 index 0000000..ea23aaa --- /dev/null +++ b/pebble/db.go @@ -0,0 +1,3050 @@ +// Copyright 2012 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 provides an ordered key/value store. +package pebble // import "github.com/cockroachdb/pebble" + +import ( + "context" + "fmt" + "io" + "os" + "strconv" + "sync" + "sync/atomic" + "time" + + "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" +) + +const ( + // minTableCacheSize is the minimum size of the table cache, for a single db. + minTableCacheSize = 64 + + // numNonTableCacheFiles is an approximation for the number of files + // that we don't use for table caches, for a given db. + numNonTableCacheFiles = 10 +) + +var ( + // ErrNotFound is returned when a get operation does not find the requested + // key. + ErrNotFound = base.ErrNotFound + // ErrClosed is panicked when an operation is performed on a closed snapshot or + // DB. Use errors.Is(err, ErrClosed) to check for this error. + ErrClosed = errors.New("pebble: closed") + // ErrReadOnly is returned when a write operation is performed on a read-only + // database. + ErrReadOnly = errors.New("pebble: read-only") + // errNoSplit indicates that the user is trying to perform a range key + // operation but the configured Comparer does not provide a Split + // implementation. + errNoSplit = errors.New("pebble: Comparer.Split required for range key operations") +) + +// Reader is a readable key/value store. +// +// It is safe to call Get and NewIter from concurrent goroutines. +type Reader interface { + // Get gets the value for the given key. It returns ErrNotFound if the DB + // does not contain the key. + // + // The caller should not modify the contents of the returned slice, but it is + // safe to modify the contents of the argument after Get returns. The + // returned slice will remain valid until the returned Closer is closed. On + // success, the caller MUST call closer.Close() or a memory leak will occur. + Get(key []byte) (value []byte, closer io.Closer, err 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. + NewIter(o *IterOptions) (*Iterator, error) + + // NewIterWithContext is like NewIter, and additionally accepts a context + // for tracing. + NewIterWithContext(ctx context.Context, 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. + // + // It is not safe to close a DB until all outstanding iterators are closed. + // It is valid to call Close multiple times. Other methods should not be + // called after the DB has been closed. + Close() error +} + +// Writer is a writable key/value store. +// +// Goroutine safety is dependent on the specific implementation. +type Writer interface { + // Apply the operations contained in the batch to the DB. + // + // It is safe to modify the contents of the arguments after Apply returns. + Apply(batch *Batch, o *WriteOptions) error + + // Delete deletes the value for the given key. Deletes are blind all will + // succeed even if the given key does not exist. + // + // 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. + // + // WARNING: Undefined (non-deterministic) behavior will result if a key is overwritten and + // then deleted using SingleDelete. The record may appear deleted immediately, but be + // resurrected at a later time after compactions have been performed. Or the record may + // be deleted permanently. A Delete operation lays down a "tombstone" which shadows all + // previous versions of a key. The SingleDelete operation is akin to "anti-matter" and will + // only delete the most recently written version for a key. These different semantics allow + // the DB to avoid propagating a SingleDelete operation during a compaction as soon as the + // corresponding Set operation is encountered. These semantics require extreme care to handle + // properly. Only use if you have a workload where the performance gain is critical and you + // can guarantee that a record is written once and then deleted once. + // + // SingleDelete is internally transformed into a Delete if the most recent record for a key is either + // a Merge or Delete record. + // + // It is safe to modify the contents of the arguments after SingleDelete returns. + SingleDelete(key []byte, o *WriteOptions) error + + // DeleteRange deletes all of the point keys (and values) in the range + // [start,end) (inclusive on start, exclusive on end). DeleteRange does NOT + // delete overlapping range keys (eg, keys set via RangeKeySet). + // + // It is safe to modify the contents of the arguments after DeleteRange + // returns. + DeleteRange(start, end []byte, o *WriteOptions) error + + // LogData adds the specified to the batch. The data will be written to the + // WAL, but not added to memtables or sstables. Log data is never indexed, + // which makes it useful for testing WAL performance. + // + // It is safe to modify the contents of the argument after LogData returns. + LogData(data []byte, opts *WriteOptions) error + + // Merge merges the value for the given key. The details of the merge are + // dependent upon the configured merge operation. + // + // It is safe to modify the contents of the arguments after Merge returns. + Merge(key, value []byte, o *WriteOptions) error + + // Set sets the value for the given key. It overwrites any previous value + // for that key; a DB is not a multi-map. + // + // It is safe to modify the contents of the arguments after Set returns. + Set(key, value []byte, o *WriteOptions) error + + // RangeKeySet sets a range key mapping the key range [start, end) at the MVCC + // timestamp suffix to value. The suffix is optional. If any portion of the key + // range [start, end) is already set by a range key with the same suffix value, + // RangeKeySet overrides it. + // + // It is safe to modify the contents of the arguments after RangeKeySet returns. + RangeKeySet(start, end, suffix, value []byte, opts *WriteOptions) error + + // RangeKeyUnset removes a range key mapping the key range [start, end) at the + // MVCC timestamp suffix. The suffix may be omitted to remove an unsuffixed + // range key. RangeKeyUnset only removes portions of range keys that fall within + // the [start, end) key span, and only range keys with suffixes that exactly + // match the unset suffix. + // + // It is safe to modify the contents of the arguments after RangeKeyUnset + // returns. + RangeKeyUnset(start, end, suffix []byte, opts *WriteOptions) error + + // RangeKeyDelete deletes all of the range keys in the range [start,end) + // (inclusive on start, exclusive on end). It does not delete point keys (for + // that use DeleteRange). RangeKeyDelete removes all range keys within the + // bounds, including those with or without suffixes. + // + // It is safe to modify the contents of the arguments after RangeKeyDelete + // returns. + RangeKeyDelete(start, end []byte, opts *WriteOptions) error +} + +// CPUWorkHandle represents a handle used by the CPUWorkPermissionGranter API. +type CPUWorkHandle interface { + // Permitted indicates whether Pebble can use additional CPU resources. + Permitted() bool +} + +// CPUWorkPermissionGranter is used to request permission to opportunistically +// use additional CPUs to speed up internal background work. +type CPUWorkPermissionGranter interface { + // GetPermission returns a handle regardless of whether permission is granted + // or not. In the latter case, the handle is only useful for recording + // the CPU time actually spent on this calling goroutine. + GetPermission(time.Duration) CPUWorkHandle + // CPUWorkDone must be called regardless of whether CPUWorkHandle.Permitted + // returns true or false. + CPUWorkDone(CPUWorkHandle) +} + +// Use a default implementation for the CPU work granter to avoid excessive nil +// checks in the code. +type defaultCPUWorkHandle struct{} + +func (d defaultCPUWorkHandle) Permitted() bool { + return false +} + +type defaultCPUWorkGranter struct{} + +func (d defaultCPUWorkGranter) GetPermission(_ time.Duration) CPUWorkHandle { + return defaultCPUWorkHandle{} +} + +func (d defaultCPUWorkGranter) CPUWorkDone(_ CPUWorkHandle) {} + +// DB provides a concurrent, persistent ordered key/value store. +// +// A DB's basic operations (Get, Set, Delete) should be self-explanatory. Get +// and Delete will return ErrNotFound if the requested key is not in the store. +// Callers are free to ignore this error. +// +// A DB also allows for iterating over the key/value pairs in key order. If d +// is a DB, the code below prints all key/value pairs whose keys are 'greater +// than or equal to' k: +// +// iter := d.NewIter(readOptions) +// for iter.SeekGE(k); iter.Valid(); iter.Next() { +// fmt.Printf("key=%q value=%q\n", iter.Key(), iter.Value()) +// } +// return iter.Close() +// +// The Options struct holds the optional parameters for the DB, including a +// Comparer to define a 'less than' relationship over keys. It is always valid +// to pass a nil *Options, which means to use the default parameter values. Any +// zero field of a non-nil *Options also means to use the default value for +// that parameter. Thus, the code below uses a custom Comparer, but the default +// values for every other parameter: +// +// db := pebble.Open(&Options{ +// Comparer: myComparer, +// }) +type DB 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, 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 atomic.Uint64 + + // The number of bytes available on disk. + diskAvailBytes atomic.Uint64 + + cacheID uint64 + dirname string + walDirname string + opts *Options + cmp Compare + equal Equal + merge Merge + split Split + abbreviatedKey AbbreviatedKey + // The threshold for determining when a batch is "large" and will skip being + // inserted into a memtable. + largeBatchThreshold uint64 + // The current OPTIONS file number. + 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 + + fileLock *Lock + dataDir vfs.File + walDir vfs.File + + tableCache *tableCacheContainer + newIters tableNewIters + tableNewRangeKeyIter keyspan.TableNewSpanIter + + commit *commitPipeline + + // readState provides access to the state needed for reading without needing + // to acquire DB.mu. + readState struct { + sync.RWMutex + val *readState + } + // logRecycler holds a set of log file numbers that are available for + // reuse. Writing to a recycled log file is faster than to a new log file on + // some common filesystems (xfs, and ext3/4) due to avoiding metadata + // updates. + logRecycler logRecycler + + closed *atomic.Value + closedCh chan struct{} + + 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. + // compactionShedulers.Wait() should not be called while the DB.mu is held. + compactionSchedulers sync.WaitGroup + + // The main mutex protecting internal DB state. This mutex encompasses many + // fields because those fields need to be accessed and updated atomically. In + // particular, the current version, log.*, mem.*, and snapshot list need to + // be accessed and updated atomically during compaction. + // + // Care is taken to avoid holding DB.mu during IO operations. Accomplishing + // this sometimes requires releasing DB.mu in a method that was called with + // it held. See versionSet.logAndApply() and DB.makeRoomForWrite() for + // examples. This is a common pattern, so be careful about expectations that + // DB.mu will be held continuously across a set of calls. + mu struct { + sync.Mutex + + formatVers struct { + // vers is the database's current format major version. + // Backwards-incompatible features are gated behind new + // format major versions and not enabled until a database's + // version is ratcheted upwards. + // + // Although this is under the `mu` prefix, readers may read vers + // atomically without holding d.mu. Writers must only write to this + // value through finalizeFormatVersUpgrade which requires d.mu is + // held. + vers atomic.Uint64 + // marker is the atomic marker for the format major version. + // When a database's version is ratcheted upwards, the + // marker is moved in order to atomically record the new + // version. + marker *atomicfs.Marker + // ratcheting when set to true indicates that the database is + // currently in the process of ratcheting the format major version + // to vers + 1. As a part of ratcheting the format major version, + // migrations may drop and re-acquire the mutex. + ratcheting bool + } + + // The ID of the next job. Job IDs are passed to event listener + // notifications and act as a mechanism for tying together the events and + // log messages for a single job such as a flush, compaction, or file + // ingestion. Job IDs are not serialized to disk or used for correctness. + nextJobID int + + // The collection of immutable versions and state about the log and visible + // sequence numbers. Use the pointer here to ensure the atomic fields in + // version set are aligned properly. + versions *versionSet + + log struct { + // The queue of logs, containing both flushed and unflushed logs. The + // flushed logs will be a prefix, the unflushed logs a suffix. The + // delimeter between flushed and unflushed logs is + // versionSet.minUnflushedLogNum. + queue []fileInfo + // The number of input bytes to the log. This is the raw size of the + // batches written to the WAL, without the overhead of the record + // envelopes. + bytesIn uint64 + // The LogWriter is protected by commitPipeline.mu. This allows log + // writes to be performed without holding DB.mu, but requires both + // commitPipeline.mu and DB.mu to be held when rotating the WAL/memtable + // (i.e. makeRoomForWrite). + *record.LogWriter + // Can be nil. + metrics struct { + fsyncLatency prometheus.Histogram + record.LogWriterMetrics + } + registerLogWriterForTesting func(w *record.LogWriter) + } + + mem struct { + // The current mutable memTable. + mutable *memTable + // Queue of flushables (the mutable memtable is at end). Elements are + // added to the end of the slice and removed from the beginning. Once an + // index is set it is never modified making a fixed slice immutable and + // safe for concurrent reads. + queue flushableList + // nextSize is the size of the next memtable. The memtable size starts at + // min(256KB,Options.MemTableSize) and doubles each time a new memtable + // 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 uint64 + } + + compact struct { + // Condition variable used to signal when a flush or compaction has + // completed. Used by the write-stall mechanism to wait for the stall + // condition to clear. See DB.makeRoomForWrite(). + cond sync.Cond + // True when a flush is in progress. + flushing bool + // The number of ongoing compactions. + compactingCount int + // The list of deletion hints, suggesting ranges for delete-only + // compactions. + deletionHints []deleteCompactionHint + // The list of manual compactions. The next manual compaction to perform + // 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 + // should be scheduled. + rescheduleReadCompaction bool + + // readCompactions is a readCompactionQueue which keeps track of the + // 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 + // bool above transitions to false. + noOngoingFlushStartTime time.Time + } + + // 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. + snapshotList + + // The cumulative count and size of snapshot-pinned keys written to + // sstables. + cumulativePinnedCount uint64 + cumulativePinnedSize uint64 + } + + tableStats struct { + // Condition variable used to signal the completion of a + // job to collect table stats. + cond sync.Cond + // True when a stat collection operation is in progress. + loading bool + // True if stat collection has loaded statistics for all tables + // other than those listed explicitly in pending. This flag starts + // as false when a database is opened and flips to true once stat + // collection has caught up. + loadedInitial bool + // A slice of files for which stats have not been computed. + // Compactions, ingests, flushes append files to be processed. An + // active stat collection goroutine clears the list and processes + // them. + pending []manifest.NewFileEntry + } + + tableValidation struct { + // cond is a condition variable used to signal the completion of a + // job to validate one or more sstables. + cond sync.Cond + // pending is a slice of metadata for sstables waiting to be + // validated. Only physical sstables should be added to the pending + // queue. + pending []newFileEntry + // validating is set to true when validation is running. + validating bool + } + } + + // 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) +var _ Writer = (*DB)(nil) + +// TestOnlyWaitForCleaning MUST only be used in tests. +func (d *DB) TestOnlyWaitForCleaning() { + d.cleanupManager.Wait() +} + +// Get gets the value for the given key. It returns ErrNotFound if the DB does +// not contain the key. +// +// The caller should not modify the contents of the returned slice, but it is +// safe to modify the contents of the argument after Get returns. The returned +// slice will remain valid until the returned Closer is closed. On success, the +// caller MUST call closer.Close() or a memory leak will occur. +func (d *DB) Get(key []byte) ([]byte, io.Closer, error) { + return d.getInternal(key, nil /* batch */, nil /* snapshot */) +} + +type getIterAlloc struct { + dbi Iterator + keyBuf []byte + get getIter +} + +var getIterAllocPool = sync.Pool{ + New: func() interface{} { + return &getIterAlloc{} + }, +} + +func (d *DB) getInternal(key []byte, b *Batch, s *Snapshot) ([]byte, io.Closer, error) { + 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() + + // 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 = d.mu.versions.visibleSeqNum.Load() + } + + buf := getIterAllocPool.Get().(*getIterAlloc) + + get := &buf.get + *get = getIter{ + logger: d.opts.Logger, + comparer: d.opts.Comparer, + newIters: d.newIters, + snapshot: seqNum, + key: key, + batch: b, + mem: readState.memtables, + l0: readState.current.L0SublevelFiles, + version: readState.current, + } + + // Strip off memtables which cannot possibly contain the seqNum being read + // at. + for len(get.mem) > 0 { + n := len(get.mem) + if logSeqNum := get.mem[n-1].logSeqNum; logSeqNum < seqNum { + break + } + get.mem = get.mem[:n-1] + } + + i := &buf.dbi + pointIter := get + *i = Iterator{ + ctx: context.Background(), + getIterAlloc: buf, + iter: pointIter, + pointIter: pointIter, + merge: d.merge, + comparer: *d.opts.Comparer, + readState: readState, + keyBuf: buf.keyBuf, + } + + if !i.First() { + err := i.Close() + if err != nil { + return nil, nil, err + } + return nil, nil, ErrNotFound + } + return i.Value(), i, nil +} + +// Set sets the value for the given key. It overwrites any previous value +// for that key; a DB is not a multi-map. +// +// It is safe to modify the contents of the arguments after Set returns. +func (d *DB) Set(key, value []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.Set(key, value, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + +// Delete deletes the value for the given key. Deletes are blind all will +// succeed even if the given key does not exist. +// +// It is safe to modify the contents of the arguments after Delete returns. +func (d *DB) Delete(key []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.Delete(key, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + 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. +// +// It is safe to modify the contents of the arguments after SingleDelete returns. +func (d *DB) SingleDelete(key []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.SingleDelete(key, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + +// DeleteRange deletes all of the keys (and values) in the range [start,end) +// (inclusive on start, exclusive on end). +// +// It is safe to modify the contents of the arguments after DeleteRange +// returns. +func (d *DB) DeleteRange(start, end []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.DeleteRange(start, end, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + +// Merge adds an action to the DB that merges the value at key with the new +// value. The details of the merge are dependent upon the configured merge +// operator. +// +// It is safe to modify the contents of the arguments after Merge returns. +func (d *DB) Merge(key, value []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.Merge(key, value, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + +// LogData adds the specified to the batch. The data will be written to the +// WAL, but not added to memtables or sstables. Log data is never indexed, +// which makes it useful for testing WAL performance. +// +// It is safe to modify the contents of the argument after LogData returns. +func (d *DB) LogData(data []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.LogData(data, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + +// RangeKeySet sets a range key mapping the key range [start, end) at the MVCC +// timestamp suffix to value. The suffix is optional. If any portion of the key +// range [start, end) is already set by a range key with the same suffix value, +// RangeKeySet overrides it. +// +// It is safe to modify the contents of the arguments after RangeKeySet returns. +func (d *DB) RangeKeySet(start, end, suffix, value []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.RangeKeySet(start, end, suffix, value, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + +// RangeKeyUnset removes a range key mapping the key range [start, end) at the +// MVCC timestamp suffix. The suffix may be omitted to remove an unsuffixed +// range key. RangeKeyUnset only removes portions of range keys that fall within +// the [start, end) key span, and only range keys with suffixes that exactly +// match the unset suffix. +// +// It is safe to modify the contents of the arguments after RangeKeyUnset +// returns. +func (d *DB) RangeKeyUnset(start, end, suffix []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.RangeKeyUnset(start, end, suffix, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + +// RangeKeyDelete deletes all of the range keys in the range [start,end) +// (inclusive on start, exclusive on end). It does not delete point keys (for +// that use DeleteRange). RangeKeyDelete removes all range keys within the +// bounds, including those with or without suffixes. +// +// It is safe to modify the contents of the arguments after RangeKeyDelete +// returns. +func (d *DB) RangeKeyDelete(start, end []byte, opts *WriteOptions) error { + b := newBatch(d) + _ = b.RangeKeyDelete(start, end, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + +// Apply the operations contained in the batch to the DB. If the batch is large +// the contents of the batch may be retained by the database. If that occurs +// the batch contents will be cleared preventing the caller from attempting to +// reuse them. +// +// It is safe to modify the contents of the arguments after Apply returns. +func (d *DB) Apply(batch *Batch, opts *WriteOptions) error { + return d.applyInternal(batch, opts, false) +} + +// ApplyNoSyncWait must only be used when opts.Sync is true and the caller +// does not want to wait for the WAL fsync to happen. The method will return +// once the mutation is applied to the memtable and is visible (note that a +// mutation is visible before the WAL sync even in the wait case, so we have +// not weakened the durability semantics). The caller must call Batch.SyncWait +// to wait for the WAL fsync. The caller must not Close the batch without +// first calling Batch.SyncWait. +// +// RECOMMENDATION: Prefer using Apply unless you really understand why you +// need ApplyNoSyncWait. +// EXPERIMENTAL: API/feature subject to change. Do not yet use outside +// CockroachDB. +func (d *DB) ApplyNoSyncWait(batch *Batch, opts *WriteOptions) error { + if !opts.Sync { + return errors.Errorf("cannot request asynchonous apply when WriteOptions.Sync is false") + } + return d.applyInternal(batch, opts, true) +} + +// REQUIRES: noSyncWait => opts.Sync +func (d *DB) applyInternal(batch *Batch, opts *WriteOptions, noSyncWait bool) error { + if err := d.closed.Load(); err != nil { + panic(err) + } + if batch.committing { + panic("pebble: batch already committing") + } + if batch.applied.Load() { + panic("pebble: batch already applied") + } + if d.opts.ReadOnly { + return ErrReadOnly + } + if batch.db != nil && batch.db != d { + panic(fmt.Sprintf("pebble: batch db mismatch: %p != %p", batch.db, d)) + } + + sync := opts.GetSync() + if sync && d.opts.DisableWAL { + 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 + } + // TODO(jackson): Assert that all range key operands are suffixless. + } + batch.committing = true + + if batch.db == nil { + if err := batch.refreshMemTableSize(); err != nil { + return err + } + } + 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 + // horked at this point. + d.opts.Logger.Fatalf("pebble: fatal commit error: %v", err) + } + // If this is a large batch, we need to clear the batch contents as the + // flushable batch may still be present in the flushables queue. + // + // TODO(peter): Currently large batches are written to the WAL. We could + // skip the WAL write and instead wait for the large batch to be flushed to + // an sstable. For a 100 MB batch, this might actually be faster. For a 1 + // GB batch this is almost certainly faster. + if batch.flushable != nil { + batch.data = nil + } + return nil +} + +func (d *DB) commitApply(b *Batch, mem *memTable) error { + if b.flushable != nil { + // This is a large batch which was already added to the immutable queue. + return nil + } + err := mem.apply(b, b.SeqNum()) + if err != nil { + return err + } + + // If the batch contains range tombstones and the database is configured + // to flush range deletions, schedule a delayed flush so that disk space + // may be reclaimed without additional writes or an explicit flush. + if b.countRangeDels > 0 && d.opts.FlushDelayDeleteRange > 0 { + d.mu.Lock() + d.maybeScheduleDelayedFlush(mem, d.opts.FlushDelayDeleteRange) + d.mu.Unlock() + } + + // If the batch contains range keys and the database is configured to flush + // range keys, schedule a delayed flush so that the range keys are cleared + // from the memtable. + if b.countRangeKeys > 0 && d.opts.FlushDelayRangeKey > 0 { + d.mu.Lock() + d.maybeScheduleDelayedFlush(mem, d.opts.FlushDelayRangeKey) + d.mu.Unlock() + } + + if mem.writerUnref() { + d.mu.Lock() + d.maybeScheduleFlush() + d.mu.Unlock() + } + return nil +} + +func (d *DB) commitWrite(b *Batch, syncWG *sync.WaitGroup, syncErr *error) (*memTable, error) { + var size int64 + repr := b.Repr() + + if b.flushable != nil { + // We have a large batch. Such batches are special in that they don't get + // added to the memtable, and are instead inserted into the queue of + // memtables. The call to makeRoomForWrite with this batch will force the + // current memtable to be flushed. We want the large batch to be part of + // the same log, so we add it to the WAL here, rather than after the call + // to makeRoomForWrite(). + // + // Set the sequence number since it was not set to the correct value earlier + // (see comment in newFlushableBatch()). + b.flushable.setSeqNum(b.SeqNum()) + if !d.opts.DisableWAL { + var err error + size, err = d.mu.log.SyncRecord(repr, syncWG, syncErr) + if err != nil { + panic(err) + } + } + } + + d.mu.Lock() + + var err error + if !b.ingestedSSTBatch { + // Batches which contain keys of kind InternalKeyKindIngestSST will + // never be applied to the memtable, so we don't need to make room for + // write. For the other cases, switch out the memtable if there was not + // enough room to store the batch. + err = d.makeRoomForWrite(b) + } + + if err == nil && !d.opts.DisableWAL { + d.mu.log.bytesIn += uint64(len(repr)) + } + + // Grab a reference to the memtable while holding DB.mu. Note that for + // non-flushable batches (b.flushable == nil) makeRoomForWrite() added a + // reference to the memtable which will prevent it from being flushed until + // we unreference it. This reference is dropped in DB.commitApply(). + mem := d.mu.mem.mutable + + d.mu.Unlock() + if err != nil { + return nil, err + } + + if d.opts.DisableWAL { + return mem, nil + } + + if b.flushable == nil { + size, err = d.mu.log.SyncRecord(repr, syncWG, syncErr) + if err != nil { + panic(err) + } + } + + d.logSize.Store(uint64(size)) + return mem, err +} + +type iterAlloc struct { + dbi Iterator + keyBuf []byte + boundsBuf [2][]byte + prefixOrFullSeekKey []byte + merging mergingIter + mlevels [3 + numLevels]mergingIterLevel + levels [3 + numLevels]levelIter + levelsPositioned [3 + numLevels]bool +} + +var iterAllocPool = sync.Pool{ + New: func() interface{} { + return &iterAlloc{} + }, +} + +// 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 +} + +type batchIterOpts struct { + batchOnly bool +} +type newIterOpts struct { + snapshot snapshotIterOpts + batch batchIterOpts +} + +// newIter constructs a new iterator, merging in batch iterators as an extra +// level. +func (d *DB) newIter( + ctx context.Context, batch *Batch, internalOpts newIterOpts, o *IterOptions, +) *Iterator { + if internalOpts.batch.batchOnly { + if batch == nil { + panic("batchOnly is true, but batch is nil") + } + if internalOpts.snapshot.vers != nil { + panic("batchOnly is true, but snapshotIterOpts is initialized") + } + } + if err := d.closed.Load(); err != nil { + panic(err) + } + seqNum := internalOpts.snapshot.seqNum + if o.rangeKeys() { + if d.FormatMajorVersion() < FormatRangeKeys { + panic(fmt.Sprintf( + "pebble: range keys require at least format major version %d (current: %d)", + FormatRangeKeys, d.FormatMajorVersion(), + )) + } + } + if o != nil && o.RangeKeyMasking.Suffix != nil && o.KeyTypes != IterKeyTypePointsAndRanges { + panic("pebble: range key masking requires IterKeyTypePointsAndRanges") + } + 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 + // DB.mem.queue[0].logSeqNum. + panic("OnlyReadGuaranteedDurable is not supported for batches or snapshots") + } + var readState *readState + var newIters tableNewIters + var newIterRangeKey keyspan.TableNewSpanIter + if !internalOpts.batch.batchOnly { + // 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(). + if internalOpts.snapshot.vers == nil { + // NB: loadReadState() calls readState.ref(). + readState = d.loadReadState() + } else { + // vers != nil + internalOpts.snapshot.vers.Ref() + } + + // Determine the seqnum to read at after grabbing the read state (current and + // memtables) above. + if seqNum == 0 { + seqNum = d.mu.versions.visibleSeqNum.Load() + } + newIters = d.newIters + newIterRangeKey = d.tableNewRangeKeyIter + } + + // Bundle various structures under a single umbrella in order to allocate + // them together. + buf := iterAllocPool.Get().(*iterAlloc) + dbi := &buf.dbi + *dbi = Iterator{ + ctx: ctx, + alloc: buf, + merge: d.merge, + comparer: *d.opts.Comparer, + readState: readState, + version: internalOpts.snapshot.vers, + keyBuf: buf.keyBuf, + prefixOrFullSeekKey: buf.prefixOrFullSeekKey, + boundsBuf: buf.boundsBuf, + batch: batch, + newIters: newIters, + newIterRangeKey: newIterRangeKey, + seqNum: seqNum, + batchOnlyIter: internalOpts.batch.batchOnly, + } + if o != nil { + dbi.opts = *o + dbi.processBounds(o.LowerBound, o.UpperBound) + } + dbi.opts.logger = d.opts.Logger + if d.opts.private.disableLazyCombinedIteration { + dbi.opts.disableLazyCombinedIteration = true + } + if batch != nil { + dbi.batchSeqNum = dbi.batch.nextSeqNum() + } + return finishInitializingIter(ctx, buf) +} + +// finishInitializingIter is a helper for doing the non-trivial initialization +// of an Iterator. It's invoked to perform the initial initialization of an +// Iterator during NewIter or Clone, and to perform reinitialization due to a +// change in IterOptions by a call to Iterator.SetOptions. +func finishInitializingIter(ctx context.Context, buf *iterAlloc) *Iterator { + // Short-hand. + dbi := &buf.dbi + var memtables flushableList + if dbi.readState != nil { + memtables = dbi.readState.memtables + } + if dbi.opts.OnlyReadGuaranteedDurable { + memtables = nil + } else { + // We only need to read from memtables which contain sequence numbers older + // than seqNum. Trim off newer memtables. + for i := len(memtables) - 1; i >= 0; i-- { + if logSeqNum := memtables[i].logSeqNum; logSeqNum < dbi.seqNum { + break + } + memtables = memtables[:i] + } + } + + if dbi.opts.pointKeys() { + // Construct the point iterator, initializing dbi.pointIter to point to + // dbi.merging. If this is called during a SetOptions call and this + // Iterator has already initialized dbi.merging, constructPointIter is a + // noop and an initialized pointIter already exists in dbi.pointIter. + dbi.constructPointIter(ctx, memtables, buf) + dbi.iter = dbi.pointIter + } else { + dbi.iter = emptyIter + } + + if dbi.opts.rangeKeys() { + dbi.rangeKeyMasking.init(dbi, dbi.comparer.Compare, dbi.comparer.Split) + + // When iterating over both point and range keys, don't create the + // range-key iterator stack immediately if we can avoid it. This + // optimization takes advantage of the expected sparseness of range + // keys, and configures the point-key iterator to dynamically switch to + // combined iteration when it observes a file containing range keys. + // + // Lazy combined iteration is not possible if a batch or a memtable + // contains any range keys. + useLazyCombinedIteration := dbi.rangeKey == nil && + dbi.opts.KeyTypes == IterKeyTypePointsAndRanges && + (dbi.batch == nil || dbi.batch.countRangeKeys == 0) && + !dbi.opts.disableLazyCombinedIteration + if useLazyCombinedIteration { + // The user requested combined iteration, and there's no indexed + // batch currently containing range keys that would prevent lazy + // combined iteration. Check the memtables to see if they contain + // any range keys. + for i := range memtables { + if memtables[i].containsRangeKeys() { + useLazyCombinedIteration = false + break + } + } + } + + if useLazyCombinedIteration { + dbi.lazyCombinedIter = lazyCombinedIter{ + parent: dbi, + pointIter: dbi.pointIter, + combinedIterState: combinedIterState{ + initialized: false, + }, + } + dbi.iter = &dbi.lazyCombinedIter + dbi.iter = invalidating.MaybeWrapIfInvariants(dbi.iter) + } else { + dbi.lazyCombinedIter.combinedIterState = combinedIterState{ + initialized: true, + } + if dbi.rangeKey == nil { + dbi.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState) + dbi.rangeKey.init(dbi.comparer.Compare, dbi.comparer.Split, &dbi.opts) + dbi.constructRangeKeyIter() + } else { + dbi.rangeKey.iterConfig.SetBounds(dbi.opts.LowerBound, dbi.opts.UpperBound) + } + + // Wrap the point iterator (currently dbi.iter) with an interleaving + // iterator that interleaves range keys pulled from + // dbi.rangeKey.rangeKeyIter. + // + // NB: The interleaving iterator is always reinitialized, even if + // 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, + keyspan.InterleavingIterOpts{ + Mask: &dbi.rangeKeyMasking, + LowerBound: dbi.opts.LowerBound, + UpperBound: dbi.opts.UpperBound, + }) + dbi.iter = &dbi.rangeKey.iiter + } + } else { + // !dbi.opts.rangeKeys() + // + // Reset the combined iterator state. The initialized=true ensures the + // iterator doesn't unnecessarily try to switch to combined iteration. + dbi.lazyCombinedIter.combinedIterState = combinedIterState{initialized: true} + } + return dbi +} + +// ScanInternal scans all internal keys within the specified bounds, truncating +// any rangedels and rangekeys to those bounds if they span past them. For use +// when an external user needs to be aware of all internal keys that make up a +// key range. +// +// Keys deleted by range deletions must not be returned or exposed by this +// 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. 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, + categoryAndQoS sstable.CategoryAndQoS, + 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 { + scanInternalOpts := &scanInternalOptions{ + CategoryAndQoS: categoryAndQoS, + visitPointKey: visitPointKey, + visitRangeDel: visitRangeDel, + visitRangeKey: visitRangeKey, + visitSharedFile: visitSharedFile, + skipSharedLevels: visitSharedFile != nil, + IterOptions: IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + LowerBound: lower, + UpperBound: upper, + }, + } + iter, err := d.newInternalIter(ctx, snapshotIterOpts{} /* snapshot */, scanInternalOpts) + if err != nil { + return err + } + defer iter.close() + return scanInternalImpl(ctx, lower, upper, iter, scanInternalOpts) +} + +// 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( + ctx context.Context, sOpts snapshotIterOpts, o *scanInternalOptions, +) (*scanInternalIterator, error) { + 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(). + 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. + 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{ + ctx: ctx, + 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, + } + dbi.opts = *o + dbi.opts.logger = d.opts.Logger + if d.opts.private.disableLazyCombinedIteration { + dbi.opts.disableLazyCombinedIteration = true + } + return finishInitializingInternalIter(buf, dbi) +} + +func finishInitializingInternalIter( + buf *iterAlloc, i *scanInternalIterator, +) (*scanInternalIterator, error) { + // Short-hand. + 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-- { + if logSeqNum := memtables[j].logSeqNum; logSeqNum < i.seqNum { + break + } + memtables = memtables[:j] + } + i.initializeBoundBufs(i.opts.LowerBound, i.opts.UpperBound) + + i.constructPointIter(i.opts.CategoryAndQoS, memtables, buf) + + // For internal iterators, we skip the lazy combined iteration optimization + // entirely, and create the range key iterator stack directly. + i.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState) + i.rangeKey.init(i.comparer.Compare, i.comparer.Split, &i.opts.IterOptions) + if err := i.constructRangeKeyIter(); err != nil { + return nil, err + } + + // 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, + keyspan.InterleavingIterOpts{ + LowerBound: i.opts.LowerBound, + UpperBound: i.opts.UpperBound, + }) + i.iter = &i.rangeKey.iiter + + return i, nil +} + +func (i *Iterator) constructPointIter( + ctx context.Context, memtables flushableList, buf *iterAlloc, +) { + if i.pointIter != nil { + // Already have one. + return + } + internalOpts := internalIterOpts{stats: &i.stats.InternalStats} + if i.opts.RangeKeyMasking.Filter != nil { + internalOpts.boundLimitedFilter = &i.rangeKeyMasking + } + + // 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 := 0 + numLevelIters := 0 + if i.batch != nil { + numMergingLevels++ + } + + var current *version + if !i.batchOnlyIter { + numMergingLevels += len(memtables) + + 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 + } + numMergingLevels++ + numLevelIters++ + } + } + + if numMergingLevels > cap(mlevels) { + mlevels = make([]mergingIterLevel, 0, numMergingLevels) + } + if numLevelIters > cap(levels) { + levels = make([]levelIter, 0, numLevelIters) + } + + // Top-level is the batch, if any. + if i.batch != nil { + if i.batch.index == nil { + // This isn't an indexed batch. We shouldn't have gotten this far. + panic(errors.AssertionFailedf("creating an iterator over an unindexed batch")) + } else { + i.batch.initInternalIter(&i.opts, &i.batchPointIter) + i.batch.initRangeDelIter(&i.opts, &i.batchRangeDelIter, i.batchSeqNum) + // Only include the batch's rangedel iterator if it's non-empty. + // This requires some subtle logic in the case a rangedel is later + // written to the batch and the view of the batch is refreshed + // during a call to SetOptions—in this case, we need to reconstruct + // the point iterator to add the batch rangedel iterator. + var rangeDelIter keyspan.FragmentIterator + if i.batchRangeDelIter.Count() > 0 { + rangeDelIter = &i.batchRangeDelIter + } + mlevels = append(mlevels, mergingIterLevel{ + iter: &i.batchPointIter, + rangeDelIter: rangeDelIter, + }) + } + } + + if !i.batchOnlyIter { + // Next are the memtables. + for j := len(memtables) - 1; j >= 0; j-- { + mem := memtables[j] + mlevels = append(mlevels, mergingIterLevel{ + iter: mem.newIter(&i.opts), + rangeDelIter: mem.newRangeDelIter(&i.opts), + }) + } + + // Next are the file levels: L0 sub-levels followed by lower levels. + mlevelsIndex := len(mlevels) + 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, i.newIters, files, level, internalOpts) + li.initRangeDel(&mlevels[mlevelsIndex].rangeDelIter) + li.initBoundaryContext(&mlevels[mlevelsIndex].levelIterBoundaryContext) + li.initCombinedIterState(&i.lazyCombinedIter.combinedIterState) + mlevels[mlevelsIndex].levelIter = li + mlevels[mlevelsIndex].iter = invalidating.MaybeWrapIfInvariants(li) + + 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 < len(current.Levels); level++ { + if current.Levels[level].Empty() { + continue + } + addLevelIterForFiles(current.Levels[level].Iter(), manifest.Level(level)) + } + } + buf.merging.init(&i.opts, &i.stats.InternalStats, i.comparer.Compare, i.comparer.Split, mlevels...) + if len(mlevels) <= cap(buf.levelsPositioned) { + buf.merging.levelsPositioned = buf.levelsPositioned[:len(mlevels)] + } + buf.merging.snapshot = i.seqNum + buf.merging.batchSnapshot = i.batchSeqNum + buf.merging.combinedIterState = &i.lazyCombinedIter.combinedIterState + i.pointIter = invalidating.MaybeWrapIfInvariants(&buf.merging) + i.merging = &buf.merging +} + +// NewBatch returns a new empty write-only batch. Any reads on the batch will +// return an error. If the batch is committed it will be applied to the DB. +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 +// for insert operations. If you do not need to perform reads on the batch, use +// NewBatch instead. +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 +// state. This view is maintained by preventing file deletions and preventing +// memtables referenced by the iterator from being deleted. Using an iterator +// 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, 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, error) { + return d.newIter(ctx, nil /* batch */, newIterOpts{}, o), nil +} + +// NewSnapshot returns a point-in-time view of the current DB state. Iterators +// created with this handle will all observe a stable snapshot of the current +// DB state. The caller must call Snapshot.Close() when the snapshot is no +// longer needed. Snapshots are not persisted across DB restarts (close -> +// open). Unlike the implicit snapshot maintained by an iterator, a snapshot +// will not prevent memtables from being released or sstables from being +// deleted. Instead, a snapshot prevents deletion of sequence numbers +// referenced by the snapshot. +func (d *DB) NewSnapshot() *Snapshot { + if err := d.closed.Load(); err != nil { + panic(err) + } + + d.mu.Lock() + s := &Snapshot{ + db: d, + 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 +// or to call Close concurrently with any other DB method. It is not valid +// to call any of a DB's methods after the DB has been closed. +func (d *DB) Close() error { + // Lock the commit pipeline for the duration of Close. This prevents a race + // with makeRoomForWrite. Rotating the WAL in makeRoomForWrite requires + // dropping d.mu several times for I/O. If Close only holds d.mu, an + // in-progress WAL rotation may re-acquire d.mu only once the database is + // closed. + // + // Additionally, locking the commit pipeline makes it more likely that + // (illegal) concurrent writes will observe d.closed.Load() != nil, creating + // more understable panics if the database is improperly used concurrently + // during Close. + d.commit.mu.Lock() + defer d.commit.mu.Unlock() + d.mu.Lock() + defer d.mu.Unlock() + if err := d.closed.Load(); err != nil { + panic(err) + } + + // Clear the finalizer that is used to check that an unreferenced DB has been + // closed. We're closing the DB here, so the check performed by that + // finalizer isn't necessary. + // + // Note: this is a no-op if invariants are disabled or race is enabled. + invariants.SetFinalizer(d.closed, nil) + + d.closed.Store(errors.WithStack(ErrClosed)) + close(d.closedCh) + + defer d.opts.Cache.Unref() + + for d.mu.compact.compactingCount > 0 || d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + for d.mu.tableStats.loading { + d.mu.tableStats.cond.Wait() + } + for d.mu.tableValidation.validating { + d.mu.tableValidation.cond.Wait() + } + + var err error + if n := len(d.mu.compact.inProgress); n > 0 { + err = errors.Errorf("pebble: %d unexpected in-progress compactions", errors.Safe(n)) + } + err = firstError(err, d.mu.formatVers.marker.Close()) + err = firstError(err, d.tableCache.close()) + if !d.opts.ReadOnly { + err = firstError(err, d.mu.log.Close()) + } else if d.mu.log.LogWriter != nil { + panic("pebble: log-writer should be nil in read-only mode") + } + err = firstError(err, d.fileLock.Close()) + + // Note that versionSet.close() only closes the MANIFEST. The versions list + // is still valid for the checks below. + err = firstError(err, d.mu.versions.close()) + + err = firstError(err, d.dataDir.Close()) + if d.dataDir != d.walDir { + err = firstError(err, d.walDir.Close()) + } + + d.readState.val.unrefLocked() + + current := d.mu.versions.currentVersion() + for v := d.mu.versions.versions.Front(); true; v = v.Next() { + refs := v.Refs() + if v == current { + if refs != 1 { + err = firstError(err, errors.Errorf("leaked iterators: current\n%s", v)) + } + break + } + if refs != 0 { + err = firstError(err, errors.Errorf("leaked iterators:\n%s", v)) + } + } + + for _, mem := range d.mu.mem.queue { + // Usually, we'd want to delete the files returned by readerUnref. But + // in this case, even if we're unreferencing the flushables, the + // flushables aren't obsolete. They will be reconstructed during WAL + // replay. + mem.readerUnrefLocked(false) + } + // 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))) + } + + // 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) + } + + d.mu.Unlock() + d.compactionSchedulers.Wait() + + // Wait for all cleaning jobs to finish. + d.cleanupManager.Close() + + // Sanity check metrics. + if invariants.Enabled { + m := d.Metrics() + if m.Compact.NumInProgress > 0 || m.Compact.InProgressBytes > 0 { + d.mu.Lock() + panic(fmt.Sprintf("invalid metrics on close:\n%s", m)) + } + } + + d.mu.Lock() + + // As a sanity check, ensure that there are no zombie tables. A non-zero count + // hints at a reference count leak. + if ztbls := len(d.mu.versions.zombieTables); ztbls > 0 { + err = firstError(err, errors.Errorf("non-zero zombie file count: %d", ztbls)) + } + + err = firstError(err, d.objProvider.Close()) + + // If the options include a closer to 'close' the filesystem, close it. + if d.opts.private.fsCloser != nil { + d.opts.private.fsCloser.Close() + } + + // Return an error if the user failed to close all open snapshots. + if v := d.mu.snapshots.count(); v > 0 { + err = firstError(err, errors.Errorf("leaked snapshots: %d open snapshots on DB %p", v, d)) + } + + return err +} + +// Compact the specified range of keys in the database. +func (d *DB) Compact(start, end []byte, parallelize bool) error { + if err := d.closed.Load(); err != nil { + panic(err) + } + if d.opts.ReadOnly { + return ErrReadOnly + } + if d.cmp(start, end) >= 0 { + return errors.Errorf("Compact start %s is not less than end %s", + d.opts.Comparer.FormatKey(start), d.opts.Comparer.FormatKey(end)) + } + iStart := base.MakeInternalKey(start, InternalKeySeqNumMax, InternalKeyKindMax) + iEnd := base.MakeInternalKey(end, 0, 0) + m := (&fileMetadata{}).ExtendPointKeyBounds(d.cmp, iStart, iEnd) + meta := []*fileMetadata{m} + + d.mu.Lock() + maxLevelWithFiles := 1 + cur := d.mu.versions.currentVersion() + for level := 0; level < numLevels; level++ { + overlaps := cur.Overlaps(level, d.cmp, start, end, iEnd.IsExclusiveSentinel()) + if !overlaps.Empty() { + maxLevelWithFiles = level + 1 + } + } + + 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) { + // Check to see if any files overlap with any of the memtables. The queue + // is ordered from oldest to newest with the mutable memtable being the + // last element in the slice. We want to wait for the newest table that + // overlaps. + for i := len(d.mu.mem.queue) - 1; i >= 0; i-- { + mem := d.mu.mem.queue[i] + 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 + // makeRoomForWrite(). Lock order requirements elsewhere force us to + // unlock DB.mu in order to grab commitPipeline.mu first. + d.mu.Unlock() + d.commit.mu.Lock() + d.mu.Lock() + defer d.commit.mu.Unlock() + if mem.flushable == d.mu.mem.mutable { + // Only flush if the active memtable is unchanged. + err = d.makeRoomForWrite(nil) + } + } + mem.flushForced = true + d.maybeScheduleFlush() + return mem, err + } + } + return nil, nil + }() + + d.mu.Unlock() + + if err != nil { + return err + } + if mem != nil { + <-mem.flushed + } + + for level := 0; level < maxLevelWithFiles; { + for { + if err := d.manualCompact( + iStart.UserKey, iEnd.UserKey, level, parallelize); err != nil { + if errors.Is(err, ErrCancelledCompaction) { + continue + } + return err + } + break + } + level++ + if level == numLevels-1 { + // A manual compaction of the bottommost level occurred. + // There is no next level to try and compact. + break + } + } + return nil +} + +func (d *DB) manualCompact(start, end []byte, level int, parallelize bool) error { + d.mu.Lock() + curr := d.mu.versions.currentVersion() + files := curr.Overlaps(level, d.cmp, start, end, false) + if files.Empty() { + d.mu.Unlock() + return nil + } + + var compactions []*manualCompaction + if parallelize { + compactions = append(compactions, d.splitManualCompaction(start, end, level)...) + } else { + compactions = append(compactions, &manualCompaction{ + level: level, + done: make(chan error, 1), + start: start, + end: end, + }) + } + d.mu.compact.manual = append(d.mu.compact.manual, compactions...) + d.maybeScheduleCompaction() + d.mu.Unlock() + + // Each of the channels is guaranteed to be eventually sent to once. After a + // compaction is possibly picked in d.maybeScheduleCompaction(), either the + // compaction is dropped, executed after being scheduled, or retried later. + // Assuming eventual progress when a compaction is retried, all outcomes send + // a value to the done channel. Since the channels are buffered, it is not + // necessary to read from each channel, and so we can exit early in the event + // of an error. + for _, compaction := range compactions { + if err := <-compaction.done; err != nil { + return err + } + } + return nil +} + +// splitManualCompaction splits a manual compaction over [start,end] on level +// such that the resulting compactions have no key overlap. +func (d *DB) splitManualCompaction( + start, end []byte, level int, +) (splitCompactions []*manualCompaction) { + curr := d.mu.versions.currentVersion() + endLevel := level + 1 + baseLevel := d.mu.versions.picker.getBaseLevel() + if level == 0 { + endLevel = baseLevel + } + keyRanges := calculateInuseKeyRanges(curr, d.cmp, level, endLevel, start, end) + for _, keyRange := range keyRanges { + splitCompactions = append(splitCompactions, &manualCompaction{ + level: level, + done: make(chan error, 1), + start: keyRange.Start, + end: keyRange.End, + split: true, + }) + } + 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() + if err != nil { + return err + } + <-flushDone + return nil +} + +// AsyncFlush asynchronously flushes the memtable to stable storage. +// +// If no error is returned, the caller can receive from the returned channel in +// order to wait for the flush to complete. +func (d *DB) AsyncFlush() (<-chan struct{}, error) { + if err := d.closed.Load(); err != nil { + panic(err) + } + if d.opts.ReadOnly { + return nil, ErrReadOnly + } + + d.commit.mu.Lock() + defer d.commit.mu.Unlock() + d.mu.Lock() + defer d.mu.Unlock() + flushed := d.mu.mem.queue[len(d.mu.mem.queue)-1].flushed + err := d.makeRoomForWrite(nil) + if err != nil { + return nil, err + } + return flushed, nil +} + +// Metrics returns metrics about the database. +func (d *DB) Metrics() *Metrics { + metrics := &Metrics{} + recycledLogsCount, recycledLogSize := d.logRecycler.stats() + + d.mu.Lock() + vers := d.mu.versions.currentVersion() + *metrics = d.mu.versions.metrics + metrics.Compact.EstimatedDebt = d.mu.versions.picker.estimatedCompactionDebt(0) + 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() + } + metrics.Snapshots.Count = d.mu.snapshots.count() + if metrics.Snapshots.Count > 0 { + metrics.Snapshots.EarliestSeqNum = d.mu.snapshots.earliest() + } + 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 = 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 = 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 + // during WAL rotation. Use the larger of the two for the current WAL. All + // the previous WALs's fileSizes in d.mu.log.queue are already updated. + metrics.WAL.PhysicalSize = metrics.WAL.Size + if len(d.mu.log.queue) > 0 && metrics.WAL.PhysicalSize < d.mu.log.queue[len(d.mu.log.queue)-1].fileSize { + metrics.WAL.PhysicalSize = d.mu.log.queue[len(d.mu.log.queue)-1].fileSize + } + for i, n := 0, len(d.mu.log.queue)-1; i < n; i++ { + metrics.WAL.PhysicalSize += d.mu.log.queue[i].fileSize + } + + metrics.WAL.BytesIn = d.mu.log.bytesIn // protected by d.mu + for i, n := 0, len(d.mu.mem.queue)-1; i < n; i++ { + metrics.WAL.Size += d.mu.mem.queue[i].logSize + } + metrics.WAL.BytesWritten = metrics.Levels[0].BytesIn + metrics.WAL.Size + if p := d.mu.versions.picker; p != nil { + compactions := d.getInProgressCompactionInfoLocked(nil) + for level, score := range p.getScores(compactions) { + metrics.Levels[level].Score = score + } + } + metrics.Table.ZombieCount = int64(len(d.mu.versions.zombieTables)) + for _, size := range d.mu.versions.zombieTables { + metrics.Table.ZombieSize += size + } + metrics.private.optionsFileSize = d.optionsFileSize + + // TODO(jackson): Consider making these metrics optional. + metrics.Keys.RangeKeySetsCount = countRangeKeySetFragments(vers) + metrics.Keys.TombstoneCount = countTombstones(vers) + + 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 + if err := metrics.LogWriter.Merge(&d.mu.log.metrics.LogWriterMetrics); err != nil { + d.opts.Logger.Errorf("metrics error: %s", err) + } + metrics.Flush.WriteThroughput = d.mu.compact.flushWriteThroughput + if d.mu.compact.flushing { + metrics.Flush.NumInProgress = 1 + } + for i := 0; i < numLevels; i++ { + metrics.Levels[i].Additional.ValueBlocksSize = valueBlocksSizeForLevel(vers, i) + } + + d.mu.Unlock() + + metrics.BlockCache = d.opts.Cache.Metrics() + metrics.TableCache, metrics.Filter = d.tableCache.metrics() + metrics.TableIters = int64(d.tableCache.iterCount()) + metrics.CategoryStats = d.tableCache.dbOpts.sstStatsCollector.GetStats() + + metrics.SecondaryCacheMetrics = d.objProvider.Metrics() + + metrics.Uptime = d.timeNow().Sub(d.openedAt) + + return metrics +} + +// sstablesOptions hold the optional parameters to retrieve TableInfo for all sstables. +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`. +type SSTablesOption func(*sstablesOptions) + +// WithProperties enable return sstable properties in each TableInfo. +// +// NOTE: if most of the sstable properties need to be read from disk, +// this options may make method `SSTables` quite slow. +func WithProperties() SSTablesOption { + return func(opt *sstablesOptions) { + opt.withProperties = true + } +} + +// 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. If Virtual is true, + // then the Properties are associated with the backing sst. + Properties *sstable.Properties +} + +// SSTables retrieves the current sstables. The returned slice is indexed by +// level and each level is indexed by the position of the sstable within the +// level. Note that this information may be out of date due to concurrent +// flushes and compactions. +func (d *DB) SSTables(opts ...SSTablesOption) ([][]SSTableInfo, error) { + opt := &sstablesOptions{} + for _, fn := range opts { + 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() + + // TODO(peter): This is somewhat expensive, especially on a large + // database. It might be worthwhile to unify TableInfo and FileMetadata and + // then we could simply return current.Files. Note that RocksDB is doing + // something similar to the current code, so perhaps it isn't too bad. + srcLevels := readState.current.Levels + var totalTables int + for i := range srcLevels { + totalTables += srcLevels[i].Len() + } + + destTables := make([]SSTableInfo, totalTables) + destLevels := make([][]SSTableInfo, len(srcLevels)) + for i := range destLevels { + 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, + ) + 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 +} + +// EstimateDiskUsage returns the estimated filesystem space used in bytes for +// storing the range `[start, end]`. The estimation is computed as follows: +// +// - For sstables fully contained in the range the whole file size is included. +// - For sstables partially contained in the range the overlapping data block sizes +// are included. Even if a data block partially overlaps, or we cannot determine +// 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, 0, 0, errors.New("invalid key-range specified (start > end)") + } + + // Grab and reference the current readState. This prevents the underlying + // files in the associated version from being deleted if there is a concurrent + // compaction. + readState := d.loadReadState() + defer readState.unref() + + for level, files := range readState.current.Levels { + iter := files.Iter() + if level > 0 { + // We can only use `Overlaps` to restrict `files` at L1+ since at L0 it + // expands the range iteratively until it has found a set of files that + // do not overlap any other L0 files outside that set. + overlaps := readState.current.Overlaps(level, d.opts.Comparer.Compare, start, end, false /* exclusiveEnd */) + iter = overlaps.Iter() + } + for file := iter.First(); file != nil; file = iter.Next() { + if d.opts.Comparer.Compare(start, file.Smallest.UserKey) <= 0 && + 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 + 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, 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, remoteSize, externalSize, nil +} + +func (d *DB) walPreallocateSize() int { + // Set the WAL preallocate size to 110% of the memtable size. Note that there + // is a bit of apples and oranges in units here as the memtabls size + // corresponds to the memory usage of the memtable while the WAL size is the + // size of the batches (plus overhead) stored in the WAL. + // + // TODO(peter): 110% of the memtable size is quite hefty for a block + // size. This logic is taken from GetWalPreallocateBlockSize in + // RocksDB. Could a smaller preallocation block size be used? + size := d.opts.MemTableSize + size = (size / 10) + size + return int(size) +} + +func (d *DB) newMemTable(logNum base.DiskFileNum, logSeqNum uint64) (*memTable, *flushableEntry) { + size := d.mu.mem.nextSize + if d.mu.mem.nextSize < d.opts.MemTableSize { + d.mu.mem.nextSize *= 2 + if d.mu.mem.nextSize > d.opts.MemTableSize { + d.mu.mem.nextSize = d.opts.MemTableSize + } + } + + memtblOpts := memTableOptions{ + Options: d.opts, + logSeqNum: logSeqNum, + } + + // 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() { + // 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 base.DiskFileNum, logSeqNum uint64, +) *flushableEntry { + fe := &flushableEntry{ + flushable: f, + flushed: make(chan struct{}), + logNum: logNum, + logSeqNum: logSeqNum, + 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 +// Batch. It reserves the space in the memtable and adds a reference to the +// memtable. The caller must later ensure that the memtable is unreferenced. If +// the memtable is full, or a nil Batch is provided, the current memtable is +// rotated (marked as immutable) and a new mutable memtable is allocated. This +// memtable rotation also causes a log rotation. +// +// Both DB.mu and commitPipeline.mu must be held by the caller. Note that DB.mu +// may be released and reacquired. +func (d *DB) makeRoomForWrite(b *Batch) error { + if b != nil && b.ingestedSSTBatch { + panic("pebble: invalid function call") + } + + force := b == nil || b.flushable != nil + stalled := false + for { + if b != nil && b.flushable == nil { + err := d.mu.mem.mutable.prepare(b) + if err != arenaskl.ErrArenaFull { + if stalled { + d.opts.EventListener.WriteStallEnd() + } + return err + } + } else if !force { + if stalled { + d.opts.EventListener.WriteStallEnd() + } + return nil + } + // force || err == ErrArenaFull, so we need to rotate the current memtable. + { + var size uint64 + for i := range d.mu.mem.queue { + size += d.mu.mem.queue[i].totalBytes() + } + 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 { + stalled = true + d.opts.EventListener.WriteStallBegin(WriteStallBeginInfo{ + Reason: "memtable count limit reached", + }) + } + now := time.Now() + d.mu.compact.cond.Wait() + if b != nil { + b.commitStats.MemTableWriteStallDuration += time.Since(now) + } + continue + } + } + l0ReadAmp := d.mu.versions.currentVersion().L0Sublevels.ReadAmplification() + if l0ReadAmp >= d.opts.L0StopWritesThreshold { + // There are too many level-0 files, so we wait. + if !stalled { + stalled = true + d.opts.EventListener.WriteStallBegin(WriteStallBeginInfo{ + Reason: "L0 file count limit exceeded", + }) + } + now := time.Now() + d.mu.compact.cond.Wait() + if b != nil { + b.commitStats.L0ReadAmpWriteStallDuration += time.Since(now) + } + continue + } + + var newLogNum base.DiskFileNum + var prevLogSize uint64 + if !d.opts.DisableWAL { + now := time.Now() + newLogNum, prevLogSize = d.recycleWAL() + if b != nil { + b.commitStats.WALRotationDuration += time.Since(now) + } + } + + immMem := d.mu.mem.mutable + imm := d.mu.mem.queue[len(d.mu.mem.queue)-1] + imm.logSize = prevLogSize + imm.flushForced = imm.flushForced || (b == nil) + + // If we are manually flushing and we used less than half of the bytes in + // the memtable, don't increase the size for the next memtable. This + // reduces memtable memory pressure when an application is frequently + // manually flushing. + if (b == nil) && uint64(immMem.availBytes()) > immMem.totalBytes()/2 { + d.mu.mem.nextSize = immMem.totalBytes() + } + + if b != nil && b.flushable != nil { + // The batch is too large to fit in the memtable so add it directly to + // the immutable queue. The flushable batch is associated with the same + // log as the immutable memtable, but logically occurs after it in + // seqnum space. We ensure while flushing that the flushable batch + // is flushed along with the previous memtable in the flushable + // queue. See the top level comment in DB.flush1 to learn how this + // is ensured. + // + // See DB.commitWrite for the special handling of log writes for large + // batches. In particular, the large batch has already written to + // imm.logNum. + entry := d.newFlushableEntry(b.flushable, imm.logNum, b.SeqNum()) + // The large batch is by definition large. Reserve space from the cache + // for it until it is flushed. + entry.releaseMemAccounting = d.opts.Cache.Reserve(int(b.flushable.totalBytes())) + d.mu.mem.queue = append(d.mu.mem.queue, entry) + } + + var logSeqNum uint64 + if b != nil { + logSeqNum = b.SeqNum() + if b.flushable != nil { + logSeqNum += uint64(b.Count()) + } + } else { + logSeqNum = d.mu.versions.logSeqNum.Load() + } + d.rotateMemtable(newLogNum, logSeqNum, immMem) + force = false + } +} + +// Both DB.mu and commitPipeline.mu must be held by the caller. +func (d *DB) rotateMemtable(newLogNum base.DiskFileNum, logSeqNum uint64, prev *memTable) { + // Create a new memtable, scheduling the previous one for flushing. We do + // this even if the previous memtable was empty because the DB.Flush + // mechanism is dependent on being able to wait for the empty memtable to + // flush. We can't just mark the empty memtable as flushed here because we + // also have to wait for all previous immutable tables to + // flush. Additionally, the memtable is tied to particular WAL file and we + // want to go through the flush path in order to recycle that WAL file. + // + // NB: newLogNum corresponds to the WAL that contains mutations that are + // present in the new memtable. When immutable memtables are flushed to + // disk, a VersionEdit will be created telling the manifest the minimum + // unflushed log number (which will be the next one in d.mu.mem.mutable + // that was not flushed). + // + // NB: prev should be the current mutable memtable. + var entry *flushableEntry + d.mu.mem.mutable, entry = d.newMemTable(newLogNum, logSeqNum) + d.mu.mem.queue = append(d.mu.mem.queue, entry) + d.updateReadStateLocked(nil) + if prev.writerUnref() { + d.maybeScheduleFlush() + } +} + +// Both DB.mu and commitPipeline.mu must be held by the caller. Note that DB.mu +// may be released and reacquired. +func (d *DB) recycleWAL() (newLogNum base.DiskFileNum, prevLogSize uint64) { + if d.opts.DisableWAL { + panic("pebble: invalid function call") + } + + jobID := d.mu.nextJobID + d.mu.nextJobID++ + newLogNum = d.mu.versions.getNextDiskFileNum() + + prevLogSize = uint64(d.mu.log.Size()) + + // The previous log may have grown past its original physical + // size. Update its file size in the queue so we have a proper + // accounting of its file size. + if d.mu.log.queue[len(d.mu.log.queue)-1].fileSize < prevLogSize { + d.mu.log.queue[len(d.mu.log.queue)-1].fileSize = prevLogSize + } + d.mu.Unlock() + + var err error + // Close the previous log first. This writes an EOF trailer + // signifying the end of the file and syncs it to disk. We must + // close the previous log before linking the new log file, + // otherwise a crash could leave both logs with unclean tails, and + // Open will treat the previous log as corrupt. + err = d.mu.log.LogWriter.Close() + metrics := d.mu.log.LogWriter.Metrics() + d.mu.Lock() + if err := d.mu.log.metrics.Merge(metrics); err != nil { + d.opts.Logger.Errorf("metrics error: %s", err) + } + d.mu.Unlock() + + newLogName := base.MakeFilepath(d.opts.FS, d.walDirname, fileTypeLog, newLogNum) + + // 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 + // already been written, than one which is being written for the first + // time. This is due to the need to sync file metadata when a file is + // being written for the first time. Note this is true even if file + // preallocation is performed (e.g. fallocate). + var recycleLog fileInfo + var recycleOK bool + var newLogFile vfs.File + if err == nil { + recycleLog, recycleOK = d.logRecycler.peek() + if recycleOK { + recycleLogName := base.MakeFilepath(d.opts.FS, d.walDirname, fileTypeLog, recycleLog.fileNum) + newLogFile, err = d.opts.FS.ReuseForWrite(recycleLogName, newLogName) + base.MustExist(d.opts.FS, newLogName, d.opts.Logger, err) + } else { + newLogFile, err = d.opts.FS.Create(newLogName) + base.MustExist(d.opts.FS, newLogName, d.opts.Logger, err) + } + } + + var newLogSize uint64 + if err == nil && recycleOK { + // Figure out the recycled WAL size. This Stat is necessary + // because ReuseForWrite's contract allows for removing the + // old file and creating a new one. We don't know whether the + // WAL was actually recycled. + // TODO(jackson): Adding a boolean to the ReuseForWrite return + // value indicating whether or not the file was actually + // reused would allow us to skip the stat and use + // recycleLog.fileSize. + var finfo os.FileInfo + finfo, err = newLogFile.Stat() + if err == nil { + newLogSize = uint64(finfo.Size()) + } + } + + if err == nil { + // TODO(peter): RocksDB delays sync of the parent directory until the + // first time the log is synced. Is that worthwhile? + err = d.walDir.Sync() + } + + if err != nil && newLogFile != nil { + newLogFile.Close() + } else if err == nil { + newLogFile = vfs.NewSyncingFile(newLogFile, vfs.SyncingFileOptions{ + NoSyncOnClose: d.opts.NoSyncOnClose, + BytesPerSync: d.opts.WALBytesPerSync, + PreallocateSize: d.walPreallocateSize(), + }) + } + + if recycleOK { + err = firstError(err, d.logRecycler.pop(recycleLog.fileNum.FileNum())) + } + + d.opts.EventListener.WALCreated(WALCreateInfo{ + JobID: jobID, + Path: newLogName, + FileNum: newLogNum, + RecycledFileNum: recycleLog.fileNum.FileNum(), + Err: err, + }) + + d.mu.Lock() + + d.mu.versions.metrics.WAL.Files++ + + if err != nil { + // TODO(peter): avoid chewing through file numbers in a tight loop if there + // is an error here. + // + // What to do here? Stumbling on doesn't seem worthwhile. If we failed to + // close the previous log it is possible we lost a write. + panic(err) + } + + d.mu.log.queue = append(d.mu.log.queue, fileInfo{fileNum: newLogNum, fileSize: newLogSize}) + d.mu.log.LogWriter = record.NewLogWriter(newLogFile, newLogNum, record.LogWriterConfig{ + WALFsyncLatency: d.mu.log.metrics.fsyncLatency, + WALMinSyncInterval: d.opts.WALMinSyncInterval, + QueueSemChan: d.commit.logSyncQSem, + }) + if d.mu.log.registerLogWriterForTesting != nil { + d.mu.log.registerLogWriterForTesting(d.mu.log.LogWriter) + } + + return +} + +func (d *DB) getEarliestUnflushedSeqNumLocked() uint64 { + seqNum := InternalKeySeqNumMax + for i := range d.mu.mem.queue { + logSeqNum := d.mu.mem.queue[i].logSeqNum + if seqNum > logSeqNum { + seqNum = logSeqNum + } + } + return seqNum +} + +func (d *DB) getInProgressCompactionInfoLocked(finishing *compaction) (rv []compactionInfo) { + for c := range d.mu.compact.inProgress { + if len(c.flushing) == 0 && (finishing == nil || c != finishing) { + info := compactionInfo{ + versionEditApplied: c.versionEditApplied, + inputs: c.inputs, + smallest: c.smallest, + largest: c.largest, + outputLevel: -1, + } + if c.outputLevel != nil { + info.outputLevel = c.outputLevel.level + } + rv = append(rv, info) + } + } + return +} + +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 + } + if !l0 { + continue + } + compactions = append(compactions, manifest.L0Compaction{ + Smallest: info.smallest, + Largest: info.largest, + IsIntraL0: info.outputLevel == 0, + }) + } + return compactions +} + +// firstError returns the first non-nil error of err0 and err1, or nil if both +// are nil. +func firstError(err0, err1 error) error { + if err0 != nil { + return err0 + } + return err1 +} + +// 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. +// +// 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.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, err := d.newInternalIter(ctx, snapshotIterOpts{}, scanInternalOpts) + if err != nil { + return LSMKeyStatistics{}, err + } + 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 + } + + objMeta, err := d.objProvider.Lookup(fileTypeTable, m.FileBacking.DiskFileNum) + if err != nil { + panic(err) + } + if objMeta.IsExternal() { + // Nothing to do; bounds are expected to be loose. + 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/pebble/db_test.go b/pebble/db_test.go new file mode 100644 index 0000000..631753d --- /dev/null +++ b/pebble/db_test.go @@ -0,0 +1,1969 @@ +// Copyright 2012 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 ( + "bytes" + "context" + "fmt" + "io" + "path/filepath" + "slices" + "strconv" + "strings" + "sync" + "testing" + "time" + + "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" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +// try repeatedly calls f, sleeping between calls with exponential back-off, +// until f returns a nil error or the total sleep time is greater than or equal +// to maxTotalSleep. It always calls f at least once. +func try(initialSleep, maxTotalSleep time.Duration, f func() error) error { + totalSleep := time.Duration(0) + for d := initialSleep; ; d *= 2 { + time.Sleep(d) + totalSleep += d + if err := f(); err == nil || totalSleep >= maxTotalSleep { + return err + } + } +} + +func TestTry(t *testing.T) { + c := make(chan struct{}) + go func() { + time.Sleep(1 * time.Millisecond) + close(c) + }() + + attemptsMu := sync.Mutex{} + attempts := 0 + + err := try(100*time.Microsecond, 20*time.Second, func() error { + attemptsMu.Lock() + attempts++ + attemptsMu.Unlock() + + select { + default: + return errors.New("timed out") + case <-c: + return nil + } + }) + require.NoError(t, err) + + attemptsMu.Lock() + a := attempts + attemptsMu.Unlock() + + if a == 0 { + t.Fatalf("attempts: got 0, want > 0") + } +} + +func TestBasicReads(t *testing.T) { + testCases := []struct { + dirname string + wantMap map[string]string + }{ + { + "db-stage-1", + map[string]string{ + "aaa": "", + "bar": "", + "baz": "", + "foo": "", + "quux": "", + "zzz": "", + }, + }, + { + "db-stage-2", + map[string]string{ + "aaa": "", + "bar": "", + "baz": "three", + "foo": "four", + "quux": "", + "zzz": "", + }, + }, + { + "db-stage-3", + map[string]string{ + "aaa": "", + "bar": "", + "baz": "three", + "foo": "four", + "quux": "", + "zzz": "", + }, + }, + { + "db-stage-4", + map[string]string{ + "aaa": "", + "bar": "", + "baz": "", + "foo": "five", + "quux": "six", + "zzz": "", + }, + }, + } + for _, tc := range testCases { + t.Run(tc.dirname, func(t *testing.T) { + fs := vfs.NewMem() + _, err := vfs.Clone(vfs.Default, fs, filepath.Join("testdata", tc.dirname), tc.dirname) + if err != nil { + t.Fatalf("%s: cloneFileSystem failed: %v", tc.dirname, err) + } + d, err := Open(tc.dirname, testingRandomized(t, &Options{ + FS: fs, + })) + if err != nil { + t.Fatalf("%s: Open failed: %v", tc.dirname, err) + } + for key, want := range tc.wantMap { + got, closer, err := d.Get([]byte(key)) + if err != nil && err != ErrNotFound { + t.Fatalf("%s: Get(%q) failed: %v", tc.dirname, key, err) + } + if string(got) != string(want) { + t.Fatalf("%s: Get(%q): got %q, want %q", tc.dirname, key, got, want) + } + if closer != nil { + closer.Close() + } + } + err = d.Close() + if err != nil { + t.Fatalf("%s: Close failed: %v", tc.dirname, err) + } + }) + } +} + +func TestBasicWrites(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + + names := []string{ + "Alatar", + "Gandalf", + "Pallando", + "Radagast", + "Saruman", + "Joe", + } + wantMap := map[string]string{} + + inBatch, batch, pending := false, &Batch{}, [][]string(nil) + set0 := func(k, v string) error { + return d.Set([]byte(k), []byte(v), nil) + } + del0 := func(k string) error { + return d.Delete([]byte(k), nil) + } + set1 := func(k, v string) error { + batch.Set([]byte(k), []byte(v), nil) + return nil + } + del1 := func(k string) error { + batch.Delete([]byte(k), nil) + return nil + } + set, del := set0, del0 + + testCases := []string{ + "set Gandalf Grey", + "set Saruman White", + "set Radagast Brown", + "delete Saruman", + "set Gandalf White", + "batch", + " set Alatar AliceBlue", + "apply", + "delete Pallando", + "set Alatar AntiqueWhite", + "set Pallando PapayaWhip", + "batch", + "apply", + "set Pallando PaleVioletRed", + "batch", + " delete Alatar", + " set Gandalf GhostWhite", + " set Saruman Seashell", + " delete Saruman", + " set Saruman SeaGreen", + " set Radagast RosyBrown", + " delete Pallando", + "apply", + "delete Radagast", + "delete Radagast", + "delete Radagast", + "set Gandalf Goldenrod", + "set Pallando PeachPuff", + "batch", + " delete Joe", + " delete Saruman", + " delete Radagast", + " delete Pallando", + " delete Gandalf", + " delete Alatar", + "apply", + "set Joe Plumber", + } + for i, tc := range testCases { + s := strings.Split(strings.TrimSpace(tc), " ") + switch s[0] { + case "set": + if err := set(s[1], s[2]); err != nil { + t.Fatalf("#%d %s: %v", i, tc, err) + } + if inBatch { + pending = append(pending, s) + } else { + wantMap[s[1]] = s[2] + } + case "delete": + if err := del(s[1]); err != nil { + t.Fatalf("#%d %s: %v", i, tc, err) + } + if inBatch { + pending = append(pending, s) + } else { + delete(wantMap, s[1]) + } + case "batch": + inBatch, batch, set, del = true, &Batch{}, set1, del1 + case "apply": + if err := d.Apply(batch, nil); err != nil { + t.Fatalf("#%d %s: %v", i, tc, err) + } + for _, p := range pending { + switch p[0] { + case "set": + wantMap[p[1]] = p[2] + case "delete": + delete(wantMap, p[1]) + } + } + inBatch, pending, set, del = false, nil, set0, del0 + default: + t.Fatalf("#%d %s: bad test case: %q", i, tc, s) + } + + fail := false + for _, name := range names { + g, closer, err := d.Get([]byte(name)) + if err != nil && err != ErrNotFound { + t.Errorf("#%d %s: Get(%q): %v", i, tc, name, err) + fail = true + } + got, gOK := string(g), err == nil + want, wOK := wantMap[name] + if got != want || gOK != wOK { + t.Errorf("#%d %s: Get(%q): got %q, %t, want %q, %t", + i, tc, name, got, gOK, want, wOK) + fail = true + } + if closer != nil { + closer.Close() + } + } + if fail { + return + } + } + + require.NoError(t, d.Close()) +} + +func TestRandomWrites(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + MemTableSize: 8 * 1024, + })) + require.NoError(t, err) + + keys := [64][]byte{} + wants := [64]int{} + for k := range keys { + keys[k] = []byte(strconv.Itoa(k)) + wants[k] = -1 + } + xxx := bytes.Repeat([]byte("x"), 512) + + rng := rand.New(rand.NewSource(123)) + const N = 1000 + for i := 0; i < N; i++ { + k := rng.Intn(len(keys)) + if rng.Intn(20) != 0 { + wants[k] = rng.Intn(len(xxx) + 1) + if err := d.Set(keys[k], xxx[:wants[k]], nil); err != nil { + t.Fatalf("i=%d: Set: %v", i, err) + } + } else { + wants[k] = -1 + if err := d.Delete(keys[k], nil); err != nil { + t.Fatalf("i=%d: Delete: %v", i, err) + } + } + + if i != N-1 || rng.Intn(50) != 0 { + continue + } + for k := range keys { + got := -1 + if v, closer, err := d.Get(keys[k]); err != nil { + if err != ErrNotFound { + t.Fatalf("Get: %v", err) + } + } else { + got = len(v) + closer.Close() + } + if got != wants[k] { + t.Errorf("i=%d, k=%d: got %d, want %d", i, k, got, wants[k]) + } + } + } + + require.NoError(t, d.Close()) +} + +func TestLargeBatch(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + MemTableSize: 1400, + MemTableStopWritesThreshold: 100, + })) + require.NoError(t, err) + + verifyLSM := func(expected string) func() error { + return func() error { + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + if expected != s { + if testing.Verbose() { + fmt.Println(strings.TrimSpace(s)) + } + return errors.Errorf("expected %s, but found %s", expected, s) + } + return nil + } + } + + logNum := func() base.DiskFileNum { + d.mu.Lock() + defer d.mu.Unlock() + return d.mu.log.queue[len(d.mu.log.queue)-1].fileNum + } + fileSize := func(fileNum base.DiskFileNum) int64 { + info, err := d.opts.FS.Stat(base.MakeFilepath(d.opts.FS, "", fileTypeLog, fileNum)) + require.NoError(t, err) + return info.Size() + } + memTableCreationSeqNum := func() uint64 { + d.mu.Lock() + defer d.mu.Unlock() + return d.mu.mem.mutable.logSeqNum + } + + startLogNum := logNum() + startLogStartSize := fileSize(startLogNum) + startSeqNum := d.mu.versions.logSeqNum.Load() + + // Write a key with a value larger than the memtable size. + require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("a"), 512), nil)) + + // Verify that the large batch was written to the WAL that existed before it + // was committed. We verify that WAL rotation occurred, where the large batch + // was written to, and that the new WAL is empty. + endLogNum := logNum() + if startLogNum == endLogNum { + t.Fatal("expected WAL rotation") + } + startLogEndSize := fileSize(startLogNum) + if startLogEndSize == startLogStartSize { + t.Fatalf("expected large batch to be written to %s.log, but file size unchanged at %d", + startLogNum, startLogEndSize) + } + endLogSize := fileSize(endLogNum) + if endLogSize != 0 { + t.Fatalf("expected %s.log to be empty, but found %d", endLogNum, endLogSize) + } + if creationSeqNum := memTableCreationSeqNum(); creationSeqNum <= startSeqNum { + t.Fatalf("expected memTable.logSeqNum=%d > largeBatch.seqNum=%d", creationSeqNum, startSeqNum) + } + + // Verify this results in one L0 table being created. + require.NoError(t, try(100*time.Microsecond, 20*time.Second, + verifyLSM("0.0:\n 000005:[a#10,SET-a#10,SET]\n"))) + + require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("b"), 512), nil)) + + // Verify this results in a second L0 table being created. + require.NoError(t, try(100*time.Microsecond, 20*time.Second, + verifyLSM("0.0:\n 000005:[a#10,SET-a#10,SET]\n 000007:[b#11,SET-b#11,SET]\n"))) + + // Allocate a bunch of batches to exhaust the batchPool. None of these + // batches should have a non-zero count. + for i := 0; i < 10; i++ { + b := d.NewBatch() + require.EqualValues(t, 0, b.Count()) + } + + require.NoError(t, d.Close()) +} + +func TestGetNoCache(t *testing.T) { + cache := NewCache(0) + defer cache.Unref() + + d, err := Open("", testingRandomized(t, &Options{ + Cache: cache, + FS: vfs.NewMem(), + })) + require.NoError(t, err) + + require.NoError(t, d.Set([]byte("a"), []byte("aa"), nil)) + require.NoError(t, d.Flush()) + verifyGet(t, d, []byte("a"), []byte("aa")) + + require.NoError(t, d.Close()) +} + +func TestGetMerge(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + + key := []byte("a") + verify := func(expected string) { + val, closer, err := d.Get(key) + require.NoError(t, err) + + if expected != string(val) { + t.Fatalf("expected %s, but got %s", expected, val) + } + closer.Close() + } + + const val = "1" + for i := 1; i <= 3; i++ { + require.NoError(t, d.Merge(key, []byte(val), nil)) + + expected := strings.Repeat(val, i) + verify(expected) + + require.NoError(t, d.Flush()) + verify(expected) + } + + require.NoError(t, d.Close()) +} + +func TestMergeOrderSameAfterFlush(t *testing.T) { + // Ensure compaction iterator (used by flush) and user iterator process merge + // operands in the same order + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + + key := []byte("a") + verify := func(expected string) { + iter, _ := d.NewIter(nil) + if !iter.SeekGE([]byte("a")) { + t.Fatal("expected one value, but got empty iterator") + } + if expected != string(iter.Value()) { + t.Fatalf("expected %s, but got %s", expected, string(iter.Value())) + } + if !iter.SeekLT([]byte("b")) { + t.Fatal("expected one value, but got empty iterator") + } + if expected != string(iter.Value()) { + t.Fatalf("expected %s, but got %s", expected, string(iter.Value())) + } + require.NoError(t, iter.Close()) + } + + require.NoError(t, d.Merge(key, []byte("0"), nil)) + require.NoError(t, d.Merge(key, []byte("1"), nil)) + + verify("01") + require.NoError(t, d.Flush()) + verify("01") + + require.NoError(t, d.Close()) +} + +type closableMerger struct { + lastBuf []byte + closed bool +} + +func (m *closableMerger) MergeNewer(value []byte) error { + m.lastBuf = append(m.lastBuf[:0], value...) + return nil +} + +func (m *closableMerger) MergeOlder(value []byte) error { + m.lastBuf = append(m.lastBuf[:0], value...) + return nil +} + +func (m *closableMerger) Finish(includesBase bool) ([]byte, io.Closer, error) { + return m.lastBuf, m, nil +} + +func (m *closableMerger) Close() error { + m.closed = true + return nil +} + +func TestMergerClosing(t *testing.T) { + m := &closableMerger{} + + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + Merger: &Merger{ + Merge: func(key, value []byte) (base.ValueMerger, error) { + return m, m.MergeNewer(value) + }, + }, + })) + require.NoError(t, err) + + defer func() { + require.NoError(t, d.Close()) + }() + + err = d.Merge([]byte("a"), []byte("b"), nil) + require.NoError(t, err) + require.False(t, m.closed) + + val, closer, err := d.Get([]byte("a")) + require.NoError(t, err) + require.Equal(t, []byte("b"), val) + require.NotNil(t, closer) + require.False(t, m.closed) + _ = closer.Close() + require.True(t, m.closed) +} + +func TestLogData(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + + defer func() { + require.NoError(t, d.Close()) + }() + + require.NoError(t, d.LogData([]byte("foo"), Sync)) + require.NoError(t, d.LogData([]byte("bar"), Sync)) + // TODO(itsbilal): Confirm that we wrote some bytes to the WAL. + // For now, LogData proceeding ahead without a panic is good enough. +} + +func TestSingleDeleteGet(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + defer func() { + require.NoError(t, d.Close()) + }() + + key := []byte("key") + val := []byte("val") + + require.NoError(t, d.Set(key, val, nil)) + verifyGet(t, d, key, val) + + key2 := []byte("key2") + val2 := []byte("val2") + + require.NoError(t, d.Set(key2, val2, nil)) + verifyGet(t, d, key2, val2) + + require.NoError(t, d.SingleDelete(key2, nil)) + verifyGetNotFound(t, d, key2) +} + +func TestSingleDeleteFlush(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + defer func() { + require.NoError(t, d.Close()) + }() + + key := []byte("key") + valFirst := []byte("first") + valSecond := []byte("second") + key2 := []byte("key2") + val2 := []byte("val2") + + require.NoError(t, d.Set(key, valFirst, nil)) + require.NoError(t, d.Set(key2, val2, nil)) + require.NoError(t, d.Flush()) + + require.NoError(t, d.SingleDelete(key, nil)) + require.NoError(t, d.Set(key, valSecond, nil)) + require.NoError(t, d.Delete(key2, nil)) + require.NoError(t, d.Set(key2, val2, nil)) + require.NoError(t, d.Flush()) + + require.NoError(t, d.SingleDelete(key, nil)) + require.NoError(t, d.Delete(key2, nil)) + require.NoError(t, d.Flush()) + + verifyGetNotFound(t, d, key) + verifyGetNotFound(t, d, key2) +} + +func TestUnremovableSingleDelete(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + L0CompactionThreshold: 8, + })) + require.NoError(t, err) + defer func() { + require.NoError(t, d.Close()) + }() + + key := []byte("key") + valFirst := []byte("valFirst") + valSecond := []byte("valSecond") + + require.NoError(t, d.Set(key, valFirst, nil)) + ss := d.NewSnapshot() + defer ss.Close() + require.NoError(t, d.SingleDelete(key, nil)) + require.NoError(t, d.Set(key, valSecond, nil)) + require.NoError(t, d.Flush()) + + verifyGet(t, ss, key, valFirst) + verifyGet(t, d, key, valSecond) + + require.NoError(t, d.SingleDelete(key, nil)) + + verifyGet(t, ss, key, valFirst) + verifyGetNotFound(t, d, key) + + require.NoError(t, d.Flush()) + + verifyGet(t, ss, key, valFirst) + verifyGetNotFound(t, d, key) +} + +func TestIterLeak(t *testing.T) { + for _, leak := range []bool{true, false} { + t.Run(fmt.Sprintf("leak=%t", leak), func(t *testing.T) { + for _, flush := range []bool{true, false} { + t.Run(fmt.Sprintf("flush=%t", flush), func(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + + require.NoError(t, d.Set([]byte("a"), []byte("a"), nil)) + if flush { + require.NoError(t, d.Flush()) + } + iter, _ := d.NewIter(nil) + iter.First() + if !leak { + require.NoError(t, iter.Close()) + require.NoError(t, d.Close()) + } else { + defer iter.Close() + if err := d.Close(); err == nil { + t.Fatalf("expected failure, but found success") + } else if !strings.HasPrefix(err.Error(), "leaked iterators:") { + t.Fatalf("expected leaked iterators, but found %+v", err) + } else { + t.Log(err.Error()) + } + } + }) + } + }) + } +} + +// Make sure that we detect an iter leak when only one DB closes +// while the second db still holds a reference to the TableCache. +func TestIterLeakSharedCache(t *testing.T) { + for _, leak := range []bool{true, false} { + t.Run(fmt.Sprintf("leak=%t", leak), func(t *testing.T) { + for _, flush := range []bool{true, false} { + t.Run(fmt.Sprintf("flush=%t", flush), func(t *testing.T) { + d1, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + + d2, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + + require.NoError(t, d1.Set([]byte("a"), []byte("a"), nil)) + if flush { + require.NoError(t, d1.Flush()) + } + + require.NoError(t, d2.Set([]byte("a"), []byte("a"), nil)) + if flush { + require.NoError(t, d2.Flush()) + } + + // Check if leak detection works with only one db closing. + { + iter1, _ := d1.NewIter(nil) + iter1.First() + if !leak { + require.NoError(t, iter1.Close()) + require.NoError(t, d1.Close()) + } else { + defer iter1.Close() + if err := d1.Close(); err == nil { + t.Fatalf("expected failure, but found success") + } else if !strings.HasPrefix(err.Error(), "leaked iterators:") { + t.Fatalf("expected leaked iterators, but found %+v", err) + } else { + t.Log(err.Error()) + } + } + } + + { + iter2, _ := d2.NewIter(nil) + iter2.First() + if !leak { + require.NoError(t, iter2.Close()) + require.NoError(t, d2.Close()) + } else { + defer iter2.Close() + if err := d2.Close(); err == nil { + t.Fatalf("expected failure, but found success") + } else if !strings.HasPrefix(err.Error(), "leaked iterators:") { + t.Fatalf("expected leaked iterators, but found %+v", err) + } else { + t.Log(err.Error()) + } + } + } + + }) + } + }) + } +} + +func TestMemTableReservation(t *testing.T) { + opts := &Options{ + Cache: NewCache(128 << 10 /* 128 KB */), + MemTableSize: initialMemTableSize, + FS: vfs.NewMem(), + } + defer opts.Cache.Unref() + opts.testingRandomized(t) + opts.EnsureDefaults() + // We're going to be looking at and asserting the global memtable reservation + // amount below so we don't want to race with any triggered stats collections. + opts.private.disableTableStats = true + + // Add a block to the cache. Note that the memtable size is larger than the + // cache size, so opening the DB should cause this block to be evicted. + tmpID := opts.Cache.NewID() + helloWorld := []byte("hello world") + value := cache.Alloc(len(helloWorld)) + copy(value.Buf(), helloWorld) + opts.Cache.Set(tmpID, base.FileNum(0).DiskFileNum(), 0, value).Release() + + d, err := Open("", opts) + require.NoError(t, err) + + checkReserved := func(expected int64) { + t.Helper() + if reserved := d.memTableReserved.Load(); expected != reserved { + t.Fatalf("expected %d reserved, but found %d", expected, reserved) + } + } + + checkReserved(int64(opts.MemTableSize)) + if refs := d.mu.mem.queue[len(d.mu.mem.queue)-1].readerRefs.Load(); refs != 2 { + t.Fatalf("expected 2 refs, but found %d", refs) + } + // Verify the memtable reservation has caused our test block to be evicted. + if h := opts.Cache.Get(tmpID, base.FileNum(0).DiskFileNum(), 0); h.Get() != nil { + t.Fatalf("expected failure, but found success: %s", h.Get()) + } + + // Flush the memtable. The memtable reservation should double because old + // memtable will be recycled, saved for the next memtable allocation. + require.NoError(t, d.Flush()) + checkReserved(int64(2 * opts.MemTableSize)) + // Flush again. The memtable reservation should be unchanged because at most + // 1 memtable may be preserved for recycling. + + // Flush in the presence of an active iterator. The iterator will hold a + // reference to a readState which will in turn hold a reader reference to the + // memtable. + iter, _ := d.NewIter(nil) + require.NoError(t, d.Flush()) + // The flush moved the recycled memtable into position as an active mutable + // memtable. There are now two allocated memtables: 1 mutable and 1 pinned + // by the iterator's read state. + checkReserved(2 * int64(opts.MemTableSize)) + + // Flushing again should increase the reservation total to 3x: 1 active + // mutable, 1 for recycling, 1 pinned by iterator's read state. + require.NoError(t, d.Flush()) + checkReserved(3 * int64(opts.MemTableSize)) + + // Closing the iterator will release the iterator's read state, and the old + // memtable will be moved into position as the next memtable to recycle. + // There was already a memtable ready to be recycled, so that memtable will + // be freed and the overall reservation total is reduced to 2x. + require.NoError(t, iter.Close()) + checkReserved(2 * int64(opts.MemTableSize)) + + require.NoError(t, d.Close()) +} + +func TestMemTableReservationLeak(t *testing.T) { + d, err := Open("", &Options{FS: vfs.NewMem()}) + require.NoError(t, err) + + d.mu.Lock() + last := d.mu.mem.queue[len(d.mu.mem.queue)-1] + last.readerRef() + defer func() { + last.readerUnref(true) + }() + d.mu.Unlock() + if err := d.Close(); err == nil { + t.Fatalf("expected failure, but found success") + } else if !strings.HasPrefix(err.Error(), "leaked memtable reservation:") { + t.Fatalf("expected leaked memtable reservation, but found %+v", err) + } else { + t.Log(err.Error()) + } +} + +func TestCacheEvict(t *testing.T) { + cache := NewCache(10 << 20) + defer cache.Unref() + + d, err := Open("", &Options{ + Cache: cache, + FS: vfs.NewMem(), + }) + require.NoError(t, err) + + for i := 0; i < 1000; i++ { + key := []byte(fmt.Sprintf("%04d", i)) + require.NoError(t, d.Set(key, key, nil)) + } + + require.NoError(t, d.Flush()) + iter, _ := d.NewIter(nil) + for iter.First(); iter.Valid(); iter.Next() { + } + require.NoError(t, iter.Close()) + + if size := cache.Size(); size == 0 { + t.Fatalf("expected non-zero cache size") + } + + for i := 0; i < 1000; i++ { + key := []byte(fmt.Sprintf("%04d", i)) + require.NoError(t, d.Delete(key, nil)) + } + + require.NoError(t, d.Compact([]byte("0"), []byte("1"), false)) + + require.NoError(t, d.Close()) + + if size := cache.Size(); size != 0 { + t.Fatalf("expected empty cache, but found %d", size) + } +} + +func TestFlushEmpty(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + + // Flushing an empty memtable should not fail. + require.NoError(t, d.Flush()) + require.NoError(t, d.Close()) +} + +func TestRollManifest(t *testing.T) { + toPreserve := rand.Int31n(5) + 1 + opts := &Options{ + MaxManifestFileSize: 1, + L0CompactionThreshold: 10, + L0StopWritesThreshold: 1000, + FS: vfs.NewMem(), + NumPrevManifest: int(toPreserve), + } + opts.DisableAutomaticCompactions = true + opts.testingRandomized(t) + d, err := Open("", opts) + require.NoError(t, err) + + manifestFileNumber := func() base.DiskFileNum { + d.mu.Lock() + defer d.mu.Unlock() + return d.mu.versions.manifestFileNum + } + sizeRolloverState := func() (int64, int64) { + d.mu.Lock() + defer d.mu.Unlock() + return d.mu.versions.rotationHelper.DebugInfo() + } + + current := func() string { + desc, err := Peek(d.dirname, d.opts.FS) + require.NoError(t, err) + return desc.ManifestFilename + } + + lastManifestNum := manifestFileNumber() + manifestNums := []base.DiskFileNum{lastManifestNum} + for i := 0; i < 5; i++ { + // MaxManifestFileSize is 1, but the rollover logic also counts edits + // since the last snapshot to decide on rollover, so do as many flushes as + // it demands. + lastSnapshotCount, editsSinceSnapshotCount := sizeRolloverState() + var expectedLastSnapshotCount, expectedEditsSinceSnapshotCount int64 + switch i { + case 0: + // DB is empty. + expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 0, 0 + case 1: + // First edit that caused rollover is not in the snapshot. + expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 0, 1 + case 2: + // One flush is in the snapshot. One flush in the edit. + expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 1, 1 + case 3: + // Two flushes in the snapshot. One flush in the edit. Will need to do + // two more flushes, the first of which will be in the next snapshot. + expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 2, 1 + case 4: + // Four flushes in the snapshot. One flush in the edit. Will need to do + // four more flushes, three of which will be in the snapshot. + expectedLastSnapshotCount, expectedEditsSinceSnapshotCount = 4, 1 + } + require.Equal(t, expectedLastSnapshotCount, lastSnapshotCount) + require.Equal(t, expectedEditsSinceSnapshotCount, editsSinceSnapshotCount) + // Number of flushes to do to trigger the rollover. + steps := int(lastSnapshotCount - editsSinceSnapshotCount + 1) + // Steps can be <= 0, but we need to do at least one edit to trigger the + // rollover logic. + if steps <= 0 { + steps = 1 + } + for j := 0; j < steps; j++ { + require.NoError(t, d.Set([]byte("a"), nil, nil)) + require.NoError(t, d.Flush()) + } + d.TestOnlyWaitForCleaning() + num := manifestFileNumber() + if lastManifestNum == num { + t.Fatalf("manifest failed to roll %d: %d == %d", i, lastManifestNum, num) + } + + manifestNums = append(manifestNums, num) + lastManifestNum = num + + expectedCurrent := fmt.Sprintf("MANIFEST-%s", lastManifestNum) + if v := current(); expectedCurrent != v { + t.Fatalf("expected %s, but found %s", expectedCurrent, v) + } + } + lastSnapshotCount, editsSinceSnapshotCount := sizeRolloverState() + require.EqualValues(t, 8, lastSnapshotCount) + require.EqualValues(t, 1, editsSinceSnapshotCount) + + files, err := d.opts.FS.List("") + require.NoError(t, err) + + var manifests []string + for _, filename := range files { + fileType, _, ok := base.ParseFilename(d.opts.FS, filename) + if !ok { + continue + } + if fileType == fileTypeManifest { + manifests = append(manifests, filename) + } + } + slices.Sort(manifests) + + var expected []string + for i := len(manifestNums) - int(toPreserve) - 1; i < len(manifestNums); i++ { + expected = append( + expected, + fmt.Sprintf("MANIFEST-%s", manifestNums[i]), + ) + } + require.EqualValues(t, expected, manifests) + + // Test the logic that uses the future snapshot size to rollover. + // Reminder: we have a snapshot with 8 files and the manifest has 1 edit + // (flush) with 1 file. + // Add 8 more files with a different key. + lastManifestNum = manifestFileNumber() + for j := 0; j < 8; j++ { + require.NoError(t, d.Set([]byte("c"), nil, nil)) + require.NoError(t, d.Flush()) + } + lastSnapshotCount, editsSinceSnapshotCount = sizeRolloverState() + // Need 16 more files in edits to trigger a rollover. + require.EqualValues(t, 16, lastSnapshotCount) + require.EqualValues(t, 1, editsSinceSnapshotCount) + require.NotEqual(t, manifestFileNumber(), lastManifestNum) + lastManifestNum = manifestFileNumber() + // Do a compaction that moves 8 of the files from L0 to 1 file in L6. This + // adds 9 files in edits. We still need 6 more files in edits based on the + // last snapshot. But the current version has only 9 L0 files and 1 L6 file, + // for a total of 10 files. So 1 flush should push us over that threshold. + d.Compact([]byte("c"), []byte("d"), false) + lastSnapshotCount, editsSinceSnapshotCount = sizeRolloverState() + require.EqualValues(t, 16, lastSnapshotCount) + require.EqualValues(t, 10, editsSinceSnapshotCount) + require.Equal(t, manifestFileNumber(), lastManifestNum) + require.NoError(t, d.Set([]byte("c"), nil, nil)) + require.NoError(t, d.Flush()) + lastSnapshotCount, editsSinceSnapshotCount = sizeRolloverState() + require.EqualValues(t, 10, lastSnapshotCount) + require.EqualValues(t, 1, editsSinceSnapshotCount) + require.NotEqual(t, manifestFileNumber(), lastManifestNum) + + require.NoError(t, d.Close()) +} + +func TestDBClosed(t *testing.T) { + d, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + require.NoError(t, d.Close()) + + catch := func(f func()) (err error) { + defer func() { + if r := recover(); r != nil { + err = r.(error) + } + }() + f() + return nil + } + + require.True(t, errors.Is(catch(func() { _ = d.Close() }), ErrClosed)) + + require.True(t, errors.Is(catch(func() { _ = d.Compact(nil, nil, false) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.Flush() }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _, _ = d.AsyncFlush() }), ErrClosed)) + + require.True(t, errors.Is(catch(func() { _, _, _ = d.Get(nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.Delete(nil, nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.DeleteRange(nil, nil, nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.Ingest(nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.LogData(nil, nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.Merge(nil, nil, nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.RatchetFormatMajorVersion(internalFormatNewest) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.Set(nil, nil, nil) }), ErrClosed)) + + require.True(t, errors.Is(catch(func() { _ = d.NewSnapshot() }), ErrClosed)) + + b := d.NewIndexedBatch() + require.True(t, errors.Is(catch(func() { _ = b.Commit(nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _ = d.Apply(b, nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _, _ = b.NewIter(nil) }), ErrClosed)) +} + +func TestDBConcurrentCommitCompactFlush(t *testing.T) { + d, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + })) + require.NoError(t, err) + + // Concurrently commit, compact, and flush in order to stress the locking around + // those operations. + const n = 1000 + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go func(i int) { + defer wg.Done() + _ = d.Set([]byte(fmt.Sprint(i)), nil, nil) + var err error + switch i % 3 { + case 0: + err = d.Compact(nil, []byte("\xff"), false) + case 1: + err = d.Flush() + case 2: + _, err = d.AsyncFlush() + } + require.NoError(t, err) + }(i) + } + wg.Wait() + + require.NoError(t, d.Close()) +} + +func TestDBConcurrentCompactClose(t *testing.T) { + // Test closing while a compaction is ongoing. This ensures compaction code + // detects the close and finishes cleanly. + mem := vfs.NewMem() + for i := 0; i < 100; i++ { + opts := &Options{ + FS: mem, + MaxConcurrentCompactions: func() int { + return 2 + }, + } + d, err := Open("", testingRandomized(t, opts)) + require.NoError(t, err) + + // Ingest a series of files containing a single key each. As the outer + // loop progresses, these ingestions will build up compaction debt + // causing compactions to be running concurrently with the close below. + for j := 0; j < 10; j++ { + path := fmt.Sprintf("ext%d", j) + f, err := mem.Create(path) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + require.NoError(t, w.Set([]byte(fmt.Sprint(j)), nil)) + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{path})) + } + + require.NoError(t, d.Close()) + } +} + +func TestDBApplyBatchNilDB(t *testing.T) { + d, err := Open("", &Options{FS: vfs.NewMem()}) + require.NoError(t, err) + + b1 := &Batch{} + b1.Set([]byte("test"), nil, nil) + + b2 := &Batch{} + b2.Apply(b1, nil) + if b2.memTableSize != 0 { + t.Fatalf("expected memTableSize to not be set") + } + require.NoError(t, d.Apply(b2, nil)) + if b1.memTableSize != b2.memTableSize { + t.Fatalf("expected memTableSize %d, but found %d", b1.memTableSize, b2.memTableSize) + } + + require.NoError(t, d.Close()) +} + +func TestDBApplyBatchMismatch(t *testing.T) { + srcDB, err := Open("", &Options{FS: vfs.NewMem()}) + require.NoError(t, err) + + applyDB, err := Open("", &Options{FS: vfs.NewMem()}) + require.NoError(t, err) + + err = func() (err error) { + defer func() { + if v := recover(); v != nil { + err = errors.Errorf("%v", v) + } + }() + + b := srcDB.NewBatch() + b.Set([]byte("test"), nil, nil) + return applyDB.Apply(b, nil) + }() + if err == nil || !strings.Contains(err.Error(), "pebble: batch db mismatch:") { + t.Fatalf("expected error, but found %v", err) + } + + require.NoError(t, srcDB.Close()) + require.NoError(t, applyDB.Close()) +} + +func TestCloseCleanerRace(t *testing.T) { + mem := vfs.NewMem() + for i := 0; i < 20; i++ { + db, err := Open("", testingRandomized(t, &Options{FS: mem})) + require.NoError(t, err) + require.NoError(t, db.Set([]byte("a"), []byte("something"), Sync)) + require.NoError(t, db.Flush()) + // Ref the sstables so cannot be deleted. + it, _ := db.NewIter(nil) + require.NotNil(t, it) + require.NoError(t, db.DeleteRange([]byte("a"), []byte("b"), Sync)) + require.NoError(t, db.Compact([]byte("a"), []byte("b"), false)) + // Only the iterator is keeping the sstables alive. + files, err := mem.List("/") + require.NoError(t, err) + var found bool + for _, f := range files { + if strings.HasSuffix(f, ".sst") { + found = true + break + } + } + require.True(t, found) + // Close the iterator and the db in succession so file cleaning races with DB.Close() -- + // latter should wait for file cleaning to finish. + require.NoError(t, it.Close()) + require.NoError(t, db.Close()) + files, err = mem.List("/") + require.NoError(t, err) + for _, f := range files { + if strings.HasSuffix(f, ".sst") { + t.Fatalf("found sst: %s", f) + } + } + } +} + +func TestSSTablesWithApproximateSpanBytes(t *testing.T) { + d, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + // Create two sstables. + // sstable is contained within keyspan (fileNum = 5). + require.NoError(t, d.Set([]byte("c"), nil, nil)) + require.NoError(t, d.Set([]byte("d"), nil, nil)) + require.NoError(t, d.Flush()) + + // sstable partially overlaps keyspan (fileNum = 7). + require.NoError(t, d.Set([]byte("d"), nil, nil)) + require.NoError(t, d.Set([]byte("g"), nil, nil)) + require.NoError(t, d.Flush()) + + // cannot use WithApproximateSpanBytes without WithProperties. + _, err = d.SSTables(WithKeyRangeFilter([]byte("a"), []byte("e")), WithApproximateSpanBytes()) + require.Error(t, err) + + // cannot use WithApproximateSpanBytes without WithKeyRangeFilter. + _, err = d.SSTables(WithProperties(), WithApproximateSpanBytes()) + require.Error(t, err) + + tableInfos, err := d.SSTables(WithProperties(), WithKeyRangeFilter([]byte("a"), []byte("e")), WithApproximateSpanBytes()) + require.NoError(t, err) + + for _, levelTables := range tableInfos { + for _, table := range levelTables { + approximateSpanBytes, err := strconv.ParseInt(table.Properties.UserProperties["approximate-span-bytes"], 10, 64) + require.NoError(t, err) + if table.FileNum == 5 { + require.Equal(t, uint64(approximateSpanBytes), table.Size) + } + if table.FileNum == 7 { + require.Less(t, uint64(approximateSpanBytes), table.Size) + } + } + } +} + +func TestFilterSSTablesWithOption(t *testing.T) { + d, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + // Create two sstables. + require.NoError(t, d.Set([]byte("/Table/5"), nil, nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("/Table/10"), nil, nil)) + require.NoError(t, d.Flush()) + + tableInfos, err := d.SSTables(WithKeyRangeFilter([]byte("/Table/5"), []byte("/Table/6"))) + require.NoError(t, err) + + totalTables := 0 + for _, levelTables := range tableInfos { + totalTables += len(levelTables) + } + + // with filter second sstable should not be returned + require.EqualValues(t, 1, totalTables) + + tableInfos, err = d.SSTables() + require.NoError(t, err) + + totalTables = 0 + for _, levelTables := range tableInfos { + totalTables += len(levelTables) + } + + // without filter + require.EqualValues(t, 2, totalTables) +} + +func TestSSTables(t *testing.T) { + d, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + // Create two sstables. + require.NoError(t, d.Set([]byte("hello"), nil, nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("world"), nil, nil)) + require.NoError(t, d.Flush()) + + // by default returned table infos should not contain Properties + tableInfos, err := d.SSTables() + require.NoError(t, err) + for _, levelTables := range tableInfos { + for _, info := range levelTables { + require.Nil(t, info.Properties) + } + } + + // with opt `WithProperties()` the `Properties` in table info should not be nil + tableInfos, err = d.SSTables(WithProperties()) + require.NoError(t, err) + for _, levelTables := range tableInfos { + for _, info := range levelTables { + require.NotNil(t, info.Properties) + } + } +} + +type testTracer struct { + enabledOnlyForNonBackgroundContext bool + buf strings.Builder +} + +func (t *testTracer) Infof(format string, args ...interface{}) {} +func (t *testTracer) Errorf(format string, args ...interface{}) {} +func (t *testTracer) Fatalf(format string, args ...interface{}) {} + +func (t *testTracer) Eventf(ctx context.Context, format string, args ...interface{}) { + if t.enabledOnlyForNonBackgroundContext && ctx == context.Background() { + return + } + fmt.Fprintf(&t.buf, format, args...) + fmt.Fprint(&t.buf, "\n") +} + +func (t *testTracer) IsTracingEnabled(ctx context.Context) bool { + if t.enabledOnlyForNonBackgroundContext && ctx == context.Background() { + return false + } + return true +} + +func TestTracing(t *testing.T) { + if !invariants.Enabled { + // The test relies on timing behavior injected when invariants.Enabled. + return + } + var tracer testTracer + c := NewCache(0) + defer c.Unref() + d, err := Open("", &Options{ + FS: vfs.NewMem(), + Cache: c, + LoggerAndTracer: &tracer, + }) + require.NoError(t, err) + defer func() { + require.NoError(t, d.Close()) + }() + + // Create a sstable. + require.NoError(t, d.Set([]byte("hello"), nil, nil)) + require.NoError(t, d.Flush()) + _, closer, err := d.Get([]byte("hello")) + require.NoError(t, err) + closer.Close() + readerInitTraceString := "reading 37 bytes took 5ms\nreading 628 bytes took 5ms\n" + iterTraceString := "reading 27 bytes took 5ms\nreading 29 bytes took 5ms\n" + require.Equal(t, readerInitTraceString+iterTraceString, tracer.buf.String()) + + // Get again, but since it currently uses context.Background(), no trace + // output is produced. + tracer.buf.Reset() + tracer.enabledOnlyForNonBackgroundContext = true + _, closer, err = d.Get([]byte("hello")) + require.NoError(t, err) + closer.Close() + require.Equal(t, "", tracer.buf.String()) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + iter, _ := d.NewIterWithContext(ctx, nil) + iter.SeekGE([]byte("hello")) + iter.Close() + require.Equal(t, iterTraceString, tracer.buf.String()) + + tracer.buf.Reset() + snap := d.NewSnapshot() + iter, _ = snap.NewIterWithContext(ctx, nil) + iter.SeekGE([]byte("hello")) + iter.Close() + require.Equal(t, iterTraceString, tracer.buf.String()) + snap.Close() + + tracer.buf.Reset() + b := d.NewIndexedBatch() + iter, err = b.NewIterWithContext(ctx, nil) + require.NoError(t, err) + iter.SeekGE([]byte("hello")) + iter.Close() + require.Equal(t, iterTraceString, tracer.buf.String()) + b.Close() +} + +func TestMemtableIngestInversion(t *testing.T) { + memFS := vfs.NewMem() + opts := &Options{ + FS: memFS, + MemTableSize: 256 << 10, // 4KB + MemTableStopWritesThreshold: 1000, + L0StopWritesThreshold: 1000, + L0CompactionThreshold: 2, + MaxConcurrentCompactions: func() int { + return 1000 + }, + } + + const channelTimeout = 5 * time.Second + + // We induce delay in compactions by passing in an EventListener that stalls on + // the first TableCreated event for a compaction job we want to block. + // FlushBegin and CompactionBegin has info on compaction start/output levels + // which is what we need to identify what compactions to block. However + // FlushBegin and CompactionBegin are called while holding db.mu, so we cannot + // block those events forever. Instead, we grab the job ID from those events + // and store it. Then during TableCreated, we check if we're creating an output + // for a job we have identified earlier as one to block, and then hold on a + // semaphore there until there's a signal from the test code to resume with the + // compaction. + // + // If nextBlockedCompaction is non-zero, we must block the next compaction + // out of the nextBlockedCompaction - 3 start level. 1 means block the next + // intra-L0 compaction and 2 means block the next flush (as flushes have + // a -1 start level). + var nextBlockedCompaction, blockedJobID int + var blockedCompactionsMu sync.Mutex // protects the above two variables. + nextSem := make(chan chan struct{}, 1) + var el EventListener + el.EnsureDefaults(testLogger{t: t}) + el.FlushBegin = func(info FlushInfo) { + blockedCompactionsMu.Lock() + defer blockedCompactionsMu.Unlock() + if nextBlockedCompaction == 2 { + nextBlockedCompaction = 0 + blockedJobID = info.JobID + } + } + el.CompactionBegin = func(info CompactionInfo) { + // 0 = block nothing, 1 = block intra-L0 compaction, 2 = block flush, + // 3 = block L0 -> LBase compaction, 4 = block compaction out of L1, and so on. + blockedCompactionsMu.Lock() + defer blockedCompactionsMu.Unlock() + blockValue := info.Input[0].Level + 3 + if info.Input[0].Level == 0 && info.Output.Level == 0 { + // Intra L0 compaction, denoted by casValue of 1. + blockValue = 1 + } + if nextBlockedCompaction == blockValue { + nextBlockedCompaction = 0 + blockedJobID = info.JobID + } + } + el.TableCreated = func(info TableCreateInfo) { + blockedCompactionsMu.Lock() + if info.JobID != blockedJobID { + blockedCompactionsMu.Unlock() + return + } + blockedJobID = 0 + blockedCompactionsMu.Unlock() + sem := make(chan struct{}) + nextSem <- sem + <-sem + } + tel := TeeEventListener(MakeLoggingEventListener(testLogger{t: t}), el) + opts.EventListener = &tel + opts.Experimental.L0CompactionConcurrency = 1 + d, err := Open("", opts) + require.NoError(t, err) + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + printLSM := func() { + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + t.Logf("%s", s) + } + + // Create some sstables. These should go into L6. These are irrelevant for + // the rest of the test. + require.NoError(t, d.Set([]byte("b"), []byte("foo"), nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("d"), []byte("bar"), nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact([]byte("a"), []byte("z"), true)) + + var baseCompactionSem, flushSem, intraL0Sem chan struct{} + // Block an L0 -> LBase compaction. This is necessary to induce intra-L0 + // compactions later on. + blockedCompactionsMu.Lock() + nextBlockedCompaction = 3 + blockedCompactionsMu.Unlock() + timeoutSem := time.After(channelTimeout) + t.Log("blocking an L0 -> LBase compaction") + // Write an sstable to L0 until we're blocked on an L0 -> LBase compaction. + breakLoop := false + for !breakLoop { + select { + case sem := <-nextSem: + baseCompactionSem = sem + breakLoop = true + case <-timeoutSem: + t.Fatal("did not get blocked on an LBase compaction") + default: + require.NoError(t, d.Set([]byte("b"), []byte("foo"), nil)) + require.NoError(t, d.Set([]byte("g"), []byte("bar"), nil)) + require.NoError(t, d.Flush()) + time.Sleep(100 * time.Millisecond) + } + } + printLSM() + + // Do 4 ingests, one with the key cc, one with bb and cc, and two with just bb. + // The purpose of the sstable containing cc is to inflate the L0 sublevel + // count of the interval at cc, as that's where we want the intra-L0 compaction + // to be seeded. However we also need a file left of that interval to have + // the same (or higher) sublevel to trigger the bug in + // cockroachdb/cockroach#101896. That's why we ingest a file after it to + // "bridge" the bb/cc intervals, and then ingest a file at bb. These go + // into sublevels like this: + // + // bb + // bb + // bb-----cc + // cc + // + // Eventually, we'll drop an ingested file containing a range del starting at + // cc around here: + // + // bb + // bb cc---... + // bb-----cc + // cc + { + path := "ingest1.sst" + f, err := memFS.Create(path) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + require.NoError(t, w.Set([]byte("cc"), []byte("foo"))) + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{path})) + } + { + path := "ingest2.sst" + f, err := memFS.Create(path) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + require.NoError(t, w.Set([]byte("bb"), []byte("foo2"))) + require.NoError(t, w.Set([]byte("cc"), []byte("foo2"))) + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{path})) + } + { + path := "ingest3.sst" + f, err := memFS.Create(path) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + require.NoError(t, w.Set([]byte("bb"), []byte("foo3"))) + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{path})) + } + { + path := "ingest4.sst" + f, err := memFS.Create(path) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + require.NoError(t, w.Set([]byte("bb"), []byte("foo4"))) + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{path})) + } + + // We now have a base compaction blocked. Block a memtable flush to cause + // memtables to queue up. + // + // Memtable (stuck): + // + // b-----------------g + // + // Relevant L0 ssstables + // + // bb + // bb + // bb-----cc + // cc + blockedCompactionsMu.Lock() + nextBlockedCompaction = 2 + blockedCompactionsMu.Unlock() + t.Log("blocking a flush") + require.NoError(t, d.Set([]byte("b"), []byte("foo2"), nil)) + require.NoError(t, d.Set([]byte("g"), []byte("bar2"), nil)) + _, _ = d.AsyncFlush() + select { + case sem := <-nextSem: + flushSem = sem + case <-time.After(channelTimeout): + t.Fatal("did not get blocked on a flush") + } + // Add one memtable to flush queue, and finish it off. + // + // Memtables (stuck): + // + // b-----------------g (waiting to flush) + // b-----------------g (flushing, blocked) + // + // Relevant L0 ssstables + // + // bb + // bb + // bb-----cc + // cc + require.NoError(t, d.Set([]byte("b"), []byte("foo3"), nil)) + require.NoError(t, d.Set([]byte("g"), []byte("bar3"), nil)) + // note: this flush will wait for the earlier, blocked flush, but it closes + // off the memtable which is what we want. + _, _ = d.AsyncFlush() + + // Open a new mutable memtable. This gets us an earlier earlierUnflushedSeqNum + // than the ingest below it. + require.NoError(t, d.Set([]byte("c"), []byte("somethingbigishappening"), nil)) + // Block an intra-L0 compaction, as one might happen around this time. + blockedCompactionsMu.Lock() + nextBlockedCompaction = 1 + blockedCompactionsMu.Unlock() + t.Log("blocking an intra-L0 compaction") + // Ingest a file containing a cc-e rangedel. + // + // Memtables: + // + // c (mutable) + // b-----------------g (waiting to flush) + // b-----------------g (flushing, blocked) + // + // Relevant L0 ssstables + // + // bb + // bb cc-----e (just ingested) + // bb-----cc + // cc + { + path := "ingest5.sst" + f, err := memFS.Create(path) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + require.NoError(t, w.DeleteRange([]byte("cc"), []byte("e"))) + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{path})) + } + t.Log("main ingest complete") + printLSM() + t.Logf("%s", d.Metrics().String()) + + require.NoError(t, d.Set([]byte("d"), []byte("ThisShouldNotBeDeleted"), nil)) + + // Do another ingest with a seqnum newer than d. The purpose of this is to + // increase the LargestSeqNum of the intra-L0 compaction output *beyond* + // the flush that contains d=ThisShouldNotBeDeleted, therefore causing + // that point key to be deleted (in the buggy code). + // + // Memtables: + // + // c-----d (mutable) + // b-----------------g (waiting to flush) + // b-----------------g (flushing, blocked) + // + // Relevant L0 ssstables + // + // bb cc + // bb cc-----e (just ingested) + // bb-----cc + // cc + { + path := "ingest6.sst" + f, err := memFS.Create(path) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + require.NoError(t, w.Set([]byte("cc"), []byte("doesntmatter"))) + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{path})) + } + + // Unblock earlier flushes. We will first finish flushing the blocked + // memtable, and end up in this state: + // + // Memtables: + // + // c-----d (mutable) + // b-----------------g (waiting to flush) + // + // Relevant L0 ssstables + // + // b-------------------g (irrelevant, just flushed) + // bb cc (has LargestSeqNum > earliestUnflushedSeqNum) + // bb cc-----e (has a rangedel) + // bb-----cc + // cc + // + // Note that while b----g is relatively old (and so has a low LargestSeqNum), + // it bridges a bunch of intervals. Had we regenerated sublevels from scratch, + // it'd have gone below the cc-e sstable. But due to #101896, we just slapped + // it on top. Now, as long as our seed interval is the one at cc and our seed + // file is the just-flushed L0 sstable, we will go down and include anything + // in that interval even if it has a LargestSeqNum > earliestUnflushedSeqNum. + // + // All asterisked L0 sstables should now get picked in an intra-L0 compaction + // right after the flush finishes, that we then block: + // + // b-------------------g* + // bb* cc* + // bb* cc-----e* + // bb-----cc* + // cc* + t.Log("unblocking flush") + flushSem <- struct{}{} + printLSM() + + select { + case sem := <-nextSem: + intraL0Sem = sem + case <-time.After(channelTimeout): + t.Fatal("did not get blocked on an intra L0 compaction") + } + + // Ensure all memtables are flushed. This will mean d=ThisShouldNotBeDeleted + // will land in L0 and since that was the last key written to a memtable, + // and the ingestion at cc came after it, the output of the intra-L0 + // compaction will elevate the cc-e rangedel above it and delete it + // (if #101896 is not fixed). + ch, _ := d.AsyncFlush() + <-ch + + // Unblock earlier intra-L0 compaction. + t.Log("unblocking intraL0") + intraL0Sem <- struct{}{} + printLSM() + + // Try reading d a couple times. + for i := 0; i < 2; i++ { + val, closer, err := d.Get([]byte("d")) + require.NoError(t, err) + require.Equal(t, []byte("ThisShouldNotBeDeleted"), val) + if closer != nil { + closer.Close() + } + time.Sleep(100 * time.Millisecond) + } + + // Unblock everything. + baseCompactionSem <- struct{}{} +} + +func BenchmarkDelete(b *testing.B) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + const keyCount = 10000 + var keys [keyCount][]byte + for i := 0; i < keyCount; i++ { + keys[i] = []byte(strconv.Itoa(rng.Int())) + } + val := bytes.Repeat([]byte("x"), 10) + + benchmark := func(b *testing.B, useSingleDelete bool) { + d, err := Open( + "", + &Options{ + FS: vfs.NewMem(), + }) + if err != nil { + b.Fatal(err) + } + defer func() { + if err := d.Close(); err != nil { + b.Fatal(err) + } + }() + + b.StartTimer() + for _, key := range keys { + _ = d.Set(key, val, nil) + if useSingleDelete { + _ = d.SingleDelete(key, nil) + } else { + _ = d.Delete(key, nil) + } + } + // Manually flush as it is flushing/compaction where SingleDelete + // performance shows up. With SingleDelete, we can elide all of the + // SingleDelete and Set records. + if err := d.Flush(); err != nil { + b.Fatal(err) + } + b.StopTimer() + } + + b.Run("delete", func(b *testing.B) { + for i := 0; i < b.N; i++ { + benchmark(b, false) + } + }) + + b.Run("single-delete", func(b *testing.B) { + for i := 0; i < b.N; i++ { + benchmark(b, true) + } + }) +} + +func BenchmarkNewIterReadAmp(b *testing.B) { + for _, readAmp := range []int{10, 100, 1000} { + b.Run(strconv.Itoa(readAmp), func(b *testing.B) { + opts := &Options{ + FS: vfs.NewMem(), + L0StopWritesThreshold: 1000, + } + opts.DisableAutomaticCompactions = true + + d, err := Open("", opts) + require.NoError(b, err) + + for i := 0; i < readAmp; i++ { + require.NoError(b, d.Set([]byte("a"), []byte("b"), NoSync)) + require.NoError(b, d.Flush()) + } + + require.Equal(b, d.Metrics().ReadAmp(), readAmp) + + b.StopTimer() + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StartTimer() + iter, _ := d.NewIter(nil) + b.StopTimer() + require.NoError(b, iter.Close()) + } + + require.NoError(b, d.Close()) + }) + } +} + +func verifyGet(t *testing.T, r Reader, key, expected []byte) { + val, closer, err := r.Get(key) + require.NoError(t, err) + if !bytes.Equal(expected, val) { + t.Fatalf("expected %s, but got %s", expected, val) + } + closer.Close() +} + +func verifyGetNotFound(t *testing.T, r Reader, key []byte) { + val, _, err := r.Get(key) + if err != base.ErrNotFound { + t.Fatalf("expected nil, but got %s", val) + } +} + +func BenchmarkRotateMemtables(b *testing.B) { + o := &Options{FS: vfs.NewMem(), MemTableSize: 64 << 20 /* 64 MB */} + d, err := Open("", o) + require.NoError(b, err) + + // We want to jump to full-sized memtables. + d.mu.Lock() + d.mu.mem.nextSize = o.MemTableSize + d.mu.Unlock() + require.NoError(b, d.Flush()) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := d.Flush(); err != nil { + b.Fatal(err) + } + } +} diff --git a/pebble/docs/RFCS/20211018_range_keys.md b/pebble/docs/RFCS/20211018_range_keys.md new file mode 100644 index 0000000..890fa58 --- /dev/null +++ b/pebble/docs/RFCS/20211018_range_keys.md @@ -0,0 +1,961 @@ +- Feature Name: Range Keys +- Status: draft +- Start Date: 2021-10-18 +- Authors: Sumeer Bhola, Jackson Owens +- RFC PR: #1341 +- Pebble Issues: + https://github.com/cockroachdb/pebble/issues/1339 +- Cockroach Issues: + https://github.com/cockroachdb/cockroach/issues/70429 + https://github.com/cockroachdb/cockroach/issues/70412 + +** Design Draft** + +# Summary + +An ongoing effort within CockroachDB to preserve MVCC history across all SQL +operations (see cockroachdb/cockroach#69380) requires a more efficient method of +deleting ranges of MVCC history. + +This document describes an extension to Pebble introducing first-class support +for range keys. Range keys map a range of keyspace to a value. Optionally, the +key range may include an suffix encoding a version (eg, MVCC timestamp). Pebble +iterators may be configured to surface range keys during iteration, or to mask +point keys at lower MVCC timestamps covered by range keys. + +CockroachDB will make use of these range keys to enable history-preserving +removal of contiguous ranges of MVCC keys with constant writes, and efficient +iteration past deleted versions. + +# Background + +A previous CockroachDB RFC cockroach/cockroachdb#69380 describes the motivation +for the larger project of migrating MVCC-noncompliant operations into MVCC +compliance. Implemented with the existing MVCC primitives, some operations like +removal of an index or table would require performing writes linearly +proportional to the size of the table. Dropping a large table using existing +MVCC point-delete primitives would be prohibitively expensive. The desire for a +sublinear delete of an MVCC range motivates this work. + +The detailed design for MVCC compliant bulk operations ([high-level +description](https://github.com/cockroachdb/cockroach/blob/master/docs/RFCS/20210825_mvcc_bulk_ops.md); +detailed design draft for DeleteRange in internal +[doc](https://docs.google.com/document/d/1ItxpitNwuaEnwv95RJORLCGuOczuS2y_GoM2ckJCnFs/edit#heading=h.x6oktstoeb9t)), +ran into complexity by placing range operations above the Pebble layer, such +that Pebble sees these as points. The complexity causes are various: (a) which +key (start or end) to anchor this range on, when represented as a point (there +are performance consequences), (b) rewriting on CockroachDB range splits (and +concerns about rewrite volume), (c) fragmentation on writes and complexity +thereof (and performance concerns for reads when not fragmenting), (d) inability +to efficiently skip older MVCC versions that are masked by a `[k1,k2)@ts` (where +ts is the MVCC timestamp). + +Pebble currently has only one kind of key that is associated with a range: +`RANGEDEL [k1, k2)#seq`, where [k1, k2) is supplied by the caller, and is used +to efficiently remove a set of point keys. + +First-class support for range keys in Pebble eliminates all these issues. +Additionally, it allows for future extensions like efficient transactional range +operations. This issue describes how this feature would work from the +perspective of a user of Pebble (like CockroachDB), and sketches some +implementation details. + +# Design + +## Interface + +### New `Comparer` requirements + +The Pebble `Comparer` type allows users to optionally specify a `Split` function +that splits a user key into a prefix and a suffix. This Split allows users +implementing MVCC (Multi-Version Concurrency Control) to inform Pebble which +part of the key encodes the user key and which part of the key encodes the +version (eg, a timestamp). Pebble does not dictate the encoding of an MVCC +version, only that the version form a suffix on keys. + +The range keys design described in this RFC introduces stricter requirements for +user-provided `Split` implementations and the ordering of keys: + +1. The user key consisting of just a key prefix `k` must sort before all + other user keys containing that prefix. Specifically + `Compare(k[:Split(k)], k) < 0` where `Split(k) < len(k)`. +2. A key consisting of a bare suffix must be a valid key and comparable. The + ordering of the empty key prefix with any suffixes must be consistent with + the ordering of those same suffixes applied to any other key prefix. + Specifically `Compare(k[Split(k):], k2[Split(k2):]) == Compare(k, k2)` where + `Compare(k[:Split(k)], k2[:Split(k2)]) == 0`. + +The details of why these new requirements are necessary are explained in the +implementation section. + +### Writes + +This design introduces three new write operations: + +- `RangeKeySet([k1, k2), [optional suffix], )`: This represents the + mapping `[k1, k2)@suffix => value`. Keys `k1` and `k2` must not contain a + suffix (i.e., `Split(k1)==len(k1)` and `Split(k2)==len(k2))`. + +- `RangeKeyUnset([k1, k2), [optional suffix])`: This removes a mapping + previously applied by `RangeKeySet`. The unset may use a smaller key range + than the original `RangeKeySet`, in which case only part of the range is + deleted. The unset only applies to range keys with a matching optional suffix. + If the optional suffix is absent in both the RangeKeySet and RangeKeyUnset, + they are considered matching. + +- `RangeKeyDelete([k1, k2))`: This removes all range keys within the provided + key span. It behaves like an `Unset` unencumbered by suffix restrictions. + +For example, consider `RangeKeySet([a,d), foo)` (i.e., no suffix). If +there is a later call `RangeKeyUnset([b,c))`, the resulting state seen by +a reader is `[a,b) => foo`, `[c,d) => foo`. Note that the value is not +modified when the key is fragmented. + +Partially overlapping `RangeKeySet`s with the same suffix overwrite one +another. For example, consider `RangeKeySet([a,d), foo)`, followed by +`RangeKeySet([c,e), bar)`. The resulting state is `[a,c) => foo`, `[c,e) +=> bar`. + +Point keys (eg, traditional keys defined at a singular byte string key) and +range keys do not overwrite one another. They have a parallel existence. Point +deletes only apply to points. Range unsets only apply to range keys. However, +users may configure iterators to mask point keys covered by newer range keys. +This masking behavior is explicitly requested by the user in the context of the +iteration. Masking is described in more detail below. + +There exist separate range delete operations for point keys and range keys. A +`RangeKeyDelete` may remove part of a range key, just like the new +`RangeKeyUnset` operation introduced earlier. `RangeKeyDelete`s differ from +`RangeKeyUnset`s, because the latter requires that the suffix matches and +applies only to range keys. `RangeKeyDelete`s completely clear all existing +range keys within their span at all suffix values. + +The optional suffix in `RangeKeySet` and `RangeKeyUnset` operations is related +to the pebble `Comparer.Split` operation which is explicitly documented as being +for [MVCC +keys](https://github.com/cockroachdb/pebble/blob/e95e73745ce8a85d605ef311d29a6574db8ed3bf/internal/base/comparer.go#L69-L88), +without mandating exactly how the versions are represented. `RangeKeySet` and +`RangeKeyUnset` keys with different suffixes do not interact logically, although +Pebble will observably fragment ranges at intersection points. + +### Iteration + +A user iterating over a key interval [k1,k2) can request: + +- **[I1]** An iterator over only point keys. + +- **[I2]** A combined iterator over point and range keys. This is what + we mainly discuss below in the implementation discussion. + +- **[I3]** An iterator over only range keys. In the CockroachDB use + case, range keys will need to be subject to MVCC GC just like + point keys — this iterator may be useful for that purpose. + +The `pebble.Iterator` type will be extended to provide accessors for +range keys for use in the combined and exclusively range iteration +modes. + +``` +// HasPointAndRange indicates whether there exists a point key, a range key or +// both at the current iterator position. +HasPointAndRange() (hasPoint, hasRange bool) + +// RangeKeyChanged indicates whether the most recent iterator positioning +// operation resulted in the iterator stepping into or out of a new range key. +// If true previously returned range key bounds and data has been invalidated. +// If false, previously obtained range key bounds, suffix and value slices are +// still valid and may continue to be read. +RangeKeyChanged() bool + +// Key returns the key of the current key/value pair, or nil if done. If +// positioned at an iterator position that only holds a range key, Key() +// always returns the start bound of the range key. Otherwise, it returns +// the point key's key. +Key() []byte + +// RangeBounds returns the start (inclusive) and end (exclusive) bounds of the +// range key covering the current iterator position. RangeBounds returns nil +// bounds if there is no range key covering the current iterator position, or +// the iterator is not configured to surface range keys. +// +// If valid, the returned start bound is less than or equal to Key() and the +// returned end bound is greater than Key(). +RangeBounds() (start, end []byte) + +// Value returns the value of the current key/value pair, or nil if done. +// The caller should not modify the contents of the returned slice, and +// its contents may change on the next call to Next. +// +// Only valid if HasPointAndRange() returns true for hasPoint. +Value() []byte + +// RangeKeys returns the range key values and their suffixes covering the +// current iterator position. The range bounds may be retrieved separately +// through RangeBounds(). +RangeKeys() []RangeKey + +type RangeKey struct { + Suffix []byte + Value []byte +} +``` + +When a combined iterator exposes range keys, it exposes all the range +keys covering `Key`. During iteration with a combined iterator, an +iteration position may surface just a point key, just a range key or +both at the currently-positioned `Key`. + +Described another way, a Pebble combined iterator guarantees that it +will stop at all positions within the keyspace where: +1. There exists a point key at that position. +2. There exists a range key that logically begins at that postition. + +In addition to the above positions, a Pebble iterator may also stop at keys +in-between the above positions due to fragmentation. Range keys are defined over +continuous spans of keyspace. Range keys with different suffix values may +overlap each other arbitrarily. To surface these arbitrarily overlapping spans +in an understandable and efficient way, the Pebble iterator surfaces range keys +fragmented at intersection points. Consider the following sequence of writes: + +``` + RangeKeySet([a,z), @1, 'apple') + RangeKeySet([c,e), @3, 'banana') + RangeKeySet([e,m), @5, 'orange') + RangeKeySet([b,k), @7, 'kiwi') +``` + +This yields a database containing overlapping range keys: +``` + @7 → kiwi |-----------------) + @5 → orange |---------------) + @3 → banana |---) + @1 → apple |-------------------------------------------------) + a b c d e f g h i j k l m n o p q r s t u v w x y z +``` + +During iteration, these range keys are surfaced using the bounds of their +intersection points. For example, a scan across the keyspace containing only +these range keys would observe the following iterator positions: + +``` + Key() = a RangeKeyBounds() = [a,b) RangeKeys() = {(@1,apple)} + Key() = b RangeKeyBounds() = [b,c) RangeKeys() = {(@7,kiwi), (@1,apple)} + Key() = c RangeKeyBounds() = [c,e) RangeKeys() = {(@7,kiwi), (@3,banana), (@1,apple)} + Key() = e RangeKeyBounds() = [e,k) RangeKeys() = {(@7,kiwi), (@5,orange), (@1,apple)} + Key() = k RangeKeyBounds() = [k,m) RangeKeys() = {(@5,orange), (@1,apple)} + Key() = m RangeKeyBounds() = [m,z) RangeKeys() = {(@1,apple)} +``` + +This fragmentation produces a more understandable interface, and avoids forcing +iterators to read all range keys within the bounds of the broadest range key. +Consider this example: + +``` + iterator pos [ ] - sstable bounds + | +L1: [a----v1@t2--|-h] [l-----unset@t1----u] +L2: [e---|------v1@t1----------r] + a b c d e f g h i j k l m n o p q r s t u v w x y z +``` + +If the iterator is positioned at a point key `g`, there are two overlapping +physical range keys: `[a,h)@t2→v1` and `[e,r)@t1→v1`. + +However, the `RangeKeyUnset([l,u), @t1)` removes part of the `[e,r)@t1→v1` range +key, truncating it to the bounds `[e,l)`. The iterator must return the truncated +bounds that correctly respect the `RangeKeyUnset`. However, when the range keys +are stored within a log-structured merge tree like Pebble, the `RangeKeyUnset` +may not be contained within the level's sstable that overlaps the current point +key. Searching for the unset could require reading an unbounded number of +sstables, losing the log-structured merge tree's property that bounds read +amplification to the number of levels in the tree. + +Fragmenting range keys to intersection points avoids this problem. The iterator +positioned at `g` only surfaces range key state with the bounds `[e,h)`, the +widest bounds in which it can guarantee t2→v1 and t1→v1 without loading +additional sstables. + +#### Iteration order + +Recall that the user-provided `Comparer.Split(k)` function divides all user keys +into a prefix and a suffix, such that the prefix is `k[:Split(k)]`, and the +suffix is `k[Split(k):]`. If a key does not contain a suffix, the key equals the +prefix. + +An iterator that is configured to surface range keys alongside point keys will +surface all range keys covering the current `Key()` position. Revisiting an +earlier example with the addition of three new point key-value pairs: +a→artichoke, b@2→beet and t@3→turnip. Consider '@' to form the suffix +where present, with `` denoting a MVCC timestamp. Higher, more-recent +timestamps sort before lower, older timestamps. + +``` + . a → artichoke + @7 → kiwi |-----------------) + @5 → orange |---------------) + . b@2 b@2 → beet + @3 → banana |---) . t@3 t@3 → turnip + @1 → apple |-------------------------------------------------) + a b c d e f g h i j k l m n o p q r s t u v w x y z +``` + +An iterator configured to surface both point and range keys will visit the +following iterator positions during forward iteration: + +``` + Key() HasPointAndRange() Value() RangeKeyBounds() RangeKeys() + a (true, true) artichoke [a,b) {(@1,apple)} + b (false, true) - [b,c) {(@7,kiwi), (@1,apple)} + b@2 (true, true) beet [b,c) {(@7,kiwi), (@1,apple)} + c (false, true) - [c,e) {(@7,kiwi), (@3,banana), (@1,apple)} + e (false, true) - [e,k) {(@7,kiwi), (@5,orange), (@1,apple)} + k (false, true) - [k,m) {(@5,orange), (@1,apple)} + m (false, true) - [m,z) {(@1,apple)} + t@3 (true, true) turnip [m,z) {(@1,apple)} +``` + +Note that: + +- While positioned over a point key (eg, Key() = 'a', 'b@2' or t@3'), the + iterator exposes both the point key's value through Value() and the + overlapping range keys values through `RangeKeys()`. + +- There can be multiple range keys covering a `Key()`, each with a different + suffix. + +- There cannot be multiple range keys covering a `Key()` with the same suffix, + since the most-recently committed one (eg, the one with the highest sequence + number) will win, just like for point keys. + +- If the iterator has configured lower and/or upper bounds, they will truncate + the range key to those bounds. For example, if the above iterator had an upper + bound 'y', the `[m,z)` range key would be surfaced with the bounds `[m,y)` + instead. + +#### Masking + +Range key masking provides additional, optional functionality designed +specifically for the use case of implementing a MVCC-compatible delete range. + +When constructing an iterator that iterators over both point and range keys, a +user may request that range keys mask point keys. Masking is configured with a +suffix parameter that determines which range keys may mask point keys. Only +range keys with suffixes that sort after the mask's suffix mask point keys. A +range key that meets this condition only masks points with suffixes that sort +after the range key's suffix. + +``` +type IterOptions struct { + // ... + RangeKeyMasking RangeKeyMasking +} + +// 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 masking range key's bounds and have suffixes greater than the range key's +// suffix are automatically skipped. +// +// Specifically, when configured with a RangeKeyMasking.Suffix _s_, and there +// exists a range key with suffix _r_ covering a point key with suffix _p_, and +// +// _s_ ≤ _r_ < _p_ +// +// then the point key is elided. +// +// Range-key masking may only be used when iterating over both point keys and +// range keys. +type RangeKeyMasking struct { + // Suffix configures which range keys may mask point keys. Only range keys + // that are defined at suffixes greater than or equal to Suffix will mask + // point keys. + Suffix []byte + // Filter is an optional field that may be used to improve performance of + // range-key masking through a block-property filter defined over key + // suffixes. If non-nil, Filter is called by Pebble to construct a + // block-property filter mask at iterator creation. The filter is used to + // skip whole point-key blocks containing point keys with suffixes greater + // than a covering range-key's suffix. + // + // To use this functionality, the caller must create and configure (through + // Options.BlockPropertyCollectors) a block-property collector that records + // the maxmimum suffix contained within a block. The caller then must write + // and provide a BlockPropertyFilterMask implementation on that same + // property. See the BlockPropertyFilterMask type for more information. + Filter func() BlockPropertyFilterMask +} +``` + +Example: A user may construct an iterator with `RangeKeyMasking.Suffix` set to +`@50`. The range key `[a, c)@60` would mask nothing, because `@60` is a more +recent timestamp than `@50`. However a range key `[a,c)@30` would mask `a@20` +and `apple@10` but not `apple@40`. A range key can only mask keys with MVCC +timestamps older than the range key's own timestamp. Only range keys with +suffixes (eg, MVCC timestamps) may mask anything at all. + +The pebble Iterator surfaces all range keys when masking is enabled. Only point +keys are ever skipped, and only when they are contained within the bounds of a +range key with a more-recent suffix, and the range key's suffix is older than +the timestamp encoded in `RangeKeyMasking.Sufffix`. + +## Implementation + +### Write operations + +This design introduces three new Pebble write operations: `RangeKeySet`, +`RangeKeyUnset` and `RangeKeyDelete`. Internally, these operations are +represented as internal keys with new corresponding key kinds encoded as a part +of the key trailer. These keys are stored within special range key blocks +separate from point keys, but within the same sstable. The range key blocks hold +`RangeKeySet`, `RangeKeyUnset` and `RangeKeyDelete` keys, but do not hold keys +of any other kind. Within the memtables, these range keys are stored in a +separate skip list. + +- `RangeKeySet([k1,k2), @suffix, value)` is encoded as a `k1.RANGEKEYSET` key + with a value encoding the tuple `(k2,@suffix,value)`. +- `RangeKeyUnset([k1,k2), @suffix)` is encoded as a `k1.RANGEUNSET` key + with a value encoding the tuple `(k2,@suffix)`. +- `RangeKeyDelete([k1,k2)` is encoded as a `k1.RANGEKEYDELETE` key with a value + encoding `k2`. + +Range keys are physically fragmented as an artifact of the log-structured merge +tree structure and internal sstable boundaries. This fragmentation is essential +for preserving the performance characteristics of a log-structured merge tree. +Although the public interface operations for `RangeKeySet` and `RangeKeyUnset` +require both boundary keys `[k1,k2)` to always be bare prefixes (eg, to not have +a suffix), internally these keys may be fragmented to bounds containing +suffixes. + +Example: If a user attempts to write `RangeKeySet([a@v1, c@v2), @v3, value)`, +Pebble will return an error to the user. If a user writes `RangeKeySet([a, c), +@v3, value)`, Pebble will allow the write and may later internally fragment the +`RangeKeySet` into three internal keys: + - `RangeKeySet([a, a@v1), @v3, value)` + - `RangeKeySet([a@v1, c@v2), @v3, value)` + - `RangeKeySet([c@v2, c), @v3, value)` + +This fragmentation preserve log-structured merge tree performance +characteristics because it allows a range key to be split across many sstables, +while preserving locality between range keys and point keys. Consider a +`RangeKeySet([a,z), @1, foo)` on a database that contains millions of point keys +in the range [a,z). If the [a,z) range key was not permitted to be fragmented +internally, it would either need to be stored completely separately from the +point keys in a separate sstable or in a single intractably large sstable +containing all the overlapping point keys. Fragmentation allows locality, +ensuring point keys and range keys in the same region of the keyspace can be +stored in the same sstable. + +`RangeKeySet`, `RangeKeyUnset` and `RangeKeyDelete` keys are assigned sequence +numbers, like other internal keys. Log-structured merge tree level invariants +are valid across range key, point keys and between the two. That is: + + 1. The point key `k1#s2` cannot be at a lower level than `k2#s1` where + `k1==k2` and `s1 < s2`. This is the invariant implemented by all LSMs. + 2. `RangeKeySet([k1,k2))#s2` cannot be at a lower level than + `RangeKeySet([k3,k4))#s1` where `[k1,k2)` overlaps `[k3,k4)` and `s1 < s2`. + 3. `RangeKeySet([k1,k2))#s2` cannot be at a lower level than a point key + `k3#s1` where `k3 \in [k1,k2)` and `s1 < s2`. + +Like other tombstones, the `RangeKeyUnset` and `RangeKeyDelete` keys are elided +when they fall to the bottomost level of the LSM and there is no snapshot +preventing its elision. There is no additional garbage collection problem +introduced by these keys. + +There is no Merge operation that affects range keys. + +#### Physical representation + +`RangeKeySet`, `RangeKeyUnset` and `RangeKeyDelete` keys are keyed by their +start key. This poses an obstacle. We must be able to support multiple range +keys at the same sequence number, because all keys within an ingested sstable +adopt the same sequence number. Duplicate internal keys (keys with equal user +keys, sequence numbers and kinds) are prohibited within Pebble. To resolve this +issue, fragments with the same bounds are merged within snapshot stripes into a +single physical key-value, representing multiple logical key-value pairs: + +``` +k1.RangeKeySet#s2 → (k2,[(@t2,v2),(@t1,v1)]) +``` + +Within a physical key-value pair, suffix-value pairs are stored sorted by +suffix, descending. This has a minor advantage of reducing iteration-time +user-key comparisons when there exist multiple range keys in a table. + +Unlike other Pebble keys, the `RangeKeySet` and `RangeKeyUnset` keys have values +that encode fields of data known to Pebble. The value that the user sets in a +call to `RangeKeySet` is opaque to Pebble, but the physical representation of +the `RangeKeySet`'s value is known. This encoding is a sequence of fields: + +* End key, `varstring`, encodes the end user key of the fragment. +* A series of (suffix, value) tuples representing the logical range keys that + were merged into this one physical `RangeKeySet` key: + * Suffix, `varstring` + * Value, `varstring` + +Similarly, `RangeKeyUnset` keys are merged within snapshot stripes and have a +physical representation like: + +``` +k1.RangeKeyUnset#s2 → (k2,[(@t2),(@t1)]) +``` + +A `RangeKeyUnset` key's value is encoded as: +* End key, `varstring`, encodes the end user key of the fragment. +* A series of suffix `varstring`s. + +When `RangeKeySet` and `RangeKeyUnset` fragments with identical bounds meet +within the same snapshot stripe within a compaction, any of the +`RangeKeyUnset`'s suffixes that exist within the `RangeKeySet` key are removed. + +A `RangeKeyDelete` key has no additional data beyond its end key, which is +encoded directly in the value. + +NB: `RangeKeySet` and `RangeKeyUnset` keys are not merged within batches or the +memtable. That's okay, because batches are append-only and indexed batches will +refragment and merge the range keys on-demand. In the memtable, every key is +guaranteed to have a unique sequence number. + +### Sequence numbers + +Like all Pebble keys, `RangeKeySet`, `RangeKeyUnset` and `RangeKeyDelete` are +assigned sequence numbers when committed. As described above, overlapping +`RangeKeySet`s and `RangeKeyUnset`s are fragmented to have matching start and +end bounds. Then the resulting exactly-overlapping range key fragments are +merged into a single internal key-value pair, within the same snapshot stripe +and sstable. The original, unmerged internal keys each have their own sequence +numbers, indicating the moment they were committed within the history of all +write operations. + +Recall that sequence numbers are used within Pebble to determine which keys +appear live to which iterators. When an iterator is constructed, it takes note +of the current _visible sequence number_, and for the lifetime of the iterator, +only surfaces keys less than that sequence number. Similarly, snapshots read the +current _visible sequence number_, remember it, but also leave a note asking +compactions to preserve history at that sequence number. The space between +snapshotted sequence numbers is referred to as a _snapshot stripe_, and +operations cannot drop or otherwise mutate keys unless they fall within the same +_snapshot stripe_. For example a `k.MERGE#5` key may not be merged with a +`k.MERGE#1` operation if there's an open snapshot at `#3`. + +The new `RangeKeySet`, `RangeKeyUnset` and `RangeKeyDelete` keys behave +similarly. Overlapping range keys won't be merged if there's an open snapshot +separating them. Consider a range key `a-z` written at sequence number `#1` and +a point key `d.SET#2`. A combined point-and-range iterator using a sequence +number `#3` and positioned at `d` will surface both the range key `a-z` and the +point key `d`. + +In the context of masking, the suffix-based masking of range keys can cause +potentially unexpected behavior. A range key `[a,z)@10` may be committed as +sequence number `#1`. Afterwards, a point key `d@5#2` may be committed. An +iterator that is configured with range-key masking with suffix `@20` would mask +the point key `d@5#2` because although `d@5#2`'s sequence number is higher, +range-key masking uses suffixes to impose order, not sequence numbers. + +### Boundaries for sstables + +Range keys follow the same relationship to sstable bounadries as the existing +`RANGEDEL` tombstones. The bounds of an internal range key are user keys. Every +range key is limited by its containing sstable's bounds. + +Consider these keys, annotated with sequence numbers: + +``` +Point keys: a#50, b#70, b#49, b#48, c#47, d#46, e#45, f#44 +Range key: [a,e)#60 +``` + +We have created three versions of `b` in this example. In previous versions, +Pebble could split output sstables during a compaction such that the different +`b` versions span more than one sstable. This creates problems for `RANGEDEL`s +which span these two sstables which are discussed in the section on [improperly +truncated RANGEDELS](https://github.com/cockroachdb/pebble/blob/master/docs/range_deletions.md#improperly-truncated-range-deletes). +We manage to tolerate this for `RANGEDEL`s since their semantics are defined by +the system, which is not true for these range keys where the actual semantics +are up to the user. + +Pebble now disallows such sstable split points. In this example, by postponing +the sstable split point to the user key c, we can cleanly split the range key +into `[a,c)#60` and `[c,e)#60`. The sstable end bound for the first sstable +(sstable bounds are inclusive) will be c#inf (where inf is the largest possible +seqnum, which is unused except for these cases), and sstable start bound for the +second sstable will be c#60. + +The above example deals exclusively with point and range keys without suffixes. +Consider this example with suffixed keys, and compaction outputs split in the +middle of the `b` prefix: + +``` +first sstable: points: a@100, a@30, b@100, b@40 ranges: [a,c)@50 +second sstable: points: b@30, c@40, d@40, e@30, ranges: [c,e)@50 +``` + +When the compaction code decides to defer `b@30` to the next sstable and finish +the first sstable, the range key `[a,c)@50` is sitting in the fragmenter. The +compaction must split the range key at the bounds determined by the user key. +The compaction uses the first point key of the next sstable, in this case +`b@30`, to truncate the range key. The compaction flushes the fragment +`[a,b@30)@50` to the first sstable and updates the existing fragment to begin at +`b@30`. + +If a range key extends into the next file, the range key's truncated end is used +for the purposes of determining the sstable end boundary. The first sstable's +end boundary becomes `b@30#inf`, signifying the range key does not cover `b@30`. +The second sstable's start boundary is `b@30`. + +### Block property collectors + +Separate block property collectors may be configured to collect separate +properties about range keys. This is necessary for CockroachDB's MVCC block +property collectors to ensure the sstable-level properties are correct. + +### Iteration + +This design extends the `*pebble.Iterator` with the ability to iterate over +exclusively range keys, range keys and point keys together or exclusively point +keys (the previous behavior). + +- Pebble already requires that the prefix `k` follows the same key validity + rules as `k@suffix`. + +- Previously, Pebble did not require that a user key consisting of just a prefix + `k` sort before the same prefix with a non-empty suffix. CockroachDB has + adopted this behavior since it results in the following clean behavior: + `RANGEDEL` over [k1, k2) deletes all versioned keys which have prefixes in the + interval [k1, k2). Pebble will now require this behavior for all users using + MVCC keys. Specifically, it must hold that `Compare(k[:Split(k)], k) < 0` if + `Split(k) < len(k)`. + +# TKTK: Discuss merging iterator + +#### Determinism + +Range keys will be split based on boundaries of sstables in an LSM. Users of an +LSM typically expect that two different LSMs with different sstable settings +that receive the same writes should output the same key-value pairs when +iterating. To provide this behavior, the iterator implementation may be +configured to defragment range keys during iteration time. The defragmentation +behavior would be: + +- Two visible ranges `[k1,k2)@suffix1=>val1`, `[k2,k3)@suffix2=>val2` are + defragmented if suffix1==suffix2 and val1==val2, and become [k1,k3). + +- Defragmentation during user iteration does not consider the sequence number. + This is necessary since LSM state can be exported to another LSM via the use + of sstable ingestion, which can collapse different seqnums to the same seqnum. + We would like both LSMs to look identical to the user when iterating. + +The above defragmentation is conceptually simple, but hard to implement +efficiently, since it requires stepping ahead from the current position to +defragment range keys. This stepping ahead could switch sstables while there are +still points to be consumed in a previous sstable. This determinism is useful +for testing and verification purposes: + +- Randomized and metamorphic testing is used extensively to reliably test + software including Pebble and CockroachDB. Defragmentation provides + the determinism necessary for this form of testing. + +- CockroachDB's replica divergence detector requires a consistent view of the + database on each replica. + +In order to provide determinism, Pebble constructs an internal range key +iterator stack that's separate from the point iterator stack, even when +performing combined iteration over both range and point keys. The separate range +key iterator allows the internal range key iterator to move independently of the +point key iterator. This allows the range key iterator to independently visit +adjacent sstables in order to defragment their range keys if necessary, without +repositioning the point iterator. + +Two spans [k1,k2) and [k3, k4) of range keys are defragmented if their bounds +abut and their user observable-state is identical. That is, `k2==k3` and each +spans' contains exactly the same set of range key (, ) pairs. In +order to support `RangeKeyUnset` and `RangeKeyDelete`, defragmentation must be +applied _after_ resolving unset and deletes. + +#### Merging iteration + +Recall that range keys are stored in the same sstables as point keys. In a +log-structured merge tree, these sstables are distributed across levels. Within +a level, sstables are non-overlapping but between levels sstables may overlap +arbitrarily. During iteration, keys across levels must be merged together. For +point keys, this is typically done with a heap. + +Range keys too must be merged across levels, and the earlier described +fragmentation at intersection boundaries must be applied. To implement this, a +range key merging iterator is defined. + +A merging iterator is initialized with an arbitrary number of child iterators +over fragmented spans. Each child iterator exposes fragmented range keys, such +that overlapping range keys are surfaced in a single span with a single set of +bounds. Range keys from one child iterator may overlap key spans from another +child iterator arbitrarily. The high-level algorithm is: + +1. Initialize a heap with bound keys from child iterators' range keys. +2. Find the next [or previous, if in reverse] two unique user keys' from bounds. +3. Consider the span formed between the two unique user keys a candidate span. +4. Determine if any of the child iterators' spans overlap the candidate span. + 4a. If any of the child iterator's current bounds are end keys (during + forward iteration) or start keys (during reverse iteration), then all the + spans with that bound overlap the candidate span. + 4b. If no spans overlap, forget the smallest (forward iteration) or largest + (reverse iteration) unique user key and advance the iterators to the next + unique user key. Start again from 3. + +Consider the example: + +``` + i0: b---d e-----h + i1: a---c h-----k + i2: a------------------------------p + +fragments: a-b-c-d-e-----h-----k----------p +``` + +None of the individual child iterators contain a span with the exact bounds +[c,d), but the merging iterator must produce a span [c,d). To accomplish this, +the merging iterator visits every span between unique boundary user keys. In the +above example, this is: + +``` +[a,b), [b,c), [c,d), [d,e), [e, h), [h, k), [k, p) +``` + +The merging iterator first initializes the heap to prepare for iteration. The +description below discusses the mechanics of forward iteration after a call to +First, but the mechanics are similar for reverse iteration and other positioning +methods. + +During a call to First, the heap is initialized by seeking every level to the +first bound of the first fragment. In the above example, this seeks the child +iterators to: + +``` +i0: (b, boundKindStart, [ [b,d) ]) +i1: (a, boundKindStart, [ [a,c) ]) +i2: (a, boundKindStart, [ [a,p) ]) +``` + +After fixing up the heap, the root of the heap is the bound with the smallest +user key ('a' in the example). During forward iteration, the root of the heap's +user key is the start key of next merged span. The merging iterator records this +key as the start key. The heap may contain other levels with range keys that +also have the same user key as a bound of a range key, so the merging iterator +pulls from the heap until it finds the first bound greater than the recorded +start key. + +In the above example, this results in the bounds `[a,b)` and child iterators in +the following positions: + +``` +i0: (b, boundKindStart, [ [b,d) ]) +i1: (c, boundKindEnd, [ [a,c) ]) +i2: (p, boundKindEnd, [ [a,p) ]) +``` + +With the user key bounds of the next merged span established, the merging +iterator must determine which, if any, of the range keys overlap the span. +During forward iteration any child iterator that is now positioned at an end +boundary has an overlapping span. (Justification: The child iterator's end +boundary is ≥ the new end bound. The child iterator's range key's corresponding +start boundary must be ≤ the new start bound since there were no other user keys +between the new span's bounds. So the fragments associated with the iterator's +current end boundary have start and end bounds such that start ≤ < ≤ end). + +The merging iterator iterates over the levels, collecting keys from any child +iterators positioned at end boundaries. In the above example, i1 and i2 are +positioned at end boundaries, so the merging iterator collects the keys of [a,c) +and [a,p). These spans contain the merging iterator's [a,b) span, but they may +also extend beyond the new span's start and end. The merging iterator returns +the keys with the new start and end bounds, preserving the underlying keys' +sequence numbers, key kinds and values. + +It may be the case that the merging iterator finds no levels positioned at span +end boundaries in which case the span overlaps with nothing. In this case the +merging iterator loops, repeating the above process again until it finds a span +that does contain keys. + +#### Efficient masking + +Recollect that in the earlier example from the iteration interface, during +forward iteration an iterator would output the following keys: + +``` + Key() HasPointAndRange() Value() RangeKeyBounds() RangeKeys() + a (true, true) artichoke [a,b) {(@1,apple)} + b (false, true) - [b,c) {(@7,kiwi), (@1,apple)} + b@2 (true, true) beet [b,c) {(@7,kiwi), (@1,apple)} + c (false, true) - [c,e) {(@7,kiwi), (@3,banana), (@1,apple)} + e (false, true) - [e,k) {(@7,kiwi), (@5,orange), (@1,apple)} + k (false, true) - [k,m) {(@5,orange), (@1,apple)} + m (false, true) - [m,z) {(@1,apple)} + t@3 (true, true) turnip [m,z) {(@1,apple)} +``` + +When implementing an MVCC "soft delete range" operation using range keys, the +range key `[b,k)@7→kiwi` may represent that all keys within the range [b,k) are +deleted at MVCC timestamp @7. During iteration, it would be desirable if the +caller could indicate that it does not want to observe any "soft deleted" point +keys, and the iterator can safely skip them. Note that in a MVCC system, whether +or not a key is soft deleted depends on the timestamp at which the database is +read. + +This is implemented through "range key masking," where a range key may act as a +mask, hiding point keys with MVCC timestamps beneath the range key. This +iterator option requires that the client configure the iterator with a MVCC +timestamp `suffix` representing the timestamp at which history should be read. +All range keys with suffixes (MVCC timestamps) less than or equal to the +configured suffix serve as masks. All point keys with suffixes (MVCC timestamps) +less than a covering, masking range key's suffix are hidden. + +Specifically, when configured with a RangeKeyMasking.Suffix _s_, and there +exists a range key with suffix _r_ covering a point key with suffix _p_, and _s_ +≤ _r_ < _p_ then the point key is elided. + +In the above example, if `RangeKeyMasking.Suffix` is set to `@7`, every range +key serves as a mask and the point key `b@2` is hidden during iteration because +it's contained within the masking `[b,k)@7→kiwi` range key. Note that `t@3` +would _not_ be masked, because its timestamp `@3` is more recent than the only +range key that covers it (`[a,z)@1→apple`). + +If `RangeKeyMasking.Suffix` were set to `@6` (a historical, point-in-time read), +the `[b,k)@7→kiwi` range key would no longer serve as a mask, and `b@2` would be +visible. + +To efficiently implement masking, we cannot rely on the LSM invariant since +`b@100` can be at a lower level than `[a,e)@50`. Instead, we build on +block-property filters, supporting special use of a MVCC timestamp block +property in order to skip blocks wholly containing point keys that are masked by +a range key. The client may configure a block-property collector to record the +highest MVCC timestamps of point keys within blocks. + +During read time, when positioned within a range key with a suffix ≤ +`RangeKeyMasking.Suffix`, the iterator configures sstable readers to use a +block-property filter to skip any blocks for which the highest MVCC timestamp is +less than the provided suffix. Additionally, these iterators must consult index +block bounds to ensure the block-property filter is not applied beyond the +bounds of the masking range key. + +### CockroachDB use + +CockroachDB initially will only use range keys to represent MVCC range +tombstones. See the MVCC range tombstones tech note for more details: + +https://github.com/cockroachdb/cockroach/blob/master/docs/tech-notes/mvcc-range-tombstones.md + +### Alternatives + +#### A1. Automatic elision of range keys that don't cover keys + +We could decide that range keys: + +- Don't contribute to `MVCCStats` themselves. +- May be elided by Pebble when they cover zero point keys. + +This means that CockroachDB garbage collection does not need to explicitly +remove the range keys, only the point keys they deleted. This option is clean +when paired with `RANGEDEL`s dropping both point and range keys. CockroachDB can +issue `RANGEDEL`s whenever it wants to drop a contiguous swath of points, and +not worry about the fact that it might also need to update the MVCC stats for +overlapping range keys. + +However, this option makes deterministic iteration over defragmented range keys +for replica divergence detection challenging, because internal fragmentation may +elide regions of a range key at any point. Producing a normalized form would +require storing state in the value (ie, the original start key) and +recalculating the smallest and largest extant covered point keys within the +range key and replica bounds. This would require maintaining _O_(range-keys) +state during the `storage.ComputeStatsForRange` pass over a replica's combined +point and range iterator. + +This likely forces replica divergence detection to use other means (eg, altering +the checksum of covered points) to incorporate MVCC range tombstone state. + +This option is also highly tailored to the MVCC Delete Range use case. Other +range key usages, like ranged intents, would not want this behavior, so we don't +consider it further. + +#### A2. Separate LSM of range keys + +There are two viable options for where to store range keys. They may be encoded +within the same sstables as points in separate blocks, or in separate sstables +forming a parallel range-key LSM. We examine the tradeoffs between storing range +keys in the same sstable in different blocks ("shared sstables") or separate +sstables forming a parallel LSM ("separate sstables"): + +- Storing range keys in separate sstables is possible because the only + iteractions between range keys and point keys happens at a global level. + Masking is defined over suffixes. It may be extended to be defined over + sequence numbers too (see 'Sequence numbers' section below), but that is + optional. Unlike range deletion tombstones, range keys have no effect on point + keys during compactions. + +- With separate sstables, reads may need to open additional sstable(s) and read + additional blocks. The number of additional sstables is the number of nonempty + levels in the range-key LSM, so it grows logarithmically with the number of + range keys. For each sstable, a read must read the index block and a data + block. + +- With our expectation of few range keys, the range-key LSM is expected to be + small, with one or two levels. Heuristics around sstable boundaries may + prevent unnecessary range-key reads when there is no covering range key. Range + key sstables and blocks are expected to have much higher table and block cache + hit rates, since they are orders of magnitude less dense. Reads in any + overlapping point sstables all access the same range key sstables. + +- With shared sstables, `SeekPrefixGE` cannot use bloom filters to entirely + eliminate sstables that contain range keys. Pebble does not always use bloom + filters in L6, so once a range key is compacted into L6 its impact to + `SeekPrefixGE` is lessened. With separate sstables, `SeekPrefixGE` can always + use bloom filters for point-key sstables. If there are any overlapping + range-key sstables, the read must read them. + +- With shared sstables, range keys create dense sstable boundaries. A range key + spanning an sstable boundary leaves no gap between the sstables' bounds. This + can force ingested sstables into higher levels of the LSM, even if the + sstables' point key spans don't overlap. This problem was previously observed + with wide `RANGEDEL` tombstones and was mitigated by prioritizing compaction + of sstables that contain `RANGEDEL` keys. We could do the same with range + keys, but the write amplification is expected to be much worse. The `RANGEDEL` + tombstones drop keys and eventually are dropped themselves as long as there is + not an open snapshot. Range keys do not drop data and are expected to persist + in L6 for long durations, always requiring ingested sstables to be inserted + into L5 or above. + +- With separate sstables, compaction logic is separate, which helps avoid + complexity of tricky sstable boundary conditions. Because there are expected + to be an order of magnitude fewer range keys, we could impose the constraint + that a prefix cannot be split across multiple range key sstables. The + simplified compaction logic comes at the cost of higher levels, iterators, etc + all needing to deal with the concept of two parallel LSMs. + +- With shared sstables, the LSM invariant is maintained between range keys and + point keys. For example, if the point key `b@20` is committed, and + subsequently a range key `RangeKey([a,c), @25, ...)` is committed, the range + key will never fall below the covered point `b@20` within the LSM. + +We decide to share sstables, because preserving the LSM invariant between range +keys and point keys is expected to be useful in the long-term. + +#### A3. Sequence number masking + +In the CockroachDB MVCC range tombstone use case, a point key should never be +written below an existing range key with a higher timestamp. The MVCC range +tombstone use case would allow us to dictate that an overlapping range key with +a higher sequence number always masks range keys with lower sequence numbers. +Adding this additional masking scope would avoid the comparatively costly suffix +comparison when a point key _is_ masked by a range key. We need to consider how +sequence number masking might be affected by the merging of range keys within +snapshot stripes. + +Consider the committing of range key `[a,z)@{t1}#10`, followed by point keys +`d@t2#11` and `m@t2#11`, followed by range key `[j,z)@{t3}#12`. This sequencing +respects the expected timestamp, sequence number relationship in CockroachDB's +use case. If all keys are flushed within the same sstable, fragmentation and +merging overlapping fragments yields range keys `[a,j)@{t1}#10`, +`[j,z)@{t3,t1}#12`. The key `d@t2#11` must not be masked because it's not +covered by the new range key, and indeed that's the case because the covering +range key's fragment is unchanged `[a,j)@{t1}#10`. + +For now we defer this optimization, with the expectation that we may not be able +to preserve this relationship between sequence numbers and suffixes in all range +key use cases. diff --git a/pebble/docs/RFCS/20220112_pebble_sstable_format_versions.md b/pebble/docs/RFCS/20220112_pebble_sstable_format_versions.md new file mode 100644 index 0000000..c2f792f --- /dev/null +++ b/pebble/docs/RFCS/20220112_pebble_sstable_format_versions.md @@ -0,0 +1,290 @@ +- Feature Name: Pebble SSTable Format Versions +- Status: completed +- Start Date: 2022-01-12 +- Authors: Nick Travers +- RFC PR: https://github.com/cockroachdb/pebble/pull/1450 +- Pebble Issues: + https://github.com/cockroachdb/pebble/issues/1409 + https://github.com/cockroachdb/pebble/issues/1339 +- Cockroach Issues: + +# Summary + +To safely support changes to the SSTable structure, a new versioning scheme +under a Pebble magic number is proposed. + +This RFC also outlines the relationship between the SSTable format version and +the existing Pebble format major version, in addition to how the two are to +be used in Cockroach for safely enabling new table format versions. + +# Motivation + +Pebble currently uses a "format major version" scheme for the store (or DB) +that indicates which Pebble features should be enabled when the store is first +opened, before any SSTables are opened. The versions indicate points of +backwards incompatibility for a store. For example, the introduction of the +`SetWithDelete` key kind is gated behind a version, as is block property +collection. This format major version scheme was introduced in +[#1227](https://github.com/cockroachdb/pebble/issues/1227). + +While Pebble can use the format major version to infer how to load and +interpret data in the LSM, the SSTables that make up the store itself have +their own notion of a "version". This "SSTable version" (also referred to as a +"table format") is written to the footer (or trailing section) of each SSTable +file and determines how the file is to be interpreted by Pebble. As of the time +of writing, Pebble supports two table formats - LevelDB's format, and RocksDB's +v2 format. Pebble inherited the latter as the default table format as it was +the version that RocksDB used at the time Pebble was being developed, and +remained the default to allow for a simpler migration path from Cockroach +clusters that were originally using RocksDB as the storage engine. The +RocksDBv2 table format adds various features on top of the LevelDB format, +including a two-level index, configurable checksum algorithms, and an explicit +versioning scheme to allow for the introduction of changes, amongst other +features. + +While the RocksDBv2 SSTable format has been sufficient for Pebble's needs since +inception, new Pebble features and potential backports from RocksDB itself +require that the SSTable format evolve over time and therefore that the table +format be updated. As the majority of new features added over time will be +specific to Pebble, it does not make sense to repurpose the RocksDB format +versions that exist upstream for use with Pebble features (at the time of +writing, RocksDB had added versions 3 and 4 on top of the version 2 in use by +Pebble). A new Pebble-specific table format scheme is proposed. + +In the context of a distributed system such as Cockroach, certain SSTable +features are backwards incompatible (e.g. the block property collection and +filtering feature extends the RocksDBv2 SSTable block index format to encoded +various block properties, which is a breaking change). Participants must +_first_ ensure that their stores have the code-level features available to read +and write these newer SSTables (indicated by Pebble's format major version). +Once all stores agree that they are running the minimum Pebble format major +version and will not roll back (e.g. Cockroach cluster version finalization), +SSTables can be written and read using more recent table formats. The Pebble +"format major version" and "table format version" are therefore no longer +independent - the former implies an upper bound on the latter. + +Additionally, certain SSTable generation operations are independent of a +specific Pebble instance. For example, SSTable construction for the purposes of +backup and restore generates SSTables that are stored external to a specific +Pebble store (e.g. in cloud storage) can be used at a later point in time to +restore a store. SSTables constructed for such purposes must be carefully +versioned to ensure compatibility with existing clusters that may run with a +mixture of Pebble versions. + +As a real-world example of the need for the above, consider two Cockroach nodes +each with a Pebble store, one at version A, the other at version B (version A +(newer) > B (older)). Store A constructs an SSTable for an external backup +containing a newer block index format (for block property collection). This +SSTable is then imported in to store B. Store B fails to read the SSTable as it +is not running with a format major version recent enough make sense of the +newer index format. The two stores require a method for agreeing on a minimum +supported table format. + +The remainder of this document outlines a new table format for Pebble. This new +table format will be used for new table-level features such as block properties +and range keys (see +[#1339](https://github.com/cockroachdb/pebble/issues/1339)), but also for +backporting table-level features from RocksDB that would be useful to Pebble +(e.g. version 3 avoids encoding sequence numbers in the index, and version 4 +uses delta encoding for the block offsets in the index, both of which are +useful for Pebble). + +# Technical design + +## Pebble magic number + +The last 8 bytes of an SSTable is referred to as the "magic number". + +LevelDB uses the first 8 bytes of the SHA1 hash of the string +`http://code.google.com/p/leveldb/` for the magic number. + +RocksDB uses its own magic number, which indicates the use of a slightly +different table layout - the footer (the name for the end of an SSTable) is +slightly larger to accommodate a 32-bit version number and 8 bits for a +checksum type to be used for all blocks in the SSTable. + +A new 8-byte magic number will be introduced for Pebble: + +``` +\xf0\x9f\xaa\xb3\xf0\x9f\xaa\xb3 // 🪳🪳 +``` + +## Pebble version scheme + +Tables with a Pebble magic number will use a dedicated versioning scheme, +starting with version `1`. No new versions other than version `2` will be +supported for tables containing the RocksDB magic number. + +The choice of switching to a Pebble versioning scheme starting `1` simplifies +the implementation. Essentially all existing Pebble stores are managed via +Cockroach, and were either previously using RocksDB and migrated to Pebble, or +were created with Pebble stores. In both situations the table format used is +RocksDB v2. + +Given that Pebble has not needed (and likely will not need) to support other +RocksDB table formats, it is reasonable to introduce a new magic number for +Pebble and reset the version counter to v1. + +The following initial versions will correspond to the following new Pebble +features, that have yet to be introduced to Cockroach clusters as of the time +of writing: + +- Version 1: block property collectors (block properties are encoded into the + block index) +- Version 2: range keys (a new block is present in the table for range keys). + +Subsequent alterations to the SSTable format should only increment the _Pebble +version number_. It should be noted that backported RocksDB table format +features (e.g. RocksDB versions 3 and 4) would use a different version number, +within the Pebble version sequence. While possibly confusing, the RocksDB +features are being "adopted" by Pebble, rather than directly ported, so a +Pebble specific version number is appropriate. + +An alternative would be to allow RocksDB table format features to be backported +into Pebble under their existing RocksDB magic number, _alongside_ +Pebble-specific features. The complexity required to determine the set of +characteristics to read and write to each SSTable would increase with such a +scheme, compared to the simpler "linear history" approach described above, +where new features simply ratchet the Pebble table format version number. + +## Footer format + +The footer format for SSTables with Pebble magic numbers _will remain the same_ +as the RocksDB footer format - specifically, the trailing 53-bytes of the +SSTable consisting of the following fields with the given indices, +little-endian encoded: + +- `0`: Checksum type +- `1-20`: Meta-index block handle +- `21-40`: Index block handle +- `41-44`: Version number +- `45-52`: Magic number + +## Changes / additions to `sstable.TableFormat` + +The `sstable.TableFormat` enum is a `uint32` representation of the tuple +`(magic number, format version). The current values are: + +```go +type TableFormat uint32 + +const ( + TableFormatRocksDBv2 TableFormat = iota + TableFormatLevelDB +) +``` + +It should be noted that this enum is _not_ persisted in the SSTable. It is +purely an internal type that represents the tuple that simplifies a number of +version checks when reading / writing an SSTable. The values are free to +change, provided care is taken with default values and existing usage. + +The existing `sstable.TableFormat` will be altered to reflect the "linear" +nature of the version history. New versions will be added with the next value +in the sequence. + +```go +const ( + TableFormatUnspecified TableFormat = iota + TableFormatLevelDB // The original LevelDB table format. + TableFormatRocksDBv2 // The current default table format. + TableFormatPebblev1 // Block properties. + TableFormatPebblev2 // Range keys. + ... + TableFormatPebbleDBvN +) +``` + +The introduction of `TableFormatUnspecified` can be used to ensure that where a +`sstable.TableFormat` is _not_ specified, Pebble can select a suitable default +for writing the table (most likely based on the format major version in use by +the store; more in the next section). + +## Interaction with the format major version + +The `FormatMajorVersion` type is used to determine the set of features the +store supports. + +A Pebble store may be read-from / written-to by a Pebble binary that supports +newer features, with more recent Pebble format major versions. These newer +features could include the ability to read and write more recent SSTables. +While the store _could_ read and write SSTables at the most recent version the +binary supports, it is not safe to do so, for reasons outlined earlier. + +The format major version will have a "maximum table format version" associated +with it that indicates the maximum `sstable.TableFormat` that can be safely +handled by the store. + +When introducing a new _table format_ version, it should be gated behind an +associated `FormatMajorVersion` that has the new table format as its "maximum +table format version". + +For example: + +```go +// Existing verisons. +FormatDefault.MaxTableFormat() // sstable.TableFormatRocksDBv2 +... +FormatSetWithDelete.MaxTableFormat() // sstable.TableFormatRocksDBv2 +// Proposed versions with Pebble version scheme. +FormatBlockPropertyCollector.MaxTableFormat() // sstable.TableFormatPebbleDBv1 +FormatRangeKeys.MaxTableFormat() // sstable.TableFormatPebbleDBv2 +``` + +## Usage in Cockroach + +The introduction of new SSTable format versions needs to be carefully +coordinated between stores to ensure there are no incompatibilities (i.e. newer +store writes an SSTable that cannot be understood by other stores). + +It is only safe to use a new table format when all nodes in a cluster have been +finalized. A newer Cockroach node, with newer Pebble code, should continue to +write SSTables with a table format version equal to or less than the smallest +table format version across all nodes in the cluster. Once the cluster version +has been finalized, and `(*DB).RatchetFormatMajorVersion(FormatMajorVersion)` +has been called, nodes are free to write SSTables at newer table format +versions. + +At runtime, Pebble exposes a `(*DB).FormatMajorVersion()` method, which may be +used to determine the current format major version of the store, and hence, the +associated table format version. + +In addition to the above, there are situations where SSTables are created for +consumption at a later point in time, independent of any Pebble store - +specifically backup and restore. Currently, Cockroach uses two functions in +`pkg/sstable` to construct SSTables for both ingestion and backup +([here](https://github.com/cockroachdb/cockroach/blob/20eaf0b415f1df361246804e5d1d80c7a20a8eb6/pkg/storage/sst_writer.go#L57) +and +[here](https://github.com/cockroachdb/cockroach/blob/20eaf0b415f1df361246804e5d1d80c7a20a8eb6/pkg/storage/sst_writer.go#L78)). +Both will need to be updated to take into account the cluster version to ensure +that SSTables with newer versions are only written once the cluster version has +been finalized. + +### Cluster version migration sequencing + +Cockroach uses cluster versions as a guarantee that all nodes in a cluster are +running at a particular binary version, with a particular set of features +enabled. The Pebble store is ratcheted as the cluster version passes certain +versions that correspond to new Pebble functionality. Care must be taken to +prevent subtle race conditions while the cluster version is being updated +across all nodes in a cluster. + +Consider a cluster at cluster version `n-1` with corresponding Pebble format +major version `A`. A new cluster version `n` introduces a new Pebble format +major version `B` with new table level features. One by one, nodes will bump +their format major versions from `A` to `B` as they are upgraded to cluster +version `n`. There exists a period of time where nodes in a cluster are split +between cluster versions `n-1` and `n`, and Pebble format major versions `A` +and `B`. If version `B` introduces SSTable level features that nodes with +stores at format major version `A` do not yet understand, there exists the risk +for runtime incompatibilities. + +To guard against the window of incompatibility, _two_ cluster versions are +employed when bumping Pebble format major versions that correspond to new +SSTable level features. The first cluster verison is uesd to synchronize all +stores at the same Pebble format major version (and therefore table format +version). The second cluster version is used as a feature gate that enables +Cockroach nodes to make use of the newer table format, relying on the guarantee +that if a node is at version `n + 1`, then all other nodes in the cluster must +all be at least at version `n`, and therefore have Pebble stores at format +major version `B`. diff --git a/pebble/docs/RFCS/20220311_pebble_flushable_ingested_sstable.md b/pebble/docs/RFCS/20220311_pebble_flushable_ingested_sstable.md new file mode 100644 index 0000000..fb6ff7f --- /dev/null +++ b/pebble/docs/RFCS/20220311_pebble_flushable_ingested_sstable.md @@ -0,0 +1,280 @@ +- Feature Name: Flushable Ingested SSTable +- Status: in-progress +- Start Date: 2022-03-11 +- Authors: Mufeez Amjad +- RFC PR: [#1586](https://github.com/cockroachdb/pebble/pull/1586) +- Pebble Issues: [#25](https://github.com/cockroachdb/pebble/issues/25) +- Cockroach Issues: + +## Summary + +To avoid a forced flush when ingesting SSTables that have an overlap with a +memtable, we "lazily" add the SSTs to the LSM as a `*flushableEntry` to +`d.mu.mem.queue`. In comparison to a regular ingest which adds the SSTs to the +lowest possible level, the SSTs will get placed in the memtable queue before +they are eventually flushed (to the lowest level possible). This state is only +persisted in memory until a flush occurs, thus we require a WAL entry to replay +the ingestion in the event of a crash. + +## Motivation + +Currently, if any of the SSTs that need to be ingested have an overlap with a +memtable, we +[wait](https://github.com/cockroachdb/pebble/blob/56c5aebe151977964db7e464bb6c87ebd3451bd5/ingest.go#L671) +for the memtable to be flushed before the ingestion can proceed. This is to +satisfy the invariant that newer entries (those in the ingested SSTs) in the LSM +have a higher sequence number than old entries (those in the memtables). This +problem is also present for subsequent normal writes that are blocked behind the +ingest waiting for their sequence number to be made visible. + +## Technical Design + +The proposed design is mostly taken from Peter's suggestion in #25. The core +requirements are: +1. Replayable WAL entry for the ingest. +2. Implementation of the `flushable` interface for a new `ingestedSSTables` struct. +3. Lazily adding the ingested SSTs to the LSM. +4. Flushing logic to move SSTs into L0-L6. + +
+ +### 1. WAL Entry + +We require a WAL entry to make the ingestion into the flushable queue +replayable, and there is a need for a new type of WAL entry that does not get +applied to the memtable. 2 approaches were considered: +1. Using `seqnum=0` to differentiate this new WAL entry. +2. Introduce a new `InternalKeyKind` for the new WAL entry, + `InternalKeyKindIngestSST`. + +We believe the second approach is better because it avoids modifying batch +headers which can be messy/hacky and because `seqnum=0` is already used for +unapplied batches. The second approach also gives way for a simpler/cleaner +implementation because it utilizes the extensibility of `InternalKeyKind` and is +similar to the treatment of `InternalKeyKindLogData`. It also follows the +correct seqnum semantics for SSTable ingestion in the event of a WAL replay — +each SST in the ingestion batch already gets its own sequence number. + +This change will need to be gated on a `FormatMajorVersion` because if the store +is opened with an older version of Pebble, Pebble will not understand any WAL +entry that contains the new `InternalKeyKind`. + +
+ +When performing an ingest (with overlap), we create a batch with the header: + +``` ++-------------+------------+--- ... ---+ +| SeqNum (8B) | Count (4B) | Entries | ++-------------+------------+--- ... ---+ +``` + +where`SeqNum` is the current running sequence number in the WAL, `Count` is the +number of ingested SSTs, and each entry has the form: + +``` ++-----------+-----------------+-------------------+ +| Kind (1B) | Key (varstring) | Value (varstring) | ++-----------+-----------------+-------------------+ +``` + +where `Kind` is `InternalKeyKindIngestSST`, and `Key` is a path to the +ingested SST on disk. + +When replaying the WAL, we check every batch's first entry and if `keykind == +InternalKeyKindIngestSSTs` then we continue reading the rest of the entries in +the batch of SSTs and replay the ingestion steps - we construct a +`flushableEntry` and add it to the flushable queue: + +```go +b = Batch{db: d} +b.SetRepr(buf.Bytes()) +seqNum := b.SeqNum() +maxSeqNum = seqNum + uint64(b.Count()) +br := b.Reader() +if kind, _, _, _ := br.Next(); kind == InternalKeyKindIngestSST { + // Continue reading the rest of the batch and construct flushable + // of sstables with correct seqnum and add to queue. + buf.Reset() + continue +} +``` + + +### 2. `flushable` Implementation + +Introduce a new flushable type: `ingestedSSTables`. + +```go +type ingestedSSTables struct { + files []*fileMetadata + size uint64 + + cmp Compare + newIters tableNewIters +} +``` +which implements the following functions from the `flushable` interface: + +#### 1. `newIter(o *IterOptions) internalIterator` + +We return a `levelIter` since the ingested SSTables have no overlap, and we can +treat them like a level in the LSM. + +```go +levelSlice := manifest.NewLevelSliceKeySorted(s.cmp, s.files) +return newLevelIter(*o, s.cmp, nil, s.newIters, levelSlice.Iter(), 0, nil) +``` + +
+ +On the client-side, this iterator would have to be used like this: +```go +var iter internalIteratorWithStats +var rangeDelIter keyspan.FragmentIterator +iter = base.WrapIterWithStats(mem.newIter(&dbi.opts)) +switch mem.flushable.(type) { +case *ingestedSSTables: + iter.(*levelIter).initRangeDel(&rangeDelIter) +default: + rangeDelIter = mem.newRangeDelIter(&dbi.opts) +} + +mlevels = append(mlevels, mergingIterLevel{ + iter: iter, + rangeDelIter: rangeDelIter, +}) +``` + +#### 2. `newFlushIter(o *IterOptions, bytesFlushed *uint64) internalIterator` + +#### 3. `newRangeDelIter(o *IterOptions) keyspan.FragmentIterator` + +The above two methods would return `nil`. By doing so, in `c.newInputIter()`: +```go +if flushIter := f.newFlushIter(nil, &c.bytesIterated); flushIter != nil { + iters = append(iters, flushIter) +} +if rangeDelIter := f.newRangeDelIter(nil); rangeDelIter != nil { + iters = append(iters, rangeDelIter) +} +``` +we ensure that no iterators on `ingestedSSTables` will be used while flushing in +`c.runCompaction()`. + +The special-cased flush process for this flushable is described in [Section +4](#4-flushing-logic-to-move-ssts-into-l0). + +#### 4. `newRangeKeyIter(o *IterOptions) keyspan.FragmentIterator` + +Will wait on range key support in `levelIter` to land before implementing. + +#### 5. `inuseBytes() uint64` and `totalBytes() uint64` + +For both functions, we return 0. + +Returning 0 for `inuseBytes()` means that the calculation of `c.maxOverlapBytes` +is not affected by the SSTs (the ingested SSTs don't participate in the +compaction). + +We don't want the size of the ingested SSTs to contribute to the size of the +memtable when determining whether or not to stall writes +(`MemTableStopWritesThreshold`); they should contribute to the L0 read-amp +instead (`L0StopWritesThreshold`). Thus, we'll have to special case for ingested +SSTs in `d.makeRoomForWrite()` to address this detail. + +`totalBytes()` represents the number of bytes allocated by the flushable, which +in our case is 0. A consequence for this is that the size of the SSTs do not +count towards the flush threshold calculation. However, by setting +`flushableEntry.flushForced` we can achieve the same behaviour. + +#### 6. `readyForFlush() bool` + +The flushable of ingested SSTs can always be flushed because the files are +already on disk, so we return true. + +### 3. Lazily adding the ingested SSTs to the LSM + +The steps to add the ingested SSTs to the flushable queue are: +1. Detect an overlap exists (existing logic). + +Add a check that falls back to the old ingestion logic of blocking the ingest on +the flush when `len(d.mu.mem.queue) >= MemtablesStopWritesThreshold - 1`. This +reduces the chance that many short, overlapping, and successive ingestions cause +a memtable write stall. + +Additionally, to mitigate the hiccup on subsequent normal writes, we could wait +before the call to `d.commit.AllocateSeqNum` until: +1. the number of immutable memtables and `ingestedSSTs` in the flushable queue + is below a certain threshold (to prevent building up too many sublevels) +2. the number of immutable memtables is low. This could lead to starvation if + there is a high rate of normal writes. + +2. Create a batch with the list of ingested SSTs. +```go +b := newBatch() +for _, path := range paths: + b.IngestSSTs([]byte(path), nil) +``` +3. Apply the batch. + +In the call to `d.commit.AllocateSeqNum`, `b.count` sequence numbers are already +allocated before the `prepare` step. When we identify a memtable overlap, we +commit the batch to the WAL manually (through logic similar to +`commitPipeline.prepare`). The `apply` step would be a no-op if we performed a +WAL write in the `prepare` step. We would also need to truncate the memtable/WAL +after this step. + +5. Create `ingestedSSTables` flushable and `flushableEntry`. + +We'd need to call `ingestUpdateSeqNum` on these SSTs before adding them to the +flushable. This is to respect the sequence number ordering invariant while the +SSTs reside in the flushable queue. + +6. Add to flushable queue. + +Pebble requires that the last entry in `d.mu.mem.queue` is the mutable memtable +with value `d.mu.mem.mutable`. When adding a `flushableEntry` to the queue, we +want to maintain this invariant. To do this we pass `nil` as the batch to +`d.makeRoomForWrite()`. The result is + +``` +| immutable old memtable | mutable new memtable | +``` + +We then append our new `flushableEntry`, and swap the last two elements in +`d.mu.mem.queue`: + +``` +| immutable old memtable | ingestedSSTables | mutable new memtable | +``` + +Because we add the ingested SSTs to the flushable queue when there is overlap, +and are skipping applying the version edit through the `apply` step of the +ingestion, we ensure that the SSTs are only added to the LSM once. + +7. Call `d.maybeScheduleFlush()`. + +Because we've added an immutable memtable to the flushable queue and set +`flushForced` on the `flushableEntry`, this will surely result in a flush. This +call can be done asynchronously. + +We can then return to caller without waiting for the flush to finish. + +### 4. Flushing logic to move SSTs into L0-L6 + +By returning `nil` for both `flushable.newFlushIter()` and +`flushable.newRangeDelIter()`, the `ingestedSSTables` flushable will not be +flushed normally. + +The suggestion in issue #25 is to move the SSTs from the flushable queue into +L0. However, only the tables that overlap with the memtable will need to target +L0 (because they will likely overlap with L0 post flush), the others can be +moved to lower levels in the LSM. We can use the existing logic in +`ingestTargetLevel` to determine which level to move the ingested SSTables to +during `c.runCompaction()`. However, it's important to do this step after the +memtable has been flushed to use the correct `version` when determining overlap. + +The flushable of ingested SSTs should not influence the bounds on the +compaction, so we will have to skip updating `c.smallest` and `c.largest` in +`d.newFlush()` for this flushable. diff --git a/pebble/docs/RFCS/20221122_virtual_sstable.md b/pebble/docs/RFCS/20221122_virtual_sstable.md new file mode 100644 index 0000000..168dbf6 --- /dev/null +++ b/pebble/docs/RFCS/20221122_virtual_sstable.md @@ -0,0 +1,366 @@ +- Feature Name: Virtual sstables +- Status: in-progress +- Start Date: 2022-10-27 +- Authors: Arjun Nair +- RFC PR: https://github.com/cockroachdb/pebble/pull/2116 +- Pebble Issues: + https://github.com/cockroachdb/pebble/issues/1683 + + +** Design Draft** + +# Summary + +The RFC outlines the design to enable virtualizing of physical sstables +in Pebble. + +A virtual sstable has no associated physical data on disk, and is instead backed +by an existing physical sstable. Each physical sstable may be shared by one, or +more than one virtual sstable. + +Initially, the design will be used to lower the read-amp and the write-amp +caused by certain ingestions. Sometimes, ingestions are unable to place incoming +files, which have no data overlap with other files in the lsm, lower in the lsm +because of file boundary overlap with files in the lsm. In this case, we are +forced to place files higher in the lsm, sometimes in L0, which can cause higher +read-amp and unnecessary write-amp as the file is moved lower down the lsm. See +https://github.com/cockroachdb/cockroach/issues/80589 for the problem occurring +in practice. + +Eventually, the design will also be used for the disaggregated storage masking +use-case: https://github.com/cockroachdb/cockroach/pull/70419/files. + +This document describes the design of virtual sstables in Pebble with enough +detail to aid the implementation and code review. + +# Design + +### Ingestion + +When an sstable is ingested into Pebble, we try to place it in the lowest level +without any data overlap, or any file boundary overlap. We can make use of +virtual sstables in the cases where we're forced to place the ingested sstable +at a higher level due to file boundary overlap, but no data overlap. + +``` + s2 +ingest: [i-j-------n] + s1 +L6: [e---g-----------------p---r] + a b c d e f g h i j k l m n o p q r s t u v w x y z +``` + +Consider the sstable s1 in L6 and the ingesting sstable s2. It is clear that +the file boundaries of s1 and s2 overlap, but there is no data overlap as shown +in the diagram. Currently, we will be forced to ingest the sstable s2 into a +level higher than L6. With virtual sstables, we can split the existing sstable +s1 into two sstables s3 and s4 as shown in the following diagram. + +``` + s3 s2 s4 +L6: [e---g]-[i-j-------n]-[p---r] + a b c d e f g h i j k l m n o p q r s t u v w x y z +``` + +The sstable s1 will be deleted from the lsm. If s1 was a physical sstable, then +we will keep the file on disk as long as we need to so that it can back the +virtual sstables. + +There are cases where the ingesting sstables have no data overlap with existing +sstables, but we can't make use of virtual sstables. Consider: +``` + s2 +ingest: [f-----i-j-------n] + s1 +L6: [e---g-----------------p---r] + a b c d e f g h i j k l m n o p q r s t u v w x y z +``` +We cannot use virtual sstables in the above scenario for two reasons: +1. We don't have a quick method of detecting no data overlap. +2. We will be forced to split the sstable in L6 into more than two virtual + sstables, but we want to avoid many small virtual sstables in the lsm. + +Note that in Cockroach, the easier-to-solve case happens very regularly when an +sstable spans a range boundary (which pebble has no knowledge of), and we ingest +a snapshot of a range in between the two already-present ranges. + +slide in between two existing sstables is more likely to happen. It occurs when +we ingest a snapshot of a range in between two already present ranges. + +`ingestFindTargetLevel` changes: +- The `ingestFindTargetLevel` function is used to determine the target level + of the file which is being ingested. Currently, this function returns an `int` + which is the target level for the ingesting file. Two additional return + parameters, `[]manifest.NewFileEntry` and `*manifest.DeletedFileEntry`, will be + added to the function. +- If `ingestFindTargetLevel` decides to split an existing sstable into virtual + sstables, then it will return new and deleted entries. Otherwise, it will only + return the target level of the ingesting file. +- Within the `ingestFindTargetLevel` function, the `overlapWithIterator` + function is used to quickly detect data overlap. In the case with file + boundary overlap, but no data overlap, in the lowest possible level, we will + split the existing sstable into virtual sstables and generate the + `NewFileEntry`s and the `DeletedFileEntry`. The `FilemetaData` section + describes how the various fields in the `FilemetaData` will be computed for + the newly created virtual sstables. + +- Note that we will not split physical sstables into virtual sstables in L0 for + the use case described in this RFC. The benefit of doing so would be to reduce + the number of L0 sublevels, but the cost would be additional implementation + complexity(see the `FilemetaData` section). We also want to avoid too many + virtual sstables in the lsm as they can lead to space amp(see `Compaction` + section). However, in the future, for the disaggregated storage masking case, + we would need to support ingestion and use of virtual sstables in L0. + +- Note that we may need an upper bound on the number of times an sstable is + split into smaller virtual sstables. We can further reduce the risk of many + small sstables: + 1. For CockroachDB's snapshot ingestion, there is one large sst (up to 512MB) + and many tiny ones. We can choose the apply this splitting logic only for + the large sst. It is ok for the tiny ssts to be ingested into L0. + 2. Split only if the ingested sst is at least half the size of the sst being + split. So if we have a smaller ingested sst, we will pick a higher level to + split at (where the ssts are smaller). The lifetime of virtual ssts at a + higher level is smaller, so there is lower risk of littering the LSM with + long-lived small virtual ssts. + 3. For disaggregated storage implementation, we can avoid masking for tiny + sstables being ingested and instead write a range delete like we currently + do. Precise details on the masking use case are out of the scope of this + RFC. + +`ingestApply` changes: +- The new and deleted file entries returned by the `ingestFindTargetLevel` + function will be added to the version edit in `ingestApply`. +- We will appropriately update the `levelMetrics` based on the new information + returned by `ingestFindTargetLevel`. + + +### `FilemetaData` changes + +Each virtual sstables will have a unique file metadata value associated with it. +The metadata may be borrowed from the backing physical sstable, or it may be +unique to the virtual sstable. + +This rfc lists out the fields in the `FileMetadata` struct with information on +how each field will be populated. + +`Atomic.AllowedSeeks`: Field is used for read triggered compactions, and we can +populate this field for each virtual sstable since virtual sstables can be +picked for compactions. + +`Atomic.statsValid`: We can set this to true(`1`) when the virtual sstable is +created. On virtual sstable creation we will estimate the table stats of the +virtual sstable based on the table stats of the physical sstable. We can also +set this to `0` and let the table stats job asynchronously compute the stats. + +`refs`: The will be turned into a pointer which will be shared by the +virtual/physical sstables. See the deletion section of the RFC to learn how the +`refs` count will be used. + +`FileNum`: We could give each virtual sstable its own file number or share +the file number between all the virtual sstables. In the former case, the virtual +sstables will be distinguished by the file number, and will have an additional +metadata field to indicate the file number of the parent sstable. In the latter +case, we can use a few of the most significant bits of the 64 bit file number to +distinguish the virtual sstables. + +The benefit of using a single file number for each virtual sstable, is that we +don't need to use additional space to store the file number of the backing +physical sstable. + +It might make sense to give each virtual sstable its own file number. Virtual +sstables are picked for compactions, and compactions and compaction picking +expect a unique file number for each of the files which it is compacting. +For example, read compactions will use the file number of the file to determine +if a file picked for compaction has already been compacted, the version edit +will expect a different file number for each virtual sstable, etc. + +There are direct references to the `FilemetaData.FileNum` throughout Pebble. For +example, the file number is accessed when the the `DB.Checkpoint` function is +called. This function iterates through the files in each level of the lsm, +constructs the filepath using the file number, and reads the file from disk. In +such cases, it is important to exclude virtual sstables. + +`Size`: We compute this using linear interpolation on the number of blocks in +the parent sstable and the number of blocks in the newly created virtual sstable. + +`SmallestSeqNum/LargestSeqNum`: These fields depend on the parent sstable, +but we would need to perform a scan of the physical sstable to compute these +accurately for the virtual sstable upon creation. Instead, we could convert +these fields into lower and upper bounds of the sequence numbers in a file. + +These fields are used for l0 sublevels, pebble tooling, delete compaction hints, +and a lot of plumbing. We don't need to worry about the L0 sublevels use case +because we won't have virtual sstables in L0 for the use case in this RFC. For +the rest of the use cases we can use lower bound for the smallest seq number, +and an upper bound for the largest seq number work. + +TODO(bananabrick): Add more detail for any delete compaction hint changes if +necessary. + +`Smallest/Largest`: These, along with the smallest/largest ranges for the range +and point keys can be computed upon virtual sstable creation. Precisely, these +can be computed when we try and detect data overlap in the `overlapWithIterator` +function during ingestion. + +`Stats`: `TableStats` will either be computed upon virtual sstable creation +using linear interpolation on the block counts of the virtual/physical sstables +or asynchronously using the file bounds of the virtual sstable. + +`PhysicalState`: We can add an additional struct with state associated with +physical ssts which have been virtualized. + +``` +type PhysicalState struct { + // Total refs across all virtual ssts * versions. That is, if the same virtual + // sst is present in multiple versions, it may have multiple refs, if the + // btree node is not the same. + totalRefs int32 + + // Number of virtual ssts in the latest version that refer to this physical + // SST. Will be 1 if there is only a physical sst, or there is only 1 virtual + // sst referencing this physical sst. + // INVARIANT: refsInLatestVersion <= totalRefs + // refsInLatestVersion == 0 is a zombie sstable. + refsInLatestVersion int32 + + fileSize uint64 + + // If sst is not virtualized and in latest version + // virtualSizeSumInLatestVersion == fileSize. If + // virtualSizeSumInLatestVersion > 0 and + // virtualSizeSumInLatestVersion/fileSize is very small, the corresponding + // virtual sst(s) should be candidates for compaction. These candidates can be + // tracked via btree annotations. Incrementlly updated in + // BulkVersionEdit.Apply, when updating refsInLatestVersion. + virtualSizeSumInLatestVersion uint64 +} +``` + +The `Deletion` section and the `Compactions` section describe why we need to +store the `PhysicalState`. + +### Deletion of physical and virtual sstables + +We want to ensure that the physical sstable is only deleted from disk when no +version references it, and when there are no virtual sstables which are backed +by the physical sstable. + +Since `FilemetaData.refs` is a pointer which is shared by the physical and +virtual sstables, the physical sstable won't be deleted when it is removed +from the latest version as the `FilemetaData.refs` will have been increased +when the virtual sstable is added to a version. Therefore, we only need to +ensure that the physical sstable is eventually deleted when there are no +versions which reference it. + +Sstables are deleted from disk by the `DB.doDeleteObsoleteFiles` function which +looks for files to delete in the the `DB.mu.versions.obsoleteTables` slice. +So we need to ensure that any physical sstable which was virtualized is added to +the obsolete tables list iff `FilemetaData.refs` is 0. + +Sstable are added to the obsolete file list when a `Version` is unrefed and +when `DB.scanObsoleteFiles` is called when Pebble is opened. + +When a `Version` is unrefed, sstables referenced by it are only added to the +obsolete table list if the `FilemetaData.refs` hits 0 for the sstable. With +virtual sstables, we can have a case where the last version which directly +references a physical sstable is unrefed, but the physical sstable is not added +to the obsolete table list because its `FilemetaData.refs` count is not 0 +because of indirect references through virtual sstables. Since the last Version +which directly references the physical sstable is deleted, the physical sstable +will never get added to the obsolete table list. Since virtual sstables keep +track of their parent physical sstable, we can just add the physical sstable to +the obsolete table list when the last virtual sstable which references it is +deleted. + +`DB.scanObsoleteFiles` will delete any file which isn't referenced by the +`VersionSet.versions` list. So, it's possible that a physical sstable associated +with a virtual sstable will be deleted. This problem can be fixed by a small +tweak in the `d.mu.versions.addLiveFileNums` to treat the parent sstable of +a virtual sstable as a live file. + +Deleted files still referenced by older versions are considered zombie sstables. +We can extend the definition of zombie sstables to be any sstable which is not +directly, or indirectly through virtual sstables, referenced by the latest +version. See the `PhysicalState` subsection of the `FilemetaData` section +where we describe how the references in the latest version will be tracked. + + +### Reading from virtual sstables + +Since virtual sstables do not exist on disk, we will have to redirect reads +to the physical sstable which backs the virtual sstable. + +All reads to the physical files go through the table cache which opens the file +on disk and creates a `Reader` for the reads. The table cache currently creates +a `FileNum` -> `Reader` mapping for the physical sstables. + +Most of the functions in table cache API take the file metadata of the file as +a parameter. Examples include `newIters`, `newRangeKeyIter`, `withReader`, etc. +Each of these functions then calls a subsequent function on the sstable +`Reader`. + +In the `Reader` API, some functions only really need to be called on physical +sstables, whereas some functions need to be called on both physical and virtual +sstables. For example, the `Reader.EstimateDiskUsage` usage function, or the +`Reader.Layout` function only need to be called on physical sstables, whereas, +some function like, `Reader.NewIter`, and `Reader.NewCompactionIter` need to +work with virtual sstables. + +We could either have an abstraction over the physical sstable `Reader` per +virtual sstable, or update the `Reader` API to accept file bounds of the +sstable. In the latter case, we would create one `Reader` on the physical +sstable for all of the virtual sstables, and update the `Reader` API to accept +the file bounds of the sstable. + +Changes required to share a `Reader` on the physical sstable among the virtual +sstable: +- If the file metadata of the virtual sstable is passed into the table cache, on + a table cache miss, the table cache will load the Reader for the physical + sstable. This step can be performed in the `tableCacheValue.load` function. On + a table cache hit, the file number of the parent sstable will be used to fetch + the appropriate sstable `Reader`. +- The `Reader` api will be updated to support reads from virtual sstables. For + example, the `NewCompactionIter` function will take additional + `lower,upper []byte` parameters. + +Updates to iterators: +- `Reader.NewIter` already has `lower,upper []byte` parameters so this requires + no change. +- Add `lower,upper` fields to the `Reader.NewCompactionIter`. The function + initializes single level and two level iterators, and we can pass in the + `lower,upper` values to those. TODO(bananabrick): Make sure that the value + of `bytesIterated` in the compaction iterator is still accurate. +- `Reader.NewRawRangeKeyIter/NewRawRangeDelIter`: We need to add `lower/upper` + fields to the functions. Both iterators make use of a `fragmentBlockIter`. We + could filter keys above the `fragmentBlockIter` or add filtering within the + `fragmentBlockIter`. To add filtering within the `fragmentBlockIter` we will + initialize it with two additional `lower/upper []byte` fields. +- We would need to update the `SetBounds` logic for the sstable iterators to + never set bounds for the iterators outside the virtual sstable bounds. This + could lead to keys outside the virtual sstable bounds, but inside the physical + sstable bounds, to be surfaced. + +TODO(bananabrick): Add a section about sstable properties, if necessary. + +### Compactions + +Virtual sstables can be picked for compactions. If the `FilemetaData` and the +iterator stack changes work, then compaction shouldn't require much, if any, +additional work. + +Virtual sstables which are picked for compactions may cause space amplification. +For example, if we have two virtual sstables `a` and `b` in L5, backed by a +physical sstable `c`, and the sstable `a` is picked for a compaction. We will +write some additional data into L6, but we won't delete sstable `c` because +sstable `b` still refers to it. In the worst case, sstable `b` will never be +picked for compaction and will never be compacted into and we'll have permanent +space amplification. We should try prioritize compaction of sstable `b` to +prevent such a scenario. + +See the `PhysicalState` subsection in the `FilemetaData` section to see how +we'll store compaction picking metrics to reduce virtual sstable space-amp. + +### `VersionEdit` decode/encode +Any additional fields added to the `FilemetaData` need to be supported in the +version edit `decode/encode` functions. diff --git a/pebble/docs/css/app.css b/pebble/docs/css/app.css new file mode 100644 index 0000000..71e1527 --- /dev/null +++ b/pebble/docs/css/app.css @@ -0,0 +1,117 @@ +body { + margin: 10; + background-color: #fff; + font: 10pt -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; +} + +.divider { + border-top: 1px solid #eee; +} + +.columns { + display: flex; + flex-direction: row; + align-items: baseline; + justify-content: space-between; +} + +.rows { + display: flex; + flex-direction: column; + flex-wrap: wrap; +} + +.center { + margin: auto; + width: 90%; + min-width: 400px; + max-width: 1200px; +} + +.section { + flex: 100%; + margin-top: 10px; + overflow: auto; +} + +.title { + font-size: 24pt; + font-weight: bold; +} + +.subtitle { + font-size: 12pt; + font-weight: bold; +} + +.updated { + font-size: 9pt; + text-align: right; +} + +div.annotation { + display: none; +} + +.code { + font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; +} + +.overview { + max-width: 800px; +} + +.controls { + margin: 5px; +} + +a { + text-decoration: none; +} + +.selected { + font-weight: bold; + text-decoration: underline; +} + +path.line1 { + fill: none; + stroke-width: 1.5px; +} + +path.line2 { + fill: none; + stroke-width: 1.5px; +} + +svg.chart { + flex: 50%; + height: 200px; +} + +.write-throughput { + flex: 50%; + height: 300px; +} + +.write-throughput-detail { + flex: 50%; + height: 300px; +} + +text.hover { + font-size: 8pt; + font-weight: bold; + filter: url(#textBackground); +} + +@media only screen and (max-width: 900px) { + .columns { + flex-direction: column; + } + + svg.chart { + width: 100%; + flex: auto; + } +} diff --git a/pebble/docs/index.html b/pebble/docs/index.html new file mode 100644 index 0000000..cc70942 --- /dev/null +++ b/pebble/docs/index.html @@ -0,0 +1,166 @@ + + + + + + + Pebble Benchmarks + + +
+
+
+
Pebble Benchmarks
+
Last updated
+
+
+
+
+ Benchmarks are run nightly using pebble + bench ycsb on AWS m6id.4xlarge machines equipped with + local SSD storage. The AWS instances show remarkably high + instance to instance performance variability. In order to + smooth out that variability the benchmarks are run multiple + times each (using different instances) and outliers are + excluded. +
+
+
+ Detail: + Bytes Read | + Bytes Written | + Read Amp | + Write Amp +
+
+ Options: + Local scale +
+
+
+
+
L0-sublevels and + flush-splits enabled
+
Increased + LogWriter free blocks 4->16
+
Began tracking + ycsb/E read-amp
+
Level metadata + switched to use a B-Tree
+
Enabled + read-triggered compactions
+
Readahead + and preallocation bug fixed
+
Removed excess + read samples for read-triggered compactions
+
Switched to Ubuntu + 20.04.2 LTS AMI
+
Read compaction fixes
+
Bumped benchmark + runtime to 90 minutes
+
Data quality issue introduced (YCSB A only)
+
Data quality issue fixed (YCSB A only)
+
Began zeroing reused + iterator structs (#1822)
+
Grandparent boundary + compaction splitting
+
Infrastructure + change (#2578)
+
ycsb/F sampling bug
+
Switched to m6id.4xlarge + (from 5d.4xlarge)
+
+
+
+ YCSB A + (50% reads, 50% updates, zipf key distribution) +
+
+ + +
+
+
+
+ YCSB B + (95 reads, 5% updates, zipf key distribution) +
+
+ + +
+
+
+
+ YCSB C + (100% reads, zipf key distribution) +
+
+ + +
+
+
+
+ YCSB D + (95% reads, 5% updates, uniform key distribution) +
+
+ + +
+
+
+
+ YCSB E + (95% scans, 5% updates, zipf key distribution) +
+
+ + +
+
+
+
+ Insert-only + (100% inserts, zipf key distribution) +
+
+ + +
+
+
+
+
+
+
+ Write throughput + (100% inserts, zipf key distribution) +
+
+
+ This benchmark attempts to find the optimal write throughput by + driving more and more load against the DB until a target heuristic + fails (currently a mixture of number of L0 sublevels, L0 files, and + whether the DB has experienced a write stall). These benchmarks are + run nightly using pebble + bench write on GCP n2-standard-32 machines equipped with 16 local + SSDs in a RAID 0 array. +
+
+
+
+ + +
+
+
+ + + + + + diff --git a/pebble/docs/io_profiling.md b/pebble/docs/io_profiling.md new file mode 100644 index 0000000..8fd6230 --- /dev/null +++ b/pebble/docs/io_profiling.md @@ -0,0 +1,231 @@ +# I/O Profiling + +Linux provide extensive kernel profiling capabilities, including the +ability to trace operations at the block I/O layer. These tools are +incredibly powerful, though sometimes overwhelming in their +flexibility. This document captures some common recipes for profiling +Linux I/O. + +* [Perf](#perf) +* [Blktrace](#blktrace) + +## Perf + +The Linux `perf` command can instrument CPU performance counters, and +the extensive set of kernel trace points. A great place to get started +understanding `perf` are Brendan Gregg's [perf +examples](http://www.brendangregg.com/perf.html). + +The two modes of operation are "live" reporting via `perf top`, and +record and report via `perf record` and `perf +{report,script}`. + +Recording the stack traces for `block:block_rq_insert` event allows +determination of what Pebble level code is generating block requests. + +### Installation + +Ubuntu AWS installation: + +``` +sudo apt-get install linux-tools-common linux-tools-4.4.0-1049-aws linux-cloud-tools-4.4.0-1049-aws +``` + +### Recording + +`perf record` (and `perf top`) requires read and write access to +`/sys/kernel/debug/tracing`. Running as root as an easiest way to get +the right permissions. + +``` +# Trace all block device (disk I/O) requests with stack traces, until Ctrl-C. +sudo perf record -e block:block_rq_insert -ag + +# Trace all block device (disk I/O) issues and completions with stack traces, until Ctrl-C. +sudo perf record -e block:block_rq_issue -e block:block_rq_complete -ag +``` + +The `-a` flag records events on all CPUs (almost always desirable). + +The `-g` flag records call graphs (a.k.a stack traces). Capturing the +stack trace makes the recording somewhat more expensive, but it +enables determining the originator of the event. Note the stack traces +include both the kernel and application code, allowing pinpointing the +source of I/O as due to flush, compaction, WAL writes, etc. + +The `-e` flag controls which events are instrumented. The list of +`perf` events is enormous. See `sudo perf list`. + +The `-o` flag controls where output is recorded. The default is +`perf.data`. + +In order to record events for a specific duration, you can append `-- +sleep ` to the command line. + +``` +# Trace all block device (disk I/O) requests with stack traces for 10s. +sudo perf record -e block:block_rq_insert -ag -- sleep 10 +``` + +### Reporting + +The recorded perf data (`perf.data`) can be explored using `perf +report` and `perf script`. + +``` +# Show perf.data in an ncurses browser. +sudo perf report + +# Show perf.data as a text report. +sudo perf report --stdio +``` + +As an example, `perf report --stdio` from perf data gathered using +`perf record -e block:block_rq_insert -ag` will show something like: + +``` + 96.76% 0.00% pebble pebble [.] runtime.goexit + | + ---runtime.goexit + | + |--85.58%-- github.com/cockroachdb/pebble/internal/record.NewLogWriter.func2 + | runtime/pprof.Do + | github.com/cockroachdb/pebble/internal/record.(*LogWriter).flushLoop-fm + | github.com/cockroachdb/pebble/internal/record.(*LogWriter).flushLoop + | github.com/cockroachdb/pebble/internal/record.(*LogWriter).flushPending + | github.com/cockroachdb/pebble/vfs.(*syncingFile).Sync + | github.com/cockroachdb/pebble/vfs.(*syncingFile).syncFdatasync-fm + | github.com/cockroachdb/pebble/vfs.(*syncingFile).syncFdatasync + | syscall.Syscall + | entry_SYSCALL_64_fastpath + | sys_fdatasync + | do_fsync + | vfs_fsync_range + | ext4_sync_file + | filemap_write_and_wait_range + | __filemap_fdatawrite_range + | do_writepages + | ext4_writepages + | blk_finish_plug + | blk_flush_plug_list + | blk_mq_flush_plug_list + | blk_mq_insert_requests +``` + +This is showing that `96.76%` of block device requests on the entire +system were generated by the `pebble` process, and `85.58%` of the +block device requests on the entire system were generated from WAL +syncing within this `pebble` process. + +The `perf script` command provides access to the raw request +data. While there are various pre-recorded scripts that can be +executed, it is primarily useful for seeing call stacks along with the +"trace" data. For block requests, the trace data shows the device, the +operation type, the offset, and the size. + +``` +# List all events from perf.data with recommended header and fields. +sudo perf script --header -F comm,pid,tid,cpu,time,event,ip,sym,dso,trace +... +pebble 6019/6019 [008] 16492.555957: block:block_rq_insert: 259,0 WS 0 () 3970952 + 256 [pebble] + 7fff813d791a blk_mq_insert_requests + 7fff813d8878 blk_mq_flush_plug_list + 7fff813ccc96 blk_flush_plug_list + 7fff813cd20c blk_finish_plug + 7fff812a143d ext4_writepages + 7fff8119ea1e do_writepages + 7fff81191746 __filemap_fdatawrite_range + 7fff8119188a filemap_write_and_wait_range + 7fff81297c41 ext4_sync_file + 7fff81244ecb vfs_fsync_range + 7fff81244f8d do_fsync + 7fff81245243 sys_fdatasync + 7fff8181ae6d entry_SYSCALL_64_fastpath + 3145e0 syscall.Syscall + 6eddf3 github.com/cockroachdb/pebble/vfs.(*syncingFile).syncFdatasync + 6f069a github.com/cockroachdb/pebble/vfs.(*syncingFile).syncFdatasync-fm + 6ed8d2 github.com/cockroachdb/pebble/vfs.(*syncingFile).Sync + 72542f github.com/cockroachdb/pebble/internal/record.(*LogWriter).flushPending + 724f5c github.com/cockroachdb/pebble/internal/record.(*LogWriter).flushLoop + 72855e github.com/cockroachdb/pebble/internal/record.(*LogWriter).flushLoop-fm + 7231d8 runtime/pprof.Do + 727b09 github.com/cockroachdb/pebble/internal/record.NewLogWriter.func2 + 2c0281 runtime.goexit +``` + +Let's break down the trace data: + +``` +259,0 WS 0 () 3970952 + 256 + | | | | + | | | + size (sectors) + | | | + | | + offset (sectors) + | | + | +- flags: R(ead), W(rite), B(arrier), S(ync), D(iscard), N(one) + | + +- device: , +``` + +The above is indicating that a synchronous write of `256` sectors was +performed starting at sector `3970952`. The sector size is device +dependent and can be determined with `blockdev --report `, +though it is almost always `512` bytes. In this case, the sector size +is `512` bytes indicating that this is a write of 128 KB. + +## Blktrace + +The `blktrace` tool records similar info to `perf`, but is targeted to +the block layer instead of being general purpose. The `blktrace` +command records data, while the `blkparse` command parses and displays +data. The `btrace` command is a shortcut for piping the output from +`blktrace` directly into `blkparse. + +### Installation + +Ubuntu AWS installation: + +``` +sudo apt-get install blktrace +``` + +## Usage + +``` +# Pipe the output of blktrace directly into blkparse. +sudo blktrace -d /dev/nvme1n1 -o - | blkparse -i - + +# Equivalently. +sudo btrace /dev/nvme1n1 +``` + +The information captured by `blktrace` is similar to what `perf` captures: + +``` +sudo btrace /dev/nvme1n1 +... +259,0 4 186 0.016411295 11538 Q WS 129341760 + 296 [pebble] +259,0 4 187 0.016412100 11538 Q WS 129342016 + 40 [pebble] +259,0 4 188 0.016412200 11538 G WS 129341760 + 256 [pebble] +259,0 4 189 0.016412714 11538 G WS 129342016 + 40 [pebble] +259,0 4 190 0.016413148 11538 U N [pebble] 2 +259,0 4 191 0.016413255 11538 I WS 129341760 + 256 [pebble] +259,0 4 192 0.016413321 11538 I WS 129342016 + 40 [pebble] +259,0 4 193 0.016414271 11538 D WS 129341760 + 256 [pebble] +259,0 4 194 0.016414860 11538 D WS 129342016 + 40 [pebble] +259,0 12 217 0.016687595 0 C WS 129341760 + 256 [0] +259,0 12 218 0.016700021 0 C WS 129342016 + 40 [0] +``` + +The standard format is: + +``` + + [] +``` + +See `man blkparse` for an explanation of the actions. + +The `blktrace` output can be used to highlight problematic I/O +patterns. For example, it can be used to determine there are an +excessive number of small sequential read I/Os indicating that dynamic +readahead is not working correctly. diff --git a/pebble/docs/js/app.js b/pebble/docs/js/app.js new file mode 100644 index 0000000..d944350 --- /dev/null +++ b/pebble/docs/js/app.js @@ -0,0 +1,695 @@ +// TODO(peter) +// - Save pan/zoom settings in query params +// +// TODO(travers): There exists an awkward ordering script loading issue where +// write-throughput.js is loaded first, but contains references to functions +// defined in this file. Work out a better way of modularizing this code. + +const parseTime = d3.timeParse("%Y%m%d"); +const formatTime = d3.timeFormat("%b %d"); +const dateBisector = d3.bisector(d => d.date).left; + +let minDate; +let max = { + date: new Date(), + perChart: {}, + opsSec: 0, + readBytes: 0, + writeBytes: 0, + readAmp: 0, + writeAmp: 0 +}; +let usePerChartMax = false; +let detail; +let detailName; +let detailFormat; + +let annotations = []; + +function getMaxes(chartKey) { + return usePerChartMax ? max.perChart[chartKey] : max; +} + +function styleWidth(e) { + const width = +e.style("width").slice(0, -2); + return Math.round(Number(width)); +} + +function styleHeight(e) { + const height = +e.style("height").slice(0, -2); + return Math.round(Number(height)); +} + +function pathGetY(path, x) { + // Walk along the path using binary search to locate the point + // with the supplied x value. + let start = 0; + let end = path.getTotalLength(); + while (start < end) { + const target = (start + end) / 2; + const pos = path.getPointAtLength(target); + if (Math.abs(pos.x - x) < 0.01) { + // Close enough. + return pos.y; + } else if (pos.x > x) { + end = target; + } else { + start = target; + } + } + return path.getPointAtLength(start).y; +} + +// Pretty formatting of a number in human readable units. +function humanize(s) { + const iecSuffixes = [" B", " KB", " MB", " GB", " TB", " PB", " EB"]; + if (s < 10) { + return "" + s; + } + let e = Math.floor(Math.log(s) / Math.log(1024)); + let suffix = iecSuffixes[Math.floor(e)]; + let val = Math.floor(s / Math.pow(1024, e) * 10 + 0.5) / 10; + return val.toFixed(val < 10 ? 1 : 0) + suffix; +} + +function dirname(path) { + return path.match(/.*\//)[0]; +} + +function equalDay(d1, d2) { + return ( + d1.getYear() == d2.getYear() && + d1.getMonth() == d2.getMonth() && + d1.getDate() == d2.getDate() + ); +} + +function computeSegments(data) { + return data.reduce(function(segments, d) { + if (segments.length == 0) { + segments.push([d]); + return segments; + } + + const lastSegment = segments[segments.length - 1]; + const lastDatum = lastSegment[lastSegment.length - 1]; + const days = Math.round( + (d.date.getTime() - lastDatum.date.getTime()) / + (24 * 60 * 60 * 1000) + ); + if (days == 1) { + lastSegment.push(d); + } else { + segments.push([d]); + } + return segments; + }, []); +} + +function computeGaps(segments) { + let gaps = []; + for (let i = 1; i < segments.length; ++i) { + const last = segments[i - 1]; + const cur = segments[i]; + gaps.push([last[last.length - 1], cur[0]]); + } + + // If the last day is not equal to the current day, add a gap that + // spans to the current day. + const last = segments[segments.length - 1]; + const lastDay = last[last.length - 1]; + if (!equalDay(lastDay.date, max.date)) { + const maxDay = Object.assign({}, lastDay); + maxDay.date = max.date; + gaps.push([lastDay, maxDay]); + } + return gaps; +} + +function renderChart(chart) { + const chartKey = chart.attr("data-key"); + const vals = data[chartKey]; + + const svg = chart.html(""); + + const margin = { top: 25, right: 60, bottom: 25, left: 60 }; + + const width = styleWidth(svg) - margin.left - margin.right, + height = styleHeight(svg) - margin.top - margin.bottom; + + const defs = svg.append("defs"); + const filter = defs + .append("filter") + .attr("id", "textBackground") + .attr("x", 0) + .attr("y", 0) + .attr("width", 1) + .attr("height", 1); + filter.append("feFlood").attr("flood-color", "white"); + filter.append("feComposite").attr("in", "SourceGraphic"); + + defs + .append("clipPath") + .attr("id", chartKey) + .append("rect") + .attr("x", 0) + .attr("y", -margin.top) + .attr("width", width) + .attr("height", margin.top + height + 10); + + const title = svg + .append("text") + .attr("class", "chart-title") + .attr("x", margin.left + width / 2) + .attr("y", 15) + .style("text-anchor", "middle") + .style("font", "8pt sans-serif") + .text(chartKey); + + const g = svg + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + const x = d3.scaleTime().range([0, width]); + const x2 = d3.scaleTime().range([0, width]); + const y1 = d3.scaleLinear().range([height, 0]); + const z = d3.scaleOrdinal(d3.schemeCategory10); + const xFormat = formatTime; + + x.domain([minDate, max.date]); + x2.domain([minDate, max.date]); + + y1.domain([0, getMaxes(chartKey).opsSec]); + + const xAxis = d3.axisBottom(x).ticks(5); + + g + .append("g") + .attr("class", "axis axis--x") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + g + .append("g") + .attr("class", "axis axis--y") + .call(d3.axisLeft(y1).ticks(5)); + + if (!vals) { + // That's all we can draw for an empty chart. + svg + .append("text") + .attr("x", margin.left + width / 2) + .attr("y", margin.top + height / 2) + .style("text-anchor", "middle") + .style("font", "8pt sans-serif") + .text("No data"); + return; + } + + const view = g + .append("g") + .attr("class", "view") + .attr("clip-path", "url(#" + chartKey + ")"); + + const triangle = d3 + .symbol() + .type(d3.symbolTriangle) + .size(12); + view + .selectAll("path.annotation") + .data(annotations) + .enter() + .append("path") + .attr("class", "annotation") + .attr("d", triangle) + .attr("stroke", "#2b2") + .attr("fill", "#2b2") + .attr( + "transform", + d => "translate(" + (x(d.date) + "," + (height + 5) + ")") + ); + + view + .selectAll("line.annotation") + .data(annotations) + .enter() + .append("line") + .attr("class", "annotation") + .attr("fill", "none") + .attr("stroke", "#2b2") + .attr("stroke-width", "1px") + .attr("stroke-dasharray", "1 2") + .attr("x1", d => x(d.date)) + .attr("x2", d => x(d.date)) + .attr("y1", 0) + .attr("y2", height); + + // Divide the data into contiguous days so that we can avoid + // interpolating days where there is missing data. + const segments = computeSegments(vals); + const gaps = computeGaps(segments); + + const line1 = d3 + .line() + .x(d => x(d.date)) + .y(d => y1(d.opsSec)); + const path1 = view + .selectAll(".line1") + .data(segments) + .enter() + .append("path") + .attr("class", "line1") + .attr("d", line1) + .style("stroke", d => z(0)); + + view + .selectAll(".line1-gaps") + .data(gaps) + .enter() + .append("path") + .attr("class", "line1-gaps") + .attr("d", line1) + .attr("opacity", 0.8) + .style("stroke", d => z(0)) + .style("stroke-dasharray", "1 2"); + + let y2 = d3.scaleLinear().range([height, 0]); + let line2; + let path2; + if (detail) { + y2 = d3.scaleLinear().range([height, 0]); + y2.domain([0, detail(getMaxes(chartKey))]); + g + .append("g") + .attr("class", "axis axis--y") + .attr("transform", "translate(" + width + ",0)") + .call( + d3 + .axisRight(y2) + .ticks(5) + .tickFormat(detailFormat) + ); + + line2 = d3 + .line() + .x(d => x(d.date)) + .y(d => y2(detail(d))); + path2 = view + .selectAll(".line2") + .data(segments) + .enter() + .append("path") + .attr("class", "line2") + .attr("d", line2) + .style("stroke", d => z(1)); + view + .selectAll(".line2-gaps") + .data(gaps) + .enter() + .append("path") + .attr("class", "line2-gaps") + .attr("d", line2) + .attr("opacity", 0.8) + .style("stroke", d => z(1)) + .style("stroke-dasharray", "1 2"); + } + + const updateZoom = function(t) { + x.domain(t.rescaleX(x2).domain()); + g.select(".axis--x").call(xAxis); + g.selectAll(".line1").attr("d", line1); + g.selectAll(".line1-gaps").attr("d", line1); + if (detail) { + g.selectAll(".line2").attr("d", line2); + g.selectAll(".line2-gaps").attr("d", line2); + } + g + .selectAll("path.annotation") + .attr( + "transform", + d => "translate(" + (x(d.date) + "," + (height + 5) + ")") + ); + g + .selectAll("line.annotation") + .attr("x1", d => x(d.date)) + .attr("x2", d => x(d.date)); + }; + svg.node().updateZoom = updateZoom; + + const hoverSeries = function(mouse) { + if (!detail) { + return 1; + } + const mousex = mouse[0]; + const mousey = mouse[1] - margin.top; + const path1Y = pathGetY(path1.node(), mousex); + const path2Y = pathGetY(path2.node(), mousex); + return Math.abs(mousey - path1Y) < Math.abs(mousey - path2Y) ? 1 : 2; + }; + + // This is a bit funky: initDate() initializes the date range to + // [today-90,today]. We then allow zooming out by 4x which will + // give a maximum range of 360 days. We limit translation to the + // 360 day period. The funkiness is that it would be more natural + // to start at the maximum zoomed amount and then initialize the + // zoom. But that doesn't work because we want to maintain the + // existing zoom settings whenever we have to (re-)render(). + const zoom = d3 + .zoom() + .scaleExtent([0.25, 2]) + .translateExtent([[-width * 3, 0], [width, 1]]) + .extent([[0, 0], [width, 1]]) + .on("zoom", function() { + const t = d3.event.transform; + if (!d3.event.sourceEvent) { + updateZoom(t); + return; + } + + d3.selectAll(".chart").each(function() { + if (this.updateZoom != null) { + this.updateZoom(t); + } + }); + + d3.selectAll(".chart").each(function() { + this.__zoom = t.translate(0, 0); + }); + + const mouse = d3.mouse(this); + if (mouse) { + mouse[0] -= margin.left; // adjust for rect.mouse position + const date = x.invert(mouse[0]); + const hover = hoverSeries(mouse); + d3.selectAll(".chart.ycsb").each(function() { + this.updateMouse(mouse, date, hover); + }); + } + }); + + svg.call(zoom); + svg.call(zoom.transform, d3.zoomTransform(svg.node())); + + const lineHover = g + .append("line") + .attr("class", "hover") + .style("fill", "none") + .style("stroke", "#f99") + .style("stroke-width", "1px"); + + const dateHover = g + .append("text") + .attr("class", "hover") + .attr("fill", "#f22") + .attr("text-anchor", "middle") + .attr("alignment-baseline", "hanging") + .attr("transform", "translate(0, 0)"); + + const opsHover = g + .append("text") + .attr("class", "hover") + .attr("fill", "#f22") + .attr("text-anchor", "middle") + .attr("transform", "translate(0, 0)"); + + const marker = g + .append("circle") + .attr("class", "hover") + .attr("r", 3) + .style("opacity", "0") + .style("stroke", "#f22") + .style("fill", "#f22"); + + svg.node().updateMouse = function(mouse, date, hover) { + const mousex = mouse[0]; + const mousey = mouse[1]; + const i = dateBisector(vals, date, 1); + const v = + i == vals.length + ? vals[i - 1] + : mousex - x(vals[i - 1].date) < x(vals[i].date) - mousex + ? vals[i - 1] + : vals[i]; + const noData = mousex < x(vals[0].date); + + let lineY = height; + if (!noData) { + if (hover == 1) { + lineY = pathGetY(path1.node(), mousex); + } else { + lineY = pathGetY(path2.node(), mousex); + } + } + + let val, valY, valFormat; + if (hover == 1) { + val = v.opsSec; + valY = y1(val); + valFormat = d3.format(",.0f"); + } else { + val = detail(v); + valY = y2(val); + valFormat = detailFormat; + } + + lineHover + .attr("x1", mousex) + .attr("x2", mousex) + .attr("y1", lineY) + .attr("y2", height); + marker.attr("transform", "translate(" + x(v.date) + "," + valY + ")"); + dateHover + .attr("transform", "translate(" + mousex + "," + (height + 8) + ")") + .text(xFormat(date)); + opsHover + .attr( + "transform", + "translate(" + x(v.date) + "," + (valY - 7) + ")" + ) + .text(valFormat(val)); + }; + + const rect = svg + .append("rect") + .attr("class", "mouse") + .attr("cursor", "move") + .attr("fill", "none") + .attr("pointer-events", "all") + .attr("width", width) + .attr("height", height + margin.top + margin.bottom) + .attr("transform", "translate(" + margin.left + "," + 0 + ")") + .on("mousemove", function() { + const mouse = d3.mouse(this); + const date = x.invert(mouse[0]); + const hover = hoverSeries(mouse); + + let resetTitle = true; + for (let i in annotations) { + if (Math.abs(mouse[0] - x(annotations[i].date)) <= 5) { + title + .style("font-size", "9pt") + .text(annotations[i].message); + resetTitle = false; + break; + } + } + if (resetTitle) { + title.style("font-size", "8pt").text(chartKey); + } + + d3.selectAll(".chart").each(function() { + if (this.updateMouse != null) { + this.updateMouse(mouse, date, hover); + } + }); + }) + .on("mouseover", function() { + d3 + .selectAll(".chart") + .selectAll(".hover") + .style("opacity", 1.0); + }) + .on("mouseout", function() { + d3 + .selectAll(".chart") + .selectAll(".hover") + .style("opacity", 0); + }); +} + +function renderYCSB() { + d3.selectAll(".chart.ycsb").each(function(d, i) { + renderChart(d3.select(this)); + }); +} + +function initData() { + for (key in data) { + data[key] = d3.csvParseRows(data[key], function(d, i) { + return { + date: parseTime(d[0]), + opsSec: +d[1], + readBytes: +d[2], + writeBytes: +d[3], + readAmp: +d[4], + writeAmp: +d[5] + }; + }); + + const vals = data[key]; + max.perChart[key] = { + opsSec: d3.max(vals, d => d.opsSec), + readBytes: d3.max(vals, d => d.readBytes), + writeBytes: d3.max(vals, d => d.writeBytes), + readAmp: d3.max(vals, d => d.readAmp), + writeAmp: d3.max(vals, d => d.writeAmp), + } + max.opsSec = Math.max(max.opsSec, max.perChart[key].opsSec); + max.readBytes = Math.max(max.readBytes, max.perChart[key].readBytes); + max.writeBytes = Math.max( + max.writeBytes, + max.perChart[key].writeBytes, + ); + max.readAmp = Math.max(max.readAmp, max.perChart[key].readAmp); + max.writeAmp = Math.max(max.writeAmp, max.perChart[key].writeAmp); + } + + // Load the write-throughput data and merge with the existing data. We + // return a promise here to allow us to continue to make progress elsewhere. + return fetch(writeThroughputSummaryURL()) + .then(response => response.json()) + .then(wtData => { + for (let key in wtData) { + data[key] = wtData[key]; + } + }); +} + +function initDateRange() { + max.date.setHours(0, 0, 0, 0); + minDate = new Date(new Date().setDate(max.date.getDate() - 90)); +} + +function initAnnotations() { + d3.selectAll(".annotation").each(function() { + const annotation = d3.select(this); + const date = parseTime(annotation.attr("data-date")); + annotations.push({ date: date, message: annotation.text() }); + }); +} + +function setQueryParams() { + var params = new URLSearchParams(); + if (detailName) { + params.set("detail", detailName); + } + if (usePerChartMax) { + params.set("max", "local"); + } + var search = "?" + params; + if (window.location.search != search) { + window.history.pushState(null, null, search); + } +} + +function setDetail(name) { + detail = undefined; + detailFormat = undefined; + detailName = name; + + switch (detailName) { + case "readBytes": + detail = d => d.readBytes; + detailFormat = humanize; + break; + case "writeBytes": + detail = d => d.writeBytes; + detailFormat = humanize; + break; + case "readAmp": + detail = d => d.readAmp; + detailFormat = d3.format(",.1f"); + break; + case "writeAmp": + detail = d => d.writeAmp; + detailFormat = d3.format(",.1f"); + break; + } + + d3.selectAll(".toggle").classed("selected", false); + d3.select("#" + detailName).classed("selected", detail != null); +} + +function initQueryParams() { + var params = new URLSearchParams(window.location.search.substring(1)); + setDetail(params.get("detail")); + usePerChartMax = params.get("max") == "local"; + d3.select("#localMax").classed("selected", usePerChartMax); +} + +function toggleDetail(name) { + const link = d3.select("#" + name); + const selected = !link.classed("selected"); + link.classed("selected", selected); + if (selected) { + setDetail(name); + } else { + setDetail(null); + } + setQueryParams(); + renderYCSB(); +} + +function toggleLocalMax() { + const link = d3.select("#localMax"); + const selected = !link.classed("selected"); + link.classed("selected", selected); + usePerChartMax = selected; + setQueryParams(); + renderYCSB(); +} + +window.onload = function init() { + d3.selectAll(".toggle").each(function() { + const link = d3.select(this); + link.attr("href", 'javascript:toggleDetail("' + link.attr("id") + '")'); + }); + d3.selectAll("#localMax").each(function() { + const link = d3.select(this); + link.attr("href", 'javascript:toggleLocalMax()'); + }); + + initData().then(_ => { + initDateRange(); + initAnnotations(); + initQueryParams(); + + renderYCSB(); + renderWriteThroughputSummary(data); + + // Use the max date to bisect into the workload data to pluck out the + // correct datapoint. + let workloadData = data[writeThroughputWorkload]; + bisectAndRenderWriteThroughputDetail(workloadData, max.date); + + let lastUpdate; + for (let key in data) { + const max = d3.max(data[key], d => d.date); + if (!lastUpdate || lastUpdate < max) { + lastUpdate = max; + } + } + d3.selectAll(".updated") + .text("Last updated: " + d3.timeFormat("%b %e, %Y")(lastUpdate)); + }) + + // By default, display each panel with its local max, which makes spotting + // regressions simpler. + toggleLocalMax(); +}; + +window.onpopstate = function() { + initQueryParams(); + renderYCSB(); +}; + +window.addEventListener("resize", renderYCSB); diff --git a/pebble/docs/js/d3.v5.min.js b/pebble/docs/js/d3.v5.min.js new file mode 100644 index 0000000..a75674c --- /dev/null +++ b/pebble/docs/js/d3.v5.min.js @@ -0,0 +1,2 @@ +// https://d3js.org Version 5.1.0. Copyright 2018 Mike Bostock. +(function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})})(this,function(t){"use strict";function n(t,n){return tn?1:t>=n?0:NaN}function e(t){return 1===t.length&&(t=function(t){return function(e,r){return n(t(e),r)}}(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}function r(t,n){return[t,n]}function i(t){return null===t?NaN:+t}function o(t,n){var e,r,o=t.length,a=0,u=-1,f=0,c=0;if(null==n)for(;++u1)return c/(a-1)}function a(t,n){var e=o(t,n);return e?Math.sqrt(e):e}function u(t,n){var e,r,i,o=t.length,a=-1;if(null==n){for(;++a=e)for(r=i=e;++ae&&(r=e),i=e)for(r=i=e;++ae&&(r=e),i0)return[t];if((r=n0)for(t=Math.ceil(t/a),n=Math.floor(n/a),o=new Array(i=Math.ceil(n-t+1));++u=0?(o>=es?10:o>=rs?5:o>=is?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=es?10:o>=rs?5:o>=is?2:1)}function d(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=es?i*=10:o>=rs?i*=5:o>=is&&(i*=2),n=1)return+e(t[r-1],r-1,t);var r,o=(r-1)*n,a=Math.floor(o),u=+e(t[a],a,t);return u+(+e(t[a+1],a+1,t)-u)*(o-a)}}function g(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++or&&(r=e)}else for(;++o=e)for(r=e;++or&&(r=e);return r}function y(t){for(var n,e,r,i=t.length,o=-1,a=0;++o=0;)for(n=(r=t[i]).length;--n>=0;)e[--a]=r[n];return e}function _(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++oe&&(r=e)}else for(;++o=e)for(r=e;++oe&&(r=e);return r}function b(t){if(!(i=t.length))return[];for(var n=-1,e=_(t,m),r=new Array(e);++n=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),ds.hasOwnProperty(n)?{space:ds[n],local:t}:t}function C(t){var n=k(t);return(n.local?function(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}:function(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===hs&&n.documentElement.namespaceURI===hs?n.createElement(t):n.createElementNS(e,t)}})(n)}function P(){}function z(t){return null==t?P:function(){return this.querySelector(t)}}function R(){return[]}function L(t){return null==t?R:function(){return this.querySelectorAll(t)}}function D(t){return new Array(t.length)}function U(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function q(t,n,e,r,i,o){for(var a,u=0,f=n.length,c=o.length;un?1:t>=n?0:NaN}function B(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function F(t,n){return t.style.getPropertyValue(n)||B(t).getComputedStyle(t,null).getPropertyValue(n)}function I(t){return t.trim().split(/^|\s+/)}function j(t){return t.classList||new H(t)}function H(t){this._node=t,this._names=I(t.getAttribute("class")||"")}function X(t,n){for(var e=j(t),r=-1,i=n.length;++r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Ns.exec(t))?Ct(parseInt(n[1],16)):(n=Ss.exec(t))?new Lt(n[1],n[2],n[3],1):(n=Es.exec(t))?new Lt(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=ks.exec(t))?Pt(n[1],n[2],n[3],n[4]):(n=Cs.exec(t))?Pt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Ps.exec(t))?Dt(n[1],n[2]/100,n[3]/100,1):(n=zs.exec(t))?Dt(n[1],n[2]/100,n[3]/100,n[4]):Rs.hasOwnProperty(t)?Ct(Rs[t]):"transparent"===t?new Lt(NaN,NaN,NaN,0):null}function Ct(t){return new Lt(t>>16&255,t>>8&255,255&t,1)}function Pt(t,n,e,r){return r<=0&&(t=n=e=NaN),new Lt(t,n,e,r)}function zt(t){return t instanceof Et||(t=kt(t)),t?(t=t.rgb(),new Lt(t.r,t.g,t.b,t.opacity)):new Lt}function Rt(t,n,e,r){return 1===arguments.length?zt(t):new Lt(t,n,e,null==r?1:r)}function Lt(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Dt(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new qt(t,n,e,r)}function Ut(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof qt)return new qt(t.h,t.s,t.l,t.opacity);if(t instanceof Et||(t=kt(t)),!t)return new qt;if(t instanceof qt)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,f=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&f<1?0:a,new qt(a,u,f,t.opacity)}(t):new qt(t,n,e,null==r?1:r)}function qt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Ot(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Yt(t){if(t instanceof Ft)return new Ft(t.l,t.a,t.b,t.opacity);if(t instanceof $t){if(isNaN(t.h))return new Ft(t.l,0,0,t.opacity);var n=t.h*Ls;return new Ft(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof Lt||(t=zt(t));var e,r,i=Xt(t.r),o=Xt(t.g),a=Xt(t.b),u=It((.2225045*i+.7168786*o+.0606169*a)/qs);return i===o&&o===a?e=r=u:(e=It((.4360747*i+.3850649*o+.1430804*a)/Us),r=It((.0139322*i+.0971045*o+.7141733*a)/Os)),new Ft(116*u-16,500*(e-u),200*(u-r),t.opacity)}function Bt(t,n,e,r){return 1===arguments.length?Yt(t):new Ft(t,n,e,null==r?1:r)}function Ft(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function It(t){return t>Is?Math.pow(t,1/3):t/Fs+Ys}function jt(t){return t>Bs?t*t*t:Fs*(t-Ys)}function Ht(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Xt(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Gt(t){if(t instanceof $t)return new $t(t.h,t.c,t.l,t.opacity);if(t instanceof Ft||(t=Yt(t)),0===t.a&&0===t.b)return new $t(NaN,0,t.l,t.opacity);var n=Math.atan2(t.b,t.a)*Ds;return new $t(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Vt(t,n,e,r){return 1===arguments.length?Gt(t):new $t(t,n,e,null==r?1:r)}function $t(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}function Wt(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof Zt)return new Zt(t.h,t.s,t.l,t.opacity);t instanceof Lt||(t=zt(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=($s*r+Gs*n-Vs*e)/($s+Gs-Vs),o=r-i,a=(Xs*(e-i)-js*o)/Hs,u=Math.sqrt(a*a+o*o)/(Xs*i*(1-i)),f=u?Math.atan2(a,o)*Ds-120:NaN;return new Zt(f<0?f+360:f,u,i,t.opacity)}(t):new Zt(t,n,e,null==r?1:r)}function Zt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Qt(t,n,e,r,i){var o=t*t,a=o*t;return((1-3*t+3*o-a)*n+(4-6*o+3*a)*e+(1+3*t+3*o-3*a)*r+a*i)/6}function Jt(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r180||e<-180?e-360*Math.round(e/360):e):tn(isNaN(t)?n:t)}function rn(t){return 1==(t=+t)?on:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):tn(isNaN(n)?e:n)}}function on(t,n){var e=n-t;return e?nn(t,e):tn(isNaN(t)?n:t)}function an(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,f.push({i:a,x:cn(e,r)})),o=ol.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:cn(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,f),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:cn(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,f),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:cn(t,e)},{i:u-2,x:cn(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,f),o=a=null,function(t){for(var n,e=-1,r=f.length;++e=0&&n._call.call(null,t),n=n._next;--ml}function Nn(){Tl=(Al=Sl.now())+Nl,ml=xl=0;try{Tn()}finally{ml=0,function(){var t,n,e=Ks,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:Ks=n);tl=t,En(r)}(),Tl=0}}function Sn(){var t=Sl.now(),n=t-Al;n>Ml&&(Nl-=n,Al=t)}function En(t){if(!ml){xl&&(xl=clearTimeout(xl));t-Tl>24?(t<1/0&&(xl=setTimeout(Nn,t-Sl.now()-Nl)),wl&&(wl=clearInterval(wl))):(wl||(Al=Sl.now(),wl=setInterval(Sn,Ml)),ml=1,El(Nn))}}function kn(t,n,e){var r=new Mn;return n=null==n?0:+n,r.restart(function(e){r.stop(),t(e+n)},n,e),r}function Cn(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};(function(t,n,e){function r(f){var c,s,l,h;if(e.state!==zl)return o();for(c in u)if((h=u[c]).name===e.name){if(h.state===Ll)return kn(r);h.state===Dl?(h.state=ql,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete u[c]):+cPl)throw new Error("too late; already scheduled");return e}function zn(t,n){var e=Rn(t,n);if(e.state>Rl)throw new Error("too late; already started");return e}function Rn(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Ln(t,n){var e,r,i,o=t.__transition,a=!0;if(o){n=null==n?null:n+"";for(i in o)(e=o[i]).name===n?(r=e.state>Rl&&e.stateMath.abs(t[1]-U[1])?x=!0:m=!0),U=t,b=!0,Wn(),o()}function o(){var t;switch(y=U[0]-D[0],_=U[1]-D[1],A){case hh:case lh:T&&(y=Math.max(C-u,Math.min(z-d,y)),c=u+y,p=d+y),N&&(_=Math.max(P-l,Math.min(R-v,_)),h=l+_,g=v+_);break;case dh:T<0?(y=Math.max(C-u,Math.min(z-u,y)),c=u+y,p=d):T>0&&(y=Math.max(C-d,Math.min(z-d,y)),c=u,p=d+y),N<0?(_=Math.max(P-l,Math.min(R-l,_)),h=l+_,g=v):N>0&&(_=Math.max(P-v,Math.min(R-v,_)),h=l,g=v+_);break;case ph:T&&(c=Math.max(C,Math.min(z,u-y*T)),p=Math.max(C,Math.min(z,d+y*T))),N&&(h=Math.max(P,Math.min(R,l-_*N)),g=Math.max(P,Math.min(R,v+_*N)))}p0&&(u=c-y),N<0?v=g-_:N>0&&(l=h-_),A=hh,Y.attr("cursor",_h.selection),o());break;default:return}Wn()},!0).on("keyup.brush",function(){switch(t.event.keyCode){case 16:L&&(m=x=L=!1,o());break;case 18:A===ph&&(T<0?d=p:T>0&&(u=c),N<0?v=g:N>0&&(l=h),A=dh,o());break;case 32:A===hh&&(t.event.altKey?(T&&(d=p-y*T,u=c+y*T),N&&(v=g-_*N,l=h+_*N),A=ph):(T<0?d=p:T>0&&(u=c),N<0?v=g:N>0&&(l=h),A=dh),Y.attr("cursor",_h[M]),o());break;default:return}Wn()},!0).on("mousemove.brush",e,!0).on("mouseup.brush",a,!0);_t(t.event.view)}$n(),Ln(w),r.call(w),q.start()}}function u(){var t=this.__brush||{selection:null};return t.extent=c.apply(this,arguments),t.dim=n,t}var f,c=Jn,s=Qn,l=N(e,"start","brush","end"),h=6;return e.move=function(t,e){t.selection?t.on("start.brush",function(){i(this,arguments).beforestart().start()}).on("interrupt.brush end.brush",function(){i(this,arguments).end()}).tween("brush",function(){function t(t){a.selection=1===t&&te(c)?null:s(t),r.call(o),u.brush()}var o=this,a=o.__brush,u=i(o,arguments),f=a.selection,c=n.input("function"==typeof e?e.apply(this,arguments):e,a.extent),s=hn(f,c);return f&&c?t:t(1)}):t.each(function(){var t=arguments,o=this.__brush,a=n.input("function"==typeof e?e.apply(this,t):e,o.extent),u=i(this,t).beforestart();Ln(this),o.selection=null==a||te(a)?null:a,r.call(this),u.start().brush().end()})},o.prototype={beforestart:function(){return 1==++this.active&&(this.state.emitter=this,this.starting=!0),this},start:function(){return this.starting&&(this.starting=!1,this.emit("start")),this},brush:function(){return this.emit("brush"),this},end:function(){return 0==--this.active&&(delete this.state.emitter,this.emit("end")),this},emit:function(t){ot(new function(t,n,e){this.target=t,this.type=n,this.selection=e}(e,t,n.output(this.state.selection)),l.apply,l,[t,this.that,this.args])}},e.extent=function(t){return arguments.length?(c="function"==typeof t?t:Vn([[+t[0][0],+t[0][1]],[+t[1][0],+t[1][1]]]),e):c},e.filter=function(t){return arguments.length?(s="function"==typeof t?t:Vn(!!t),e):s},e.handleSize=function(t){return arguments.length?(h=+t,e):h},e.on=function(){var t=l.on.apply(l,arguments);return t===l?e:t},e}function ee(t){return function(){return t}}function re(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function ie(){return new re}function oe(t){return t.source}function ae(t){return t.target}function ue(t){return t.radius}function fe(t){return t.startAngle}function ce(t){return t.endAngle}function se(){}function le(t,n){var e=new se;if(t instanceof se)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++ir!=d>r&&e<(h-c)*(r-s)/(d-s)+c&&(i=-i)}return i}(t,n[r]))return e;return 0}function xe(){}function we(){function t(t){var e=a(t);if(Array.isArray(e))e=e.slice().sort(_e);else{var r=u(t),i=r[0],o=r[1];e=d(i,o,e),e=s(Math.floor(i/e)*e,Math.floor(o/e)*e,e)}return e.map(function(e){return n(t,e)})}function n(t,n){var r=[],a=[];return function(t,n,r){function a(t){var n,i,o=[t[0][0]+u,t[0][1]+f],a=[t[1][0]+u,t[1][1]+f],c=e(o),s=e(a);(n=p[c])?(i=d[s])?(delete p[n.end],delete d[i.start],n===i?(n.ring.push(a),r(n.ring)):d[n.start]=p[i.end]={start:n.start,end:i.end,ring:n.ring.concat(i.ring)}):(delete p[n.end],n.ring.push(a),p[n.end=s]=n):(n=d[s])?(i=p[c])?(delete d[n.start],delete p[i.end],n===i?(n.ring.push(a),r(n.ring)):d[i.start]=p[n.end]={start:i.start,end:n.end,ring:i.ring.concat(n.ring)}):(delete d[n.start],n.ring.unshift(o),d[n.start=c]=n):d[c]=p[s]={start:c,end:s,ring:[o,a]}}var u,f,c,s,l,h,d=new Array,p=new Array;u=f=-1,s=t[0]>=n,Dh[s<<1].forEach(a);for(;++u=n,Dh[c|s<<1].forEach(a);Dh[s<<0].forEach(a);for(;++f=n,l=t[f*i]>=n,Dh[s<<1|l<<2].forEach(a);++u=n,h=l,l=t[f*i+u+1]>=n,Dh[c|s<<1|l<<2|h<<3].forEach(a);Dh[s|l<<3].forEach(a)}u=-1,l=t[f*i]>=n,Dh[l<<2].forEach(a);for(;++u=n,Dh[l<<2|h<<3].forEach(a);Dh[l<<3].forEach(a)}(t,n,function(e){f(e,t,n),function(t){for(var n=0,e=t.length,r=t[e-1][1]*t[0][0]-t[e-1][0]*t[0][1];++n0?r.push([e]):a.push(e)}),a.forEach(function(t){for(var n,e=0,i=r.length;e0&&a0&&u0&&r>0))throw new Error("invalid size");return i=e,o=r,t},t.thresholds=function(n){return arguments.length?(a="function"==typeof n?n:Array.isArray(n)?be(Lh.call(n)):be(n),t):a},t.smooth=function(n){return arguments.length?(f=n?r:xe,t):f===r},t}function Me(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(f-=t.data[u-o+a*r]),n.data[u-e+a*r]=f/Math.min(u+1,r-1+o-u,o))}function Ae(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(f-=t.data[a+(u-o)*r]),n.data[a+(u-e)*r]=f/Math.min(u+1,i-1+o-u,o))}function Te(t){return t[0]}function Ne(t){return t[1]}function Se(t){return new Function("d","return {"+t.map(function(t,n){return JSON.stringify(t)+": d["+n+"]"}).join(",")+"}")}function Ee(t){function n(t,n){function e(){if(c)return qh;if(s)return s=!1,Uh;var n,e,r=u;if(t.charCodeAt(r)===Oh){for(;u++=a?c=!0:(e=t.charCodeAt(u++))===Yh?s=!0:e===Bh&&(s=!0,t.charCodeAt(u)===Yh&&++u),t.slice(r+1,n-1).replace(/""/g,'"')}for(;u=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a,i=d,!(d=d[l=s<<1|c]))return i[l]=p,t;if(u=+t._x.call(null,d.data),f=+t._y.call(null,d.data),n===u&&e===f)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(c=n>=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a}while((l=s<<1|c)==(h=(f>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function Ye(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Be(t){return t[0]}function Fe(t){return t[1]}function Ie(t,n,e){var r=new je(null==n?Be:n,null==e?Fe:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function je(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function He(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}function Xe(t){return t.x+t.vx}function Ge(t){return t.y+t.vy}function Ve(t){return t.index}function $e(t,n){var e=t.get(n);if(!e)throw new Error("missing: "+n);return e}function We(t){return t.x}function Ze(t){return t.y}function Qe(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,r=t.slice(0,e);return[r.length>1?r[0]+r.slice(2):r,+t.slice(e+1)]}function Je(t){return(t=Qe(Math.abs(t)))?t[1]:NaN}function Ke(t,n){var e=Qe(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}function tr(t){return new nr(t)}function nr(t){if(!(n=ud.exec(t)))throw new Error("invalid format: "+t);var n,e=n[1]||" ",r=n[2]||">",i=n[3]||"-",o=n[4]||"",a=!!n[5],u=n[6]&&+n[6],f=!!n[7],c=n[8]&&+n[8].slice(1),s=n[9]||"";"n"===s?(f=!0,s="g"):ad[s]||(s=""),(a||"0"===e&&"="===r)&&(a=!0,e="0",r="="),this.fill=e,this.align=r,this.sign=i,this.symbol=o,this.zero=a,this.width=u,this.comma=f,this.precision=c,this.type=s}function er(t){return t}function rr(t){function n(t){function n(t){var n,r,a,s=g,m=y;if("c"===v)m=_(t)+m,t="";else{var x=(t=+t)<0;if(t=_(Math.abs(t),p),x&&0==+t&&(x=!1),s=(x?"("===c?c:"-":"-"===c||"("===c?"":c)+s,m=("s"===v?cd[8+rd/3]:"")+m+(x&&"("===c?")":""),b)for(n=-1,r=t.length;++n(a=t.charCodeAt(n))||a>57){m=(46===a?i+t.slice(n+1):t.slice(n))+m,t=t.slice(0,n);break}}d&&!l&&(t=e(t,1/0));var w=s.length+t.length+m.length,M=w>1)+s+t+m+M.slice(w);break;default:t=M+s+t+m}return o(t)}var u=(t=tr(t)).fill,f=t.align,c=t.sign,s=t.symbol,l=t.zero,h=t.width,d=t.comma,p=t.precision,v=t.type,g="$"===s?r[0]:"#"===s&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",y="$"===s?r[1]:/[%p]/.test(v)?a:"",_=ad[v],b=!v||/[defgprs%]/.test(v);return p=null==p?v?6:12:/[gprs]/.test(v)?Math.max(1,Math.min(21,p)):Math.max(0,Math.min(20,p)),n.toString=function(){return t+""},n}var e=t.grouping&&t.thousands?function(t,n){return function(e,r){for(var i=e.length,o=[],a=0,u=t[0],f=0;i>0&&u>0&&(f+u+1>r&&(u=Math.max(1,r-f)),o.push(e.substring(i-=u,i+u)),!((f+=u+1)>r));)u=t[a=(a+1)%t.length];return o.reverse().join(n)}}(t.grouping,t.thousands):er,r=t.currency,i=t.decimal,o=t.numerals?function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}}(t.numerals):er,a=t.percent||"%";return{format:n,formatPrefix:function(t,e){var r=n((t=tr(t),t.type="f",t)),i=3*Math.max(-8,Math.min(8,Math.floor(Je(e)/3))),o=Math.pow(10,-i),a=cd[8+i/3];return function(t){return r(o*t)+a}}}}function ir(n){return fd=rr(n),t.format=fd.format,t.formatPrefix=fd.formatPrefix,fd}function or(t){return Math.max(0,-Je(Math.abs(t)))}function ar(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Je(n)/3)))-Je(Math.abs(t)))}function ur(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Je(n)-Je(t))+1}function fr(){return new cr}function cr(){this.reset()}function sr(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}function lr(t){return t>1?0:t<-1?Hd:Math.acos(t)}function hr(t){return t>1?Xd:t<-1?-Xd:Math.asin(t)}function dr(t){return(t=ip(t/2))*t}function pr(){}function vr(t,n){t&&cp.hasOwnProperty(t.type)&&cp[t.type](t,n)}function gr(t,n,e){var r,i=-1,o=t.length-e;for(n.lineStart();++i=0?1:-1,i=r*e,o=Kd(n),a=ip(n),u=pd*a,f=dd*o+u*Kd(i),c=u*r*ip(i);sp.add(Jd(c,f)),hd=t,dd=o,pd=a}function Mr(t){return[Jd(t[1],t[0]),hr(t[2])]}function Ar(t){var n=t[0],e=t[1],r=Kd(e);return[r*Kd(n),r*ip(n),ip(e)]}function Tr(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Nr(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function Sr(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function Er(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function kr(t){var n=ap(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function Cr(t,n){Md.push(Ad=[vd=t,yd=t]),n_d&&(_d=n)}function Pr(t,n){var e=Ar([t*Wd,n*Wd]);if(wd){var r=Nr(wd,e),i=Nr([r[1],-r[0],0],r);kr(i),i=Mr(i);var o,a=t-bd,u=a>0?1:-1,f=i[0]*$d*u,c=Zd(a)>180;c^(u*bd_d&&(_d=o):(f=(f+360)%360-180,c^(u*bd_d&&(_d=n))),c?tqr(vd,yd)&&(yd=t):qr(t,yd)>qr(vd,yd)&&(vd=t):yd>=vd?(tyd&&(yd=t)):t>bd?qr(vd,t)>qr(vd,yd)&&(yd=t):qr(t,yd)>qr(vd,yd)&&(vd=t)}else Md.push(Ad=[vd=t,yd=t]);n_d&&(_d=n),wd=e,bd=t}function zr(){pp.point=Pr}function Rr(){Ad[0]=vd,Ad[1]=yd,pp.point=Cr,wd=null}function Lr(t,n){if(wd){var e=t-bd;dp.add(Zd(e)>180?e+(e>0?360:-360):e)}else md=t,xd=n;hp.point(t,n),Pr(t,n)}function Dr(){hp.lineStart()}function Ur(){Lr(md,xd),hp.lineEnd(),Zd(dp)>Id&&(vd=-(yd=180)),Ad[0]=vd,Ad[1]=yd,wd=null}function qr(t,n){return(n-=t)<0?n+360:n}function Or(t,n){return t[0]-n[0]}function Yr(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nHd?t-Vd:t<-Hd?t+Vd:t,n]}function Kr(t,n,e){return(t%=Vd)?n||e?Qr(ni(t),ei(n,e)):ni(t):n||e?ei(n,e):Jr}function ti(t){return function(n,e){return n+=t,[n>Hd?n-Vd:n<-Hd?n+Vd:n,e]}}function ni(t){var n=ti(t);return n.invert=ti(-t),n}function ei(t,n){function e(t,n){var e=Kd(n),u=Kd(t)*e,f=ip(t)*e,c=ip(n),s=c*r+u*i;return[Jd(f*o-s*a,u*r-c*i),hr(s*o+f*a)]}var r=Kd(t),i=ip(t),o=Kd(n),a=ip(n);return e.invert=function(t,n){var e=Kd(n),u=Kd(t)*e,f=ip(t)*e,c=ip(n),s=c*o-f*a;return[Jd(f*o+c*a,u*r+s*i),hr(s*r-u*i)]},e}function ri(t){function n(n){return n=t(n[0]*Wd,n[1]*Wd),n[0]*=$d,n[1]*=$d,n}return t=Kr(t[0]*Wd,t[1]*Wd,t.length>2?t[2]*Wd:0),n.invert=function(n){return n=t.invert(n[0]*Wd,n[1]*Wd),n[0]*=$d,n[1]*=$d,n},n}function ii(t,n,e,r,i,o){if(e){var a=Kd(n),u=ip(n),f=r*e;null==i?(i=n+r*Vd,o=n-f/2):(i=oi(a,i),o=oi(a,o),(r>0?io)&&(i+=r*Vd));for(var c,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function ui(t,n){return Zd(t[0]-n[0])=0;--o)i.point((s=c[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}c=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function si(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,T=A*M,N=T>Hd,S=v*x;if(Sp.add(Jd(S*A*ip(T),g*w+S*Kd(T))),a+=N?M+A*Vd:M,N^d>=e^b>=e){var E=Nr(Ar(h),Ar(_));kr(E);var k=Nr(o,E);kr(k);var C=(N^M>=0?-1:1)*hr(k[2]);(r>C||r===C&&(E[0]||E[1]))&&(u+=N^M>=0?1:-1)}}return(a<-Id||a0){for(b||(i.polygonStart(),b=!0),i.lineStart(),t=0;t1&&2&o&&a.push(a.pop().concat(a.shift())),d.push(a.filter(di))}var h,d,p,v=n(i),g=ai(),_=n(g),b=!1,m={point:o,lineStart:u,lineEnd:f,polygonStart:function(){m.point=c,m.lineStart=s,m.lineEnd=l,d=[],h=[]},polygonEnd:function(){m.point=o,m.lineStart=u,m.lineEnd=f,d=y(d);var t=li(h,r);d.length?(b||(i.polygonStart(),b=!0),ci(d,pi,t,e,i)):t&&(b||(i.polygonStart(),b=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),b&&(i.polygonEnd(),b=!1),d=h=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}};return m}}function di(t){return t.length>1}function pi(t,n){return((t=t.x)[0]<0?t[1]-Xd-Id:Xd-t[1])-((n=n.x)[0]<0?n[1]-Xd-Id:Xd-n[1])}function vi(t){function n(t,n){return Kd(t)*Kd(n)>i}function e(t,n,e){var r=[1,0,0],o=Nr(Ar(t),Ar(n)),a=Tr(o,o),u=o[0],f=a-u*u;if(!f)return!e&&t;var c=i*a/f,s=-i*u/f,l=Nr(r,o),h=Er(r,c);Sr(h,Er(o,s));var d=l,p=Tr(h,d),v=Tr(d,d),g=p*p-v*(Tr(h,h)-1);if(!(g<0)){var y=ap(g),_=Er(d,(-p-y)/v);if(Sr(_,h),_=Mr(_),!e)return _;var b,m=t[0],x=n[0],w=t[1],M=n[1];x0^_[1]<(Zd(_[0]-m)Hd^(m<=_[0]&&_[0]<=x)){var N=Er(d,(-p+y)/v);return Sr(N,h),[_,Mr(N)]}}}function r(n,e){var r=a?t:Hd-t,i=0;return n<-r?i|=1:n>r&&(i|=2),e<-r?i|=4:e>r&&(i|=8),i}var i=Kd(t),o=6*Wd,a=i>0,u=Zd(i)>Id;return hi(n,function(t){var i,o,f,c,s;return{lineStart:function(){c=f=!1,s=1},point:function(l,h){var d,p=[l,h],v=n(l,h),g=a?v?0:r(l,h):v?r(l+(l<0?Hd:-Hd),h):0;if(!i&&(c=f=v)&&t.lineStart(),v!==f&&(!(d=e(i,p))||ui(i,d)||ui(p,d))&&(p[0]+=Id,p[1]+=Id,v=n(p[0],p[1])),v!==f)s=0,v?(t.lineStart(),d=e(p,i),t.point(d[0],d[1])):(d=e(i,p),t.point(d[0],d[1]),t.lineEnd()),i=d;else if(u&&i&&a^v){var y;g&o||!(y=e(p,i,!0))||(s=0,a?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1])))}!v||i&&ui(i,p)||t.point(p[0],p[1]),i=p,f=v,o=g},lineEnd:function(){f&&t.lineEnd(),i=null},clean:function(){return s|(c&&f)<<1}}},function(n,e,r,i){ii(i,t,o,r,n,e)},a?[0,-t]:[-Hd,t-Hd])}function gi(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,c){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||f(i,o)<0^u>0)do{c.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else c.point(o[0],o[1])}function a(r,i){return Zd(r[0]-t)0?0:3:Zd(r[0]-e)0?2:1:Zd(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return f(t.x,n.x)}function f(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){function f(t,n){i(t,n)&&w.point(t,n)}function c(o,a){var u=i(o,a);if(l&&h.push([o,a]),m)d=o,p=a,v=u,m=!1,u&&(w.lineStart(),w.point(o,a));else if(u&&b)w.point(o,a);else{var f=[g=Math.max(Cp,Math.min(kp,g)),_=Math.max(Cp,Math.min(kp,_))],c=[o=Math.max(Cp,Math.min(kp,o)),a=Math.max(Cp,Math.min(kp,a))];!function(t,n,e,r,i,o){var a,u=t[0],f=t[1],c=0,s=1,l=n[0]-u,h=n[1]-f;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>c&&(c=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>c&&(c=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>c&&(c=a)}if(a=o-f,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>c&&(c=a)}else if(h>0){if(a0&&(t[0]=u+c*l,t[1]=f+c*h),s<1&&(n[0]=u+s*l,n[1]=f+s*h),!0}}}}}(f,c,t,n,e,r)?u&&(w.lineStart(),w.point(o,a),x=!1):(b||(w.lineStart(),w.point(f[0],f[1])),w.point(c[0],c[1]),u||w.lineEnd(),x=!1)}g=o,_=a,b=u}var s,l,h,d,p,v,g,_,b,m,x,w=a,M=ai(),A={point:f,lineStart:function(){A.point=c,l&&l.push(h=[]),m=!0,b=!1,g=_=NaN},lineEnd:function(){s&&(c(d,p),v&&b&&M.rejoin(),s.push(M.result())),A.point=f,b&&w.lineEnd()},polygonStart:function(){w=M,s=[],l=[],x=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=l.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=x&&n,i=(s=y(s)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&ci(s,u,n,o,a),a.polygonEnd()),w=a,s=l=h=null}};return A}}function yi(){zp.point=zp.lineEnd=pr}function _i(t,n){gp=t*=Wd,yp=ip(n*=Wd),_p=Kd(n),zp.point=bi}function bi(t,n){t*=Wd;var e=ip(n*=Wd),r=Kd(n),i=Zd(t-gp),o=Kd(i),a=r*ip(i),u=_p*e-yp*r*o,f=yp*e+_p*r*o;Pp.add(Jd(ap(a*a+u*u),f)),gp=t,yp=e,_p=r}function mi(t){return Pp.reset(),_r(t,zp),+Pp}function xi(t,n){return Rp[0]=t,Rp[1]=n,mi(Lp)}function wi(t,n){return!(!t||!Up.hasOwnProperty(t.type))&&Up[t.type](t,n)}function Mi(t,n){return 0===xi(t,n)}function Ai(t,n){var e=xi(t[0],t[1]);return xi(t[0],n)+xi(n,t[1])<=e+Id}function Ti(t,n){return!!li(t.map(Ni),Si(n))}function Ni(t){return(t=t.map(Si)).pop(),t}function Si(t){return[t[0]*Wd,t[1]*Wd]}function Ei(t,n,e){var r=s(t,n-Id,e).concat(n);return function(t){return r.map(function(n){return[t,n]})}}function ki(t,n,e){var r=s(t,n-Id,e).concat(n);return function(t){return r.map(function(n){return[n,t]})}}function Ci(){function t(){return{type:"MultiLineString",coordinates:n()}}function n(){return s(tp(o/y)*y,i,y).map(d).concat(s(tp(c/_)*_,f,_).map(p)).concat(s(tp(r/v)*v,e,v).filter(function(t){return Zd(t%y)>Id}).map(l)).concat(s(tp(u/g)*g,a,g).filter(function(t){return Zd(t%_)>Id}).map(h))}var e,r,i,o,a,u,f,c,l,h,d,p,v=10,g=v,y=90,_=360,b=2.5;return t.lines=function(){return n().map(function(t){return{type:"LineString",coordinates:t}})},t.outline=function(){return{type:"Polygon",coordinates:[d(o).concat(p(f).slice(1),d(i).reverse().slice(1),p(c).reverse().slice(1))]}},t.extent=function(n){return arguments.length?t.extentMajor(n).extentMinor(n):t.extentMinor()},t.extentMajor=function(n){return arguments.length?(o=+n[0][0],i=+n[1][0],c=+n[0][1],f=+n[1][1],o>i&&(n=o,o=i,i=n),c>f&&(n=c,c=f,f=n),t.precision(b)):[[o,c],[i,f]]},t.extentMinor=function(n){return arguments.length?(r=+n[0][0],e=+n[1][0],u=+n[0][1],a=+n[1][1],r>e&&(n=r,r=e,e=n),u>a&&(n=u,u=a,a=n),t.precision(b)):[[r,u],[e,a]]},t.step=function(n){return arguments.length?t.stepMajor(n).stepMinor(n):t.stepMinor()},t.stepMajor=function(n){return arguments.length?(y=+n[0],_=+n[1],t):[y,_]},t.stepMinor=function(n){return arguments.length?(v=+n[0],g=+n[1],t):[v,g]},t.precision=function(n){return arguments.length?(b=+n,l=Ei(u,a,90),h=ki(r,e,b),d=Ei(c,f,90),p=ki(o,i,b),t):b},t.extentMajor([[-180,-90+Id],[180,90-Id]]).extentMinor([[-180,-80-Id],[180,80+Id]])}function Pi(t){return t}function zi(){Yp.point=Ri}function Ri(t,n){Yp.point=Li,bp=xp=t,mp=wp=n}function Li(t,n){Op.add(wp*t-xp*n),xp=t,wp=n}function Di(){Li(bp,mp)}function Ui(t,n){Xp+=t,Gp+=n,++Vp}function qi(){tv.point=Oi}function Oi(t,n){tv.point=Yi,Ui(Tp=t,Np=n)}function Yi(t,n){var e=t-Tp,r=n-Np,i=ap(e*e+r*r);$p+=i*(Tp+t)/2,Wp+=i*(Np+n)/2,Zp+=i,Ui(Tp=t,Np=n)}function Bi(){tv.point=Ui}function Fi(){tv.point=ji}function Ii(){Hi(Mp,Ap)}function ji(t,n){tv.point=Hi,Ui(Mp=Tp=t,Ap=Np=n)}function Hi(t,n){var e=t-Tp,r=n-Np,i=ap(e*e+r*r);$p+=i*(Tp+t)/2,Wp+=i*(Np+n)/2,Zp+=i,Qp+=(i=Np*t-Tp*n)*(Tp+t),Jp+=i*(Np+n),Kp+=3*i,Ui(Tp=t,Np=n)}function Xi(t){this._context=t}function Gi(t,n){uv.point=Vi,ev=iv=t,rv=ov=n}function Vi(t,n){iv-=t,ov-=n,av.add(ap(iv*iv+ov*ov)),iv=t,ov=n}function $i(){this._string=[]}function Wi(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function Zi(t){return function(n){var e=new Qi;for(var r in t)e[r]=t[r];return e.stream=n,e}}function Qi(){}function Ji(t,n,e){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),_r(e,t.stream(Hp)),n(Hp.result()),null!=r&&t.clipExtent(r),t}function Ki(t,n,e){return Ji(t,function(e){var r=n[1][0]-n[0][0],i=n[1][1]-n[0][1],o=Math.min(r/(e[1][0]-e[0][0]),i/(e[1][1]-e[0][1])),a=+n[0][0]+(r-o*(e[1][0]+e[0][0]))/2,u=+n[0][1]+(i-o*(e[1][1]+e[0][1]))/2;t.scale(150*o).translate([a,u])},e)}function to(t,n,e){return Ki(t,[[0,0],n],e)}function no(t,n,e){return Ji(t,function(e){var r=+n,i=r/(e[1][0]-e[0][0]),o=(r-i*(e[1][0]+e[0][0]))/2,a=-i*e[0][1];t.scale(150*i).translate([o,a])},e)}function eo(t,n,e){return Ji(t,function(e){var r=+n,i=r/(e[1][1]-e[0][1]),o=-i*e[0][0],a=(r-i*(e[1][1]+e[0][1]))/2;t.scale(150*i).translate([o,a])},e)}function ro(t,n){return+n?function(t,n){function e(r,i,o,a,u,f,c,s,l,h,d,p,v,g){var y=c-r,_=s-i,b=y*y+_*_;if(b>4*n&&v--){var m=a+h,x=u+d,w=f+p,M=ap(m*m+x*x+w*w),A=hr(w/=M),T=Zd(Zd(w)-1)n||Zd((y*k+_*C)/b-.5)>.3||a*h+u*d+f*p2?t[2]%360*Wd:0,e()):[b*$d,m*$d,x*$d]},n.angle=function(t){return arguments.length?(w=t%360*Wd,e()):w*$d},n.precision=function(t){return arguments.length?(c=ro(s,S=t*t),r()):ap(S)},n.fitExtent=function(t,e){return Ki(n,t,e)},n.fitSize=function(t,e){return to(n,t,e)},n.fitWidth=function(t,e){return no(n,t,e)},n.fitHeight=function(t,e){return eo(n,t,e)},function(){return i=t.apply(this,arguments),n.invert=i.invert&&function(t){return(t=l.invert(t[0],t[1]))&&[t[0]*$d,t[1]*$d]},e()}}function uo(t){var n=0,e=Hd/3,r=ao(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*Wd,e=t[1]*Wd):[n*$d,e*$d]},i}function fo(t,n){function e(t,n){var e=ap(o-2*i*ip(n))/i;return[e*ip(t*=i),a-e*Kd(t)]}var r=ip(t),i=(r+ip(n))/2;if(Zd(i)0?n<-Xd+Id&&(n=-Xd+Id):n>Xd-Id&&(n=Xd-Id);var e=o/rp(go(n),i);return[e*ip(i*t),o-e*Kd(i*t)]}var r=Kd(t),i=t===n?ip(t):ep(r/Kd(n))/ep(go(n)/go(t)),o=r*rp(go(t),i)/i;return i?(e.invert=function(t,n){var e=o-n,r=op(i)*ap(t*t+e*e);return[Jd(t,Zd(e))/i*op(e),2*Qd(rp(o/r,1/i))-Xd]},e):po}function _o(t,n){return[t,n]}function bo(t,n){function e(t,n){var e=o-n,r=i*t;return[e*ip(r),o-e*Kd(r)]}var r=Kd(t),i=t===n?ip(t):(r-Kd(n))/(n-t),o=r/i+t;return Zd(i)=0;)n+=e[r].value;else n=1;t.value=n}function Co(t,n){var e,r,i,o,a,u=new Lo(t),f=+t.value&&(u.value=t.value),c=[u];for(null==n&&(n=Po);e=c.pop();)if(f&&(e.value=+e.data.value),(i=n(e.data))&&(a=i.length))for(e.children=new Array(a),o=a-1;o>=0;--o)c.push(r=e.children[o]=new Lo(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(Ro)}function Po(t){return t.children}function zo(t){t.data=t.data.data}function Ro(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Lo(t){this.data=t,this.depth=this.height=0,this.parent=null}function Do(t){for(var n,e,r=0,i=(t=function(t){for(var n,e,r=t.length;r;)e=Math.random()*r--|0,n=t[r],t[r]=t[e],t[e]=n;return t}(dv.call(t))).length,o=[];r0&&e*e>r*r+i*i}function Oo(t,n){for(var e=0;e(a*=a)?(r=(c+a-i)/(2*c),o=Math.sqrt(Math.max(0,a/c-r*r)),e.x=t.x-r*u-o*f,e.y=t.y-r*f+o*u):(r=(c+i-a)/(2*c),o=Math.sqrt(Math.max(0,i/c-r*r)),e.x=n.x+r*u-o*f,e.y=n.y+r*f+o*u)):(e.x=n.x+e.r,e.y=n.y)}function Io(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function jo(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function Ho(t){this._=t,this.next=null,this.previous=null}function Xo(t){if(!(i=t.length))return 0;var n,e,r,i,o,a,u,f,c,s,l;if(n=t[0],n.x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Fo(e,n,r=t[2]),n=new Ho(n),e=new Ho(e),r=new Ho(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(u=3;uh&&(h=u),g=s*s*v,(d=Math.max(h/g,g/l))>p){s-=u;break}p=d}y.push(a={value:s,dice:f1&&la(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function pa(){return Math.random()}function va(t){function n(n){var o=n+"",a=e.get(o);if(!a){if(i!==kv)return i;e.set(o,a=r.push(n))}return t[(a-1)%t.length]}var e=le(),r=[],i=kv;return t=null==t?[]:Ev.call(t),n.domain=function(t){if(!arguments.length)return r.slice();r=[],e=le();for(var i,o,a=-1,u=t.length;++a2?wa:xa,o=a=null,r}function r(n){return(o||(o=i(u,f,s?function(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=n?0:t>=e?1:r(t)}}}(t):t,c)))(+n)}var i,o,a,u=Cv,f=Cv,c=hn,s=!1;return r.invert=function(t){return(a||(a=i(f,u,ma,s?function(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:r(t)}}}(n):n)))(+t)},r.domain=function(t){return arguments.length?(u=Sv.call(t,ba),e()):u.slice()},r.range=function(t){return arguments.length?(f=Ev.call(t),e()):f.slice()},r.rangeRound=function(t){return f=Ev.call(t),c=dn,e()},r.clamp=function(t){return arguments.length?(s=!!t,e()):s},r.interpolate=function(t){return arguments.length?(c=t,e()):c},e()}function Ta(n){var e=n.domain;return n.ticks=function(t){var n=e();return l(n[0],n[n.length-1],null==t?10:t)},n.tickFormat=function(n,r){return function(n,e,r){var i,o=n[0],a=n[n.length-1],u=d(o,a,null==e?10:e);switch((r=tr(null==r?",f":r)).type){case"s":var f=Math.max(Math.abs(o),Math.abs(a));return null!=r.precision||isNaN(i=ar(u,f))||(r.precision=i),t.formatPrefix(r,f);case"":case"e":case"g":case"p":case"r":null!=r.precision||isNaN(i=ur(u,Math.max(Math.abs(o),Math.abs(a))))||(r.precision=i-("e"===r.type));break;case"f":case"%":null!=r.precision||isNaN(i=or(u))||(r.precision=i-2*("%"===r.type))}return t.format(r)}(e(),n,r)},n.nice=function(t){null==t&&(t=10);var r,i=e(),o=0,a=i.length-1,u=i[o],f=i[a];return f0?r=h(u=Math.floor(u/r)*r,f=Math.ceil(f/r)*r,t):r<0&&(r=h(u=Math.ceil(u*r)/r,f=Math.floor(f*r)/r,t)),r>0?(i[o]=Math.floor(u/r)*r,i[a]=Math.ceil(f/r)*r,e(i)):r<0&&(i[o]=Math.ceil(u*r)/r,i[a]=Math.floor(f*r)/r,e(i)),n},n}function Na(){var t=Aa(ma,cn);return t.copy=function(){return Ma(t,Na())},Ta(t)}function Sa(){function t(t){return+t}var n=[0,1];return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=Sv.call(e,ba),t):n.slice()},t.copy=function(){return Sa().domain(n)},Ta(t)}function Ea(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a0){for(;df)break;g.push(h)}}else for(;d=1;--s)if(!((h=c*s)f)break;g.push(h)}}else g=l(d,p,Math.min(p-d,v)).map(a);return n?g.reverse():g},e.tickFormat=function(n,r){if(null==r&&(r=10===i?".0e":","),"function"!=typeof r&&(r=t.format(r)),n===1/0)return r;null==n&&(n=10);var u=Math.max(1,i*n/e.ticks().length);return function(t){var n=t/a(Math.round(o(t)));return n*i0?o[n-1]:r[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},t.copy=function(){return Ya().domain([e,r]).range(a)},Ta(t)}function Ba(){function t(t){if(t<=t)return e[Qc(n,t,0,r)]}var n=[.5],e=[0,1],r=1;return t.domain=function(i){return arguments.length?(n=Ev.call(i),r=Math.min(n.length,e.length-1),t):n.slice()},t.range=function(i){return arguments.length?(e=Ev.call(i),r=Math.min(n.length,e.length-1),t):e.slice()},t.invertExtent=function(t){var r=e.indexOf(t);return[n[r-1],n[r]]},t.copy=function(){return Ba().domain(n).range(e)},t}function Fa(t,n,e,r){function i(n){return t(n=new Date(+n)),n}return i.floor=i,i.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},i.round=function(t){var n=i(t),e=i.ceil(t);return t-n0))return u;do{u.push(a=new Date(+e)),n(e,o),t(e)}while(a=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return Pv.setTime(+n),zv.setTime(+r),t(Pv),t(zv),Math.floor(e(Pv,zv))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}function Ia(t){return Fa(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Dv)/Uv})}function ja(t){return Fa(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Uv})}function Ha(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Xa(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function Ga(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function Va(t){function n(t,n){return function(e){var r,i,o,a=[],u=-1,f=0,c=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in a||(a.w=1),"Z"in a?(i=(o=(i=Xa(Ga(a.y))).getUTCDay())>4||0===o?gg.ceil(i):gg(i),i=dg.offset(i,7*(a.V-1)),a.y=i.getUTCFullYear(),a.m=i.getUTCMonth(),a.d=i.getUTCDate()+(a.w+6)%7):(i=(o=(i=n(Ga(a.y))).getDay())>4||0===o?Gv.ceil(i):Gv(i),i=jv.offset(i,7*(a.V-1)),a.y=i.getFullYear(),a.m=i.getMonth(),a.d=i.getDate()+(a.w+6)%7)}else("W"in a||"U"in a)&&("w"in a||(a.w="u"in a?a.u%7:"W"in a?1:0),o="Z"in a?Xa(Ga(a.y)).getUTCDay():n(Ga(a.y)).getDay(),a.m=0,a.d="W"in a?(a.w+6)%7+7*a.W-(o+5)%7:a.w+7*a.U-(o+6)%7);return"Z"in a?(a.H+=a.Z/100|0,a.M+=a.Z%100,Xa(a)):n(a)}}function r(t,n,e,r){for(var i,o,a=0,u=n.length,f=e.length;a=f)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=A[i in Lg?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}var i=t.dateTime,o=t.date,a=t.time,u=t.periods,f=t.days,c=t.shortDays,s=t.months,l=t.shortMonths,h=Za(u),d=Qa(u),p=Za(f),v=Qa(f),g=Za(c),y=Qa(c),_=Za(s),b=Qa(s),m=Za(l),x=Qa(l),w={a:function(t){return c[t.getDay()]},A:function(t){return f[t.getDay()]},b:function(t){return l[t.getMonth()]},B:function(t){return s[t.getMonth()]},c:null,d:yu,e:yu,f:wu,H:_u,I:bu,j:mu,L:xu,m:Mu,M:Au,p:function(t){return u[+(t.getHours()>=12)]},Q:Ju,s:Ku,S:Tu,u:Nu,U:Su,V:Eu,w:ku,W:Cu,x:null,X:null,y:Pu,Y:zu,Z:Ru,"%":Qu},M={a:function(t){return c[t.getUTCDay()]},A:function(t){return f[t.getUTCDay()]},b:function(t){return l[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:Lu,e:Lu,f:Yu,H:Du,I:Uu,j:qu,L:Ou,m:Bu,M:Fu,p:function(t){return u[+(t.getUTCHours()>=12)]},Q:Ju,s:Ku,S:Iu,u:ju,U:Hu,V:Xu,w:Gu,W:Vu,x:null,X:null,y:$u,Y:Wu,Z:Zu,"%":Qu},A={a:function(t,n,e){var r=g.exec(n.slice(e));return r?(t.w=y[r[0].toLowerCase()],e+r[0].length):-1},A:function(t,n,e){var r=p.exec(n.slice(e));return r?(t.w=v[r[0].toLowerCase()],e+r[0].length):-1},b:function(t,n,e){var r=m.exec(n.slice(e));return r?(t.m=x[r[0].toLowerCase()],e+r[0].length):-1},B:function(t,n,e){var r=_.exec(n.slice(e));return r?(t.m=b[r[0].toLowerCase()],e+r[0].length):-1},c:function(t,n,e){return r(t,i,n,e)},d:uu,e:uu,f:du,H:cu,I:cu,j:fu,L:hu,m:au,M:su,p:function(t,n,e){var r=h.exec(n.slice(e));return r?(t.p=d[r[0].toLowerCase()],e+r[0].length):-1},Q:vu,s:gu,S:lu,u:Ka,U:tu,V:nu,w:Ja,W:eu,x:function(t,n,e){return r(t,o,n,e)},X:function(t,n,e){return r(t,a,n,e)},y:iu,Y:ru,Z:ou,"%":pu};return w.x=n(o,w),w.X=n(a,w),w.c=n(i,w),M.x=n(o,M),M.X=n(a,M),M.c=n(i,M),{format:function(t){var e=n(t+="",w);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",Ha);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",M);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,Xa);return n.toString=function(){return t},n}}}function $a(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function ou(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function au(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function uu(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function fu(t,n,e){var r=Dg.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function cu(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function su(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function lu(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function hu(t,n,e){var r=Dg.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function du(t,n,e){var r=Dg.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function pu(t,n,e){var r=Ug.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function vu(t,n,e){var r=Dg.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function gu(t,n,e){var r=Dg.exec(n.slice(e));return r?(t.Q=1e3*+r[0],e+r[0].length):-1}function yu(t,n){return $a(t.getDate(),n,2)}function _u(t,n){return $a(t.getHours(),n,2)}function bu(t,n){return $a(t.getHours()%12||12,n,2)}function mu(t,n){return $a(1+jv.count(ug(t),t),n,3)}function xu(t,n){return $a(t.getMilliseconds(),n,3)}function wu(t,n){return xu(t,n)+"000"}function Mu(t,n){return $a(t.getMonth()+1,n,2)}function Au(t,n){return $a(t.getMinutes(),n,2)}function Tu(t,n){return $a(t.getSeconds(),n,2)}function Nu(t){var n=t.getDay();return 0===n?7:n}function Su(t,n){return $a(Xv.count(ug(t),t),n,2)}function Eu(t,n){var e=t.getDay();return t=e>=4||0===e?Wv(t):Wv.ceil(t),$a(Wv.count(ug(t),t)+(4===ug(t).getDay()),n,2)}function ku(t){return t.getDay()}function Cu(t,n){return $a(Gv.count(ug(t),t),n,2)}function Pu(t,n){return $a(t.getFullYear()%100,n,2)}function zu(t,n){return $a(t.getFullYear()%1e4,n,4)}function Ru(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+$a(n/60|0,"0",2)+$a(n%60,"0",2)}function Lu(t,n){return $a(t.getUTCDate(),n,2)}function Du(t,n){return $a(t.getUTCHours(),n,2)}function Uu(t,n){return $a(t.getUTCHours()%12||12,n,2)}function qu(t,n){return $a(1+dg.count(Pg(t),t),n,3)}function Ou(t,n){return $a(t.getUTCMilliseconds(),n,3)}function Yu(t,n){return Ou(t,n)+"000"}function Bu(t,n){return $a(t.getUTCMonth()+1,n,2)}function Fu(t,n){return $a(t.getUTCMinutes(),n,2)}function Iu(t,n){return $a(t.getUTCSeconds(),n,2)}function ju(t){var n=t.getUTCDay();return 0===n?7:n}function Hu(t,n){return $a(vg.count(Pg(t),t),n,2)}function Xu(t,n){var e=t.getUTCDay();return t=e>=4||0===e?bg(t):bg.ceil(t),$a(bg.count(Pg(t),t)+(4===Pg(t).getUTCDay()),n,2)}function Gu(t){return t.getUTCDay()}function Vu(t,n){return $a(gg.count(Pg(t),t),n,2)}function $u(t,n){return $a(t.getUTCFullYear()%100,n,2)}function Wu(t,n){return $a(t.getUTCFullYear()%1e4,n,4)}function Zu(){return"+0000"}function Qu(){return"%"}function Ju(t){return+t}function Ku(t){return Math.floor(+t/1e3)}function tf(n){return zg=Va(n),t.timeFormat=zg.format,t.timeParse=zg.parse,t.utcFormat=zg.utcFormat,t.utcParse=zg.utcParse,zg}function nf(t){return new Date(t)}function ef(t){return t instanceof Date?+t:+new Date(+t)}function rf(t,n,r,i,o,a,u,f,c){function s(e){return(u(e)=1?m_:t<=-1?-m_:Math.asin(t)}function lf(t){return t.innerRadius}function hf(t){return t.outerRadius}function df(t){return t.startAngle}function pf(t){return t.endAngle}function vf(t){return t&&t.padAngle}function gf(t,n,e,r,i,o,a){var u=t-e,f=n-r,c=(a?o:-o)/y_(u*u+f*f),s=c*f,l=-c*u,h=t+s,d=n+l,p=e+s,v=r+l,g=(h+p)/2,y=(d+v)/2,_=p-h,b=v-d,m=_*_+b*b,x=i-o,w=h*v-p*d,M=(b<0?-1:1)*y_(p_(0,x*x*m-w*w)),A=(w*b-_*M)/m,T=(-w*_-b*M)/m,N=(w*b+_*M)/m,S=(-w*_+b*M)/m,E=A-g,k=T-y,C=N-g,P=S-y;return E*E+k*k>C*C+P*P&&(A=N,T=S),{cx:A,cy:T,x01:-s,y01:-l,x11:A*(i/x-1),y11:T*(i/x-1)}}function yf(t){this._context=t}function _f(t){return new yf(t)}function bf(t){return t[0]}function mf(t){return t[1]}function xf(){function t(t){var u,f,c,s=t.length,l=!1;for(null==i&&(a=o(c=ie())),u=0;u<=s;++u)!(u=s;--l)c.point(g[l],y[l]);c.lineEnd(),c.areaEnd()}v&&(g[n]=+e(h,n,t),y[n]=+i(h,n,t),c.point(r?+r(h,n,t):g[n],o?+o(h,n,t):y[n]))}if(d)return c=null,d+""||null}function n(){return xf().defined(a).curve(f).context(u)}var e=bf,r=null,i=cf(0),o=mf,a=cf(!0),u=null,f=_f,c=null;return t.x=function(n){return arguments.length?(e="function"==typeof n?n:cf(+n),r=null,t):e},t.x0=function(n){return arguments.length?(e="function"==typeof n?n:cf(+n),t):e},t.x1=function(n){return arguments.length?(r=null==n?null:"function"==typeof n?n:cf(+n),t):r},t.y=function(n){return arguments.length?(i="function"==typeof n?n:cf(+n),o=null,t):i},t.y0=function(n){return arguments.length?(i="function"==typeof n?n:cf(+n),t):i},t.y1=function(n){return arguments.length?(o=null==n?null:"function"==typeof n?n:cf(+n),t):o},t.lineX0=t.lineY0=function(){return n().x(e).y(i)},t.lineY1=function(){return n().x(e).y(o)},t.lineX1=function(){return n().x(r).y(i)},t.defined=function(n){return arguments.length?(a="function"==typeof n?n:cf(!!n),t):a},t.curve=function(n){return arguments.length?(f=n,null!=u&&(c=f(u)),t):f},t.context=function(n){return arguments.length?(null==n?u=c=null:c=f(u=n),t):u},t}function Mf(t,n){return nt?1:n>=t?0:NaN}function Af(t){return t}function Tf(t){this._curve=t}function Nf(t){function n(n){return new Tf(t(n))}return n._curve=t,n}function Sf(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Nf(t)):n()._curve},t}function Ef(){return Sf(xf().curve(w_))}function kf(){var t=wf().curve(w_),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Sf(e())},delete t.lineX0,t.lineEndAngle=function(){return Sf(r())},delete t.lineX1,t.lineInnerRadius=function(){return Sf(i())},delete t.lineY0,t.lineOuterRadius=function(){return Sf(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Nf(t)):n()._curve},t}function Cf(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}function Pf(t){return t.source}function zf(t){return t.target}function Rf(t){function n(){var n,u=M_.call(arguments),f=e.apply(this,u),c=r.apply(this,u);if(a||(a=n=ie()),t(a,+i.apply(this,(u[0]=f,u)),+o.apply(this,u),+i.apply(this,(u[0]=c,u)),+o.apply(this,u)),n)return a=null,n+""||null}var e=Pf,r=zf,i=bf,o=mf,a=null;return n.source=function(t){return arguments.length?(e=t,n):e},n.target=function(t){return arguments.length?(r=t,n):r},n.x=function(t){return arguments.length?(i="function"==typeof t?t:cf(+t),n):i},n.y=function(t){return arguments.length?(o="function"==typeof t?t:cf(+t),n):o},n.context=function(t){return arguments.length?(a=null==t?null:t,n):a},n}function Lf(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n=(n+r)/2,e,n,i,r,i)}function Df(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n,e=(e+i)/2,r,e,r,i)}function Uf(t,n,e,r,i){var o=Cf(n,e),a=Cf(n,e=(e+i)/2),u=Cf(r,e),f=Cf(r,i);t.moveTo(o[0],o[1]),t.bezierCurveTo(a[0],a[1],u[0],u[1],f[0],f[1])}function qf(){}function Of(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function Yf(t){this._context=t}function Bf(t){this._context=t}function Ff(t){this._context=t}function If(t,n){this._basis=new Yf(t),this._beta=n}function jf(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Hf(t,n){this._context=t,this._k=(1-n)/6}function Xf(t,n){this._context=t,this._k=(1-n)/6}function Gf(t,n){this._context=t,this._k=(1-n)/6}function Vf(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>__){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,f=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/f,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/f}if(t._l23_a>__){var c=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*c+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*c+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function $f(t,n){this._context=t,this._alpha=n}function Wf(t,n){this._context=t,this._alpha=n}function Zf(t,n){this._context=t,this._alpha=n}function Qf(t){this._context=t}function Jf(t){return t<0?-1:1}function Kf(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(Jf(o)+Jf(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function tc(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function nc(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function ec(t){this._context=t}function rc(t){this._context=new ic(t)}function ic(t){this._context=t}function oc(t){this._context=t}function ac(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function sc(t,n){return t[n]}function lc(t){var n=t.map(hc);return cc(t).sort(function(t,e){return n[t]-n[e]})}function hc(t){for(var n,e=0,r=-1,i=t.length;++r0)){if(o/=h,h<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=r-f,h||!(o<0)){if(o/=h,h<0){if(o>l)return;o>s&&(s=o)}else if(h>0){if(o0)){if(o/=d,d<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=i-c,d||!(o<0)){if(o/=d,d<0){if(o>l)return;o>s&&(s=o)}else if(d>0){if(o0||l<1)||(s>0&&(t[0]=[f+s*h,c+s*d]),l<1&&(t[1]=[f+l*h,c+l*d]),!0)}}}}}function Tc(t,n,e,r,i){var o=t[1];if(o)return!0;var a,u,f=t[0],c=t.left,s=t.right,l=c[0],h=c[1],d=s[0],p=s[1],v=(l+d)/2,g=(h+p)/2;if(p===h){if(v=r)return;if(l>d){if(f){if(f[1]>=i)return}else f=[v,e];o=[v,i]}else{if(f){if(f[1]1)if(l>d){if(f){if(f[1]>=i)return}else f=[(e-u)/a,e];o=[(i-u)/a,i]}else{if(f){if(f[1]=r)return}else f=[n,a*n+u];o=[r,a*r+u]}else{if(f){if(f[0]=-eb)){var d=f*f+c*c,p=s*s+l*l,v=(l*d-c*p)/h,g=(f*p-s*d)/h,y=K_.pop()||new function(){yc(this),this.x=this.y=this.arc=this.site=this.cy=null};y.arc=t,y.site=i,y.x=v+a,y.y=(y.cy=g+u)+Math.sqrt(v*v+g*g),t.circle=y;for(var _=null,b=Q_._;b;)if(y.ynb)u=u.L;else{if(!((i=o-function(t,n){var e=t.N;if(e)return Dc(e,n);var r=t.site;return r[1]===n?r[0]:1/0}(u,a))>nb)){r>-nb?(n=u.P,e=u):i>-nb?(n=u,e=u.N):n=e=u;break}if(!u.R){n=u;break}u=u.R}(function(t){Z_[t.index]={site:t,halfedges:[]}})(t);var f=Pc(t);if(W_.insert(n,f),n||e){if(n===e)return Cc(n),e=Pc(n.site),W_.insert(f,e),f.edge=e.edge=xc(n.site,f.site),kc(n),void kc(e);if(e){Cc(n),Cc(e);var c=n.site,s=c[0],l=c[1],h=t[0]-s,d=t[1]-l,p=e.site,v=p[0]-s,g=p[1]-l,y=2*(h*g-d*v),_=h*h+d*d,b=v*v+g*g,m=[(g*_-d*b)/y+s,(h*b-v*_)/y+l];Mc(e.edge,c,p,m),f.edge=xc(c,t,null,m),e.edge=xc(t,p,null,m),kc(n),kc(e)}else f.edge=xc(n.site,f.site)}}function Dc(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var a=t.P;if(!a)return-1/0;var u=(e=a.site)[0],f=e[1],c=f-n;if(!c)return u;var s=u-r,l=1/o-1/c,h=s/c;return l?(-h+Math.sqrt(h*h-2*l*(s*s/(-2*c)-f+c/2+i-o/2)))/l+r:(r+u)/2}function Uc(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function qc(t,n){return n[1]-t[1]||n[0]-t[0]}function Oc(t,n){var e,r,i,o=t.sort(qc).pop();for(J_=[],Z_=new Array(t.length),W_=new gc,Q_=new gc;;)if(i=$_,o&&(!i||o[1]nb||Math.abs(i[0][1]-i[1][1])>nb)||delete J_[o]})(a,u,f,c),function(t,n,e,r){var i,o,a,u,f,c,s,l,h,d,p,v,g=Z_.length,y=!0;for(i=0;inb||Math.abs(v-h)>nb)&&(f.splice(u,0,J_.push(wc(a,d,Math.abs(p-t)nb?[t,Math.abs(l-t)nb?[Math.abs(h-r)nb?[e,Math.abs(l-e)nb?[Math.abs(h-n)r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}var Zc=e(n),Qc=Zc.right,Jc=Zc.left,Kc=Array.prototype,ts=Kc.slice,ns=Kc.map,es=Math.sqrt(50),rs=Math.sqrt(10),is=Math.sqrt(2),os=Array.prototype.slice,as=1,us=2,fs=3,cs=4,ss=1e-6,ls={value:function(){}};S.prototype=N.prototype={constructor:S,on:function(t,n){var e,r=this._,i=function(t,n){return t.trim().split(/^|\s+/).map(function(t){var e="",r=t.indexOf(".");if(r>=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}(t+"",r),o=-1,a=i.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o0)for(var e,r,i=new Array(e),o=0;o=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var bs={};if(t.event=null,"undefined"!=typeof document){"onmouseenter"in document.documentElement||(bs={mouseenter:"mouseover",mouseleave:"mouseout"})}var ms=[null];ut.prototype=ft.prototype={constructor:ut,select:function(t){"function"!=typeof t&&(t=z(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=m&&(m=b+1);!(_=g[m])&&++m=0;)(r=i[o])&&(a&&a!==r.nextSibling&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=Y);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?function(t){return function(){this.style.removeProperty(t)}}:"function"==typeof n?function(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}:function(t,n,e){return function(){this.style.setProperty(t,n,e)}})(t,n,null==e?"":e)):F(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?function(t){return function(){delete this[t]}}:"function"==typeof n?function(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}:function(t,n){return function(){this[t]=n}})(t,n)):this.node()[t]},classed:function(t,n){var e=I(t+"");if(arguments.length<2){for(var r=j(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?it:rt,null==e&&(e=!1),r=0;r=240?t-240:t+120,i,r),Ot(t,i,r),Ot(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Ls=Math.PI/180,Ds=180/Math.PI,Us=.96422,qs=1,Os=.82521,Ys=4/29,Bs=6/29,Fs=3*Bs*Bs,Is=Bs*Bs*Bs;Nt(Ft,Bt,St(Et,{brighter:function(t){return new Ft(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Ft(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return n=Us*jt(n),t=qs*jt(t),e=Os*jt(e),new Lt(Ht(3.1338561*n-1.6168667*t-.4906146*e),Ht(-.9787684*n+1.9161415*t+.033454*e),Ht(.0719453*n-.2289914*t+1.4052427*e),this.opacity)}})),Nt($t,Vt,St(Et,{brighter:function(t){return new $t(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new $t(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return Yt(this).rgb()}}));var js=-.29227,Hs=-.90649,Xs=1.97294,Gs=Xs*Hs,Vs=1.78277*Xs,$s=1.78277*js- -.14861*Hs;Nt(Zt,Wt,St(Et,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new Zt(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new Zt(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Ls,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new Lt(255*(n+e*(-.14861*r+1.78277*i)),255*(n+e*(js*r+Hs*i)),255*(n+e*(Xs*r)),this.opacity)}}));var Ws,Zs,Qs,Js,Ks,tl,nl=function t(n){function e(t,n){var e=r((t=Rt(t)).r,(n=Rt(n)).r),i=r(t.g,n.g),o=r(t.b,n.b),a=on(t.opacity,n.opacity);return function(n){return t.r=e(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}var r=rn(n);return e.gamma=t,e}(1),el=an(Jt),rl=an(Kt),il=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,ol=new RegExp(il.source,"g"),al=180/Math.PI,ul={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1},fl=vn(function(t){return"none"===t?ul:(Ws||(Ws=document.createElement("DIV"),Zs=document.documentElement,Qs=document.defaultView),Ws.style.transform=t,t=Qs.getComputedStyle(Zs.appendChild(Ws),null).getPropertyValue("transform"),Zs.removeChild(Ws),t=t.slice(7,-1).split(","),pn(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))},"px, ","px)","deg)"),cl=vn(function(t){return null==t?ul:(Js||(Js=document.createElementNS("http://www.w3.org/2000/svg","g")),Js.setAttribute("transform",t),(t=Js.transform.baseVal.consolidate())?(t=t.matrix,pn(t.a,t.b,t.c,t.d,t.e,t.f)):ul)},", ",")",")"),sl=Math.SQRT2,ll=2,hl=4,dl=1e-12,pl=_n(en),vl=_n(on),gl=bn(en),yl=bn(on),_l=mn(en),bl=mn(on),ml=0,xl=0,wl=0,Ml=1e3,Al=0,Tl=0,Nl=0,Sl="object"==typeof performance&&performance.now?performance:Date,El="object"==typeof window&&window.requestAnimationFrame?window.requestAnimationFrame.bind(window):function(t){setTimeout(t,17)};Mn.prototype=An.prototype={constructor:Mn,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?xn():+e)+(null==n?0:+n),this._next||tl===this||(tl?tl._next=this:Ks=this,tl=this),this._call=t,this._time=e,En()},stop:function(){this._call&&(this._call=null,this._time=1/0,En())}};var kl=N("start","end","interrupt"),Cl=[],Pl=0,zl=1,Rl=2,Ll=3,Dl=4,Ul=5,ql=6,Ol=ft.prototype.constructor,Yl=0,Bl=ft.prototype;qn.prototype=On.prototype={constructor:qn,select:function(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=z(t));for(var r=this._groups,i=r.length,o=new Array(i),a=0;a=0&&(t=t.slice(0,n)),!t||"start"===t})}(n)?Pn:zn;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=k(t),r="transform"===e?cl:Un;return this.attrTween(t,"function"==typeof n?(e.local?function(t,n,e){var r,i,o;return function(){var a,u=e(this);if(null!=u)return(a=this.getAttributeNS(t.space,t.local))===u?null:a===r&&u===i?o:o=n(r=a,i=u);this.removeAttributeNS(t.space,t.local)}}:function(t,n,e){var r,i,o;return function(){var a,u=e(this);if(null!=u)return(a=this.getAttribute(t))===u?null:a===r&&u===i?o:o=n(r=a,i=u);this.removeAttribute(t)}})(e,r,Dn(this,"attr."+t,n)):null==n?(e.local?function(t){return function(){this.removeAttributeNS(t.space,t.local)}}:function(t){return function(){this.removeAttribute(t)}})(e):(e.local?function(t,n,e){var r,i;return function(){var o=this.getAttributeNS(t.space,t.local);return o===e?null:o===r?i:i=n(r=o,e)}}:function(t,n,e){var r,i;return function(){var o=this.getAttribute(t);return o===e?null:o===r?i:i=n(r=o,e)}})(e,r,n+""))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=k(t);return this.tween(e,(r.local?function(t,n){function e(){var e=this,r=n.apply(e,arguments);return r&&function(n){e.setAttributeNS(t.space,t.local,r(n))}}return e._value=n,e}:function(t,n){function e(){var e=this,r=n.apply(e,arguments);return r&&function(n){e.setAttribute(t,r(n))}}return e._value=n,e})(r,n))},style:function(t,n,e){var r="transform"==(t+="")?fl:Un;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=F(this,t),a=(this.style.removeProperty(t),F(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,function(t){return function(){this.style.removeProperty(t)}}(t)):this.styleTween(t,"function"==typeof n?function(t,n,e){var r,i,o;return function(){var a=F(this,t),u=e(this);return null==u&&(this.style.removeProperty(t),u=F(this,t)),a===u?null:a===r&&u===i?o:o=n(r=a,i=u)}}(t,r,Dn(this,"style."+t,n)):function(t,n,e){var r,i;return function(){var o=F(this,t);return o===e?null:o===r?i:i=n(r=o,e)}}(t,r,n+""),e)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Dn(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Rn(this.node(),e).tween,o=0,a=i.length;o1e-6)if(Math.abs(s*u-f*c)>1e-6&&i){var h=e-o,d=r-a,p=u*u+f*f,v=h*h+d*d,g=Math.sqrt(p),y=Math.sqrt(l),_=i*Math.tan((Ch-Math.acos((p+l-v)/(2*g*y)))/2),b=_/y,m=_/g;Math.abs(b-1)>1e-6&&(this._+="L"+(t+b*c)+","+(n+b*s)),this._+="A"+i+","+i+",0,0,"+ +(s*h>c*d)+","+(this._x1=t+m*u)+","+(this._y1=n+m*f)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n;var a=(e=+e)*Math.cos(r),u=e*Math.sin(r),f=t+a,c=n+u,s=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+f+","+c:(Math.abs(this._x1-f)>1e-6||Math.abs(this._y1-c)>1e-6)&&(this._+="L"+f+","+c),e&&(l<0&&(l=l%Ph+Ph),l>zh?this._+="A"+e+","+e+",0,1,"+s+","+(t-a)+","+(n-u)+"A"+e+","+e+",0,1,"+s+","+(this._x1=f)+","+(this._y1=c):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Ch)+","+s+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};se.prototype=le.prototype={constructor:se,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var Rh=le.prototype;ge.prototype=ye.prototype={constructor:ge,has:Rh.has,add:function(t){return t+="",this["$"+t]=t,this},remove:Rh.remove,clear:Rh.clear,values:Rh.keys,size:Rh.size,empty:Rh.empty,each:Rh.each};var Lh=Array.prototype.slice,Dh=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]],Uh={},qh={},Oh=34,Yh=10,Bh=13,Fh=Ee(","),Ih=Fh.parse,jh=Fh.parseRows,Hh=Fh.format,Xh=Fh.formatRows,Gh=Ee("\t"),Vh=Gh.parse,$h=Gh.parseRows,Wh=Gh.format,Zh=Gh.formatRows,Qh=Re(Ih),Jh=Re(Vh),Kh=De("application/xml"),td=De("text/html"),nd=De("image/svg+xml"),ed=Ie.prototype=je.prototype;ed.copy=function(){var t,n,e=new je(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=He(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=He(n));return e},ed.add=function(t){var n=+this._x.call(null,t),e=+this._y.call(null,t);return Oe(this.cover(n,e),n,e,t)},ed.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),f=1/0,c=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));for(st||t>i||r>n||n>o))return this;var a,u,f=i-e,c=this._root;switch(u=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do{a=new Array(4),a[u]=c,c=a}while(f*=2,i=e+f,o=r+f,t>i||n>o);break;case 1:do{a=new Array(4),a[u]=c,c=a}while(f*=2,e=i-f,o=r+f,e>t||n>o);break;case 2:do{a=new Array(4),a[u]=c,c=a}while(f*=2,i=e+f,r=o-f,t>i||r>n);break;case 3:do{a=new Array(4),a[u]=c,c=a}while(f*=2,e=i-f,r=o-f,e>t||r>n)}this._root&&this._root.length&&(this._root=c)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this},ed.data=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},ed.extent=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},ed.find=function(t,n,e){var r,i,o,a,u,f,c,s=this._x0,l=this._y0,h=this._x1,d=this._y1,p=[],v=this._root;for(v&&p.push(new Ye(v,s,l,h,d)),null==e?e=1/0:(s=t-e,l=n-e,h=t+e,d=n+e,e*=e);f=p.pop();)if(!(!(v=f.node)||(i=f.x0)>h||(o=f.y0)>d||(a=f.x1)=y)<<1|t>=g)&&(f=p[p.length-1],p[p.length-1]=p[p.length-1-c],p[p.length-1-c]=f)}else{var _=t-+this._x.call(null,v.data),b=n-+this._y.call(null,v.data),m=_*_+b*b;if(m=(u=(p+g)/2))?p=u:g=u,(s=a>=(f=(v+y)/2))?v=f:y=f,n=d,!(d=d[l=s<<1|c]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},ed.removeAll=function(t){for(var n=0,e=t.length;n0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Ke(100*t,n)},r:Ke,s:function(t,n){var e=Qe(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(rd=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Qe(t,Math.max(0,n+o-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},ud=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i;tr.prototype=nr.prototype,nr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var fd,cd=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];ir({decimal:".",thousands:",",grouping:[3],currency:["$",""]}),cr.prototype={constructor:cr,reset:function(){this.s=this.t=0},add:function(t){sr(Fd,t,this.t),sr(this,Fd.s,this.s),this.s?this.t+=Fd.t:this.s=Fd.t},valueOf:function(){return this.s}};var sd,ld,hd,dd,pd,vd,gd,yd,_d,bd,md,xd,wd,Md,Ad,Td,Nd,Sd,Ed,kd,Cd,Pd,zd,Rd,Ld,Dd,Ud,qd,Od,Yd,Bd,Fd=new cr,Id=1e-6,jd=1e-12,Hd=Math.PI,Xd=Hd/2,Gd=Hd/4,Vd=2*Hd,$d=180/Hd,Wd=Hd/180,Zd=Math.abs,Qd=Math.atan,Jd=Math.atan2,Kd=Math.cos,tp=Math.ceil,np=Math.exp,ep=Math.log,rp=Math.pow,ip=Math.sin,op=Math.sign||function(t){return t>0?1:t<0?-1:0},ap=Math.sqrt,up=Math.tan,fp={Feature:function(t,n){vr(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++rId?_d=90:dp<-Id&&(gd=-90),Ad[0]=vd,Ad[1]=yd}},vp={sphere:pr,point:Br,lineStart:Ir,lineEnd:Xr,polygonStart:function(){vp.lineStart=Gr,vp.lineEnd=Vr},polygonEnd:function(){vp.lineStart=Ir,vp.lineEnd=Xr}};Jr.invert=Jr;var gp,yp,_p,bp,mp,xp,wp,Mp,Ap,Tp,Np,Sp=fr(),Ep=hi(function(){return!0},function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?Hd:-Hd,f=Zd(o-e);Zd(f-Hd)0?Xd:-Xd),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&f>=Hd&&(Zd(e-i)Id?Qd((ip(n)*(o=Kd(r))*ip(e)-ip(r)*(i=Kd(n))*ip(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}},function(t,n,e,r){var i;if(null==t)i=e*Xd,r.point(-Hd,i),r.point(0,i),r.point(Hd,i),r.point(Hd,0),r.point(Hd,-i),r.point(0,-i),r.point(-Hd,-i),r.point(-Hd,0),r.point(-Hd,i);else if(Zd(t[0]-n[0])>Id){var o=t[0]Ip&&(Ip=t),njp&&(jp=n)},lineStart:pr,lineEnd:pr,polygonStart:pr,polygonEnd:pr,result:function(){var t=[[Bp,Fp],[Ip,jp]];return Ip=jp=-(Fp=Bp=1/0),t}},Xp=0,Gp=0,Vp=0,$p=0,Wp=0,Zp=0,Qp=0,Jp=0,Kp=0,tv={point:Ui,lineStart:qi,lineEnd:Bi,polygonStart:function(){tv.lineStart=Fi,tv.lineEnd=Ii},polygonEnd:function(){tv.point=Ui,tv.lineStart=qi,tv.lineEnd=Bi},result:function(){var t=Kp?[Qp/Kp,Jp/Kp]:Zp?[$p/Zp,Wp/Zp]:Vp?[Xp/Vp,Gp/Vp]:[NaN,NaN];return Xp=Gp=Vp=$p=Wp=Zp=Qp=Jp=Kp=0,t}};Xi.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,Vd)}},result:pr};var nv,ev,rv,iv,ov,av=fr(),uv={point:pr,lineStart:function(){uv.point=Gi},lineEnd:function(){nv&&Vi(ev,rv),uv.point=pr},polygonStart:function(){nv=!0},polygonEnd:function(){nv=null},result:function(){var t=+av;return av.reset(),t}};$i.prototype={_radius:4.5,_circle:Wi(4.5),pointRadius:function(t){return(t=+t)!==this._radius&&(this._radius=t,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:null==this._circle&&(this._circle=Wi(this._radius)),this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}return null}},Qi.prototype={constructor:Qi,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var fv=16,cv=Kd(30*Wd),sv=Zi({point:function(t,n){this.stream.point(t*Wd,n*Wd)}}),lv=lo(function(t){return ap(2/(1+t))});lv.invert=ho(function(t){return 2*hr(t/2)});var hv=lo(function(t){return(t=lr(t))&&t/ip(t)});hv.invert=ho(function(t){return t}),po.invert=function(t,n){return[t,2*Qd(np(n))-Xd]},_o.invert=_o,mo.invert=ho(Qd),wo.invert=function(t,n){var e,r=n,i=25;do{var o=r*r,a=o*o;r-=e=(r*(1.007226+o*(.015085+a*(.028874*o-.044475-.005916*a)))-n)/(1.007226+o*(.045255+a*(.259866*o-.311325-.005916*11*a)))}while(Zd(e)>Id&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},Mo.invert=ho(hr),Ao.invert=ho(function(t){return 2*Qd(t)}),To.invert=function(t,n){return[-n,2*Qd(np(t))-Xd]},Lo.prototype=Co.prototype={constructor:Lo,count:function(){return this.eachAfter(ko)},each:function(t){var n,e,r,i,o=this,a=[o];do{for(n=a.reverse(),a=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},sum:function(t){return this.eachAfter(function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e})},sort:function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){var t=[];return this.each(function(n){t.push(n)}),t},leaves:function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},links:function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n},copy:function(){return Co(this).eachBefore(zo)}};var dv=Array.prototype.slice,pv="$",vv={depth:-1},gv={};fa.prototype=Object.create(Lo.prototype);var yv=(1+Math.sqrt(5))/2,_v=function t(n){function e(t,e,r,i,o){sa(n,t,e,r,i,o)}return e.ratio=function(n){return t((n=+n)>1?n:1)},e}(yv),bv=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,f,c,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(yv),mv=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(pa),xv=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(pa),wv=function t(n){function e(){var t=xv.source(n).apply(this,arguments);return function(){return Math.exp(t())}}return e.source=t,e}(pa),Mv=function t(n){function e(t){return function(){for(var e=0,r=0;r0?t>1?Fa(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Rv:null};var Lv=Rv.range,Dv=6e4,Uv=6048e5,qv=Fa(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),Ov=qv.range,Yv=Fa(function(t){t.setTime(Math.floor(t/Dv)*Dv)},function(t,n){t.setTime(+t+n*Dv)},function(t,n){return(n-t)/Dv},function(t){return t.getMinutes()}),Bv=Yv.range,Fv=Fa(function(t){var n=t.getTimezoneOffset()*Dv%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),Iv=Fv.range,jv=Fa(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Dv)/864e5},function(t){return t.getDate()-1}),Hv=jv.range,Xv=Ia(0),Gv=Ia(1),Vv=Ia(2),$v=Ia(3),Wv=Ia(4),Zv=Ia(5),Qv=Ia(6),Jv=Xv.range,Kv=Gv.range,tg=Vv.range,ng=$v.range,eg=Wv.range,rg=Zv.range,ig=Qv.range,og=Fa(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ag=og.range,ug=Fa(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});ug.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Fa(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var fg=ug.range,cg=Fa(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Dv)},function(t,n){return(n-t)/Dv},function(t){return t.getUTCMinutes()}),sg=cg.range,lg=Fa(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),hg=lg.range,dg=Fa(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),pg=dg.range,vg=ja(0),gg=ja(1),yg=ja(2),_g=ja(3),bg=ja(4),mg=ja(5),xg=ja(6),wg=vg.range,Mg=gg.range,Ag=yg.range,Tg=_g.range,Ng=bg.range,Sg=mg.range,Eg=xg.range,kg=Fa(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),Cg=kg.range,Pg=Fa(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Pg.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Fa(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var zg,Rg=Pg.range,Lg={"-":"",_:" ",0:"0"},Dg=/^\s*\d+/,Ug=/^%/,qg=/[\\^$*+?|[\]().{}]/g;tf({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Og="%Y-%m-%dT%H:%M:%S.%LZ",Yg=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat(Og),Bg=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse(Og),Fg=1e3,Ig=60*Fg,jg=60*Ig,Hg=24*jg,Xg=7*Hg,Gg=30*Hg,Vg=365*Hg,$g=af("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Wg=af("7fc97fbeaed4fdc086ffff99386cb0f0027fbf5b17666666"),Zg=af("1b9e77d95f027570b3e7298a66a61ee6ab02a6761d666666"),Qg=af("a6cee31f78b4b2df8a33a02cfb9a99e31a1cfdbf6fff7f00cab2d66a3d9affff99b15928"),Jg=af("fbb4aeb3cde3ccebc5decbe4fed9a6ffffcce5d8bdfddaecf2f2f2"),Kg=af("b3e2cdfdcdaccbd5e8f4cae4e6f5c9fff2aef1e2cccccccc"),ty=af("e41a1c377eb84daf4a984ea3ff7f00ffff33a65628f781bf999999"),ny=af("66c2a5fc8d628da0cbe78ac3a6d854ffd92fe5c494b3b3b3"),ey=af("8dd3c7ffffb3bebadafb807280b1d3fdb462b3de69fccde5d9d9d9bc80bdccebc5ffed6f"),ry=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(af),iy=uf(ry),oy=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(af),ay=uf(oy),uy=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(af),fy=uf(uy),cy=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(af),sy=uf(cy),ly=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(af),hy=uf(ly),dy=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(af),py=uf(dy),vy=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(af),gy=uf(vy),yy=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(af),_y=uf(yy),by=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(af),my=uf(by),xy=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(af),wy=uf(xy),My=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(af),Ay=uf(My),Ty=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(af),Ny=uf(Ty),Sy=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(af),Ey=uf(Sy),ky=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(af),Cy=uf(ky),Py=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(af),zy=uf(Py),Ry=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(af),Ly=uf(Ry),Dy=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(af),Uy=uf(Dy),qy=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(af),Oy=uf(qy),Yy=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(af),By=uf(Yy),Fy=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(af),Iy=uf(Fy),jy=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(af),Hy=uf(jy),Xy=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(af),Gy=uf(Xy),Vy=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(af),$y=uf(Vy),Wy=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(af),Zy=uf(Wy),Qy=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(af),Jy=uf(Qy),Ky=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(af),t_=uf(Ky),n_=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(af),e_=uf(n_),r_=bl(Wt(300,.5,0),Wt(-240,.5,1)),i_=bl(Wt(-100,.75,.35),Wt(80,1.5,.8)),o_=bl(Wt(260,.75,.35),Wt(80,1.5,.8)),a_=Wt(),u_=ff(af("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),f_=ff(af("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),c_=ff(af("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),s_=ff(af("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921")),l_=Math.abs,h_=Math.atan2,d_=Math.cos,p_=Math.max,v_=Math.min,g_=Math.sin,y_=Math.sqrt,__=1e-12,b_=Math.PI,m_=b_/2,x_=2*b_;yf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var w_=Nf(_f);Tf.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var M_=Array.prototype.slice,A_={draw:function(t,n){var e=Math.sqrt(n/b_);t.moveTo(e,0),t.arc(0,0,e,0,x_)}},T_={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},N_=Math.sqrt(1/3),S_=2*N_,E_={draw:function(t,n){var e=Math.sqrt(n/S_),r=e*N_;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},k_=Math.sin(b_/10)/Math.sin(7*b_/10),C_=Math.sin(x_/10)*k_,P_=-Math.cos(x_/10)*k_,z_={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=C_*e,i=P_*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var a=x_*o/5,u=Math.cos(a),f=Math.sin(a);t.lineTo(f*e,-u*e),t.lineTo(u*r-f*i,f*r+u*i)}t.closePath()}},R_={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},L_=Math.sqrt(3),D_={draw:function(t,n){var e=-Math.sqrt(n/(3*L_));t.moveTo(0,2*e),t.lineTo(-L_*e,-e),t.lineTo(L_*e,-e),t.closePath()}},U_=Math.sqrt(3)/2,q_=1/Math.sqrt(12),O_=3*(q_/2+1),Y_={draw:function(t,n){var e=Math.sqrt(n/O_),r=e/2,i=e*q_,o=r,a=e*q_+e,u=-o,f=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,f),t.lineTo(-.5*r-U_*i,U_*r+-.5*i),t.lineTo(-.5*o-U_*a,U_*o+-.5*a),t.lineTo(-.5*u-U_*f,U_*u+-.5*f),t.lineTo(-.5*r+U_*i,-.5*i-U_*r),t.lineTo(-.5*o+U_*a,-.5*a-U_*o),t.lineTo(-.5*u+U_*f,-.5*f-U_*u),t.closePath()}},B_=[A_,T_,E_,R_,z_,D_,Y_];Yf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Of(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Of(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Bf.prototype={areaStart:qf,areaEnd:qf,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:Of(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ff.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:Of(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},If.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,f=-1;++f<=e;)r=f/e,this._basis.point(this._beta*t[f]+(1-this._beta)*(i+r*a),this._beta*n[f]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var F_=function t(n){function e(t){return 1===n?new Yf(t):new If(t,n)}return e.beta=function(n){return t(+n)},e}(.85);Hf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:jf(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:jf(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var I_=function t(n){function e(t){return new Hf(t,n)}return e.tension=function(n){return t(+n)},e}(0);Xf.prototype={areaStart:qf,areaEnd:qf,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:jf(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var j_=function t(n){function e(t){return new Xf(t,n)}return e.tension=function(n){return t(+n)},e}(0);Gf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:jf(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var H_=function t(n){function e(t){return new Gf(t,n)}return e.tension=function(n){return t(+n)},e}(0);$f.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Vf(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var X_=function t(n){function e(t){return n?new $f(t,n):new Hf(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Wf.prototype={areaStart:qf,areaEnd:qf,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Vf(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var G_=function t(n){function e(t){return n?new Wf(t,n):new Xf(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Zf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Vf(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var V_=function t(n){function e(t){return n?new Zf(t,n):new Gf(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Qf.prototype={areaStart:qf,areaEnd:qf,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}},ec.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:nc(this,this._t0,tc(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,nc(this,tc(this,e=Kf(this,t,n)),e);break;default:nc(this,this._t0,e=Kf(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(rc.prototype=Object.create(ec.prototype)).point=function(t,n){ec.prototype.point.call(this,n,t)},ic.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,r,i,o){this._context.bezierCurveTo(n,t,r,e,o,i)}},oc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,e=t.length;if(e)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===e)this._context.lineTo(t[1],n[1]);else for(var r=ac(t),i=ac(n),o=0,a=1;a=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}},gc.prototype={constructor:gc,insert:function(t,n){var e,r,i;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=mc(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)e===(r=e.U).L?(i=r.R)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.R&&(_c(this,e),e=(t=e).U),e.C=!1,r.C=!0,bc(this,r)):(i=r.L)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.L&&(bc(this,e),e=(t=e).U),e.C=!1,r.C=!0,_c(this,r)),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,r,i=t.U,o=t.L,a=t.R;if(e=o?a?mc(a):o:a,i?i.L===t?i.L=e:i.R=e:this._=e,o&&a?(r=e.C,e.C=t.C,e.L=o,o.U=e,e!==a?(i=e.U,e.U=t.U,t=e.R,i.L=t,e.R=a,a.U=e):(e.U=i,i=e,t=e.R)):(r=t.C,t=e),t&&(t.U=i),!r)if(t&&t.C)t.C=!1;else{do{if(t===this._)break;if(t===i.L){if((n=i.R).C&&(n.C=!1,i.C=!0,_c(this,i),n=i.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,bc(this,n),n=i.R),n.C=i.C,i.C=n.R.C=!1,_c(this,i),t=this._;break}}else if((n=i.L).C&&(n.C=!1,i.C=!0,bc(this,i),n=i.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,_c(this,n),n=i.L),n.C=i.C,i.C=n.L.C=!1,bc(this,i),t=this._;break}n.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};var $_,W_,Z_,Q_,J_,K_=[],tb=[],nb=1e-6,eb=1e-12;Oc.prototype={constructor:Oc,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return Sc(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,r){if(o=(i=e.halfedges).length)for(var i,o,a,u=e.site,f=-1,c=n[i[o-1]],s=c.left===u?c.right:c.left;++f=u)return null;var f=t-i.site[0],c=n-i.site[1],s=f*f+c*c;do{i=o.cells[r=a],a=null,i.halfedges.forEach(function(e){var r=o.edges[e],u=r.left;if(u!==i.site&&u||(u=r.right)){var f=t-u[0],c=n-u[1],l=f*f+c*c;lt?1:n>=t?0:NaN},t.deviation=a,t.extent=u,t.histogram=function(){function t(t){var i,o,a=t.length,u=new Array(a);for(i=0;il;)h.pop(),--p;var v,g=new Array(p+1);for(i=0;i<=p;++i)(v=g[i]=[]).x0=i>0?h[i-1]:c,v.x1=i=o.length)return null!=e&&n.sort(e),null!=r?r(n):n;for(var f,c,s,l=-1,h=n.length,d=o[i++],p=le(),v=a();++lo.length)return t;var i,u=a[e-1];return null!=r&&e>=o.length?i=t.entries():(i=[],t.each(function(t,r){i.push({key:r,values:n(t,e)})})),null!=u?i.sort(function(t,n){return u(t.key,n.key)}):i}var e,r,i,o=[],a=[];return i={object:function(n){return t(n,0,he,de)},map:function(n){return t(n,0,pe,ve)},entries:function(e){return n(t(e,0,pe,ve),0)},key:function(t){return o.push(t),i},sortKeys:function(t){return a[o.length-1]=t,i},sortValues:function(t){return e=t,i},rollup:function(t){return r=t,i}}},t.set=ye,t.map=le,t.keys=function(t){var n=[];for(var e in t)n.push(e);return n},t.values=function(t){var n=[];for(var e in t)n.push(t[e]);return n},t.entries=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},t.color=kt,t.rgb=Rt,t.hsl=Ut,t.lab=Bt,t.hcl=Vt,t.lch=function(t,n,e,r){return 1===arguments.length?Gt(t):new $t(e,n,t,null==r?1:r)},t.gray=function(t,n){return new Ft(t,0,0,null==n?1:n)},t.cubehelix=Wt,t.contours=we,t.contourDensity=function(){function t(t){var e=new Float32Array(v*y),r=new Float32Array(v*y);t.forEach(function(t,n,r){var i=a(t,n,r)+p>>h,o=u(t,n,r)+p>>h;i>=0&&i=0&&o>h),Ae({width:v,height:y,data:r},{width:v,height:y,data:e},l>>h),Me({width:v,height:y,data:e},{width:v,height:y,data:r},l>>h),Ae({width:v,height:y,data:r},{width:v,height:y,data:e},l>>h),Me({width:v,height:y,data:e},{width:v,height:y,data:r},l>>h),Ae({width:v,height:y,data:r},{width:v,height:y,data:e},l>>h);var i=_(e);if(!Array.isArray(i)){var o=g(e);i=d(0,o,i),(i=s(0,Math.floor(o/i)*i,i)).shift()}return we().thresholds(i).size([v,y])(e).map(n)}function n(t){return t.value*=Math.pow(2,-2*h),t.coordinates.forEach(e),t}function e(t){t.forEach(r)}function r(t){t.forEach(i)}function i(t){t[0]=t[0]*Math.pow(2,h)-p,t[1]=t[1]*Math.pow(2,h)-p}function o(){return p=3*l,v=f+2*p>>h,y=c+2*p>>h,t}var a=Te,u=Ne,f=960,c=500,l=20,h=2,p=3*l,v=f+2*p>>h,y=c+2*p>>h,_=be(20);return t.x=function(n){return arguments.length?(a="function"==typeof n?n:be(+n),t):a},t.y=function(n){return arguments.length?(u="function"==typeof n?n:be(+n),t):u},t.size=function(t){if(!arguments.length)return[f,c];var n=Math.ceil(t[0]),e=Math.ceil(t[1]);if(!(n>=0||n>=0))throw new Error("invalid size");return f=n,c=e,o()},t.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return h=Math.floor(Math.log(t)/Math.LN2),o()},t.thresholds=function(n){return arguments.length?(_="function"==typeof n?n:Array.isArray(n)?be(Lh.call(n)):be(n),t):_},t.bandwidth=function(t){if(!arguments.length)return Math.sqrt(l*(l+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return l=Math.round((Math.sqrt(4*t*t+1)-1)/2),o()},t},t.dispatch=N,t.drag=function(){function n(t){t.on("mousedown.drag",e).filter(g).on("touchstart.drag",o).on("touchmove.drag",a).on("touchend.drag touchcancel.drag",u).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function e(){if(!h&&d.apply(this,arguments)){var n=f("mouse",p.apply(this,arguments),pt,this,arguments);n&&(ct(t.event.view).on("mousemove.drag",r,!0).on("mouseup.drag",i,!0),_t(t.event.view),gt(),l=!1,c=t.event.clientX,s=t.event.clientY,n("start"))}}function r(){if(yt(),!l){var n=t.event.clientX-c,e=t.event.clientY-s;l=n*n+e*e>m}y.mouse("drag")}function i(){ct(t.event.view).on("mousemove.drag mouseup.drag",null),bt(t.event.view,l),yt(),y.mouse("end")}function o(){if(d.apply(this,arguments)){var n,e,r=t.event.changedTouches,i=p.apply(this,arguments),o=r.length;for(n=0;nf+d||ic+d||or.index){var p=f-u.x-u.vx,v=c-u.y-u.vy,g=p*p+v*v;gt.r&&(t.r=t[n].r)}function r(){if(i){var n,e,r=i.length;for(o=new Array(r),n=0;n=s)){(t.data!==o||t.next)&&(0===i&&(i=qe(),d+=i*i),0===f&&(f=qe(),d+=f*f),d1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,a,u,f,c=0,s=t.length;for(null==r?r=1/0:r*=r,c=0;c1?(d.on(t,n),o):d.on(t)}}},t.forceX=function(t){function n(t){for(var n,e=0,a=r.length;eqr(r[0],r[1])&&(r[1]=i[1]),qr(i[0],r[1])>qr(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=qr(r[1],i[0]))>a&&(a=u,vd=i[0],yd=r[1])}return Md=Ad=null,vd===1/0||gd===1/0?[[NaN,NaN],[NaN,NaN]]:[[vd,gd],[yd,_d]]},t.geoCentroid=function(t){Td=Nd=Sd=Ed=kd=Cd=Pd=zd=Rd=Ld=Dd=0,_r(t,vp);var n=Rd,e=Ld,r=Dd,i=n*n+e*e+r*r;return i=.12&&i<.234&&r>=-.425&&r<-.214?c:i>=.166&&i<.234&&r>=-.214&&r<-.115?s:f).invert(t)},t.stream=function(t){return e&&r===t?e:e=function(t){var n=t.length;return{point:function(e,r){for(var i=-1;++i2?t[2]+90:90]):(t=e(),[t[0],t[1],t[2]-90])},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=To,t.geoRotation=ri,t.geoStream=_r,t.geoTransform=function(t){return{stream:Zi(t)}},t.cluster=function(){function t(t){var o,a=0;t.eachAfter(function(t){var e=t.children;e?(t.x=function(t){return t.reduce(So,0)/t.length}(e),t.y=function(t){return 1+t.reduce(Eo,0)}(e)):(t.x=o?a+=n(t,o):0,t.y=0,o=t)});var u=function(t){for(var n;n=t.children;)t=n[0];return t}(t),f=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(t),c=u.x-n(u,f)/2,s=f.x+n(f,u)/2;return t.eachAfter(i?function(n){n.x=(n.x-t.x)*e,n.y=(t.y-n.y)*r}:function(n){n.x=(n.x-c)/(s-c)*e,n.y=(1-(t.y?n.y/t.y:1))*r})}var n=No,e=1,r=1,i=!1;return t.separation=function(e){return arguments.length?(n=e,t):n},t.size=function(n){return arguments.length?(i=!1,e=+n[0],r=+n[1],t):i?null:[e,r]},t.nodeSize=function(n){return arguments.length?(i=!0,e=+n[0],r=+n[1],t):i?[e,r]:null},t},t.hierarchy=Co,t.pack=function(){function t(t){return t.x=e/2,t.y=r/2,n?t.eachBefore(Zo(n)).eachAfter(Qo(i,.5)).eachBefore(Jo(1)):t.eachBefore(Zo(Wo)).eachAfter(Qo(Vo,1)).eachAfter(Qo(i,t.r/Math.min(e,r))).eachBefore(Jo(Math.min(e,r)/(2*t.r))),t}var n=null,e=1,r=1,i=Vo;return t.radius=function(e){return arguments.length?(n=function(t){return null==t?null:Go(t)}(e),t):n},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i="function"==typeof n?n:$o(+n),t):i},t},t.packSiblings=function(t){return Xo(t),t},t.packEnclose=Do,t.partition=function(){function t(t){var o=t.height+1;return t.x0=t.y0=r,t.x1=n,t.y1=e/o,t.eachBefore(function(t,n){return function(e){e.children&&ta(e,e.x0,t*(e.depth+1)/n,e.x1,t*(e.depth+2)/n);var i=e.x0,o=e.y0,a=e.x1-r,u=e.y1-r;a0)throw new Error("cycle");return o}var n=na,e=ea;return t.id=function(e){return arguments.length?(n=Go(e),t):n},t.parentId=function(n){return arguments.length?(e=Go(n),t):e},t},t.tree=function(){function t(t){var f=function(t){for(var n,e,r,i,o,a=new fa(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new fa(r[i],i)),e.parent=n;return(a.parent=new fa(null,0)).children=[a],a}(t);if(f.eachAfter(n),f.parent.m=-f.z,f.eachBefore(e),u)t.eachBefore(r);else{var c=t,s=t,l=t;t.eachBefore(function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)});var h=c===s?1:i(c,s)/2,d=h-c.x,p=o/(s.x+h+d),v=a/(l.depth||1);t.eachBefore(function(t){t.x=(t.x+d)*p,t.y=t.depth*v})}return t}function n(t){var n=t.children,e=t.parent.children,r=t.i?e[t.i-1]:null;if(n){(function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)})(t);var o=(n[0].z+n[n.length-1].z)/2;r?(t.z=r.z+i(t._,r._),t.m=t.z-o):t.z=o}else r&&(t.z=r.z+i(t._,r._));t.parent.A=function(t,n,e){if(n){for(var r,o=t,a=t,u=n,f=o.parent.children[0],c=o.m,s=a.m,l=u.m,h=f.m;u=oa(u),o=ia(o),u&&o;)f=ia(f),(a=oa(a)).a=t,(r=u.z+l-o.z-c+i(u._,o._))>0&&(aa(ua(u,t,e),t,r),c+=r,s+=r),l+=u.m,c+=o.m,h+=f.m,s+=a.m;u&&!oa(a)&&(a.t=u,a.m+=l-s),o&&!ia(f)&&(f.t=o,f.m+=c-h,e=t)}return e}(t,r,t.parent.A||e[0])}function e(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function r(t){t.x*=o,t.y=t.depth*a}var i=ra,o=1,a=1,u=null;return t.separation=function(n){return arguments.length?(i=n,t):i},t.size=function(n){return arguments.length?(u=!1,o=+n[0],a=+n[1],t):u?null:[o,a]},t.nodeSize=function(n){return arguments.length?(u=!0,o=+n[0],a=+n[1],t):u?[o,a]:null},t},t.treemap=function(){function t(t){return t.x0=t.y0=0,t.x1=i,t.y1=o,t.eachBefore(n),a=[0],r&&t.eachBefore(Ko),t}function n(t){var n=a[t.depth],r=t.x0+n,i=t.y0+n,o=t.x1-n,h=t.y1-n;o=n-1){var c=f[t];return c.x0=r,c.y0=i,c.x1=a,void(c.y1=u)}for(var l=s[t],h=e/2+l,d=t+1,p=n-1;d>>1;s[v]u-i){var _=(r*y+a*g)/e;o(t,d,g,r,i,_,u),o(d,n,y,_,i,a,u)}else{var b=(i*y+u*g)/e;o(t,d,g,r,i,a,b),o(d,n,y,r,b,a,u)}}var a,u,f=t.children,c=f.length,s=new Array(c+1);for(s[0]=u=a=0;a=0;--n)c.push(t[r[o[n]][2]]);for(n=+u;nu!=c>u&&a<(f-e)*(u-r)/(c-r)+e&&(s=!s),f=e,c=r;return s},t.polygonLength=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],a=o[0],u=o[1],f=0;++r1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return a_.h=360*t-100,a_.s=1.5-1.5*n,a_.l=.8-.9*n,a_+""},t.interpolateWarm=i_,t.interpolateCool=o_,t.interpolateViridis=u_,t.interpolateMagma=f_,t.interpolateInferno=c_,t.interpolatePlasma=s_,t.create=function(t){return ct(C(t).call(document.documentElement))},t.creator=C,t.local=st,t.matcher=ys,t.mouse=pt,t.namespace=k,t.namespaces=ds,t.clientPoint=dt,t.select=ct,t.selectAll=function(t){return"string"==typeof t?new ut([document.querySelectorAll(t)],[document.documentElement]):new ut([null==t?[]:t],ms)},t.selection=ft,t.selector=z,t.selectorAll=L,t.style=F,t.touch=vt,t.touches=function(t,n){null==n&&(n=ht().touches);for(var e=0,r=n?n.length:0,i=new Array(r);eh;if(f||(f=t=ie()),l__)if(p>x_-__)f.moveTo(l*d_(h),l*g_(h)),f.arc(0,0,l,h,d,!v),s>__&&(f.moveTo(s*d_(d),s*g_(d)),f.arc(0,0,s,d,h,v));else{var g,y,_=h,b=d,m=h,x=d,w=p,M=p,A=u.apply(this,arguments)/2,T=A>__&&(i?+i.apply(this,arguments):y_(s*s+l*l)),N=v_(l_(l-s)/2,+r.apply(this,arguments)),S=N,E=N;if(T>__){var k=sf(T/s*g_(A)),C=sf(T/l*g_(A));(w-=2*k)>__?(k*=v?1:-1,m+=k,x-=k):(w=0,m=x=(h+d)/2),(M-=2*C)>__?(C*=v?1:-1,_+=C,b-=C):(M=0,_=b=(h+d)/2)}var P=l*d_(_),z=l*g_(_),R=s*d_(x),L=s*g_(x);if(N>__){var D=l*d_(b),U=l*g_(b),q=s*d_(m),O=s*g_(m);if(p__?function(t,n,e,r,i,o,a,u){var f=e-t,c=r-n,s=a-i,l=u-o,h=(s*(n-o)-l*(t-i))/(l*f-s*c);return[t+h*f,n+h*c]}(P,z,q,O,D,U,R,L):[R,L],B=P-Y[0],F=z-Y[1],I=D-Y[0],j=U-Y[1],H=1/g_(function(t){return t>1?0:t<-1?b_:Math.acos(t)}((B*I+F*j)/(y_(B*B+F*F)*y_(I*I+j*j)))/2),X=y_(Y[0]*Y[0]+Y[1]*Y[1]);S=v_(N,(s-X)/(H-1)),E=v_(N,(l-X)/(H+1))}}M>__?E>__?(g=gf(q,O,P,z,l,E,v),y=gf(D,U,R,L,l,E,v),f.moveTo(g.cx+g.x01,g.cy+g.y01),E__&&w>__?S>__?(g=gf(R,L,D,U,s,-S,v),y=gf(P,z,q,O,s,-S,v),f.lineTo(g.cx+g.x01,g.cy+g.y01),S0&&(d+=l);for(null!=e?p.sort(function(t,n){return e(v[t],v[n])}):null!=r&&p.sort(function(n,e){return r(t[n],t[e])}),u=0,c=d?(y-h*b)/d:0;u0?l*c:0)+b,v[f]={data:t[f],index:u,value:l,startAngle:g,endAngle:s,padAngle:_};return v}var n=Af,e=Mf,r=null,i=cf(0),o=cf(x_),a=cf(0);return t.value=function(e){return arguments.length?(n="function"==typeof e?e:cf(+e),t):n},t.sortValues=function(n){return arguments.length?(e=n,r=null,t):e},t.sort=function(n){return arguments.length?(r=n,e=null,t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:cf(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:cf(+n),t):o},t.padAngle=function(n){return arguments.length?(a="function"==typeof n?n:cf(+n),t):a},t},t.areaRadial=kf,t.radialArea=kf,t.lineRadial=Ef,t.radialLine=Ef,t.pointRadial=Cf,t.linkHorizontal=function(){return Rf(Lf)},t.linkVertical=function(){return Rf(Df)},t.linkRadial=function(){var t=Rf(Uf);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.symbol=function(){function t(){var t;if(r||(r=t=ie()),n.apply(this,arguments).draw(r,+e.apply(this,arguments)),t)return r=null,t+""||null}var n=cf(A_),e=cf(64),r=null;return t.type=function(e){return arguments.length?(n="function"==typeof e?e:cf(e),t):n},t.size=function(n){return arguments.length?(e="function"==typeof n?n:cf(+n),t):e},t.context=function(n){return arguments.length?(r=null==n?null:n,t):r},t},t.symbols=B_,t.symbolCircle=A_,t.symbolCross=T_,t.symbolDiamond=E_,t.symbolSquare=R_,t.symbolStar=z_,t.symbolTriangle=D_,t.symbolWye=Y_,t.curveBasisClosed=function(t){return new Bf(t)},t.curveBasisOpen=function(t){return new Ff(t)},t.curveBasis=function(t){return new Yf(t)},t.curveBundle=F_,t.curveCardinalClosed=j_,t.curveCardinalOpen=H_,t.curveCardinal=I_,t.curveCatmullRomClosed=G_,t.curveCatmullRomOpen=V_,t.curveCatmullRom=X_,t.curveLinearClosed=function(t){return new Qf(t)},t.curveLinear=_f,t.curveMonotoneX=function(t){return new ec(t)},t.curveMonotoneY=function(t){return new rc(t)},t.curveNatural=function(t){return new oc(t)},t.curveStep=function(t){return new uc(t,.5)},t.curveStepAfter=function(t){return new uc(t,1)},t.curveStepBefore=function(t){return new uc(t,0)},t.stack=function(){function t(t){var o,a,u=n.apply(this,arguments),f=t.length,c=u.length,s=new Array(c);for(o=0;o0){for(var e,r,i,o=0,a=t[0].length;o1)for(var e,r,i,o,a,u,f=0,c=t[n[0]].length;f=0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):r[0]=o},t.stackOffsetNone=fc,t.stackOffsetSilhouette=function(t,n){if((e=t.length)>0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;azl&&e.name===n)return new qn([[t]],sh,n,+r)}return null},t.interrupt=Ln,t.voronoi=function(){function t(t){return new Oc(t.map(function(r,i){var o=[Math.round(n(r,i,t)/nb)*nb,Math.round(e(r,i,t)/nb)*nb];return o.index=i,o.data=r,o}),r)}var n=pc,e=vc,r=null;return t.polygons=function(n){return t(n).polygons()},t.links=function(n){return t(n).links()},t.triangles=function(n){return t(n).triangles()},t.x=function(e){return arguments.length?(n="function"==typeof e?e:dc(+e),t):n},t.y=function(n){return arguments.length?(e="function"==typeof n?n:dc(+n),t):e},t.extent=function(n){return arguments.length?(r=null==n?null:[[+n[0][0],+n[0][1]],[+n[1][0],+n[1][1]]],t):r&&[[r[0][0],r[0][1]],[r[1][0],r[1][1]]]},t.size=function(n){return arguments.length?(r=null==n?null:[[0,0],[+n[0],+n[1]]],t):r&&[r[1][0]-r[0][0],r[1][1]-r[0][1]]},t},t.zoom=function(){function n(t){t.property("__zoom",Gc).on("wheel.zoom",f).on("mousedown.zoom",c).on("dblclick.zoom",s).filter(m).on("touchstart.zoom",l).on("touchmove.zoom",h).on("touchend.zoom touchcancel.zoom",d).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function e(t,n){return(n=Math.max(x[0],Math.min(x[1],n)))===t.k?t:new Bc(n,t.x,t.y)}function r(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new Bc(t.k,r,i)}function i(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function o(t,n,e){t.on("start.zoom",function(){a(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){a(this,arguments).end()}).tween("zoom",function(){var t=arguments,r=a(this,t),o=y.apply(this,t),u=e||i(o),f=Math.max(o[1][0]-o[0][0],o[1][1]-o[0][1]),c=this.__zoom,s="function"==typeof n?n.apply(this,t):n,l=A(c.invert(u).concat(f/c.k),s.invert(u).concat(f/s.k));return function(t){if(1===t)t=s;else{var n=l(t),e=f/n[2];t=new Bc(e,u[0]-n[0]*e,u[1]-n[1]*e)}r.zoom(null,t)}})}function a(t,n){for(var e,r=0,i=T.length;rC}n.zoom("mouse",_(r(n.that.__zoom,n.mouse[0]=pt(n.that),n.mouse[1]),n.extent,w))},!0).on("mouseup.zoom",function(){e.on("mousemove.zoom mouseup.zoom",null),bt(t.event.view,n.moved),jc(),n.end()},!0),i=pt(this),o=t.event.clientX,u=t.event.clientY;_t(t.event.view),Ic(),n.mouse=[i,this.__zoom.invert(i)],Ln(this),n.start()}}function s(){if(g.apply(this,arguments)){var i=this.__zoom,a=pt(this),u=i.invert(a),f=i.k*(t.event.shiftKey?.5:2),c=_(r(e(i,f),a,u),y.apply(this,arguments),w);jc(),M>0?ct(this).transition().duration(M).call(o,c,a):ct(this).call(n.transform,c)}}function l(){if(g.apply(this,arguments)){var n,e,r,i,o=a(this,arguments),u=t.event.changedTouches,f=u.length;for(Ic(),e=0;e parseTime(d.date)).left; + let i = bisect(data, detailDate, 1); + + let workload = data[i]; + let date = workload.date; + let name = workload.name; + let opsSec = workload.opsSec; + let filename = workload.summaryPath; + + fetchWriteThroughputSummaryData(filename) + .then( + d => renderWriteThroughputSummaryDetail(name, date, opsSec, d), + _ => renderWriteThroughputSummaryDetail(name, date, opsSec, null), + ); +} + +/* + * Renders the write-throughput summary view, given the correspnding data. + * + * This function generates a time-series similar to the YCSB benchmark data. + * The x-axis represents the day on which the becnhmark was run, and the y-axis + * represents the calculated "max sustainable throughput" in ops-second. + * + * Clicking on an individual day renders the detail view for the given day, + * allowing the user to drill down into the per-worker performance. + */ +function renderWriteThroughputSummary(allData) { + const svg = d3.select(".chart.write-throughput"); + + // Filter on the appropriate time-series. + const dataKey = "write/values=1024"; + const data = allData[dataKey]; + + // Set up axes. + + const margin = {top: 25, right: 60, bottom: 25, left: 60}; + let maxY = d3.max(data, d => d.opsSec); + + const width = styleWidth(svg) - margin.left - margin.right; + const height = styleHeight(svg) - margin.top - margin.bottom; + + const x = d3.scaleTime() + .domain([minDate, max.date]) + .range([0, width]); + const x2 = d3.scaleTime() + .domain([minDate, max.date]) + .range([0, width]); + + const y = d3.scaleLinear() + .domain([0, maxY * 1.1]) + .range([height, 0]); + + const z = d3.scaleOrdinal(d3.schemeCategory10); + + const xAxis = d3.axisBottom(x) + .ticks(5); + + const yAxis = d3.axisLeft(y) + .ticks(5); + + const g = svg + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + g.append("g") + .attr("class", "axis axis--x") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + + g.append("g") + .attr("class", "axis axis--y") + .call(yAxis); + + g.append("text") + .attr("class", "chart-title") + .attr("x", margin.left + width / 2) + .attr("y", 0) + .style("text-anchor", "middle") + .style("font", "8pt sans-serif") + .text(dataKey); + + // Create a rectangle that can be used to clip the data. This avoids having + // the time-series spill across the y-axis when panning and zooming. + + const defs = svg.append("defs"); + + defs.append("clipPath") + .attr("id", dataKey) + .append("rect") + .attr("x", 0) + .attr("y", -margin.top) + .attr("width", width) + .attr("height", margin.top + height + 10); + + // Plot time-series. + + const view = g.append("g") + .attr("class", "view") + .attr("clip-path", "url(#" + dataKey + ")"); + + const line = d3.line() + .x(d => x(parseTime(d.date))) + .y(d => y(d.opsSec)); + + const path = view.selectAll(".line1") + .data([data]) + .enter() + .append("path") + .attr("class", "line1") + .attr("d", line) + .style("stroke", z(0)); + + // Hover to show labels. + + const lineHover = g + .append("line") + .attr("class", "hover") + .style("fill", "none") + .style("stroke", "#f99") + .style("stroke-width", "1px"); + + const dateHover = g + .append("text") + .attr("class", "hover") + .attr("fill", "#f22") + .attr("text-anchor", "middle") + .attr("alignment-baseline", "hanging") + .attr("transform", "translate(0, 0)"); + + const opsHover = g + .append("text") + .attr("class", "hover") + .attr("fill", "#f22") + .attr("text-anchor", "middle") + .attr("transform", "translate(0, 0)"); + + const marker = g + .append("circle") + .attr("class", "hover") + .attr("r", 3) + .style("opacity", "0") + .style("stroke", "#f22") + .style("fill", "#f22"); + + svg.node().updateMouse = function (mouse, date, hover) { + const mousex = mouse[0]; + const bisect = d3.bisector(d => parseTime(d.date)).left; + const i = bisect(data, date, 1); + const v = + i === data.length + ? data[i - 1] + : mousex - x(parseTime(data[i - 1].date)) < x(parseTime(data[i].date)) - mousex + ? data[i - 1] + : data[i]; + const noData = mousex < x(parseTime(data[0].date)); + + let lineY = height; + if (!noData) { + lineY = pathGetY(path.node(), mousex); + } + + let val, valY, valFormat; + val = v.opsSec; + valY = y(val); + valFormat = d3.format(",.0f"); + + lineHover + .attr("x1", mousex) + .attr("x2", mousex) + .attr("y1", lineY) + .attr("y2", height); + marker.attr("transform", "translate(" + x(parseTime(v.date)) + "," + valY + ")"); + dateHover + .attr("transform", "translate(" + mousex + "," + (height + 8) + ")") + .text(formatTime(date)); + opsHover + .attr("transform", "translate(" + x(parseTime(v.date)) + "," + (valY - 7) + ")") + .text(valFormat(val)); + }; + + // Panning and zooming. + + const updateZoom = function (t) { + x.domain(t.rescaleX(x2).domain()); + g.select(".axis--x").call(xAxis); + g.selectAll(".line1").attr("d", line); + }; + svg.node().updateZoom = updateZoom; + + const zoom = d3.zoom() + .extent([[0, 0], [width, 1]]) + .scaleExtent([0.25, 2]) // [45, 360] days + .translateExtent([[-width * 3, 0], [width, 1]]) // [today-360, today] + .on("zoom", function () { + const t = d3.event.transform; + if (!d3.event.sourceEvent) { + updateZoom(t); + return; + } + + d3.selectAll(".chart").each(function () { + if (this.updateZoom != null) { + this.updateZoom(t); + } + }); + + d3.selectAll(".chart").each(function () { + this.__zoom = t.translate(0, 0); + }); + }); + + svg.call(zoom); + svg.call(zoom.transform, d3.zoomTransform(svg.node())); + + svg.append("rect") + .attr("class", "mouse") + .attr("cursor", "move") + .attr("fill", "none") + .attr("pointer-events", "all") + .attr("width", width) + .attr("height", height + margin.top + margin.bottom) + .attr("transform", "translate(" + margin.left + "," + 0 + ")") + .on("mousemove", function () { + const mouse = d3.mouse(this); + const date = x.invert(mouse[0]); + + d3.selectAll(".chart").each(function () { + if (this.updateMouse != null) { + this.updateMouse(mouse, date, 1); + } + }); + }) + .on("mouseover", function () { + d3.selectAll(".chart") + .selectAll(".hover") + .style("opacity", 1.0); + }) + .on("mouseout", function () { + d3.selectAll(".chart") + .selectAll(".hover") + .style("opacity", 0); + }) + .on("click", function(d) { + // Use the date corresponding to the clicked data point to bisect + // into the workload data to pluck out the correct datapoint. + const mouse = d3.mouse(this); + let detailDate = d3.timeDay.floor(x.invert(mouse[0])); + bisectAndRenderWriteThroughputDetail(data, detailDate); + }); +} + +function fetchWriteThroughputSummaryData(file) { + return fetch(writeThroughputDetailURL(file)) + .then(response => response.json()) + .then(data => { + for (let key in data) { + let csvData = data[key].rawData; + data[key].data = d3.csvParseRows(csvData, function (d, i) { + return { + elapsed: +d[0], + opsSec: +d[1], + passed: d[2] === 'true', + size: +d[3], + levels: +d[4], + }; + }); + delete data[key].rawData; + } + return data; + }); +} + +/* + * Renders the write-throughput detail view, given the correspnding data, and + * the particular workload and date on which it was run. + * + * This function generates a series with the x-axis representing the elapsed + * time since the start of the benchmark, and the measured write load at that + * point in time (in ops/second). Each series is a worker that participated in + * the benchmark on the selected date. + */ +function renderWriteThroughputSummaryDetail(workload, date, opsSec, rawData) { + const svg = d3.select(".chart.write-throughput-detail"); + + // Remove anything that was previously on the canvas. This ensures that a + // user clicking multiple times does not keep adding data to the canvas. + svg.selectAll("*").remove(); + + const margin = {top: 25, right: 60, bottom: 25, left: 60}; + let maxX = 0; + let maxY = 0; + for (let key in rawData) { + let run = rawData[key]; + maxX = Math.max(maxX, d3.max(run.data, d => d.elapsed)); + maxY = Math.max(maxY, d3.max(run.data, d => d.opsSec)); + } + + const width = styleWidth(svg) - margin.left - margin.right; + const height = styleHeight(svg) - margin.top - margin.bottom; + + // Panning and zooming. + // These callbacks are defined as they are called from the panning / + // zooming functions elsewhere, however, they are simply no-ops on this + // chart, as they x-axis is a measure of "elapsed time" rather than a date. + + svg.node().updateMouse = function (mouse, date, hover) {} + svg.node().updateZoom = function () {}; + + // Set up axes. + + const x = d3.scaleLinear() + .domain([0, 8.5 * 3600]) + .range([0, width]); + + const y = d3.scaleLinear() + .domain([0, maxY * 1.1]) + .range([height, 0]); + + const z = d3.scaleOrdinal(d3.schemeCategory10); + + const xAxis = d3.axisBottom(x) + .ticks(5) + .tickFormat(d => Math.floor(d / 3600) + "h"); + + const yAxis = d3.axisLeft(y) + .ticks(5); + + const g = svg + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + g.append("g") + .attr("class", "axis axis--x") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + + g.append("g") + .attr("class", "axis axis--y") + .call(yAxis); + + // If we get no data, we just render an empty chart. + if (rawData == null) { + g.append("text") + .attr("class", "chart-title") + .attr("x", margin.left + width / 2) + .attr("y", height / 2) + .style("text-anchor", "middle") + .style("font", "8pt sans-serif") + .text("Data unavailable"); + return; + } + + g.append("text") + .attr("class", "chart-title") + .attr("x", margin.left + width / 2) + .attr("y", 0) + .style("text-anchor", "middle") + .style("font", "8pt sans-serif") + .text("Ops/sec over time"); + + // Plot data. + + const view = g.append("g") + .attr("class", "view"); + + let values = []; + for (let key in rawData) { + values.push({ + id: key, + values: rawData[key].data, + }); + } + + const line = d3.line() + .x(d => x(d.elapsed)) + .y(d => y(d.opsSec)); + + const path = view.selectAll(".line1") + .data(values) + .enter() + .append("path") + .attr("class", "line1") + .attr("d", d => line(d.values)) + .style("stroke", d => z(d.id)); + + // Draw a horizontal line for the calculated ops/sec average. + + view.append("path") + .attr("d", d3.line()([[x(0), y(opsSec)], [x(maxX), y(opsSec)]])) + .attr("stroke", "black") + .attr("stroke-width", "2") + .style("stroke-dasharray", ("2, 5")); +} diff --git a/pebble/docs/memory.md b/pebble/docs/memory.md new file mode 100644 index 0000000..b9f4a63 --- /dev/null +++ b/pebble/docs/memory.md @@ -0,0 +1,92 @@ +# Memory Management + +## Background + +Pebble has two significant sources of memory usage: MemTables and the +Block Cache. MemTables buffer data that has been written to the WAL +but not yet flushed to an SSTable. The Block Cache provides a cache of +uncompressed SSTable data blocks. + +Originally, Pebble used regular Go memory allocation for the memory +backing both MemTables and the Block Cache. This was problematic as it +put significant pressure on the Go GC. The higher the bandwidth of +memory allocations, the more work GC has to do to reclaim the +memory. In order to lessen the pressure on the Go GC, an "allocation +cache" was introduced to the Block Cache which allowed reusing the +memory backing cached blocks in most circumstances. This produced a +dramatic reduction in GC pressure and a measurable performance +improvement in CockroachDB workloads. + +Unfortunately, the use of Go allocated memory still caused a +problem. CockroachDB running on top of Pebble often resulted in an RSS +(resident set size) 2x what it was when using RocksDB. The cause of +this effect is due to the Go runtime's heuristic for triggering GC: + +> A collection is triggered when the ratio of freshly allocated data +> to live data remaining after the previous collection reaches this +> percentage. + +This percentage can be configured by the +[`GOGC`](https://golang.org/pkg/runtime/) environment variable or by +calling +[`debug.SetGCPercent`](https://golang.org/pkg/runtime/debug/#SetGCPercent). The +default value is `100`, which means that GC is triggered when the +freshly allocated data is equal to the amount of live data at the end +of the last collection period. This generally works well in practice, +but the Pebble Block Cache is often configured to be 10s of gigabytes +in size. Waiting for 10s of gigabytes of data to be allocated before +triggering a GC results in very large Go heap sizes. + +## Manual Memory Management + +Attempting to adjust `GOGC` to account for the significant amount of +memory used by the Block Cache is fraught. What value should be used? +`10%`? `20%`? Should the setting be tuned dynamically? Rather than +introducing a heuristic which may have cascading effects on the +application using Pebble, we decided to move the Block Cache and +MemTable memory out of the Go heap. This is done by using the C memory +allocator, though it could also be done by providing a simple memory +allocator in Go which uses `mmap` to allocate memory. + +In order to support manual memory management for the Block Cache and +MemTables, Pebble needs to precisely track their lifetime. This was +already being done for the MemTable in order to account for its memory +usage in metrics. It was mostly being done for the Block Cache. Values +stores in the Block Cache are reference counted and are returned to +the "alloc cache" when their reference count falls +to 0. Unfortunately, this tracking wasn't precise and there were +numerous cases where the cache values were being leaked. This was +acceptable in a world where the Go GC would clean up after us. It is +unacceptable if the leak becomes permanent. + +## Leak Detection + +In order to find all of the cache value leaks, Pebble has a leak +detection facility built on top of +[`runtime.SetFinalizer`](https://golang.org/pkg/runtime/#SetFinalizer). A +finalizer is a function associated with an object which is run when +the object is no longer reachable. On the surface, this sounds perfect +as a facility for performing all memory reclamation. Unfortunately, +finalizers are generally frowned upon by the Go implementors, and come +with very loose guarantees: + +> The finalizer is scheduled to run at some arbitrary time after the +> program can no longer reach the object to which obj points. There is +> no guarantee that finalizers will run before a program exits, so +> typically they are useful only for releasing non-memory resources +> associated with an object during a long-running program + +This language is somewhat frightening, but in practice finalizers are run at the +end of every GC period. Pebble primarily relies on finalizers for its leak +detection facility. In the block cache, a finalizer is associated with the Go +allocated `cache.Value` object. When the finalizer is run, it checks that the +buffer backing the `cache.Value` has been freed. This leak detection facility is +enabled by the `"invariants"` build tag which is enabled by the Pebble unit +tests. + +There also exists a very specific memory reclamation use case in the block cache +that ensures that structs with transitively reachable fields backed by manually +allocated memory that are pooled in a `sync.Pool` are freed correctly when their +parent struct is released from the pool and consequently garbage collected by +the Go runtime (see `cache/entry_normal.go`). The loose guarantees provided by +the runtime are reasonable to rely on in this case to prevent a memory leak. diff --git a/pebble/docs/range_deletions.md b/pebble/docs/range_deletions.md new file mode 100644 index 0000000..19b8f06 --- /dev/null +++ b/pebble/docs/range_deletions.md @@ -0,0 +1,471 @@ +# Range Deletions + +TODO: The following explanation of range deletions does not take into account +the recent change to prohibit splitting of a user key between sstables. This +change simplifies the logic, removing 'improperly truncated range tombstones.' + +TODO: The following explanation of range deletions ignores the +kind/trailer that appears at the end of keys after the sequence +number. This should be harmless but need to add a justification on why +it is harmless. + +## Background and Notation + +Range deletions are represented as `[start, end)#seqnum`. Points +(set/merge/...) are represented as `key#seqnum`. A range delete `[s, e)#n1` +deletes every point `k#n2` where `k \in [s, e)` and `n2 < n1`. +The inequality `n2 < n1` is to handle the case where a range delete and +a point have the same sequence number -- this happens during sstable +ingestion where the whole sstable is assigned a single sequence number +that applies to all the data in it. + +There is additionally an infinity sequence number, represented as +`inf`, which is not used for any point, that we can use for reasoning +about range deletes. + +It has been asked why range deletes use an exclusive end key instead +of an inclusive end key. For string keys, one can convert a desired +range delete on `[s, e]` into a range delete on `[s, ImmediateSuccessor(e))`. +For strings, the immediate successor of a key +is that key with a \0 appended to it. However one cannot go in the +other direction: if one could represent only inclusive end keys in a +range delete and one desires to delete a range with an exclusive end +key `[s, e)#n`, one needs to compute `ImmediatePredecessor(e)` which +is an infinite length string. For example, +`ImmediatePredecessor("ab")` is `"aa\xff\xff...."`. Additionally, +regardless of user needs, the exclusive end key helps with splitting a +range delete as we will see later. + +We will sometimes use ImmediatePredecessor and ImmediateSuccessor in +the following for illustrating an idea, but we do not rely on them as +something that is viable to produce for a particular kind of key. And +even if viable, these functions are not currently provided to +RockDB/Pebble. + +### Visualization + +If we consider a 2 dimensional space with increasing keys on the X +axis (with every possible user key represented) and increasing +sequence numbers on the Y axis, range deletes apply to a rectangle +whose bottom edge sits on the X axis. + +The actual space represented by the ordering in our sstables is a one +dimensional space where `k1#n1` is less than `k2#n2` if either of the +following holds: + +- k1 < k2 + +- k1 = k2 and n1 > n2 (under the assumption that no two points with +the same key have the same sequence number). + +``` + ^ + | . > . > . > yy + | . > . > . > . + | . > . > . > . +n | V > xx > . > V + | . > x. > x. > . + | . > x. > x. > . + | . > x. > x. > . + | .> x.> x.> . + ------------------------------------------> + k IS(k) IS(IS(k)) +``` + +The above figure uses `.` to represent points and the X axis is dense in +that it represents all possible keys. `xx` represents the start of a +range delete and `x.` are the points which it deletes. The arrows `V` and +`>` represent the ordering of the points in the one dimensional space. +`IS` is shorthand for `ImmediateSuccessor` and the range delete represented +there is `[k, IS(IS(k)))#n`. Ignore `yy` for now. + +The one dimensional space works fine in a world with only points. But +issues arise when storing range deletes, that represent an action in 2 +dimensional space, into this one dimensional space. + +## Range Delete Boundaries and the Simplest World + +RocksDB/Pebble store the inclusive bounds of each sstable in one dimensional +space. The range deletes two dimensional behavior and exclusive end key needs +to be adapted to this requirement. For a range delete `[s, e)#n`, +the smallest key it acts on is `s#(n-1)` and the largest key it +acts on is `ImmediatePredecessor(e)#0`. So if we position the range delete +immediately before the smallest key it acts on and immediately after +the largest key it acts on we can give it a tight inclusive bound of +`[s#n, e#inf]`. + +Note again that this range delete does not delete everything in its +inclusive bound. For example, range delete `["c", "h")#10` has a tight +inclusive bound of `["c"#10, "h"#inf]` but does not delete `"d"#11` +which lies in that bound. Going back to our earlier diagram, the one +dimensional inclusive bounds go from the `xx` to `yy` but there are +`.`s in between, in the one dimensional order, that are not deleted. + +This is the reason why one cannot in general +use a range delete to seek over all points within its bounds. The one +exception to this seeking behaviour is that when we can order sstables +from new to old, one can "blindly" use this range delete in a newer +sstable to seek to `"h"` in all older sstables since we know those +older sstables must only have point keys with sequence numbers `< 10` +for the keys in interval `["c", "h")`. This partial order across +sstables exists in RocksDB/Pebble between memtable, L0 sstables (where +it is a total order) and across sstables in different levels. + +Coming back to the inclusive bounds of the range delete, `[s#n, e#inf]`: +these bounds participate in deciding the bounds of the +sstable. In this world, one can read all the entries in an sstable and +compute its bounds. However being able to construct these bounds by +reading an sstable is not essential -- RocksDB/Pebble store these +bounds in the `MANIFEST`. This latter fact has been exploited to +construct a real world (later section) where the bounds of an sstable +are not computable by reading all its keys. + +If we had a system with one sstable per level, for each level lower +than L0, we are effectively done. We have represented the tight bounds +of each range delete and it is within the bounds of the sstable. This +works even with L0 => L0 compactions assuming they output exactly one +sstable. + +## The Mostly Simple World + +Here we have multiple files for levels lower than L0 that are non +overlapping in the file bounds. These multiple files occur because +compactions produce multiple files. This introduces the need to split a +range delete across the files being produced by a compaction. + +There is a clean way to split a range delete `[s, e)#n` into 2 parts +(which can be recursively applied to split into arbitrarily many +parts): split into `[s, m)#n` and `[m, e)#n`. These range deletes +apply to non-overlapping points and their tight bounds are `[s#m, +m#inf]`, `[m#n, e#inf]` which are also non-overlapping. + +Consider the following example of an input range delete `["c", "h")#10` and +the following two output files from a compaction: + +``` + sst1 sst2 +last point is "e"#7 | first point is "f"#20 +``` + +The range delete can be split into `["c", "f")#10` and `["f", +"h")#10`, by using the first point key of sst2 as the split +point. Then the bounds of sst1 and sst2 will be `[..., "f"#inf]` and +`["f"#20, ...]` which are non-overlapping. It is still possible to compute +the sstable bounds by looking at all the entries in the sstable. + +## The Real World + +Continuing with the same range delete `["c", "h")#10`, we can have the +following sstables produced during a compaction: + +``` + sst1 sst2 sst3 sst4 sst5 +points: "e"#7 | "f"#12 "f"#7 | "f"#4 "f"#3 | "f"#1 | "g"#15 +``` + +The range deletes written to these ssts are + +``` + sst1 sst2 sst3 sst4 sst5 +["c", "h")#10 | ["f", "h")#10 | ["f", "h")#10 | ["f", "h")#10 | ["g", "h")#10 +``` + +The Pebble code that achieves this effect is in +`rangedel.Fragmenter`. It is a code structuring artifact that sst1 +does not contain a range delete equal to `["c", "f")#10` and sst4 does +not contain `["f", "g")#10`. However for the range deletes in sst2 and +sst3 we cannot do any better because we don't know what the key +following "f" will be (the compaction cannot look ahead) and because +we don't have an `ImmediateSuccessor` function (otherwise we could +have written `["f", ImmediateSuccessor("f"))#10` to sst2, sst3). But +the code artifacts are not the ones introducing the real complexity. + +The range delete bounds are + +``` + sst1 sst2, sst3, sst4 sst5 +["c"#10, "h"#inf] ["f"#10, "h"#inf] ["g"#10, "h"#inf] + +``` + +We note the following: + +- The bounds of range deletes are overlapping since we have been + unable to split the range deletes. If these decide the sstable + bounds, the sstables will have overlapping bounds. This is not + permissible. + +- The range deletes included in each sstable result in that sstable + being "self-sufficient" wrt having the range delete that deletes + some of the points in the sstable (let us assume that the points in + this example have not been dropped from that sstable because of a + snapshot). + +- The transitions from sst1 to sst2 and sst4 to sst5 are **clean** in + that we can pretend that the range deletes in those files are actually: + +``` + sst1 sst2 sst3 sst4 sst5 +["c", "f")#10 | ["f", "g")#10 | ["f", "g")#10 | ["f", "g")#10 | ["g", "h")#10 +``` + +We could achieve some of these **clean** transitions (but not all) with a +code change. Also note that these better range deletes maintain the +"self-sufficient" property. + +### Making Non-overlapping SSTable bounds + +We force the sstable bounds to be non-overlapping by setting them to: + +``` + sst1 sst2 sst3 sst4 sst5 +["c"#10, "f"#inf] ["f"#12, "f"#7] ["f"#4, "f"#3] ["f"#1, "g"#inf] ["g"#15, "h"#inf] +``` + +Note that for sst1...sst4 the sstable bounds are smaller than the +bounds of the range deletes contained in them. The code that +accomplishes this is Pebble is in `compaction.go` -- we will not discuss the +details of that logic here but note that it is placing an `inf` +sequence number for a clean transition and for an unclean transition +it is using the point keys as the bounds. + +Associated with these narrower bounds, we add the following +requirement: a range delete in an sstable must **act-within** the bounds of +the sstable it is contained in. In the above example: + +- sst1: range delete `["c", "h")#10` must act-within the bound `["c"#10, "f"#inf]` + +- sst2: range delete `["f", "h")#10` must act-within the bound `["f"#12, "f"#7]` + +- sst3: range delete `["f", "h")#10` must act-within the bound `["f"#4, "f"#3]` + +- sst4: range delete `["f", "h")#10` must act-within the bound ["f"#1, "g"#inf] + +- And so on. + +The intuitive reason for the **act-within** requirement is that +sst5 can be compacted and moved down to a lower level independent of +sst1-sst4, since it was at a **clean** boundary. We do not want the +range delete `["f", "h")#10` sitting in sst1...sst4 at the higher +level to act on `"g"#15` that has been moved to the lower level. Note +that this incorrect action can happen due to 2 reasons: + +1. the invariant that lower levels have older data for keys + that also exist in higher levels means we can (a) seek a lower level + sstable to the end of a range delete from a higher level, (b) for a key + lookup, stop searching in lower levels once a range delete is encountered + for that key in a higher level. + +2. Sequence number zeroing logic can change the sequence number of + `"g"#15` to `"g"#0` (for better compression) once it realizes that + there are no older versions of `"g"`. It would be incorrect for this + `"g"#0` to be deleted. + + +#### Loss of Power + +This act-within behavior introduces some "loss of power" for +the original range delete `["c", "h")#10`. By acting within sst2...sst4 +it can no longer delete keys `"f"#6`, `"f"#5`, `"f"#2`. + +Luckily for us, this is harmless since these keys cannot have existed +in the system due to the levelling behavior: we cannot be writing +sst2...sst4 to level `i` if versions of `"f"` younger than `"f"#4` are +already in level `i` or version older than `"f"#7` have been left in +level i - 1. There is some trickery possible to prevent this "loss of +power" for queries (see the "Putting it together" section), but given +the history of bugs in this area, we should be cautious. + +### Improperly truncated Range Deletes + +We refer to range deletes that have experienced this "loss of power" +as **improper**. In the above example the range deletions in sst2, sst3, sst4 +are improper. The problem with improper range deletions occurs +when they need to participate in a future compaction: even though we +have restricted them to act-within their current sstable boundary, we +don't have a way of **"writing"** this restriction to a new sstable, +since they still need to be written in the `[s, e)#n` format. + +For example, sst2 has delete `["f", "h")#10` that must act-within +the bound `["f"#12, "f"#7]`. If sst2 was compacted down to the next +level into a new sstable (whose bounds we cannot predict because they +depend on other data written to that sstable) we need to be able to +write a range delete entry that follows the original restriction. But +the narrowest we can write is `["f", ImmediateSuccessor("f"))#10`. This +is an expansion of the act-within restriction with potentially +unintended consequences. In this case the expansion happened in the suffix. +For sst4, the range deletion `["f", "h")#10` must act-within `["f"#1, "g"#inf]`, +and we can precisely represent the constraint on the suffix by writing +`["f", "g")#10` but it does not precisely represent that this range delete +should not apply to `"f"#9`...`"f"#2`. + +In comparison, the sst1 range delete `["c", "h")#10` that must act-within +the bound `["c"#10, "f"#inf]` is not improper. This restriction can +be applied precisely to get a range delete `["c", "f")#10`. + +The solution to this is to note that while individual sstables have +improper range deletes, if we look at a collection of sstables we +can restore the improper range deletes spread across them to their proper self +(and their full power). To accurately find these improper range +deletes would require looking into the contents of a file, which is +expensive. But we can construct a pessimistic set based on +looking at the sequence of all files in a level and partitioning them: +adjacent files `f1`, `f2` with largest and smallest bound `k1#n1`, +`k2#n2` must be in the same partition if + +``` +k1 = k2 and n1 != inf +``` + +In the above example sst2, sst3, sst4 are one partition. The +**spanning bound** of this partition is `["f"#12, "g"#inf]` and the +range delete `["f", "h")#10` when constrained to act-within this +spanning bound is precisely the range delete `["f", +"g")#10`. Intuitively, the "loss of power" of this range delete has +been restored for the sake of making it proper, so it can be +accurately "written" in the output of the compaction (it may be +improperly fragmented again in the output, but we have already +discussed that). Such partitions are called "atomic compaction groups" +and must participate as a whole in a compaction (and a +compaction can use multiple atomic compaction groups as input). + +Consider another example: + +``` + sst1 sst2 +points: "e"#12 | "e"#10 +delete: ["c", "g")#8 | ["c", "g")#8 +bounds ["c"#8, "e"#12] | ["e"#10, "g"#inf] +``` + +sst1, sst2 are an atomic compaction group. Say we violated the +requirement that both be inputs in a compaction and only compacted +sst2 down to level `i + 1` and then down to level `i + 2`. Then we add +sst3 with bounds `["h"#10, "j"#5]` to level `i` and sst1 and sst3 are +compacted to level `i + 1` into a single sstable. This new sstable +will have bounds `["c"#8, "j"#5]` so these bounds do not help with the +original apply-witin constraint on `["c", "g")#8` (that it should +apply-within `["c"#8, "e"#12]`). The narrowest we can construct (if we had +`ImmediateSuccessor`) would be `["c", ImmediateSuccessor("e"))#8`. Now we +can incorrectly apply this range delete that is in level `i + 1` to `"e"#10` +sitting in level `i + 2`. Note that this example can be made worse using +sequence number zeroing -- `"e"#10` may have been rewritten to `"e"#0`. + +If a range delete `[s, e)#n` is in an atomic compaction group with +spanning bounds `[k1#n1, k2#n2]` our construction above guarantees the +following properties + +- `k1#n1 <= s#n`, so the bounds do not constrain the start of the + range delete. + +- `k2 >= e` or `n2 = inf`, so if `k2` is constraining the range delete + it will properly truncate the range delete. + + +#### New sstable at sequence number 0 + +A new sstable can be assigned sequence number 0 (and be written to L0) +if the keys in the sstable are not in any other sstable. This +comparison uses the keys and not key#seqnum, so the loss and +restoration of power does not cause problems since that occurs within +the versions of a single key. + +#### Flawed optimizations + +For the case where the atomic compaction group correspond to the lower +level of a compaction, it may initially seem to be correct to use only +a prefix or suffix of that group in a compaction. In this case the +prefix (suffix) will correspond to the largest key (smallest key) in +the input sstables in the compaction and so can continue to constrain +the range delete. For example, sst1 and sst2 are in the same atomic +compaction group + +``` + sst1 sst2 +points: "c"#10 "e"#12 | "e"#10 +delete: ["c", "g")#8 | ["c", "g")#8 +bounds ["c"#10, "e"#12] | ["e"#10, "g"#inf] +``` + +and this is the lower level of a compaction with + +``` + sst3 +points: "a"#14 "d"#15 +bounds ["a"#14, "d"#15] +``` + +we could allow for a compaction involving sst1 and sst3 which would produce + +``` + sst4 +points: "a"#14 "c"#10 "d"#15 "e"#12 +delete: ["c", "g")#8 +bounds ["a"#14, "e"#12] +``` + +and the range delete is still improper but its act-within constraint has +not expanded. + +But we have to be very careful to not have a more significant loss of power +of this range delete. Consider a situation where sst3 had a single delete +`"e"#16`. It still does not overlap in bounds with sst2 and we again pick +sst1 and sst3 for compaction. This single delete will cause `"e"#12` to be deleted +and sst4 bounds would be (unless we had complicated code preventing it): + +``` + sst4 +points: "a"#14 "c"#10 "d"#15 +delete: ["c", "g")#8 +bounds ["a"#14, "d"#15] +``` + +Now this delete cannot delete `"dd"#6` and we have lost the ability to know +that sst4 and sst2 are in the same atomic compaction group. + + +### Putting it together + +Summarizing the above, we have: + +- SStable bounds logic that ensures sstables are not +overlapping. These sstables contain range deletes that extend outside +these bounds. But these range deletes should **apply-within** the +sstable bounds. + +- Compactions: they need to constrain the range deletes in the inputs +to **apply-within**, but this can create problems with **writing** the +**improper** range deletes. The solution is to include the full +**atomic compaction group** in a compaction so we can restore the +**improper** range deletes to their **proper** self and then apply the +constraints of the atomic compaction group. + +- Queries: We need to act-within the file bound constraint on the range delete. + Say the range delete is `[s, e)#n` and the file bound is `[b1#n1, + b2#n2]`. We are guaranteed that `b1#n1 <= s#n` so the only + constraint can come from `b2#n2`. + + - Deciding whether a range delete covers a key in the same or lower levels. + + - `b2 >= e`: there is no act-within constraint. + - `b2 < e`: to be precise we cannot let it delete `b2#n2-1` or + later keys. But it is likely that allowing it to delete up to + `b2#0` would be ok due to the atomic compaction group. This + would prevent the so-called "loss of power" discussed earlier if + one also includes the argument that the gap in the file bounds + that also represents the loss of power is harmless (the gap + exists within versions of key, and anyone doing a query for that + key will start from the sstable to the left of the gap). But it + may be better to be cautious. + + - For using the range delete to seek sstables at lower levels. + - `b2 >= e`: seek to `e` since there is no act-within constraint. + - `b2 < e`: seek to `b2`. We are ignoring that this range delete + is allowed to delete some versions of `b2` since this is just a + performance optimization. + + + + + + diff --git a/pebble/docs/rocksdb.md b/pebble/docs/rocksdb.md new file mode 100644 index 0000000..8cf7ae9 --- /dev/null +++ b/pebble/docs/rocksdb.md @@ -0,0 +1,757 @@ +# Pebble vs RocksDB: Implementation Differences + +RocksDB is a key-value store implemented using a Log-Structured +Merge-Tree (LSM). This document is not a primer on LSMs. There exist +some decent +[introductions](http://www.benstopford.com/2015/02/14/log-structured-merge-trees/) +on the web, or try chapter 3 of [Designing Data-Intensive +Applications](https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321). + +Pebble inherits the RocksDB file formats, has a similar API, and +shares many implementation details, but it also has many differences +that improve performance, reduce implementation complexity, or extend +functionality. This document highlights some of the more important +differences. + +* [Internal Keys](#internal-keys) +* [Indexed Batches](#indexed-batches) +* [Large Batches](#large-batches) +* [Commit Pipeline](#commit-pipeline) +* [Range Deletions](#range-deletions) +* [Flush and Compaction Pacing](#flush-and-compaction-pacing) +* [Write Throttling](#write-throttling) +* [Other Differences](#other-differences) + +## Internal Keys + +The external RocksDB API accepts keys and values. Due to the LSM +structure, keys are never updated in place, but overwritten with new +versions. Inside RocksDB, these versioned keys are known as Internal +Keys. An Internal Key is composed of the user specified key, a +sequence number and a kind. On disk, sstables always store Internal +Keys. + +``` + +-------------+------------+----------+ + | UserKey (N) | SeqNum (7) | Kind (1) | + +-------------+------------+----------+ +``` + +The `Kind` field indicates the type of key: set, merge, delete, etc. + +While Pebble inherits the Internal Key encoding for format +compatibility, it diverges from RocksDB in how it manages Internal +Keys in its implementation. In RocksDB, Internal Keys are represented +either in encoded form (as a string) or as a `ParsedInternalKey`. The +latter is a struct with the components of the Internal Key as three +separate fields. + +```c++ +struct ParsedInternalKey { + Slice user_key; + uint64 seqnum; + uint8 kind; +} +``` + +The component format is convenient: changing the `SeqNum` or `Kind` is +field assignment. Extracting the `UserKey` is a field +reference. However, RocksDB tends to only use `ParsedInternalKey` +locally. The major internal APIs, such as `InternalIterator`, operate +using encoded internal keys (i.e. strings) for parameters and return +values. + +To give a concrete example of the overhead this causes, consider +`Iterator::Seek(user_key)`. The external `Iterator` is implemented on +top of an `InternalIterator`. `Iterator::Seek` ends up calling +`InternalIterator::Seek`. Both Seek methods take a key, but +`InternalIterator::Seek` expects an encoded Internal Key. This is both +error prone and expensive. The key passed to `Iterator::Seek` needs to +be copied into a temporary string in order to append the `SeqNum` and +`Kind`. In Pebble, Internal Keys are represented in memory using an +`InternalKey` struct that is the analog of `ParsedInternalKey`. All +internal APIs use `InternalKeys`, with the exception of the lowest +level routines for decoding data from sstables. In Pebble, since the +interfaces all take and return the `InternalKey` struct, we don’t need +to allocate to construct the Internal Key from the User Key, but +RocksDB sometimes needs to allocate, and encode (i.e. make a +copy). The use of the encoded form also causes RocksDB to pass encoded +keys to the comparator routines, sometimes decoding the keys multiple +times during the course of processing. + +## Indexed Batches + +In RocksDB, a batch is the unit for all write operations. Even writing +a single key is transformed internally to a batch. The batch internal +representation is a contiguous byte buffer with a fixed 12-byte +header, followed by a series of records. + +``` + +------------+-----------+--- ... ---+ + | SeqNum (8) | Count (4) | Entries | + +------------+-----------+--- ... ---+ +``` + +Each record has a 1-byte kind tag prefix, followed by 1 or 2 length +prefixed strings (varstring): + +``` + +----------+-----------------+-------------------+ + | Kind (1) | Key (varstring) | Value (varstring) | + +----------+-----------------+-------------------+ +``` + +(The `Kind` indicates if there are 1 or 2 varstrings. `Set`, `Merge`, +and `DeleteRange` have 2 varstrings, while `Delete` has 1.) + +Adding a mutation to a batch involves appending a new record to the +buffer. This format is extremely fast for writes, but the lack of +indexing makes it untenable to use directly for reads. In order to +support iteration, a separate indexing structure is created. Both +RocksDB and Pebble use a skiplist for the indexing structure, but with +a clever twist. Rather than the skiplist storing a copy of the key, it +simply stores the offset of the record within the mutation buffer. The +result is that the skiplist acts a multi-map (i.e. a map that can have +duplicate entries for a given key). The iteration order for this map +is constructed so that records sort on key, and for equal keys they +sort on descending offset. Newer records for the same key appear +before older records. + +While the indexing structure for batches is nearly identical between +RocksDB and Pebble, how the index structure is used is completely +different. In RocksDB, a batch is indexed using the +`WriteBatchWithIndex` class. The `WriteBatchWithIndex` class provides +a `NewIteratorWithBase` method that allows iteration over the merged +view of the batch contents and an underlying "base" iterator created +from the database. `BaseDeltaIterator` contains logic to iterate over +the batch entries and the base iterator in parallel which allows us to +perform reads on a snapshot of the database as though the batch had +been applied to it. On the surface this sounds reasonable, yet the +implementation is incomplete. Merge and DeleteRange operations are not +supported. The reason they are not supported is because handling them +is complex and requires duplicating logic that already exists inside +RocksDB for normal iterator processing. + +Pebble takes a different approach to iterating over a merged view of a +batch's contents and the underlying database: it treats the batch as +another level in the LSM. Recall that an LSM is composed of zero or +more memtable layers and zero or more sstable layers. Internally, both +RocksDB and Pebble contain a `MergingIterator` that knows how to merge +the operations from different levels, including processing overwritten +keys, merge operations, and delete range operations. The challenge +with treating the batch as another level to be used by a +`MergingIterator` is that the records in a batch do not have a +sequence number. The sequence number in the batch header is not +assigned until the batch is committed. The solution is to give the +batch records temporary sequence numbers. We need these temporary +sequence numbers to be larger than any other sequence number in the +database so that the records in the batch are considered newer than +any committed record. This is accomplished by reserving the high-bit +in the 56-bit sequence number for use as a marker for batch sequence +numbers. The sequence number for a record in an uncommitted batch is: + +``` + RecordOffset | (1<<55) +``` + +Newer records in a given batch will have a larger sequence number than +older records in the batch. And all of the records in a batch will +have larger sequence numbers than any committed record in the +database. + +The end result is that Pebble's batch iterators support all of the +functionality of regular database iterators with minimal additional +code. + +## Large Batches + +The size of a batch is limited only by available memory, yet the +required memory is not just the batch representation. When a batch is +committed, the commit operation iterates over the records in the batch +from oldest to newest and inserts them into the current memtable. The +memtable is an in-memory structure that buffers mutations that have +been committed (written to the Write Ahead Log), but not yet written +to an sstable. Internally, a memtable uses a skiplist to index +records. Each skiplist entry has overhead for the index links and +other metadata that is a dozen bytes at minimum. A large batch +composed of many small records can require twice as much memory when +inserted into a memtable than it required in the batch. And note that +this causes a temporary increase in memory requirements because the +batch memory is not freed until it is completely committed. + +A non-obvious implementation restriction present in both RocksDB and +Pebble is that there is a one-to-one correspondence between WAL files +and memtables. That is, a given WAL file has a single memtable +associated with it and vice-versa. While this restriction could be +removed, doing so is onerous and intricate. It should also be noted +that committing a batch involves writing it to a single WAL file. The +combination of restrictions results in a batch needing to be written +entirely to a single memtable. + +What happens if a batch is too large to fit in a memtable? Memtables +are generally considered to have a fixed size, yet this is not +actually true in RocksDB. In RocksDB, the memtable skiplist is +implemented on top of an arena structure. An arena is composed of a +list of fixed size chunks, with no upper limit set for the number of +chunks that can be associated with an arena. So RocksDB handles large +batches by allowing a memtable to grow beyond its configured +size. Concretely, while RocksDB may be configured with a 64MB memtable +size, a 1GB batch will cause the memtable to grow to accomodate +it. Functionally, this is good, though there is a practical problem: a +large batch is first written to the WAL, and then added to the +memtable. Adding the large batch to the memtable may consume so much +memory that the system runs out of memory and is killed by the +kernel. This can result in a death loop because upon restarting as the +batch is read from the WAL and applied to the memtable again. + +In Pebble, the memtable is also implemented using a skiplist on top of +an arena. Significantly, the Pebble arena is a fixed size. While the +RocksDB skiplist uses pointers, the Pebble skiplist uses offsets from +the start of the arena. The fixed size arena means that the Pebble +memtable cannot expand arbitrarily. A batch that is too large to fit +in the memtable causes the current mutable memtable to be marked as +immutable and the batch is wrapped in a `flushableBatch` structure and +added to the list of immutable memtables. Because the `flushableBatch` +is readable as another layer in the LSM, the batch commit can return +as soon as the `flushableBatch` has been added to the immutable +memtable list. + +Internally, a `flushableBatch` provides iterator support by sorting +the batch contents (the batch is sorted once, when it is added to the +memtable list). Sorting the batch contents and insertion of the +contents into a memtable have the same big-O time, but the constant +factor dominates here. Sorting is significantly faster and uses +significantly less memory due to not having to copy the batch records. + +Note that an effect of this large batch support is that Pebble can be +configured as an efficient on-disk sorter: specify a small memtable +size, disable the WAL, and set a large L0 compaction threshold. In +order to sort a large amount of data, create batches that are larger +than the memtable size and commit them. When committed these batches +will not be inserted into a memtable, but instead sorted and then +written out to L0. The fully sorted data can later be read and the +normal merging process will take care of the final ordering. + +## Commit Pipeline + +The commit pipeline is the component which manages the steps in +committing write batches, such as writing the batch to the WAL and +applying its contents to the memtable. While simple conceptually, the +commit pipeline is crucial for high performance. In the absence of +concurrency, commit performance is limited by how fast a batch can be +written (and synced) to the WAL and then added to the memtable, both +of which are outside of the purview of the commit pipeline. + +To understand the challenge here, it is useful to have a conception of +the WAL (write-ahead log). The WAL contains a record of all of the +batches that have been committed to the database. As a record is +written to the WAL it is added to the memtable. Each record is +assigned a sequence number which is used to distinguish newer updates +from older ones. Conceptually the WAL looks like: + +``` ++--------------------------------------+ +| Batch(SeqNum=1,Count=9,Records=...) | ++--------------------------------------+ +| Batch(SeqNum=10,Count=5,Records=...) | ++--------------------------------------+ +| Batch(SeqNum=15,Count=7,Records...) | ++--------------------------------------+ +| ... | ++--------------------------------------+ +``` + +Note that each WAL entry is precisely the batch representation +described earlier in the [Indexed Batches](#indexed-batches) +section. The monotonically increasing sequence numbers are a critical +component in allowing RocksDB and Pebble to provide fast snapshot +views of the database for reads. + +If concurrent performance was not a concern, the commit pipeline could +simply be a mutex which serialized writes to the WAL and application +of the batch records to the memtable. Concurrent performance is a +concern, though. + +The primary challenge in concurrent performance in the commit pipeline +is maintaining two invariants: + +1. Batches need to be written to the WAL in sequence number order. +2. Batches need to be made visible for reads in sequence number + order. This invariant arises from the use of a single sequence + number which indicates which mutations are visible. + +The second invariant deserves explanation. RocksDB and Pebble both +keep track of a visible sequence number. This is the sequence number +for which records in the database are visible during reads. The +visible sequence number exists because committing a batch is an atomic +operation, yet adding records to the memtable is done without an +exclusive lock (the skiplists used by both Pebble and RocksDB are +lock-free). When the records from a batch are being added to the +memtable, a concurrent read operation may see those records, but will +skip over them because they are newer than the visible sequence +number. Once all of the records in the batch have been added to the +memtable, the visible sequence number is atomically incremented. + +So we have four steps in committing a write batch: + +1. Write the batch to the WAL +2. Apply the mutations in the batch to the memtable +3. Bump the visible sequence number +4. (Optionally) sync the WAL + +Writing the batch to the WAL is actually very fast as it is just a +memory copy. Applying the mutations in the batch to the memtable is by +far the most CPU intensive part of the commit pipeline. Syncing the +WAL is the most expensive from a wall clock perspective. + +With that background out of the way, let's examine how RocksDB commits +batches. This description is of the traditional commit pipeline in +RocksDB (i.e. the one used by CockroachDB). + +RocksDB achieves concurrency in the commit pipeline by grouping +concurrently committed batches into a batch group. Each group is +assigned a "leader" which is the first batch to be added to the +group. The batch group is written atomically to the WAL by the leader +thread, and then the individual batches making up the group are +concurrently applied to the memtable. Lastly, the visible sequence +number is bumped such that all of the batches in the group become +visible in a single atomic step. While a batch group is being applied, +other concurrent commits are added to a waiting list. When the group +commit finishes, the waiting commits form the next group. + +There are two criticisms of the batch grouping approach. The first is +that forming a batch group involves copying batch contents. RocksDB +partially alleviates this for large batches by placing a limit on the +total size of a group. A large batch will end up in its own group and +not be copied, but the criticism still applies for small batches. Note +that there are actually two copies here. The batch contents are +concatenated together to form the group, and then the group contents +are written into an in memory buffer for the WAL before being written +to disk. + +The second criticism is about the thread synchronization points. Let's +consider what happens to a commit which becomes the leader: + +1. Lock commit mutex +2. Wait to become leader +3. Form (concatenate) batch group and write to the WAL +4. Notify followers to apply their batch to the memtable +5. Apply own batch to memtable +6. Wait for followers to finish +7. Bump visible sequence number +8. Unlock commit mutex +9. Notify followers that the commit is complete + +The follower's set of operations looks like: + +1. Lock commit mutex +2. Wait to become follower +3. Wait to be notified that it is time to apply batch +4. Unlock commit mutex +5. Apply batch to memtable +6. Wait to be notified that commit is complete + +The thread synchronization points (all of the waits and notifies) are +overhead. Reducing that overhead can improve performance. + +The Pebble commit pipeline addresses both criticisms. The main +innovation is a commit queue that mirrors the commit order. The Pebble +commit pipeline looks like: + +1. Lock commit mutex + * Add batch to commit queue + * Assign batch sequence number + * Write batch to the WAL +2. Unlock commit mutex +3. Apply batch to memtable (concurrently) +4. Publish batch sequence number + +Pebble does not use the concept of a batch group. Each batch is +individually written to the WAL, but note that the WAL write is just a +memory copy into an internal buffer in the WAL. + +Step 4 deserves further scrutiny as it is where the invariant on the +visible batch sequence number is maintained. Publishing the batch +sequence number cannot simply bump the visible sequence number because +batches with earlier sequence numbers may still be applying to the +memtable. If we were to ratchet the visible sequence number without +waiting for those applies to finish, a concurrent reader could see +partial batch contents. Note that RocksDB has experimented with +allowing these semantics with its unordered writes option. + +We want to retain the atomic visibility of batch commits. The publish +batch sequence number step needs to ensure that we don't ratchet the +visible sequence number until all batches with earlier sequence +numbers have applied. Enter the commit queue: a lock-free +single-producer, multi-consumer queue. Batches are added to the commit +queue with the commit mutex held, ensuring the same order as the +sequence number assignment. After a batch finishes applying to the +memtable, it atomically marks the batch as applied. It then removes +the prefix of applied batches from the commit queue, bumping the +visible sequence number, and marking the batch as committed (via a +`sync.WaitGroup`). If the first batch in the commit queue has not be +applied we wait for our batch to be committed, relying on another +concurrent committer to perform the visible sequence ratcheting for +our batch. We know a concurrent commit is taking place because if +there was only one batch committing it would be at the head of the +commit queue. + +There are two possibilities when publishing a sequence number. The +first is that there is an unapplied batch at the head of the +queue. Consider the following scenario where we're trying to publish +the sequence number for batch `B`. + +``` + +---------------+-------------+---------------+-----+ + | A (unapplied) | B (applied) | C (unapplied) | ... | + +---------------+-------------+---------------+-----+ +``` + +The publish routine will see that `A` is unapplied and then simply +wait for `B's` done `sync.WaitGroup` to be signalled. This is safe +because `A` must still be committing. And if `A` has concurrently been +marked as applied, the goroutine publishing `A` will then publish +`B`. What happens when `A` publishes its sequence number? The commit +queue state becomes: + +``` + +-------------+-------------+---------------+-----+ + | A (applied) | B (applied) | C (unapplied) | ... | + +-------------+-------------+---------------+-----+ +``` + +The publish routine pops `A` from the queue, ratchets the sequence +number, then pops `B` and ratchets the sequence number again, and then +finds `C` and stops. A detail that it is important to notice is that +the committer for batch `B` didn't have to do any more work. An +alternative approach would be to have `B` wakeup and ratchet its own +sequence number, but that would serialize the remainder of the commit +queue behind that goroutine waking up. + +The commit queue reduces the number of thread synchronization +operations required to commit a batch. There is no leader to notify, +or followers to wait for. A commit either publishes its own sequence +number, or performs one synchronization operation to wait for a +concurrent committer to publish its sequence number. + +## Range Deletions + +Deletion of an individual key in RocksDB and Pebble is accomplished by +writing a deletion tombstone. A deletion tombstone shadows an existing +value for a key, causing reads to treat the key as not present. The +deletion tombstone mechanism works well for deleting small sets of +keys, but what happens if you want to all of the keys within a range +of keys that might number in the thousands or millions? A range +deletion is an operation which deletes an entire range of keys with a +single record. In contrast to a point deletion tombstone which +specifies a single key, a range deletion tombstone (a.k.a. range +tombstone) specifies a start key (inclusive) and an end key +(exclusive). This single record is much faster to write than thousands +or millions of point deletion tombstones, and can be done blindly -- +without iterating over the keys that need to be deleted. The downside +to range tombstones is that they require additional processing during +reads. How the processing of range tombstones is done significantly +affects both the complexity of the implementation, and the efficiency +of read operations in the presence of range tombstones. + +A range tombstone is composed of a start key, end key, and sequence +number. Any key that falls within the range is considered deleted if +the key's sequence number is less than the range tombstone's sequence +number. RocksDB stores range tombstones segregated from point +operations in a special range deletion block within each sstable. +Conceptually, the range tombstones stored within an sstable are +truncated to the boundaries of the sstable, though there are +complexities that cause this to not actually be physically true. + +In RocksDB, the main structure implementing range tombstone processing +is the `RangeDelAggregator`. Each read operation and iterator has its +own `RangeDelAggregator` configured for the sequence number the read +is taking place at. The initial implementation of `RangeDelAggregator` +built up a "skyline" for the range tombstones visible at the read +sequence number. + +``` +10 +---+ + 9 | | + 8 | | + 7 | +----+ + 6 | | + 5 +-+ | +----+ + 4 | | | | + 3 | | | +---+ + 2 | | | | + 1 | | | | + 0 | | | | + abcdefghijklmnopqrstuvwxyz +``` + +The above diagram shows the skyline created for the range tombstones +`[b,j)#5`, `[d,h)#10`, `[f,m)#7`, `[p,u)#5`, and `[t,y)#3`. The +skyline is queried for each key read to see if the key should be +considered deleted or not. The skyline structure is stored in a binary +tree, making the queries an O(logn) operation in the number of +tombstones, though there is an optimization to make this O(1) for +`next`/`prev` iteration. Note that the skyline representation loses +information about the range tombstones. This requires the structure to +be rebuilt on every read which has a significant performance impact. + +The initial skyline range tombstone implementation has since been +replaced with a more efficient lookup structure. See the +[DeleteRange](https://rocksdb.org/blog/2018/11/21/delete-range.html) +blog post for a good description of both the original implementation +and the new (v2) implementation. The key change in the new +implementation is to "fragment" the range tombstones that are stored +in an sstable. The fragmented range tombstones provide the same +benefit as the skyline representation: the ability to binary search +the fragments in order to find the tombstone covering a key. But +unlike the skyline approach, the fragmented tombstones can be cached +on a per-sstable basis. In the v2 approach, `RangeDelAggregator` keeps +track of the fragmented range tombstones for each sstable encountered +during a read or iterator, and logically merges them together. + +Fragmenting range tombstones involves splitting range tombstones at +overlap points. Let's consider the tombstones in the skyline example +above: + +``` +10: d---h + 7: f------m + 5: b-------j p----u + 3: t----y +``` + +Fragmenting the range tombstones at the overlap points creates a +larger number of range tombstones: + +``` +10: d-f-h + 7: f-h-j--m + 5: b-d-f-h-j p---tu + 3: tu---y +``` + +While the number of tombstones is larger there is a significant +advantage: we can order the tombstones by their start key and then +binary search to find the set of tombstones overlapping a particular +point. This is possible because due to the fragmenting, all the +tombstones that overlap a range of keys will have the same start and +end key. The v2 `RangeDelAggregator` and associated classes perform +fragmentation of range tombstones stored in each sstable and those +fragmented tombstones are then cached. + +In summary, in RocksDB `RangeDelAggregator` acts as an oracle for +answering whether a key is deleted at a particular sequence +number. Due to caching of fragmented tombstones, the v2 implementation +of `RangeDelAggregator` implementation is significantly faster to +populate than v1, yet the overall approach to processing range +tombstones remains similar. + +Pebble takes a different approach: it integrates range tombstones +processing directly into the `mergingIter` structure. `mergingIter` is +the internal structure which provides a merged view of the levels in +an LSM. RocksDB has a similar class named +`MergingIterator`. Internally, `mergingIter` maintains a heap over the +levels in the LSM (note that each memtable and L0 table is a separate +"level" in `mergingIter`). In RocksDB, `MergingIterator` knows nothing +about range tombstones, and it is thus up to higher-level code to +process range tombstones using `RangeDelAggregator`. + +While the separation of `MergingIterator` and range tombstones seems +reasonable at first glance, there is an optimization that RocksDB does +not perform which is awkward with the `RangeDelAggregator` approach: +skipping swaths of deleted keys. A range tombstone often shadows more +than one key. Rather than iterating over the deleted keys, it is much +quicker to seek to the end point of the range tombstone. The challenge +in implementing this optimization is that a key might be newer than +the range tombstone and thus shouldn't be skipped. An insight to be +utilized is that the level structure itself provides sufficient +information. A range tombstone at `Ln` is guaranteed to be newer than +any key it overlaps in `Ln+1`. + +Pebble utilizes the insight above to integrate range deletion +processing with `mergingIter`. A `mergingIter` maintains a point +iterator and a range deletion iterator per level in the LSM. In this +context, every L0 table is a separate level, as is every +memtable. Within a level, when a range deletion contains a point +operation the sequence numbers must be checked to determine if the +point operation is newer or older than the range deletion +tombstone. The `mergingIter` maintains the invariant that the range +deletion iterators for all levels newer that the current iteration key +are positioned at the next (or previous during reverse iteration) +range deletion tombstone. We know those levels don't contain a range +deletion tombstone that covers the current key because if they did the +current key would be deleted. The range deletion iterator for the +current key's level is positioned at a range tombstone covering or +past the current key. The position of all of other range deletion +iterators is unspecified. Whenever a key from those levels becomes the +current key, their range deletion iterators need to be +positioned. This lazy positioning avoids seeking the range deletion +iterators for keys that are never considered. + +For a full example, consider the following setup: + +``` + p0: o + r0: m---q + + p1: n p + r1: g---k + + p2: b d i + r2: a---e q----v + + p3: e + r3: +``` + +The diagram above shows is showing 4 levels, with `pX` indicating the +point operations in a level and `rX` indicating the range tombstones. + +If we start iterating from the beginning, the first key we encounter +is `b` in `p2`. When the mergingIter is pointing at a valid entry, the +range deletion iterators for all of the levels less that the current +key's level are positioned at the next range tombstone past the +current key. So `r0` will point at `[m,q)` and `r1` at `[g,k)`. When +the key `b` is encountered, we check to see if the current tombstone +for `r0` or `r1` contains it, and whether the tombstone for `r2`, +`[a,e)`, contains and is newer than `b`. + +Advancing the iterator finds the next key at `d`. This is in the same +level as the previous key `b` so we don't have to reposition any of +the range deletion iterators, but merely check whether `d` is now +contained by any of the range tombstones at higher levels or has +stepped past the range tombstone in its own level. In this case, there +is nothing to be done. + +Advancing the iterator again finds `e`. Since `e` comes from `p3`, we +have to position the `r3` range deletion iterator, which is empty. `e` +is past the `r2` tombstone of `[a,e)` so we need to advance the `r2` +range deletion iterator to `[q,v)`. + +The next key is `i`. Because this key is in `p2`, a level above `e`, +we don't have to reposition any range deletion iterators and instead +see that `i` is covered by the range tombstone `[g,k)`. The iterator +is immediately advanced to `n` which is covered by the range tombstone +`[m,q)` causing the iterator to advance to `o` which is visible. + +## Flush and Compaction Pacing + +Flushes and compactions in LSM trees are problematic because they +contend with foreground traffic, resulting in write and read latency +spikes. Without throttling the rate of flushes and compactions, they +occur "as fast as possible" (which is not entirely true, since we +have a `bytes_per_sync` option). This instantaneous usage of CPU and +disk IO results in potentially huge latency spikes for writes and +reads which occur in parallel to the flushes and compactions. + +RocksDB attempts to solve this issue by offering an option to limit +the speed of flushes and compactions. A maximum `bytes/sec` can be +specified through the options, and background IO usage will be limited +to the specified amount. Flushes are given priority over compactions, +but they still use the same rate limiter. Though simple to implement +and understand, this option is fragile for various reasons. + +1) If the rate limit is configured too low, the DB will stall and +write throughput will be affected. +2) If the rate limit is configured too high, the write and read +latency spikes will persist. +3) A different configuration is needed per system depending on the +speed of the storage device. +4) Write rates typically do not stay the same throughout the lifetime +of the DB (higher throughput during certain times of the day, etc) but +the rate limit cannot be configured during runtime. + +RocksDB also offers an +["auto-tuned" rate limiter](https://rocksdb.org/blog/2017/12/18/17-auto-tuned-rate-limiter.html) +which uses a simple multiplicative-increase, multiplicative-decrease +algorithm to dynamically adjust the background IO rate limit depending +on how much of the rate limiter has been exhausted in an interval. +This solves the problem of having a static rate limit, but Pebble +attempts to improve on this with a different pacing mechanism. + +Pebble's pacing mechanism uses separate rate limiters for flushes and +compactions. Both the flush and compaction pacing mechanisms work by +attempting to flush and compact only as fast as needed and no faster. +This is achieved differently for flushes versus compactions. + +For flush pacing, Pebble keeps the rate at which the memtable is +flushed at the same rate as user writes. This ensures that disk IO +used by flushes remains steady. When a mutable memtable becomes full +and is marked immutable, it is typically flushed as fast as possible. +Instead of flushing as fast as possible, what we do is look at the +total number of bytes in all the memtables (mutable + queue of +immutables) and subtract the number of bytes that have been flushed in +the current flush. This number gives us the total number of bytes +which remain to be flushed. If we keep this number steady at a constant +level, we have the invariant that the flush rate is equal to the write +rate. + +When the number of bytes remaining to be flushed falls below our +target level, we slow down the speed of flushing. We keep a minimum +rate at which the memtable is flushed so that flushes proceed even if +writes have stopped. When the number of bytes remaining to be flushed +goes above our target level, we allow the flush to proceed as fast as +possible, without applying any rate limiting. However, note that the +second case would indicate that writes are occurring faster than the +memtable can flush, which would be an unsustainable rate. The LSM +would soon hit the memtable count stall condition and writes would be +completely stopped. + +For compaction pacing, Pebble uses an estimation of compaction debt, +which is the number of bytes which need to be compacted before no +further compactions are needed. This estimation is calculated by +looking at the number of bytes that have been flushed by the current +flush routine, adding those bytes to the size of the level 0 sstables, +then seeing how many bytes exceed the target number of bytes for the +level 0 sstables. We multiply the number of bytes exceeded by the +level ratio and add that number to the compaction debt estimate. +We repeat this process until the final level, which gives us a final +compaction debt estimate for the entire LSM tree. + +Like with flush pacing, we want to keep the compaction debt at a +constant level. This ensures that compactions occur only as fast as +needed and no faster. If the compaction debt estimate falls below our +target level, we slow down compactions. We maintain a minimum +compaction rate so that compactions proceed even if flushes have +stopped. If the compaction debt goes above our target level, we let +compactions proceed as fast as possible without any rate limiting. +Just like with flush pacing, this would indicate that writes are +occurring faster than the background compactions can keep up with, +which is an unsustainable rate. The LSM's read amplification would +increase and the L0 file count stall condition would be hit. + +With the combined flush and compaction pacing mechanisms, flushes and +compactions only occur as fast as needed and no faster, which reduces +latency spikes for user read and write operations. + +## Write throttling + +RocksDB adds artificial delays to user writes when certain thresholds +are met, such as `l0_slowdown_writes_threshold`. These artificial +delays occur when the system is close to stalling to lessen the write +pressure so that flushing and compactions can catch up. On the surface +this seems good, since write stalls would seemingly be eliminated and +replaced with gradual slowdowns. Closed loop write latency benchmarks +would show the elimination of abrupt write stalls, which seems +desirable. + +However, this doesn't do anything to improve latencies in an open loop +model, which is the model more likely to resemble real world use +cases. Artificial delays increase write latencies without a clear +benefit. Writes stalls in an open loop system would indicate that +writes are generated faster than the system could possibly handle, +which adding artificial delays won't solve. + +For this reason, Pebble doesn't add artificial delays to user writes +and writes are served as quickly as possible. + +### Other Differences + +* `internalIterator` API which minimizes indirect (virtual) function + calls +* Previous pointers in the memtable and indexed batch skiplists +* Elision of per-key lower/upper bound checks in long range scans +* Improved `Iterator` API + + `SeekPrefixGE` for prefix iteration + + `SetBounds` for adjusting the bounds on an existing `Iterator` +* Simpler `Get` implementation diff --git a/pebble/error_iter.go b/pebble/error_iter.go new file mode 100644 index 0000000..10bc9cc --- /dev/null +++ b/pebble/error_iter.go @@ -0,0 +1,86 @@ +// Copyright 2018 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" +) + +type errorIter struct { + err error +} + +// errorIter implements the base.InternalIterator interface. +var _ internalIterator = (*errorIter)(nil) + +func (c *errorIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, base.LazyValue) { + return nil, base.LazyValue{} +} + +func (c *errorIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + return nil, base.LazyValue{} +} + +func (c *errorIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, base.LazyValue) { + return nil, base.LazyValue{} +} + +func (c *errorIter) First() (*InternalKey, base.LazyValue) { + return nil, base.LazyValue{} +} + +func (c *errorIter) Last() (*InternalKey, base.LazyValue) { + return nil, base.LazyValue{} +} + +func (c *errorIter) Next() (*InternalKey, base.LazyValue) { + return nil, base.LazyValue{} +} + +func (c *errorIter) Prev() (*InternalKey, base.LazyValue) { + return nil, base.LazyValue{} +} + +func (c *errorIter) NextPrefix([]byte) (*InternalKey, base.LazyValue) { + return nil, base.LazyValue{} +} + +func (c *errorIter) Error() error { + return c.err +} + +func (c *errorIter) Close() error { + return c.err +} + +func (c *errorIter) String() string { + return "error" +} + +func (c *errorIter) SetBounds(lower, upper []byte) {} + +func (c *errorIter) SetContext(_ context.Context) {} + +type errorKeyspanIter struct { + err error +} + +// errorKeyspanIter implements the keyspan.FragmentIterator interface. +var _ keyspan.FragmentIterator = (*errorKeyspanIter)(nil) + +func (*errorKeyspanIter) SeekGE(key []byte) *keyspan.Span { return nil } +func (*errorKeyspanIter) SeekLT(key []byte) *keyspan.Span { return nil } +func (*errorKeyspanIter) First() *keyspan.Span { return nil } +func (*errorKeyspanIter) Last() *keyspan.Span { return nil } +func (*errorKeyspanIter) Next() *keyspan.Span { return nil } +func (*errorKeyspanIter) Prev() *keyspan.Span { return nil } +func (i *errorKeyspanIter) Error() error { return i.err } +func (i *errorKeyspanIter) Close() error { return i.err } +func (*errorKeyspanIter) String() string { return "error" } diff --git a/pebble/error_test.go b/pebble/error_test.go new file mode 100644 index 0000000..82af4a4 --- /dev/null +++ b/pebble/error_test.go @@ -0,0 +1,429 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "bytes" + "fmt" + "math" + "strings" + "sync/atomic" + "testing" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/stretchr/testify/require" +) + +type panicLogger struct{} + +func (l panicLogger) Infof(format string, args ...interface{}) {} +func (l panicLogger) Errorf(format string, args ...interface{}) {} + +func (l panicLogger) Fatalf(format string, args ...interface{}) { + panic(errors.Errorf("fatal: "+format, args...)) +} + +// corruptFS injects a corruption in the `index`th byte read. +type corruptFS struct { + vfs.FS + // index is the index of the byte which we will corrupt. + index atomic.Int32 + bytesRead atomic.Int32 +} + +func (fs *corruptFS) maybeCorrupt(n int32, p []byte) { + newBytesRead := fs.bytesRead.Add(n) + pIdx := newBytesRead - 1 - fs.index.Load() + if pIdx >= 0 && pIdx < n { + p[pIdx]++ + } +} + +func (fs *corruptFS) maybeCorruptAt(n int32, p []byte, offset int64) { + pIdx := fs.index.Load() - int32(offset) + if pIdx >= 0 && pIdx < n { + p[pIdx]++ + } +} + +func (fs *corruptFS) Open(name string, opts ...vfs.OpenOption) (vfs.File, error) { + f, err := fs.FS.Open(name) + if err != nil { + return nil, err + } + cf := corruptFile{f, fs} + for _, opt := range opts { + opt.Apply(cf) + } + return cf, nil +} + +type corruptFile struct { + vfs.File + fs *corruptFS +} + +func (f corruptFile) Read(p []byte) (int, error) { + n, err := f.File.Read(p) + f.fs.maybeCorrupt(int32(n), p) + return n, err +} + +func (f corruptFile) ReadAt(p []byte, off int64) (int, error) { + n, err := f.File.ReadAt(p, off) + f.fs.maybeCorruptAt(int32(n), p, off) + return n, err +} + +func expectLSM(expected string, d *DB, t *testing.T) { + t.Helper() + expected = strings.TrimSpace(expected) + d.mu.Lock() + actual := d.mu.versions.currentVersion().String() + d.mu.Unlock() + actual = strings.TrimSpace(actual) + if expected != actual { + t.Fatalf("expected\n%s\nbut found\n%s", expected, actual) + } +} + +// TestErrors repeatedly runs a short sequence of operations, injecting FS +// errors at different points, until success is achieved. +func TestErrors(t *testing.T) { + run := func(fs *errorfs.FS) (err error) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = e + } else { + t.Fatal(r) + } + } + }() + + d, err := Open("", &Options{ + FS: fs, + Logger: panicLogger{}, + }) + if err != nil { + return err + } + + key := []byte("a") + value := []byte("b") + if err := d.Set(key, value, nil); err != nil { + return err + } + if err := d.Flush(); err != nil { + return err + } + if err := d.Compact(nil, []byte("\xff"), false); err != nil { + return err + } + + iter, _ := d.NewIter(nil) + for valid := iter.First(); valid; valid = iter.Next() { + } + if err := iter.Close(); err != nil { + return err + } + return d.Close() + } + + errorCounts := make(map[string]int) + for i := int32(0); ; i++ { + fs := errorfs.Wrap(vfs.NewMem(), errorfs.ErrInjected.If(errorfs.OnIndex(i))) + err := run(fs) + if err == nil { + t.Logf("success %d\n", i) + break + } + errorCounts[err.Error()]++ + } + + expectedErrors := []string{ + "fatal: MANIFEST flush failed: injected error", + "fatal: MANIFEST sync failed: injected error", + "fatal: MANIFEST set current failed: injected error", + "fatal: MANIFEST dirsync failed: injected error", + } + for _, expected := range expectedErrors { + if errorCounts[expected] == 0 { + t.Errorf("expected error %q did not occur", expected) + } + } +} + +// TestRequireReadError injects FS errors into read operations at successively later +// points until all operations can complete. It requires an operation fails any time +// an error was injected. This differs from the TestErrors case above as that one +// cannot require operations fail since it involves flush/compaction, which retry +// internally and succeed following an injected error. +func TestRequireReadError(t *testing.T) { + run := func(formatVersion FormatMajorVersion, index int32) (err error) { + // Perform setup with error injection disabled as it involves writes/background ops. + ii := errorfs.OnIndex(-1) + fs := errorfs.Wrap(vfs.NewMem(), errorfs.ErrInjected.If(ii)) + opts := &Options{ + FS: fs, + Logger: panicLogger{}, + FormatMajorVersion: formatVersion, + } + opts.private.disableTableStats = true + d, err := Open("", opts) + require.NoError(t, err) + + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + key1 := []byte("a1") + key2 := []byte("a2") + value := []byte("b") + require.NoError(t, d.Set(key1, value, nil)) + require.NoError(t, d.Set(key2, value, nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact(key1, key2, false)) + require.NoError(t, d.DeleteRange(key1, key2, nil)) + require.NoError(t, d.Set(key1, value, nil)) + require.NoError(t, d.Flush()) + if formatVersion < FormatSetWithDelete { + expectLSM(` +0.0: + 000007:[a1#13,SET-a2#inf,RANGEDEL] +6: + 000005:[a1#10,SET-a2#11,SET] +`, d, t) + } else { + expectLSM(` +0.0: + 000007:[a1#13,SETWITHDEL-a2#inf,RANGEDEL] +6: + 000005:[a1#10,SET-a2#11,SET] +`, d, t) + } + + // Now perform foreground ops with error injection enabled. + ii.Store(index) + iter, _ := d.NewIter(nil) + if err := iter.Error(); err != nil { + return err + } + numFound := 0 + expectedKeys := [][]byte{key1, key2} + for valid := iter.First(); valid; valid = iter.Next() { + if !bytes.Equal(iter.Key(), expectedKeys[numFound]) { + t.Fatalf("expected key %v; found %v", expectedKeys[numFound], iter.Key()) + } + if !bytes.Equal(iter.Value(), value) { + t.Fatalf("expected value %v; found %v", value, iter.Value()) + } + numFound++ + } + if err := iter.Close(); err != nil { + return err + } + if err := d.Close(); err != nil { + d = nil + return err + } + d = nil + // Reaching here implies all read operations succeeded. This + // should only happen when we reached a large enough index at + // which `errorfs.FS` did not return any error. + if i := ii.Load(); i < 0 { + t.Errorf("FS error injected %d ops ago went unreported", -i) + } + if numFound != 2 { + t.Fatalf("expected 2 values; found %d", numFound) + } + return nil + } + + versions := []FormatMajorVersion{FormatMostCompatible, FormatSetWithDelete} + for _, version := range versions { + t.Run(fmt.Sprintf("version-%s", version), func(t *testing.T) { + for i := int32(0); ; i++ { + err := run(version, i) + if err == nil { + t.Logf("no failures reported at index %d", i) + break + } + } + }) + } +} + +// TestCorruptReadError verifies that reads to a corrupted file detect the +// corruption and return an error. In this case the filesystem reads return +// successful status but the data they return is corrupt. +func TestCorruptReadError(t *testing.T) { + run := func(formatVersion FormatMajorVersion, index int32) (err error) { + // Perform setup with corruption injection disabled as it involves writes/background ops. + fs := &corruptFS{ + FS: vfs.NewMem(), + } + fs.index.Store(-1) + opts := &Options{ + FS: fs, + Logger: panicLogger{}, + FormatMajorVersion: formatVersion, + } + opts.private.disableTableStats = true + d, err := Open("", opts) + if err != nil { + t.Fatalf("%v", err) + } + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + key1 := []byte("a1") + key2 := []byte("a2") + value := []byte("b") + require.NoError(t, d.Set(key1, value, nil)) + require.NoError(t, d.Set(key2, value, nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact(key1, key2, false)) + require.NoError(t, d.DeleteRange(key1, key2, nil)) + require.NoError(t, d.Set(key1, value, nil)) + require.NoError(t, d.Flush()) + if formatVersion < FormatSetWithDelete { + expectLSM(` +0.0: + 000007:[a1#13,SET-a2#inf,RANGEDEL] +6: + 000005:[a1#10,SET-a2#11,SET] +`, d, t) + + } else { + expectLSM(` +0.0: + 000007:[a1#13,SETWITHDEL-a2#inf,RANGEDEL] +6: + 000005:[a1#10,SET-a2#11,SET] +`, d, t) + } + + // Now perform foreground ops with corruption injection enabled. + fs.index.Store(index) + iter, _ := d.NewIter(nil) + if err := iter.Error(); err != nil { + return err + } + + numFound := 0 + expectedKeys := [][]byte{key1, key2} + for valid := iter.First(); valid; valid = iter.Next() { + if !bytes.Equal(iter.Key(), expectedKeys[numFound]) { + t.Fatalf("expected key %v; found %v", expectedKeys[numFound], iter.Key()) + } + if !bytes.Equal(iter.Value(), value) { + t.Fatalf("expected value %v; found %v", value, iter.Value()) + } + numFound++ + } + if err := iter.Close(); err != nil { + return err + } + if err := d.Close(); err != nil { + return err + } + d = nil + // Reaching here implies all read operations succeeded. This + // should only happen when we reached a large enough index at + // which `corruptFS` did not inject any corruption. + if bytesRead := fs.bytesRead.Load(); bytesRead > index { + t.Errorf("corruption error injected at index %d went unreported", index) + } + if numFound != 2 { + t.Fatalf("expected 2 values; found %d", numFound) + } + return nil + } + versions := []FormatMajorVersion{FormatMostCompatible, FormatSetWithDelete} + for _, version := range versions { + t.Run(fmt.Sprintf("version-%s", version), func(t *testing.T) { + for i := int32(0); ; i++ { + err := run(version, i) + if err == nil { + t.Logf("no failures reported at index %d", i) + break + } + } + }) + } +} + +func TestDBWALRotationCrash(t *testing.T) { + memfs := vfs.NewStrictMem() + + var index atomic.Int32 + inj := errorfs.InjectorFunc(func(op errorfs.Op) error { + if op.Kind.ReadOrWrite() == errorfs.OpIsWrite && index.Add(-1) == -1 { + memfs.SetIgnoreSyncs(true) + } + return nil + }) + triggered := func() bool { return index.Load() < 0 } + + run := func(fs *errorfs.FS, k int32) (err error) { + opts := &Options{ + FS: fs, + Logger: panicLogger{}, + MemTableSize: 2048, + } + opts.private.disableTableStats = true + d, err := Open("", opts) + if err != nil || triggered() { + return err + } + + // Write keys with the FS set up to simulate a crash by ignoring + // syncs on the k-th write operation. + index.Store(k) + key := []byte("test") + for i := 0; i < 10; i++ { + v := []byte(strings.Repeat("b", i)) + err = d.Set(key, v, nil) + if err != nil || triggered() { + break + } + } + err = firstError(err, d.Close()) + return err + } + + fs := errorfs.Wrap(memfs, inj) + for k := int32(0); ; k++ { + // Run, simulating a crash by ignoring syncs after the k-th write + // operation after Open. + index.Store(math.MaxInt32) + err := run(fs, k) + if !triggered() { + // Stop when we reach a value of k greater than the number of + // write operations performed during `run`. + t.Logf("No crash at write operation %d\n", k) + if err != nil { + t.Fatalf("Filesystem did not 'crash', but error returned: %s", err) + } + break + } + t.Logf("Crashed at write operation % 2d, error: %v\n", k, err) + + // Reset the filesystem to its state right before the simulated + // "crash", restore syncs, and run again without crashing. + memfs.ResetToSyncedState() + memfs.SetIgnoreSyncs(false) + index.Store(math.MaxInt32) + require.NoError(t, run(fs, k)) + } +} diff --git a/pebble/event.go b/pebble/event.go new file mode 100644 index 0000000..ea527ef --- /dev/null +++ b/pebble/event.go @@ -0,0 +1,767 @@ +// Copyright 2018 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 ( + "fmt" + "strings" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/redact" +) + +// TableInfo exports the manifest.TableInfo type. +type TableInfo = manifest.TableInfo + +func tablesTotalSize(tables []TableInfo) uint64 { + var size uint64 + for i := range tables { + size += tables[i].Size + } + return size +} + +func formatFileNums(tables []TableInfo) string { + var buf strings.Builder + for i := range tables { + if i > 0 { + buf.WriteString(" ") + } + buf.WriteString(tables[i].FileNum.String()) + } + return buf.String() +} + +// LevelInfo contains info pertaining to a particular level. +type LevelInfo struct { + Level int + Tables []TableInfo + Score float64 +} + +func (i LevelInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i LevelInfo) SafeFormat(w redact.SafePrinter, _ rune) { + 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. +type CompactionInfo struct { + // JobID is the ID of the compaction job. + JobID int + // Reason is the reason for the compaction. + Reason string + // Input contains the input tables for the compaction organized by level. + Input []LevelInfo + // Output contains the output tables generated by the compaction. The output + // tables are empty for the compaction begin event. + Output LevelInfo + // Duration is the time spent compacting, including reading and writing + // sstables. + Duration time.Duration + // TotalDuration is the total wall-time duration of the compaction, + // including applying the compaction to the database. TotalDuration is + // always ≥ Duration. + 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 { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i CompactionInfo) SafeFormat(w redact.SafePrinter, _ rune) { + if i.Err != nil { + w.Printf("[JOB %d] compaction(%s) to L%d error: %s", + redact.Safe(i.JobID), redact.SafeString(i.Reason), redact.Safe(i.Output.Level), i.Err) + return + } + + if !i.Done { + 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.Bytes.Uint64(outputSize)), + redact.Safe(i.Duration.Seconds()), + redact.Safe(i.TotalDuration.Seconds()), + redact.Safe(humanize.Bytes.Uint64(uint64(float64(outputSize)/i.Duration.Seconds())))) +} + +type levelInfos []LevelInfo + +func (i levelInfos) SafeFormat(w redact.SafePrinter, _ rune) { + for j, levelInfo := range i { + if j > 0 { + w.Printf(" + ") + } + w.Print(levelInfo) + } +} + +// DiskSlowInfo contains the info for a disk slowness event when writing to a +// file. +type DiskSlowInfo = vfs.DiskSlowInfo + +// FlushInfo contains the info for a flush event. +type FlushInfo struct { + // JobID is the ID of the flush job. + JobID int + // Reason is the reason for the flush. + 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 + // Duration is the time spent flushing. This duration includes writing and + // syncing all of the flushed keys to sstables. + Duration time.Duration + // TotalDuration is the total wall-time duration of the flush, including + // applying the flush to the database. TotalDuration is always ≥ Duration. + TotalDuration time.Duration + // Ingest is set to true if the flush is handling tables that were added to + // the flushable queue via an ingestion operation. + Ingest bool + // IngestLevels are the output levels for each ingested table in the flush. + // This field is only populated when Ingest is true. + IngestLevels []int + Done bool + Err error +} + +func (i FlushInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i FlushInfo) SafeFormat(w redact.SafePrinter, _ rune) { + if i.Err != nil { + w.Printf("[JOB %d] flush error: %s", redact.Safe(i.JobID), i.Err) + return + } + + plural := redact.SafeString("s") + if i.Input == 1 { + plural = "" + } + if !i.Done { + w.Printf("[JOB %d] ", redact.Safe(i.JobID)) + if !i.Ingest { + w.Printf("flushing %d memtable", redact.Safe(i.Input)) + w.SafeString(plural) + 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) + } + return + } + + outputSize := tablesTotalSize(i.Output) + if !i.Ingest { + if invariants.Enabled && len(i.IngestLevels) > 0 { + panic(errors.AssertionFailedf("pebble: expected len(IngestedLevels) == 0")) + } + 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.Bytes.Uint64(outputSize)), + redact.Safe(i.Duration.Seconds()), + redact.Safe(i.TotalDuration.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")) + } + w.Printf("[JOB %d] flushed %d ingested flushable%s", + redact.Safe(i.JobID), redact.Safe(len(i.Output)), plural) + for j, level := range i.IngestLevels { + file := i.Output[j] + if j > 0 { + w.Printf(" +") + } + 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.Bytes.Uint64(uint64(float64(outputSize)/i.Duration.Seconds())))) + } +} + +// ManifestCreateInfo contains info about a manifest creation event. +type ManifestCreateInfo struct { + // JobID is the ID of the job the caused the manifest to be created. + JobID int + Path string + // The file number of the new Manifest. + FileNum base.DiskFileNum + Err error +} + +func (i ManifestCreateInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i ManifestCreateInfo) SafeFormat(w redact.SafePrinter, _ rune) { + if i.Err != nil { + 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), i.FileNum) +} + +// ManifestDeleteInfo contains the info for a Manifest deletion event. +type ManifestDeleteInfo struct { + // JobID is the ID of the job the caused the Manifest to be deleted. + JobID int + Path string + FileNum FileNum + Err error +} + +func (i ManifestDeleteInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i ManifestDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) { + if i.Err != nil { + 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), i.FileNum) +} + +// TableCreateInfo contains the info for a table creation event. +type TableCreateInfo struct { + JobID int + // Reason is the reason for the table creation: "compacting", "flushing", or + // "ingesting". + Reason string + Path string + FileNum FileNum +} + +func (i TableCreateInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// 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), i.FileNum) +} + +// TableDeleteInfo contains the info for a table deletion event. +type TableDeleteInfo struct { + JobID int + Path string + FileNum FileNum + Err error +} + +func (i TableDeleteInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +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), i.FileNum, i.Err) + return + } + w.Printf("[JOB %d] sstable deleted %s", redact.Safe(i.JobID), i.FileNum) +} + +// TableIngestInfo contains the info for a table ingestion event. +type TableIngestInfo struct { + // JobID is the ID of the job the caused the table to be ingested. + JobID int + Tables []struct { + TableInfo + Level int + } + // GlobalSeqNum is the sequence number that was assigned to all entries in + // the ingested table. + GlobalSeqNum uint64 + // flushable indicates whether the ingested sstable was treated as a + // flushable. + flushable bool + Err error +} + +func (i TableIngestInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i TableIngestInfo) SafeFormat(w redact.SafePrinter, _ rune) { + if i.Err != nil { + w.Printf("[JOB %d] ingest error: %s", redact.Safe(i.JobID), i.Err) + return + } + + if i.flushable { + w.Printf("[JOB %d] ingested as flushable", redact.Safe(i.JobID)) + } else { + w.Printf("[JOB %d] ingested", redact.Safe(i.JobID)) + } + + for j := range i.Tables { + t := &i.Tables[j] + if j > 0 { + w.Printf(",") + } + levelStr := "" + if !i.flushable { + levelStr = fmt.Sprintf("L%d:", t.Level) + } + w.Printf(" %s%s (%s)", redact.Safe(levelStr), t.FileNum, + redact.Safe(humanize.Bytes.Uint64(t.Size))) + } +} + +// TableStatsInfo contains the info for a table stats loaded event. +type TableStatsInfo struct { + // JobID is the ID of the job that finished loading the initial tables' + // stats. + JobID int +} + +func (i TableStatsInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i TableStatsInfo) SafeFormat(w redact.SafePrinter, _ rune) { + w.Printf("[JOB %d] all initial table stats loaded", redact.Safe(i.JobID)) +} + +// TableValidatedInfo contains information on the result of a validation run +// on an sstable. +type TableValidatedInfo struct { + JobID int + Meta *fileMetadata +} + +func (i TableValidatedInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i TableValidatedInfo) SafeFormat(w redact.SafePrinter, _ rune) { + w.Printf("[JOB %d] validated table: %s", redact.Safe(i.JobID), i.Meta) +} + +// WALCreateInfo contains info about a WAL creation event. +type WALCreateInfo struct { + // JobID is the ID of the job the caused the WAL to be created. + JobID int + Path string + // The file number of the new WAL. + FileNum base.DiskFileNum + // The file number of a previous WAL which was recycled to create this + // one. Zero if recycling did not take place. + RecycledFileNum FileNum + Err error +} + +func (i WALCreateInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i WALCreateInfo) SafeFormat(w redact.SafePrinter, _ rune) { + if i.Err != nil { + w.Printf("[JOB %d] WAL create error: %s", redact.Safe(i.JobID), i.Err) + return + } + + if i.RecycledFileNum == 0 { + 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), i.FileNum, i.RecycledFileNum) +} + +// WALDeleteInfo contains the info for a WAL deletion event. +type WALDeleteInfo struct { + // JobID is the ID of the job the caused the WAL to be deleted. + JobID int + Path string + FileNum FileNum + Err error +} + +func (i WALDeleteInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i WALDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) { + if i.Err != nil { + 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), i.FileNum) +} + +// WriteStallBeginInfo contains the info for a write stall begin event. +type WriteStallBeginInfo struct { + Reason string +} + +func (i WriteStallBeginInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i WriteStallBeginInfo) SafeFormat(w redact.SafePrinter, _ rune) { + w.Printf("write stall beginning: %s", redact.Safe(i.Reason)) +} + +// EventListener contains a set of functions that will be invoked when various +// significant DB events occur. Note that the functions should not run for an +// excessive amount of time as they are invoked synchronously by the DB and may +// block continued DB work. For a similar reason it is advisable to not perform +// any synchronous calls back into the DB. +type EventListener struct { + // BackgroundError is invoked whenever an error occurs during a background + // operation such as flush or compaction. + BackgroundError func(error) + + // CompactionBegin is invoked after the inputs to a compaction have been + // determined, but before the compaction has produced any output. + CompactionBegin func(CompactionInfo) + + // CompactionEnd is invoked after a compaction has completed and the result + // has been installed. + CompactionEnd func(CompactionInfo) + + // DiskSlow is invoked after a disk write operation on a file created with a + // disk health checking vfs.FS (see vfs.DefaultWithDiskHealthChecks) is + // observed to exceed the specified disk slowness threshold duration. DiskSlow + // is called on a goroutine that is monitoring slowness/stuckness. The callee + // MUST return without doing any IO, or blocking on anything (like a mutex) + // that is waiting on IO. This is imperative in order to reliably monitor for + // slowness, since if this goroutine gets stuck, the monitoring will stop + // working. + DiskSlow func(DiskSlowInfo) + + // FlushBegin is invoked after the inputs to a flush have been determined, + // but before the flush has produced any output. + FlushBegin func(FlushInfo) + + // FlushEnd is invoked after a flush has complated and the result has been + // installed. + FlushEnd func(FlushInfo) + + // FormatUpgrade is invoked after the database's FormatMajorVersion + // is upgraded. + FormatUpgrade func(FormatMajorVersion) + + // ManifestCreated is invoked after a manifest has been created. + ManifestCreated func(ManifestCreateInfo) + + // ManifestDeleted is invoked after a manifest has been deleted. + ManifestDeleted func(ManifestDeleteInfo) + + // TableCreated is invoked when a table has been created. + TableCreated func(TableCreateInfo) + + // TableDeleted is invoked after a table has been deleted. + TableDeleted func(TableDeleteInfo) + + // TableIngested is invoked after an externally created table has been + // ingested via a call to DB.Ingest(). + TableIngested func(TableIngestInfo) + + // TableStatsLoaded is invoked at most once, when the table stats + // collector has loaded statistics for all tables that existed at Open. + TableStatsLoaded func(TableStatsInfo) + + // TableValidated is invoked after validation runs on an sstable. + TableValidated func(TableValidatedInfo) + + // WALCreated is invoked after a WAL has been created. + WALCreated func(WALCreateInfo) + + // WALDeleted is invoked after a WAL has been deleted. + WALDeleted func(WALDeleteInfo) + + // WriteStallBegin is invoked when writes are intentionally delayed. + WriteStallBegin func(WriteStallBeginInfo) + + // WriteStallEnd is invoked when delayed writes are released. + WriteStallEnd func() +} + +// EnsureDefaults ensures that background error events are logged to the +// specified logger if a handler for those events hasn't been otherwise +// specified. Ensure all handlers are non-nil so that we don't have to check +// for nil-ness before invoking. +func (l *EventListener) EnsureDefaults(logger Logger) { + if l.BackgroundError == nil { + if logger != nil { + l.BackgroundError = func(err error) { + logger.Errorf("background error: %s", err) + } + } else { + l.BackgroundError = func(error) {} + } + } + if l.CompactionBegin == nil { + l.CompactionBegin = func(info CompactionInfo) {} + } + if l.CompactionEnd == nil { + l.CompactionEnd = func(info CompactionInfo) {} + } + if l.DiskSlow == nil { + l.DiskSlow = func(info DiskSlowInfo) {} + } + if l.FlushBegin == nil { + l.FlushBegin = func(info FlushInfo) {} + } + if l.FlushEnd == nil { + l.FlushEnd = func(info FlushInfo) {} + } + if l.FormatUpgrade == nil { + l.FormatUpgrade = func(v FormatMajorVersion) {} + } + if l.ManifestCreated == nil { + l.ManifestCreated = func(info ManifestCreateInfo) {} + } + if l.ManifestDeleted == nil { + l.ManifestDeleted = func(info ManifestDeleteInfo) {} + } + if l.TableCreated == nil { + l.TableCreated = func(info TableCreateInfo) {} + } + if l.TableDeleted == nil { + l.TableDeleted = func(info TableDeleteInfo) {} + } + if l.TableIngested == nil { + l.TableIngested = func(info TableIngestInfo) {} + } + if l.TableStatsLoaded == nil { + l.TableStatsLoaded = func(info TableStatsInfo) {} + } + if l.TableValidated == nil { + l.TableValidated = func(validated TableValidatedInfo) {} + } + if l.WALCreated == nil { + l.WALCreated = func(info WALCreateInfo) {} + } + if l.WALDeleted == nil { + l.WALDeleted = func(info WALDeleteInfo) {} + } + if l.WriteStallBegin == nil { + l.WriteStallBegin = func(info WriteStallBeginInfo) {} + } + if l.WriteStallEnd == nil { + l.WriteStallEnd = func() {} + } +} + +// MakeLoggingEventListener creates an EventListener that logs all events to the +// specified logger. +func MakeLoggingEventListener(logger Logger) EventListener { + if logger == nil { + logger = DefaultLogger + } + + return EventListener{ + BackgroundError: func(err error) { + logger.Errorf("background error: %s", err) + }, + CompactionBegin: func(info CompactionInfo) { + logger.Infof("%s", info) + }, + CompactionEnd: func(info CompactionInfo) { + logger.Infof("%s", info) + }, + DiskSlow: func(info DiskSlowInfo) { + logger.Infof("%s", info) + }, + FlushBegin: func(info FlushInfo) { + logger.Infof("%s", info) + }, + FlushEnd: func(info FlushInfo) { + logger.Infof("%s", info) + }, + FormatUpgrade: func(v FormatMajorVersion) { + logger.Infof("upgraded to format version: %s", v) + }, + ManifestCreated: func(info ManifestCreateInfo) { + logger.Infof("%s", info) + }, + ManifestDeleted: func(info ManifestDeleteInfo) { + logger.Infof("%s", info) + }, + TableCreated: func(info TableCreateInfo) { + logger.Infof("%s", info) + }, + TableDeleted: func(info TableDeleteInfo) { + logger.Infof("%s", info) + }, + TableIngested: func(info TableIngestInfo) { + logger.Infof("%s", info) + }, + TableStatsLoaded: func(info TableStatsInfo) { + logger.Infof("%s", info) + }, + TableValidated: func(info TableValidatedInfo) { + logger.Infof("%s", info) + }, + WALCreated: func(info WALCreateInfo) { + logger.Infof("%s", info) + }, + WALDeleted: func(info WALDeleteInfo) { + logger.Infof("%s", info) + }, + WriteStallBegin: func(info WriteStallBeginInfo) { + logger.Infof("%s", info) + }, + WriteStallEnd: func() { + logger.Infof("write stall ending") + }, + } +} + +// TeeEventListener wraps two EventListeners, forwarding all events to both. +func TeeEventListener(a, b EventListener) EventListener { + a.EnsureDefaults(nil) + b.EnsureDefaults(nil) + return EventListener{ + BackgroundError: func(err error) { + a.BackgroundError(err) + b.BackgroundError(err) + }, + CompactionBegin: func(info CompactionInfo) { + a.CompactionBegin(info) + b.CompactionBegin(info) + }, + CompactionEnd: func(info CompactionInfo) { + a.CompactionEnd(info) + b.CompactionEnd(info) + }, + DiskSlow: func(info DiskSlowInfo) { + a.DiskSlow(info) + b.DiskSlow(info) + }, + FlushBegin: func(info FlushInfo) { + a.FlushBegin(info) + b.FlushBegin(info) + }, + FlushEnd: func(info FlushInfo) { + a.FlushEnd(info) + b.FlushEnd(info) + }, + FormatUpgrade: func(v FormatMajorVersion) { + a.FormatUpgrade(v) + b.FormatUpgrade(v) + }, + ManifestCreated: func(info ManifestCreateInfo) { + a.ManifestCreated(info) + b.ManifestCreated(info) + }, + ManifestDeleted: func(info ManifestDeleteInfo) { + a.ManifestDeleted(info) + b.ManifestDeleted(info) + }, + TableCreated: func(info TableCreateInfo) { + a.TableCreated(info) + b.TableCreated(info) + }, + TableDeleted: func(info TableDeleteInfo) { + a.TableDeleted(info) + b.TableDeleted(info) + }, + TableIngested: func(info TableIngestInfo) { + a.TableIngested(info) + b.TableIngested(info) + }, + TableStatsLoaded: func(info TableStatsInfo) { + a.TableStatsLoaded(info) + b.TableStatsLoaded(info) + }, + TableValidated: func(info TableValidatedInfo) { + a.TableValidated(info) + b.TableValidated(info) + }, + WALCreated: func(info WALCreateInfo) { + a.WALCreated(info) + b.WALCreated(info) + }, + WALDeleted: func(info WALDeleteInfo) { + a.WALDeleted(info) + b.WALDeleted(info) + }, + WriteStallBegin: func(info WriteStallBeginInfo) { + a.WriteStallBegin(info) + b.WriteStallBegin(info) + }, + WriteStallEnd: func() { + a.WriteStallEnd() + b.WriteStallEnd() + }, + } +} diff --git a/pebble/event_listener_test.go b/pebble/event_listener_test.go new file mode 100644 index 0000000..69325d9 --- /dev/null +++ b/pebble/event_listener_test.go @@ -0,0 +1,376 @@ +// Copyright 2018 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 ( + "bytes" + "fmt" + "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/redact" + "github.com/stretchr/testify/require" +) + +// Verify event listener actions, as well as expected filesystem operations. +func TestEventListener(t *testing.T) { + var d *DB + var memLog base.InMemLogger + mem := vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + + datadriven.RunTest(t, "testdata/event_listener", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "open": + memLog.Reset() + lel := MakeLoggingEventListener(&memLog) + flushBegin, flushEnd := lel.FlushBegin, lel.FlushEnd + lel.FlushBegin = func(info FlushInfo) { + // Make deterministic. + info.InputBytes = 100 + flushBegin(info) + } + lel.FlushEnd = func(info FlushInfo) { + // Make deterministic. + info.InputBytes = 100 + flushEnd(info) + } + opts := &Options{ + FS: vfs.WithLogging(mem, memLog.Infof), + FormatMajorVersion: internalFormatNewest, + EventListener: &lel, + MaxManifestFileSize: 1, + L0CompactionThreshold: 10, + WALDir: "wal", + } + // The table stats collector runs asynchronously and its + // timing is less predictable. It increments nextJobID, which + // can make these tests flaky. The TableStatsLoaded event is + // tested separately in TestTableStats. + opts.private.disableTableStats = true + var err error + d, err = Open("db", opts) + if err != nil { + return err.Error() + } + t := time.Now() + d.timeNow = func() time.Time { + t = t.Add(time.Second) + return t + } + d.opts.private.testingAlwaysWaitForCleanup = true + return memLog.String() + + case "close": + memLog.Reset() + if err := d.Close(); err != nil { + return err.Error() + } + return memLog.String() + + case "flush": + memLog.Reset() + if err := d.Set([]byte("a"), nil, nil); err != nil { + return err.Error() + } + if err := d.Flush(); err != nil { + return err.Error() + } + return memLog.String() + + case "compact": + memLog.Reset() + if err := d.Set([]byte("a"), nil, nil); err != nil { + return err.Error() + } + if err := d.Compact([]byte("a"), []byte("b"), false); err != nil { + return err.Error() + } + return memLog.String() + + case "checkpoint": + memLog.Reset() + if err := d.Checkpoint("checkpoint"); err != nil { + return err.Error() + } + return memLog.String() + + case "disable-file-deletions": + memLog.Reset() + d.mu.Lock() + d.disableFileDeletions() + d.mu.Unlock() + return memLog.String() + + case "enable-file-deletions": + memLog.Reset() + func() { + defer func() { + if r := recover(); r != nil { + memLog.Infof("%v", r) + } + }() + d.mu.Lock() + defer d.mu.Unlock() + d.enableFileDeletions() + }() + d.TestOnlyWaitForCleaning() + return memLog.String() + + case "ingest": + memLog.Reset() + f, err := mem.Create("ext/0") + if err != nil { + return err.Error() + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + if err := w.Add(base.MakeInternalKey([]byte("a"), 0, InternalKeyKindSet), nil); err != nil { + return err.Error() + } + if err := w.Close(); err != nil { + return err.Error() + } + if err := d.Ingest([]string{"ext/0"}); err != nil { + return err.Error() + } + return memLog.String() + + case "ingest-flushable": + memLog.Reset() + + // Prevent flushes during this test to ensure determinism. + d.mu.Lock() + d.mu.compact.flushing = true + d.mu.Unlock() + + b := d.NewBatch() + if err := b.Set([]byte("a"), nil, nil); err != nil { + return err.Error() + } + if err := d.Apply(b, nil); err != nil { + return err.Error() + } + writeTable := func(name string, key byte) error { + f, err := mem.Create(name) + if err != nil { + return err + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: d.FormatMajorVersion().MaxTableFormat(), + }) + if err := w.Add(base.MakeInternalKey([]byte{key}, 0, InternalKeyKindSet), nil); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + return nil + } + tableA, tableB := "ext/a", "ext/b" + if err := writeTable(tableA, 'a'); err != nil { + return err.Error() + } + if err := writeTable(tableB, 'b'); err != nil { + return err.Error() + } + if err := d.Ingest([]string{tableA, tableB}); err != nil { + return err.Error() + } + + // Re-enable flushes, to allow the subsequent flush to proceed. + d.mu.Lock() + d.mu.compact.flushing = false + d.mu.Unlock() + if err := d.Flush(); err != nil { + return err.Error() + } + return memLog.String() + + case "metrics": + // The asynchronous loading of table stats can change metrics, so + // wait for all the tables' stats to be loaded. + d.mu.Lock() + d.waitTableStats() + d.mu.Unlock() + + return d.Metrics().StringForTests() + + case "sstables": + var buf bytes.Buffer + tableInfos, _ := d.SSTables() + for i, level := range tableInfos { + if len(level) == 0 { + continue + } + fmt.Fprintf(&buf, "%d:\n", i) + for _, m := range level { + fmt.Fprintf(&buf, " %d:[%s-%s]\n", + m.FileNum, m.Smallest.UserKey, m.Largest.UserKey) + } + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestWriteStallEvents(t *testing.T) { + const flushCount = 10 + const writeStallEnd = "write stall ending" + + testCases := []struct { + delayFlush bool + expected string + }{ + {true, "memtable count limit reached"}, + {false, "L0 file count limit exceeded"}, + } + + for _, c := range testCases { + t.Run("", func(t *testing.T) { + stallEnded := make(chan struct{}, 1) + createReleased := make(chan struct{}, flushCount) + var log base.InMemLogger + var delayOnce sync.Once + listener := &EventListener{ + TableCreated: func(info TableCreateInfo) { + if c.delayFlush == (info.Reason == "flushing") { + delayOnce.Do(func() { + <-createReleased + }) + } + }, + WriteStallBegin: func(info WriteStallBeginInfo) { + log.Infof("%s", info.String()) + createReleased <- struct{}{} + }, + WriteStallEnd: func() { + log.Infof("%s", writeStallEnd) + select { + case stallEnded <- struct{}{}: + default: + } + }, + } + d, err := Open("db", &Options{ + EventListener: listener, + FS: vfs.NewMem(), + MemTableSize: initialMemTableSize, + MemTableStopWritesThreshold: 2, + L0CompactionThreshold: 2, + L0StopWritesThreshold: 2, + }) + require.NoError(t, err) + defer d.Close() + + for i := 0; i < flushCount; i++ { + require.NoError(t, d.Set([]byte("a"), nil, NoSync)) + + ch, err := d.AsyncFlush() + require.NoError(t, err) + + // If we're delaying the flush (because we're testing for memtable + // write stalls), we can't wait for the flush to finish as doing so + // would deadlock. If we're not delaying the flush (because we're + // testing for L0 write stals), we wait for the flush to finish so we + // don't create too many memtables which would trigger a memtable write + // stall. + if !c.delayFlush { + <-ch + } + if strings.Contains(log.String(), c.expected) { + break + } + } + <-stallEnded + + events := log.String() + require.Contains(t, events, c.expected) + require.Contains(t, events, writeStallEnd) + if testing.Verbose() { + t.Logf("\n%s", events) + } + }) + } +} + +type redactLogger struct { + logger Logger +} + +// Infof implements the Logger.Infof interface. +func (l redactLogger) Infof(format string, args ...interface{}) { + l.logger.Infof("%s", redact.Sprintf(format, args...).Redact()) +} + +// Errorf implements the Logger.Errorf interface. +func (l redactLogger) Errorf(format string, args ...interface{}) { + l.logger.Errorf("%s", redact.Sprintf(format, args...).Redact()) +} + +// Fatalf implements the Logger.Fatalf interface. +func (l redactLogger) Fatalf(format string, args ...interface{}) { + l.logger.Fatalf("%s", redact.Sprintf(format, args...).Redact()) +} + +func TestEventListenerRedact(t *testing.T) { + // The vast majority of event listener fields logged are safe and do not + // need to be redacted. Verify that the rare, unsafe error does appear in + // the log redacted. + var log base.InMemLogger + l := MakeLoggingEventListener(redactLogger{logger: &log}) + l.WALDeleted(WALDeleteInfo{ + JobID: 5, + FileNum: FileNum(20), + Err: errors.Errorf("unredacted error: %s", "unredacted string"), + }) + require.Equal(t, "[JOB 5] WAL delete error: unredacted error: ‹×›\n", log.String()) +} + +func TestEventListenerEnsureDefaultsBackgroundError(t *testing.T) { + e := EventListener{} + e.EnsureDefaults(nil) + e.BackgroundError(errors.New("an example error")) +} + +func TestEventListenerEnsureDefaultsSetsAllCallbacks(t *testing.T) { + e := EventListener{} + e.EnsureDefaults(nil) + testAllCallbacksSetInEventListener(t, e) +} + +func TestMakeLoggingEventListenerSetsAllCallbacks(t *testing.T) { + e := MakeLoggingEventListener(nil) + testAllCallbacksSetInEventListener(t, e) +} + +func TestTeeEventListenerSetsAllCallbacks(t *testing.T) { + e := TeeEventListener(EventListener{}, EventListener{}) + testAllCallbacksSetInEventListener(t, e) +} + +func testAllCallbacksSetInEventListener(t *testing.T, e EventListener) { + t.Helper() + v := reflect.ValueOf(e) + for i := 0; i < v.NumField(); i++ { + fType := v.Type().Field(i) + fVal := v.Field(i) + require.Equal(t, reflect.Func, fType.Type.Kind(), "unexpected non-func field: %s", fType.Name) + require.False(t, fVal.IsNil(), "unexpected nil field: %s", fType.Name) + } +} diff --git a/pebble/example_test.go b/pebble/example_test.go new file mode 100644 index 0000000..5c13df1 --- /dev/null +++ b/pebble/example_test.go @@ -0,0 +1,37 @@ +// Copyright 2020 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_test + +import ( + "fmt" + "log" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/vfs" +) + +func Example() { + db, err := pebble.Open("", &pebble.Options{FS: vfs.NewMem()}) + if err != nil { + log.Fatal(err) + } + key := []byte("hello") + if err := db.Set(key, []byte("world"), pebble.Sync); err != nil { + log.Fatal(err) + } + value, closer, err := db.Get(key) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s %s\n", key, value) + if err := closer.Close(); err != nil { + log.Fatal(err) + } + if err := db.Close(); err != nil { + log.Fatal(err) + } + // Output: + // hello world +} diff --git a/pebble/external_iterator.go b/pebble/external_iterator.go new file mode 100644 index 0000000..078d016 --- /dev/null +++ b/pebble/external_iterator.go @@ -0,0 +1,561 @@ +// 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 pebble + +import ( + "context" + "fmt" + "sort" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/sstable" +) + +// ExternalIterOption provide an interface to specify open-time options to +// NewExternalIter. +type ExternalIterOption interface { + // iterApply is called on the iterator during opening in order to set internal + // parameters. + iterApply(*Iterator) + // readerOptions returns any reader options added by this iter option. + readerOptions() []sstable.ReaderOption +} + +type externalIterReaderOptions struct { + opts []sstable.ReaderOption +} + +func (e *externalIterReaderOptions) iterApply(iterator *Iterator) { + // Do nothing. +} + +func (e *externalIterReaderOptions) readerOptions() []sstable.ReaderOption { + return e.opts +} + +// ExternalIterReaderOptions returns an ExternalIterOption that specifies +// sstable.ReaderOptions to be applied on sstable readers in NewExternalIter. +func ExternalIterReaderOptions(opts ...sstable.ReaderOption) ExternalIterOption { + return &externalIterReaderOptions{opts: opts} +} + +// ExternalIterForwardOnly is an ExternalIterOption that specifies this iterator +// will only be used for forward positioning operations (First, SeekGE, Next). +// This could enable optimizations that take advantage of this invariant. +// Behaviour when a reverse positioning operation is done on an iterator +// opened with this option is unpredictable, though in most cases it should. +type ExternalIterForwardOnly struct{} + +func (e ExternalIterForwardOnly) iterApply(iter *Iterator) { + iter.forwardOnly = true +} + +func (e ExternalIterForwardOnly) readerOptions() []sstable.ReaderOption { + return nil +} + +// NewExternalIter takes an input 2d array of sstable files which may overlap +// across subarrays but not within a subarray (at least as far as points are +// concerned; range keys are allowed to overlap arbitrarily even within a +// subarray), and returns an Iterator over the merged contents of the sstables. +// Input sstables may contain point keys, range keys, range deletions, etc. The +// input files slice must be sorted in reverse chronological ordering. A key in a +// file at a lower index subarray will shadow a key with an identical user key +// contained within a file at a higher index subarray. Each subarray must be +// sorted in internal key order, where lower index files contain keys that sort +// left of files with higher indexes. +// +// Input sstables must only contain keys with the zero sequence number. +// +// Iterators constructed through NewExternalIter do not support all iterator +// options, including block-property and table filters. NewExternalIter errors +// if an incompatible option is set. +func NewExternalIter( + o *Options, + iterOpts *IterOptions, + files [][]sstable.ReadableFile, + extraOpts ...ExternalIterOption, +) (it *Iterator, err error) { + return NewExternalIterWithContext(context.Background(), o, iterOpts, files, extraOpts...) +} + +// NewExternalIterWithContext is like NewExternalIter, and additionally +// accepts a context for tracing. +func NewExternalIterWithContext( + ctx context.Context, + o *Options, + iterOpts *IterOptions, + files [][]sstable.ReadableFile, + extraOpts ...ExternalIterOption, +) (it *Iterator, err error) { + if iterOpts != nil { + if err := validateExternalIterOpts(iterOpts); err != nil { + return nil, err + } + } + + var readers [][]*sstable.Reader + + // Ensure we close all the opened readers if we error out. + defer func() { + if err != nil { + for i := range readers { + for j := range readers[i] { + _ = readers[i][j].Close() + } + } + } + }() + seqNumOffset := 0 + var extraReaderOpts []sstable.ReaderOption + for i := range extraOpts { + extraReaderOpts = append(extraReaderOpts, extraOpts[i].readerOptions()...) + } + for _, levelFiles := range files { + seqNumOffset += len(levelFiles) + } + for _, levelFiles := range files { + var subReaders []*sstable.Reader + seqNumOffset -= len(levelFiles) + subReaders, err = openExternalTables(o, levelFiles, seqNumOffset, o.MakeReaderOptions(), extraReaderOpts...) + readers = append(readers, subReaders) + } + if err != nil { + return nil, err + } + + buf := iterAllocPool.Get().(*iterAlloc) + dbi := &buf.dbi + *dbi = Iterator{ + ctx: ctx, + alloc: buf, + merge: o.Merger.Merge, + comparer: *o.Comparer, + readState: nil, + keyBuf: buf.keyBuf, + prefixOrFullSeekKey: buf.prefixOrFullSeekKey, + boundsBuf: buf.boundsBuf, + batch: nil, + // Add the readers to the Iterator so that Close closes them, and + // SetOptions can re-construct iterators from them. + externalReaders: readers, + newIters: func( + ctx context.Context, f *manifest.FileMetadata, opts *IterOptions, + internalOpts internalIterOpts) (internalIterator, keyspan.FragmentIterator, error) { + // NB: External iterators are currently constructed without any + // `levelIters`. newIters should never be called. When we support + // organizing multiple non-overlapping files into a single level + // (see TODO below), we'll need to adjust this tableNewIters + // implementation to open iterators by looking up f in a map + // of readers indexed by *fileMetadata. + panic("unreachable") + }, + seqNum: base.InternalKeySeqNumMax, + } + if iterOpts != nil { + dbi.opts = *iterOpts + dbi.processBounds(iterOpts.LowerBound, iterOpts.UpperBound) + } + for i := range extraOpts { + extraOpts[i].iterApply(dbi) + } + if err := finishInitializingExternal(ctx, dbi); err != nil { + dbi.Close() + return nil, err + } + return dbi, nil +} + +func validateExternalIterOpts(iterOpts *IterOptions) error { + switch { + case iterOpts.TableFilter != nil: + return errors.Errorf("pebble: external iterator: TableFilter unsupported") + case iterOpts.PointKeyFilters != nil: + return errors.Errorf("pebble: external iterator: PointKeyFilters unsupported") + case iterOpts.RangeKeyFilters != nil: + return errors.Errorf("pebble: external iterator: RangeKeyFilters unsupported") + case iterOpts.OnlyReadGuaranteedDurable: + return errors.Errorf("pebble: external iterator: OnlyReadGuaranteedDurable unsupported") + case iterOpts.UseL6Filters: + return errors.Errorf("pebble: external iterator: UseL6Filters unsupported") + } + return nil +} + +func createExternalPointIter(ctx context.Context, it *Iterator) (internalIterator, error) { + // TODO(jackson): In some instances we could generate fewer levels by using + // L0Sublevels code to organize nonoverlapping files into the same level. + // This would allow us to use levelIters and keep a smaller set of data and + // files in-memory. However, it would also require us to identify the bounds + // of all the files upfront. + + if !it.opts.pointKeys() { + return emptyIter, nil + } else if it.pointIter != nil { + return it.pointIter, nil + } + mlevels := it.alloc.mlevels[:0] + + if len(it.externalReaders) > cap(mlevels) { + mlevels = make([]mergingIterLevel, 0, len(it.externalReaders)) + } + for _, readers := range it.externalReaders { + var combinedIters []internalIterator + for _, r := range readers { + var ( + rangeDelIter keyspan.FragmentIterator + pointIter internalIterator + err error + ) + // 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, it.opts.CategoryAndQoS, nil, + sstable.TrivialReaderProvider{Reader: r}) + if err != nil { + return nil, err + } + rangeDelIter, err = r.NewRawRangeDelIter() + if err != nil { + return nil, err + } + if rangeDelIter == nil && pointIter != nil && it.forwardOnly { + // TODO(bilal): Consider implementing range key pausing in + // simpleLevelIter so we can reduce mergingIterLevels even more by + // sending all sstable iterators to combinedIters, not just those + // corresponding to sstables without range deletes. + combinedIters = append(combinedIters, pointIter) + continue + } + mlevels = append(mlevels, mergingIterLevel{ + iter: pointIter, + rangeDelIter: rangeDelIter, + }) + } + if len(combinedIters) == 1 { + mlevels = append(mlevels, mergingIterLevel{ + iter: combinedIters[0], + }) + } else if len(combinedIters) > 1 { + sli := &simpleLevelIter{ + cmp: it.cmp, + iters: combinedIters, + } + sli.init(it.opts) + mlevels = append(mlevels, mergingIterLevel{ + iter: sli, + rangeDelIter: nil, + }) + } + } + if len(mlevels) == 1 && mlevels[0].rangeDelIter == nil { + // Set closePointIterOnce to true. This is because we're bypassing the + // merging iter, which turns Close()s on it idempotent for any child + // iterators. The outer Iterator could call Close() on a point iter twice, + // which sstable iterators do not support (as they release themselves to + // a pool). + it.closePointIterOnce = true + return mlevels[0].iter, nil + } + + it.alloc.merging.init(&it.opts, &it.stats.InternalStats, it.comparer.Compare, it.comparer.Split, mlevels...) + it.alloc.merging.snapshot = base.InternalKeySeqNumMax + if len(mlevels) <= cap(it.alloc.levelsPositioned) { + it.alloc.merging.levelsPositioned = it.alloc.levelsPositioned[:len(mlevels)] + } + return &it.alloc.merging, nil +} + +func finishInitializingExternal(ctx context.Context, it *Iterator) error { + pointIter, err := createExternalPointIter(ctx, it) + if err != nil { + return err + } + it.pointIter = pointIter + it.iter = it.pointIter + + if it.opts.rangeKeys() { + it.rangeKeyMasking.init(it, it.comparer.Compare, it.comparer.Split) + var rangeKeyIters []keyspan.FragmentIterator + if it.rangeKey == nil { + // We could take advantage of the lack of overlaps in range keys within + // each slice in it.externalReaders, and generate keyspan.LevelIters + // out of those. However, since range keys are expected to be sparse to + // begin with, the performance gain might not be significant enough to + // warrant it. + // + // TODO(bilal): Explore adding a simpleRangeKeyLevelIter that does not + // operate on FileMetadatas (similar to simpleLevelIter), and implements + // this optimization. + for _, readers := range it.externalReaders { + for _, r := range readers { + if rki, err := r.NewRawRangeKeyIter(); err != nil { + return err + } else if rki != nil { + rangeKeyIters = append(rangeKeyIters, rki) + } + } + } + if len(rangeKeyIters) > 0 { + it.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState) + it.rangeKey.init(it.comparer.Compare, it.comparer.Split, &it.opts) + it.rangeKey.rangeKeyIter = it.rangeKey.iterConfig.Init( + &it.comparer, + base.InternalKeySeqNumMax, + it.opts.LowerBound, it.opts.UpperBound, + &it.hasPrefix, &it.prefixOrFullSeekKey, + false /* internalKeys */, &it.rangeKey.internal, + ) + for i := range rangeKeyIters { + it.rangeKey.iterConfig.AddLevel(rangeKeyIters[i]) + } + } + } + if it.rangeKey != nil { + 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 + } + } + return nil +} + +func openExternalTables( + o *Options, + files []sstable.ReadableFile, + seqNumOffset int, + readerOpts sstable.ReaderOptions, + extraReaderOpts ...sstable.ReaderOption, +) (readers []*sstable.Reader, err error) { + readers = make([]*sstable.Reader, 0, len(files)) + for i := range files { + readable, err := sstable.NewSimpleReadable(files[i]) + if err != nil { + return readers, err + } + r, err := sstable.NewReader(readable, readerOpts, extraReaderOpts...) + if err != nil { + return readers, err + } + // Use the index of the file in files as the sequence number for all of + // its keys. + r.Properties.GlobalSeqNum = uint64(len(files) - i + seqNumOffset) + readers = append(readers, r) + } + return readers, err +} + +// simpleLevelIter is similar to a levelIter in that it merges the points +// from multiple point iterators that are non-overlapping in the key ranges +// they return. It is only expected to support forward iteration and forward +// regular seeking; reverse iteration and prefix seeking is not supported. +// Intended to be a low-overhead, non-FileMetadata dependent option for +// NewExternalIter. To optimize seeking and forward iteration, it maintains +// two slices of child iterators; one of all iterators, and a subset of it that +// contains just the iterators that contain point keys within the current +// bounds. +// +// Note that this levelIter does not support pausing at file boundaries +// in case of range tombstones in this file that could apply to points outside +// of this file (and outside of this level). This is sufficient for optimizing +// the main use cases of NewExternalIter, however for completeness it would make +// sense to build this pausing functionality in. +type simpleLevelIter struct { + cmp Compare + err error + lowerBound []byte + iters []internalIterator + filtered []internalIterator + firstKeys [][]byte + firstKeysBuf []byte + currentIdx int +} + +var _ internalIterator = &simpleLevelIter{} + +// init initializes this simpleLevelIter. +func (s *simpleLevelIter) init(opts IterOptions) { + s.currentIdx = 0 + s.lowerBound = opts.LowerBound + s.resetFilteredIters() +} + +func (s *simpleLevelIter) resetFilteredIters() { + s.filtered = s.filtered[:0] + s.firstKeys = s.firstKeys[:0] + s.firstKeysBuf = s.firstKeysBuf[:0] + s.err = nil + for i := range s.iters { + var iterKey *base.InternalKey + if s.lowerBound != nil { + iterKey, _ = s.iters[i].SeekGE(s.lowerBound, base.SeekGEFlagsNone) + } else { + iterKey, _ = s.iters[i].First() + } + if iterKey != nil { + s.filtered = append(s.filtered, s.iters[i]) + bufStart := len(s.firstKeysBuf) + s.firstKeysBuf = append(s.firstKeysBuf, iterKey.UserKey...) + s.firstKeys = append(s.firstKeys, s.firstKeysBuf[bufStart:bufStart+len(iterKey.UserKey)]) + } else if err := s.iters[i].Error(); err != nil { + s.err = err + } + } +} + +func (s *simpleLevelIter) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + if s.err != nil { + return nil, base.LazyValue{} + } + // Find the first file that is entirely >= key. The file before that could + // contain the key we're looking for. + n := sort.Search(len(s.firstKeys), func(i int) bool { + return s.cmp(key, s.firstKeys[i]) <= 0 + }) + if n > 0 { + s.currentIdx = n - 1 + } else { + s.currentIdx = n + } + if s.currentIdx < len(s.filtered) { + if iterKey, val := s.filtered[s.currentIdx].SeekGE(key, flags); iterKey != nil { + return iterKey, val + } + if err := s.filtered[s.currentIdx].Error(); err != nil { + s.err = err + } + s.currentIdx++ + } + return s.skipEmptyFileForward(key, flags) +} + +func (s *simpleLevelIter) skipEmptyFileForward( + seekKey []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + var iterKey *base.InternalKey + var val base.LazyValue + for s.currentIdx >= 0 && s.currentIdx < len(s.filtered) && s.err == nil { + if seekKey != nil { + iterKey, val = s.filtered[s.currentIdx].SeekGE(seekKey, flags) + } else if s.lowerBound != nil { + iterKey, val = s.filtered[s.currentIdx].SeekGE(s.lowerBound, flags) + } else { + iterKey, val = s.filtered[s.currentIdx].First() + } + if iterKey != nil { + return iterKey, val + } + if err := s.filtered[s.currentIdx].Error(); err != nil { + s.err = err + } + s.currentIdx++ + } + return nil, base.LazyValue{} +} + +func (s *simpleLevelIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +func (s *simpleLevelIter) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +func (s *simpleLevelIter) First() (*base.InternalKey, base.LazyValue) { + if s.err != nil { + return nil, base.LazyValue{} + } + s.currentIdx = 0 + return s.skipEmptyFileForward(nil /* seekKey */, base.SeekGEFlagsNone) +} + +func (s *simpleLevelIter) Last() (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +func (s *simpleLevelIter) Next() (*base.InternalKey, base.LazyValue) { + if s.err != nil { + return nil, base.LazyValue{} + } + if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) { + return nil, base.LazyValue{} + } + if iterKey, val := s.filtered[s.currentIdx].Next(); iterKey != nil { + return iterKey, val + } + s.currentIdx++ + return s.skipEmptyFileForward(nil /* seekKey */, base.SeekGEFlagsNone) +} + +func (s *simpleLevelIter) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) { + if s.err != nil { + return nil, base.LazyValue{} + } + if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) { + return nil, base.LazyValue{} + } + if iterKey, val := s.filtered[s.currentIdx].NextPrefix(succKey); iterKey != nil { + return iterKey, val + } + s.currentIdx++ + return s.skipEmptyFileForward(succKey /* seekKey */, base.SeekGEFlagsNone) +} + +func (s *simpleLevelIter) Prev() (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +func (s *simpleLevelIter) Error() error { + if s.currentIdx >= 0 && s.currentIdx < len(s.filtered) { + s.err = firstError(s.err, s.filtered[s.currentIdx].Error()) + } + return s.err +} + +func (s *simpleLevelIter) Close() error { + var err error + for i := range s.iters { + err = firstError(err, s.iters[i].Close()) + } + return err +} + +func (s *simpleLevelIter) SetBounds(lower, upper []byte) { + s.currentIdx = -1 + s.lowerBound = lower + for i := range s.iters { + s.iters[i].SetBounds(lower, upper) + } + s.resetFilteredIters() +} + +func (s *simpleLevelIter) SetContext(_ context.Context) {} + +func (s *simpleLevelIter) String() string { + if s.currentIdx < 0 || s.currentIdx >= len(s.filtered) { + return "simpleLevelIter: current=" + } + return fmt.Sprintf("simpleLevelIter: current=%s", s.filtered[s.currentIdx]) +} + +var _ internalIterator = &simpleLevelIter{} diff --git a/pebble/external_iterator_test.go b/pebble/external_iterator_test.go new file mode 100644 index 0000000..77afd4d --- /dev/null +++ b/pebble/external_iterator_test.go @@ -0,0 +1,380 @@ +// 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 pebble + +import ( + "bytes" + "fmt" + "math" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/itertest" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func TestExternalIterator(t *testing.T) { + mem := vfs.NewMem() + o := &Options{ + FS: mem, + Comparer: testkeys.Comparer, + FormatMajorVersion: FormatRangeKeys, + } + o.EnsureDefaults() + d, err := Open("", o) + require.NoError(t, err) + defer func() { require.NoError(t, d.Close()) }() + + datadriven.RunTest(t, "testdata/external_iterator", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + mem = vfs.NewMem() + return "" + case "build": + if err := runBuildCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + case "iter": + opts := IterOptions{KeyTypes: IterKeyTypePointsAndRanges} + var externalIterOpts []ExternalIterOption + var files [][]sstable.ReadableFile + for _, arg := range td.CmdArgs { + switch arg.Key { + case "fwd-only": + externalIterOpts = append(externalIterOpts, ExternalIterForwardOnly{}) + case "mask-suffix": + opts.RangeKeyMasking.Suffix = []byte(arg.Vals[0]) + case "lower": + opts.LowerBound = []byte(arg.Vals[0]) + case "upper": + opts.UpperBound = []byte(arg.Vals[0]) + case "files": + for _, v := range arg.Vals { + f, err := mem.Open(v) + require.NoError(t, err) + files = append(files, []sstable.ReadableFile{f}) + } + } + } + it, err := NewExternalIter(o, &opts, files, externalIterOpts...) + require.NoError(t, err) + return runIterCmd(td, it, true /* close iter */) + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestSimpleLevelIter(t *testing.T) { + mem := vfs.NewMem() + o := &Options{ + FS: mem, + Comparer: testkeys.Comparer, + FormatMajorVersion: FormatRangeKeys, + } + o.EnsureDefaults() + d, err := Open("", o) + require.NoError(t, err) + defer func() { require.NoError(t, d.Close()) }() + + datadriven.RunTest(t, "testdata/simple_level_iter", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + mem = vfs.NewMem() + return "" + case "build": + if err := runBuildCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + case "iter": + var files []sstable.ReadableFile + var filenames []string + td.ScanArgs(t, "files", &filenames) + for _, name := range filenames { + f, err := mem.Open(name) + require.NoError(t, err) + files = append(files, f) + } + readers, err := openExternalTables(o, files, 0, o.MakeReaderOptions()) + require.NoError(t, err) + defer func() { + for i := range readers { + _ = readers[i].Close() + } + }() + var internalIters []internalIterator + for i := range readers { + iter, err := readers[i].NewIter(nil, nil) + require.NoError(t, err) + internalIters = append(internalIters, iter) + } + it := &simpleLevelIter{cmp: o.Comparer.Compare, iters: internalIters} + it.init(IterOptions{}) + + response := itertest.RunInternalIterCmd(t, td, it) + require.NoError(t, it.Close()) + return response + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestSimpleIterError(t *testing.T) { + s := simpleLevelIter{cmp: DefaultComparer.Compare, iters: []internalIterator{&errorIter{err: errors.New("injected")}}} + s.init(IterOptions{}) + defer s.Close() + + iterKey, _ := s.First() + require.Nil(t, iterKey) + require.Error(t, s.Error()) +} + +func TestIterRandomizedMaybeFilteredKeys(t *testing.T) { + mem := vfs.NewMem() + + seed := *seed + if seed == 0 { + seed = uint64(time.Now().UnixNano()) + t.Logf("seed: %d", seed) + } + rng := rand.New(rand.NewSource(seed)) + numKeys := 100 + rng.Intn(5000) + // The block property filter will exclude keys with suffixes [0, tsSeparator-1]. + // We use the first "part" of the keyspace below to write keys >= tsSeparator, + // and the second part to write keys < tsSeparator. Successive parts (if any) + // will contain keys at random before or after the separator. + tsSeparator := 10 + rng.Int63n(5000) + const keyLen = 5 + + // We split the keyspace into logical "parts" which are disjoint slices of the + // keyspace. That is, the keyspace a-z could be comprised of parts {a-k, l-z}. + // We rely on this partitioning when generating timestamps to give us some + // predictable clustering of timestamps in sstable blocks, however it is not + // strictly necessary for this test. + alpha := testkeys.Alpha(keyLen) + numParts := rng.Intn(3) + 2 + blockSize := 16 + rng.Intn(64) + + c := cache.New(128 << 20) + defer c.Unref() + + for fileIdx, twoLevelIndex := range []bool{false, true} { + t.Run(fmt.Sprintf("twoLevelIndex=%v", twoLevelIndex), func(t *testing.T) { + keys := make([][]byte, 0, numKeys) + + filename := fmt.Sprintf("test-%d", fileIdx) + f0, err := mem.Create(filename) + require.NoError(t, err) + + indexBlockSize := 4096 + if twoLevelIndex { + indexBlockSize = 1 + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f0), sstable.WriterOptions{ + BlockSize: blockSize, + Comparer: testkeys.Comparer, + IndexBlockSize: indexBlockSize, + TableFormat: sstable.TableFormatPebblev2, + BlockPropertyCollectors: []func() BlockPropertyCollector{ + func() BlockPropertyCollector { + return sstable.NewTestKeysBlockPropertyCollector() + }, + }, + }) + buf := make([]byte, alpha.MaxLen()+testkeys.MaxSuffixLen) + valBuf := make([]byte, 20) + keyIdx := int64(0) + for i := 0; i < numParts; i++ { + // The first two parts of the keyspace are special. The first one has + // all keys with timestamps greater than tsSeparator, while the second + // one has all keys with timestamps less than tsSeparator. Any additional + // keys could have timestamps at random before or after the tsSeparator. + maxKeysPerPart := numKeys / numParts + for j := 0; j < maxKeysPerPart; j++ { + var ts int64 + if i == 0 { + ts = rng.Int63n(5000) + tsSeparator + } else if i == 1 { + ts = rng.Int63n(tsSeparator) + } else { + ts = rng.Int63n(tsSeparator + 5000) + } + n := testkeys.WriteKeyAt(buf, alpha, keyIdx*alpha.Count()/int64(numKeys), ts) + keys = append(keys, append([]byte(nil), buf[:n]...)) + randStr(valBuf, rng) + require.NoError(t, w.Set(buf[:n], valBuf)) + keyIdx++ + } + } + require.NoError(t, w.Close()) + + // Re-open that filename for reading. + f1, err := mem.Open(filename) + require.NoError(t, err) + + readable, err := sstable.NewSimpleReadable(f1) + require.NoError(t, err) + + r, err := sstable.NewReader(readable, sstable.ReaderOptions{ + Cache: c, + Comparer: testkeys.Comparer, + }) + require.NoError(t, err) + defer r.Close() + + filter := sstable.NewTestKeysBlockPropertyFilter(uint64(tsSeparator), math.MaxUint64) + filterer, err := sstable.IntersectsTable([]BlockPropertyFilter{filter}, nil, r.Properties.UserProperties) + require.NoError(t, err) + require.NotNil(t, filterer) + + var iter sstable.Iterator + iter, err = r.NewIterWithBlockPropertyFilters( + nil, nil, filterer, false /* useFilterBlock */, nil, /* stats */ + sstable.CategoryAndQoS{}, nil, sstable.TrivialReaderProvider{Reader: r}) + require.NoError(t, err) + defer iter.Close() + var lastSeekKey, lowerBound, upperBound []byte + narrowBoundsMode := false + + for i := 0; i < 10000; i++ { + if rng.Intn(8) == 0 { + // Toggle narrow bounds mode. + if narrowBoundsMode { + // Reset bounds. + lowerBound, upperBound = nil, nil + iter.SetBounds(nil /* lower */, nil /* upper */) + } + narrowBoundsMode = !narrowBoundsMode + } + keyIdx := rng.Intn(len(keys)) + seekKey := keys[keyIdx] + if narrowBoundsMode { + // Case 1: We just entered narrow bounds mode, and both bounds + // are nil. Set a lower/upper bound. + // + // Case 2: The seek key is outside our last bounds. + // + // In either case, pick a narrow range of keys to set bounds on, + // let's say keys[keyIdx-5] and keys[keyIdx+5], before doing our + // seek operation. Picking narrow bounds increases the chance of + // monotonic bound changes. + cmp := testkeys.Comparer.Compare + case1 := lowerBound == nil && upperBound == nil + case2 := (lowerBound != nil && cmp(lowerBound, seekKey) > 0) || (upperBound != nil && cmp(upperBound, seekKey) <= 0) + if case1 || case2 { + lowerBound = nil + if keyIdx-5 >= 0 { + lowerBound = keys[keyIdx-5] + } + upperBound = nil + if keyIdx+5 < len(keys) { + upperBound = keys[keyIdx+5] + } + iter.SetBounds(lowerBound, upperBound) + } + // Case 3: The current seek key is within the previously-set bounds. + // No need to change bounds. + } + flags := base.SeekGEFlagsNone + if lastSeekKey != nil && bytes.Compare(seekKey, lastSeekKey) > 0 { + flags = flags.EnableTrySeekUsingNext() + } + lastSeekKey = append(lastSeekKey[:0], seekKey...) + + newKey, _ := iter.SeekGE(seekKey, flags) + if newKey == nil || !bytes.Equal(newKey.UserKey, seekKey) { + // We skipped some keys. Check if maybeFilteredKeys is true. + formattedNewKey := "" + if newKey != nil { + formattedNewKey = fmt.Sprintf("%s", testkeys.Comparer.FormatKey(newKey.UserKey)) + } + require.True(t, iter.MaybeFilteredKeys(), "seeked for key = %s, got key = %s indicating block property filtering but MaybeFilteredKeys = false", testkeys.Comparer.FormatKey(seekKey), formattedNewKey) + } + } + }) + } +} + +func BenchmarkExternalIter_NonOverlapping_SeekNextScan(b *testing.B) { + ks := testkeys.Alpha(6) + opts := (&Options{}).EnsureDefaults() + iterOpts := &IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + } + writeOpts := opts.MakeWriterOptions(6, sstable.TableFormatPebblev2) + + for _, keyCount := range []int{100, 10_000, 100_000} { + b.Run(fmt.Sprintf("keys=%d", keyCount), func(b *testing.B) { + for _, fileCount := range []int{1, 10, 100} { + b.Run(fmt.Sprintf("files=%d", fileCount), func(b *testing.B) { + var fs vfs.FS = vfs.NewMem() + filenames := make([]string, fileCount) + var keys [][]byte + for i := 0; i < fileCount; i++ { + filename := fmt.Sprintf("%03d.sst", i) + wf, err := fs.Create(filename) + require.NoError(b, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(wf), writeOpts) + for j := 0; j < keyCount/fileCount; j++ { + key := testkeys.Key(ks, int64(len(keys))) + keys = append(keys, key) + require.NoError(b, w.Set(key, key)) + } + require.NoError(b, w.Close()) + filenames[i] = filename + } + + for _, forwardOnly := range []bool{false, true} { + b.Run(fmt.Sprintf("forward-only=%t", forwardOnly), func(b *testing.B) { + var externalIterOpts []ExternalIterOption + if forwardOnly { + externalIterOpts = append(externalIterOpts, ExternalIterForwardOnly{}) + } + + for i := 0; i < b.N; i++ { + func() { + files := make([][]sstable.ReadableFile, fileCount) + for i := 0; i < fileCount; i++ { + f, err := fs.Open(filenames[i]) + require.NoError(b, err) + files[i] = []sstable.ReadableFile{f} + } + + it, err := NewExternalIter(opts, iterOpts, files, externalIterOpts...) + require.NoError(b, err) + defer it.Close() + + for k := 0; k+1 < len(keys); k += 2 { + if !it.SeekGE(keys[k]) { + b.Fatalf("key %q not found", keys[k]) + } + if !it.Next() { + b.Fatalf("key %q not found", keys[k+1]) + } + if !bytes.Equal(it.Key(), keys[k+1]) { + b.Fatalf("expected key %q, found %q", keys[k+1], it.Key()) + } + } + }() + } + }) + } + }) + } + }) + } +} diff --git a/pebble/filenames.go b/pebble/filenames.go new file mode 100644 index 0000000..07d74c8 --- /dev/null +++ b/pebble/filenames.go @@ -0,0 +1,54 @@ +// Copyright 2012 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 ( + "fmt" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/vfs" +) + +type fileType = base.FileType + +// FileNum is an identifier for a file within a database. +type FileNum = base.FileNum + +const ( + fileTypeLog = base.FileTypeLog + fileTypeLock = base.FileTypeLock + fileTypeTable = base.FileTypeTable + fileTypeManifest = base.FileTypeManifest + fileTypeCurrent = base.FileTypeCurrent + fileTypeOptions = base.FileTypeOptions + fileTypeTemp = base.FileTypeTemp + fileTypeOldTemp = base.FileTypeOldTemp +) + +// setCurrentFile sets the CURRENT file to point to the manifest with +// provided file number. +// +// 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 base.DiskFileNum) error { + newFilename := base.MakeFilepath(fs, dirname, fileTypeCurrent, fileNum) + oldFilename := base.MakeFilepath(fs, dirname, fileTypeTemp, fileNum) + fs.Remove(oldFilename) + f, err := fs.Create(oldFilename) + if err != nil { + return err + } + if _, err := fmt.Fprintf(f, "MANIFEST-%s\n", fileNum); err != nil { + return err + } + if err := f.Sync(); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + return fs.Rename(oldFilename, newFilename) +} diff --git a/pebble/filenames_test.go b/pebble/filenames_test.go new file mode 100644 index 0000000..287352e --- /dev/null +++ b/pebble/filenames_test.go @@ -0,0 +1,110 @@ +// Copyright 2020 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 ( + "testing" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +// TestSetCurrentFileCrash tests a crash that occurs during +// a MANIFEST roll, leaving the temporary CURRENT file on +// the filesystem. These temporary files should be cleaned +// up on Open. +func TestSetCurrentFileCrash(t *testing.T) { + mem := vfs.NewMem() + + // Initialize a fresh database to write the initial MANIFEST. + { + d, err := Open("", &Options{FS: mem}) + require.NoError(t, err) + require.NoError(t, d.Close()) + } + + // Open the database again, this time with a FS that + // errors on Rename and a tiny max manifest file size + // to force manifest rolls. + { + wantErr := errors.New("rename error") + _, err := Open("", &Options{ + FS: renameErrorFS{FS: mem, err: wantErr}, + Logger: noFatalLogger{t: t}, + MaxManifestFileSize: 1, + L0CompactionThreshold: 10, + }) + // Open should fail during a manifest roll, + // leaving a temp dir on the filesystem. + if !errors.Is(err, wantErr) { + t.Fatal(err) + } + } + + // A temp file should be left on the filesystem + // from the failed Rename of the CURRENT file. + if temps := allTempFiles(t, mem); len(temps) == 0 { + t.Fatal("no temp files on the filesystem") + } + + // Open the database a third time with a normal + // filesystem again. It should clean up any temp + // files on Open. + { + d, err := Open("", &Options{ + FS: mem, + MaxManifestFileSize: 1, + L0CompactionThreshold: 10, + }) + require.NoError(t, err) + require.NoError(t, d.Close()) + if temps := allTempFiles(t, mem); len(temps) > 0 { + t.Fatalf("temporary files still on disk: %#v\n", temps) + } + } +} + +func allTempFiles(t *testing.T, fs vfs.FS) []string { + var files []string + ls, err := fs.List("") + require.NoError(t, err) + for _, f := range ls { + ft, _, ok := base.ParseFilename(fs, f) + if ok && ft == fileTypeTemp { + files = append(files, f) + } + } + return files +} + +type renameErrorFS struct { + vfs.FS + err error +} + +func (fs renameErrorFS) Rename(oldname string, newname string) error { + return fs.err +} + +// noFatalLogger implements Logger, logging to the contained +// *testing.T. Notably it does not panic on calls to Fatalf +// to enable unit tests of fatal logic. +type noFatalLogger struct { + t *testing.T +} + +func (l noFatalLogger) Infof(format string, args ...interface{}) { + l.t.Logf(format, args...) +} + +func (l noFatalLogger) Errorf(format string, args ...interface{}) { + l.t.Logf(format, args...) +} + +func (l noFatalLogger) Fatalf(format string, args ...interface{}) { + l.t.Logf(format, args...) +} diff --git a/pebble/flush_test.go b/pebble/flush_test.go new file mode 100644 index 0000000..0031420 --- /dev/null +++ b/pebble/flush_test.go @@ -0,0 +1,117 @@ +// Copyright 2018 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 ( + "fmt" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestManualFlush(t *testing.T) { + getOptions := func() *Options { + opts := &Options{ + FS: vfs.NewMem(), + L0CompactionThreshold: 10, + } + opts.DisableAutomaticCompactions = true + return opts + } + d, err := Open("", getOptions()) + require.NoError(t, err) + defer func() { + require.NoError(t, d.Close()) + }() + + datadriven.RunTest(t, "testdata/manual_flush", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "batch": + b := d.NewBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + b.Commit(nil) + return "" + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "async-flush": + d.mu.Lock() + cur := d.mu.versions.currentVersion() + d.mu.Unlock() + + if _, err := d.AsyncFlush(); err != nil { + return err.Error() + } + + err := try(100*time.Microsecond, 20*time.Second, func() error { + d.mu.Lock() + defer d.mu.Unlock() + if cur == d.mu.versions.currentVersion() { + return errors.New("flush has not occurred") + } + return nil + }) + if err != nil { + return err.Error() + } + + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "reset": + if err := d.Close(); err != nil { + return err.Error() + } + d, err = Open("", getOptions()) + if err != nil { + return err.Error() + } + return "" + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +// TestFlushDelRangeEmptyKey tests flushing a range tombstone that begins with +// an empty key. The empty key is a valid key but can be confused with nil. +func TestFlushDelRangeEmptyKey(t *testing.T) { + d, err := Open("", &Options{FS: vfs.NewMem()}) + require.NoError(t, err) + require.NoError(t, d.DeleteRange([]byte{}, []byte("z"), nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Close()) +} + +// TestFlushEmptyKey tests that flushing an empty key does not trigger that key +// order invariant assertions. +func TestFlushEmptyKey(t *testing.T) { + d, err := Open("", &Options{FS: vfs.NewMem()}) + require.NoError(t, err) + require.NoError(t, d.Set(nil, []byte("hello"), nil)) + require.NoError(t, d.Flush()) + val, closer, err := d.Get(nil) + require.NoError(t, err) + require.Equal(t, val, []byte("hello")) + require.NoError(t, closer.Close()) + require.NoError(t, d.Close()) +} diff --git a/pebble/flushable.go b/pebble/flushable.go new file mode 100644 index 0000000..473dc6a --- /dev/null +++ b/pebble/flushable.go @@ -0,0 +1,254 @@ +// Copyright 2020 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" + "sync/atomic" + "time" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" +) + +// flushable defines the interface for immutable memtables. +type flushable interface { + newIter(o *IterOptions) internalIterator + newFlushIter(o *IterOptions, bytesFlushed *uint64) internalIterator + newRangeDelIter(o *IterOptions) keyspan.FragmentIterator + newRangeKeyIter(o *IterOptions) keyspan.FragmentIterator + containsRangeKeys() bool + // inuseBytes returns the number of inuse bytes by the flushable. + inuseBytes() uint64 + // totalBytes returns the total number of bytes allocated by the flushable. + totalBytes() uint64 + // readyForFlush returns true when the flushable is ready for flushing. See + // memTable.readyForFlush for one implementation which needs to check whether + // there are any outstanding write references. + readyForFlush() bool +} + +// flushableEntry wraps a flushable and adds additional metadata and +// functionality that is common to all flushables. +type flushableEntry struct { + flushable + // Channel which is closed when the flushable has been flushed. + flushed chan struct{} + // flushForced indicates whether a flush was forced on this memtable (either + // manual, or due to ingestion). Protected by DB.mu. + flushForced bool + // delayedFlushForcedAt indicates whether a timer has been set to force a + // flush on this memtable at some point in the future. Protected by DB.mu. + // Holds the timestamp of when the flush will be issued. + delayedFlushForcedAt time.Time + // logNum corresponds to the WAL that contains the records present in the + // receiver. + logNum base.DiskFileNum + // logSize is the size in bytes of the associated WAL. Protected by DB.mu. + logSize uint64 + // 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 + // readerRefs tracks the read references on the flushable. The two sources of + // reader references are DB.mu.mem.queue and readState.memtables. The memory + // reserved by the flushable in the cache is released when the reader refs + // drop to zero. If the flushable is referencing sstables, then the file + // refount is also decreased once the reader refs drops to 0. If the + // 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 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() []*fileBacking + // deleteFnLocked should be called if the caller is holding DB.mu. + deleteFnLocked func(obsolete []*fileBacking) + // deleteFn should be called if the caller is not holding DB.mu. + deleteFn func(obsolete []*fileBacking) +} + +func (e *flushableEntry) readerRef() { + switch v := e.readerRefs.Add(1); { + case v <= 1: + panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) + } +} + +// db.mu must not be held when this is called. +func (e *flushableEntry) readerUnref(deleteFiles bool) { + e.readerUnrefHelper(deleteFiles, e.deleteFn) +} + +// db.mu must be held when this is called. +func (e *flushableEntry) readerUnrefLocked(deleteFiles bool) { + e.readerUnrefHelper(deleteFiles, e.deleteFnLocked) +} + +func (e *flushableEntry) readerUnrefHelper( + deleteFiles bool, deleteFn func(obsolete []*fileBacking), +) { + switch v := e.readerRefs.Add(-1); { + case v < 0: + panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) + case v == 0: + if e.releaseMemAccounting == nil { + panic("pebble: memtable reservation already released") + } + e.releaseMemAccounting() + e.releaseMemAccounting = nil + if e.unrefFiles != nil { + obsolete := e.unrefFiles() + e.unrefFiles = nil + if deleteFiles { + deleteFn(obsolete) + } + } + } +} + +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 []physicalMeta + comparer *Comparer + newIters tableNewIters + newRangeKeyIters keyspan.TableNewSpanIter + + // Since the level slice is immutable, we construct and set it once. It + // should be safe to read from slice in future reads. + slice manifest.LevelSlice + // hasRangeKeys is set on ingestedFlushable construction. + hasRangeKeys bool +} + +func newIngestedFlushable( + files []*fileMetadata, + 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: physicalFiles, + comparer: comparer, + newIters: newIters, + newRangeKeyIters: newRangeKeyIters, + // slice is immutable and can be set once and used many times. + slice: manifest.NewLevelSliceKeySorted(comparer.Compare, files), + hasRangeKeys: hasRangeKeys, + } + + return ret +} + +// 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 { + opts = *o + } + // TODO(bananabrick): The manifest.Level in newLevelIter is only used for + // logging. Update the manifest.Level encoding to account for levels which + // 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( + context.Background(), 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 + // make use of a flush iter. + panic("pebble: not implemented") +} + +func (s *ingestedFlushable) constructRangeDelIter( + 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. + iter, rangeDelIter, err := s.newIters(context.Background(), file, nil, internalIterOpts{}) + if err != nil { + return emptyKeyspanIter, err + } + iter.Close() + if rangeDelIter == nil { + return emptyKeyspanIter, nil + } + 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. +// +// TODO(sumeer): *IterOptions are being ignored, so the index block load for +// the point iterator in constructRangeDeIter is not tracked. +func (s *ingestedFlushable) newRangeDelIter(_ *IterOptions) keyspan.FragmentIterator { + return keyspan.NewLevelIter( + 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.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 + // flushed before an ingestedFlushable can be flushed. This is because the + // ingested sstables need an updated view of the Version to + // determine where to place the files in the lsm. + return true +} diff --git a/pebble/flushable_test.go b/pebble/flushable_test.go new file mode 100644 index 0000000..c5d1d9c --- /dev/null +++ b/pebble/flushable_test.go @@ -0,0 +1,168 @@ +package pebble + +import ( + "bytes" + "fmt" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +// Simple sanity tests for the flushable interface implementation for ingested +// sstables. +func TestIngestedSSTFlushableAPI(t *testing.T) { + var mem vfs.FS + var d *DB + defer func() { + require.NoError(t, d.Close()) + }() + var flushable flushable + + reset := func() { + if d != nil { + require.NoError(t, d.Close()) + } + + mem = vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + opts := &Options{ + FS: mem, + L0CompactionThreshold: 100, + L0StopWritesThreshold: 100, + DebugCheck: DebugCheckLevels, + FormatMajorVersion: internalFormatNewest, + } + // Disable automatic compactions because otherwise we'll race with + // delete-only compactions triggered by ingesting range tombstones. + opts.DisableAutomaticCompactions = true + + var err error + d, err = Open("", opts) + require.NoError(t, err) + flushable = nil + } + reset() + + loadFileMeta := func(paths []string) []*fileMetadata { + d.mu.Lock() + pendingOutputs := make([]base.DiskFileNum, len(paths)) + for i := range paths { + pendingOutputs[i] = d.mu.versions.getNextDiskFileNum() + } + jobID := d.mu.nextJobID + d.mu.nextJobID++ + d.mu.Unlock() + + // We can reuse the ingestLoad function for this test even if we're + // not actually ingesting a file. + lr, err := ingestLoad(d.opts, d.FormatMajorVersion(), paths, nil, nil, d.cacheID, pendingOutputs, d.objProvider, jobID) + if err != nil { + panic(err) + } + meta := lr.localMeta + if len(meta) == 0 { + // All of the sstables to be ingested were empty. Nothing to do. + panic("empty sstable") + } + // The table cache requires the *fileMetadata to have a positive + // reference count. Fake a reference before we try to load the file. + for _, f := range meta { + f.Ref() + } + + // Verify the sstables do not overlap. + if err := ingestSortAndVerify(d.cmp, lr, KeyRange{}); err != nil { + panic("unsorted sstables") + } + + // Hard link the sstables into the DB directory. Since the sstables aren't + // referenced by a version, they won't be used. If the hard linking fails + // (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, lr, nil /* shared */); err != nil { + panic("couldn't hard link sstables") + } + + // Fsync the directory we added the tables to. 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 + // directory. + if err := d.dataDir.Sync(); err != nil { + panic("Couldn't sync data directory") + } + + return meta + } + + datadriven.RunTest(t, "testdata/ingested_flushable_api", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + reset() + return "" + case "build": + if err := runBuildCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + case "flushable": + // Creates an ingestedFlushable over the input files. + paths := make([]string, 0, len(td.CmdArgs)) + for _, arg := range td.CmdArgs { + paths = append(paths, arg.String()) + } + + meta := loadFileMeta(paths) + flushable = newIngestedFlushable( + meta, d.opts.Comparer, d.newIters, d.tableNewRangeKeyIter, + ) + return "" + case "iter": + iter := flushable.newIter(nil) + var buf bytes.Buffer + for x, _ := iter.First(); x != nil; x, _ = iter.Next() { + buf.WriteString(x.String()) + buf.WriteString("\n") + } + iter.Close() + return buf.String() + case "rangekeyIter": + iter := flushable.newRangeKeyIter(nil) + var buf bytes.Buffer + if iter != nil { + for span := iter.First(); span != nil; span = iter.Next() { + buf.WriteString(span.String()) + buf.WriteString("\n") + } + iter.Close() + } + return buf.String() + case "rangedelIter": + iter := flushable.newRangeDelIter(nil) + var buf bytes.Buffer + if iter != nil { + for span := iter.First(); span != nil; span = iter.Next() { + buf.WriteString(span.String()) + buf.WriteString("\n") + } + iter.Close() + } + return buf.String() + case "readyForFlush": + if flushable.readyForFlush() { + return "true" + } + return "false" + case "containsRangeKey": + if flushable.containsRangeKeys() { + return "true" + } + return "false" + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} diff --git a/pebble/format_major_version.go b/pebble/format_major_version.go new file mode 100644 index 0000000..89be161 --- /dev/null +++ b/pebble/format_major_version.go @@ -0,0 +1,678 @@ +// Copyright 2021 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 ( + "fmt" + "strconv" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/atomicfs" +) + +// FormatMajorVersion is a constant controlling the format of persisted +// data. Backwards incompatible changes to durable formats are gated +// behind new format major versions. +// +// At any point, a database's format major version may be bumped. +// However, once a database's format major version is increased, +// previous versions of Pebble will refuse to open the database. +// +// The zero value format is the FormatDefault constant. The exact +// 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 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 + // number today, but the padding may change. + return fmt.Sprintf("%03d", v) +} + +const ( + // 21.2 versions. + + // FormatDefault leaves the format version unspecified. The + // FormatDefault constant may be ratcheted upwards over time. + FormatDefault FormatMajorVersion = iota + // FormatMostCompatible maintains the most backwards compatibility, + // maintaining bi-directional compatibility with RocksDB 6.2.1 in + // the particular configuration described in the Pebble README. + FormatMostCompatible + // formatVersionedManifestMarker is the first + // backwards-incompatible change made to Pebble, introducing the + // format-version marker file for handling backwards-incompatible + // changes more broadly, and replacing the `CURRENT` file with a + // marker file. + // + // This format version is intended as an intermediary version state. + // It is deliberately unexported to discourage direct use of this + // format major version. Clients should use FormatVersioned which + // also ensures earlier versions of Pebble fail to open a database + // written in a future format major version. + formatVersionedManifestMarker + // FormatVersioned is a new format major version that replaces the + // old `CURRENT` file with a new 'marker' file scheme. Previous + // Pebble versions will be unable to open the database unless + // they're aware of format versions. + FormatVersioned + // FormatSetWithDelete is a format major version that introduces a new key + // kind, base.InternalKeyKindSetWithDelete. Previous Pebble versions will be + // unable to open this database. + FormatSetWithDelete + + // 22.1 versions. + + // FormatBlockPropertyCollector is a format major version that introduces + // BlockPropertyCollectors. + FormatBlockPropertyCollector + // FormatSplitUserKeysMarked is a format major version that guarantees that + // all files that share user keys with neighbors are marked for compaction + // in the manifest. Ratcheting to FormatSplitUserKeysMarked will block + // (without holding mutexes) until the scan of the LSM is complete and the + // manifest has been rotated. + FormatSplitUserKeysMarked + + // 22.2 versions. + + // FormatSplitUserKeysMarkedCompacted is a format major version that + // guarantees that all files explicitly marked for compaction in the manifest + // have been compacted. Combined with the FormatSplitUserKeysMarked format + // major version, this version guarantees that there are no user keys split + // across multiple files within a level L1+. Ratcheting to this format version + // will block (without holding mutexes) until all necessary compactions for + // files marked for compaction are complete. + FormatSplitUserKeysMarkedCompacted + // FormatRangeKeys is a format major version that introduces range keys. + FormatRangeKeys + // FormatMinTableFormatPebblev1 is a format major version that guarantees that + // tables created by or ingested into the DB at or above this format major + // version will have a table format version of at least Pebblev1 (Block + // Properties). + FormatMinTableFormatPebblev1 + // FormatPrePebblev1Marked is a format major version that guarantees that all + // sstables with a table format version pre-Pebblev1 (i.e. those that are + // guaranteed to not contain block properties) are marked for compaction in + // the manifest. Ratcheting to FormatPrePebblev1Marked will block (without + // holding mutexes) until the scan of the LSM is complete and the manifest has + // been rotated. + FormatPrePebblev1Marked + + // 23.1 versions. + + // 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 + + // FormatSSTableValueBlocks is a format major version that adds support for + // storing values in value blocks in the sstable. Value block support is not + // necessarily enabled when writing sstables, when running with this format + // major version. + // + // WARNING: In development, so no production code should upgrade to this + // format, since a DB with this format major version will not actually + // interoperate correctly with another DB with the same format major + // version. This format major version is introduced so that tests can start + // being executed up to this version. Note that these tests succeed despite + // the incomplete support since they do not enable value blocks and use + // TableFormatPebblev2. + FormatSSTableValueBlocks + + // FormatFlushableIngest is a format major version that enables lazy + // addition of ingested sstables into the LSM structure. When an ingest + // overlaps with a memtable, a record of the ingest is written to the WAL + // without waiting for a flush. Subsequent reads treat the ingested files as + // a level above the overlapping memtable. Once the memtable is flushed, the + // ingested files are moved into the lowest possible levels. + // + // This feature is behind a format major version because it required + // breaking changes to the WAL format. + FormatFlushableIngest + + // 23.2 versions. + + // FormatPrePebblev1MarkedCompacted is a format major version that guarantees + // that all sstables explicitly marked for compaction in the manifest (see + // FormatPrePebblev1Marked) have been compacted. Ratcheting to this format + // version will block (without holding mutexes) until all necessary + // 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 = internalFormatNewest +) + +// MaxTableFormat returns the maximum sstable.TableFormat that can be used at +// this FormatMajorVersion. +func (v FormatMajorVersion) MaxTableFormat() sstable.TableFormat { + switch v { + case FormatDefault, FormatMostCompatible, formatVersionedManifestMarker, + FormatVersioned, FormatSetWithDelete: + return sstable.TableFormatRocksDBv2 + case FormatBlockPropertyCollector, FormatSplitUserKeysMarked, + FormatSplitUserKeysMarkedCompacted: + return sstable.TableFormatPebblev1 + case FormatRangeKeys, FormatMinTableFormatPebblev1, FormatPrePebblev1Marked, + formatUnusedPrePebblev1MarkedCompacted: + return sstable.TableFormatPebblev2 + case FormatSSTableValueBlocks, FormatFlushableIngest, FormatPrePebblev1MarkedCompacted: + return sstable.TableFormatPebblev3 + case FormatDeleteSizedAndObsolete, FormatVirtualSSTables: + return sstable.TableFormatPebblev4 + default: + panic(fmt.Sprintf("pebble: unsupported format major version: %s", v)) + } +} + +// MinTableFormat returns the minimum sstable.TableFormat that can be used at +// this FormatMajorVersion. +func (v FormatMajorVersion) MinTableFormat() sstable.TableFormat { + switch v { + case FormatDefault, FormatMostCompatible, formatVersionedManifestMarker, + FormatVersioned, FormatSetWithDelete, FormatBlockPropertyCollector, + FormatSplitUserKeysMarked, FormatSplitUserKeysMarkedCompacted, + FormatRangeKeys: + return sstable.TableFormatLevelDB + case FormatMinTableFormatPebblev1, FormatPrePebblev1Marked, + 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 +// version is committed. Migrations must be idempotent. Migrations are +// invoked with d.mu locked. +// +// Each migration is responsible for invoking finalizeFormatVersUpgrade +// to set the new format major version. RatchetFormatMajorVersion will +// panic if a migration returns a nil error but fails to finalize the +// new format major version. +var formatMajorVersionMigrations = map[FormatMajorVersion]func(*DB) error{ + FormatMostCompatible: func(d *DB) error { return nil }, + formatVersionedManifestMarker: func(d *DB) error { + // formatVersionedManifestMarker introduces the use of a marker + // file for pointing to the current MANIFEST file. + + // Lock the manifest. + d.mu.versions.logLock() + defer d.mu.versions.logUnlock() + + // Construct the filename of the currently active manifest and + // move the manifest marker to that filename. The marker is + // guaranteed to exist, because we unconditionally locate it + // during Open. + manifestFileNum := d.mu.versions.manifestFileNum + filename := base.MakeFilename(fileTypeManifest, manifestFileNum) + if err := d.mu.versions.manifestMarker.Move(filename); err != nil { + return errors.Wrap(err, "moving manifest marker") + } + + // Now that we have a manifest marker file in place and pointing + // to the current MANIFEST, finalize the upgrade. If we fail for + // some reason, a retry of this migration is guaranteed to again + // move the manifest marker file to the latest manifest. If + // we're unable to finalize the upgrade, a subsequent call to + // Open will ignore the manifest marker. + if err := d.finalizeFormatVersUpgrade(formatVersionedManifestMarker); err != nil { + return err + } + + // We've finalized the upgrade. All subsequent Open calls will + // ignore the CURRENT file and instead read the manifest marker. + // Before we unlock the manifest, we need to update versionSet + // to use the manifest marker on future rotations. + d.mu.versions.setCurrent = setCurrentFuncMarker( + d.mu.versions.manifestMarker, + d.mu.versions.fs, + d.mu.versions.dirname) + return nil + }, + // The FormatVersioned version is split into two, each with their + // own migration to ensure the post-migration cleanup happens even + // if there's a crash immediately after finalizing the version. Once + // a new format major version is finalized, its migration will never + // run again. Post-migration cleanup like the one in the migration + // below must be performed in a separate migration or every time the + // database opens. + FormatVersioned: func(d *DB) error { + // Replace the `CURRENT` file with one that points to the + // nonexistent `MANIFEST-000000` file. If an earlier Pebble + // 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, base.FileNum(0).DiskFileNum()); err != nil { + return err + } + return d.finalizeFormatVersUpgrade(FormatVersioned) + }, + // As SetWithDelete is a new key kind, there is nothing to migrate. We can + // simply finalize the format version and we're done. + FormatSetWithDelete: func(d *DB) error { + return d.finalizeFormatVersUpgrade(FormatSetWithDelete) + }, + FormatBlockPropertyCollector: func(d *DB) error { + return d.finalizeFormatVersUpgrade(FormatBlockPropertyCollector) + }, + FormatSplitUserKeysMarked: func(d *DB) error { + // Mark any unmarked files with split-user keys. Note all format major + // versions migrations are invoked with DB.mu locked. + if err := d.markFilesLocked(markFilesWithSplitUserKeys(d.opts.Comparer.Equal)); err != nil { + return err + } + return d.finalizeFormatVersUpgrade(FormatSplitUserKeysMarked) + }, + FormatSplitUserKeysMarkedCompacted: func(d *DB) error { + // Before finalizing the format major version, rewrite any sstables + // still marked for compaction. Note all format major versions + // migrations are invoked with DB.mu locked. + if err := d.compactMarkedFilesLocked(); err != nil { + return err + } + return d.finalizeFormatVersUpgrade(FormatSplitUserKeysMarkedCompacted) + }, + FormatRangeKeys: func(d *DB) error { + return d.finalizeFormatVersUpgrade(FormatRangeKeys) + }, + FormatMinTableFormatPebblev1: func(d *DB) error { + return d.finalizeFormatVersUpgrade(FormatMinTableFormatPebblev1) + }, + FormatPrePebblev1Marked: func(d *DB) error { + // Mark any unmarked files that contain only table properties. Note all + // format major versions migrations are invoked with DB.mu locked. + if err := d.markFilesLocked(markFilesPrePebblev1(d.tableCache)); err != nil { + return err + } + return d.finalizeFormatVersUpgrade(FormatPrePebblev1Marked) + }, + formatUnusedPrePebblev1MarkedCompacted: func(d *DB) error { + // Intentional no-op. + return d.finalizeFormatVersUpgrade(formatUnusedPrePebblev1MarkedCompacted) + }, + FormatSSTableValueBlocks: func(d *DB) error { + return d.finalizeFormatVersUpgrade(FormatSSTableValueBlocks) + }, + FormatFlushableIngest: func(d *DB) error { + return d.finalizeFormatVersUpgrade(FormatFlushableIngest) + }, + FormatPrePebblev1MarkedCompacted: func(d *DB) error { + // Before finalizing the format major version, rewrite any sstables + // still marked for compaction. Note all format major versions + // migrations are invoked with DB.mu locked. + if err := d.compactMarkedFilesLocked(); err != nil { + return err + } + 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` + +func lookupFormatMajorVersion( + fs vfs.FS, dirname string, +) (FormatMajorVersion, *atomicfs.Marker, error) { + m, versString, err := atomicfs.LocateMarker(fs, dirname, formatVersionMarkerName) + if err != nil { + return 0, nil, err + } + if versString == "" { + return FormatMostCompatible, m, nil + } + v, err := strconv.ParseUint(versString, 10, 64) + if err != nil { + return 0, nil, errors.Wrap(err, "parsing format major version") + } + vers := FormatMajorVersion(v) + if vers == FormatDefault { + return 0, nil, errors.Newf("pebble: default format major version should not persisted", vers) + } + if vers > internalFormatNewest { + return 0, nil, errors.Newf("pebble: database %q written in format major version %d", dirname, vers) + } + return vers, m, nil +} + +// FormatMajorVersion returns the database's active format major +// version. The format major version may be higher than the one +// provided in Options when the database was opened if the existing +// database was written with a higher format version. +func (d *DB) FormatMajorVersion() FormatMajorVersion { + return FormatMajorVersion(d.mu.formatVers.vers.Load()) +} + +// RatchetFormatMajorVersion ratchets the opened database's format major +// version to the provided version. It errors if the provided format +// major version is below the database's current version. Once a +// database's format major version is upgraded, previous Pebble versions +// that do not know of the format version will be unable to open the +// database. +func (d *DB) RatchetFormatMajorVersion(fmv FormatMajorVersion) error { + if err := d.closed.Load(); err != nil { + panic(err) + } + + d.mu.Lock() + defer d.mu.Unlock() + return d.ratchetFormatMajorVersionLocked(fmv) +} + +func (d *DB) ratchetFormatMajorVersionLocked(formatVers FormatMajorVersion) error { + if d.opts.ReadOnly { + return ErrReadOnly + } + 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 { + return errors.Newf("pebble: database already at format major version %d; cannot reduce to %d", + currentVers, formatVers) + } + if d.mu.formatVers.ratcheting { + return errors.Newf("pebble: database format major version upgrade is in-progress") + } + d.mu.formatVers.ratcheting = true + defer func() { d.mu.formatVers.ratcheting = false }() + + for nextVers := d.FormatMajorVersion() + 1; nextVers <= formatVers; nextVers++ { + if err := formatMajorVersionMigrations[nextVers](d); err != nil { + return errors.Wrapf(err, "migrating to version %d", nextVers) + } + + // NB: The migration is responsible for calling + // finalizeFormatVersUpgrade to finalize the upgrade. This + // structure is necessary because some migrations may need to + // update in-memory state (without ever dropping locks) after + // the upgrade is finalized. Here we assert that the upgrade + // did occur. + if d.FormatMajorVersion() != nextVers { + d.opts.Logger.Fatalf("pebble: successful migration to format version %d never finalized the upgrade", nextVers) + } + } + return nil +} + +// finalizeFormatVersUpgrade is typically only be called from within a +// format major version migration. +// +// See formatMajorVersionMigrations. +func (d *DB) finalizeFormatVersUpgrade(formatVers FormatMajorVersion) error { + // We use the marker to encode the active format version in the + // marker filename. Unlike other uses of the atomic marker, there is + // no file with the filename `formatVers.String()` on the + // filesystem. + if err := d.mu.formatVers.marker.Move(formatVers.String()); err != nil { + return err + } + d.mu.formatVers.vers.Store(uint64(formatVers)) + d.opts.EventListener.FormatUpgrade(formatVers) + return nil +} + +// compactMarkedFilesLocked performs a migration that schedules rewrite +// compactions to compact away any sstables marked for compaction. +// compactMarkedFilesLocked is run while ratcheting the database's format major +// version to FormatSplitUserKeysMarkedCompacted. +// +// Note that while this method is called with the DB.mu held, and will not +// return until all marked files have been compacted, the mutex is dropped while +// waiting for compactions to complete (or for slots to free up). +func (d *DB) compactMarkedFilesLocked() error { + curr := d.mu.versions.currentVersion() + for curr.Stats.MarkedForCompaction > 0 { + // Attempt to schedule a compaction to rewrite a file marked for + // compaction. + d.maybeScheduleCompactionPicker(func(picker compactionPicker, env compactionEnv) *pickedCompaction { + return picker.pickRewriteCompaction(env) + }) + + // The above attempt might succeed and schedule a rewrite compaction. Or + // there might not be available compaction concurrency to schedule the + // compaction. Or compaction of the file might have already been in + // progress. In any scenario, wait until there's some change in the + // state of active compactions. + + // Before waiting, check that the database hasn't been closed. Trying to + // schedule the compaction may have dropped d.mu while waiting for a + // manifest write to complete. In that dropped interim, the database may + // have been closed. + if err := d.closed.Load(); err != nil { + return err.(error) + } + + // Some flush or compaction may have scheduled or completed while we waited + // for the manifest lock in maybeScheduleCompactionPicker. Get the latest + // Version before waiting on a compaction. + curr = d.mu.versions.currentVersion() + + // Only wait on compactions if there are files still marked for compaction. + // NB: Waiting on this condition variable drops d.mu while blocked. + if curr.Stats.MarkedForCompaction > 0 { + if d.mu.compact.compactingCount == 0 { + panic("expected a compaction of marked files in progress") + } + d.mu.compact.cond.Wait() + // Refresh the current version again. + curr = d.mu.versions.currentVersion() + } + } + return nil +} + +// findFilesFunc scans the LSM for files, returning true if at least one +// file was found. The returned array contains the matched files, if any, per +// level. +type findFilesFunc func(v *version) (found bool, files [numLevels][]*fileMetadata, _ error) + +// markFilesWithSplitUserKeys scans the LSM's levels 1 through 6 for adjacent +// files that contain the same user key. Such arrangements of files were +// permitted in RocksDB and in Pebble up to SHA a860bbad. +var markFilesWithSplitUserKeys = func(equal Equal) findFilesFunc { + return func(v *version) (found bool, files [numLevels][]*fileMetadata, _ error) { + // Files with split user keys are expected to be rare and performing key + // comparisons for every file within the LSM is expensive, so drop the + // database lock while scanning the file metadata. + for l := numLevels - 1; l > 0; l-- { + iter := v.Levels[l].Iter() + var prevFile *fileMetadata + var prevUserKey []byte + for f := iter.First(); f != nil; f = iter.Next() { + if prevUserKey != nil && equal(prevUserKey, f.Smallest.UserKey) { + // NB: We may append a file twice, once as prevFile and once + // as f. That's okay, and handled below. + files[l] = append(files[l], prevFile, f) + found = true + } + if f.Largest.IsExclusiveSentinel() { + prevUserKey = nil + prevFile = nil + } else { + prevUserKey = f.Largest.UserKey + prevFile = f + } + } + } + return + } +} + +// markFilesPrePebblev1 scans the LSM for files that do not support block +// properties (i.e. a table format version pre-Pebblev1). +var markFilesPrePebblev1 = func(tc *tableCacheContainer) findFilesFunc { + return func(v *version) (found bool, files [numLevels][]*fileMetadata, err error) { + for l := numLevels - 1; l > 0; l-- { + iter := v.Levels[l].Iter() + for f := iter.First(); f != nil; f = iter.Next() { + 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 + } + } + } + return + } +} + +// markFilesLock durably marks the files that match the given findFilesFunc for +// compaction. +func (d *DB) markFilesLocked(findFn findFilesFunc) error { + jobID := d.mu.nextJobID + d.mu.nextJobID++ + + // Acquire a read state to have a view of the LSM and a guarantee that none + // of the referenced files will be deleted until we've unreferenced the read + // state. Some findFilesFuncs may read the files, requiring they not be + // deleted. + rs := d.loadReadState() + var ( + found bool + files [numLevels][]*fileMetadata + err error + ) + func() { + defer rs.unrefLocked() + // Note the unusual locking: unlock, defer Lock(). The scan of the files in + // the version does not need to block other operations that require the + // DB.mu. Drop it for the scan, before re-acquiring it. + d.mu.Unlock() + defer d.mu.Lock() + found, files, err = findFn(rs.current) + }() + if err != nil { + return err + } + + // The database lock has been acquired again by the defer within the above + // anonymous function. + if !found { + // Nothing to do. + return nil + } + + // After scanning, if we found files to mark, we fetch the current state of + // the LSM (which may have changed) and set MarkedForCompaction on the files, + // and update the version's Stats.MarkedForCompaction count, which are both + // protected by d.mu. + + // Lock the manifest for a coherent view of the LSM. The database lock has + // been re-acquired by the defer within the above anonymous function. + d.mu.versions.logLock() + vers := d.mu.versions.currentVersion() + for l, filesToMark := range files { + if len(filesToMark) == 0 { + continue + } + for _, f := range filesToMark { + // Ignore files to be marked that have already been compacted or marked. + if f.CompactionState == manifest.CompactionStateCompacted || + f.MarkedForCompaction { + continue + } + // Else, mark the file for compaction in this version. + vers.Stats.MarkedForCompaction++ + f.MarkedForCompaction = true + } + // The compaction picker uses the markedForCompactionAnnotator to + // quickly find files marked for compaction, or to quickly determine + // that there are no such files marked for compaction within a level. + // A b-tree node may be annotated with an annotation recording that + // there are no files marked for compaction within the node's subtree, + // based on the assumption that it's static. + // + // Since we're marking files for compaction, these b-tree nodes' + // annotations will be out of date. Clear the compaction-picking + // annotation, so that it's recomputed the next time the compaction + // picker looks for a file marked for compaction. + vers.Levels[l].InvalidateAnnotation(markedForCompactionAnnotator{}) + } + + // The 'marked-for-compaction' bit is persisted in the MANIFEST file + // metadata. We've already modified the in-memory file metadata, but the + // manifest hasn't been updated. Force rotation to a new MANIFEST file, + // which will write every file metadata to the new manifest file and ensure + // that the now marked-for-compaction file metadata are persisted as marked. + // NB: This call to logAndApply will unlockthe MANIFEST, which we locked up + // above before obtaining `vers`. + return d.mu.versions.logAndApply( + jobID, + &manifest.VersionEdit{}, + map[int]*LevelMetrics{}, + true, /* forceRotation */ + func() []compactionInfo { return d.getInProgressCompactionInfoLocked(nil) }) +} diff --git a/pebble/format_major_version_test.go b/pebble/format_major_version_test.go new file mode 100644 index 0000000..bbca42b --- /dev/null +++ b/pebble/format_major_version_test.go @@ -0,0 +1,580 @@ +// Copyright 2021 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 ( + "bytes" + "fmt" + "strconv" + "sync" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/atomicfs" + "github.com/stretchr/testify/require" +) + +func TestFormatMajorVersion_MigrationDefined(t *testing.T) { + for v := FormatMostCompatible; v <= FormatNewest; v++ { + if _, ok := formatMajorVersionMigrations[v]; !ok { + t.Errorf("format major version %d has no migration defined", v) + } + } +} + +func TestRatchetFormat(t *testing.T) { + fs := vfs.NewMem() + d, err := Open("", (&Options{FS: fs}).WithFSDefaults()) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("foo"), []byte("bar"), Sync)) + require.Equal(t, FormatMostCompatible, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatVersioned)) + require.Equal(t, FormatVersioned, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatVersioned)) + require.Equal(t, FormatVersioned, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatSetWithDelete)) + require.Equal(t, FormatSetWithDelete, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatBlockPropertyCollector)) + require.Equal(t, FormatBlockPropertyCollector, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatSplitUserKeysMarked)) + require.Equal(t, FormatSplitUserKeysMarked, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatSplitUserKeysMarkedCompacted)) + require.Equal(t, FormatSplitUserKeysMarkedCompacted, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatRangeKeys)) + require.Equal(t, FormatRangeKeys, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatMinTableFormatPebblev1)) + require.Equal(t, FormatMinTableFormatPebblev1, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatPrePebblev1Marked)) + require.Equal(t, FormatPrePebblev1Marked, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(formatUnusedPrePebblev1MarkedCompacted)) + require.Equal(t, formatUnusedPrePebblev1MarkedCompacted, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatSSTableValueBlocks)) + require.Equal(t, FormatSSTableValueBlocks, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatFlushableIngest)) + require.Equal(t, FormatFlushableIngest, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatPrePebblev1MarkedCompacted)) + require.Equal(t, FormatPrePebblev1MarkedCompacted, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatDeleteSizedAndObsolete)) + require.Equal(t, FormatDeleteSizedAndObsolete, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(FormatVirtualSSTables)) + require.Equal(t, FormatVirtualSSTables, d.FormatMajorVersion()) + + require.NoError(t, d.Close()) + + // If we Open the database again, leaving the default format, the + // database should Open using the persisted FormatNewest. + d, err = Open("", (&Options{FS: fs}).WithFSDefaults()) + require.NoError(t, err) + require.Equal(t, internalFormatNewest, d.FormatMajorVersion()) + require.NoError(t, d.Close()) + + // Move the marker to a version that does not exist. + m, _, err := atomicfs.LocateMarker(fs, "", formatVersionMarkerName) + require.NoError(t, err) + require.NoError(t, m.Move("999999")) + require.NoError(t, m.Close()) + + _, err = Open("", (&Options{ + FS: fs, + FormatMajorVersion: FormatVersioned, + }).WithFSDefaults()) + require.Error(t, err) + require.EqualError(t, err, `pebble: database "" written in format major version 999999`) +} + +func testBasicDB(d *DB) error { + key := []byte("a") + value := []byte("b") + if err := d.Set(key, value, nil); err != nil { + return err + } + if err := d.Flush(); err != nil { + return err + } + if err := d.Compact(nil, []byte("\xff"), false); err != nil { + return err + } + + iter, _ := d.NewIter(nil) + for valid := iter.First(); valid; valid = iter.Next() { + } + if err := iter.Close(); err != nil { + return err + } + return nil +} + +func TestFormatMajorVersions(t *testing.T) { + for vers := FormatMostCompatible; vers <= FormatNewest; vers++ { + t.Run(fmt.Sprintf("vers=%03d", vers), func(t *testing.T) { + fs := vfs.NewStrictMem() + opts := (&Options{ + FS: fs, + FormatMajorVersion: vers, + }).WithFSDefaults() + + // Create a database at this format major version and perform + // some very basic operations. + d, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, testBasicDB(d)) + require.NoError(t, d.Close()) + + // Re-open the database at this format major version, and again + // perform some basic operations. + d, err = Open("", opts) + require.NoError(t, err) + require.NoError(t, testBasicDB(d)) + require.NoError(t, d.Close()) + + t.Run("upgrade-at-open", func(t *testing.T) { + for upgradeVers := vers + 1; upgradeVers <= FormatNewest; upgradeVers++ { + t.Run(fmt.Sprintf("upgrade-vers=%03d", upgradeVers), func(t *testing.T) { + // We use vfs.MemFS's option to ignore syncs so + // that we can perform an upgrade on the current + // database state in fs, and revert it when this + // subtest is complete. + fs.SetIgnoreSyncs(true) + defer fs.ResetToSyncedState() + + // Re-open the database, passing a higher format + // major version in the Options to automatically + // ratchet the format major version. Ensure some + // basic operations pass. + opts := opts.Clone() + opts.FormatMajorVersion = upgradeVers + d, err = Open("", opts) + require.NoError(t, err) + require.Equal(t, upgradeVers, d.FormatMajorVersion()) + require.NoError(t, testBasicDB(d)) + require.NoError(t, d.Close()) + + // Re-open to ensure the upgrade persisted. + d, err = Open("", opts) + require.NoError(t, err) + require.Equal(t, upgradeVers, d.FormatMajorVersion()) + require.NoError(t, testBasicDB(d)) + require.NoError(t, d.Close()) + }) + } + }) + + t.Run("upgrade-while-open", func(t *testing.T) { + for upgradeVers := vers + 1; upgradeVers <= FormatNewest; upgradeVers++ { + t.Run(fmt.Sprintf("upgrade-vers=%03d", upgradeVers), func(t *testing.T) { + // Ensure the previous tests don't overwrite our + // options. + require.Equal(t, vers, opts.FormatMajorVersion) + + // We use vfs.MemFS's option to ignore syncs so + // that we can perform an upgrade on the current + // database state in fs, and revert it when this + // subtest is complete. + fs.SetIgnoreSyncs(true) + defer fs.ResetToSyncedState() + + // Re-open the database, still at the current format + // major version. Perform some basic operations, + // ratchet the format version up, and perform + // additional basic operations. + d, err = Open("", opts) + require.NoError(t, err) + require.NoError(t, testBasicDB(d)) + require.Equal(t, vers, d.FormatMajorVersion()) + require.NoError(t, d.RatchetFormatMajorVersion(upgradeVers)) + require.Equal(t, upgradeVers, d.FormatMajorVersion()) + require.NoError(t, testBasicDB(d)) + require.NoError(t, d.Close()) + + // Re-open to ensure the upgrade persisted. + d, err = Open("", opts) + require.NoError(t, err) + require.Equal(t, upgradeVers, d.FormatMajorVersion()) + require.NoError(t, testBasicDB(d)) + require.NoError(t, d.Close()) + }) + } + }) + }) + } +} + +func TestFormatMajorVersions_TableFormat(t *testing.T) { + // NB: This test is intended to validate the mapping between every + // FormatMajorVersion and sstable.TableFormat exhaustively. This serves as a + // sanity check that new versions have a corresponding mapping. The test + // fixture is intentionally verbose. + + m := map[FormatMajorVersion][2]sstable.TableFormat{ + FormatDefault: {sstable.TableFormatLevelDB, sstable.TableFormatRocksDBv2}, + FormatMostCompatible: {sstable.TableFormatLevelDB, sstable.TableFormatRocksDBv2}, + formatVersionedManifestMarker: {sstable.TableFormatLevelDB, sstable.TableFormatRocksDBv2}, + FormatVersioned: {sstable.TableFormatLevelDB, sstable.TableFormatRocksDBv2}, + FormatSetWithDelete: {sstable.TableFormatLevelDB, sstable.TableFormatRocksDBv2}, + FormatBlockPropertyCollector: {sstable.TableFormatLevelDB, sstable.TableFormatPebblev1}, + FormatSplitUserKeysMarked: {sstable.TableFormatLevelDB, sstable.TableFormatPebblev1}, + FormatSplitUserKeysMarkedCompacted: {sstable.TableFormatLevelDB, sstable.TableFormatPebblev1}, + FormatRangeKeys: {sstable.TableFormatLevelDB, sstable.TableFormatPebblev2}, + FormatMinTableFormatPebblev1: {sstable.TableFormatPebblev1, sstable.TableFormatPebblev2}, + FormatPrePebblev1Marked: {sstable.TableFormatPebblev1, sstable.TableFormatPebblev2}, + formatUnusedPrePebblev1MarkedCompacted: {sstable.TableFormatPebblev1, sstable.TableFormatPebblev2}, + FormatSSTableValueBlocks: {sstable.TableFormatPebblev1, sstable.TableFormatPebblev3}, + FormatFlushableIngest: {sstable.TableFormatPebblev1, sstable.TableFormatPebblev3}, + FormatPrePebblev1MarkedCompacted: {sstable.TableFormatPebblev1, sstable.TableFormatPebblev3}, + FormatDeleteSizedAndObsolete: {sstable.TableFormatPebblev1, sstable.TableFormatPebblev4}, + FormatVirtualSSTables: {sstable.TableFormatPebblev1, sstable.TableFormatPebblev4}, + } + + // Valid versions. + for fmv := FormatMostCompatible; fmv <= internalFormatNewest; fmv++ { + got := [2]sstable.TableFormat{fmv.MinTableFormat(), fmv.MaxTableFormat()} + require.Equalf(t, m[fmv], got, "got %s; want %s", got, m[fmv]) + require.True(t, got[0] <= got[1] /* min <= max */) + } + + // Invalid versions. + fmv := internalFormatNewest + 1 + require.Panics(t, func() { _ = fmv.MaxTableFormat() }) + require.Panics(t, func() { _ = fmv.MinTableFormat() }) +} + +func TestSplitUserKeyMigration(t *testing.T) { + var d *DB + var opts *Options + var fs vfs.FS + var buf bytes.Buffer + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + datadriven.RunTest(t, "testdata/format_major_version_split_user_key_migration", + func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + if d != nil { + if err := d.Close(); err != nil { + return err.Error() + } + buf.Reset() + } + opts = (&Options{ + FormatMajorVersion: FormatBlockPropertyCollector, + EventListener: &EventListener{ + CompactionEnd: func(info CompactionInfo) { + // Fix the job ID and durations for determinism. + info.JobID = 100 + info.Duration = time.Second + info.TotalDuration = 2 * time.Second + fmt.Fprintln(&buf, info) + }, + }, + DisableAutomaticCompactions: true, + }).WithFSDefaults() + var err error + if d, err = runDBDefineCmd(td, opts); err != nil { + return err.Error() + } + + fs = d.opts.FS + d.mu.Lock() + defer d.mu.Unlock() + return d.mu.versions.currentVersion().DebugString(base.DefaultFormatter) + case "reopen": + if d != nil { + if err := d.Close(); err != nil { + return err.Error() + } + buf.Reset() + } + opts.FS = fs + opts.DisableAutomaticCompactions = true + var err error + d, err = Open("", opts) + if err != nil { + return err.Error() + } + return "OK" + case "build": + if err := runBuildCmd(td, d, fs); err != nil { + return err.Error() + } + return "" + case "force-ingest": + if err := runForceIngestCmd(td, d); err != nil { + return err.Error() + } + d.mu.Lock() + defer d.mu.Unlock() + return d.mu.versions.currentVersion().DebugString(base.DefaultFormatter) + case "format-major-version": + return d.FormatMajorVersion().String() + case "ratchet-format-major-version": + v, err := strconv.Atoi(td.CmdArgs[0].String()) + if err != nil { + return err.Error() + } + if err := d.RatchetFormatMajorVersion(FormatMajorVersion(v)); err != nil { + return err.Error() + } + return buf.String() + case "lsm": + return runLSMCmd(td, d) + case "marked-file-count": + m := d.Metrics() + return fmt.Sprintf("%d files marked for compaction", m.Compact.MarkedFiles) + case "disable-automatic-compactions": + d.mu.Lock() + defer d.mu.Unlock() + switch v := td.CmdArgs[0].String(); v { + case "true": + d.opts.DisableAutomaticCompactions = true + case "false": + d.opts.DisableAutomaticCompactions = false + default: + return fmt.Sprintf("unknown value %q", v) + } + return "" + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +func TestPebblev1Migration(t *testing.T) { + var d *DB + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + + datadriven.RunTest(t, "testdata/format_major_version_pebblev1_migration", + func(t *testing.T, td *datadriven.TestData) string { + switch cmd := td.Cmd; cmd { + case "open": + var version int + var err error + for _, cmdArg := range td.CmdArgs { + switch cmd := cmdArg.Key; cmd { + case "version": + version, err = strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return err.Error() + } + default: + return fmt.Sprintf("unknown argument: %s", cmd) + } + } + opts := (&Options{ + FS: vfs.NewMem(), + FormatMajorVersion: FormatMajorVersion(version), + }).WithFSDefaults() + d, err = Open("", opts) + if err != nil { + return err.Error() + } + return "" + + case "format-major-version": + return d.FormatMajorVersion().String() + + case "min-table-format": + return d.FormatMajorVersion().MinTableFormat().String() + + case "max-table-format": + return d.FormatMajorVersion().MaxTableFormat().String() + + case "disable-automatic-compactions": + d.mu.Lock() + defer d.mu.Unlock() + switch v := td.CmdArgs[0].String(); v { + case "true": + d.opts.DisableAutomaticCompactions = true + case "false": + d.opts.DisableAutomaticCompactions = false + default: + return fmt.Sprintf("unknown value %q", v) + } + return "" + + case "batch": + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + return "" + + case "ingest": + if err := runBuildCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + // Only the first arg is a filename. + td.CmdArgs = td.CmdArgs[:1] + if err := runIngestCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + return "" + + case "lsm": + return runLSMCmd(td, d) + + case "tally-table-formats": + d.mu.Lock() + defer d.mu.Unlock() + v := d.mu.versions.currentVersion() + tally := make([]int, sstable.TableFormatMax+1) + for _, l := range v.Levels { + iter := l.Iter() + for m := iter.First(); m != nil; m = iter.Next() { + err := d.tableCache.withReader(m.PhysicalMeta(), + func(r *sstable.Reader) error { + f, err := r.TableFormat() + if err != nil { + return err + } + tally[f]++ + return nil + }) + if err != nil { + return err.Error() + } + } + } + var b bytes.Buffer + for i := 1; i <= int(sstable.TableFormatMax); i++ { + _, _ = fmt.Fprintf(&b, "%s: %d\n", sstable.TableFormat(i), tally[i]) + } + return b.String() + + case "ratchet-format-major-version": + v, err := strconv.Atoi(td.CmdArgs[0].String()) + if err != nil { + return err.Error() + } + if err = d.RatchetFormatMajorVersion(FormatMajorVersion(v)); err != nil { + return err.Error() + } + return "" + + case "marked-file-count": + m := d.Metrics() + return fmt.Sprintf("%d files marked for compaction", m.Compact.MarkedFiles) + + default: + return fmt.Sprintf("unknown command: %s", cmd) + } + }, + ) +} + +// TestPebblev1MigrationRace exercises the race between a PrePebbleV1Marked +// format major version upgrade that needs to open sstables to read their table +// format, and concurrent compactions that may delete the same files from the +// LSM. +// +// Regression test for #2019. +func TestPebblev1MigrationRace(t *testing.T) { + // Use a smaller table cache size to slow down the PrePebbleV1Marked + // migration, ensuring each table read needs to re-open the file. + cache := NewCache(4 << 20) + defer cache.Unref() + tableCache := NewTableCache(cache, 1, 5) + defer tableCache.Unref() + d, err := Open("", (&Options{ + Cache: cache, + FS: vfs.NewMem(), + FormatMajorVersion: FormatMajorVersion(FormatPrePebblev1Marked - 1), + TableCache: tableCache, + Levels: []LevelOptions{{TargetFileSize: 1}}, + }).WithFSDefaults()) + require.NoError(t, err) + defer d.Close() + + ks := testkeys.Alpha(3).EveryN(10) + var key [3]byte + for i := int64(0); i < ks.Count(); i++ { + n := testkeys.WriteKey(key[:], ks, i) + require.NoError(t, d.Set(key[:n], key[:n], nil)) + require.NoError(t, d.Flush()) + } + + // Asynchronously write and flush range deletes that will cause compactions + // to delete the existing sstables. These deletes will race with the format + // major version upgrade's migration will attempt to delete the files. + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for i := ks.Count() - 1; i > 0; i -= 50 { + endKey := testkeys.Key(ks, i) + startIndex := i - 50 + if startIndex < 0 { + startIndex = 0 + } + startKey := testkeys.Key(ks, startIndex) + + require.NoError(t, d.DeleteRange(startKey, endKey, nil)) + _, err := d.AsyncFlush() + require.NoError(t, err) + } + }() + require.NoError(t, d.RatchetFormatMajorVersion(FormatPrePebblev1Marked)) + wg.Wait() +} + +// Regression test for #2044, where multiple concurrent compactions can lead +// to an indefinite wait on the compaction goroutine in compactMarkedFilesLocked. +func TestPebblev1MigrationConcurrencyRace(t *testing.T) { + opts := (&Options{ + Comparer: testkeys.Comparer, + FS: vfs.NewMem(), + FormatMajorVersion: FormatSplitUserKeysMarked, + Levels: []LevelOptions{{FilterPolicy: bloom.FilterPolicy(10)}}, + MaxConcurrentCompactions: func() int { + return 4 + }, + }).WithFSDefaults() + func() { + d, err := Open("", opts) + require.NoError(t, err) + defer func() { + require.NoError(t, d.Close()) + }() + + ks := testkeys.Alpha(3).EveryN(10) + var key [3]byte + for i := int64(0); i < ks.Count(); i++ { + n := testkeys.WriteKey(key[:], ks, i) + require.NoError(t, d.Set(key[:n], key[:n], nil)) + if i%100 == 0 { + require.NoError(t, d.Flush()) + } + } + require.NoError(t, d.Flush()) + }() + + opts.FormatMajorVersion = formatUnusedPrePebblev1MarkedCompacted + d, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d.RatchetFormatMajorVersion(formatUnusedPrePebblev1MarkedCompacted)) + require.NoError(t, d.Close()) +} diff --git a/pebble/get_iter.go b/pebble/get_iter.go new file mode 100644 index 0000000..6ebdd59 --- /dev/null +++ b/pebble/get_iter.go @@ -0,0 +1,258 @@ +// Copyright 2018 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/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/sstable" +) + +// getIter is an internal iterator used to perform gets. It iterates through +// the values for a particular key, level by level. It is not a general purpose +// internalIterator, but specialized for Get operations so that it loads data +// lazily. +type getIter struct { + logger Logger + comparer *Comparer + newIters tableNewIters + snapshot uint64 + key []byte + iter internalIterator + rangeDelIter keyspan.FragmentIterator + tombstone *keyspan.Span + levelIter levelIter + level int + batch *Batch + mem flushableList + l0 []manifest.LevelSlice + version *version + iterKey *InternalKey + iterValue base.LazyValue + err error +} + +// TODO(sumeer): CockroachDB code doesn't use getIter, but, for completeness, +// make this implement InternalIteratorWithStats. + +// getIter implements the base.InternalIterator interface. +var _ base.InternalIterator = (*getIter)(nil) + +func (g *getIter) String() string { + return fmt.Sprintf("len(l0)=%d, len(mem)=%d, level=%d", len(g.l0), len(g.mem), g.level) +} + +func (g *getIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, base.LazyValue) { + panic("pebble: SeekGE unimplemented") +} + +func (g *getIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("pebble: SeekPrefixGE unimplemented") +} + +func (g *getIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, base.LazyValue) { + panic("pebble: SeekLT unimplemented") +} + +func (g *getIter) First() (*InternalKey, base.LazyValue) { + return g.Next() +} + +func (g *getIter) Last() (*InternalKey, base.LazyValue) { + panic("pebble: Last unimplemented") +} + +func (g *getIter) Next() (*InternalKey, base.LazyValue) { + if g.iter != nil { + g.iterKey, g.iterValue = g.iter.Next() + } + + for { + if g.iter != nil { + // We have to check rangeDelIter on each iteration because a single + // user-key can be spread across multiple tables in a level. A range + // tombstone will appear in the table corresponding to its start + // 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.comparer.Compare, g.rangeDelIter, g.key) + if g.err = g.rangeDelIter.Close(); g.err != nil { + return nil, base.LazyValue{} + } + g.rangeDelIter = nil + } + + if g.iterKey != nil { + key := g.iterKey + if g.tombstone != nil && g.tombstone.CoversAt(g.snapshot, key.SeqNum()) { + // We have a range tombstone covering this key. Rather than return a + // point or range deletion here, we return false and close our + // internal iterator which will make Valid() return false, + // effectively stopping iteration. + g.err = g.iter.Close() + g.iter = nil + return nil, base.LazyValue{} + } + if g.comparer.Equal(g.key, key.UserKey) { + if !key.Visible(g.snapshot, base.InternalKeySeqNumMax) { + g.iterKey, g.iterValue = g.iter.Next() + continue + } + return g.iterKey, g.iterValue + } + } + // We've advanced the iterator passed the desired key. Move on to the + // next memtable / level. + g.err = g.iter.Close() + g.iter = nil + if g.err != nil { + return nil, base.LazyValue{} + } + } + + // Create an iterator from the batch. + if g.batch != nil { + if g.batch.index == nil { + g.err = ErrNotIndexed + g.iterKey, g.iterValue = nil, base.LazyValue{} + return nil, base.LazyValue{} + } + g.iter = g.batch.newInternalIter(nil) + g.rangeDelIter = g.batch.newRangeDelIter( + nil, + // Get always reads the entirety of the batch's history, so no + // batch keys should be filtered. + base.InternalKeySeqNumMax, + ) + g.iterKey, g.iterValue = g.iter.SeekGE(g.key, base.SeekGEFlagsNone) + g.batch = nil + continue + } + + // If we have a tombstone from a previous level it is guaranteed to delete + // keys in lower levels. + if g.tombstone != nil && g.tombstone.VisibleAt(g.snapshot) { + return nil, base.LazyValue{} + } + + // Create iterators from memtables from newest to oldest. + if n := len(g.mem); n > 0 { + m := g.mem[n-1] + g.iter = m.newIter(nil) + g.rangeDelIter = m.newRangeDelIter(nil) + g.mem = g.mem[:n-1] + g.iterKey, g.iterValue = g.iter.SeekGE(g.key, base.SeekGEFlagsNone) + continue + } + + if g.level == 0 { + // Create iterators from L0 from newest to oldest. + if n := len(g.l0); n > 0 { + files := g.l0[n-1].Iter() + g.l0 = g.l0[:n-1] + iterOpts := IterOptions{ + // TODO(sumeer): replace with a parameter provided by the caller. + CategoryAndQoS: sstable.CategoryAndQoS{ + Category: "pebble-get", + QoSLevel: sstable.LatencySensitiveQoSLevel, + }, + 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 + + // 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++ + } + + if g.level >= numLevels { + return nil, base.LazyValue{} + } + if g.version.Levels[g.level].Empty() { + g.level++ + continue + } + + iterOpts := IterOptions{ + // TODO(sumeer): replace with a parameter provided by the caller. + CategoryAndQoS: sstable.CategoryAndQoS{ + Category: "pebble-get", + QoSLevel: sstable.LatencySensitiveQoSLevel, + }, 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 + + // 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{} + } + } +} + +func (g *getIter) Prev() (*InternalKey, base.LazyValue) { + panic("pebble: Prev unimplemented") +} + +func (g *getIter) NextPrefix([]byte) (*InternalKey, base.LazyValue) { + panic("pebble: NextPrefix unimplemented") +} + +func (g *getIter) Valid() bool { + return g.iterKey != nil && g.err == nil +} + +func (g *getIter) Error() error { + return g.err +} + +func (g *getIter) Close() error { + if g.iter != nil { + if err := g.iter.Close(); err != nil && g.err == nil { + g.err = err + } + g.iter = nil + } + return g.err +} + +func (g *getIter) SetBounds(lower, upper []byte) { + panic("pebble: SetBounds unimplemented") +} + +func (g *getIter) SetContext(_ context.Context) {} diff --git a/pebble/get_iter_test.go b/pebble/get_iter_test.go new file mode 100644 index 0000000..ab6e67e --- /dev/null +++ b/pebble/get_iter_test.go @@ -0,0 +1,576 @@ +// Copyright 2018 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" + "strings" + "testing" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/internal/testkeys" +) + +func TestGetIter(t *testing.T) { + // testTable is a table to insert into a version. + // Each element of data is a string of the form "internalKey value". + type testTable struct { + level int + fileNum FileNum + data []string + } + + testCases := []struct { + description string + // badOrdering is whether this test case has a table ordering violation. + badOrdering bool + // tables are the tables to populate the version with. + tables []testTable + // queries are the queries to run against the version. Each element has + // the form "internalKey wantedValue". The internalKey is passed to the + // version.get method, wantedValue may be "ErrNotFound" if the query + // should return that error. + queries []string + }{ + { + description: "empty: an empty version", + queries: []string{ + "abc.SEPARATOR.101 ErrNotFound", + }, + }, + + { + description: "single-0: one level-0 table", + tables: []testTable{ + { + level: 0, + fileNum: 10, + data: []string{ + "the.SET.101 a", + "cat.SET.102 b", + "on_.SET.103 c", + "the.SET.104 d", + "mat.SET.105 e", + "the.DEL.106 ", + "the.MERGE.107 g", + }, + }, + }, + queries: []string{ + "aaa.SEPARATOR.105 ErrNotFound", + "cat.SEPARATOR.105 b", + "hat.SEPARATOR.105 ErrNotFound", + "mat.SEPARATOR.105 e", + "the.SEPARATOR.108 g", + "the.SEPARATOR.107 g", + "the.SEPARATOR.106 ErrNotFound", + "the.SEPARATOR.105 d", + "the.SEPARATOR.104 d", + "the.SEPARATOR.104 d", + "the.SEPARATOR.103 a", + "the.SEPARATOR.102 a", + "the.SEPARATOR.101 a", + "the.SEPARATOR.100 ErrNotFound", + "zzz.SEPARATOR.105 ErrNotFound", + }, + }, + + { + description: "triple-0: three level-0 tables", + tables: []testTable{ + { + level: 0, + fileNum: 10, + data: []string{ + "the.SET.101 a", + "cat.SET.102 b", + "on_.SET.103 c", + "the.SET.104 d", + "mat.SET.105 e", + "the.DEL.106 ", + "the.SET.107 g", + }, + }, + { + level: 0, + fileNum: 11, + data: []string{ + "awk.SET.111 w", + "cat.SET.112 x", + "man.SET.113 y", + "sed.SET.114 z", + }, + }, + { + level: 0, + fileNum: 12, + data: []string{ + "the.DEL.121 ", + "cat.DEL.122 ", + "man.DEL.123 ", + "was.SET.124 D", + "not.SET.125 E", + "the.SET.126 F", + "man.SET.127 G", + }, + }, + }, + queries: []string{ + "aaa.SEPARATOR.105 ErrNotFound", + "awk.SEPARATOR.135 w", + "awk.SEPARATOR.125 w", + "awk.SEPARATOR.115 w", + "awk.SEPARATOR.105 ErrNotFound", + "cat.SEPARATOR.135 ErrNotFound", + "cat.SEPARATOR.125 ErrNotFound", + "cat.SEPARATOR.115 x", + "cat.SEPARATOR.105 b", + "man.SEPARATOR.135 G", + "man.SEPARATOR.125 ErrNotFound", + "man.SEPARATOR.115 y", + "man.SEPARATOR.105 ErrNotFound", + "on_.SEPARATOR.135 c", + "on_.SEPARATOR.125 c", + "on_.SEPARATOR.115 c", + "on_.SEPARATOR.105 c", + "the.SEPARATOR.135 F", + "the.SEPARATOR.127 F", + "the.SEPARATOR.126 F", + "the.SEPARATOR.125 ErrNotFound", + "the.SEPARATOR.122 ErrNotFound", + "the.SEPARATOR.121 ErrNotFound", + "the.SEPARATOR.120 g", + "the.SEPARATOR.115 g", + "the.SEPARATOR.114 g", + "the.SEPARATOR.111 g", + "the.SEPARATOR.110 g", + "the.SEPARATOR.108 g", + "the.SEPARATOR.107 g", + "the.SEPARATOR.106 ErrNotFound", + "the.SEPARATOR.105 d", + "the.SEPARATOR.104 d", + "the.SEPARATOR.104 d", + "the.SEPARATOR.103 a", + "the.SEPARATOR.102 a", + "the.SEPARATOR.101 a", + "the.SEPARATOR.100 ErrNotFound", + "zzz.SEPARATOR.105 ErrNotFound", + }, + }, + + { + description: "quad-4: four level-4 tables", + tables: []testTable{ + { + level: 4, + fileNum: 11, + data: []string{ + "aardvark.SET.101 a1", + "alpaca__.SET.201 a2", + "anteater.SET.301 a3", + }, + }, + { + level: 4, + fileNum: 22, + data: []string{ + "baboon__.SET.102 b1", + "baboon__.DEL.202 ", + "baboon__.SET.302 b3", + "bear____.SET.402 b4", + "bear____.DEL.502 ", + "buffalo_.SET.602 b6", + }, + }, + { + level: 4, + fileNum: 33, + data: []string{ + "buffalo_.SET.103 B1", + }, + }, + { + level: 4, + fileNum: 44, + data: []string{ + "chipmunk.SET.104 c1", + "chipmunk.SET.204 c2", + }, + }, + }, + queries: []string{ + "a_______.SEPARATOR.999 ErrNotFound", + "aardvark.SEPARATOR.999 a1", + "aardvark.SEPARATOR.102 a1", + "aardvark.SEPARATOR.101 a1", + "aardvark.SEPARATOR.100 ErrNotFound", + "alpaca__.SEPARATOR.999 a2", + "alpaca__.SEPARATOR.200 ErrNotFound", + "anteater.SEPARATOR.999 a3", + "anteater.SEPARATOR.302 a3", + "anteater.SEPARATOR.301 a3", + "anteater.SEPARATOR.300 ErrNotFound", + "anteater.SEPARATOR.000 ErrNotFound", + "b_______.SEPARATOR.999 ErrNotFound", + "baboon__.SEPARATOR.999 b3", + "baboon__.SEPARATOR.302 b3", + "baboon__.SEPARATOR.301 ErrNotFound", + "baboon__.SEPARATOR.202 ErrNotFound", + "baboon__.SEPARATOR.201 b1", + "baboon__.SEPARATOR.102 b1", + "baboon__.SEPARATOR.101 ErrNotFound", + "bear____.SEPARATOR.999 ErrNotFound", + "bear____.SEPARATOR.500 b4", + "bear____.SEPARATOR.000 ErrNotFound", + "buffalo_.SEPARATOR.999 b6", + "buffalo_.SEPARATOR.603 b6", + "buffalo_.SEPARATOR.602 b6", + "buffalo_.SEPARATOR.601 B1", + "buffalo_.SEPARATOR.104 B1", + "buffalo_.SEPARATOR.103 B1", + "buffalo_.SEPARATOR.102 ErrNotFound", + "buffalo_.SEPARATOR.000 ErrNotFound", + "c_______.SEPARATOR.999 ErrNotFound", + "chipmunk.SEPARATOR.999 c2", + "chipmunk.SEPARATOR.205 c2", + "chipmunk.SEPARATOR.204 c2", + "chipmunk.SEPARATOR.203 c1", + "chipmunk.SEPARATOR.105 c1", + "chipmunk.SEPARATOR.104 c1", + "chipmunk.SEPARATOR.103 ErrNotFound", + "chipmunk.SEPARATOR.000 ErrNotFound", + "d_______.SEPARATOR.999 ErrNotFound", + }, + }, + + { + description: "complex: many tables at many levels", + tables: []testTable{ + { + level: 0, + fileNum: 50, + data: []string{ + "alfalfa__.SET.501 p1", + "asparagus.SET.502 p2", + "cabbage__.DEL.503 ", + "spinach__.MERGE.504 p3", + }, + }, + { + level: 0, + fileNum: 51, + data: []string{ + "asparagus.SET.511 q1", + "asparagus.SET.512 q2", + "asparagus.SET.513 q3", + "beans____.SET.514 q4", + "broccoli_.SET.515 q5", + "cabbage__.SET.516 q6", + "celery___.SET.517 q7", + "spinach__.MERGE.518 q8", + }, + }, + { + level: 1, + fileNum: 40, + data: []string{ + "alfalfa__.SET.410 r1", + "asparagus.SET.420 r2", + "arugula__.SET.430 r3", + }, + }, + { + level: 1, + fileNum: 41, + data: []string{ + "beans____.SET.411 s1", + "beans____.SET.421 s2", + "bokchoy__.DEL.431 ", + "broccoli_.SET.441 s4", + }, + }, + { + level: 1, + fileNum: 42, + data: []string{ + "cabbage__.SET.412 t1", + "corn_____.DEL.422 ", + "spinach__.MERGE.432 t2", + }, + }, + { + level: 2, + fileNum: 30, + data: []string{ + "alfalfa__.SET.310 u1", + "bokchoy__.SET.320 u2", + "celery___.SET.330 u3", + "corn_____.SET.340 u4", + "spinach__.MERGE.350 u5", + }, + }, + }, + queries: []string{ + "a________.SEPARATOR.999 ErrNotFound", + "alfalfa__.SEPARATOR.520 p1", + "alfalfa__.SEPARATOR.510 p1", + "alfalfa__.SEPARATOR.500 r1", + "alfalfa__.SEPARATOR.400 u1", + "alfalfa__.SEPARATOR.300 ErrNotFound", + "asparagus.SEPARATOR.520 q3", + "asparagus.SEPARATOR.510 p2", + "asparagus.SEPARATOR.500 r2", + "asparagus.SEPARATOR.400 ErrNotFound", + "asparagus.SEPARATOR.300 ErrNotFound", + "arugula__.SEPARATOR.520 r3", + "arugula__.SEPARATOR.510 r3", + "arugula__.SEPARATOR.500 r3", + "arugula__.SEPARATOR.400 ErrNotFound", + "arugula__.SEPARATOR.300 ErrNotFound", + "beans____.SEPARATOR.520 q4", + "beans____.SEPARATOR.510 s2", + "beans____.SEPARATOR.500 s2", + "beans____.SEPARATOR.400 ErrNotFound", + "beans____.SEPARATOR.300 ErrNotFound", + "bokchoy__.SEPARATOR.520 ErrNotFound", + "bokchoy__.SEPARATOR.510 ErrNotFound", + "bokchoy__.SEPARATOR.500 ErrNotFound", + "bokchoy__.SEPARATOR.400 u2", + "bokchoy__.SEPARATOR.300 ErrNotFound", + "broccoli_.SEPARATOR.520 q5", + "broccoli_.SEPARATOR.510 s4", + "broccoli_.SEPARATOR.500 s4", + "broccoli_.SEPARATOR.400 ErrNotFound", + "broccoli_.SEPARATOR.300 ErrNotFound", + "cabbage__.SEPARATOR.520 q6", + "cabbage__.SEPARATOR.510 ErrNotFound", + "cabbage__.SEPARATOR.500 t1", + "cabbage__.SEPARATOR.400 ErrNotFound", + "cabbage__.SEPARATOR.300 ErrNotFound", + "celery___.SEPARATOR.520 q7", + "celery___.SEPARATOR.510 u3", + "celery___.SEPARATOR.500 u3", + "celery___.SEPARATOR.400 u3", + "celery___.SEPARATOR.300 ErrNotFound", + "corn_____.SEPARATOR.520 ErrNotFound", + "corn_____.SEPARATOR.510 ErrNotFound", + "corn_____.SEPARATOR.500 ErrNotFound", + "corn_____.SEPARATOR.400 u4", + "corn_____.SEPARATOR.300 ErrNotFound", + "d________.SEPARATOR.999 ErrNotFound", + "spinach__.SEPARATOR.999 u5t2p3q8", + "spinach__.SEPARATOR.518 u5t2p3q8", + "spinach__.SEPARATOR.517 u5t2p3", + "spinach__.SEPARATOR.504 u5t2p3", + "spinach__.SEPARATOR.503 u5t2", + "spinach__.SEPARATOR.432 u5t2", + "spinach__.SEPARATOR.431 u5", + "spinach__.SEPARATOR.350 u5", + "spinach__.SEPARATOR.349 ErrNotFound", + }, + }, + + { + description: "broken invariants 0: non-increasing level 0 sequence numbers", + badOrdering: true, + tables: []testTable{ + { + level: 0, + fileNum: 19, + data: []string{ + "a.SET.101 a", + "b.SET.102 b", + }, + }, + { + level: 0, + fileNum: 20, + data: []string{ + "c.SET.101 c", + }, + }, + }, + }, + + { + description: "broken invariants 1: non-increasing level 0 sequence numbers", + badOrdering: true, + tables: []testTable{ + { + level: 0, + fileNum: 19, + data: []string{ + "a.SET.101 a", + "b.SET.102 b", + }, + }, + { + level: 0, + fileNum: 20, + data: []string{ + "c.SET.100 c", + "d.SET.101 d", + }, + }, + }, + }, + + { + description: "broken invariants 2: matching level 0 sequence numbers, considered acceptable", + badOrdering: false, + tables: []testTable{ + { + level: 0, + fileNum: 19, + data: []string{ + "a.SET.101 a", + }, + }, + { + level: 0, + fileNum: 20, + data: []string{ + "a.SET.101 a", + }, + }, + }, + }, + + { + description: "broken invariants 3: level non-0 overlapping internal key ranges", + badOrdering: true, + tables: []testTable{ + { + level: 5, + fileNum: 11, + data: []string{ + "bat.SET.101 xxx", + "dog.SET.102 xxx", + }, + }, + { + level: 5, + fileNum: 12, + data: []string{ + "cow.SET.103 xxx", + "pig.SET.104 xxx", + }, + }, + }, + }, + } + + cmp := testkeys.Comparer.Compare + for _, tc := range testCases { + desc := tc.description[:strings.Index(tc.description, ":")] + + // m is a map from file numbers to DBs. + m := map[FileNum]*memTable{} + newIter := func( + _ context.Context, file *manifest.FileMetadata, _ *IterOptions, _ internalIterOpts, + ) (internalIterator, keyspan.FragmentIterator, error) { + d, ok := m[file.FileNum] + if !ok { + return nil, nil, errors.New("no such file") + } + return d.newIter(nil), nil, nil + } + + var files [numLevels][]*fileMetadata + for _, tt := range tc.tables { + d := newMemTable(memTableOptions{}) + m[tt.fileNum] = d + + meta := &fileMetadata{ + FileNum: tt.fileNum, + } + meta.InitPhysicalBacking() + for i, datum := range tt.data { + s := strings.Split(datum, " ") + ikey := base.ParseInternalKey(s[0]) + err := d.set(ikey, []byte(s[1])) + if err != nil { + t.Fatalf("desc=%q: memtable Set: %v", desc, err) + } + + meta.ExtendPointKeyBounds(cmp, ikey, ikey) + if i == 0 { + meta.SmallestSeqNum = ikey.SeqNum() + meta.LargestSeqNum = ikey.SeqNum() + } else { + if meta.SmallestSeqNum > ikey.SeqNum() { + meta.SmallestSeqNum = ikey.SeqNum() + } + if meta.LargestSeqNum < ikey.SeqNum() { + meta.LargestSeqNum = ikey.SeqNum() + } + } + } + + files[tt.level] = append(files[tt.level], meta) + } + v := manifest.NewVersion(cmp, base.DefaultFormatter, 10<<20, files) + err := v.CheckOrdering(cmp, base.DefaultFormatter, manifest.AllowSplitUserKeys) + if tc.badOrdering && err == nil { + t.Errorf("desc=%q: want bad ordering, got nil error", desc) + continue + } else if !tc.badOrdering && err != nil { + t.Errorf("desc=%q: bad ordering: %v", desc, err) + continue + } + + get := func(v *version, ikey InternalKey) ([]byte, error) { + var buf struct { + dbi Iterator + get getIter + } + + get := &buf.get + get.comparer = testkeys.Comparer + get.newIters = newIter + get.key = ikey.UserKey + get.l0 = v.L0SublevelFiles + get.version = v + get.snapshot = ikey.SeqNum() + 1 + + i := &buf.dbi + i.comparer = *testkeys.Comparer + i.merge = DefaultMerger.Merge + i.iter = get + + defer i.Close() + if !i.First() { + err := i.Error() + if err != nil { + return nil, err + } + return nil, ErrNotFound + } + return i.Value(), nil + } + + for _, query := range tc.queries { + s := strings.Split(query, " ") + ikey := base.ParseInternalKey(s[0]) + value, err := get(v, ikey) + got, want := "", s[1] + if err != nil { + if err != ErrNotFound { + t.Errorf("desc=%q: query=%q: %v", desc, s[0], err) + continue + } + got = "ErrNotFound" + } else { + got = string(value) + } + if got != want { + t.Errorf("desc=%q: query=%q: got %q, want %q", desc, s[0], got, want) + } + } + } +} diff --git a/pebble/go.mod b/pebble/go.mod new file mode 100644 index 0000000..d882642 --- /dev/null +++ b/pebble/go.mod @@ -0,0 +1,49 @@ +module github.com/cockroachdb/pebble + +require ( + github.com/DataDog/zstd v1.4.5 + github.com/HdrHistogram/hdrhistogram-go v1.1.2 + github.com/cespare/xxhash/v2 v2.2.0 + github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f + github.com/cockroachdb/errors v1.11.1 + github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895 + github.com/cockroachdb/redact v1.1.5 + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 + github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9 + github.com/golang/snappy v0.0.4 + github.com/guptarohit/asciigraph v0.5.5 + github.com/klauspost/compress v1.15.15 + github.com/kr/pretty v0.3.1 + github.com/pkg/errors v0.9.1 + github.com/pmezard/go-difflib v1.0.0 + github.com/prometheus/client_golang v1.12.0 + github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a + github.com/spf13/cobra v1.0.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df + golang.org/x/perf v0.0.0-20230113213139-801c7ef9e5c5 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 + golang.org/x/sys v0.11.0 +) + +require ( + github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/getsentry/sentry-go v0.18.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/text v0.7.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +go 1.20 diff --git a/pebble/go.sum b/pebble/go.sum new file mode 100644 index 0000000..89e7f53 --- /dev/null +++ b/pebble/go.sum @@ -0,0 +1,666 @@ +cloud.google.com/go v0.0.0-20170206221025-ce650573d812/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190129172621-c8b1d7a94ddf/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aclements/go-gg v0.0.0-20170118225347-6dbb4e4fefb0/go.mod h1:55qNq4vcpkIuHowELi5C8e+1yUHtoLoOUR9QU5j7Tes= +github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= +github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20210923152817-c3b6e2f0c527/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +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/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +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/metamorphic v0.0.0-20231108215700-4ba948b56895 h1:XANOgPYtvELQ/h4IrmPAohXqe2pWA8Bwhejr3VQoZsA= +github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895/go.mod h1:aPd7gM9ov9M8v32Yy5NJrDyOcD8z642dqs+F0CeNXfA= +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/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= +github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9 h1:r5GgOLGbza2wVHRzK7aAj6lWZjfbAwiu/RDCVOKjRyM= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= +github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg= +github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks= +github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A= +github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= +github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/guptarohit/asciigraph v0.5.5 h1:ccFnUF8xYIOUPPY3tmdvRyHqmn1MYI9iv1pLKX+/ZkQ= +github.com/guptarohit/asciigraph v0.5.5/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= +github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= +github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20170207211851-4464e7848382/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/perf v0.0.0-20230113213139-801c7ef9e5c5 h1:ObuXPmIgI4ZMyQLIz48cJYgSyWdjUXc2SZAdyJMwEAU= +golang.org/x/perf v0.0.0-20230113213139-801c7ef9e5c5/go.mod h1:UBKtEnL8aqnd+0JHqZ+2qoMDwtuy6cYhhKNoHLBiTQc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.0/go.mod h1:JWIHJ7U20drSQb/aDpTetJzfC1KlAPldJLpkSy88dvQ= +google.golang.org/api v0.0.0-20170206182103-3d017632ea10/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v0.0.0-20170208002647-2a6bf6142e96/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pebble/ingest.go b/pebble/ingest.go new file mode 100644 index 0000000..149340d --- /dev/null +++ b/pebble/ingest.go @@ -0,0 +1,2410 @@ +// Copyright 2018 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" + "sort" + "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/shims/slices" + "github.com/cockroachdb/pebble/sstable" +) + +func sstableKeyCompare(userCmp Compare, a, b InternalKey) int { + c := userCmp(a.UserKey, b.UserKey) + if c != 0 { + return c + } + if a.IsExclusiveSentinel() { + if !b.IsExclusiveSentinel() { + return -1 + } + } else if b.IsExclusiveSentinel() { + return +1 + } + 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", + key.Pretty(opts.Comparer.FormatKey)) + } + if key.SeqNum() != 0 { + return base.CorruptionErrorf("pebble: external sstable has non-zero seqnum: %s", + key.Pretty(opts.Comparer.FormatKey)) + } + return nil +} + +// 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) { + 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 + } + 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 { + return nil, err + } + defer r.Close() + + // Avoid ingesting tables with format versions this DB doesn't support. + tf, err := r.TableFormat() + if err != nil { + return nil, err + } + if tf < fmv.MinTableFormat() || tf > fmv.MaxTableFormat() { + return nil, errors.Newf( + "pebble: table format %s is not within range supported at DB format major version %d, (%s,%s)", + tf, fmv, fmv.MinTableFormat(), fmv.MaxTableFormat(), + ) + } + + meta := &fileMetadata{} + 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 + // information to compute the stats here. + // + // This is helpful in tests for avoiding awkwardness around deletion of + // ingested files from MemFS. MemFS implements the Windows semantics of + // 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.PhysicalMeta(), &r.Properties) + + { + iter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return nil, err + } + defer iter.Close() + var smallest InternalKey + if key, _ := iter.First(); key != nil { + if err := ingestValidateKey(opts, key); err != nil { + return nil, err + } + smallest = (*key).Clone() + } + if err := iter.Error(); err != nil { + return nil, err + } + if key, _ := iter.Last(); key != nil { + if err := ingestValidateKey(opts, key); err != nil { + return nil, err + } + meta.ExtendPointKeyBounds(opts.Comparer.Compare, smallest, key.Clone()) + } + if err := iter.Error(); err != nil { + return nil, err + } + } + + iter, err := r.NewRawRangeDelIter() + if err != nil { + return nil, err + } + if iter != nil { + defer iter.Close() + var smallest InternalKey + if s := iter.First(); s != nil { + key := s.SmallestKey() + if err := ingestValidateKey(opts, &key); err != nil { + return nil, err + } + smallest = key.Clone() + } + if err := iter.Error(); err != nil { + return nil, err + } + if s := iter.Last(); s != nil { + k := s.SmallestKey() + if err := ingestValidateKey(opts, &k); err != nil { + return nil, err + } + largest := s.LargestKey().Clone() + meta.ExtendPointKeyBounds(opts.Comparer.Compare, smallest, largest) + } + } + + // Update the range-key bounds for the table. + { + iter, err := r.NewRawRangeKeyIter() + if err != nil { + return nil, err + } + if iter != nil { + defer iter.Close() + var smallest InternalKey + if s := iter.First(); s != nil { + key := s.SmallestKey() + if err := ingestValidateKey(opts, &key); err != nil { + return nil, err + } + smallest = key.Clone() + } + if err := iter.Error(); err != nil { + return nil, err + } + if s := iter.Last(); s != nil { + k := s.SmallestKey() + if err := ingestValidateKey(opts, &k); err != nil { + return nil, err + } + // As range keys are fragmented, the end key of the last range key in + // the table provides the upper bound for the table. + largest := s.LargestKey().Clone() + meta.ExtendRangeKeyBounds(opts.Comparer.Compare, smallest, largest) + } + if err := iter.Error(); err != nil { + return nil, err + } + } + } + + if !meta.HasPointKeys && !meta.HasRangeKeys { + return nil, nil + } + + // Sanity check that the various bounds on the file were set consistently. + if err := meta.Validate(opts.Comparer.Compare, opts.Comparer.FormatKey); err != nil { + return nil, err + } + + 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, + 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 { + f, err := opts.FS.Open(paths[i]) + if err != nil { + 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]) + } + } + 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 +// matching path also gets swapped to the same index. For use in +// ingestSortAndVerify. +type metaAndPaths struct { + meta []*fileMetadata + paths []string + cmp Compare +} + +func (m metaAndPaths) Len() int { + return len(m.meta) +} + +func (m metaAndPaths) Less(i, j int) bool { + return m.cmp(m.meta[i].Smallest.UserKey, m.meta[j].Smallest.UserKey) < 0 +} + +func (m metaAndPaths) Swap(i, j int) { + m.meta[i], m.meta[j] = m.meta[j], m.meta[i] + if m.paths != nil { + m.paths[i], m.paths[j] = m.paths[j], m.paths[i] + } +} + +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: lr.localMeta, + paths: lr.localPaths, + cmp: cmp, + }) + + 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]) + } + } + slices.SortFunc(filesInLevel, func(a, b *fileMetadata) int { + return cmp(a.Smallest.UserKey, b.Smallest.UserKey) + }) + 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 { + var firstErr error + for i := range meta { + if err := objProvider.Remove(fileTypeTable, meta[i].FileBacking.DiskFileNum); err != nil { + firstErr = firstError(firstErr, err) + } + } + return firstErr +} + +// ingestLink creates new objects which are backed by either hardlinks to or +// copies of the ingested files. It also attaches shared objects to the provider. +func ingestLink( + jobID int, + opts *Options, + objProvider objstorage.Provider, + lr ingestLoadResult, + shared []SharedSSTMeta, +) error { + 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, lr.localMeta[:i]); err2 != nil { + opts.Logger.Errorf("ingest cleanup failed: %v", err2) + } + return err + } + if opts.EventListener.TableCreated != nil { + opts.EventListener.TableCreated(TableCreateInfo{ + JobID: jobID, + Reason: "ingesting", + Path: objProvider.Path(objMeta), + 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, keyRanges []internalKeyRange) bool { + iter := mem.newIter(nil) + rangeDelIter := mem.newRangeDelIter(nil) + rkeyIter := mem.newRangeKeyIter(nil) + + closeIters := func() error { + err := iter.Close() + if rangeDelIter != nil { + err = firstError(err, rangeDelIter.Close()) + } + if rkeyIter != nil { + err = firstError(err, rkeyIter.Close()) + } + return err + } + + for _, kr := range keyRanges { + if overlapWithIterator(iter, &rangeDelIter, rkeyIter, kr, cmp) { + closeIters() + return true + } + } + + // Assume overlap if any iterator errored out. + return closeIters() != nil +} + +func ingestUpdateSeqNum( + 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()) + } + 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 { + m.SmallestPointKey = setSeqFn(m.SmallestPointKey) + } + if m.HasRangeKeys { + m.SmallestRangeKey = setSeqFn(m.SmallestRangeKey) + } + m.Smallest = setSeqFn(m.Smallest) + // Only update the seqnum for the largest key if that key is not an + // "exclusive sentinel" (i.e. a range deletion sentinel or a range key + // boundary), as doing so effectively drops the exclusive sentinel (by + // lowering the seqnum from the max value), and extends the bounds of the + // table. + // NB: as the largest range key is always an exclusive sentinel, it is never + // updated. + if m.HasPointKeys && !m.LargestPointKey.IsExclusiveSentinel() { + m.LargestPointKey = setSeqFn(m.LargestPointKey) + } + if !m.Largest.IsExclusiveSentinel() { + m.Largest = setSeqFn(m.Largest) + } + // Setting smallestSeqNum == largestSeqNum triggers the setting of + // Properties.GlobalSeqNum when an sstable is loaded. + m.SmallestSeqNum = seqNum + m.LargestSeqNum = seqNum + // Ensure the new bounds are consistent. + if err := m.Validate(cmp, format); err != nil { + 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, + keyRange internalKeyRange, + cmp Compare, +) bool { + // Check overlap with point operations. + // + // When using levelIter, it seeks to the SST whose boundaries + // 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 keyRange.largest(L) below + // is subtle but maintains correctness. + // 1) boundary < L, + // since boundary is also > S (initial seek), + // whatever the boundary's start key may be, we're always overlapping. + // 2) boundary > L, + // overlap with boundary cannot be determined since we don't know boundary's start key. + // We require checking for overlap with rangeDelIter. + // 3) boundary == L and L is not sentinel, + // 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(keyRange.smallest.UserKey, base.SeekGEFlagsNone) + if key != nil { + c := sstableKeyCompare(cmp, *key, keyRange.largest) + if c <= 0 { + return true + } + } + // Assume overlap if iterator errored. + if err := iter.Error(); err != nil { + return true + } + + computeOverlapWithSpans := func(rIter keyspan.FragmentIterator) bool { + // NB: The spans surfaced by the fragment iterator are non-overlapping. + span := rIter.SeekLT(keyRange.smallest.UserKey) + if span == nil { + span = rIter.Next() + } + for ; span != nil; span = rIter.Next() { + if span.Empty() { + continue + } + key := span.SmallestKey() + 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, 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". + return true + } + } + // Assume overlap if iterator errored. + if err := rIter.Error(); err != nil { + return true + } + return false + } + + // rkeyIter is either a range key level iter, or a range key iterator + // over a single file. + if rkeyIter != nil { + if computeOverlapWithSpans(rkeyIter) { + return true + } + } + + // Check overlap with range deletions. + if rangeDelIter == nil || *rangeDelIter == nil { + return false + } + 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, + comparer *Comparer, + v *version, + baseLevel int, + compactions map[*compaction]struct{}, + meta *fileMetadata, + 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 + // increasing level number) in order to reduce write amplification. + // + // There are 2 kinds of overlap we need to check for: file boundary overlap + // and data overlap. Data overlap implies file boundary overlap. Note that it + // is always possible to ingest into L0. + // + // To place meta at level i where i > 0: + // - there must not be any data overlap with levels <= i, since that will + // 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 + // after the file being considered. + // + // |--| |--| ingested file: [a,b] or [f,g] + // |-----| existing file: [c,e] + // _____________________ + // a b c d e f g + // + // In both cases the ingested file can move to considering the next level. + // + // File boundary overlap does not necessarily imply data overlap. The check + // for data overlap is a little more nuanced. Consider the following examples: + // + // 1. No data overlap: + // + // |-| |--| ingested file: [cc-d] or [ee-ff] + // |*--*--*----*------*| existing file: [a-g], points: [a, b, c, dd, g] + // _____________________ + // a b c d e f g + // + // In this case the ingested files can "fall through" this level. The checks + // continue at the next level. + // + // 2. Data overlap: + // + // |--| ingested file: [d-e] + // |*--*--*----*------*| existing file: [a-g], points: [a, b, c, dd, g] + // _____________________ + // a b c d e f g + // + // In this case the file cannot be ingested into this level as the point 'dd' + // is in the way. + // + // It is worth noting that the check for data overlap is only approximate. In + // the previous example, the ingested table [d-e] could contain only the + // points 'd' and 'e', in which case the table would be eligible for + // considering lower levels. However, such a fine-grained check would need to + // be exhaustive (comparing points and ranges in both the ingested existing + // tables) and such a check is prohibitively expensive. Thus Pebble treats any + // existing point that falls within the ingested table bounds as being "data + // overlap". + + // 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") + } + iterOps.CategoryAndQoS = sstable.CategoryAndQoS{ + Category: "pebble-ingest", + QoSLevel: sstable.LatencySensitiveQoSLevel, + } + // Check for overlap over the keys of L0 by iterating over the sublevels. + for subLevel := 0; subLevel < len(v.L0SublevelFiles); subLevel++ { + iter := newLevelIter(context.Background(), + iterOps, comparer, newIters, v.L0Sublevels.Levels[subLevel].Iter(), manifest.Level(0), internalIterOpts{}) + + 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) + + 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, nil, err + } + if overlap { + return targetLevel, nil, nil + } + } + + level := baseLevel + for ; level < numLevels; level++ { + levelIter := newLevelIter(context.Background(), + 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. + levelIter.initRangeDel(&rangeDelIter) + + rkeyLevelIter := &keyspan.LevelIter{} + rkeyLevelIter.Init( + keyspan.SpanIterOptions{}, comparer.Compare, newRangeKeyIter, + v.Levels[level].Iter(), manifest.Level(level), manifest.KeyTypeRange, + ) + + 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, nil, err + } + if overlap { + return targetLevel, splitFile, nil + } + + // Check boundary overlap. + var candidateSplitFile *fileMetadata + boundaryOverlaps := v.Overlaps(level, comparer.Compare, meta.Smallest.UserKey, + meta.Largest.UserKey, meta.Largest.IsExclusiveSentinel()) + if !boundaryOverlaps.Empty() { + // 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. 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 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 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, splitFile, nil +} + +// Ingest ingests a set of sstables into the DB. Ingestion of the files is +// atomic and semantically equivalent to creating a single batch containing all +// of the mutations in the sstables. Ingestion may require the memtable to be +// flushed. The ingested sstable files are moved into the DB and must reside on +// 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 +// platform-and-FS-agnostic way, ensure that all sstables in the input are +// properly synced to disk. Opening new file handles and Sync()-ing them +// does not always guarantee durability; see the discussion here on that: +// https://github.com/cockroachdb/pebble/pull/835#issuecomment-663075379 +// +// 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. 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 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 +// 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 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 +// memtable is forced equivalent to DB.Flush. Additionally, subsequent +// mutations that get sequence numbers larger than the ingestion sequence +// number get queued up behind the ingestion waiting for it to complete. This +// can produce a noticeable hiccup in performance. See +// https://github.com/cockroachdb/pebble/issues/25 for an idea for how to fix +// this hiccup. +func (d *DB) Ingest(paths []string) error { + if err := d.closed.Load(); err != nil { + panic(err) + } + if d.opts.ReadOnly { + return ErrReadOnly + } + _, err := d.ingest(paths, ingestTargetLevel, nil /* shared */, KeyRange{}, nil /* external */) + return err +} + +// IngestOperationStats provides some information about where in the LSM the +// bytes were ingested. +type IngestOperationStats struct { + // Bytes is the total bytes in the ingested sstables. + Bytes uint64 + // ApproxIngestedIntoL0Bytes is the approximate number of bytes ingested + // into L0. This value is approximate when flushable ingests are active and + // an ingest overlaps an entry in the flushable queue. Currently, this + // approximation is very rough, only including tables that overlapped the + // memtable. This estimate may be improved with #2112. + ApproxIngestedIntoL0Bytes uint64 + // MemtableOverlappingFiles is the count of ingested sstables + // that overlapped keys in the memtables. + 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) { + if err := d.closed.Load(); err != nil { + panic(err) + } + if d.opts.ReadOnly { + return IngestOperationStats{}, ErrReadOnly + } + 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. +func (d *DB) newIngestedFlushableEntry( + meta []*fileMetadata, seqNum uint64, logNum base.DiskFileNum, +) (*flushableEntry, error) { + // 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. + // In this case, a version edit will only be written to the manifest + // when the flushable is eventually flushed. If Pebble restarts in that + // 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, ingestLoadResult{localMeta: meta}, + ); err != nil { + return nil, err + } + + 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 + // to. + entry := d.newFlushableEntry(f, logNum, seqNum) + // The flushable entry starts off with a single reader ref, so increment + // the FileMetadata.Refs. + for _, file := range f.files { + file.Ref() + } + entry.unrefFiles = func() []*fileBacking { + var obsolete []*fileBacking + for _, file := range f.files { + if file.Unref() == 0 { + obsolete = append(obsolete, file.FileMetadata.FileBacking) + } + } + return obsolete + } + + entry.flushForced = true + entry.releaseMemAccounting = func() {} + return entry, nil +} + +// Both DB.mu and commitPipeline.mu must be held while this is called. Since +// we're holding both locks, the order in which we rotate the memtable or +// recycle the WAL in this function is irrelevant as long as the correct log +// numbers are assigned to the appropriate flushable. +func (d *DB) handleIngestAsFlushable(meta []*fileMetadata, seqNum uint64) error { + b := d.NewBatch() + for _, m := range meta { + b.ingestSST(m.FileNum) + } + b.setSeqNum(seqNum) + + // If the WAL is disabled, then the logNum used to create the flushable + // entry doesn't matter. We just use the logNum assigned to the current + // mutable memtable. If the WAL is enabled, then this logNum will be + // overwritten by the logNum of the log which will contain the log entry + // for the ingestedFlushable. + logNum := d.mu.mem.queue[len(d.mu.mem.queue)-1].logNum + if !d.opts.DisableWAL { + // We create a new WAL for the flushable instead of reusing the end of + // the previous WAL. This simplifies the increment of the minimum + // unflushed log number, and also simplifies WAL replay. + logNum, _ = d.recycleWAL() + d.mu.Unlock() + err := d.commit.directWrite(b) + if err != nil { + d.opts.Logger.Fatalf("%v", err) + } + d.mu.Lock() + } + + entry, err := d.newIngestedFlushableEntry(meta, seqNum, logNum) + if err != nil { + return err + } + nextSeqNum := seqNum + uint64(b.Count()) + + // Set newLogNum to the logNum of the previous flushable. This value is + // irrelevant if the WAL is disabled. If the WAL is enabled, then we set + // the appropriate value below. + newLogNum := d.mu.mem.queue[len(d.mu.mem.queue)-1].logNum + if !d.opts.DisableWAL { + // This is WAL num of the next mutable memtable which comes after the + // ingestedFlushable in the flushable queue. The mutable memtable + // will be created below. + newLogNum, _ = d.recycleWAL() + if err != nil { + return err + } + } + + currMem := d.mu.mem.mutable + // NB: Placing ingested sstables above the current memtables + // requires rotating of the existing memtables/WAL. There is + // some concern of churning through tiny memtables due to + // ingested sstables being placed on top of them, but those + // memtables would have to be flushed anyways. + d.mu.mem.queue = append(d.mu.mem.queue, entry) + d.rotateMemtable(newLogNum, nextSeqNum, currMem) + d.updateReadStateLocked(d.opts.DebugCheck) + d.maybeScheduleFlush() + return nil +} + +// See comment at Ingest() for details on how this works. +func (d *DB) ingest( + 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([]base.DiskFileNum, len(paths)+len(shared)+len(external)) + for i := 0; i < len(paths)+len(shared)+len(external); i++ { + pendingOutputs[i] = d.mu.versions.getNextDiskFileNum() + } + + jobID := d.mu.nextJobID + d.mu.nextJobID++ + d.mu.Unlock() + + // Load the metadata for all the files being ingested. This step detects + // and elides empty sstables. + loadResult, err := ingestLoad(d.opts, d.FormatMajorVersion(), paths, shared, external, d.cacheID, pendingOutputs, d.objProvider, jobID) + if err != nil { + return IngestOperationStats{}, err + } + + 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, loadResult, exciseSpan); err != nil { + return IngestOperationStats{}, err + } + + // Hard link the sstables into the DB directory. Since the sstables aren't + // referenced by a version, they won't be used. If the hard linking fails + // (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, 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. + if err := d.objProvider.Sync(); err != nil { + return IngestOperationStats{}, err + } + + // 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, loadResult.fileCount) + var mem *flushableEntry + var mut *memTable + // asFlushable indicates whether the sstable was ingested as a flushable. + var asFlushable bool + iterOps := IterOptions{ + CategoryAndQoS: sstable.CategoryAndQoS{ + Category: "pebble-ingest", + QoSLevel: sstable.LatencySensitiveQoSLevel, + }, + } + prepare := func(seqNum uint64) { + // Note that d.commit.mu is held by commitPipeline when calling prepare. + + d.mu.Lock() + defer d.mu.Unlock() + + // Check to see if any files overlap with any of the memtables. The queue + // is ordered from oldest to newest with the mutable memtable being the + // last element in the slice. We want to wait for the newest table that + // overlaps. + + for i := len(d.mu.mem.queue) - 1; i >= 0; i-- { + m := d.mu.mem.queue[i] + iter := m.newIter(&iterOps) + rangeDelIter := m.newRangeDelIter(&iterOps) + rkeyIter := m.newRangeKeyIter(&iterOps) + + checkForOverlap := func(i int, meta *fileMetadata) { + if metaFlushableOverlaps[i] { + // This table already overlapped a more recent flushable. + return + } + 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. + if mem == nil { + mem = m + } + 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()) + } + if rkeyIter != nil { + err = firstError(err, rkeyIter.Close()) + } + if err != nil { + d.opts.Logger.Errorf("ingest error reading flushable for log %s: %s", m.logNum, err) + } + } + + if mem == nil { + // No overlap with any of the queued flushables, so no need to queue + // after them. + + // New writes with higher sequence numbers may be concurrently + // committed. We must ensure they don't flush before this ingest + // completes. To do that, we ref the mutable memtable as a writer, + // preventing its flushing (and the flushing of all subsequent + // flushables in the queue). Once we've acquired the manifest lock + // to add the ingested sstables to the LSM, we can unref as we're + // guaranteed that the flush won't edit the LSM before this ingest. + mut = d.mu.mem.mutable + mut.writerRef() + return + } + // 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) + } + // New writes with higher sequence numbers may be concurrently + // committed. We must ensure they don't flush before this ingest + // completes. To do that, we ref the mutable memtable as a writer, + // preventing its flushing (and the flushing of all subsequent + // flushables in the queue). Once we've acquired the manifest lock + // to add the ingested sstables to the LSM, we can unref as we're + // guaranteed that the flush won't edit the LSM before this ingest. + mut = d.mu.mem.mutable + mut.writerRef() + mem.flushForced = true + d.maybeScheduleFlush() + return + } + // 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(loadResult.localMeta, seqNum) + } + + var ve *versionEdit + apply := func(seqNum uint64) { + if err != nil || asFlushable { + // An error occurred during prepare. + if mut != nil { + if mut.writerUnref() { + d.mu.Lock() + d.maybeScheduleFlush() + d.mu.Unlock() + } + } + return + } + + // 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, loadResult, + ); err != nil { + if mut != nil { + if mut.writerUnref() { + d.mu.Lock() + d.maybeScheduleFlush() + d.mu.Unlock() + } + } + return + } + + // If we overlapped with a memtable in prepare wait for the flush to + // finish. + if mem != nil { + <-mem.flushed + } + + // Assign the sstables to the correct level in the LSM and apply the + // version edit. + ve, err = d.ingestApply(jobID, loadResult, targetLevelFunc, mut, exciseSpan) + } + + // 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, loadResult.localMeta); err2 != nil { + d.opts.Logger.Errorf("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 loadResult.localPaths { + if err2 := d.opts.FS.Remove(path); err2 != nil { + d.opts.Logger.Errorf("ingest failed to remove original file: %s", err2) + } + } + } + + info := TableIngestInfo{ + 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 { + info.Tables = make([]struct { + TableInfo + Level int + }, len(ve.NewFiles)) + for i := range ve.NewFiles { + e := &ve.NewFiles[i] + info.Tables[i].Level = e.Level + info.Tables[i].TableInfo = e.Meta.TableInfo() + stats.Bytes += e.Meta.Size + if e.Level == 0 { + stats.ApproxIngestedIntoL0Bytes += e.Meta.Size + } + 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(loadResult.localMeta)) + for i, f := range loadResult.localMeta { + info.Tables[i].Level = -1 + info.Tables[i].TableInfo = f.TableInfo() + stats.Bytes += f.Size + // We don't have exact stats on which files will be ingested into + // L0, because actual ingestion into the LSM has been deferred until + // flush time. Instead, we infer based on memtable overlap. + // + // TODO(jackson): If we optimistically compute data overlap (#2112) + // before entering the commit pipeline, we can use that overlap to + // improve our approximation by incorporating overlap with L0, not + // just memtables. + if metaFlushableOverlaps[i] { + stats.ApproxIngestedIntoL0Bytes += f.Size + stats.MemtableOverlappingFiles++ + } + } + } + d.opts.EventListener.TableIngested(info) + + 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{ + CategoryAndQoS: sstable.CategoryAndQoS{ + Category: "pebble-ingest", + QoSLevel: sstable.LatencySensitiveQoSLevel, + }, + 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) + 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{ + CategoryAndQoS: sstable.CategoryAndQoS{ + Category: "pebble-ingest", + QoSLevel: sstable.LatencySensitiveQoSLevel, + }, + 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) + 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, + comparer *Comparer, + v *version, + baseLevel int, + compactions map[*compaction]struct{}, + meta *fileMetadata, + 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, + lr ingestLoadResult, + findTargetLevel ingestTargetLevelFunc, + mut *memTable, + exciseSpan KeyRange, +) (*versionEdit, error) { + d.mu.Lock() + defer d.mu.Unlock() + + ve := &versionEdit{ + 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) + + // Lock the manifest for writing before we use the current version to + // determine the target level. This prevents two concurrent ingestion jobs + // from using the same version to determine the target level, and also + // provides serialization with concurrent compaction and flush jobs. + // logAndApply unconditionally releases the manifest lock, but any earlier + // returns must unlock the manifest. + d.mu.versions.logLock() + + 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. 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} + // 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. + 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 + 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 + } + f.Meta = m + levelMetrics := metrics[f.Level] + if levelMetrics == nil { + levelMetrics = &LevelMetrics{} + metrics[f.Level] = levelMetrics + } + levelMetrics.NumFiles++ + levelMetrics.Size += int64(m.Size) + 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) + // 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() + 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(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. + if !d.opts.Experimental.ValidateOnIngest { + return + } + + d.mu.tableValidation.pending = append(d.mu.tableValidation.pending, newFiles...) + if d.shouldValidateSSTablesLocked() { + go d.validateSSTables() + } +} + +// shouldValidateSSTablesLocked returns true if SSTable validation should run. +// DB.mu must be locked when calling. +func (d *DB) shouldValidateSSTablesLocked() bool { + return !d.mu.tableValidation.validating && + d.closed.Load() == nil && + d.opts.Experimental.ValidateOnIngest && + len(d.mu.tableValidation.pending) > 0 +} + +// validateSSTables runs a round of validation on the tables in the pending +// queue. +func (d *DB) validateSSTables() { + d.mu.Lock() + if !d.shouldValidateSSTablesLocked() { + d.mu.Unlock() + return + } + + pending := d.mu.tableValidation.pending + d.mu.tableValidation.pending = nil + d.mu.tableValidation.validating = true + jobID := d.mu.nextJobID + d.mu.nextJobID++ + rs := d.loadReadState() + + // Drop DB.mu before performing IO. + d.mu.Unlock() + + // Validate all tables in the pending queue. This could lead to a situation + // where we are starving IO from other tasks due to having to page through + // all the blocks in all the sstables in the queue. + // TODO(travers): Add some form of pacing to avoid IO starvation. + + // If we fail to validate any files due to reasons other than uncovered + // corruption, accumulate them and re-queue them for another attempt. + var retry []manifest.NewFileEntry + + for _, f := range pending { + // The file may have been moved or deleted since it was ingested, in + // which case we skip. + if !rs.current.Contains(f.Level, d.cmp, f.Meta) { + // Assume the file was moved to a lower level. It is rare enough + // that a table is moved or deleted between the time it was ingested + // and the time the validation routine runs that the overall cost of + // this inner loop is tolerably low, when amortized over all + // ingested tables. + found := false + for i := f.Level + 1; i < numLevels; i++ { + if rs.current.Contains(i, d.cmp, f.Meta) { + found = true + break + } + } + if !found { + continue + } + } + + 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 { + if IsCorruptionError(err) { + // TODO(travers): Hook into the corruption reporting pipeline, once + // available. See pebble#1192. + d.opts.Logger.Fatalf("pebble: encountered corruption during ingestion: %s", err) + } else { + // If there was some other, possibly transient, error that + // caused table validation to fail inform the EventListener and + // move on. We remember the table so that we can retry it in a + // subsequent table validation job. + // + // TODO(jackson): If the error is not transient, this will retry + // validation indefinitely. While not great, it's the same + // behavior as erroring flushes and compactions. We should + // address this as a part of #270. + d.opts.EventListener.BackgroundError(err) + retry = append(retry, f) + continue + } + } + + d.opts.EventListener.TableValidated(TableValidatedInfo{ + JobID: jobID, + Meta: f.Meta, + }) + } + rs.unref() + d.mu.Lock() + defer d.mu.Unlock() + d.mu.tableValidation.pending = append(d.mu.tableValidation.pending, retry...) + d.mu.tableValidation.validating = false + d.mu.tableValidation.cond.Broadcast() + if d.shouldValidateSSTablesLocked() { + go d.validateSSTables() + } +} diff --git a/pebble/ingest_test.go b/pebble/ingest_test.go new file mode 100644 index 0000000..c4dcc2a --- /dev/null +++ b/pebble/ingest_test.go @@ -0,0 +1,3516 @@ +// Copyright 2018 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 ( + "bytes" + "context" + "fmt" + "io" + "math" + "os" + "path/filepath" + "slices" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/kr/pretty" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func TestSSTableKeyCompare(t *testing.T) { + var buf bytes.Buffer + datadriven.RunTest(t, "testdata/sstable_key_compare", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "cmp": + buf.Reset() + for _, line := range strings.Split(td.Input, "\n") { + fields := strings.Fields(line) + a := base.ParseInternalKey(fields[0]) + b := base.ParseInternalKey(fields[1]) + got := sstableKeyCompare(testkeys.Comparer.Compare, a, b) + fmt.Fprintf(&buf, "%38s", fmt.Sprint(a.Pretty(base.DefaultFormatter))) + switch got { + case -1: + fmt.Fprint(&buf, " < ") + case +1: + fmt.Fprint(&buf, " > ") + case 0: + fmt.Fprint(&buf, " = ") + } + fmt.Fprintf(&buf, "%s\n", fmt.Sprint(b.Pretty(base.DefaultFormatter))) + } + return buf.String() + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +func TestIngestLoad(t *testing.T) { + mem := vfs.NewMem() + + datadriven.RunTest(t, "testdata/ingest_load", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "load": + writerOpts := sstable.WriterOptions{} + var dbVersion FormatMajorVersion + for _, cmdArgs := range td.CmdArgs { + v, err := strconv.Atoi(cmdArgs.Vals[0]) + if err != nil { + return err.Error() + } + switch k := cmdArgs.Key; k { + case "writer-version": + fmv := FormatMajorVersion(v) + writerOpts.TableFormat = fmv.MaxTableFormat() + case "db-version": + dbVersion = FormatMajorVersion(v) + default: + return fmt.Sprintf("unknown cmd %s\n", k) + } + } + f, err := mem.Create("ext") + if err != nil { + return err.Error() + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), writerOpts) + for _, data := range strings.Split(td.Input, "\n") { + if strings.HasPrefix(data, "rangekey: ") { + data = strings.TrimPrefix(data, "rangekey: ") + s := keyspan.ParseSpan(data) + err := rangekey.Encode(&s, w.AddRangeKey) + if err != nil { + return err.Error() + } + continue + } + + j := strings.Index(data, ":") + if j < 0 { + return fmt.Sprintf("malformed input: %s\n", data) + } + key := base.ParseInternalKey(data[:j]) + value := []byte(data[j+1:]) + if err := w.Add(key, value); err != nil { + return err.Error() + } + } + if err := w.Close(); err != nil { + return err.Error() + } + + opts := (&Options{ + Comparer: DefaultComparer, + FS: mem, + }).WithFSDefaults() + lr, err := ingestLoad(opts, dbVersion, []string{"ext"}, nil, nil, 0, []base.DiskFileNum{base.FileNum(1).DiskFileNum()}, nil, 0) + if err != nil { + return err.Error() + } + var buf bytes.Buffer + for _, m := range lr.localMeta { + fmt.Fprintf(&buf, "%d: %s-%s\n", m.FileNum, m.Smallest, m.Largest) + fmt.Fprintf(&buf, " points: %s-%s\n", m.SmallestPointKey, m.LargestPointKey) + fmt.Fprintf(&buf, " ranges: %s-%s\n", m.SmallestRangeKey, m.LargestRangeKey) + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestIngestLoadRand(t *testing.T) { + mem := vfs.NewMem() + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + cmp := DefaultComparer.Compare + version := internalFormatNewest + + randBytes := func(size int) []byte { + data := make([]byte, size) + for i := range data { + data[i] = byte(rng.Int() & 0xff) + } + return data + } + + paths := make([]string, 1+rng.Intn(10)) + pending := make([]base.DiskFileNum, len(paths)) + expected := make([]*fileMetadata, len(paths)) + for i := range paths { + paths[i] = fmt.Sprint(i) + pending[i] = base.FileNum(rng.Uint64()).DiskFileNum() + expected[i] = &fileMetadata{ + FileNum: pending[i].FileNum(), + } + expected[i].StatsMarkValid() + + func() { + f, err := mem.Create(paths[i]) + require.NoError(t, err) + + keys := make([]InternalKey, 1+rng.Intn(100)) + for i := range keys { + keys[i] = base.MakeInternalKey( + randBytes(1+rng.Intn(10)), + 0, + InternalKeyKindSet) + } + slices.SortFunc(keys, func(a, b base.InternalKey) int { + return base.InternalCompare(cmp, a, b) + }) + + expected[i].ExtendPointKeyBounds(cmp, keys[0], keys[len(keys)-1]) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: version.MaxTableFormat(), + }) + var count uint64 + for i := range keys { + if i > 0 && base.InternalCompare(cmp, keys[i-1], keys[i]) == 0 { + // Duplicate key, ignore. + continue + } + w.Add(keys[i], nil) + count++ + } + expected[i].Stats.NumEntries = count + require.NoError(t, w.Close()) + + meta, err := w.Metadata() + require.NoError(t, err) + + expected[i].Size = meta.Size + expected[i].InitPhysicalBacking() + }() + } + + opts := (&Options{ + Comparer: DefaultComparer, + FS: mem, + }).WithFSDefaults() + lr, err := ingestLoad(opts, version, paths, nil, nil, 0, pending, nil, 0) + require.NoError(t, err) + + for _, m := range lr.localMeta { + m.CreationTime = 0 + } + t.Log(strings.Join(pretty.Diff(expected, lr.localMeta), "\n")) + require.Equal(t, expected, lr.localMeta) +} + +func TestIngestLoadInvalid(t *testing.T) { + mem := vfs.NewMem() + f, err := mem.Create("invalid") + require.NoError(t, err) + require.NoError(t, f.Close()) + + opts := (&Options{ + Comparer: DefaultComparer, + FS: mem, + }).WithFSDefaults() + if _, err := ingestLoad(opts, internalFormatNewest, []string{"invalid"}, nil, nil, 0, []base.DiskFileNum{base.FileNum(1).DiskFileNum()}, nil, 0); err == nil { + t.Fatalf("expected error, but found success") + } +} + +func TestIngestSortAndVerify(t *testing.T) { + comparers := map[string]Compare{ + "default": DefaultComparer.Compare, + "reverse": func(a, b []byte) int { + return DefaultComparer.Compare(b, a) + }, + } + + t.Run("", func(t *testing.T) { + datadriven.RunTest(t, "testdata/ingest_sort_and_verify", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "ingest": + var buf bytes.Buffer + var meta []*fileMetadata + var paths []string + var cmpName string + d.ScanArgs(t, "cmp", &cmpName) + cmp := comparers[cmpName] + if cmp == nil { + return fmt.Sprintf("%s unknown comparer: %s", d.Cmd, cmpName) + } + for i, data := range strings.Split(d.Input, "\n") { + parts := strings.Split(data, "-") + if len(parts) != 2 { + return fmt.Sprintf("malformed test case: %s", d.Input) + } + smallest := base.ParseInternalKey(parts[0]) + largest := base.ParseInternalKey(parts[1]) + if cmp(smallest.UserKey, largest.UserKey) > 0 { + return fmt.Sprintf("range %v-%v is not valid", smallest, largest) + } + m := (&fileMetadata{}).ExtendPointKeyBounds(cmp, smallest, largest) + m.InitPhysicalBacking() + meta = append(meta, m) + paths = append(paths, strconv.Itoa(i)) + } + lr := ingestLoadResult{localPaths: paths, localMeta: meta} + err := ingestSortAndVerify(cmp, lr, KeyRange{}) + if err != nil { + return fmt.Sprintf("%v\n", err) + } + for i := range meta { + fmt.Fprintf(&buf, "%s: %v-%v\n", paths[i], meta[i].Smallest, meta[i].Largest) + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) + }) +} + +func TestIngestLink(t *testing.T) { + // Test linking of tables into the DB directory. Test cleanup when one of the + // tables cannot be linked. + + const dir = "db" + const count = 10 + for i := 0; i <= count; i++ { + t.Run("", func(t *testing.T) { + opts := &Options{FS: vfs.NewMem()} + opts.EnsureDefaults().WithFSDefaults() + require.NoError(t, opts.FS.MkdirAll(dir, 0755)) + objProvider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(opts.FS, dir)) + require.NoError(t, err) + defer objProvider.Close() + + paths := make([]string, 10) + meta := make([]*fileMetadata, len(paths)) + contents := make([][]byte, len(paths)) + for j := range paths { + paths[j] = fmt.Sprintf("external%d", j) + meta[j] = &fileMetadata{} + meta[j].FileNum = FileNum(j) + meta[j].InitPhysicalBacking() + f, err := opts.FS.Create(paths[j]) + require.NoError(t, err) + + contents[j] = []byte(fmt.Sprintf("data%d", j)) + // memFile.Write will modify the supplied buffer when invariants are + // enabled, so provide a throw-away copy. + _, err = f.Write(append([]byte(nil), contents[j]...)) + require.NoError(t, err) + require.NoError(t, f.Close()) + } + + if i < count { + opts.FS.Remove(paths[i]) + } + + lr := ingestLoadResult{localMeta: meta, localPaths: paths} + err = ingestLink(0 /* jobID */, opts, objProvider, lr, nil /* shared */) + if i < count { + if err == nil { + t.Fatalf("expected error, but found success") + } + } else { + require.NoError(t, err) + } + + files, err := opts.FS.List(dir) + require.NoError(t, err) + + sort.Strings(files) + + if i < count { + if len(files) > 0 { + t.Fatalf("expected all of the files to be cleaned up, but found:\n%s", + strings.Join(files, "\n")) + } + } else { + if len(files) != count { + t.Fatalf("expected %d files, but found:\n%s", count, strings.Join(files, "\n")) + } + for j := range files { + ftype, fileNum, ok := base.ParseFilename(opts.FS, files[j]) + if !ok { + t.Fatalf("unable to parse filename: %s", files[j]) + } + if fileTypeTable != ftype { + t.Fatalf("expected table, but found %d", ftype) + } + if j != int(fileNum.FileNum()) { + t.Fatalf("expected table %d, but found %d", j, fileNum) + } + f, err := opts.FS.Open(opts.FS.PathJoin(dir, files[j])) + require.NoError(t, err) + + data, err := io.ReadAll(f) + require.NoError(t, err) + require.NoError(t, f.Close()) + if !bytes.Equal(contents[j], data) { + t.Fatalf("expected %s, but found %s", contents[j], data) + } + } + } + }) + } +} + +func TestIngestLinkFallback(t *testing.T) { + // Verify that ingestLink succeeds if linking fails by falling back to + // copying. + mem := vfs.NewMem() + src, err := mem.Create("source") + require.NoError(t, err) + + opts := &Options{FS: errorfs.Wrap(mem, errorfs.ErrInjected.If(errorfs.OnIndex(1)))} + opts.EnsureDefaults().WithFSDefaults() + objSettings := objstorageprovider.DefaultSettings(opts.FS, "") + // Prevent the provider from listing the dir (where we may get an injected error). + objSettings.FSDirInitialListing = []string{} + objProvider, err := objstorageprovider.Open(objSettings) + require.NoError(t, err) + defer objProvider.Close() + + meta := []*fileMetadata{{FileNum: 1}} + meta[0].InitPhysicalBacking() + lr := ingestLoadResult{localMeta: meta, localPaths: []string{"source"}} + err = ingestLink(0, opts, objProvider, lr, nil /* shared */) + require.NoError(t, err) + + dest, err := mem.Open("000001.sst") + require.NoError(t, err) + + // We should be able to write bytes to src, and not have them show up in + // dest. + _, _ = src.Write([]byte("test")) + data, err := io.ReadAll(dest) + require.NoError(t, err) + if len(data) != 0 { + t.Fatalf("expected copy, but files appear to be hard linked: [%s] unexpectedly found", data) + } +} + +func TestOverlappingIngestedSSTs(t *testing.T) { + dir := "" + var ( + mem vfs.FS + d *DB + opts *Options + closed = false + blockFlush = false + ) + defer func() { + if !closed { + require.NoError(t, d.Close()) + } + }() + + reset := func(strictMem bool) { + if d != nil && !closed { + require.NoError(t, d.Close()) + } + blockFlush = false + + if strictMem { + mem = vfs.NewStrictMem() + } else { + mem = vfs.NewMem() + } + + require.NoError(t, mem.MkdirAll("ext", 0755)) + opts = (&Options{ + FS: mem, + MemTableStopWritesThreshold: 4, + L0CompactionThreshold: 100, + L0StopWritesThreshold: 100, + DebugCheck: DebugCheckLevels, + FormatMajorVersion: internalFormatNewest, + }).WithFSDefaults() + // Disable automatic compactions because otherwise we'll race with + // delete-only compactions triggered by ingesting range tombstones. + opts.DisableAutomaticCompactions = true + + var err error + d, err = Open(dir, opts) + require.NoError(t, err) + d.TestOnlyWaitForCleaning() + } + waitForFlush := func() { + if d == nil { + return + } + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + } + reset(false) + + datadriven.RunTest(t, "testdata/flushable_ingest", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + reset(td.HasArg("strictMem")) + return "" + + case "ignoreSyncs": + var ignoreSyncs bool + if len(td.CmdArgs) == 1 && td.CmdArgs[0].String() == "true" { + ignoreSyncs = true + } + mem.(*vfs.MemFS).SetIgnoreSyncs(ignoreSyncs) + return "" + + case "resetToSynced": + mem.(*vfs.MemFS).ResetToSyncedState() + files, err := mem.List(dir) + sort.Strings(files) + require.NoError(t, err) + return strings.Join(files, "\n") + + case "batch": + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + + case "build": + if err := runBuildCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + + case "ingest": + if err := runIngestCmd(td, d, mem); err != nil { + return err.Error() + } + if !blockFlush { + waitForFlush() + } + return "" + + case "iter": + iter, _ := d.NewIter(nil) + return runIterCmd(td, iter, true) + + case "lsm": + return runLSMCmd(td, d) + + case "close": + if closed { + return "already closed" + } + require.NoError(t, d.Close()) + closed = true + return "" + + case "ls": + files, err := mem.List(dir) + sort.Strings(files) + require.NoError(t, err) + return strings.Join(files, "\n") + + case "open": + opts.ReadOnly = td.HasArg("readOnly") + var err error + d, err = Open(dir, opts) + closed = false + require.NoError(t, err) + waitForFlush() + d.TestOnlyWaitForCleaning() + return "" + + case "blockFlush": + blockFlush = true + d.mu.Lock() + d.mu.compact.flushing = true + d.mu.Unlock() + return "" + + case "allowFlush": + blockFlush = false + d.mu.Lock() + d.mu.compact.flushing = false + d.mu.Unlock() + return "" + + case "flush": + d.maybeScheduleFlush() + waitForFlush() + d.TestOnlyWaitForCleaning() + return "" + + case "get": + return runGetCmd(t, td, d) + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestExcise(t *testing.T) { + var mem vfs.FS + var d *DB + var flushed bool + defer func() { + require.NoError(t, d.Close()) + }() + + var opts *Options + reset := func() { + if d != nil { + require.NoError(t, d.Close()) + } + + mem = vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + opts = &Options{ + FS: mem, + L0CompactionThreshold: 100, + L0StopWritesThreshold: 100, + DebugCheck: DebugCheckLevels, + EventListener: &EventListener{FlushEnd: func(info FlushInfo) { + flushed = true + }}, + FormatMajorVersion: FormatVirtualSSTables, + Comparer: testkeys.Comparer, + } + // Disable automatic compactions because otherwise we'll race with + // delete-only compactions triggered by ingesting range tombstones. + opts.DisableAutomaticCompactions = true + // Set this to true to add some testing for the virtual sstable validation + // code paths. + opts.Experimental.ValidateOnIngest = true + + var err error + d, err = Open("", opts) + require.NoError(t, err) + } + reset() + + datadriven.RunTest(t, "testdata/excise", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + reset() + return "" + case "reopen": + require.NoError(t, d.Close()) + var err error + d, err = Open("", opts) + require.NoError(t, err) + + return "" + case "batch": + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + case "build": + if err := runBuildCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + return "" + + case "ingest": + flushed = false + if err := runIngestCmd(td, d, mem); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + if flushed { + return "memtable flushed" + } + return "" + + case "ingest-and-excise": + flushed = false + if err := runIngestAndExciseCmd(td, d, mem); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + if flushed { + return "memtable flushed" + } + return "" + + case "get": + return runGetCmd(t, td, d) + + case "iter": + iter, _ := d.NewIter(&IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + }) + return runIterCmd(td, iter, true) + + case "lsm": + return runLSMCmd(td, d) + + case "metrics": + // The asynchronous loading of table stats can change metrics, so + // wait for all the tables' stats to be loaded. + d.mu.Lock() + d.waitTableStats() + d.mu.Unlock() + + return d.Metrics().StringForTests() + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "excise": + ve := &versionEdit{ + DeletedFiles: map[deletedFileEntry]*fileMetadata{}, + } + var exciseSpan KeyRange + if len(td.CmdArgs) != 2 { + panic("insufficient args for compact command") + } + exciseSpan.Start = []byte(td.CmdArgs[0].Key) + exciseSpan.End = []byte(td.CmdArgs[1].Key) + + d.mu.Lock() + d.mu.versions.logLock() + d.mu.Unlock() + current := d.mu.versions.currentVersion() + for level := range current.Levels { + iter := current.Levels[level].Iter() + for m := iter.SeekGE(d.cmp, exciseSpan.Start); m != nil && d.cmp(m.Smallest.UserKey, exciseSpan.End) < 0; m = iter.Next() { + _, err := d.excise(exciseSpan, m, ve, level) + if err != nil { + d.mu.Lock() + d.mu.versions.logUnlock() + d.mu.Unlock() + return fmt.Sprintf("error when excising %s: %s", m.FileNum, err.Error()) + } + } + } + d.mu.Lock() + d.mu.versions.logUnlock() + d.mu.Unlock() + return fmt.Sprintf("would excise %d files, use ingest-and-excise to excise.\n%s", len(ve.DeletedFiles), ve.DebugString(base.DefaultFormatter)) + + case "confirm-backing": + // Confirms that the files have the same FileBacking. + fileNums := make(map[base.FileNum]struct{}) + for i := range td.CmdArgs { + fNum, err := strconv.Atoi(td.CmdArgs[i].Key) + if err != nil { + panic("invalid file number") + } + fileNums[base.FileNum(fNum)] = struct{}{} + } + d.mu.Lock() + currVersion := d.mu.versions.currentVersion() + var ptr *manifest.FileBacking + for _, level := range currVersion.Levels { + lIter := level.Iter() + for f := lIter.First(); f != nil; f = lIter.Next() { + if _, ok := fileNums[f.FileNum]; ok { + if ptr == nil { + ptr = f.FileBacking + continue + } + if f.FileBacking != ptr { + d.mu.Unlock() + return "file backings are not the same" + } + } + } + } + d.mu.Unlock() + return "file backings are the same" + case "compact": + if len(td.CmdArgs) != 2 { + panic("insufficient args for compact command") + } + l := td.CmdArgs[0].Key + r := td.CmdArgs[1].Key + err := d.Compact([]byte(l), []byte(r), false) + if err != nil { + return err.Error() + } + return "" + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func testIngestSharedImpl( + t *testing.T, createOnShared remote.CreateOnSharedStrategy, fileName string, +) { + var d, d1, d2 *DB + var efos map[string]*EventuallyFileOnlySnapshot + defer func() { + for _, e := range efos { + require.NoError(t, e.Close()) + } + if d1 != nil { + require.NoError(t, d1.Close()) + } + if d2 != nil { + require.NoError(t, d2.Close()) + } + }() + creatorIDCounter := uint64(1) + replicateCounter := 1 + var opts1, opts2 *Options + + reset := func() { + for _, e := range efos { + require.NoError(t, e.Close()) + } + if d1 != nil { + require.NoError(t, d1.Close()) + } + if d2 != nil { + require.NoError(t, d2.Close()) + } + efos = make(map[string]*EventuallyFileOnlySnapshot) + + sstorage := remote.NewInMem() + mem1 := vfs.NewMem() + mem2 := vfs.NewMem() + require.NoError(t, mem1.MkdirAll("ext", 0755)) + require.NoError(t, mem2.MkdirAll("ext", 0755)) + opts1 = &Options{ + Comparer: testkeys.Comparer, + FS: mem1, + LBaseMaxBytes: 1, + L0CompactionThreshold: 100, + L0StopWritesThreshold: 100, + DebugCheck: DebugCheckLevels, + FormatMajorVersion: FormatVirtualSSTables, + } + // lel. + lel := MakeLoggingEventListener(DefaultLogger) + opts1.EventListener = &lel + opts1.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": sstorage, + }) + opts1.Experimental.CreateOnShared = createOnShared + opts1.Experimental.CreateOnSharedLocator = "" + // Disable automatic compactions because otherwise we'll race with + // delete-only compactions triggered by ingesting range tombstones. + opts1.DisableAutomaticCompactions = true + + opts2 = &Options{} + *opts2 = *opts1 + opts2.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": sstorage, + }) + opts2.Experimental.CreateOnShared = createOnShared + opts2.Experimental.CreateOnSharedLocator = "" + opts2.FS = mem2 + + var err error + d1, err = Open("", opts1) + require.NoError(t, err) + require.NoError(t, d1.SetCreatorID(creatorIDCounter)) + creatorIDCounter++ + d2, err = Open("", opts2) + require.NoError(t, err) + require.NoError(t, d2.SetCreatorID(creatorIDCounter)) + creatorIDCounter++ + d = d1 + } + reset() + + datadriven.RunTest(t, fmt.Sprintf("testdata/%s", fileName), func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "restart": + for _, e := range efos { + require.NoError(t, e.Close()) + } + if d1 != nil { + require.NoError(t, d1.Close()) + } + if d2 != nil { + require.NoError(t, d2.Close()) + } + + var err error + d1, err = Open("", opts1) + if err != nil { + return err.Error() + } + d2, err = Open("", opts2) + if err != nil { + return err.Error() + } + d = d1 + return "ok, note that the active db has been set to 1 (use 'switch' to change)" + case "reset": + reset() + return "" + case "switch": + if len(td.CmdArgs) != 1 { + return "usage: switch <1 or 2>" + } + switch td.CmdArgs[0].Key { + case "1": + d = d1 + case "2": + d = d2 + default: + return "usage: switch <1 or 2>" + } + return "ok" + case "batch": + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + case "build": + if err := runBuildCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + return "" + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + return "" + + case "ingest": + if err := runIngestCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + return "" + + case "ingest-and-excise": + if err := runIngestAndExciseCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + return "" + + case "replicate": + if len(td.CmdArgs) != 4 { + return "usage: replicate " + } + var from, to *DB + switch td.CmdArgs[0].Key { + case "1": + from = d1 + case "2": + from = d2 + default: + return "usage: replicate " + } + switch td.CmdArgs[1].Key { + case "1": + to = d1 + case "2": + to = d2 + default: + return "usage: replicate " + } + startKey := []byte(td.CmdArgs[2].Key) + endKey := []byte(td.CmdArgs[3].Key) + + writeOpts := d.opts.MakeWriterOptions(0 /* level */, to.opts.FormatMajorVersion.MaxTableFormat()) + sstPath := fmt.Sprintf("ext/replicate%d.sst", replicateCounter) + f, err := to.opts.FS.Create(sstPath) + require.NoError(t, err) + replicateCounter++ + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), writeOpts) + + var sharedSSTs []SharedSSTMeta + err = from.ScanInternal(context.TODO(), sstable.CategoryAndQoS{}, startKey, endKey, + func(key *InternalKey, value LazyValue, _ IteratorLevel) error { + val, _, err := value.Value(nil) + require.NoError(t, err) + require.NoError(t, w.Add(base.MakeInternalKey(key.UserKey, 0, key.Kind()), val)) + return nil + }, + func(start, end []byte, seqNum uint64) error { + require.NoError(t, w.DeleteRange(start, end)) + return nil + }, + func(start, end []byte, keys []keyspan.Key) error { + s := keyspan.Span{ + Start: start, + End: end, + Keys: keys, + KeysOrder: 0, + } + require.NoError(t, rangekey.Encode(&s, func(k base.InternalKey, v []byte) error { + return w.AddRangeKey(base.MakeInternalKey(k.UserKey, 0, k.Kind()), v) + })) + return nil + }, + func(sst *SharedSSTMeta) error { + sharedSSTs = append(sharedSSTs, *sst) + return nil + }, + ) + require.NoError(t, err) + require.NoError(t, w.Close()) + + _, err = to.IngestAndExcise([]string{sstPath}, sharedSSTs, KeyRange{Start: startKey, End: endKey}) + require.NoError(t, err) + return fmt.Sprintf("replicated %d shared SSTs", len(sharedSSTs)) + + case "get": + return runGetCmd(t, td, d) + + case "iter": + o := &IterOptions{KeyTypes: IterKeyTypePointsAndRanges} + var reader Reader + reader = d + for _, arg := range td.CmdArgs { + switch arg.Key { + case "mask-suffix": + o.RangeKeyMasking.Suffix = []byte(arg.Vals[0]) + case "mask-filter": + o.RangeKeyMasking.Filter = func() BlockPropertyFilterMask { + return sstable.NewTestKeysMaskingFilter() + } + case "snapshot": + reader = efos[arg.Vals[0]] + } + } + iter, err := reader.NewIter(o) + if err != nil { + return err.Error() + } + return runIterCmd(td, iter, true) + + case "lsm": + return runLSMCmd(td, d) + + case "metrics": + // The asynchronous loading of table stats can change metrics, so + // wait for all the tables' stats to be loaded. + d.mu.Lock() + d.waitTableStats() + d.mu.Unlock() + + return d.Metrics().StringForTests() + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "excise": + ve := &versionEdit{ + DeletedFiles: map[deletedFileEntry]*fileMetadata{}, + } + var exciseSpan KeyRange + if len(td.CmdArgs) != 2 { + panic("insufficient args for excise command") + } + exciseSpan.Start = []byte(td.CmdArgs[0].Key) + exciseSpan.End = []byte(td.CmdArgs[1].Key) + + d.mu.Lock() + d.mu.versions.logLock() + d.mu.Unlock() + current := d.mu.versions.currentVersion() + for level := range current.Levels { + iter := current.Levels[level].Iter() + for m := iter.SeekGE(d.cmp, exciseSpan.Start); m != nil && d.cmp(m.Smallest.UserKey, exciseSpan.End) < 0; m = iter.Next() { + _, err := d.excise(exciseSpan, m, ve, level) + if err != nil { + d.mu.Lock() + d.mu.versions.logUnlock() + d.mu.Unlock() + return fmt.Sprintf("error when excising %s: %s", m.FileNum, err.Error()) + } + } + } + d.mu.Lock() + d.mu.versions.logUnlock() + d.mu.Unlock() + return fmt.Sprintf("would excise %d files, use ingest-and-excise to excise.\n%s", len(ve.DeletedFiles), ve.String()) + + case "file-only-snapshot": + if len(td.CmdArgs) != 1 { + panic("insufficient args for file-only-snapshot command") + } + name := td.CmdArgs[0].Key + var keyRanges []KeyRange + for _, line := range strings.Split(td.Input, "\n") { + fields := strings.Fields(line) + if len(fields) != 2 { + return "expected two fields for file-only snapshot KeyRanges" + } + kr := KeyRange{Start: []byte(fields[0]), End: []byte(fields[1])} + keyRanges = append(keyRanges, kr) + } + + s := d.NewEventuallyFileOnlySnapshot(keyRanges) + efos[name] = s + return "ok" + + case "wait-for-file-only-snapshot": + if len(td.CmdArgs) != 1 { + panic("insufficient args for file-only-snapshot command") + } + name := td.CmdArgs[0].Key + err := efos[name].WaitForFileOnlySnapshot(context.TODO(), 1*time.Millisecond) + if err != nil { + return err.Error() + } + return "ok" + + case "compact": + err := runCompactCmd(td, d) + if err != nil { + return err.Error() + } + return "ok" + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestIngestShared(t *testing.T) { + for _, strategy := range []remote.CreateOnSharedStrategy{remote.CreateOnSharedAll, remote.CreateOnSharedLower} { + strategyStr := "all" + if strategy == remote.CreateOnSharedLower { + strategyStr = "lower" + } + t.Run(fmt.Sprintf("createOnShared=%s", strategyStr), func(t *testing.T) { + fileName := "ingest_shared" + if strategy == remote.CreateOnSharedLower { + fileName = "ingest_shared_lower" + } + testIngestSharedImpl(t, strategy, fileName) + }) + } +} + +func TestSimpleIngestShared(t *testing.T) { + mem := vfs.NewMem() + var d *DB + var provider2 objstorage.Provider + opts2 := Options{FS: vfs.NewMem(), FormatMajorVersion: FormatVirtualSSTables} + opts2.EnsureDefaults() + + // Create an objProvider where we will fake-create some sstables that can + // then be shared back to the db instance. + providerSettings := objstorageprovider.Settings{ + Logger: opts2.Logger, + FS: opts2.FS, + FSDirName: "", + FSDirInitialListing: nil, + FSCleaner: opts2.Cleaner, + NoSyncOnClose: opts2.NoSyncOnClose, + BytesPerSync: opts2.BytesPerSync, + } + providerSettings.Remote.StorageFactory = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": remote.NewInMem(), + }) + providerSettings.Remote.CreateOnShared = remote.CreateOnSharedAll + providerSettings.Remote.CreateOnSharedLocator = "" + + provider2, err := objstorageprovider.Open(providerSettings) + require.NoError(t, err) + creatorIDCounter := uint64(1) + provider2.SetCreatorID(objstorage.CreatorID(creatorIDCounter)) + creatorIDCounter++ + + defer func() { + require.NoError(t, d.Close()) + }() + + reset := func() { + if d != nil { + require.NoError(t, d.Close()) + } + + mem = vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + opts := &Options{ + FormatMajorVersion: FormatVirtualSSTables, + FS: mem, + L0CompactionThreshold: 100, + L0StopWritesThreshold: 100, + } + opts.Experimental.RemoteStorage = providerSettings.Remote.StorageFactory + opts.Experimental.CreateOnShared = providerSettings.Remote.CreateOnShared + opts.Experimental.CreateOnSharedLocator = providerSettings.Remote.CreateOnSharedLocator + + var err error + d, err = Open("", opts) + require.NoError(t, err) + require.NoError(t, d.SetCreatorID(creatorIDCounter)) + creatorIDCounter++ + } + reset() + + metaMap := map[base.DiskFileNum]objstorage.ObjectMetadata{} + + require.NoError(t, d.Set([]byte("d"), []byte("unexpected"), nil)) + require.NoError(t, d.Set([]byte("e"), []byte("unexpected"), nil)) + require.NoError(t, d.Set([]byte("a"), []byte("unexpected"), nil)) + require.NoError(t, d.Set([]byte("f"), []byte("unexpected"), nil)) + d.Flush() + + { + // Create a shared file. + fn := base.FileNum(2) + f, meta, err := provider2.Create(context.TODO(), fileTypeTable, fn.DiskFileNum(), objstorage.CreateOptions{PreferSharedStorage: true}) + require.NoError(t, err) + w := sstable.NewWriter(f, d.opts.MakeWriterOptions(0, d.opts.FormatMajorVersion.MaxTableFormat())) + w.Set([]byte("d"), []byte("shared")) + w.Set([]byte("e"), []byte("shared")) + w.Close() + metaMap[fn.DiskFileNum()] = meta + } + + m := metaMap[base.FileNum(2).DiskFileNum()] + handle, err := provider2.RemoteObjectBacking(&m) + require.NoError(t, err) + size, err := provider2.Size(m) + require.NoError(t, err) + + sharedSSTMeta := SharedSSTMeta{ + Backing: handle, + Smallest: base.MakeInternalKey([]byte("d"), 0, InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte("e"), 0, InternalKeyKindSet), + SmallestPointKey: base.MakeInternalKey([]byte("d"), 0, InternalKeyKindSet), + LargestPointKey: base.MakeInternalKey([]byte("e"), 0, InternalKeyKindSet), + Level: 6, + Size: uint64(size + 5), + } + _, err = d.IngestAndExcise([]string{}, []SharedSSTMeta{sharedSSTMeta}, KeyRange{Start: []byte("d"), End: []byte("ee")}) + require.NoError(t, err) + + // TODO(bilal): Once reading of shared sstables is in, verify that the values + // of d and e have been updated. +} + +type blockedCompaction struct { + startBlock, unblock chan struct{} +} + +func TestConcurrentExcise(t *testing.T) { + var d, d1, d2 *DB + var efos map[string]*EventuallyFileOnlySnapshot + backgroundErrs := make(chan error, 5) + var compactions map[string]*blockedCompaction + defer func() { + for _, e := range efos { + require.NoError(t, e.Close()) + } + if d1 != nil { + require.NoError(t, d1.Close()) + } + if d2 != nil { + require.NoError(t, d2.Close()) + } + }() + creatorIDCounter := uint64(1) + replicateCounter := 1 + + var wg sync.WaitGroup + defer wg.Wait() + var blockNextCompaction bool + var blockedJobID int + var blockedCompactionName string + var blockedCompactionsMu sync.Mutex // protects the above three variables. + + reset := func() { + wg.Wait() + for _, e := range efos { + require.NoError(t, e.Close()) + } + if d1 != nil { + require.NoError(t, d1.Close()) + } + if d2 != nil { + require.NoError(t, d2.Close()) + } + efos = make(map[string]*EventuallyFileOnlySnapshot) + compactions = make(map[string]*blockedCompaction) + backgroundErrs = make(chan error, 5) + + var el EventListener + el.EnsureDefaults(testLogger{t: t}) + el.FlushBegin = func(info FlushInfo) { + // Don't block flushes + } + el.BackgroundError = func(err error) { + backgroundErrs <- err + } + el.CompactionBegin = func(info CompactionInfo) { + if info.Reason == "move" { + return + } + blockedCompactionsMu.Lock() + defer blockedCompactionsMu.Unlock() + if blockNextCompaction { + blockNextCompaction = false + blockedJobID = info.JobID + } + } + el.TableCreated = func(info TableCreateInfo) { + blockedCompactionsMu.Lock() + if info.JobID != blockedJobID { + blockedCompactionsMu.Unlock() + return + } + blockedJobID = 0 + c := compactions[blockedCompactionName] + blockedCompactionName = "" + blockedCompactionsMu.Unlock() + c.startBlock <- struct{}{} + <-c.unblock + } + + sstorage := remote.NewInMem() + mem1 := vfs.NewMem() + mem2 := vfs.NewMem() + require.NoError(t, mem1.MkdirAll("ext", 0755)) + require.NoError(t, mem2.MkdirAll("ext", 0755)) + opts1 := &Options{ + Comparer: testkeys.Comparer, + LBaseMaxBytes: 1, + FS: mem1, + L0CompactionThreshold: 100, + L0StopWritesThreshold: 100, + DebugCheck: DebugCheckLevels, + FormatMajorVersion: FormatVirtualSSTables, + } + // lel. + lel := MakeLoggingEventListener(DefaultLogger) + tel := TeeEventListener(lel, el) + opts1.EventListener = &tel + opts1.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": sstorage, + }) + opts1.Experimental.CreateOnShared = remote.CreateOnSharedAll + opts1.Experimental.CreateOnSharedLocator = "" + // Disable automatic compactions because otherwise we'll race with + // delete-only compactions triggered by ingesting range tombstones. + opts1.DisableAutomaticCompactions = true + + opts2 := &Options{} + *opts2 = *opts1 + opts2.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": sstorage, + }) + opts2.Experimental.CreateOnShared = remote.CreateOnSharedAll + opts2.Experimental.CreateOnSharedLocator = "" + opts2.FS = mem2 + + var err error + d1, err = Open("", opts1) + require.NoError(t, err) + require.NoError(t, d1.SetCreatorID(creatorIDCounter)) + creatorIDCounter++ + d2, err = Open("", opts2) + require.NoError(t, err) + require.NoError(t, d2.SetCreatorID(creatorIDCounter)) + creatorIDCounter++ + d = d1 + } + reset() + + datadriven.RunTest(t, "testdata/concurrent_excise", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + reset() + return "" + case "switch": + if len(td.CmdArgs) != 1 { + return "usage: switch <1 or 2>" + } + switch td.CmdArgs[0].Key { + case "1": + d = d1 + case "2": + d = d2 + default: + return "usage: switch <1 or 2>" + } + return "ok" + case "batch": + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + case "build": + if err := runBuildCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + return "" + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + return "" + + case "ingest": + if err := runIngestCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + return "" + + case "ingest-and-excise": + if err := runIngestAndExciseCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + return "" + + case "replicate": + if len(td.CmdArgs) != 4 { + return "usage: replicate " + } + var from, to *DB + switch td.CmdArgs[0].Key { + case "1": + from = d1 + case "2": + from = d2 + default: + return "usage: replicate " + } + switch td.CmdArgs[1].Key { + case "1": + to = d1 + case "2": + to = d2 + default: + return "usage: replicate " + } + startKey := []byte(td.CmdArgs[2].Key) + endKey := []byte(td.CmdArgs[3].Key) + + writeOpts := d.opts.MakeWriterOptions(0 /* level */, to.opts.FormatMajorVersion.MaxTableFormat()) + sstPath := fmt.Sprintf("ext/replicate%d.sst", replicateCounter) + f, err := to.opts.FS.Create(sstPath) + require.NoError(t, err) + replicateCounter++ + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), writeOpts) + + var sharedSSTs []SharedSSTMeta + err = from.ScanInternal(context.TODO(), sstable.CategoryAndQoS{}, startKey, endKey, + func(key *InternalKey, value LazyValue, _ IteratorLevel) error { + val, _, err := value.Value(nil) + require.NoError(t, err) + require.NoError(t, w.Add(base.MakeInternalKey(key.UserKey, 0, key.Kind()), val)) + return nil + }, + func(start, end []byte, seqNum uint64) error { + require.NoError(t, w.DeleteRange(start, end)) + return nil + }, + func(start, end []byte, keys []keyspan.Key) error { + s := keyspan.Span{ + Start: start, + End: end, + Keys: keys, + KeysOrder: 0, + } + require.NoError(t, rangekey.Encode(&s, func(k base.InternalKey, v []byte) error { + return w.AddRangeKey(base.MakeInternalKey(k.UserKey, 0, k.Kind()), v) + })) + return nil + }, + func(sst *SharedSSTMeta) error { + sharedSSTs = append(sharedSSTs, *sst) + return nil + }, + ) + require.NoError(t, err) + require.NoError(t, w.Close()) + + _, err = to.IngestAndExcise([]string{sstPath}, sharedSSTs, KeyRange{Start: startKey, End: endKey}) + require.NoError(t, err) + return fmt.Sprintf("replicated %d shared SSTs", len(sharedSSTs)) + + case "get": + return runGetCmd(t, td, d) + + case "iter": + o := &IterOptions{KeyTypes: IterKeyTypePointsAndRanges} + var reader Reader + reader = d + for _, arg := range td.CmdArgs { + switch arg.Key { + case "mask-suffix": + o.RangeKeyMasking.Suffix = []byte(arg.Vals[0]) + case "mask-filter": + o.RangeKeyMasking.Filter = func() BlockPropertyFilterMask { + return sstable.NewTestKeysMaskingFilter() + } + case "snapshot": + reader = efos[arg.Vals[0]] + } + } + iter, err := reader.NewIter(o) + if err != nil { + return err.Error() + } + return runIterCmd(td, iter, true) + + case "lsm": + return runLSMCmd(td, d) + + case "metrics": + // The asynchronous loading of table stats can change metrics, so + // wait for all the tables' stats to be loaded. + d.mu.Lock() + d.waitTableStats() + d.mu.Unlock() + + return d.Metrics().StringForTests() + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "excise": + ve := &versionEdit{ + DeletedFiles: map[deletedFileEntry]*fileMetadata{}, + } + var exciseSpan KeyRange + if len(td.CmdArgs) != 2 { + panic("insufficient args for excise command") + } + exciseSpan.Start = []byte(td.CmdArgs[0].Key) + exciseSpan.End = []byte(td.CmdArgs[1].Key) + + d.mu.Lock() + d.mu.versions.logLock() + d.mu.Unlock() + current := d.mu.versions.currentVersion() + for level := range current.Levels { + iter := current.Levels[level].Iter() + for m := iter.SeekGE(d.cmp, exciseSpan.Start); m != nil && d.cmp(m.Smallest.UserKey, exciseSpan.End) < 0; m = iter.Next() { + _, err := d.excise(exciseSpan, m, ve, level) + if err != nil { + d.mu.Lock() + d.mu.versions.logUnlock() + d.mu.Unlock() + return fmt.Sprintf("error when excising %s: %s", m.FileNum, err.Error()) + } + } + } + d.mu.Lock() + d.mu.versions.logUnlock() + d.mu.Unlock() + return fmt.Sprintf("would excise %d files, use ingest-and-excise to excise.\n%s", len(ve.DeletedFiles), ve.String()) + + case "file-only-snapshot": + if len(td.CmdArgs) != 1 { + panic("insufficient args for file-only-snapshot command") + } + name := td.CmdArgs[0].Key + var keyRanges []KeyRange + for _, line := range strings.Split(td.Input, "\n") { + fields := strings.Fields(line) + if len(fields) != 2 { + return "expected two fields for file-only snapshot KeyRanges" + } + kr := KeyRange{Start: []byte(fields[0]), End: []byte(fields[1])} + keyRanges = append(keyRanges, kr) + } + + s := d.NewEventuallyFileOnlySnapshot(keyRanges) + efos[name] = s + return "ok" + + case "wait-for-file-only-snapshot": + if len(td.CmdArgs) != 1 { + panic("insufficient args for file-only-snapshot command") + } + name := td.CmdArgs[0].Key + err := efos[name].WaitForFileOnlySnapshot(context.TODO(), 1*time.Millisecond) + if err != nil { + return err.Error() + } + return "ok" + + case "unblock": + name := td.CmdArgs[0].Key + blockedCompactionsMu.Lock() + c := compactions[name] + delete(compactions, name) + blockedCompactionsMu.Unlock() + c.unblock <- struct{}{} + return "ok" + + case "compact": + async := false + var otherArgs []datadriven.CmdArg + var bc *blockedCompaction + for i := range td.CmdArgs { + switch td.CmdArgs[i].Key { + case "block": + name := td.CmdArgs[i].Vals[0] + bc = &blockedCompaction{startBlock: make(chan struct{}), unblock: make(chan struct{})} + blockedCompactionsMu.Lock() + compactions[name] = bc + blockNextCompaction = true + blockedCompactionName = name + blockedCompactionsMu.Unlock() + async = true + default: + otherArgs = append(otherArgs, td.CmdArgs[i]) + } + } + var tdClone datadriven.TestData + tdClone = *td + tdClone.CmdArgs = otherArgs + if !async { + err := runCompactCmd(td, d) + if err != nil { + return err.Error() + } + } else { + wg.Add(1) + go func() { + defer wg.Done() + _ = runCompactCmd(&tdClone, d) + }() + <-bc.startBlock + return "spun off in separate goroutine" + } + return "ok" + case "wait-for-background-error": + err := <-backgroundErrs + return err.Error() + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestIngestExternal(t *testing.T) { + var mem vfs.FS + var d *DB + var flushed bool + defer func() { + require.NoError(t, d.Close()) + }() + + var remoteStorage remote.Storage + + reset := func() { + if d != nil { + require.NoError(t, d.Close()) + } + + mem = vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + remoteStorage = remote.NewInMem() + opts := &Options{ + FS: mem, + L0CompactionThreshold: 100, + L0StopWritesThreshold: 100, + DebugCheck: DebugCheckLevels, + EventListener: &EventListener{FlushEnd: func(info FlushInfo) { + flushed = true + }}, + FormatMajorVersion: FormatVirtualSSTables, + } + opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "external-locator": remoteStorage, + }) + opts.Experimental.CreateOnShared = remote.CreateOnSharedNone + // Disable automatic compactions because otherwise we'll race with + // delete-only compactions triggered by ingesting range tombstones. + opts.DisableAutomaticCompactions = true + + var err error + d, err = Open("", opts) + require.NoError(t, err) + require.NoError(t, d.SetCreatorID(1)) + } + reset() + + datadriven.RunTest(t, "testdata/ingest_external", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + reset() + return "" + case "batch": + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + case "build-remote": + if err := runBuildRemoteCmd(td, d, remoteStorage); err != nil { + return err.Error() + } + return "" + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + return "" + + case "ingest-external": + flushed = false + if err := runIngestExternalCmd(td, d, "external-locator"); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + if flushed { + return "memtable flushed" + } + return "" + + case "get": + return runGetCmd(t, td, d) + + case "iter": + iter, _ := d.NewIter(&IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + }) + return runIterCmd(td, iter, true) + + case "lsm": + return runLSMCmd(td, d) + + case "metrics": + // The asynchronous loading of table stats can change metrics, so + // wait for all the tables' stats to be loaded. + d.mu.Lock() + d.waitTableStats() + d.mu.Unlock() + + return d.Metrics().StringForTests() + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "compact": + if len(td.CmdArgs) != 2 { + panic("insufficient args for compact command") + } + l := td.CmdArgs[0].Key + r := td.CmdArgs[1].Key + err := d.Compact([]byte(l), []byte(r), false) + if err != nil { + return err.Error() + } + return "" + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestIngestMemtableOverlaps(t *testing.T) { + comparers := []Comparer{ + {Name: "default", Compare: DefaultComparer.Compare, FormatKey: DefaultComparer.FormatKey}, + { + Name: "reverse", + Compare: func(a, b []byte) int { return DefaultComparer.Compare(b, a) }, + FormatKey: DefaultComparer.FormatKey, + }, + } + m := make(map[string]*Comparer) + for i := range comparers { + c := &comparers[i] + m[c.Name] = c + } + + for _, comparer := range comparers { + t.Run(comparer.Name, func(t *testing.T) { + var mem *memTable + + parseMeta := func(s string) *fileMetadata { + parts := strings.Split(s, "-") + meta := &fileMetadata{} + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + var smallest, largest base.InternalKey + if strings.Contains(parts[0], ".") { + if !strings.Contains(parts[1], ".") { + t.Fatalf("malformed table spec: %s", s) + } + smallest = base.ParseInternalKey(parts[0]) + largest = base.ParseInternalKey(parts[1]) + } else { + smallest = InternalKey{UserKey: []byte(parts[0])} + largest = InternalKey{UserKey: []byte(parts[1])} + } + // If we're using a reverse comparer, flip the file bounds. + if mem.cmp(smallest.UserKey, largest.UserKey) > 0 { + smallest, largest = largest, smallest + } + meta.ExtendPointKeyBounds(comparer.Compare, smallest, largest) + meta.InitPhysicalBacking() + return meta + } + + datadriven.RunTest(t, "testdata/ingest_memtable_overlaps", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + b := newBatch(nil) + if err := runBatchDefineCmd(d, b); err != nil { + return err.Error() + } + + opts := &Options{ + Comparer: &comparer, + } + opts.EnsureDefaults().WithFSDefaults() + if len(d.CmdArgs) > 1 { + return fmt.Sprintf("%s expects at most 1 argument", d.Cmd) + } + if len(d.CmdArgs) == 1 { + opts.Comparer = m[d.CmdArgs[0].String()] + if opts.Comparer == nil { + return fmt.Sprintf("%s unknown comparer: %s", d.Cmd, d.CmdArgs[0].String()) + } + } + + mem = newMemTable(memTableOptions{Options: opts}) + if err := mem.apply(b, 0); err != nil { + return err.Error() + } + return "" + + case "overlaps": + var buf bytes.Buffer + for _, data := range strings.Split(d.Input, "\n") { + var keyRanges []internalKeyRange + for _, part := range strings.Fields(data) { + meta := parseMeta(part) + keyRanges = append(keyRanges, internalKeyRange{smallest: meta.Smallest, largest: meta.Largest}) + } + fmt.Fprintf(&buf, "%t\n", ingestMemtableOverlaps(mem.cmp, mem, keyRanges)) + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) + }) + } +} + +func TestKeyRangeBasic(t *testing.T) { + cmp := base.DefaultComparer.Compare + k1 := KeyRange{Start: []byte("b"), End: []byte("c")} + + // Tests for Contains() + require.True(t, k1.Contains(cmp, base.MakeInternalKey([]byte("b"), 1, InternalKeyKindSet))) + require.False(t, k1.Contains(cmp, base.MakeInternalKey([]byte("c"), 1, InternalKeyKindSet))) + require.True(t, k1.Contains(cmp, base.MakeInternalKey([]byte("bb"), 1, InternalKeyKindSet))) + require.True(t, k1.Contains(cmp, base.MakeExclusiveSentinelKey(InternalKeyKindRangeDelete, []byte("c")))) + + m1 := &fileMetadata{ + Smallest: base.MakeInternalKey([]byte("b"), 1, InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte("c"), 1, InternalKeyKindSet), + } + require.True(t, k1.Overlaps(cmp, m1)) + m2 := &fileMetadata{ + Smallest: base.MakeInternalKey([]byte("c"), 1, InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte("d"), 1, InternalKeyKindSet), + } + require.False(t, k1.Overlaps(cmp, m2)) + m3 := &fileMetadata{ + Smallest: base.MakeInternalKey([]byte("a"), 1, InternalKeyKindSet), + Largest: base.MakeExclusiveSentinelKey(InternalKeyKindRangeDelete, []byte("b")), + } + require.False(t, k1.Overlaps(cmp, m3)) + m4 := &fileMetadata{ + Smallest: base.MakeInternalKey([]byte("a"), 1, InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte("b"), 1, InternalKeyKindSet), + } + require.True(t, k1.Overlaps(cmp, m4)) +} + +func BenchmarkIngestOverlappingMemtable(b *testing.B) { + assertNoError := func(err error) { + b.Helper() + if err != nil { + b.Fatal(err) + } + } + + for count := 1; count < 6; count++ { + b.Run(fmt.Sprintf("memtables=%d", count), func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + mem := vfs.NewMem() + d, err := Open("", &Options{ + FS: mem, + }) + assertNoError(err) + + // Create memtables. + for { + assertNoError(d.Set([]byte("a"), nil, nil)) + d.mu.Lock() + done := len(d.mu.mem.queue) == count + d.mu.Unlock() + if done { + break + } + } + + // Create the overlapping sstable that will force a flush when ingested. + f, err := mem.Create("ext") + assertNoError(err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + assertNoError(w.Set([]byte("a"), nil)) + assertNoError(w.Close()) + + b.StartTimer() + assertNoError(d.Ingest([]string{"ext"})) + } + }) + } +} + +func TestIngestTargetLevel(t *testing.T) { + var d *DB + defer func() { + if d != nil { + // Ignore errors because this test defines fake in-progress transactions + // that prohibit clean shutdown. + _ = d.Close() + } + }() + + parseMeta := func(s string) *fileMetadata { + var rkey bool + if len(s) >= 4 && s[0:4] == "rkey" { + rkey = true + s = s[5:] + } + parts := strings.Split(s, "-") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + var m *fileMetadata + if rkey { + m = (&fileMetadata{}).ExtendRangeKeyBounds( + d.cmp, + InternalKey{UserKey: []byte(parts[0])}, + InternalKey{UserKey: []byte(parts[1])}, + ) + } else { + m = (&fileMetadata{}).ExtendPointKeyBounds( + d.cmp, + InternalKey{UserKey: []byte(parts[0])}, + InternalKey{UserKey: []byte(parts[1])}, + ) + } + m.InitPhysicalBacking() + return m + } + + datadriven.RunTest(t, "testdata/ingest_target_level", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + if d != nil { + // Ignore errors because this test defines fake in-progress + // transactions that prohibit clean shutdown. + _ = d.Close() + } + + var err error + opts := Options{ + FormatMajorVersion: internalFormatNewest, + } + opts.WithFSDefaults() + if d, err = runDBDefineCmd(td, &opts); err != nil { + return err.Error() + } + + readState := d.loadReadState() + c := &checkConfig{ + logger: d.opts.Logger, + comparer: d.opts.Comparer, + readState: readState, + newIters: d.newIters, + // TODO: runDBDefineCmd doesn't properly update the visible + // sequence number. So we have to explicitly configure level checker with a very large + // sequence number, otherwise the DB appears empty. + seqNum: InternalKeySeqNumMax, + } + if err := checkLevelsInternal(c); err != nil { + return err.Error() + } + readState.unref() + + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "target": + var buf bytes.Buffer + suggestSplit := false + for _, cmd := range td.CmdArgs { + switch cmd.Key { + case "suggest-split": + suggestSplit = true + } + } + for _, target := range strings.Split(td.Input, "\n") { + meta := parseMeta(target) + level, overlapFile, err := ingestTargetLevel( + d.newIters, d.tableNewRangeKeyIter, IterOptions{logger: d.opts.Logger}, + d.opts.Comparer, d.mu.versions.currentVersion(), 1, d.mu.compact.inProgress, meta, + suggestSplit) + if err != nil { + return err.Error() + } + if overlapFile != nil { + fmt.Fprintf(&buf, "%d (split file: %s)\n", level, overlapFile.FileNum) + } else { + fmt.Fprintf(&buf, "%d\n", level) + } + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestIngest(t *testing.T) { + var mem vfs.FS + var d *DB + var flushed bool + defer func() { + require.NoError(t, d.Close()) + }() + + reset := func(split bool) { + if d != nil { + require.NoError(t, d.Close()) + } + + mem = vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + opts := &Options{ + FS: mem, + L0CompactionThreshold: 100, + L0StopWritesThreshold: 100, + DebugCheck: DebugCheckLevels, + EventListener: &EventListener{FlushEnd: func(info FlushInfo) { + flushed = true + }}, + FormatMajorVersion: internalFormatNewest, + } + opts.Experimental.IngestSplit = func() bool { + return split + } + // Disable automatic compactions because otherwise we'll race with + // delete-only compactions triggered by ingesting range tombstones. + opts.DisableAutomaticCompactions = true + + var err error + d, err = Open("", opts) + require.NoError(t, err) + } + reset(false /* split */) + + datadriven.RunTest(t, "testdata/ingest", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + split := false + for _, cmd := range td.CmdArgs { + switch cmd.Key { + case "enable-split": + split = true + default: + return fmt.Sprintf("unexpected key: %s", cmd.Key) + } + } + reset(split) + return "" + case "batch": + b := d.NewIndexedBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + + case "build": + if err := runBuildCmd(td, d, mem); err != nil { + return err.Error() + } + return "" + + case "ingest": + flushed = false + if err := runIngestCmd(td, d, mem); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + if flushed { + return "memtable flushed" + } + return "" + + case "get": + return runGetCmd(t, td, d) + + case "iter": + iter, _ := d.NewIter(&IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + }) + return runIterCmd(td, iter, true) + + case "lsm": + return runLSMCmd(td, d) + + case "metrics": + // The asynchronous loading of table stats can change metrics, so + // wait for all the tables' stats to be loaded. + d.mu.Lock() + d.waitTableStats() + d.mu.Unlock() + + return d.Metrics().StringForTests() + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "compact": + if len(td.CmdArgs) != 2 { + panic("insufficient args for compact command") + } + l := td.CmdArgs[0].Key + r := td.CmdArgs[1].Key + err := d.Compact([]byte(l), []byte(r), false) + if err != nil { + return err.Error() + } + return "" + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestIngestError(t *testing.T) { + for i := int32(0); ; i++ { + mem := vfs.NewMem() + + f0, err := mem.Create("ext0") + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f0), sstable.WriterOptions{}) + require.NoError(t, w.Set([]byte("d"), nil)) + require.NoError(t, w.Close()) + f1, err := mem.Create("ext1") + require.NoError(t, err) + w = sstable.NewWriter(objstorageprovider.NewFileWritable(f1), sstable.WriterOptions{}) + require.NoError(t, w.Set([]byte("d"), nil)) + require.NoError(t, w.Close()) + + ii := errorfs.OnIndex(-1) + d, err := Open("", &Options{ + FS: errorfs.Wrap(mem, errorfs.ErrInjected.If(ii)), + Logger: panicLogger{}, + L0CompactionThreshold: 8, + }) + require.NoError(t, err) + // Force the creation of an L0 sstable that overlaps with the tables + // we'll attempt to ingest. This ensures that we exercise filesystem + // codepaths when determining the ingest target level. + require.NoError(t, d.Set([]byte("a"), nil, nil)) + require.NoError(t, d.Set([]byte("d"), nil, nil)) + require.NoError(t, d.Flush()) + + t.Run(fmt.Sprintf("index-%d", i), func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok && errors.Is(e, errorfs.ErrInjected) { + return + } + // d.opts.Logger.Fatalf won't propagate ErrInjected + // itself, but should contain the error message. + if strings.HasSuffix(fmt.Sprint(r), errorfs.ErrInjected.Error()) { + return + } + t.Fatal(r) + } + }() + + ii.Store(i) + err1 := d.Ingest([]string{"ext0"}) + err2 := d.Ingest([]string{"ext1"}) + err := firstError(err1, err2) + if err != nil && !errors.Is(err, errorfs.ErrInjected) { + t.Fatal(err) + } + }) + + // d.Close may error if we failed to flush the manifest. + _ = d.Close() + + // If the injector's index is non-negative, the i-th filesystem + // operation was never executed. + if ii.Load() >= 0 { + break + } + } +} + +func TestIngestIdempotence(t *testing.T) { + // Use an on-disk filesystem, because Ingest with a MemFS will copy, not + // link the ingested file. + dir, err := os.MkdirTemp("", "ingest-idempotence") + require.NoError(t, err) + defer os.RemoveAll(dir) + fs := vfs.Default + + path := fs.PathJoin(dir, "ext") + f, err := fs.Create(fs.PathJoin(dir, "ext")) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(t, w.Set([]byte("d"), nil)) + require.NoError(t, w.Close()) + + d, err := Open(dir, &Options{ + FS: fs, + }) + require.NoError(t, err) + const count = 4 + for i := 0; i < count; i++ { + ingestPath := fs.PathJoin(dir, fmt.Sprintf("ext%d", i)) + require.NoError(t, fs.Link(path, ingestPath)) + require.NoError(t, d.Ingest([]string{ingestPath})) + } + require.NoError(t, d.Close()) +} + +func TestIngestCompact(t *testing.T) { + mem := vfs.NewMem() + lel := MakeLoggingEventListener(&base.InMemLogger{}) + d, err := Open("", &Options{ + EventListener: &lel, + FS: mem, + L0CompactionThreshold: 1, + L0StopWritesThreshold: 1, + }) + require.NoError(t, err) + + src := func(i int) string { + return fmt.Sprintf("ext%d", i) + } + f, err := mem.Create(src(0)) + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + key := []byte("a") + require.NoError(t, w.Add(base.MakeInternalKey(key, 0, InternalKeyKindSet), nil)) + require.NoError(t, w.Close()) + + // Make N copies of the sstable. + const count = 20 + for i := 1; i < count; i++ { + require.NoError(t, vfs.Copy(d.opts.FS, src(0), src(i))) + } + + // Ingest the same sstable multiple times. Compaction should take place as + // ingestion happens, preventing an indefinite write stall from occurring. + for i := 0; i < count; i++ { + if i == 10 { + // Half-way through the ingestions, set a key in the memtable to force + // overlap with the memtable which will require the memtable to be + // flushed. + require.NoError(t, d.Set(key, nil, nil)) + } + require.NoError(t, d.Ingest([]string{src(i)})) + } + + require.NoError(t, d.Close()) +} + +func TestConcurrentIngest(t *testing.T) { + mem := vfs.NewMem() + d, err := Open("", &Options{ + FS: mem, + }) + require.NoError(t, err) + + // Create an sstable with 2 keys. This is necessary to trigger the overlap + // bug because an sstable with a single key will not have overlap in internal + // key space and the sequence number assignment had already guaranteed + // correct ordering. + src := func(i int) string { + return fmt.Sprintf("ext%d", i) + } + f, err := mem.Create(src(0)) + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(t, w.Set([]byte("a"), nil)) + require.NoError(t, w.Set([]byte("b"), nil)) + require.NoError(t, w.Close()) + + // Make N copies of the sstable. + errCh := make(chan error, 5) + for i := 1; i < cap(errCh); i++ { + require.NoError(t, vfs.Copy(d.opts.FS, src(0), src(i))) + } + + // Perform N ingestions concurrently. + for i := 0; i < cap(errCh); i++ { + go func(i int) { + err := d.Ingest([]string{src(i)}) + if err == nil { + if _, err = d.opts.FS.Stat(src(i)); oserror.IsNotExist(err) { + err = nil + } + } + errCh <- err + }(i) + } + for i := 0; i < cap(errCh); i++ { + require.NoError(t, <-errCh) + } + + require.NoError(t, d.Close()) +} + +func TestConcurrentIngestCompact(t *testing.T) { + for i := 0; i < 2; i++ { + t.Run("", func(t *testing.T) { + mem := vfs.NewMem() + compactionReady := make(chan struct{}) + compactionBegin := make(chan struct{}) + d, err := Open("", &Options{ + FS: mem, + EventListener: &EventListener{ + TableCreated: func(info TableCreateInfo) { + if info.Reason == "compacting" { + close(compactionReady) + <-compactionBegin + } + }, + }, + }) + require.NoError(t, err) + + ingest := func(keys ...string) { + t.Helper() + f, err := mem.Create("ext") + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + for _, k := range keys { + require.NoError(t, w.Set([]byte(k), nil)) + } + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{"ext"})) + } + + compact := func(start, end string) { + t.Helper() + require.NoError(t, d.Compact([]byte(start), []byte(end), false)) + } + + lsm := func() string { + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + } + + expectLSM := func(expected string) { + t.Helper() + expected = strings.TrimSpace(expected) + actual := strings.TrimSpace(lsm()) + if expected != actual { + t.Fatalf("expected\n%s\nbut found\n%s", expected, actual) + } + } + + ingest("a") + ingest("a") + ingest("c") + ingest("c") + + expectLSM(` +0.0: + 000005:[a#11,SET-a#11,SET] + 000007:[c#13,SET-c#13,SET] +6: + 000004:[a#10,SET-a#10,SET] + 000006:[c#12,SET-c#12,SET] +`) + + // At this point ingestion of an sstable containing only key "b" will be + // targeted at L6. Yet a concurrent compaction of sstables 5 and 7 will + // create a new sstable in L6 spanning ["a"-"c"]. So the ingestion must + // actually target L5. + + switch i { + case 0: + // Compact, then ingest. + go func() { + <-compactionReady + + ingest("b") + + close(compactionBegin) + }() + + compact("a", "z") + + expectLSM(` +0.0: + 000009:[b#14,SET-b#14,SET] +6: + 000008:[a#0,SET-c#0,SET] +`) + + case 1: + // Ingest, then compact + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + close(compactionBegin) + compact("a", "z") + }() + + ingest("b") + wg.Wait() + + // Because we're performing the ingestion and compaction concurrently, + // we can't guarantee any particular LSM structure at this point. The + // test will fail with an assertion error due to overlapping sstables + // if there is insufficient synchronization between ingestion and + // compaction. + } + + require.NoError(t, d.Close()) + }) + } +} + +func TestIngestFlushQueuedMemTable(t *testing.T) { + // Verify that ingestion forces a flush of a queued memtable. + + // Test with a format major version prior to FormatFlushableIngest and one + // after. Both should result in the same statistic calculations. + for _, fmv := range []FormatMajorVersion{FormatFlushableIngest - 1, internalFormatNewest} { + func(fmv FormatMajorVersion) { + mem := vfs.NewMem() + d, err := Open("", &Options{ + FS: mem, + FormatMajorVersion: fmv, + }) + require.NoError(t, err) + + // Add the key "a" to the memtable, then fill up the memtable with the key + // "b". The ingested sstable will only overlap with the queued memtable. + require.NoError(t, d.Set([]byte("a"), nil, nil)) + for { + require.NoError(t, d.Set([]byte("b"), nil, nil)) + d.mu.Lock() + done := len(d.mu.mem.queue) == 2 + d.mu.Unlock() + if done { + break + } + } + + ingest := func(keys ...string) { + t.Helper() + f, err := mem.Create("ext") + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: fmv.MinTableFormat(), + }) + for _, k := range keys { + require.NoError(t, w.Set([]byte(k), nil)) + } + require.NoError(t, w.Close()) + stats, err := d.IngestWithStats([]string{"ext"}) + require.NoError(t, err) + require.Equal(t, stats.ApproxIngestedIntoL0Bytes, stats.Bytes) + require.Equal(t, stats.MemtableOverlappingFiles, 1) + require.Less(t, uint64(0), stats.Bytes) + } + + ingest("a") + + require.NoError(t, d.Close()) + }(fmv) + } +} + +func TestIngestStats(t *testing.T) { + mem := vfs.NewMem() + d, err := Open("", &Options{ + FS: mem, + }) + require.NoError(t, err) + + ingest := func(expectedLevel int, keys ...string) { + t.Helper() + f, err := mem.Create("ext") + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + for _, k := range keys { + require.NoError(t, w.Set([]byte(k), nil)) + } + require.NoError(t, w.Close()) + stats, err := d.IngestWithStats([]string{"ext"}) + require.NoError(t, err) + if expectedLevel == 0 { + require.Equal(t, stats.ApproxIngestedIntoL0Bytes, stats.Bytes) + } else { + require.EqualValues(t, 0, stats.ApproxIngestedIntoL0Bytes) + } + require.Less(t, uint64(0), stats.Bytes) + } + ingest(6, "a") + ingest(0, "a") + ingest(6, "b", "g") + ingest(0, "c") + require.NoError(t, d.Close()) +} + +func TestIngestFlushQueuedLargeBatch(t *testing.T) { + // Verify that ingestion forces a flush of a queued large batch. + + mem := vfs.NewMem() + d, err := Open("", &Options{ + FS: mem, + }) + require.NoError(t, err) + + // The default large batch threshold is slightly less than 1/2 of the + // memtable size which makes triggering a problem with flushing queued large + // batches irritating. Manually adjust the threshold to 1/8 of the memtable + // size in order to more easily create a situation where a large batch is + // queued but not automatically flushed. + d.mu.Lock() + d.largeBatchThreshold = d.opts.MemTableSize / 8 + d.mu.Unlock() + + // Set a record with a large value. This will be transformed into a large + // batch and placed in the flushable queue. + require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("v"), int(d.largeBatchThreshold)), nil)) + + ingest := func(keys ...string) { + t.Helper() + f, err := mem.Create("ext") + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + for _, k := range keys { + require.NoError(t, w.Set([]byte(k), nil)) + } + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{"ext"})) + } + + ingest("a") + + require.NoError(t, d.Close()) +} + +func TestIngestMemtablePendingOverlap(t *testing.T) { + mem := vfs.NewMem() + d, err := Open("", &Options{ + FS: mem, + }) + require.NoError(t, err) + + d.mu.Lock() + // Use a custom commit pipeline apply function to give us control over + // timing of events. + assignedBatch := make(chan struct{}) + applyBatch := make(chan struct{}) + originalApply := d.commit.env.apply + d.commit.env.apply = func(b *Batch, mem *memTable) error { + assignedBatch <- struct{}{} + applyBatch <- struct{}{} + return originalApply(b, mem) + } + d.mu.Unlock() + + ingest := func(keys ...string) { + t.Helper() + f, err := mem.Create("ext") + require.NoError(t, err) + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + for _, k := range keys { + require.NoError(t, w.Set([]byte(k), nil)) + } + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{"ext"})) + } + + var wg sync.WaitGroup + wg.Add(2) + + // First, Set('c') begins. This call will: + // + // * enqueue the batch to the pending queue. + // * allocate a sequence number `x`. + // * write the batch to the WAL. + // + // and then block until we read from the `applyBatch` channel down below. + go func() { + err := d.Set([]byte("c"), nil, nil) + if err != nil { + t.Error(err) + } + wg.Done() + }() + + // When the above Set('c') is ready to apply, it sends on the + // `assignedBatch` channel. Once that happens, we start Ingest('a', 'c'). + // The Ingest('a', 'c') allocates sequence number `x + 1`. + go func() { + // Wait until the Set has grabbed a sequence number before ingesting. + <-assignedBatch + ingest("a", "c") + wg.Done() + }() + + // The Set('c')#1 and Ingest('a', 'c')#2 are both pending. To maintain + // sequence number invariants, the Set needs to be applied and flushed + // before the Ingest determines its target level. + // + // Sleep a bit to ensure that the Ingest has time to call into + // AllocateSeqNum. Once it allocates its sequence number, it should see + // that there are unpublished sequence numbers below it and spin until the + // Set's sequence number is published. After sleeping, read from + // `applyBatch` to actually allow the Set to apply and publish its + // sequence number. + time.Sleep(100 * time.Millisecond) + <-applyBatch + + // Wait for both calls to complete. + wg.Wait() + require.NoError(t, d.Flush()) + require.NoError(t, d.CheckLevels(nil)) + require.NoError(t, d.Close()) +} + +type testLogger struct { + t testing.TB +} + +func (l testLogger) Infof(format string, args ...interface{}) { + l.t.Logf(format, args...) +} + +func (l testLogger) Errorf(format string, args ...interface{}) { + l.t.Logf(format, args...) +} + +func (l testLogger) Fatalf(format string, args ...interface{}) { + l.t.Fatalf(format, args...) +} + +// TestIngestMemtableOverlapRace is a regression test for the race described in +// #2196. If an ingest that checks for overlap with the mutable memtable and +// finds no overlap, it must not allow overlapping keys with later sequence +// numbers to be applied to the memtable and the memtable to be flushed before +// the ingest completes. +// +// This test operates by committing the same key concurrently: +// - 1 goroutine repeatedly ingests the same sstable writing the key `foo` +// - n goroutines repeatedly apply batches writing the key `foo` and trigger +// flushes. +// +// After a while, the database is closed and the manifest is verified. Version +// edits should contain new files with monotonically increasing sequence +// numbers, since every flush and every ingest conflicts with one another. +func TestIngestMemtableOverlapRace(t *testing.T) { + mem := vfs.NewMem() + el := MakeLoggingEventListener(testLogger{t: t}) + d, err := Open("", &Options{ + FS: mem, + // Disable automatic compactions to keep the manifest clean; only + // flushes and ingests. + DisableAutomaticCompactions: true, + // Disable the WAL to speed up batch commits. + DisableWAL: true, + EventListener: &el, + // We're endlessly appending to L0 without clearing it, so set a maximal + // stop writes threshold. + L0StopWritesThreshold: math.MaxInt, + // Accumulating more than 1 immutable memtable doesn't help us exercise + // the bug, since the committed keys need to be flushed promptly. + MemTableStopWritesThreshold: 2, + }) + require.NoError(t, err) + + // Prepare a sstable `ext` deleting foo. + f, err := mem.Create("ext") + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(t, w.Delete([]byte("foo"))) + require.NoError(t, w.Close()) + + var done atomic.Bool + const numSetters = 2 + var wg sync.WaitGroup + wg.Add(numSetters + 1) + + untilDone := func(fn func()) { + defer wg.Done() + for !done.Load() { + fn() + } + } + + // Ingest in the background. + totalIngests := 0 + go untilDone(func() { + filename := fmt.Sprintf("ext%d", totalIngests) + require.NoError(t, mem.Link("ext", filename)) + require.NoError(t, d.Ingest([]string{filename})) + totalIngests++ + }) + + // Apply batches and trigger flushes in the background. + wo := &WriteOptions{Sync: false} + var localCommits [numSetters]int + for i := 0; i < numSetters; i++ { + i := i + v := []byte(fmt.Sprintf("v%d", i+1)) + go untilDone(func() { + // Commit a batch setting foo=vN. + b := d.NewBatch() + require.NoError(t, b.Set([]byte("foo"), v, nil)) + require.NoError(t, b.Commit(wo)) + localCommits[i]++ + d.AsyncFlush() + }) + } + time.Sleep(100 * time.Millisecond) + done.Store(true) + wg.Wait() + + var totalCommits int + for i := 0; i < numSetters; i++ { + totalCommits += localCommits[i] + } + m := d.Metrics() + tot := m.Total() + t.Logf("Committed %d batches.", totalCommits) + t.Logf("Flushed %d times.", m.Flush.Count) + t.Logf("Ingested %d sstables.", tot.TablesIngested) + require.NoError(t, d.CheckLevels(nil)) + require.NoError(t, d.Close()) + + // Replay the manifest. Every flush and ingest is a separate version edit. + // Since they all write the same key and compactions are disabled, sequence + // numbers of new files should be monotonically increasing. + // + // This check is necessary because most of these sstables are ingested into + // L0. The L0 sublevels construction will order them by LargestSeqNum, even + // if they're added to L0 out-of-order. The CheckLevels call at the end of + // the test may find that the sublevels are all appropriately ordered, but + // the manifest may reveal they were added to the LSM out-of-order. + dbDesc, err := Peek("", mem) + require.NoError(t, err) + require.True(t, dbDesc.Exists) + f, err = mem.Open(dbDesc.ManifestFilename) + require.NoError(t, err) + defer f.Close() + rr := record.NewReader(f, 0 /* logNum */) + var largest *fileMetadata + for { + r, err := rr.Next() + if err == io.EOF || err == record.ErrInvalidChunk { + break + } + require.NoError(t, err) + var ve manifest.VersionEdit + require.NoError(t, ve.Decode(r)) + t.Log(ve.String()) + for _, f := range ve.NewFiles { + if largest != nil { + require.Equal(t, 0, f.Level) + if largest.LargestSeqNum > f.Meta.LargestSeqNum { + t.Fatalf("previous largest file %s has sequence number > next file %s", largest, f.Meta) + } + } + largest = f.Meta + } + } +} + +type ingestCrashFS struct { + vfs.FS +} + +func (fs ingestCrashFS) Link(oldname, newname string) error { + if err := fs.FS.Link(oldname, newname); err != nil { + return err + } + panic(errorfs.ErrInjected) +} + +type noRemoveFS struct { + vfs.FS +} + +func (fs noRemoveFS) Remove(string) error { + return errorfs.ErrInjected +} + +func TestIngestFileNumReuseCrash(t *testing.T) { + const count = 10 + // Use an on-disk filesystem, because Ingest with a MemFS will copy, not + // link the ingested file. + dir, err := os.MkdirTemp("", "ingest-filenum-reuse") + require.NoError(t, err) + defer os.RemoveAll(dir) + fs := vfs.Default + + readFile := func(s string) []byte { + f, err := fs.Open(fs.PathJoin(dir, s)) + require.NoError(t, err) + b, err := io.ReadAll(f) + require.NoError(t, err) + require.NoError(t, f.Close()) + return b + } + + // Create sstables to ingest. + var files []string + var fileBytes [][]byte + for i := 0; i < count; i++ { + name := fmt.Sprintf("ext%d", i) + f, err := fs.Create(fs.PathJoin(dir, name)) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(t, w.Set([]byte(fmt.Sprintf("foo%d", i)), nil)) + require.NoError(t, w.Close()) + files = append(files, name) + fileBytes = append(fileBytes, readFile(name)) + } + + // Open a database with a filesystem that will successfully link the + // ingested files but then panic. This is an approximation of what a crash + // after linking but before updating the manifest would look like. + d, err := Open(dir, &Options{ + FS: ingestCrashFS{FS: fs}, + }) + // A flush here ensures the file num bumps from creating OPTIONS files, + // etc get recorded in the manifest. We want the nextFileNum after the + // restart to be the same as one of our ingested sstables. + require.NoError(t, err) + require.NoError(t, d.Set([]byte("boop"), nil, nil)) + require.NoError(t, d.Flush()) + for _, f := range files { + func() { + defer func() { err = recover().(error) }() + err = d.Ingest([]string{fs.PathJoin(dir, f)}) + }() + if err == nil || !errors.Is(err, errorfs.ErrInjected) { + t.Fatalf("expected injected error, got %v", err) + } + } + // Leave something in the WAL so that Open will flush while replaying the + // WAL. + require.NoError(t, d.Set([]byte("wal"), nil, nil)) + require.NoError(t, d.Close()) + + // There are now two links to each external file: the original extX link + // and a numbered sstable link. The sstable files are still not a part of + // the manifest and so they may be overwritten. Open will detect the + // obsolete number sstables and try to remove them. The FS here is wrapped + // to induce errors on Remove calls. Even if we're unsuccessful in + // removing the obsolete files, the external files should not be + // overwritten. + d, err = Open(dir, &Options{FS: noRemoveFS{FS: fs}}) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("bar"), nil, nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Close()) + + // None of the external files should change despite modifying the linked + // versions. + for i, f := range files { + afterBytes := readFile(f) + require.Equal(t, fileBytes[i], afterBytes) + } +} + +func TestIngest_UpdateSequenceNumber(t *testing.T) { + mem := vfs.NewMem() + cmp := base.DefaultComparer.Compare + parse := func(input string) (*sstable.Writer, error) { + f, err := mem.Create("ext") + if err != nil { + return nil, err + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: sstable.TableFormatMax, + }) + for _, data := range strings.Split(input, "\n") { + if strings.HasPrefix(data, "rangekey: ") { + data = strings.TrimPrefix(data, "rangekey: ") + s := keyspan.ParseSpan(data) + err := rangekey.Encode(&s, w.AddRangeKey) + if err != nil { + return nil, err + } + continue + } + j := strings.Index(data, ":") + if j < 0 { + return nil, errors.Newf("malformed input: %s\n", data) + } + key := base.ParseInternalKey(data[:j]) + value := []byte(data[j+1:]) + if err := w.Add(key, value); err != nil { + return nil, err + } + } + return w, nil + } + + var ( + seqnum uint64 + err error + metas []*fileMetadata + ) + datadriven.RunTest(t, "testdata/ingest_update_seqnums", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "starting-seqnum": + seqnum, err = strconv.ParseUint(td.Input, 10, 64) + if err != nil { + return err.Error() + } + return "" + + case "reset": + metas = metas[:0] + return "" + + case "load": + w, err := parse(td.Input) + if err != nil { + return err.Error() + } + if err = w.Close(); err != nil { + return err.Error() + } + defer w.Close() + + // Format the bounds of the table. + wm, err := w.Metadata() + if err != nil { + return err.Error() + } + + // Upper bounds for range dels and range keys are expected to be sentinel + // keys. + maybeUpdateUpperBound := func(key base.InternalKey) base.InternalKey { + switch k := key.Kind(); { + case k == base.InternalKeyKindRangeDelete: + key.Trailer = base.InternalKeyRangeDeleteSentinel + case rangekey.IsRangeKey(k): + return base.MakeExclusiveSentinelKey(k, key.UserKey) + } + return key + } + + // Construct the file metadata from the writer metadata. + m := &fileMetadata{ + SmallestSeqNum: 0, // Simulate an ingestion. + LargestSeqNum: 0, + } + if wm.HasPointKeys { + m.ExtendPointKeyBounds(cmp, wm.SmallestPoint, wm.LargestPoint) + } + if wm.HasRangeDelKeys { + m.ExtendPointKeyBounds( + cmp, + wm.SmallestRangeDel, + maybeUpdateUpperBound(wm.LargestRangeDel), + ) + } + if wm.HasRangeKeys { + m.ExtendRangeKeyBounds( + cmp, + wm.SmallestRangeKey, + maybeUpdateUpperBound(wm.LargestRangeKey), + ) + } + m.InitPhysicalBacking() + if err := m.Validate(cmp, base.DefaultFormatter); err != nil { + return err.Error() + } + + // Collect this file. + metas = append(metas, m) + + // Return an index number for the file. + return fmt.Sprintf("file %d\n", len(metas)-1) + + case "update-files": + // Update the bounds across all files. + if err = ingestUpdateSeqNum(cmp, base.DefaultFormatter, seqnum, ingestLoadResult{localMeta: metas}); err != nil { + return err.Error() + } + + var buf bytes.Buffer + for i, m := range metas { + fmt.Fprintf(&buf, "file %d:\n", i) + fmt.Fprintf(&buf, " combined: %s-%s\n", m.Smallest, m.Largest) + fmt.Fprintf(&buf, " points: %s-%s\n", m.SmallestPointKey, m.LargestPointKey) + fmt.Fprintf(&buf, " ranges: %s-%s\n", m.SmallestRangeKey, m.LargestRangeKey) + } + + return buf.String() + + default: + return fmt.Sprintf("unknown command %s\n", td.Cmd) + } + }) +} + +func TestIngestCleanup(t *testing.T) { + fns := []base.FileNum{0, 1, 2} + + testCases := []struct { + closeFiles []base.FileNum + cleanupFiles []base.FileNum + wantErr string + }{ + // Close and remove all files. + { + closeFiles: fns, + cleanupFiles: fns, + }, + // Remove a non-existent file. + { + closeFiles: fns, + cleanupFiles: []base.FileNum{3}, + wantErr: "unknown to the objstorage provider", + }, + // Remove a file that has not been closed. + { + closeFiles: []base.FileNum{0, 2}, + cleanupFiles: fns, + wantErr: oserror.ErrInvalid.Error(), + }, + // Remove all files, one of which is still open, plus a file that does not exist. + { + closeFiles: []base.FileNum{0, 2}, + cleanupFiles: []base.FileNum{0, 1, 2, 3}, + wantErr: oserror.ErrInvalid.Error(), // The first error encountered is due to the open file. + }, + } + + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + mem := vfs.NewMem() + mem.UseWindowsSemantics(true) + objProvider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(mem, "")) + require.NoError(t, err) + defer objProvider.Close() + + // Create the files in the VFS. + metaMap := make(map[base.FileNum]objstorage.Writable) + for _, fn := range fns { + w, _, err := objProvider.Create(context.Background(), base.FileTypeTable, fn.DiskFileNum(), objstorage.CreateOptions{}) + require.NoError(t, err) + + metaMap[fn] = w + } + + // Close a select number of files. + for _, m := range tc.closeFiles { + w, ok := metaMap[m] + if !ok { + continue + } + require.NoError(t, w.Finish()) + } + + // Cleanup the set of files in the FS. + var toRemove []*fileMetadata + for _, fn := range tc.cleanupFiles { + m := &fileMetadata{FileNum: fn} + m.InitPhysicalBacking() + toRemove = append(toRemove, m) + } + + err = ingestCleanup(objProvider, toRemove) + if tc.wantErr != "" { + require.Error(t, err, "got no error, expected %s", tc.wantErr) + require.Contains(t, err.Error(), tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +// fatalCapturingLogger captures a fatal error instead of panicking. +type fatalCapturingLogger struct { + t testing.TB + err error +} + +// Infof implements the Logger interface. +func (l *fatalCapturingLogger) Infof(fmt string, args ...interface{}) { + l.t.Logf(fmt, args...) +} + +// Errorf implements the Logger interface. +func (l *fatalCapturingLogger) Errorf(fmt string, args ...interface{}) { + l.t.Logf(fmt, args...) +} + +// Fatalf implements the Logger interface. +func (l *fatalCapturingLogger) Fatalf(_ string, args ...interface{}) { + l.err = args[0].(error) +} + +func TestIngestValidation(t *testing.T) { + type keyVal struct { + key, val []byte + } + // The corruptionLocation enum defines where to corrupt an sstable if + // anywhere. corruptionLocation{Start,End} describe the start and end + // data blocks. corruptionLocationInternal describes a random data block + // that's neither the start or end blocks. The Ingest operation does not + // read the entire sstable, only the start and end blocks, so corruption + // introduced using corruptionLocationInternal will not be discovered until + // the asynchronous validation job runs. + type corruptionLocation int + const ( + corruptionLocationNone corruptionLocation = iota + corruptionLocationStart + corruptionLocationEnd + corruptionLocationInternal + ) + // The errReportLocation type defines an enum to allow tests to enforce + // expectations about how an error surfaced during ingestion or validation + // is reported. Asynchronous validation that uncovers corruption should call + // Fatalf on the Logger. Asychronous validation that encounters + // non-corruption errors should surface it through the + // EventListener.BackgroundError func. + type errReportLocation int + const ( + errReportLocationNone errReportLocation = iota + errReportLocationIngest + errReportLocationFatal + errReportLocationBackgroundError + ) + const ( + nKeys = 1_000 + keySize = 16 + valSize = 100 + blockSize = 100 + + ingestTableName = "ext" + ) + + seed := uint64(time.Now().UnixNano()) + rng := rand.New(rand.NewSource(seed)) + t.Logf("rng seed = %d", seed) + + // errfsCounter is used by test cases that make use of an errorfs.Injector + // to inject errors into the ingest validation code path. + var errfsCounter atomic.Int32 + testCases := []struct { + description string + cLoc corruptionLocation + wantErrType errReportLocation + wantErr error + errorfsInjector errorfs.Injector + }{ + { + description: "no corruption", + cLoc: corruptionLocationNone, + wantErrType: errReportLocationNone, + }, + { + description: "start block", + cLoc: corruptionLocationStart, + wantErr: ErrCorruption, + wantErrType: errReportLocationIngest, + }, + { + description: "end block", + cLoc: corruptionLocationEnd, + wantErr: ErrCorruption, + wantErrType: errReportLocationIngest, + }, + { + description: "non-end block", + cLoc: corruptionLocationInternal, + wantErr: ErrCorruption, + wantErrType: errReportLocationFatal, + }, + { + description: "non-corruption error", + cLoc: corruptionLocationNone, + wantErr: errorfs.ErrInjected, + wantErrType: errReportLocationBackgroundError, + errorfsInjector: errorfs.InjectorFunc(func(op errorfs.Op) error { + // Inject an error on the first read-at operation on an sstable + // (excluding the read on the sstable before ingestion has + // linked it in). + if op.Path != "ext" && op.Kind != errorfs.OpFileReadAt || filepath.Ext(op.Path) != ".sst" { + return nil + } + if errfsCounter.Add(1) == 1 { + return errorfs.ErrInjected + } + return nil + }), + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + errfsCounter.Store(0) + var wg sync.WaitGroup + wg.Add(1) + + fs := vfs.NewMem() + var testFS vfs.FS = fs + if tc.errorfsInjector != nil { + testFS = errorfs.Wrap(fs, tc.errorfsInjector) + } + + // backgroundErr is populated by EventListener.BackgroundError. + var backgroundErr error + logger := &fatalCapturingLogger{t: t} + opts := &Options{ + FS: testFS, + Logger: logger, + EventListener: &EventListener{ + TableValidated: func(i TableValidatedInfo) { + wg.Done() + }, + BackgroundError: func(err error) { + backgroundErr = err + }, + }, + } + // Disable table stats so that injected errors can't be accidentally + // injected into the table stats collector read, and so the table + // stats collector won't prime the table+block cache such that the + // error injection won't trigger at all during ingest validation. + opts.private.disableTableStats = true + opts.Experimental.ValidateOnIngest = true + d, err := Open("", opts) + require.NoError(t, err) + defer func() { require.NoError(t, d.Close()) }() + + corrupt := func(f vfs.File) { + readable, err := sstable.NewSimpleReadable(f) + require.NoError(t, err) + // Compute the layout of the sstable in order to find the + // appropriate block locations to corrupt. + r, err := sstable.NewReader(readable, sstable.ReaderOptions{}) + require.NoError(t, err) + l, err := r.Layout() + require.NoError(t, err) + + // Select an appropriate data block to corrupt. + var blockIdx int + switch tc.cLoc { + case corruptionLocationStart: + blockIdx = 0 + case corruptionLocationEnd: + blockIdx = len(l.Data) - 1 + case corruptionLocationInternal: + blockIdx = 1 + rng.Intn(len(l.Data)-2) + default: + t.Fatalf("unknown corruptionLocation: %T", tc.cLoc) + } + bh := l.Data[blockIdx] + + // Corrupting a key will cause the ingestion to fail due to a + // malformed key, rather than a block checksum mismatch. + // Instead, we corrupt the last byte in the selected block, + // before the trailer, which corresponds to a value. + offset := bh.Offset + bh.Length - 1 + _, err = f.WriteAt([]byte("\xff"), int64(offset)) + require.NoError(t, err) + require.NoError(t, r.Close()) + } + + type errT struct { + errLoc errReportLocation + err error + } + runIngest := func(keyVals []keyVal) (et errT) { + f, err := fs.Create(ingestTableName) + require.NoError(t, err) + defer func() { _ = fs.Remove(ingestTableName) }() + + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + BlockSize: blockSize, // Create many smaller blocks. + Compression: NoCompression, // For simpler debugging. + }) + for _, kv := range keyVals { + require.NoError(t, w.Set(kv.key, kv.val)) + } + require.NoError(t, w.Close()) + + // Possibly corrupt the file. + if tc.cLoc != corruptionLocationNone { + f, err = fs.OpenReadWrite(ingestTableName) + require.NoError(t, err) + corrupt(f) + } + + // Ingest the external table. + err = d.Ingest([]string{ingestTableName}) + if err != nil { + et.errLoc = errReportLocationIngest + et.err = err + return + } + + // Wait for the validation on the sstable to complete. + wg.Wait() + + // Return any error encountered during validation. + if logger.err != nil { + et.errLoc = errReportLocationFatal + et.err = logger.err + } else if backgroundErr != nil { + et.errLoc = errReportLocationBackgroundError + et.err = backgroundErr + } + return + } + + // Construct a set of keys to ingest. + var keyVals []keyVal + for i := 0; i < nKeys; i++ { + key := make([]byte, keySize) + _, err = rng.Read(key) + require.NoError(t, err) + + val := make([]byte, valSize) + _, err = rng.Read(val) + require.NoError(t, err) + + keyVals = append(keyVals, keyVal{key, val}) + } + + // Keys must be sorted. + slices.SortFunc(keyVals, func(a, b keyVal) int { return d.cmp(a.key, b.key) }) + + // Run the ingestion. + et := runIngest(keyVals) + + // Assert we saw the errors we expect. + switch tc.wantErrType { + case errReportLocationNone: + require.Equal(t, errReportLocationNone, et.errLoc) + require.NoError(t, et.err) + case errReportLocationIngest: + require.Equal(t, errReportLocationIngest, et.errLoc) + require.Error(t, et.err) + require.True(t, errors.Is(et.err, tc.wantErr)) + case errReportLocationFatal: + require.Equal(t, errReportLocationFatal, et.errLoc) + require.Error(t, et.err) + require.True(t, errors.Is(et.err, tc.wantErr)) + case errReportLocationBackgroundError: + require.Equal(t, errReportLocationBackgroundError, et.errLoc) + require.Error(t, et.err) + require.True(t, errors.Is(et.err, tc.wantErr)) + default: + t.Fatalf("unknown wantErrType %T", tc.wantErrType) + } + }) + } +} + +// BenchmarkManySSTables measures the cost of various operations with various +// counts of SSTables within the database. +func BenchmarkManySSTables(b *testing.B) { + counts := []int{10, 1_000, 10_000, 100_000, 1_000_000} + ops := []string{"ingest", "calculateInuseKeyRanges"} + for _, op := range ops { + b.Run(op, func(b *testing.B) { + for _, count := range counts { + b.Run(fmt.Sprintf("sstables=%d", count), func(b *testing.B) { + mem := vfs.NewMem() + d, err := Open("", &Options{ + FS: mem, + }) + require.NoError(b, err) + + var paths []string + for i := 0; i < count; i++ { + n := fmt.Sprintf("%07d", i) + f, err := mem.Create(n) + require.NoError(b, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(b, w.Set([]byte(n), nil)) + require.NoError(b, w.Close()) + paths = append(paths, n) + } + require.NoError(b, d.Ingest(paths)) + + { + const broadIngest = "broad.sst" + f, err := mem.Create(broadIngest) + require.NoError(b, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(b, w.Set([]byte("0"), nil)) + require.NoError(b, w.Set([]byte("Z"), nil)) + require.NoError(b, w.Close()) + require.NoError(b, d.Ingest([]string{broadIngest})) + } + + switch op { + case "ingest": + runBenchmarkManySSTablesIngest(b, d, mem, count) + case "calculateInuseKeyRanges": + runBenchmarkManySSTablesInUseKeyRanges(b, d, count) + } + require.NoError(b, d.Close()) + }) + } + }) + } +} + +func runBenchmarkManySSTablesIngest(b *testing.B, d *DB, fs vfs.FS, count int) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + n := fmt.Sprintf("%07d", count+i) + f, err := fs.Create(n) + require.NoError(b, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(b, w.Set([]byte(n), nil)) + require.NoError(b, w.Close()) + require.NoError(b, d.Ingest([]string{n})) + } +} + +func runBenchmarkManySSTablesInUseKeyRanges(b *testing.B, d *DB, count int) { + // This benchmark is pretty contrived, but it's not easy to write a + // microbenchmark for this in a more natural way. L6 has many files, and + // L5 has 1 file spanning the entire breadth of L5. + d.mu.Lock() + defer d.mu.Unlock() + v := d.mu.versions.currentVersion() + b.ResetTimer() + + smallest := []byte("0") + largest := []byte("z") + for i := 0; i < b.N; i++ { + _ = calculateInuseKeyRanges(v, d.cmp, 0, numLevels-1, smallest, largest) + } +} diff --git a/pebble/internal.go b/pebble/internal.go new file mode 100644 index 0000000..61a4284 --- /dev/null +++ b/pebble/internal.go @@ -0,0 +1,51 @@ +// Copyright 2018 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 "github.com/cockroachdb/pebble/internal/base" + +// InternalKeyKind exports the base.InternalKeyKind type. +type InternalKeyKind = base.InternalKeyKind + +// These constants are part of the file format, and should not be changed. +const ( + InternalKeyKindDelete = base.InternalKeyKindDelete + InternalKeyKindSet = base.InternalKeyKindSet + InternalKeyKindMerge = base.InternalKeyKindMerge + InternalKeyKindLogData = base.InternalKeyKindLogData + InternalKeyKindSingleDelete = base.InternalKeyKindSingleDelete + InternalKeyKindRangeDelete = base.InternalKeyKindRangeDelete + InternalKeyKindMax = base.InternalKeyKindMax + InternalKeyKindSetWithDelete = base.InternalKeyKindSetWithDelete + InternalKeyKindRangeKeySet = base.InternalKeyKindRangeKeySet + InternalKeyKindRangeKeyUnset = base.InternalKeyKindRangeKeyUnset + InternalKeyKindRangeKeyDelete = base.InternalKeyKindRangeKeyDelete + InternalKeyKindIngestSST = base.InternalKeyKindIngestSST + InternalKeyKindDeleteSized = base.InternalKeyKindDeleteSized + InternalKeyKindInvalid = base.InternalKeyKindInvalid + InternalKeySeqNumBatch = base.InternalKeySeqNumBatch + InternalKeySeqNumMax = base.InternalKeySeqNumMax + InternalKeyRangeDeleteSentinel = base.InternalKeyRangeDeleteSentinel +) + +// InternalKey exports the base.InternalKey type. +type InternalKey = base.InternalKey + +type internalIterator = base.InternalIterator + +// ErrCorruption is a marker to indicate that data in a file (WAL, MANIFEST, +// sstable) isn't in the expected format. +var ErrCorruption = base.ErrCorruption + +// AttributeAndLen exports the base.AttributeAndLen type. +type AttributeAndLen = base.AttributeAndLen + +// ShortAttribute exports the base.ShortAttribute type. +type ShortAttribute = base.ShortAttribute + +// LazyFetcher exports the base.LazyFetcher type. This export is needed since +// LazyValue.Clone requires a pointer to a LazyFetcher struct to avoid +// allocations. No code outside Pebble needs to peer into a LazyFetcher. +type LazyFetcher = base.LazyFetcher diff --git a/pebble/internal/ackseq/ackseq.go b/pebble/internal/ackseq/ackseq.go new file mode 100644 index 0000000..f2c682f --- /dev/null +++ b/pebble/internal/ackseq/ackseq.go @@ -0,0 +1,83 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package ackseq + +import ( + "sync" + "sync/atomic" + + "github.com/cockroachdb/errors" +) + +const ( + // The window size constants. These values specify a window that can hold ~1m + // pending unacknowledged sequence numbers using 128 KB of memory. + windowSize = 1 << 20 + windowMask = windowSize - 1 + windowBytes = (windowSize + 7) / 8 +) + +// S keeps track of the largest sequence number such that all sequence numbers +// in the range [0,v) have been acknowledged. +type S struct { + next atomic.Uint64 + mu struct { + sync.Mutex + base uint64 + window [windowBytes]uint8 + } +} + +// New creates a new acknowledged sequence tracker with the specified base +// sequence number. All of the sequence numbers in the range [0,base) are +// considered acknowledged. Next() will return base upon first call. +func New(base uint64) *S { + s := &S{} + s.next.Store(base) + s.mu.base = base + return s +} + +// Next returns the next sequence number to use. +func (s *S) Next() uint64 { + return s.next.Add(1) - 1 +} + +// Ack acknowledges the specified seqNum, adjusting base as necessary, +// returning the number of newly acknowledged sequence numbers. +func (s *S) Ack(seqNum uint64) (int, error) { + s.mu.Lock() + if s.getLocked(seqNum) { + defer s.mu.Unlock() + return 0, errors.Errorf( + "pending acks exceeds window size: %d has been acked, but %d has not", + errors.Safe(seqNum), errors.Safe(s.mu.base)) + } + + var count int + s.setLocked(seqNum) + for s.getLocked(s.mu.base) { + s.clearLocked(s.mu.base) + s.mu.base++ + count++ + } + s.mu.Unlock() + return count, nil +} + +func (s *S) getLocked(seqNum uint64) bool { + bit := seqNum & windowMask + return (s.mu.window[bit/8] & (1 << (bit % 8))) != 0 +} + +func (s *S) setLocked(seqNum uint64) { + bit := seqNum & windowMask + s.mu.window[bit/8] |= (1 << (bit % 8)) +} + +func (s *S) clearLocked(seqNum uint64) { + bit := seqNum & windowMask + s.mu.window[bit/8] &^= (1 << (bit % 8)) +} diff --git a/pebble/internal/arenaskl/LICENSE b/pebble/internal/arenaskl/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/pebble/internal/arenaskl/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/pebble/internal/arenaskl/README.md b/pebble/internal/arenaskl/README.md new file mode 100644 index 0000000..93a7d32 --- /dev/null +++ b/pebble/internal/arenaskl/README.md @@ -0,0 +1,93 @@ +# arenaskl + +Fast, lock-free, arena-based Skiplist implementation in Go that supports iteration +in both directions. + +## Advantages + +Arenaskl offers several advantages over other skiplist implementations: + +* High performance that linearly scales with the number of cores. This is + achieved by allocating from a fixed-size arena and by avoiding locks. +* Iterators that can be allocated on the stack and easily cloned by value. +* Simple-to-use and low overhead model for detecting and handling race conditions + with other threads. +* Support for iterating in reverse (i.e. previous links). + +## Limitations + +The advantages come at a cost that prevents arenaskl from being a general-purpose +skiplist implementation: + +* The size of the arena sets a hard upper bound on the combined size of skiplist + nodes, keys, and values. This limit includes even the size of deleted nodes, + keys, and values. +* Deletion is not supported. Instead, higher-level code is expected to + add deletion tombstones and needs to process those tombstones + appropriately. + +## Pedigree + +This code is based on Andy Kimball's arenaskl code: + +https://github.com/andy-kimball/arenaskl + +The arenaskl code is based on the skiplist found in Badger, a Go-based +KV store: + +https://github.com/dgraph-io/badger/tree/master/skl + +The skiplist in Badger is itself based on a C++ skiplist built for +Facebook's RocksDB: + +https://github.com/facebook/rocksdb/tree/master/memtable + +## Benchmarks + +The benchmarks consist of a mix of reads and writes executed in parallel. The +fraction of reads is indicated in the run name: "frac_X" indicates a run where +X percent of the operations are reads. + +The results are much better than `skiplist` and `slist`. + +``` +name time/op +ReadWrite/frac_0-8 470ns ±11% +ReadWrite/frac_10-8 462ns ± 3% +ReadWrite/frac_20-8 436ns ± 2% +ReadWrite/frac_30-8 410ns ± 2% +ReadWrite/frac_40-8 385ns ± 2% +ReadWrite/frac_50-8 360ns ± 4% +ReadWrite/frac_60-8 386ns ± 1% +ReadWrite/frac_70-8 352ns ± 2% +ReadWrite/frac_80-8 306ns ± 3% +ReadWrite/frac_90-8 253ns ± 4% +ReadWrite/frac_100-8 28.1ns ± 2% +``` + +Note that the above numbers are for concurrent operations using 8x +parallelism. The same benchmarks without concurrency (use these +numbers when comparing vs batchskl): + +``` +name time/op +ReadWrite/frac_0 1.53µs ± 1% +ReadWrite/frac_10 1.46µs ± 2% +ReadWrite/frac_20 1.39µs ± 3% +ReadWrite/frac_30 1.28µs ± 3% +ReadWrite/frac_40 1.21µs ± 2% +ReadWrite/frac_50 1.11µs ± 3% +ReadWrite/frac_60 1.23µs ±17% +ReadWrite/frac_70 1.16µs ± 4% +ReadWrite/frac_80 959ns ± 3% +ReadWrite/frac_90 738ns ± 5% +ReadWrite/frac_100 81.9ns ± 2% +``` + +Forward and backward iteration are also fast: + +``` +name time/op +IterNext 3.97ns ± 5% +IterPrev 3.88ns ± 3% +``` diff --git a/pebble/internal/arenaskl/arena.go b/pebble/internal/arenaskl/arena.go new file mode 100644 index 0000000..011c3b0 --- /dev/null +++ b/pebble/internal/arenaskl/arena.go @@ -0,0 +1,125 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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 arenaskl + +import ( + "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 atomic.Uint64 + buf []byte +} + +const nodeAlignment = 4 + +var ( + // ErrArenaFull indicates that the arena is full and cannot perform any more + // allocations. + ErrArenaFull = errors.New("allocation failed because arena is full") +) + +// NewArena allocates a new arena using the specified buffer as the backing +// store. +func NewArena(buf []byte) *Arena { + 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 := 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) +} + +// Capacity returns the capacity of the arena. +func (a *Arena) Capacity() uint32 { + return uint32(len(a.buf)) +} + +// 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 := 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 := uint64(size) + uint64(alignment) - 1 + + newSize := a.n.Add(padded) + if newSize+uint64(overflow) > uint64(len(a.buf)) { + return 0, 0, ErrArenaFull + } + + // Return the aligned offset. + offset := (uint32(newSize) - size) & ^(alignment - 1) + return offset, uint32(padded), nil +} + +func (a *Arena) getBytes(offset uint32, size uint32) []byte { + if offset == 0 { + return nil + } + return a.buf[offset : offset+size : offset+size] +} + +func (a *Arena) getPointer(offset uint32) unsafe.Pointer { + if offset == 0 { + return nil + } + return unsafe.Pointer(&a.buf[offset]) +} + +func (a *Arena) getPointerOffset(ptr unsafe.Pointer) uint32 { + if ptr == nil { + return 0 + } + return uint32(uintptr(ptr) - uintptr(unsafe.Pointer(&a.buf[0]))) +} diff --git a/pebble/internal/arenaskl/arena_test.go b/pebble/internal/arenaskl/arena_test.go new file mode 100644 index 0000000..f264b8d --- /dev/null +++ b/pebble/internal/arenaskl/arena_test.go @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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 arenaskl + +import ( + "math" + "testing" + + "github.com/cockroachdb/pebble/internal/constants" + "github.com/stretchr/testify/require" +) + +func newArena(n uint32) *Arena { + return NewArena(make([]byte, n)) +} + +// TestArenaSizeOverflow tests that large allocations do not cause Arena's +// internal size accounting to overflow and produce incorrect results. +func TestArenaSizeOverflow(t *testing.T) { + a := newArena(constants.MaxUint32OrInt) + + // Allocating under the limit throws no error. + offset, _, err := a.alloc(math.MaxUint16, 1, 0) + require.Nil(t, err) + require.Equal(t, uint32(1), offset) + require.Equal(t, uint32(math.MaxUint16)+1, a.Size()) + + // Allocating over the limit could cause an accounting + // overflow if 32-bit arithmetic was used. It shouldn't. + _, _, err = a.alloc(math.MaxUint32, 1, 0) + require.Equal(t, ErrArenaFull, err) + require.Equal(t, uint32(constants.MaxUint32OrInt), a.Size()) + + // Continuing to allocate continues to throw an error. + _, _, err = a.alloc(math.MaxUint16, 1, 0) + require.Equal(t, ErrArenaFull, err) + require.Equal(t, uint32(constants.MaxUint32OrInt), a.Size()) +} diff --git a/pebble/internal/arenaskl/flush_iterator.go b/pebble/internal/arenaskl/flush_iterator.go new file mode 100644 index 0000000..2a7ea03 --- /dev/null +++ b/pebble/internal/arenaskl/flush_iterator.go @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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 arenaskl + +import "github.com/cockroachdb/pebble/internal/base" + +// flushIterator is an iterator over the skiplist object. Use Skiplist.NewFlushIter +// to construct an iterator. The current state of the iterator can be cloned by +// simply value copying the struct. +type flushIterator struct { + Iterator + bytesIterated *uint64 +} + +// flushIterator implements the base.InternalIterator interface. +var _ base.InternalIterator = (*flushIterator)(nil) + +func (it *flushIterator) String() string { + return "memtable" +} + +func (it *flushIterator) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("pebble: SeekGE unimplemented") +} + +func (it *flushIterator) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("pebble: SeekPrefixGE unimplemented") +} + +func (it *flushIterator) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*base.InternalKey, base.LazyValue) { + panic("pebble: SeekLT unimplemented") +} + +// First seeks position at the first entry in list. Returns the key and value +// if the iterator is pointing at a valid entry, and (nil, nil) otherwise. 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. +func (it *flushIterator) First() (*base.InternalKey, base.LazyValue) { + key, val := it.Iterator.First() + if key == nil { + return nil, base.LazyValue{} + } + *it.bytesIterated += uint64(it.nd.allocSize) + return key, val +} + +// Next advances to the next position. Returns the key and value if the +// iterator is pointing at a valid entry, and (nil, nil) otherwise. +// Note: flushIterator.Next mirrors the implementation of Iterator.Next +// due to performance. Keep the two in sync. +func (it *flushIterator) Next() (*base.InternalKey, base.LazyValue) { + it.nd = it.list.getNext(it.nd, 0) + if it.nd == it.list.tail { + return nil, base.LazyValue{} + } + it.decodeKey() + *it.bytesIterated += uint64(it.nd.allocSize) + return &it.key, base.MakeInPlaceValue(it.value()) +} + +func (it *flushIterator) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) { + panic("pebble: NextPrefix unimplemented") +} + +func (it *flushIterator) Prev() (*base.InternalKey, base.LazyValue) { + panic("pebble: Prev unimplemented") +} diff --git a/pebble/internal/arenaskl/iterator.go b/pebble/internal/arenaskl/iterator.go new file mode 100644 index 0000000..a41dd7e --- /dev/null +++ b/pebble/internal/arenaskl/iterator.go @@ -0,0 +1,275 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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 arenaskl + +import ( + "context" + "sync" + + "github.com/cockroachdb/pebble/internal/base" +) + +type splice struct { + prev *node + next *node +} + +func (s *splice) init(prev, next *node) { + s.prev = prev + s.next = next +} + +// Iterator is an iterator over the skiplist object. Use Skiplist.NewIter +// to construct an iterator. The current state of the iterator can be cloned by +// simply value copying the struct. All iterator methods are thread-safe. +type Iterator struct { + list *Skiplist + nd *node + key base.InternalKey + lower []byte + upper []byte +} + +// Iterator implements the base.InternalIterator interface. +var _ base.InternalIterator = (*Iterator)(nil) + +var iterPool = sync.Pool{ + New: func() interface{} { + return &Iterator{} + }, +} + +// Close resets the iterator. +func (it *Iterator) Close() error { + it.list = nil + it.nd = nil + it.lower = nil + it.upper = nil + iterPool.Put(it) + return nil +} + +func (it *Iterator) String() string { + return "memtable" +} + +// Error returns any accumulated error. +func (it *Iterator) Error() error { + return nil +} + +// SeekGE moves the iterator to the first entry whose key is greater than or +// equal to the given key. Returns the key and value if the iterator is +// pointing at a valid entry, and (nil, nil) otherwise. 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 (it *Iterator) SeekGE(key []byte, flags base.SeekGEFlags) (*base.InternalKey, base.LazyValue) { + if flags.TrySeekUsingNext() { + if it.nd == it.list.tail { + // Iterator is done. + return nil, base.LazyValue{} + } + less := it.list.cmp(it.key.UserKey, key) < 0 + // Arbitrary constant. By measuring the seek cost as a function of the + // number of elements in the skip list, and fitting to a model, we + // could adjust the number of nexts based on the current size of the + // skip list. + const numNexts = 5 + for i := 0; less && i < numNexts; i++ { + k, _ := it.Next() + if k == nil { + // Iterator is done. + return nil, base.LazyValue{} + } + less = it.list.cmp(it.key.UserKey, key) < 0 + } + if !less { + return &it.key, base.MakeInPlaceValue(it.value()) + } + } + _, it.nd, _ = it.seekForBaseSplice(key) + if it.nd == it.list.tail { + return nil, base.LazyValue{} + } + it.decodeKey() + if it.upper != nil && it.list.cmp(it.upper, it.key.UserKey) <= 0 { + it.nd = it.list.tail + return nil, base.LazyValue{} + } + return &it.key, base.MakeInPlaceValue(it.value()) +} + +// SeekPrefixGE moves the iterator to the first entry whose key is greater than +// or equal to the given key. This method is equivalent to SeekGE and is +// provided so that an arenaskl.Iterator implements the +// internal/base.InternalIterator interface. +func (it *Iterator) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + return it.SeekGE(key, flags) +} + +// SeekLT moves the iterator to the last entry whose key is less than the given +// key. Returns the key and value if the iterator is pointing at a valid entry, +// and (nil, nil) otherwise. 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 (it *Iterator) SeekLT(key []byte, flags base.SeekLTFlags) (*base.InternalKey, base.LazyValue) { + // NB: the top-level Iterator has already adjusted key based on + // the upper-bound. + it.nd, _, _ = it.seekForBaseSplice(key) + if it.nd == it.list.head { + return nil, base.LazyValue{} + } + it.decodeKey() + if it.lower != nil && it.list.cmp(it.lower, it.key.UserKey) > 0 { + it.nd = it.list.head + return nil, base.LazyValue{} + } + return &it.key, base.MakeInPlaceValue(it.value()) +} + +// First seeks position at the first entry in list. Returns the key and value +// if the iterator is pointing at a valid entry, and (nil, nil) otherwise. 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 (it *Iterator) First() (*base.InternalKey, base.LazyValue) { + it.nd = it.list.getNext(it.list.head, 0) + if it.nd == it.list.tail { + return nil, base.LazyValue{} + } + it.decodeKey() + if it.upper != nil && it.list.cmp(it.upper, it.key.UserKey) <= 0 { + it.nd = it.list.tail + return nil, base.LazyValue{} + } + return &it.key, base.MakeInPlaceValue(it.value()) +} + +// Last seeks position at the last entry in list. Returns the key and value if +// the iterator is pointing at a valid entry, and (nil, nil) otherwise. 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 (it *Iterator) Last() (*base.InternalKey, base.LazyValue) { + it.nd = it.list.getPrev(it.list.tail, 0) + if it.nd == it.list.head { + return nil, base.LazyValue{} + } + it.decodeKey() + if it.lower != nil && it.list.cmp(it.lower, it.key.UserKey) > 0 { + it.nd = it.list.head + return nil, base.LazyValue{} + } + return &it.key, base.MakeInPlaceValue(it.value()) +} + +// Next advances to the next position. Returns the key and value if the +// iterator is pointing at a valid entry, and (nil, nil) otherwise. +// Note: flushIterator.Next mirrors the implementation of Iterator.Next +// due to performance. Keep the two in sync. +func (it *Iterator) Next() (*base.InternalKey, base.LazyValue) { + it.nd = it.list.getNext(it.nd, 0) + if it.nd == it.list.tail { + return nil, base.LazyValue{} + } + it.decodeKey() + if it.upper != nil && it.list.cmp(it.upper, it.key.UserKey) <= 0 { + it.nd = it.list.tail + return nil, base.LazyValue{} + } + return &it.key, base.MakeInPlaceValue(it.value()) +} + +// NextPrefix advances to the next position with a new prefix. Returns the key +// and value if the iterator is pointing at a valid entry, and (nil, nil) +// otherwise. +func (it *Iterator) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) { + return it.SeekGE(succKey, base.SeekGEFlagsNone.EnableTrySeekUsingNext()) +} + +// Prev moves to the previous position. Returns the key and value if the +// iterator is pointing at a valid entry, and (nil, nil) otherwise. +func (it *Iterator) Prev() (*base.InternalKey, base.LazyValue) { + it.nd = it.list.getPrev(it.nd, 0) + if it.nd == it.list.head { + return nil, base.LazyValue{} + } + it.decodeKey() + if it.lower != nil && it.list.cmp(it.lower, it.key.UserKey) > 0 { + it.nd = it.list.head + return nil, base.LazyValue{} + } + return &it.key, base.MakeInPlaceValue(it.value()) +} + +// value returns the value at the current position. +func (it *Iterator) value() []byte { + return it.nd.getValue(it.list.arena) +} + +// Head true iff the iterator is positioned at the sentinel head node. +func (it *Iterator) Head() bool { + return it.nd == it.list.head +} + +// Tail true iff the iterator is positioned at the sentinel tail node. +func (it *Iterator) Tail() bool { + return it.nd == it.list.tail +} + +// SetBounds sets the lower and upper bounds for the iterator. Note that the +// result of Next and Prev will be undefined until the iterator has been +// repositioned with SeekGE, SeekPrefixGE, SeekLT, First, or Last. +func (it *Iterator) SetBounds(lower, upper []byte) { + it.lower = lower + it.upper = upper +} + +// SetContext implements base.InternalIterator. +func (it *Iterator) SetContext(_ context.Context) {} + +func (it *Iterator) decodeKey() { + 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) { + ikey := base.MakeSearchKey(key) + level := int(it.list.Height() - 1) + + prev = it.list.head + for { + prev, next, found = it.list.findSpliceForLevel(ikey, level, prev) + + if found { + if level != 0 { + // next is pointing at the target node, but we need to find previous on + // the bottom level. + prev = it.list.getPrev(next, 0) + } + break + } + + if level == 0 { + break + } + + level-- + } + + return +} diff --git a/pebble/internal/arenaskl/node.go b/pebble/internal/arenaskl/node.go new file mode 100644 index 0000000..d464bc5 --- /dev/null +++ b/pebble/internal/arenaskl/node.go @@ -0,0 +1,133 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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 arenaskl + +import ( + "math" + "sync/atomic" + + "github.com/cockroachdb/pebble/internal/base" +) + +// MaxNodeSize returns the maximum space needed for a node with the specified +// key and value sizes. This could overflow a uint32, which is why a uint64 +// is used here. If a key/value overflows a uint32, it should not be added to +// the skiplist. +func MaxNodeSize(keySize, valueSize uint32) uint64 { + const maxPadding = nodeAlignment - 1 + return uint64(maxNodeSize) + uint64(keySize) + uint64(valueSize) + maxPadding +} + +type links struct { + nextOffset atomic.Uint32 + prevOffset atomic.Uint32 +} + +func (l *links) init(prevOffset, nextOffset uint32) { + 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 + 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 + // these elements are never accessed, they do not need to be allocated. + // Therefore, when a node is allocated in the arena, its memory footprint + // is deliberately truncated to not include unneeded tower elements. + // + // All accesses to elements should use CAS operations, with no need to lock. + tower [maxHeight]links +} + +func newNode( + arena *Arena, height uint32, key base.InternalKey, value []byte, +) (nd *node, err error) { + if height < 1 || height > maxHeight { + panic("height cannot be less than one or greater than the max height") + } + keySize := len(key.UserKey) + if int64(keySize) > math.MaxUint32 { + panic("key is too large") + } + valueSize := len(value) + if int64(len(value)) > math.MaxUint32 { + panic("value is too large") + } + if int64(len(value))+int64(keySize)+int64(maxNodeSize) > math.MaxUint32 { + panic("combined key and value size is too large") + } + + nd, err = newRawNode(arena, height, uint32(keySize), uint32(valueSize)) + if err != nil { + return + } + nd.keyTrailer = key.Trailer + copy(nd.getKeyBytes(arena), key.UserKey) + copy(nd.getValue(arena), value) + return +} + +func newRawNode(arena *Arena, height uint32, keySize, valueSize uint32) (nd *node, err error) { + // Compute the amount of the tower that will never be used, since the height + // is less than maxHeight. + unusedSize := uint32((maxHeight - int(height)) * linksSize) + nodeSize := uint32(maxNodeSize) - unusedSize + + nodeOffset, allocSize, err := arena.alloc(nodeSize+keySize+valueSize, nodeAlignment, unusedSize) + if err != nil { + return + } + + nd = (*node)(arena.getPointer(nodeOffset)) + nd.keyOffset = nodeOffset + nodeSize + nd.keySize = keySize + nd.valueSize = valueSize + nd.allocSize = allocSize + return +} + +func (n *node) getKeyBytes(arena *Arena) []byte { + return arena.getBytes(n.keyOffset, n.keySize) +} + +func (n *node) getValue(arena *Arena) []byte { + return arena.getBytes(n.keyOffset+n.keySize, uint32(n.valueSize)) +} + +func (n *node) nextOffset(h int) uint32 { + return n.tower[h].nextOffset.Load() +} + +func (n *node) prevOffset(h int) uint32 { + return n.tower[h].prevOffset.Load() +} + +func (n *node) casNextOffset(h int, old, val uint32) bool { + return n.tower[h].nextOffset.CompareAndSwap(old, val) +} + +func (n *node) casPrevOffset(h int, old, val uint32) bool { + return n.tower[h].prevOffset.CompareAndSwap(old, val) +} diff --git a/pebble/internal/arenaskl/race_test.go b/pebble/internal/arenaskl/race_test.go new file mode 100644 index 0000000..b9310c9 --- /dev/null +++ b/pebble/internal/arenaskl/race_test.go @@ -0,0 +1,42 @@ +//go:build race +// +build race + +// Copyright 2020 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 arenaskl + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestNodeArenaEnd tests allocating a node at the boundary of an arena. In Go +// 1.14 when the race detector is running, Go will also perform some pointer +// alignment checks. It will detect alignment issues, for example #667 where a +// node's memory would straddle the arena boundary, with unused regions of the +// node struct dipping into unallocated memory. This test is only run when the +// race build tag is provided. +func TestNodeArenaEnd(t *testing.T) { + ikey := makeIkey("a") + val := []byte("b") + + // Rather than hardcode an arena size at just the right size, try + // allocating using successively larger arena sizes until we allocate + // successfully. The prior attempt will have exercised the right code + // path. + for i := uint32(1); i < 256; i++ { + a := newArena(i) + _, err := newNode(a, 1, ikey, val) + if err == nil { + // We reached an arena size big enough to allocate a node. + // If there's an issue at the boundary, the race detector would + // have found it by now. + t.Log(i) + break + } + require.Equal(t, ErrArenaFull, err) + } +} diff --git a/pebble/internal/arenaskl/skl.go b/pebble/internal/arenaskl/skl.go new file mode 100644 index 0000000..ef1ebfc --- /dev/null +++ b/pebble/internal/arenaskl/skl.go @@ -0,0 +1,464 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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. + */ + +/* +Adapted from RocksDB inline skiplist. + +Key differences: +- No optimization for sequential inserts (no "prev"). +- No custom comparator. +- Support overwrites. This requires care when we see the same key when inserting. + For RocksDB or LevelDB, overwrites are implemented as a newer sequence number in the key, so + there is no need for values. We don't intend to support versioning. In-place updates of values + would be more efficient. +- We discard all non-concurrent code. +- We do not support Splices. This simplifies the code a lot. +- No AllocateNode or other pointer arithmetic. +- We combine the findLessThan, findGreaterOrEqual, etc into one function. +*/ + +/* +Further adapted from Badger: https://github.com/dgraph-io/badger. + +Key differences: +- Support for previous pointers - doubly linked lists. Note that it's up to higher + level code to deal with the intermediate state that occurs during insertion, + where node A is linked to node B, but node B is not yet linked back to node A. +- Iterator includes mutator functions. +*/ + +package arenaskl // import "github.com/cockroachdb/pebble/internal/arenaskl" + +import ( + "math" + "runtime" + "sync/atomic" + "unsafe" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/fastrand" +) + +const ( + maxHeight = 20 + maxNodeSize = int(unsafe.Sizeof(node{})) + linksSize = int(unsafe.Sizeof(links{})) + pValue = 1 / math.E +) + +// ErrRecordExists indicates that an entry with the specified key already +// exists in the skiplist. Duplicate entries are not directly supported and +// instead must be handled by the user by appending a unique version suffix to +// keys. +var ErrRecordExists = errors.New("record with this key already exists") + +// 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 +// entries that shadow existing entries and perform deletion via tombstones. It +// is up to the user to process these shadow entries and tombstones +// appropriately during retrieval. +type Skiplist struct { + arena *Arena + cmp base.Compare + head *node + tail *node + 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. + testing bool +} + +// Inserter TODO(peter) +type Inserter struct { + spl [maxHeight]splice + height uint32 +} + +// Add TODO(peter) +func (ins *Inserter) Add(list *Skiplist, key base.InternalKey, value []byte) error { + return list.addInternal(key, value, ins) +} + +var ( + probabilities [maxHeight]uint32 +) + +func init() { + // Precompute the skiplist probabilities so that only a single random number + // needs to be generated and so that the optimal pvalue can be used (inverse + // of Euler's number). + p := float64(1.0) + for i := 0; i < maxHeight; i++ { + probabilities[i] = uint32(float64(math.MaxUint32) * p) + p *= pValue + } +} + +// NewSkiplist constructs and initializes a new, empty skiplist. All nodes, keys, +// and values in the skiplist will be allocated from the given arena. +func NewSkiplist(arena *Arena, cmp base.Compare) *Skiplist { + skl := &Skiplist{} + skl.Reset(arena, cmp) + return skl +} + +// Reset the skiplist to empty and re-initialize. +func (s *Skiplist) Reset(arena *Arena, cmp base.Compare) { + // Allocate head and tail nodes. + head, err := newRawNode(arena, maxHeight, 0, 0) + if err != nil { + panic("arenaSize is not large enough to hold the head node") + } + head.keyOffset = 0 + + tail, err := newRawNode(arena, maxHeight, 0, 0) + if err != nil { + panic("arenaSize is not large enough to hold the tail node") + } + tail.keyOffset = 0 + + // Link all head/tail levels together. + headOffset := arena.getPointerOffset(unsafe.Pointer(head)) + tailOffset := arena.getPointerOffset(unsafe.Pointer(tail)) + for i := 0; i < maxHeight; i++ { + head.tower[i].nextOffset.Store(tailOffset) + tail.tower[i].prevOffset.Store(headOffset) + } + + *s = Skiplist{ + 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 s.height.Load() } + +// Arena returns the arena backing this skiplist. +func (s *Skiplist) Arena() *Arena { return s.arena } + +// Size returns the number of bytes that have allocated from the arena. +func (s *Skiplist) Size() uint32 { return s.arena.Size() } + +// Add adds a new key if it does not yet exist. If the key already exists, then +// Add returns ErrRecordExists. If there isn't enough room in the arena, then +// Add returns ErrArenaFull. +func (s *Skiplist) Add(key base.InternalKey, value []byte) error { + var ins Inserter + return s.addInternal(key, value, &ins) +} + +func (s *Skiplist) addInternal(key base.InternalKey, value []byte, ins *Inserter) error { + if s.findSplice(key, ins) { + // Found a matching node, but handle case where it's been deleted. + return ErrRecordExists + } + + if s.testing { + // Add delay to make it easier to test race between this thread + // and another thread that sees the intermediate state between + // finding the splice and using it. + runtime.Gosched() + } + + nd, height, err := s.newNode(key, value) + if err != nil { + return err + } + + ndOffset := s.arena.getPointerOffset(unsafe.Pointer(nd)) + + // We always insert from the base level and up. After you add a node in base + // level, we cannot create a node in the level above because it would have + // discovered the node in the base level. + var found bool + var invalidateSplice bool + for i := 0; i < int(height); i++ { + prev := ins.spl[i].prev + next := ins.spl[i].next + + if prev == nil { + // New node increased the height of the skiplist, so assume that the + // new level has not yet been populated. + if next != nil { + panic("next is expected to be nil, since prev is nil") + } + + prev = s.head + next = s.tail + } + + // +----------------+ +------------+ +----------------+ + // | prev | | nd | | next | + // | prevNextOffset |---->| | | | + // | |<----| prevOffset | | | + // | | | nextOffset |---->| | + // | | | |<----| nextPrevOffset | + // +----------------+ +------------+ +----------------+ + // + // 1. Initialize prevOffset and nextOffset to point to prev and next. + // 2. CAS prevNextOffset to repoint from next to nd. + // 3. CAS nextPrevOffset to repoint from prev to nd. + for { + prevOffset := s.arena.getPointerOffset(unsafe.Pointer(prev)) + nextOffset := s.arena.getPointerOffset(unsafe.Pointer(next)) + nd.tower[i].init(prevOffset, nextOffset) + + // Check whether next has an updated link to prev. If it does not, + // that can mean one of two things: + // 1. The thread that added the next node hasn't yet had a chance + // to add the prev link (but will shortly). + // 2. Another thread has added a new node between prev and next. + nextPrevOffset := next.prevOffset(i) + if nextPrevOffset != prevOffset { + // Determine whether #1 or #2 is true by checking whether prev + // is still pointing to next. As long as the atomic operations + // have at least acquire/release semantics (no need for + // sequential consistency), this works, as it is equivalent to + // the "publication safety" pattern. + prevNextOffset := prev.nextOffset(i) + if prevNextOffset == nextOffset { + // Ok, case #1 is true, so help the other thread along by + // updating the next node's prev link. + next.casPrevOffset(i, nextPrevOffset, prevOffset) + } + } + + if prev.casNextOffset(i, nextOffset, ndOffset) { + // Managed to insert nd between prev and next, so update the next + // node's prev link and go to the next level. + if s.testing { + // Add delay to make it easier to test race between this thread + // and another thread that sees the intermediate state between + // setting next and setting prev. + runtime.Gosched() + } + + next.casPrevOffset(i, prevOffset, ndOffset) + break + } + + // CAS failed. We need to recompute prev and next. It is unlikely to + // be helpful to try to use a different level as we redo the search, + // because it is unlikely that lots of nodes are inserted between prev + // and next. + prev, next, found = s.findSpliceForLevel(key, i, prev) + if found { + if i != 0 { + panic("how can another thread have inserted a node at a non-base level?") + } + + return ErrRecordExists + } + invalidateSplice = true + } + } + + // If we had to recompute the splice for a level, invalidate the entire + // cached splice. + if invalidateSplice { + ins.height = 0 + } else { + // The splice was valid. We inserted a node between spl[i].prev and + // spl[i].next. Optimistically update spl[i].prev for use in a subsequent + // call to add. + for i := uint32(0); i < height; i++ { + ins.spl[i].prev = nd + } + } + + return nil +} + +// NewIter returns a new Iterator object. The lower and upper bound parameters +// control the range of keys the iterator will return. Specifying for nil for +// lower or upper bound disables the check for that boundary. Note that lower +// bound is not checked on {SeekGE,First} and upper bound is not check on +// {SeekLT,Last}. The user is expected to perform that check. Note that it is +// safe for an iterator to be copied by value. +func (s *Skiplist) NewIter(lower, upper []byte) *Iterator { + it := iterPool.Get().(*Iterator) + *it = Iterator{list: s, nd: s.head, lower: lower, upper: upper} + return it +} + +// NewFlushIter returns a new flushIterator, which is similar to an Iterator +// but also sets the current number of the bytes that have been iterated +// through. +func (s *Skiplist) NewFlushIter(bytesFlushed *uint64) base.InternalIterator { + return &flushIterator{ + Iterator: Iterator{list: s, nd: s.head}, + bytesIterated: bytesFlushed, + } +} + +func (s *Skiplist) newNode( + key base.InternalKey, value []byte, +) (nd *node, height uint32, err error) { + height = s.randomHeight() + nd, err = newNode(s.arena, height, key, value) + if err != nil { + return + } + + // Try to increase s.height via CAS. + listHeight := s.Height() + for height > listHeight { + if s.height.CompareAndSwap(listHeight, height) { + // Successfully increased skiplist.height. + break + } + + listHeight = s.Height() + } + + return +} + +func (s *Skiplist) randomHeight() uint32 { + rnd := fastrand.Uint32() + + h := uint32(1) + for h < maxHeight && rnd <= probabilities[h] { + h++ + } + + return h +} + +func (s *Skiplist) findSplice(key base.InternalKey, ins *Inserter) (found bool) { + listHeight := s.Height() + var level int + + prev := s.head + if ins.height < listHeight { + // Our cached height is less than the list height, which means there were + // inserts that increased the height of the list. Recompute the splice from + // scratch. + ins.height = listHeight + level = int(ins.height) + } else { + // Our cached height is equal to the list height. + for ; level < int(listHeight); level++ { + spl := &ins.spl[level] + if s.getNext(spl.prev, level) != spl.next { + // One or more nodes have been inserted between the splice at this + // level. + continue + } + if spl.prev != s.head && !s.keyIsAfterNode(spl.prev, key) { + // Key lies before splice. + level = int(listHeight) + break + } + if spl.next != s.tail && s.keyIsAfterNode(spl.next, key) { + // Key lies after splice. + level = int(listHeight) + break + } + // The splice brackets the key! + prev = spl.prev + break + } + } + + for level = level - 1; level >= 0; level-- { + var next *node + prev, next, found = s.findSpliceForLevel(key, level, prev) + if next == nil { + next = s.tail + } + ins.spl[level].init(prev, next) + } + + return +} + +func (s *Skiplist) findSpliceForLevel( + key base.InternalKey, level int, start *node, +) (prev, next *node, found bool) { + prev = start + + for { + // Assume prev.key < key. + next = s.getNext(prev, level) + if next == s.tail { + // Tail node, so done. + break + } + + offset, size := next.keyOffset, next.keySize + nextKey := s.arena.buf[offset : offset+size] + 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. + if key.Trailer == next.keyTrailer { + // Internal key equality. + found = true + break + } + if key.Trailer > next.keyTrailer { + // We are done for this level, since prev.key < key < next.key. + break + } + } + + // Keep moving right on this level. + prev = next + } + + return +} + +func (s *Skiplist) keyIsAfterNode(nd *node, key base.InternalKey) bool { + ndKey := s.arena.buf[nd.keyOffset : nd.keyOffset+nd.keySize] + cmp := s.cmp(ndKey, key.UserKey) + if cmp < 0 { + return true + } + if cmp > 0 { + return false + } + // User-key equality. + if key.Trailer == nd.keyTrailer { + // Internal key equality. + return false + } + return key.Trailer < nd.keyTrailer +} + +func (s *Skiplist) getNext(nd *node, h int) *node { + offset := nd.tower[h].nextOffset.Load() + return (*node)(s.arena.getPointer(offset)) +} + +func (s *Skiplist) getPrev(nd *node, h int) *node { + offset := nd.tower[h].prevOffset.Load() + return (*node)(s.arena.getPointer(offset)) +} diff --git a/pebble/internal/arenaskl/skl_test.go b/pebble/internal/arenaskl/skl_test.go new file mode 100644 index 0000000..6e74a4a --- /dev/null +++ b/pebble/internal/arenaskl/skl_test.go @@ -0,0 +1,972 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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 arenaskl + +import ( + "bytes" + "encoding/binary" + "fmt" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +const arenaSize = 1 << 20 + +// iterAdapter adapts the new Iterator API which returns the key and value from +// positioning methods (Seek*, First, Last, Next, Prev) to the old API which +// returned a boolean corresponding to Valid. Only used by test code. +type iterAdapter struct { + *Iterator + key *base.InternalKey + val []byte +} + +func newIterAdapter(iter *Iterator) *iterAdapter { + return &iterAdapter{ + Iterator: iter, + } +} + +func (i *iterAdapter) update(key *base.InternalKey, val base.LazyValue) bool { + i.key = key + i.val = val.InPlaceValue() + return i.key != nil +} + +func (i *iterAdapter) String() string { + return "iter-adapter" +} + +func (i *iterAdapter) SeekGE(key []byte, flags base.SeekGEFlags) bool { + return i.update(i.Iterator.SeekGE(key, flags)) +} + +func (i *iterAdapter) SeekPrefixGE(prefix, key []byte, flags base.SeekGEFlags) bool { + return i.update(i.Iterator.SeekPrefixGE(prefix, key, flags)) +} + +func (i *iterAdapter) SeekLT(key []byte, flags base.SeekLTFlags) bool { + return i.update(i.Iterator.SeekLT(key, flags)) +} + +func (i *iterAdapter) First() bool { + return i.update(i.Iterator.First()) +} + +func (i *iterAdapter) Last() bool { + return i.update(i.Iterator.Last()) +} + +func (i *iterAdapter) Next() bool { + return i.update(i.Iterator.Next()) +} + +func (i *iterAdapter) Prev() bool { + return i.update(i.Iterator.Prev()) +} + +func (i *iterAdapter) Key() base.InternalKey { + return *i.key +} + +func (i *iterAdapter) Value() []byte { + return i.val +} + +func (i *iterAdapter) Valid() bool { + return i.key != nil +} + +func makeIntKey(i int) base.InternalKey { + return base.InternalKey{UserKey: []byte(fmt.Sprintf("%05d", i))} +} + +func makeKey(s string) []byte { + return []byte(s) +} + +func makeIkey(s string) base.InternalKey { + return base.InternalKey{UserKey: []byte(s)} +} + +func makeValue(i int) []byte { + return []byte(fmt.Sprintf("v%05d", i)) +} + +func makeInserterAdd(s *Skiplist) func(key base.InternalKey, value []byte) error { + ins := &Inserter{} + return func(key base.InternalKey, value []byte) error { + return ins.Add(s, key, value) + } +} + +// length iterates over skiplist to give exact size. +func length(s *Skiplist) int { + count := 0 + + it := newIterAdapter(s.NewIter(nil, nil)) + for valid := it.First(); valid; valid = it.Next() { + count++ + } + + return count +} + +// length iterates over skiplist in reverse order to give exact size. +func lengthRev(s *Skiplist) int { + count := 0 + + it := newIterAdapter(s.NewIter(nil, nil)) + for valid := it.Last(); valid; valid = it.Prev() { + count++ + } + + return count +} + +func TestEmpty(t *testing.T) { + key := makeKey("aaa") + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + it := newIterAdapter(l.NewIter(nil, nil)) + + require.False(t, it.Valid()) + + it.First() + require.False(t, it.Valid()) + + it.Last() + require.False(t, it.Valid()) + + require.False(t, it.SeekGE(key, base.SeekGEFlagsNone)) + require.False(t, it.Valid()) +} + +func TestFull(t *testing.T) { + l := NewSkiplist(newArena(1000), bytes.Compare) + + foundArenaFull := false + for i := 0; i < 100; i++ { + err := l.Add(makeIntKey(i), makeValue(i)) + if err == ErrArenaFull { + foundArenaFull = true + break + } + } + + require.True(t, foundArenaFull) + + err := l.Add(makeIkey("someval"), nil) + require.Equal(t, ErrArenaFull, err) +} + +// TestBasic tests single-threaded seeks and adds. +func TestBasic(t *testing.T) { + for _, inserter := range []bool{false, true} { + t.Run(fmt.Sprintf("inserter=%t", inserter), func(t *testing.T) { + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + it := newIterAdapter(l.NewIter(nil, nil)) + + add := l.Add + if inserter { + add = makeInserterAdd(l) + } + + // Try adding values. + add(makeIkey("key1"), makeValue(1)) + add(makeIkey("key3"), makeValue(3)) + add(makeIkey("key2"), makeValue(2)) + + require.True(t, it.SeekGE(makeKey("key"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.NotEqual(t, "key", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey("key1"), base.SeekGEFlagsNone)) + require.EqualValues(t, "key1", it.Key().UserKey) + require.EqualValues(t, makeValue(1), it.Value()) + + require.True(t, it.SeekGE(makeKey("key2"), base.SeekGEFlagsNone)) + require.EqualValues(t, "key2", it.Key().UserKey) + require.EqualValues(t, makeValue(2), it.Value()) + + require.True(t, it.SeekGE(makeKey("key3"), base.SeekGEFlagsNone)) + require.EqualValues(t, "key3", it.Key().UserKey) + require.EqualValues(t, makeValue(3), it.Value()) + + key := makeIkey("a") + key.SetSeqNum(1) + add(key, nil) + key.SetSeqNum(2) + add(key, nil) + + require.True(t, it.SeekGE(makeKey("a"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "a", it.Key().UserKey) + require.EqualValues(t, 2, it.Key().SeqNum()) + + require.True(t, it.Next()) + require.True(t, it.Valid()) + require.EqualValues(t, "a", it.Key().UserKey) + require.EqualValues(t, 1, it.Key().SeqNum()) + + key = makeIkey("b") + key.SetSeqNum(2) + add(key, nil) + key.SetSeqNum(1) + add(key, nil) + + require.True(t, it.SeekGE(makeKey("b"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "b", it.Key().UserKey) + require.EqualValues(t, 2, it.Key().SeqNum()) + + require.True(t, it.Next()) + require.True(t, it.Valid()) + require.EqualValues(t, "b", it.Key().UserKey) + require.EqualValues(t, 1, it.Key().SeqNum()) + }) + } +} + +// TestConcurrentBasic tests concurrent writes followed by concurrent reads. +func TestConcurrentBasic(t *testing.T) { + const n = 1000 + + for _, inserter := range []bool{false, true} { + t.Run(fmt.Sprintf("inserter=%t", inserter), func(t *testing.T) { + // Set testing flag to make it easier to trigger unusual race conditions. + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + l.testing = true + + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + + if inserter { + var ins Inserter + ins.Add(l, makeIntKey(i), makeValue(i)) + } else { + l.Add(makeIntKey(i), makeValue(i)) + } + }(i) + } + wg.Wait() + + // Check values. Concurrent reads. + for i := 0; i < n; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + + it := newIterAdapter(l.NewIter(nil, nil)) + require.True(t, it.SeekGE(makeKey(fmt.Sprintf("%05d", i)), base.SeekGEFlagsNone)) + require.EqualValues(t, fmt.Sprintf("%05d", i), it.Key().UserKey) + }(i) + } + wg.Wait() + require.Equal(t, n, length(l)) + require.Equal(t, n, lengthRev(l)) + }) + } +} + +// TestConcurrentOneKey will read while writing to one single key. +func TestConcurrentOneKey(t *testing.T) { + const n = 100 + key := makeKey("thekey") + ikey := makeIkey("thekey") + + for _, inserter := range []bool{false, true} { + t.Run(fmt.Sprintf("inserter=%t", inserter), func(t *testing.T) { + // Set testing flag to make it easier to trigger unusual race conditions. + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + l.testing = true + + var wg sync.WaitGroup + writeDone := make(chan struct{}, 1) + for i := 0; i < n; i++ { + wg.Add(1) + go func(i int) { + defer func() { + wg.Done() + select { + case writeDone <- struct{}{}: + default: + } + }() + + if inserter { + var ins Inserter + ins.Add(l, ikey, makeValue(i)) + } else { + l.Add(ikey, makeValue(i)) + } + }(i) + } + // Wait until at least some write made it such that reads return a value. + <-writeDone + var sawValue atomic.Int32 + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + it := newIterAdapter(l.NewIter(nil, nil)) + it.SeekGE(key, base.SeekGEFlagsNone) + require.True(t, it.Valid()) + require.True(t, bytes.Equal(key, it.Key().UserKey)) + + sawValue.Add(1) + v, err := strconv.Atoi(string(it.Value()[1:])) + require.NoError(t, err) + require.True(t, 0 <= v && v < n) + }() + } + wg.Wait() + require.Equal(t, int32(n), sawValue.Load()) + require.Equal(t, 1, length(l)) + require.Equal(t, 1, lengthRev(l)) + }) + } +} + +func TestSkiplistAdd(t *testing.T) { + for _, inserter := range []bool{false, true} { + t.Run(fmt.Sprintf("inserter=%t", inserter), func(t *testing.T) { + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + it := newIterAdapter(l.NewIter(nil, nil)) + + add := l.Add + if inserter { + add = makeInserterAdd(l) + } + + // Add nil key and value (treated same as empty). + err := add(base.InternalKey{}, nil) + require.Nil(t, err) + require.True(t, it.SeekGE([]byte{}, base.SeekGEFlagsNone)) + require.EqualValues(t, []byte{}, it.Key().UserKey) + require.EqualValues(t, []byte{}, it.Value()) + + l = NewSkiplist(newArena(arenaSize), bytes.Compare) + it = newIterAdapter(l.NewIter(nil, nil)) + + add = l.Add + if inserter { + add = makeInserterAdd(l) + } + + // Add empty key and value (treated same as nil). + err = add(makeIkey(""), []byte{}) + require.Nil(t, err) + require.True(t, it.SeekGE([]byte{}, base.SeekGEFlagsNone)) + require.EqualValues(t, []byte{}, it.Key().UserKey) + require.EqualValues(t, []byte{}, it.Value()) + + // Add to empty list. + err = add(makeIntKey(2), makeValue(2)) + require.Nil(t, err) + require.True(t, it.SeekGE(makeKey("00002"), base.SeekGEFlagsNone)) + require.EqualValues(t, "00002", it.Key().UserKey) + require.EqualValues(t, makeValue(2), it.Value()) + + // Add first element in non-empty list. + err = add(makeIntKey(1), makeValue(1)) + require.Nil(t, err) + require.True(t, it.SeekGE(makeKey("00001"), base.SeekGEFlagsNone)) + require.EqualValues(t, "00001", it.Key().UserKey) + require.EqualValues(t, makeValue(1), it.Value()) + + // Add last element in non-empty list. + err = add(makeIntKey(4), makeValue(4)) + require.Nil(t, err) + require.True(t, it.SeekGE(makeKey("00004"), base.SeekGEFlagsNone)) + require.EqualValues(t, "00004", it.Key().UserKey) + require.EqualValues(t, makeValue(4), it.Value()) + + // Add element in middle of list. + err = add(makeIntKey(3), makeValue(3)) + require.Nil(t, err) + require.True(t, it.SeekGE(makeKey("00003"), base.SeekGEFlagsNone)) + require.EqualValues(t, "00003", it.Key().UserKey) + require.EqualValues(t, makeValue(3), it.Value()) + + // Try to add element that already exists. + err = add(makeIntKey(2), nil) + require.Equal(t, ErrRecordExists, err) + require.EqualValues(t, "00003", it.Key().UserKey) + require.EqualValues(t, makeValue(3), it.Value()) + + require.Equal(t, 5, length(l)) + require.Equal(t, 5, lengthRev(l)) + }) + } +} + +// TestConcurrentAdd races between adding same nodes. +func TestConcurrentAdd(t *testing.T) { + for _, inserter := range []bool{false, true} { + t.Run(fmt.Sprintf("inserter=%t", inserter), func(t *testing.T) { + const n = 100 + + // Set testing flag to make it easier to trigger unusual race conditions. + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + l.testing = true + + start := make([]sync.WaitGroup, n) + end := make([]sync.WaitGroup, n) + + for i := 0; i < n; i++ { + start[i].Add(1) + end[i].Add(2) + } + + for f := 0; f < 2; f++ { + go func(f int) { + it := newIterAdapter(l.NewIter(nil, nil)) + add := l.Add + if inserter { + add = makeInserterAdd(l) + } + + for i := 0; i < n; i++ { + start[i].Wait() + + key := makeIntKey(i) + if add(key, nil) == nil { + require.True(t, it.SeekGE(key.UserKey, base.SeekGEFlagsNone)) + require.EqualValues(t, key, it.Key()) + } + + end[i].Done() + } + }(f) + } + + for i := 0; i < n; i++ { + start[i].Done() + end[i].Wait() + } + + require.Equal(t, n, length(l)) + require.Equal(t, n, lengthRev(l)) + }) + } +} + +// TestIteratorNext tests a basic iteration over all nodes from the beginning. +func TestIteratorNext(t *testing.T) { + const n = 100 + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + it := newIterAdapter(l.NewIter(nil, nil)) + + require.False(t, it.Valid()) + + it.First() + require.False(t, it.Valid()) + + for i := n - 1; i >= 0; i-- { + l.Add(makeIntKey(i), makeValue(i)) + } + + it.First() + for i := 0; i < n; i++ { + require.True(t, it.Valid()) + require.EqualValues(t, makeIntKey(i), it.Key()) + require.EqualValues(t, makeValue(i), it.Value()) + it.Next() + } + require.False(t, it.Valid()) +} + +// TestIteratorPrev tests a basic iteration over all nodes from the end. +func TestIteratorPrev(t *testing.T) { + const n = 100 + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + it := newIterAdapter(l.NewIter(nil, nil)) + + require.False(t, it.Valid()) + + it.Last() + require.False(t, it.Valid()) + + var ins Inserter + for i := 0; i < n; i++ { + ins.Add(l, makeIntKey(i), makeValue(i)) + } + + it.Last() + for i := n - 1; i >= 0; i-- { + require.True(t, it.Valid()) + require.EqualValues(t, makeIntKey(i), it.Key()) + require.EqualValues(t, makeValue(i), it.Value()) + it.Prev() + } + require.False(t, it.Valid()) +} + +func TestIteratorSeekGEAndSeekPrefixGE(t *testing.T) { + const n = 100 + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + it := newIterAdapter(l.NewIter(nil, nil)) + + require.False(t, it.Valid()) + it.First() + require.False(t, it.Valid()) + // 1000, 1010, 1020, ..., 1990. + + var ins Inserter + for i := n - 1; i >= 0; i-- { + v := i*10 + 1000 + ins.Add(l, makeIntKey(v), makeValue(v)) + } + + require.True(t, it.SeekGE(makeKey(""), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + require.EqualValues(t, "v01000", it.Value()) + + require.True(t, it.SeekGE(makeKey("01000"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + require.EqualValues(t, "v01000", it.Value()) + + require.True(t, it.SeekGE(makeKey("01005"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01010", it.Key().UserKey) + require.EqualValues(t, "v01010", it.Value()) + + require.True(t, it.SeekGE(makeKey("01010"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01010", it.Key().UserKey) + require.EqualValues(t, "v01010", it.Value()) + + require.False(t, it.SeekGE(makeKey("99999"), base.SeekGEFlagsNone)) + require.False(t, it.Valid()) + + // Test SeekGE with trySeekUsingNext optimization. + { + require.True(t, it.SeekGE(makeKey("01000"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + require.EqualValues(t, "v01000", it.Value()) + + // Seeking to the same key. + require.True(t, it.SeekGE(makeKey("01000"), base.SeekGEFlagsNone.EnableTrySeekUsingNext())) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + require.EqualValues(t, "v01000", it.Value()) + + // Seeking to a nearby key that can be reached using Next. + require.True(t, it.SeekGE(makeKey("01020"), base.SeekGEFlagsNone.EnableTrySeekUsingNext())) + require.True(t, it.Valid()) + require.EqualValues(t, "01020", it.Key().UserKey) + require.EqualValues(t, "v01020", it.Value()) + + // Seeking to a key that cannot be reached using Next. + require.True(t, it.SeekGE(makeKey("01200"), base.SeekGEFlagsNone.EnableTrySeekUsingNext())) + require.True(t, it.Valid()) + require.EqualValues(t, "01200", it.Key().UserKey) + require.EqualValues(t, "v01200", it.Value()) + + // Seeking to an earlier key, but the caller lies. Incorrect result. + require.True(t, it.SeekGE(makeKey("01100"), base.SeekGEFlagsNone.EnableTrySeekUsingNext())) + require.True(t, it.Valid()) + require.EqualValues(t, "01200", it.Key().UserKey) + require.EqualValues(t, "v01200", it.Value()) + + // Telling the truth works. + require.True(t, it.SeekGE(makeKey("01100"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01100", it.Key().UserKey) + require.EqualValues(t, "v01100", it.Value()) + } + + // Test SeekPrefixGE with trySeekUsingNext optimization. + { + require.True(t, it.SeekPrefixGE(makeKey("01000"), makeKey("01000"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + require.EqualValues(t, "v01000", it.Value()) + + // Seeking to the same key. + require.True(t, it.SeekPrefixGE(makeKey("01000"), makeKey("01000"), base.SeekGEFlagsNone.EnableTrySeekUsingNext())) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + require.EqualValues(t, "v01000", it.Value()) + + // Seeking to a nearby key that can be reached using Next. + require.True(t, it.SeekPrefixGE(makeKey("01020"), makeKey("01020"), base.SeekGEFlagsNone.EnableTrySeekUsingNext())) + require.True(t, it.Valid()) + require.EqualValues(t, "01020", it.Key().UserKey) + require.EqualValues(t, "v01020", it.Value()) + + // Seeking to a key that cannot be reached using Next. + require.True(t, it.SeekPrefixGE(makeKey("01200"), makeKey("01200"), base.SeekGEFlagsNone.EnableTrySeekUsingNext())) + require.True(t, it.Valid()) + require.EqualValues(t, "01200", it.Key().UserKey) + require.EqualValues(t, "v01200", it.Value()) + + // Seeking to an earlier key, but the caller lies. Incorrect result. + require.True(t, it.SeekPrefixGE(makeKey("01100"), makeKey("01100"), base.SeekGEFlagsNone.EnableTrySeekUsingNext())) + require.True(t, it.Valid()) + require.EqualValues(t, "01200", it.Key().UserKey) + require.EqualValues(t, "v01200", it.Value()) + + // Telling the truth works. + require.True(t, it.SeekPrefixGE(makeKey("01100"), makeKey("01100"), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01100", it.Key().UserKey) + require.EqualValues(t, "v01100", it.Value()) + } + + // Test seek for empty key. + ins.Add(l, base.InternalKey{}, nil) + require.True(t, it.SeekGE([]byte{}, base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey(""), base.SeekGEFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "", it.Key().UserKey) +} + +func TestIteratorSeekLT(t *testing.T) { + const n = 100 + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + it := newIterAdapter(l.NewIter(nil, nil)) + + require.False(t, it.Valid()) + it.First() + require.False(t, it.Valid()) + // 1000, 1010, 1020, ..., 1990. + var ins Inserter + for i := n - 1; i >= 0; i-- { + v := i*10 + 1000 + ins.Add(l, makeIntKey(v), makeValue(v)) + } + + require.False(t, it.SeekLT(makeKey(""), base.SeekLTFlagsNone)) + require.False(t, it.Valid()) + + require.False(t, it.SeekLT(makeKey("01000"), base.SeekLTFlagsNone)) + require.False(t, it.Valid()) + + require.True(t, it.SeekLT(makeKey("01001"), base.SeekLTFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + require.EqualValues(t, "v01000", it.Value()) + + require.True(t, it.SeekLT(makeKey("01005"), base.SeekLTFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + require.EqualValues(t, "v01000", it.Value()) + + require.True(t, it.SeekLT(makeKey("01991"), base.SeekLTFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01990", it.Key().UserKey) + require.EqualValues(t, "v01990", it.Value()) + + require.True(t, it.SeekLT(makeKey("99999"), base.SeekLTFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "01990", it.Key().UserKey) + require.EqualValues(t, "v01990", it.Value()) + + // Test seek for empty key. + ins.Add(l, base.InternalKey{}, nil) + require.False(t, it.SeekLT([]byte{}, base.SeekLTFlagsNone)) + require.False(t, it.Valid()) + + require.True(t, it.SeekLT(makeKey("\x01"), base.SeekLTFlagsNone)) + require.True(t, it.Valid()) + require.EqualValues(t, "", it.Key().UserKey) +} + +// TODO(peter): test First and Last. +func TestIteratorBounds(t *testing.T) { + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + for i := 1; i < 10; i++ { + require.NoError(t, l.Add(makeIntKey(i), makeValue(i))) + } + + key := func(i int) []byte { + return makeIntKey(i).UserKey + } + + it := newIterAdapter(l.NewIter(key(3), key(7))) + + // SeekGE within the lower and upper bound succeeds. + for i := 3; i <= 6; i++ { + k := key(i) + require.True(t, it.SeekGE(k, base.SeekGEFlagsNone)) + require.EqualValues(t, string(k), string(it.Key().UserKey)) + } + + // SeekGE before the lower bound still succeeds (only the upper bound is + // checked). + for i := 1; i < 3; i++ { + k := key(i) + require.True(t, it.SeekGE(k, base.SeekGEFlagsNone)) + require.EqualValues(t, string(k), string(it.Key().UserKey)) + } + + // SeekGE beyond the upper bound fails. + for i := 7; i < 10; i++ { + require.False(t, it.SeekGE(key(i), base.SeekGEFlagsNone)) + } + + require.True(t, it.SeekGE(key(6), base.SeekGEFlagsNone)) + require.EqualValues(t, "00006", it.Key().UserKey) + require.EqualValues(t, "v00006", it.Value()) + + // Next into the upper bound fails. + require.False(t, it.Next()) + + // SeekLT within the lower and upper bound succeeds. + for i := 4; i <= 7; i++ { + require.True(t, it.SeekLT(key(i), base.SeekLTFlagsNone)) + require.EqualValues(t, string(key(i-1)), string(it.Key().UserKey)) + } + + // SeekLT beyond the upper bound still succeeds (only the lower bound is + // checked). + for i := 8; i < 9; i++ { + require.True(t, it.SeekLT(key(8), base.SeekLTFlagsNone)) + require.EqualValues(t, string(key(i-1)), string(it.Key().UserKey)) + } + + // SeekLT before the lower bound fails. + for i := 1; i < 4; i++ { + require.False(t, it.SeekLT(key(i), base.SeekLTFlagsNone)) + } + + require.True(t, it.SeekLT(key(4), base.SeekLTFlagsNone)) + require.EqualValues(t, "00003", it.Key().UserKey) + require.EqualValues(t, "v00003", it.Value()) + + // Prev into the lower bound fails. + require.False(t, it.Prev()) +} + +func TestBytesIterated(t *testing.T) { + l := NewSkiplist(newArena(arenaSize), bytes.Compare) + emptySize := l.arena.Size() + for i := 0; i < 200; i++ { + bytesIterated := l.bytesIterated(t) + expected := uint64(l.arena.Size() - emptySize) + if bytesIterated != expected { + t.Fatalf("bytesIterated: got %d, want %d", bytesIterated, expected) + } + l.Add(base.InternalKey{UserKey: []byte{byte(i)}}, nil) + } +} + +// bytesIterated returns the number of bytes iterated in the skiplist. +func (s *Skiplist) bytesIterated(t *testing.T) (bytesIterated uint64) { + x := s.NewFlushIter(&bytesIterated) + var prevIterated uint64 + for key, _ := x.First(); key != nil; key, _ = x.Next() { + if bytesIterated < prevIterated { + t.Fatalf("bytesIterated moved backward: %d < %d", bytesIterated, prevIterated) + } + prevIterated = bytesIterated + } + if x.Close() != nil { + return 0 + } + return bytesIterated +} + +func randomKey(rng *rand.Rand, b []byte) base.InternalKey { + key := rng.Uint32() + key2 := rng.Uint32() + binary.LittleEndian.PutUint32(b, key) + binary.LittleEndian.PutUint32(b[4:], key2) + return base.InternalKey{UserKey: b} +} + +// Standard test. Some fraction is read. Some fraction is write. Writes have +// to go through mutex lock. +func BenchmarkReadWrite(b *testing.B) { + for i := 0; i <= 10; i++ { + readFrac := float32(i) / 10.0 + b.Run(fmt.Sprintf("frac_%d", i*10), func(b *testing.B) { + l := NewSkiplist(newArena(uint32((b.N+2)*maxNodeSize)), bytes.Compare) + b.ResetTimer() + var count int + b.RunParallel(func(pb *testing.PB) { + it := l.NewIter(nil, nil) + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + buf := make([]byte, 8) + + for pb.Next() { + if rng.Float32() < readFrac { + key, _ := it.SeekGE(randomKey(rng, buf).UserKey, base.SeekGEFlagsNone) + if key != nil { + _ = key + count++ + } + } else { + _ = l.Add(randomKey(rng, buf), nil) + } + } + }) + }) + } +} + +func BenchmarkOrderedWrite(b *testing.B) { + l := NewSkiplist(newArena(8<<20), bytes.Compare) + var ins Inserter + buf := make([]byte, 8) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + binary.BigEndian.PutUint64(buf, uint64(i)) + if err := ins.Add(l, base.InternalKey{UserKey: buf}, nil); err == ErrArenaFull { + b.StopTimer() + l = NewSkiplist(newArena(uint32((b.N+2)*maxNodeSize)), bytes.Compare) + ins = Inserter{} + b.StartTimer() + } + } +} + +func BenchmarkIterNext(b *testing.B) { + l := NewSkiplist(newArena(64<<10), bytes.Compare) + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + buf := make([]byte, 8) + for { + if err := l.Add(randomKey(rng, buf), nil); err == ErrArenaFull { + break + } + } + + it := l.NewIter(nil, nil) + b.ResetTimer() + for i := 0; i < b.N; i++ { + key, _ := it.Next() + if key == nil { + key, _ = it.First() + } + _ = key + } +} + +func BenchmarkIterPrev(b *testing.B) { + l := NewSkiplist(newArena(64<<10), bytes.Compare) + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + buf := make([]byte, 8) + for { + if err := l.Add(randomKey(rng, buf), nil); err == ErrArenaFull { + break + } + } + + it := l.NewIter(nil, nil) + _, _ = it.Last() + b.ResetTimer() + for i := 0; i < b.N; i++ { + key, _ := it.Prev() + if key == nil { + key, _ = it.Last() + } + _ = key + } +} + +// BenchmarkSeekPrefixGE looks at the performance of repeated calls to +// SeekPrefixGE, with different skip distances and different settings of +// trySeekUsingNext. +func BenchmarkSeekPrefixGE(b *testing.B) { + l := NewSkiplist(newArena(64<<10), bytes.Compare) + var count int + // count was measured to be 1279. + for count = 0; ; count++ { + if err := l.Add(makeIntKey(count), makeValue(count)); err == ErrArenaFull { + break + } + } + for _, skip := range []int{1, 2, 4, 8, 16} { + for _, useNext := range []bool{false, true} { + b.Run(fmt.Sprintf("skip=%d/use-next=%t", skip, useNext), func(b *testing.B) { + it := l.NewIter(nil, nil) + j := 0 + var k []byte + makeKey := func() { + k = []byte(fmt.Sprintf("%05d", j)) + } + makeKey() + it.SeekPrefixGE(k, k, base.SeekGEFlagsNone) + b.ResetTimer() + for i := 0; i < b.N; i++ { + j += skip + var flags base.SeekGEFlags + if useNext { + flags = flags.EnableTrySeekUsingNext() + } + if j >= count { + j = 0 + flags = flags.DisableTrySeekUsingNext() + } + makeKey() + it.SeekPrefixGE(k, k, flags) + } + }) + } + } +} + +// Standard test. Some fraction is read. Some fraction is write. Writes have +// to go through mutex lock. +// func BenchmarkReadWriteMap(b *testing.B) { +// for i := 0; i <= 10; i++ { +// readFrac := float32(i) / 10.0 +// b.Run(fmt.Sprintf("frac_%d", i*10), func(b *testing.B) { +// m := make(map[string]struct{}) +// var mutex sync.RWMutex +// b.ResetTimer() +// var count int +// b.RunParallel(func(pb *testing.PB) { +// rng := rand.New(rand.NewSource(time.Now().UnixNano())) +// for pb.Next() { +// if rng.Float32() < readFrac { +// mutex.RLock() +// _, ok := m[string(randomKey(rng))] +// mutex.RUnlock() +// if ok { +// count++ +// } +// } else { +// mutex.Lock() +// m[string(randomKey(rng))] = struct{}{} +// mutex.Unlock() +// } +// } +// }) +// }) +// } +// } diff --git a/pebble/internal/base/cleaner.go b/pebble/internal/base/cleaner.go new file mode 100644 index 0000000..b86d455 --- /dev/null +++ b/pebble/internal/base/cleaner.go @@ -0,0 +1,60 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package base + +import "github.com/cockroachdb/pebble/vfs" + +// Cleaner cleans obsolete files. +type Cleaner interface { + Clean(fs vfs.FS, fileType FileType, path string) error +} + +// NeedsFileContents is implemented by a cleaner that needs the contents of the +// files that it is being asked to clean. +type NeedsFileContents interface { + needsFileContents() +} + +// DeleteCleaner deletes file. +type DeleteCleaner struct{} + +// Clean removes file. +func (DeleteCleaner) Clean(fs vfs.FS, fileType FileType, path string) error { + return fs.Remove(path) +} + +func (DeleteCleaner) String() string { + return "delete" +} + +// ArchiveCleaner archives file instead delete. +type ArchiveCleaner struct{} + +var _ NeedsFileContents = ArchiveCleaner{} + +// Clean archives file. +func (ArchiveCleaner) Clean(fs vfs.FS, fileType FileType, path string) error { + switch fileType { + case FileTypeLog, FileTypeManifest, FileTypeTable: + destDir := fs.PathJoin(fs.PathDir(path), "archive") + + if err := fs.MkdirAll(destDir, 0755); err != nil { + return err + } + + destPath := fs.PathJoin(destDir, fs.PathBase(path)) + return fs.Rename(path, destPath) + + default: + return fs.Remove(path) + } +} + +func (ArchiveCleaner) String() string { + return "archive" +} + +func (ArchiveCleaner) needsFileContents() { +} diff --git a/pebble/internal/base/comparer.go b/pebble/internal/base/comparer.go new file mode 100644 index 0000000..a630962 --- /dev/null +++ b/pebble/internal/base/comparer.go @@ -0,0 +1,260 @@ +// 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 base + +import ( + "bytes" + "encoding/binary" + "fmt" + "strconv" + "unicode/utf8" +) + +// Compare returns -1, 0, or +1 depending on whether a is 'less than', 'equal +// to' or 'greater than' b. The two arguments can only be 'equal' if their +// contents are exactly equal. Furthermore, the empty slice must be 'less than' +// any non-empty slice. Compare is used to compare user keys, such as those +// passed as arguments to the various DB methods, as well as those returned +// from Separator, Successor, and Split. +type Compare func(a, b []byte) int + +// Equal returns true if a and b are equivalent. For a given Compare, +// Equal(a,b) must return true iff Compare(a,b) returns zero, that is, +// Equal is a (potentially faster) specialization of Compare. +type Equal func(a, b []byte) bool + +// AbbreviatedKey returns a fixed length prefix of a user key such that AbbreviatedKey(a) +// < AbbreviatedKey(b) iff a < b and AbbreviatedKey(a) > AbbreviatedKey(b) iff a > b. If +// AbbreviatedKey(a) == AbbreviatedKey(b) an additional comparison is required to +// determine if the two keys are actually equal. +// +// This helps optimize indexed batch comparisons for cache locality. If a Split +// function is specified, AbbreviatedKey usually returns the first eight bytes +// of the user key prefix in the order that gives the correct ordering. +type AbbreviatedKey func(key []byte) uint64 + +// FormatKey returns a formatter for the user key. +type FormatKey func(key []byte) fmt.Formatter + +// FormatValue returns a formatter for the user value. The key is also +// specified for the value formatter in order to support value formatting that +// is dependent on the key. +type FormatValue func(key, value []byte) fmt.Formatter + +// Separator is used to construct SSTable index blocks. A trivial implementation +// is `return a`, but appending fewer bytes leads to smaller SSTables. +// +// Given keys a, b for which Compare(a, b) < 0, Separator returns a key k such +// that: +// +// 1. Compare(a, k) <= 0, and +// 2. Compare(k, b) < 0. +// +// As a special case, b may be nil in which case the second condition is dropped. +// +// For example, if dst, a and b are the []byte equivalents of the strings +// "aqua", "black" and "blue", then the result may be "aquablb". +// Similarly, if the arguments were "aqua", "green" and "", then the result +// may be "aquah". +type Separator func(dst, a, b []byte) []byte + +// Successor returns a shortened key given a key a, such that Compare(k, a) >= +// 0. A simple implementation may return a unchanged. The dst parameter may be +// used to store the returned key, though it is valid to pass nil. The returned +// key must be valid to pass to Compare. +type Successor func(dst, a []byte) []byte + +// ImmediateSuccessor is invoked with a prefix key ([Split(a) == len(a)]) and +// returns the smallest key that is larger than the given prefix a. +// ImmediateSuccessor must return a prefix key k such that: +// +// Split(k) == len(k) and Compare(k, a) > 0 +// +// and there exists no representable k2 such that: +// +// Split(k2) == len(k2) and Compare(k2, a) > 0 and Compare(k2, k) < 0 +// +// As an example, an implementation built on the natural byte ordering using +// bytes.Compare could append a `\0` to `a`. +// +// The dst parameter may be used to store the returned key, though it is valid +// to pass nil. The returned key must be valid to pass to Compare. +type ImmediateSuccessor func(dst, a []byte) []byte + +// Split returns the length of the prefix of the user key that corresponds to +// the key portion of an MVCC encoding scheme to enable the use of prefix bloom +// filters. +// +// The method will only ever be called with valid MVCC keys, that is, keys that +// the user could potentially store in the database. Pebble does not know which +// keys are MVCC keys and which are not, and may call Split on both MVCC keys +// and non-MVCC keys. +// +// A trivial MVCC scheme is one in which Split() returns len(a). This +// corresponds to assigning a constant version to each key in the database. For +// performance reasons, it is preferable to use a `nil` split in this case. +// +// The returned prefix must have the following properties: +// +// 1. The prefix must be a byte prefix: +// +// bytes.HasPrefix(a, prefix(a)) +// +// 2. A key consisting of just a prefix must sort before all other keys with +// that prefix: +// +// Compare(prefix(a), a) < 0 if len(suffix(a)) > 0 +// +// 3. Prefixes must be used to order keys before suffixes: +// +// If Compare(a, b) <= 0, then Compare(prefix(a), prefix(b)) <= 0 +// +// 4. Suffixes themselves must be valid keys and comparable, respecting the same +// ordering as within a key. +// +// If Compare(prefix(a), prefix(b)) == 0, then Compare(suffix(a), suffix(b)) == Compare(a, b) +type Split func(a []byte) int + +// Comparer defines a total ordering over the space of []byte keys: a 'less +// than' relationship. +type Comparer struct { + Compare Compare + Equal Equal + AbbreviatedKey AbbreviatedKey + FormatKey FormatKey + FormatValue FormatValue + Separator Separator + Split Split + Successor Successor + ImmediateSuccessor ImmediateSuccessor + + // Name is the name of the comparer. + // + // The Level-DB on-disk format stores the comparer name, and opening a + // database with a different comparer from the one it was created with + // will result in an error. + Name string +} + +// DefaultFormatter is the default implementation of user key formatting: +// non-ASCII data is formatted as escaped hexadecimal values. +var DefaultFormatter = func(key []byte) fmt.Formatter { + return FormatBytes(key) +} + +// DefaultComparer is the default implementation of the Comparer interface. +// It uses the natural ordering, consistent with bytes.Compare. +var DefaultComparer = &Comparer{ + Compare: bytes.Compare, + Equal: bytes.Equal, + + AbbreviatedKey: func(key []byte) uint64 { + if len(key) >= 8 { + return binary.BigEndian.Uint64(key) + } + var v uint64 + for _, b := range key { + v <<= 8 + v |= uint64(b) + } + return v << uint(8*(8-len(key))) + }, + + FormatKey: DefaultFormatter, + + Separator: func(dst, a, b []byte) []byte { + i, n := SharedPrefixLen(a, b), len(dst) + dst = append(dst, a...) + + min := len(a) + if min > len(b) { + min = len(b) + } + if i >= min { + // Do not shorten if one string is a prefix of the other. + return dst + } + + if a[i] >= b[i] { + // b is smaller than a or a is already the shortest possible. + return dst + } + + if i < len(b)-1 || a[i]+1 < b[i] { + i += n + dst[i]++ + return dst[:i+1] + } + + i += n + 1 + for ; i < len(dst); i++ { + if dst[i] != 0xff { + dst[i]++ + return dst[:i+1] + } + } + return dst + }, + + Successor: func(dst, a []byte) (ret []byte) { + for i := 0; i < len(a); i++ { + if a[i] != 0xff { + dst = append(dst, a[:i+1]...) + dst[len(dst)-1]++ + return dst + } + } + // a is a run of 0xffs, leave it alone. + return append(dst, a...) + }, + + ImmediateSuccessor: func(dst, a []byte) (ret []byte) { + return append(append(dst, a...), 0x00) + }, + + // This name is part of the C++ Level-DB implementation's default file + // format, and should not be changed. + Name: "leveldb.BytewiseComparator", +} + +// SharedPrefixLen returns the largest i such that a[:i] equals b[:i]. +// This function can be useful in implementing the Comparer interface. +func SharedPrefixLen(a, b []byte) int { + i, n := 0, len(a) + if n > len(b) { + n = len(b) + } + asUint64 := func(c []byte, i int) uint64 { + return binary.LittleEndian.Uint64(c[i:]) + } + for i < n-7 && asUint64(a, i) == asUint64(b, i) { + i += 8 + } + for i < n && a[i] == b[i] { + i++ + } + return i +} + +// FormatBytes formats a byte slice using hexadecimal escapes for non-ASCII +// data. +type FormatBytes []byte + +const lowerhex = "0123456789abcdef" + +// Format implements the fmt.Formatter interface. +func (p FormatBytes) Format(s fmt.State, c rune) { + buf := make([]byte, 0, len(p)) + for _, b := range p { + if b < utf8.RuneSelf && strconv.IsPrint(rune(b)) { + buf = append(buf, b) + continue + } + buf = append(buf, `\x`...) + buf = append(buf, lowerhex[b>>4]) + buf = append(buf, lowerhex[b&0xF]) + } + s.Write(buf) +} diff --git a/pebble/internal/base/comparer_test.go b/pebble/internal/base/comparer_test.go new file mode 100644 index 0000000..ae49a31 --- /dev/null +++ b/pebble/internal/base/comparer_test.go @@ -0,0 +1,117 @@ +// 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 base + +import ( + "fmt" + "slices" + "testing" + "time" + + "golang.org/x/exp/rand" +) + +func TestDefAppendSeparator(t *testing.T) { + testCases := []struct { + a, b, want string + }{ + // Examples from the doc comments. + {"black", "blue", "blb"}, + {"green", "", "green"}, + // Non-empty b values. The C++ Level-DB code calls these separators. + {"", "2", ""}, + {"1", "2", "1"}, + {"1", "29", "2"}, + {"13", "19", "14"}, + {"13", "99", "2"}, + {"135", "19", "14"}, + {"1357", "19", "14"}, + {"1357", "2", "14"}, + {"13\xff", "14", "13\xff"}, + {"13\xff", "19", "14"}, + {"1\xff\xff", "19", "1\xff\xff"}, + {"1\xff\xff", "2", "1\xff\xff"}, + {"1\xff\xff", "9", "2"}, + // Empty b values. The C++ Level-DB code calls these successors. + {"", "", ""}, + {"1", "", "1"}, + {"11", "", "11"}, + {"11\xff", "", "11\xff"}, + {"1\xff", "", "1\xff"}, + {"1\xff\xff", "", "1\xff\xff"}, + {"\xff", "", "\xff"}, + {"\xff\xff", "", "\xff\xff"}, + {"\xff\xff\xff", "", "\xff\xff\xff"}, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + got := string(DefaultComparer.Separator(nil, []byte(tc.a), []byte(tc.b))) + if got != tc.want { + t.Errorf("a, b = %q, %q: got %q, want %q", tc.a, tc.b, got, tc.want) + } + }) + } +} + +func TestAbbreviatedKey(t *testing.T) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + randBytes := func(size int) []byte { + data := make([]byte, size) + for i := range data { + data[i] = byte(rng.Int() & 0xff) + } + return data + } + + keys := make([][]byte, 10000) + for i := range keys { + keys[i] = randBytes(rng.Intn(16)) + } + slices.SortFunc(keys, DefaultComparer.Compare) + + for i := 1; i < len(keys); i++ { + last := DefaultComparer.AbbreviatedKey(keys[i-1]) + cur := DefaultComparer.AbbreviatedKey(keys[i]) + cmp := DefaultComparer.Compare(keys[i-1], keys[i]) + if cmp == 0 { + if last != cur { + t.Fatalf("expected equal abbreviated keys: %x[%x] != %x[%x]", + last, keys[i-1], cur, keys[i]) + } + } else { + if last > cur { + t.Fatalf("unexpected abbreviated key ordering: %x[%x] > %x[%x]", + last, keys[i-1], cur, keys[i]) + } + } + } +} + +func BenchmarkAbbreviatedKey(b *testing.B) { + rng := rand.New(rand.NewSource(1449168817)) + randBytes := func(size int) []byte { + data := make([]byte, size) + for i := range data { + data[i] = byte(rng.Int() & 0xff) + } + return data + } + keys := make([][]byte, 10000) + for i := range keys { + keys[i] = randBytes(8) + } + + b.ResetTimer() + var sum uint64 + for i := 0; i < b.N; i++ { + j := i % len(keys) + sum += DefaultComparer.AbbreviatedKey(keys[j]) + } + + if testing.Verbose() { + // Ensure the compiler doesn't optimize away our benchmark. + fmt.Println(sum) + } +} diff --git a/pebble/internal/base/error.go b/pebble/internal/base/error.go new file mode 100644 index 0000000..6ef7783 --- /dev/null +++ b/pebble/internal/base/error.go @@ -0,0 +1,28 @@ +// 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 base + +import "github.com/cockroachdb/errors" + +// ErrNotFound means that a get or delete call did not find the requested key. +var ErrNotFound = errors.New("pebble: not found") + +// ErrCorruption is a marker to indicate that data in a file (WAL, MANIFEST, +// sstable) isn't in the expected format. +var ErrCorruption = errors.New("pebble: corruption") + +// MarkCorruptionError marks given error as a corruption error. +func MarkCorruptionError(err error) error { + if errors.Is(err, ErrCorruption) { + return err + } + return errors.Mark(err, ErrCorruption) +} + +// CorruptionErrorf formats according to a format specifier and returns +// the string as an error value that is marked as a corruption error. +func CorruptionErrorf(format string, args ...interface{}) error { + return errors.Mark(errors.Newf(format, args...), ErrCorruption) +} diff --git a/pebble/internal/base/filenames.go b/pebble/internal/base/filenames.go new file mode 100644 index 0000000..06098ab --- /dev/null +++ b/pebble/internal/base/filenames.go @@ -0,0 +1,202 @@ +// Copyright 2012 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 base + +import ( + "fmt" + "strconv" + "strings" + + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/redact" +) + +// FileNum is an internal DB identifier for a file. +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 uint64 + +func (dfn DiskFileNum) String() string { return fmt.Sprintf("%06d", dfn) } + +// SafeFormat implements redact.SafeFormatter. +func (dfn DiskFileNum) SafeFormat(w redact.SafePrinter, verb rune) { + w.Printf("%06d", redact.SafeUint(dfn)) +} + +// FileNum converts a DiskFileNum to a FileNum. This conversion is always valid. +func (dfn DiskFileNum) FileNum() FileNum { + return FileNum(dfn) +} + +// FileType enumerates the types of files found in a DB. +type FileType int + +// The FileType enumeration. +const ( + FileTypeLog FileType = iota + FileTypeLock + FileTypeTable + FileTypeManifest + FileTypeCurrent + FileTypeOptions + FileTypeOldTemp + FileTypeTemp +) + +// MakeFilename builds a filename from components. +func MakeFilename(fileType FileType, dfn DiskFileNum) string { + switch fileType { + case FileTypeLog: + return fmt.Sprintf("%s.log", dfn) + case FileTypeLock: + return "LOCK" + case FileTypeTable: + return fmt.Sprintf("%s.sst", dfn) + case FileTypeManifest: + return fmt.Sprintf("MANIFEST-%s", dfn) + case FileTypeCurrent: + return "CURRENT" + case FileTypeOptions: + return fmt.Sprintf("OPTIONS-%s", dfn) + case FileTypeOldTemp: + return fmt.Sprintf("CURRENT.%s.dbtmp", dfn) + case FileTypeTemp: + return fmt.Sprintf("temporary.%s.dbtmp", dfn) + } + panic("unreachable") +} + +// MakeFilepath builds a filepath from components. +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, dfn DiskFileNum, ok bool) { + filename = fs.PathBase(filename) + switch { + case filename == "CURRENT": + return FileTypeCurrent, 0, true + case filename == "LOCK": + return FileTypeLock, 0, true + case strings.HasPrefix(filename, "MANIFEST-"): + dfn, ok = parseDiskFileNum(filename[len("MANIFEST-"):]) + if !ok { + break + } + return FileTypeManifest, dfn, true + case strings.HasPrefix(filename, "OPTIONS-"): + dfn, ok = parseDiskFileNum(filename[len("OPTIONS-"):]) + if !ok { + break + } + return FileTypeOptions, dfn, ok + case strings.HasPrefix(filename, "CURRENT.") && strings.HasSuffix(filename, ".dbtmp"): + s := strings.TrimSuffix(filename[len("CURRENT."):], ".dbtmp") + dfn, ok = parseDiskFileNum(s) + if !ok { + break + } + return FileTypeOldTemp, dfn, ok + case strings.HasPrefix(filename, "temporary.") && strings.HasSuffix(filename, ".dbtmp"): + s := strings.TrimSuffix(filename[len("temporary."):], ".dbtmp") + dfn, ok = parseDiskFileNum(s) + if !ok { + break + } + return FileTypeTemp, dfn, ok + default: + i := strings.IndexByte(filename, '.') + if i < 0 { + break + } + dfn, ok = parseDiskFileNum(filename[:i]) + if !ok { + break + } + switch filename[i+1:] { + case "sst": + return FileTypeTable, dfn, true + case "log": + return FileTypeLog, dfn, true + } + } + return 0, dfn, false +} + +func parseDiskFileNum(s string) (dfn DiskFileNum, ok bool) { + u, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return dfn, false + } + return DiskFileNum(u), true +} + +// A Fataler fatals a process with a message when called. +type Fataler interface { + Fatalf(format string, args ...interface{}) +} + +// MustExist checks if err is an error indicating a file does not exist. +// If it is, it lists the containing directory's files to annotate the error +// with counts of the various types of files and invokes the provided fataler. +// See cockroachdb/cockroach#56490. +func MustExist(fs vfs.FS, filename string, fataler Fataler, err error) { + if err == nil || !oserror.IsNotExist(err) { + return + } + + ls, lsErr := fs.List(fs.PathDir(filename)) + if lsErr != nil { + // TODO(jackson): if oserror.IsNotExist(lsErr), the the data directory + // doesn't exist anymore. Another process likely deleted it before + // killing the process. We want to fatal the process, but without + // triggering error reporting like Sentry. + fataler.Fatalf("%s:\norig err: %s\nlist err: %s", redact.Safe(fs.PathBase(filename)), err, lsErr) + } + var total, unknown, tables, logs, manifests int + total = len(ls) + for _, f := range ls { + typ, _, ok := ParseFilename(fs, f) + if !ok { + unknown++ + continue + } + switch typ { + case FileTypeTable: + tables++ + case FileTypeLog: + logs++ + case FileTypeManifest: + manifests++ + } + } + + fataler.Fatalf("%s:\n%s\ndirectory contains %d files, %d unknown, %d tables, %d logs, %d manifests", + fs.PathBase(filename), err, total, unknown, tables, logs, manifests) +} diff --git a/pebble/internal/base/filenames_test.go b/pebble/internal/base/filenames_test.go new file mode 100644 index 0000000..07b7430 --- /dev/null +++ b/pebble/internal/base/filenames_test.go @@ -0,0 +1,114 @@ +// Copyright 2012 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 base + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/redact" + "github.com/stretchr/testify/require" +) + +func TestParseFilename(t *testing.T) { + testCases := map[string]bool{ + "000000.log": true, + "000000.log.zip": false, + "000000..log": false, + "a000000.log": false, + "abcdef.log": false, + "000001ldb": false, + "000001.sst": true, + "CURRENT": true, + "CURRaNT": false, + "LOCK": true, + "xLOCK": false, + "x.LOCK": false, + "MANIFEST": false, + "MANIFEST123456": false, + "MANIFEST-": false, + "MANIFEST-123456": true, + "MANIFEST-123456.doc": false, + "OPTIONS": false, + "OPTIONS123456": false, + "OPTIONS-": false, + "OPTIONS-123456": true, + "OPTIONS-123456.doc": false, + "CURRENT.123456": false, + "CURRENT.dbtmp": false, + "CURRENT.123456.dbtmp": true, + "temporary.123456.dbtmp": true, + } + fs := vfs.NewMem() + for tc, want := range testCases { + _, _, got := ParseFilename(fs, fs.PathJoin("foo", tc)) + if got != want { + t.Errorf("%q: got %v, want %v", tc, got, want) + } + } +} + +func TestFilenameRoundTrip(t *testing.T) { + testCases := map[FileType]bool{ + // CURRENT and LOCK files aren't numbered. + FileTypeCurrent: false, + FileTypeLock: false, + // The remaining file types are numbered. + FileTypeLog: true, + FileTypeManifest: true, + FileTypeTable: true, + FileTypeOptions: true, + FileTypeOldTemp: true, + FileTypeTemp: true, + } + fs := vfs.NewMem() + for fileType, numbered := range testCases { + fileNums := []FileNum{0} + if numbered { + fileNums = []FileNum{0, 1, 2, 3, 10, 42, 99, 1001} + } + for _, fileNum := range fileNums { + filename := MakeFilepath(fs, "foo", fileType, fileNum.DiskFileNum()) + gotFT, gotFN, gotOK := ParseFilename(fs, filename) + if !gotOK { + t.Errorf("could not parse %q", filename) + continue + } + if gotFT != fileType || gotFN.FileNum() != fileNum { + t.Errorf("filename=%q: got %v, %v, want %v, %v", filename, gotFT, gotFN, fileType, fileNum) + continue + } + } + } +} + +type bufferFataler struct { + buf bytes.Buffer +} + +func (b *bufferFataler) Fatalf(msg string, args ...interface{}) { + fmt.Fprintf(&b.buf, msg, args...) +} + +func TestMustExist(t *testing.T) { + err := os.ErrNotExist + fs := vfs.Default + var buf bufferFataler + filename := fs.PathJoin("..", "..", "testdata", "db-stage-4", "000000.sst") + + MustExist(fs, filename, &buf, err) + require.Equal(t, `000000.sst: +file does not exist +directory contains 9 files, 2 unknown, 1 tables, 1 logs, 2 manifests`, buf.buf.String()) +} + +func TestRedactFileNum(t *testing.T) { + // Ensure that redaction never redacts file numbers. + require.Equal(t, redact.RedactableString("000005"), redact.Sprint(FileNum(5))) + require.Equal(t, redact.RedactableString("000005"), redact.Sprint(DiskFileNum(5))) +} diff --git a/pebble/internal/base/internal.go b/pebble/internal/base/internal.go new file mode 100644 index 0000000..db691ee --- /dev/null +++ b/pebble/internal/base/internal.go @@ -0,0 +1,502 @@ +// 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 base // import "github.com/cockroachdb/pebble/internal/base" + +import ( + "encoding/binary" + "fmt" + "strconv" + "strings" + + "github.com/cockroachdb/redact" + "github.com/cockroachdb/pebble/shims/cmp" +) + +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 +// value, a merged value, etc. +type InternalKeyKind uint8 + +// These constants are part of the file format, and should not be changed. +const ( + InternalKeyKindDelete InternalKeyKind = 0 + InternalKeyKindSet InternalKeyKind = 1 + InternalKeyKindMerge InternalKeyKind = 2 + InternalKeyKindLogData InternalKeyKind = 3 + //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 + //InternalKeyKindEndPrepareXID InternalKeyKind = 10 + //InternalKeyKindCommitXID InternalKeyKind = 11 + //InternalKeyKindRollbackXID InternalKeyKind = 12 + //InternalKeyKindNoop InternalKeyKind = 13 + //InternalKeyKindColumnFamilyRangeDelete InternalKeyKind = 14 + InternalKeyKindRangeDelete InternalKeyKind = 15 + //InternalKeyKindColumnFamilyBlobIndex InternalKeyKind = 16 + //InternalKeyKindBlobIndex InternalKeyKind = 17 + + // InternalKeyKindSeparator is a key used for separator / successor keys + // written to sstable block indexes. + // + // NOTE: the RocksDB value has been repurposed. This was done to ensure that + // keys written to block indexes with value "17" (when 17 happened to be the + // max value, and InternalKeyKindMax was therefore set to 17), remain stable + // when new key kinds are supported in Pebble. + InternalKeyKindSeparator InternalKeyKind = 17 + + // InternalKeyKindSetWithDelete keys are SET keys that have met with a + // DELETE or SINGLEDEL key in a prior compaction. This key kind is + // specific to Pebble. See + // https://github.com/cockroachdb/pebble/issues/1255. + InternalKeyKindSetWithDelete InternalKeyKind = 18 + + // InternalKeyKindRangeKeyDelete removes all range keys within a key range. + // See the internal/rangekey package for more details. + InternalKeyKindRangeKeyDelete InternalKeyKind = 19 + // InternalKeyKindRangeKeySet and InternalKeyKindRangeUnset represent + // keys that set and unset values associated with ranges of key + // space. See the internal/rangekey package for more details. + InternalKeyKindRangeKeyUnset InternalKeyKind = 20 + InternalKeyKindRangeKeySet InternalKeyKind = 21 + + // 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. + InternalKeyKindIngestSST InternalKeyKind = 22 + + // 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 + // user key and decreasing by sequence number). Thus, use InternalKeyKindMax, + // 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 = 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(255) + + // A marker for an invalid key. + InternalKeyKindInvalid InternalKeyKind = InternalKeyKindSSTableInternalObsoleteMask + + // InternalKeySeqNumBatch is a bit that is set on batch sequence numbers + // which prevents those entries from being excluded from iteration. + InternalKeySeqNumBatch = uint64(1 << 55) + + // InternalKeySeqNumMax is the largest valid sequence number. + InternalKeySeqNumMax = uint64(1<<56 - 1) + + // InternalKeyRangeDeleteSentinel is the marker for a range delete sentinel + // key. This sequence number and kind are used for the upper stable boundary + // when a range deletion tombstone is the largest key in an sstable. This is + // necessary because sstable boundaries are inclusive, while the end key of a + // range deletion tombstone is exclusive. + InternalKeyRangeDeleteSentinel = (InternalKeySeqNumMax << 8) | uint64(InternalKeyKindRangeDelete) + + // InternalKeyBoundaryRangeKey is the marker for a range key boundary. This + // sequence number and kind are used during interleaved range key and point + // iteration to allow an iterator to stop at range key start keys where + // there exists no point key. + InternalKeyBoundaryRangeKey = (InternalKeySeqNumMax << 8) | uint64(InternalKeyKindRangeKeySet) +) + +// Assert InternalKeyKindSSTableInternalObsoleteBit > InternalKeyKindMax +const _ = uint(InternalKeyKindSSTableInternalObsoleteBit - InternalKeyKindMax - 1) + +var internalKeyKindNames = []string{ + InternalKeyKindDelete: "DEL", + InternalKeyKindSet: "SET", + InternalKeyKindMerge: "MERGE", + InternalKeyKindLogData: "LOGDATA", + InternalKeyKindSingleDelete: "SINGLEDEL", + InternalKeyKindRangeDelete: "RANGEDEL", + InternalKeyKindSeparator: "SEPARATOR", + InternalKeyKindSetWithDelete: "SETWITHDEL", + InternalKeyKindRangeKeySet: "RANGEKEYSET", + InternalKeyKindRangeKeyUnset: "RANGEKEYUNSET", + InternalKeyKindRangeKeyDelete: "RANGEKEYDEL", + InternalKeyKindIngestSST: "INGESTSST", + InternalKeyKindDeleteSized: "DELSIZED", + InternalKeyKindInvalid: "INVALID", +} + +func (k InternalKeyKind) String() string { + if int(k) < len(internalKeyKindNames) { + return internalKeyKindNames[k] + } + 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. +// +// It consists of the user key (as given by the code that uses package pebble) +// followed by 8-bytes of metadata: +// - 1 byte for the type of internal key: delete or set, +// - 7 bytes for a uint56 sequence number, in little-endian format. +type InternalKey struct { + UserKey []byte + Trailer uint64 +} + +// InvalidInternalKey is an invalid internal key for which Valid() will return +// false. +var InvalidInternalKey = MakeInternalKey(nil, 0, InternalKeyKindInvalid) + +// MakeInternalKey constructs an internal key from a specified user key, +// sequence number and kind. +func MakeInternalKey(userKey []byte, seqNum uint64, kind InternalKeyKind) InternalKey { + return InternalKey{ + UserKey: userKey, + Trailer: (seqNum << 8) | uint64(kind), + } +} + +// MakeTrailer constructs an internal key trailer from the specified sequence +// number and kind. +func MakeTrailer(seqNum uint64, kind InternalKeyKind) uint64 { + return (seqNum << 8) | uint64(kind) +} + +// MakeSearchKey constructs an internal key that is appropriate for searching +// for a the specified user key. The search key contain the maximal sequence +// number and kind ensuring that it sorts before any other internal keys for +// the same user key. +func MakeSearchKey(userKey []byte) InternalKey { + return InternalKey{ + UserKey: userKey, + Trailer: (InternalKeySeqNumMax << 8) | uint64(InternalKeyKindMax), + } +} + +// MakeRangeDeleteSentinelKey constructs an internal key that is a range +// deletion sentinel key, used as the upper boundary for an sstable when a +// range deletion is the largest key in an sstable. +func MakeRangeDeleteSentinelKey(userKey []byte) InternalKey { + return InternalKey{ + UserKey: userKey, + Trailer: InternalKeyRangeDeleteSentinel, + } +} + +// MakeExclusiveSentinelKey constructs an internal key that is an +// exclusive sentinel key, used as the upper boundary for an sstable +// when a ranged key is the largest key in an sstable. +func MakeExclusiveSentinelKey(kind InternalKeyKind, userKey []byte) InternalKey { + return InternalKey{ + UserKey: userKey, + Trailer: (InternalKeySeqNumMax << 8) | uint64(kind), + } +} + +var kindsMap = map[string]InternalKeyKind{ + "DEL": InternalKeyKindDelete, + "SINGLEDEL": InternalKeyKindSingleDelete, + "RANGEDEL": InternalKeyKindRangeDelete, + "LOGDATA": InternalKeyKindLogData, + "SET": InternalKeyKindSet, + "MERGE": InternalKeyKindMerge, + "INVALID": InternalKeyKindInvalid, + "SEPARATOR": InternalKeyKindSeparator, + "SETWITHDEL": InternalKeyKindSetWithDelete, + "RANGEKEYSET": InternalKeyKindRangeKeySet, + "RANGEKEYUNSET": InternalKeyKindRangeKeyUnset, + "RANGEKEYDEL": InternalKeyKindRangeKeyDelete, + "INGESTSST": InternalKeyKindIngestSST, + "DELSIZED": InternalKeyKindDeleteSized, +} + +// ParseInternalKey parses the string representation of an internal key. The +// format is ... If the seq-num starts with a "b" it +// is marked as a batch-seq-num (i.e. the InternalKeySeqNumBatch bit is set). +func ParseInternalKey(s string) InternalKey { + x := strings.Split(s, ".") + ukey := x[0] + kind, ok := kindsMap[x[1]] + if !ok { + panic(fmt.Sprintf("unknown kind: %q", x[1])) + } + j := 0 + if x[2][0] == 'b' { + j = 1 + } + seqNum, _ := strconv.ParseUint(x[2][j:], 10, 64) + if x[2][0] == 'b' { + seqNum |= InternalKeySeqNumBatch + } + return MakeInternalKey([]byte(ukey), seqNum, kind) +} + +// ParseKind parses the string representation of an internal key kind. +func ParseKind(s string) InternalKeyKind { + kind, ok := kindsMap[s] + if !ok { + panic(fmt.Sprintf("unknown kind: %q", s)) + } + return kind +} + +// InternalTrailerLen is the number of bytes used to encode InternalKey.Trailer. +const InternalTrailerLen = 8 + +// DecodeInternalKey decodes an encoded internal key. See InternalKey.Encode(). +func DecodeInternalKey(encodedKey []byte) InternalKey { + n := len(encodedKey) - InternalTrailerLen + var trailer uint64 + if n >= 0 { + trailer = binary.LittleEndian.Uint64(encodedKey[n:]) + encodedKey = encodedKey[:n:n] + } else { + trailer = uint64(InternalKeyKindInvalid) + encodedKey = nil + } + return InternalKey{ + UserKey: encodedKey, + Trailer: trailer, + } +} + +// InternalCompare compares two internal keys using the specified comparison +// function. For equal user keys, internal keys compare in descending sequence +// number order. For equal user keys and sequence numbers, internal keys +// compare in descending kind order (this may happen in practice among range +// keys). +func InternalCompare(userCmp Compare, a, b InternalKey) int { + if x := userCmp(a.UserKey, b.UserKey); x != 0 { + return x + } + // Reverse order for trailer comparison. + return cmp.Compare(b.Trailer, a.Trailer) +} + +// Encode encodes the receiver into the buffer. The buffer must be large enough +// to hold the encoded data. See InternalKey.Size(). +func (k InternalKey) Encode(buf []byte) { + i := copy(buf, k.UserKey) + binary.LittleEndian.PutUint64(buf[i:], k.Trailer) +} + +// EncodeTrailer returns the trailer encoded to an 8-byte array. +func (k InternalKey) EncodeTrailer() [8]byte { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], k.Trailer) + return buf +} + +// Separator returns a separator key such that k <= x && x < other, where less +// than is consistent with the Compare function. The buf parameter may be used +// to store the returned InternalKey.UserKey, though it is valid to pass a +// nil. See the Separator type for details on separator keys. +func (k InternalKey) Separator( + cmp Compare, sep Separator, buf []byte, other InternalKey, +) InternalKey { + buf = sep(buf, k.UserKey, other.UserKey) + if len(buf) <= len(k.UserKey) && cmp(k.UserKey, buf) < 0 { + // The separator user key is physically shorter than k.UserKey (if it is + // longer, we'll continue to use "k"), but logically after. Tack on the max + // sequence number to the shortened user key. Note that we could tack on + // any sequence number and kind here to create a valid separator key. We + // use the max sequence number to match the behavior of LevelDB and + // RocksDB. + return MakeInternalKey(buf, InternalKeySeqNumMax, InternalKeyKindSeparator) + } + return k +} + +// Successor returns a successor key such that k <= x. A simple implementation +// may return k unchanged. The buf parameter may be used to store the returned +// InternalKey.UserKey, though it is valid to pass a nil. +func (k InternalKey) Successor(cmp Compare, succ Successor, buf []byte) InternalKey { + buf = succ(buf, k.UserKey) + if len(buf) <= len(k.UserKey) && cmp(k.UserKey, buf) < 0 { + // The successor user key is physically shorter that k.UserKey (if it is + // longer, we'll continue to use "k"), but logically after. Tack on the max + // sequence number to the shortened user key. Note that we could tack on + // any sequence number and kind here to create a valid separator key. We + // use the max sequence number to match the behavior of LevelDB and + // RocksDB. + return MakeInternalKey(buf, InternalKeySeqNumMax, InternalKeyKindSeparator) + } + return k +} + +// Size returns the encoded size of the key. +func (k InternalKey) Size() int { + return len(k.UserKey) + 8 +} + +// SetSeqNum sets the sequence number component of the key. +func (k *InternalKey) SetSeqNum(seqNum uint64) { + k.Trailer = (seqNum << 8) | (k.Trailer & 0xff) +} + +// SeqNum returns the sequence number component of the key. +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 { + return Visible(k.SeqNum(), snapshot, batchSnapshot) +} + +// Visible returns true if a key with the provided sequence number is visible at +// the specified snapshot sequence numbers. +func Visible(seqNum uint64, snapshot, batchSnapshot uint64) bool { + // There are two snapshot sequence numbers, one for committed keys and one + // for batch keys. If a seqNum is less than `snapshot`, then seqNum + // corresponds to a committed key that is visible. If seqNum has its batch + // bit set, then seqNum corresponds to an uncommitted batch key. Its + // visible if its snapshot is less than batchSnapshot. + // + // There's one complication. The maximal sequence number + // (`InternalKeySeqNumMax`) is used across Pebble for exclusive sentinel + // keys and other purposes. The maximal sequence number has its batch bit + // set, but it can never be < `batchSnapshot`, since there is no expressible + // larger snapshot. We dictate that the maximal sequence number is always + // visible. + return seqNum < snapshot || + ((seqNum&InternalKeySeqNumBatch) != 0 && seqNum < batchSnapshot) || + seqNum == InternalKeySeqNumMax +} + +// SetKind sets the kind component of the key. +func (k *InternalKey) SetKind(kind InternalKeyKind) { + k.Trailer = (k.Trailer &^ 0xff) | uint64(kind) +} + +// Kind returns the kind component of the key. +func (k InternalKey) Kind() InternalKeyKind { + return TrailerKind(k.Trailer) +} + +// TrailerKind returns the key kind of the key trailer. +func TrailerKind(trailer uint64) InternalKeyKind { + return InternalKeyKind(trailer & 0xff) +} + +// Valid returns true if the key has a valid kind. +func (k InternalKey) Valid() bool { + return k.Kind() <= InternalKeyKindMax +} + +// Clone clones the storage for the UserKey component of the key. +func (k InternalKey) Clone() InternalKey { + if len(k.UserKey) == 0 { + return k + } + return InternalKey{ + UserKey: append([]byte(nil), k.UserKey...), + Trailer: k.Trailer, + } +} + +// 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()) +} + +// Pretty returns a formatter for the key. +func (k InternalKey) Pretty(f FormatKey) fmt.Formatter { + return prettyInternalKey{k, f} +} + +// IsExclusiveSentinel returns whether this internal key excludes point keys +// with the same user key if used as an end boundary. See the comment on +// InternalKeyRangeDeletionSentinel. +func (k InternalKey) IsExclusiveSentinel() bool { + switch kind := k.Kind(); kind { + case InternalKeyKindRangeDelete: + return k.Trailer == InternalKeyRangeDeleteSentinel + case InternalKeyKindRangeKeyDelete, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeySet: + return (k.Trailer >> 8) == InternalKeySeqNumMax + default: + return false + } +} + +type prettyInternalKey struct { + InternalKey + formatKey FormatKey +} + +func (k prettyInternalKey) Format(s fmt.State, c rune) { + if seqNum := k.SeqNum(); seqNum == InternalKeySeqNumMax { + fmt.Fprintf(s, "%s#inf,%s", k.formatKey(k.UserKey), k.Kind()) + } else { + fmt.Fprintf(s, "%s#%d,%s", k.formatKey(k.UserKey), k.SeqNum(), k.Kind()) + } +} + +// ParsePrettyInternalKey parses the pretty string representation of an +// internal key. The format is #,. +func ParsePrettyInternalKey(s string) InternalKey { + x := strings.FieldsFunc(s, func(c rune) bool { return c == '#' || c == ',' }) + ukey := x[0] + kind, ok := kindsMap[x[2]] + if !ok { + panic(fmt.Sprintf("unknown kind: %q", x[2])) + } + var seqNum uint64 + if x[1] == "max" || x[1] == "inf" { + seqNum = InternalKeySeqNumMax + } else { + seqNum, _ = strconv.ParseUint(x[1], 10, 64) + } + return MakeInternalKey([]byte(ukey), seqNum, kind) +} diff --git a/pebble/internal/base/internal_test.go b/pebble/internal/base/internal_test.go new file mode 100644 index 0000000..39466cd --- /dev/null +++ b/pebble/internal/base/internal_test.go @@ -0,0 +1,226 @@ +// Copyright 2012 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 base + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func (k InternalKey) encodedString() string { + buf := make([]byte, k.Size()) + k.Encode(buf) + return string(buf) +} + +func TestInternalKey(t *testing.T) { + k := MakeInternalKey([]byte("foo"), 0x08070605040302, 1) + if got, want := k.encodedString(), "foo\x01\x02\x03\x04\x05\x06\x07\x08"; got != want { + t.Fatalf("k = %q want %q", got, want) + } + if !k.Valid() { + t.Fatalf("invalid key") + } + if got, want := string(k.UserKey), "foo"; got != want { + t.Errorf("ukey = %q want %q", got, want) + } + if got, want := k.Kind(), InternalKeyKind(1); got != want { + t.Errorf("kind = %d want %d", got, want) + } + if got, want := k.SeqNum(), uint64(0x08070605040302); got != want { + t.Errorf("seqNum = %d want %d", got, want) + } +} + +func TestInvalidInternalKey(t *testing.T) { + testCases := []string{ + "", + "\x01\x02\x03\x04\x05\x06\x07", + "foo", + "foo\x08\x07\x06\x05\x04\x03\x02", + "foo\x18\x07\x06\x05\x04\x03\x02\x01", + } + for _, tc := range testCases { + k := DecodeInternalKey([]byte(tc)) + if k.Valid() { + t.Errorf("%q is a valid key, want invalid", tc) + } + // Invalid key kind because the key doesn't have an 8 byte trailer. + if k.Kind() == InternalKeyKindInvalid && k.UserKey != nil { + t.Errorf("expected nil UserKey after decoding encodedKey=%q", tc) + } + } +} + +func TestInternalKeyComparer(t *testing.T) { + // keys are some internal keys, in sorted order. + keys := []string{ + // The remaining test keys are all valid. + "" + "\x01\xff\xff\xff\xff\xff\xff\xff", + "" + "\x00\xff\xff\xff\xff\xff\xff\xff", + "" + "\x01\x01\x00\x00\x00\x00\x00\x00", + "" + "\x00\x01\x00\x00\x00\x00\x00\x00", + // Invalid internal keys have no user key, but have trailer "\xff \x00 \x00 \x00 \x00 \x00 \x00 \x00" + // i.e. seqNum 0 and kind 255 (InternalKeyKindInvalid). + "", + "" + "\x01\x00\x00\x00\x00\x00\x00\x00", + "" + "\x00\x00\x00\x00\x00\x00\x00\x00", + "\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00", + "\x00blue" + "\x01\x11\x00\x00\x00\x00\x00\x00", + "bl\x00ue" + "\x01\x11\x00\x00\x00\x00\x00\x00", + "blue" + "\x01\x11\x00\x00\x00\x00\x00\x00", + "blue\x00" + "\x01\x11\x00\x00\x00\x00\x00\x00", + "green" + "\xff\x11\x00\x00\x00\x00\x00\x00", + "green" + "\x01\x11\x00\x00\x00\x00\x00\x00", + "green" + "\x01\x00\x00\x00\x00\x00\x00\x00", + "red" + "\x01\xff\xff\xff\xff\xff\xff\xff", + "red" + "\x01\x72\x73\x74\x75\x76\x77\x78", + "red" + "\x01\x00\x00\x00\x00\x00\x00\x11", + "red" + "\x01\x00\x00\x00\x00\x00\x11\x00", + "red" + "\x01\x00\x00\x00\x00\x11\x00\x00", + "red" + "\x01\x00\x00\x00\x11\x00\x00\x00", + "red" + "\x01\x00\x00\x11\x00\x00\x00\x00", + "red" + "\x01\x00\x11\x00\x00\x00\x00\x00", + "red" + "\x01\x11\x00\x00\x00\x00\x00\x00", + "red" + "\x00\x11\x00\x00\x00\x00\x00\x00", + "red" + "\x00\x00\x00\x00\x00\x00\x00\x00", + "\xfe" + "\x01\xff\xff\xff\xff\xff\xff\xff", + "\xfe" + "\x00\x00\x00\x00\x00\x00\x00\x00", + "\xff" + "\x01\xff\xff\xff\xff\xff\xff\xff", + "\xff" + "\x00\x00\x00\x00\x00\x00\x00\x00", + "\xff\x40" + "\x01\xff\xff\xff\xff\xff\xff\xff", + "\xff\x40" + "\x00\x00\x00\x00\x00\x00\x00\x00", + "\xff\xff" + "\x01\xff\xff\xff\xff\xff\xff\xff", + "\xff\xff" + "\x00\x00\x00\x00\x00\x00\x00\x00", + } + c := DefaultComparer.Compare + for i := range keys { + for j := range keys { + ik := DecodeInternalKey([]byte(keys[i])) + jk := DecodeInternalKey([]byte(keys[j])) + got := InternalCompare(c, ik, jk) + want := 0 + if i < j { + want = -1 + } else if i > j { + want = +1 + } + if got != want { + t.Errorf("i=%d, j=%d, keys[i]=%q, keys[j]=%q: got %d, want %d", + i, j, keys[i], keys[j], got, want) + } + } + } +} + +func TestKindsRoundtrip(t *testing.T) { + for kindNum, prettied := range internalKeyKindNames { + if prettied == "" { + continue + } + kind := InternalKeyKind(kindNum) + got := ParseKind(kind.String()) + require.Equal(t, got, kind) + } +} + +func TestInternalKeySeparator(t *testing.T) { + testCases := []struct { + a string + b string + expected string + }{ + {"foo.SET.100", "foo.SET.99", "foo.SET.100"}, + {"foo.SET.100", "foo.SET.100", "foo.SET.100"}, + {"foo.SET.100", "foo.DEL.100", "foo.SET.100"}, + {"foo.SET.100", "foo.SET.101", "foo.SET.100"}, + {"foo.SET.100", "bar.SET.99", "foo.SET.100"}, + {"foo.SET.100", "hello.SET.200", "g.SEPARATOR.72057594037927935"}, + {"ABC1AAAAA.SET.100", "ABC2ABB.SET.200", "ABC2.SEPARATOR.72057594037927935"}, + {"AAA1AAA.SET.100", "AAA2AA.SET.200", "AAA2.SEPARATOR.72057594037927935"}, + {"AAA1AAA.SET.100", "AAA4.SET.200", "AAA2.SEPARATOR.72057594037927935"}, + {"AAA1AAA.SET.100", "AAA2.SET.200", "AAA1B.SEPARATOR.72057594037927935"}, + {"AAA1AAA.SET.100", "AAA2A.SET.200", "AAA2.SEPARATOR.72057594037927935"}, + {"AAA1.SET.100", "AAA2.SET.200", "AAA1.SET.100"}, + {"foo.SET.100", "foobar.SET.200", "foo.SET.100"}, + {"foobar.SET.100", "foo.SET.200", "foobar.SET.100"}, + {"foo.INGESTSST.100", "foo.INGESTSST.99", "foo.INGESTSST.100"}, + } + d := DefaultComparer + for _, c := range testCases { + t.Run("", func(t *testing.T) { + a := ParseInternalKey(c.a) + b := ParseInternalKey(c.b) + expected := ParseInternalKey(c.expected) + result := a.Separator(d.Compare, d.Separator, nil, b) + if cmp := InternalCompare(d.Compare, expected, result); cmp != 0 { + t.Fatalf("expected %s, but found %s", expected, result) + } + }) + } +} + +func TestIsExclusiveSentinel(t *testing.T) { + userKey := []byte("foo") + testCases := []struct { + name string + key InternalKey + want bool + }{ + { + name: "rangedel; max seqnum", + key: MakeInternalKey(userKey, InternalKeySeqNumMax, InternalKeyKindRangeKeyDelete), + want: true, + }, + { + name: "rangedel; non-max seqnum", + key: MakeInternalKey(userKey, 42, InternalKeyKindRangeKeyDelete), + want: false, + }, + { + name: "rangekeyset; max seqnum", + key: MakeInternalKey(userKey, InternalKeySeqNumMax, InternalKeyKindRangeKeySet), + want: true, + }, + { + name: "rangekeyset; non-max seqnum", + key: MakeInternalKey(userKey, 42, InternalKeyKindRangeKeySet), + want: false, + }, + { + name: "rangekeyunset; max seqnum", + key: MakeInternalKey(userKey, InternalKeySeqNumMax, InternalKeyKindRangeKeyUnset), + want: true, + }, + { + name: "rangekeyunset; non-max seqnum", + key: MakeInternalKey(userKey, 42, InternalKeyKindRangeKeyUnset), + want: false, + }, + { + name: "rangekeydel; max seqnum", + key: MakeInternalKey(userKey, InternalKeySeqNumMax, InternalKeyKindRangeKeyDelete), + want: true, + }, + { + name: "rangekeydel; non-max seqnum", + key: MakeInternalKey(userKey, 42, InternalKeyKindRangeKeyDelete), + want: false, + }, + { + name: "neither rangedel nor rangekey", + key: MakeInternalKey(userKey, InternalKeySeqNumMax, InternalKeyKindSet), + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := tc.key.IsExclusiveSentinel() + require.Equal(t, tc.want, got) + }) + } +} diff --git a/pebble/internal/base/iterator.go b/pebble/internal/base/iterator.go new file mode 100644 index 0000000..1b72432 --- /dev/null +++ b/pebble/internal/base/iterator.go @@ -0,0 +1,414 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package base + +import ( + "context" + "fmt" + "time" +) + +// InternalIterator iterates over a DB's key/value pairs in key order. Unlike +// the Iterator interface, the returned keys are InternalKeys composed of the +// user-key, a sequence number and a key kind. In forward iteration, key/value +// pairs for identical user-keys are returned in descending sequence order. In +// reverse iteration, key/value pairs for identical user-keys are returned in +// ascending sequence order. +// +// InternalIterators provide 5 absolute positioning methods and 2 relative +// positioning methods. The absolute positioning methods are: +// +// - SeekGE +// - SeekPrefixGE +// - SeekLT +// - First +// - Last +// +// The relative positioning methods are: +// +// - Next +// - Prev +// +// The relative positioning methods can be used in conjunction with any of the +// absolute positioning methods with one exception: SeekPrefixGE does not +// support reverse iteration via Prev. It is undefined to call relative +// positioning methods without ever calling an absolute positioning method. +// +// InternalIterators can optionally implement a prefix iteration mode. This +// mode is entered by calling SeekPrefixGE and exited by any other absolute +// positioning method (SeekGE, SeekLT, First, Last). When in prefix iteration +// mode, a call to Next will advance to the next key which has the same +// "prefix" as the one supplied to SeekPrefixGE. Note that "prefix" in this +// context is not a strict byte prefix, but defined by byte equality for the +// result of the Comparer.Split method. An InternalIterator is not required to +// support prefix iteration mode, and can implement SeekPrefixGE by forwarding +// to SeekGE. When the iteration prefix is exhausted, it is not valid to call +// Next on an internal iterator that's already returned (nil,nilv) or a key +// beyond the prefix. +// +// Bounds, [lower, upper), can be set on iterators, either using the SetBounds() +// function in the interface, or in implementation specific ways during iterator +// creation. The forward positioning routines (SeekGE, First, and Next) only +// check the upper bound. The reverse positioning routines (SeekLT, Last, and +// Prev) only check the lower bound. It is up to the caller to ensure that the +// forward positioning routines respect the lower bound and the reverse +// positioning routines respect the upper bound (i.e. calling SeekGE instead of +// First if there is a lower bound, and SeekLT instead of Last if there is an +// upper bound). This imposition is done in order to elevate that enforcement to +// the caller (generally pebble.Iterator or pebble.mergingIter) rather than +// having it duplicated in every InternalIterator implementation. +// +// Additionally, the caller needs to ensure that SeekGE/SeekPrefixGE are not +// called with a key > the upper bound, and SeekLT is not called with a key < +// the lower bound. InternalIterator implementations are required to respect +// the iterator bounds, never returning records outside of the bounds with one +// exception: an iterator may generate synthetic RANGEDEL marker records. See +// levelIter.syntheticBoundary for the sole existing example of this behavior. +// Specifically, levelIter can return synthetic keys whose user key is equal to +// the lower/upper bound. +// +// The bounds provided to an internal iterator must remain valid until a +// subsequent call to SetBounds has returned. This requirement exists so that +// iterator implementations may compare old and new bounds to apply low-level +// optimizations. The pebble.Iterator satisfies this requirement by maintaining +// two bound buffers and switching between them. +// +// An iterator must be closed after use, but it is not necessary to read an +// iterator until exhaustion. +// +// An iterator is not goroutine-safe, but it is safe to use multiple iterators +// concurrently, either in separate goroutines or switching between the +// iterators in a single goroutine. +// +// It is also safe to use an iterator concurrently with modifying its +// underlying DB, if that DB permits modification. However, the resultant +// key/value pairs are not guaranteed to be a consistent snapshot of that DB +// at a particular point in time. +// +// InternalIterators accumulate errors encountered during operation, exposing +// them through the Error method. All of the absolute positioning methods +// reset any accumulated error before positioning. Relative positioning +// methods return without advancing if the iterator has accumulated an error. +// +// nilv == shorthand for LazyValue{}, which represents a nil value. +type InternalIterator interface { + // SeekGE moves the iterator to the first key/value pair whose key is greater + // than or equal to the given key. Returns the key and value if the iterator + // is pointing at a valid entry, and (nil, nilv) otherwise. 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. + SeekGE(key []byte, flags SeekGEFlags) (*InternalKey, LazyValue) + + // SeekPrefixGE moves the iterator to the first key/value pair whose key is + // greater than or equal to the given key. Returns the key and value if the + // iterator is pointing at a valid entry, and (nil, nilv) otherwise. 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. + // + // The prefix argument is used by some InternalIterator implementations (e.g. + // sstable.Reader) to avoid expensive operations. A user-defined Split + // function must be supplied to the Comparer for the DB. The supplied prefix + // will be the prefix of the given key returned by that Split function. If + // the iterator is able to determine that no key with the prefix exists, it + // can return (nil,nilv). Unlike SeekGE, this is not an indication that + // iteration is exhausted. + // + // Note that the iterator may return keys not matching the prefix. It is up + // to the caller to check if the prefix matches. + // + // Calling SeekPrefixGE places the receiver into prefix iteration mode. Once + // in this mode, reverse iteration may not be supported and will return an + // error. Note that pebble/Iterator.SeekPrefixGE has this same restriction on + // not supporting reverse iteration in prefix iteration mode until a + // different positioning routine (SeekGE, SeekLT, First or Last) switches the + // iterator out of prefix iteration. + SeekPrefixGE(prefix, key []byte, flags SeekGEFlags) (*InternalKey, LazyValue) + + // SeekLT moves the iterator to the last key/value pair whose key is less + // than the given key. Returns the key and value if the iterator is pointing + // at a valid entry, and (nil, nilv) otherwise. Note that SeekLT only checks + // the lower bound. It is up to the caller to ensure that key is less than + // the upper bound. + SeekLT(key []byte, flags SeekLTFlags) (*InternalKey, LazyValue) + + // First moves the iterator the the first key/value pair. Returns the key and + // value if the iterator is pointing at a valid entry, and (nil, nilv) + // otherwise. Note that First only checks the upper bound. It is up to the + // caller to ensure that First() is not called when there is a lower bound, + // and instead call SeekGE(lower). + First() (*InternalKey, LazyValue) + + // Last moves the iterator the the last key/value pair. Returns the key and + // value if the iterator is pointing at a valid entry, and (nil, nilv) + // otherwise. Note that Last only checks the lower bound. It is up to the + // caller to ensure that Last() is not called when there is an upper bound, + // and instead call SeekLT(upper). + Last() (*InternalKey, LazyValue) + + // Next moves the iterator to the next key/value pair. Returns the key and + // value if the iterator is pointing at a valid entry, and (nil, nilv) + // otherwise. Note that Next only checks the upper bound. It is up to the + // caller to ensure that key is greater than or equal to the lower bound. + // + // It is valid to call Next when the iterator is positioned before the first + // key/value pair due to either a prior call to SeekLT or Prev which returned + // (nil, nilv). It is not allowed to call Next when the previous call to SeekGE, + // SeekPrefixGE or Next returned (nil, nilv). + Next() (*InternalKey, LazyValue) + + // NextPrefix moves the iterator to the next key/value pair with a different + // prefix than the key at the current iterator position. Returns the key and + // value if the iterator is pointing at a valid entry, and (nil, nil) + // otherwise. Note that NextPrefix only checks the upper bound. It is up to + // the caller to ensure that key is greater than or equal to the lower + // bound. + // + // NextPrefix is passed the immediate successor to the current prefix key. A + // valid implementation of NextPrefix is to call SeekGE with succKey. + // + // It is not allowed to call NextPrefix when the previous call was a reverse + // positioning operation or a call to a forward positioning method that + // returned (nil, nilv). It is also not allowed to call NextPrefix when the + // iterator is in prefix iteration mode. + NextPrefix(succKey []byte) (*InternalKey, LazyValue) + + // Prev moves the iterator to the previous key/value pair. Returns the key + // and value if the iterator is pointing at a valid entry, and (nil, nilv) + // otherwise. Note that Prev only checks the lower bound. It is up to the + // caller to ensure that key is less than the upper bound. + // + // It is valid to call Prev when the iterator is positioned after the last + // key/value pair due to either a prior call to SeekGE or Next which returned + // (nil, nilv). It is not allowed to call Prev when the previous call to SeekLT + // or Prev returned (nil, nilv). + Prev() (*InternalKey, LazyValue) + + // Error returns any accumulated error. It may not include errors returned + // to the client when calling LazyValue.Value(). + Error() error + + // Close closes the iterator and returns any accumulated error. Exhausting + // all the key/value pairs in a table is not considered to be an error. + // It is valid to call Close multiple times. Other methods should not be + // called after the iterator has been closed. + Close() error + + // SetBounds sets the lower and upper bounds for the iterator. Note that the + // result of Next and Prev will be undefined until the iterator has been + // repositioned with SeekGE, SeekPrefixGE, SeekLT, First, or Last. + // + // The bounds provided must remain valid until a subsequent call to + // SetBounds has returned. This requirement exists so that iterator + // implementations may compare old and new bounds to apply low-level + // optimizations. + SetBounds(lower, upper []byte) + + // SetContext replaces the context provided at iterator creation, or the + // last one provided by SetContext. + SetContext(ctx context.Context) + + fmt.Stringer +} + +// SeekGEFlags holds flags that may configure the behavior of a forward seek. +// Not all flags are relevant to all iterators. +type SeekGEFlags uint8 + +const ( + seekGEFlagTrySeekUsingNext uint8 = iota + seekGEFlagRelativeSeek + seekGEFlagBatchJustRefreshed +) + +// SeekGEFlagsNone is the default value of SeekGEFlags, with all flags disabled. +const SeekGEFlagsNone = SeekGEFlags(0) + +// TrySeekUsingNext indicates whether a performance optimization was enabled +// by a caller, indicating the caller has not done any action to move this +// iterator beyond the first key that would be found if this iterator were to +// honestly do the intended seek. For example, say the caller did a +// SeekGE(k1...), followed by SeekGE(k2...) where k1 <= k2, without any +// intermediate positioning calls. The caller can safely specify true for this +// parameter in the second call. As another example, say the caller did do one +// call to Next between the two Seek calls, and k1 < k2. Again, the caller can +// safely specify a true value for this parameter. Note that a false value is +// always safe. The callee is free to ignore the true value if its +// implementation does not permit this optimization. +// +// We make the caller do this determination since a string comparison of k1, k2 +// is not necessarily cheap, and there may be many iterators in the iterator +// stack. Doing it once at the root of the iterator stack is cheaper. +// +// This optimization could also be applied to SeekLT (where it would be +// trySeekUsingPrev). We currently only do it for SeekPrefixGE and SeekGE +// because this is where this optimization helps the performance of CockroachDB. +// The SeekLT cases in CockroachDB are typically accompanied with bounds that +// change between seek calls, and is optimized inside certain iterator +// implementations, like singleLevelIterator, without any extra parameter +// passing (though the same amortization of string comparisons could be done to +// improve that optimization, by making the root of the iterator stack do it). +func (s SeekGEFlags) TrySeekUsingNext() bool { return (s & (1 << seekGEFlagTrySeekUsingNext)) != 0 } + +// RelativeSeek is set when in the course of a forward positioning operation, a +// higher-level iterator seeks a lower-level iterator to a larger key than the +// one at the current iterator position. +// +// Concretely, this occurs when the merging iterator observes a range deletion +// covering the key at a level's current position, and the merging iterator +// seeks the level to the range deletion's end key. During lazy-combined +// iteration, this flag signals to the level iterator that the seek is NOT an +// absolute-positioning operation from the perspective of the pebble.Iterator, +// and the level iterator must look for range keys in tables between the current +// iterator position and the new seeked position. +func (s SeekGEFlags) RelativeSeek() bool { return (s & (1 << seekGEFlagRelativeSeek)) != 0 } + +// BatchJustRefreshed is set by Seek[Prefix]GE when an iterator's view of an +// indexed batch was just refreshed. It serves as a signal to the batch iterator +// to ignore the TrySeekUsingNext optimization, because the external knowledge +// imparted by the TrySeekUsingNext flag does not apply to the batch iterator's +// position. See (pebble.Iterator).batchJustRefreshed. +func (s SeekGEFlags) BatchJustRefreshed() bool { return (s & (1 << seekGEFlagBatchJustRefreshed)) != 0 } + +// EnableTrySeekUsingNext returns the provided flags with the +// try-seek-using-next optimization enabled. See TrySeekUsingNext for an +// explanation of this optimization. +func (s SeekGEFlags) EnableTrySeekUsingNext() SeekGEFlags { + return s | (1 << seekGEFlagTrySeekUsingNext) +} + +// DisableTrySeekUsingNext returns the provided flags with the +// try-seek-using-next optimization disabled. +func (s SeekGEFlags) DisableTrySeekUsingNext() SeekGEFlags { + return s &^ (1 << seekGEFlagTrySeekUsingNext) +} + +// EnableRelativeSeek returns the provided flags with the relative-seek flag +// enabled. See RelativeSeek for an explanation of this flag's use. +func (s SeekGEFlags) EnableRelativeSeek() SeekGEFlags { + return s | (1 << seekGEFlagRelativeSeek) +} + +// DisableRelativeSeek returns the provided flags with the relative-seek flag +// disabled. +func (s SeekGEFlags) DisableRelativeSeek() SeekGEFlags { + return s &^ (1 << seekGEFlagRelativeSeek) +} + +// EnableBatchJustRefreshed returns the provided flags with the +// batch-just-refreshed bit set. See BatchJustRefreshed for an explanation of +// this flag. +func (s SeekGEFlags) EnableBatchJustRefreshed() SeekGEFlags { + return s | (1 << seekGEFlagBatchJustRefreshed) +} + +// DisableBatchJustRefreshed returns the provided flags with the +// batch-just-refreshed bit unset. +func (s SeekGEFlags) DisableBatchJustRefreshed() SeekGEFlags { + return s &^ (1 << seekGEFlagBatchJustRefreshed) +} + +// SeekLTFlags holds flags that may configure the behavior of a reverse seek. +// Not all flags are relevant to all iterators. +type SeekLTFlags uint8 + +const ( + seekLTFlagRelativeSeek uint8 = iota +) + +// SeekLTFlagsNone is the default value of SeekLTFlags, with all flags disabled. +const SeekLTFlagsNone = SeekLTFlags(0) + +// RelativeSeek is set when in the course of a reverse positioning operation, a +// higher-level iterator seeks a lower-level iterator to a smaller key than the +// one at the current iterator position. +// +// Concretely, this occurs when the merging iterator observes a range deletion +// covering the key at a level's current position, and the merging iterator +// seeks the level to the range deletion's start key. During lazy-combined +// iteration, this flag signals to the level iterator that the seek is NOT an +// absolute-positioning operation from the perspective of the pebble.Iterator, +// and the level iterator must look for range keys in tables between the current +// iterator position and the new seeked position. +func (s SeekLTFlags) RelativeSeek() bool { return s&(1<= len(ref) { + // Verify that ref matches the flag predicates. + for j := 0; j < i; j++ { + if got := flags[j].pred(); ref[j] != got { + t.Errorf("%s() = %t, want %t", flags[j].label, got, ref[j]) + } + } + return + } + + // flag i remains unset. + t.Run(fmt.Sprintf("%s begin unset", flags[i].label), func(t *testing.T) { + checkCombination(t, i+1, flags, ref) + }) + + // set flag i + ref[i] = true + flags[i].set() + t.Run(fmt.Sprintf("%s set", flags[i].label), func(t *testing.T) { + checkCombination(t, i+1, flags, ref) + }) + + // unset flag i + ref[i] = false + flags[i].unset() + t.Run(fmt.Sprintf("%s unset", flags[i].label), func(t *testing.T) { + checkCombination(t, i+1, flags, ref) + }) +} diff --git a/pebble/internal/base/lazy_value.go b/pebble/internal/base/lazy_value.go new file mode 100644 index 0000000..cc6d56d --- /dev/null +++ b/pebble/internal/base/lazy_value.go @@ -0,0 +1,287 @@ +// 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 base + +import "github.com/cockroachdb/pebble/internal/invariants" + +// A value can have user-defined attributes that are a function of the value +// byte slice. For now, we only support "short attributes", which can be +// encoded in 3 bits. We will likely extend this to "long attributes" later +// for values that are even more expensive to access than those in value +// blocks in the same sstable. +// +// When a sstable writer chooses not to store a value together with the key, +// it can call the ShortAttributeExtractor to extract the attribute and store +// it together with the key. This allows for cheap retrieval of +// AttributeAndLen on the read-path, without doing a more expensive retrieval +// of the value. In general, the extraction code may want to also look at the +// key to decide how to treat the value, hence the key* parameters. +// +// Write path performance: The ShortAttributeExtractor func cannot be inlined, +// so we will pay the cost of this function call. However, we will only pay +// this when (a) the value is not being stored together with the key, and (b) +// the key-value pair is being initially written to the DB, or a compaction is +// transitioning the key-value pair from being stored together to being stored +// separately. + +// ShortAttribute encodes a user-specified attribute of the value. +type ShortAttribute uint8 + +// MaxShortAttribute is the maximum value of the short attribute (3 bits). +const MaxShortAttribute = 7 + +// ShortAttributeExtractor is an extractor that given the value, will return +// the ShortAttribute. +type ShortAttributeExtractor func( + key []byte, keyPrefixLen int, value []byte) (ShortAttribute, error) + +// AttributeAndLen represents the pair of value length and the short +// attribute. +type AttributeAndLen struct { + ValueLen int32 + ShortAttribute ShortAttribute +} + +// LazyValue represents a value that may not already have been extracted. +// Currently, it can represent either an in-place value (stored with the key) +// or a value stored in the value section. However, the interface is general +// enough to support values that are stored in separate files. +// +// LazyValue is used in the InternalIterator interface, such that all +// positioning calls return (*InternalKey, LazyValue). It is also exposed via +// the public Iterator for callers that need to remember a recent but not +// necessarily latest LazyValue, in case they need the actual value in the +// future. An example is a caller that is iterating in reverse and looking for +// the latest MVCC version for a key -- it cannot identify the latest MVCC +// version without stepping to the previous key-value pair e.g. +// storage.pebbleMVCCScanner in CockroachDB. +// +// Performance note: It is important for this struct to not exceed a sizeof 32 +// bytes, for optimizing the common case of the in-place value. Prior to +// introducing LazyValue, we were passing around a []byte which is 24 bytes. +// Passing a 40 byte or larger struct causes performance to drop by 75% on +// some benchmarks that do tight iteration loops. +// +// Memory management: +// This is subtle, but important for performance. +// +// A LazyValue returned by an InternalIterator or Iterator is unstable in that +// repositioning the iterator will invalidate the memory inside it. A caller +// wishing to maintain that LazyValue needs to call LazyValue.Clone(). Note +// that this does not fetch the value if it is not in-place. Clone() should +// ideally not be called if LazyValue.Value() has been called, since the +// cloned LazyValue will forget the extracted/fetched value, and calling +// Value() on this clone will cause the value to be extracted again. That is, +// Clone() does not make any promise about the memory stability of the +// underlying value. +// +// A user of an iterator that calls LazyValue.Value() wants as much as +// possible for the returned value []byte to point to iterator owned memory. +// +// 1. [P1] The underlying iterator that owns that memory also needs a promise +// from that user that at any time there is at most one value []byte slice +// that the caller is expecting it to maintain. Otherwise, the underlying +// iterator has to maintain multiple such []byte slices which results in +// more complicated and inefficient code. +// +// 2. [P2] The underlying iterator, in order to make the promise that it is +// maintaining the one value []byte slice, also needs a way to know when +// it is relieved of that promise. One way it is relieved of that promise +// is by being told that it is being repositioned. Typically, the owner of +// the value []byte slice is a sstable iterator, and it will know that it +// is relieved of the promise when it is repositioned. However, consider +// the case where the caller has used LazyValue.Clone() and repositioned +// the iterator (which is actually a tree of iterators). In this case the +// underlying sstable iterator may not even be open. LazyValue.Value() +// will still work (at a higher cost), but since the sstable iterator is +// not open, it does not have a mechanism to know when the retrieved value +// is no longer in use. We refer to this situation as "not satisfying P2". +// To handle this situation, the LazyValue.Value() method accepts a caller +// owned buffer, that the callee will use if needed. The callee explicitly +// tells the caller whether the []byte slice for the value is now owned by +// the caller. This will be true if the callee attempted to use buf and +// either successfully used it or allocated a new []byte slice. +// +// To ground the above in reality, we consider three examples of callers of +// LazyValue.Value(): +// +// - Iterator: it calls LazyValue.Value for its own use when merging values. +// When merging during reverse iteration, it may have cloned the LazyValue. +// In this case it calls LazyValue.Value() on the cloned value, merges it, +// and then calls LazyValue.Value() on the current iterator position and +// merges it. So it is honoring P1. +// +// - Iterator on behalf of Iterator clients: The Iterator.Value() method +// needs to call LazyValue.Value(). The client of Iterator is satisfying P1 +// because of the inherent Iterator interface constraint, i.e., it is calling +// Iterator.Value() on the current Iterator position. It is possible that +// the Iterator has cloned this LazyValue (for the reverse iteration case), +// which the client is unaware of, so the underlying sstable iterator may +// not be able to satisfy P2. This is ok because Iterator will call +// LazyValue.Value with its (reusable) owned buffer. +// +// - CockroachDB's pebbleMVCCScanner: This will use LazyValues from Iterator +// since during reverse iteration in order to find the highest version that +// satisfies a read it needs to clone the LazyValue, step back the iterator +// and then decide whether it needs the value from the previously cloned +// LazyValue. The pebbleMVCCScanner will satisfy P1. The P2 story is +// similar to the previous case in that it will call LazyValue.Value with +// its (reusable) owned buffer. +// +// Corollary: callers that directly use InternalIterator can know that they +// have done nothing to interfere with promise P2 can pass in a nil buf and be +// sure that it will not trigger an allocation. +// +// Repeated calling of LazyValue.Value: +// This is ok as long as the caller continues to satisfy P1. The previously +// fetched value will be remembered inside LazyValue to avoid fetching again. +// So if the caller's buffer is used the first time the value was fetched, it +// is still in use. +// +// LazyValue fields are visible outside the package for use in +// InternalIterator implementations and in Iterator, but not meant for direct +// use by users of Pebble. +type LazyValue struct { + // ValueOrHandle represents a value, or a handle to be passed to ValueFetcher. + // - Fetcher == nil: ValueOrHandle is a value. + // - Fetcher != nil: ValueOrHandle is a handle and Fetcher.Attribute is + // initialized. + // The ValueOrHandle exposed by InternalIterator or Iterator may not be stable + // if the iterator is stepped. To make it stable, make a copy using Clone. + ValueOrHandle []byte + // Fetcher provides support for fetching an actually lazy value. + Fetcher *LazyFetcher +} + +// LazyFetcher supports fetching a lazy value. +// +// Fetcher and Attribute are to be initialized at creation time. The fields +// are arranged to reduce the sizeof this struct. +type LazyFetcher struct { + // Fetcher, given a handle, returns the value. + Fetcher ValueFetcher + err error + value []byte + // Attribute includes the short attribute and value length. + Attribute AttributeAndLen + fetched bool + callerOwned bool +} + +// ValueFetcher is an interface for fetching a value. +type ValueFetcher interface { + // Fetch returns the value, given the handle. It is acceptable to call the + // ValueFetcher.Fetch as long as the DB is open. However, one should assume + // there is a fast-path when the iterator tree has not moved off the sstable + // iterator that initially provided this LazyValue. Hence, to utilize this + // fast-path the caller should try to decide whether it needs the value or + // not as soon as possible, with minimal possible stepping of the iterator. + // + // buf will be used if the fetcher cannot satisfy P2 (see earlier comment). + // If the fetcher attempted to use buf *and* len(buf) was insufficient, it + // will allocate a new slice for the value. In either case it will set + // callerOwned to true. + Fetch( + handle []byte, valLen int32, buf []byte) (val []byte, callerOwned bool, err error) +} + +// Value returns the underlying value. +func (lv *LazyValue) Value(buf []byte) (val []byte, callerOwned bool, err error) { + if lv.Fetcher == nil { + return lv.ValueOrHandle, false, nil + } + // Do the rest of the work in a separate method to attempt mid-stack + // inlining of Value(). Unfortunately, this still does not inline since the + // cost of 85 exceeds the budget of 80. + // + // TODO(sumeer): Packing the return values into a struct{[]byte error bool} + // causes it to be below the budget. Consider this if we need to recover + // more performance. I suspect that inlining this only matters in + // micro-benchmarks, and in actual use cases in CockroachDB it will not + // matter because there is substantial work done with a fetched value. + return lv.fetchValue(buf) +} + +// INVARIANT: lv.Fetcher != nil +func (lv *LazyValue) fetchValue(buf []byte) (val []byte, callerOwned bool, err error) { + f := lv.Fetcher + if !f.fetched { + f.fetched = true + f.value, f.callerOwned, f.err = f.Fetcher.Fetch( + lv.ValueOrHandle, lv.Fetcher.Attribute.ValueLen, buf) + } + return f.value, f.callerOwned, f.err +} + +// InPlaceValue returns the value under the assumption that it is in-place. +// This is for Pebble-internal code. +func (lv *LazyValue) InPlaceValue() []byte { + if invariants.Enabled && lv.Fetcher != nil { + panic("value must be in-place") + } + return lv.ValueOrHandle +} + +// Len returns the length of the value. +func (lv *LazyValue) Len() int { + if lv.Fetcher == nil { + return len(lv.ValueOrHandle) + } + return int(lv.Fetcher.Attribute.ValueLen) +} + +// TryGetShortAttribute returns the ShortAttribute and a bool indicating +// whether the ShortAttribute was populated. +func (lv *LazyValue) TryGetShortAttribute() (ShortAttribute, bool) { + if lv.Fetcher == nil { + return 0, false + } + return lv.Fetcher.Attribute.ShortAttribute, true +} + +// Clone creates a stable copy of the LazyValue, by appending bytes to buf. +// The fetcher parameter must be non-nil and may be over-written and used +// inside the returned LazyValue -- this is needed to avoid an allocation. +// Most callers have at most K cloned LazyValues, where K is hard-coded, so +// they can have a pool of exactly K LazyFetcher structs they can reuse in +// these calls. The alternative of allocating LazyFetchers from a sync.Pool is +// not viable since we have no code trigger for returning to the pool +// (LazyValues are simply GC'd). +// +// NB: It is highly preferable that LazyValue.Value() has not been called, +// since the Clone will forget any previously extracted value, and a future +// call to Value will cause it to be fetched again. We do this since we don't +// want to reason about whether or not to clone an already extracted value +// inside the Fetcher (we don't). Property P1 applies here too: if lv1.Value() +// has been called, and then lv2 is created as a clone of lv1, then calling +// lv2.Value() can invalidate any backing memory maintained inside the fetcher +// for lv1 (even though these are the same values). We initially prohibited +// calling LazyValue.Clone() if LazyValue.Value() has been called, but there +// is at least one complex caller (pebbleMVCCScanner inside CockroachDB) where +// it is not easy to prove this invariant. +func (lv *LazyValue) Clone(buf []byte, fetcher *LazyFetcher) (LazyValue, []byte) { + var lvCopy LazyValue + if lv.Fetcher != nil { + *fetcher = LazyFetcher{ + Fetcher: lv.Fetcher.Fetcher, + Attribute: lv.Fetcher.Attribute, + // Not copying anything that has been extracted. + } + lvCopy.Fetcher = fetcher + } + vLen := len(lv.ValueOrHandle) + if vLen == 0 { + return lvCopy, buf + } + bufLen := len(buf) + buf = append(buf, lv.ValueOrHandle...) + lvCopy.ValueOrHandle = buf[bufLen : bufLen+vLen] + return lvCopy, buf +} + +// MakeInPlaceValue constructs an in-place value. +func MakeInPlaceValue(val []byte) LazyValue { + return LazyValue{ValueOrHandle: val} +} diff --git a/pebble/internal/base/lazy_value_test.go b/pebble/internal/base/lazy_value_test.go new file mode 100644 index 0000000..82ad51c --- /dev/null +++ b/pebble/internal/base/lazy_value_test.go @@ -0,0 +1,74 @@ +// 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 base + +import ( + "bytes" + "testing" + "unsafe" + + "github.com/stretchr/testify/require" +) + +type valueFetcherFunc func( + handle []byte, valLen int32, buf []byte) (val []byte, callerOwned bool, err error) + +func (v valueFetcherFunc) Fetch( + handle []byte, valLen int32, buf []byte, +) (val []byte, callerOwned bool, err error) { + return v(handle, valLen, buf) +} + +func TestLazyValue(t *testing.T) { + // Both 40 and 48 bytes makes iteration benchmarks like + // BenchmarkIteratorScan/keys=1000,r-amp=1,key-types=points-only 75% + // slower. + require.True(t, unsafe.Sizeof(LazyValue{}) <= 32) + + fooBytes1 := []byte("foo") + fooLV1 := MakeInPlaceValue(fooBytes1) + require.Equal(t, 3, fooLV1.Len()) + _, hasAttr := fooLV1.TryGetShortAttribute() + require.False(t, hasAttr) + fooLV2, fooBytes2 := fooLV1.Clone(nil, &LazyFetcher{}) + require.Equal(t, 3, fooLV2.Len()) + _, hasAttr = fooLV2.TryGetShortAttribute() + require.False(t, hasAttr) + require.Equal(t, fooLV1.InPlaceValue(), fooLV2.InPlaceValue()) + getValue := func(lv LazyValue, expectedCallerOwned bool) []byte { + v, callerOwned, err := lv.Value(nil) + require.NoError(t, err) + require.Equal(t, expectedCallerOwned, callerOwned) + return v + } + require.Equal(t, getValue(fooLV1, false), getValue(fooLV2, false)) + fooBytes2[0] = 'b' + require.False(t, bytes.Equal(fooLV1.InPlaceValue(), fooLV2.InPlaceValue())) + + for _, callerOwned := range []bool{false, true} { + numCalls := 0 + fooLV3 := LazyValue{ + ValueOrHandle: []byte("foo-handle"), + Fetcher: &LazyFetcher{ + Fetcher: valueFetcherFunc( + func(handle []byte, valLen int32, buf []byte) ([]byte, bool, error) { + numCalls++ + require.Equal(t, []byte("foo-handle"), handle) + require.Equal(t, int32(3), valLen) + return fooBytes1, callerOwned, nil + }), + Attribute: AttributeAndLen{ValueLen: 3, ShortAttribute: 7}, + }, + } + require.Equal(t, []byte("foo"), getValue(fooLV3, callerOwned)) + require.Equal(t, 1, numCalls) + require.Equal(t, []byte("foo"), getValue(fooLV3, callerOwned)) + require.Equal(t, 1, numCalls) + require.Equal(t, 3, fooLV3.Len()) + attr, hasAttr := fooLV3.TryGetShortAttribute() + require.True(t, hasAttr) + require.Equal(t, ShortAttribute(7), attr) + } +} diff --git a/pebble/internal/base/logger.go b/pebble/internal/base/logger.go new file mode 100644 index 0000000..5448137 --- /dev/null +++ b/pebble/internal/base/logger.go @@ -0,0 +1,158 @@ +// 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 base + +import ( + "bytes" + "context" + "fmt" + "log" + "os" + "runtime" + "sync" + + "github.com/cockroachdb/pebble/internal/invariants" +) + +// Logger defines an interface for writing log messages. +type Logger interface { + Infof(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) +} +type defaultLogger struct{} + +// DefaultLogger logs to the Go stdlib logs. +var DefaultLogger defaultLogger + +var _ Logger = DefaultLogger + +// Infof implements the Logger.Infof interface. +func (defaultLogger) Infof(format string, args ...interface{}) { + _ = log.Output(2, fmt.Sprintf(format, args...)) +} + +// Errorf implements the Logger.Errorf interface. +func (defaultLogger) Errorf(format string, args ...interface{}) { + _ = log.Output(2, fmt.Sprintf(format, args...)) +} + +// Fatalf implements the Logger.Fatalf interface. +func (defaultLogger) Fatalf(format string, args ...interface{}) { + _ = log.Output(2, fmt.Sprintf(format, args...)) + os.Exit(1) +} + +// InMemLogger implements Logger using an in-memory buffer (used for testing). +// The buffer can be read via String() and cleared via Reset(). +type InMemLogger struct { + mu struct { + sync.Mutex + buf bytes.Buffer + } +} + +var _ Logger = (*InMemLogger)(nil) + +// Reset clears the internal buffer. +func (b *InMemLogger) Reset() { + b.mu.Lock() + defer b.mu.Unlock() + b.mu.buf.Reset() +} + +// String returns the current internal buffer. +func (b *InMemLogger) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.mu.buf.String() +} + +// Infof is part of the Logger interface. +func (b *InMemLogger) Infof(format string, args ...interface{}) { + s := fmt.Sprintf(format, args...) + b.mu.Lock() + defer b.mu.Unlock() + b.mu.buf.Write([]byte(s)) + if n := len(s); n == 0 || s[n-1] != '\n' { + b.mu.buf.Write([]byte("\n")) + } +} + +// Errorf is part of the Logger interface. +func (b *InMemLogger) Errorf(format string, args ...interface{}) { + b.Infof(format, args...) +} + +// Fatalf is part of the Logger interface. +func (b *InMemLogger) Fatalf(format string, args ...interface{}) { + b.Infof(format, args...) + runtime.Goexit() +} + +// LoggerAndTracer defines an interface for logging and tracing. +type LoggerAndTracer interface { + Logger + // Eventf formats and emits a tracing log, if tracing is enabled in the + // current context. + Eventf(ctx context.Context, format string, args ...interface{}) + // IsTracingEnabled returns true if tracing is enabled. It can be used as an + // optimization to avoid calling Eventf (which will be a noop when tracing + // is not enabled) to avoid the overhead of boxing the args. + IsTracingEnabled(ctx context.Context) bool +} + +// LoggerWithNoopTracer wraps a logger and does no tracing. +type LoggerWithNoopTracer struct { + Logger +} + +var _ LoggerAndTracer = &LoggerWithNoopTracer{} + +// Eventf implements LoggerAndTracer. +func (*LoggerWithNoopTracer) Eventf(ctx context.Context, format string, args ...interface{}) { + if invariants.Enabled && ctx == nil { + panic("Eventf context is nil") + } +} + +// IsTracingEnabled implements LoggerAndTracer. +func (*LoggerWithNoopTracer) IsTracingEnabled(ctx context.Context) bool { + if invariants.Enabled && ctx == nil { + panic("IsTracingEnabled ctx is nil") + } + return false +} + +// NoopLoggerAndTracer does no logging and tracing. Remember that struct{} is +// special cased in Go and does not incur an allocation when it backs the +// interface LoggerAndTracer. +type NoopLoggerAndTracer struct{} + +var _ LoggerAndTracer = NoopLoggerAndTracer{} + +// Infof implements LoggerAndTracer. +func (l NoopLoggerAndTracer) Infof(format string, args ...interface{}) {} + +// Errorf implements LoggerAndTracer. +func (l NoopLoggerAndTracer) Errorf(format string, args ...interface{}) {} + +// Fatalf implements LoggerAndTracer. +func (l NoopLoggerAndTracer) Fatalf(format string, args ...interface{}) {} + +// Eventf implements LoggerAndTracer. +func (l NoopLoggerAndTracer) Eventf(ctx context.Context, format string, args ...interface{}) { + if invariants.Enabled && ctx == nil { + panic("Eventf context is nil") + } +} + +// IsTracingEnabled implements LoggerAndTracer. +func (l NoopLoggerAndTracer) IsTracingEnabled(ctx context.Context) bool { + if invariants.Enabled && ctx == nil { + panic("IsTracingEnabled ctx is nil") + } + return false +} diff --git a/pebble/internal/base/merger.go b/pebble/internal/base/merger.go new file mode 100644 index 0000000..757d150 --- /dev/null +++ b/pebble/internal/base/merger.go @@ -0,0 +1,133 @@ +// Copyright 2018 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 base + +import "io" + +// Merge creates a ValueMerger for the specified key initialized with the value +// of one merge operand. +type Merge func(key, value []byte) (ValueMerger, error) + +// ValueMerger receives merge operands one by one. The operand received is either +// newer or older than all operands received so far as indicated by the function +// names, `MergeNewer()` and `MergeOlder()`. Once all operands have been received, +// the client will invoke `Finish()` to obtain the final result. The order of +// a merge is not changed after the first call to `MergeNewer()` or +// `MergeOlder()`, i.e. the same method is used to submit all operands. +// +// The implementation may choose to merge values into the result immediately upon +// receiving each operand, or buffer operands until Finish() is called. For example, +// buffering may be useful to avoid (de)serializing partial merge results. +// +// The merge operation must be associative. That is, for the values A, B, C: +// +// Merge(A).MergeOlder(B).MergeOlder(C) == Merge(C).MergeNewer(B).MergeNewer(A) +// +// Examples of merge operators are integer addition, list append, and string +// concatenation. +type ValueMerger interface { + // MergeNewer adds an operand that is newer than all existing operands. + // The caller retains ownership of value. + // + // If an error is returned the merge is aborted and no other methods must + // be called. + MergeNewer(value []byte) error + + // MergeOlder adds an operand that is older than all existing operands. + // The caller retains ownership of value. + // + // If an error is returned the merge is aborted and no other methods must + // be called. + MergeOlder(value []byte) error + + // Finish does any final processing of the added operands and returns a + // result. The caller can assume the returned byte slice will not be mutated. + // + // Finish must be the last function called on the ValueMerger. The caller + // must not call any other ValueMerger functions after calling Finish. + // + // If `includesBase` is true, the oldest merge operand was part of the + // merge. This will always be the true during normal iteration, but may be + // false during compaction when only a subset of operands may be + // available. Note that `includesBase` is set to true conservatively: a false + // value means that we could not definitely determine that the base merge + // operand was included. + // + // If a Closer is returned, the returned slice will remain valid until it is + // closed. The caller must arrange for the closer to be eventually closed. + Finish(includesBase bool) ([]byte, io.Closer, error) +} + +// DeletableValueMerger is an extension to ValueMerger which allows indicating that the +// result of a merge operation is non-existent. Such non-existent entries will eventually +// be deleted during compaction. Note that during compaction, non-existence of the result +// of a merge means that the merge operands will not result in any record being output. +// This is not the same as transforming the merge operands into a deletion tombstone, as +// older merge operands will still be visible during iteration. Deletion of the merge operands +// in this way is akin to the way a SingleDelete+Set combine into non-existence while leaving +// older records for the same key unaffected. +type DeletableValueMerger interface { + ValueMerger + + // DeletableFinish enables a value merger to indicate that the result of a merge operation + // is non-existent. See Finish for a description of includesBase. + DeletableFinish(includesBase bool) (value []byte, delete bool, closer io.Closer, err error) +} + +// Merger defines an associative merge operation. The merge operation merges +// two or more values for a single key. A merge operation is requested by +// writing a value using {Batch,DB}.Merge(). The value at that key is merged +// with any existing value. It is valid to Set a value at a key and then Merge +// a new value. Similar to non-merged values, a merged value can be deleted by +// either Delete or DeleteRange. +// +// The merge operation is invoked when a merge value is encountered during a +// read, either during a compaction or during iteration. +type Merger struct { + Merge Merge + + // Name is the name of the merger. + // + // Pebble stores the merger name on disk, and opening a database with a + // different merger from the one it was created with will result in an error. + Name string +} + +// AppendValueMerger concatenates merge operands in order from oldest to newest. +type AppendValueMerger struct { + buf []byte +} + +// MergeNewer appends value to the result. +func (a *AppendValueMerger) MergeNewer(value []byte) error { + a.buf = append(a.buf, value...) + return nil +} + +// MergeOlder prepends value to the result, which involves allocating a new buffer. +func (a *AppendValueMerger) MergeOlder(value []byte) error { + buf := make([]byte, len(a.buf)+len(value)) + copy(buf, value) + copy(buf[len(value):], a.buf) + a.buf = buf + return nil +} + +// Finish returns the buffer that was constructed on-demand in `Merge{OlderNewer}()` calls. +func (a *AppendValueMerger) Finish(includesBase bool) ([]byte, io.Closer, error) { + return a.buf, nil, nil +} + +// DefaultMerger is the default implementation of the Merger interface. It +// concatenates the two values to merge. +var DefaultMerger = &Merger{ + Merge: func(key, value []byte) (ValueMerger, error) { + res := &AppendValueMerger{} + res.buf = append(res.buf, value...) + return res, nil + }, + + Name: "pebble.concatenate", +} diff --git a/pebble/internal/base/metrics.go b/pebble/internal/base/metrics.go new file mode 100644 index 0000000..520edc3 --- /dev/null +++ b/pebble/internal/base/metrics.go @@ -0,0 +1,98 @@ +// 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 base + +import "time" + +// ThroughputMetric is used to measure the byte throughput of some component +// that performs work in a single-threaded manner. The throughput can be +// approximated by Bytes/(WorkDuration+IdleTime). The idle time is represented +// separately, so that the user of this metric could approximate the peak +// throughput as Bytes/WorkTime. The metric is designed to be cumulative (see +// Merge). +type ThroughputMetric struct { + // Bytes is the processes bytes by the component. + Bytes int64 + // WorkDuration is the duration that the component spent doing work. + WorkDuration time.Duration + // IdleDuration is the duration that the component was idling, waiting for + // work. + IdleDuration time.Duration +} + +// Merge accumulates the information from another throughput metric. +func (tm *ThroughputMetric) Merge(x ThroughputMetric) { + tm.Bytes += x.Bytes + tm.WorkDuration += x.WorkDuration + tm.IdleDuration += x.IdleDuration +} + +// Subtract subtracts the information from another ThroughputMetric +func (tm *ThroughputMetric) Subtract(x ThroughputMetric) { + tm.Bytes -= x.Bytes + tm.WorkDuration -= x.WorkDuration + tm.IdleDuration -= x.IdleDuration +} + +// PeakRate returns the approximate peak rate if there was no idling. +func (tm *ThroughputMetric) PeakRate() int64 { + if tm.Bytes == 0 { + return 0 + } + return int64((float64(tm.Bytes) / float64(tm.WorkDuration)) * float64(time.Second)) +} + +// Rate returns the observed rate. +func (tm *ThroughputMetric) Rate() int64 { + if tm.Bytes == 0 { + return 0 + } + return int64((float64(tm.Bytes) / float64(tm.WorkDuration+tm.IdleDuration)) * + 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 { + // The sum of all the samples. + sampleSum int64 + // The number of samples. + count int64 +} + +// AddSample adds the given sample. +func (gsm *GaugeSampleMetric) AddSample(sample int64) { + gsm.sampleSum += sample + gsm.count++ +} + +// Merge accumulates the information from another gauge metric. +func (gsm *GaugeSampleMetric) Merge(x GaugeSampleMetric) { + gsm.sampleSum += x.sampleSum + gsm.count += x.count +} + +// Subtract subtracts the information from another gauge metric. +func (gsm *GaugeSampleMetric) Subtract(x GaugeSampleMetric) { + gsm.sampleSum -= x.sampleSum + gsm.count -= x.count +} + +// Mean returns the mean value. +func (gsm *GaugeSampleMetric) Mean() float64 { + if gsm.count == 0 { + return 0 + } + return float64(gsm.sampleSum) / float64(gsm.count) +} diff --git a/pebble/internal/base/metrics_test.go b/pebble/internal/base/metrics_test.go new file mode 100644 index 0000000..90e3166 --- /dev/null +++ b/pebble/internal/base/metrics_test.go @@ -0,0 +1,79 @@ +// 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 base + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestThroughputMetric(t *testing.T) { + m1 := ThroughputMetric{ + Bytes: 10, + WorkDuration: time.Millisecond, + IdleDuration: 9 * time.Millisecond, + } + var m2 ThroughputMetric + m2.Merge(m1) + require.Equal(t, m1, m2) + m2.Merge(m1) + doubleM1 := ThroughputMetric{ + Bytes: 2 * m1.Bytes, + WorkDuration: 2 * m1.WorkDuration, + IdleDuration: 2 * m1.IdleDuration, + } + require.Equal(t, doubleM1, m2) + require.EqualValues(t, 10*100, m1.Rate()) + require.EqualValues(t, 10*1000, m1.PeakRate()) +} + +func TestThroughputMetric_Subtract(t *testing.T) { + m1 := ThroughputMetric{ + Bytes: 10, + WorkDuration: time.Millisecond, + IdleDuration: 9 * time.Millisecond, + } + m2 := ThroughputMetric{ + Bytes: 100, + WorkDuration: time.Millisecond, + IdleDuration: 90 * time.Millisecond, + } + + m2.Subtract(m1) + require.Equal(t, int64(90), m2.Bytes) + require.Equal(t, 0*time.Millisecond, m2.WorkDuration) + require.Equal(t, 81*time.Millisecond, m2.IdleDuration) +} + +func TestGaugeSampleMetric(t *testing.T) { + g1 := GaugeSampleMetric{} + g1.AddSample(10) + g1.AddSample(20) + g2 := GaugeSampleMetric{} + g2.Merge(g1) + g2.AddSample(60) + require.EqualValues(t, 30, g2.Mean()) + require.EqualValues(t, 3, g2.count) + require.EqualValues(t, 15, g1.Mean()) + require.EqualValues(t, 2, g1.count) +} + +func TestGaugeSampleMetricSubtract(t *testing.T) { + g1 := GaugeSampleMetric{} + g2 := GaugeSampleMetric{} + g1.AddSample(10) + g1.AddSample(20) + g1.AddSample(0) + + g2.AddSample(10) + + g1.Subtract(g2) + + require.Equal(t, int64(20), g1.sampleSum) + require.Equal(t, int64(2), g1.count) + +} diff --git a/pebble/internal/base/options.go b/pebble/internal/base/options.go new file mode 100644 index 0000000..316717e --- /dev/null +++ b/pebble/internal/base/options.go @@ -0,0 +1,76 @@ +// 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 base + +// SSTable block defaults. +const ( + DefaultBlockRestartInterval = 16 + DefaultBlockSize = 4096 + DefaultBlockSizeThreshold = 90 +) + +// FilterType is the level at which to apply a filter: block or table. +type FilterType int + +// The available filter types. +const ( + TableFilter FilterType = iota +) + +func (t FilterType) String() string { + switch t { + case TableFilter: + return "table" + } + return "unknown" +} + +// FilterWriter provides an interface for creating filter blocks. See +// FilterPolicy for more details about filters. +type FilterWriter interface { + // AddKey adds a key to the current filter block. + AddKey(key []byte) + + // Finish appends to dst an encoded filter tha holds the current set of + // keys. The writer state is reset after the call to Finish allowing the + // writer to be reused for the creation of additional filters. + Finish(dst []byte) []byte +} + +// FilterPolicy is an algorithm for probabilistically encoding a set of keys. +// The canonical implementation is a Bloom filter. +// +// Every FilterPolicy has a name. This names the algorithm itself, not any one +// particular instance. Aspects specific to a particular instance, such as the +// set of keys or any other parameters, will be encoded in the []byte filter +// returned by NewWriter. +// +// The name may be written to files on disk, along with the filter data. To use +// these filters, the FilterPolicy name at the time of writing must equal the +// name at the time of reading. If they do not match, the filters will be +// ignored, which will not affect correctness but may affect performance. +type FilterPolicy interface { + // Name names the filter policy. + Name() string + + // MayContain returns whether the encoded filter may contain given key. + // False positives are possible, where it returns true for keys not in the + // original set. + MayContain(ftype FilterType, filter, key []byte) bool + + // NewWriter creates a new FilterWriter. + NewWriter(ftype FilterType) FilterWriter +} + +// BlockPropertyFilter is used in an Iterator to filter sstables and blocks +// within the sstable. It should not maintain any per-sstable state, and must +// be thread-safe. +type BlockPropertyFilter interface { + // Name returns the name of the block property collector. + Name() string + // Intersects returns true if the set represented by prop intersects with + // the set in the filter. + Intersects(prop []byte) (bool, error) +} diff --git a/pebble/internal/batchskl/README.md b/pebble/internal/batchskl/README.md new file mode 100644 index 0000000..1e0aa2d --- /dev/null +++ b/pebble/internal/batchskl/README.md @@ -0,0 +1,56 @@ +# batchskl + +Fast, non-concurrent skiplist implementation in Go that supports +forward and backward iteration. + +## Limitations + +* The interface is tailored for use in indexing pebble batches. Keys + and values are stored outside of the skiplist making the skiplist + awkward for general purpose use. +* Deletion is not supported. Instead, higher-level code is expected to + add deletion tombstones and needs to process those tombstones + appropriately. + +## Pedigree + +This code is based on Andy Kimball's arenaskl code. + +The arenaskl code is based on the skiplist found in Badger, a Go-based +KV store: + +https://github.com/dgraph-io/badger/tree/master/skl + +The skiplist in Badger is itself based on a C++ skiplist built for +Facebook's RocksDB: + +https://github.com/facebook/rocksdb/tree/master/memtable + +## Benchmarks + +The benchmarks consist of a mix of reads and writes executed in parallel. The +fraction of reads is indicated in the run name: "frac_X" indicates a run where +X percent of the operations are reads. + +``` +name time/op +ReadWrite/frac_0 1.03µs ± 2% +ReadWrite/frac_10 1.32µs ± 1% +ReadWrite/frac_20 1.26µs ± 1% +ReadWrite/frac_30 1.18µs ± 1% +ReadWrite/frac_40 1.09µs ± 1% +ReadWrite/frac_50 987ns ± 2% +ReadWrite/frac_60 1.07µs ± 1% +ReadWrite/frac_70 909ns ± 1% +ReadWrite/frac_80 693ns ± 2% +ReadWrite/frac_90 599ns ± 2% +ReadWrite/frac_100 45.3ns ± 3% +``` + +Forward and backward iteration are also fast: + +``` +name time/op +IterNext 4.49ns ± 3% +IterPrev 4.48ns ± 3% +``` diff --git a/pebble/internal/batchskl/iterator.go b/pebble/internal/batchskl/iterator.go new file mode 100644 index 0000000..5917ed1 --- /dev/null +++ b/pebble/internal/batchskl/iterator.go @@ -0,0 +1,223 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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 batchskl + +import "github.com/cockroachdb/pebble/internal/base" + +type splice struct { + prev uint32 + next uint32 +} + +// Iterator is an iterator over the skiplist object. Use Skiplist.NewIter +// to construct an iterator. The current state of the iterator can be cloned +// by simply value copying the struct. +type Iterator struct { + list *Skiplist + nd uint32 + key base.InternalKey + lower []byte + upper []byte +} + +// Close resets the iterator. +func (it *Iterator) Close() error { + it.list = nil + it.nd = 0 + return nil +} + +// SeekGE moves the iterator to the first entry whose key is greater than or +// equal to the given key. Returns true if the iterator is pointing at a valid +// entry and false otherwise. 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 (it *Iterator) SeekGE(key []byte, flags base.SeekGEFlags) *base.InternalKey { + if flags.TrySeekUsingNext() { + if it.nd == it.list.tail { + // Iterator is done. + return nil + } + less := it.list.cmp(it.key.UserKey, key) < 0 + // Arbitrary constant. By measuring the seek cost as a function of the + // number of elements in the skip list, and fitting to a model, we + // could adjust the number of nexts based on the current size of the + // skip list. + const numNexts = 5 + for i := 0; less && i < numNexts; i++ { + k := it.Next() + if k == nil { + // Iterator is done. + return nil + } + less = it.list.cmp(k.UserKey, key) < 0 + } + if !less { + return &it.key + } + } + + _, it.nd = it.seekForBaseSplice(key, it.list.abbreviatedKey(key)) + if it.nd == it.list.tail { + return nil + } + nodeKey := it.list.getKey(it.nd) + if it.upper != nil && it.list.cmp(it.upper, nodeKey.UserKey) <= 0 { + it.nd = it.list.tail + return nil + } + it.key = nodeKey + return &it.key +} + +// SeekLT moves the iterator to the last entry whose key is less the given +// key. Returns true if the iterator is pointing at a valid entry and false +// otherwise. 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 (it *Iterator) SeekLT(key []byte) *base.InternalKey { + it.nd, _ = it.seekForBaseSplice(key, it.list.abbreviatedKey(key)) + if it.nd == it.list.head { + return nil + } + nodeKey := it.list.getKey(it.nd) + if it.lower != nil && it.list.cmp(it.lower, nodeKey.UserKey) > 0 { + it.nd = it.list.head + return nil + } + it.key = nodeKey + return &it.key +} + +// First seeks position at the first entry in list. Final state of iterator is +// Valid() iff list is not empty. 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 (it *Iterator) First() *base.InternalKey { + it.nd = it.list.getNext(it.list.head, 0) + if it.nd == it.list.tail { + return nil + } + nodeKey := it.list.getKey(it.nd) + if it.upper != nil && it.list.cmp(it.upper, nodeKey.UserKey) <= 0 { + it.nd = it.list.tail + return nil + } + it.key = nodeKey + return &it.key +} + +// Last seeks position at the last entry in list. Final state of iterator is +// Valid() iff list is not empty. 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 (it *Iterator) Last() *base.InternalKey { + it.nd = it.list.getPrev(it.list.tail, 0) + if it.nd == it.list.head { + return nil + } + nodeKey := it.list.getKey(it.nd) + if it.lower != nil && it.list.cmp(it.lower, nodeKey.UserKey) > 0 { + it.nd = it.list.head + return nil + } + it.key = nodeKey + return &it.key +} + +// Next advances to the next position. If there are no following nodes, then +// Valid() will be false after this call. +func (it *Iterator) Next() *base.InternalKey { + it.nd = it.list.getNext(it.nd, 0) + if it.nd == it.list.tail { + return nil + } + nodeKey := it.list.getKey(it.nd) + if it.upper != nil && it.list.cmp(it.upper, nodeKey.UserKey) <= 0 { + it.nd = it.list.tail + return nil + } + it.key = nodeKey + return &it.key +} + +// Prev moves to the previous position. If there are no previous nodes, then +// Valid() will be false after this call. +func (it *Iterator) Prev() *base.InternalKey { + it.nd = it.list.getPrev(it.nd, 0) + if it.nd == it.list.head { + return nil + } + nodeKey := it.list.getKey(it.nd) + if it.lower != nil && it.list.cmp(it.lower, nodeKey.UserKey) > 0 { + it.nd = it.list.head + return nil + } + it.key = nodeKey + return &it.key +} + +// Key returns the key at the current position. +func (it *Iterator) Key() *base.InternalKey { + return &it.key +} + +// KeyInfo returns the offset of the start of the record, the start of the key, +// and the end of the key. +func (it *Iterator) KeyInfo() (offset, keyStart, keyEnd uint32) { + n := it.list.node(it.nd) + return n.offset, n.keyStart, n.keyEnd +} + +// Head true iff the iterator is positioned at the sentinel head node. +func (it *Iterator) Head() bool { + return it.nd == it.list.head +} + +// Tail true iff the iterator is positioned at the sentinel tail node. +func (it *Iterator) Tail() bool { + return it.nd == it.list.tail +} + +// Valid returns nil iff the iterator is positioned at a valid node. +func (it *Iterator) Valid() bool { + return it.list != nil && it.nd != it.list.head && it.nd != it.list.tail +} + +func (it *Iterator) String() string { + return "batch" +} + +// SetBounds sets the lower and upper bounds for the iterator. Note that the +// result of Next and Prev will be undefined until the iterator has been +// repositioned with SeekGE, SeekLT, First, or Last. +func (it *Iterator) SetBounds(lower, upper []byte) { + it.lower = lower + it.upper = upper +} + +func (it *Iterator) seekForBaseSplice(key []byte, abbreviatedKey uint64) (prev, next uint32) { + prev = it.list.head + for level := it.list.height - 1; ; level-- { + prev, next = it.list.findSpliceForLevel(key, abbreviatedKey, level, prev) + if level == 0 { + break + } + } + + return +} diff --git a/pebble/internal/batchskl/skl.go b/pebble/internal/batchskl/skl.go new file mode 100644 index 0000000..f56d95c --- /dev/null +++ b/pebble/internal/batchskl/skl.go @@ -0,0 +1,442 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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. + */ + +/* +Adapted from RocksDB inline skiplist. + +Key differences: +- No optimization for sequential inserts (no "prev"). +- No custom comparator. +- Support overwrites. This requires care when we see the same key when inserting. + For RocksDB or LevelDB, overwrites are implemented as a newer sequence number in the key, so + there is no need for values. We don't intend to support versioning. In-place updates of values + would be more efficient. +- We discard all non-concurrent code. +- We do not support Splices. This simplifies the code a lot. +- No AllocateNode or other pointer arithmetic. +- We combine the findLessThan, findGreaterOrEqual, etc into one function. +*/ + +/* +Further adapted from Badger: https://github.com/dgraph-io/badger. + +Key differences: +- Support for previous pointers - doubly linked lists. Note that it's up to higher + level code to deal with the intermediate state that occurs during insertion, + where node A is linked to node B, but node B is not yet linked back to node A. +- Iterator includes mutator functions. +*/ + +/* +Further adapted from arenaskl: https://github.com/andy-kimball/arenaskl + +Key differences: +- Removed support for deletion. +- Removed support for concurrency. +- External storage of keys. +- Node storage grows to an arbitrary size. +*/ + +package batchskl // import "github.com/cockroachdb/pebble/internal/batchskl" + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + "time" + "unsafe" + + "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 = uint64(unsafe.Sizeof(node{})) + linksSize = uint64(unsafe.Sizeof(links{})) + maxNodesSize = constants.MaxUint32OrInt +) + +var ( + // ErrExists indicates that a duplicate record was inserted. This should never + // happen for normal usage of batchskl as every key should have a unique + // sequence number. + ErrExists = errors.New("record with this key already exists") + + // ErrTooManyRecords is a sentinel error returned when the size of the raw + // nodes slice exceeds the maximum allowed size (currently 1 << 32 - 1). This + // corresponds to ~117 M skiplist entries. + ErrTooManyRecords = errors.New("too many records") +) + +type links struct { + next uint32 + prev uint32 +} + +type node struct { + // The offset of the start of the record in the storage. + offset uint32 + // The offset of the start and end of the key in storage. + keyStart uint32 + keyEnd uint32 + // A fixed 8-byte abbreviation of the key, used to avoid retrieval of the key + // during seek operations. The key retrieval can be expensive purely due to + // cache misses while the abbreviatedKey stored here will be in the same + // cache line as the key and the links making accessing and comparing against + // it almost free. + abbreviatedKey uint64 + // Most nodes do not need to use the full height of the link tower, since the + // probability of each successive level decreases exponentially. Because + // these elements are never accessed, they do not need to be allocated. + // Therefore, when a node is allocated, its memory footprint is deliberately + // truncated to not include unneeded link elements. + links [maxHeight]links +} + +// Skiplist is a fast, non-cocnurrent skiplist implementation that supports +// forward and backward iteration. See arenaskl.Skiplist for a concurrent +// skiplist. Keys and values are stored externally from the skiplist via the +// Storage interface. Deletion is not supported. Instead, higher-level code is +// expected to perform deletion via tombstones and needs to process those +// tombstones appropriately during retrieval operations. +type Skiplist struct { + storage *[]byte + cmp base.Compare + abbreviatedKey base.AbbreviatedKey + nodes []byte + head uint32 + tail uint32 + height uint32 // Current height: 1 <= height <= maxHeight + rand rand.PCGSource +} + +var ( + probabilities [maxHeight]uint32 +) + +func init() { + const pValue = 1 / math.E + + // Precompute the skiplist probabilities so that only a single random number + // needs to be generated and so that the optimal pvalue can be used (inverse + // of Euler's number). + p := float64(1.0) + for i := 0; i < maxHeight; i++ { + probabilities[i] = uint32(float64(math.MaxUint32) * p) + p *= pValue + } +} + +// NewSkiplist constructs and initializes a new, empty skiplist. +func NewSkiplist(storage *[]byte, cmp base.Compare, abbreviatedKey base.AbbreviatedKey) *Skiplist { + s := &Skiplist{} + s.Init(storage, cmp, abbreviatedKey) + return s +} + +// Reset the fields in the skiplist for reuse. +func (s *Skiplist) Reset() { + *s = Skiplist{ + nodes: s.nodes[:0], + height: 1, + } + const batchMaxRetainedSize = 1 << 20 // 1 MB + if cap(s.nodes) > batchMaxRetainedSize { + s.nodes = nil + } +} + +// Init the skiplist to empty and re-initialize. +func (s *Skiplist) Init(storage *[]byte, cmp base.Compare, abbreviatedKey base.AbbreviatedKey) { + *s = Skiplist{ + storage: storage, + cmp: cmp, + abbreviatedKey: abbreviatedKey, + nodes: s.nodes[:0], + height: 1, + } + s.rand.Seed(uint64(time.Now().UnixNano())) + + const initBufSize = 256 + if cap(s.nodes) < initBufSize { + s.nodes = make([]byte, 0, initBufSize) + } + + // Allocate head and tail nodes. While allocating a new node can fail, in the + // context of initializing the skiplist we consider it unrecoverable. + var err error + s.head, err = s.newNode(maxHeight, 0, 0, 0, 0) + if err != nil { + panic(err) + } + s.tail, err = s.newNode(maxHeight, 0, 0, 0, 0) + if err != nil { + panic(err) + } + + // Link all head/tail levels together. + headNode := s.node(s.head) + tailNode := s.node(s.tail) + for i := uint32(0); i < maxHeight; i++ { + headNode.links[i].next = s.tail + tailNode.links[i].prev = s.head + } +} + +// Add adds a new key to the skiplist if it does not yet exist. If the record +// already exists, then Add returns ErrRecordExists. +func (s *Skiplist) Add(keyOffset uint32) error { + data := (*s.storage)[keyOffset+1:] + v, n := binary.Uvarint(data) + if n <= 0 { + return errors.Errorf("corrupted batch entry: %d", errors.Safe(keyOffset)) + } + data = data[n:] + if v > uint64(len(data)) { + return errors.Errorf("corrupted batch entry: %d", errors.Safe(keyOffset)) + } + keyStart := 1 + keyOffset + uint32(n) + keyEnd := keyStart + uint32(v) + key := data[:v] + abbreviatedKey := s.abbreviatedKey(key) + + // spl holds the list of next and previous links for each level in the + // skiplist indicating where the new node will be inserted. + var spl [maxHeight]splice + + // Fast-path for in-order insertion of keys: compare the new key against the + // last key. + prev := s.getPrev(s.tail, 0) + if prevNode := s.node(prev); prev == s.head || + abbreviatedKey > prevNode.abbreviatedKey || + (abbreviatedKey == prevNode.abbreviatedKey && + s.cmp(key, (*s.storage)[prevNode.keyStart:prevNode.keyEnd]) > 0) { + for level := uint32(0); level < s.height; level++ { + spl[level].prev = s.getPrev(s.tail, level) + spl[level].next = s.tail + } + } else { + s.findSplice(key, abbreviatedKey, &spl) + } + + height := s.randomHeight() + // Increase s.height as necessary. + for ; s.height < height; s.height++ { + spl[s.height].next = s.tail + spl[s.height].prev = s.head + } + + // We always insert from the base level and up. After you add a node in base + // level, we cannot create a node in the level above because it would have + // discovered the node in the base level. + nd, err := s.newNode(height, keyOffset, keyStart, keyEnd, abbreviatedKey) + if err != nil { + return err + } + newNode := s.node(nd) + for level := uint32(0); level < height; level++ { + next := spl[level].next + prev := spl[level].prev + newNode.links[level].next = next + newNode.links[level].prev = prev + s.node(next).links[level].prev = nd + s.node(prev).links[level].next = nd + } + + return nil +} + +// NewIter returns a new Iterator object. The lower and upper bound parameters +// control the range of keys the iterator will return. Specifying for nil for +// lower or upper bound disables the check for that boundary. Note that lower +// bound is not checked on {SeekGE,First} and upper bound is not check on +// {SeekLT,Last}. The user is expected to perform that check. Note that it is +// safe for an iterator to be copied by value. +func (s *Skiplist) NewIter(lower, upper []byte) Iterator { + return Iterator{list: s, lower: lower, upper: upper} +} + +func (s *Skiplist) newNode( + height, + offset, keyStart, keyEnd uint32, abbreviatedKey uint64, +) (uint32, error) { + if height < 1 || height > maxHeight { + panic("height cannot be less than one or greater than the max height") + } + + unusedSize := uint64(maxHeight-int(height)) * linksSize + nodeOffset, err := s.alloc(uint32(maxNodeSize - unusedSize)) + if err != nil { + return 0, err + } + nd := s.node(nodeOffset) + + nd.offset = offset + nd.keyStart = keyStart + nd.keyEnd = keyEnd + nd.abbreviatedKey = abbreviatedKey + return nodeOffset, nil +} + +func (s *Skiplist) alloc(size uint32) (uint32, error) { + 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 uint64(cap(s.nodes)) < minAllocSize { + allocSize := uint64(cap(s.nodes)) * 2 + if allocSize < minAllocSize { + allocSize = minAllocSize + } + // Cap the allocation at the max allowed size to avoid wasted capacity. + if allocSize > maxNodesSize { + // The new record may still not fit within the allocation, in which case + // we return early with an error. This avoids the panic below when we + // resize the slice. It also avoids the allocation and copy. + if uint64(offset)+uint64(size) > maxNodesSize { + return 0, errors.Wrapf(ErrTooManyRecords, + "alloc of new record (size=%d) would overflow uint32 (current size=%d)", + uint64(offset)+uint64(size), offset, + ) + } + allocSize = maxNodesSize + } + tmp := make([]byte, len(s.nodes), allocSize) + copy(tmp, s.nodes) + s.nodes = tmp + } + + newSize := uint32(offset) + size + s.nodes = s.nodes[:newSize] + return uint32(offset), nil +} + +func (s *Skiplist) node(offset uint32) *node { + return (*node)(unsafe.Pointer(&s.nodes[offset])) +} + +func (s *Skiplist) randomHeight() uint32 { + rnd := uint32(s.rand.Uint64()) + h := uint32(1) + for h < maxHeight && rnd <= probabilities[h] { + h++ + } + return h +} + +func (s *Skiplist) findSplice(key []byte, abbreviatedKey uint64, spl *[maxHeight]splice) { + prev := s.head + + for level := s.height - 1; ; level-- { + // The code in this loop is the same as findSpliceForLevel(). For some + // reason, calling findSpliceForLevel() here is much much slower than the + // inlined code below. The excess time is also caught up in the final + // return statement which makes little sense. Revisit when in go1.14 or + // later if inlining improves. + + next := s.getNext(prev, level) + for next != s.tail { + // Assume prev.key < key. + nextNode := s.node(next) + nextAbbreviatedKey := nextNode.abbreviatedKey + if abbreviatedKey < nextAbbreviatedKey { + // We are done for this level, since prev.key < key < next.key. + break + } + if abbreviatedKey == nextAbbreviatedKey { + if s.cmp(key, (*s.storage)[nextNode.keyStart:nextNode.keyEnd]) <= 0 { + // We are done for this level, since prev.key < key <= next.key. + break + } + } + + // Keep moving right on this level. + prev = next + next = nextNode.links[level].next + } + + spl[level].prev = prev + spl[level].next = next + if level == 0 { + break + } + } +} + +func (s *Skiplist) findSpliceForLevel( + key []byte, abbreviatedKey uint64, level, start uint32, +) (prev, next uint32) { + prev = start + next = s.getNext(prev, level) + + for next != s.tail { + // Assume prev.key < key. + nextNode := s.node(next) + nextAbbreviatedKey := nextNode.abbreviatedKey + if abbreviatedKey < nextAbbreviatedKey { + // We are done for this level, since prev.key < key < next.key. + break + } + if abbreviatedKey == nextAbbreviatedKey { + if s.cmp(key, (*s.storage)[nextNode.keyStart:nextNode.keyEnd]) <= 0 { + // We are done for this level, since prev.key < key < next.key. + break + } + } + + // Keep moving right on this level. + prev = next + next = nextNode.links[level].next + } + + return +} + +func (s *Skiplist) getKey(nd uint32) base.InternalKey { + n := s.node(nd) + kind := base.InternalKeyKind((*s.storage)[n.offset]) + key := (*s.storage)[n.keyStart:n.keyEnd] + return base.MakeInternalKey(key, uint64(n.offset)|base.InternalKeySeqNumBatch, kind) +} + +func (s *Skiplist) getNext(nd, h uint32) uint32 { + return s.node(nd).links[h].next +} + +func (s *Skiplist) getPrev(nd, h uint32) uint32 { + return s.node(nd).links[h].prev +} + +func (s *Skiplist) debug() string { + var buf bytes.Buffer + for level := uint32(0); level < s.height; level++ { + var count int + for nd := s.head; nd != s.tail; nd = s.getNext(nd, level) { + count++ + } + fmt.Fprintf(&buf, "%d: %d\n", level, count) + } + return buf.String() +} + +// Silence unused warning. +var _ = (*Skiplist).debug diff --git a/pebble/internal/batchskl/skl_test.go b/pebble/internal/batchskl/skl_test.go new file mode 100644 index 0000000..4f67a8b --- /dev/null +++ b/pebble/internal/batchskl/skl_test.go @@ -0,0 +1,539 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * Modifications copyright (C) 2017 Andy Kimball and Contributors + * + * 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 batchskl + +import ( + "bytes" + "encoding/binary" + "fmt" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +// iterAdapter adapts the new Iterator API which returns the key and value from +// positioning methods (Seek*, First, Last, Next, Prev) to the old API which +// returned a boolean corresponding to Valid. Only used by test code. +type iterAdapter struct { + Iterator +} + +func (i *iterAdapter) verify(key *base.InternalKey) bool { + valid := key != nil + if valid != i.Valid() { + panic(fmt.Sprintf("inconsistent valid: %t != %t", valid, i.Valid())) + } + if valid { + if base.InternalCompare(bytes.Compare, *key, i.Key()) != 0 { + panic(fmt.Sprintf("inconsistent key: %s != %s", *key, i.Key())) + } + } + return valid +} + +func (i *iterAdapter) SeekGE(key []byte) bool { + return i.verify(i.Iterator.SeekGE(key, base.SeekGEFlagsNone)) +} + +func (i *iterAdapter) SeekLT(key []byte) bool { + return i.verify(i.Iterator.SeekLT(key)) +} + +func (i *iterAdapter) First() bool { + return i.verify(i.Iterator.First()) +} + +func (i *iterAdapter) Last() bool { + return i.verify(i.Iterator.Last()) +} + +func (i *iterAdapter) Next() bool { + return i.verify(i.Iterator.Next()) +} + +func (i *iterAdapter) Prev() bool { + return i.verify(i.Iterator.Prev()) +} + +func (i *iterAdapter) Key() base.InternalKey { + return *i.Iterator.Key() +} + +// length iterates over skiplist to give exact size. +func length(s *Skiplist) int { + count := 0 + + it := iterAdapter{s.NewIter(nil, nil)} + for valid := it.First(); valid; valid = it.Next() { + count++ + } + + return count +} + +// length iterates over skiplist in reverse order to give exact size. +func lengthRev(s *Skiplist) int { + count := 0 + + it := iterAdapter{s.NewIter(nil, nil)} + for valid := it.Last(); valid; valid = it.Prev() { + count++ + } + + return count +} + +func makeKey(s string) []byte { + return []byte(s) +} + +type testStorage struct { + data []byte +} + +func (d *testStorage) add(key string) uint32 { + offset := uint32(len(d.data)) + d.data = append(d.data, uint8(base.InternalKeyKindSet)) + var buf [binary.MaxVarintLen64]byte + n := binary.PutUvarint(buf[:], uint64(len(key))) + d.data = append(d.data, buf[:n]...) + d.data = append(d.data, key...) + return offset +} + +func (d *testStorage) addBytes(key []byte) uint32 { + offset := uint32(len(d.data)) + d.data = append(d.data, uint8(base.InternalKeyKindSet)) + var buf [binary.MaxVarintLen64]byte + n := binary.PutUvarint(buf[:], uint64(len(key))) + d.data = append(d.data, buf[:n]...) + d.data = append(d.data, key...) + return offset +} + +func newTestSkiplist(storage *testStorage) *Skiplist { + return NewSkiplist(&storage.data, base.DefaultComparer.Compare, + base.DefaultComparer.AbbreviatedKey) +} + +func TestEmpty(t *testing.T) { + key := makeKey("aaa") + l := newTestSkiplist(&testStorage{}) + it := iterAdapter{l.NewIter(nil, nil)} + + require.False(t, it.Valid()) + + it.First() + require.False(t, it.Valid()) + + it.Last() + require.False(t, it.Valid()) + + require.False(t, it.SeekGE(key)) + require.False(t, it.Valid()) +} + +// TestBasic tests seeks and adds. +func TestBasic(t *testing.T) { + d := &testStorage{} + l := newTestSkiplist(d) + it := iterAdapter{l.NewIter(nil, nil)} + + // Try adding values. + require.Nil(t, l.Add(d.add("key1"))) + require.Nil(t, l.Add(d.add("key2"))) + require.Nil(t, l.Add(d.add("key3"))) + + require.True(t, it.SeekGE(makeKey("key"))) + require.EqualValues(t, "key1", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey("key1"))) + require.EqualValues(t, "key1", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey("key2"))) + require.EqualValues(t, "key2", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey("key3"))) + require.EqualValues(t, "key3", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey("key2"))) + require.True(t, it.SeekGE(makeKey("key3"))) +} + +func TestSkiplistAdd(t *testing.T) { + d := &testStorage{} + l := newTestSkiplist(d) + it := iterAdapter{l.NewIter(nil, nil)} + + // Add empty key. + require.Nil(t, l.Add(d.add(""))) + require.EqualValues(t, []byte(nil), it.Key().UserKey) + require.True(t, it.First()) + require.EqualValues(t, []byte{}, it.Key().UserKey) + + // Add to empty list. + require.Nil(t, l.Add(d.add("00002"))) + require.True(t, it.SeekGE(makeKey("00002"))) + require.EqualValues(t, "00002", it.Key().UserKey) + + // Add first element in non-empty list. + require.Nil(t, l.Add(d.add("00001"))) + require.True(t, it.SeekGE(makeKey("00001"))) + require.EqualValues(t, "00001", it.Key().UserKey) + + // Add last element in non-empty list. + require.Nil(t, l.Add(d.add("00004"))) + require.True(t, it.SeekGE(makeKey("00004"))) + require.EqualValues(t, "00004", it.Key().UserKey) + + // Add element in middle of list. + require.Nil(t, l.Add(d.add("00003"))) + require.True(t, it.SeekGE(makeKey("00003"))) + require.EqualValues(t, "00003", it.Key().UserKey) + + // Try to add element that already exists. + require.Nil(t, l.Add(d.add("00002"))) + require.Equal(t, 6, length(l)) + require.Equal(t, 6, lengthRev(l)) +} + +func TestSkiplistAdd_Overflow(t *testing.T) { + // Regression test for cockroachdb/pebble#1258. The length of the nodes buffer + // cannot exceed the maximum allowable size. + d := &testStorage{} + l := newTestSkiplist(d) + + // Simulate a full nodes slice. This speeds up the test significantly, as + // opposed to adding data to the list. + l.nodes = make([]byte, maxNodesSize) + + // Adding a new node to the list would overflow the nodes slice. Note that it + // is the size of a new node struct that is relevant here, rather than the + // size of the data being added to the list. + err := l.Add(d.add("too much!")) + require.Error(t, err) + require.True(t, errors.Is(err, ErrTooManyRecords)) +} + +// TestIteratorNext tests a basic iteration over all nodes from the beginning. +func TestIteratorNext(t *testing.T) { + const n = 100 + d := &testStorage{} + l := newTestSkiplist(d) + it := iterAdapter{l.NewIter(nil, nil)} + + require.False(t, it.Valid()) + + it.First() + require.False(t, it.Valid()) + + for i := n - 1; i >= 0; i-- { + require.Nil(t, l.Add(d.add(fmt.Sprintf("%05d", i)))) + } + + it.First() + for i := 0; i < n; i++ { + require.True(t, it.Valid()) + require.EqualValues(t, fmt.Sprintf("%05d", i), it.Key().UserKey) + it.Next() + } + require.False(t, it.Valid()) +} + +// // TestIteratorPrev tests a basic iteration over all nodes from the end. +func TestIteratorPrev(t *testing.T) { + const n = 100 + d := &testStorage{} + l := newTestSkiplist(d) + it := iterAdapter{l.NewIter(nil, nil)} + + require.False(t, it.Valid()) + + it.Last() + require.False(t, it.Valid()) + + for i := 0; i < n; i++ { + l.Add(d.add(fmt.Sprintf("%05d", i))) + } + + it.Last() + for i := n - 1; i >= 0; i-- { + require.True(t, it.Valid()) + require.EqualValues(t, fmt.Sprintf("%05d", i), string(it.Key().UserKey)) + it.Prev() + } + require.False(t, it.Valid()) +} + +func TestIteratorSeekGE(t *testing.T) { + const n = 1000 + d := &testStorage{} + l := newTestSkiplist(d) + it := iterAdapter{l.NewIter(nil, nil)} + + require.False(t, it.Valid()) + it.First() + require.False(t, it.Valid()) + // 1000, 1010, 1020, ..., 1990. + for i := n - 1; i >= 0; i-- { + require.Nil(t, l.Add(d.add(fmt.Sprintf("%05d", i*10+1000)))) + } + + require.True(t, it.SeekGE(makeKey(""))) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey("01000"))) + require.True(t, it.Valid()) + require.EqualValues(t, "01000", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey("01005"))) + require.True(t, it.Valid()) + require.EqualValues(t, "01010", it.Key().UserKey) + + require.True(t, it.SeekGE(makeKey("01010"))) + require.True(t, it.Valid()) + require.EqualValues(t, "01010", it.Key().UserKey) + + require.False(t, it.SeekGE(makeKey("99999"))) + require.False(t, it.Valid()) + + // Test seek for empty key. + require.Nil(t, l.Add(d.add(""))) + require.True(t, it.SeekGE([]byte{})) + require.True(t, it.Valid()) + + require.True(t, it.SeekGE(makeKey(""))) + require.True(t, it.Valid()) +} + +func TestIteratorSeekLT(t *testing.T) { + const n = 100 + d := &testStorage{} + l := newTestSkiplist(d) + it := iterAdapter{l.NewIter(nil, nil)} + + require.False(t, it.Valid()) + it.First() + require.False(t, it.Valid()) + // 1000, 1010, 1020, ..., 1990. + for i := n - 1; i >= 0; i-- { + require.Nil(t, l.Add(d.add(fmt.Sprintf("%05d", i*10+1000)))) + } + + require.False(t, it.SeekLT(makeKey(""))) + require.False(t, it.Valid()) + + require.False(t, it.SeekLT(makeKey("01000"))) + require.False(t, it.Valid()) + + require.True(t, it.SeekLT(makeKey("01001"))) + require.EqualValues(t, "01000", it.Key().UserKey) + require.True(t, it.Valid()) + + require.True(t, it.SeekLT(makeKey("01005"))) + require.EqualValues(t, "01000", it.Key().UserKey) + require.True(t, it.Valid()) + + require.True(t, it.SeekLT(makeKey("01991"))) + require.EqualValues(t, "01990", it.Key().UserKey) + require.True(t, it.Valid()) + + require.True(t, it.SeekLT(makeKey("99999"))) + require.True(t, it.Valid()) + require.EqualValues(t, "01990", it.Key().UserKey) + + // Test seek for empty key. + require.Nil(t, l.Add(d.add(""))) + require.False(t, it.SeekLT([]byte{})) + require.False(t, it.Valid()) + require.True(t, it.SeekLT(makeKey("\x01"))) + require.True(t, it.Valid()) + require.EqualValues(t, "", it.Key().UserKey) +} + +// TODO(peter): test First and Last. +func TestIteratorBounds(t *testing.T) { + d := &testStorage{} + l := newTestSkiplist(d) + for i := 1; i < 10; i++ { + require.NoError(t, l.Add(d.add(fmt.Sprintf("%05d", i)))) + } + + it := iterAdapter{l.NewIter(makeKey("00003"), makeKey("00007"))} + + // SeekGE within the lower and upper bound succeeds. + for i := 3; i <= 6; i++ { + key := makeKey(fmt.Sprintf("%05d", i)) + require.True(t, it.SeekGE(key)) + require.EqualValues(t, string(key), string(it.Key().UserKey)) + } + + // SeekGE before the lower bound still succeeds (only the upper bound is + // checked). + for i := 1; i < 3; i++ { + key := makeKey(fmt.Sprintf("%05d", i)) + require.True(t, it.SeekGE(key)) + require.EqualValues(t, string(key), string(it.Key().UserKey)) + } + + // SeekGE beyond the upper bound fails. + for i := 7; i < 10; i++ { + key := makeKey(fmt.Sprintf("%05d", i)) + require.False(t, it.SeekGE(key)) + } + + require.True(t, it.SeekGE(makeKey("00006"))) + require.EqualValues(t, "00006", it.Key().UserKey) + + // Next into the upper bound fails. + require.False(t, it.Next()) + + // SeekLT within the lower and upper bound succeeds. + for i := 4; i <= 7; i++ { + key := makeKey(fmt.Sprintf("%05d", i)) + require.True(t, it.SeekLT(key)) + require.EqualValues(t, fmt.Sprintf("%05d", i-1), string(it.Key().UserKey)) + } + + // SeekLT beyond the upper bound still succeeds (only the lower bound is + // checked). + for i := 8; i < 9; i++ { + key := makeKey(fmt.Sprintf("%05d", i)) + require.True(t, it.SeekLT(key)) + require.EqualValues(t, fmt.Sprintf("%05d", i-1), string(it.Key().UserKey)) + } + + // SeekLT before the lower bound fails. + for i := 1; i < 4; i++ { + key := makeKey(fmt.Sprintf("%05d", i)) + require.False(t, it.SeekLT(key)) + } + + require.True(t, it.SeekLT(makeKey("00004"))) + require.EqualValues(t, "00003", it.Key().UserKey) + + // Prev into the lower bound fails. + require.False(t, it.Prev()) +} + +func randomKey(rng *rand.Rand, b []byte) []byte { + key := rng.Uint32() + key2 := rng.Uint32() + binary.LittleEndian.PutUint32(b, key) + binary.LittleEndian.PutUint32(b[4:], key2) + return b +} + +// Standard test. Some fraction is read. Some fraction is write. Writes have +// to go through mutex lock. +func BenchmarkReadWrite(b *testing.B) { + for i := 0; i <= 10; i++ { + readFrac := float32(i) / 10.0 + b.Run(fmt.Sprintf("frac_%d", i*10), func(b *testing.B) { + var buf [8]byte + d := &testStorage{ + data: make([]byte, 0, b.N*10), + } + l := newTestSkiplist(d) + it := l.NewIter(nil, nil) + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := randomKey(rng, buf[:]) + if rng.Float32() < readFrac { + _ = it.SeekGE(key, base.SeekGEFlagsNone) + } else { + offset := d.addBytes(buf[:]) + _ = l.Add(offset) + } + } + b.StopTimer() + }) + } +} + +func BenchmarkOrderedWrite(b *testing.B) { + var buf [8]byte + d := &testStorage{ + data: make([]byte, 0, b.N*10), + } + l := newTestSkiplist(d) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + binary.BigEndian.PutUint64(buf[:], uint64(i)) + offset := d.addBytes(buf[:]) + _ = l.Add(offset) + } +} + +func BenchmarkIterNext(b *testing.B) { + var buf [8]byte + d := &testStorage{ + data: make([]byte, 0, 64<<10), + } + l := newTestSkiplist(d) + + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + for len(d.data)+20 < cap(d.data) { + key := randomKey(rng, buf[:]) + offset := d.addBytes(key) + err := l.Add(offset) + require.NoError(b, err) + } + + it := l.NewIter(nil, nil) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if !it.Valid() { + it.First() + } + it.Next() + } +} + +func BenchmarkIterPrev(b *testing.B) { + var buf [8]byte + d := &testStorage{ + data: make([]byte, 0, 64<<10), + } + l := newTestSkiplist(d) + + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + for len(d.data)+20 < cap(d.data) { + key := randomKey(rng, buf[:]) + offset := d.addBytes(key) + err := l.Add(offset) + require.NoError(b, err) + } + + it := l.NewIter(nil, nil) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if !it.Valid() { + it.Last() + } + it.Prev() + } +} diff --git a/pebble/internal/bytealloc/bytealloc.go b/pebble/internal/bytealloc/bytealloc.go new file mode 100644 index 0000000..b905270 --- /dev/null +++ b/pebble/internal/bytealloc/bytealloc.go @@ -0,0 +1,69 @@ +// Copyright 2016 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 bytealloc + +import "github.com/cockroachdb/pebble/internal/rawalloc" + +// An A provides chunk allocation of []byte, amortizing the overhead of each +// allocation. Because the underlying storage for the slices is shared, they +// should share a similar lifetime in order to avoid pinning large amounts of +// memory unnecessarily. The allocator itself is a []byte where cap() indicates +// the total amount of memory and len() is the amount already allocated. The +// size of the buffer to allocate from is grown exponentially when it runs out +// of room up to a maximum size (chunkAllocMaxSize). +type A []byte + +const chunkAllocMinSize = 512 +const chunkAllocMaxSize = 512 << 10 // 512 KB + +func (a A) reserve(n int) A { + allocSize := cap(a) * 2 + if allocSize < chunkAllocMinSize { + allocSize = chunkAllocMinSize + } else if allocSize > chunkAllocMaxSize { + allocSize = chunkAllocMaxSize + } + if allocSize < n { + allocSize = n + } + return rawalloc.New(0, allocSize) +} + +// Alloc allocates a new chunk of memory with the specified length. +func (a A) Alloc(n int) (A, []byte) { + if cap(a)-len(a) < n { + a = a.reserve(n) + } + p := len(a) + r := a[p : p+n : p+n] + a = a[:p+n] + return a, r +} + +// Copy allocates a new chunk of memory, initializing it from src. +func (a A) Copy(src []byte) (A, []byte) { + var alloc []byte + a, alloc = a.Alloc(len(src)) + copy(alloc, src) + return a, alloc +} + +// Reset returns the current chunk, resetting allocated memory back to none. +// Future allocations will use memory previously allocated by previous calls to +// Alloc or Copy, so the caller must know know that none of the previously +// allocated byte slices are still in use. +func (a A) Reset() A { + return a[:0] +} diff --git a/pebble/internal/cache/LICENSE b/pebble/internal/cache/LICENSE new file mode 100644 index 0000000..daa739e --- /dev/null +++ b/pebble/internal/cache/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Damian Gryski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pebble/internal/cache/cgo_disabled.go b/pebble/internal/cache/cgo_disabled.go new file mode 100644 index 0000000..0e75574 --- /dev/null +++ b/pebble/internal/cache/cgo_disabled.go @@ -0,0 +1,10 @@ +// Copyright 2020 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 !cgo +// +build !cgo + +package cache + +const cgoEnabled = false diff --git a/pebble/internal/cache/cgo_enabled.go b/pebble/internal/cache/cgo_enabled.go new file mode 100644 index 0000000..b7014cb --- /dev/null +++ b/pebble/internal/cache/cgo_enabled.go @@ -0,0 +1,10 @@ +// Copyright 2020 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 cgo +// +build cgo + +package cache + +const cgoEnabled = true diff --git a/pebble/internal/cache/clockpro.go b/pebble/internal/cache/clockpro.go new file mode 100644 index 0000000..cdae6a9 --- /dev/null +++ b/pebble/internal/cache/clockpro.go @@ -0,0 +1,909 @@ +// Copyright 2018. All rights reserved. Use of this source code is governed by +// an MIT-style license that can be found in the LICENSE file. + +// Package cache implements the CLOCK-Pro caching algorithm. +// +// CLOCK-Pro is a patent-free alternative to the Adaptive Replacement Cache, +// https://en.wikipedia.org/wiki/Adaptive_replacement_cache. +// It is an approximation of LIRS ( https://en.wikipedia.org/wiki/LIRS_caching_algorithm ), +// much like the CLOCK page replacement algorithm is an approximation of LRU. +// +// This implementation is based on the python code from https://bitbucket.org/SamiLehtinen/pyclockpro . +// +// Slides describing the algorithm: http://fr.slideshare.net/huliang64/clockpro +// +// The original paper: http://static.usenix.org/event/usenix05/tech/general/full_papers/jiang/jiang_html/html.html +// +// It is MIT licensed, like the original. +package cache // import "github.com/cockroachdb/pebble/internal/cache" + +import ( + "fmt" + "os" + "runtime" + "runtime/debug" + "strings" + "sync" + "sync/atomic" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" +) + +type fileKey struct { + // id is the namespace for fileNums. + id uint64 + fileNum base.DiskFileNum +} + +type key struct { + fileKey + offset uint64 +} + +// file returns the "file key" for the receiver. This is the key used for the +// shard.files map. +func (k key) file() key { + k.offset = 0 + return k +} + +func (k key) String() string { + return fmt.Sprintf("%d/%d/%d", k.id, k.fileNum, k.offset) +} + +// Handle provides a strong reference to a value in the cache. The reference +// does not pin the value in the cache, but it does prevent the underlying byte +// slice from being reused. +type Handle struct { + value *Value +} + +// Get returns the value stored in handle. +func (h Handle) Get() []byte { + if h.value != nil { + // NB: We don't increment shard.hits in this code path because we only want + // to record a hit when the handle is retrieved from the cache. + return h.value.buf + } + return nil +} + +// Release releases the reference to the cache entry. +func (h Handle) Release() { + h.value.release() +} + +type shard struct { + hits atomic.Int64 + misses atomic.Int64 + + mu sync.RWMutex + + reservedSize int64 + maxSize int64 + coldTarget int64 + blocks robinHoodMap // fileNum+offset -> block + files robinHoodMap // fileNum -> list of blocks + + // The blocks and files maps store values in manually managed memory that is + // invisible to the Go GC. This is fine for Value and entry objects that are + // stored in manually managed memory, but when the "invariants" build tag is + // set, all Value and entry objects are Go allocated and the entries map will + // contain a reference to every entry. + entries map[*entry]struct{} + + handHot *entry + handCold *entry + handTest *entry + + sizeHot int64 + sizeCold int64 + sizeTest int64 + + // The count fields are used exclusively for asserting expectations. + // We've seen infinite looping (cockroachdb/cockroach#70154) that + // could be explained by a corrupted sizeCold. Through asserting on + // these fields, we hope to gain more insight from any future + // reproductions. + countHot int64 + countCold int64 + countTest int64 +} + +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 { + e.referenced.Store(true) + } + } + c.mu.RUnlock() + if value == nil { + c.misses.Add(1) + return Handle{} + } + c.hits.Add(1) + return Handle{value: value} +} + +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)) + } + + c.mu.Lock() + defer c.mu.Unlock() + + k := key{fileKey{id, fileNum}, offset} + e := c.blocks.Get(k) + + switch { + case e == nil: + // no cache entry? add it + e = newEntry(c, k, int64(len(value.buf))) + e.setValue(value) + if c.metaAdd(k, e) { + value.ref.trace("add-cold") + c.sizeCold += e.size + c.countCold++ + } else { + value.ref.trace("skip-cold") + e.free() + e = nil + } + + case e.peekValue() != nil: + // cache entry was a hot or cold page + e.setValue(value) + e.referenced.Store(true) + delta := int64(len(value.buf)) - e.size + e.size = int64(len(value.buf)) + if e.ptype == etHot { + value.ref.trace("add-hot") + c.sizeHot += delta + } else { + value.ref.trace("add-cold") + c.sizeCold += delta + } + c.evict() + + default: + // cache entry was a test page + c.sizeTest -= e.size + c.countTest-- + c.metaDel(e).release() + c.metaCheck(e) + + e.size = int64(len(value.buf)) + c.coldTarget += e.size + if c.coldTarget > c.targetSize() { + c.coldTarget = c.targetSize() + } + + e.referenced.Store(false) + e.setValue(value) + e.ptype = etHot + if c.metaAdd(k, e) { + value.ref.trace("add-hot") + c.sizeHot += e.size + c.countHot++ + } else { + value.ref.trace("skip-hot") + e.free() + e = nil + } + } + + c.checkConsistency() + + // Values are initialized with a reference count of 1. That reference count + // is being transferred to the returned Handle. + return Handle{value: value} +} + +func (c *shard) checkConsistency() { + // See the comment above the count{Hot,Cold,Test} fields. + switch { + case c.sizeHot < 0 || c.sizeCold < 0 || c.sizeTest < 0 || c.countHot < 0 || c.countCold < 0 || c.countTest < 0: + panic(fmt.Sprintf("pebble: unexpected negative: %d (%d bytes) hot, %d (%d bytes) cold, %d (%d bytes) test", + c.countHot, c.sizeHot, c.countCold, c.sizeCold, c.countTest, c.sizeTest)) + case c.sizeHot > 0 && c.countHot == 0: + panic(fmt.Sprintf("pebble: mismatch %d hot size, %d hot count", c.sizeHot, c.countHot)) + case c.sizeCold > 0 && c.countCold == 0: + panic(fmt.Sprintf("pebble: mismatch %d cold size, %d cold count", c.sizeCold, c.countCold)) + case c.sizeTest > 0 && c.countTest == 0: + panic(fmt.Sprintf("pebble: mismatch %d test size, %d test count", c.sizeTest, c.countTest)) + } +} + +// Delete deletes the cached value for the specified file and offset. +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} + c.mu.RLock() + exists := c.blocks.Get(k) != nil + c.mu.RUnlock() + if !exists { + return + } + + var deletedValue *Value + func() { + c.mu.Lock() + defer c.mu.Unlock() + + 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.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 { + // No blocks for this file. + return false + } + + // 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() { + c.mu.Lock() + defer c.mu.Unlock() + + // NB: we use metaDel rather than metaEvict in order to avoid the expensive + // metaCheck call when the "invariants" build tag is specified. + for c.handHot != nil { + e := c.handHot + c.metaDel(c.handHot).release() + e.free() + } + + c.blocks.free() + c.files.free() +} + +func (c *shard) Reserve(n int) { + c.mu.Lock() + defer c.mu.Unlock() + c.reservedSize += int64(n) + + // Changing c.reservedSize will either increase or decrease + // the targetSize. But we want coldTarget to be in the range + // [0, targetSize]. So, if c.targetSize decreases, make sure + // that the coldTarget fits within the limits. + targetSize := c.targetSize() + if c.coldTarget > targetSize { + c.coldTarget = targetSize + } + + c.evict() + c.checkConsistency() +} + +// Size returns the current space used by the cache. +func (c *shard) Size() int64 { + c.mu.RLock() + size := c.sizeHot + c.sizeCold + c.mu.RUnlock() + return size +} + +func (c *shard) targetSize() int64 { + target := c.maxSize - c.reservedSize + // Always return a positive integer for targetSize. This is so that we don't + // end up in an infinite loop in evict(), in cases where reservedSize is + // greater than or equal to maxSize. + if target < 1 { + return 1 + } + return target +} + +// Add the entry to the cache, returning true if the entry was added and false +// if it would not fit in the cache. +func (c *shard) metaAdd(key key, e *entry) bool { + c.evict() + if e.size > c.targetSize() { + // The entry is larger than the target cache size. + return false + } + + c.blocks.Put(key, e) + if entriesGoAllocated { + // Go allocated entries need to be referenced from Go memory. The entries + // map provides that reference. + c.entries[e] = struct{}{} + } + + if c.handHot == nil { + // first element + c.handHot = e + c.handCold = e + c.handTest = e + } else { + c.handHot.link(e) + } + + if c.handCold == c.handHot { + c.handCold = c.handCold.prev() + } + + fkey := key.file() + if fileBlocks := c.files.Get(fkey); fileBlocks == nil { + c.files.Put(fkey, e) + } else { + fileBlocks.linkFile(e) + } + return true +} + +// 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. 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") + } + // Remove the pointer to the value. + deletedValue = e.val + e.val = nil + + c.blocks.Delete(e.key) + if entriesGoAllocated { + // Go allocated entries need to be referenced from Go memory. The entries + // map provides that reference. + delete(c.entries, e) + } + + if e == c.handHot { + c.handHot = c.handHot.prev() + } + if e == c.handCold { + c.handCold = c.handCold.prev() + } + if e == c.handTest { + c.handTest = c.handTest.prev() + } + + if e.unlink() == e { + // This was the last entry in the cache. + c.handHot = nil + c.handCold = nil + c.handTest = nil + } + + fkey := e.key.file() + if next := e.unlinkFile(); e == next { + c.files.Delete(fkey) + } else { + c.files.Put(fkey, next) + } + return deletedValue +} + +// Check that the specified entry is not referenced by the cache. +func (c *shard) metaCheck(e *entry) { + if invariants.Enabled { + if _, ok := c.entries[e]; ok { + fmt.Fprintf(os.Stderr, "%p: %s unexpectedly found in entries map\n%s", + e, e.key, debug.Stack()) + os.Exit(1) + } + if c.blocks.findByValue(e) != nil { + fmt.Fprintf(os.Stderr, "%p: %s unexpectedly found in blocks map\n%s\n%s", + e, e.key, &c.blocks, debug.Stack()) + os.Exit(1) + } + if c.files.findByValue(e) != nil { + fmt.Fprintf(os.Stderr, "%p: %s unexpectedly found in files map\n%s\n%s", + e, e.key, &c.files, debug.Stack()) + os.Exit(1) + } + // NB: c.hand{Hot,Cold,Test} are pointers into a single linked list. We + // only have to traverse one of them to check all of them. + var countHot, countCold, countTest int64 + var sizeHot, sizeCold, sizeTest int64 + for t := c.handHot.next(); t != nil; t = t.next() { + // Recompute count{Hot,Cold,Test} and size{Hot,Cold,Test}. + switch t.ptype { + case etHot: + countHot++ + sizeHot += t.size + case etCold: + countCold++ + sizeCold += t.size + case etTest: + countTest++ + sizeTest += t.size + } + if e == t { + fmt.Fprintf(os.Stderr, "%p: %s unexpectedly found in blocks list\n%s", + e, e.key, debug.Stack()) + os.Exit(1) + } + if t == c.handHot { + break + } + } + if countHot != c.countHot || countCold != c.countCold || countTest != c.countTest || + sizeHot != c.sizeHot || sizeCold != c.sizeCold || sizeTest != c.sizeTest { + fmt.Fprintf(os.Stderr, `divergence of Hot,Cold,Test statistics + cache's statistics: hot %d, %d, cold %d, %d, test %d, %d + recalculated statistics: hot %d, %d, cold %d, %d, test %d, %d\n%s`, + c.countHot, c.sizeHot, c.countCold, c.sizeCold, c.countTest, c.sizeTest, + countHot, sizeHot, countCold, sizeCold, countTest, sizeTest, + debug.Stack()) + os.Exit(1) + } + } +} + +func (c *shard) metaEvict(e *entry) (evictedValue *Value) { + switch e.ptype { + case etHot: + c.sizeHot -= e.size + c.countHot-- + case etCold: + c.sizeCold -= e.size + c.countCold-- + case etTest: + c.sizeTest -= e.size + c.countTest-- + } + evictedValue = c.metaDel(e) + c.metaCheck(e) + e.free() + return evictedValue +} + +func (c *shard) evict() { + for c.targetSize() <= c.sizeHot+c.sizeCold && c.handCold != nil { + c.runHandCold(c.countCold, c.sizeCold) + } +} + +func (c *shard) runHandCold(countColdDebug, sizeColdDebug int64) { + // countColdDebug and sizeColdDebug should equal c.countCold and + // c.sizeCold. They're parameters only to aid in debugging of + // cockroachdb/cockroach#70154. Since they're parameters, their + // arguments will appear within stack traces should we encounter + // a reproduction. + if c.countCold != countColdDebug || c.sizeCold != sizeColdDebug { + panic(fmt.Sprintf("runHandCold: cold count and size are %d, %d, arguments are %d and %d", + c.countCold, c.sizeCold, countColdDebug, sizeColdDebug)) + } + + e := c.handCold + if e.ptype == etCold { + if e.referenced.Load() { + e.referenced.Store(false) + e.ptype = etHot + c.sizeCold -= e.size + c.countCold-- + c.sizeHot += e.size + c.countHot++ + } else { + e.setValue(nil) + e.ptype = etTest + c.sizeCold -= e.size + c.countCold-- + c.sizeTest += e.size + c.countTest++ + for c.targetSize() < c.sizeTest && c.handTest != nil { + c.runHandTest() + } + } + } + + c.handCold = c.handCold.next() + + for c.targetSize()-c.coldTarget <= c.sizeHot && c.handHot != nil { + c.runHandHot() + } +} + +func (c *shard) runHandHot() { + if c.handHot == c.handTest && c.handTest != nil { + c.runHandTest() + if c.handHot == nil { + return + } + } + + e := c.handHot + if e.ptype == etHot { + if e.referenced.Load() { + e.referenced.Store(false) + } else { + e.ptype = etCold + c.sizeHot -= e.size + c.countHot-- + c.sizeCold += e.size + c.countCold++ + } + } + + c.handHot = c.handHot.next() +} + +func (c *shard) runHandTest() { + if c.sizeCold > 0 && c.handTest == c.handCold && c.handCold != nil { + // sizeCold is > 0, so assert that countCold == 0. See the + // comment above count{Hot,Cold,Test}. + if c.countCold == 0 { + panic(fmt.Sprintf("pebble: mismatch %d cold size, %d cold count", c.sizeCold, c.countCold)) + } + + c.runHandCold(c.countCold, c.sizeCold) + if c.handTest == nil { + return + } + } + + e := c.handTest + if e.ptype == etTest { + c.sizeTest -= e.size + c.countTest-- + c.coldTarget -= e.size + if c.coldTarget < 0 { + c.coldTarget = 0 + } + c.metaDel(e).release() + c.metaCheck(e) + e.free() + } + + c.handTest = c.handTest.next() +} + +// Metrics holds metrics for the cache. +type Metrics struct { + // The number of bytes inuse by the cache. + Size int64 + // The count of objects (blocks or tables) in the cache. + Count int64 + // The number of cache hits. + Hits int64 + // The number of cache misses. + Misses int64 +} + +// 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, 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. +// +// Blocks are keyed by an (id, fileNum, offset) triple. The ID is a namespace +// for file numbers and allows a single Cache to be shared between multiple +// Pebble instances. The fileNum and offset refer to an sstable file number and +// the offset of the block within the file. Because sstables are immutable and +// file numbers are never reused, (fileNum,offset) are unique for the lifetime +// of a Pebble instance. +// +// In addition to maintaining a map from (fileNum,offset) to data, each shard +// maintains a map of the cached blocks for a particular fileNum. This allows +// efficient eviction of all of the blocks for a file which is used when an +// sstable is deleted from disk. +// +// # Memory Management +// +// In order to reduce pressure on the Go GC, manual memory management is +// performed for the data stored in the cache. Manual memory management is +// performed by calling into C.{malloc,free} to allocate memory. Cache.Values +// are reference counted and the memory backing a manual value is freed when +// the reference count drops to 0. +// +// Manual memory management brings the possibility of memory leaks. It is +// imperative that every Handle returned by Cache.{Get,Set} is eventually +// released. The "invariants" build tag enables a leak detection facility that +// places a GC finalizer on cache.Value. When the cache.Value finalizer is run, +// if the underlying buffer is still present a leak has occurred. The "tracing" +// build tag enables tracing of cache.Value reference count manipulation and +// eases finding where a leak has occurred. These two facilities are usually +// used in combination by specifying `-tags invariants,tracing`. Note that +// "tracing" produces a significant slowdown, while "invariants" does not. +type Cache struct { + refs atomic.Int64 + maxSize int64 + idAlloc atomic.Uint64 + shards []shard + + // Traces recorded by Cache.trace. Used for debugging. + tr struct { + sync.Mutex + msgs []string + } +} + +// New creates a new cache of the specified size. Memory for the cache is +// allocated on demand, not during initialization. The cache is created with a +// reference count of 1. Each DB it is associated with adds a reference, so the +// creator of the cache should usually release their reference after the DB is +// created. +// +// c := cache.New(...) +// defer c.Unref() +// d, err := pebble.Open(pebble.Options{Cache: c}) +func New(size int64) *Cache { + // 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{ + maxSize: size, + shards: make([]shard, shards), + } + 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)), + coldTarget: size / int64(len(c.shards)), + } + if entriesGoAllocated { + c.shards[i].entries = make(map[*entry]struct{}) + } + c.shards[i].blocks.init(16) + c.shards[i].files.init(16) + } + + // 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 := c.refs.Load(); v != 0 { + c.tr.Lock() + fmt.Fprintf(os.Stderr, + "pebble: cache (%p) has non-zero reference count: %d\n", c, v) + if len(c.tr.msgs) > 0 { + fmt.Fprintf(os.Stderr, "%s\n", strings.Join(c.tr.msgs, "\n")) + } + c.tr.Unlock() + os.Exit(1) + } + }) + return c +} + +func (c *Cache) getShard(id uint64, fileNum base.DiskFileNum, offset uint64) *shard { + if id == 0 { + panic("pebble: 0 cache ID is invalid") + } + + // Inlined version of fnv.New64 + Write. + const offset64 = 14695981039346656037 + const prime64 = 1099511628211 + + h := uint64(offset64) + for i := 0; i < 8; i++ { + h *= prime64 + h ^= uint64(id & 0xff) + id >>= 8 + } + fileNumVal := uint64(fileNum.FileNum()) + for i := 0; i < 8; i++ { + h *= prime64 + h ^= uint64(fileNumVal) & 0xff + fileNumVal >>= 8 + } + for i := 0; i < 8; i++ { + h *= prime64 + h ^= uint64(offset & 0xff) + offset >>= 8 + } + + return &c.shards[h%uint64(len(c.shards))] +} + +// 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 := c.refs.Add(1) + if v <= 1 { + panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) + } + c.trace("ref", v) +} + +// Unref releases a reference on the cache. +func (c *Cache) Unref() { + v := c.refs.Add(-1) + c.trace("unref", v) + switch { + case v < 0: + panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) + case v == 0: + for i := range c.shards { + c.shards[i].Free() + } + } +} + +// 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.DiskFileNum, offset uint64) Handle { + return c.getShard(id, fileNum, offset).Get(id, fileNum, offset) +} + +// Set sets the cache value for the specified file and offset, overwriting an +// 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.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.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.DiskFileNum) { + if id == 0 { + panic("pebble: 0 cache ID is invalid") + } + for i := range c.shards { + c.shards[i].EvictFile(id, fileNum) + } +} + +// MaxSize returns the max size of the cache. +func (c *Cache) MaxSize() int64 { + return c.maxSize +} + +// Size returns the current space used by the cache. +func (c *Cache) Size() int64 { + var size int64 + for i := range c.shards { + size += c.shards[i].Size() + } + return size +} + +// Alloc allocates a byte slice of the specified size, possibly reusing +// previously allocated but unused memory. The memory backing the value is +// 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 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 Free(v *Value) { + if n := v.refs(); n > 1 { + panic(fmt.Sprintf("pebble: Value has been added to the cache: refs=%d", n)) + } + v.release() +} + +// Reserve N bytes in the cache. This effectively shrinks the size of the cache +// by N bytes, without actually consuming any memory. The returned closure +// should be invoked to release the reservation. +func (c *Cache) Reserve(n int) func() { + // Round-up the per-shard reservation. Most reservations should be large, so + // this probably doesn't matter in practice. + shardN := (n + len(c.shards) - 1) / len(c.shards) + for i := range c.shards { + c.shards[i].Reserve(shardN) + } + return func() { + if shardN == -1 { + panic("pebble: cache reservation already released") + } + for i := range c.shards { + c.shards[i].Reserve(-shardN) + } + shardN = -1 + } +} + +// Metrics returns the metrics for the cache. +func (c *Cache) Metrics() Metrics { + var m Metrics + for i := range c.shards { + s := &c.shards[i] + s.mu.RLock() + m.Count += int64(s.blocks.Count()) + m.Size += s.sizeHot + s.sizeCold + s.mu.RUnlock() + m.Hits += s.hits.Load() + m.Misses += s.misses.Load() + } + return m +} + +// NewID returns a new ID to be used as a namespace for cached file +// blocks. +func (c *Cache) NewID() uint64 { + return c.idAlloc.Add(1) +} diff --git a/pebble/internal/cache/clockpro_normal.go b/pebble/internal/cache/clockpro_normal.go new file mode 100644 index 0000000..ae49938 --- /dev/null +++ b/pebble/internal/cache/clockpro_normal.go @@ -0,0 +1,10 @@ +// 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. + +//go:build !tracing +// +build !tracing + +package cache + +func (c *Cache) trace(_ string, _ int64) {} diff --git a/pebble/internal/cache/clockpro_test.go b/pebble/internal/cache/clockpro_test.go new file mode 100644 index 0000000..5ec7b7f --- /dev/null +++ b/pebble/internal/cache/clockpro_test.go @@ -0,0 +1,279 @@ +// Copyright 2018. All rights reserved. Use of this source code is governed by +// an MIT-style license that can be found in the LICENSE file. + +package cache + +import ( + "bufio" + "bytes" + "fmt" + "os" + "runtime" + "strconv" + "sync" + "testing" + "time" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func TestCache(t *testing.T) { + // Test data was generated from the python code + f, err := os.Open("testdata/cache") + require.NoError(t, err) + + cache := newShards(200, 1) + defer cache.Unref() + + scanner := bufio.NewScanner(f) + line := 1 + + for scanner.Scan() { + fields := bytes.Fields(scanner.Bytes()) + + key, err := strconv.Atoi(string(fields[0])) + require.NoError(t, err) + + wantHit := fields[1][0] == 'h' + + var hit bool + h := cache.Get(1, base.FileNum(uint64(key)).DiskFileNum(), 0) + if v := h.Get(); v == nil { + value := Alloc(1) + value.Buf()[0] = fields[0][0] + cache.Set(1, base.FileNum(uint64(key)).DiskFileNum(), 0, value).Release() + } else { + hit = true + if !bytes.Equal(v, fields[0][:1]) { + t.Errorf("%d: cache returned bad data: got %s , want %s\n", line, v, fields[0][:1]) + } + } + h.Release() + if hit != wantHit { + t.Errorf("%d: cache hit mismatch: got %v, want %v\n", line, hit, wantHit) + } + line++ + } +} + +func testValue(cache *Cache, s string, repeat int) *Value { + b := bytes.Repeat([]byte(s), repeat) + v := Alloc(len(b)) + copy(v.Buf(), b) + return v +} + +func TestCacheDelete(t *testing.T) { + cache := newShards(100, 1) + defer cache.Unref() + + cache.Set(1, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 5)).Release() + cache.Set(1, base.FileNum(1).DiskFileNum(), 0, testValue(cache, "a", 5)).Release() + cache.Set(1, base.FileNum(2).DiskFileNum(), 0, testValue(cache, "a", 5)).Release() + if expected, size := int64(15), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } + cache.Delete(1, base.FileNum(1).DiskFileNum(), 0) + if expected, size := int64(10), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } + if h := cache.Get(1, base.FileNum(0).DiskFileNum(), 0); h.Get() == nil { + t.Fatalf("expected to find block 0/0") + } else { + h.Release() + } + if h := cache.Get(1, base.FileNum(1).DiskFileNum(), 0); h.Get() != nil { + t.Fatalf("expected to not find block 1/0") + } else { + h.Release() + } + // Deleting a non-existing block does nothing. + cache.Delete(1, base.FileNum(1).DiskFileNum(), 0) + if expected, size := int64(10), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } +} + +func TestEvictFile(t *testing.T) { + cache := newShards(100, 1) + defer cache.Unref() + + cache.Set(1, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 5)).Release() + cache.Set(1, base.FileNum(1).DiskFileNum(), 0, testValue(cache, "a", 5)).Release() + cache.Set(1, base.FileNum(2).DiskFileNum(), 0, testValue(cache, "a", 5)).Release() + cache.Set(1, base.FileNum(2).DiskFileNum(), 1, testValue(cache, "a", 5)).Release() + cache.Set(1, base.FileNum(2).DiskFileNum(), 2, testValue(cache, "a", 5)).Release() + if expected, size := int64(25), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } + cache.EvictFile(1, base.FileNum(0).DiskFileNum()) + if expected, size := int64(20), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } + cache.EvictFile(1, base.FileNum(1).DiskFileNum()) + if expected, size := int64(15), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } + cache.EvictFile(1, base.FileNum(2).DiskFileNum()) + if expected, size := int64(0), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } +} + +func TestEvictAll(t *testing.T) { + // Verify that it is okay to evict all of the data from a cache. Previously + // this would trigger a nil-pointer dereference. + cache := newShards(100, 1) + defer cache.Unref() + + cache.Set(1, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 101)).Release() + cache.Set(1, base.FileNum(1).DiskFileNum(), 0, testValue(cache, "a", 101)).Release() +} + +func TestMultipleDBs(t *testing.T) { + cache := newShards(100, 1) + defer cache.Unref() + + cache.Set(1, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 5)).Release() + cache.Set(2, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "b", 5)).Release() + if expected, size := int64(10), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } + cache.EvictFile(1, base.FileNum(0).DiskFileNum()) + if expected, size := int64(5), cache.Size(); expected != size { + t.Fatalf("expected cache size %d, but found %d", expected, size) + } + h := cache.Get(1, base.FileNum(0).DiskFileNum(), 0) + if v := h.Get(); v != nil { + t.Fatalf("expected not present, but found %s", v) + } + h = cache.Get(2, base.FileNum(0).DiskFileNum(), 0) + if v := h.Get(); string(v) != "bbbbb" { + t.Fatalf("expected bbbbb, but found %s", v) + } else { + h.Release() + } +} + +func TestZeroSize(t *testing.T) { + cache := newShards(0, 1) + defer cache.Unref() + + cache.Set(1, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 5)).Release() +} + +func TestReserve(t *testing.T) { + cache := newShards(4, 2) + defer cache.Unref() + + cache.Set(1, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + cache.Set(2, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + require.EqualValues(t, 2, cache.Size()) + r := cache.Reserve(1) + require.EqualValues(t, 0, cache.Size()) + cache.Set(1, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + cache.Set(2, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + cache.Set(3, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + cache.Set(4, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + require.EqualValues(t, 2, cache.Size()) + r() + require.EqualValues(t, 2, cache.Size()) + cache.Set(1, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + cache.Set(2, base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + require.EqualValues(t, 4, cache.Size()) +} + +func TestReserveDoubleRelease(t *testing.T) { + cache := newShards(100, 1) + defer cache.Unref() + + r := cache.Reserve(10) + r() + + result := func() (result string) { + defer func() { + if v := recover(); v != nil { + result = fmt.Sprint(v) + } + }() + r() + return "" + }() + const expected = "pebble: cache reservation already released" + if expected != result { + t.Fatalf("expected %q, but found %q", expected, result) + } +} + +func TestCacheStressSetExisting(t *testing.T) { + cache := newShards(1, 1) + defer cache.Unref() + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 10000; j++ { + cache.Set(1, base.FileNum(0).DiskFileNum(), uint64(i), testValue(cache, "a", 1)).Release() + runtime.Gosched() + } + }(i) + } + wg.Wait() +} + +func BenchmarkCacheGet(b *testing.B) { + const size = 100000 + + cache := newShards(size, 1) + defer cache.Unref() + + for i := 0; i < size; i++ { + v := testValue(cache, "a", 1) + cache.Set(1, base.FileNum(0).DiskFileNum(), uint64(i), v).Release() + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + for pb.Next() { + h := cache.Get(1, base.FileNum(0).DiskFileNum(), uint64(rng.Intn(size))) + if h.Get() == nil { + b.Fatal("failed to lookup value") + } + h.Release() + } + }) +} + +func TestReserveColdTarget(t *testing.T) { + // If coldTarget isn't updated when we call shard.Reserve, + // then we unnecessarily remove nodes from the + // cache. + + cache := newShards(100, 1) + defer cache.Unref() + + for i := 0; i < 50; i++ { + cache.Set(uint64(i+1), base.FileNum(0).DiskFileNum(), 0, testValue(cache, "a", 1)).Release() + } + + if cache.Size() != 50 { + require.Equal(t, 50, cache.Size(), "nodes were unnecessarily evicted from the cache") + } + + // There won't be enough space left for 50 nodes in the cache after + // we call shard.Reserve. This should trigger a call to evict. + cache.Reserve(51) + + // If we don't update coldTarget in Reserve then the cache gets emptied to + // size 0. In shard.Evict, we loop until shard.Size() < shard.targetSize(). + // Therefore, 100 - 51 = 49, but we evict one more node. + if cache.Size() != 48 { + t.Fatalf("expected positive cache size %d, but found %d", 48, cache.Size()) + } +} diff --git a/pebble/internal/cache/clockpro_tracing.go b/pebble/internal/cache/clockpro_tracing.go new file mode 100644 index 0000000..d14c1cd --- /dev/null +++ b/pebble/internal/cache/clockpro_tracing.go @@ -0,0 +1,20 @@ +// 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. + +//go:build tracing +// +build tracing + +package cache + +import ( + "fmt" + "runtime/debug" +) + +func (c *Cache) trace(msg string, refs int64) { + s := fmt.Sprintf("%s: refs=%d\n%s", msg, refs, debug.Stack()) + c.tr.Lock() + c.tr.msgs = append(c.tr.msgs, s) + c.tr.Unlock() +} diff --git a/pebble/internal/cache/entry.go b/pebble/internal/cache/entry.go new file mode 100644 index 0000000..a49fde6 --- /dev/null +++ b/pebble/internal/cache/entry.go @@ -0,0 +1,155 @@ +// Copyright 2020 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 cache + +import "sync/atomic" + +type entryType int8 + +const ( + etTest entryType = iota + etCold + etHot +) + +func (p entryType) String() string { + switch p { + case etTest: + return "test" + case etCold: + return "cold" + case etHot: + return "hot" + } + return "unknown" +} + +// entry holds the metadata for a cache entry. The memory for an entry is +// allocated from manually managed memory. +// +// Using manual memory management for entries is technically a volation of the +// Cgo pointer rules: +// +// https://golang.org/cmd/cgo/#hdr-Passing_pointers +// +// Specifically, Go pointers should not be stored in C allocated memory. The +// reason for this rule is that the Go GC will not look at C allocated memory +// to find pointers to Go objects. If the only reference to a Go object is +// stored in C allocated memory, the object will be reclaimed. The shard field +// of the entry struct points to a Go allocated object, thus the +// violation. What makes this "safe" is that the Cache guarantees that there +// are other pointers to the shard which will keep it alive. +type entry struct { + key key + // The value associated with the entry. The entry holds a reference on the + // value which is maintained by entry.setValue(). + val *Value + blockLink struct { + next *entry + prev *entry + } + fileLink struct { + next *entry + prev *entry + } + size int64 + 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 atomic.Bool + shard *shard + // Reference count for the entry. The entry is freed when the reference count + // drops to zero. + ref refcnt +} + +func newEntry(s *shard, key key, size int64) *entry { + e := entryAllocNew() + *e = entry{ + key: key, + size: size, + ptype: etCold, + shard: s, + } + e.blockLink.next = e + e.blockLink.prev = e + e.fileLink.next = e + e.fileLink.prev = e + e.ref.init(1) + return e +} + +func (e *entry) free() { + e.setValue(nil) + *e = entry{} + entryAllocFree(e) +} + +func (e *entry) next() *entry { + if e == nil { + return nil + } + return e.blockLink.next +} + +func (e *entry) prev() *entry { + if e == nil { + return nil + } + return e.blockLink.prev +} + +func (e *entry) link(s *entry) { + s.blockLink.prev = e.blockLink.prev + s.blockLink.prev.blockLink.next = s + s.blockLink.next = e + s.blockLink.next.blockLink.prev = s +} + +func (e *entry) unlink() *entry { + next := e.blockLink.next + e.blockLink.prev.blockLink.next = e.blockLink.next + e.blockLink.next.blockLink.prev = e.blockLink.prev + e.blockLink.prev = e + e.blockLink.next = e + return next +} + +func (e *entry) linkFile(s *entry) { + s.fileLink.prev = e.fileLink.prev + s.fileLink.prev.fileLink.next = s + s.fileLink.next = e + s.fileLink.next.fileLink.prev = s +} + +func (e *entry) unlinkFile() *entry { + next := e.fileLink.next + e.fileLink.prev.fileLink.next = e.fileLink.next + e.fileLink.next.fileLink.prev = e.fileLink.prev + e.fileLink.prev = e + e.fileLink.next = e + return next +} + +func (e *entry) setValue(v *Value) { + if v != nil { + v.acquire() + } + old := e.val + e.val = v + old.release() +} + +func (e *entry) peekValue() *Value { + return e.val +} + +func (e *entry) acquireValue() *Value { + v := e.val + if v != nil { + v.acquire() + } + return v +} diff --git a/pebble/internal/cache/entry_invariants.go b/pebble/internal/cache/entry_invariants.go new file mode 100644 index 0000000..31c54e4 --- /dev/null +++ b/pebble/internal/cache/entry_invariants.go @@ -0,0 +1,38 @@ +// Copyright 2020 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 (invariants && !race) || (tracing && !race) +// +build invariants,!race tracing,!race + +package cache + +import ( + "fmt" + "os" + + "github.com/cockroachdb/pebble/internal/invariants" +) + +// When the "invariants" or "tracing" build tags are enabled, we need to +// allocate entries using the Go allocator so entry.val properly maintains a +// reference to the Value. +const entriesGoAllocated = true + +func entryAllocNew() *entry { + e := &entry{} + // Note: this is a no-op if invariants and tracing are disabled or race is + // enabled. + invariants.SetFinalizer(e, func(obj interface{}) { + e := obj.(*entry) + if v := e.ref.refs(); v != 0 { + fmt.Fprintf(os.Stderr, "%p: cache entry has non-zero reference count: %d\n%s", + e, v, e.ref.traces()) + os.Exit(1) + } + }) + return e +} + +func entryAllocFree(e *entry) { +} diff --git a/pebble/internal/cache/entry_normal.go b/pebble/internal/cache/entry_normal.go new file mode 100644 index 0000000..92afb04 --- /dev/null +++ b/pebble/internal/cache/entry_normal.go @@ -0,0 +1,103 @@ +// Copyright 2020 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 (!invariants && !tracing) || race +// +build !invariants,!tracing race + +package cache + +import ( + "runtime" + "sync" + "unsafe" + + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/manual" +) + +const ( + entrySize = int(unsafe.Sizeof(entry{})) + entryAllocCacheLimit = 128 + // Avoid using runtime.SetFinalizer in race builds as finalizers tickle a bug + // in the Go race detector in go1.15 and earlier versions. This requires that + // entries are Go allocated rather than manually allocated. + // + // If cgo is disabled we need to allocate the entries using the Go allocator + // and is violates the Go GC rules to put Go pointers (such as the entry + // pointer fields) into untyped memory (i.e. a []byte). + entriesGoAllocated = invariants.RaceEnabled || !cgoEnabled +) + +var entryAllocPool = sync.Pool{ + New: func() interface{} { + return newEntryAllocCache() + }, +} + +func entryAllocNew() *entry { + a := entryAllocPool.Get().(*entryAllocCache) + e := a.alloc() + entryAllocPool.Put(a) + return e +} + +func entryAllocFree(e *entry) { + a := entryAllocPool.Get().(*entryAllocCache) + a.free(e) + entryAllocPool.Put(a) +} + +type entryAllocCache struct { + entries []*entry +} + +func newEntryAllocCache() *entryAllocCache { + c := &entryAllocCache{} + if !entriesGoAllocated { + // Note the use of a "real" finalizer here (as opposed to a build tag-gated + // no-op finalizer). Without the finalizer, objects released from the pool + // and subsequently GC'd by the Go runtime would fail to have their manually + // allocated memory freed, which results in a memory leak. + // lint:ignore SetFinalizer + runtime.SetFinalizer(c, freeEntryAllocCache) + } + return c +} + +func freeEntryAllocCache(obj interface{}) { + c := obj.(*entryAllocCache) + for i, e := range c.entries { + c.dealloc(e) + c.entries[i] = nil + } +} + +func (c *entryAllocCache) alloc() *entry { + n := len(c.entries) + if n == 0 { + if entriesGoAllocated { + return &entry{} + } + b := manual.New(entrySize) + return (*entry)(unsafe.Pointer(&b[0])) + } + e := c.entries[n-1] + c.entries = c.entries[:n-1] + return e +} + +func (c *entryAllocCache) dealloc(e *entry) { + if !entriesGoAllocated { + buf := (*[manual.MaxArrayLen]byte)(unsafe.Pointer(e))[:entrySize:entrySize] + manual.Free(buf) + } +} + +func (c *entryAllocCache) free(e *entry) { + if len(c.entries) == entryAllocCacheLimit { + c.dealloc(e) + return + } + c.entries = append(c.entries, e) +} diff --git a/pebble/internal/cache/refcnt_normal.go b/pebble/internal/cache/refcnt_normal.go new file mode 100644 index 0000000..9ab3348 --- /dev/null +++ b/pebble/internal/cache/refcnt_normal.go @@ -0,0 +1,59 @@ +// Copyright 2020 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 !tracing +// +build !tracing + +package cache + +import ( + "fmt" + "sync/atomic" + + "github.com/cockroachdb/redact" +) + +// refcnt provides an atomic reference count. This version is used when the +// "tracing" build tag is not enabled. See refcnt_tracing.go for the "tracing" +// enabled version. +type refcnt struct { + val atomic.Int32 +} + +// initialize the reference count to the specified value. +func (v *refcnt) init(val int32) { + v.val.Store(val) +} + +func (v *refcnt) refs() int32 { + return v.val.Load() +} + +func (v *refcnt) acquire() { + switch v := v.val.Add(1); { + case v <= 1: + panic(redact.Safe(fmt.Sprintf("pebble: inconsistent reference count: %d", v))) + } +} + +func (v *refcnt) release() bool { + switch v := v.val.Add(-1); { + case v < 0: + panic(redact.Safe(fmt.Sprintf("pebble: inconsistent reference count: %d", v))) + case v == 0: + return true + default: + return false + } +} + +func (v *refcnt) trace(msg string) { +} + +func (v *refcnt) traces() string { + return "" +} + +// Silence unused warning. +var _ = (*refcnt)(nil).traces diff --git a/pebble/internal/cache/refcnt_tracing.go b/pebble/internal/cache/refcnt_tracing.go new file mode 100644 index 0000000..1d5e6c0 --- /dev/null +++ b/pebble/internal/cache/refcnt_tracing.go @@ -0,0 +1,66 @@ +// Copyright 2020 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 tracing +// +build tracing + +package cache + +import ( + "fmt" + "runtime/debug" + "strings" + "sync" + "sync/atomic" +) + +// refcnt provides an atomic reference count, along with a tracing facility for +// debugging logic errors in manipulating the reference count. This version is +// used when the "tracing" build tag is enabled. +type refcnt struct { + val atomic.Int32 + sync.Mutex + msgs []string +} + +func (v *refcnt) init(val int32) { + v.val.Store(val) + v.trace("init") +} + +func (v *refcnt) refs() int32 { + return v.val.Load() +} + +func (v *refcnt) acquire() { + switch n := v.val.Add(1); { + case n <= 1: + panic(fmt.Sprintf("pebble: inconsistent reference count: %d", n)) + } + v.trace("acquire") +} + +func (v *refcnt) release() bool { + n := v.val.Add(-1) + switch { + case n < 0: + panic(fmt.Sprintf("pebble: inconsistent reference count: %d", n)) + } + v.trace("release") + return n == 0 +} + +func (v *refcnt) trace(msg string) { + s := fmt.Sprintf("%s: refs=%d\n%s", msg, v.refs(), debug.Stack()) + v.Lock() + v.msgs = append(v.msgs, s) + v.Unlock() +} + +func (v *refcnt) traces() string { + v.Lock() + s := strings.Join(v.msgs, "\n") + v.Unlock() + return s +} diff --git a/pebble/internal/cache/robin_hood.go b/pebble/internal/cache/robin_hood.go new file mode 100644 index 0000000..6e093fd --- /dev/null +++ b/pebble/internal/cache/robin_hood.go @@ -0,0 +1,320 @@ +// Copyright 2020 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 cache + +import ( + "fmt" + "math/bits" + "os" + "runtime/debug" + "strings" + "time" + "unsafe" + + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/manual" +) + +var hashSeed = uint64(time.Now().UnixNano()) + +// Fibonacci hash: https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ +func robinHoodHash(k key, shift uint32) uint32 { + const m = 11400714819323198485 + h := hashSeed + h ^= k.id * m + h ^= uint64(k.fileNum.FileNum()) * m + h ^= k.offset * m + return uint32(h >> shift) +} + +type robinHoodEntry struct { + key key + // Note that value may point to a Go allocated object (if the "invariants" + // build tag was specified), even though the memory for the entry itself is + // manually managed. This is technically a volation of the Cgo pointer rules: + // + // https://golang.org/cmd/cgo/#hdr-Passing_pointers + // + // Specifically, Go pointers should not be stored in C allocated memory. The + // reason for this rule is that the Go GC will not look at C allocated memory + // to find pointers to Go objects. If the only reference to a Go object is + // stored in C allocated memory, the object will be reclaimed. What makes + // this "safe" is that the Cache guarantees that there are other pointers to + // the entry and shard which will keep them alive. In particular, every Go + // allocated entry in the cache is referenced by the shard.entries map. And + // every shard is referenced by the Cache.shards map. + value *entry + // The distance the entry is from its desired position. + dist uint32 +} + +type robinHoodEntries struct { + ptr unsafe.Pointer + len uint32 +} + +func newRobinHoodEntries(n uint32) robinHoodEntries { + size := uintptr(n) * unsafe.Sizeof(robinHoodEntry{}) + return robinHoodEntries{ + ptr: unsafe.Pointer(&(manual.New(int(size)))[0]), + len: n, + } +} + +func (e robinHoodEntries) at(i uint32) *robinHoodEntry { + return (*robinHoodEntry)(unsafe.Pointer(uintptr(e.ptr) + + uintptr(i)*unsafe.Sizeof(robinHoodEntry{}))) +} + +func (e robinHoodEntries) free() { + size := uintptr(e.len) * unsafe.Sizeof(robinHoodEntry{}) + buf := (*[manual.MaxArrayLen]byte)(e.ptr)[:size:size] + manual.Free(buf) +} + +// robinHoodMap is an implementation of Robin Hood hashing. Robin Hood hashing +// is an open-address hash table using linear probing. The twist is that the +// linear probe distance is reduced by moving existing entries when inserting +// and deleting. This is accomplished by keeping track of how far an entry is +// from its "desired" slot (hash of key modulo number of slots). During +// insertion, if the new entry being inserted is farther from its desired slot +// than the target entry, we swap the target and new entry. This effectively +// steals from the "rich" target entry and gives to the "poor" new entry (thus +// the origin of the name). +// +// An extension over the base Robin Hood hashing idea comes from +// https://probablydance.com/2017/02/26/i-wrote-the-fastest-hashtable/. A cap +// is placed on the max distance an entry can be from its desired slot. When +// this threshold is reached during insertion, the size of the table is doubled +// and insertion is restarted. Additionally, the entries slice is given "max +// dist" extra entries on the end. The very last entry in the entries slice is +// never used and acts as a sentinel which terminates loops. The previous +// maxDist-1 entries act as the extra entries. For example, if the size of the +// table is 2, maxDist is computed as 4 and the actual size of the entry slice +// is 6. +// +// +---+---+---+---+---+---+ +// | 0 | 1 | 2 | 3 | 4 | 5 | +// +---+---+---+---+---+---+ +// ^ +// size +// +// In this scenario, the target entry for a key will always be in the range +// [0,1]. Valid entries may reside in the range [0,4] due to the linear probing +// of up to maxDist entries. The entry at index 5 will never contain a value, +// and instead acts as a sentinel (its distance is always 0). The max distance +// threshold is set to log2(num-entries). This ensures that retrieval is O(log +// N), though note that N is the number of total entries, not the count of +// valid entries. +// +// Deletion is implemented via the backward shift delete mechanism instead of +// tombstones. This preserves the performance of the table in the presence of +// deletions. See +// http://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion +// for details. +type robinHoodMap struct { + entries robinHoodEntries + size uint32 + shift uint32 + count uint32 + maxDist uint32 +} + +func maxDistForSize(size uint32) uint32 { + desired := uint32(bits.Len32(size)) + if desired < 4 { + desired = 4 + } + return desired +} + +func newRobinHoodMap(initialCapacity int) *robinHoodMap { + m := &robinHoodMap{} + m.init(initialCapacity) + + // Note: this is a no-op if invariants are disabled or race is enabled. + invariants.SetFinalizer(m, func(obj interface{}) { + m := obj.(*robinHoodMap) + if m.entries.ptr != nil { + fmt.Fprintf(os.Stderr, "%p: robin-hood map not freed\n", m) + os.Exit(1) + } + }) + return m +} + +func (m *robinHoodMap) init(initialCapacity int) { + if initialCapacity < 1 { + initialCapacity = 1 + } + targetSize := 1 << (uint(bits.Len(uint(2*initialCapacity-1))) - 1) + m.rehash(uint32(targetSize)) +} + +func (m *robinHoodMap) free() { + if m.entries.ptr != nil { + m.entries.free() + m.entries.ptr = nil + } +} + +func (m *robinHoodMap) rehash(size uint32) { + oldEntries := m.entries + + m.size = size + m.shift = uint32(64 - bits.Len32(m.size-1)) + m.maxDist = maxDistForSize(size) + m.entries = newRobinHoodEntries(size + m.maxDist) + m.count = 0 + + for i := uint32(0); i < oldEntries.len; i++ { + e := oldEntries.at(i) + if e.value != nil { + m.Put(e.key, e.value) + } + } + + if oldEntries.ptr != nil { + oldEntries.free() + } +} + +// Find an entry containing the specified value. This is intended to be used +// from debug and test code. +func (m *robinHoodMap) findByValue(v *entry) *robinHoodEntry { + for i := uint32(0); i < m.entries.len; i++ { + e := m.entries.at(i) + if e.value == v { + return e + } + } + return nil +} + +func (m *robinHoodMap) Count() int { + return int(m.count) +} + +func (m *robinHoodMap) Put(k key, v *entry) { + maybeExists := true + n := robinHoodEntry{key: k, value: v, dist: 0} + for i := robinHoodHash(k, m.shift); ; i++ { + e := m.entries.at(i) + if maybeExists && k == e.key { + // Entry already exists: overwrite. + e.value = n.value + m.checkEntry(i) + return + } + + if e.value == nil { + // Found an empty entry: insert here. + *e = n + m.count++ + m.checkEntry(i) + return + } + + if e.dist < n.dist { + // Swap the new entry with the current entry because the current is + // rich. We then continue to loop, looking for a new location for the + // current entry. Note that this is also the not-found condition for + // retrieval, which means that "k" is not present in the map. See Get(). + n, *e = *e, n + m.checkEntry(i) + maybeExists = false + } + + // The new entry gradually moves away from its ideal position. + n.dist++ + + // If we've reached the max distance threshold, grow the table and restart + // the insertion. + if n.dist == m.maxDist { + m.rehash(2 * m.size) + i = robinHoodHash(n.key, m.shift) - 1 + n.dist = 0 + maybeExists = false + } + } +} + +func (m *robinHoodMap) Get(k key) *entry { + var dist uint32 + for i := robinHoodHash(k, m.shift); ; i++ { + e := m.entries.at(i) + if k == e.key { + // Found. + return e.value + } + if e.dist < dist { + // Not found. + return nil + } + dist++ + } +} + +func (m *robinHoodMap) Delete(k key) { + var dist uint32 + for i := robinHoodHash(k, m.shift); ; i++ { + e := m.entries.at(i) + if k == e.key { + m.checkEntry(i) + // We found the entry to delete. Shift the following entries backwards + // until the next empty value or entry with a zero distance. Note that + // empty values are guaranteed to have "dist == 0". + m.count-- + for j := i + 1; ; j++ { + t := m.entries.at(j) + if t.dist == 0 { + *e = robinHoodEntry{} + return + } + e.key = t.key + e.value = t.value + e.dist = t.dist - 1 + e = t + m.checkEntry(j) + } + } + if dist > e.dist { + // Not found. + return + } + dist++ + } +} + +func (m *robinHoodMap) checkEntry(i uint32) { + if invariants.Enabled { + e := m.entries.at(i) + if e.value != nil { + pos := robinHoodHash(e.key, m.shift) + if (uint32(i) - pos) != e.dist { + fmt.Fprintf(os.Stderr, "%d: invalid dist=%d, expected %d: %s\n%s", + i, e.dist, uint32(i)-pos, e.key, debug.Stack()) + os.Exit(1) + } + if e.dist > m.maxDist { + fmt.Fprintf(os.Stderr, "%d: invalid dist=%d > maxDist=%d: %s\n%s", + i, e.dist, m.maxDist, e.key, debug.Stack()) + os.Exit(1) + } + } + } +} + +func (m *robinHoodMap) String() string { + var buf strings.Builder + fmt.Fprintf(&buf, "count: %d\n", m.count) + for i := uint32(0); i < m.entries.len; i++ { + e := m.entries.at(i) + if e.value != nil { + fmt.Fprintf(&buf, "%d: [%s,%p,%d]\n", i, e.key, e.value, e.dist) + } + } + return buf.String() +} diff --git a/pebble/internal/cache/robin_hood_test.go b/pebble/internal/cache/robin_hood_test.go new file mode 100644 index 0000000..d72c1b3 --- /dev/null +++ b/pebble/internal/cache/robin_hood_test.go @@ -0,0 +1,241 @@ +// Copyright 2020 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 cache + +import ( + "fmt" + "io" + "runtime" + "testing" + "time" + + "github.com/cockroachdb/pebble/internal/base" + "golang.org/x/exp/rand" +) + +func TestRobinHoodMap(t *testing.T) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + rhMap := newRobinHoodMap(0) + defer rhMap.free() + + goMap := make(map[key]*entry) + + randomKey := func() key { + n := rng.Intn(len(goMap)) + for k := range goMap { + if n == 0 { + return k + } + n-- + } + return key{} + } + + ops := 10000 + rng.Intn(10000) + for i := 0; i < ops; i++ { + var which float64 + if len(goMap) > 0 { + which = rng.Float64() + } + + switch { + case which < 0.4: + // 40% insert. + var k key + k.id = rng.Uint64() + k.fileNum = base.FileNum(rng.Uint64()).DiskFileNum() + k.offset = rng.Uint64() + e := &entry{} + goMap[k] = e + rhMap.Put(k, e) + if len(goMap) != rhMap.Count() { + t.Fatalf("map sizes differ: %d != %d", len(goMap), rhMap.Count()) + } + + case which < 0.1: + // 10% overwrite. + k := randomKey() + e := &entry{} + goMap[k] = e + rhMap.Put(k, e) + if len(goMap) != rhMap.Count() { + t.Fatalf("map sizes differ: %d != %d", len(goMap), rhMap.Count()) + } + + case which < 0.75: + // 25% delete. + k := randomKey() + delete(goMap, k) + rhMap.Delete(k) + if len(goMap) != rhMap.Count() { + t.Fatalf("map sizes differ: %d != %d", len(goMap), rhMap.Count()) + } + + default: + // 25% lookup. + k := randomKey() + v := goMap[k] + u := rhMap.Get(k) + if v != u { + t.Fatalf("%s: expected %p, but found %p", k, v, u) + } + } + } + + t.Logf("map size: %d", len(goMap)) +} + +const benchSize = 1 << 20 + +func BenchmarkGoMapInsert(b *testing.B) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + keys := make([]key, benchSize) + for i := range keys { + keys[i].fileNum = base.FileNum(rng.Uint64n(1 << 20)).DiskFileNum() + keys[i].offset = uint64(rng.Intn(1 << 20)) + } + b.ResetTimer() + + var m map[key]*entry + for i, j := 0, 0; i < b.N; i, j = i+1, j+1 { + if m == nil || j == len(keys) { + b.StopTimer() + m = make(map[key]*entry, len(keys)) + j = 0 + b.StartTimer() + } + m[keys[j]] = nil + } +} + +func BenchmarkRobinHoodInsert(b *testing.B) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + keys := make([]key, benchSize) + for i := range keys { + keys[i].fileNum = base.FileNum(rng.Uint64n(1 << 20)).DiskFileNum() + keys[i].offset = uint64(rng.Intn(1 << 20)) + } + e := &entry{} + b.ResetTimer() + + var m *robinHoodMap + for i, j := 0, 0; i < b.N; i, j = i+1, j+1 { + if m == nil || j == len(keys) { + b.StopTimer() + m = newRobinHoodMap(len(keys)) + j = 0 + b.StartTimer() + } + m.Put(keys[j], e) + } + + runtime.KeepAlive(e) +} + +func BenchmarkGoMapLookupHit(b *testing.B) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + keys := make([]key, benchSize) + m := make(map[key]*entry, len(keys)) + e := &entry{} + for i := range keys { + keys[i].fileNum = base.FileNum(rng.Uint64n(1 << 20)).DiskFileNum() + keys[i].offset = uint64(rng.Intn(1 << 20)) + m[keys[i]] = e + } + b.ResetTimer() + + var p *entry + for i, j := 0, 0; i < b.N; i, j = i+1, j+1 { + if j == len(keys) { + j = 0 + } + p = m[keys[j]] + } + + if testing.Verbose() { + fmt.Fprintln(io.Discard, p) + } +} + +func BenchmarkRobinHoodLookupHit(b *testing.B) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + keys := make([]key, benchSize) + m := newRobinHoodMap(len(keys)) + e := &entry{} + for i := range keys { + keys[i].fileNum = base.FileNum(rng.Uint64n(1 << 20)).DiskFileNum() + keys[i].offset = uint64(rng.Intn(1 << 20)) + m.Put(keys[i], e) + } + b.ResetTimer() + + var p *entry + for i, j := 0, 0; i < b.N; i, j = i+1, j+1 { + if j == len(keys) { + j = 0 + } + p = m.Get(keys[j]) + } + + if testing.Verbose() { + fmt.Fprintln(io.Discard, p) + } + runtime.KeepAlive(e) +} + +func BenchmarkGoMapLookupMiss(b *testing.B) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + keys := make([]key, benchSize) + m := make(map[key]*entry, len(keys)) + e := &entry{} + for i := range keys { + keys[i].id = 1 + keys[i].fileNum = base.FileNum(rng.Uint64n(1 << 20)).DiskFileNum() + keys[i].offset = uint64(rng.Intn(1 << 20)) + m[keys[i]] = e + keys[i].id = 2 + } + b.ResetTimer() + + var p *entry + for i, j := 0, 0; i < b.N; i, j = i+1, j+1 { + if j == len(keys) { + j = 0 + } + p = m[keys[j]] + } + + if testing.Verbose() { + fmt.Fprintln(io.Discard, p) + } +} + +func BenchmarkRobinHoodLookupMiss(b *testing.B) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + keys := make([]key, benchSize) + m := newRobinHoodMap(len(keys)) + e := &entry{} + for i := range keys { + keys[i].id = 1 + keys[i].fileNum = base.FileNum(rng.Uint64n(1 << 20)).DiskFileNum() + keys[i].offset = uint64(rng.Intn(1 << 20)) + m.Put(keys[i], e) + keys[i].id = 2 + } + b.ResetTimer() + + var p *entry + for i, j := 0, 0; i < b.N; i, j = i+1, j+1 { + if j == len(keys) { + j = 0 + } + p = m.Get(keys[j]) + } + + if testing.Verbose() { + fmt.Fprintln(io.Discard, p) + } + runtime.KeepAlive(e) +} diff --git a/pebble/internal/cache/testdata/cache b/pebble/internal/cache/testdata/cache new file mode 100644 index 0000000..fc7b218 --- /dev/null +++ b/pebble/internal/cache/testdata/cache @@ -0,0 +1,99991 @@ +0 m +1 m +2 m +3 m +1 h +1 h +4 m +5 m +6 m +1 h +7 m +8 m +9 m +4 h +10 m +4 h +1 h +11 m +10 h +4 h +12 m +4 h +13 m +1 h +4 h +14 m +15 m +16 m +17 m +18 m +19 m +4 h +1 h +1 h +4 h +4 h +20 m +21 m +10 h +22 m +4 h +4 h +4 h +4 h +8 h +23 m +4 h +24 m +1 h +1 h +1 h +4 h +25 m +26 m +27 m +4 h +28 m +29 m +10 h +30 m +1 h +4 h +1 h +11 h +31 m +4 h +3 h +4 h +1 h +31 h +10 h +32 m +33 m +10 h +34 m +35 m +36 m +37 m +4 h +4 h +1 h +1 h +38 m +39 m +40 m +41 m +42 m +43 d +10 h +4 h +4 h +44 m +45 m +46 m +1 h +1 h +1 h +4 h +10 h +47 m +1 h +1 h +48 m +1 h +49 m +10 h +11 h +4 h +50 m +27 h +10 h +10 h +51 m +11 h +3 h +4 h +52 m +10 h +53 m +1 h +1 h +3 h +54 m +55 m +56 m +57 m +58 m +59 m +60 m +4 h +4 h +1 h +61 m +1 h +1 h +62 m +63 m +1 h +64 m +65 m +66 m +36 h +67 m +4 h +4 h +59 h +68 m +10 h +69 m +4 h +4 h +4 h +70 m +1 h +71 m +1 h +72 m +10 h +73 m +4 h +74 m +10 h +4 h +11 h +4 h +10 h +75 m +76 m +4 h +77 m +78 m +1 h +79 m +80 m +81 m +4 h +4 h +82 m +83 m +84 m +1 h +85 m +1 h +4 h +10 h +86 m +1 h +87 m +11 h +4 h +59 h +1 h +88 m +89 m +90 m +91 m +4 h +4 h +92 m +3 h +4 h +4 h +4 h +93 m +1 h +94 m +10 h +95 m +1 h +82 h +96 m +82 h +1 h +10 h +10 h +97 m +1 h +4 h +4 h +98 m +31 h +99 m +100 m +41 h +10 h +101 m +102 m +1 h +103 m +11 h +104 m +105 m +57 h +106 m +4 h +107 m +83 h +4 h +10 h +108 m +1 h +109 m +1 h +110 m +1 h +10 h +111 m +4 h +4 h +4 h +10 h +112 m +10 h +113 m +4 h +114 m +115 m +116 m +1 h +57 h +117 m +118 m +4 h +1 h +1 h +1 h +10 h +10 h +119 m +4 h +120 m +1 h +1 h +4 h +69 h +4 h +121 m +73 h +122 m +4 h +123 m +4 h +124 m +1 h +4 h +45 h +10 h +10 h +125 m +4 h +109 h +1 h +126 m +82 h +1 h +127 m +11 h +1 h +10 h +10 h +4 h +1 h +1 h +4 h +128 m +10 h +4 h +10 h +129 m +83 h +4 h +1 h +130 m +131 m +132 m +1 h +133 m +134 m +135 m +136 m +1 h +137 m +4 h +4 h +4 h +11 h +4 h +4 h +138 m +139 m +140 m +141 m +4 h +1 h +4 h +65 h +4 h +142 m +4 h +11 h +124 h +143 m +4 h +144 m +145 m +10 h +97 h +146 m +147 m +4 h +148 m +10 h +4 h +82 h +1 h +4 h +3 h +149 m +65 h +150 m +4 h +151 m +152 m +1 h +153 m +91 h +59 h +4 h +4 h +154 m +1 h +4 h +155 m +156 m +157 m +25 h +4 h +158 m +159 m +3 h +82 h +160 m +4 h +161 m +4 h +4 h +162 m +74 h +163 m +4 h +10 h +1 h +4 h +164 m +10 h +165 m +166 m +4 h +167 m +4 h +4 h +74 h +10 h +10 h +1 h +4 h +168 m +169 m +4 h +4 h +143 h +4 h +55 h +170 m +171 m +10 h +11 h +124 h +124 h +1 h +1 h +172 m +4 h +173 m +174 m +4 h +1 h +124 h +4 h +123 h +4 h +4 h +104 h +82 h +175 m +176 m +94 h +4 h +1 h +177 m +1 h +178 m +1 h +179 m +45 h +10 h +4 h +1 h +1 h +1 h +180 m +146 h +181 m +25 h +4 h +182 m +183 m +184 m +185 m +4 h +4 h +11 h +1 h +186 m +143 h +4 h +187 m +188 m +189 m +190 m +57 h +1 h +147 h +10 h +1 h +10 h +4 h +1 h +1 h +10 h +191 m +192 m +10 h +193 m +22 h +1 h +194 m +195 m +25 h +196 m +197 m +4 h +4 h +198 m +92 h +4 h +199 m +147 h +4 h +200 m +201 m +202 m +65 h +4 h +1 h +203 m +10 h +204 m +205 m +4 h +206 m +207 m +1 h +1 h +208 m +4 h +209 m +4 h +124 h +45 h +210 m +59 h +211 m +212 m +4 h +213 m +1 h +10 h +41 h +4 h +109 h +10 h +214 m +215 m +124 h +4 h +1 h +216 m +10 h +4 h +11 h +109 h +4 h +4 h +217 m +10 h +4 h +8 h +1 h +10 h +4 h +4 h +218 m +36 h +219 m +4 h +1 h +220 m +221 m +4 h +4 h +10 h +222 m +4 h +73 h +3 h +51 h +223 m +158 h +11 h +224 m +59 h +11 h +10 h +97 h +74 h +4 h +225 m +102 h +1 h +56 h +4 h +4 h +4 h +226 m +4 h +1 h +227 m +4 h +228 m +229 m +109 h +10 h +230 m +1 h +10 h +231 m +1 h +232 m +10 h +1 h +10 h +82 h +233 m +4 h +4 h +234 m +1 h +4 h +235 m +12 m +10 h +28 m +236 m +237 m +1 h +10 h +238 m +10 h +4 h +239 m +113 h +10 h +40 m +240 m +10 h +4 h +4 h +10 h +1 h +1 h +10 h +82 h +4 h +241 m +4 h +4 h +242 m +243 m +110 h +31 h +244 m +245 m +246 m +4 h +1 h +247 m +248 m +4 h +1 h +1 h +249 m +4 h +124 h +119 h +4 h +4 h +10 h +4 h +10 h +123 h +250 m +251 m +10 h +4 h +252 m +253 m +254 m +238 h +8 h +1 h +1 h +4 h +57 h +4 h +255 m +4 h +10 h +4 h +41 h +10 h +256 m +257 m +92 h +129 h +258 m +125 h +57 h +10 h +97 h +4 h +1 h +1 h +31 h +259 m +1 h +260 m +4 h +4 h +1 h +261 m +196 h +262 m +1 h +1 h +263 m +1 h +3 h +1 h +264 m +265 m +1 h +45 h +82 h +10 h +266 m +267 m +268 m +4 h +262 h +10 h +269 m +4 h +270 m +271 m +272 m +11 h +140 h +10 h +4 h +10 h +273 m +82 h +25 h +4 h +274 m +1 h +10 h +4 h +275 m +1 h +10 h +276 m +4 h +1 h +277 m +10 h +10 h +4 h +4 h +108 m +4 h +278 m +4 h +279 m +4 h +10 h +1 h +57 h +1 h +1 h +57 h +3 h +1 h +10 h +4 h +1 h +280 m +11 h +4 h +1 h +281 m +282 m +10 h +283 m +284 m +285 m +143 h +4 h +1 h +124 h +57 h +12 h +266 h +4 h +4 h +104 h +4 h +11 h +4 h +4 h +286 m +109 h +4 h +4 h +10 h +4 h +287 m +1 h +288 m +79 m +125 h +94 h +139 h +289 m +290 m +4 h +1 h +1 h +1 h +291 m +292 m +293 m +4 h +4 h +294 m +295 m +1 h +10 h +11 h +10 h +92 h +112 m +11 h +10 h +167 h +1 h +296 m +4 h +297 m +73 h +298 m +258 h +65 h +4 h +1 h +4 h +299 m +4 h +1 h +1 h +74 h +300 m +1 h +1 h +10 h +82 h +301 m +83 h +302 m +139 h +4 h +4 h +278 h +303 m +304 m +147 h +305 m +4 h +306 m +1 h +135 m +1 h +4 h +1 h +4 h +307 m +4 h +57 h +10 h +1 h +11 h +25 h +125 h +57 h +4 h +1 h +4 h +4 h +308 m +1 h +10 h +309 m +1 h +310 m +83 h +119 h +311 m +312 m +313 m +4 h +119 h +314 m +1 h +4 h +315 m +316 m +317 m +10 h +10 h +1 h +4 h +59 h +318 m +1 h +319 m +1 h +4 h +10 h +4 h +320 m +109 h +321 m +10 h +322 m +323 m +108 h +10 h +324 m +10 h +4 h +135 h +1 h +325 m +4 h +10 h +272 h +1 h +10 h +4 h +1 h +64 m +326 m +119 h +327 m +328 m +329 m +60 m +36 h +330 m +119 h +4 h +331 m +332 m +4 h +10 h +4 h +333 m +10 h +10 h +4 h +1 h +1 h +10 h +1 h +1 h +334 m +335 m +1 h +65 h +167 h +336 m +143 h +266 h +114 m +337 m +10 h +3 h +124 h +338 m +4 h +339 m +10 h +10 h +340 m +82 h +10 h +11 h +83 h +10 h +4 h +4 h +4 h +295 h +10 h +4 h +1 h +1 h +341 m +1 h +342 m +343 m +57 h +1 h +79 h +110 h +1 h +1 h +344 m +82 h +181 m +11 h +345 m +10 h +1 h +1 h +1 h +346 m +1 h +347 m +1 h +13 m +4 h +348 m +349 m +10 h +13 h +4 h +4 h +1 h +4 h +11 h +350 m +4 h +351 m +10 h +4 h +352 m +353 m +104 h +4 h +82 h +354 m +1 h +4 h +124 h +355 m +11 h +295 h +250 h +12 h +10 h +356 m +109 h +10 h +357 m +1 h +10 h +73 h +25 h +358 m +1 h +1 h +57 h +4 h +359 m +11 h +360 m +1 h +10 h +31 h +361 m +59 h +1 h +4 h +4 h +1 h +362 m +1 h +12 h +41 h +363 m +1 h +10 h +11 h +4 h +147 h +114 h +10 h +10 h +5 m +10 h +10 h +364 m +365 m +1 h +10 h +11 h +4 h +366 m +1 h +4 h +4 h +1 h +367 m +368 m +12 h +147 h +129 h +65 h +10 h +369 m +10 h +82 h +4 h +4 h +370 m +25 h +1 h +371 m +10 h +119 h +4 h +372 m +94 h +373 m +4 h +1 h +4 h +374 m +12 h +4 h +10 h +375 m +28 h +61 m +4 h +114 h +147 h +195 m +376 m +1 h +377 m +378 m +170 m +4 h +10 h +196 h +4 h +1 h +4 h +379 m +380 m +4 h +381 m +4 h +144 m +41 h +1 h +382 m +383 m +384 m +1 h +190 m +112 h +10 h +10 h +10 h +385 m +1 h +1 h +4 h +10 h +1 h +4 h +386 m +4 h +4 h +31 h +10 h +4 h +10 h +74 h +387 m +1 h +388 m +31 h +10 h +389 m +10 h +3 h +10 h +383 h +10 h +1 h +110 h +390 m +10 h +391 m +392 m +25 h +4 h +238 h +10 h +393 m +394 m +4 h +10 h +22 m +4 h +282 h +1 h +4 h +1 h +395 m +396 m +230 m +1 h +1 h +4 h +1 h +10 h +4 h +4 h +195 h +92 h +307 h +10 h +397 m +4 h +4 h +1 h +1 h +4 h +229 m +398 m +4 h +1 h +4 h +4 h +399 m +45 h +79 h +4 h +1 h +27 m +140 h +1 h +400 m +1 h +10 h +1 h +48 m +401 m +402 m +403 m +404 m +4 h +10 h +405 m +1 h +406 m +1 h +1 h +407 m +408 m +10 h +4 h +270 m +195 h +4 h +1 h +409 m +4 h +135 h +1 h +4 h +3 h +410 m +4 h +411 m +4 h +10 h +412 m +10 h +413 m +10 h +138 m +104 h +4 h +172 m +1 h +4 h +1 h +414 m +4 h +4 h +4 h +11 h +195 h +1 h +83 h +109 h +1 h +10 h +10 h +10 h +4 h +415 m +4 h +10 h +416 m +1 h +1 h +4 h +417 m +10 h +10 h +4 h +4 h +368 h +74 h +10 h +65 h +295 h +383 h +4 h +4 h +82 h +10 h +25 h +11 h +64 h +143 h +418 m +10 h +92 h +419 m +420 m +421 m +1 h +59 h +57 h +4 h +422 m +10 h +258 h +423 m +424 m +1 h +4 h +1 h +11 h +425 m +426 m +10 h +4 h +4 h +4 h +427 m +1 h +56 h +1 h +428 m +279 m +429 m +11 h +1 h +430 m +83 h +124 h +4 h +4 h +1 h +1 h +11 h +1 h +31 h +10 h +1 h +266 h +4 h +431 m +432 m +10 h +1 h +1 h +433 m +1 h +11 h +1 h +4 h +1 h +434 m +143 h +4 h +112 h +435 m +436 m +437 m +1 h +10 h +4 h +1 h +438 m +439 m +10 h +110 h +1 h +440 m +10 h +12 h +1 h +441 m +4 h +442 m +1 h +1 h +147 h +4 h +4 h +10 h +1 h +4 h +443 m +10 h +444 m +4 h +445 m +4 h +10 h +1 h +4 h +79 h +74 h +1 h +31 h +1 h +146 h +446 m +10 h +10 h +1 h +164 m +31 h +4 h +83 h +4 h +82 h +4 h +4 h +4 h +10 h +1 h +447 m +10 h +11 h +10 h +104 h +448 m +449 m +450 m +1 h +451 m +1 h +41 h +36 h +452 m +82 h +453 m +10 h +1 h +371 h +4 h +454 m +455 m +31 h +1 h +10 h +456 m +10 h +358 h +457 m +74 h +458 m +10 h +459 m +4 h +65 h +12 h +10 h +4 h +460 m +4 h +11 h +156 m +125 h +118 m +1 h +10 h +10 h +461 m +4 h +114 h +11 h +4 h +462 m +31 h +124 h +463 m +464 m +1 h +1 h +109 h +135 h +10 h +1 h +1 h +465 m +1 h +74 h +466 m +4 h +467 m +4 h +4 h +10 h +468 m +11 h +4 h +1 h +10 h +1 h +469 m +1 h +4 h +4 h +1 h +10 h +195 h +10 h +470 m +471 m +4 h +472 m +4 h +125 h +4 h +146 h +172 h +473 m +4 h +10 h +59 h +4 h +31 h +4 h +474 m +4 h +475 m +4 h +266 h +8 h +4 h +27 h +57 h +476 m +477 m +478 m +10 h +4 h +1 h +10 h +479 m +371 h +1 h +4 h +59 h +94 h +4 h +1 h +480 m +481 m +4 h +482 m +483 m +1 h +484 m +1 h +169 m +1 h +4 h +4 h +4 h +266 h +4 h +4 h +4 h +485 m +41 h +124 h +1 h +1 h +25 h +486 m +487 m +25 h +488 m +1 h +1 h +489 m +4 h +1 h +25 h +4 h +10 h +490 m +4 h +4 h +224 m +97 h +491 m +10 h +22 h +492 m +10 h +493 m +1 h +494 m +1 h +10 h +170 h +495 m +1 h +83 h +496 m +497 m +498 m +4 h +11 h +114 h +1 h +499 m +500 m +4 h +196 h +82 h +1 h +1 h +57 h +10 h +501 m +1 h +10 h +1 h +1 h +27 h +83 h +502 m +4 h +503 m +4 h +4 h +332 m +1 h +4 h +4 h +238 h +504 m +505 m +195 h +83 h +10 h +156 h +4 h +506 m +4 h +507 m +4 h +1 h +4 h +1 h +307 h +59 h +508 m +1 h +97 h +4 h +509 m +10 h +510 m +10 h +1 h +4 h +125 h +185 m +4 h +511 m +4 h +4 h +10 h +4 h +347 m +92 h +190 m +169 h +196 h +110 h +1 h +4 h +10 h +10 h +1 h +11 h +1 h +512 m +4 h +513 m +514 m +10 h +515 m +516 m +1 h +517 m +57 h +138 h +11 h +330 m +1 h +4 h +518 m +4 h +10 h +82 h +31 h +1 h +10 h +1 h +519 m +1 h +4 h +1 h +4 h +1 h +1 h +109 h +10 h +520 m +55 m +521 m +522 m +523 m +297 m +108 h +10 h +10 h +31 h +164 h +524 m +97 h +525 m +526 m +41 h +4 h +4 h +1 h +124 h +4 h +1 h +527 m +195 h +528 m +529 m +4 h +109 h +241 m +4 h +4 h +4 h +1 h +530 m +146 h +10 h +135 h +11 h +4 h +358 h +169 h +531 m +1 h +532 m +4 h +533 m +4 h +4 h +94 h +10 h +4 h +4 h +55 h +1 h +266 h +1 h +10 h +1 h +4 h +82 h +4 h +534 m +535 m +10 h +10 h +4 h +181 m +10 h +536 m +3 h +10 h +1 h +1 h +1 h +10 h +4 h +1 h +265 m +537 m +10 h +82 h +56 h +538 m +4 h +539 m +1 h +92 h +56 h +540 m +541 m +1 h +10 h +1 h +542 m +4 h +64 h +543 m +82 h +544 m +4 h +258 h +4 h +545 m +1 h +170 h +10 h +4 h +297 h +368 h +1 h +1 h +10 h +190 h +135 h +1 h +1 h +546 m +4 h +4 h +307 h +4 h +1 h +169 h +4 h +4 h +143 h +10 h +1 h +547 m +4 h +4 h +10 h +10 h +97 h +548 m +1 h +549 m +550 m +1 h +1 h +551 m +4 h +8 h +4 h +4 h +1 h +552 m +4 h +553 m +538 h +1 h +554 m +1 h +555 m +10 h +4 h +10 h +556 m +10 h +10 h +557 m +558 m +4 h +125 h +184 m +559 m +560 m +4 h +10 h +92 h +10 h +561 m +562 m +57 h +3 h +4 h +4 h +563 m +190 h +4 h +10 h +4 h +564 m +4 h +4 h +59 h +1 h +565 m +10 h +4 h +1 h +566 m +41 h +1 h +1 h +143 h +567 m +10 h +10 h +568 m +82 h +1 h +10 h +4 h +569 m +353 m +570 m +110 h +571 m +572 m +31 h +82 h +573 m +574 m +55 h +1 h +1 h +4 h +4 h +386 m +575 m +576 m +93 m +4 h +577 m +4 h +578 m +579 m +580 m +4 h +4 h +581 m +4 h +582 m +4 h +10 h +4 h +583 m +4 h +1 h +4 h +4 h +584 m +143 h +45 h +1 h +4 h +585 m +59 h +4 h +11 h +586 m +587 m +4 h +588 m +1 h +10 h +10 h +1 h +589 m +1 h +590 m +11 h +1 h +4 h +591 m +4 h +592 m +4 h +4 h +593 m +10 h +594 m +595 m +4 h +241 h +596 m +1 h +443 m +1 h +1 h +597 m +598 m +4 h +10 h +10 h +36 h +599 m +1 h +13 h +4 h +4 h +229 m +1 h +1 h +1 h +4 h +10 h +1 h +10 h +600 m +601 m +4 h +104 h +11 h +4 h +11 h +1 h +10 h +602 m +1 h +83 h +386 h +4 h +83 h +10 h +158 h +603 m +604 m +605 m +83 h +4 h +10 h +83 h +606 m +607 m +10 h +109 h +4 h +25 h +608 m +609 m +4 h +10 h +10 h +610 m +31 h +611 m +10 h +10 h +4 h +612 m +330 h +613 m +10 h +10 h +79 h +614 m +518 h +92 h +4 h +615 m +147 h +4 h +10 h +616 m +4 h +4 h +113 m +617 m +4 h +4 h +108 h +618 m +11 h +1 h +1 h +619 m +620 m +307 h +4 h +4 h +11 h +109 h +10 h +621 m +622 m +386 h +4 h +1 h +274 m +10 h +10 h +623 m +97 h +10 h +10 h +3 h +4 h +4 h +4 h +1 h +443 h +624 m +1 h +1 h +10 h +170 h +3 h +625 m +626 m +4 h +4 h +11 h +4 h +59 h +31 h +627 m +143 h +628 m +13 h +629 m +10 h +1 h +4 h +1 h +4 h +630 m +56 h +10 h +631 m +129 h +22 h +27 h +1 h +4 h +632 m +1 h +25 h +1 h +4 h +82 h +633 m +4 h +634 m +1 h +635 m +636 m +274 h +114 h +1 h +637 m +1 h +638 m +639 m +97 h +10 h +10 h +332 h +4 h +12 h +640 m +368 h +450 m +641 m +11 h +4 h +92 h +4 h +4 h +642 m +4 h +459 m +10 h +1 h +643 m +1 h +1 h +10 h +270 m +3 h +644 m +536 m +10 h +4 h +645 m +646 m +647 m +1 h +114 h +5 m +1 h +10 h +65 h +224 h +170 h +648 m +82 h +10 h +4 h +649 m +1 h +10 h +1 h +650 m +11 h +651 m +1 h +652 m +653 m +4 h +654 m +97 h +109 h +83 h +10 h +56 h +146 h +4 h +10 h +65 h +4 h +4 h +655 m +656 m +4 h +1 h +657 m +11 h +11 h +1 h +270 h +25 h +10 h +1 h +147 h +658 m +64 h +1 h +1 h +1 h +59 h +1 h +659 m +660 m +10 h +27 h +661 m +4 h +662 m +1 h +1 h +4 h +104 h +663 m +10 h +4 h +664 m +10 h +665 m +666 m +11 h +278 h +10 h +181 h +10 h +667 m +4 h +668 m +1 h +669 m +330 h +670 m +671 m +1 h +64 h +4 h +11 h +11 h +672 m +4 h +4 h +673 m +10 h +4 h +674 m +675 m +4 h +676 m +677 m +4 h +10 h +4 h +97 h +4 h +1 h +1 h +1 h +1 h +10 h +678 m +4 h +10 h +114 h +679 m +4 h +59 h +59 h +4 h +4 h +1 h +4 h +4 h +680 m +4 h +65 h +45 h +4 h +41 h +73 h +4 h +31 h +1 h +681 m +10 h +109 h +146 h +22 h +682 m +683 m +4 h +684 m +10 h +10 h +685 m +10 h +10 h +4 h +4 h +4 h +686 m +1 h +4 h +687 m +1 h +688 m +1 h +1 h +4 h +1 h +10 h +434 m +689 m +690 m +41 h +4 h +691 m +4 h +13 h +692 m +4 h +1 h +693 m +10 h +10 h +694 m +1 h +10 h +695 m +59 h +41 h +1 h +4 h +696 m +1 h +10 h +113 h +697 m +698 m +23 m +11 h +699 m +10 h +700 m +94 h +701 m +640 h +702 m +250 h +10 h +1 h +703 m +4 h +1 h +1 h +4 h +10 h +10 h +704 m +265 h +4 h +10 h +74 h +147 h +705 m +4 h +4 h +706 m +59 h +707 m +4 h +569 m +4 h +135 h +4 h +708 m +4 h +1 h +709 m +31 h +1 h +143 h +4 h +710 m +711 m +11 h +57 h +1 h +4 h +1 h +110 h +10 h +712 m +4 h +713 m +12 h +1 h +714 m +541 m +10 h +1 h +97 h +10 h +1 h +359 m +1 h +715 m +716 m +1 h +10 h +717 m +10 h +4 h +57 h +1 h +10 h +1 h +4 h +4 h +4 h +1 h +1 h +718 m +1 h +10 h +295 h +719 m +720 m +4 h +119 m +4 h +11 h +266 h +721 m +4 h +36 h +722 m +1 h +4 h +4 h +723 m +4 h +724 m +353 h +1 h +10 h +1 h +195 h +10 h +1 h +250 h +725 m +726 m +31 h +727 m +4 h +196 h +1 h +36 h +4 h +493 m +575 m +4 h +728 m +1 h +146 h +729 m +1 h +82 h +1 h +4 h +730 m +59 h +731 m +10 h +119 h +4 h +1 h +732 m +10 h +1 h +10 h +1 h +733 m +82 h +4 h +11 h +1 h +4 h +4 h +4 h +4 h +734 m +10 h +36 h +10 h +4 h +27 h +1 h +735 m +736 m +79 h +45 h +737 m +10 h +4 h +10 h +1 h +4 h +10 h +10 h +4 h +1 h +1 h +25 h +738 m +1 h +10 h +739 m +27 h +167 h +4 h +740 m +10 h +10 h +692 h +1 h +57 h +741 m +4 h +156 h +4 h +10 h +1 h +4 h +4 h +464 m +1 h +59 h +4 h +742 m +1 h +1 h +743 m +744 m +169 h +25 h +4 h +1 h +10 h +295 h +745 m +250 h +12 h +9 m +746 m +747 m +124 h +748 m +4 h +749 m +307 h +92 h +4 h +10 h +4 h +10 h +1 h +1 h +750 m +174 m +1 h +278 h +1 h +8 h +258 h +751 m +4 h +4 h +1 h +752 m +65 h +10 h +1 h +753 m +258 h +4 h +59 h +164 h +4 h +754 m +755 m +82 h +1 h +10 h +4 h +10 h +1 h +756 m +57 h +1 h +48 m +757 m +1 h +276 m +82 h +1 h +758 m +1 h +358 h +4 h +759 m +760 m +4 h +4 h +761 m +238 h +717 h +762 m +10 h +4 h +241 h +10 h +1 h +4 h +10 h +10 h +4 h +446 m +763 m +1 h +764 m +83 h +4 h +765 m +766 m +767 m +4 h +768 m +158 h +238 h +1 h +1 h +83 h +82 h +82 h +4 h +109 h +4 h +31 h +1 h +4 h +109 h +769 m +770 m +112 h +229 h +1 h +31 h +771 m +4 h +11 h +204 m +1 h +4 h +4 h +204 h +772 m +272 h +4 h +4 h +1 h +83 h +536 h +773 m +4 h +774 m +4 h +3 h +775 m +776 m +718 h +57 h +1 h +46 m +777 m +1 h +778 m +82 h +82 h +4 h +1 h +779 m +124 h +97 h +266 h +780 m +781 m +4 h +10 h +4 h +782 m +11 h +783 m +4 h +1 h +784 m +10 h +10 h +785 m +4 h +31 h +786 m +787 m +4 h +1 h +10 h +4 h +788 m +4 h +789 m +790 m +143 h +10 h +3 h +110 h +791 m +1 h +10 h +27 h +792 m +4 h +1 h +4 h +1 h +4 h +793 m +143 h +4 h +4 h +4 h +794 m +795 m +13 h +4 h +125 h +4 h +4 h +796 m +4 h +94 h +195 h +4 h +36 h +1 h +4 h +4 h +59 h +4 h +174 h +797 m +289 m +82 h +4 h +798 m +123 h +1 h +10 h +4 h +799 m +1 h +800 m +4 h +801 m +146 h +55 h +802 m +3 h +10 h +83 h +82 h +803 m +4 h +1 h +1 h +1 h +4 h +804 m +82 h +57 h +278 h +805 m +94 h +1 h +4 h +25 h +806 m +57 h +1 h +4 h +4 h +807 m +1 h +443 h +808 m +1 h +1 h +809 m +13 h +1 h +64 h +4 h +810 m +1 h +1 h +811 m +57 h +812 m +4 h +41 h +10 h +813 m +814 m +11 h +4 h +45 h +4 h +25 h +204 h +4 h +1 h +41 h +28 h +815 m +4 h +4 h +10 h +816 m +817 m +1 h +4 h +172 h +4 h +208 m +818 m +819 m +435 m +820 m +4 h +821 m +124 h +4 h +4 h +186 m +4 h +1 h +536 h +4 h +123 h +822 m +123 h +10 h +1 h +10 h +1 h +4 h +10 h +1 h +823 m +158 h +824 m +1 h +825 m +4 h +826 m +4 h +4 h +1 h +4 h +4 h +827 m +828 m +4 h +109 h +4 h +4 h +10 h +64 h +4 h +829 m +1 h +1 h +10 h +4 h +830 m +57 h +57 h +4 h +1 h +831 m +832 m +1 h +833 m +10 h +1 h +834 m +10 h +11 h +835 m +4 h +10 h +4 h +299 m +299 h +55 h +1 h +118 h +74 h +4 h +104 h +10 h +56 h +10 h +10 h +836 m +4 h +124 h +1 h +119 h +1 h +1 h +94 h +10 h +44 m +837 m +82 h +1 h +838 m +109 h +10 h +83 h +1 h +839 m +83 h +840 m +841 m +11 h +1 h +11 h +10 h +10 h +718 h +4 h +842 m +843 m +1 h +4 h +281 m +77 m +45 h +10 h +332 h +844 m +97 h +4 h +25 h +845 m +846 m +10 h +847 m +265 h +848 m +10 h +10 h +4 h +849 m +1 h +850 m +4 h +157 m +1 h +851 m +852 m +4 h +4 h +853 m +854 m +855 m +4 h +1 h +856 m +10 h +4 h +857 m +358 h +4 h +59 h +858 m +4 h +3 h +10 h +4 h +1 h +4 h +10 h +172 h +12 h +114 h +4 h +146 h +57 h +859 m +82 h +4 h +860 m +1 h +861 m +1 h +10 h +4 h +862 m +146 h +4 h +4 h +1 h +533 m +10 h +863 m +1 h +11 h +1 h +12 h +1 h +83 h +864 m +4 h +10 h +4 h +4 h +1 h +865 m +10 h +866 m +3 h +867 m +289 h +4 h +10 h +10 h +868 m +1 h +1 h +10 h +13 h +1 h +4 h +1 h +170 h +146 h +4 h +4 h +94 h +1 h +869 m +1 h +870 m +1 h +1 h +871 m +1 h +4 h +92 h +1 h +4 h +872 m +873 m +874 m +8 h +358 h +28 h +875 m +4 h +119 h +4 h +876 m +83 h +1 h +57 h +279 m +4 h +1 h +1 h +400 m +4 h +536 h +11 h +172 h +877 m +4 h +4 h +10 h +10 h +1 h +4 h +1 h +69 m +1 h +1 h +878 m +879 m +1 h +10 h +4 h +1 h +880 m +59 h +881 m +10 h +25 h +882 m +4 h +1 h +10 h +4 h +1 h +10 h +883 m +884 m +276 m +4 h +125 h +4 h +885 m +124 h +4 h +125 h +1 h +59 h +1 h +1 h +4 h +4 h +886 m +1 h +887 m +196 h +888 m +79 h +27 h +11 h +889 m +4 h +10 h +4 h +4 h +41 h +890 m +4 h +4 h +10 h +891 m +892 m +90 m +893 m +894 m +91 m +10 h +1 h +59 h +1 h +4 h +1 h +1 h +1 h +895 m +56 h +4 h +1 h +896 m +897 m +898 m +4 h +4 h +11 h +82 h +1 h +899 m +258 h +4 h +900 m +901 m +902 m +109 h +1 h +4 h +903 m +135 h +57 h +94 h +1 h +158 h +297 h +56 h +904 m +905 m +4 h +906 m +687 m +1 h +25 h +196 h +1 h +10 h +907 m +1 h +10 h +4 h +1 h +170 h +11 h +1 h +110 h +4 h +10 h +10 h +4 h +908 m +1 h +61 m +10 h +1 h +4 h +909 m +1 h +185 h +910 m +10 h +911 m +105 m +912 m +10 h +10 h +83 h +10 h +4 h +59 h +913 m +10 h +192 m +1 h +1 h +4 h +4 h +4 h +124 h +914 m +4 h +915 m +10 h +916 m +10 h +83 h +1 h +4 h +1 h +307 h +172 h +917 m +143 h +1 h +4 h +918 m +196 h +1 h +4 h +874 h +82 h +919 m +920 g +1 h +1 h +1 h +4 h +55 h +1 h +921 m +922 m +170 h +1 h +923 m +10 h +10 h +4 h +1 h +924 m +192 h +857 h +11 h +10 h +4 h +925 m +926 m +927 m +1 h +11 h +10 h +928 m +4 h +1 h +4 h +104 h +929 m +1 h +10 h +82 h +930 m +196 h +4 h +931 m +3 h +932 m +933 m +10 h +1 h +934 m +10 h +164 h +266 h +4 h +935 m +10 h +4 h +1 h +447 m +936 m +937 m +4 h +938 m +4 h +4 h +4 h +4 h +4 h +939 m +124 h +83 h +940 m +1 h +36 h +941 m +383 h +10 h +942 m +83 h +4 h +11 h +1 h +4 h +4 h +4 h +1 h +1 h +943 m +4 h +4 h +944 m +10 h +1 h +3 h +945 m +10 h +10 h +10 h +1 h +946 m +1 h +1 h +55 h +4 h +947 m +1 h +948 m +4 h +124 h +190 h +949 m +950 m +10 h +951 m +4 h +952 m +278 h +31 h +1 h +953 m +1 h +299 h +4 h +954 m +1 h +69 h +955 m +10 h +1 h +10 h +4 h +156 h +4 h +10 h +956 m +307 h +4 h +4 h +55 h +1 h +1 h +957 m +74 h +4 h +229 h +174 h +195 h +10 h +939 h +4 h +4 h +4 h +195 h +10 h +1 h +1 h +57 h +190 h +4 h +1 h +10 h +48 m +104 h +1 h +1 h +435 m +1 h +1 h +1 h +4 h +4 h +11 h +10 h +31 h +10 h +83 h +4 h +250 h +4 h +4 h +10 h +11 h +1 h +958 m +4 h +83 h +25 h +4 h +959 m +10 h +4 h +158 h +1 h +1 h +10 h +4 h +960 m +25 h +4 h +961 m +10 h +119 h +10 h +4 h +1 h +962 m +146 h +104 h +1 h +10 h +156 h +57 h +4 h +1 h +963 m +1 h +1 h +10 h +964 m +125 h +4 h +1 h +10 h +965 m +966 m +45 h +967 m +10 h +4 h +55 h +1 h +4 h +4 h +8 h +10 h +27 h +59 h +1 h +10 h +1 h +4 h +129 h +10 h +164 h +4 h +4 h +4 h +1 h +10 h +4 h +968 m +10 h +82 h +1 h +1 h +969 m +10 h +4 h +970 m +971 m +972 m +10 h +1 h +4 h +1 h +238 h +203 m +77 m +1 h +45 h +973 m +4 h +13 h +4 h +55 h +45 h +974 m +1 h +757 m +3 h +4 h +4 h +975 m +104 h +976 m +330 h +109 h +1 h +4 h +10 h +536 h +763 m +57 h +4 h +4 h +977 m +125 h +10 h +978 m +4 h +1 h +196 h +358 h +4 h +979 m +4 h +11 h +4 h +980 m +10 h +25 h +10 h +981 m +1 h +57 h +1 h +73 h +10 h +92 h +1 h +41 h +4 h +1 h +4 h +1 h +10 h +11 h +1 h +982 m +10 h +983 m +687 h +12 h +11 h +1 h +4 h +196 h +1 h +41 h +984 m +4 h +4 h +985 m +386 h +10 h +1 h +986 m +59 h +987 m +1 h +988 m +10 h +1 h +11 h +10 h +238 h +146 h +4 h +757 h +10 h +989 m +990 m +991 m +383 h +992 m +1 h +993 m +59 h +4 h +4 h +258 h +97 h +4 h +1 h +994 m +1 h +995 m +1 h +1 h +4 h +10 h +996 m +4 h +1 h +124 h +12 h +4 h +997 m +998 m +4 h +1 h +4 h +10 h +1 h +4 h +10 h +36 h +999 m +4 h +443 h +11 h +359 m +1 h +10 h +4 h +1000 m +4 h +10 h +1001 m +438 m +1 h +1002 m +1003 m +1004 m +109 h +1 h +1005 m +4 h +196 h +1006 m +97 h +1 h +4 h +10 h +10 h +1007 m +1008 m +83 h +104 h +1009 m +1 h +4 h +56 h +1010 m +477 m +1011 m +3 h +1012 m +56 h +10 h +12 h +4 h +3 h +25 h +1013 m +10 h +1 h +1 h +10 h +1014 m +1015 m +10 h +1016 m +279 m +10 h +4 h +4 h +4 h +1017 m +10 h +4 h +114 h +1 h +4 h +1 h +4 h +172 h +1 h +125 h +1 h +10 h +1018 m +4 h +258 h +4 h +10 h +10 h +74 h +1 h +25 h +10 h +124 h +581 m +195 h +1019 m +57 h +1020 m +319 m +109 h +1021 m +4 h +4 h +181 h +65 h +10 h +1022 m +92 h +1 h +79 h +109 h +278 h +10 h +27 h +4 h +40 h +1 h +1023 m +4 h +10 h +1024 m +250 h +74 h +1 h +250 h +1025 m +11 h +59 h +10 h +10 h +1026 m +10 h +4 h +143 h +267 m +4 h +1 h +478 m +25 h +1027 m +1028 m +1 h +1029 m +4 h +4 h +1030 m +1 h +4 h +1 h +83 h +4 h +10 h +955 m +10 h +1031 m +1 h +4 h +41 h +164 h +4 h +1032 m +4 h +1 h +1033 m +1034 m +1035 m +1036 m +1037 m +83 h +119 h +1 h +272 h +10 h +1038 m +4 h +11 h +1039 m +4 h +79 h +4 h +1040 m +28 h +10 h +11 h +1041 m +59 h +41 h +4 h +13 h +4 h +1042 m +1043 m +10 h +1 h +4 h +1 h +1044 m +1 h +1045 m +10 h +4 h +10 h +1046 m +1 h +1047 m +4 h +1048 m +83 h +1049 m +1 h +10 h +3 h +4 h +112 h +36 h +1 h +1 h +4 h +1003 h +1050 m +114 h +4 h +1051 m +1052 m +1 h +1053 m +3 h +11 h +1 h +1 h +4 h +1 h +1054 m +1055 m +4 h +10 h +1056 m +4 h +1027 h +150 m +4 h +73 h +1057 m +109 h +83 h +779 m +1 h +195 h +10 h +25 h +4 h +4 h +4 h +10 h +4 h +4 h +1 h +4 h +41 h +1 h +112 h +1 h +4 h +1 h +10 h +1058 m +1 h +1 h +4 h +1059 m +1060 m +1 h +4 h +10 h +278 h +59 h +41 h +10 h +1 h +10 h +97 h +10 h +4 h +1061 m +1062 m +10 h +1063 m +11 h +1064 m +10 h +4 h +10 h +64 h +10 h +279 h +10 h +1 h +250 h +45 h +10 h +10 h +11 h +1 h +1065 m +10 h +3 h +10 h +170 h +41 h +27 h +4 h +1066 m +1067 m +11 h +57 h +1 h +1 h +10 h +1068 m +41 h +92 h +1069 m +262 h +4 h +1 h +1 h +4 h +1070 m +386 h +4 h +1071 m +1 h +1 h +12 h +1 h +1072 m +1 h +114 h +4 h +10 h +4 h +57 h +403 m +1073 m +330 h +1074 m +4 h +1 h +109 h +10 h +4 h +10 h +1075 m +1 h +1 h +1076 m +94 h +10 h +1 h +11 h +45 h +10 h +4 h +10 h +1 h +4 h +4 h +1077 m +10 h +1 h +124 h +10 h +10 h +4 h +1 h +41 h +1078 m +146 h +4 h +1 h +1079 m +1080 m +1081 m +1 h +92 h +1 h +4 h +10 h +25 h +4 h +4 h +77 h +11 h +4 h +4 h +1082 m +1083 m +1 h +1 h +1084 m +172 h +10 h +885 m +4 h +1 h +3 h +4 h +1 h +10 h +104 h +124 h +11 h +1 h +10 h +4 h +4 h +10 h +1 h +1 h +1085 m +105 m +265 h +8 h +1086 m +1087 m +4 h +4 h +4 h +109 h +27 h +4 h +1 h +4 h +4 h +1088 m +10 h +4 h +10 h +1 h +57 h +1089 m +367 m +125 h +4 h +1 h +4 h +1090 m +1091 m +146 h +1092 m +1093 m +1094 m +1095 m +195 h +57 h +4 h +1096 m +4 h +4 h +4 h +10 h +1097 m +74 h +1098 m +97 h +1 h +4 h +169 h +13 h +1099 m +4 h +1 h +10 h +4 h +4 h +10 h +4 h +4 h +1100 m +1 h +10 h +173 m +4 h +1101 m +1102 m +4 h +1 h +1 h +1103 m +11 h +1104 m +4 h +1105 m +1 h +4 h +112 h +4 h +1 h +1 h +1106 m +59 h +1 h +82 h +10 h +1107 m +4 h +1108 m +55 h +10 h +124 h +79 h +1109 m +358 h +258 h +1 h +1110 m +146 h +10 h +433 m +1111 m +56 h +1 h +4 h +4 h +1 h +1 h +4 h +4 h +1112 m +4 h +1 h +83 h +57 h +1113 m +10 h +4 h +13 h +434 h +1114 m +1 h +238 h +1115 m +1116 m +4 h +109 h +1117 m +10 h +1118 m +1 h +97 h +1 h +1119 m +1120 m +10 h +1121 m +4 h +4 h +4 h +4 h +4 h +36 h +1122 m +104 h +59 h +1123 m +10 h +4 h +129 h +1124 m +10 h +135 h +1125 m +1 h +1 h +1126 m +1 h +1 h +1127 m +1128 m +4 h +12 h +10 h +4 h +82 h +10 h +1 h +4 h +1 h +169 h +1 h +1129 m +1130 m +1 h +1131 m +1132 m +172 h +4 h +1133 m +10 h +10 h +1 h +1 h +1 h +1134 m +4 h +1 h +109 h +443 h +1 h +4 h +1135 m +1136 m +1137 m +1 h +1138 m +1 h +31 h +4 h +1139 m +1 h +10 h +4 h +4 h +1140 m +4 h +1 h +1 h +82 h +83 h +91 m +1141 m +4 h +1142 m +4 h +82 h +3 h +4 h +69 h +1143 m +4 h +1 h +1144 m +79 h +109 h +4 h +4 h +10 h +3 h +1145 m +82 h +297 h +4 h +1 h +1146 m +1 h +10 h +114 h +1 h +1147 m +13 h +1 h +3 h +12 h +4 h +4 h +1148 m +11 h +4 h +1 h +1149 m +79 h +4 h +1 h +124 h +10 h +64 h +10 h +4 h +10 h +1150 m +1151 m +41 h +1152 m +1153 m +4 h +1 h +4 h +1154 m +104 h +1 h +4 h +59 h +1155 m +1 h +338 m +59 h +1 h +4 h +1 h +1156 m +1157 m +112 h +1 h +4 h +1158 m +65 h +1 h +10 h +1159 m +1160 m +4 h +10 h +1161 m +1 h +109 h +4 h +11 h +4 h +1162 m +91 h +4 h +125 h +1 h +10 h +4 h +4 h +1 h +4 h +173 h +59 h +1163 m +124 h +1164 m +1165 m +104 h +73 h +1166 m +1167 m +1168 m +1169 m +10 h +4 h +10 h +22 h +82 h +11 h +1170 m +4 h +885 h +10 h +4 h +11 h +4 h +4 h +4 h +4 h +4 h +4 h +4 h +146 h +45 h +1171 m +1172 m +169 h +3 h +4 h +108 h +4 h +1 h +1173 m +4 h +114 h +1174 m +1 h +1175 m +4 h +1027 h +1176 m +10 h +4 h +1 h +97 h +75 m +4 h +10 h +83 h +4 h +25 h +1177 m +1178 m +536 h +4 h +4 h +10 h +109 h +73 h +1179 m +4 h +3 h +31 h +25 h +10 h +1180 m +10 h +1181 m +1182 m +4 h +1 h +1 h +4 h +1016 m +10 h +10 h +4 h +4 h +11 h +4 h +11 h +1183 m +1 h +1184 m +4 h +4 h +493 h +1 h +4 h +4 h +1 h +10 h +10 h +74 h +1 h +4 h +4 h +10 h +4 h +59 h +143 h +129 h +1185 m +10 h +1 h +1186 m +258 h +4 h +1187 m +1 h +10 h +1 h +1188 m +10 h +403 m +10 h +1189 m +1190 m +10 h +10 h +79 h +1 h +10 h +4 h +4 h +1191 m +10 h +1 h +10 h +94 h +4 h +11 h +4 h +1 h +1192 m +1 h +4 h +4 h +135 h +31 h +1193 m +4 h +4 h +1 h +368 h +10 h +4 h +73 h +4 h +10 h +4 h +1016 h +219 m +11 h +250 h +4 h +4 h +1 h +10 h +109 h +1194 m +1 h +12 h +1195 m +27 h +10 h +270 h +3 h +1 h +10 h +10 h +1062 m +10 h +297 h +1 h +170 h +282 h +57 h +10 h +1196 m +4 h +82 h +1197 m +1 h +1 h +1 h +13 h +1 h +1198 m +10 h +1199 m +1198 h +1 h +135 h +11 h +4 h +386 h +1200 m +4 h +10 h +57 h +1201 m +10 h +4 h +1202 m +10 h +1203 m +1204 m +4 h +1 h +11 h +649 m +41 h +1 h +469 m +172 h +74 h +4 h +1 h +4 h +1 h +94 h +1205 m +256 m +3 h +224 h +1206 m +11 h +1207 m +4 h +386 h +4 h +1 h +75 h +10 h +10 h +353 h +4 h +1208 m +4 h +4 h +1 h +10 h +10 h +4 h +4 h +1 h +1 h +10 h +1209 m +11 h +1210 m +4 h +4 h +135 h +11 h +1 h +10 h +31 h +4 h +327 m +1 h +4 h +4 h +1211 m +57 h +4 h +4 h +1 h +1212 m +1213 m +1 h +1 h +10 h +1 h +10 h +4 h +56 h +10 h +1214 m +1215 m +1216 m +196 h +11 h +97 h +1217 m +1218 m +1 h +4 h +4 h +1219 m +10 h +1220 m +1221 m +4 h +1222 m +1 h +123 h +10 h +1223 m +4 h +65 h +169 h +1224 m +10 h +4 h +1 h +10 h +1225 m +4 h +10 h +13 h +4 h +4 h +1226 m +250 h +1227 m +1 h +10 h +4 h +87 m +1228 m +4 h +4 h +4 h +353 h +138 h +10 h +10 h +1 h +1 h +10 h +1229 m +1230 m +4 h +4 h +1231 m +1 h +1232 m +1 h +1233 m +4 h +4 h +1 h +36 h +4 h +4 h +4 h +327 h +1 h +25 h +4 h +4 h +41 h +82 h +1 h +4 h +10 h +238 h +10 h +11 h +1 h +1 h +1 h +383 h +10 h +97 h +448 m +1234 m +1235 m +11 h +4 h +1 h +10 h +4 h +1 h +4 h +11 h +94 h +4 h +144 m +1236 m +10 h +125 h +1237 m +4 h +10 h +322 m +4 h +1 h +57 h +4 h +112 h +124 h +1238 m +1239 m +10 h +10 h +1240 m +1241 m +10 h +10 h +4 h +57 h +1242 m +1 h +258 h +109 h +170 h +1 h +1243 m +1 h +1 h +4 h +181 h +1244 m +10 h +1 h +4 h +4 h +1 h +4 h +358 h +10 h +4 h +113 h +97 h +1245 m +4 h +25 h +4 h +238 h +1246 m +124 h +4 h +10 h +1 h +23 m +4 h +1247 m +1 h +332 h +1 h +83 h +92 h +124 h +1 h +1248 m +4 h +1249 m +4 h +1250 m +10 h +1251 m +1252 m +158 h +1253 m +4 h +10 h +1254 m +1255 m +1256 m +4 h +1 h +4 h +1257 m +4 h +158 h +10 h +1258 m +1 h +4 h +276 h +4 h +575 h +1259 m +4 h +4 h +1260 m +1 h +11 h +1261 m +8 h +10 h +1262 m +1263 m +1 h +1264 m +4 h +1265 m +3 h +264 m +4 h +1266 m +4 h +124 h +4 h +4 h +4 h +1 h +1 h +36 h +1267 m +1 h +1 h +1268 m +4 h +4 h +12 h +1 h +74 h +124 h +195 h +1269 m +10 h +358 h +10 h +109 h +190 h +4 h +10 h +1270 m +1271 m +1272 m +4 h +4 h +1 h +10 h +4 h +169 h +11 h +41 h +1 h +1273 m +1274 m +4 h +1275 m +1276 m +195 h +1 h +1277 m +10 h +1 h +1 h +1 h +1278 m +73 h +1 h +1279 m +1096 m +10 h +10 h +1 h +10 h +4 h +124 h +170 h +4 h +4 h +25 h +4 h +97 h +138 h +4 h +1280 m +10 h +8 h +10 h +1 h +196 h +195 h +1281 m +173 h +195 h +1282 m +1 h +1283 m +1284 m +4 h +1285 m +1286 m +57 h +1287 m +582 m +4 h +10 h +11 h +1288 m +1289 m +1 h +56 h +434 h +146 h +1290 m +1291 m +164 h +10 h +11 h +4 h +1 h +1292 m +4 h +1293 m +265 h +4 h +10 h +1294 m +327 h +1295 m +1 h +55 h +1296 m +538 h +1297 m +10 h +1 h +181 h +94 h +1 h +1 h +4 h +4 h +1298 m +4 h +10 h +1299 m +12 h +1300 m +10 h +114 h +10 h +1 h +124 h +119 h +4 h +1301 m +3 h +4 h +1 h +10 h +1302 m +1303 m +10 h +1 h +10 h +4 h +1304 m +1 h +4 h +4 h +147 h +1305 m +4 h +4 h +1 h +57 h +4 h +3 h +1306 m +4 h +41 h +1307 m +4 h +48 h +10 h +139 h +4 h +11 h +13 h +10 h +109 h +3 h +1308 m +10 h +1 h +1309 m +4 h +1310 m +4 h +119 h +1 h +4 h +4 h +1311 m +22 h +146 h +1312 m +4 h +1 h +258 h +4 h +1 h +1313 m +307 h +1314 m +135 h +10 h +124 h +4 h +10 h +4 h +146 h +10 h +184 m +4 h +135 h +10 h +1315 m +4 h +4 h +10 h +4 h +10 h +27 h +1 h +10 h +10 h +840 m +1 h +4 h +4 h +493 h +307 h +65 h +1 h +112 h +181 h +25 h +1316 m +4 h +447 m +4 h +4 h +140 h +1317 m +1318 m +109 h +11 h +1319 m +1320 m +1 h +10 h +1321 m +123 h +4 h +173 h +1322 m +4 h +10 h +1323 m +4 h +10 h +135 h +1324 m +1 h +109 h +1325 m +1326 m +11 h +1327 m +1 h +1 h +158 h +10 h +144 m +36 h +57 h +4 h +10 h +10 h +278 h +1 h +4 h +10 h +124 h +1 h +4 h +4 h +4 h +4 h +4 h +1 h +10 h +1328 m +59 h +4 h +94 h +10 h +1329 m +1 h +1 h +4 h +1330 m +10 h +10 h +4 h +4 h +1 h +1 h +10 h +1331 m +1 h +10 h +82 h +57 h +4 h +4 h +1332 m +10 h +4 h +29 m +399 m +1 h +59 h +119 h +1 h +12 h +4 h +1333 m +629 m +1 h +1334 m +1335 m +1336 m +1 h +11 h +1337 m +10 h +1338 m +36 h +1 h +4 h +4 h +1339 m +1340 m +25 h +124 h +124 h +4 h +1 h +92 h +65 h +41 h +4 h +82 h +10 h +4 h +10 h +204 h +10 h +31 h +125 h +1341 m +4 h +1 h +10 h +123 h +276 h +77 h +170 h +1 h +12 h +10 h +10 h +10 h +1342 m +10 h +31 h +11 h +1 h +1 h +4 h +4 h +1343 m +10 h +1016 h +4 h +1344 m +4 h +1345 m +4 h +13 h +45 h +3 h +1346 m +1 h +10 h +1347 m +1348 m +1 h +1349 m +266 h +1 h +1350 m +4 h +1 h +59 h +1351 m +1 h +1 h +10 h +10 h +4 h +1352 m +4 h +1353 m +172 h +4 h +4 h +48 h +3 h +4 h +4 h +11 h +1354 m +1355 m +1356 m +4 h +4 h +1 h +10 h +1357 m +1 h +167 h +41 h +10 h +236 m +10 h +1358 m +1 h +10 h +1359 m +1 h +25 h +1360 m +10 h +3 h +4 h +1361 m +48 h +4 h +1362 m +119 h +1363 m +1 h +1 h +10 h +1364 m +10 h +258 h +82 h +82 h +4 h +195 h +4 h +1365 m +4 h +1366 m +10 h +10 h +170 h +55 h +25 h +1367 m +1368 m +4 h +11 h +1 h +124 h +1 h +4 h +113 h +10 h +339 m +12 h +11 h +1369 m +10 h +146 h +4 h +10 h +82 h +1 h +1370 m +1 h +181 h +1371 m +59 h +1 h +4 h +1372 m +1373 m +986 m +10 h +1 h +65 h +1374 m +1 h +10 h +10 h +1 h +1271 m +1375 m +1 h +92 h +1 h +157 m +1376 m +1377 m +1 h +1 h +82 h +13 h +65 h +4 h +1378 m +10 h +4 h +10 h +1379 m +630 m +737 m +1 h +1 h +4 h +4 h +97 h +4 h +4 h +4 h +1380 m +1261 m +1189 m +1381 m +1382 m +10 h +4 h +4 h +1 h +1383 m +4 h +57 h +1384 m +4 h +11 h +41 h +1385 m +65 h +4 h +11 h +83 h +4 h +1386 m +1387 m +1 h +1105 m +1388 m +135 h +1389 m +1390 m +1391 m +106 m +4 h +1392 m +10 h +173 h +1 h +1 h +1393 m +1394 m +1198 h +10 h +4 h +1 h +1 h +125 h +28 h +1 h +10 h +4 h +601 m +104 h +28 h +57 h +1 h +1395 m +104 h +1396 m +1 h +4 h +1397 m +4 h +10 h +41 h +1 h +57 h +10 h +4 h +12 h +124 h +4 h +1 h +4 h +1 h +4 h +4 h +124 h +4 h +10 h +10 h +1398 m +94 h +10 h +4 h +4 h +4 h +10 h +109 h +1399 m +1 h +1 h +1 h +10 h +1400 m +4 h +4 h +1 h +1401 m +74 h +4 h +1402 m +41 h +1 h +1 h +10 h +79 h +144 h +1 h +92 h +1403 m +4 h +4 h +1 h +82 h +1404 m +1405 m +1406 m +195 h +4 h +3 h +105 h +10 h +10 h +1 h +4 h +250 h +146 h +4 h +22 h +1 h +46 m +10 h +1407 m +31 h +4 h +367 m +1 h +1408 m +359 m +1409 m +140 h +4 h +74 h +91 h +124 h +1 h +10 h +158 h +1 h +4 h +4 h +4 h +10 h +4 h +1410 m +276 h +1411 m +1 h +59 h +74 h +4 h +11 h +146 h +4 h +10 h +1 h +4 h +4 h +118 h +4 h +1412 m +10 h +1 h +1413 m +4 h +1414 m +124 h +10 h +1415 m +10 h +1416 m +3 h +1027 h +1417 m +1 h +224 h +1 h +1418 m +4 h +282 h +4 h +1419 m +1420 m +1250 m +1421 m +1 h +10 h +4 h +1422 m +65 h +4 h +59 h +112 h +4 h +1309 m +94 h +1423 m +10 h +1424 m +1 h +190 h +4 h +4 h +1105 h +82 h +1425 m +1426 m +104 h +41 h +687 h +82 h +1 h +1 h +109 h +4 h +4 h +1427 m +12 h +4 h +1428 m +94 h +4 h +1 h +4 h +125 h +274 h +1429 m +4 h +258 h +192 h +1 h +123 h +125 h +83 h +4 h +129 h +173 h +1 h +1430 m +124 h +1431 m +79 h +3 h +10 h +4 h +1432 m +4 h +10 h +4 h +1433 m +1434 m +1 h +1435 m +10 h +4 h +4 h +1 h +11 h +1250 h +4 h +4 h +4 h +1436 m +1437 m +1 h +4 h +11 h +4 h +139 h +4 h +4 h +4 h +4 h +1438 m +10 h +4 h +1 h +1 h +270 h +1439 m +1440 m +1 h +124 h +1441 m +1442 m +10 h +1 h +4 h +4 h +10 h +4 h +10 h +1443 m +4 h +1444 m +4 h +4 h +10 h +4 h +278 h +1445 m +1446 m +4 h +1447 m +181 h +1448 m +1449 m +399 m +73 h +1 h +82 h +124 h +1 h +1 h +1450 m +1451 m +1452 m +4 h +1453 m +59 h +1454 m +1 h +367 h +10 h +4 h +4 h +1455 m +1456 m +615 m +929 m +1 h +4 h +4 h +11 h +1 h +4 h +1 h +1457 m +1458 m +1459 m +10 h +109 h +1460 m +1461 m +1462 m +27 h +4 h +169 h +4 h +1463 m +1464 m +1465 m +4 h +83 h +447 m +11 h +25 h +10 h +4 h +1466 m +12 h +4 h +25 h +164 h +332 h +1 h +11 h +10 h +1467 m +196 h +4 h +36 h +10 h +332 h +1468 m +73 h +258 h +1469 m +4 h +105 h +10 h +4 h +59 h +4 h +1 h +1470 m +1471 m +1 h +1 h +4 h +10 h +167 h +10 h +1 h +11 h +1472 m +1473 m +185 h +1474 m +1 h +4 h +4 h +4 h +36 h +1 h +1 h +238 h +1475 m +64 h +11 h +1476 m +59 h +4 h +1 h +1477 m +146 h +4 h +196 h +4 h +368 h +124 h +4 h +10 h +143 h +1478 m +4 h +1 h +146 h +4 h +65 h +97 h +1 h +1479 m +276 h +4 h +1478 h +1 h +196 h +4 h +1480 m +1481 m +1482 m +1483 m +119 h +57 h +399 h +1484 m +1485 m +1486 m +1487 m +135 h +4 h +1 h +4 h +935 m +73 h +158 h +3 h +59 h +4 h +173 h +1488 m +1 h +10 h +4 h +1 h +1489 m +4 h +1 h +1490 m +4 h +1 h +1491 m +1492 m +4 h +109 h +1493 m +1 h +10 h +1 h +36 h +1494 m +1495 m +10 h +1496 m +4 h +185 h +1497 m +1 h +4 h +1 h +1 h +146 h +4 h +64 h +412 m +4 h +4 h +1 h +266 h +1 h +4 h +1359 m +1498 m +1499 m +13 h +4 h +10 h +359 m +10 h +172 h +1 h +4 h +480 m +307 h +109 h +1 h +297 h +1500 m +25 h +1 h +10 h +3 h +1 h +10 h +4 h +1501 m +1 h +10 h +1 h +124 h +4 h +1502 m +10 h +158 h +4 h +4 h +1503 m +4 h +119 h +1 h +83 h +31 h +10 h +1 h +996 m +195 h +4 h +1504 m +1 h +4 h +737 m +1505 m +1506 m +57 h +1 h +10 h +770 m +1507 m +4 h +1508 m +173 h +1509 m +10 h +1 h +1510 m +1511 m +4 h +279 h +228 m +124 h +3 h +1512 m +4 h +1 h +172 h +1 h +10 h +1513 m +4 h +1514 m +40 h +10 h +41 h +10 h +1 h +124 h +1515 m +4 h +1516 m +55 h +1 h +4 h +1517 m +11 h +1518 m +4 h +4 h +1519 m +1520 m +4 h +10 h +1 h +1521 m +59 h +10 h +1522 m +1 h +1523 m +83 h +10 h +74 h +140 h +41 h +10 h +10 h +1 h +10 h +4 h +10 h +443 h +4 h +56 h +82 h +258 h +536 h +13 h +10 h +1524 m +10 h +4 h +10 h +4 h +1525 m +1 h +1362 m +1526 m +57 h +1527 m +266 h +4 h +1528 m +124 h +185 h +1 h +4 h +185 h +4 h +167 h +4 h +1 h +114 h +4 h +1 h +266 h +4 h +10 h +4 h +1529 m +1530 m +1531 m +10 h +10 h +195 h +4 h +10 h +1532 m +601 m +1 h +1533 m +10 h +1218 m +1 h +4 h +1534 m +4 h +1535 m +4 h +82 h +11 h +1 h +1536 m +10 h +195 h +10 h +124 h +4 h +1 h +1537 m +1538 m +1539 m +4 h +276 h +114 h +4 h +1 h +4 h +10 h +258 h +1 h +1540 m +82 h +1003 h +92 h +1541 m +156 h +4 h +1542 m +4 h +4 h +40 h +13 h +1543 m +4 h +73 h +10 h +4 h +22 h +1544 m +1545 m +4 h +4 h +4 h +4 h +4 h +4 h +59 h +83 h +4 h +4 h +1546 m +601 h +1 h +10 h +4 h +1 h +10 h +1 h +1547 m +1 h +1 h +83 h +112 h +4 h +208 m +1 h +11 h +10 h +10 h +1 h +1 h +4 h +4 h +4 h +1548 m +857 h +4 h +4 h +1549 m +109 h +59 h +1550 m +10 h +45 h +65 h +1 h +25 h +4 h +4 h +1551 m +4 h +109 h +228 h +12 h +4 h +1552 m +1 h +1553 m +1 h +4 h +10 h +4 h +1554 m +4 h +27 h +1555 m +4 h +1 h +10 h +4 h +129 h +192 h +1556 m +25 h +1557 m +1 h +1558 m +1559 m +124 h +4 h +4 h +443 h +10 h +1560 m +10 h +1 h +1 h +1561 m +10 h +57 h +4 h +10 h +1562 m +4 h +4 h +1563 m +1564 m +10 h +10 h +1565 m +4 h +1566 m +1567 m +1568 m +1569 m +4 h +1570 m +83 h +4 h +4 h +1 h +1 h +167 h +371 h +97 h +1299 m +59 h +10 h +10 h +4 h +1571 m +4 h +4 h +109 h +11 h +1572 m +4 h +10 h +74 h +10 h +1573 m +4 h +10 h +1574 m +1575 m +1 h +124 h +10 h +10 h +1576 m +4 h +10 h +1359 h +13 h +104 h +59 h +1577 m +1027 h +1578 m +1579 m +1580 m +266 h +1 h +1581 m +196 h +295 h +1582 m +1 h +1 h +1583 m +1 h +1584 m +4 h +1 h +1344 m +1 h +1585 m +1 h +1 h +4 h +1586 m +11 h +36 h +97 h +1 h +1 h +25 h +1 h +987 m +241 h +1587 m +10 h +156 h +195 h +82 h +1 h +4 h +1 h +10 h +1588 m +278 h +1589 m +1590 m +1 h +109 h +1591 m +888 m +1 h +1592 m +4 h +13 h +4 h +12 h +823 m +1593 m +4 h +146 h +82 h +1 h +73 h +1594 m +1 h +1595 m +1596 m +1 h +1597 m +10 h +1598 m +73 h +4 h +10 h +109 h +1 h +31 h +1 h +1599 m +4 h +1600 m +1 h +1601 m +204 h +10 h +10 h +1602 m +1603 m +1 h +4 h +1604 m +1 h +10 h +1 h +1 h +4 h +10 h +10 h +1 h +1 h +1605 m +297 h +55 h +124 h +140 h +10 h +59 h +1606 m +10 h +4 h +73 h +4 h +4 h +4 h +276 h +10 h +1607 m +1 h +1 h +1 h +125 h +1 h +1608 m +10 h +433 m +615 m +1535 m +10 h +10 h +10 h +1609 m +69 h +4 h +536 h +1 h +1610 m +4 h +4 h +1611 m +1 h +1612 m +4 h +25 h +10 h +1613 m +4 h +1614 m +1 h +1615 m +3 h +196 h +25 h +41 h +1616 m +1617 m +181 h +1 h +4 h +4 h +1 h +124 h +10 h +10 h +1 h +1 h +1618 m +1 h +1 h +10 h +1 h +3 h +10 h +4 h +1619 m +1620 m +4 h +4 h +4 h +262 h +41 h +167 h +1621 m +4 h +10 h +1622 m +64 h +1623 m +167 h +1 h +4 h +124 h +4 h +1624 m +4 h +1 h +353 h +1625 m +1437 m +11 h +383 h +1 h +1626 m +135 h +4 h +1627 m +181 h +92 h +31 h +104 h +730 m +1628 m +4 h +11 h +1629 m +10 h +1 h +73 h +1630 m +1631 m +4 h +10 h +55 h +1 h +1632 m +4 h +1 h +4 h +256 m +1 h +4 h +4 h +1633 m +1 h +368 h +4 h +1 h +4 h +114 h +1634 m +1 h +1635 m +4 h +10 h +1636 m +4 h +172 h +10 h +190 h +10 h +124 h +1637 m +195 h +57 h +10 h +4 h +4 h +1638 m +4 h +1 h +181 h +1639 m +11 h +1640 m +1 h +1641 m +1642 m +1643 m +4 h +167 h +1644 m +10 h +1645 m +4 h +4 h +10 h +4 h +1646 m +10 h +4 h +10 h +307 h +64 h +692 h +368 h +1 h +386 h +41 h +538 h +265 h +1 h +59 h +1647 m +1648 m +4 h +1 h +28 h +443 h +186 m +125 h +10 h +1649 m +3 h +10 h +1650 m +4 h +1 h +10 h +1651 m +4 h +1652 m +4 h +10 h +1653 m +1 h +139 h +10 h +1654 m +4 h +83 h +4 h +1 h +443 h +10 h +10 h +4 h +1655 m +1 h +124 h +1656 m +307 h +10 h +1657 m +4 h +10 h +10 h +1658 m +1 h +4 h +79 h +1 h +4 h +1659 m +1660 m +59 h +1661 m +1662 m +1 h +36 h +4 h +1663 m +83 h +4 h +1 h +114 h +359 h +65 h +1664 m +1665 m +1666 m +10 h +1667 m +3 h +1668 m +1 h +59 h +45 h +1669 m +4 h +104 h +1670 m +1671 m +83 h +119 h +10 h +1 h +10 h +1 h +56 h +1672 m +1 h +10 h +241 h +575 h +4 h +4 h +536 h +109 h +167 h +266 h +4 h +4 h +1673 m +10 h +4 h +119 h +4 h +146 h +10 h +54 m +10 h +1 h +124 h +1 h +91 h +4 h +1 h +1674 m +1 h +59 h +1675 m +278 h +1 h +4 h +4 h +1676 m +1 h +1 h +1677 m +36 h +1 h +10 h +10 h +48 h +1 h +59 h +1678 m +40 h +1 h +1 h +371 h +1 h +1679 m +4 h +1680 m +4 h +10 h +57 h +1 h +250 h +4 h +1 h +1 h +4 h +56 h +4 h +11 h +74 h +74 h +4 h +12 h +59 h +1681 m +10 h +1 h +1682 m +10 h +270 h +1 h +1 h +4 h +10 h +1 h +1683 m +69 h +3 h +1684 m +10 h +10 h +11 h +10 h +4 h +459 h +41 h +872 m +1 h +1685 m +4 h +1 h +1686 m +25 h +1 h +15 m +4 h +1687 m +109 h +104 h +1 h +1 h +1688 m +4 h +1689 m +10 h +10 h +4 h +295 h +1690 m +1 h +59 h +10 h +4 h +1 h +4 h +4 h +190 h +1691 m +1 h +1692 m +640 h +4 h +1 h +1 h +4 h +10 h +4 h +41 h +4 h +1 h +1693 m +4 h +36 h +25 h +4 h +4 h +1694 m +1695 m +1696 m +10 h +109 h +135 h +1 h +1697 m +1698 m +4 h +4 h +12 h +1699 m +1016 h +278 h +1 h +11 h +10 h +1 h +4 h +1700 m +1137 m +10 h +170 h +4 h +1701 m +1137 h +1074 m +1702 m +1703 m +4 h +125 h +4 h +10 h +64 h +1704 m +10 h +1 h +278 h +1 h +1705 m +238 h +687 h +1706 m +1 h +124 h +36 h +1707 m +388 m +1708 m +25 h +1 h +1709 m +10 h +57 h +570 m +90 m +1 h +10 h +1710 m +10 h +4 h +1711 m +10 h +1 h +264 m +4 h +4 h +1712 m +1713 m +1107 m +4 h +82 h +1 h +1714 m +1 h +104 h +359 h +4 h +11 h +1 h +1 h +11 h +10 h +285 m +4 h +1 h +10 h +1715 m +1 h +119 h +57 h +41 h +83 h +976 m +169 h +11 h +1716 m +1250 h +3 h +10 h +1 h +1717 m +1 h +65 h +124 h +649 m +1269 m +1718 m +57 h +656 m +1 h +112 h +1719 m +1720 m +4 h +1 h +10 h +1721 m +92 h +109 h +10 h +1722 m +1 h +1723 m +4 h +10 h +4 h +1261 h +4 h +4 h +4 h +10 h +10 h +640 h +1 h +185 h +1 h +1 h +11 h +94 h +1067 m +1724 m +10 h +1725 m +1726 m +1 h +4 h +190 h +4 h +181 h +4 h +4 h +57 h +57 h +185 h +10 h +1727 m +93 m +4 h +737 h +10 h +140 h +1728 m +338 m +4 h +1729 m +143 h +4 h +1 h +4 h +4 h +8 h +4 h +4 h +10 h +1730 m +13 h +367 h +4 h +3 h +1731 m +1732 m +1 h +23 m +4 h +272 h +31 h +10 h +97 h +1733 m +4 h +124 h +124 h +1 h +1734 m +157 m +1 h +258 h +1735 m +1736 m +1 h +1191 m +36 h +109 h +74 h +104 h +3 h +10 h +135 h +45 h +185 h +238 h +1737 m +4 h +10 h +4 h +1738 m +4 h +4 h +1 h +1 h +4 h +10 h +1739 m +1740 m +10 h +1 h +4 h +1741 m +1 h +1742 m +1 h +4 h +13 h +4 h +1321 m +59 h +4 h +164 h +157 h +4 h +4 h +1743 m +10 h +83 h +4 h +13 h +1744 m +1 h +1745 m +114 h +65 h +1261 h +10 h +359 h +1 h +83 h +10 h +1746 m +83 h +25 h +1747 m +1748 m +1749 m +156 h +1 h +1750 m +73 h +4 h +1751 m +1 h +1752 m +4 h +12 h +1753 m +4 h +109 h +10 h +1754 m +4 h +1755 m +27 h +4 h +10 h +1 h +10 h +10 h +10 h +57 h +1756 m +4 h +4 h +59 h +10 h +1757 m +1758 m +1759 m +181 h +1 h +1760 m +109 h +1 h +1406 m +1761 m +10 h +1 h +4 h +4 h +1762 m +10 h +1763 m +1764 m +10 h +1 h +11 h +1765 m +10 h +10 h +1 h +1 h +41 h +1766 m +1 h +11 h +11 h +4 h +109 h +1 h +1767 m +1768 m +25 h +4 h +1309 h +10 h +1 h +125 h +3 h +10 h +10 h +1769 m +1 h +1 h +1770 m +4 h +307 h +1 h +443 h +4 h +169 h +1771 m +1772 m +1 h +1773 m +1 h +4 h +10 h +1774 m +4 h +4 h +4 h +4 h +1775 m +124 h +64 h +1776 m +1777 m +1 h +1 h +4 h +1778 m +10 h +172 h +36 h +4 h +1779 m +41 h +601 h +104 h +4 h +4 h +97 h +1780 m +4 h +3 h +1781 m +4 h +4 h +1 h +57 h +1 h +10 h +59 h +41 h +1 h +1 h +4 h +25 h +158 h +1 h +10 h +10 h +1782 m +4 h +10 h +1783 m +1784 m +10 h +4 h +55 h +1785 m +1786 m +174 h +4 h +1 h +4 h +1787 m +10 h +4 h +1788 m +11 h +4 h +4 h +1789 m +1790 m +4 h +41 h +97 h +4 h +33 m +1791 m +1792 m +4 h +1 h +10 h +36 h +106 m +506 m +156 h +1793 m +1794 m +615 h +4 h +1 h +4 h +4 h +1795 m +4 h +1 h +1 h +83 h +1796 m +1797 m +1798 m +73 h +4 h +1799 m +82 h +109 h +1800 m +4 h +4 h +185 h +1 h +4 h +10 h +158 h +1801 m +1108 m +4 h +59 h +4 h +1802 m +1 h +399 h +1 h +1803 m +4 h +4 h +1804 m +83 h +113 h +4 h +4 h +4 h +1805 m +114 h +4 h +4 h +1406 m +119 h +124 h +1806 m +3 h +1 h +184 m +1 h +10 h +4 h +104 h +109 h +109 h +10 h +4 h +125 h +4 h +97 h +59 h +10 h +4 h +4 h +692 h +1807 m +4 h +3 h +4 h +1808 m +4 h +10 h +31 h +1809 m +4 h +113 h +1 h +123 h +1810 m +1811 m +4 h +1 h +11 h +10 h +3 h +4 h +1 h +82 h +1812 m +4 h +1 h +123 h +1 h +82 h +976 m +10 h +12 h +1 h +12 h +10 h +1 h +4 h +124 h +10 h +4 h +4 h +1 h +4 h +4 h +1813 m +55 h +1814 m +1359 h +4 h +1815 m +4 h +1816 m +3 h +388 m +820 m +1 h +1817 m +1 h +4 h +1 h +11 h +4 h +11 h +4 h +1818 m +1819 m +31 h +185 h +4 h +1820 m +4 h +1 h +1821 m +4 h +73 h +1261 h +124 h +1822 m +10 h +4 h +1823 m +25 h +10 h +1824 m +4 h +1825 m +578 m +10 h +109 h +59 h +186 m +10 h +1826 m +4 h +1 h +1827 m +1470 m +83 h +4 h +10 h +1 h +41 h +447 h +1828 m +4 h +1 h +10 h +10 h +22 h +4 h +11 h +1829 m +4 h +106 m +4 h +1830 m +10 h +1831 m +10 h +322 m +4 h +278 h +4 h +109 h +869 m +1 h +1832 m +1833 m +4 h +1 h +25 h +1834 m +1 h +143 h +4 h +10 h +1 h +4 h +1 h +1835 m +1836 m +56 h +1 h +1837 m +4 h +307 h +4 h +1 h +1 h +1838 m +986 m +4 h +1 h +1 h +1 h +185 h +1 h +4 h +157 h +1839 m +4 h +4 h +4 h +92 h +4 h +1840 m +1841 m +10 h +109 h +4 h +1842 m +443 h +1843 m +109 h +1 h +4 h +4 h +1 h +1844 m +31 h +10 h +1 h +124 h +59 h +1 h +4 h +4 h +4 h +3 h +10 h +4 h +1 h +1 h +10 h +4 h +10 h +25 h +4 h +359 h +1 h +83 h +1845 m +144 h +4 h +4 h +386 h +4 h +1846 m +135 h +1847 m +40 h +158 h +82 h +1 h +82 h +4 h +1848 m +4 h +56 h +1849 m +1 h +10 h +10 h +1850 m +10 h +1127 m +4 h +1851 m +74 h +1 h +1852 m +1853 m +1854 m +1855 m +4 h +11 h +1856 m +1857 m +31 h +1 h +4 h +295 h +1403 m +10 h +4 h +57 h +1030 m +1 h +104 h +1 h +1858 m +173 h +1859 m +82 h +1 h +1860 m +1030 h +1 h +15 m +112 h +8 h +185 h +1482 m +4 h +11 h +1861 m +10 h +4 h +10 h +4 h +1 h +4 h +10 h +328 m +4 h +10 h +976 h +4 h +1862 m +4 h +10 h +4 h +1863 m +1 h +4 h +1864 m +468 m +1 h +1865 m +1089 m +4 h +10 h +1 h +10 h +11 h +1866 m +10 h +114 h +1 h +1867 m +1409 m +10 h +4 h +1868 m +31 h +129 h +4 h +1869 m +1870 m +1 h +143 h +124 h +1871 m +1872 m +1873 m +908 m +1796 m +4 h +4 h +113 h +10 h +1874 m +1875 m +10 h +31 h +10 h +230 m +4 h +10 h +1876 m +1 h +124 h +1877 m +4 h +4 h +1 h +10 h +59 h +1205 m +935 m +1 h +143 h +10 h +10 h +1878 m +4 h +1879 m +1880 m +69 h +10 h +55 h +1 h +1 h +4 h +1 h +10 h +4 h +1881 m +4 h +4 h +4 h +85 m +901 m +1882 m +1 h +1883 m +1 h +4 h +10 h +11 h +1 h +4 h +57 h +569 h +1884 m +4 h +3 h +4 h +1 h +1885 m +124 h +4 h +520 m +1886 m +1 h +1887 m +1888 m +3 h +82 h +31 h +4 h +10 h +10 h +4 h +4 h +1 h +109 h +1 h +1889 m +616 m +4 h +185 h +4 h +1 h +56 h +10 h +1 h +1890 m +10 h +860 m +332 h +4 h +4 h +297 h +1 h +57 h +1 h +156 h +649 m +109 h +10 h +1891 m +4 h +1892 m +31 h +11 h +10 h +36 h +1 h +10 h +4 h +1893 m +73 h +4 h +10 h +1 h +10 h +1894 m +1895 m +4 h +4 h +10 h +4 h +4 h +4 h +4 h +4 h +10 h +4 h +238 h +1896 m +1 h +1897 m +1898 m +10 h +3 h +13 h +4 h +1 h +1899 m +4 h +4 h +1 h +64 h +10 h +10 h +278 h +4 h +4 h +4 h +10 h +12 h +1900 m +79 h +11 h +4 h +1250 h +10 h +1901 m +1902 m +3 h +1903 m +92 h +4 h +307 h +626 m +11 h +1 h +1 h +1904 m +164 h +27 h +1905 m +4 h +1389 m +4 h +4 h +1 h +25 h +10 h +10 h +1 h +4 h +10 h +1 h +538 h +1 h +307 h +11 h +1906 m +1 h +1 h +13 h +82 h +4 h +1 h +69 h +1907 m +3 h +1908 m +4 h +4 h +94 h +1909 m +1910 m +83 h +10 h +371 h +1911 m +10 h +4 h +1912 m +1913 m +1914 m +278 h +10 h +371 h +1772 m +83 h +4 h +11 h +82 h +1 h +230 m +8 h +1 h +4 h +1915 m +10 h +10 h +1916 m +1917 m +1918 m +4 h +1919 m +4 h +1920 m +4 h +4 h +359 h +1321 m +4 h +1 h +11 h +1 h +146 h +4 h +11 h +25 h +4 h +1 h +1921 m +1 h +4 h +1922 m +10 h +46 m +1923 m +4 h +1924 m +1470 m +1925 m +241 h +1926 m +1 h +4 h +4 h +8 h +10 h +10 h +124 h +172 h +1927 m +1 h +4 h +10 h +4 h +55 h +1 h +1 h +4 h +4 h +4 h +1928 m +1 h +56 h +4 h +10 h +27 h +4 h +4 h +59 h +1 h +11 h +1 h +31 h +146 h +4 h +1929 m +109 h +1 h +4 h +250 h +10 h +4 h +11 h +143 h +10 h +10 h +1930 m +10 h +4 h +79 h +1619 m +4 h +31 h +569 h +4 h +124 h +4 h +11 h +169 h +4 h +167 h +1 h +4 h +1931 m +399 h +4 h +266 h +4 h +1 h +1932 m +1780 m +10 h +1933 m +196 h +1934 m +1 h +1935 m +4 h +10 h +1936 m +1937 m +1 h +1 h +1 h +10 h +1938 m +1939 m +25 h +41 h +4 h +1 h +10 h +146 h +31 h +59 h +1940 m +1941 m +10 h +4 h +1942 m +4 h +1 h +1 h +4 h +313 m +4 h +1 h +83 h +10 h +1943 m +114 h +278 h +1944 m +1 h +1945 m +12 h +57 h +4 h +143 h +4 h +196 h +10 h +10 h +4 h +82 h +10 h +1946 m +1947 m +10 h +1 h +10 h +10 h +1 h +92 h +190 h +11 h +1 h +11 h +4 h +258 h +1948 m +185 h +92 h +1949 m +299 h +1950 m +41 h +1 h +4 h +1 h +11 h +10 h +1951 m +125 h +1260 m +10 h +124 h +1 h +1 h +12 h +10 h +186 h +1 h +1 h +1952 m +10 h +7 m +4 h +4 h +4 h +1953 m +4 h +4 h +4 h +238 h +82 h +82 h +1 h +1954 m +10 h +4 h +1 h +4 h +266 h +10 h +1955 m +1 h +1956 m +173 h +1 h +1 h +1027 h +1957 m +443 h +10 h +11 h +1958 m +4 h +164 h +386 h +4 h +1 h +1959 m +57 h +4 h +4 h +82 h +27 h +1960 m +1961 m +4 h +40 h +10 h +1962 m +319 m +10 h +3 h +10 h +10 h +11 h +1963 m +1964 m +1 h +10 h +3 h +270 h +10 h +10 h +1 h +109 h +4 h +113 h +307 h +1780 h +1 h +4 h +1965 m +1966 m +986 m +10 h +4 h +4 h +1967 m +1968 m +124 h +3 h +1969 m +1 h +4 h +87 m +4 h +1 h +1 h +65 h +1970 m +146 h +104 h +1632 m +4 h +10 h +4 h +125 h +83 h +109 h +1 h +4 h +124 h +10 h +124 h +11 h +10 h +1 h +4 h +4 h +1971 m +10 h +4 h +59 h +3 h +1 h +1972 m +10 h +139 h +195 h +10 h +1 h +1973 m +1 h +10 h +135 h +1 h +1 h +1 h +119 h +146 h +4 h +1 h +1974 m +1281 m +10 h +4 h +1975 m +1976 m +1977 m +110 h +146 h +1978 m +1 h +204 h +1 h +4 h +1979 m +124 h +1980 m +10 h +888 m +1 h +1835 m +4 h +10 h +94 h +109 h +10 h +4 h +1 h +4 h +10 h +4 h +4 h +10 h +1 h +1 h +4 h +238 h +1981 m +3 h +4 h +1982 m +11 h +10 h +1 h +4 h +41 h +1 h +1 h +1983 m +1 h +1 h +31 h +4 h +10 h +4 h +1 h +1 h +1 h +10 h +266 h +10 h +1714 m +147 h +4 h +1984 m +140 h +11 h +1985 m +4 h +59 h +1986 m +10 h +1987 m +4 h +173 h +104 h +10 h +73 h +4 h +57 h +4 h +1 h +82 h +10 h +1 h +1988 m +40 h +1 h +4 h +25 h +1989 m +307 h +4 h +1 h +295 h +1137 h +1 h +77 h +3 h +124 h +295 h +1990 m +10 h +4 h +265 h +11 h +12 h +190 h +1991 m +1992 m +4 h +1 h +59 h +1362 h +10 h +57 h +190 h +1 h +1 h +1993 m +1994 m +1 h +536 h +386 h +10 h +10 h +10 h +1 h +1995 m +4 h +13 h +172 h +276 h +1 h +4 h +1 h +692 h +10 h +10 h +1996 m +4 h +1997 m +10 h +4 h +1998 m +1999 m +2000 m +4 h +10 h +82 h +1 h +2001 m +4 h +147 h +1 h +2002 m +109 h +10 h +4 h +11 h +104 h +238 h +10 h +1 h +1 h +125 h +195 h +36 h +4 h +547 m +4 h +4 h +10 h +164 h +59 h +278 h +2003 m +2004 m +1 h +45 h +170 h +79 h +125 h +10 h +10 h +1478 h +1 h +2005 m +1 h +185 h +1 h +36 h +10 h +10 h +4 h +10 h +10 h +2006 m +57 h +2007 m +41 h +1 h +2008 m +2009 m +1 h +79 h +1 h +2010 m +124 h +536 h +97 h +2011 m +11 h +1 h +4 h +1 h +250 h +74 h +1 h +4 h +36 h +140 h +204 h +10 h +4 h +1 h +1403 m +1 h +10 h +4 h +330 h +10 h +1 h +1316 m +4 h +4 h +1 h +4 h +195 h +82 h +25 h +2012 m +307 h +11 h +2013 m +196 h +59 h +31 h +4 h +25 h +10 h +2014 m +1016 h +2015 m +2016 m +935 m +4 h +4 h +110 h +104 h +692 h +56 h +10 h +109 h +4 h +10 h +2017 m +2018 m +4 h +1 h +4 h +2019 m +2020 m +4 h +258 h +1 h +1 h +2021 m +10 h +13 h +1 h +172 h +4 h +196 h +4 h +10 h +59 h +10 h +2022 m +164 h +79 h +4 h +2023 m +4 h +2024 m +10 h +4 h +31 h +55 h +10 h +2025 m +97 h +278 h +4 h +4 h +1 h +10 h +41 h +2026 m +2027 m +1409 m +83 h +4 h +4 h +1 h +59 h +4 h +2028 m +10 h +520 m +2029 m +4 h +2030 m +2031 m +966 m +25 h +45 h +2032 m +4 h +74 h +4 h +83 h +2033 m +2034 m +331 m +1 h +4 h +57 h +2035 m +4 h +97 h +104 h +2036 m +1 h +31 h +278 h +139 h +241 h +4 h +2037 m +170 h +2038 m +2039 m +1 h +1 h +986 h +2040 m +1 h +83 h +4 h +2041 m +1 h +4 h +4 h +2042 m +1 h +2043 m +65 h +1 h +109 h +2044 m +1 h +10 h +10 h +1 h +2045 m +1 h +83 h +10 h +4 h +1 h +157 h +4 h +2046 m +1 h +124 h +10 h +11 h +4 h +2047 m +4 h +57 h +1 h +1 h +2048 m +2049 m +1 h +2050 m +4 h +1 h +83 h +25 h +1650 m +2051 m +1096 h +4 h +1 h +2052 m +11 h +10 h +1 h +2053 m +83 h +1 h +2054 m +1 h +1 h +4 h +10 h +2055 m +2056 m +2057 m +2058 m +2059 m +1 h +45 h +2060 m +123 h +2061 m +1607 m +104 h +4 h +1 h +4 h +1 h +4 h +1 h +169 h +1 h +79 h +1 h +258 h +124 h +11 h +82 h +2062 m +2063 m +1 h +31 h +1 h +1 h +1 h +1 h +10 h +1 h +3 h +331 m +4 h +1321 h +1 h +10 h +4 h +4 h +1 h +41 h +3 h +1 h +2064 m +4 h +4 h +2065 m +25 h +10 h +1 h +2066 m +3 h +57 h +33 m +22 h +4 h +124 h +1 h +4 h +10 h +41 h +10 h +295 h +1 h +2067 m +10 h +11 h +92 h +1016 h +2068 m +83 h +25 h +1 h +2069 m +224 h +157 h +12 h +2070 m +986 h +656 m +10 h +4 h +41 h +1 h +2071 m +1 h +109 h +238 h +204 h +2072 m +1074 m +4 h +1 h +2073 m +1 h +4 h +2074 m +4 h +82 h +59 h +2075 m +2076 m +2077 m +11 h +1 h +82 h +4 h +1 h +10 h +358 h +4 h +83 h +10 h +4 h +4 h +45 h +4 h +110 h +2078 m +1 h +25 h +4 h +2079 m +125 h +11 h +1 h +10 h +83 h +94 h +25 h +4 h +124 h +2080 m +10 h +2081 m +1 h +4 h +1 h +4 h +59 h +109 h +1 h +4 h +10 h +4 h +2082 m +1 h +2083 m +1 h +157 h +2084 m +10 h +4 h +55 h +2085 m +1 h +4 h +10 h +2086 m +4 h +10 h +4 h +4 h +986 h +2087 m +65 h +2088 m +10 h +172 h +10 h +4 h +1 h +59 h +2089 m +79 h +4 h +109 h +2090 m +2091 m +1 h +173 h +4 h +4 h +11 h +1 h +4 h +10 h +8 h +4 h +2092 m +4 h +4 h +2093 m +1 h +164 h +4 h +4 h +4 h +4 h +1 h +146 h +57 h +57 h +4 h +4 h +10 h +1128 m +172 h +2094 m +2095 m +10 h +2096 m +276 h +10 h +266 h +124 h +56 h +4 h +4 h +1 h +704 m +4 h +1 h +124 h +2097 m +10 h +10 h +25 h +4 h +4 h +103 m +114 h +10 h +10 h +2098 m +4 h +2099 m +2100 m +10 h +359 h +1 h +2101 m +22 h +143 h +1 h +4 h +4 h +1293 m +1 h +1 h +4 h +4 h +2102 m +262 h +57 h +192 h +172 h +4 h +1 h +2103 m +172 h +258 h +4 h +10 h +2104 m +4 h +4 h +3 h +4 h +250 h +2105 m +1 h +1 h +1 h +2106 m +144 h +1 h +112 h +297 h +2107 m +196 h +4 h +1 h +2108 m +4 h +83 h +1016 h +4 h +4 h +1 h +1 h +1 h +1 h +4 h +1766 m +2109 m +1 h +4 h +541 h +1 h +55 h +2110 m +185 h +105 h +10 h +31 h +2111 m +83 h +1 h +4 h +4 h +1089 m +4 h +2112 m +2113 m +4 h +2114 m +4 h +10 h +2115 m +4 h +4 h +10 h +1 h +2116 m +119 h +2117 m +4 h +4 h +2118 m +10 h +10 h +1 h +11 h +2119 m +4 h +2120 m +4 h +10 h +4 h +4 h +2121 m +1 h +1 h +4 h +74 h +2122 m +2123 m +79 h +4 h +2124 m +4 h +4 h +172 h +264 m +4 h +1 h +10 h +1 h +109 h +2125 m +4 h +1 h +258 h +4 h +196 h +4 h +10 h +4 h +1 h +10 h +104 h +4 h +1 h +82 h +1 h +4 h +2126 m +4 h +125 h +2127 m +2128 m +2129 m +172 h +135 h +2130 m +224 h +4 h +1 h +113 h +1 h +4 h +4 h +10 h +4 h +2131 m +10 h +10 h +55 h +2132 m +1 h +2133 m +4 h +2134 m +1 h +4 h +2135 m +146 h +110 h +2136 m +25 h +266 h +92 h +4 h +10 h +59 h +2137 m +10 h +2138 m +4 h +2139 m +2140 m +79 h +2141 m +4 h +4 h +106 h +4 h +2142 m +109 h +10 h +939 h +10 h +2143 m +4 h +41 h +4 h +4 h +125 h +1957 m +4 h +4 h +383 h +2144 m +842 m +1 h +265 h +10 h +1 h +10 h +2145 m +2146 m +1 h +59 h +1 h +2147 m +4 h +4 h +4 h +1 h +74 h +4 h +4 h +1 h +156 h +4 h +55 h +4 h +83 h +2148 m +1 h +2149 m +2150 m +172 h +2151 m +10 h +4 h +4 h +2152 m +1 h +265 h +2153 m +2154 m +4 h +59 h +1 h +1 h +2155 m +2156 m +11 h +4 h +10 h +169 h +10 h +4 h +4 h +4 h +1137 h +1 h +2157 m +4 h +1137 h +181 h +4 h +10 h +59 h +1 h +4 h +4 h +10 h +1 h +4 h +1 h +770 m +4 h +2158 m +4 h +104 h +55 h +1 h +258 h +4 h +2159 m +2160 m +4 h +10 h +4 h +2161 m +185 h +10 h +4 h +4 h +1 h +28 h +371 h +224 h +4 h +4 h +1 h +119 h +358 h +10 h +74 h +2162 m +10 h +4 h +11 h +10 h +10 h +332 h +57 h +238 h +238 h +2163 m +4 h +4 h +4 h +4 h +1 h +10 h +1 h +4 h +2164 m +10 h +4 h +2165 m +938 m +2166 m +4 h +11 h +4 h +1 h +41 h +256 m +224 h +4 h +2167 m +4 h +2168 m +2169 m +1 h +1 h +1796 h +4 h +2170 m +2171 m +4 h +4 h +4 h +2172 m +1 h +1 h +4 h +2173 m +4 h +307 h +2174 m +1137 h +1642 m +2175 m +10 h +1 h +196 h +25 h +361 m +10 h +4 h +2176 m +2177 m +36 h +2178 m +11 h +2179 m +11 h +2180 m +4 h +4 h +94 h +447 h +4 h +4 h +2181 m +45 h +2182 m +11 h +2183 m +4 h +1 h +31 h +59 h +10 h +124 h +4 h +1 h +196 h +4 h +1635 m +146 h +1309 h +2184 m +1 h +1535 h +2185 m +174 h +11 h +1 h +4 h +1 h +1 h +270 h +2186 m +82 h +10 h +1 h +84 m +403 h +2187 m +2188 m +2189 m +1 h +10 h +704 m +4 h +4 h +11 h +2190 m +2191 m +238 h +10 h +4 h +195 h +4 h +3 h +258 h +109 h +2192 m +4 h +2193 m +1 h +2194 m +4 h +1 h +4 h +4 h +536 h +2195 m +2196 m +4 h +4 h +10 h +57 h +57 h +2197 m +2198 m +10 h +57 h +1 h +10 h +69 h +2199 m +31 h +10 h +2200 m +2201 m +1 h +2202 m +1 h +27 h +1 h +976 h +1 h +114 h +279 h +82 h +4 h +1 h +2203 m +10 h +2204 m +1 h +2205 m +10 h +109 h +74 h +4 h +57 h +4 h +104 h +10 h +1 h +4 h +1 h +1559 m +4 h +4 h +1 h +2206 m +1 h +332 h +2207 m +224 h +2208 m +4 h +1 h +358 h +3 h +123 h +185 h +2209 m +2210 m +1 h +174 h +2211 m +10 h +2212 m +1 h +1 h +4 h +2213 m +1137 h +1 h +4 h +41 h +4 h +4 h +4 h +2214 m +10 h +186 h +2215 m +1 h +10 h +2216 m +109 h +1 h +10 h +2217 m +4 h +59 h +13 h +2218 m +1 h +2219 m +1 h +57 h +109 h +10 h +119 h +10 h +1 h +941 m +2148 m +1 h +1 h +4 h +31 h +443 h +10 h +109 h +2220 m +10 h +10 h +2221 m +82 h +2222 m +4 h +4 h +258 h +2223 m +97 h +4 h +3 h +1 h +1 h +2224 m +4 h +10 h +1 h +10 h +1 h +1 h +1 h +31 h +258 h +2225 m +174 h +1 h +10 h +4 h +1 h +4 h +135 h +1 h +2226 m +2227 m +84 m +195 h +1 h +4 h +55 h +4 h +10 h +25 h +119 h +4 h +10 h +64 h +2228 m +2229 m +1 h +4 h +4 h +2172 m +757 h +4 h +4 h +4 h +10 h +2230 m +262 h +2231 m +11 h +46 m +10 h +2232 m +4 h +3 h +276 h +41 h +4 h +114 h +1 h +1 h +4 h +1 h +383 h +190 h +25 h +1 h +1 h +1 h +1981 m +587 m +97 h +110 h +109 h +10 h +2233 m +10 h +4 h +403 h +2234 m +278 h +4 h +4 h +2235 m +146 h +2236 m +1 h +1 h +1 h +10 h +397 m +11 h +4 h +41 h +10 h +367 h +1 h +4 h +2237 m +1 h +196 h +1 h +10 h +250 h +1 h +82 h +65 h +3 h +4 h +83 h +1 h +403 h +4 h +1 h +59 h +2238 m +10 h +25 h +1 h +4 h +4 h +1 h +1 h +2239 m +2240 m +267 m +10 h +1 h +4 h +297 h +8 h +1 h +4 h +65 h +229 h +64 h +692 h +1 h +2241 m +2242 m +10 h +4 h +4 h +2243 m +10 h +2244 m +1 h +2245 m +258 h +4 h +569 h +1 h +192 h +10 h +4 h +195 h +31 h +1 h +4 h +1 h +2246 m +11 h +10 h +1 h +4 h +2247 m +2248 m +4 h +4 h +2249 m +1 h +2250 m +10 h +135 h +2251 m +4 h +4 h +11 h +2252 m +2253 m +10 h +4 h +4 h +143 h +2254 m +57 h +1 h +124 h +13 h +1 h +3 h +447 h +11 h +1 h +41 h +10 h +1 h +4 h +2255 m +1 h +1 h +2256 m +4 h +11 h +73 h +97 h +172 h +4 h +4 h +4 h +11 h +332 h +1403 h +2257 m +2258 m +1024 m +83 h +109 h +368 h +1 h +289 h +69 h +11 h +109 h +2259 m +113 h +2260 m +11 h +1 h +4 h +4 h +4 h +297 h +976 h +10 h +1 h +10 h +59 h +82 h +4 h +1 h +4 h +190 h +2261 m +4 h +4 h +4 h +1868 m +2262 m +4 h +1 h +8 h +4 h +258 h +2263 m +2264 m +10 h +4 h +4 h +2265 m +4 h +4 h +11 h +124 h +10 h +10 h +2266 m +2267 m +1 h +10 h +10 h +1 h +8 h +3 h +230 h +196 h +4 h +10 h +2268 m +2269 m +4 h +2270 m +4 h +4 h +4 h +10 h +2271 m +1 h +4 h +10 h +41 h +10 h +10 h +4 h +4 h +10 h +125 h +322 m +4 h +104 h +2272 m +2273 m +65 h +74 h +2274 m +1 h +4 h +2275 m +2276 m +2277 m +11 h +4 h +57 h +11 h +1 h +10 h +10 h +4 h +10 h +1406 h +4 h +28 h +1 h +10 h +2278 m +1938 m +109 h +2279 m +2280 m +2281 m +10 h +1 h +4 h +2282 m +1 h +4 h +36 h +2283 m +1 h +4 h +2284 m +91 h +4 h +2285 m +4 h +1 h +1 h +11 h +1 h +10 h +2286 m +4 h +359 h +2287 m +97 h +10 h +1 h +4 h +1 h +2288 m +582 m +1027 h +4 h +4 h +250 h +2289 m +164 h +4 h +250 h +2290 m +2291 m +1 h +2292 m +10 h +1 h +297 h +1 h +1 h +1 h +4 h +2293 m +109 h +4 h +4 h +4 h +2294 m +10 h +83 h +2295 m +2257 m +4 h +10 h +3 h +82 h +2296 m +4 h +2297 m +11 h +3 h +279 h +4 h +3 h +65 h +4 h +10 h +4 h +123 h +2298 m +1 h +4 h +4 h +167 h +4 h +4 h +1 h +4 h +157 h +2299 m +4 h +83 h +10 h +4 h +10 h +2300 m +74 h +11 h +295 h +1 h +4 h +1 h +10 h +2301 m +10 h +1 h +4 h +1 h +1 h +1 h +4 h +10 h +1 h +1 h +4 h +56 h +10 h +4 h +386 h +4 h +353 h +1 h +10 h +1 h +4 h +2302 m +1 h +307 h +4 h +2303 m +1 h +2124 m +10 h +1985 m +10 h +10 h +4 h +4 h +2304 m +124 h +359 h +2305 m +10 h +59 h +4 h +119 h +139 h +692 h +4 h +1 h +2306 m +10 h +4 h +4 h +4 h +2307 m +10 h +278 h +1 h +10 h +332 h +31 h +97 h +1 h +4 h +10 h +1016 h +82 h +114 h +4 h +10 h +276 h +4 h +307 h +1 h +2308 m +4 h +4 h +10 h +170 h +1 h +1 h +4 h +4 h +11 h +332 h +83 h +4 h +1 h +2309 m +10 h +59 h +74 h +2310 m +10 h +1220 m +2311 m +4 h +1 h +25 h +2312 m +4 h +2313 m +4 h +4 h +2314 m +11 h +1 h +4 h +2315 m +2316 m +4 h +241 h +4 h +1 h +2317 m +79 h +297 h +4 h +2318 m +2319 m +11 h +1 h +4 h +2320 m +10 h +4 h +10 h +2321 m +2322 m +1 h +2323 m +1 h +11 h +2324 m +1 h +124 h +4 h +10 h +79 h +4 h +2325 m +65 h +2326 m +4 h +2327 m +718 h +2328 m +2329 m +57 h +2330 m +4 h +10 h +2331 m +299 h +4 h +477 m +94 h +11 h +10 h +45 h +10 h +976 h +2332 m +109 h +2333 m +10 h +11 h +3 h +2334 m +4 h +4 h +1 h +147 h +109 h +41 h +11 h +10 h +4 h +2335 m +2336 m +4 h +4 h +11 h +2337 m +2338 m +94 h +13 h +1 h +1 h +1 h +4 h +4 h +195 h +1261 h +10 h +10 h +1 h +1 h +2339 m +11 h +4 h +4 h +10 h +4 h +1 h +4 h +1 h +169 h +2340 m +94 h +4 h +82 h +2341 m +536 h +41 h +274 h +184 m +11 h +1 h +10 h +2342 m +11 h +2343 m +10 h +93 m +3 h +4 h +10 h +83 h +2344 m +25 h +11 h +2345 m +1 h +2346 m +2347 m +4 h +4 h +135 h +2348 m +4 h +1 h +4 h +2349 m +4 h +10 h +4 h +10 h +802 m +10 h +1 h +12 h +10 h +65 h +4 h +2350 m +4 h +2351 m +10 h +11 h +278 h +10 h +2352 m +1 h +4 h +10 h +174 h +2353 m +2354 m +238 h +1766 m +64 h +1 h +4 h +2355 m +4 h +2356 m +1 h +2357 m +2358 m +857 h +368 h +434 h +10 h +11 h +65 h +1 h +241 h +4 h +1 h +3 h +2359 m +1 h +8 h +12 h +779 m +4 h +10 h +94 h +1 h +10 h +2360 m +25 h +1 h +1 h +2361 m +2362 m +2363 m +4 h +4 h +412 m +93 m +1 h +2364 m +1 h +2365 m +1 h +10 h +4 h +1 h +4 h +31 h +4 h +2366 m +82 h +10 h +2367 m +124 h +2368 m +22 h +238 h +185 h +1 h +10 h +4 h +1627 m +10 h +10 h +1 h +3 h +4 h +4 h +4 h +4 h +172 h +2369 m +4 h +4 h +2370 m +10 h +1 h +1 h +2371 m +10 h +4 h +4 h +2372 m +2373 m +4 h +1 h +2374 m +1 h +3 h +1 h +1 h +11 h +156 h +10 h +4 h +1 h +204 h +146 h +857 h +10 h +3 h +4 h +1 h +4 h +74 h +59 h +59 h +4 h +1 h +4 h +25 h +2375 m +196 h +1 h +4 h +4 h +41 h +2376 m +330 h +11 h +1030 h +1 h +2377 m +104 h +10 h +2378 m +4 h +10 h +4 h +4 h +1 h +2379 m +4 h +172 h +4 h +1 h +1 h +4 h +4 h +4 h +4 h +12 h +167 h +2380 m +2381 m +2382 m +22 h +4 h +2383 m +1 h +1 h +1 h +2384 m +4 h +1 h +125 h +2385 m +2386 m +185 h +1 h +195 h +10 h +2387 m +262 h +1 h +1 h +1 h +2388 m +2389 m +4 h +1 h +2390 m +10 h +4 h +10 h +1 h +10 h +1 h +2391 m +11 h +4 h +146 h +11 h +119 h +4 h +10 h +2392 m +4 h +94 h +146 h +1 h +2393 m +4 h +2394 m +2395 m +2396 m +2397 m +192 h +4 h +1 h +2398 m +1 h +1 h +4 h +4 h +41 h +65 h +730 m +2399 m +2400 m +1250 h +2401 m +135 h +403 h +1 h +1 h +1 h +10 h +3 h +2402 m +2403 m +1128 m +11 h +69 h +196 h +110 h +1952 m +4 h +1 h +1 h +1 h +2404 m +4 h +2405 m +10 h +10 h +1 h +164 h +110 h +2406 m +83 h +10 h +2407 m +276 h +1 h +1 h +11 h +351 m +125 h +192 h +196 h +996 m +2408 m +2409 m +4 h +3 h +278 h +434 h +2410 m +4 h +4 h +10 h +1 h +4 h +10 h +4 h +2411 m +2412 m +2413 m +4 h +1 h +1 h +2414 m +258 h +83 h +204 h +1 h +82 h +2415 m +74 h +124 h +10 h +1 h +97 h +2416 m +2417 m +4 h +520 h +583 m +1 h +4 h +1 h +1 h +4 h +2418 m +4 h +59 h +2419 m +4 h +2420 m +2421 m +1 h +1 h +97 h +2422 m +2172 h +2423 m +79 h +4 h +2424 m +82 h +2425 m +41 h +10 h +2426 m +1 h +1 h +1 h +2427 m +630 m +124 h +10 h +2428 m +11 h +2429 m +1 h +4 h +2430 m +181 h +45 h +10 h +2431 m +22 h +10 h +1 h +1 h +11 h +4 h +1 h +2432 m +83 h +10 h +2433 m +4 h +4 h +4 h +10 h +1083 m +1 h +195 h +2434 m +2435 m +4 h +147 h +192 h +4 h +4 h +10 h +10 h +1 h +2436 m +1 h +1 h +41 h +4 h +1 h +2437 m +83 h +2438 m +1089 m +4 h +2439 m +4 h +4 h +10 h +2440 m +4 h +3 h +11 h +2441 m +4 h +170 h +1 h +109 h +91 h +31 h +4 h +65 h +2442 m +87 m +285 m +2443 m +2444 m +4 h +4 h +146 h +10 h +46 m +737 h +170 h +1 h +146 h +10 h +124 h +10 h +10 h +3 h +1027 h +10 h +2445 m +74 h +135 h +3 h +2446 m +4 h +1 h +1 h +4 h +808 m +10 h +2041 m +1481 m +4 h +83 h +83 h +2447 m +4 h +1 h +1 h +4 h +1 h +2448 m +10 h +57 h +2449 m +10 h +1 h +282 h +3 h +4 h +74 h +2450 m +4 h +1 h +167 h +25 h +1880 m +604 m +2451 m +2452 m +4 h +41 h +2453 m +3 h +147 h +83 h +4 h +4 h +2454 m +169 h +10 h +2455 m +1 h +55 h +11 h +1884 m +1 h +4 h +190 h +4 h +4 h +82 h +1 h +2456 m +3 h +4 h +2457 m +10 h +295 h +1 h +65 h +4 h +2458 m +258 h +4 h +4 h +109 h +2459 m +1 h +45 h +2460 m +4 h +1 h +2461 m +195 h +10 h +1 h +4 h +4 h +1 h +2462 m +4 h +25 h +2463 m +1 h +2464 m +4 h +2465 m +1448 m +2466 m +4 h +1 h +2467 m +1 h +1 h +4 h +2468 m +4 h +195 h +1 h +1650 m +1 h +1137 h +1 h +10 h +1 h +10 h +10 h +4 h +1 h +4 h +4 h +1 h +4 h +4 h +10 h +1 h +2469 m +4 h +4 h +581 m +195 h +2470 m +4 h +10 h +2471 m +82 h +2472 m +4 h +278 h +25 h +911 m +36 h +1 h +2473 m +2474 m +4 h +4 h +757 h +1 h +25 h +4 h +1 h +184 m +41 h +1 h +11 h +11 h +129 h +1 h +1 h +4 h +794 m +10 h +2475 m +4 h +2476 m +83 h +4 h +805 m +1 h +359 h +2477 m +2478 m +10 h +2479 m +4 h +4 h +2480 m +10 h +2481 m +59 h +4 h +10 h +4 h +10 h +3 h +11 h +10 h +3 h +1 h +2482 m +1 h +4 h +195 h +4 h +10 h +2483 m +2484 m +10 h +41 h +109 h +10 h +4 h +1 h +13 h +97 h +94 h +4 h +13 h +4 h +10 h +4 h +976 h +4 h +4 h +1 h +1 h +4 h +4 h +4 h +1 h +538 h +2485 m +109 h +10 h +10 h +10 h +1 h +4 h +79 h +2486 m +1 h +204 h +144 h +3 h +2487 m +10 h +31 h +1 h +92 h +10 h +10 h +59 h +135 h +4 h +65 h +45 h +1 h +4 h +4 h +65 h +28 h +4 h +1 h +10 h +10 h +4 h +1 h +140 h +2488 m +1205 m +10 h +82 h +156 h +3 h +1646 m +935 h +1 h +1 h +10 h +481 m +1 h +2489 m +2490 m +10 h +1 h +1 h +4 h +4 h +4 h +1 h +2491 m +4 h +109 h +10 h +31 h +4 h +2492 m +1 h +1 h +805 m +4 h +4 h +2493 m +3 h +4 h +4 h +2494 m +4 h +2444 m +55 h +31 h +2495 m +2496 m +1 h +129 h +11 h +4 h +2497 m +2498 m +4 h +4 h +4 h +4 h +4 h +12 h +4 h +1 h +2499 m +258 h +4 h +4 h +156 h +1766 m +295 h +258 h +55 h +82 h +4 h +79 h +4 h +4 h +195 h +2500 m +2501 m +2502 m +258 h +4 h +2503 m +4 h +4 h +124 h +4 h +109 h +2504 m +1 h +1308 m +170 h +4 h +1 h +4 h +164 h +4 h +4 h +1 h +1250 h +2505 m +74 h +4 h +31 h +109 h +10 h +31 h +4 h +2506 m +10 h +2507 m +147 h +10 h +1 h +2508 m +10 h +10 h +1 h +2509 m +170 h +195 h +109 h +4 h +1 h +1 h +2510 m +4 h +4 h +4 h +172 h +2511 m +4 h +1 h +258 h +1030 h +270 h +79 h +41 h +2512 m +109 h +4 h +1 h +110 h +4 h +73 h +10 h +57 h +41 h +10 h +4 h +4 h +2513 m +2514 m +4 h +56 h +1 h +2515 m +4 h +11 h +4 h +1 h +1 h +82 h +1 h +10 h +2516 m +1327 m +4 h +2517 m +1 h +55 h +1 h +1 h +83 h +2518 m +4 h +11 h +4 h +2519 m +4 h +1 h +10 h +10 h +4 h +74 h +1 h +11 h +1 h +1 h +1 h +2520 m +25 h +1 h +2521 m +2522 m +2523 m +55 h +1 h +2524 m +10 h +1 h +506 m +2525 m +82 h +4 h +10 h +1 h +57 h +4 h +10 h +2526 m +4 h +1 h +2527 m +114 h +10 h +10 h +10 h +2528 m +1 h +41 h +10 h +2529 m +119 h +4 h +10 h +1 h +1 h +1504 m +1 h +1738 m +10 h +2530 m +2531 m +55 h +4 h +4 h +1 h +266 h +10 h +1 h +4 h +2532 m +4 h +4 h +4 h +1359 h +4 h +4 h +4 h +195 h +4 h +181 h +4 h +4 h +119 h +4 h +11 h +57 h +10 h +1 h +1 h +332 h +11 h +4 h +408 m +57 h +45 h +1 h +1 h +4 h +124 h +3 h +4 h +4 h +10 h +2533 m +22 h +4 h +1 h +1 h +13 h +11 h +1 h +10 h +4 h +319 m +1 h +10 h +135 h +4 h +1 h +10 h +2534 m +1184 m +11 h +4 h +10 h +1 h +1 h +250 h +4 h +2535 m +1 h +110 h +4 h +2536 m +2537 m +2538 m +170 h +4 h +2539 m +4 h +1 h +1 h +172 h +4 h +569 h +2540 m +4 h +4 h +319 m +4 h +1 h +368 h +1 h +82 h +2541 m +4 h +3 h +1 h +143 h +83 h +4 h +10 h +2542 m +1 h +2543 m +4 h +1 h +10 h +10 h +4 h +4 h +1137 h +2544 m +11 h +4 h +2545 m +2546 m +2547 m +4 h +2548 m +82 h +8 h +4 h +83 h +59 h +10 h +10 h +2549 m +1 h +4 h +1 h +10 h +45 h +31 h +2550 m +1 h +4 h +11 h +1 h +1 h +4 h +25 h +4 h +1 h +1454 m +22 h +266 h +2551 m +1 h +2552 m +2553 m +2554 m +10 h +135 h +1 h +278 h +97 h +1 h +4 h +2555 m +113 h +493 h +196 h +59 h +10 h +195 h +563 m +109 h +10 h +10 h +794 m +2556 m +2557 m +430 m +425 m +41 h +4 h +4 h +4 h +2558 m +10 h +258 h +4 h +4 h +2559 m +4 h +1 h +4 h +11 h +2560 m +1 h +976 h +578 m +12 h +1780 h +2561 m +83 h +276 h +1 h +2562 m +10 h +79 h +28 h +4 h +2563 m +2564 m +4 h +10 h +10 h +94 h +143 h +1 h +2565 m +4 h +11 h +2566 m +4 h +56 h +11 h +1 h +2567 m +1016 h +2568 m +4 h +332 h +1 h +77 h +4 h +11 h +4 h +82 h +4 h +2569 m +238 h +4 h +1 h +4 h +4 h +4 h +278 h +11 h +4 h +1619 m +1 h +1 h +4 h +10 h +3 h +2570 m +2571 m +195 h +4 h +4 h +1 h +10 h +2572 m +119 h +1 h +1 h +1 h +31 h +12 h +4 h +2573 m +2574 m +10 h +10 h +4 h +65 h +276 h +2575 m +124 h +1 h +57 h +31 h +4 h +4 h +4 h +10 h +2576 m +41 h +2577 m +46 m +125 h +2578 m +1939 m +2579 m +2580 m +4 h +25 h +2581 m +2582 m +10 h +10 h +124 h +2583 m +3 h +164 h +10 h +4 h +2584 m +4 h +1 h +10 h +31 h +33 m +2585 m +97 h +4 h +4 h +4 h +808 m +4 h +2586 m +4 h +4 h +4 h +4 h +2587 m +4 h +1 h +2588 m +1 h +4 h +912 m +386 h +4 h +92 h +1 h +4 h +1 h +167 h +3 h +1 h +2589 m +1 h +2590 m +4 h +54 m +97 h +4 h +1 h +10 h +2591 m +74 h +10 h +1 h +2592 m +10 h +4 h +1 h +10 h +4 h +1 h +2593 m +704 h +4 h +57 h +2594 m +2265 m +2595 m +2596 m +109 h +104 h +2597 m +1 h +1 h +1 h +2598 m +31 h +1 h +1 h +4 h +1 h +4 h +146 h +57 h +4 h +2599 m +13 h +10 h +69 h +92 h +1 h +2600 m +4 h +139 h +4 h +4 h +4 h +57 h +10 h +4 h +279 h +2601 m +2602 m +1 h +1 h +11 h +10 h +4 h +2603 m +10 h +332 h +11 h +4 h +2604 m +2605 m +1 h +4 h +1 h +2606 m +4 h +2607 m +4 h +147 h +2608 m +295 h +1 h +2609 m +10 h +4 h +10 h +4 h +4 h +104 h +13 h +1 h +10 h +56 h +10 h +1 h +1 h +2610 m +1 h +2611 m +36 h +2612 m +1 h +59 h +4 h +4 h +4 h +4 h +10 h +195 h +297 h +4 h +125 h +10 h +1 h +2613 m +4 h +692 h +4 h +2614 m +1 h +114 h +1 h +265 h +1968 m +124 h +4 h +2615 m +358 h +124 h +4 h +2616 m +2617 m +2618 m +190 h +11 h +2619 m +316 m +1 h +2620 m +2621 m +10 h +1 h +10 h +10 h +913 m +11 h +262 h +25 h +2622 m +2623 m +25 h +139 h +10 h +83 h +169 h +109 h +1 h +1 h +4 h +1 h +1 h +4 h +146 h +377 m +143 h +10 h +2624 m +2625 m +2626 m +2627 m +1751 m +2628 m +103 m +140 h +4 h +31 h +74 h +10 h +83 h +1 h +196 h +1 h +4 h +1508 m +2629 m +10 h +10 h +238 h +10 h +229 h +11 h +1 h +57 h +4 h +57 h +4 h +2630 m +10 h +2631 m +4 h +4 h +13 h +2632 m +2633 m +386 h +41 h +10 h +104 h +125 h +1 h +2047 m +279 h +4 h +4 h +10 h +195 h +2634 m +1 h +4 h +4 h +11 h +109 h +173 h +10 h +4 h +4 h +10 h +12 h +2635 m +1 h +1 h +3 h +229 h +11 h +2636 m +1261 h +59 h +2637 m +2638 m +2639 m +92 h +1 h +4 h +4 h +4 h +258 h +1 h +1 h +4 h +10 h +146 h +2640 m +1 h +1 h +11 h +278 h +27 h +4 h +1089 m +41 h +14 m +10 h +1 h +230 h +74 h +2641 m +4 h +2642 m +4 h +10 h +2643 m +1 h +1 h +1 h +10 h +158 h +10 h +2644 m +1 h +25 h +4 h +10 h +10 h +1 h +73 h +1 h +113 h +2645 m +359 h +36 h +2646 m +10 h +1406 h +1 h +4 h +1 h +4 h +1 h +10 h +2647 m +2648 m +2649 m +250 h +2650 m +1 h +4 h +2651 m +2652 m +2653 m +297 h +10 h +13 h +1535 h +2654 m +10 h +4 h +1 h +10 h +4 h +2655 m +4 h +219 m +1 h +10 h +4 h +108 h +1 h +11 h +1 h +2656 m +1 h +140 h +2657 m +1 h +408 m +119 h +1 h +31 h +59 h +4 h +447 h +1 h +1 h +114 h +45 h +2658 m +82 h +31 h +4 h +4 h +2659 m +4 h +2660 m +94 h +1 h +4 h +4 h +2661 m +10 h +250 h +10 h +4 h +1003 h +2662 m +2663 m +28 h +79 h +4 h +368 h +10 h +601 h +1 h +4 h +10 h +1 h +2664 m +1 h +1322 m +169 h +4 h +4 h +1 h +83 h +4 h +170 h +10 h +10 h +10 h +65 h +10 h +4 h +1 h +2665 m +2666 m +10 h +10 h +4 h +1 h +4 h +2667 m +299 h +10 h +468 m +2668 m +10 h +250 h +2669 m +1 h +31 h +169 h +2670 m +4 h +11 h +2671 m +976 h +10 h +110 h +10 h +2672 m +4 h +2673 m +4 h +330 h +4 h +4 h +4 h +11 h +1 h +2674 m +2675 m +10 h +1493 m +92 h +10 h +11 h +1981 m +11 h +1 h +4 h +138 h +10 h +4 h +4 h +4 h +2676 m +10 h +109 h +1 h +118 h +83 h +4 h +258 h +4 h +4 h +10 h +1 h +59 h +1 h +1 h +2677 m +10 h +4 h +10 h +2678 m +4 h +114 h +1 h +2679 m +1045 m +2680 m +1 h +4 h +10 h +33 m +2681 m +2682 m +4 h +1482 m +2683 m +238 h +10 h +1120 m +10 h +1 h +2684 m +2685 m +195 h +22 h +4 h +124 h +36 h +4 h +10 h +2686 m +55 h +92 h +2687 m +4 h +125 h +4 h +2688 m +4 h +12 h +124 h +2689 m +4 h +57 h +4 h +4 h +3 h +1780 h +1 h +4 h +10 h +10 h +124 h +2690 m +10 h +2691 m +10 h +2692 m +4 h +4 h +488 m +119 h +322 m +2693 m +2694 m +3 h +10 h +144 h +123 h +4 h +1 h +10 h +2695 m +399 h +1403 h +3 h +10 h +1 h +4 h +4 h +1 h +1 h +10 h +692 h +1 h +31 h +1 h +4 h +1 h +2696 m +2697 m +55 h +1 h +195 h +2698 m +1 h +11 h +4 h +4 h +2699 m +477 m +2700 m +65 h +10 h +332 h +10 h +83 h +10 h +2701 m +976 h +238 h +4 h +1 h +2702 m +3 h +1 h +1 h +22 h +1 h +2703 m +2494 m +2704 m +4 h +2705 m +2706 m +2707 m +10 h +125 h +64 h +25 h +125 h +464 h +125 h +1 h +1 h +4 h +40 h +1 h +2708 m +2709 m +1 h +82 h +57 h +1 h +1 h +2710 m +182 m +1 h +386 h +377 m +1 h +2711 m +4 h +10 h +1 h +250 h +4 h +1 h +4 h +1 h +79 h +2712 m +2713 m +2714 m +4 h +4 h +2715 m +1685 m +10 h +4 h +10 h +10 h +2716 m +4 h +4 h +10 h +1 h +2717 m +4 h +3 h +1 h +82 h +10 h +1 h +1 h +41 h +10 h +41 h +10 h +2718 m +4 h +1 h +156 h +2719 m +4 h +1 h +27 h +1 h +2720 m +297 h +10 h +10 h +1184 m +11 h +10 h +2721 m +2722 m +4 h +4 h +2723 m +1 h +65 h +10 h +1 h +1 h +4 h +4 h +4 h +125 h +4 h +10 h +4 h +1 h +2724 m +383 h +4 h +10 h +2725 m +4 h +92 h +10 h +59 h +1 h +2726 m +4 h +11 h +10 h +4 h +1 h +4 h +4 h +4 h +10 h +692 h +1 h +4 h +2727 m +1 h +41 h +1 h +4 h +82 h +297 h +55 h +10 h +264 m +10 h +2728 m +10 h +4 h +1 h +4 h +10 h +4 h +359 h +2729 m +36 h +79 h +185 h +4 h +2730 m +4 h +266 h +94 h +2731 m +1 h +4 h +3 h +4 h +4 h +2732 m +2733 m +4 h +4 h +135 h +45 h +2734 m +1 h +2735 m +4 h +2736 m +123 h +488 m +4 h +10 h +1 h +4 h +27 h +1 h +258 h +2737 m +2738 m +10 h +55 h +83 h +4 h +10 h +2739 m +3 h +10 h +4 h +11 h +4 h +10 h +10 h +425 m +10 h +4 h +4 h +1016 h +687 h +2740 m +1249 m +4 h +4 h +2741 m +2742 m +1 h +2743 m +2744 m +146 h +2745 m +2746 m +4 h +1493 m +1 h +82 h +274 h +238 h +1 h +146 h +10 h +4 h +1 h +4 h +4 h +2747 m +4 h +265 h +2748 m +2749 m +2750 m +1 h +1 h +1780 h +4 h +2751 m +2752 m +4 h +147 h +4 h +2753 m +2754 m +966 m +10 h +2309 m +4 h +2755 m +10 h +2756 m +4 h +2757 m +1 h +123 h +2758 m +1 h +1 h +1 h +520 h +10 h +10 h +4 h +10 h +2759 m +230 h +1 h +4 h +4 h +10 h +1 h +4 h +4 h +4 h +11 h +4 h +167 h +1 h +10 h +158 h +2760 m +1 h +2688 m +4 h +57 h +2761 m +4 h +74 h +4 h +10 h +4 h +125 h +2762 m +468 m +1 h +2763 m +4 h +4 h +358 h +4 h +83 h +11 h +11 h +10 h +1 h +31 h +59 h +2764 m +2379 m +82 h +2765 m +1 h +2766 m +79 h +82 h +73 h +55 h +1 h +4 h +4 h +59 h +3 h +2767 m +31 h +2768 m +10 h +1 h +2769 m +83 h +10 h +10 h +1 h +2770 m +4 h +196 h +1370 m +185 h +238 h +2771 m +25 h +1 h +4 h +4 h +55 h +109 h +10 h +279 h +1470 h +4 h +10 h +4 h +10 h +1642 m +1 h +4 h +10 h +119 h +3 h +1406 h +114 h +2772 m +1 h +2773 m +10 h +4 h +996 m +124 h +601 h +4 h +4 h +45 h +59 h +11 h +4 h +10 h +4 h +123 h +110 h +2079 m +2774 m +1 h +1 h +1 h +2775 m +1 h +230 h +10 h +10 h +2776 m +2777 m +1 h +1 h +250 h +2778 m +1 h +10 h +2779 m +2780 m +55 h +4 h +2781 m +11 h +119 h +56 h +1 h +125 h +1 h +82 h +1 h +64 h +57 h +103 m +2782 m +169 h +4 h +167 h +108 h +59 h +2783 m +1 h +10 h +10 h +2784 m +10 h +4 h +82 h +2785 m +1 h +109 h +4 h +2786 m +79 h +2787 m +31 h +1 h +1 h +10 h +22 h +10 h +4 h +1 h +4 h +1409 h +4 h +2788 m +2789 m +4 h +1 h +1 h +1 h +10 h +2314 m +1 h +11 h +1 h +4 h +4 h +986 h +2790 m +1308 m +278 h +3 h +124 h +4 h +1 h +10 h +31 h +2791 m +10 h +109 h +1030 h +124 h +4 h +4 h +367 h +2792 m +25 h +135 h +4 h +82 h +2793 m +104 h +10 h +4 h +10 h +2794 m +2795 m +1 h +2374 m +4 h +10 h +1 h +4 h +2796 m +1 h +4 h +3 h +4 h +1 h +1409 h +1 h +1 h +112 h +124 h +4 h +1 h +383 h +2139 m +10 h +1 h +4 h +12 h +4 h +10 h +2797 m +1 h +358 h +1089 m +41 h +2798 m +55 h +2799 m +1299 m +1838 m +10 h +2800 m +1 h +4 h +2801 m +109 h +1 h +10 h +4 h +2802 m +2803 m +144 h +238 h +2804 m +2805 m +2806 m +570 m +10 h +2807 m +74 h +700 m +1 h +125 h +4 h +10 h +104 h +157 h +10 h +4 h +1 h +2808 m +2809 m +4 h +1 h +12 h +1595 m +2810 m +2811 m +1 h +4 h +64 h +1 h +3 h +65 h +94 h +146 h +2812 m +2813 m +2814 m +4 h +4 h +1 h +10 h +2815 m +4 h +386 h +1 h +1 h +4 h +93 h +2816 m +2817 m +368 h +3 h +10 h +10 h +4 h +278 h +4 h +2818 m +69 h +10 h +11 h +4 h +10 h +2819 m +1 h +4 h +10 h +1 h +443 h +10 h +4 h +2820 m +1089 h +4 h +4 h +1 h +4 h +113 h +2821 m +91 h +2822 m +10 h +1 h +41 h +1 h +4 h +4 h +1 h +64 h +10 h +57 h +443 h +2823 m +2824 m +1 h +10 h +10 h +1 h +11 h +262 h +4 h +4 h +2825 m +2826 m +2827 m +1 h +1 h +1 h +2828 m +123 h +1 h +4 h +2829 m +2830 m +2831 m +1 h +1 h +2832 m +92 h +10 h +1822 m +4 h +10 h +2833 m +2834 m +4 h +538 h +1 h +1 h +1 h +25 h +10 h +656 m +4 h +2835 m +146 h +1 h +2836 m +4 h +1 h +536 h +10 h +10 h +443 h +59 h +4 h +59 h +114 h +92 h +4 h +172 h +4 h +1 h +83 h +2837 m +1 h +4 h +2838 m +11 h +11 h +2839 m +2840 m +2841 m +2842 m +4 h +157 h +1048 m +1 h +1261 h +209 m +258 h +2843 m +10 h +4 h +10 h +1 h +2844 m +4 h +2845 m +4 h +557 m +520 h +170 h +556 m +1 h +2846 m +1 h +31 h +4 h +4 h +1 h +57 h +2847 m +10 h +1 h +1 h +129 h +1 h +2848 m +125 h +4 h +279 h +4 h +2849 m +4 h +10 h +185 h +4 h +10 h +10 h +250 h +2850 m +173 h +11 h +4 h +64 h +2851 m +1261 h +12 h +10 h +509 m +2852 m +82 h +626 m +4 h +59 h +4 h +1 h +4 h +2853 m +2854 m +10 h +10 h +12 h +295 h +4 h +556 m +2855 m +10 h +4 h +10 h +10 h +119 h +2856 m +2857 m +403 h +4 h +1 h +2858 m +2859 m +2860 m +1 h +4 h +4 h +4 h +13 h +2861 m +181 h +10 h +4 h +1 h +57 h +10 h +31 h +4 h +3 h +2862 m +1 h +4 h +4 h +4 h +164 h +4 h +2863 m +2864 m +1 h +4 h +109 h +1 h +2374 m +1 h +10 h +10 h +4 h +4 h +1 h +307 h +25 h +4 h +2865 m +4 h +2866 m +976 h +2867 m +2868 m +195 h +313 m +10 h +4 h +11 h +4 h +4 h +2869 m +169 h +10 h +295 h +1 h +2870 m +10 h +195 h +2871 m +10 h +928 m +3 h +172 h +11 h +403 h +4 h +2872 m +1 h +1 h +1 h +10 h +258 h +74 h +4 h +135 h +2873 m +1 h +172 h +10 h +4 h +22 h +190 h +2874 m +4 h +167 h +1 h +1 h +10 h +1 h +4 h +1 h +109 h +4 h +143 h +4 h +4 h +4 h +2875 m +190 h +2876 m +83 h +10 h +10 h +2877 m +4 h +13 h +1470 h +1 h +297 h +59 h +10 h +2878 m +25 h +57 h +82 h +359 h +10 h +219 m +1 h +1 h +2879 m +1 h +4 h +2025 m +4 h +4 h +2880 m +4 h +4 h +1 h +1 h +820 m +3 h +1 h +2022 m +2881 m +2882 m +2883 m +93 h +4 h +124 h +11 h +10 h +4 h +10 h +2884 m +250 h +278 h +2885 m +747 m +4 h +11 h +124 h +114 h +4 h +1 h +124 h +11 h +10 h +10 h +1127 m +1 h +10 h +10 h +1 h +4 h +2886 m +36 h +1 h +2887 m +2888 m +13 h +82 h +10 h +11 h +2889 m +1 h +10 h +1 h +10 h +196 h +1 h +4 h +2890 m +10 h +82 h +2891 m +195 h +2892 m +2893 m +1 h +1261 h +1 h +27 h +4 h +10 h +4 h +10 h +1 h +1 h +79 h +59 h +1 h +10 h +1 h +1 h +10 h +4 h +4 h +10 h +31 h +10 h +124 h +939 h +2625 m +1 h +1 h +82 h +4 h +1 h +1 h +4 h +2894 m +158 h +295 h +2895 m +2896 m +2897 m +2898 m +4 h +41 h +4 h +144 h +146 h +4 h +1 h +358 h +2899 m +10 h +11 h +4 h +2900 m +1 h +10 h +2901 m +2902 m +10 h +1 h +4 h +4 h +4 h +10 h +2903 m +4 h +2904 m +4 h +1 h +2905 m +2906 m +4 h +164 h +1 h +359 h +59 h +4 h +10 h +10 h +2907 m +2908 m +386 h +4 h +173 h +4 h +57 h +4 h +2909 m +4 h +4 h +45 h +1 h +22 h +11 h +4 h +41 h +1 h +4 h +1 h +1 h +4 h +4 h +124 h +4 h +1 h +82 h +1 h +1 h +124 h +4 h +2910 m +1 h +10 h +41 h +4 h +11 h +4 h +2911 m +2912 m +27 h +10 h +10 h +57 h +230 h +2913 m +10 h +4 h +10 h +2891 m +10 h +4 h +4 h +2914 m +2915 m +97 h +2916 m +36 h +2917 m +533 m +146 h +266 h +2918 m +123 h +4 h +2919 m +25 h +10 h +307 h +2920 m +4 h +1 h +83 h +2921 m +4 h +10 h +186 h +4 h +1 h +4 h +4 h +4 h +2025 m +2922 m +4 h +4 h +4 h +2923 m +11 h +11 h +4 h +2924 m +10 h +2925 m +2926 m +109 h +116 m +2927 m +1 h +2928 m +238 h +124 h +4 h +2929 m +10 h +41 h +4 h +1 h +4 h +82 h +10 h +2930 m +332 h +10 h +1 h +4 h +82 h +1 h +2931 m +1 h +4 h +4 h +1 h +1 h +4 h +1 h +10 h +169 h +4 h +1 h +2932 m +27 h +59 h +4 h +536 h +3 h +2933 m +10 h +1 h +10 h +109 h +195 h +569 h +2934 m +59 h +2935 m +10 h +10 h +10 h +33 h +31 h +11 h +4 h +4 h +10 h +83 h +10 h +156 h +4 h +2936 m +888 m +10 h +10 h +10 h +12 h +10 h +1650 m +2937 m +1 h +1 h +258 h +3 h +1 h +2938 m +10 h +1 h +1089 h +10 h +4 h +4 h +1 h +2939 m +4 h +2940 m +79 h +4 h +332 h +10 h +2941 m +2942 m +2943 m +59 h +2944 m +1 h +1 h +4 h +190 h +10 h +10 h +97 h +2945 m +4 h +4 h +59 h +2946 m +150 m +408 h +27 h +1 h +2947 m +2948 m +4 h +97 h +2949 m +173 h +2950 m +2951 m +2794 m +2952 m +2953 m +3 h +4 h +1 h +144 h +10 h +135 h +4 h +1 h +10 h +1 h +2954 m +10 h +2955 m +1 h +2956 m +2957 m +1 h +3 h +1 h +1 h +4 h +92 h +570 m +2245 m +2958 m +10 h +4 h +1 h +109 h +2935 m +4 h +10 h +10 h +1 h +144 h +2004 m +1 h +358 h +124 h +41 h +2959 m +2960 m +4 h +2961 m +124 h +2054 m +4 h +1 h +2962 m +4 h +4 h +11 h +2963 m +2964 m +109 h +4 h +2965 m +4 h +4 h +57 h +2966 m +2967 m +2968 m +11 h +10 h +2969 m +2970 m +110 h +4 h +2971 m +4 h +536 h +2972 m +10 h +4 h +447 h +272 h +59 h +10 h +69 h +10 h +4 h +10 h +2973 m +241 h +4 h +4 h +195 h +4 h +1 h +10 h +1 h +10 h +332 h +1 h +2974 m +10 h +2975 m +2976 m +57 h +1 h +1 h +57 h +10 h +10 h +11 h +139 h +10 h +2977 m +10 h +22 h +11 h +55 h +2978 m +2979 m +2980 m +2981 m +10 h +954 m +1 h +164 h +2982 m +10 h +2983 m +2984 m +4 h +10 h +4 h +1 h +1 h +3 h +25 h +4 h +4 h +4 h +82 h +56 h +59 h +1163 m +2985 m +31 h +2986 m +1 h +56 h +4 h +4 h +11 h +97 h +1 h +10 h +4 h +10 h +57 h +450 m +2987 m +4 h +10 h +2988 m +13 h +4 h +82 h +1 h +181 h +4 h +109 h +10 h +1 h +1 h +59 h +2989 m +2990 m +135 h +1 h +74 h +1 h +125 h +1 h +139 h +4 h +2991 m +109 h +82 h +82 h +4 h +2992 m +1 h +1 h +4 h +10 h +124 h +10 h +1 h +11 h +1 h +11 h +73 h +4 h +4 h +10 h +4 h +2993 m +1 h +1 h +4 h +82 h +74 h +69 h +4 h +11 h +2994 m +4 h +109 h +2995 m +4 h +1 h +2379 m +4 h +10 h +79 h +238 h +10 h +1 h +4 h +10 h +1 h +1478 h +74 h +4 h +185 h +186 h +4 h +4 h +2996 m +4 h +10 h +204 h +3 h +10 h +2997 m +2998 m +2999 m +1 h +109 h +28 h +170 h +3000 m +1766 h +4 h +1 h +250 h +4 h +1 h +10 h +3001 m +13 h +367 h +10 h +295 h +1 h +4 h +3002 m +3003 m +11 h +4 h +4 h +250 h +4 h +575 h +10 h +10 h +3004 m +10 h +1 h +4 h +4 h +3005 m +4 h +3006 m +3007 m +1 h +1 h +4 h +1 h +4 h +4 h +170 h +4 h +195 h +857 h +31 h +4 h +3008 m +4 h +27 h +1 h +124 h +4 h +3009 m +10 h +3010 m +1030 h +10 h +57 h +41 h +1 h +10 h +911 m +104 h +10 h +3011 m +313 m +1 h +4 h +3012 m +10 h +4 h +1 h +3013 m +10 h +3014 m +4 h +104 h +3015 m +10 h +3016 m +91 h +4 h +1 h +10 h +195 h +1 h +3 h +4 h +10 h +538 h +12 h +10 h +1 h +3017 m +2272 m +4 h +3018 m +25 h +4 h +1 h +195 h +10 h +1 h +10 h +4 h +4 h +10 h +10 h +1 h +4 h +3019 m +3020 m +313 h +70 m +1 h +4 h +3021 m +12 h +601 h +1 h +1 h +13 h +3022 m +4 h +4 h +10 h +156 h +1 h +3023 m +4 h +4 h +1 h +10 h +10 h +3024 m +41 h +2928 m +295 h +10 h +3025 m +4 h +4 h +3026 m +3 h +4 h +1 h +3027 m +146 h +10 h +3028 m +3029 m +4 h +4 h +4 h +1 h +4 h +1 h +41 h +289 h +4 h +10 h +1 h +10 h +3030 m +10 h +4 h +3031 m +3032 m +4 h +1 h +59 h +4 h +4 h +3033 m +3034 m +41 h +3035 m +4 h +109 h +59 h +45 h +119 h +3036 m +3037 m +109 h +10 h +1 h +3038 m +4 h +10 h +3039 m +10 h +4 h +22 h +123 h +4 h +3040 m +4 h +3041 m +4 h +4 h +3042 m +241 h +10 h +295 h +1 h +1 h +3043 m +1 h +59 h +185 h +190 h +3044 m +1 h +1 h +1 h +10 h +1 h +94 h +1 h +3045 m +10 h +3046 m +59 h +3047 m +57 h +4 h +229 h +4 h +22 h +3048 m +4 h +10 h +3049 m +4 h +82 h +4 h +4 h +3050 m +3051 m +1 h +10 h +279 h +2038 m +3052 m +92 h +3053 m +10 h +59 h +83 h +41 h +4 h +4 h +1 h +4 h +1 h +64 h +147 h +4 h +4 h +4 h +10 h +4 h +4 h +2851 m +25 h +69 h +4 h +1 h +4 h +2607 m +4 h +1 h +278 h +1619 m +25 h +11 h +1 h +1 h +4 h +73 h +10 h +146 h +278 h +1 h +4 h +13 h +3054 m +966 m +56 h +36 h +4 h +1 h +1 h +1 h +3055 m +3056 m +10 h +4 h +1 h +1 h +10 h +1 h +11 h +1796 h +4 h +3057 m +41 h +3058 m +3059 m +4 h +1 h +3060 m +12 h +1 h +1 h +3061 m +1 h +55 h +3062 m +10 h +4 h +10 h +158 h +3063 m +82 h +3064 m +11 h +3065 m +1 h +4 h +83 h +3066 m +57 h +10 h +172 h +10 h +3067 m +1861 m +11 h +2582 m +4 h +74 h +3068 m +3069 m +976 h +1 h +4 h +4 h +332 h +4 h +4 h +224 h +10 h +276 h +1 h +4 h +10 h +11 h +479 m +57 h +10 h +4 h +3070 m +368 h +25 h +4 h +129 h +10 h +1 h +10 h +4 h +4 h +10 h +5 m +1 h +4 h +4 h +1 h +4 h +10 h +4 h +1 h +10 h +3071 m +10 h +3072 m +83 h +1116 m +3073 m +1 h +3074 m +11 h +4 h +463 m +10 h +4 h +195 h +59 h +11 h +250 h +3075 m +3076 m +1 h +4 h +170 h +2532 m +1 h +1089 h +3077 m +10 h +4 h +3078 m +3079 m +10 h +4 h +3080 m +11 h +10 h +1 h +4 h +857 h +3081 m +3082 m +4 h +1 h +1 h +1444 m +11 h +3 h +358 h +91 h +3083 m +10 h +3084 m +4 h +1 h +10 h +788 m +1 h +1261 h +4 h +1 h +4 h +10 h +488 h +3085 m +1 h +601 h +4 h +3086 m +10 h +10 h +10 h +1 h +3087 m +74 h +1 h +4 h +1 h +119 h +4 h +3088 m +10 h +69 h +4 h +10 h +125 h +147 h +11 h +114 h +31 h +3089 m +3090 m +368 h +4 h +4 h +4 h +986 h +3091 m +413 m +10 h +307 h +1 h +11 h +10 h +4 h +3092 m +1 h +104 h +11 h +266 h +8 h +4 h +1 h +4 h +1884 m +278 h +3093 m +1359 h +164 h +124 h +45 h +4 h +4 h +4 h +4 h +256 m +1 h +4 h +4 h +4 h +1 h +1 h +3094 m +1 h +1 h +800 m +3095 m +4 h +4 h +1817 m +11 h +1 h +77 h +4 h +59 h +2733 m +1 h +3096 m +3097 m +195 h +4 h +3098 m +536 h +3099 m +640 h +10 h +295 h +3100 m +3101 m +1 h +36 h +196 h +3102 m +109 h +91 h +4 h +4 h +1 h +146 h +4 h +125 h +4 h +4 h +3103 m +4 h +3104 m +11 h +3105 m +57 h +11 h +82 h +169 h +368 h +3106 m +4 h +1 h +4 h +4 h +82 h +82 h +10 h +3107 m +10 h +65 h +83 h +1881 m +358 h +1 h +10 h +83 h +1 h +408 h +57 h +4 h +4 h +3108 m +3109 m +4 h +1 h +583 m +4 h +3110 m +82 h +10 h +59 h +64 h +3111 m +135 h +31 h +146 h +10 h +1790 m +265 h +1 h +3112 m +4 h +3113 m +57 h +10 h +82 h +82 h +3114 m +1 h +109 h +65 h +4 h +10 h +109 h +1 h +3115 m +3116 m +94 h +10 h +82 h +45 h +4 h +640 h +10 h +11 h +3117 m +3118 m +65 h +10 h +1 h +10 h +3 h +4 h +139 h +3119 m +4 h +10 h +41 h +3120 m +1 h +4 h +4 h +10 h +1822 m +332 h +1 h +3121 m +10 h +3122 m +4 h +4 h +4 h +4 h +4 h +3123 m +10 h +4 h +146 h +3124 m +3125 m +4 h +4 h +4 h +10 h +1 h +3126 m +3127 m +3128 m +4 h +11 h +10 h +146 h +41 h +45 h +1 h +10 h +1 h +3129 m +911 m +10 h +3130 m +1 h +125 h +3131 m +371 h +4 h +36 h +109 h +10 h +3132 m +1 h +1 h +4 h +3133 m +10 h +4 h +3 h +3134 m +10 h +332 h +1 h +4 h +1 h +3135 m +3136 m +10 h +276 h +3137 m +87 m +109 h +3138 m +4 h +1 h +10 h +25 h +167 h +4 h +3139 m +4 h +25 h +3140 m +1 h +25 h +22 h +270 h +3141 m +139 h +3142 m +10 h +27 h +779 m +10 h +4 h +1 h +55 h +1 h +4 h +4 h +27 h +25 h +57 h +274 h +4 h +3143 m +1 h +74 h +184 h +1 h +94 h +11 h +1 h +536 h +1 h +1 h +59 h +1 h +1 h +3144 m +3145 m +10 h +92 h +4 h +11 h +3146 m +1 h +3147 m +3148 m +4 h +1 h +124 h +40 h +1 h +3149 m +3150 m +307 h +10 h +465 m +3151 m +1 h +10 h +4 h +4 h +10 h +110 h +11 h +31 h +10 h +1 h +11 h +59 h +1 h +1 h +3152 m +10 h +4 h +2710 m +4 h +3153 m +10 h +4 h +3154 m +3155 m +4 h +3156 m +4 h +3157 m +10 h +1 h +4 h +10 h +1 h +3158 m +1 h +10 h +10 h +10 h +10 h +1184 m +4 h +3159 m +955 m +3160 m +10 h +4 h +976 h +2688 h +4 h +4 h +1 h +3161 m +109 h +1 h +147 h +4 h +3162 m +3163 m +83 h +434 h +1 h +11 h +4 h +1939 m +3164 m +41 h +59 h +12 h +184 h +143 h +4 h +4 h +1 h +82 h +1 h +4 h +3165 m +2964 m +10 h +4 h +4 h +3166 m +173 h +10 h +10 h +172 h +1 h +109 h +75 m +3167 m +3168 m +10 h +2865 m +4 h +3169 m +3170 m +358 h +368 h +4 h +1 h +1 h +4 h +3171 m +4 h +274 h +3172 m +57 h +1 h +3173 m +3174 m +3175 m +10 h +82 h +31 h +10 h +1 h +185 h +386 h +3176 m +4 h +46 h +2475 m +31 h +125 h +138 h +10 h +147 h +10 h +97 h +4 h +3 h +3177 m +3178 m +13 h +10 h +3179 m +4 h +1 h +266 h +110 h +10 h +123 h +4 h +109 h +4 h +83 h +3180 m +41 h +10 h +33 h +3181 m +10 h +3182 m +1218 m +4 h +4 h +1 h +3183 m +1 h +59 h +1 h +4 h +10 h +4 h +4 h +358 h +4 h +4 h +11 h +4 h +4 h +4 h +27 h +4 h +4 h +4 h +3184 m +10 h +1 h +3185 m +4 h +4 h +1 h +8 h +4 h +3186 m +3187 m +4 h +1 h +3188 m +3189 m +229 h +94 h +1880 m +3190 m +1 h +3191 m +3192 m +114 h +1 h +4 h +3193 m +3194 m +4 h +79 h +70 m +73 h +74 h +2851 m +74 h +3195 m +4 h +3196 m +4 h +10 h +1685 m +481 m +97 h +399 h +3197 m +56 h +41 h +1544 m +172 h +3198 m +97 h +94 h +181 h +11 h +1 h +295 h +116 m +4 h +104 h +4 h +3199 m +10 h +4 h +124 h +169 h +93 h +4 h +1 h +3200 m +25 h +3201 m +1 h +4 h +4 h +69 h +4 h +1306 m +1822 m +10 h +3202 m +1 h +172 h +3203 m +25 h +1 h +1 h +3204 m +1 h +4 h +256 m +1 h +4 h +3205 m +172 h +1 h +10 h +181 h +730 m +4 h +3206 m +11 h +2205 m +4 h +1953 m +4 h +4 h +3207 m +4 h +124 h +4 h +1 h +4 h +3208 m +190 h +425 m +1 h +10 h +146 h +4 h +41 h +4 h +147 h +10 h +10 h +3209 m +109 h +4 h +4 h +109 h +83 h +3210 m +3211 m +3212 m +3213 m +4 h +123 h +10 h +3214 m +3215 m +10 h +3216 m +1016 h +4 h +3217 m +1 h +4 h +1 h +1 h +79 h +8 h +4 h +3218 m +10 h +1 h +110 h +4 h +94 h +10 h +3219 m +1493 h +10 h +10 h +3 h +77 h +147 h +4 h +1 h +3220 m +276 h +434 h +3221 m +10 h +297 h +3222 m +11 h +10 h +104 h +11 h +10 h +83 h +3223 m +3224 m +1 h +169 h +4 h +1 h +299 h +3225 m +1642 m +1 h +11 h +4 h +3 h +12 h +4 h +1620 m +3226 m +1403 h +11 h +3 h +114 h +143 h +172 h +1 h +48 h +10 h +10 h +3227 m +10 h +468 h +3228 m +3229 m +3230 m +3231 m +1 h +3232 m +3233 m +4 h +10 h +3234 m +1 h +3235 m +1 h +3236 m +3237 m +10 h +195 h +3238 m +1 h +195 h +4 h +1 h +11 h +1 h +1504 m +4 h +3239 m +1 h +1 h +74 h +82 h +383 h +1 h +1 h +4 h +40 h +11 h +59 h +74 h +3240 m +10 h +1 h +1 h +4 h +4 h +4 h +3241 m +10 h +1 h +2558 m +10 h +4 h +57 h +10 h +1 h +447 h +196 h +3242 m +238 h +4 h +1 h +3 h +3243 m +386 h +11 h +174 h +656 m +569 h +4 h +3244 m +3245 m +10 h +3214 m +10 h +10 h +3246 m +3247 m +1 h +10 h +1 h +443 h +1 h +10 h +55 h +3248 m +1 h +156 h +10 h +4 h +104 h +1357 m +256 h +1 h +1 h +1 h +10 h +1 h +92 h +1 h +509 m +10 h +808 h +83 h +12 h +13 h +8 h +4 h +4 h +1261 h +4 h +125 h +4 h +82 h +3249 m +4 h +10 h +3250 m +1 h +124 h +986 h +10 h +1 h +4 h +4 h +4 h +4 h +4 h +3251 m +4 h +10 h +4 h +3252 m +4 h +10 h +4 h +1 h +238 h +157 h +1 h +31 h +3253 m +4 h +4 h +4 h +1 h +3254 m +3255 m +3256 m +69 h +4 h +4 h +477 m +3 h +4 h +147 h +82 h +4 h +59 h +1 h +3257 m +1764 m +10 h +4 h +408 h +10 h +3258 m +25 h +196 h +3259 m +3260 m +1321 h +167 h +156 h +1 h +109 h +3261 m +10 h +4 h +3262 m +124 h +3263 m +11 h +11 h +3264 m +4 h +1 h +3265 m +25 h +12 h +94 h +3266 m +1 h +307 h +10 h +1 h +3267 m +3 h +8 h +41 h +10 h +4 h +3268 m +1089 h +3269 m +2522 m +1535 h +3270 m +3271 m +1 h +3272 m +4 h +1 h +3273 m +156 h +3274 m +3275 m +3276 m +3277 m +41 h +4 h +3278 m +1 h +4 h +386 h +10 h +3279 m +3274 m +3280 m +146 h +4 h +3 h +3281 m +10 h +1 h +97 h +59 h +1 h +3282 m +359 h +3 h +4 h +10 h +10 h +73 h +4 h +4 h +4 h +4 h +25 h +3283 m +1 h +109 h +1 h +10 h +31 h +119 h +4 h +278 h +10 h +3284 m +3285 m +1437 m +3286 m +1070 m +4 h +3287 m +10 h +4 h +124 h +4 h +3288 m +3289 m +3290 m +3291 m +3067 m +3292 m +4 h +181 h +3293 m +3294 m +1261 h +3295 m +164 h +4 h +4 h +1 h +27 h +1 h +83 h +64 h +1 h +10 h +1 h +1 h +1 h +238 h +1 h +4 h +1 h +238 h +1 h +4 h +3296 m +4 h +4 h +4 h +83 h +3297 m +11 h +59 h +97 h +258 h +3298 m +143 h +41 h +265 h +1 h +10 h +10 h +97 h +139 h +3299 m +11 h +1 h +59 h +1 h +64 h +1 h +4 h +10 h +11 h +10 h +13 h +10 h +1 h +3300 m +10 h +3301 m +4 h +295 h +10 h +3302 m +1 h +403 h +383 h +4 h +3303 m +1 h +124 h +4 h +4 h +3 h +196 h +3304 m +1 h +4 h +3305 m +25 h +10 h +4 h +4 h +3306 m +1 h +4 h +10 h +97 h +10 h +1 h +4 h +4 h +3307 m +3308 m +174 h +4 h +295 h +1 h +4 h +1 h +10 h +279 h +1030 h +11 h +3309 m +3310 m +4 h +195 h +4 h +27 h +1 h +1 h +10 h +143 h +1 h +3311 m +64 h +1 h +4 h +167 h +4 h +1 h +3312 m +258 h +4 h +3313 m +3314 m +4 h +196 h +3315 m +73 h +190 h +4 h +258 h +368 h +1250 h +276 h +110 h +1 h +156 h +4 h +1 h +143 h +129 h +1 h +4 h +3316 m +779 m +11 h +3317 m +10 h +1 h +57 h +4 h +4 h +3318 m +109 h +1 h +1650 m +4 h +124 h +4 h +12 h +2163 m +3319 m +124 h +1 h +3320 m +3321 m +10 h +3322 m +2920 m +25 h +10 h +276 h +4 h +3323 m +119 h +1981 m +3324 m +4 h +3325 m +59 h +262 h +3326 m +10 h +31 h +3327 m +4 h +83 h +3328 m +4 h +869 m +25 h +10 h +3329 m +4 h +10 h +4 h +4 h +4 h +4 h +1 h +1 h +169 h +3330 m +1 h +3331 m +4 h +45 h +4 h +4 h +4 h +143 h +135 h +4 h +3332 m +1 h +1 h +1 h +10 h +3333 m +1 h +4 h +190 h +4 h +4 h +3334 m +3335 m +11 h +10 h +3336 m +10 h +31 h +1 h +990 m +4 h +1 h +4 h +124 h +25 h +4 h +4 h +4 h +69 h +97 h +190 h +3337 m +10 h +195 h +995 m +1 h +10 h +11 h +3338 m +2733 m +3339 m +1 h +230 h +3340 m +57 h +31 h +10 h +1 h +45 h +10 h +278 h +40 h +4 h +3341 m +4 h +3342 m +4 h +1 h +3343 m +3344 m +4 h +22 h +4 h +3345 m +3346 m +114 h +4 h +109 h +1 h +1 h +12 h +4 h +25 h +3347 m +1 h +4 h +3348 m +3349 m +4 h +258 h +10 h +3350 m +4 h +3351 m +3352 m +1 h +1 h +3353 m +4 h +10 h +4 h +1 h +4 h +1 h +4 h +1 h +2025 h +3354 m +4 h +1 h +3355 m +4 h +4 h +4 h +4 h +11 h +1 h +3 h +838 m +1 h +10 h +10 h +74 h +4 h +3356 m +332 h +238 h +4 h +3357 m +1053 m +1250 h +3358 m +4 h +124 h +4 h +3359 m +135 h +285 m +59 h +4 h +4 h +11 h +1 h +1 h +31 h +97 h +3360 m +11 h +4 h +3361 m +3362 m +1 h +556 h +3363 m +172 h +3364 m +1 h +195 h +3365 m +1137 h +964 m +146 h +10 h +10 h +1 h +1 h +3366 m +3367 m +164 h +4 h +4 h +156 h +3368 m +383 h +3369 m +3370 m +1359 h +10 h +3371 m +1 h +10 h +10 h +4 h +3372 m +332 h +25 h +1 h +4 h +1 h +4 h +1 h +4 h +3373 m +10 h +135 h +4 h +10 h +83 h +1 h +1 h +4 h +4 h +10 h +1 h +4 h +59 h +4 h +4 h +169 h +4 h +10 h +4 h +10 h +1 h +109 h +1 h +1 h +83 h +3374 m +4 h +1772 m +10 h +10 h +3375 m +3376 m +1 h +270 h +3377 m +10 h +3378 m +1 h +4 h +4 h +4 h +1822 h +147 h +3379 m +4 h +1 h +4 h +10 h +3380 m +258 h +4 h +1 h +2928 h +11 h +3381 m +10 h +10 h +10 h +92 h +3382 m +10 h +3383 m +4 h +1 h +1 h +104 h +1 h +22 h +82 h +1 h +1 h +10 h +10 h +3384 m +41 h +3209 m +3385 m +10 h +3386 m +1 h +319 h +1 h +158 h +4 h +82 h +196 h +4 h +1 h +1 h +97 h +1 h +4 h +56 h +10 h +3387 m +124 h +278 h +114 h +4 h +4 h +10 h +1 h +1 h +97 h +4 h +3388 m +1835 m +31 h +1 h +3389 m +3390 m +192 h +4 h +10 h +4 h +10 h +4 h +41 h +4 h +3391 m +278 h +4 h +4 h +4 h +4 h +10 h +3392 m +4 h +11 h +4 h +4 h +3393 m +4 h +1 h +3394 m +59 h +3395 m +4 h +1 h +3396 m +196 h +11 h +10 h +718 h +3397 m +3398 m +25 h +109 h +10 h +4 h +56 h +4 h +4 h +59 h +124 h +4 h +4 h +3399 m +3033 m +94 h +1 h +1 h +164 h +1770 m +3400 m +164 h +4 h +1 h +174 h +1 h +4 h +1 h +3401 m +170 h +4 h +3402 m +1 h +1 h +1 h +10 h +3403 m +1 h +4 h +1 h +4 h +10 h +82 h +203 m +3404 m +4 h +4 h +1 h +1 h +278 h +3405 m +125 h +4 h +307 h +1 h +3406 m +135 h +3407 m +276 h +1 h +10 h +1 h +1 h +1 h +278 h +1 h +3408 m +1 h +4 h +109 h +1 h +3409 m +97 h +3410 m +3411 m +3412 m +10 h +250 h +3413 m +40 h +36 h +4 h +27 h +10 h +3414 m +3415 m +3416 m +94 h +4 h +41 h +172 h +4 h +10 h +3417 m +3418 m +4 h +1 h +4 h +3419 m +119 h +3420 m +3 h +4 h +1 h +10 h +4 h +3421 m +10 h +4 h +272 h +3422 m +347 m +4 h +1 h +3423 m +4 h +4 h +1 h +359 h +1836 m +1723 m +10 h +25 h +332 h +4 h +92 h +4 h +397 m +4 h +129 h +4 h +195 h +10 h +1766 h +4 h +36 h +4 h +3424 m +4 h +10 h +698 m +3425 m +1 h +57 h +10 h +3426 m +1 h +3427 m +10 h +11 h +3428 m +146 h +79 h +1 h +1 h +74 h +109 h +55 h +10 h +64 h +10 h +3429 m +10 h +3430 m +3278 m +10 h +10 h +4 h +4 h +4 h +3431 m +10 h +82 h +1822 h +857 h +3432 m +4 h +238 h +11 h +1 h +4 h +279 h +1 h +1 h +3433 m +10 h +3434 m +1 h +10 h +322 m +4 h +4 h +1 h +64 h +167 h +10 h +1089 h +3435 m +276 h +10 h +56 h +196 h +10 h +10 h +1 h +4 h +4 h +3436 m +1 h +3437 m +3438 m +1 h +3439 m +125 h +1 h +1740 m +3440 m +1 h +28 h +4 h +3441 m +1 h +4 h +204 h +1571 m +3 h +1 h +1 h +583 m +1 h +4 h +1 h +4 h +316 m +4 h +4 h +4 h +4 h +1418 m +10 h +74 h +1 h +1 h +1 h +3442 m +10 h +10 h +3443 m +1 h +3444 m +4 h +11 h +109 h +10 h +36 h +3445 m +4 h +258 h +10 h +10 h +196 h +4 h +3446 m +258 h +164 h +3447 m +3448 m +3449 m +779 m +79 h +195 h +1074 m +3450 m +3451 m +10 h +10 h +4 h +1 h +3452 m +4 h +59 h +10 h +92 h +125 h +79 h +3453 m +11 h +10 h +1 h +3454 m +4 h +4 h +10 h +64 h +1 h +10 h +4 h +4 h +109 h +31 h +4 h +603 m +4 h +477 m +45 h +4 h +1 h +3455 m +1 h +123 h +1 h +4 h +368 h +4 h +3456 m +4 h +4 h +1127 m +4 h +4 h +10 h +109 h +1 h +1 h +1 h +64 h +704 h +4 h +4 h +1830 m +57 h +4 h +40 h +41 h +65 h +4 h +3457 m +41 h +1 h +3458 m +4 h +10 h +3459 m +4 h +1 h +3460 m +3461 m +3462 m +1403 h +1 h +1 h +4 h +4 h +10 h +3463 m +124 h +10 h +146 h +1 h +3464 m +56 h +4 h +1 h +4 h +10 h +4 h +3465 m +368 h +83 h +3466 m +124 h +11 h +11 h +4 h +4 h +10 h +4 h +4 h +1 h +65 h +1 h +74 h +3467 m +31 h +3468 m +4 h +1595 m +4 h +4 h +1 h +1 h +3469 m +109 h +730 m +57 h +1 h +82 h +10 h +258 h +3470 m +174 h +3471 m +10 h +4 h +3472 m +4 h +1 h +196 h +31 h +1 h +3473 m +4 h +10 h +11 h +1 h +4 h +3474 m +31 h +1 h +4 h +4 h +10 h +4 h +464 h +3475 m +1 h +2124 h +10 h +135 h +1 h +110 h +1 h +1 h +1 h +3476 m +4 h +10 h +1 h +1 h +3477 m +4 h +10 h +4 h +1 h +3478 m +1 h +2887 m +1016 h +4 h +119 h +1 h +3479 m +1 h +4 h +3480 m +10 h +13 h +4 h +3481 m +10 h +186 h +3482 m +3483 m +1 h +2614 m +3484 m +4 h +1 h +61 m +1 h +1 h +1 h +3485 m +59 h +3486 m +1 h +3487 m +10 h +169 h +1 h +10 h +3488 m +4 h +74 h +4 h +1070 m +4 h +1685 m +82 h +541 h +83 h +614 m +583 m +4 h +10 h +10 h +4 h +3489 m +10 h +4 h +114 h +3490 m +4 h +92 h +4 h +57 h +4 h +4 h +1 h +41 h +4 h +124 h +3491 m +4 h +181 h +3 h +4 h +1 h +4 h +36 h +3492 m +13 h +109 h +146 h +1 h +57 h +3493 m +238 h +4 h +10 h +3494 m +1 h +146 h +10 h +3495 m +82 h +4 h +1 h +3 h +4 h +11 h +4 h +230 h +3496 m +4 h +4 h +3497 m +10 h +4 h +3498 m +10 h +4 h +4 h +4 h +124 h +3499 m +4 h +3500 m +1 h +92 h +25 h +11 h +65 h +4 h +1710 m +1 h +4 h +1 h +4 h +1 h +10 h +3501 m +25 h +10 h +11 h +620 m +10 h +1886 m +4 h +4 h +10 h +1250 h +1 h +3502 m +1 h +4 h +109 h +3503 m +124 h +11 h +69 h +270 h +3504 m +3505 m +3506 m +536 h +10 h +10 h +4 h +3507 m +10 h +73 h +1 h +995 m +157 h +13 h +10 h +4 h +2002 m +2303 m +3508 m +10 h +4 h +1 h +65 h +10 h +3509 m +147 h +11 h +307 h +2436 m +10 h +57 h +10 h +3510 m +11 h +4 h +1 h +1 h +1 h +1 h +10 h +10 h +146 h +3511 m +3512 m +2028 m +114 h +4 h +570 h +25 h +4 h +10 h +1 h +801 m +147 h +4 h +4 h +4 h +92 h +10 h +10 h +195 h +4 h +10 h +1 h +11 h +3 h +1 h +1 h +3513 m +1914 m +124 h +79 h +4 h +10 h +56 h +4 h +11 h +167 h +3514 m +11 h +3396 m +1 h +3515 m +79 h +278 h +3516 m +434 h +4 h +241 h +4 h +13 h +447 h +57 h +108 h +173 h +4 h +4 h +57 h +94 h +3517 m +1 h +4 h +1 h +10 h +3518 m +3519 m +4 h +83 h +3520 m +1 h +258 h +79 h +264 m +3521 m +192 h +4 h +10 h +4 h +4 h +10 h +11 h +110 h +3522 m +1 h +124 h +27 h +3523 m +4 h +3524 m +41 h +4 h +3525 m +4 h +10 h +1470 h +4 h +1 h +4 h +82 h +83 h +4 h +4 h +4 h +1 h +79 h +11 h +4 h +4 h +1 h +10 h +104 h +10 h +4 h +3526 m +10 h +4 h +1260 m +1 h +1 h +3527 m +1 h +83 h +1 h +4 h +3528 m +3529 m +10 h +144 h +3530 m +2843 m +1 h +1 h +10 h +184 h +1 h +358 h +109 h +10 h +4 h +3531 m +4 h +41 h +1 h +83 h +185 h +3532 m +4 h +1 h +129 h +3533 m +3534 m +1 h +238 h +4 h +1 h +10 h +1 h +1 h +3535 m +124 h +10 h +10 h +1 h +4 h +10 h +4 h +3536 m +1 h +4 h +1 h +601 h +10 h +10 h +10 h +10 h +4 h +1 h +3537 m +12 h +2379 m +82 h +3538 m +4 h +4 h +1 h +48 h +4 h +1 h +3539 m +1 h +10 h +83 h +1 h +358 h +4 h +10 h +11 h +3540 m +3541 m +10 h +4 h +82 h +3542 m +65 h +25 h +4 h +4 h +4 h +1 h +4 h +4 h +3543 m +135 h +4 h +3544 m +3545 m +4 h +11 h +368 h +266 h +1 h +3546 m +119 h +4 h +1470 h +83 h +3547 m +1081 m +82 h +3115 m +4 h +1250 h +10 h +3548 m +4 h +3549 m +1 h +59 h +581 m +10 h +25 h +4 h +25 h +3550 m +186 h +332 h +403 h +4 h +109 h +10 h +109 h +92 h +1 h +1 h +3551 m +4 h +10 h +10 h +4 h +84 m +10 h +204 h +97 h +10 h +10 h +3 h +4 h +4 h +3552 m +1 h +4 h +82 h +11 h +3553 m +4 h +4 h +276 h +3554 m +3555 m +1 h +124 h +173 h +10 h +10 h +59 h +1985 m +10 h +3556 m +4 h +1 h +3557 m +12 h +4 h +4 h +10 h +8 h +4 h +59 h +10 h +276 h +3558 m +1 h +3559 m +1 h +4 h +10 h +10 h +1 h +4 h +4 h +3560 m +1 h +4 h +3561 m +1218 m +4 h +27 h +114 h +112 h +1 h +79 h +2846 m +1 h +10 h +4 h +4 h +97 h +4 h +125 h +12 h +82 h +3278 h +332 h +4 h +10 h +10 h +1 h +11 h +3562 m +1 h +3563 m +1003 h +3564 m +91 h +1 h +1 h +3565 m +3 h +1 h +1 h +4 h +3566 m +4 h +10 h +3567 m +1027 h +987 m +10 h +3568 m +4 h +4 h +3569 m +1 h +1 h +1 h +1 h +4 h +103 h +41 h +10 h +3570 m +4 h +1564 m +4 h +3571 m +10 h +108 h +3572 m +3573 m +1020 m +4 h +3574 m +1 h +604 m +4 h +3575 m +125 h +170 h +4 h +3576 m +1128 m +1 h +1 h +3577 m +1 h +124 h +11 h +447 h +4 h +45 h +195 h +4 h +3578 m +3579 m +1 h +4 h +4 h +57 h +4 h +1 h +3580 m +3581 m +10 h +1 h +3582 m +4 h +10 h +278 h +4 h +3583 m +195 h +10 h +11 h +1 h +1 h +3584 m +4 h +156 h +11 h +4 h +59 h +1 h +1 h +1016 h +10 h +10 h +3585 m +109 h +4 h +1884 m +4 h +4 h +112 h +3586 m +1 h +4 h +10 h +1 h +3587 m +185 h +1 h +55 h +1 h +4 h +3588 m +4 h +4 h +4 h +3589 m +4 h +1 h +10 h +238 h +2367 m +65 h +3590 m +25 h +64 h +10 h +1 h +3591 m +10 h +3592 m +169 h +1 h +1 h +3593 m +4 h +10 h +3594 m +157 h +1 h +157 h +10 h +41 h +10 h +1 h +10 h +11 h +1 h +124 h +41 h +74 h +4 h +4 h +3595 m +299 h +195 h +3596 m +10 h +567 m +10 h +1 h +1 h +40 h +45 h +3597 m +10 h +129 h +4 h +1 h +272 h +3 h +4 h +4 h +4 h +10 h +131 m +74 h +112 h +1 h +10 h +119 h +99 m +3598 m +3 h +41 h +4 h +10 h +5 m +74 h +4 h +4 h +92 h +4 h +692 h +119 h +10 h +1 h +4 h +939 h +1409 h +3599 m +109 h +717 m +1 h +4 h +1 h +181 h +1 h +1646 m +170 h +1 h +4 h +4 h +1 h +687 h +74 h +3600 m +156 h +1 h +3601 m +1 h +3602 m +73 h +4 h +14 m +33 h +3603 m +3604 m +57 h +1 h +4 h +27 h +4 h +109 h +1 h +10 h +10 h +1 h +578 m +10 h +146 h +4 h +12 h +3605 m +104 h +41 h +1 h +276 h +82 h +57 h +1 h +332 h +31 h +265 h +1 h +3606 m +3607 m +4 h +83 h +10 h +3608 m +41 h +10 h +2819 m +3609 m +3610 m +3 h +124 h +3611 m +642 m +11 h +3 h +1 h +4 h +3612 m +3613 m +3614 m +74 h +3615 m +10 h +4 h +4 h +83 h +123 h +3616 m +3617 m +4 h +185 h +3618 m +3619 m +11 h +83 h +3620 m +3621 m +4 h +143 h +4 h +4 h +10 h +190 h +10 h +1 h +4 h +4 h +3622 m +4 h +172 h +3623 m +55 h +3624 m +92 h +4 h +2124 h +22 h +1 h +4 h +358 h +4 h +1220 m +11 h +4 h +1 h +1 h +3625 m +1359 h +4 h +10 h +170 h +4 h +1089 h +10 h +11 h +25 h +1403 h +1 h +164 h +82 h +10 h +3381 m +1 h +10 h +4 h +36 h +377 h +3626 m +1 h +4 h +2362 m +3627 m +3 h +3628 m +3629 m +4 h +1 h +1 h +74 h +4 h +4 h +11 h +4 h +83 h +4 h +3630 m +3631 m +10 h +4 h +4 h +1 h +109 h +4 h +4 h +1 h +976 h +3632 m +4 h +1 h +4 h +69 h +10 h +1791 m +3633 m +10 h +338 m +10 h +1936 m +57 h +3634 m +10 h +1 h +489 m +4 h +4 h +1 h +112 h +1 h +1 h +31 h +4 h +3635 m +4 h +3636 m +4 h +4 h +4 h +83 h +4 h +114 h +4 h +3637 m +51 m +3638 m +3639 m +146 h +10 h +3640 m +1 h +1 h +3641 m +4 h +3642 m +10 h +4 h +10 h +10 h +10 h +92 h +1 h +1 h +59 h +258 h +794 h +2628 m +1 h +1 h +918 m +55 h +1 h +4 h +3643 m +3644 m +3645 m +4 h +4 h +195 h +4 h +3646 m +3647 m +82 h +4 h +3648 m +10 h +1 h +195 h +143 h +28 h +56 h +12 h +520 h +1 h +83 h +10 h +1 h +13 h +3 h +626 m +10 h +3649 m +1137 h +124 h +25 h +167 h +10 h +1 h +1 h +1 h +4 h +2379 m +10 h +4 h +10 h +4 h +83 h +114 h +1 h +3650 m +4 h +805 m +11 h +129 h +3651 m +97 h +3 h +4 h +124 h +3652 m +4 h +1 h +139 h +10 h +195 h +3653 m +307 h +4 h +48 h +3654 m +10 h +57 h +3655 m +1 h +1 h +1 h +109 h +41 h +4 h +4 h +4 h +1406 h +3656 m +3657 m +695 m +1 h +1835 h +11 h +3658 m +31 h +10 h +172 h +3 h +3659 m +146 h +124 h +4 h +3660 m +1 h +4 h +3661 m +4 h +4 h +1 h +4 h +10 h +801 m +31 h +10 h +4 h +45 h +4 h +1 h +1 h +195 h +3662 m +1 h +1 h +3663 m +4 h +31 h +620 m +3664 m +48 h +1740 m +156 h +185 h +65 h +4 h +1796 h +3665 m +113 h +10 h +3666 m +10 h +4 h +4 h +1 h +10 h +4 h +3667 m +258 h +4 h +31 h +3668 m +25 h +158 h +2846 m +3669 m +158 h +1 h +129 h +1 h +158 h +10 h +1 h +3670 m +3671 m +3672 m +147 h +1250 h +25 h +1 h +25 h +74 h +3673 m +4 h +157 h +2442 m +169 h +3674 m +158 h +10 h +11 h +74 h +3675 m +1 h +31 h +1 h +41 h +1 h +3676 m +3677 m +1650 m +195 h +4 h +3678 m +57 h +10 h +3679 m +57 h +3680 m +195 h +3681 m +83 h +3682 m +59 h +10 h +3683 m +1 h +4 h +3684 m +1 h +3685 m +386 h +3686 m +3687 m +3688 m +4 h +10 h +1 h +65 h +157 h +3689 m +190 h +4 h +3690 m +766 m +3691 m +4 h +1 h +57 h +4 h +11 h +1 h +3692 m +146 h +4 h +1 h +4 h +3693 m +1 h +3694 m +4 h +4 h +4 h +1 h +1 h +4 h +4 h +3695 m +3696 m +3697 m +3698 m +3699 m +3700 m +10 h +4 h +3701 m +3702 m +4 h +3 h +147 h +3703 m +1 h +4 h +10 h +1 h +1 h +4 h +10 h +3 h +986 h +4 h +4 h +10 h +56 h +1030 h +3704 m +4 h +1 h +10 h +3705 m +4 h +1 h +3706 m +82 h +4 h +45 h +4 h +158 h +3707 m +3708 m +885 h +114 h +3709 m +4 h +1 h +3710 m +4 h +94 h +4 h +704 h +184 h +1105 m +125 h +3711 m +4 h +31 h +3712 m +1 h +4 h +10 h +1714 m +2688 h +358 h +1 h +3713 m +3714 m +25 h +109 h +11 h +1470 h +1 h +368 h +1 h +4 h +4 h +3715 m +10 h +119 h +289 h +4 h +4 h +3716 m +4 h +4 h +4 h +4 h +3717 m +0 m +4 h +10 h +1 h +4 h +1 h +57 h +170 h +3 h +10 h +601 h +1 h +1 h +569 h +22 h +4 h +113 h +1 h +10 h +3718 m +1 h +113 h +3719 m +1 h +4 h +4 h +1 h +10 h +83 h +109 h +4 h +57 h +1 h +109 h +601 h +79 h +1 h +169 h +4 h +4 h +1 h +4 h +1 h +1 h +1875 m +1 h +3720 m +4 h +2730 m +10 h +11 h +1 h +10 h +11 h +10 h +55 h +57 h +4 h +332 h +4 h +10 h +155 m +4 h +583 h +367 h +10 h +4 h +65 h +3721 m +10 h +4 h +3722 m +1372 m +4 h +4 h +630 m +4 h +57 h +1 h +4 h +10 h +4 h +3723 m +4 h +520 h +4 h +488 h +4 h +1 h +3724 m +3725 m +190 h +3726 m +10 h +536 h +1 h +10 h +3727 m +135 h +4 h +41 h +3728 m +3729 m +10 h +181 h +4 h +82 h +258 h +10 h +3730 m +10 h +976 h +55 h +1 h +4 h +4 h +1 h +1 h +1 h +265 h +143 h +4 h +82 h +3731 m +3732 m +4 h +4 h +1 h +82 h +170 h +801 h +2278 m +10 h +463 m +3733 m +10 h +4 h +109 h +3734 m +1 h +4 h +869 m +938 m +4 h +4 h +1 h +3735 m +1 h +65 h +4 h +123 h +1 h +11 h +4 h +3736 m +4 h +1137 h +1 h +97 h +4 h +1 h +3737 m +27 h +1 h +1 h +4 h +1 h +1 h +2625 m +45 h +3738 m +3739 m +57 h +147 h +3740 m +147 h +386 h +1100 m +3741 m +3 h +3742 m +3743 m +1 h +3744 m +1 h +104 h +138 h +10 h +3745 m +1 h +146 h +10 h +57 h +10 h +1 h +3746 m +4 h +3747 m +1083 m +59 h +10 h +1 h +4 h +1 h +1 h +10 h +3748 m +4 h +258 h +1 h +3749 m +1 h +146 h +3750 m +3751 m +3752 m +11 h +4 h +3753 m +3754 m +12 h +11 h +83 h +4 h +41 h +4 h +307 h +4 h +106 h +4 h +1403 h +4 h +10 h +4 h +1 h +10 h +4 h +447 h +4 h +55 h +4 h +10 h +4 h +4 h +11 h +109 h +135 h +4 h +1 h +3755 m +359 h +1 h +4 h +1 h +3756 m +3757 m +4 h +1 h +10 h +4 h +124 h +12 h +112 h +3758 m +4 h +1 h +857 h +3759 m +10 h +3760 m +97 h +3761 m +1 h +104 h +3762 m +41 h +4 h +1 h +10 h +1 h +1 h +4 h +10 h +1 h +1 h +10 h +3763 m +4 h +1 h +2139 m +55 h +4 h +3764 m +59 h +10 h +1 h +4 h +203 m +3765 m +146 h +64 h +4 h +4 h +1 h +3766 m +1 h +4 h +10 h +3767 m +5 m +10 h +3768 m +4 h +4 h +447 h +3769 m +10 h +4 h +92 h +3770 m +25 h +11 h +4 h +3771 m +10 h +190 h +82 h +4 h +1 h +41 h +186 h +1 h +1 h +1 h +59 h +3772 m +10 h +1 h +3773 m +3774 m +1 h +10 h +10 h +1 h +1 h +82 h +10 h +4 h +1 h +135 h +258 h +195 h +4 h +3775 m +4 h +3776 m +10 h +4 h +25 h +4 h +109 h +1 h +3777 m +1 h +3778 m +10 h +12 h +92 h +1 h +4 h +123 h +4 h +3779 m +4 h +4 h +97 h +1 h +1627 m +1 h +3780 m +3781 m +3782 m +4 h +56 h +3783 m +3784 m +82 h +3785 m +4 h +3 h +59 h +1 h +2163 m +250 h +4 h +3786 m +10 h +3787 m +4 h +3788 m +196 h +3789 m +10 h +258 h +3790 m +1714 m +10 h +1 h +4 h +36 h +4 h +2522 m +65 h +4 h +4 h +1 h +3791 m +4 h +25 h +4 h +65 h +10 h +4 h +4 h +77 h +4 h +10 h +4 h +1 h +1 h +190 h +1 h +4 h +3792 m +4 h +1 h +64 h +55 h +1 h +10 h +1981 m +4 h +3793 m +31 h +82 h +1003 h +278 h +278 h +125 h +4 h +464 h +3794 m +3795 m +10 h +1 h +1 h +27 h +3796 m +4 h +736 m +3797 m +1 h +12 h +897 m +3798 m +443 h +114 h +4 h +4 h +74 h +3799 m +3800 m +56 h +11 h +4 h +4 h +91 h +4 h +4 h +1 h +3801 m +4 h +4 h +74 h +125 h +3802 m +3803 m +12 h +10 h +10 h +10 h +4 h +1 h +3804 m +1024 m +1 h +10 h +1 h +10 h +3805 m +10 h +536 h +3806 m +3807 m +13 h +135 h +990 m +1 h +1 h +4 h +124 h +1 h +1 h +10 h +57 h +65 h +1 h +4 h +3808 m +1 h +8 h +3809 m +8 h +1 h +73 h +10 h +3810 m +4 h +3811 m +1 h +1 h +3812 m +10 h +3813 m +3814 m +147 h +10 h +3815 m +113 h +1 h +4 h +146 h +10 h +97 h +274 h +10 h +4 h +4 h +124 h +3816 m +11 h +3817 m +3818 m +10 h +25 h +1 h +1 h +1 h +3819 m +4 h +135 h +4 h +10 h +1 h +258 h +1470 h +4 h +1 h +1 h +1 h +3820 m +3821 m +4 h +93 h +1 h +4 h +10 h +11 h +167 h +1 h +1 h +4 h +3822 m +27 h +11 h +3823 m +3824 m +4 h +4 h +1 h +3825 m +4 h +399 h +10 h +83 h +146 h +3826 m +195 h +1 h +4 h +1 h +1 h +3827 m +10 h +10 h +1677 m +587 m +1 h +224 h +4 h +1 h +3828 m +3829 m +4 h +4 h +1 h +1 h +55 h +59 h +1 h +10 h +4 h +264 m +10 h +10 h +4 h +3830 m +3831 m +1 h +238 h +4 h +911 h +1 h +1 h +3832 m +4 h +1 h +11 h +55 h +11 h +57 h +4 h +3833 m +3834 m +2379 h +4 h +3835 m +4 h +467 m +3836 m +124 h +1 h +65 h +1 h +83 h +11 h +3837 m +4 h +250 h +31 h +1016 h +4 h +10 h +3838 m +94 h +313 h +4 h +1 h +1 h +1 h +10 h +4 h +4 h +173 h +4 h +1 h +3839 m +897 m +4 h +1 h +3840 m +1 h +4 h +4 h +718 h +3841 m +1 h +3842 m +83 h +195 h +3843 m +1 h +1 h +1 h +3844 m +3845 m +10 h +4 h +4 h +4 h +250 h +4 h +3846 m +4 h +359 h +1 h +1 h +109 h +1 h +4 h +8 h +2119 m +4 h +4 h +1 h +4 h +3847 m +4 h +1 h +4 h +82 h +4 h +3848 m +1 h +1 h +55 h +3849 m +3850 m +3851 m +82 h +1 h +10 h +1 h +3852 m +1 h +3853 m +25 h +10 h +4 h +3854 m +190 h +3855 m +3856 m +1 h +10 h +3857 m +265 h +1 h +3 h +10 h +31 h +1 h +2474 m +3858 m +3859 m +10 h +10 h +938 m +3860 m +3861 m +3862 m +104 h +3 h +2300 m +10 h +1 h +1 h +3863 m +4 h +169 h +4 h +181 h +808 h +3864 m +2101 m +3025 m +92 h +4 h +181 h +3865 m +1 h +69 h +4 h +23 m +125 h +57 h +3866 m +27 h +1 h +1 h +4 h +3867 m +1 h +3868 m +4 h +229 h +4 h +538 h +31 h +3869 m +10 h +4 h +3870 m +64 h +4 h +4 h +1 h +23 m +3558 m +10 h +4 h +4 h +1 h +10 h +297 h +4 h +1 h +109 h +4 h +4 h +33 h +1 h +4 h +4 h +1470 h +10 h +143 h +185 h +1 h +1 h +256 h +4 h +204 h +3871 m +3872 m +1 h +3873 m +125 h +885 h +11 h +3874 m +322 m +65 h +4 h +3875 m +258 h +3876 m +1835 h +10 h +4 h +4 h +238 h +1 h +4 h +11 h +4 h +4 h +4 h +1 h +4 h +190 h +4 h +41 h +1 h +769 m +4 h +3877 m +59 h +1 h +55 h +1 h +556 h +3878 m +77 h +104 h +3879 m +10 h +1 h +4 h +3880 m +3881 m +1 h +119 h +1 h +3882 m +1 h +1074 m +4 h +10 h +2300 m +3883 m +3884 m +278 h +3885 m +83 h +4 h +59 h +10 h +123 h +119 h +3886 m +1 h +4 h +313 h +4 h +10 h +3887 m +238 h +1 h +31 h +125 h +3888 m +10 h +10 h +4 h +717 m +13 h +4 h +57 h +3067 m +129 h +319 h +423 m +3889 m +3890 m +1 h +447 h +4 h +1 h +630 m +4 h +4 h +4 h +938 h +79 h +3891 m +1 h +1 h +97 h +10 h +59 h +10 h +4 h +3892 m +157 h +83 h +3893 m +3 h +11 h +3894 m +214 m +1 h +57 h +3895 m +4 h +41 h +83 h +11 h +3896 m +11 h +3897 m +258 h +59 h +10 h +3898 m +146 h +4 h +3 h +4 h +3899 m +4 h +4 h +4 h +3900 m +1016 h +40 h +520 h +1 h +3901 m +3902 m +1 h +687 h +3903 m +3904 m +10 h +2954 m +1 h +4 h +73 h +147 h +3905 m +3906 m +10 h +10 h +3907 m +10 h +1 h +73 h +124 h +1 h +10 h +3908 m +3089 m +73 h +11 h +4 h +1772 h +61 m +4 h +1 h +278 h +1 h +139 h +1 h +3909 m +10 h +1646 m +3910 m +3911 m +147 h +10 h +4 h +11 h +4 h +11 h +57 h +65 h +4 h +3912 m +119 h +4 h +3913 m +3914 m +4 h +1 h +1 h +31 h +3915 m +1359 h +195 h +10 h +4 h +10 h +10 h +3916 m +1089 h +10 h +278 h +3917 m +3918 m +11 h +3919 m +10 h +1 h +224 h +3920 m +10 h +3921 m +114 h +4 h +3922 m +3923 m +4 h +10 h +1 h +3924 m +3925 m +806 m +10 h +94 h +1 h +3926 m +10 h +736 m +11 h +581 m +4 h +10 h +104 h +3927 m +3 h +3928 m +4 h +3929 m +4 h +3930 m +124 h +4 h +10 h +36 h +1 h +125 h +1 h +4 h +13 h +114 h +1 h +82 h +3931 m +1 h +4 h +109 h +4 h +3932 m +3933 m +1 h +3934 m +1 h +11 h +477 m +3935 m +3 h +1 h +170 h +11 h +3936 m +1137 h +10 h +3937 m +36 h +31 h +82 h +3 h +10 h +1 h +1 h +1 h +41 h +10 h +3938 m +3939 m +11 h +3940 m +195 h +4 h +4 h +11 h +4 h +56 h +3941 m +11 h +1 h +4 h +4 h +10 h +1 h +3942 m +1344 m +10 h +4 h +97 h +13 h +4 h +10 h +4 h +1 h +83 h +74 h +236 m +10 h +1 h +1 h +3943 m +3944 m +10 h +4 h +1 h +10 h +1 h +74 h +82 h +4 h +3945 m +1114 m +3946 m +3947 m +10 h +3948 m +3949 m +4 h +3950 m +3951 m +386 h +36 h +3952 m +1 h +1 h +3953 m +10 h +10 h +1 h +1 h +3 h +10 h +1 h +4 h +4 h +1 h +74 h +4 h +83 h +10 h +3954 m +36 h +1 h +10 h +10 h +3955 m +1 h +10 h +704 h +3956 m +3957 m +1 h +27 h +195 h +124 h +1 h +10 h +1 h +3958 m +4 h +4 h +3959 m +1 h +3960 m +10 h +1 h +4 h +109 h +4 h +3961 m +10 h +2887 m +36 h +3962 m +4 h +1 h +57 h +4 h +83 h +10 h +3622 m +1 h +1650 m +195 h +1 h +4 h +57 h +25 h +3 h +3963 m +196 h +4 h +10 h +1 h +4 h +4 h +4 h +4 h +265 h +4 h +11 h +74 h +10 h +41 h +3964 m +3965 m +4 h +4 h +4 h +4 h +4 h +11 h +10 h +3966 m +11 h +10 h +241 h +1 h +3967 m +4 h +601 h +3968 m +10 h +10 h +3969 m +65 h +56 h +2205 m +1780 h +4 h +164 h +3970 m +4 h +3971 m +64 h +4 h +3972 m +104 h +289 h +3973 m +3974 m +146 h +1 h +10 h +1861 m +4 h +262 h +4 h +10 h +4 h +173 h +3975 m +3976 m +109 h +10 h +258 h +3977 m +4 h +4 h +10 h +4 h +1 h +1 h +4 h +125 h +146 h +124 h +57 h +10 h +97 h +3978 m +4 h +82 h +4 h +1 h +10 h +3979 m +1 h +123 h +1 h +3980 m +4 h +1 h +1 h +82 h +3981 m +3982 m +10 h +3983 m +8 h +4 h +10 h +1 h +10 h +4 h +3984 m +82 h +1 h +10 h +3985 m +4 h +1 h +1 h +3986 m +10 h +1 h +1822 h +4 h +4 h +4 h +4 h +3987 m +536 h +1 h +10 h +10 h +124 h +1642 m +23 h +3988 m +1 h +3989 m +48 h +3990 m +3991 m +135 h +57 h +3992 m +3993 m +1 h +1 h +11 h +3994 m +3 h +83 h +1218 m +3995 m +264 m +1 h +11 h +1822 h +1 h +10 h +1 h +3996 m +10 h +3997 m +10 h +40 h +3998 m +1 h +1 h +3999 m +12 h +3177 m +4 h +1 h +118 m +276 h +104 h +4 h +11 h +83 h +139 h +4000 m +10 h +4001 m +1137 h +4 h +173 h +4 h +4 h +27 h +976 h +4002 m +109 h +10 h +10 h +278 h +800 m +64 h +4 h +10 h +4 h +4003 m +1 h +59 h +1 h +4 h +4004 m +195 h +4 h +1 h +10 h +4 h +1685 m +4005 m +4 h +4 h +1 h +4006 m +1 h +5 m +4 h +4007 m +4 h +4008 m +59 h +10 h +158 h +109 h +1 h +4 h +10 h +763 m +4 h +4 h +1444 m +4 h +110 h +4 h +4 h +4 h +4 h +3 h +10 h +4 h +10 h +135 h +10 h +27 h +1 h +4 h +190 h +3 h +4009 m +1 h +1722 m +4010 m +147 h +4011 m +1 h +1 h +4012 m +4 h +10 h +10 h +4013 m +4014 m +12 h +4 h +23 h +4 h +83 h +4015 m +1 h +520 h +83 h +114 h +1 h +4016 m +59 h +692 h +1 h +83 h +114 h +1 h +4 h +79 h +12 h +114 h +1 h +4017 m +4 h +146 h +41 h +4 h +10 h +4018 m +1 h +4 h +10 h +4 h +94 h +10 h +124 h +747 m +4019 m +4020 m +1 h +10 h +4021 m +164 h +4 h +110 h +146 h +4022 m +4023 m +4 h +1 h +4024 m +82 h +1 h +620 m +1 h +4025 m +1 h +4026 m +4027 m +2002 m +10 h +4 h +4028 m +578 m +4 h +11 h +17 m +125 h +4029 m +4 h +59 h +4 h +10 h +1 h +169 h +4 h +4030 m +4 h +11 h +4 h +124 h +4 h +1 h +1 h +1 h +4 h +4031 m +4032 m +4 h +4033 m +83 h +1642 m +238 h +25 h +4 h +1 h +4034 m +4035 m +10 h +4036 m +10 h +4 h +1635 m +91 h +4037 m +355 m +4038 m +109 h +1 h +113 h +1 h +872 m +4 h +358 h +1 h +169 h +1 h +4 h +1 h +104 h +1 h +4 h +11 h +2347 m +4 h +4039 m +114 h +1 h +125 h +10 h +4040 m +10 h +4 h +190 h +4041 m +4042 m +4 h +4043 m +4 h +10 h +353 m +1 h +4044 m +10 h +1 h +4 h +1 h +4045 m +4046 m +83 h +1 h +4047 m +10 h +4 h +4 h +256 h +4048 m +1 h +1 h +11 h +10 h +65 h +10 h +297 h +10 h +10 h +10 h +97 h +4049 m +59 h +11 h +4050 m +4 h +10 h +4 h +74 h +1 h +4051 m +1 h +10 h +1 h +4 h +4 h +4052 m +4 h +1 h +4 h +4053 m +4054 m +1 h +4 h +1 h +976 h +119 h +4 h +11 h +4055 m +82 h +10 h +124 h +10 h +190 h +4056 m +1 h +4057 m +4058 m +1 h +10 h +59 h +3 h +140 m +65 h +221 m +1 h +4 h +10 h +10 h +4 h +59 h +10 h +4 h +4 h +4 h +258 h +10 h +4059 m +104 h +4060 m +4 h +4 h +4061 m +1 h +1 h +4062 m +73 h +82 h +4 h +4063 m +13 h +4 h +4 h +4064 m +4 h +82 h +4 h +1 h +4065 m +4066 m +10 h +779 h +4067 m +4 h +82 h +4 h +74 h +10 h +4 h +1 h +4068 m +4069 m +1 h +1 h +125 h +11 h +399 h +114 h +4 h +4070 m +4071 m +10 h +1655 m +4 h +11 h +4 h +278 h +1 h +1 h +27 h +4 h +65 h +4072 m +10 h +4 h +10 h +185 h +4 h +4073 m +41 h +4 h +1 h +1 h +4074 m +358 h +1 h +4 h +4075 m +10 h +1 h +170 h +4 h +4076 m +25 h +3 h +238 h +5 m +3 h +332 h +1 h +640 h +4 h +986 h +1 h +1 h +10 h +83 h +4 h +25 h +270 h +82 h +10 h +11 h +22 h +4 h +4 h +4 h +4 h +41 h +59 h +64 h +4 h +10 h +10 h +55 h +3342 m +1 h +3 h +4077 m +4078 m +82 h +4079 m +4 h +97 h +10 h +4 h +264 m +10 h +3 h +4 h +4 h +10 h +1 h +59 h +4 h +381 m +4 h +10 h +4 h +1 h +10 h +1454 m +4080 m +1 h +4081 m +92 h +1 h +118 m +57 h +4082 m +399 h +4083 m +1 h +10 h +4084 m +1 h +4 h +4 h +11 h +4 h +4 h +10 h +3048 m +4085 m +4 h +1 h +939 h +4086 m +4 h +1 h +2418 m +124 h +31 h +110 h +266 h +82 h +10 h +74 h +10 h +57 h +4087 m +4 h +4088 m +1 h +4 h +4 h +911 h +4089 m +4 h +4 h +10 h +172 h +1 h +4090 m +4 h +4 h +4 h +83 h +41 h +11 h +4091 m +4 h +4 h +4092 m +10 h +10 h +520 h +1 h +4093 m +146 h +112 h +4094 m +4095 m +10 h +10 h +4096 m +109 h +4097 m +1 h +10 h +1751 m +4 h +10 h +359 h +156 h +4 h +4098 m +4099 m +41 h +4100 m +1 h +57 h +4101 m +4 h +4102 m +1 h +1 h +36 h +10 h +1 h +1 h +10 h +1 h +125 h +55 h +4103 m +1 h +4104 m +4 h +358 h +12 h +10 h +4 h +139 h +4105 m +4106 m +4107 m +3 h +4108 m +1 h +4 h +82 h +10 h +4 h +4109 m +4110 m +4111 m +4 h +3 h +547 m +4112 m +4 h +11 h +278 h +4113 m +4 h +45 h +10 h +10 h +4 h +1 h +4 h +57 h +2606 m +4 h +4 h +10 h +185 h +4114 m +4115 m +4116 m +4117 m +27 h +4118 m +4119 m +3025 m +56 h +10 h +82 h +4 h +114 h +1 h +10 h +4120 m +59 h +10 h +1 h +11 h +104 h +10 h +10 h +124 h +146 h +167 h +12 h +4 h +195 h +4 h +10 h +4121 m +10 h +65 h +4 h +4122 m +4123 m +10 h +125 h +1 h +4124 m +79 h +4 h +1 h +1 h +4 h +113 h +124 h +4 h +4 h +12 h +124 h +1 h +57 h +4 h +4125 m +1308 m +10 h +1 h +4126 m +10 h +1 h +1 h +4127 m +1 h +4 h +4128 m +860 m +270 h +4 h +41 h +1564 m +4 h +10 h +1 h +4129 m +4 h +10 h +55 h +1 h +4 h +297 h +4130 m +10 h +4 h +1 h +1790 m +4 h +10 h +10 h +4131 m +1 h +1 h +22 h +31 h +4 h +10 h +4132 m +1 h +11 h +4 h +4133 m +4 h +1 h +109 h +1374 m +368 h +11 h +1 h +4134 m +59 h +4 h +10 h +1 h +4 h +114 h +4 h +4 h +10 h +147 h +4 h +2379 h +4 h +4135 m +4 h +4136 m +10 h +1 h +1 h +1403 h +488 h +4137 m +4 h +4138 m +10 h +4139 m +1 h +4 h +4140 m +10 h +3 h +493 m +4141 m +10 h +1 h +172 h +1 h +4142 m +4 h +10 h +4143 m +4144 m +4145 m +4 h +2087 m +368 h +1 h +73 h +1 h +4146 m +125 h +10 h +10 h +4 h +4147 m +1 h +4 h +4 h +1 h +1261 h +1 h +4148 m +11 h +4 h +1 h +1 h +10 h +4149 m +10 h +1 h +4150 m +757 m +949 m +4151 m +104 h +109 h +1 h +10 h +82 h +569 h +4 h +57 h +74 h +10 h +123 h +4152 m +10 h +4 h +4 h +4 h +4 h +4153 m +1 h +11 h +4154 m +2733 m +4155 m +1 h +10 h +57 h +97 h +4 h +4156 m +1914 m +224 h +4157 m +4158 m +1 h +1 h +4 h +4 h +316 m +4159 m +123 h +31 h +1261 h +31 h +10 h +104 h +1 h +4160 m +94 h +430 m +25 h +1 h +31 h +1835 h +10 h +170 h +1 h +125 h +57 h +1914 m +297 h +4 h +4161 m +11 h +1 h +10 h +4 h +1 h +3177 m +4 h +2215 m +1 h +4162 m +10 h +31 h +1 h +10 h +4163 m +64 h +1 h +4164 m +4 h +4165 m +10 h +4166 m +4 h +4167 m +83 h +1 h +73 h +1 h +27 h +11 h +4 h +11 h +195 h +104 h +843 m +10 h +57 h +147 h +278 h +195 h +3 h +4168 m +857 h +4169 m +10 h +4 h +4170 m +4171 m +1260 m +31 h +1 h +1 h +1 h +258 h +1 h +361 m +4 h +12 h +10 h +1 h +4 h +1 h +104 h +4 h +307 h +1 h +195 h +4172 m +1607 m +4173 m +27 h +4 h +27 h +692 h +447 h +4 h +4174 m +41 h +338 m +4 h +4175 m +1 h +687 h +4 h +4 h +3112 m +1 h +10 h +3272 m +4176 m +65 h +10 h +4177 m +8 h +164 h +4178 m +10 h +4 h +4 h +536 h +64 h +4179 m +13 h +1 h +10 h +12 h +4 h +10 h +10 h +262 h +4 h +125 h +1122 m +1 h +1470 h +45 h +4180 m +125 h +4 h +4 h +157 h +4181 m +10 h +1 h +4182 m +10 h +1 h +4183 m +4 h +1 h +1953 m +4184 m +4185 m +4186 m +4 h +976 h +1 h +3 h +185 h +1 h +4187 m +4188 m +79 h +4 h +82 h +12 h +339 m +4189 m +1 h +4190 m +10 h +1 h +11 h +4191 m +4192 m +4 h +4 h +4193 m +11 h +11 h +4194 m +10 h +4 h +4 h +56 h +4 h +158 h +10 h +4 h +110 h +1 h +10 h +4 h +1 h +10 h +1 h +82 h +4195 m +143 h +4196 m +4197 m +11 h +10 h +4198 m +1 h +1 h +338 m +371 h +10 h +57 h +4199 m +69 h +4 h +4200 m +10 h +10 h +13 h +1 h +4 h +1 h +195 h +353 m +109 h +4201 m +10 h +4 h +4 h +4 h +4 h +10 h +1 h +1 h +4202 m +4 h +10 h +1 h +10 h +994 m +4 h +4203 m +386 h +4 h +1 h +4204 m +135 h +4205 m +10 h +4 h +4206 m +31 h +4 h +1261 h +2964 m +383 h +12 h +4 h +1321 h +4207 m +10 h +4 h +4208 m +36 h +4209 m +4 h +4210 m +129 h +33 h +1 h +1 h +1 h +10 h +1 h +4211 m +82 h +4212 m +1250 h +4 h +4213 m +10 h +135 h +4214 m +4 h +13 h +4215 m +10 h +1 h +110 h +1822 h +10 h +184 h +4 h +4216 m +4 h +10 h +31 h +276 h +4217 m +10 h +1296 m +4218 m +4219 m +4220 m +10 h +4 h +41 h +1 h +10 h +770 m +167 h +4 h +1 h +1 h +4 h +4 h +4221 m +79 h +10 h +4 h +1 h +4 h +4 h +10 h +4222 m +265 h +4 h +1 h +104 h +4 h +1835 h +4223 m +1142 m +1 h +4 h +1 h +1 h +10 h +4224 m +124 h +4225 m +1 h +10 h +4 h +11 h +10 h +4226 m +1642 h +4227 m +11 h +4228 m +4 h +10 h +57 h +74 h +10 h +1 h +4229 m +4230 m +4 h +4 h +135 h +4 h +82 h +57 h +1 h +4 h +1 h +10 h +4 h +12 h +4 h +4231 m +4232 m +4 h +10 h +4233 m +4234 m +73 h +4235 m +4236 m +4237 m +656 m +4238 m +4239 m +1 h +4 h +4240 m +10 h +3221 m +4241 m +1 h +338 h +4 h +1 h +1 h +10 h +11 h +65 h +4242 m +4 h +258 h +3257 m +1 h +79 h +1 h +1 h +4 h +4243 m +10 h +4 h +1 h +10 h +4244 m +10 h +10 h +57 h +4 h +190 h +1 h +109 h +83 h +1 h +4245 m +11 h +1 h +4 h +386 h +4 h +83 h +124 h +4246 m +57 h +4247 m +4 h +238 h +10 h +897 h +82 h +10 h +4 h +1 h +1822 h +10 h +4 h +4248 m +12 h +1 h +4249 m +3 h +4 h +169 h +4250 m +4 h +65 h +4251 m +10 h +10 h +10 h +10 h +4 h +4 h +4252 m +1 h +11 h +10 h +1 h +1 h +4253 m +4 h +10 h +1 h +11 h +4 h +83 h +1 h +1137 h +139 h +83 h +4 h +4 h +10 h +1796 h +83 h +4254 m +10 h +4255 m +4256 m +10 h +10 h +11 h +1 h +4257 m +10 h +4 h +10 h +10 h +10 h +10 h +4258 m +4 h +4259 m +4260 m +4 h +278 h +138 h +1 h +4 h +4261 m +4 h +10 h +1 h +65 h +4 h +4 h +4 h +64 h +1 h +79 h +10 h +1 h +4 h +1 h +1 h +4 h +4 h +4262 m +1 h +4263 m +4264 m +10 h +92 h +1 h +1470 h +4265 m +4266 m +4 h +1 h +22 h +10 h +4267 m +4 h +4268 m +4 h +4269 m +1 h +10 h +4270 m +368 h +65 h +4 h +4 h +238 h +4 h +1 h +57 h +56 h +1 h +4 h +4 h +4271 m +124 h +1 h +11 h +4 h +4272 m +82 h +31 h +13 h +1 h +4 h +1 h +74 h +164 h +10 h +181 h +4273 m +4 h +123 h +11 h +1 h +4274 m +4275 m +82 h +4276 m +12 h +10 h +11 h +45 h +4277 m +11 h +4 h +59 h +45 h +1 h +4 h +1 h +4 h +10 h +1838 m +59 h +10 h +4 h +124 h +11 h +10 h +4278 m +10 h +4 h +1 h +74 h +1 h +11 h +4279 m +10 h +1 h +4280 m +4281 m +10 h +4282 m +4283 m +4 h +4284 m +4 h +13 h +266 h +4285 m +1 h +57 h +4286 m +4 h +10 h +4287 m +1 h +4 h +110 h +1 h +1 h +10 h +4288 m +1016 h +4289 m +169 h +1 h +13 h +1 h +82 h +4 h +10 h +1 h +1 h +3 h +1 h +83 h +136 m +1137 h +258 h +1619 m +267 m +25 h +11 h +10 h +4 h +4290 m +1766 h +4291 m +1 h +4292 m +22 h +4293 m +4 h +4 h +2733 m +74 h +1 h +2054 m +10 h +1 h +1 h +4 h +1 h +4294 m +1 h +4295 m +129 h +3 h +10 h +10 h +110 h +1 h +1 h +124 h +1 h +36 h +4296 m +4297 m +4 h +10 h +4298 m +1677 m +11 h +10 h +10 h +4299 m +10 h +56 h +4300 m +4301 m +1 h +1780 h +4302 m +10 h +1 h +31 h +31 h +114 h +10 h +4 h +4 h +4 h +4303 m +3 h +3 h +1 h +4 h +10 h +4304 m +4305 m +10 h +55 h +4 h +181 h +1201 m +274 h +4306 m +10 h +10 h +10 h +1 h +4 h +3 h +1 h +10 h +4 h +4307 m +1 h +110 h +4 h +55 h +79 h +278 h +157 h +4308 m +276 h +297 h +124 h +4 h +4 h +1 h +10 h +4309 m +10 h +82 h +4 h +1 h +65 h +59 h +25 h +184 h +129 h +196 h +1218 m +10 h +4310 m +4311 m +13 h +1 h +307 h +4312 m +4 h +4313 m +1 h +1 h +4314 m +2028 m +1 h +112 h +4315 m +3025 m +10 h +219 m +125 h +146 h +41 h +3 h +4316 m +1 h +146 h +1 h +11 h +4 h +4317 m +10 h +4318 m +1650 m +4319 m +10 h +4320 m +1 h +195 h +10 h +4 h +10 h +109 h +10 h +10 h +10 h +56 h +4321 m +10 h +1 h +4 h +195 h +1 h +11 h +10 h +4 h +4 h +65 h +10 h +170 h +4 h +4 h +4322 m +4323 m +4 h +4324 m +4 h +138 h +195 h +4325 m +1 h +135 h +4 h +59 h +79 h +10 h +195 h +4326 m +4 h +10 h +4 h +10 h +4327 m +1 h +4 h +139 h +4 h +4 h +3396 h +4 h +1 h +1 h +4 h +4328 m +64 h +10 h +295 h +10 h +278 h +358 h +15 m +4329 m +4330 m +1975 m +1 h +1607 m +1 h +82 h +11 h +4 h +4 h +1406 h +4331 m +10 h +4 h +4332 m +1 h +4333 m +4 h +1 h +4 h +4334 m +4 h +4335 m +45 h +4336 m +10 h +4337 m +45 h +538 h +4338 m +278 h +11 h +1 h +104 h +570 h +4339 m +1 h +4340 m +114 h +10 h +3 h +4341 m +1089 h +10 h +4 h +1 h +1 h +82 h +1642 h +195 h +4 h +1 h +1 h +59 h +129 h +297 h +4 h +4342 m +1 h +388 m +164 h +1 h +571 m +276 h +4343 m +4 h +4344 m +73 h +4 h +400 m +65 h +4 h +4 h +4345 m +22 h +4346 m +4 h +1 h +4 h +1 h +698 m +1 h +4347 m +4348 m +4 h +59 h +77 h +1 h +27 h +1 h +4 h +4 h +1 h +4 h +279 h +10 h +4 h +4 h +170 h +11 h +4349 m +2374 m +1196 m +1 h +4 h +4350 m +1 h +4 h +4 h +157 h +4 h +10 h +1 h +4 h +4 h +10 h +4256 m +25 h +1016 h +1 h +4351 m +4 h +125 h +1403 h +4352 m +181 h +4 h +4 h +4353 m +230 h +1796 h +443 h +4 h +195 h +297 h +1 h +41 h +1 h +4354 m +1220 m +10 h +4355 m +1 h +196 h +10 h +4356 m +1 h +757 m +4357 m +4 h +4 h +4 h +10 h +25 h +186 h +196 h +4358 m +4 h +4359 m +124 h +1198 m +4360 m +1 h +1 h +990 m +1 h +1 h +4 h +4361 m +125 h +359 h +4 h +278 h +4 h +4362 m +4363 m +4364 m +10 h +4 h +195 h +10 h +4365 m +4366 m +104 h +4367 m +10 h +10 h +10 h +147 h +1 h +1 h +4368 m +4 h +4 h +4369 m +4 h +82 h +22 h +57 h +5 h +367 h +164 h +4370 m +569 h +4 h +1 h +1 h +4371 m +4372 m +1 h +869 m +4 h +359 h +4373 m +1 h +91 h +4374 m +1 h +1 h +59 h +1 h +31 h +4 h +59 h +4375 m +4 h +1 h +1137 h +520 h +11 h +10 h +4 h +79 h +11 h +139 h +4 h +91 h +4376 m +488 h +307 h +10 h +4 h +4 h +1 h +4377 m +10 h +569 h +10 h +1 h +1 h +4378 m +82 h +1 h +4 h +4379 m +4 h +45 h +4 h +3209 m +1 h +3 h +10 h +4380 m +10 h +4 h +10 h +4 h +1 h +82 h +4381 m +258 h +109 h +147 h +2022 m +4382 m +4 h +4383 m +4384 m +330 m +57 h +4 h +11 h +4385 m +4386 m +10 h +1 h +4387 m +11 h +4388 m +3 h +82 h +4389 m +4 h +10 h +10 h +10 h +1 h +4390 m +173 h +1 h +4391 m +4 h +1 h +4392 m +1 h +4 h +1 h +1 h +1 h +83 h +4 h +1 h +192 h +109 h +4393 m +31 h +4394 m +1 h +1 h +10 h +4395 m +10 h +10 h +139 h +10 h +4 h +1 h +83 h +4396 m +10 h +1316 m +1 h +4397 m +10 h +4398 m +114 h +31 h +4 h +4 h +174 h +4399 m +10 h +146 h +4 h +4292 m +1 h +1137 h +4 h +4400 m +10 h +4 h +1 h +4 h +55 h +4401 m +1 h +10 h +10 h +10 h +10 h +10 h +10 h +10 h +59 h +11 h +4 h +4402 m +4 h +4403 m +4 h +83 h +1 h +1 h +4404 m +383 h +41 h +4 h +3398 m +1 h +82 h +4 h +10 h +4 h +4 h +1 h +3 h +83 h +10 h +1 h +1 h +4 h +4 h +1 h +4 h +173 h +332 h +4405 m +1 h +4 h +443 h +1 h +4406 m +4 h +4407 m +1 h +4408 m +4409 m +258 h +4 h +4410 m +4 h +820 m +1 h +146 h +4 h +1 h +10 h +10 h +4411 m +41 h +2002 m +4 h +1 h +4 h +1 h +10 h +4412 m +1 h +4 h +1 h +1 h +185 h +1 h +4 h +170 h +4 h +4 h +10 h +4 h +114 h +4 h +10 h +4 h +1 h +59 h +4413 m +1 h +2564 m +10 h +4 h +1 h +1 h +4 h +1659 m +11 h +1 h +4414 m +1 h +1 h +4 h +11 h +4415 m +447 h +4 h +4 h +4416 m +289 h +4 h +10 h +4 h +125 h +4 h +4 h +4 h +295 h +4417 m +1 h +114 h +1 h +172 h +4 h +4418 m +41 h +4419 m +4420 m +11 h +1201 m +4 h +4 h +4421 m +12 h +10 h +1 h +82 h +4 h +185 h +4 h +258 h +65 h +1643 m +1 h +4422 m +4423 m +842 m +1362 m +4424 m +4425 m +36 h +74 h +4 h +125 h +56 h +1 h +1 h +1650 m +10 h +10 h +45 h +4426 m +40 h +4427 m +4 h +4428 m +4429 m +1 h +307 h +10 h +59 h +4 h +4430 m +1 h +4431 m +83 h +4 h +4432 m +4433 m +125 h +1 h +1 h +3 h +1261 h +1 h +4 h +1 h +10 h +25 h +4434 m +92 h +10 h +995 m +10 h +4 h +1 h +31 h +4 h +124 h +82 h +1 h +4 h +10 h +4435 m +4 h +1 h +82 h +1 h +1 h +1 h +4 h +229 h +27 h +158 h +4 h +4 h +106 h +266 h +1 h +4436 m +4437 m +4 h +4 h +22 h +170 h +82 h +10 h +4 h +92 h +1 h +4 h +190 h +2788 m +897 h +4438 m +4 h +4 h +10 h +4 h +4 h +4 h +4 h +4439 m +10 h +1 h +11 h +4440 m +4 h +10 h +1 h +1 h +31 h +278 h +1 h +124 h +1 h +4 h +4441 m +1 h +4 h +10 h +4 h +10 h +1 h +4 h +92 h +31 h +10 h +4442 m +307 h +11 h +110 h +10 h +1 h +25 h +4 h +124 h +820 m +4 h +4 h +119 h +4 h +1 h +10 h +4 h +692 h +4 h +4 h +4 h +4443 m +3 h +4 h +1 h +4 h +31 h +4444 m +1 h +59 h +1 h +4445 m +1 h +4446 m +1 h +4 h +1027 h +2186 m +4 h +83 h +4447 m +4448 m +4 h +46 h +4 h +368 h +10 h +4 h +10 h +4 h +4 h +4 h +10 h +4449 m +74 h +4 h +25 h +4 h +104 h +4450 m +601 h +167 h +1 h +4297 m +1 h +4451 m +4452 m +4453 m +4454 m +65 h +4455 m +4456 m +4 h +4457 m +4 h +4 h +184 h +1261 h +4458 m +4459 m +147 h +4 h +4 h +125 h +4 h +1 h +124 h +10 h +4460 m +4461 m +10 h +1 h +4462 m +1 h +4463 m +11 h +65 h +169 h +4464 m +82 h +4 h +383 h +3376 m +10 h +8 h +10 h +435 m +1 h +4 h +4 h +1 h +1 h +10 h +4 h +4465 m +403 h +4466 m +4 h +4 h +4 h +4467 m +10 h +488 h +4 h +10 h +238 h +3 h +10 h +4468 m +28 h +10 h +295 h +1 h +4469 m +158 h +8 h +4470 m +4471 m +1766 h +1 h +1 h +4472 m +10 h +114 h +4 h +4 h +4 h +1 h +1 h +4 h +1 h +10 h +1 h +4473 m +4 h +332 h +92 h +1 h +1137 h +36 h +10 h +2794 m +10 h +10 h +4 h +4 h +4474 m +4475 m +4 h +4476 m +4477 m +1 h +13 h +104 h +109 h +10 h +109 h +1 h +4478 m +820 h +4 h +4 h +4479 m +4 h +4 h +79 h +146 h +4 h +578 m +125 h +266 h +10 h +4480 m +4 h +11 h +4481 m +2780 m +4 h +10 h +10 h +1 h +4482 m +4483 m +4484 m +94 h +4485 m +4486 m +41 h +167 h +146 h +4 h +10 h +10 h +1 h +1083 m +4 h +4487 m +1 h +1 h +1 h +10 h +1 h +82 h +4488 m +10 h +12 h +10 h +4 h +4 h +82 h +1780 h +4489 m +11 h +447 h +4 h +83 h +124 h +10 h +13 h +4490 m +1 h +4 h +10 h +4 h +10 h +146 h +4491 m +4492 m +4493 m +1 h +4 h +4494 m +10 h +10 h +125 h +4495 m +10 h +10 h +125 h +4 h +82 h +425 m +4 h +56 h +10 h +1128 m +46 h +986 h +146 h +11 h +266 h +3 h +4496 m +4497 m +4498 m +4499 m +108 h +10 h +4500 m +83 h +2931 m +4 h +4501 m +36 h +10 h +4 h +10 h +4 h +27 h +1 h +4 h +70 m +1 h +25 h +10 h +332 h +10 h +4502 m +4503 m +10 h +11 h +1 h +1 h +1 h +313 h +109 h +1 h +4504 m +10 h +94 h +4 h +4505 m +10 h +1650 h +3 h +4506 m +4507 m +1 h +112 h +4508 m +83 h +258 h +10 h +3 h +1 h +1 h +11 h +1 h +4 h +1 h +4 h +124 h +4 h +4509 m +12 h +48 h +1 h +11 h +10 h +1 h +4 h +4510 m +4511 m +65 h +4 h +4 h +1 h +4 h +4512 m +4 h +4513 m +4 h +1250 h +124 h +1 h +4 h +55 h +4 h +4 h +1 h +4514 m +4 h +108 h +57 h +10 h +4515 m +4516 m +4517 m +4 h +10 h +156 h +1 h +164 h +4518 m +4 h +10 h +4 h +4519 m +82 h +4 h +1 h +4520 m +82 h +4 h +4 h +170 h +4521 m +1030 h +157 h +1 h +11 h +4522 m +4523 m +4524 m +4 h +4 h +4 h +10 h +56 h +65 h +10 h +1 h +359 h +1 h +4 h +4 h +195 h +59 h +65 h +4525 m +108 h +114 h +10 h +4526 m +4527 m +1 h +1 h +4 h +1362 h +1 h +1 h +1 h +4 h +1309 m +4528 m +2733 h +10 h +174 h +1003 h +4 h +1027 h +4529 m +276 h +4530 m +4 h +4531 m +4532 m +4 h +1089 h +1 h +4 h +109 h +4533 m +4534 m +520 h +10 h +4535 m +10 h +4 h +1260 m +1 h +11 h +626 m +4 h +4 h +1375 m +601 h +4 h +1 h +146 h +10 h +4536 m +79 h +170 h +4537 m +4538 m +4539 m +10 h +139 h +124 h +4540 m +25 h +10 h +3 h +4 h +4541 m +82 h +146 h +4542 m +4 h +4 h +25 h +185 h +4543 m +4 h +10 h +443 h +1 h +359 h +8 h +278 h +10 h +83 h +4544 m +4 h +10 h +10 h +4545 m +57 h +4546 m +11 h +1 h +557 m +1 h +4547 m +954 m +1 h +1 h +4 h +1548 m +112 h +4548 m +2494 m +4549 m +4550 m +4 h +4 h +10 h +57 h +857 h +4551 m +73 h +358 h +1 h +10 h +1 h +297 h +4552 m +4 h +4 h +307 h +4 h +4553 m +1 h +124 h +1 h +59 h +10 h +4554 m +10 h +4 h +4555 m +4 h +1 h +4 h +332 h +138 h +4 h +1 h +1 h +1 h +1 h +4556 m +79 h +355 m +10 h +1418 m +11 h +4 h +939 h +1137 h +118 h +12 h +575 h +10 h +172 h +1 h +4 h +4 h +10 h +10 h +4 h +4557 m +1016 h +186 h +4 h +10 h +10 h +11 h +4558 m +4559 m +8 h +10 h +10 h +4560 m +10 h +4 h +1 h +4 h +238 h +1 h +4 h +4561 m +4562 m +4 h +57 h +1 h +1 h +108 h +1 h +135 h +11 h +1 h +1 h +10 h +4563 m +1 h +97 h +4564 m +4 h +4565 m +4 h +1 h +4566 m +10 h +1 h +10 h +4567 m +4 h +4568 m +4569 m +4570 m +82 h +4571 m +1 h +1 h +10 h +113 h +4 h +109 h +83 h +4 h +5 h +4572 m +986 h +1 h +114 h +1 h +1 h +4573 m +4574 m +4 h +11 h +185 h +11 h +169 h +4 h +1 h +4575 m +332 h +11 h +4576 m +104 h +1 h +4577 m +4 h +4 h +4 h +1564 m +1 h +4 h +4 h +10 h +10 h +4 h +4 h +125 h +1359 h +59 h +73 h +4578 m +11 h +10 h +25 h +4 h +1 h +73 h +40 h +307 h +1 h +138 h +4579 m +104 h +10 h +64 h +1 h +1 h +4580 m +82 h +4 h +4 h +4581 m +1083 m +4 h +4582 m +109 h +125 h +79 h +10 h +45 h +10 h +1 h +10 h +119 h +4583 m +10 h +82 h +4584 m +203 m +4585 m +27 h +857 h +4586 m +65 h +1 h +4587 m +566 m +11 h +4 h +4588 m +4589 m +65 h +11 h +4590 m +4 h +1 h +4 h +4 h +4591 m +97 h +4 h +4 h +10 h +4592 m +109 h +4 h +4593 m +57 h +10 h +4594 m +77 h +1 h +10 h +1 h +4 h +157 h +1 h +1 h +4 h +4 h +1201 h +10 h +4 h +1 h +4 h +4 h +1105 m +10 h +1 h +57 h +185 h +4595 m +4596 m +4 h +143 h +147 h +4597 m +3 h +83 h +4598 m +195 h +4 h +1 h +143 h +172 h +4 h +195 h +4 h +4 h +1 h +4 h +1 h +10 h +146 h +1 h +279 h +3 h +270 h +4599 m +97 h +4600 m +4 h +10 h +4 h +4 h +4 h +4601 m +4 h +110 h +4602 m +10 h +1 h +10 h +4 h +4 h +4603 m +4 h +4604 m +1 h +57 h +4605 m +109 h +139 h +4292 h +10 h +258 h +4606 m +1 h +10 h +4607 m +4608 m +41 h +82 h +274 h +266 h +4609 m +10 h +10 h +11 h +1 h +4 h +4 h +687 h +4 h +1 h +10 h +4 h +4 h +190 h +135 h +56 h +4610 m +351 m +82 h +4 h +4 h +4 h +4611 m +4 h +10 h +4 h +12 h +59 h +1 h +13 h +1 h +31 h +1 h +4612 m +97 h +1 h +4 h +48 h +4613 m +986 h +1 h +94 h +10 h +4614 m +10 h +281 m +4615 m +1 h +79 h +673 m +4616 m +109 h +10 h +4 h +64 h +4617 m +192 h +10 h +4618 m +10 h +1868 m +1261 h +4619 m +4620 m +114 h +4 h +10 h +4 h +11 h +1 h +4621 m +170 h +1 h +83 h +4 h +4622 m +351 m +4623 m +79 h +1083 h +4 h +538 h +4624 m +4 h +4625 m +10 h +59 h +4626 m +10 h +4627 m +1 h +4628 m +1220 m +1 h +1 h +4629 m +4630 m +4631 m +4 h +8 h +4632 m +4 h +4633 m +173 h +4634 m +4635 m +4636 m +716 m +10 h +4637 m +41 h +4638 m +146 h +1261 h +10 h +3 h +307 h +4 h +4 h +10 h +1 h +45 h +3742 m +10 h +383 h +10 h +4 h +4 h +1 h +11 h +8 h +4639 m +10 h +4 h +1 h +4640 m +10 h +4641 m +135 h +10 h +4642 m +1 h +4 h +1185 m +10 h +4 h +4 h +4 h +4 h +4643 m +4644 m +4 h +4 h +1 h +1 h +4 h +4645 m +124 h +4 h +10 h +4646 m +4 h +10 h +1 h +4 h +41 h +541 h +1 h +4647 m +1 h +4 h +11 h +4648 m +4649 m +1 h +4650 m +1 h +4651 m +1 h +4652 m +4653 m +22 h +4 h +41 h +1 h +4 h +465 m +10 h +1 h +4 h +10 h +258 h +10 h +4 h +4654 m +4 h +1 h +4655 m +82 h +1 h +4656 m +1 h +4657 m +10 h +4 h +4658 m +4 h +4 h +55 h +4659 m +1 h +4 h +4660 m +109 h +59 h +31 h +4608 m +2925 m +11 h +1 h +1 h +4 h +4 h +4661 m +4 h +10 h +276 h +4662 m +59 h +4663 m +1 h +10 h +4664 m +82 h +1 h +4 h +266 h +1 h +4665 m +4666 m +83 h +4667 m +1 h +22 h +4668 m +181 h +57 h +10 h +1 h +1045 m +1 h +1571 m +1759 m +10 h +3 h +4 h +125 h +4 h +10 h +4669 m +4 h +1 h +4670 m +4 h +1861 m +65 h +4 h +124 h +4 h +1 h +4671 m +489 m +1 h +31 h +4 h +135 h +4 h +10 h +489 h +1650 h +4 h +1 h +1 h +4672 m +48 h +1 h +4673 m +1 h +4674 m +278 h +4 h +10 h +40 h +4675 m +1 h +278 h +1 h +4676 m +4677 m +1 h +82 h +332 h +12 h +4678 m +4679 m +4680 m +4681 m +4 h +4682 m +4 h +4683 m +59 h +10 h +4684 m +82 h +1 h +4 h +10 h +4 h +10 h +1 h +1 h +368 h +4685 m +195 h +10 h +4 h +4 h +1403 h +22 h +1261 h +1 h +11 h +4 h +92 h +4686 m +10 h +185 h +4 h +10 h +195 h +1430 m +1 h +1 h +4687 m +986 h +4688 m +11 h +463 m +1 h +297 h +4689 m +4 h +1 h +1 h +1851 m +4 h +10 h +4 h +601 h +4690 m +59 h +10 h +3177 m +1403 h +4 h +4691 m +65 h +10 h +4692 m +4693 m +65 h +124 h +1 h +82 h +4694 m +4695 m +4696 m +4 h +4 h +11 h +4 h +65 h +4 h +911 h +1 h +196 h +10 h +4697 m +4698 m +4699 m +10 h +4 h +1 h +11 h +59 h +10 h +1 h +1 h +4 h +4 h +1 h +10 h +74 h +4 h +1 h +4 h +4700 m +114 h +41 h +139 h +4701 m +4 h +258 h +10 h +11 h +4 h +4 h +4 h +4702 m +10 h +57 h +4 h +1 h +4 h +10 h +1 h +1 h +238 h +4703 m +59 h +4 h +1 h +1 h +10 h +1 h +10 h +4704 m +4 h +400 m +1 h +94 h +1 h +4705 m +1 h +4706 m +1 h +4707 m +12 h +10 h +4708 m +97 h +976 h +4 h +3702 m +4 h +1 h +10 h +386 h +4709 m +4710 m +4711 m +104 h +10 h +493 m +11 h +10 h +4712 m +4 h +83 h +322 m +186 h +1 h +1 h +1 h +10 h +1691 m +4713 m +1 h +4714 m +10 h +84 m +536 h +823 m +125 h +11 h +10 h +10 h +1 h +1 h +97 h +1 h +4 h +10 h +1751 m +124 h +4 h +4 h +3679 m +8 h +1449 m +146 h +4 h +4 h +1 h +123 h +125 h +10 h +10 h +1 h +10 h +4715 m +4716 m +27 h +1 h +10 h +1 h +4 h +4717 m +1 h +31 h +83 h +97 h +1 h +10 h +1 h +1 h +976 h +68 m +1 h +4 h +4718 m +83 h +164 h +4719 m +1556 m +4720 m +4 h +4 h +481 m +119 h +4721 m +4722 m +11 h +10 h +48 h +125 h +4723 m +10 h +4724 m +12 h +25 h +10 h +1 h +10 h +204 h +1 h +2719 m +4 h +11 h +4 h +4 h +1 h +94 h +1 h +11 h +4725 m +4726 m +4727 m +10 h +4728 m +1 h +4256 h +172 h +4 h +4729 m +1 h +4730 m +10 h +11 h +10 h +1 h +4731 m +140 m +4 h +4732 m +4733 m +4734 m +10 h +1 h +1838 m +4735 m +1 h +4736 m +113 h +4 h +4 h +386 h +55 h +1045 m +4737 m +10 h +10 h +4 h +4738 m +27 h +45 h +10 h +4739 m +1 h +1 h +4740 m +4741 m +2617 m +170 h +4 h +4742 m +3 h +64 h +4 h +911 h +2041 m +1 h +4 h +57 h +10 h +45 h +4 h +4743 m +11 h +1 h +4 h +36 h +1 h +11 h +258 h +74 h +1884 m +4744 m +4 h +8 h +1 h +1 h +4745 m +304 m +144 h +4 h +1 h +4746 m +83 h +4 h +4 h +109 h +1 h +41 h +4 h +601 h +1 h +4747 m +10 h +83 h +36 h +4 h +4 h +4 h +4 h +3 h +3396 h +4748 m +4 h +4 h +4 h +4749 m +4750 m +10 h +4 h +4 h +10 h +359 h +84 h +4 h +72 m +11 h +4 h +4751 m +4752 m +1 h +97 h +4 h +322 m +10 h +1 h +10 h +2923 m +123 h +4753 m +4 h +10 h +124 h +1074 m +1 h +1 h +4754 m +4 h +10 h +1 h +10 h +4755 m +4756 m +1 h +55 h +1770 m +4757 m +11 h +73 h +1 h +10 h +73 h +59 h +4256 h +1 h +4758 m +4759 m +4 h +4760 m +1 h +77 h +1 h +1 h +4761 m +1 h +1 h +4 h +124 h +1 h +83 h +1 h +27 h +2447 m +1 h +4 h +83 h +94 h +10 h +4 h +1 h +1 h +10 h +4111 m +10 h +1 h +8 h +170 h +4 h +4762 m +61 m +10 h +1861 m +4 h +10 h +11 h +4763 m +10 h +4 h +3 h +1 h +82 h +1 h +4764 m +1 h +1 h +4765 m +4 h +10 h +238 h +10 h +10 h +1 h +1 h +10 h +258 h +4 h +10 h +4766 m +10 h +10 h +4 h +55 h +4767 m +146 h +27 h +4768 m +1 h +10 h +295 h +4769 m +4770 m +297 h +57 h +10 h +10 h +12 h +4 h +1 h +10 h +172 h +4771 m +10 h +4772 m +386 h +4 h +601 h +278 h +104 h +1 h +1 h +4 h +195 h +569 h +109 h +41 h +10 h +1 h +10 h +4 h +4 h +4773 m +230 h +195 h +31 h +4774 m +4775 m +4776 m +10 h +10 h +4777 m +4778 m +56 h +1 h +4202 m +3 h +10 h +4779 m +4 h +4780 m +1 h +11 h +1 h +184 h +4781 m +41 h +4 h +1 h +1 h +626 m +1 h +1 h +4 h +82 h +1 h +28 h +4 h +1 h +4 h +1 h +270 h +1 h +4 h +1 h +1 h +4 h +4 h +297 h +27 h +4782 m +10 h +339 m +156 h +10 h +4 h +1650 h +1 h +4783 m +4 h +1 h +196 h +1 h +10 h +4 h +1454 m +4 h +185 h +4297 h +4 h +1 h +4784 m +4 h +4 h +1 h +11 h +4 h +4 h +196 h +281 m +4 h +10 h +4 h +57 h +4 h +10 h +266 h +10 h +4 h +184 h +41 h +1 h +69 h +169 h +11 h +82 h +41 h +4785 m +4 h +4 h +65 h +4786 m +10 h +976 h +4 h +4 h +4 h +4 h +27 h +4787 m +10 h +4 h +4788 m +4789 m +11 h +4 h +4 h +4790 m +4 h +10 h +4 h +3904 m +1 h +2887 m +1 h +11 h +4 h +82 h +1 h +4791 m +110 h +4792 m +4 h +4793 m +10 h +4 h +4 h +143 h +54 m +4 h +10 h +4794 m +4 h +4 h +1 h +4 h +1 h +443 h +11 h +10 h +83 h +147 h +4 h +4795 m +65 h +4 h +279 h +10 h +1 h +4 h +1 h +65 h +4796 m +1 h +55 h +41 h +4 h +4 h +104 h +4 h +3 h +4797 m +195 h +1 h +4798 m +1508 m +1 h +9 m +4799 m +13 h +4800 m +10 h +1 h +4 h +25 h +4801 m +1 h +1089 h +4802 m +1 h +4 h +1 h +4 h +4803 m +11 h +4804 m +4 h +10 h +4805 m +108 h +4806 m +4807 m +124 h +1 h +82 h +258 h +82 h +1 h +57 h +83 h +1 h +10 h +172 h +3768 m +4 h +10 h +1 h +73 h +4 h +4808 m +4809 m +4 h +4 h +4 h +92 h +4 h +45 h +4 h +11 h +4 h +10 h +1379 m +1 h +4810 m +4 h +4811 m +10 h +4812 m +4 h +4 h +109 h +4813 m +10 h +10 h +4 h +1 h +4 h +11 h +10 h +4 h +109 h +4 h +4814 m +443 h +36 h +258 h +1 h +1 h +1 h +10 h +10 h +4815 m +1 h +4816 m +1 h +4817 m +687 h +10 h +1 h +1 h +601 h +4 h +4818 m +4 h +11 h +4 h +4 h +4819 m +1 h +4 h +4 h +4820 m +4 h +10 h +1024 m +1 h +10 h +4821 m +230 h +4 h +4 h +10 h +1 h +4822 m +31 h +11 h +1 h +4 h +1 h +1 h +59 h +4 h +279 h +1 h +31 h +1 h +1116 m +1 h +31 h +61 m +146 h +1 h +4 h +1403 h +1 h +1 h +4 h +2002 m +4823 m +74 h +4824 m +4 h +4 h +170 h +939 h +4 h +1 h +4825 m +1309 m +338 h +10 h +4 h +41 h +10 h +4 h +4826 m +1 h +1 h +1016 h +4827 m +4828 m +4 h +4829 m +1 h +4830 m +1030 h +4 h +195 h +123 h +11 h +4831 m +10 h +123 h +10 h +10 h +40 h +4832 m +4 h +4833 m +4 h +1 h +4 h +1316 m +124 h +1 h +358 h +184 h +1 h +4 h +57 h +319 h +4834 m +1 h +258 h +4 h +964 m +976 h +4 h +4 h +10 h +4 h +4 h +4 h +4 h +4 h +4835 m +4836 m +4 h +1 h +386 h +4 h +4837 m +79 h +10 h +4 h +4 h +4 h +4 h +10 h +4838 m +10 h +4 h +10 h +4839 m +2558 m +4 h +4724 m +4840 m +10 h +976 h +4 h +1 h +371 h +4514 m +4 h +10 h +4 h +65 h +124 h +94 h +5 h +119 h +4 h +4 h +110 h +4841 m +4 h +10 h +10 h +4 h +10 h +10 h +195 h +10 h +4842 m +1 h +4 h +13 h +4843 m +4 h +55 h +10 h +4844 m +265 h +10 h +278 h +4845 m +999 m +74 h +493 m +1 h +1 h +4846 m +1 h +4847 m +4 h +1 h +2379 h +4848 m +10 h +4849 m +10 h +10 h +4 h +4850 m +1016 h +83 h +10 h +4851 m +1 h +266 h +10 h +4852 m +11 h +1 h +4853 m +1 h +1 h +4854 m +17 m +119 h +965 m +4 h +1 h +4 h +1 h +4 h +1 h +10 h +59 h +97 h +4855 m +65 h +11 h +124 h +4856 m +1 h +57 h +575 h +4 h +1027 h +1 h +1 h +4857 m +601 h +1508 m +10 h +4858 m +4 h +4859 m +74 h +4 h +4860 m +10 h +125 h +757 m +4 h +79 h +4 h +4861 m +4862 m +443 h +41 h +4 h +1 h +4 h +10 h +4 h +1 h +4 h +4863 m +1 h +4 h +230 h +73 h +1 h +10 h +11 h +4 h +4864 m +4865 m +82 h +10 h +4 h +4866 m +1 h +332 h +1 h +4 h +4 h +307 h +4 h +57 h +4867 m +4868 m +4869 m +10 h +10 h +1 h +57 h +57 h +1 h +4870 m +1 h +1 h +2887 m +1 h +4 h +4 h +3050 m +1 h +10 h +4871 m +4872 m +4873 m +1344 m +1 h +4874 m +4 h +4 h +1 h +4875 m +41 h +181 h +4876 m +10 h +11 h +1 h +718 m +10 h +4 h +536 h +59 h +1 h +4 h +1 h +4877 m +4 h +4 h +4 h +4 h +1 h +10 h +92 h +97 h +4878 m +4879 m +10 h +1 h +4 h +656 m +1128 m +4 h +4880 m +1 h +10 h +97 h +11 h +124 h +1 h +1 h +11 h +3 h +4881 m +4 h +4 h +4 h +4882 m +4883 m +4 h +10 h +4 h +10 h +4 h +10 h +4 h +307 h +1 h +172 h +1 h +1 h +1 h +4 h +13 h +25 h +224 h +109 h +4 h +4884 m +109 h +10 h +11 h +1 h +4 h +12 h +4 h +1 h +147 h +4 h +10 h +3 h +74 h +4885 m +119 h +1470 h +332 h +4886 m +1359 h +57 h +4887 m +1 h +4 h +4888 m +147 h +1 h +10 h +10 h +4889 m +4 h +4890 m +506 m +726 m +238 h +83 h +36 h +1 h +82 h +10 h +4891 m +55 h +104 h +10 h +4 h +4 h +57 h +119 h +1 h +10 h +10 h +4892 m +238 h +57 h +4 h +4893 m +10 h +4 h +4894 m +1 h +97 h +109 h +11 h +13 h +4 h +4895 m +1 h +4 h +1 h +4896 m +1 h +4897 m +170 h +156 h +139 h +4 h +4898 m +3 h +4 h +10 h +65 h +10 h +4 h +1 h +4899 m +1105 m +57 h +25 h +1 h +73 h +3435 m +4900 m +4901 m +1 h +4 h +1 h +4902 m +143 h +10 h +1 h +1 h +4903 m +97 h +4 h +82 h +1016 h +4 h +4 h +258 h +425 m +114 h +4904 m +4905 m +4906 m +4 h +4907 m +12 h +1 h +4908 m +229 h +1 h +569 h +10 h +10 h +4 h +10 h +4909 m +10 h +4910 m +4 h +1 h +4 h +4 h +1 h +11 h +4 h +4 h +143 h +1 h +73 h +113 h +4911 m +10 h +996 m +4912 m +57 h +4 h +4 h +4913 m +181 h +1 h +4 h +10 h +4 h +124 h +11 h +1 h +1261 h +4 h +4914 m +4 h +92 h +1 h +1 h +4915 m +167 h +59 h +4 h +57 h +10 h +25 h +45 h +4916 m +10 h +10 h +10 h +371 h +4 h +1 h +82 h +1 h +4 h +4917 m +1 h +4 h +83 h +4 h +4918 m +4919 m +10 h +97 h +1 h +4 h +12 h +1 h +307 h +1 h +4920 m +10 h +4 h +10 h +4921 m +1 h +124 h +4 h +4922 m +124 h +4 h +83 h +23 h +1 h +4 h +22 h +1 h +11 h +82 h +195 h +4 h +4923 m +4 h +146 h +4924 m +258 h +4 h +3 h +3025 m +4 h +4 h +146 h +4 h +10 h +4 h +4925 m +144 h +1 h +10 h +4 h +4 h +4 h +4 h +1772 h +4 h +4 h +69 h +4 h +4926 m +2887 h +147 h +10 h +1 h +4927 m +399 h +4 h +57 h +4928 m +1027 h +1030 h +4 h +238 h +4 h +4 h +1 h +1089 h +10 h +1 h +4929 m +4930 m +1 h +10 h +4931 m +1 h +1 h +4932 m +12 h +994 m +104 h +1 h +4933 m +4 h +4934 m +4 h +57 h +4 h +4935 m +4 h +4 h +4936 m +196 h +4937 m +10 h +1 h +1 h +4 h +4938 m +10 h +83 h +4939 m +13 h +1 h +4940 m +4 h +169 h +4941 m +190 h +27 h +4929 m +4 h +4 h +347 m +4942 m +1650 h +10 h +4943 m +124 h +57 h +1 h +4944 m +169 h +4 h +4 h +4945 m +10 h +167 h +1 h +4 h +109 h +1 h +10 h +4 h +4 h +10 h +1 h +27 h +135 h +1 h +4946 m +4947 m +1 h +4297 h +1 h +104 h +4948 m +4949 m +22 h +1 h +4950 m +4 h +172 h +10 h +4 h +976 h +11 h +1499 m +1 h +11 h +1 h +4 h +10 h +59 h +119 h +74 h +4520 m +13 h +425 m +82 h +10 h +1309 m +146 h +4951 m +4952 m +125 h +10 h +1 h +186 h +57 h +4953 m +4954 m +1737 m +4 h +1576 m +10 h +4955 m +11 h +1 h +4 h +104 h +1105 m +4 h +1 h +4956 m +57 h +477 m +2148 m +1 h +4 h +1780 h +566 m +2625 m +1 h +204 h +4 h +4 h +10 h +4957 m +4 h +1 h +125 h +4958 m +1293 m +4 h +10 h +4959 m +73 h +4 h +104 h +1 h +10 h +147 h +1 h +10 h +181 h +258 h +4960 m +10 h +4 h +82 h +1137 h +4961 m +4962 m +4963 m +1 h +238 h +1714 m +1975 m +10 h +4 h +64 h +256 h +4 h +4964 m +1 h +4 h +170 h +82 h +195 h +1 h +92 h +4 h +97 h +4965 m +4 h +1105 h +717 m +4966 m +135 h +1 h +4 h +4967 m +4968 m +74 h +1 h +74 h +156 h +4969 m +31 h +1 h +41 h +4970 m +1 h +4 h +4 h +307 h +82 h +11 h +4971 m +10 h +4 h +4972 m +347 m +10 h +8 h +4 h +3 h +4973 m +10 h +70 m +359 h +1 h +10 h +493 h +1 h +4 h +10 h +1137 h +1105 h +4 h +4 h +56 h +4974 m +195 h +10 h +41 h +1 h +4975 m +4 h +1 h +4 h +10 h +1 h +4 h +4 h +1 h +1 h +1 h +4 h +1 h +1470 h +4 h +1 h +4 h +4 h +625 m +4976 m +172 h +64 h +27 h +1 h +196 h +4 h +4 h +4977 m +1 h +4978 m +4 h +4 h +124 h +4979 m +1 h +4 h +10 h +10 h +4 h +124 h +1337 m +4 h +1 h +31 h +1 h +186 h +1 h +12 h +10 h +92 h +4980 m +4 h +4 h +41 h +11 h +4441 m +10 h +4 h +1 h +112 h +4981 m +297 h +4982 m +4 h +4 h +4 h +79 h +83 h +1 h +3558 m +1379 m +4 h +10 h +57 h +1 h +4983 m +4984 m +4 h +83 h +4985 m +869 m +10 h +1 h +1 h +1 h +4986 m +4 h +41 h +4987 m +10 h +10 h +4988 m +4989 m +717 m +1650 h +10 h +46 h +13 h +4 h +1089 h +10 h +265 h +11 h +4990 m +109 h +4 h +1 h +10 h +4 h +4991 m +1 h +1 h +186 h +91 h +4992 m +10 h +1 h +2148 m +36 h +195 h +4993 m +4994 m +11 h +4995 m +25 h +1 h +4 h +1 h +82 h +1 h +4 h +4996 m +4997 m +1 h +4998 m +4999 m +10 h +10 h +5000 m +4 h +1 h +5001 m +5002 m +4 h +4 h +272 m +5003 m +1 h +1 h +5004 m +1 h +10 h +4 h +83 h +4 h +5005 m +92 h +3 h +4 h +11 h +4 h +4 h +4 h +1 h +57 h +4 h +1 h +434 m +5006 m +4 h +4 h +4 h +1 h +1 h +4 h +169 h +5007 m +4 h +4 h +5008 m +104 h +4 h +1 h +1 h +10 h +1 h +25 h +5009 m +10 h +10 h +109 h +4 h +5010 m +1898 m +10 h +5011 m +5012 m +4 h +10 h +1 h +509 m +4 h +1 h +1 h +5013 m +5014 m +74 h +1 h +114 h +3 h +1027 h +1337 m +5015 m +10 h +4 h +4 h +5016 m +307 h +4 h +1 h +1016 h +144 h +11 h +4 h +5017 m +190 h +5018 m +2379 h +10 h +1 h +5019 m +57 h +10 h +135 h +1 h +5020 m +1 h +167 h +4 h +4 h +5021 m +10 h +41 h +143 h +10 h +10 h +147 h +1250 h +1 h +5022 m +5023 m +1 h +10 h +1 h +5024 m +5025 m +338 h +74 h +4 h +1 h +10 h +10 h +61 h +307 h +135 h +10 h +10 h +10 h +1 h +125 h +41 h +5026 m +167 h +5027 m +11 h +4 h +276 h +5028 m +25 h +5029 m +4 h +5030 m +195 h +4 h +10 h +124 h +716 m +224 h +10 h +4 h +5031 m +10 h +4 h +195 h +276 h +5032 m +5033 m +5034 m +1261 h +1 h +5035 m +10 h +1 h +5036 m +276 h +25 h +1 h +278 h +5037 m +10 h +4 h +359 h +3 h +4 h +4 h +109 h +5038 m +556 h +692 h +1 h +4 h +1 h +4 h +1 h +10 h +196 h +5039 m +3 h +4 h +4 h +55 h +1 h +1 h +4 h +5040 m +4 h +10 h +190 h +5041 m +1 h +359 h +5042 m +11 h +170 h +4 h +11 h +143 h +276 h +1 h +4 h +74 h +10 h +31 h +1 h +10 h +10 h +57 h +4 h +4 h +5043 m +4 h +5044 m +4 h +5045 m +10 h +5046 m +4 h +167 h +5047 m +147 h +65 h +5048 m +443 h +4 h +3 h +4 h +10 h +4 h +1 h +10 h +5049 m +10 h +5050 m +4 h +129 h +464 m +10 h +869 m +1 h +3338 m +1 h +570 h +57 h +4 h +5051 m +143 h +5052 m +109 h +31 h +1 h +1 h +4 h +4 h +5053 m +170 h +4 h +4 h +4 h +10 h +114 h +10 h +4464 m +45 h +10 h +10 h +4 h +4 h +5054 m +4 h +5055 m +5056 m +1 h +1 h +5057 m +5058 m +83 h +5059 m +5060 m +3 h +737 m +4 h +5061 m +11 h +4 h +10 h +77 h +1281 m +4 h +4 h +687 h +5062 m +123 h +4 h +11 h +4 h +4 h +41 h +1 h +10 h +83 h +4 h +10 h +4 h +1790 m +112 h +10 h +10 h +82 h +5063 m +4 h +13 h +4 h +1089 h +307 h +83 h +4 h +1 h +4 h +82 h +124 h +5064 m +5065 m +4 h +1 h +1 h +73 h +125 h +10 h +338 h +10 h +2459 m +5066 m +5067 m +1 h +1 h +5068 m +4 h +1 h +10 h +11 h +4 h +195 h +5069 m +1 h +119 h +5070 m +139 h +10 h +802 m +1017 m +322 h +1835 h +4 h +687 h +1 h +1 h +146 h +5071 m +82 h +1 h +59 h +10 h +164 h +114 h +4 h +1 h +1137 h +1 h +1 h +1 h +425 h +1835 h +3 h +156 h +538 h +5072 m +5073 m +5074 m +5075 m +4 h +1081 m +238 h +5076 m +4 h +1 h +5077 m +10 h +203 m +319 h +10 h +124 h +1 h +5078 m +83 h +4 h +1 h +10 h +10 h +1 h +4 h +45 h +10 h +10 h +4 h +57 h +5079 m +92 h +1634 m +5080 m +10 h +5081 m +64 h +995 m +41 h +1 h +5082 m +22 h +25 h +4 h +4 h +4 h +5083 m +1 h +1 h +4 h +45 h +5084 m +10 h +5085 m +92 h +5086 m +4 h +5087 m +4 h +83 h +190 h +4 h +4 h +45 h +156 h +11 h +4 h +4 h +10 h +10 h +4 h +1 h +1 h +4 h +4 h +1362 h +4 h +1 h +1 h +94 h +1 h +5088 m +464 h +11 h +1 h +4 h +986 h +4 h +4 h +5089 m +2418 m +5090 m +83 h +1 h +5091 m +4695 m +1 h +4 h +10 h +1191 m +82 h +4 h +5092 m +185 h +10 h +10 h +1 h +1 h +10 h +10 h +1710 m +10 h +1 h +173 h +4 h +124 h +2520 m +570 h +250 h +10 h +8 h +4 h +4 h +1 h +5093 m +169 h +1 h +10 h +64 h +5094 m +3 h +4 h +4 h +1 h +5095 m +1 h +5096 m +5097 m +569 h +170 h +4 h +83 h +57 h +83 h +169 h +4 h +4 h +110 h +31 h +5098 m +538 h +4 h +1 h +4 h +5099 m +124 h +10 h +5100 m +10 h +5101 m +3025 m +10 h +1 h +5102 m +1 h +5103 m +1 h +4 h +1 h +4 h +1470 h +4 h +59 h +10 h +4 h +12 h +10 h +1 h +135 h +5104 m +10 h +4 h +113 h +4 h +5105 m +278 h +5106 m +4 h +1089 h +94 h +4 h +5107 m +1 h +36 h +4 h +1308 m +10 h +5108 m +1 h +2028 m +5109 m +5110 m +1 h +25 h +4 h +763 m +25 h +1 h +1 h +2558 m +4 h +1780 h +79 h +1 h +82 h +109 h +4 h +347 h +25 h +3 h +5111 m +11 h +1 h +5112 m +1 h +1 h +25 h +10 h +229 h +1 h +1 h +41 h +536 h +5113 m +5114 m +4 h +4 h +5115 m +5116 m +1 h +64 h +1083 h +5117 m +4 h +13 h +5118 m +4 h +10 h +10 h +2710 m +4 h +10 h +1 h +1 h +97 h +211 m +1 h +181 h +172 h +4 h +4 h +1 h +1 h +1574 m +170 h +124 h +3028 m +1 h +103 m +5119 m +11 h +10 h +4 h +10 h +1 h +5120 m +59 h +4 h +83 h +4 h +5121 m +1 h +4 h +1 h +5122 m +4 h +196 h +1 h +2688 m +31 h +82 h +10 h +4 h +10 h +10 h +25 h +69 h +4 h +195 h +5123 m +79 h +1 h +258 h +10 h +5124 m +4 h +5125 m +562 m +1 h +5126 m +4 h +1 h +353 m +4 h +4 h +1 h +4 h +4 h +4 h +59 h +5127 m +4 h +4 h +125 h +4 h +5128 m +5129 m +1 h +4 h +172 h +5130 m +4 h +11 h +4 h +5131 m +10 h +10 h +4 h +4 h +1 h +41 h +10 h +278 h +4 h +1 h +10 h +1 h +4 h +124 h +1 h +1 h +1 h +10 h +1403 h +5132 m +125 h +5133 m +4 h +5134 m +307 h +74 h +5135 m +1 h +5136 m +5137 m +10 h +4 h +11 h +1 h +4 h +1 h +10 h +25 h +4 h +1 h +1 h +4 h +4 h +1 h +113 h +1 h +109 h +4 h +10 h +64 h +297 h +4 h +4 h +118 h +4 h +4 h +10 h +10 h +4 h +172 h +5138 m +5139 m +74 h +4 h +140 m +1053 m +1027 h +4 h +1 h +5140 m +4 h +1122 m +45 h +5141 m +1 h +1499 m +5142 m +4 h +4 h +4 h +1 h +5143 m +1650 h +2251 m +10 h +4 h +1403 h +4 h +5144 m +123 h +4 h +4 h +478 m +4 h +4 h +55 h +4 h +174 h +1 h +4 h +10 h +4 h +4 h +1 h +11 h +1 h +4 h +5145 m +4 h +4 h +1 h +4 h +4 h +11 h +10 h +412 m +4 h +11 h +1379 m +4 h +108 h +1 h +5146 m +1 h +11 h +116 m +10 h +123 h +10 h +10 h +4 h +124 h +489 h +10 h +4 h +185 h +10 h +146 h +10 h +276 h +1 h +13 h +1 h +4 h +5147 m +285 m +4 h +109 h +190 h +4 h +170 h +388 m +11 h +10 h +10 h +185 h +5148 m +5149 m +10 h +1 h +10 h +1278 m +5150 m +10 h +1261 h +4 h +5151 m +423 m +4 h +4 h +1 h +4 h +1 h +10 h +4 h +10 h +10 h +10 h +114 h +10 h +966 m +190 h +41 h +278 h +11 h +129 h +65 h +443 h +83 h +1 h +5152 m +119 h +5153 m +1 h +4 h +10 h +4 h +1 h +82 h +5154 m +299 m +1 h +5155 m +4 h +10 h +4 h +10 h +10 h +11 h +4 h +1 h +69 h +1 h +1 h +5156 m +172 h +57 h +1 h +5157 m +4 h +5158 m +10 h +74 h +4 h +1 h +10 h +5159 m +230 h +935 m +10 h +5160 m +1 h +4 h +4 h +4 h +718 h +986 h +1 h +5161 m +840 m +11 h +5162 m +4 h +4 h +11 h +1 h +5163 m +31 h +1 h +1 h +5164 m +1 h +976 h +10 h +4 h +4 h +10 h +1 h +1 h +10 h +173 h +195 h +5165 m +10 h +125 h +135 h +5166 m +1 h +5167 m +1 h +5168 m +10 h +4 h +4 h +57 h +1 h +82 h +1 h +4 h +4 h +3546 m +1 h +4 h +307 h +1 h +1 h +64 h +4 h +11 h +3 h +10 h +4 h +5169 m +3 h +4 h +4 h +1 h +10 h +1647 m +1470 h +169 h +3396 h +5170 m +1 h +258 h +4 h +59 h +1 h +358 h +3 h +10 h +1 h +124 h +74 h +114 h +1710 m +5171 m +4 h +1 h +1 h +10 h +60 m +10 h +5172 m +5173 m +4 h +25 h +109 h +4 h +5174 m +4 h +10 h +5175 m +10 h +10 h +83 h +5176 m +10 h +5177 m +11 h +5178 m +5179 m +10 h +1 h +12 h +10 h +5180 m +5181 m +1 h +5182 m +238 h +74 h +11 h +173 h +5183 m +4 h +10 h +64 h +1 h +4 h +5184 m +5185 m +1 h +5186 m +4 h +109 h +10 h +55 h +1 h +10 h +1201 h +146 h +4 h +10 h +4 h +1 h +5187 m +1 h +4 h +1 h +11 h +1 h +2418 m +10 h +10 h +4 h +11 h +4 h +4 h +1 h +5188 m +1 h +1 h +1 h +5189 m +4 h +4 h +1 h +109 h +5190 m +25 h +5191 m +57 h +4 h +57 h +11 h +4 h +258 h +1 h +5192 m +1 h +10 h +10 h +109 h +4 h +10 h +1 h +3 h +1 h +258 h +36 h +4 h +4 h +1 h +5193 m +25 h +5194 m +935 m +25 h +10 h +601 h +1 h +1 h +4 h +83 h +5195 m +109 h +10 h +109 h +2971 m +4 h +4 h +4 h +1 h +5196 m +65 h +5197 m +11 h +1 h +266 h +172 h +1766 h +5198 m +1 h +1 h +5199 m +10 h +92 h +10 h +4 h +1 h +5200 m +181 h +5201 m +135 h +10 h +10 h +1 h +4 h +4 h +4 h +1 h +361 m +4 h +5202 m +5203 m +1 h +5204 m +1 h +4 h +1 h +10 h +4 h +5205 m +124 h +1893 m +12 h +46 h +509 m +5206 m +1 h +4 h +4 h +1 h +5207 m +5208 m +1 h +10 h +601 h +4 h +1 h +5209 m +5210 m +4 h +48 h +169 h +10 h +59 h +172 h +10 h +1 h +65 h +5211 m +371 h +4 h +14 m +1053 m +10 h +4 h +5212 m +5213 m +82 h +10 h +1 h +10 h +5214 m +11 h +4 h +1 h +5215 m +1 h +779 h +1955 m +5216 m +1370 m +575 h +59 h +5217 m +5218 m +1 h +4 h +10 h +10 h +5219 m +10 h +4 h +195 h +770 m +295 h +195 h +10 h +1 h +4 h +8 h +10 h +5220 m +10 h +59 h +1 h +84 h +4 h +4 h +4 h +4 h +4 h +97 h +266 h +11 h +5221 m +1 h +1 h +10 h +73 h +10 h +4 h +5222 m +1 h +109 h +4 h +4 h +4 h +4 h +5223 m +464 h +5224 m +5225 m +11 h +11 h +4 h +4 h +1470 h +114 h +4 h +83 h +139 h +129 h +190 h +1 h +5226 m +10 h +289 h +45 h +64 h +4 h +1 h +11 h +1 h +5227 m +82 h +5228 m +5229 m +3 h +83 h +250 h +3 h +10 h +4 h +5230 m +1 h +97 h +299 m +3555 m +4 h +5231 m +97 h +4 h +5232 m +10 h +147 h +4 h +5233 m +41 h +4 h +1 h +4 h +129 h +1 h +1 h +57 h +10 h +5234 m +4 h +57 h +56 h +5235 m +118 h +135 h +4 h +1 h +4 h +1822 m +606 m +124 h +25 h +5236 m +1 h +601 h +4 h +4 h +90 m +92 h +59 h +332 h +4 h +1 h +11 h +1 h +4 h +4 h +4 h +92 h +1 h +4 h +1 h +3 h +4 h +5237 m +164 h +10 h +4 h +593 m +4 h +125 h +10 h +1 h +5238 m +124 h +1 h +4 h +10 h +1 h +5239 m +55 h +4 h +4 h +125 h +1 h +1 h +1 h +4 h +11 h +10 h +4 h +10 h +10 h +5240 m +674 m +4 h +1 h +65 h +97 h +41 h +687 h +4 h +10 h +172 h +1 h +4 h +885 m +1261 h +10 h +1 h +11 h +2928 m +147 h +5241 m +114 h +266 h +5242 m +170 h +2769 m +4 h +140 m +1 h +10 h +25 h +1 h +10 h +1 h +57 h +1 h +59 h +4 h +4 h +4 h +4 h +5243 m +5244 m +4 h +5245 m +345 m +3630 m +5246 m +5247 m +10 h +104 h +10 h +79 h +55 h +93 h +4 h +687 h +5248 m +339 m +5249 m +1 h +79 h +4 h +279 h +104 h +12 h +10 h +5250 m +10 h +1 h +5251 m +536 h +4 h +25 h +1 h +1 h +10 h +4 h +5252 m +4 h +4 h +1 h +11 h +10 h +1 h +1 h +10 h +124 h +1 h +1 h +4 h +5253 m +147 h +2550 m +4 h +143 h +4 h +4 h +4 h +4 h +4 h +8 h +5254 m +4 h +4 h +11 h +258 h +10 h +4 h +1828 m +4 h +5255 m +10 h +4 h +31 h +73 h +10 h +195 h +10 h +1 h +1 h +1108 m +8 h +10 h +4 h +276 h +110 h +82 h +5256 m +10 h +79 h +5257 m +10 h +5258 m +31 h +570 h +5259 m +4 h +1 h +170 h +1 h +447 h +11 h +297 h +135 h +773 m +1 h +4 h +4 h +79 h +4 h +13 h +4 h +4 h +74 h +167 h +1 h +4 h +10 h +28 h +332 h +124 h +5260 m +1 h +10 h +4 h +1 h +5261 m +1 h +538 h +4 h +1 h +4 h +77 h +1 h +1697 m +1 h +59 h +5262 m +5263 m +4 h +386 h +4 h +5264 m +36 h +4 h +4 h +92 h +124 h +4 h +10 h +12 h +3 h +3 h +4 h +1 h +195 h +25 h +4 h +10 h +10 h +3 h +2788 m +10 h +5265 m +10 h +358 h +5266 m +4 h +55 h +1 h +147 h +1 h +83 h +1 h +10 h +1 h +935 h +5267 m +5268 m +5269 m +4 h +1 h +5270 m +5271 m +5272 m +1 h +59 h +4 h +1 h +4 h +15 m +5273 m +10 h +10 h +5274 m +82 h +1116 m +59 h +5275 m +10 h +83 h +31 h +1 h +1 h +1 h +1 h +5276 m +4 h +297 h +4 h +5277 m +79 h +10 h +4 h +5278 m +1 h +1 h +4 h +5279 m +5280 m +64 h +10 h +113 h +5281 m +5282 m +4 h +5283 m +4 h +31 h +3112 m +4 h +195 h +1 h +10 h +104 h +181 h +1 h +1 h +4 h +28 h +10 h +146 h +83 h +10 h +1 h +1 h +4 h +5284 m +4 h +57 h +4 h +5285 m +1 h +1 h +4 h +79 h +5286 m +779 h +11 h +1 h +4 h +1 h +41 h +1 h +2625 m +10 h +258 h +65 h +11 h +1 h +5287 m +1 h +59 h +615 m +8 h +169 h +92 h +5288 m +83 h +28 h +4 h +1138 m +5289 m +31 h +4 h +55 h +167 h +5290 m +2928 m +125 h +4 h +4 h +1 h +1 h +5291 m +1 h +10 h +10 h +5292 m +5293 m +1 h +10 h +185 h +4 h +1 h +10 h +4 h +146 h +10 h +4 h +5294 m +4 h +4 h +25 h +5295 m +5296 m +10 h +5297 m +59 h +620 m +1 h +4 h +94 h +4 h +74 h +5298 m +73 h +92 h +135 h +181 h +1 h +57 h +170 h +1 h +371 h +10 h +4 h +5299 m +10 h +4 h +1 h +5300 m +10 h +5301 m +41 h +450 m +4 h +5302 m +5303 m +10 h +10 h +4 h +4 h +11 h +82 h +4 h +4 h +4 h +1 h +1 h +4 h +109 h +10 h +11 h +124 h +10 h +125 h +1 h +5304 m +5305 m +10 h +4 h +10 h +4 h +1 h +935 h +10 h +1981 m +10 h +2116 m +41 h +10 h +4 h +5306 m +167 h +1 h +5307 m +2002 m +36 h +5308 m +4 h +10 h +4 h +10 h +4 h +10 h +1955 m +5309 m +4 h +322 h +4 h +11 h +57 h +4 h +4 h +25 h +184 h +5310 m +1 h +59 h +1 h +4 h +1 h +124 h +1 h +4 h +4 h +1 h +82 h +757 m +332 h +1938 m +5311 m +4 h +1 h +1 h +1 h +5312 m +5313 m +10 h +5314 m +4 h +4 h +5315 m +41 h +5316 m +1 h +4 h +10 h +4 h +1 h +5317 m +1 h +10 h +4 h +83 h +4 h +11 h +4528 m +4 h +10 h +4 h +359 h +5318 m +4 h +448 m +10 h +10 h +10 h +4 h +1685 m +3033 m +146 h +4 h +10 h +4 h +601 h +83 h +195 h +1 h +41 h +1 h +4 h +10 h +11 h +4 h +5319 m +5320 m +185 h +124 h +4 h +278 h +147 h +3111 m +173 h +5321 m +1 h +5322 m +3558 m +59 h +4 h +1 h +119 h +578 m +1 h +1 h +4 h +10 h +4 h +10 h +1 h +143 h +11 h +5323 m +4 h +4 h +5324 m +190 h +250 h +11 h +146 h +1 h +5325 m +1 h +319 h +109 h +11 h +1 h +36 h +1 h +11 h +104 h +3188 m +692 h +1 h +4 h +4 h +10 h +3 h +4 h +57 h +11 h +1 h +10 h +146 h +4 h +4 h +4 h +434 h +190 h +4 h +4 h +5326 m +10 h +92 h +4301 m +4 h +1 h +5327 m +1 h +1 h +4 h +83 h +5328 m +83 h +4 h +10 h +57 h +79 h +5329 m +5330 m +10 h +65 h +4 h +190 h +4 h +1 h +10 h +5331 m +4 h +1738 m +112 h +10 h +4 h +91 h +1 h +11 h +109 h +1 h +1 h +4 h +4 h +5332 m +5333 m +10 h +69 h +4 h +5334 m +195 h +10 h +1 h +31 h +4 h +109 h +10 h +219 m +5335 m +386 h +4 h +83 h +4 h +4 h +5336 m +82 h +3025 m +11 h +4 h +10 h +5337 m +5338 m +59 h +4 h +5339 m +1 h +10 h +1642 h +5340 m +22 h +1 h +10 h +4 h +104 h +10 h +4 h +5341 m +1 h +138 h +5342 m +620 m +5343 m +27 h +1122 m +10 h +2418 h +4 h +31 h +185 h +4 h +10 h +4 h +4 h +97 h +5344 m +135 h +1 h +1 h +5345 m +104 h +1 h +4 h +808 h +5346 m +1 h +1105 h +299 h +4 h +65 h +10 h +649 m +13 h +4 h +5347 m +83 h +1 h +104 h +1 h +929 m +59 h +170 h +10 h +144 h +1 h +5348 m +5349 m +10 h +5350 m +4 h +5351 m +8 h +4 h +4 h +4 h +5352 m +4 h +5353 m +10 h +56 h +332 h +4 h +11 h +5354 m +5355 m +10 h +4240 m +185 h +4 h +4 h +104 h +4 h +5356 m +5357 m +5358 m +4 h +10 h +5359 m +1096 m +5360 m +25 h +4 h +5361 m +10 h +1 h +1 h +4 h +4 h +4 h +11 h +4 h +1 h +10 h +4 h +279 h +1454 m +4 h +4 h +4 h +164 h +4 h +1 h +10 h +4 h +1 h +4 h +1 h +5362 m +5363 m +11 h +5364 m +4 h +10 h +4 h +83 h +4 h +1 h +5365 m +10 h +10 h +104 h +4 h +274 h +5366 m +65 h +5367 m +3 h +1 h +1 h +45 h +4 h +1 h +4 h +25 h +4 h +5368 m +4 h +4 h +83 h +110 h +4 h +5369 m +5370 m +10 h +4 h +11 h +10 h +4 h +83 h +1 h +5371 m +4 h +4 h +1 h +4 h +4 h +5372 m +4 h +5373 m +4 h +10 h +383 h +4 h +4 h +1 h +195 h +4 h +10 h +1 h +10 h +109 h +266 h +5374 m +36 h +4029 m +4 h +4 h +1 h +5375 m +2794 m +8 h +31 h +4 h +1003 h +557 m +185 h +11 h +4 h +229 h +10 h +5376 m +5377 m +59 h +1796 m +164 h +10 h +1 h +158 h +5378 m +124 h +1 h +5379 m +147 h +4 h +99 m +4 h +10 h +31 h +83 h +1 h +10 h +57 h +4 h +279 h +73 h +358 h +4 h +5380 m +31 h +4 h +5381 m +1 h +5382 m +27 h +1 h +124 h +4240 m +4 h +5383 m +10 h +5384 m +1952 m +4 h +4 h +143 h +185 h +4 h +1685 m +10 h +5385 m +10 h +4 h +57 h +5386 m +10 h +4 h +4 h +3025 m +820 h +10 h +5387 m +5388 m +1 h +3 h +4 h +10 h +196 h +1 h +11 h +4 h +4 h +4 h +4 h +779 h +5389 m +10 h +10 h +1 h +11 h +5390 m +1 h +4 h +4 h +1 h +1 h +167 h +10 h +4 h +109 h +4 h +262 m +3089 m +203 m +4 h +4 h +274 h +1 h +1 h +4 h +147 h +83 h +820 h +73 h +109 h +4 h +41 h +5391 m +258 h +172 h +140 h +4 h +10 h +10 h +5392 m +4 h +278 h +4 h +10 h +4 h +55 h +4 h +692 h +265 h +181 h +358 h +1 h +5393 m +1 h +4 h +5394 m +10 h +97 h +181 h +10 h +4 h +82 h +5395 m +93 h +569 h +45 h +5396 m +1 h +4 h +5397 m +266 h +338 h +2124 m +2308 m +10 h +4 h +4 h +10 h +10 h +4 h +4 h +4 h +114 h +45 h +97 h +10 h +74 h +4 h +1 h +4 h +1771 m +4 h +5398 m +146 h +4 h +4 h +59 h +167 h +5399 m +150 m +386 h +1 h +113 h +10 h +1 h +4 h +10 h +135 h +4 h +1 h +1 h +4 h +5400 m +307 h +4 h +10 h +73 h +97 h +3668 m +4 h +97 h +1 h +4 h +10 h +1 h +5401 m +57 h +65 h +5402 m +1 h +1 h +1 h +195 h +1 h +79 h +1 h +1 h +4 h +143 h +5403 m +57 h +140 h +124 h +83 h +5404 m +3 h +10 h +1 h +2494 m +10 h +1 h +5405 m +4 h +1 h +70 m +1 h +4 h +5406 m +112 h +11 h +1772 h +5407 m +1 h +10 h +10 h +2595 m +10 h +3 h +1725 m +5408 m +5409 m +1 h +538 h +5410 m +5411 m +5412 m +13 h +1 h +10 h +5413 m +5414 m +97 h +10 h +10 h +4 h +5415 m +1 h +5416 m +1 h +4 h +4 h +11 h +10 h +4 h +10 h +10 h +976 h +82 h +10 h +5417 m +1 h +1 h +1642 h +5418 m +4 h +5419 m +169 h +4 h +5420 m +4 h +4 h +25 h +5421 m +4 h +533 m +82 h +5422 m +5423 m +4 h +5424 m +1250 h +4 h +4 h +4 h +4101 m +4 h +1 h +4 h +5425 m +258 h +1 h +10 h +4 h +4 h +1 h +4 h +1 h +5426 m +4 h +83 h +1 h +1393 m +11 h +556 h +57 h +4 h +4 h +1 h +383 h +4 h +5427 m +5428 m +10 h +56 h +10 h +5429 m +31 h +92 h +4 h +10 h +11 h +4 h +4 h +5430 m +11 h +5431 m +5432 m +105 m +10 h +5433 m +4 h +5434 m +2359 m +125 h +10 h +82 h +109 h +10 h +45 h +1 h +10 h +57 h +5435 m +4 h +4 h +57 h +642 m +123 h +1372 m +1 h +59 h +4 h +83 h +1835 h +5436 m +1 h +2436 m +59 h +4 h +4 h +4 h +1 h +144 h +83 h +83 h +11 h +338 h +4 h +109 h +10 h +28 h +10 h +4 h +1 h +10 h +1 h +1 h +13 h +4 h +4 h +77 h +1281 m +4 h +5437 m +5438 m +1 h +25 h +10 h +4 h +82 h +2582 m +5439 m +4 h +10 h +1780 h +10 h +5440 m +5441 m +5442 m +5443 m +4 h +124 h +157 h +10 h +1 h +4 h +520 m +8 h +4 h +1 h +10 h +885 m +935 h +10 h +164 h +10 h +4 h +5444 m +10 h +10 h +10 h +4 h +1 h +4 h +13 h +4 h +10 h +10 h +4 h +5445 m +5446 m +4 h +113 h +5447 m +73 h +65 h +1 h +83 h +1 h +1 h +31 h +5448 m +4 h +10 h +11 h +4 h +1685 h +164 h +1 h +2281 m +10 h +297 h +110 h +10 h +12 h +5449 m +5450 m +4 h +1 h +144 h +4 h +332 h +5451 m +10 h +65 h +10 h +195 h +12 h +10 h +28 h +2769 m +124 h +5452 m +57 h +36 h +5453 m +1016 h +125 h +5454 m +1 h +11 h +3768 m +4 h +5455 m +278 h +125 h +4 h +41 h +1 h +5456 m +5457 m +1 h +4 h +4 h +4057 m +4 h +5458 m +1 h +2459 m +114 h +224 h +11 h +10 h +1 h +5459 m +5460 m +10 h +8 h +4 h +10 h +3 h +1 h +4 h +5461 m +1 h +4 h +5462 m +5463 m +10 h +3837 m +4 h +1 h +4 h +25 h +4 h +1 h +5464 m +4 h +1685 h +4 h +4 h +4218 m +5465 m +10 h +25 h +10 h +146 h +4 h +25 h +1 h +157 h +4 h +10 h +10 h +358 h +1 h +4 h +5466 m +10 h +4 h +10 h +172 h +386 h +5467 m +181 h +10 h +4306 m +41 h +5468 m +4 h +10 h +83 h +1 h +5469 m +4 h +5470 m +36 h +1 h +5471 m +41 h +10 h +4 h +10 h +10 h +5472 m +4 h +4 h +1089 h +5473 m +256 h +10 h +4 h +4 h +4 h +4 h +1 h +425 h +4 h +5474 m +4 h +5475 m +10 h +4 h +4 h +10 h +386 h +1 h +5476 m +1309 h +1 h +1472 m +135 h +4 h +10 h +124 h +31 h +147 h +4 h +5477 m +4 h +45 h +10 h +1 h +97 h +5478 m +10 h +82 h +4 h +4 h +4 h +5479 m +5480 m +10 h +1 h +4 h +5481 m +10 h +12 h +4 h +10 h +10 h +4 h +276 h +4 h +77 h +147 h +4 h +10 h +1 h +10 h +4 h +4 h +2846 m +1 h +82 h +1 h +4 h +4 h +10 h +5482 m +12 h +11 h +4 h +10 h +10 h +12 h +1 h +1 h +4 h +5483 m +10 h +169 h +4 h +358 h +5484 m +4 h +5485 m +5348 m +140 h +5486 m +1 h +5487 m +964 m +2172 m +82 h +1 h +5488 m +307 h +4 h +4 h +10 h +5489 m +332 h +57 h +1 h +1027 h +1 h +92 h +1 h +4 h +4 h +5490 m +5491 m +4 h +4 h +1 h +31 h +1 h +4 h +5492 m +4 h +319 h +5493 m +4 h +4 h +5494 m +5495 m +10 h +1 h +12 h +10 h +1 h +1 h +1 h +4 h +5496 m +4 h +1 h +1 h +5497 m +10 h +4 h +4 h +4 h +74 h +5498 m +5499 m +4 h +5500 m +1201 h +1 h +5501 m +97 h +4 h +5502 m +125 h +4 h +12 h +25 h +82 h +368 h +4 h +4 h +94 h +157 h +4 h +125 h +4 h +1 h +4 h +109 h +4 h +1 h +383 h +57 h +10 h +1780 h +65 h +716 m +368 h +1 h +5503 m +1 h +55 h +4 h +4 h +1137 h +4 h +41 h +4 h +10 h +11 h +104 h +5504 m +10 h +45 h +181 h +5505 m +10 h +4 h +129 h +10 h +28 h +5506 m +692 h +83 h +332 h +4 h +1 h +4 h +5507 m +4 h +5508 m +10 h +124 h +4 h +1 h +4 h +1553 m +5509 m +4 h +5510 m +10 h +5511 m +1553 m +10 h +109 h +5512 m +5513 m +10 h +1650 h +196 h +219 m +4 h +10 h +104 h +4 h +36 h +10 h +266 h +10 h +57 h +1 h +1822 h +104 h +195 h +5514 m +4 h +5515 m +1 h +4 h +4 h +82 h +5516 m +1 h +1403 h +4 h +1403 h +124 h +1 h +1 h +1 h +5517 m +2961 m +10 h +4 h +10 h +5518 m +25 h +1 h +4 h +1 h +5519 m +4 h +10 h +4 h +5520 m +56 h +5521 m +10 h +1 h +156 h +2245 m +1 h +1 h +1 h +10 h +4 h +57 h +82 h +1 h +1548 m +4 h +1 h +23 h +185 h +295 h +5522 m +4 h +5523 m +4 h +4 h +1 h +536 h +10 h +104 h +4 h +4 h +5524 m +10 h +10 h +10 h +10 h +1 h +4 h +4 h +615 m +5525 m +5526 m +1 h +4 h +1 h +1 h +10 h +1 h +4 h +976 h +1 h +4 h +4 h +258 h +1772 h +5527 m +5528 m +4 h +5529 m +1 h +10 h +4 h +10 h +4 h +4 h +143 h +27 h +976 h +10 h +57 h +83 h +13 h +4 h +2111 m +5530 m +5531 m +2418 h +1 h +5532 m +184 h +383 h +1 h +4 h +10 h +4 h +10 h +170 h +4 h +4 h +5533 m +10 h +4 h +10 h +10 h +4 h +59 h +10 h +5534 m +82 h +4 h +172 h +1 h +12 h +1 h +83 h +4 h +4 h +266 h +4 h +5505 m +3 h +59 h +109 h +5535 m +3216 m +5536 m +11 h +447 h +129 h +1 h +5537 m +582 m +4 h +41 h +124 h +4 h +1 h +5538 m +4576 m +4 h +94 h +3 h +4 h +1835 h +238 h +383 h +5539 m +285 m +31 h +5540 m +57 h +1 h +10 h +5541 m +5542 m +718 h +143 h +5543 m +4 h +4 h +5544 m +1 h +718 h +4 h +4 h +5545 m +332 h +97 h +5546 m +10 h +5547 m +10 h +4 h +10 h +10 h +228 m +10 h +1 h +25 h +5548 m +5549 m +4 h +5550 m +59 h +976 h +4 h +5551 m +5552 m +5553 m +4 h +10 h +4 h +41 h +1 h +57 h +5554 m +172 h +1 h +3 h +4 h +3499 m +2110 m +1 h +587 m +1 h +10 h +1 h +10 h +11 h +1 h +1116 m +59 h +5555 m +1 h +83 h +4 h +5556 m +10 h +1 h +45 h +10 h +1 h +1948 m +1 h +143 h +5557 m +4 h +10 h +297 h +74 h +195 h +297 h +112 h +143 h +5558 m +146 h +10 h +5559 m +5560 m +1 h +4 h +1 h +258 h +5561 m +10 h +11 h +10 h +4 h +91 h +1 h +208 m +119 h +4 h +1 h +1 h +1 h +10 h +1642 h +1 h +65 h +181 h +4 h +1780 h +4 h +82 h +4 h +4 h +59 h +1 h +4 h +1 h +5562 m +5563 m +185 h +1 h +1 h +1 h +2257 m +5564 m +5565 m +4 h +146 h +4 h +10 h +2846 m +3 h +2719 m +2124 h +399 h +5566 m +119 h +5567 m +5568 m +1 h +1 h +56 h +5569 m +4 h +3209 m +10 h +5570 m +1 h +238 h +1 h +5571 m +11 h +5572 m +1 h +4 h +5573 m +124 h +1 h +173 h +359 h +4 h +57 h +4 h +4 h +4 h +1 h +4 h +1737 m +92 h +10 h +4 h +10 h +4 h +59 h +5574 m +1 h +108 h +57 h +4 h +11 h +1 h +4 h +1 h +10 h +5575 m +4 h +10 h +82 h +1 h +4 h +4 h +5576 m +5577 m +124 h +1 h +1 h +5578 m +2374 m +1 h +4 h +5579 m +4 h +5580 m +10 h +25 h +1 h +5581 m +1 h +11 h +4 h +11 h +10 h +4 h +25 h +10 h +4 h +5582 m +4 h +1 h +10 h +4 h +443 h +4 h +4 h +1 h +4 h +5583 m +10 h +5584 m +276 h +10 h +4 h +109 h +3 h +124 h +28 h +4 h +10 h +10 h +69 h +1 h +10 h +4 h +1 h +1 h +10 h +4 h +97 h +4 h +10 h +4 h +289 h +1 h +10 h +4 h +5585 m +10 h +5586 m +1 h +4 h +1 h +5587 m +91 h +1766 h +158 h +1 h +5588 m +5589 m +4 h +5590 m +4 h +4 h +1470 h +4 h +4 h +1 h +5591 m +1 h +74 h +57 h +4 h +10 h +5592 m +1 h +4039 m +56 h +10 h +10 h +83 h +5593 m +4 h +11 h +13 h +4 h +2475 m +1 h +1 h +10 h +167 h +4 h +1 h +5594 m +1 h +10 h +1 h +13 h +10 h +139 h +5595 m +22 h +57 h +5596 m +563 m +11 h +4 h +1 h +139 h +5597 m +10 h +4 h +262 h +169 h +1790 m +3 h +779 h +4 h +1 h +4 h +425 h +569 h +123 h +5598 m +1 h +1 h +4 h +1 h +10 h +57 h +57 h +11 h +10 h +146 h +1 h +10 h +5599 m +1 h +11 h +59 h +4 h +10 h +1 h +1 h +5600 m +1 h +4 h +5601 m +31 h +4 h +11 h +5602 m +4 h +211 m +4645 m +11 h +11 h +11 h +11 h +57 h +238 h +4 h +1 h +5603 m +164 h +1 h +4 h +10 h +181 h +299 h +4 h +358 h +105 m +1 h +10 h +109 h +10 h +10 h +1 h +5604 m +1 h +5605 m +5606 m +295 h +5607 m +5608 m +5609 m +5610 m +278 h +272 m +10 h +1 h +5611 m +4 h +5612 m +4 h +5613 m +4 h +5614 m +4 h +5615 m +10 h +124 h +31 h +5616 m +5617 m +5618 m +5619 m +1 h +4 h +4 h +1 h +4 h +5620 m +5621 m +4 h +433 m +1 h +4 h +1 h +158 h +3 h +73 h +124 h +4 h +5622 m +4 h +118 h +1 h +109 h +82 h +1016 h +4 h +4 h +1 h +73 h +278 h +5623 m +31 h +5624 m +10 h +5625 m +114 h +64 h +4 h +1 h +4 h +4 h +10 h +4 h +4 h +4 h +4 h +124 h +97 h +10 h +1 h +10 h +10 h +4 h +1 h +5626 m +4 h +5627 m +3558 m +1389 m +4 h +4 h +229 h +10 h +5628 m +4 h +10 h +368 h +170 h +45 h +4 h +10 h +332 h +4 h +1 h +4 h +4 h +1 h +1 h +4 h +11 h +4 h +5629 m +1 h +224 h +4 h +4 h +5630 m +1 h +1 h +11 h +4 h +10 h +4 h +5631 m +73 h +124 h +83 h +649 m +146 h +97 h +119 h +297 h +5632 m +1 h +4 h +5633 m +12 h +5634 m +10 h +570 h +4 h +109 h +41 h +1737 m +55 h +1 h +82 h +4 h +5635 m +5636 m +143 h +41 h +5637 m +4 h +4 h +5638 m +1 h +10 h +1 h +1 h +10 h +282 m +10 h +1 h +59 h +1 h +2423 m +1 h +649 m +1 h +1 h +57 h +1 h +5639 m +4 h +3 h +4 h +41 h +124 h +371 h +10 h +5640 m +4 h +82 h +1 h +4 h +4 h +3 h +4 h +5641 m +59 h +1 h +4 h +57 h +265 h +10 h +5642 m +5643 m +1 h +1 h +5644 m +125 h +5645 m +10 h +4 h +556 h +4 h +5646 m +488 m +1 h +1 h +4 h +4 h +1030 h +1 h +1 h +4 h +4 h +10 h +170 h +4 h +5647 m +65 h +478 m +4 h +10 h +74 h +433 m +69 h +4 h +4 h +4 h +5648 m +82 h +4 h +403 h +4 h +5649 m +10 h +1 h +4 h +1 h +1 h +82 h +1 h +5650 m +4 h +1 h +1 h +1 h +4 h +4 h +11 h +1 h +1 h +13 h +73 h +1 h +4 h +114 h +10 h +3 h +1 h +4 h +5651 m +82 h +124 h +5652 m +276 h +1564 m +11 h +464 h +4 h +69 h +1 h +5653 m +4 h +1 h +59 h +2688 m +5654 m +1 h +1 h +4 h +5655 m +276 h +1 h +4 h +4 h +4 h +4 h +181 h +4 h +170 h +4 h +124 h +129 h +5656 m +22 h +59 h +1 h +4 h +109 h +5657 m +10 h +5658 m +230 h +593 m +10 h +238 h +4 h +4 h +3 h +4 h +3 h +1 h +4 h +10 h +104 h +5659 m +10 h +1 h +5660 m +1 h +1 h +1 h +5661 m +72 m +4 h +4 h +4 h +10 h +1 h +170 h +97 h +1 h +4 h +1 h +109 h +94 h +139 h +4 h +536 h +4 h +10 h +10 h +5662 m +4 h +135 h +4 h +4 h +4 h +4 h +12 h +5650 m +1 h +4 h +4 h +31 h +270 h +5663 m +4 h +5664 m +25 h +649 h +1 h +4 h +4 h +10 h +10 h +5665 m +5666 m +11 h +1120 m +23 h +1 h +10 h +258 h +4 h +185 h +4 h +57 h +1 h +4 h +1 h +4 h +11 h +1 h +4 h +4 h +976 h +307 h +10 h +10 h +4 h +4 h +4 h +5667 m +112 h +4 h +157 h +5668 m +5669 m +2625 m +31 h +4 h +1 h +173 h +5670 m +147 h +114 h +4 h +4 h +1 h +83 h +1 h +1 h +601 h +5671 m +4 h +601 h +83 h +1 h +4 h +1137 h +4 h +10 h +5672 m +4 h +31 h +4 h +11 h +1 h +44 m +1 h +104 h +109 h +10 h +4 h +4 h +110 h +5673 m +5674 m +10 h +1 h +4 h +4 h +5675 m +4 h +3 h +5225 m +386 h +5676 m +4 h +5141 m +238 h +1 h +11 h +1 h +146 h +5677 m +1 h +4 h +192 h +10 h +4 h +4 h +10 h +4 h +5678 m +5679 m +2591 m +5680 m +41 h +4 h +4 h +5681 m +4 h +5682 m +10 h +8 h +10 h +5683 m +1 h +5684 m +1 h +4 h +4 h +1 h +1 h +5685 m +143 h +4 h +1 h +5686 m +1 h +4 h +4 h +278 h +4 h +4 h +13 h +104 h +10 h +25 h +5687 m +83 h +4 h +1 h +64 h +5688 m +1 h +1 h +4 h +1470 h +10 h +4 h +1790 m +1 h +4 h +10 h +5689 m +5690 m +1 h +4 h +1914 m +10 h +5691 m +4 h +1218 m +1359 h +5692 m +4 h +4 h +5693 m +1627 m +56 h +109 h +65 h +4 h +1737 h +4 h +10 h +4 h +464 h +238 h +3 h +104 h +109 h +630 m +167 h +4 h +1 h +5694 m +4 h +4 h +11 h +1 h +4 h +114 h +5695 m +4 h +1751 m +41 h +94 h +4 h +1 h +4 h +4 h +97 h +5696 m +5697 m +1 h +4 h +911 h +41 h +40 h +10 h +1 h +1 h +113 h +10 h +1 h +4 h +5698 m +4 h +1 h +1 h +5699 m +10 h +1 h +73 h +1 h +4 h +25 h +250 h +10 h +10 h +79 h +4 h +5230 m +5700 m +1 h +10 h +10 h +10 h +11 h +4 h +1 h +1 h +73 h +1 h +186 h +109 h +4 h +4 h +4 h +125 h +5701 m +1714 m +5702 m +11 h +5703 m +229 h +31 h +10 h +5704 m +10 h +11 h +4 h +1 h +10 h +4 h +10 h +4 h +1 h +22 h +5705 m +8 h +5706 m +1 h +4 h +1 h +1 h +381 m +4 h +5707 m +10 h +83 h +4 h +4 h +270 h +4 h +124 h +5708 m +4 h +156 h +1 h +125 h +4 h +1 h +5709 m +11 h +4 h +5710 m +4 h +10 h +5711 m +4 h +4 h +278 h +74 h +4 h +4 h +4 h +1 h +31 h +10 h +4 h +1 h +1 h +1 h +5712 m +10 h +4 h +4 h +4 h +5713 m +1 h +1948 m +5714 m +4 h +1027 h +1 h +10 h +97 h +1269 m +4 h +4 h +4 h +10 h +4 h +4 h +57 h +1191 m +185 h +109 h +90 m +3 h +10 h +83 h +4 h +5715 m +10 h +4 h +10 h +5716 m +1 h +4 h +5717 m +124 h +135 h +5718 m +1 h +4 h +10 h +10 h +25 h +10 h +5719 m +4 h +1 h +11 h +82 h +1 h +4 h +10 h +1 h +10 h +204 h +1 h +4 h +158 h +10 h +143 h +4 h +5720 m +5721 m +5722 m +4 h +10 h +5723 m +1 h +4 h +25 h +5724 m +11 h +45 h +928 m +10 h +10 h +112 h +10 h +109 h +4 h +10 h +5725 m +5726 m +5727 m +5728 m +114 h +1 h +124 h +1389 m +135 h +5729 m +4 h +5730 m +4 h +158 h +10 h +10 h +1 h +83 h +1 h +5731 m +5732 m +1 h +4 h +1 h +10 h +2625 m +5733 m +4 h +4 h +4 h +5734 m +11 h +4 h +31 h +1 h +1 h +10 h +10 h +11 h +10 h +5735 m +1 h +10 h +94 h +4 h +4 h +4 h +246 m +74 h +169 h +4 h +4 h +181 h +10 h +4 h +65 h +82 h +3 h +5736 m +1 h +10 h +1442 m +31 h +11 h +4 h +5737 m +5738 m +4 h +10 h +2851 m +73 h +1 h +1 h +3477 m +41 h +4 h +10 h +196 h +1 h +10 h +5739 m +4 h +1 h +5740 m +4 h +2998 m +1 h +4 h +104 h +4 h +5741 m +5742 m +5743 m +10 h +5744 m +4 h +5745 m +10 h +4 h +5746 m +5747 m +5748 m +109 h +97 h +10 h +129 h +1 h +1 h +10 h +10 h +10 h +10 h +4 h +4 h +5749 m +5750 m +83 h +238 h +4 h +13 h +5751 m +4 h +5752 m +10 h +10 h +10 h +4 h +5753 m +5754 m +4 h +430 m +4 h +4 h +1 h +4 h +11 h +83 h +55 h +4 h +1785 m +4 h +4 h +10 h +4 h +5755 m +10 h +4 h +4 h +601 h +4 h +5756 m +59 h +4 h +1 h +1 h +1 h +94 h +10 h +10 h +8 h +4 h +11 h +1 h +1 h +319 h +4 h +11 h +4 h +113 h +41 h +1 h +11 h +5757 m +1 h +3150 m +4 h +10 h +56 h +4 h +10 h +4 h +1 h +2775 m +109 h +5758 m +1 h +156 h +73 h +1 h +1 h +31 h +1 h +1 h +5759 m +1 h +5760 m +4 h +124 h +1 h +10 h +11 h +1 h +4 h +4 h +4 h +1 h +82 h +146 h +4 h +10 h +5761 m +289 h +1 h +5762 m +5763 m +4 h +4 h +4 h +185 h +4 h +4 h +185 h +109 h +124 h +156 h +1 h +59 h +1 h +5764 m +5765 m +1138 m +5766 m +5767 m +31 h +109 h +4 h +92 h +5768 m +4 h +4 h +83 h +10 h +3 h +74 h +36 h +4 h +31 h +64 h +59 h +1 h +4 h +757 m +1 h +4 h +11 h +4 h +4 h +10 h +1 h +5769 m +1 h +4 h +238 h +5770 m +986 h +4 h +5771 m +12 h +10 h +5772 m +4 h +1650 h +4 h +1 h +1 h +109 h +4 h +10 h +4 h +1127 m +4 h +4 h +110 h +5773 m +5774 m +10 h +65 h +1 h +954 m +1 h +5775 m +5206 m +447 h +4549 m +640 m +4 h +1 h +83 h +123 h +717 h +11 h +1 h +195 h +4 h +82 h +4 h +170 h +1 h +1 h +5776 m +5777 m +4 h +928 m +170 h +13 h +1 h +4 h +124 h +4 h +238 h +2617 m +5778 m +1 h +1 h +10 h +4 h +4 h +1 h +124 h +5779 m +383 h +10 h +10 h +4 h +1 h +4 h +1362 h +5780 m +10 h +869 h +1 h +5781 m +1 h +5542 m +4 h +1 h +1 h +10 h +10 h +4 h +10 h +124 h +10 h +4 h +45 h +1838 m +109 h +1 h +11 h +10 h +1 h +10 h +1 h +4 h +79 h +4 h +10 h +10 h +278 h +4 h +295 h +5782 m +5783 m +1 h +4906 m +4 h +4 h +1 h +1655 m +1 h +11 h +4 h +4 h +1 h +10 h +185 h +272 m +1 h +10 h +109 h +10 h +11 h +1 h +5784 m +1 h +4 h +104 h +1 h +5785 m +5786 m +5787 m +250 h +3212 m +4 h +5788 m +616 m +5789 m +1 h +11 h +10 h +986 h +4 h +10 h +5790 m +5791 m +626 m +36 h +4 h +10 h +10 h +196 h +10 h +5792 m +25 h +1 h +27 h +1470 h +5793 m +56 h +1 h +5794 m +10 h +83 h +4 h +4 h +297 h +10 h +4 h +196 h +5795 m +5796 m +5797 m +82 h +4 h +10 h +4 h +10 h +5798 m +10 h +143 h +1 h +5799 m +1 h +3 h +5800 m +2887 h +31 h +1089 h +59 h +1 h +1 h +1 h +601 h +1 h +4 h +4 h +4 h +10 h +5801 m +1 h +5802 m +10 h +4 h +57 h +4 h +4 h +164 h +59 h +41 h +5803 m +124 h +5804 m +5059 m +403 h +104 h +167 h +4 h +5805 m +1 h +1 h +10 h +119 h +1 h +10 h +4 h +1 h +10 h +4 h +2769 m +10 h +1 h +1 h +1 h +5806 m +5807 m +92 h +10 h +10 h +4 h +4 h +4 h +569 h +13 h +10 h +4 h +2958 m +5808 m +266 h +1 h +1 h +4 h +4 h +10 h +4 h +1 h +5809 m +56 h +1 h +169 h +4 h +4 h +1 h +10 h +5810 m +1 h +4 h +31 h +4 h +1 h +4 h +109 h +1 h +5811 m +1 h +4 h +4106 m +1 h +4 h +5812 m +888 m +4 h +1 h +5813 m +11 h +108 h +4 h +4 h +103 m +10 h +5814 m +464 h +4 h +295 h +3 h +5815 m +10 h +170 h +5816 m +5817 m +10 h +386 h +1 h +4 h +83 h +1 h +56 h +1 h +1 h +11 h +4 h +295 h +10 h +4 h +10 h +4 h +1 h +4 h +109 h +1 h +124 h +10 h +1 h +10 h +4 h +1 h +22 h +10 h +5818 m +4780 m +4 h +1 h +10 h +83 h +274 h +5819 m +10 h +10 h +4 h +5820 m +5821 m +5822 m +10 h +10 h +11 h +266 h +4 h +10 h +265 h +158 h +1 h +114 h +55 h +4 h +4 h +10 h +4 h +56 h +65 h +5823 m +1 h +4 h +5824 m +10 h +11 h +190 h +463 m +4 h +1714 m +10 h +1250 h +41 h +4 h +147 h +1 h +5825 m +4 h +4 h +5826 m +4 h +83 h +83 h +10 h +170 h +1 h +25 h +10 h +57 h +10 h +1 h +4 h +5827 m +11 h +5828 m +1 h +5829 m +5830 m +4 h +5831 m +4 h +2883 m +4 h +1 h +59 h +91 h +4 h +10 h +56 h +57 h +5832 m +4 h +4 h +5833 m +1 h +10 h +5834 m +4 h +1 h +4 h +10 h +4 h +82 h +1 h +330 m +4 h +31 h +4 h +4 h +4 h +1 h +1 h +12 h +4 h +5835 m +3 h +11 h +1 h +4 h +4 h +5836 m +173 h +5837 m +1 h +10 h +5838 m +65 h +4 h +5839 m +5840 m +1724 m +4 h +10 h +77 h +4 h +65 h +5841 m +65 h +1 h +1 h +338 h +1886 m +31 h +10 h +1 h +41 h +170 h +10 h +10 h +113 h +278 h +64 h +5842 m +79 h +4 h +1 h +5584 m +4 h +1 h +10 h +74 h +41 h +84 h +5843 m +1 h +1 h +5844 m +1 h +4 h +10 h +2733 h +4 h +939 m +10 h +1 h +83 h +48 h +4 h +692 h +4 h +40 h +1 h +10 h +4 h +25 h +1 h +10 h +1 h +5845 m +10 h +4 h +10 h +4 h +1 h +11 h +266 h +11 h +5846 m +1 h +447 h +1 h +5847 m +464 h +289 h +109 h +59 h +10 h +1 h +3799 m +4 h +10 h +4 h +83 h +11 h +4 h +447 h +4 h +4 h +5848 m +5849 m +1 h +10 h +4 h +79 h +83 h +1 h +33 m +5850 m +11 h +4 h +1 h +4 h +124 h +25 h +10 h +5851 m +4 h +1 h +4 h +278 h +204 h +10 h +4 h +4 h +59 h +911 h +10 h +2172 m +40 h +5852 m +3 h +4 h +5853 m +5854 m +4 h +1 h +10 h +79 h +250 h +164 h +1261 h +5855 m +4 h +4 h +1409 m +4 h +5856 m +147 h +1 h +10 h +5857 m +1 h +1 h +5858 m +368 h +1105 h +1 h +10 h +10 h +1 h +278 h +4 h +79 h +3 h +125 h +10 h +1 h +5859 m +5860 m +1 h +4 h +4933 m +1 h +5861 m +10 h +1 h +10 h +4 h +5862 m +265 h +185 h +332 h +556 h +10 h +1 h +82 h +219 m +196 h +1 h +11 h +135 h +4 h +4 h +1 h +1 h +1 h +1 h +172 h +4 h +5863 m +195 h +4 h +10 h +5864 m +4 h +41 h +4 h +1 h +4 h +630 m +125 h +4 h +65 h +4 h +4 h +1 h +1 h +135 h +1 h +1 h +5865 m +1 h +1 h +5141 m +4 h +1 h +5866 m +1 h +10 h +5867 m +10 h +10 h +5868 m +10 h +4 h +5869 m +5870 m +316 m +10 h +1 h +83 h +4 h +123 h +5871 m +10 h +1 h +4564 m +5872 m +146 h +4 h +5873 m +4 h +4 h +4 h +5874 m +1 h +4 h +4 h +5875 m +10 h +1 h +5876 m +4 h +82 h +1 h +4 h +5877 m +4 h +5878 m +4 h +1 h +4 h +4 h +265 h +10 h +82 h +1 h +5879 m +1 h +3 h +1 h +1 h +265 h +74 h +5880 m +4 h +10 h +11 h +10 h +10 h +289 h +4 h +4 h +5881 m +5882 m +10 h +4 h +4 h +4 h +41 h +113 h +156 h +1 h +4 h +5883 m +10 h +5884 m +57 h +4 h +5885 m +258 h +10 h +1 h +5886 m +27 h +1 h +2116 m +4 h +5887 m +5888 m +1 h +1 h +5889 m +434 h +4 h +4 h +1 h +4 h +196 h +5890 m +4 h +794 m +1 h +4 h +5891 m +5892 m +4 h +108 h +5893 m +82 h +2308 m +1 h +1 h +1 h +10 h +1725 m +112 h +5894 m +31 h +196 h +4 h +1894 m +4 h +4 h +5895 m +5896 m +150 m +4 h +146 h +10 h +10 h +4 h +10 h +5897 m +125 h +1 h +5898 m +57 h +192 h +1 h +57 h +5899 m +45 h +1 h +10 h +307 h +5900 m +125 h +258 h +31 h +124 h +10 h +1 h +10 h +1 h +1 h +4 h +368 h +83 h +4 h +692 h +10 h +83 h +10 h +83 h +5901 m +10 h +4 h +10 h +5902 m +195 h +2459 m +4 h +4 h +104 h +1 h +4 h +5903 m +146 h +129 h +10 h +59 h +1790 h +986 h +4 h +5904 m +5905 m +146 h +5906 m +368 h +4 h +1 h +3 h +307 h +110 h +1201 h +3469 m +10 h +10 h +4 h +911 h +1 h +538 h +4 h +4 h +403 h +5907 m +601 h +4 h +4 h +4 h +55 h +5908 m +104 h +1 h +83 h +1 h +13 h +83 h +25 h +4 h +447 h +5909 m +1 h +190 h +135 h +59 h +4 h +124 h +1 h +5910 m +5911 m +143 h +4 h +4 h +4 h +5912 m +1639 m +5913 m +109 h +1 h +143 h +4 h +4 h +1 h +4 h +196 h +5914 m +5915 m +5916 m +1 h +5917 m +5918 m +10 h +10 h +5919 m +2459 m +10 h +4 h +3555 m +11 h +5920 m +1 h +82 h +13 h +4 h +1 h +10 h +1 h +4 h +12 h +5921 m +5922 m +1955 h +1 h +124 h +1 h +297 h +295 h +13 h +56 h +10 h +11 h +135 h +56 h +5923 m +338 h +1 h +125 h +41 h +46 h +3534 m +11 h +10 h +692 h +5924 m +1 h +5925 m +4 h +82 h +5926 m +10 h +48 h +4 h +4 h +4 h +4 h +10 h +45 h +219 m +5927 m +172 h +164 h +10 h +1 h +4 h +4 h +10 h +10 h +4780 m +4 h +5928 m +10 h +1016 h +4 h +4 h +1 h +10 h +64 h +11 h +59 h +164 h +4 h +601 h +5929 m +10 h +4 h +4441 m +1 h +4 h +1 h +5930 m +79 h +258 h +5931 m +185 h +25 h +1316 m +4 h +1 h +31 h +1796 m +4 h +4 h +94 h +45 h +5932 m +5933 m +4 h +1 h +279 h +1 h +10 h +258 h +29 m +1 h +167 h +1 h +5934 m +55 h +4 h +146 h +10 h +1 h +10 h +5935 m +4 h +687 h +57 h +1 h +5936 m +59 h +10 h +92 h +109 h +5937 m +10 h +11 h +425 h +809 m +1 h +4 h +5938 m +5939 m +83 h +167 h +1016 h +4 h +1 h +4 h +5940 m +5941 m +4 h +185 h +2475 m +4 h +10 h +262 h +4 h +1 h +4 h +147 h +10 h +1650 h +5942 m +5943 m +4 h +358 h +196 h +5944 m +5945 m +13 h +1 h +195 h +109 h +124 h +73 h +1 h +1 h +1 h +4 h +109 h +83 h +4941 m +4 h +11 h +119 h +114 h +4 h +10 h +157 h +538 h +5946 m +5947 m +1 h +1 h +1 h +5948 m +4 h +5949 m +4 h +2844 m +1780 h +10 h +1 h +4 h +106 h +5950 m +10 h +4 h +5951 m +185 h +1 h +5952 m +4 h +4 h +4 h +5953 m +36 h +4805 m +172 h +4 h +330 m +238 h +4 h +4 h +4 h +626 m +5954 m +10 h +4 h +5955 m +2719 m +4 h +10 h +4 h +5956 m +5944 m +4 h +3 h +4 h +4 h +10 h +4 h +4 h +238 h +4 h +4 h +109 h +164 h +383 h +4 h +5957 m +1722 m +5958 m +5959 m +12 h +383 h +97 h +31 h +11 h +5960 m +1 h +986 h +109 h +10 h +649 h +912 m +4 h +4 h +4 h +10 h +11 h +4 h +4503 m +1 h +4 h +1 h +164 h +140 h +55 h +5961 m +1 h +11 h +4 h +757 m +82 h +10 h +1039 m +5962 m +4 h +4 h +5963 m +1 h +10 h +5964 m +13 h +10 h +1 h +11 h +1 h +1 h +10 h +10 h +5965 m +5966 m +4 h +59 h +10 h +10 h +4 h +5967 m +4 h +25 h +1595 m +4 h +278 h +4 h +1619 m +124 h +4 h +10 h +4 h +10 h +11 h +434 h +4 h +1053 h +276 h +5968 m +10 h +74 h +59 h +4 h +2022 m +4 h +10 h +1 h +5969 m +31 h +10 h +4 h +1 h +11 h +4 h +614 m +1 h +4 h +4 h +83 h +4 h +278 h +83 h +5970 m +4 h +13 h +124 h +41 h +4 h +4 h +10 h +1 h +59 h +4 h +4 h +640 h +1 h +124 h +4 h +1 h +10 h +5971 m +2887 h +459 m +5972 m +5973 m +4 h +5974 m +65 h +1 h +4 h +5975 m +10 h +11 h +1 h +10 h +4 h +157 h +5976 m +10 h +5977 m +5978 m +59 h +279 h +156 h +359 h +144 h +4 h +1 h +8 h +77 h +1 h +1 h +1 h +1 h +1 h +4 h +5979 m +1105 h +1 h +11 h +1 h +59 h +1 h +1 h +208 m +1 h +112 h +1 h +94 h +10 h +1 h +41 h +4 h +5980 m +5981 m +10 h +1 h +195 h +4 h +5982 m +4 h +4 h +1 h +1137 h +5983 m +10 h +5984 m +185 h +1 h +4 h +5985 m +5986 m +5987 m +5988 m +82 h +114 h +7 m +169 h +1 h +10 h +1 h +5989 m +55 h +73 h +5990 m +250 h +10 h +1197 m +10 h +36 h +383 h +119 h +4 h +167 h +4 h +10 h +55 h +4 h +124 h +83 h +65 h +4 h +10 h +4 h +4 h +4 h +4 h +8 h +4810 m +109 h +1 h +4 h +1137 h +1 h +5991 m +1 h +91 h +10 h +170 h +5992 m +5993 m +169 h +1 h +10 h +4 h +368 h +3 h +5994 m +4 h +5464 m +1 h +1 h +5995 m +1 h +56 h +104 h +5996 m +4 h +1 h +73 h +4 h +258 h +4 h +1 h +1 h +10 h +10 h +1 h +1 h +258 h +10 h +41 h +83 h +1 h +1 h +10 h +274 h +4 h +718 h +11 h +10 h +224 h +1 h +83 h +10 h +4 h +190 h +5997 m +143 h +5980 m +2148 m +1 h +4 h +5998 m +4 h +4 h +5999 m +238 h +2794 m +6000 m +219 h +1 h +79 h +368 h +10 h +1 h +4 h +22 h +4 h +11 h +6001 m +1822 h +109 h +108 h +12 h +939 h +6002 m +4 h +167 h +6003 m +4 h +250 h +2719 m +256 h +1 h +1 h +57 h +1260 m +140 h +307 h +6004 m +1 h +4 h +11 h +195 h +1 h +83 h +6005 m +5422 m +4 h +1 h +1 h +6006 m +4 h +6007 m +59 h +6008 m +4 h +4 h +6009 m +190 h +4 h +79 h +4 h +11 h +195 h +10 h +6010 m +4 h +4 h +4 h +4 h +4297 m +2475 m +1 h +4 h +4 h +11 h +31 h +4 h +1 h +1 h +109 h +195 h +2788 m +10 h +1 h +6011 m +11 h +4 h +6012 m +1 h +1 h +4 h +4 h +6013 m +4 h +1 h +4 h +6014 m +6015 m +1 h +4 h +48 h +6016 m +10 h +6017 m +4 h +6018 m +4 h +158 h +1 h +10 h +4 h +4 h +11 h +1 h +4 h +40 h +195 h +4 h +6019 m +1 h +800 m +4 h +1 h +4 h +1 h +1 h +6020 m +6021 m +112 h +1 h +10 h +181 h +6022 m +11 h +10 h +11 h +1403 h +4 h +156 h +1 h +6023 m +204 h +1 h +4 h +10 h +6024 m +4 h +6025 m +12 h +57 h +6026 m +1 h +10 h +82 h +45 h +4 h +6027 m +109 h +10 h +4 h +44 m +10 h +1 h +2038 m +11 h +6028 m +1454 m +4 h +10 h +4 h +1123 m +4 h +4 h +1470 h +1 h +1 h +6029 m +1790 h +1 h +6030 m +10 h +109 h +443 h +1 h +1027 h +3847 m +31 h +185 h +4 h +4 h +6031 m +113 h +4 h +4 h +11 h +6032 m +435 m +1 h +6033 m +10 h +1 h +46 h +1 h +1 h +4 h +1 h +295 h +3558 m +10 h +6034 m +1 h +6035 m +4 h +1 h +307 h +1127 m +4 h +1 h +1 h +1 h +4 h +45 h +4 h +83 h +278 h +383 h +6036 m +4 h +6037 m +6038 m +6039 m +10 h +10 h +6040 m +1 h +1 h +279 h +4 h +1 h +156 h +4 h +4 h +11 h +1 h +6041 m +3214 m +97 h +119 h +1105 h +4 h +4 h +1 h +4 h +6042 m +4 h +10 h +4 h +59 h +4 h +147 h +22 h +13 h +10 h +4 h +4 h +10 h +6043 m +119 h +4 h +1 h +6044 m +381 m +4 h +10 h +1 h +1 h +6045 m +4 h +6046 m +109 h +10 h +4 h +289 h +6047 m +11 h +4 h +6048 m +82 h +4 h +1 h +6049 m +40 h +6050 m +307 h +266 h +6051 m +10 h +1 h +10 h +6052 m +6053 m +169 h +1780 h +2520 m +694 m +4 h +6054 m +6055 m +10 h +1 h +4 h +278 h +4 h +10 h +13 h +143 h +2041 m +6056 m +31 h +4 h +4498 m +55 h +1403 h +10 h +1 h +2484 m +6057 m +6058 m +4 h +10 h +104 h +1 h +1 h +1 h +4 h +1 h +10 h +2840 m +6059 m +6060 m +11 h +4 h +1 h +2887 h +6061 m +1 h +124 h +10 h +4 h +4 h +4 h +10 h +1677 m +4 h +4 h +10 h +10 h +6062 m +10 h +6063 m +6064 m +10 h +4 h +6065 m +1 h +6066 m +802 m +6067 m +4 h +104 h +6068 m +6069 m +6070 m +1105 h +56 h +6071 m +6072 m +6073 m +4 h +371 h +6074 m +4 h +6075 m +4 h +4 h +6076 m +1 h +10 h +6077 m +6078 m +6079 m +1 h +6080 m +1261 h +6081 m +64 h +4 h +6082 m +12 h +4 h +4 h +109 h +75 m +224 h +1 h +10 h +12 h +4 h +278 h +82 h +278 h +4 h +108 h +4 h +6083 m +59 h +332 h +6084 m +4 h +82 h +10 h +11 h +4 h +10 h +6085 m +857 m +4 h +6086 m +4 h +4 h +6087 m +82 h +4 h +1 h +1 h +4 h +4596 m +265 h +4 h +184 h +75 m +1 h +4 h +10 h +11 h +1 h +6088 m +6089 m +10 h +10 h +4 h +4 h +6090 m +10 h +104 h +41 h +6091 m +10 h +13 h +57 h +4 h +4 h +10 h +258 h +82 h +6092 m +4 h +4 h +6093 m +4 h +1359 h +1 h +4 h +6094 m +4 h +6095 m +1 h +1619 m +57 h +1 h +6096 m +4 h +6097 m +1024 m +4 h +569 h +4 h +1 h +10 h +6098 m +4 h +6099 m +757 m +36 h +4 h +258 h +4 h +1 h +386 h +570 h +110 h +4 h +1 h +4 h +1 h +170 h +124 h +1 h +4 h +6100 m +79 h +10 h +169 h +4 h +6101 m +8 h +57 h +4 h +1 h +4 h +1766 h +6102 m +12 h +6103 m +6104 m +10 h +4 h +1 h +114 h +6105 m +5809 m +6106 m +4 h +4 h +1250 h +4 h +97 h +4 h +4 h +4 h +4 h +124 h +11 h +57 h +174 h +10 h +1 h +601 h +12 h +1 h +4 h +10 h +6107 m +135 h +10 h +6108 m +258 h +6109 m +10 h +976 h +41 h +250 h +4 h +41 h +10 h +4 h +4 h +1 h +10 h +6110 m +73 h +6111 m +4 h +10 h +6112 m +83 h +4 h +109 h +1 h +1 h +4 h +1 h +278 h +82 h +10 h +4 h +6113 m +109 h +6114 m +4 h +1 h +1 h +25 h +6115 m +238 h +1 h +10 h +10 h +6116 m +986 h +25 h +4 h +11 h +1 h +116 m +109 h +1 h +1 h +1 h +22 h +4 h +64 h +6117 m +1 h +4 h +6118 m +10 h +25 h +10 h +4 h +10 h +11 h +4 h +10 h +124 h +1952 m +4 h +41 h +3115 m +1 h +6119 m +430 m +4 h +272 m +4 h +6120 m +6121 m +1 h +79 h +4 h +147 h +10 h +1 h +6122 m +1030 h +6123 m +31 h +1 h +1 h +10 h +6124 m +11 h +1 h +10 h +143 h +146 h +41 h +1 h +10 h +4 h +4 h +1 h +6125 m +4 h +238 h +11 h +4 h +124 h +4 h +4 h +1 h +10 h +1 h +4 h +170 h +6126 m +1 h +6127 m +1 h +4 h +10 h +4 h +77 h +4 h +8 h +4 h +6128 m +4 h +10 h +10 h +4 h +307 h +23 h +172 h +448 m +2158 m +146 h +1389 h +10 h +10 h +6129 m +4 h +125 h +6130 m +4 h +1 h +700 m +11 h +4 h +2591 m +10 h +6131 m +6132 m +4 h +1 h +10 h +6133 m +4 h +10 h +4 h +10 h +57 h +1 h +4 h +6134 m +57 h +10 h +6135 m +6136 m +1 h +3 h +10 h +94 h +266 h +4 h +4 h +10 h +1 h +1 h +11 h +6137 m +6138 m +1 h +10 h +10 h +10 h +4 h +1 h +1 h +4 h +4 h +10 h +6139 m +6140 m +146 h +6141 m +1137 h +10 h +4 h +1089 h +4 h +4 h +1 h +6142 m +1 h +6143 m +1822 h +10 h +203 m +435 m +4 h +6144 m +11 h +112 h +4 h +124 h +6145 m +1 h +986 h +10 h +1 h +10 h +6146 m +1 h +238 h +31 h +146 h +10 h +10 h +31 h +4 h +41 h +10 h +65 h +1 h +6147 m +73 h +4 h +6148 m +109 h +82 h +109 h +6149 m +41 h +4 h +4 h +1 h +10 h +6150 m +1 h +10 h +11 h +6151 m +4 h +6152 m +109 h +4 h +6153 m +185 h +1 h +25 h +4 h +4 h +10 h +1 h +108 h +104 h +278 h +6154 m +6155 m +1 h +1 h +4 h +6156 m +1403 h +6157 m +3 h +1 h +10 h +6132 m +1 h +1 h +169 h +4 h +4 h +297 h +31 h +1 h +1 h +4 h +614 m +6158 m +295 h +41 h +1 h +124 h +12 h +2887 h +4 h +10 h +1 h +6159 m +4 h +6160 m +6161 m +4359 m +1 h +6162 m +10 h +6163 m +332 h +10 h +10 h +4 h +6164 m +4 h +4 h +6165 m +109 h +4 h +10 h +10 h +195 h +6166 m +4 h +1 h +538 h +1 h +4 h +4 h +1089 h +10 h +10 h +6167 m +1 h +3 h +4 h +1128 m +1 h +4 h +1 h +12 h +79 h +1 h +6168 m +94 h +10 h +4520 m +6169 m +6170 m +1 h +1357 m +1100 m +4 h +6171 m +4 h +6172 m +11 h +196 h +1 h +11 h +4 h +10 h +4 h +359 h +6173 m +6174 m +6175 m +332 h +1 h +1 h +4 h +10 h +6176 m +1 h +55 h +1 h +6177 m +6178 m +1 h +82 h +10 h +6179 m +6180 m +1 h +1 h +1 h +371 h +4 h +4 h +6181 m +8 h +10 h +6182 m +1 h +4 h +6183 m +4 h +6184 m +4 h +6185 m +4 h +57 h +4 h +2374 m +6186 m +1 h +10 h +6187 m +3321 m +10 h +10 h +6188 m +1 h +1 h +6189 m +368 h +10 h +272 m +6190 m +97 h +307 h +6191 m +10 h +4 h +4 h +10 h +25 h +11 h +4 h +4 h +4 h +403 h +4 h +6192 m +10 h +538 h +1 h +6193 m +1 h +4 h +10 h +10 h +181 h +82 h +6194 m +4 h +4 h +338 h +112 h +31 h +4 h +10 h +6195 m +1 h +1 h +104 h +4 h +359 h +1 h +4 h +11 h +1 h +6196 m +10 h +266 h +3 h +57 h +1 h +1 h +6197 m +4 h +4 h +135 h +10 h +4 h +4 h +41 h +31 h +6198 m +6199 m +1 h +4 h +1 h +6200 m +10 h +1 h +10 h +1524 m +6201 m +935 h +6202 m +79 h +139 h +10 h +10 h +186 h +1 h +4 h +92 h +1 h +1 h +97 h +1218 m +6203 m +1 h +1 h +59 h +4 h +4 h +10 h +1 h +4 h +1 h +48 h +57 h +138 h +4 h +83 h +1 h +6204 m +6205 m +4 h +6206 m +575 h +11 h +1 h +4 h +4 h +6207 m +270 h +4 h +11 h +6208 m +4 h +10 h +4 h +11 h +4 h +83 h +109 h +258 h +4 h +6209 m +1 h +41 h +65 h +6210 m +146 h +4 h +11 h +31 h +1 h +6211 m +4 h +6212 m +1 h +1 h +820 h +10 h +388 m +4 h +4 h +4 h +276 h +1070 m +6213 m +4 h +250 h +1 h +1 h +4 h +10 h +124 h +155 m +6214 m +1 h +4 h +36 h +6215 m +6216 m +4 h +146 h +6217 m +6218 m +10 h +10 h +569 h +1 h +185 h +6219 m +10 h +1 h +104 h +59 h +6220 m +1016 h +6221 m +489 m +65 h +1 h +125 h +4 h +10 h +4 h +4 h +82 h +77 h +4 h +1 h +192 h +10 h +266 h +6222 m +124 h +1 h +1772 h +4 h +11 h +4 h +6223 m +4 h +36 h +1 h +59 h +1 h +6224 m +1 h +1 h +3 h +172 h +6225 m +4 h +4 h +2374 m +124 h +10 h +224 h +60 m +4 h +10 h +25 h +195 h +196 h +677 m +1 h +1 h +6226 m +6227 m +1642 h +1642 h +1 h +109 h +6228 m +6229 m +1 h +11 h +4 h +6230 m +12 h +6231 m +6232 m +146 h +1 h +10 h +4 h +1 h +1 h +10 h +25 h +10 h +1337 m +10 h +56 h +1 h +1 h +1 h +3 h +5348 m +11 h +6233 m +3 h +123 h +11 h +10 h +478 m +403 h +1 h +6234 m +6235 m +4 h +4 h +6236 m +4 h +11 h +4 h +10 h +57 h +6237 m +12 h +6238 m +4 h +4 h +1 h +6239 m +4 h +295 h +196 h +4 h +74 h +1 h +4 h +1 h +10 h +56 h +1 h +138 h +4 h +4 h +976 h +10 h +6240 m +6241 m +6242 m +1 h +1 h +10 h +1 h +59 h +4 h +45 h +1 h +368 h +4 h +10 h +25 h +4 h +6243 m +172 h +79 h +6244 m +649 h +10 h +1 h +204 h +4 h +928 h +10 h +83 h +45 h +6245 m +1 h +4 h +94 h +4 h +10 h +3 h +45 h +83 h +59 h +4 h +6246 m +3341 m +10 h +6247 m +4 h +124 h +31 h +6248 m +307 h +1 h +6249 m +6250 m +4 h +4 h +6251 m +1 h +4 h +4 h +6252 m +6253 m +10 h +113 h +238 h +6254 m +186 h +74 h +74 h +4 h +6255 m +1 h +82 h +155 m +2733 h +10 h +6256 m +6257 m +4 h +1 h +1 h +10 h +22 h +146 h +1 h +1893 m +124 h +4 h +1 h +1 h +10 h +104 h +6258 m +10 h +4 h +464 h +1619 h +6259 m +316 m +10 h +1 h +4 h +55 h +6260 m +6261 m +1 h +1 h +6262 m +4 h +6263 m +4 h +4 h +4 h +4 h +2069 m +4 h +11 h +1 h +4 h +1 h +6264 m +92 h +4 h +10 h +258 h +57 h +4 h +10 h +4 h +4 h +4 h +124 h +4 h +109 h +79 h +46 h +6265 m +4 h +368 h +614 m +6266 m +4 h +6267 m +4 h +575 h +1 h +1 h +3 h +11 h +56 h +1 h +31 h +6268 m +10 h +6269 m +10 h +1772 h +6270 m +4 h +22 h +1 h +1105 h +4576 m +6271 m +1 h +1 h +123 h +73 h +1 h +1 h +65 h +6272 m +4 h +1 h +351 m +4 h +6273 m +4 h +4 h +1453 m +6274 m +6275 m +4 h +4 h +190 h +358 h +28 h +94 h +1 h +1 h +48 h +4 h +22 h +6276 m +23 h +10 h +4 h +214 m +73 h +6277 m +4 h +1 h +1 h +10 h +4 h +4111 m +6278 m +10 h +4 h +6279 m +1 h +4 h +1 h +1 h +6280 m +10 h +4538 m +1 h +6281 m +1 h +135 h +6282 m +11 h +266 h +10 h +4 h +10 h +6283 m +4 h +11 h +10 h +147 h +935 h +4 h +4 h +1 h +1 h +4 h +4 h +6284 m +1 h +1 h +1 h +316 m +4 h +11 h +6285 m +3 h +4 h +4 h +1 h +6286 m +1359 h +6287 m +83 h +181 h +4 h +4 h +10 h +4 h +10 h +4 h +1 h +6288 m +4 h +6289 m +2308 m +2769 m +1 h +10 h +31 h +59 h +1 h +8 h +265 h +6290 m +10 h +1 h +6291 m +146 h +1 h +97 h +1 h +1 h +6292 m +97 h +1250 h +57 h +1 h +135 h +113 h +6293 m +4 h +119 h +1 h +124 h +158 h +6294 m +4 h +11 h +10 h +4 h +2719 h +1 h +196 h +4 h +10 h +1074 m +6295 m +6296 m +4 h +4 h +190 h +1 h +1 h +3 h +4626 m +41 h +172 h +1 h +1 h +82 h +6297 m +1 h +73 h +1 h +6298 m +1 h +4 h +4 h +108 h +1 h +25 h +10 h +184 h +4 h +167 h +11 h +10 h +109 h +4 h +488 m +1 h +97 h +57 h +238 h +6299 m +939 h +4 h +1 h +10 h +11 h +4 h +1 h +4 h +1 h +6300 m +1 h +1 h +10 h +3 h +6301 m +74 h +6302 m +1 h +1 h +10 h +6303 m +6304 m +1454 m +3 h +6305 m +488 h +4 h +6306 m +400 m +4 h +114 h +73 h +73 h +83 h +4 h +6307 m +1 h +1 h +1 h +229 h +4 h +196 h +57 h +4 h +4 h +1 h +6308 m +1 h +1 h +3 h +536 h +6309 m +626 m +10 h +10 h +196 h +1 h +4 h +57 h +4 h +1 h +41 h +11 h +6310 m +4 h +1 h +11 h +59 h +4 h +601 h +1 h +4 h +6311 m +10 h +6312 m +146 h +1 h +6313 m +1 h +119 h +82 h +6314 m +10 h +4 h +4 h +1 h +6315 m +1 h +1 h +167 h +6316 m +146 h +3558 m +25 h +3 h +124 h +10 h +4 h +1 h +3111 m +563 m +4 h +6317 m +4 h +10 h +591 m +91 h +12 h +1 h +31 h +4 h +1 h +4 h +4 h +4 h +55 h +1 h +4 h +41 h +11 h +109 h +1 h +1 h +4 h +6318 m +1 h +4 h +10 h +4 h +77 h +4 h +4 h +4 h +1 h +6319 m +1 h +4 h +4 h +3834 m +5765 m +57 h +4 h +157 h +399 h +91 h +10 h +169 h +10 h +57 h +124 h +10 h +4 h +4 h +73 h +172 h +124 h +93 h +104 h +2172 m +6320 m +1074 m +1 h +25 h +109 h +1 h +10 h +6321 m +897 m +1 h +6322 m +1096 m +4 h +322 m +40 h +82 h +6323 m +6324 m +4 h +4 h +4 h +135 h +56 h +4 h +4 h +536 h +10 h +1 h +6325 m +125 h +1 h +6326 m +1 h +124 h +1 h +11 h +1 h +4 h +64 h +6327 m +109 h +1 h +11 h +10 h +10 h +6328 m +4 h +4 h +1 h +1 h +4 h +4 h +25 h +140 h +4 h +1 h +10 h +109 h +109 h +82 h +6329 m +10 h +10 h +6330 m +1089 h +1 h +6331 m +4 h +6332 m +4151 m +6333 m +1 h +6334 m +4 h +56 h +40 h +10 h +10 h +4 h +185 h +1 h +104 h +10 h +4 h +1 h +1548 m +109 h +332 h +83 h +4 h +368 h +59 h +6335 m +1 h +104 h +1 h +4 h +278 h +5917 m +6336 m +12 h +10 h +119 h +1 h +10 h +10 h +6337 m +135 h +4 h +70 m +4 h +10 h +412 m +1 h +6338 m +10 h +6339 m +6340 m +4 h +6341 m +4 h +82 h +6342 m +3 h +10 h +146 h +6343 m +4 h +575 h +10 h +6344 m +1655 m +57 h +195 h +4 h +4 h +1 h +64 h +601 h +6345 m +1 h +97 h +1 h +1 h +55 h +1 h +10 h +6346 m +31 h +6347 m +8 h +1 h +11 h +10 h +10 h +94 h +156 h +6348 m +459 m +4 h +4 h +1677 m +6349 m +11 h +1 h +1 h +1 h +6350 m +4 h +6351 m +11 h +57 h +4 h +4 h +1 h +41 h +4 h +1 h +1 h +10 h +1 h +10 h +6352 m +1 h +10 h +57 h +10 h +4 h +1 h +10 h +10 h +13 h +55 h +3562 m +4 h +1 h +10 h +6353 m +6354 m +91 h +3 h +258 h +10 h +4 h +59 h +10 h +10 h +4 h +124 h +147 h +368 h +1 h +10 h +6355 m +6356 m +4 h +25 h +1 h +4 h +4 h +4 h +3068 m +1 h +4 h +4 h +4 h +4 h +4 h +6357 m +104 h +718 h +1 h +10 h +3 h +4 h +4 h +1 h +6358 m +4 h +6359 m +10 h +6360 m +113 h +1 h +6361 m +6362 m +25 h +1 h +1 h +1 h +10 h +4 h +10 h +6363 m +575 h +4 h +4 h +48 h +6364 m +1 h +3 h +10 h +6365 m +1 h +124 h +59 h +692 h +4 h +443 h +6366 m +4 h +91 h +6367 m +41 h +195 h +2261 m +4 h +4 h +1 h +10 h +6368 m +1 h +4 h +4 h +4 h +104 h +83 h +6369 m +10 h +3779 m +4 h +1362 h +1 h +82 h +4 h +3 h +6370 m +91 h +6371 m +1822 h +6372 m +125 h +6373 m +4 h +1 h +25 h +6374 m +238 h +123 h +57 h +2887 h +6375 m +104 h +10 h +10 h +10 h +1 h +4 h +1 h +2379 m +82 h +10 h +1 h +6376 m +603 m +31 h +4 h +4 h +4 h +3025 m +3141 m +3 h +10 h +10 h +157 h +6377 m +6378 m +1 h +13 h +27 h +1 h +41 h +10 h +6379 m +6380 m +1 h +270 h +11 h +4 h +4 h +4 h +6381 m +73 h +10 h +10 h +10 h +6382 m +10 h +307 h +4 h +6383 m +10 h +1 h +6384 m +65 h +918 m +4 h +383 h +125 h +10 h +6385 m +808 m +25 h +4 h +4 h +4 h +939 h +6386 m +83 h +184 h +4 h +10 h +1296 m +1 h +1261 h +1 h +10 h +97 h +10 h +6387 m +687 h +4 h +135 h +4 h +5475 m +65 h +6388 m +1185 m +4 h +6389 m +10 h +1 h +538 h +1 h +5070 m +10 h +4 h +4 h +1 h +4 h +6390 m +1 h +4 h +6391 m +10 h +4 h +57 h +6392 m +10 h +10 h +2146 m +1 h +65 h +371 h +10 h +195 h +1 h +59 h +4 h +3558 m +170 h +4 h +6393 m +4 h +4 h +31 h +1 h +6394 m +22 h +1 h +4 h +4 h +4 h +109 h +10 h +4 h +41 h +4 h +3 h +4 h +297 h +4 h +990 m +25 h +4 h +4 h +4 h +359 h +83 h +4 h +45 h +41 h +1 h +10 h +59 h +1 h +4 h +11 h +6395 m +6396 m +4 h +147 h +4 h +6397 m +4 h +104 h +4 h +83 h +4 h +3383 m +6398 m +10 h +124 h +1370 m +4 h +276 h +97 h +1 h +10 h +238 h +4 h +6399 m +4 h +4 h +6400 m +687 h +4 h +1 h +4 h +82 h +55 h +31 h +6401 m +10 h +4 h +6402 m +64 h +65 h +6403 m +6404 m +4 h +11 h +6405 m +6406 m +6407 m +10 h +4 h +4 h +4 h +10 h +1 h +6408 m +1 h +1 h +4 h +1 h +4 h +6409 m +6410 m +10 h +10 h +1 h +1 h +31 h +82 h +3278 m +486 m +6411 m +4 h +10 h +4 h +11 h +55 h +1 h +10 h +295 h +6412 m +6413 m +4 h +443 h +4 h +1 h +4 h +6414 m +276 h +10 h +31 h +190 h +1 h +4 h +1 h +6415 m +10 h +4 h +1 h +640 h +4 h +4 h +57 h +1 h +6416 m +4 h +185 h +6417 m +4 h +82 h +1 h +2288 m +1 h +82 h +6418 m +4 h +125 h +6419 m +4 h +57 h +125 h +4 h +443 h +146 h +5 m +4 h +12 h +10 h +6420 m +6421 m +1027 h +4 h +6422 m +10 h +4 h +1 h +31 h +4 h +1 h +1 h +6423 m +10 h +4 h +4 h +112 h +6424 m +1 h +6425 m +11 h +1 h +656 m +10 h +1 h +230 h +4 h +6426 m +6427 m +1 h +125 h +10 h +65 h +56 h +1 h +10 h +1 h +109 h +6428 m +1 h +10 h +59 h +1 h +1 h +6429 m +4 h +1 h +13 h +4 h +91 h +119 h +10 h +41 h +41 h +13 h +1685 h +6430 m +2339 m +1 h +4 h +10 h +6431 m +386 h +1 h +79 h +135 h +59 h +6432 m +3435 m +6433 m +6434 m +1 h +4 h +6435 m +6436 m +6437 m +6438 m +1 h +6439 m +6440 m +57 h +4 h +1 h +1 h +57 h +57 h +4 h +332 h +6441 m +4 h +64 h +119 h +493 m +6442 m +6443 m +4 h +82 h +1 h +4 h +12 h +1 h +10 h +156 h +805 m +569 h +4 h +104 h +10 h +6444 m +6445 m +4 h +1 h +10 h +6446 m +10 h +11 h +10 h +4 h +87 m +6447 m +6448 m +258 h +4 h +11 h +330 h +4 h +10 h +1 h +82 h +4 h +11 h +1 h +10 h +4 h +4 h +4 h +4 h +4 h +4 h +6449 m +4 h +6450 m +6451 m +6452 m +6399 m +6453 m +11 h +109 h +4 h +10 h +1685 h +4 h +1 h +4 h +1 h +4 h +4 h +4 h +6454 m +6455 m +10 h +1 h +4 h +41 h +4 h +6456 m +1 h +10 h +488 h +10 h +4 h +10 h +73 h +4 h +4914 m +1 h +10 h +11 h +6457 m +737 m +11 h +69 m +4 h +6458 m +1 h +12 h +6459 m +12 h +1 h +4 h +1 h +4 h +6460 m +1 h +10 h +1 h +6461 m +976 h +146 h +10 h +57 h +10 h +11 h +4 h +170 h +1 h +184 h +6462 m +10 h +4 h +4 h +4 h +10 h +114 h +10 h +477 m +4 h +4 h +11 h +6463 m +74 h +64 h +322 h +3561 m +1 h +27 h +403 h +93 h +10 h +83 h +4 h +4 h +6464 m +11 h +82 h +4 h +6465 m +6466 m +4 h +4229 m +1 h +6467 m +10 h +124 h +55 h +224 h +10 h +79 h +6468 m +1 h +36 h +1 h +6469 m +1 h +10 h +4 h +10 h +124 h +6470 m +1 h +6471 m +4 h +1 h +4 h +6472 m +368 h +297 h +4 h +4 h +150 m +1 h +1541 m +6473 m +6474 m +4 h +4 h +1 h +229 h +6475 m +114 h +1886 m +4 h +10 h +4 h +31 h +10 h +82 h +6476 m +6477 m +94 h +4858 m +1 h +11 h +1 h +13 h +6105 m +6478 m +6479 m +158 h +4 h +10 h +4 h +250 h +1 h +4 h +1 h +4 h +4 h +4 h +1 h +358 h +4 h +4 h +82 h +83 h +41 h +83 h +4 h +4 h +25 h +124 h +138 h +4 h +448 m +1 h +575 h +1 h +6480 m +1 h +104 h +10 h +1 h +4 h +55 h +6481 m +6482 m +6483 m +10 h +25 h +4 h +307 h +6484 m +4 h +1761 m +6485 m +6486 m +1 h +57 h +443 h +10 h +4 h +172 h +10 h +10 h +143 h +10 h +10 h +4 h +4 h +6487 m +10 h +124 h +1 h +6488 m +4 h +1 h +258 h +10 h +10 h +6489 m +1 h +6490 m +3 h +17 m +97 h +4 h +6491 m +1 h +1 h +1 h +258 h +4 h +1 h +857 m +55 h +6492 m +4 h +124 h +1 h +93 h +104 h +6493 m +6494 m +229 h +5478 m +4 h +31 h +104 h +10 h +156 h +1 h +10 h +1 h +181 h +83 h +4 h +4 h +3 h +6495 m +41 h +146 h +601 h +6496 m +1 h +4 h +6497 m +6498 m +6499 m +6500 m +6501 m +82 h +10 h +4 h +4 h +1 h +6502 m +10 h +11 h +10 h +6503 m +181 h +4 h +4 h +109 h +4 h +41 h +6504 m +4 h +4 h +94 h +59 h +6505 m +1619 h +167 h +228 m +1 h +6506 m +1 h +4 h +6507 m +4 h +6508 m +4 h +1751 m +4 h +6509 m +1 h +6423 m +4 h +4 h +1260 m +11 h +6510 m +10 h +4 h +6511 m +124 h +4 h +10 h +10 h +4 h +6512 m +1 h +3170 m +4 h +12 h +1 h +112 h +4 h +41 h +1 h +6513 m +412 m +73 h +6514 m +1 h +1 h +1 h +6515 m +10 h +10 h +1 h +92 h +10 h +4 h +6516 m +125 h +11 h +6517 m +10 h +4 h +4 h +114 h +82 h +4 h +4 h +4 h +10 h +4 h +3396 m +10 h +1 h +2163 m +6518 m +1 h +4 h +10 h +4 h +6519 m +10 h +1 h +4 h +4 h +1 h +1 h +4 h +3680 m +1 h +4 h +6520 m +114 h +4 h +65 h +10 h +82 h +10 h +82 h +1 h +1 h +6521 m +4 h +59 h +536 h +10 h +6522 m +737 m +1 h +230 h +4 h +1 h +4 h +6523 m +10 h +10 h +1089 h +109 h +6524 m +6525 m +10 h +1548 m +802 m +1 h +4 h +4 h +36 h +4 h +10 h +1 h +4 h +6526 m +146 h +10 h +6527 m +124 h +1 h +4 h +1 h +299 h +1 h +6528 m +4 h +1 h +10 h +82 h +83 h +31 h +11 h +6529 m +1 h +386 h +6530 m +1 h +1322 m +10 h +4 h +6531 m +83 h +6532 m +6533 m +359 h +1 h +4 h +82 h +10 h +6534 m +41 h +10 h +4 h +10 h +4 h +1 h +83 h +2442 m +59 h +4 h +6535 m +6536 m +914 m +82 h +1 h +6537 m +1 h +4 h +109 h +1 h +6538 m +59 h +4 h +4 h +1 h +146 h +1788 m +6539 m +6540 m +10 h +156 h +1 h +1 h +77 h +10 h +22 h +1 h +4 h +4 h +6541 m +1 h +1 h +6542 m +1 h +6543 m +10 h +69 h +6544 m +147 h +1027 h +4 h +1 h +92 h +4 h +6545 m +4 h +6546 m +1 h +10 h +6547 m +195 h +1 h +4 h +6548 m +297 h +1 h +59 h +124 h +4 h +10 h +10 h +4 h +1 h +10 h +6549 m +1 h +11 h +6550 m +4 h +4 h +59 h +6551 m +829 m +59 h +1 h +6552 m +185 h +10 h +10 h +757 h +1 h +6553 m +4 h +3 h +4 h +640 h +59 h +73 h +4 h +83 h +1 h +10 h +147 h +6554 m +12 h +1 h +1 h +1 h +143 h +4 h +4 h +4 h +10 h +358 h +6555 m +4 h +1 h +6556 m +1 h +125 h +147 h +6557 m +1 h +447 h +124 h +1 h +6558 m +10 h +10 h +6559 m +6560 m +92 h +6561 m +5944 h +170 h +4 h +4 h +629 m +1 h +1 h +10 h +74 h +4 h +104 h +104 h +10 h +6562 m +57 h +4 h +28 h +6563 m +265 h +4 h +6564 m +41 h +6565 m +4 h +124 h +265 h +184 h +125 h +1 h +6566 m +6567 m +1 h +82 h +1 h +4 h +4 h +129 h +1137 h +4 h +5581 m +6568 m +1 h +4 h +6569 m +6570 m +56 h +1 h +6571 m +4 h +4 h +10 h +10 h +13 h +4 h +129 h +6572 m +109 h +4 h +1201 h +10 h +4 h +195 h +173 h +10 h +10 h +1137 h +164 h +27 h +125 h +10 h +1 h +5125 m +1 h +10 h +4 h +1 h +4 h +6573 m +4 h +4 h +185 h +1 h +1 h +1650 h +1137 h +83 h +6574 m +118 h +1 h +4 h +278 h +6575 m +124 h +1 h +45 h +4 h +1 h +1 h +10 h +6576 m +4 h +25 h +4 h +1137 h +4 h +1 h +82 h +656 m +6577 m +10 h +6578 m +1 h +12 h +2116 m +109 h +4 h +3 h +4 h +4 h +4 h +6579 m +6580 m +4 h +10 h +4 h +6581 m +169 h +6582 m +1632 m +276 h +1 h +1 h +192 h +3 h +6583 m +1 h +1 h +2184 m +1 h +170 h +6584 m +4 h +6585 m +1 h +4 h +1 h +4 h +31 h +2891 m +41 h +94 h +97 h +4 h +4 h +4 h +10 h +124 h +6586 m +4 h +4 h +1 h +4 h +10 h +4 h +57 h +692 h +4 h +10 h +10 h +6587 m +45 h +4 h +358 h +10 h +124 h +4 h +6588 m +1 h +4 h +74 h +6589 m +6590 m +6591 m +11 h +129 h +4 h +1250 h +4 h +82 h +6592 m +229 h +4 h +6593 m +4 h +1 h +10 h +6594 m +25 h +10 h +4 h +10 h +104 h +1 h +6595 m +10 h +6596 m +6597 m +10 h +4 h +10 h +368 h +109 h +125 h +1 h +6598 m +4 h +6599 m +4 h +1 h +6600 m +4 h +4 h +170 h +4 h +4 h +129 h +6601 m +57 h +10 h +6602 m +10 h +6603 m +10 h +10 h +41 h +1 h +10 h +83 h +4 h +1 h +1 h +4 h +4 h +4 h +12 h +10 h +11 h +1595 m +10 h +11 h +6604 m +6605 m +6606 m +488 h +109 h +10 h +6607 m +1122 m +4 h +94 h +31 h +4 h +1 h +10 h +4 h +6608 m +170 h +6609 m +10 h +10 h +4 h +4 h +1 h +1 h +6610 m +4 h +4 h +170 h +6611 m +1 h +6612 m +4 h +82 h +109 h +4 h +10 h +8 h +6613 m +307 h +1 h +4 h +11 h +4 h +3 h +229 h +4 h +4 h +6614 m +10 h +192 h +6615 m +10 h +4 h +4 h +124 h +1 h +6616 m +6617 m +1 h +1 h +104 h +1 h +13 h +119 h +468 m +1 h +114 h +73 h +1 h +4 h +74 h +6618 m +601 h +4 h +25 h +10 h +4 h +1 h +4 h +270 h +41 h +6619 m +6620 m +4 h +477 m +1 h +4 h +6621 m +196 h +4 h +10 h +1 h +1403 h +4 h +4 h +4 h +4 h +6622 m +10 h +10 h +11 h +83 h +2303 m +1 h +10 h +11 h +1 h +25 h +11 h +2268 m +10 h +83 h +1 h +1 h +6623 m +6624 m +6625 m +1 h +4 h +12 h +1 h +31 h +10 h +6626 m +73 h +181 h +1 h +4 h +6627 m +6628 m +10 h +6629 m +4 h +4 h +4 h +184 h +82 h +10 h +262 h +6630 m +10 h +4 h +10 h +4 h +10 h +184 h +45 h +443 h +1 h +4 h +6631 m +5341 m +4932 m +10 h +6200 m +12 h +59 h +6632 m +170 h +6633 m +6634 m +167 h +4 h +4 h +1 h +91 h +857 m +12 h +4 h +6635 m +6636 m +13 h +10 h +4 h +94 h +1 h +31 h +55 h +1 h +6637 m +601 h +5590 m +1 h +104 h +1 h +6638 m +4 h +358 h +11 h +6639 m +11 h +1 h +4 h +4 h +56 h +11 h +12 h +6640 m +4 h +10 h +6641 m +6642 m +238 h +10 h +6643 m +4 h +403 h +6644 m +6645 m +1 h +4 h +4 h +6646 m +1 h +1576 m +25 h +1 h +4 h +1 h +4 h +6647 m +4 h +31 h +10 h +82 h +6648 m +1 h +2591 m +79 h +965 m +1 h +1 h +10 h +1 h +25 h +10 h +1304 m +64 h +11 h +10 h +83 h +4 h +167 h +4 h +4 h +1 h +6649 m +570 h +784 m +4 h +146 h +10 h +4 h +1 h +57 h +29 m +4 h +1 h +10 h +1 h +4 h +297 h +4 h +109 h +109 h +6650 m +1 h +184 h +266 h +11 h +443 h +1 h +6651 m +10 h +1769 m +11 h +123 h +603 m +10 h +1 h +4 h +4 h +1 h +250 h +1 h +1 h +6652 m +109 h +4 h +4 h +10 h +4 h +10 h +64 h +10 h +82 h +125 h +1218 m +146 h +575 h +6653 m +135 h +6654 m +4 h +4 h +82 h +124 h +10 h +79 h +4 h +1504 m +1 h +4 h +6655 m +4 h +6656 m +1 h +10 h +196 h +65 h +10 h +10 h +186 h +114 h +1 h +4 h +1321 m +10 h +1 h +1 h +36 h +6657 m +10 h +1 h +4 h +4 h +1 h +1 h +6658 m +1 h +1 h +135 h +57 h +13 h +1 h +4 h +6659 m +4 h +649 h +4 h +4 h +4 h +79 h +11 h +83 h +6660 m +10 h +4 h +6661 m +10 h +4 h +1 h +6031 m +4 h +733 m +6662 m +6663 m +3 h +10 h +4 h +1003 m +4 h +4 h +6664 m +4 h +1 h +6665 m +5505 m +192 h +4 h +83 h +10 h +5762 m +59 h +4 h +13 h +13 h +4 h +1 h +1 h +1 h +6666 m +6667 m +41 h +6668 m +4 h +1 h +6669 m +1 h +4 h +4 h +1 h +2418 h +4 h +6670 m +10 h +6671 m +1 h +1322 m +1 h +1 h +4 h +4 h +1646 m +195 h +3188 m +113 h +4 h +8 h +4 h +1 h +4 h +6672 m +10 h +10 h +6673 m +6674 m +10 h +6675 m +4 h +124 h +110 h +10 h +83 h +1 h +147 h +1 h +139 h +6676 m +1 h +12 h +4 h +135 h +40 h +4 h +4 h +4723 m +83 h +10 h +4 h +6677 m +6678 m +1 h +195 h +4 h +10 h +10 h +4 h +4 h +10 h +6679 m +10 h +1 h +10 h +1576 m +97 h +1650 h +4 h +6680 m +1 h +167 h +1 h +4 h +1948 m +4 h +10 h +6681 m +10 h +119 h +172 h +1 h +4 h +4 h +4 h +57 h +575 h +6682 m +124 h +11 h +1 h +2256 m +4359 m +6683 m +3680 m +4 h +4 h +55 h +82 h +1 h +10 h +11 h +1 h +135 h +4 h +8 h +4 h +109 h +82 h +25 h +125 h +4 h +10 h +4 h +1 h +11 h +4 h +79 h +10 h +4 h +601 h +74 h +4 h +10 h +3837 m +10 h +4 h +1 h +6684 m +59 h +6685 m +6686 m +6687 m +83 h +4 h +10 h +6688 m +1 h +820 h +4 h +6689 m +6690 m +4 h +104 h +109 h +6691 m +6692 m +601 h +1 h +10 h +10 h +195 h +8 h +10 h +911 h +4520 m +443 h +6693 m +4 h +4 h +1 h +147 h +3177 m +10 h +6694 m +82 h +6695 m +6696 m +55 h +12 h +1 h +6697 m +6698 m +4966 m +10 h +1 h +6699 m +124 h +6700 m +6701 m +4 h +74 h +4 h +4 h +6702 m +1 h +4 h +4 h +124 h +146 h +297 h +1 h +184 h +1 h +6703 m +12 h +4 h +123 h +10 h +1 h +77 h +27 h +1 h +10 h +1 h +1 h +6704 m +92 h +4 h +1 h +6705 m +1 h +4 h +6706 m +6707 m +1650 h +10 h +3 h +1 h +10 h +1 h +118 h +10 h +1 h +6708 m +1 h +4 h +97 h +10 h +1 h +6709 m +801 m +169 h +10 h +41 h +146 h +4 h +4 h +4 h +1 h +6710 m +4 h +4 h +1 h +4 h +10 h +6711 m +1 h +10 h +1 h +241 m +4 h +1 h +4 h +1 h +1 h +238 h +10 h +4 h +3150 m +4 h +10 h +10 h +10 h +82 h +820 h +1 h +6712 m +4 h +6713 m +4 h +10 h +1 h +125 h +10 h +10 h +4 h +6714 m +48 h +1 h +6715 m +1 h +57 h +4 h +6716 m +64 h +238 h +6717 m +195 h +1 h +6718 m +4 h +368 h +493 h +6719 m +1 h +57 h +104 h +4 h +124 h +25 h +575 h +10 h +1 h +11 h +4 h +10 h +4 h +1 h +1 h +10 h +57 h +173 h +1 h +143 h +4 h +4 h +6720 m +11 h +1764 m +6721 m +4 h +10 h +4 h +6722 m +10 h +1 h +1 h +1 h +6723 m +6724 m +6725 m +124 h +6726 m +6727 m +1089 h +4 h +6728 m +4 h +4 h +224 h +4 h +12 h +6729 m +386 h +4 h +10 h +4 h +31 h +4 h +6730 m +11 h +82 h +83 h +6731 m +4 h +73 h +986 h +1 h +4 h +10 h +4 h +1 h +10 h +41 h +2914 m +4 h +1197 m +4 h +1 h +10 h +10 h +82 h +4 h +1 h +4 h +4 h +13 h +1 h +4 h +4 h +10 h +640 h +10 h +1 h +6732 m +1 h +11 h +10 h +11 h +10 h +4 h +6733 m +1 h +11 h +6734 m +1 h +97 h +4 h +6735 m +4 h +10 h +1 h +94 h +4 h +10 h +11 h +1 h +119 h +1 h +4 h +3 h +25 h +1 h +97 h +185 h +4 h +4 h +601 h +6736 m +1725 m +6737 m +1185 m +1 h +97 h +10 h +11 h +190 h +4 h +4 h +1 h +6738 m +4 h +4 h +258 h +4 h +4 h +104 h +1 h +4 h +4 h +4 h +31 h +1 h +6739 m +4 h +10 h +10 h +6740 m +1 h +10 h +195 h +10 h +6741 m +2266 m +10 h +8 h +57 h +10 h +57 h +6742 m +169 h +1 h +10 h +6743 m +4 h +6744 m +6745 m +31 h +258 h +10 h +1 h +1 h +1 h +4 h +25 h +10 h +6746 m +4 h +6747 m +444 m +3 h +4 h +1 h +65 h +10 h +97 h +10 h +10 h +10 h +8 h +6748 m +10 h +1 h +4 h +4 h +270 h +1 h +10 h +4 h +73 h +31 h +4 h +11 h +11 h +6749 m +6750 m +10 h +2319 m +229 h +25 h +1 h +6751 m +4 h +4 h +1 h +4 h +41 h +109 h +10 h +82 h +1 h +6752 m +3 h +73 h +190 h +4 h +11 h +10 h +4 h +10 h +4 h +6753 m +1 h +1 h +10 h +1 h +4 h +1 h +6754 m +1 h +10 h +4 h +1 h +6755 m +4 h +4 h +10 h +4 h +83 h +4 h +169 h +332 h +4 h +307 h +10 h +4 h +4 h +307 h +1 h +6756 m +109 h +297 h +3 h +4 h +1 h +13 h +1 h +25 h +1 h +4 h +1 h +1 h +4 h +4 h +59 h +1 h +4 h +6757 m +55 h +10 h +124 h +143 h +6758 m +31 h +10 h +4 h +10 h +1 h +65 h +74 h +82 h +6759 m +10 h +104 h +104 h +1 h +1 h +6760 m +6761 m +1 h +1 h +1 h +4 h +10 h +6762 m +3373 m +1 h +556 h +6763 m +3025 m +6764 m +4 h +4 h +3 h +784 m +629 m +10 h +4 h +4 h +10 h +6765 m +4 h +56 h +6766 m +31 h +4 h +10 h +10 h +93 h +10 h +82 h +10 h +1137 h +272 h +79 h +147 h +4 h +6767 m +74 h +6768 m +10 h +10 h +10 h +104 h +1 h +10 h +6769 m +97 h +4 h +6770 m +4 h +1 h +6771 m +1 h +69 h +4 h +718 h +31 h +6772 m +10 h +4 h +6773 m +11 h +289 h +6774 m +4 h +195 h +125 h +1 h +143 h +10 h +332 h +10 h +125 h +4 h +83 h +185 h +3199 m +1 h +31 h +4 h +109 h +10 h +10 h +82 h +10 h +4 h +6775 m +10 h +6776 m +1 h +82 h +10 h +1 h +1 h +6777 m +1 h +6778 m +40 h +1 h +6779 m +1 h +4 h +4 h +10 h +1 h +1 h +6780 m +10 h +1 h +1 h +10 h +1406 m +146 h +447 h +1 h +4 h +6781 m +123 h +4 h +4 h +4 h +4 h +4 h +11 h +4 h +11 h +278 h +10 h +976 h +11 h +73 h +11 h +4 h +2532 m +1 h +4 h +6782 m +6783 m +6784 m +6785 m +11 h +112 h +297 h +4 h +6786 m +4 h +6787 m +6788 m +4 h +10 h +2433 m +41 h +6789 m +6790 m +11 h +4 h +12 h +1 h +1409 m +238 h +65 h +11 h +6791 m +1 h +73 h +25 h +6792 m +4 h +10 h +6793 m +6794 m +6795 m +10 h +3 h +6796 m +1 h +203 m +124 h +10 h +10 h +55 h +1650 h +59 h +1 h +6270 m +10 h +185 h +25 h +10 h +1 h +1 h +73 h +110 h +10 h +1642 h +123 h +92 h +1499 m +1 h +4 h +1535 m +1 h +10 h +4 h +1406 m +1 h +575 h +6797 m +10 h +6798 m +6799 m +22 h +4 h +1 h +10 h +3177 m +4 h +4 h +4 h +119 h +493 h +1 h +10 h +10 h +83 h +11 h +6800 m +1 h +4 h +10 h +125 h +11 h +1 h +6801 m +10 h +3 h +59 h +10 h +11 h +8 h +6802 m +1 h +1 h +1 h +1470 h +10 h +77 h +1 h +6803 m +4 h +6804 m +4 h +6805 m +4 h +4 h +1 h +351 m +1 h +4 h +1 h +4 h +6806 m +4 h +4 h +135 h +4 h +10 h +11 h +4 h +687 h +359 h +6807 m +11 h +6808 m +1030 h +6809 m +6810 m +4 h +82 h +692 h +1 h +6811 m +581 m +11 h +118 h +4 h +4 h +6812 m +4 h +6813 m +1304 m +4 h +10 h +6814 m +295 h +157 h +1 h +1 h +109 h +10 h +10 h +104 h +4 h +1953 m +1 h +4 h +195 h +12 h +109 h +114 h +6815 m +31 h +6816 m +170 h +104 h +6817 m +4 h +10 h +10 h +94 h +135 h +4 h +10 h +41 h +82 h +114 h +13 h +10 h +4 h +10 h +4 h +368 h +97 h +10 h +4 h +82 h +25 h +10 h +83 h +1359 h +1 h +4 h +11 h +6818 m +10 h +1279 m +4689 m +10 h +10 h +6819 m +4 h +4 h +1 h +4 h +184 h +4 h +172 h +4 h +10 h +10 h +1 h +6820 m +69 h +10 h +1 h +25 h +620 m +6821 m +143 h +4 h +1 h +1 h +229 h +6822 m +4 h +10 h +6823 m +124 h +146 h +3 h +1 h +11 h +12 h +1 h +4 h +371 h +1 h +468 m +1 h +3161 m +6824 m +443 h +4 h +4 h +10 h +258 h +4 h +109 h +10 h +6825 m +6826 m +11 h +6827 m +4 h +1 h +6828 m +4574 m +124 h +6829 m +4 h +6830 m +1083 m +6831 m +6832 m +4 h +10 h +1 h +4 h +10 h +6833 m +10 h +57 h +45 h +124 h +4 h +5348 h +109 h +4 h +59 h +5225 m +6834 m +4 h +25 h +10 h +1 h +3240 m +1 h +10 h +10 h +6835 m +6836 m +4 h +4 h +4 h +3 h +1 h +6837 m +10 h +10 h +10 h +1642 h +4 h +112 h +6838 m +1 h +41 h +359 h +4 h +10 h +6839 m +192 h +1 h +90 m +6840 m +625 m +258 h +27 h +1 h +6841 m +4 h +4 h +48 h +692 h +10 h +358 h +10 h +4 h +104 h +3422 m +185 h +1 h +1 h +72 m +10 h +1 h +2410 m +256 h +4 h +12 h +6842 m +4 h +4 h +6843 m +4 h +4 h +358 h +575 h +10 h +77 h +4 h +12 h +4 h +10 h +25 h +11 h +6844 m +6845 m +4 h +1 h +4 h +10 h +1 h +10 h +1 h +1 h +6846 m +8 h +79 h +124 h +10 h +6847 m +4 h +3 h +185 h +6848 m +11 h +1 h +1 h +25 h +1 h +6849 m +6850 m +4 h +4 h +6851 m +45 h +1 h +443 h +185 h +11 h +6852 m +412 m +3 h +4 h +124 h +1 h +908 m +12 h +1 h +40 h +10 h +6853 m +57 h +10 h +6854 m +10 h +4 h +1 h +4 h +11 h +10 h +767 m +124 h +192 h +110 h +4 h +4 h +1 h +6855 m +4 h +230 h +1 h +6856 m +6857 m +801 m +4 h +10 h +1 h +536 h +4 h +1 h +1 h +10 h +1 h +1 h +4 h +1 h +4 h +4 h +1 h +11 h +4 h +3 h +6858 m +4 h +2096 m +6859 m +10 h +91 h +1 h +4 h +1470 h +6860 m +4 h +10 h +3 h +6861 m +1 h +104 h +10 h +10 h +6862 m +4 h +4 h +10 h +13 h +185 h +10 h +4 h +97 h +1 h +4 h +56 h +116 m +3398 m +59 h +1454 m +1 h +4 h +25 h +23 h +10 h +10 h +169 h +4 h +4 h +1 h +4 h +73 h +123 h +1 h +3 h +13 h +6863 m +10 h +10 h +4 h +1 h +195 h +82 h +2285 m +4 h +10 h +6864 m +10 h +687 h +4 h +92 h +10 h +1403 h +4 h +6865 m +4 h +6866 m +1 h +4 h +5958 m +4 h +4 h +4863 m +1 h +4 h +109 h +2984 m +4 h +6867 m +4 h +10 h +6868 m +83 h +6869 m +1 h +4 h +6870 m +1 h +6871 m +6872 m +1 h +4 h +278 h +4 h +10 h +1 h +36 h +4 h +4 h +4 h +138 h +11 h +57 h +109 h +10 h +1 h +4 h +1 h +4 h +4 h +3 h +10 h +1 h +6873 m +6874 m +1470 h +25 h +4 h +230 h +1 h +25 h +4 h +167 h +6875 m +219 h +91 h +258 h +279 h +270 h +278 h +79 h +41 h +1 h +1 h +4 h +146 h +1 h +869 m +6876 m +4 h +6877 m +1 h +3 h +4 h +41 h +31 h +6878 m +4 h +10 h +1880 m +186 h +6879 m +124 h +13 h +6880 m +4 h +4 h +1 h +10 h +6881 m +10 h +6882 m +6883 m +4 h +1 h +10 h +10 h +190 h +1 h +6884 m +4 h +31 h +1 h +4 h +4 h +10 h +112 h +1 h +1 h +4 h +4 h +1 h +10 h +6885 m +41 h +6886 m +25 h +6887 m +4 h +5505 m +83 h +1 h +6888 m +1 h +1 h +104 h +4 h +1 h +399 h +73 h +6889 m +6890 m +4 h +10 h +6891 m +74 h +3539 m +10 h +615 m +4 h +1137 h +10 h +10 h +1 h +6892 m +6893 m +4 h +6894 m +172 h +4 h +3825 m +1 h +10 h +4 h +1 h +6895 m +6896 m +1089 h +4 h +6897 m +31 h +1 h +630 m +181 h +6898 m +6899 m +10 h +1 h +25 h +4 h +4 h +1 h +1 h +3 h +6900 m +4 h +94 h +4 h +1 h +6901 m +1359 h +6902 m +1 h +6903 m +1406 h +6904 m +91 h +6905 m +6906 m +3657 m +10 h +6907 m +10 h +1697 m +1968 m +6908 m +119 h +966 m +6909 m +10 h +104 h +5863 m +109 h +6910 m +6911 m +10 h +1 h +6912 m +6913 m +265 h +93 h +433 m +11 h +10 h +1 h +4 h +1 h +6914 m +4 h +82 h +125 h +57 h +6915 m +4 h +1 h +74 h +1 h +6916 m +4 h +4 h +603 m +181 h +59 h +4 h +41 h +601 h +123 h +124 h +6917 m +6918 m +4 h +4 h +36 h +1 h +279 h +2887 h +6919 m +1 h +10 h +4 h +1 h +6920 m +10 h +1137 h +4240 m +4 h +6921 m +1 h +4 h +6922 m +109 h +4 h +538 h +6923 m +6924 m +57 h +976 h +10 h +1 h +1 h +10 h +6925 m +124 h +6926 m +1 h +195 h +4 h +857 h +10 h +6927 m +6928 m +1 h +4 h +1 h +10 h +6929 m +1 h +1 h +11 h +6930 m +2733 h +6931 m +6932 m +3513 m +6933 m +6934 m +4 h +270 h +1 h +1 h +31 h +6935 m +97 h +4 h +3 h +3 h +4 h +6936 m +40 h +4 h +64 h +6937 m +6938 m +1 h +74 h +4089 m +1 h +4 h +3 h +6939 m +65 h +73 h +6940 m +1 h +4 h +1 h +4 h +4 h +1105 h +4 h +10 h +4 h +10 h +1 h +1 h +4 h +6941 m +1 h +97 h +4 h +4 h +97 h +4 h +97 h +1 h +1 h +1 h +70 m +10 h +10 h +6942 m +2379 m +11 h +6943 m +4 h +6944 m +82 h +6945 m +1 h +10 h +10 h +10 h +4 h +10 h +6946 m +10 h +1 h +6947 m +6948 m +172 h +4 h +1 h +1685 h +6949 m +1 h +146 h +1 h +4 h +4 h +4 h +5757 m +97 h +256 h +74 h +10 h +3 h +10 h +4 h +91 h +1 h +1 h +10 h +6950 m +10 h +1 h +6951 m +104 h +10 h +1 h +6952 m +1 h +170 h +10 h +6953 m +368 h +4 h +82 h +10 h +10 h +6954 m +10 h +10 h +36 h +4 h +83 h +386 h +1 h +6955 m +11 h +4 h +83 h +1 h +124 h +6956 m +6957 m +307 h +125 h +4 h +6448 m +57 h +1 h +1 h +10 h +4 h +92 h +112 h +6958 m +806 m +6959 m +1 h +6960 m +45 h +1 h +25 h +1 h +1 h +6961 m +10 h +57 h +4 h +1 h +6962 m +41 h +3 h +4 h +6963 m +11 h +4 h +12 h +74 h +10 h +10 h +4 h +196 h +146 h +1 h +6964 m +6965 m +1 h +4 h +6966 m +4 h +10 h +10 h +1 h +84 h +22 h +6967 m +4 h +6968 m +6969 m +4 h +1 h +1 h +6970 m +1 h +4 h +10 h +4 h +3025 m +459 m +94 h +82 h +1 h +97 h +10 h +10 h +1 h +79 h +6971 m +1 h +10 h +110 h +174 h +10 h +11 h +135 h +4 h +4 h +195 h +6972 m +1 h +1 h +119 h +6973 m +1 h +6974 m +6975 m +139 h +6976 m +196 h +172 h +10 h +6977 m +6978 m +1 h +1 h +11 h +36 h +1 h +10 h +10 h +4 h +4 h +6979 m +4 h +6980 m +1 h +6981 m +1 h +4 h +64 h +250 h +1 h +4 h +4 h +143 h +276 h +4 h +1122 m +10 h +12 h +31 h +124 h +1 h +6982 m +6983 m +129 h +1 h +3607 m +6984 m +4 h +10 h +1 h +4 h +4 h +6985 m +6986 m +1 h +1220 m +6987 m +4 h +94 h +1 h +74 h +4 h +82 h +6988 m +4 h +185 h +1345 m +1 h +10 h +1 h +1 h +6989 m +4 h +1 h +8 h +25 h +1379 m +1 h +4 h +55 h +1 h +6990 m +8 h +4 h +1 h +4 h +1 h +6991 m +2339 m +65 h +4 h +55 h +6992 m +1 h +6993 m +6994 m +4 h +196 h +1 h +10 h +11 h +10 h +1 h +31 h +4 h +6995 m +97 h +10 h +57 h +1 h +1 h +1 h +6996 m +4 h +4 h +1 h +11 h +6997 m +82 h +147 h +1 h +3 h +1 h +1 h +1 h +4 h +170 h +6998 m +1 h +4 h +59 h +4 h +1 h +10 h +1 h +4 h +123 h +4 h +6999 m +138 h +4 h +1 h +1 h +124 h +4 h +1 h +4 h +4 h +4 h +4 h +4 h +7000 m +4 h +10 h +7001 m +4 h +7002 m +4 h +7003 m +1 h +4 h +1 h +1 h +1 h +7004 m +7005 m +10 h +10 h +4 h +4 h +4 h +7006 m +7007 m +258 h +10 h +241 m +114 h +4 h +10 h +1 h +250 h +4 h +10 h +359 h +11 h +4 h +538 h +59 h +7008 m +2607 m +82 h +1 h +10 h +4 h +7009 m +1 h +7010 m +82 h +1 h +124 h +10 h +1 h +468 m +203 m +4 h +1 h +97 h +7011 m +4 h +22 h +7012 m +4 h +7013 m +1 h +266 h +1 h +7014 m +7015 m +7016 m +4 h +4 h +7017 m +4 h +82 h +10 h +1 h +10 h +3845 m +1 h +7018 m +295 h +1 h +4 h +7019 m +7020 m +10 h +1 h +1 h +94 h +97 h +41 h +83 h +4702 m +109 h +3089 m +10 h +1 h +3 h +7021 m +4 h +7022 m +10 h +1 h +3 h +4 h +332 h +698 m +10 h +4 h +3 h +7023 m +173 h +7024 m +31 h +7025 m +1016 h +109 h +1 h +7026 m +4 h +10 h +332 h +7027 m +10 h +1 h +276 h +7028 m +4 h +1 h +1 h +7029 m +4 h +7030 m +7031 m +10 h +4 h +4 h +25 h +10 h +1 h +757 h +1 h +7032 m +7033 m +258 h +1 h +4 h +4 h +4 h +10 h +1 h +125 h +110 h +7034 m +1 h +169 h +4 h +1 h +4 h +307 h +7035 m +4 h +3 h +319 h +1 h +129 h +56 h +124 h +1 h +10 h +1 h +7036 m +1 h +25 h +22 h +10 h +1 h +129 h +270 h +10 h +1 h +7037 m +1 h +4 h +10 h +1975 m +10 h +1304 m +7038 m +45 h +10 h +10 h +7039 m +4177 m +307 h +4 h +173 h +4 h +1 h +65 h +7040 m +1 h +10 h +7041 m +7042 m +1 h +4 h +7043 m +4 h +4 h +1 h +7044 m +7045 m +4 h +146 h +10 h +7046 m +7047 m +4 h +93 h +104 h +4 h +4 h +7048 m +59 h +10 h +4 h +7049 m +10 h +4 h +1 h +7050 m +1 h +297 h +4 h +10 h +1 h +4 h +10 h +27 h +7051 m +1 h +11 h +7052 m +10 h +386 h +3 h +7053 m +208 m +41 h +7054 m +10 h +2163 m +7055 m +1 h +93 h +11 h +10 h +170 h +4 h +4 h +4 h +11 h +7056 m +10 h +4 h +10 h +10 h +986 h +10 h +276 h +158 h +1 h +4 h +1 h +319 h +1 h +4 h +1 h +7057 m +10 h +986 h +4 h +1 h +1 h +124 h +7058 m +7059 m +4 h +1697 m +7060 m +4 h +7061 m +11 h +4 h +4 h +7062 m +7063 m +4 h +1 h +124 h +7064 m +3 h +1 h +8 h +1 h +10 h +10 h +31 h +4 h +11 h +7065 m +10 h +4 h +7066 m +1619 h +1 h +7067 m +7068 m +1 h +264 m +1 h +4 h +119 h +4 h +10 h +1 h +2788 m +1 h +7069 m +172 h +10 h +278 h +73 h +7070 m +110 h +10 h +4 h +4 h +1 h +1868 m +59 h +4 h +1 h +10 h +1 h +4 h +1 h +195 h +10 h +1 h +7071 m +4 h +386 h +82 h +7072 m +4 h +10 h +10 h +11 h +7073 m +7074 m +10 h +7075 m +11 h +167 h +4 h +278 h +7076 m +7077 m +82 h +3 h +1 h +4 h +1 h +4 h +1 h +1 h +97 h +27 h +10 h +10 h +46 h +7078 m +11 h +4 h +73 h +4 h +59 h +4 h +4 h +10 h +1 h +7079 m +7080 m +4 h +10 h +1993 m +10 h +25 h +7081 m +10 h +1 h +10 h +10 h +1 h +4 h +10 h +1 h +4 h +57 h +25 h +10 h +10 h +1 h +92 h +1 h +4 h +3 h +31 h +1 h +7082 m +1 h +4 h +7083 m +692 h +1 h +25 h +11 h +4 h +7084 m +1 h +10 h +7085 m +10 h +10 h +13 h +1 h +737 h +7086 m +4 h +138 h +7087 m +7088 m +601 h +976 h +2494 m +1 h +4 h +109 h +7089 m +4 h +10 h +4 h +7090 m +4 h +278 h +230 h +3 h +7091 m +4 h +140 h +1 h +4 h +7092 m +1 h +4 h +7093 m +1 h +124 h +7094 m +28 h +10 h +4 h +7095 m +307 h +7096 m +41 h +1 h +11 h +7097 m +2931 m +11 h +8 h +7098 m +73 h +702 m +10 h +124 h +238 h +7099 m +1478 m +7100 m +4 h +1 h +10 h +10 h +190 h +1 h +307 h +1 h +4 h +4 h +276 h +10 h +1 h +1 h +82 h +27 h +1 h +10 h +10 h +7101 m +10 h +109 h +1 h +7102 m +157 h +45 h +3229 m +1 h +4 h +1 h +11 h +4 h +4 h +4 h +7103 m +4 h +118 h +7104 m +10 h +110 h +4 h +73 h +1 h +10 h +4 h +109 h +435 h +1 h +1 h +10 h +7105 m +1016 h +578 m +184 h +7106 m +4 h +1 h +7107 m +7108 m +4 h +4 h +7109 m +13 h +4 h +1 h +31 h +7110 m +4 h +2116 m +10 h +1 h +7111 m +4 h +7112 m +10 h +82 h +2788 m +332 h +1 h +59 h +4 h +59 h +541 m +4 h +56 h +83 h +1 h +74 h +73 h +10 h +7113 m +258 h +10 h +7114 m +1 h +4 h +7115 m +57 h +7116 m +512 m +4 h +125 h +1 h +493 h +4 h +10 h +1 h +4 h +7117 m +1 h +4 h +7118 m +59 h +1 h +56 h +7119 m +4 h +10 h +4 h +276 h +1 h +157 h +10 h +64 h +7120 m +82 h +4 h +124 h +186 h +1 h +4 h +82 h +1 h +4 h +1 h +10 h +1 h +1 h +7121 m +7122 m +7123 m +125 h +4 h +10 h +4 h +10 h +10 h +1 h +10 h +1 h +11 h +36 h +27 h +146 h +10 h +10 h +10 h +1 h +83 h +7124 m +1 h +4 h +11 h +82 h +4 h +4 h +4 h +129 h +4 h +4 h +1 h +479 m +7125 m +1 h +7126 m +7127 m +5813 m +4 h +4 h +147 h +10 h +4 h +124 h +4 h +4 h +4 h +1 h +7128 m +4 h +7129 m +4 h +4 h +40 h +6391 m +7130 m +10 h +1 h +13 h +27 h +7131 m +65 h +10 h +250 h +10 h +1 h +56 h +7132 m +4 h +4 h +7133 m +4 h +7134 m +11 h +7135 m +1016 h +7136 m +367 m +4 h +4 h +7137 m +25 h +7138 m +1 h +4 h +7139 m +83 h +7140 m +1 h +4 h +113 h +4 h +1006 m +10 h +1 h +70 m +7141 m +10 h +4 h +307 h +110 h +11 h +4 h +1 h +41 h +1 h +4 h +1 h +1 h +4 h +33 m +10 h +10 h +4292 m +12 h +11 h +4 h +57 h +4 h +1 h +36 h +4 h +1 h +4 h +4 h +7142 m +1 h +4 h +276 h +10 h +41 h +4 h +986 h +195 h +4 h +10 h +10 h +2769 m +181 h +7143 m +4 h +2540 m +7144 m +4 h +1 h +5673 m +1 h +4 h +73 h +7145 m +7146 m +2788 h +10 h +83 h +10 h +1 h +1 h +3 h +4 h +307 h +4 h +1 h +381 m +4 h +1 h +45 h +10 h +119 h +4 h +4 h +1 h +7147 m +147 h +1 h +4 h +4 h +158 h +57 h +7148 m +119 h +11 h +10 h +7149 m +10 h +7150 m +10 h +4 h +57 h +1 h +4 h +185 h +4 h +1 h +1 h +1 h +4 h +4 h +1 h +4 h +11 h +125 h +1 h +1 h +4 h +4 h +4 h +12 h +79 h +258 h +7151 m +7152 m +7153 m +4 h +10 h +4 h +4 h +83 h +4 h +4 h +10 h +1359 h +10 h +7154 m +1 h +7155 m +4 h +190 h +4 h +383 h +4 h +990 m +4 h +27 h +10 h +730 m +7156 m +1 h +4 h +4 h +31 h +1 h +10 h +1 h +4 h +7157 m +7158 m +92 h +10 h +10 h +59 h +12 h +7159 m +12 h +7160 m +146 h +4 h +4 h +41 h +4 h +25 h +79 h +4 h +25 h +104 h +2379 m +125 h +10 h +7161 m +358 h +164 h +10 h +10 h +25 h +1 h +4 h +1142 m +4 h +4 h +4 h +4 h +1 h +1 h +4 h +7162 m +10 h +12 h +1 h +139 h +7163 m +10 h +7164 m +1 h +41 h +297 h +1 h +1 h +59 h +1 h +31 h +4 h +1322 h +1 h +7165 m +10 h +692 h +147 h +55 h +276 h +7166 m +10 h +10 h +10 h +3 h +1 h +10 h +4 h +4 h +1074 h +10 h +1 h +4 h +7167 m +1 h +4 h +1 h +10 h +1 h +7168 m +4 h +258 h +3558 h +7169 m +7170 m +7171 m +1 h +214 m +2625 m +276 h +7172 m +10 h +1 h +4 h +1 h +7173 m +11 h +4 h +1 h +1697 h +3 h +7174 m +4 h +83 h +1 h +1 h +4 h +10 h +7175 m +11 h +114 h +7176 m +4 h +94 h +4 h +79 h +4 h +11 h +4276 m +13 h +4 h +4 h +123 h +114 h +3396 m +196 h +57 h +125 h +147 h +10 h +7177 m +1 h +1 h +1 h +7178 m +57 h +10 h +1 h +170 h +10 h +10 h +1 h +4 h +7179 m +83 h +258 h +10 h +7180 m +7181 m +59 h +238 h +10 h +1 h +7182 m +79 h +7183 m +7184 m +7185 m +1 h +4 h +4 h +4 h +278 h +447 h +7186 m +7187 m +59 h +167 h +7188 m +10 h +4 h +1 h +1 h +463 m +36 h +7189 m +238 h +41 h +3 h +125 h +219 h +82 h +7190 m +74 h +1 h +1 h +10 h +10 h +7191 m +11 h +4 h +7192 m +10 h +7193 m +4 h +10 h +82 h +1 h +4 h +1 h +2172 m +10 h +119 h +7194 m +7195 m +10 h +1379 m +1 h +10 h +3847 m +4 h +4 h +1 h +1 h +10 h +11 h +4 h +10 h +4 h +124 h +196 h +4 h +4 h +7196 m +7197 m +4 h +10 h +4 h +11 h +1403 h +55 h +4 h +4 h +7198 m +4 h +857 h +10 h +69 h +104 h +104 h +4 h +4 h +7199 m +10 h +11 h +4 h +1 h +4 h +204 h +4 h +7200 m +11 h +10 h +7201 m +4 h +4 h +1 h +4 h +353 m +7202 m +4 h +1 h +4 h +1 h +59 h +4 h +7203 m +195 h +4 h +195 h +7204 m +10 h +4 h +10 h +1074 h +1 h +10 h +976 h +10 h +7205 m +59 h +7206 m +7207 m +1 h +10 h +1 h +1137 h +10 h +763 m +7208 m +27 h +10 h +10 h +11 h +4 h +1 h +7209 m +7210 m +1 h +104 h +124 h +190 h +4 h +7211 m +258 h +1 h +7212 m +83 h +36 h +7213 m +10 h +221 m +10 h +10 h +1 h +1 h +10 h +4 h +7214 m +10 h +7215 m +4 h +569 h +7216 m +125 h +4 h +7217 m +1 h +10 h +7218 m +4 h +25 h +7219 m +7220 m +12 h +7221 m +7222 m +7223 m +3357 m +4 h +11 h +79 h +7224 m +7225 m +82 h +56 h +74 h +4 h +10 h +7226 m +1 h +41 h +10 h +1122 m +1 h +10 h +97 h +31 h +1 h +7227 m +1 h +11 h +186 h +7228 m +4 h +1 h +1 h +4 h +181 h +7229 m +7230 m +13 h +299 h +4 h +10 h +65 h +4 h +113 h +289 h +6747 m +4 h +10 h +7231 m +169 h +238 h +4 h +4 h +92 h +45 h +1 h +4 h +113 h +6197 m +1 h +7232 m +7233 m +10 h +12 h +10 h +692 h +10 h +7234 m +7235 m +258 h +7236 m +7237 m +10 h +31 h +1 h +7238 m +1 h +359 h +10 h +7239 m +169 h +10 h +5923 m +4 h +123 h +97 h +1 h +4 h +4 h +447 h +7240 m +82 h +91 h +65 h +7241 m +4 h +7242 m +4 h +7243 m +1 h +1 h +55 h +2172 m +4858 m +7244 m +82 h +4 h +4 h +7245 m +4 h +7246 m +1 h +10 h +3435 m +7247 m +7248 m +7249 m +114 h +1137 h +4 h +74 h +1 h +79 h +1 h +7250 m +4 h +7251 m +4 h +7252 m +123 h +10 h +1 h +1 h +4 h +4 h +129 h +7253 m +57 h +258 h +10 h +4 h +181 h +10 h +4 h +124 h +4 h +7254 m +4089 m +1 h +56 h +4 h +10 h +258 h +4 h +1 h +11 h +10 h +229 h +195 h +4 h +4 h +4 h +167 h +4 h +185 h +196 h +1 h +7255 m +4 h +1 h +5525 m +359 h +7256 m +7257 m +10 h +4 h +83 h +11 h +238 h +4 h +4 h +7258 m +8 h +4 h +1 h +4 h +59 h +7259 m +5 m +7260 m +4 h +2374 h +4 h +10 h +3 h +1 h +4 h +4 h +1 h +4 h +1 h +1607 m +6731 m +10 h +83 h +7261 m +82 h +167 h +4 h +4 h +110 h +10 h +10 h +7262 m +270 h +7263 m +147 h +10 h +7264 m +7265 m +10 h +4 h +368 h +4 h +1220 m +1 h +4 h +4 h +56 h +82 h +1 h +4 h +83 h +109 h +172 h +7266 m +10 h +1 h +10 h +4 h +4 h +949 m +7267 m +4 h +83 h +1 h +4 h +358 h +10 h +3227 m +10 h +10 h +4 h +4 h +1 h +1 h +1 h +1 h +1 h +1083 m +7268 m +1 h +1 h +443 h +129 h +10 h +74 h +7269 m +3 h +1 h +1 h +1 h +59 h +230 h +7270 m +10 h +7271 m +7272 m +11 h +10 h +7273 m +4 h +74 h +1 h +4 h +4 h +156 h +1 h +164 h +4 h +1 h +888 m +1766 h +4 h +83 h +1 h +124 h +1 h +4 h +10 h +7274 m +11 h +7275 m +4 h +10 h +83 h +146 h +10 h +7276 m +25 h +7277 m +10 h +4 h +74 h +1 h +270 h +1 h +4 h +7278 m +1 h +1 h +146 h +4 h +276 h +4 h +10 h +10 h +135 h +4 h +195 h +158 h +4 h +1619 h +4 h +1 h +4 h +4 h +4 h +10 h +82 h +31 h +10 h +109 h +83 h +169 h +1791 m +5 m +7279 m +4 h +4 h +794 m +7280 m +7281 m +11 h +2607 m +1 h +4 h +7282 m +1646 m +4 h +7283 m +11 h +10 h +195 h +10 h +7284 m +74 h +11 h +4 h +195 h +10 h +1 h +4 h +4 h +10 h +1 h +4 h +4 h +1083 m +4 h +4 h +176 m +64 h +1868 m +7285 m +4 h +4 h +45 h +3 h +1074 h +11 h +147 h +7286 m +4 h +125 h +4 h +1406 h +10 h +1 h +7287 m +7288 m +11 h +4 h +1 h +1 h +7289 m +7290 m +11 h +1 h +4 h +10 h +1 h +10 h +4 h +7291 m +7292 m +1 h +1535 m +10 h +11 h +7293 m +1 h +282 m +4 h +7294 m +307 h +4 h +7295 m +172 h +10 h +7296 m +124 h +10 h +7297 m +4 h +10 h +4 h +57 h +11 h +1 h +4 h +4 h +4 h +10 h +7298 m +4 h +94 h +7299 m +10 h +4 h +7300 m +468 m +7301 m +11 h +4 h +4 h +4 h +4 h +1 h +1 h +25 h +10 h +7302 m +4 h +109 h +1 h +1 h +25 h +1 h +10 h +79 h +10 h +4 h +203 m +4 h +7303 m +4 h +31 h +4 h +7304 m +4 h +4 h +1 h +3 h +83 h +1 h +146 h +7305 m +92 h +7306 m +10 h +4 h +1 h +3 h +7307 m +7308 m +10 h +4 h +7309 m +143 h +4 h +11 h +11 h +4 h +196 h +94 h +156 h +4 h +4 h +7310 m +124 h +1 h +223 m +5557 m +1619 h +4 h +7311 m +4 h +7312 m +1 h +10 h +10 h +327 m +8 h +4 h +110 h +10 h +4 h +5526 m +10 h +1 h +7313 m +1 h +279 h +1564 m +1 h +7314 m +1 h +4 h +7315 m +83 h +4 h +7316 m +4 h +11 h +1 h +7317 m +4 h +230 h +1 h +7318 m +1359 h +4 h +4 h +4 h +10 h +10 h +73 h +1 h +1 h +307 h +1 h +10 h +119 h +10 h +7319 m +1 h +147 h +65 h +1 h +4 h +6095 m +4 h +10 h +4 h +7320 m +59 h +170 h +7321 m +13 h +4 h +7322 m +1 h +7323 m +82 h +4 h +843 m +1 h +258 h +4 h +7324 m +4 h +10 h +7325 m +109 h +10 h +114 h +10 h +135 h +5325 m +4 h +262 h +7326 m +10 h +1 h +113 h +7327 m +114 h +1 h +7328 m +41 h +41 h +124 h +123 h +4 h +83 h +1 h +11 h +1 h +4 h +4 h +55 h +59 h +4 h +12 h +1564 m +4 h +1 h +1 h +143 h +4 h +10 h +1 h +11 h +4033 m +1 h +533 m +7329 m +1 h +7330 m +11 h +10 h +7331 m +109 h +118 h +7332 m +7333 m +1089 h +11 h +10 h +10 h +4 h +7334 m +7335 m +4 h +313 m +4 h +1 h +7336 m +358 h +1 h +5544 m +3324 m +74 h +4 h +1532 m +737 h +1 h +13 h +7337 m +7338 m +10 h +181 h +258 h +3 h +109 h +45 h +4 h +10 h +10 h +4 h +1 h +7339 m +124 h +7340 m +1 h +4 h +7341 m +125 h +1 h +4 h +4 h +7342 m +4 h +55 h +10 h +1 h +7343 m +195 h +138 h +74 h +1 h +83 h +3293 m +7344 m +7345 m +7346 m +10 h +1 h +7347 m +430 m +74 h +10 h +109 h +7348 m +10 h +11 h +1260 m +110 h +10 h +1 h +4 h +57 h +7349 m +258 h +7350 m +104 h +4 h +7351 m +7352 m +41 h +4 h +4 h +73 h +299 h +93 h +4 h +278 h +1 h +41 h +8 h +4 h +4 h +1 h +7353 m +83 h +7354 m +7355 m +4 h +250 h +10 h +5 h +1817 m +104 h +10 h +4 h +11 h +7356 m +7357 m +13 h +1 h +82 h +94 h +4 h +1 h +1 h +59 h +10 h +10 h +1 h +10 h +266 h +7358 m +1 h +4 h +11 h +4 h +1 h +110 h +4 h +10 h +10 h +1 h +1 h +4 h +4 h +10 h +1 h +7359 m +276 h +7360 m +10 h +10 h +3669 m +82 h +4 h +7361 m +167 h +386 h +4608 m +1 h +297 h +7362 m +4 h +190 h +114 h +4 h +1 h +4 h +770 m +10 h +4 h +13 h +278 h +4 h +10 h +7363 m +1 h +10 h +800 m +4 h +4 h +7364 m +10 h +1 h +135 h +4 h +57 h +83 h +139 h +1 h +82 h +10 h +4 h +11 h +195 h +10 h +1470 h +4 h +1 h +4 h +10 h +10 h +10 h +73 h +97 h +4 h +976 h +4 h +1835 m +83 h +11 h +10 h +4 h +4 h +7365 m +7366 m +164 h +986 h +10 h +31 h +11 h +1 h +10 h +7367 m +330 h +4 h +1 h +7368 m +82 h +10 h +125 h +986 h +7369 m +28 h +10 h +1 h +1016 h +4 h +229 h +2883 m +270 h +167 h +10 h +1 h +10 h +7370 m +4 h +986 h +7371 m +1 h +4 h +10 h +1 h +25 h +266 h +7372 m +74 h +3150 m +10 h +106 m +1 h +4 h +1 h +7373 m +11 h +7374 m +7375 m +204 h +4 h +4 h +7376 m +7377 m +1092 m +41 h +4 h +109 h +186 h +4 h +1 h +57 h +1 h +1 h +2909 m +297 h +1 h +4 h +7378 m +1016 h +4 h +4 h +7379 m +93 h +138 h +4 h +164 h +25 h +4 h +7380 m +10 h +4 h +4 h +4 h +4 h +7381 m +125 h +10 h +4 h +7382 m +1 h +7383 m +4 h +258 h +181 h +196 h +10 h +1 h +4 h +736 m +4 h +59 h +7384 m +10 h +1 h +4 h +4 h +170 h +7385 m +7386 m +1 h +4 h +1 h +1 h +7387 m +82 h +4 h +7388 m +4 h +4 h +7389 m +57 h +4 h +4 h +7214 m +10 h +56 h +7390 m +7391 m +10 h +31 h +1 h +4 h +1 h +4 h +332 h +4 h +10 h +4 h +4 h +10 h +4 h +4 h +40 h +7392 m +7393 m +1308 m +4 h +10 h +1 h +7394 m +10 h +45 h +4 h +1 h +4 h +104 h +7395 m +45 h +8 h +7396 m +104 h +57 h +4 h +1 h +10 h +10 h +146 h +1 h +4 h +478 m +7397 m +7398 m +7399 m +4 h +10 h +4 h +4 h +7400 m +1 h +4 h +10 h +4 h +10 h +4 h +4 h +36 h +10 h +692 h +135 h +10 h +7401 m +5225 m +4 h +2607 m +1 h +74 h +10 h +4 h +25 h +1 h +4 h +4 h +4 h +4 h +7402 m +1 h +10 h +1016 h +4 h +10 h +4 h +4 h +7403 m +11 h +146 h +7404 m +4 h +31 h +11 h +1 h +307 h +7405 m +11 h +184 h +7406 m +359 h +25 h +4 h +7407 m +11 h +13 h +10 h +10 h +4 h +7408 m +4 h +1620 m +114 h +4 h +1 h +1 h +1 h +7409 m +1 h +36 h +4564 m +36 h +4 h +11 h +4 h +124 h +7410 m +4905 m +7411 m +3 h +1 h +353 m +1772 h +10 h +4 h +157 h +10 h +4 h +278 h +1035 m +7412 m +10 h +82 h +4 h +4 h +1539 m +1 h +109 h +84 h +109 h +4 h +4 h +7413 m +125 h +139 h +109 h +1 h +56 h +5145 m +4 h +1 h +119 h +7414 m +1 h +1 h +4 h +4 h +7415 m +935 h +4 h +4 h +119 h +276 h +169 h +4 h +1 h +123 h +1 h +1 h +7416 m +7417 m +4 h +4 h +4 h +224 h +7418 m +7419 m +7420 m +7421 m +262 h +4 h +1780 h +1 h +10 h +108 h +83 h +1 h +56 h +1 h +1403 h +1 h +185 h +11 h +4 h +4 h +10 h +4 h +186 h +3025 m +7422 m +4 h +7423 m +4 h +4 h +48 h +4 h +2720 m +687 h +5254 m +5653 m +7424 m +4 h +433 m +1 h +65 h +359 h +1137 h +4 h +7425 m +4 h +87 m +4 h +1 h +169 h +11 h +4 h +4 h +4 h +4 h +4 h +7426 m +7427 m +4 h +10 h +10 h +4 h +4 h +1 h +65 h +4 h +295 h +276 h +7428 m +4 h +1 h +2961 m +1 h +59 h +7429 m +10 h +64 h +4 h +4 h +10 h +4 h +1 h +1 h +7430 m +12 h +48 h +4 h +1 h +109 h +1 h +1309 m +4 h +11 h +4 h +156 h +10 h +4 h +4 h +4 h +1 h +10 h +1 h +7431 m +488 h +59 h +190 h +1006 m +57 h +10 h +7432 m +4 h +1 h +7433 m +2002 m +59 h +7434 m +5 h +4 h +45 h +7435 m +447 h +1 h +10 h +7436 m +7437 m +4 h +7438 m +4 h +10 h +1 h +7439 m +12 h +938 m +4 h +10 h +4 h +4 h +7440 m +7441 m +4 h +7442 m +12 h +4 h +1 h +4 h +10 h +278 h +1 h +1 h +1 h +4 h +4 h +57 h +1 h +7443 m +4 h +266 h +4 h +1 h +4 h +7444 m +10 h +10 h +4 h +138 h +4 h +1 h +41 h +4 h +77 h +41 h +59 h +10 h +10 h +4 h +1 h +3561 m +75 m +124 h +7445 m +676 m +7446 m +1016 h +1 h +181 h +139 h +1 h +464 h +97 h +10 h +7447 m +1 h +186 h +7448 m +10 h +10 h +1 h +1 h +10 h +7449 m +332 h +48 h +11 h +172 h +10 h +1 h +4 h +4 h +1 h +170 h +59 h +158 h +7450 m +4 h +7451 m +10 h +274 h +7452 m +4 h +7453 m +1 h +7454 m +56 h +1 h +10 h +106 m +4 h +25 h +4 h +295 h +4 h +2475 m +7455 m +7456 m +4 h +1 h +31 h +11 h +7457 m +10 h +4 h +295 h +4 h +7458 m +11 h +1 h +79 h +181 h +4 h +7459 m +4 h +83 h +2623 m +57 h +538 h +4 h +4 h +109 h +97 h +214 m +4 h +5869 m +5917 m +12 h +1374 m +59 h +135 h +4 h +7460 m +4 h +109 h +4 h +147 h +7461 m +7462 m +10 h +4 h +10 h +4 h +10 h +25 h +25 h +7463 m +114 h +1 h +10 h +1 h +195 h +4 h +56 h +83 h +1 h +10 h +4 h +91 h +74 h +1 h +10 h +276 h +4 h +1 h +41 h +7464 m +70 m +4 h +31 h +7465 m +1 h +1 h +73 h +10 h +4 h +7466 m +1 h +4 h +454 m +3 h +69 h +4 h +13 h +40 h +1 h +7467 m +109 h +10 h +10 h +7468 m +10 h +1 h +1 h +7469 m +41 h +4 h +28 h +10 h +7470 m +55 h +1 h +10 h +4 h +4 h +11 h +4 h +4 h +10 h +3477 m +195 h +7471 m +10 h +7472 m +7473 m +1 h +7474 m +46 h +4 h +1697 h +1 h +94 h +4 h +1 h +7475 m +1 h +7476 m +114 h +7477 m +7478 m +1 h +73 h +109 h +7479 m +11 h +45 h +4 h +4 h +7480 m +4 h +1 h +4 h +4 h +59 h +10 h +104 h +7481 m +11 h +4 h +270 h +7482 m +79 h +1 h +74 h +1 h +1 h +7483 m +1504 m +1 h +147 h +146 h +124 h +7484 m +4 h +97 h +4 h +3 h +4 h +135 h +10 h +4 h +1 h +10 h +7485 m +4 h +10 h +4 h +1 h +1 h +4 h +4 h +11 h +7486 m +7487 m +7488 m +4 h +4 h +41 h +481 m +4 h +1 h +1 h +8 h +4 h +10 h +7489 m +109 h +4 h +1 h +1 h +478 m +10 h +1 h +1 h +4 h +4 h +1 h +536 h +10 h +266 h +857 h +10 h +4 h +7490 m +4 h +1 h +7491 m +190 h +10 h +359 h +10 h +82 h +4 h +2300 m +7492 m +7493 m +7494 m +7495 m +7496 m +4 h +10 h +10 h +3 h +1 h +140 h +1 h +64 h +304 m +41 h +3 h +97 h +1 h +7497 m +10 h +10 h +1 h +46 h +7498 m +146 h +82 h +4 h +4 h +1 h +4 h +7499 m +10 h +1847 m +10 h +7500 m +386 h +4 h +1 h +59 h +4 h +31 h +11 h +146 h +1 h +4 h +3 h +1 h +146 h +4 h +4 h +36 h +1 h +94 h +1 h +10 h +7501 m +7502 m +114 h +2840 m +4 h +1 h +13 h +4 h +31 h +7503 m +10 h +10 h +7504 m +1 h +7505 m +2002 m +4 h +11 h +1 h +258 h +5505 m +7506 m +4 h +25 h +114 h +4 h +28 h +4 h +238 h +307 h +2666 m +10 h +10 h +4 h +1 h +7507 m +4 h +1 h +10 h +4 h +1 h +155 m +4 h +4 h +7508 m +10 h +11 h +4 h +264 m +4 h +7509 m +4 h +1 h +10 h +4 h +332 h +7510 m +4 h +1116 m +7511 m +4 h +4 h +7512 m +1 h +10 h +4 h +241 m +1 h +1 h +123 h +172 h +4390 m +4 h +4 h +4 h +4 h +4 h +3 h +4 h +10 h +1 h +7513 m +11 h +56 h +129 h +7514 m +55 h +41 h +59 h +7515 m +4 h +11 h +11 h +109 h +10 h +4 h +108 h +7516 m +124 h +10 h +10 h +146 h +7517 m +278 h +7518 m +31 h +83 h +1 h +7519 m +83 h +7520 m +4 h +1 h +59 h +109 h +10 h +1 h +443 h +172 h +11 h +10 h +1 h +10 h +7521 m +82 h +7522 m +4723 m +7523 m +1 h +1 h +7524 m +266 h +7525 m +4 h +1 h +4 h +79 h +104 h +1 h +297 h +56 h +12 h +1 h +4 h +4 h +347 m +1 h +10 h +7526 m +1 h +7527 m +7528 m +10 h +10 h +4 h +1 h +313 m +3 h +4 h +4 h +10 h +1 h +7529 m +1 h +295 h +10 h +4 h +1 h +7530 m +184 h +65 h +124 h +4 h +10 h +4 h +7531 m +1 h +185 h +7532 m +1249 m +10 h +173 h +4 h +7533 m +7534 m +55 h +57 h +104 h +25 h +4 h +10 h +7535 m +1 h +1 h +4 h +4 h +135 h +4 h +1772 h +4 h +4 h +1 h +7536 m +4 h +4 h +11 h +6461 m +65 h +4 h +7537 m +4 h +125 h +4 h +10 h +297 h +7538 m +4 h +123 h +1 h +83 h +10 h +536 h +3028 m +5048 m +7539 m +1 h +10 h +10 h +125 h +7540 m +7541 m +1 h +10 h +7542 m +10 h +195 h +3 h +386 h +1 h +278 h +10 h +1 h +4 h +7543 m +4 h +4 h +1 h +4 h +1137 h +10 h +1 h +10 h +1835 m +935 h +4 h +82 h +7544 m +4489 m +4 h +4 h +4 h +11 h +1619 h +4 h +265 h +4 h +7545 m +10 h +7546 m +10 h +109 h +7547 m +4 h +10 h +135 h +4 h +1 h +2054 m +10 h +13 h +4 h +289 h +7548 m +1 h +4 h +2931 m +7549 m +112 h +4 h +1 h +7550 m +3112 m +1 h +10 h +10 h +7551 m +195 h +10 h +4 h +41 h +64 h +1 h +1 h +4 h +25 h +4 h +1 h +13 h +4 h +1 h +1 h +7552 m +4 h +1454 m +4 h +4 h +779 m +5897 m +46 h +7553 m +4 h +4 h +1 h +1 h +4 h +4 h +4 h +266 h +10 h +4 h +885 m +1 h +7554 m +266 h +10 h +10 h +4 h +1 h +7555 m +8 h +195 h +146 h +7556 m +4 h +258 h +4 h +64 h +7557 m +4 h +7558 m +332 h +4 h +1 h +7559 m +1 h +4 h +1 h +262 h +7560 m +1685 h +10 h +1697 h +7561 m +4 h +7562 m +7563 m +45 h +25 h +4 h +4 h +55 h +1 h +7564 m +1 h +1 h +104 h +7565 m +10 h +74 h +1 h +447 h +7566 m +1 h +10 h +1 h +7567 m +7568 m +56 h +1 h +10 h +4 h +4 h +7569 m +6491 m +4 h +57 h +10 h +7570 m +1 h +7571 m +11 h +10 h +1 h +10 h +7572 m +7573 m +1 h +1 h +7574 m +656 h +10 h +10 h +4 h +1 h +7575 m +4 h +7576 m +10 h +10 h +7577 m +65 h +114 h +56 h +7578 m +4 h +7579 m +1 h +25 h +4 h +4 h +4 h +25 h +7580 m +7581 m +7582 m +164 h +1 h +7583 m +1389 h +4 h +82 h +10 h +1 h +10 h +7584 m +4 h +7585 m +4 h +11 h +4 h +94 h +1 h +10 h +4 h +7586 m +1 h +7587 m +109 h +4 h +986 h +4 h +4 h +1 h +7588 m +1 h +74 h +954 m +1 h +7589 m +1 h +55 h +1772 h +97 h +4 h +10 h +4 h +7590 m +11 h +146 h +10 h +4 h +1403 h +124 h +11 h +1 h +692 h +10 h +7591 m +3 h +1 h +4858 m +7592 m +10 h +1 h +64 h +1 h +4 h +1 h +11 h +104 h +4 h +1 h +4 h +10 h +250 h +10 h +4 h +4 h +22 h +642 m +6381 m +10 h +7593 m +4 h +4 h +4 h +386 h +4 h +11 h +4 h +4 h +196 h +7594 m +41 h +486 m +7595 m +4 h +7596 m +7597 m +69 h +241 m +119 h +7598 m +7599 m +2002 h +12 h +7600 m +31 h +4 h +1 h +4 h +4 h +41 h +1 h +125 h +157 h +7601 m +7602 m +250 h +7603 m +7604 m +4 h +10 h +1822 h +4 h +7605 m +92 h +109 h +7606 m +464 h +3398 m +1 h +4 h +10 h +146 h +1 h +2962 m +123 h +444 m +7607 m +1886 m +4 h +1 h +1 h +1508 m +4 h +2733 h +10 h +10 h +1 h +11 h +1089 h +10 h +1771 m +7608 m +41 h +4 h +1790 h +1 h +41 h +295 h +7609 m +64 h +4 h +1 h +7610 m +4 h +7611 m +7612 m +184 h +10 h +4 h +7613 m +1 h +4 h +7614 m +4 h +185 h +857 h +4 h +7615 m +11 h +5783 m +10 h +1 h +10 h +4 h +124 h +2072 m +7616 m +1 h +4 h +109 h +7617 m +10 h +97 h +10 h +1 h +138 h +1 h +10 h +7618 m +10 h +4 h +74 h +10 h +1 h +1 h +25 h +4 h +563 m +1 h +10 h +13 h +4 h +7619 m +4 h +4 h +7620 m +83 h +1 h +4 h +4 h +4 h +757 h +10 h +4 h +1 h +10 h +1 h +4 h +181 h +278 h +4 h +4 h +7621 m +3 h +4 h +41 h +10 h +383 h +4 h +4 h +7622 m +4 h +45 h +7623 m +4 h +733 m +1 h +1790 h +4 h +7624 m +258 h +4 h +7625 m +1 h +4 h +4 h +1 h +1 h +10 h +7626 m +7627 m +4 h +7628 m +229 h +146 h +4 h +7629 m +1 h +10 h +4 h +1 h +7630 m +1 h +10 h +4 h +10 h +4 h +10 h +10 h +1 h +11 h +83 h +1 h +276 h +12 h +7631 m +36 h +10 h +7632 m +10 h +190 h +3 h +1822 h +7633 m +1 h +1 h +1 h +7634 m +7635 m +4 h +7636 m +1 h +4 h +114 h +4 h +4 h +10 h +1 h +464 h +4 h +143 h +1 h +7637 m +4 h +11 h +4 h +13 h +4 h +4 h +1 h +10 h +649 h +1 h +7638 m +4 h +7639 m +7640 m +7641 m +10 h +7642 m +7643 m +4 h +4 h +4 h +1 h +1 h +4 h +10 h +448 m +94 h +4 h +1 h +10 h +7644 m +1 h +5 h +2928 m +82 h +5822 m +1 h +258 h +4 h +1 h +4 h +94 h +12 h +7645 m +4 h +7646 m +1445 m +4 h +3025 m +5944 h +4 h +1 h +4 h +590 m +4 h +7647 m +25 h +113 h +1 h +4 h +1 h +1 h +7648 m +1 h +79 h +10 h +10 h +1 h +3 h +1 h +7649 m +7650 m +4 h +1 h +1 h +1 h +11 h +1 h +1 h +1 h +278 h +3 h +97 h +1 h +1 h +83 h +75 m +59 h +7651 m +57 h +1 h +82 h +7652 m +4 h +11 h +7653 m +4 h +7654 m +10 h +4 h +7655 m +4 h +1 h +109 h +10 h +7656 m +1 h +59 h +4 h +4 h +4 h +10 h +8 h +146 h +10 h +11 h +4 h +620 m +17 m +7657 m +7658 m +7659 m +4 h +4 h +10 h +73 h +10 h +1 h +1 h +4 h +7660 m +13 h +4 h +7661 m +1 h +1 h +10 h +4 h +4 h +7662 m +4 h +4 h +4 h +97 h +7663 m +7447 m +4 h +4 h +7664 m +1 h +4 h +10 h +13 h +77 h +65 h +7665 m +307 h +4 h +7666 m +4 h +7253 m +104 h +1 h +4 h +55 h +4 h +157 h +184 h +7667 m +10 h +10 h +59 h +297 h +10 h +36 h +59 h +4 h +7668 m +55 h +814 m +82 h +7669 m +3396 m +7670 m +468 m +10 h +1 h +10 h +82 h +4 h +1 h +10 h +10 h +1 h +1359 h +7671 m +7672 m +4 h +1 h +7673 m +7674 m +11 h +7675 m +7676 m +7677 m +4 h +10 h +31 h +1 h +1 h +97 h +1 h +10 h +97 h +520 m +64 h +4 h +110 h +4 h +4 h +4 h +1 h +83 h +92 h +1 h +109 h +1 h +1 h +7678 m +4 h +169 h +10 h +11 h +4 h +4 h +7679 m +7680 m +4 h +10 h +2522 m +84 h +10 h +4 h +581 m +7681 m +4 h +1 h +94 h +4 h +83 h +97 h +48 h +92 h +7682 m +4 h +10 h +4 h +7683 m +64 h +4 h +11 h +4 h +10 h +1 h +4 h +4 h +258 h +10 h +36 h +48 h +22 h +1 h +1 h +12 h +10 h +1 h +65 h +10 h +7684 m +4 h +12 h +1 h +65 h +7685 m +7686 m +11 h +1 h +1 h +10 h +10 h +109 h +10 h +7687 m +4 h +1 h +181 h +3 h +4 h +7688 m +10 h +55 h +7689 m +4 h +10 h +4 h +7690 m +1 h +83 h +4 h +147 h +74 h +4 h +196 h +4 h +986 h +7691 m +10 h +4 h +7692 m +7693 m +167 h +10 h +1 h +3 h +1 h +2887 h +994 m +256 h +10 h +4 h +97 h +10 h +7694 m +10 h +7695 m +156 h +10 h +4 h +195 h +144 h +10 h +11 h +4 h +640 h +7696 m +4 h +173 h +4 h +1 h +208 m +7697 m +82 h +10 h +1722 m +10 h +59 h +1 h +12 h +12 h +4 h +4 h +10 h +169 h +36 h +443 h +124 h +97 h +10 h +4 h +7698 m +7699 m +83 h +7700 m +41 h +1 h +5379 m +4 h +7701 m +1 h +1 h +7702 m +4 h +1 h +25 h +1 h +258 h +7703 m +1 h +1 h +11 h +3979 m +1 h +238 h +7704 m +1083 h +7705 m +11 h +74 h +173 h +488 h +7706 m +1 h +1 h +4 h +83 h +1 h +1 h +908 m +10 h +45 h +1 h +4 h +1027 h +4 h +10 h +1 h +4 h +4 h +447 h +195 h +146 h +10 h +4 h +1 h +4 h +7707 m +2172 h +124 h +4 h +138 h +4 h +1 h +74 h +7708 m +2733 h +3 h +1 h +4 h +10 h +7709 m +7710 m +64 h +1 h +7711 m +7712 m +10 h +7713 m +10 h +4 h +7714 m +10 h +7715 m +7716 m +1445 m +57 h +4 h +7717 m +10 h +17 m +7718 m +1 h +91 h +4 h +108 h +7719 m +10 h +1 h +10 h +1 h +4 h +295 h +104 h +10 h +4 h +2111 m +4 h +1 h +4 h +4 h +11 h +1 h +7720 m +110 h +4 h +110 h +1 h +4 h +272 h +1 h +1 h +119 h +4 h +1 h +1 h +4 h +104 h +1137 h +7721 m +10 h +10 h +4 h +7722 m +10 h +92 h +10 h +138 h +4 h +10 h +10 h +1 h +986 h +4 h +3 h +10 h +12 h +278 h +4 h +590 m +1 h +11 h +7723 m +7724 m +1 h +1 h +4 h +1 h +48 h +7725 m +1 h +1 h +278 h +10 h +7726 m +7727 m +477 m +10 h +7728 m +7729 m +1 h +1 h +10 h +10 h +104 h +13 h +4 h +4 h +4 h +1 h +4 h +1 h +1 h +4 h +4 h +4 h +10 h +59 h +4 h +10 h +4 h +10 h +7730 m +976 h +1 h +7731 m +4 h +1 h +56 h +181 h +4 h +7732 m +1 h +7733 m +4 h +10 h +4 h +10 h +10 h +10 h +10 h +7734 m +146 h +1 h +147 h +7735 m +74 h +7736 m +4 h +1 h +10 h +7737 m +31 h +10 h +7738 m +433 m +7739 m +4 h +4 h +82 h +1 h +7740 m +1 h +7741 m +4 h +4 h +10 h +4 h +57 h +4 h +31 h +556 h +7742 m +4 h +1 h +56 h +7743 m +4 h +7744 m +4 h +4 h +7745 m +10 h +11 h +4 h +4 h +1 h +7746 m +1 h +4 h +3702 m +11 h +124 h +1122 m +4 h +1 h +169 h +7747 m +368 h +1 h +11 h +1 h +1 h +113 h +7661 m +1 h +146 h +4 h +1666 m +1 h +65 h +1 h +285 m +1 h +4 h +7748 m +7749 m +4 h +1 h +140 h +69 h +1880 m +4 h +7750 m +1 h +1 h +1 h +82 h +1 h +4 h +10 h +114 h +2374 h +10 h +538 h +4 h +55 h +109 h +7751 m +2314 m +1 h +266 h +1 h +92 h +83 h +3 h +737 h +5 h +11 h +124 h +7752 m +7753 m +520 m +41 h +41 h +4 h +4 h +4 h +4 h +27 h +4 h +4 h +4 h +4 h +110 h +784 h +1 h +7754 m +1 h +59 h +7755 m +1 h +7756 m +97 h +97 h +1822 h +31 h +7757 m +7758 m +1 h +4 h +10 h +4 h +7759 m +1 h +125 h +7760 m +1650 h +7761 m +4 h +92 h +1 h +5093 m +11 h +157 h +11 h +1 h +11 h +1 h +4 h +190 h +4 h +716 m +278 h +1835 m +4 h +7762 m +1 h +1 h +1 h +74 h +1137 h +1 h +4 h +7763 m +10 h +1 h +1 h +11 h +4 h +7764 m +520 h +4 h +10 h +11 h +10 h +383 h +1 h +7765 m +31 h +4 h +10 h +7766 m +7767 m +119 h +7768 m +4 h +10 h +7769 m +1470 h +986 h +56 h +593 m +10 h +7770 m +10 h +10 h +6370 m +82 h +7771 m +4 h +4 h +82 h +185 h +4 h +1 h +3 h +10 h +7772 m +93 h +7773 m +1 h +125 h +10 h +7774 m +59 h +55 h +7775 m +1 h +7776 m +270 h +94 h +2079 m +92 h +7777 m +258 h +4 h +208 m +1 h +4 h +10 h +1 h +1260 m +40 h +4 h +4 h +4 h +146 h +4 h +7778 m +10 h +25 h +74 h +10 h +4 h +11 h +4 h +4 h +4 h +83 h +94 h +124 h +276 h +1595 m +7779 m +4 h +10 h +7780 m +10 h +4 h +1 h +7781 m +4 h +4 h +10 h +10 h +7782 m +7783 m +10 h +114 h +7784 m +447 h +4 h +10 h +3 h +1 h +7785 m +57 h +4 h +1780 h +1 h +7786 m +10 h +157 h +181 h +4 h +10 h +4 h +4 h +7787 m +1 h +4 h +4 h +73 h +57 h +10 h +1 h +7788 m +181 h +1 h +41 h +1650 h +4 h +2788 h +112 h +1 h +4 h +11 h +10 h +4 h +7789 m +4 h +4 h +139 h +10 h +25 h +10 h +5 h +4 h +1 h +69 h +7790 m +7791 m +1185 m +7792 m +1445 h +1123 m +7793 m +1 h +124 h +1 h +74 h +4 h +7794 m +7795 m +7796 m +8 h +11 h +4 h +172 h +10 h +1 h +4 h +10 h +65 h +41 h +7797 m +4 h +10 h +4 h +8 h +692 h +83 h +10 h +204 h +4 h +1198 m +7798 m +1 h +1 h +4 h +7799 m +10 h +139 h +10 h +11 h +4 h +4 h +6726 m +41 h +114 h +7800 m +11 h +92 h +7801 m +143 h +10 h +368 h +1 h +124 h +1 h +1 h +4975 m +7802 m +601 h +7803 m +7804 m +1016 h +7805 m +278 h +1 h +7806 m +12 h +7807 m +1 h +7808 m +10 h +1105 h +7809 m +174 h +4 h +1 h +4 h +1 h +3 h +5 h +109 h +4 h +124 h +4 h +228 m +7810 m +104 h +1 h +10 h +1 h +41 h +265 h +10 h +74 h +7811 m +520 h +10 h +2625 m +10 h +4 h +10 h +7812 m +1 h +7813 m +73 h +1 h +10 h +4 h +1105 h +31 h +7814 m +1 h +4 h +3 h +7815 m +4 h +7816 m +7817 m +59 h +10 h +4 h +4 h +7818 m +6221 m +4 h +167 h +443 h +7819 m +4 h +1 h +27 h +7820 m +104 h +1 h +4 h +4 h +1 h +1470 h +1 h +92 h +83 h +10 h +1 h +4 h +79 h +7821 m +1 h +1 h +4 h +7822 m +1 h +7823 m +4 h +7824 m +262 h +65 h +7825 m +1 h +125 h +4 h +11 h +4 h +2625 m +73 h +10 h +4 h +22 h +7826 m +4 h +4 h +7827 m +2851 m +4 h +1 h +1 h +83 h +195 h +59 h +4 h +57 h +4 h +1 h +1646 m +1 h +12 h +4 h +10 h +1619 h +10 h +4 h +7828 m +289 h +7829 m +278 h +10 h +4 h +11 h +4 h +7830 m +7831 m +4 h +7832 m +41 h +4 h +536 h +10 h +1 h +7833 m +1 h +10 h +4 h +11 h +97 h +10 h +692 h +7834 m +4 h +1 h +83 h +4 h +97 h +92 h +297 h +4 h +1016 h +4 h +4 h +1 h +10 h +4 h +7835 m +4 h +104 h +10 h +7836 m +25 h +7837 m +7838 m +4 h +447 h +10 h +10 h +59 h +1 h +10 h +10 h +7839 m +7840 m +7841 m +4 h +10 h +7842 m +4 h +4 h +56 h +11 h +10 h +97 h +10 h +11 h +169 h +7843 m +1 h +10 h +41 h +1083 h +1 h +10 h +1089 h +25 h +11 h +7844 m +4 h +10 h +1169 m +4 h +7845 m +10 h +7846 m +41 h +22 h +4 h +7847 m +36 h +158 h +7848 m +7849 m +4 h +109 h +7850 m +1 h +185 h +399 h +7851 m +246 m +82 h +104 h +7852 m +4 h +79 h +219 h +1 h +123 h +1 h +7853 m +4 h +25 h +307 h +7854 m +7855 m +170 h +1 h +4 h +1 h +172 h +10 h +6144 m +109 h +3 h +1016 h +7856 m +7857 m +4 h +7858 m +258 h +4 h +196 h +1751 m +1260 m +4 h +7859 m +7860 m +83 h +7861 m +307 h +1 h +1 h +1 h +4 h +276 h +4 h +10 h +7862 m +1 h +119 h +1 h +4 h +3307 m +181 h +4535 m +10 h +1 h +4 h +4 h +7863 m +1 h +2719 h +297 h +10 h +167 h +10 h +124 h +4 h +264 m +358 h +10 h +83 h +55 h +1 h +13 h +1 h +4 h +4 h +124 h +4 h +358 h +4 h +7864 m +10 h +1 h +10 h +4 h +1 h +7865 m +4 h +1 h +97 h +124 h +195 h +10 h +4 h +7866 m +73 h +124 h +250 h +371 h +59 h +1796 m +73 h +4 h +1 h +7867 m +4 h +4 h +125 h +630 m +2591 m +1 h +7868 m +7869 m +4 h +2623 m +1 h +112 h +7870 m +4 h +4 h +22 h +1 h +4 h +4 h +57 h +463 m +1 h +7871 m +3 h +4 h +10 h +1 h +1250 h +1 h +1137 h +7872 m +447 h +10 h +10 h +7873 m +7874 m +1 h +1 h +138 h +7875 m +10 h +1089 h +10 h +185 h +4 h +10 h +4 h +7876 m +538 h +4 h +31 h +4 h +2172 h +10 h +1 h +10 h +7126 m +443 h +7877 m +167 h +4 h +4 h +1 h +7878 m +57 h +4 h +4 h +1362 h +1 h +1 h +4 h +4 h +11 h +1 h +10 h +7879 m +31 h +4 h +1020 m +4 h +4 h +124 h +3 h +124 h +10 h +1 h +4 h +8 h +4 h +7880 m +4 h +158 h +4 h +4 h +1 h +114 h +278 h +83 h +5933 m +10 h +181 h +4 h +7881 m +73 h +56 h +1 h +3680 m +1309 m +10 h +112 h +3299 m +172 h +630 m +10 h +92 h +10 h +1 h +74 h +10 h +1 h +109 h +57 h +167 h +4 h +1 h +10 h +129 h +7882 m +10 h +181 h +7883 m +10 h +1 h +1 h +4 h +7884 m +27 h +7885 m +11 h +4 h +110 h +1 h +8 h +7886 m +10 h +196 h +10 h +7887 m +4 h +10 h +10 h +1 h +45 h +4 h +7888 m +7889 m +258 h +10 h +2379 m +7890 m +13 h +10 h +4 h +278 h +8 h +10 h +3479 m +371 h +1 h +4 h +1 h +1 h +59 h +4 h +2447 m +10 h +1 h +7891 m +4 h +164 h +7892 m +1 h +10 h +7893 m +4 h +295 h +10 h +1 h +1 h +4 h +7894 m +4 h +4 h +4 h +125 h +10 h +1 h +1 h +10 h +10 h +358 h +4 h +31 h +36 h +196 h +4 h +4 h +7895 m +4 h +7896 m +1 h +139 h +83 h +10 h +4 h +7897 m +7898 m +1 h +10 h +123 h +7899 m +258 h +11 h +4 h +25 h +1030 h +31 h +10 h +1 h +4 h +1 h +97 h +4 h +123 h +172 h +1 h +4 h +4 h +7900 m +27 h +4 h +1 h +4 h +7901 m +124 h +1 h +229 h +11 h +139 h +10 h +7902 m +7903 m +4 h +4 h +4 h +41 h +7904 m +7905 m +10 h +4 h +4 h +146 h +10 h +10 h +4 h +4 h +7906 m +1 h +4240 m +4350 m +10 h +10 h +7907 m +7908 m +1508 m +1 h +4 h +1 h +1 h +10 h +3 h +7909 m +25 h +97 h +10 h +1 h +4 h +45 h +7910 m +82 h +4 h +7911 m +4 h +4 h +6954 m +7912 m +196 h +4 h +7913 m +4 h +74 h +4 h +4 h +1 h +4 h +4 h +7914 m +7915 m +156 h +4 h +106 h +10 h +83 h +1 h +7916 m +7917 m +1 h +4 h +4 h +1 h +1 h +250 h +57 h +167 h +82 h +4 h +10 h +10 h +4 h +4 h +4 h +109 h +1 h +1 h +4 h +10 h +4 h +10 h +4292 m +4 h +7918 m +4 h +59 h +1 h +1 h +265 h +4 h +10 h +7919 m +4 h +64 h +7920 m +488 h +10 h +1 h +73 h +7921 m +3 h +4 h +10 h +4 h +388 m +167 h +7922 m +386 h +1 h +3 h +4 h +1 h +4 h +4 h +2887 h +7923 m +25 h +1 h +1 h +4 h +7924 m +7925 m +5141 m +8 h +146 h +7926 m +7927 m +1 h +11 h +4 h +7839 m +41 h +91 h +7928 m +4 h +4 h +1 h +114 h +1 h +7929 m +112 h +40 h +196 h +7930 m +10 h +10 h +10 h +7931 m +146 h +4 h +1 h +3 h +1 h +83 h +4 h +4 h +1337 m +11 h +4 h +7932 m +1 h +4 h +64 h +7933 m +7661 h +124 h +3 h +7934 m +10 h +7935 m +173 h +7936 m +2530 m +10 h +1957 m +10 h +7937 m +1 h +7938 m +190 h +10 h +108 h +4 h +7939 m +1 h +11 h +7940 m +4 h +1 h +1 h +4 h +1249 m +10 h +82 h +146 h +1 h +59 h +1 h +4 h +7941 m +4 h +10 h +4 h +146 h +7942 m +1 h +1 h +4 h +109 h +1 h +2813 m +55 h +368 h +4 h +1 h +4 h +4 h +7943 m +7944 m +1 h +4 h +7945 m +3342 m +83 h +1 h +1 h +5653 m +10 h +307 h +276 h +7946 m +7947 m +7948 m +4 h +45 h +1027 h +4 h +1116 m +7949 m +124 h +1 h +31 h +10 h +4 h +7950 m +10 h +1685 h +124 h +1 h +7951 m +10 h +1 h +7952 m +83 h +135 h +65 h +12 h +135 h +4 h +25 h +3422 m +1 h +124 h +92 h +139 h +82 h +109 h +1 h +1 h +4 h +10 h +124 h +4 h +4 h +4 h +92 h +146 h +10 h +10 h +4 h +4 h +11 h +109 h +10 h +4 h +1 h +10 h +1249 m +1 h +10 h +1 h +7953 m +77 h +7954 m +1403 h +4 h +4 h +7955 m +4 h +965 m +4 h +238 h +1 h +83 h +65 h +556 h +4 h +4 h +403 h +7956 m +4 h +4 h +1 h +4 h +11 h +7957 m +4 h +692 h +1064 m +172 h +1 h +1 h +1 h +319 h +371 h +31 h +1470 h +7958 m +41 h +7959 m +10 h +1 h +11 h +1 h +1 h +3341 m +104 h +82 h +45 h +4 h +4 h +7960 m +123 h +10 h +4 h +1 h +4 h +10 h +4 h +10 h +36 h +82 h +266 h +147 h +10 h +56 h +1 h +4 h +4 h +45 h +7961 m +103 m +1 h +1892 m +73 h +7962 m +4 h +4 h +1 h +583 m +4 h +10 h +1 h +1 h +7963 m +4 h +10 h +4 h +4 h +7964 m +94 h +4 h +2004 m +1 h +1 h +10 h +4 h +56 h +1 h +10 h +4 h +4 h +143 h +169 h +7965 m +976 h +1 h +1 h +82 h +7966 m +1 h +25 h +7967 m +11 h +4 h +1 h +1 h +7968 m +114 h +4 h +119 h +1 h +7969 m +7970 m +1 h +478 h +1 h +4 h +82 h +4 h +10 h +64 h +10 h +4 h +7971 m +7972 m +4 h +4 h +1 h +1261 m +718 h +1 h +1 h +65 h +7973 m +124 h +45 h +4 h +4 h +4 h +41 h +92 h +1 h +7974 m +4 h +4 h +270 h +17 h +1 h +12 h +1 h +10 h +7975 m +25 h +4 h +124 h +1 h +10 h +4 h +1 h +97 h +4 h +1 h +7976 m +10 h +7977 m +1089 h +7978 m +1 h +7979 m +4 h +4 h +4 h +1 h +7980 m +7981 m +7982 m +10 h +1 h +41 h +125 h +1535 m +10 h +601 h +1 h +10 h +1 h +7983 m +7984 m +4 h +73 h +104 h +1619 h +109 h +82 h +4 h +7985 m +11 h +7986 m +1 h +1137 h +1 h +7987 m +4 h +4 h +698 m +918 m +4 h +1 h +1 h +2733 h +383 h +1 h +4 h +10 h +7988 m +4 h +1766 h +45 h +36 h +7989 m +7990 m +144 h +7991 m +4 h +1785 m +4 h +7992 m +56 h +4 h +10 h +7993 m +4 h +4 h +10 h +7994 m +7995 m +56 h +696 m +4 h +4 h +4 h +7996 m +10 h +41 h +1 h +104 h +36 h +1 h +4 h +7997 m +1 h +144 h +1 h +172 h +7998 m +4 h +7999 m +82 h +8000 m +1 h +640 h +8001 m +27 h +8002 m +1 h +4 h +2941 m +8003 m +4 h +1053 m +8004 m +976 h +11 h +82 h +10 h +4 h +55 h +1 h +31 h +4 h +4 h +4 h +82 h +157 h +10 h +10 h +10 h +1 h +1202 m +4 h +8005 m +4 h +4 h +1 h +1 h +4 h +4 h +4 h +10 h +4 h +8006 m +3555 m +4 h +224 h +10 h +4 h +11 h +45 h +1 h +1 h +8007 m +8008 m +59 h +10 h +1 h +4 h +4 h +1 h +8009 m +1122 m +1 h +1 h +412 m +4 h +10 h +1 h +10 h +1 h +10 h +10 h +1 h +10 h +4 h +4 h +97 h +3 h +8010 m +8011 m +10 h +1796 m +10 h +8012 m +57 h +229 h +4 h +1 h +8013 m +8014 m +4 h +10 h +8015 m +3 h +184 h +185 h +4 h +1016 h +976 h +5590 m +190 h +8 h +25 h +1 h +8016 m +10 h +10 h +3 h +8017 m +4 h +4 h +4 h +10 h +8018 m +8 h +8019 m +1 h +737 h +520 h +11 h +4 h +8020 m +185 h +1 h +8021 m +8022 m +1 h +478 h +10 h +8023 m +41 h +57 h +10 h +10 h +4229 m +82 h +10 h +1114 m +4 h +447 h +1 h +11 h +3112 m +1 h +4 h +4 h +10 h +4 h +4 h +10 h +4 h +299 h +1 h +4 h +10 h +1 h +10 h +4 h +297 h +10 h +4 h +1 h +4 h +10 h +10 h +8024 m +8025 m +1 h +4 h +10 h +4 h +8026 m +1201 m +285 m +181 h +1 h +8027 m +11 h +10 h +433 m +8028 m +56 h +4 h +8029 m +4 h +8030 m +104 h +4 h +74 h +1 h +8031 m +185 h +1 h +97 h +278 h +1 h +8032 m +4 h +1016 h +11 h +1 h +1 h +1 h +4 h +10 h +4 h +59 h +4 h +11 h +8033 m +4 h +1646 m +1 h +4 h +8034 m +4 h +1 h +1 h +4 h +8035 m +56 h +4 h +4 h +8036 m +1 h +10 h +443 h +4 h +4 h +1 h +8037 m +8038 m +4 h +332 h +1 h +4 h +8039 m +10 h +123 h +1 h +4 h +8040 m +307 h +31 h +25 h +8041 m +82 h +8042 m +1 h +1 h +8043 m +8044 m +169 h +8045 m +265 h +27 h +91 h +10 h +6438 m +8046 m +4 h +124 h +10 h +1 h +8047 m +4 h +4 h +1 h +4 h +1 h +307 h +1 h +1 h +64 h +57 h +41 h +4 h +4 h +10 h +8048 m +11 h +1 h +4 h +1 h +1 h +8049 m +10 h +8050 m +8051 m +10 h +4 h +1 h +4 h +10 h +114 h +2314 m +1 h +41 h +10 h +4 h +8052 m +10 h +83 h +10 h +59 h +1 h +11 h +1406 h +1 h +687 h +25 h +11 h +4 h +10 h +447 h +4 h +36 h +41 h +1 h +10 h +10 h +82 h +4 h +57 h +4 h +1 h +11 h +173 h +265 h +170 h +11 h +1454 m +11 h +119 h +97 h +1 h +4 h +8053 m +1 h +8054 m +1 h +79 h +8055 m +10 h +8056 m +27 h +70 m +6731 m +4 h +1 h +4 h +8057 m +22 h +8058 m +10 h +10 h +5917 m +8059 m +10 h +4 h +238 h +4590 m +27 h +10 h +656 h +8060 m +8061 m +10 h +8062 m +2002 h +4 h +10 h +8063 m +1 h +2625 h +11 h +1 h +1957 m +172 h +4 h +1 h +4 h +4 h +8064 m +4 h +27 h +10 h +8065 m +4 h +8066 m +124 h +4 h +4 h +1790 h +97 h +1 h +28 h +8067 m +4 h +4 h +8068 m +4 h +8069 m +11 h +143 h +1 h +4 h +8070 m +10 h +1 h +10 h +4 h +359 h +289 h +114 h +10 h +1 h +79 h +4 h +4 h +3 h +1 h +8071 m +347 m +1 h +10 h +13 h +1 h +10 h +82 h +73 h +1 h +125 h +1 h +8072 m +8073 m +8074 m +8075 m +8076 m +11 h +238 h +3679 m +4 h +4 h +45 h +8077 m +1 h +1 h +143 h +8078 m +12 h +4 h +8079 m +1 h +8080 m +1 h +11 h +4 h +2265 m +146 h +10 h +4 h +297 h +1 h +4 h +1620 m +642 m +5917 m +97 h +4 h +8081 m +8082 m +4 h +1 h +10 h +8083 m +11 h +1 h +114 h +1 h +10 h +22 h +279 h +4 h +1 h +1 h +10 h +8084 m +1067 m +8085 m +8086 m +4 h +8087 m +8088 m +10 h +359 h +94 h +4 h +10 h +4 h +114 h +8089 m +8090 m +1 h +1 h +1 h +1 h +8091 m +4 h +10 h +4 h +4 h +31 h +8092 m +1 h +94 h +4 h +4 h +124 h +31 h +1 h +124 h +8093 m +4 h +10 h +1074 h +238 h +10 h +57 h +59 h +1 h +1 h +1 h +1105 h +1 h +8094 m +8095 m +4 h +4 h +276 h +192 h +10 h +4 h +8096 m +1 h +935 h +4 h +109 h +10 h +4 h +4 h +8097 m +8098 m +8099 m +10 h +2379 m +4 h +1 h +10 h +4 h +4 h +8100 m +1 h +10 h +1 h +8101 m +10 h +8102 m +73 h +704 m +8103 m +307 h +4 h +109 h +1 h +4 h +4 h +1 h +4 h +8104 m +4 h +190 h +10 h +1092 m +113 h +1 h +109 h +10 h +4 h +10 h +10 h +459 m +2418 m +8 h +1650 h +4 h +64 h +8105 m +4 h +1 h +1 h +41 h +8106 m +10 h +8107 m +57 h +8108 m +129 h +4 h +10 h +106 h +11 h +59 h +8109 m +10 h +8110 m +4 h +170 h +4 h +8111 m +8112 m +8113 m +4 h +10 h +10 h +1 h +347 m +57 h +1 h +8114 m +1027 h +1751 m +83 h +10 h +1 h +4 h +172 h +56 h +2616 m +1 h +443 h +10 h +10 h +1 h +8115 m +1 h +8116 m +8117 m +4 h +10 h +1 h +8118 m +10 h +1 h +11 h +8119 m +8120 m +8121 m +1 h +1 h +4 h +4 h +55 h +857 h +620 m +1 h +65 h +1 h +59 h +258 h +11 h +1 h +1 h +4 h +8122 m +4 h +8123 m +195 h +1 h +172 h +1 h +687 h +10 h +8124 m +8125 m +10 h +1 h +10 h +8126 m +10 h +8127 m +4 h +156 h +8128 m +1886 m +8129 m +4 h +10 h +4 h +57 h +4 h +94 h +1 h +1 h +8130 m +10 h +4 h +4 h +143 h +4 h +1 h +8131 m +82 h +8132 m +1 h +1 h +1 h +10 h +83 h +1 h +8133 m +125 h +25 h +10 h +8134 m +11 h +371 h +4 h +4 h +5917 h +92 h +258 h +4 h +10 h +10 h +167 h +10 h +1 h +8135 m +4 h +4 h +1 h +10 h +4 h +1 h +8136 m +146 h +10 h +4378 m +4 h +1 h +10 h +10 h +4 h +22 h +4 h +10 h +4 h +4 h +57 h +965 m +5387 m +4 h +1 h +4 h +506 m +195 h +124 h +1 h +41 h +109 h +8137 m +509 m +4 h +4 h +10 h +114 h +10 h +1 h +4 h +1 h +4 h +278 h +4 h +5229 m +1403 h +4 h +1137 h +124 h +4 h +447 h +10 h +186 h +13 h +10 h +1 h +4 h +279 h +12 h +4 h +4 h +1 h +114 h +8138 m +1 h +8139 m +10 h +8140 m +250 h +10 h +8141 m +4 h +10 h +11 h +109 h +1 h +1 h +1 h +4 h +4 h +1 h +10 h +143 h +1 h +520 h +4 h +170 h +278 h +4 h +1 h +82 h +4 h +10 h +1 h +83 h +4 h +10 h +4 h +601 h +1 h +8142 m +8143 m +4 h +8144 m +185 h +7787 m +8 h +8145 m +135 h +10 h +1 h +5929 m +1 h +272 h +4 h +8146 m +8147 m +157 h +11 h +4 h +4 h +4 h +8148 m +7348 m +4 h +1 h +31 h +4 h +4 h +1 h +196 h +75 m +1 h +10 h +4 h +10 h +56 h +1 h +2508 m +1 h +1 h +4 h +8149 m +1 h +4 h +4 h +109 h +1445 h +4 h +10 h +124 h +73 h +10 h +4 h +4 h +1403 h +1 h +10 h +4 h +8150 m +123 h +444 m +4 h +4 h +11 h +3303 m +10 h +10 h +59 h +82 h +4 h +1 h +82 h +4 h +913 m +1 h +12 h +123 h +13 h +82 h +4 h +4 h +1 h +278 h +10 h +8151 m +10 h +1 h +4 h +2951 m +1 h +1 h +10 h +1 h +135 h +11 h +64 h +10 h +10 h +4 h +8152 m +601 h +4 h +520 h +57 h +1 h +266 h +1 h +82 h +4 h +8153 m +1975 m +8154 m +1953 m +10 h +65 h +8155 m +124 h +794 m +8156 m +570 h +1261 h +578 m +4 h +4 h +10 h +4 h +8157 m +1822 h +8158 m +10 h +1 h +1 h +4 h +986 h +1642 h +4 h +1 h +1 h +8159 m +1 h +8160 m +10 h +8161 m +82 h +4 h +1 h +83 h +114 h +10 h +119 h +4 h +8162 m +10 h +1 h +11 h +601 h +144 h +1 h +83 h +1 h +8163 m +8164 m +3509 m +4 h +10 h +230 h +73 h +1 h +10 h +1016 h +10 h +4810 m +45 h +1 h +1016 h +8165 m +8166 m +10 h +11 h +1 h +10 h +5621 m +262 h +74 h +1766 h +1 h +4 h +4 h +1 h +4 h +8167 m +140 h +4 h +10 h +41 h +1 h +146 h +4 h +1 h +8168 m +4 h +4 h +4 h +4574 m +57 h +4 h +4 h +8169 m +150 m +10 h +3909 m +1445 h +4 h +10 h +10 h +10 h +1 h +8170 m +3111 m +55 h +36 h +4 h +4 h +13 h +11 h +8171 m +1 h +57 h +4 h +4 h +4 h +4 h +10 h +8172 m +4 h +1 h +1 h +358 h +1 h +83 h +1 h +82 h +4 h +8173 m +10 h +4 h +4 h +59 h +8174 m +4 h +8175 m +4 h +10 h +124 h +4 h +4 h +3 h +1 h +307 h +4 h +1 h +82 h +139 h +8176 m +10 h +10 h +4 h +4 h +4 h +4 h +4 h +8177 m +1 h +4 h +1 h +1 h +8178 m +11 h +1 h +10 h +4 h +195 h +8179 m +4 h +8180 m +601 h +8181 m +1 h +8182 m +964 m +124 h +869 m +1 h +8183 m +8184 m +4 h +8185 m +1359 h +8186 m +10 h +266 h +1 h +1822 h +8187 m +1 h +1 h +25 h +9 m +8188 m +1 h +8189 m +11 h +10 h +8190 m +8191 m +8192 m +124 h +3 h +8193 m +1 h +8194 m +83 h +1 h +13 h +25 h +1 h +8195 m +10 h +57 h +82 h +8196 m +10 h +59 h +4 h +4 h +4 h +4 h +8197 m +1 h +1 h +8198 m +143 h +8199 m +124 h +1 h +10 h +1 h +601 h +8200 m +512 m +8201 m +229 h +8202 m +1 h +3 h +10 h +12 h +8203 m +4 h +170 h +12 h +124 h +1 h +10 h +4 h +8204 m +1 h +11 h +4 h +25 h +4 h +1070 m +104 h +4 h +4 h +172 h +8205 m +3 h +8206 m +1 h +1 h +262 h +10 h +10 h +11 h +8207 m +8208 m +1 h +1556 m +1 h +1 h +4 h +1 h +4 h +36 h +5917 h +869 m +1 h +48 h +1 h +1685 h +13 h +124 h +4 h +8209 m +1650 h +1 h +8210 m +10 h +10 h +278 h +4 h +4 h +1 h +8211 m +8212 m +8213 m +31 h +297 h +4 h +4 h +1362 h +1 h +1 h +1606 m +190 h +3 h +31 h +57 h +10 h +10 h +8214 m +1 h +10 h +172 h +278 h +8215 m +1016 h +1 h +8216 m +8217 m +1 h +8218 m +5929 m +4 h +79 h +1 h +10 h +1 h +41 h +124 h +10 h +4 h +1 h +278 h +4 h +536 h +4 h +1691 m +110 h +8219 m +8220 m +138 h +10 h +74 h +8221 m +124 h +8222 m +4 h +8223 m +4 h +114 h +1 h +1 h +10 h +4986 m +4 h +4 h +4 h +1250 h +4 h +8224 m +1 h +307 h +1 h +1 h +1 h +8225 m +8226 m +170 h +1504 m +4 h +3 h +1 h +1105 h +4 h +4 h +8227 m +11 h +196 h +10 h +4 h +57 h +4 h +443 h +2746 m +4 h +911 m +4 h +10 h +125 h +45 h +41 h +4 h +11 h +109 h +4 h +1 h +4 h +5 h +8228 m +10 h +2379 m +1796 h +8229 m +1 h +8230 m +90 m +108 h +8231 m +12 h +8232 m +4 h +8233 m +1 h +1478 m +4 h +10 h +4 h +74 h +143 h +4 h +94 h +11 h +3 h +10 h +10 h +8234 m +8235 m +4 h +4 h +13 h +13 h +8236 m +4000 m +1 h +1 h +45 h +2272 m +8237 m +8238 m +4 h +4 h +1 h +8239 m +8240 m +8241 m +1 h +1714 m +10 h +1 h +8242 m +4 h +1 h +8243 m +10 h +4 h +1 h +399 h +8244 m +10 h +11 h +8245 m +10 h +4 h +125 h +4 h +10 h +4 h +109 h +57 h +3 h +4 h +4 h +4 h +10 h +1 h +8246 m +143 h +4 h +1 h +12 h +31 h +4 h +10 h +4 h +55 h +1 h +8017 m +4 h +8247 m +8248 m +10 h +8249 m +41 h +8250 m +4 h +8251 m +181 h +10 h +8252 m +31 h +84 h +8253 m +4 h +8254 m +4 h +8255 m +4 h +1 h +3 h +12 h +1 h +1 h +4 h +4 h +41 h +8256 m +4 h +4 h +4 h +4 h +4 h +4 h +112 h +8257 m +1 h +332 h +6558 m +10 h +270 h +11 h +4 h +4 h +4 h +8258 m +8259 m +109 h +266 h +353 m +4 h +4 h +1 h +278 h +1 h +1 h +1 h +10 h +1 h +8260 m +112 h +1137 h +59 h +8261 m +1 h +10 h +113 h +1 h +2887 h +8262 m +4 h +10 h +4 h +8263 m +8264 m +8265 m +125 h +4 h +4 h +4 h +569 h +10 h +10 h +1470 h +8266 m +10 h +3 h +10 h +74 h +1 h +1 h +11 h +10 h +10 h +92 h +4 h +8267 m +11 h +10 h +124 h +125 h +104 h +976 h +57 h +258 h +114 h +230 h +4 h +8268 m +41 h +8269 m +167 h +464 h +10 h +8270 m +190 h +4 h +4 h +8271 m +10 h +25 h +10 h +1 h +10 h +358 h +1 h +8272 m +4 h +1 h +55 h +4 h +157 h +8273 m +966 m +8274 m +59 h +1 h +4 h +8275 m +157 h +1 h +4 h +31 h +4 h +1 h +79 h +1 h +4 h +4 h +4 h +69 h +10 h +11 h +4 h +57 h +8276 m +97 h +124 h +3025 m +125 h +1 h +12 h +4 h +1 h +109 h +718 h +82 h +41 h +4 h +4 h +196 h +4 h +64 h +1714 m +45 h +4 h +4 h +4 h +114 h +1 h +196 h +8277 m +4 h +8278 m +8279 m +1 h +10 h +92 h +64 h +11 h +8280 m +1 h +45 h +1 h +109 h +4 h +8281 m +1 h +8282 m +1 h +2625 h +3675 m +1 h +368 h +1685 h +119 h +8283 m +164 h +1 h +4 h +1 h +8284 m +91 h +1 h +1 h +10 h +4 h +64 h +8285 m +8286 m +8287 m +3 h +8 h +8288 m +8289 m +1 h +4 h +4 h +82 h +11 h +10 h +104 h +4 h +359 h +12 h +274 h +10 h +8290 m +272 h +185 h +8291 m +8292 m +3 h +8293 m +913 m +10 h +57 h +4 h +8294 m +64 h +4 h +8295 m +10 h +11 h +1 h +8296 m +8297 m +8298 m +8299 m +1 h +8300 m +3 h +10 h +4 h +10 h +4 h +4 h +8301 m +1 h +1 h +8302 m +92 h +4 h +124 h +11 h +10 h +124 h +1 h +3 h +1 h +147 h +8303 m +4 h +4 h +8304 m +4 h +1 h +8305 m +10 h +1 h +4 h +4 h +1 h +1 h +125 h +10 h +105 m +4 h +4 h +8306 m +4 h +1 h +4 h +158 h +25 h +8307 m +1 h +1 h +10 h +8308 m +4218 m +4 h +1 h +8309 m +1 h +81 m +4 h +250 h +1 h +172 h +55 h +1 h +129 h +1 h +41 h +1 h +4 h +1 h +146 h +4 h +8310 m +4 h +4 h +59 h +135 h +10 h +4 h +4 h +8311 m +7064 m +332 h +4 h +4 h +8312 m +4 h +8313 m +4 h +10 h +167 h +8314 m +10 h +1 h +82 h +55 h +25 h +10 h +1 h +1 h +10 h +1 h +10 h +569 h +1 h +10 h +4 h +8315 m +195 h +779 m +109 h +912 m +779 h +1 h +238 h +25 h +10 h +10 h +4 h +8316 m +274 h +1772 h +4 h +41 h +10 h +195 h +8317 m +8318 m +3 h +1189 m +8319 m +10 h +31 h +1 h +59 h +8320 m +4 h +10 h +238 h +8321 m +1027 h +1 h +10 h +1 h +10 h +4 h +25 h +10 h +10 h +1 h +1 h +757 h +10 h +1 h +536 h +4 h +4 h +4 h +8322 m +8323 m +1 h +1 h +4 h +1 h +190 h +36 h +8324 m +10 h +10 h +4 h +186 h +8325 m +4 h +1 h +4 h +8326 m +170 h +1 h +1 h +332 h +1 h +10 h +8327 m +4 h +4 h +10 h +8328 m +8329 m +1 h +278 h +109 h +4292 m +4 h +1 h +8330 m +10 h +8331 m +8332 m +250 h +4 h +8333 m +10 h +1 h +265 h +1769 m +1 h +4 h +4 h +1 h +10 h +1 h +11 h +4 h +1 h +4 h +83 h +10 h +10 h +10 h +1 h +139 h +10 h +3646 m +4 h +55 h +3025 m +1 h +1 h +1 h +1 h +10 h +1 h +1 h +1 h +8334 m +8335 m +125 h +4 h +8336 m +73 h +1 h +330 h +8337 m +463 m +3 h +104 h +97 h +4 h +1 h +4 h +4 h +11 h +307 h +4 h +1 h +278 h +1 h +1955 m +57 h +45 h +8338 m +1 h +59 h +190 h +1 h +8339 m +4 h +1454 m +10 h +65 h +4 h +8340 m +169 h +1 h +258 h +1 h +4 h +5 h +1 h +4240 m +10 h +10 h +92 h +7870 m +10 h +8341 m +1 h +8342 m +4 h +1 h +4 h +94 h +10 h +73 h +583 m +1 h +1 h +1 h +8206 m +170 h +4 h +692 h +4 h +4 h +1 h +4 h +4 h +1968 m +8343 m +4 h +1 h +4 h +289 h +4 h +10 h +8344 m +2172 h +4 h +55 h +4 h +4 h +1975 m +1 h +146 h +8345 m +13 h +4 h +57 h +4 h +82 h +8346 m +1691 m +10 h +10 h +195 h +11 h +1 h +4 h +31 h +4 h +13 h +10 h +8347 m +10 h +1 h +8348 m +195 h +8349 m +10 h +8350 m +1 h +4 h +1038 m +8351 m +8352 m +1619 h +1 h +59 h +1 h +28 h +885 m +10 h +1 h +147 h +143 h +4 h +10 h +10 h +192 h +368 h +192 h +8353 m +1 h +1 h +4 h +4 h +4 h +10 h +8354 m +4 h +258 h +11 h +83 h +8355 m +1 h +10 h +538 h +10 h +10 h +4 h +4 h +27 h +8 h +11 h +10 h +143 h +139 h +1 h +4 h +8356 m +1122 m +8357 m +8358 m +4 h +114 h +8359 m +170 h +8360 m +10 h +8361 m +4 h +569 h +41 h +4 h +8362 m +10 h +990 m +11 h +4 h +4333 m +8363 m +4 h +1 h +4 h +31 h +4 h +1 h +4 h +4 h +45 h +1542 m +996 m +8364 m +4 h +1 h +4 h +1 h +10 h +1 h +403 h +10 h +146 h +114 h +10 h +1 h +4 h +13 h +8365 m +1 h +114 h +4 h +1 h +4 h +4 h +10 h +8366 m +3177 m +8367 m +124 h +4 h +4 h +4 h +8368 m +8369 m +4858 m +1 h +8370 m +143 h +8371 m +10 h +4 h +4 h +1 h +4 h +4 h +10 h +1914 m +1 h +164 h +82 h +10 h +1 h +8372 m +10 h +1 h +10 h +4 h +8373 m +1 h +109 h +109 h +4 h +1 h +56 h +1 h +4 h +8374 m +8375 m +10 h +3555 m +994 m +8376 m +4 h +1 h +1 h +10 h +1939 m +8377 m +64 h +8378 m +170 h +4 h +11 h +8379 m +10 h +4 h +94 h +4 h +109 h +8380 m +8381 m +8382 m +297 h +1 h +3 h +41 h +4 h +4 h +241 m +258 h +10 h +1 h +4 h +434 m +10 h +4 h +10 h +10 h +692 h +10 h +109 h +8383 m +1 h +8384 m +1 h +733 m +10 h +276 h +1 h +687 h +857 h +8385 m +4 h +443 h +10 h +56 h +4 h +1024 m +4 h +10 h +1 h +1 h +1 h +3768 m +4 h +4 h +4 h +1 h +124 h +1449 m +4 h +59 h +1 h +1 h +4 h +4 h +190 h +8386 m +1 h +4 h +1 h +1 h +147 h +10 h +4 h +4 h +59 h +4 h +10 h +1 h +172 h +4 h +82 h +11 h +27 h +1 h +258 h +8387 m +8388 m +8389 m +1 h +10 h +8390 m +83 h +1 h +3 h +74 h +10 h +10 h +8391 m +4 h +1 h +4 h +4 h +1 h +1 h +4 h +2172 h +808 m +1 h +477 h +8392 m +674 m +13 h +1 h +8393 m +1 h +1 h +1 h +4 h +10 h +10 h +8394 m +4 h +4 h +1835 m +1070 m +1 h +56 h +8395 m +65 h +4 h +36 h +135 h +4 h +4 h +8396 m +4 h +4 h +10 h +4 h +3 h +4 h +4 h +1 h +1 h +2617 m +1 h +31 h +4 h +4 h +1030 h +13 h +181 h +8397 m +7709 m +1 h +3 h +10 h +8398 m +83 h +1 h +4 h +4 h +8399 m +8400 m +1 h +3 h +1337 m +10 h +10 h +10 h +4 h +11 h +1 h +92 h +2920 m +1 h +4 h +31 h +59 h +1 h +8401 m +4 h +10 h +83 h +8402 m +4 h +41 h +1 h +59 h +1 h +5917 h +41 h +4 h +4 h +125 h +73 h +4 h +8403 m +10 h +104 h +4 h +10 h +4 h +74 h +1 h +4 h +358 h +4 h +45 h +41 h +4 h +181 h +83 h +265 h +10 h +8404 m +10 h +195 h +11 h +1 h +4 h +8405 m +4 h +78 m +8406 m +8407 m +1 h +8408 m +4 h +10 h +8409 m +83 h +464 h +4 h +8410 m +4 h +10 h +8411 m +1 h +5929 h +4 h +11 h +186 h +82 h +119 h +1796 h +195 h +8412 m +265 h +4 h +13 h +1016 h +8413 m +536 h +2733 h +4 h +8414 m +1 h +4 h +10 h +8415 m +1 h +1 h +4 h +4 h +195 h +8206 h +8416 m +4 h +65 h +1 h +8417 m +125 h +8418 m +266 h +4 h +8419 m +10 h +265 h +1 h +4 h +1 h +11 h +4 h +4 h +1 h +10 h +8420 m +82 h +8421 m +1 h +8422 m +10 h +83 h +1 h +8423 m +4 h +8424 m +8425 m +1 h +4 h +1 h +10 h +10 h +10 h +94 h +8426 m +8427 m +8428 m +4 h +976 h +4 h +4256 m +1 h +8429 m +1 h +167 h +1 h +4 h +8430 m +196 h +8431 m +1 h +578 m +1 h +10 h +1 h +1 h +8432 m +4 h +8433 m +718 h +4 h +912 m +4 h +1 h +229 h +8434 m +10 h +1 h +4 h +8435 m +11 h +82 h +1886 m +167 h +10 h +8436 m +8437 m +10 h +4 h +73 h +4 h +4 h +8438 m +12 h +109 h +8439 m +73 h +4 h +4 h +11 h +25 h +2592 m +10 h +1939 m +2172 h +1 h +266 h +59 h +8440 m +4 h +104 h +4 h +4 h +10 h +1 h +195 h +109 h +8441 m +4 h +4 h +1 h +1 h +1 h +6851 m +8442 m +8188 m +4 h +4 h +444 m +65 h +11 h +1 h +74 h +10 h +8443 m +8444 m +8445 m +8446 m +8447 m +1 h +536 h +4 h +10 h +1796 h +4 h +3 h +3558 m +4 h +1571 m +11 h +10 h +8448 m +1 h +8449 m +4 h +97 h +1 h +172 h +4 h +7214 m +3170 m +59 h +4 h +10 h +4 h +10 h +10 h +11 h +10 h +10 h +10 h +1 h +82 h +79 h +1 h +1508 m +1 h +4 h +11 h +4 h +601 h +493 m +10 h +8450 m +10 h +13 h +65 h +8451 m +8452 m +1 h +10 h +8453 m +1 h +10 h +10 h +4 h +400 m +1 h +31 h +468 m +4 h +8454 m +250 h +8455 m +4 h +4 h +2494 m +1 h +4 h +59 h +1822 h +25 h +8456 m +1 h +11 h +1 h +4 h +4 h +371 h +8457 m +3847 m +124 h +447 h +1 h +4 h +10 h +4824 m +278 h +4 h +10 h +31 h +8458 m +4 h +1309 m +8459 m +347 h +157 h +57 h +10 h +10 h +1 h +8460 m +8461 m +8462 m +10 h +8463 m +4 h +8464 m +1 h +185 h +109 h +147 h +4 h +5863 m +10 h +65 h +25 h +4 h +1 h +8465 m +4 h +10 h +1981 m +59 h +2041 m +4 h +10 h +25 h +10 h +11 h +8466 m +4 h +1016 h +8467 m +10 h +1 h +4 h +8468 m +1 h +8469 m +1 h +4 h +25 h +1 h +10 h +57 h +2148 m +8470 m +8471 m +82 h +8472 m +8473 m +1 h +4 h +5436 m +8474 m +4 h +1 h +4 h +28 h +8475 m +4 h +8476 m +11 h +10 h +538 h +8477 m +10 h +4 h +8478 m +1 h +4496 m +4 h +4 h +170 h +1 h +10 h +1 h +1 h +55 h +8479 m +55 h +316 m +8480 m +10 h +2022 m +386 h +4 h +8481 m +41 h +65 h +196 h +4 h +74 h +25 h +454 m +4 h +1 h +4 h +4 h +4 h +4 h +10 h +10 h +1 h +10 h +83 h +31 h +4 h +8482 m +5060 m +10 h +12 h +8483 m +3 h +238 h +8484 m +109 h +110 h +1 h +1 h +10 h +4 h +4 h +57 h +4 h +10 h +8485 m +186 h +8486 m +1 h +146 h +1 h +8487 m +4 h +45 h +8488 m +169 h +8489 m +10 h +479 m +1 h +10 h +1 h +1 h +463 m +4 h +399 h +10 h +4 h +1 h +1 h +114 h +4 h +4 h +4 h +1 h +11 h +4 h +59 h +3161 m +4132 m +4 h +4 h +190 h +83 h +8490 m +64 h +4 h +204 h +1 h +10 h +8491 m +10 h +1 h +79 h +8492 m +8493 m +8494 m +1074 h +1 h +1117 m +113 h +11 h +4 h +10 h +4 h +964 m +124 h +4 h +22 h +10 h +11 h +1370 m +1 h +8495 m +10 h +10 h +8496 m +45 h +8497 m +10 h +1017 m +1 h +8498 m +8499 m +1 h +4 h +10 h +4 h +4 h +8500 m +125 h +31 h +8501 m +1 h +279 h +8502 m +8503 m +4 h +4 h +435 m +8504 m +1 h +8505 m +1 h +1 h +22 h +1 h +1056 m +4 h +4 h +4 h +1470 h +4 h +55 h +1 h +8506 m +8507 m +1 h +8508 m +1642 h +4 h +10 h +3 h +10 h +4 h +434 m +8509 m +1 h +536 h +75 m +10 h +8510 m +1250 h +4 h +114 h +65 h +4 h +2128 m +65 h +10 h +1 h +8511 m +4 h +195 h +157 h +8512 m +92 h +1 h +10 h +4 h +31 h +4 h +8513 m +146 h +1016 h +8514 m +4 h +4 h +4 h +8515 m +10 h +82 h +1 h +75 m +196 h +1 h +4 h +4 h +4 h +46 h +8516 m +12 h +307 h +185 h +1 h +307 h +4 h +10 h +8517 m +109 h +1 h +4 h +10 h +8518 m +82 h +8519 m +1 h +8520 m +4 h +4111 m +4 h +4 h +278 h +358 h +1 h +10 h +10 h +4 h +1 h +172 h +10 h +8521 m +4 h +8522 m +4 h +103 m +1 h +8523 m +4 h +25 h +8524 m +8525 m +1 h +41 h +10 h +7870 m +10 h +4 h +687 h +358 h +276 h +4 h +8526 m +4 h +10 h +4 h +1 h +8527 m +10 h +10 h +8 h +124 h +109 h +8528 m +174 h +114 h +533 m +1 h +10 h +4 h +1 h +1 h +1 h +57 h +1 h +4 h +1 h +10 h +8529 m +8530 m +8531 m +1 h +11 h +4 h +57 h +4 h +4 h +57 h +4 h +1 h +11 h +4 h +399 h +11 h +64 h +124 h +506 m +1 h +1 h +8532 m +4 h +4 h +6133 m +8533 m +8534 m +2625 h +11 h +1 h +8535 m +1137 h +4 h +10 h +11 h +1 h +1 h +10 h +8536 m +5035 m +8537 m +1619 h +158 h +3 h +10 h +1 h +4 h +41 h +8538 m +11 h +1 h +1 h +4 h +172 h +1 h +10 h +11 h +119 h +8539 m +11 h +8540 m +3112 m +4 h +4 h +59 h +8541 m +11 h +10 h +22 h +8542 m +83 h +1642 h +8543 m +8544 m +4 h +4 h +184 h +2184 m +1 h +5757 m +1045 m +4 h +3303 m +4 h +1070 m +4 h +8545 m +4 h +1 h +10 h +1 h +8546 m +4 h +22 h +590 m +1 h +8547 m +10 h +1 h +1685 h +8548 m +10 h +10 h +447 h +806 m +4 h +83 h +1 h +8549 m +190 h +8550 m +10 h +8551 m +10 h +4 h +8552 m +4 h +57 h +1374 m +1 h +278 h +447 h +1 h +138 h +1083 h +3 h +82 h +11 h +1 h +1 h +8553 m +4 h +4 h +4 h +7839 h +1 h +10 h +8554 m +2532 m +4464 m +4651 m +83 h +12 h +10 h +59 h +4 h +10 h +1 h +4 h +4 h +59 h +1 h +8555 m +4 h +1 h +119 h +4 h +8556 m +1 h +83 h +1 h +104 h +4 h +10 h +4 h +4 h +124 h +8557 m +94 h +8558 m +4 h +4 h +1 h +8559 m +10 h +11 h +59 h +8560 m +1 h +10 h +10 h +10 h +10 h +8561 m +3 h +1261 h +55 h +65 h +8562 m +4 h +4 h +6399 m +11 h +12 h +4 h +4 h +4 h +8563 m +10 h +11 h +10 h +10 h +1 h +10 h +185 h +8564 m +8565 m +1 h +1 h +8566 m +10 h +4 h +3 h +143 h +139 h +8567 m +4 h +190 h +4 h +860 m +4 h +1 h +1939 h +8568 m +4 h +4 h +4 h +10 h +2308 m +4 h +4 h +4 h +1 h +4 h +27 h +125 h +2266 m +4 h +4 h +4 h +4 h +10 h +4 h +8569 m +4 h +10 h +8570 m +1 h +64 h +36 h +11 h +4 h +8571 m +1 h +73 h +8572 m +1 h +4 h +4 h +8573 m +4 h +4 h +1 h +170 h +118 m +10 h +4 h +3750 m +8574 m +8575 m +692 h +11 h +10 h +258 h +359 h +10 h +1 h +41 h +1 h +11 h +10 h +1105 h +8576 m +230 h +169 h +4 h +1 h +31 h +8577 m +10 h +8578 m +10 h +4 h +65 h +146 h +1 h +172 h +4 h +45 h +4 h +4 h +4 h +8579 m +4 h +8580 m +82 h +1 h +10 h +10 h +4 h +1 h +10 h +857 h +1 h +195 h +10 h +1 h +25 h +4 h +1 h +911 m +167 h +4 h +1 h +10 h +262 h +8581 m +8582 m +1 h +74 h +4 h +8583 m +79 h +41 h +8584 m +10 h +169 h +124 h +8585 m +2625 h +4 h +4 h +4 h +4 h +8586 m +10 h +10 h +4 h +4 h +109 h +11 h +4 h +10 h +1 h +8587 m +64 h +104 h +4 h +869 h +4 h +238 h +10 h +8588 m +110 h +1 h +4127 m +1508 m +4 h +1 h +97 h +83 h +4 h +4 h +8589 m +4 h +143 h +1 h +8590 m +8591 m +1 h +1 h +1 h +4 h +83 h +109 h +1975 m +4 h +1 h +8592 m +114 h +1772 h +10 h +4 h +8593 m +1 h +31 h +10 h +4 h +4 h +92 h +10 h +332 h +4 h +10 h +143 h +4 h +10 h +65 h +3600 m +8594 m +10 h +1 h +1 h +65 h +10 h +687 h +17 h +11 h +8595 m +4 h +4 h +10 h +4 h +8596 m +4 h +114 h +4 h +10 h +73 h +1 h +4896 m +4 h +4 h +10 h +468 m +10 h +10 h +13 h +8597 m +1 h +59 h +1 h +4 h +125 h +1 h +229 h +1 h +4 h +8598 m +92 h +2923 m +57 h +8599 m +1 h +8600 m +4 h +97 h +6400 m +82 h +10 h +4 h +4 h +8601 m +4 h +3634 m +1 h +4 h +59 h +8602 m +1 h +4 h +10 h +649 m +3 h +2588 m +1 h +10 h +4 h +4 h +11 h +8603 m +185 h +4 h +4 h +123 h +181 h +4 h +1 h +65 h +69 h +4 h +41 h +10 h +4 h +4 h +1 h +8604 m +3 h +55 h +57 h +4 h +4 h +4 h +196 h +1 h +8605 m +10 h +8606 m +4 h +8607 m +10 h +4 h +3 h +4 h +1020 m +10 h +4 h +10 h +1 h +626 m +506 m +1 h +10 h +10 h +146 h +3555 m +640 h +4 h +125 h +10 h +1 h +124 h +820 m +1 h +1 h +4 h +533 m +55 h +1 h +82 h +8608 m +1 h +8609 m +11 h +4 h +110 h +3293 m +157 h +8610 m +1 h +8611 m +7 m +8612 m +1 h +10 h +57 h +1 h +687 h +109 h +4 h +4 h +92 h +124 h +1 h +8613 m +124 h +4 h +8614 m +4 h +28 h +4 h +4 h +10 h +447 h +1 h +4 h +1 h +1 h +11 h +4 h +996 m +8615 m +65 h +1 h +10 h +125 h +279 h +8616 m +10 h +578 m +10 h +10 h +7938 m +4714 m +11 h +1 h +84 h +8617 m +1 h +185 h +11 h +4 h +4 h +4 h +11 h +124 h +3 h +8618 m +10 h +4283 m +4 h +8619 m +8620 m +8621 m +8622 m +8623 m +41 h +2438 m +1 h +10 h +4 h +10 h +4 h +10 h +10 h +10 h +8624 m +8625 m +1 h +11 h +8626 m +10 h +11 h +1 h +10 h +1 h +11 h +8627 m +359 h +4 h +73 h +4 h +4 h +4 h +1 h +36 h +82 h +4 h +4 h +8628 m +45 h +4349 m +10 h +8629 m +4 h +1 h +11 h +4 h +13 h +1 h +4 h +8630 m +1 h +10 h +25 h +8631 m +1 h +1 h +11 h +8 h +8632 m +5411 m +10 h +4 h +4 h +77 h +55 h +4 h +195 h +8633 m +4 h +4 h +8634 m +8635 m +97 h +1 h +109 h +4 h +4 h +55 h +1 h +4 h +1 h +250 h +4 h +22 h +1 h +83 h +4 h +13 h +1 h +1835 m +4 h +1 h +935 h +8636 m +8637 m +4 h +285 m +10 h +4 h +1 h +119 h +4 h +8638 m +8639 m +8640 m +538 h +31 h +8641 m +4 h +332 h +4 h +8642 m +4 h +4 h +4 h +11 h +779 h +2148 m +4 h +1 h +22 h +8643 m +10 h +59 h +1 h +1 h +36 h +13 h +258 h +31 h +4 h +10 h +1 h +1 h +1 h +1 h +279 h +1 h +8644 m +147 h +1 h +110 h +10 h +4 h +10 h +195 h +8645 m +73 h +3622 m +8646 m +4 h +10 h +4 h +8647 m +8648 m +1 h +4 h +4 h +135 h +31 h +8147 m +8649 m +1 h +8650 m +1 h +8651 m +55 h +8652 m +8653 m +4 h +5059 m +4 h +4 h +4 h +4 h +8654 m +4 h +1 h +57 h +8655 m +172 h +1 h +146 h +1 h +1 h +8656 m +464 h +8657 m +10 h +4 h +8658 m +1 h +10 h +1 h +135 h +8659 m +1 h +4 h +83 h +4 h +1 h +15 m +10 h +146 h +8660 m +1 h +1 h +8661 m +8662 m +169 h +289 h +57 h +3303 m +1 h +556 h +8663 m +8664 m +8 h +1 h +10 h +4 h +10 h +8665 m +8666 m +6565 m +358 h +4 h +278 h +4 h +1 h +10 h +1 h +4 h +1 h +10 h +8667 m +10 h +4 h +22 h +8668 m +31 h +124 h +4 h +3 h +1214 m +1 h +4 h +158 h +10 h +8669 m +1309 m +109 h +1 h +4 h +1 h +8670 m +1 h +692 h +41 h +10 h +8671 m +443 h +8672 m +4 h +1 h +57 h +8673 m +258 h +4 h +1261 h +1 h +8674 m +31 h +10 h +1 h +4 h +1 h +299 h +4 h +13 h +8675 m +56 h +358 h +4 h +1 h +4 h +1 h +164 h +1 h +97 h +123 h +124 h +4 h +8676 m +4 h +125 h +1 h +1 h +4 h +4 h +8677 m +125 h +33 m +10 h +313 m +4 h +119 h +31 h +4 h +4 h +10 h +687 h +10 h +36 h +4 h +10 h +8678 m +25 h +25 h +8679 m +3 h +104 h +8680 m +687 h +447 h +181 h +8681 m +8682 m +4 h +4 h +1 h +11 h +79 h +10 h +1 h +4 h +45 h +1 h +8683 m +8684 m +1 h +8685 m +8686 m +8687 m +8688 m +10 h +386 h +4 h +4 h +118 h +4 h +3 h +4 h +8689 m +8690 m +4 h +4 h +4 h +2851 m +10 h +1 h +1 h +4 h +4 h +10 h +10 h +196 h +1 h +4 h +1 h +1 h +920 p +11 h +4 h +1083 h +4 h +4 h +4 h +57 h +8691 m +11 h +8692 m +8693 m +59 h +4 h +8694 m +8695 m +10 h +4 h +4 h +10 h +4 h +692 h +4 h +4 h +8696 m +124 h +8697 m +4 h +10 h +4 h +5567 m +8698 m +10 h +1 h +10 h +104 h +4 h +8699 m +4 h +4 h +10 h +1 h +109 h +10 h +124 h +8700 m +41 h +1 h +114 h +1 h +11 h +1710 m +4 h +73 h +1 h +4 h +143 h +4 h +4 h +4 h +8701 m +4 h +536 h +1410 m +2815 m +935 h +8702 m +1 h +4 h +8703 m +520 h +1 h +8704 m +8705 m +10 h +8706 m +10 h +8707 m +1835 m +109 h +536 h +1 h +10 h +8708 m +4 h +8709 m +4 h +266 h +8710 m +3679 m +1 h +295 h +1 h +4 h +8711 m +276 h +1 h +1 h +1 h +8712 m +8713 m +4 h +10 h +10 h +8714 m +8715 m +10 h +10 h +1 h +4 h +8716 m +4 h +55 h +1 h +8717 m +10 h +4 h +8718 m +10 h +8 h +1 h +4 h +8719 m +4 h +295 h +8720 m +4 h +155 m +22 h +82 h +8721 m +1 h +8722 m +74 h +265 h +195 h +4 h +10 h +4 h +25 h +1 h +1299 m +57 h +4 h +119 h +4 h +4 h +4 h +4 h +8723 m +10 h +4 h +10 h +8724 m +74 h +27 h +10 h +8725 m +1177 m +8726 m +4 h +4 h +1 h +10 h +601 h +1 h +1 h +10 h +4 h +112 h +4 h +36 h +3837 m +11 h +278 h +11 h +1 h +10 h +1 h +33 m +8727 m +195 h +8728 m +10 h +4 h +75 h +45 h +10 h +1 h +4 h +8729 m +112 h +10 h +11 h +4 h +4 h +59 h +10 h +1 h +8730 m +1 h +4 h +5613 m +4 h +1796 h +278 h +1 h +4 h +770 m +4 h +4 h +630 m +8731 m +1 h +10 h +8732 m +170 h +4 h +10 h +4 h +92 h +8733 m +4 h +169 h +41 h +4 h +8734 m +4 h +1 h +3278 m +359 h +64 h +4 h +4 h +4 h +8735 m +4 h +238 h +4 h +4 h +139 h +8736 m +4 h +4 h +4 h +41 h +1 h +3 h +10 h +109 h +4 h +4 h +8737 m +8738 m +10 h +56 h +8739 m +238 h +1 h +8740 m +10 h +83 h +8741 m +4 h +1 h +4 h +55 h +10 h +1 h +4 h +4 h +1 h +4 h +10 h +10 h +92 h +10 h +5125 m +8742 m +10 h +1 h +4 h +11 h +10 h +10 h +8743 m +190 h +8744 m +4 h +11 h +10 h +4 h +1 h +172 h +10 h +4 h +1 h +31 h +10 h +488 h +25 h +8745 m +31 h +8746 m +1 h +4 h +8747 m +1 h +10 h +59 h +8748 m +4 h +4 h +8749 m +10 h +4 h +169 h +4 h +10 h +8750 m +1 h +10 h +8751 m +31 h +31 h +97 h +1 h +1 h +8752 m +4 h +8753 m +757 h +4 h +1 h +1 h +10 h +59 h +4 h +1 h +1 h +10 h +8754 m +1914 m +1 h +65 h +108 h +139 h +8755 m +4 h +1 h +339 m +92 h +779 h +8756 m +4 h +1214 m +92 h +8757 m +4 h +11 h +229 h +10 h +4 h +1 h +8758 m +8759 m +4 h +124 h +274 h +5963 m +10 h +4 h +12 h +4 h +1 h +10 h +1 h +5281 m +10 h +3799 m +10 h +167 h +8760 m +692 h +8761 m +4 h +8762 m +10 h +4 h +65 h +8763 m +3 h +10 h +2379 h +27 h +8764 m +1 h +4 h +4 h +195 h +383 h +8765 m +8766 m +82 h +4 h +4 h +10 h +8767 m +8768 m +4 h +146 h +1 h +6438 m +8769 m +94 h +4 h +718 h +4 h +1 h +2280 m +10 h +8770 m +1 h +74 h +692 h +83 h +4 h +59 h +56 h +7755 m +135 h +2794 m +8771 m +10 h +123 h +4 h +10 h +119 h +8772 m +3036 m +10 h +1 h +10 h +8773 m +1 h +25 h +11 h +4 h +1 h +976 h +443 h +626 m +8774 m +31 h +4 h +338 m +4 h +10 h +28 h +10 h +10 h +4 h +1790 h +986 h +4 h +10 h +11 h +4 h +8775 m +8776 m +8777 m +211 m +1 h +114 h +1 h +1 h +8778 m +1 h +8779 m +4 h +146 h +8780 m +4 h +332 h +25 h +8781 m +8782 m +4 h +27 h +1 h +186 h +601 h +65 h +6869 m +5053 m +82 h +8783 m +4 h +601 h +1642 h +4 h +10 h +238 h +56 h +11 h +1 h +10 h +4 h +8784 m +195 h +4 h +359 h +8785 m +4 h +4 h +10 h +332 h +1 h +10 h +1092 m +8786 m +1 h +8787 m +8788 m +10 h +106 h +1 h +4 h +4 h +2139 m +59 h +4 h +4 h +1 h +1 h +4 h +1 h +4 h +10 h +92 h +4 h +8789 m +8790 m +10 h +1 h +368 h +8791 m +109 h +204 h +842 m +8792 m +4 h +64 h +538 h +1 h +6200 m +4218 m +8793 m +8794 m +8795 m +114 h +1 h +1685 h +10 h +4 h +8796 m +578 m +4 h +4 h +8797 m +1406 h +57 h +10 h +25 h +4 h +1 h +1 h +4 h +55 h +1 h +8798 m +1 h +4 h +1454 m +8799 m +41 h +468 m +8800 m +8801 m +4 h +1 h +10 h +8802 m +8803 m +172 h +10 h +986 h +996 m +5379 m +10 h +146 h +8804 m +1 h +65 h +57 h +1284 m +3742 m +1 h +146 h +1 h +3 h +4 h +10 h +8805 m +8806 m +13 h +1 h +10 h +83 h +4 h +10 h +4 h +1 h +4 h +10 h +4 h +8807 m +8808 m +8809 m +1 h +11 h +8810 m +4 h +104 h +124 h +3 h +4 h +262 h +1 h +4 h +11 h +4 h +1 h +4 h +10 h +1 h +1 h +8811 m +124 h +1 h +1 h +1939 h +1 h +8812 m +8813 m +8814 m +1304 m +8815 m +82 h +59 h +8816 m +332 h +1 h +1 h +10 h +1 h +4 h +10 h +4 h +1 h +1 h +8817 m +4 h +1 h +3321 m +109 h +82 h +4 h +1 h +55 h +538 h +1 h +1 h +1 h +157 h +10 h +10 h +4 h +10 h +10 h +4 h +1 h +358 h +1 h +4 h +1478 m +1 h +156 h +1 h +1 h +3 h +2266 m +8818 m +135 h +8819 m +976 h +8820 m +64 h +59 h +570 h +10 h +109 h +56 h +25 h +1 h +8821 m +1 h +289 h +10 h +8822 m +4810 m +4 h +36 h +10 h +10 h +10 h +313 m +1 h +74 h +10 h +4 h +3 h +10 h +601 h +8823 m +1 h +10 h +4 h +1 h +4 h +1 h +2923 m +1 h +5907 m +620 m +4 h +677 m +8824 m +4 h +1 h +8825 m +10 h +10 h +25 h +10 h +40 h +1 h +1822 h +1250 h +8826 m +1 h +1 h +4 h +1 h +1 h +124 h +10 h +8827 m +109 h +4 h +1105 h +295 h +1 h +4 h +82 h +74 h +4 h +4 h +10 h +10 h +185 h +125 h +11 h +83 h +65 h +195 h +8828 m +10 h +56 h +1 h +4 h +11 h +10 h +1374 m +1 h +64 h +4 h +1 h +1 h +1 h +8829 m +1 h +5526 m +4 h +10 h +82 h +4 h +1 h +196 h +1 h +8830 m +3161 m +541 m +11 h +4203 m +4 h +8831 m +838 m +1 h +10 h +48 h +4 h +1 h +8832 m +10 h +139 h +4 h +4 h +4 h +8833 m +4 h +164 h +1725 m +8834 m +8835 m +2484 m +10 h +8836 m +10 h +4 h +4 h +1 h +11 h +8837 m +8838 m +1 h +8839 m +8840 m +3360 m +59 h +8841 m +82 h +8842 m +8843 m +167 h +8844 m +8 h +31 h +10 h +55 h +10 h +11 h +41 h +278 h +1737 m +3499 m +74 h +10 h +4 h +10 h +1309 m +31 h +224 h +8845 m +4 h +1 h +4 h +4 h +74 h +4 h +82 h +578 m +10 h +74 h +8846 m +4 h +8847 m +4 h +10 h +238 h +4 h +83 h +1 h +158 h +1 h +36 h +4 h +8848 m +4 h +6817 m +8849 m +83 h +195 h +1 h +196 h +25 h +10 h +3036 m +1 h +4 h +8850 m +4 h +4 h +10 h +582 m +1 h +986 h +110 h +12 h +190 h +1 h +435 m +8851 m +447 h +1045 m +258 h +135 h +583 m +4 h +1 h +45 h +4 h +124 h +181 h +4 h +8852 m +4 h +1 h +1 h +158 h +55 h +1 h +8853 m +114 h +327 m +112 h +1 h +8854 m +4 h +1 h +45 h +238 h +1 h +468 m +1 h +8855 m +8856 m +110 h +8857 m +1 h +4 h +1 h +119 h +8858 m +8859 m +1619 h +1 h +11 h +8860 m +10 h +8861 m +4 h +73 h +1685 h +36 h +8862 m +238 h +8863 m +4 h +4 h +4 h +4 h +40 h +91 h +114 h +4 h +61 m +10 h +10 h +10 h +1 h +8864 m +4 h +8865 m +4 h +10 h +1 h +1620 m +250 h +8866 m +4 h +8867 m +368 h +4 h +204 h +4 h +124 h +4 h +8868 m +10 h +8869 m +109 h +10 h +4 h +139 h +4 h +8870 m +8871 m +23 m +1 h +8872 m +1 h +1 h +4 h +170 h +8873 m +56 h +31 h +10 h +8874 m +2025 m +83 h +55 h +144 h +124 h +4 h +1685 h +27 h +4 h +8875 m +64 h +140 h +4 h +8876 m +1 h +1 h +1 h +8877 m +4 h +123 h +1 h +8878 m +1 h +8879 m +1 h +860 m +4 h +10 h +4 h +8880 m +8881 m +10 h +4 h +10 h +3 h +1 h +118 h +1 h +124 h +1 h +4 h +1 h +4 h +10 h +4 h +4 h +1 h +8882 m +307 h +4 h +8883 m +10 h +3 h +10 h +82 h +4 h +5254 m +1 h +1478 m +229 h +8884 m +124 h +8 h +4 h +4 h +4 h +860 m +1 h +10 h +8885 m +92 h +64 h +4 h +1 h +57 h +4 h +23 m +4350 m +41 h +4 h +8886 m +8887 m +4 h +8888 m +4 h +10 h +4 h +4 h +1 h +4 h +57 h +1 h +93 m +4 h +10 h +195 h +4 h +8889 m +4 h +22 h +11 h +41 h +626 m +8890 m +8891 m +8892 m +10 h +143 h +10 h +1 h +41 h +1 h +1 h +1 h +124 h +1 h +10 h +11 h +172 h +10 h +1 h +1 h +4 h +8893 m +10 h +1 h +266 h +4 h +4 h +10 h +1 h +125 h +8894 m +11 h +1619 h +109 h +12 h +4 h +65 h +10 h +61 m +97 h +74 h +147 h +8895 m +4 h +10 h +1 h +4 h +139 h +5537 m +4 h +1 h +10 h +11 h +4 h +1751 m +1 h +276 h +8896 m +297 h +3 h +125 h +1 h +8897 m +10 h +7760 m +11 h +83 h +74 h +164 h +230 h +3 h +4 h +8898 m +41 h +10 h +4 h +27 h +1 h +83 h +4 h +1685 h +65 h +4 h +4 h +4 h +185 h +4 h +10 h +8899 m +11 h +1 h +4 h +911 m +4 h +109 h +8900 m +278 h +8901 m +11 h +8902 m +8903 m +10 h +1 h +8904 m +295 h +4 h +4 h +8905 m +4132 m +1 h +1 h +307 h +4 h +8906 m +8907 m +170 h +4 h +8908 m +770 m +1 h +4 h +104 h +276 h +575 m +41 h +8909 m +10 h +8910 m +125 h +4 h +4 h +8911 m +10 h +353 m +11 h +1 h +10 h +7064 m +73 h +359 h +143 h +4 h +8912 m +1337 m +1 h +83 h +10 h +10 h +10 h +4 h +10 h +10 h +4 h +1 h +6135 m +1 h +79 h +1 h +4 h +8913 m +1250 h +4 h +10 h +10 h +256 m +4 h +36 h +64 h +2984 m +4 h +4 h +1 h +921 m +8914 m +4 h +1 h +4 h +316 m +10 h +4 h +4 h +4 h +123 h +8915 m +4 h +41 h +319 m +10 h +976 h +8793 m +4 h +125 h +1 h +640 h +10 h +10 h +8916 m +5060 m +8917 m +4 h +8918 m +10 h +8919 m +4 h +10 h +8920 m +1 h +4 h +4 h +57 h +11 h +124 h +8653 m +10 h +1 h +2131 m +1 h +10 h +10 h +8161 m +4 h +69 h +4 h +4 h +10 h +412 m +1 h +1 h +1 h +8921 m +8922 m +1 h +8923 m +59 h +258 h +1 h +57 h +1 h +124 h +1 h +186 h +77 h +4 h +3 h +4 h +10 h +4 h +12 h +13 h +10 h +5801 m +4 h +8924 m +4 h +4 h +4 h +1 h +10 h +173 h +386 h +10 h +56 h +4 h +888 m +10 h +167 h +5923 m +10 h +1 h +31 h +10 h +1 h +8925 m +8926 m +3 h +10 h +82 h +1 h +8927 m +2923 m +10 h +1 h +1 h +4 h +25 h +57 h +4 h +4 h +10 h +1 h +4 h +11 h +1 h +8928 m +4 h +2520 m +8929 m +8930 m +59 h +4 h +8931 m +8932 m +1 h +10 h +109 h +10 h +1 h +229 h +56 h +8933 m +4 h +1 h +11 h +1 h +4 h +125 h +10 h +4 h +10 h +8934 m +1772 h +4 h +4 h +4 h +10 h +4 h +1 h +196 h +1 h +147 h +10 h +1 h +10 h +1 h +10 h +4 h +1 h +3100 m +8935 m +1 h +146 h +8936 m +8937 m +5 h +8938 m +8939 m +4 h +1 h +4 h +4 h +698 m +4 h +4 h +10 h +1 h +1 h +295 h +4 h +146 h +123 h +11 h +103 m +8940 m +82 h +4 h +8941 m +536 h +4 h +8942 m +8943 m +8944 m +266 h +1 h +10 h +8945 m +10 h +8946 m +4301 m +73 h +3025 m +31 h +1 h +276 h +146 h +319 h +10 h +59 h +1 h +4 h +1 h +10 h +10 h +1 h +4 h +8947 m +4 h +4 h +8948 m +4 h +10 h +8949 m +8950 m +156 h +25 h +4 h +1201 m +8951 m +10 h +31 h +10 h +1137 h +8952 m +8953 m +684 m +238 h +4 h +22 h +11 h +4 h +10 h +8954 m +8955 m +4 h +10 h +4 h +4 h +327 m +1 h +8956 m +40 h +4 h +2494 m +92 h +8957 m +1620 m +190 h +265 h +11 h +8958 m +124 h +10 h +8959 m +4 h +83 h +169 h +8960 m +4 h +733 m +25 h +10 h +1 h +1 h +224 h +94 h +1 h +8961 m +4 h +1 h +4469 m +109 h +1 h +359 h +8962 m +8963 m +59 h +8964 m +108 h +6399 m +5965 m +986 h +10 h +31 h +1 h +4 h +278 h +11 h +69 h +8965 m +1470 h +10 h +869 h +10 h +8966 m +64 h +10 h +8967 m +10 h +4 h +172 h +4 h +4 h +6592 m +10 h +3396 m +8968 m +7839 h +8969 m +57 h +10 h +4 h +23 h +10 h +583 m +1409 m +181 h +8970 m +319 h +8971 m +8972 m +1 h +4 h +59 h +4 h +8973 m +1 h +1 h +10 h +10 h +4 h +1 h +55 h +4 h +1 h +8974 m +8975 m +1 h +4 h +8976 m +10 h +57 h +167 h +10 h +55 h +10 h +4 h +265 h +13 h +10 h +1 h +1 h +1638 m +8977 m +4 h +109 h +8978 m +10 h +4 h +1370 m +10 h +4 h +8979 m +10 h +1 h +8980 m +1 h +3 h +4 h +8981 m +10 h +10 h +488 h +1 h +4 h +4 h +4 h +1 h +10 h +4 h +10 h +4 h +687 h +1 h +8982 m +10 h +124 h +1685 h +8983 m +4 h +10 h +1 h +8984 m +1 h +112 h +8985 m +8986 m +143 h +267 m +10 h +74 h +1 h +8324 m +10 h +1 h +11 h +1 h +10 h +56 h +55 h +82 h +4 h +83 h +27 h +1 h +8987 m +1138 m +125 h +140 h +8988 m +1 h +10 h +104 h +8989 m +73 h +4 h +1 h +1271 m +1 h +10 h +4 h +10 h +4 h +1 h +1620 h +4 h +4 h +1 h +319 h +1 h +103 m +1 h +10 h +11 h +10 h +464 h +11 h +8990 m +109 h +59 h +195 h +10 h +57 h +124 h +4 h +1 h +10 h +4 h +2733 h +6066 m +8991 m +57 h +488 h +57 h +10 h +185 h +8992 m +164 h +8993 m +518 m +4 h +860 h +1 h +4 h +64 h +1 h +129 h +443 h +3 h +1 h +4 h +4 h +8994 m +10 h +10 h +8995 m +1714 m +4 h +1 h +8996 m +79 h +1 h +464 h +4 h +59 h +4 h +4 h +4 h +1 h +5 h +10 h +10 h +57 h +8997 m +10 h +4 h +8998 m +10 h +986 h +245 m +1 h +10 h +8999 m +4 h +185 h +8716 m +1 h +104 h +119 h +9000 m +1 h +196 h +10 h +9001 m +82 h +9002 m +1 h +172 h +125 h +146 h +10 h +278 h +4 h +10 h +9003 m +4 h +9004 m +10 h +65 h +9005 m +9006 m +9007 m +10 h +4 h +1 h +156 h +104 h +4 h +25 h +1 h +1 h +45 h +1 h +79 h +767 m +10 h +1016 h +1 h +3 h +11 h +9008 m +9009 m +520 h +9010 m +9011 m +11 h +10 h +9012 m +9013 m +1 h +92 h +169 h +4 h +4 h +9014 m +9015 m +4 h +10 h +4 h +4 h +1 h +4 h +1 h +59 h +9016 m +4 h +204 h +4 h +123 h +9017 m +1 h +74 h +41 h +4 h +4 h +4 h +1822 h +1 h +1 h +10 h +9018 m +4 h +4 h +9019 m +533 h +4 h +185 h +129 h +4 h +195 h +196 h +167 h +4 h +10 h +9020 m +10 h +4 h +4 h +1 h +1 h +4 h +4381 m +1 h +9021 m +377 m +4 h +22 h +332 h +10 h +692 h +10 h +9022 m +425 m +9023 m +1 h +399 h +9024 m +1 h +4 h +12 h +1 h +146 h +1 h +4 h +4 h +9025 m +1 h +79 h +4151 m +463 m +4 h +1 h +1127 m +9026 m +4 h +4 h +2480 m +9027 m +1 h +192 h +10 h +10 h +4 h +9028 m +10 h +6112 m +10 h +3 h +4 h +9029 m +4 h +9030 m +9031 m +92 h +1 h +1 h +10 h +10 h +10 h +1 h +9032 m +9033 m +9034 m +285 m +1 h +11 h +4 h +1056 m +124 h +22 h +1249 m +4 h +4 h +1 h +4 h +4 h +10 h +124 h +9035 m +4 h +1 h +1 h +147 h +1 h +1 h +9036 m +976 h +55 h +10 h +4 h +4 h +1548 m +79 h +3 h +4 h +1 h +1 h +108 h +9037 m +10 h +1 h +1 h +9038 m +4 h +4 h +4 h +10 h +97 h +1 h +9039 m +4 h +1 h +9040 m +9041 m +403 h +9042 m +4240 m +1 h +57 h +4 h +10 h +265 h +169 h +9043 m +10 h +463 m +9044 m +1 h +1137 h +1 h +1218 m +9045 m +9046 m +2931 m +10 h +124 h +9047 m +9048 m +3177 m +9049 m +10 h +9050 m +11 h +1 h +4 h +4 h +1 h +74 h +1 h +9051 m +258 h +9052 m +4 h +368 h +10 h +9053 m +9054 m +4 h +129 h +1 h +4 h +4 h +1 h +353 m +3 h +1 h +10 h +4 h +9055 m +4 h +10 h +3533 m +9056 m +3943 m +9057 m +9058 m +1884 m +10 h +10 h +443 h +10 h +9059 m +1 h +1 h +1 h +1691 m +9060 m +4 h +4 h +10 h +104 h +9061 m +1 h +9062 m +10 h +11 h +332 h +9063 m +82 h +3913 m +4 h +10 h +1 h +5 h +1 h +9064 m +1 h +266 h +9065 m +1835 h +897 m +9066 m +139 h +9067 m +9068 m +204 h +172 h +74 h +1 h +9069 m +4 h +9070 m +57 h +1 h +9071 m +9072 m +11 h +11 h +4 h +4 h +3 h +156 h +10 h +9073 m +9074 m +12 h +250 h +9075 m +2706 m +10 h +74 h +4 h +4 h +1 h +41 h +10 h +10 h +1105 h +11 h +10 h +4 h +190 h +1 h +9076 m +48 h +103 h +4 h +25 h +74 h +262 h +9077 m +9078 m +4 h +195 h +1 h +9079 m +4 h +9080 m +4 h +9081 m +10 h +158 h +4 h +10 h +403 h +1 h +4 h +601 h +1 h +9082 m +10 h +1 h +359 h +1 h +11 h +192 h +1 h +1 h +2374 m +9083 m +9084 m +1 h +7585 m +569 h +9085 m +1 h +3 h +2617 m +4 h +109 h +146 h +9086 m +59 h +1619 h +9087 m +4 h +10 h +4 h +164 h +1 h +4 h +9088 m +9089 m +79 h +1 h +8477 m +4 h +65 h +10 h +4 h +9090 m +119 h +4 h +25 h +9091 m +976 h +1 h +578 h +11 h +82 h +10 h +4 h +4 h +59 h +1 h +1 h +9092 m +4 h +4 h +10 h +119 h +124 h +97 h +4 h +9093 m +4 h +9094 m +4 h +4 h +65 h +9095 m +10 h +9096 m +9097 m +123 h +143 h +3558 h +9098 m +10 h +4 h +41 h +82 h +9099 m +4 h +1 h +4 h +119 h +2054 m +1737 m +40 h +9100 m +1 h +9101 m +1 h +124 h +4 h +1 h +11 h +1 h +1 h +4 h +1 h +656 h +1308 m +1 h +1 h +258 h +9102 m +167 h +869 h +109 h +9103 m +1957 m +2281 m +57 h +9104 m +8 h +109 h +6549 m +10 h +11 h +4 h +10 h +195 h +4 h +1261 h +1685 h +9105 m +4 h +82 h +3558 h +9106 m +169 h +4 h +4 h +6941 m +10 h +1 h +4 h +1 h +4 h +9107 m +27 h +195 h +10 h +146 h +11 h +4 h +4 h +4 h +9108 m +10 h +9109 m +1 h +4 h +4 h +9110 m +10 h +10 h +4 h +82 h +11 h +9111 m +10 h +4 h +9112 m +4 h +4 h +9113 m +9114 m +332 h +119 h +10 h +4 h +10 h +9115 m +9116 m +4 h +31 h +1 h +10 h +4 h +1 h +1 h +278 h +4 h +4 h +9117 m +1 h +1 h +9118 m +110 h +4 h +319 h +9119 m +1 h +40 h +1 h +368 h +109 h +278 h +278 h +4 h +4 h +4 h +169 h +823 m +82 h +9120 m +4 h +1 h +10 h +25 h +9121 m +112 h +1 h +10 h +1835 h +4 h +146 h +4 h +295 h +10 h +9122 m +11 h +4 h +77 h +9123 m +9124 m +74 h +203 m +1409 m +27 h +27 h +5053 m +4 h +9125 m +4 h +9126 m +4 h +109 h +9127 m +10 h +9128 m +2205 m +4 h +10 h +258 h +25 h +110 h +9129 m +9130 m +4 h +1796 h +4 h +4 h +478 m +1074 h +359 h +12 h +10 h +1 h +10 h +9131 m +1541 m +9132 m +11 h +9133 m +4 h +976 h +4 h +307 h +10 h +11 h +1 h +297 h +9134 m +10 h +11 h +9135 m +1 h +9136 m +4 h +13 h +4 h +9137 m +10 h +109 h +10 h +1 h +4 h +82 h +10 h +1 h +4 h +9138 m +1 h +4 h +4 h +9139 m +1 h +4 h +118 h +1 h +147 h +4 h +3995 m +9140 m +9141 m +9142 m +4 h +9143 m +109 h +4 h +273 m +11 h +9144 m +9145 m +1 h +4 h +10 h +1766 h +9146 m +1 h +9147 m +114 h +4 h +4 h +170 h +1 h +4 h +9148 m +4 h +25 h +9149 m +10 h +114 h +9150 m +45 h +4 h +9151 m +9152 m +31 h +10 h +164 h +75 h +10 h +1 h +718 h +4 h +9153 m +1 h +9154 m +1 h +9155 m +10 h +538 h +10 h +1 h +9156 m +10 h +1 h +4 h +1685 h +83 h +4 h +9157 m +74 h +10 h +1 h +73 h +143 h +1 h +9158 m +10 h +4 h +1 h +9159 m +1 h +10 h +4 h +57 h +113 h +4 h +41 h +56 h +9160 m +13 h +10 h +4 h +4 h +1 h +195 h +265 h +2002 m +12 h +74 h +10 h +4 h +4 h +10 h +266 h +4 h +4 h +1 h +230 h +4 h +9161 m +1 h +1 h +4 h +9162 m +9163 m +230 h +11 h +8638 m +10 h +4 h +4 h +10 h +9164 m +4 h +9165 m +843 m +9166 m +1 h +4 h +10 h +4 h +9167 m +687 h +1 h +4 h +1 h +11 h +9168 m +10 h +110 h +28 h +31 h +82 h +9169 m +147 h +9170 m +10 h +1016 h +11 h +1 h +9171 m +1 h +4 h +9172 m +2824 m +1 h +143 h +9173 m +1 h +4 h +4 h +10 h +8 h +9174 m +10 h +57 h +94 h +1822 h +9175 m +4 h +4 h +4 h +4 h +4 h +9176 m +1 h +4 h +9177 m +258 h +224 h +4 h +9178 m +1 h +9179 m +258 h +4 h +4 h +307 h +123 h +9180 m +124 h +123 h +1 h +9181 m +4 h +186 h +4 h +64 h +1303 m +1 h +59 h +1 h +106 h +10 h +383 h +4 h +4 h +9182 m +10 h +4 h +9183 m +9184 m +65 h +4538 m +1 h +4 h +1884 m +3 h +9185 m +3555 m +4 h +31 h +1 h +4 h +11 h +9186 m +74 h +139 h +5976 m +9187 m +185 h +4 h +9188 m +9189 m +83 h +4 h +9190 m +4 h +9191 m +31 h +258 h +59 h +9192 m +4 h +1 h +1 h +1 h +57 h +1 h +10 h +103 h +31 h +9193 m +1 h +578 h +3 h +172 h +4 h +4 h +82 h +4 h +9194 m +82 h +4 h +9195 m +10 h +11 h +9196 m +4 h +1 h +9197 m +10 h +173 h +4 h +124 h +2815 m +4 h +4966 m +10 h +4 h +10 h +282 m +104 h +9198 m +1 h +11 h +4 h +9199 m +1 h +56 h +4 h +1 h +238 h +4 h +9200 m +9201 m +1 h +5230 m +9202 m +1 h +1 h +1 h +1 h +4 h +4 h +7394 m +4 h +4 h +4 h +1 h +4 h +477 m +620 m +36 h +1 h +9203 m +403 h +468 h +41 h +278 h +1 h +241 m +135 h +4 h +801 m +10 h +2984 m +692 h +1 h +10 h +3 h +9204 m +59 h +10 h +10 h +10 h +9205 m +4 h +124 h +9206 m +9207 m +3 h +1 h +4 h +9208 m +4 h +1 h +9209 m +167 h +31 h +3 h +12 h +4 h +258 h +109 h +4 h +9210 m +146 h +4 h +10 h +1 h +9211 m +10 h +10 h +9212 m +4 h +9213 m +9214 m +1 h +798 m +118 h +4 h +31 h +109 h +10 h +4 h +10 h +1 h +4 h +1 h +9215 m +13 h +9216 m +1 h +1027 m +4 h +289 h +9217 m +119 h +10 h +192 h +10 h +1 h +4 h +1 h +6869 m +73 h +4 h +25 h +57 h +4 h +9218 m +11 h +1 h +9219 m +1 h +10 h +1 h +195 h +536 h +4 h +10 h +94 h +1 h +190 h +55 h +11 h +9220 m +4 h +464 h +74 h +1 h +9221 m +4 h +10 h +224 h +4 h +9222 m +59 h +1 h +4 h +10 h +4 h +1 h +9223 m +4 h +109 h +8221 m +4 h +9224 m +48 h +3 h +1 h +9225 m +9226 m +10 h +4 h +4 h +1772 h +129 h +4 h +123 h +3707 m +10 h +10 h +92 h +4 h +1 h +79 h +1 h +9227 m +108 h +1 h +10 h +4 h +1759 m +359 h +4 h +10 h +9228 m +4 h +4 h +976 h +1 h +174 h +9229 m +9230 m +4 h +4 h +4 h +1 h +56 h +9231 m +10 h +1 h +4 h +9232 m +9233 m +9234 m +4 h +9235 m +4 h +4 h +9236 m +3562 m +31 h +69 h +1 h +9237 m +4 h +9238 m +5141 m +4 h +4 h +10 h +4 h +9239 m +1 h +9240 m +9241 m +4 h +9242 m +65 h +73 h +167 h +570 h +3 h +4 h +9243 m +129 h +4 h +5523 m +9244 m +4 h +79 h +4 h +170 h +77 h +9245 m +10 h +1 h +1 h +10 h +25 h +4 h +11 h +9246 m +10 h +4 h +170 h +9247 m +1 h +626 h +25 h +1 h +4 h +1 h +124 h +9248 m +110 h +41 h +1619 h +9249 m +9250 m +4 h +124 h +4 h +3396 m +65 h +1 h +10 h +9251 m +614 m +4 h +56 h +144 h +9252 m +9253 m +1 h +4 h +10 h +1 h +1 h +3799 m +307 h +57 h +9254 m +1 h +1 h +1 h +173 h +9255 m +4 h +4 h +1772 h +109 h +1 h +1 h +82 h +4 h +109 h +10 h +124 h +114 h +9256 m +4 h +4 h +10 h +4 h +9257 m +4 h +4 h +228 m +1 h +12 h +9258 m +4 h +4 h +9259 m +9260 m +82 h +1375 m +319 h +4 h +4 h +59 h +9261 m +278 h +10 h +4 h +109 h +371 h +9262 m +1 h +112 h +114 h +9263 m +1 h +4 h +104 h +9264 m +10 h +1 h +4 h +9265 m +83 h +8179 m +4 h +1 h +1 h +129 h +4 h +4 h +1 h +9266 m +1955 m +4 h +9267 m +9268 m +10 h +4 h +10 h +170 h +9269 m +4 h +1 h +1 h +4 h +10 h +45 h +4 h +4 h +4 h +266 h +1 h +4 h +124 h +1 h +27 h +1 h +10 h +83 h +1 h +4 h +158 h +8 h +1 h +10 h +9270 m +3 h +4 h +1 h +10 h +1 h +48 h +9271 m +10 h +970 m +1 h +25 h +332 h +692 h +1 h +536 h +250 h +83 h +158 h +4 h +11 h +1 h +10 h +4 h +9272 m +381 m +11 h +1 h +109 h +9273 m +1 h +1 h +642 m +9274 m +11 h +1 h +9275 m +4 h +4 h +1 h +4 h +1 h +4 h +3 h +9276 m +1 h +27 h +1 h +4 h +1 h +9277 m +9278 m +1016 h +4 h +9279 m +104 h +10 h +4 h +74 h +1 h +10 h +9280 m +10 h +1 h +1 h +297 h +1 h +4 h +10 h +9281 m +4 h +1337 m +10 h +82 h +4 h +1 h +1 h +9282 m +1 h +9283 m +5809 m +10 h +4 h +10 h +779 h +276 h +371 h +935 h +9284 m +9285 m +46 h +1 h +4 h +10 h +1 h +10 h +4 h +9286 m +1 h +31 h +4 h +1766 h +146 h +313 h +190 h +9287 m +4 h +10 h +10 h +109 h +10 h +135 h +1 h +195 h +94 h +1 h +31 h +4 h +10 h +4 h +4 h +184 h +1 h +123 h +4 h +463 h +9288 m +10 h +4 h +1 h +935 h +4 h +28 h +83 h +1 h +4 h +4 h +4 h +9289 m +4 h +9290 m +123 h +82 h +1 h +1 h +1 h +10 h +4 h +11 h +4 h +59 h +9291 m +11 h +10 h +11 h +9292 m +4 h +125 h +10 h +4 h +146 h +9293 m +10 h +31 h +97 h +41 h +41 h +1 h +1 h +25 h +25 h +1 h +4 h +9294 m +1 h +4 h +11 h +55 h +3 h +1 h +9295 m +4 h +1 h +158 h +229 h +11 h +4 h +4 h +10 h +4 h +4 h +4 h +114 h +9296 m +10 h +9297 m +10 h +59 h +278 h +9298 m +1 h +4 h +64 h +4 h +9299 m +114 h +229 h +278 h +1 h +9300 m +169 h +140 h +10 h +9301 m +4 h +1 h +1556 m +1 h +1 h +238 h +1 h +3 h +25 h +9302 m +10 h +10 h +1 h +1 h +9303 m +1 h +10 h +10 h +4 h +4 h +1 h +10 h +1 h +1191 m +10 h +79 h +4 h +92 h +41 h +27 h +3533 m +9304 m +114 h +13 h +4 h +1 h +2625 h +41 h +368 h +3 h +9305 m +10 h +1 h +9306 m +1 h +73 h +4 h +124 h +9307 m +69 h +10 h +4 h +4 h +1 h +83 h +4 h +4 h +10 h +11 h +307 h +358 h +536 h +464 h +31 h +976 h +1030 h +1 h +10 h +4 h +83 h +4 h +123 h +1 h +10 h +59 h +4 h +1 h +1 h +31 h +4 h +190 h +10 h +3 h +1 h +25 h +4 h +9308 m +224 h +1 h +9309 m +1 h +1 h +10 h +332 h +9310 m +4 h +4 h +10 h +1 h +4 h +1886 m +65 h +9311 m +106 h +124 h +195 h +10 h +4 h +569 h +10 h +4 h +1 h +1 h +2592 m +966 m +3 h +4 h +124 h +386 h +13 h +9312 m +9313 m +41 h +1 h +1 h +1 h +4 h +113 h +113 h +57 h +139 h +124 h +9314 m +69 h +1089 m +9315 m +9316 m +8 h +2184 m +4 h +9317 m +10 h +9318 m +11 h +1 h +1 h +256 m +13 h +10 h +11 h +9319 m +8 h +11 h +9320 m +307 h +4 h +55 h +139 h +1 h +1 h +4 h +2522 m +135 h +4 h +885 m +4 h +41 h +10 h +10 h +10 h +4 h +11 h +9321 m +9322 m +9323 m +4 h +9293 m +9324 m +9325 m +157 h +443 h +4 h +1 h +11 h +1 h +45 h +10 h +10 h +443 h +55 h +146 h +4 h +1 h +185 h +4 h +4 h +1642 h +4718 m +1 h +4 h +4 h +10 h +10 h +4 h +9326 m +1 h +97 h +10 h +258 h +377 m +10 h +73 h +1 h +125 h +125 h +74 h +11 h +9327 m +10 h +9328 m +4 h +9329 m +489 m +423 m +83 h +25 h +45 h +83 h +55 h +9330 m +94 h +10 h +9331 m +1 h +9332 m +4 h +9333 m +4 h +1822 h +238 h +157 h +10 h +10 h +4 h +10 h +123 h +9334 m +10 h +1 h +10 h +10 h +1 h +4 h +124 h +4 h +4 h +11 h +1 h +4 h +4 h +4 h +4 h +1 h +10 h +56 h +11 h +123 h +10 h +25 h +109 h +2769 m +1 h +9335 m +9336 m +124 h +1 h +124 h +4 h +109 h +4 h +4 h +1 h +1 h +4 h +4 h +270 m +11 h +1 h +266 h +10 h +4 h +10 h +1 h +9337 m +73 h +9338 m +1 h +4 h +4 h +9339 m +119 h +4 h +1 h +4 h +9340 m +9341 m +2425 m +146 h +2720 m +10 h +4 h +9342 m +4 h +4 h +4 h +9343 m +9344 m +367 m +9345 m +1 h +9346 m +4 h +4 h +9027 m +196 h +4 h +9347 m +1 h +84 h +1650 h +10 h +104 h +9348 m +4 h +2625 h +4 h +4 h +4 h +4 h +4 h +2733 h +1 h +146 h +4 h +109 h +9349 m +9350 m +4 h +114 h +9351 m +8 h +123 h +10 h +4 h +9352 m +4 h +4 h +1 h +10 h +3742 m +1 h +4 h +4 h +4 h +4 h +4 h +4 h +1 h +129 h +1 h +1 h +9353 m +4 h +1 h +124 h +170 h +9354 m +167 h +1 h +10 h +11 h +10 h +28 h +65 h +9355 m +9356 m +4 h +1 h +83 h +1 h +4 h +10 h +109 h +10 h +1 h +1 h +4 h +1 h +91 h +9357 m +9358 m +9359 m +1205 m +10 h +9360 m +9361 m +1 h +4 h +9362 m +9363 m +82 h +3 h +112 h +1 h +9364 m +82 h +10 h +1 h +5557 m +1 h +4 h +9365 m +4 h +4 h +9366 m +1 h +299 h +9367 m +9368 m +9007 m +10 h +447 h +1 h +10 h +4 h +9369 m +2281 m +965 m +10 h +1 h +601 h +4 h +9370 m +9371 m +737 m +4 h +10 h +9372 m +9373 m +9374 m +4 h +692 h +1 h +1 h +4 h +3 h +4 h +4 h +1 h +10 h +10 h +1 h +9375 m +124 h +4 h +12 h +1 h +9376 m +1 h +12 h +1 h +1 h +9377 m +1 h +9378 m +9379 m +9380 m +9381 m +4 h +4 h +185 h +9382 m +114 h +912 m +4 h +135 h +41 h +45 h +190 h +1 h +10 h +4 h +4 h +10 h +4 h +10 h +9383 m +10 h +1 h +538 h +9384 m +114 h +270 m +4 h +59 h +4 h +4 h +3341 m +1 h +4 h +4 h +4 h +4 h +4 h +4 h +4 h +1 h +9385 m +9386 m +282 m +82 h +1 h +124 h +1 h +488 h +11 h +4 h +83 h +1 h +1 h +1847 m +172 h +9387 m +9388 m +83 h +1 h +9389 m +9 m +10 h +4 h +4 h +167 h +94 h +10 h +1 h +4 h +9390 m +4 h +170 h +4 h +1 h +10 h +2391 m +9391 m +10 h +10 h +109 h +4 h +1 h +83 h +7253 m +125 h +4 h +9392 m +167 h +1105 h +8002 m +9393 m +11 h +195 h +4 h +10 h +4 h +1 h +109 h +9394 m +4 h +124 h +10 h +4 h +996 m +1 h +10 h +143 h +9395 m +8 h +10 h +278 h +4 h +12 h +1 h +4441 m +4 h +9396 m +9397 m +1 h +9398 m +1 h +10 h +9399 m +104 h +10 h +687 h +125 h +939 m +83 h +4 h +190 h +3622 m +4367 m +9400 m +1 h +11 h +4 h +9401 m +4 h +9402 m +295 h +9403 m +464 h +1 h +10 h +74 h +1 h +4378 m +123 h +10 h +10 h +447 h +4 h +104 h +195 h +25 h +332 h +1 h +9404 m +84 h +10 h +59 h +4 h +1 h +10 h +97 h +59 h +10 h +4 h +9405 m +1 h +140 h +135 h +7792 m +59 h +9406 m +1 h +4 h +113 h +10 h +4 h +4 h +4 h +9407 m +9408 m +3 h +1 h +9409 m +976 h +9410 m +9411 m +4 h +10 h +278 h +4 h +1 h +4 h +1445 m +10 h +9412 m +9413 m +4 h +10 h +4 h +295 h +9414 m +104 h +590 h +266 h +1 h +1 h +9415 m +196 h +1 h +4 h +1 h +10 h +4 h +125 h +1 h +4 h +4 h +4 h +1 h +10 h +92 h +1 h +1 h +25 h +10 h +238 h +464 h +10 h +97 h +4 h +1 h +11 h +1 h +9416 m +9417 m +11 h +4 h +1 h +9 m +10 h +9418 m +4 h +11 h +10 h +4 h +65 h +4 h +4 h +82 h +1284 m +10 h +10 h +1 h +147 h +9419 m +9420 m +4 h +57 h +4 h +4 h +4 h +10 h +4 h +10 h +9421 m +9422 m +9423 m +506 h +10 h +11 h +10 h +9424 m +1 h +10 h +9425 m +10 h +1 h +1 h +9426 m +9427 m +9428 m +172 h +1 h +83 h +1 h +4 h +4 h +1 h +157 h +4 h +1 h +4 h +4 h +10 h +9429 m +1650 h +9430 m +9431 m +4 h +4 h +4 h +124 h +9432 m +9433 m +36 h +4 h +57 h +185 h +10 h +109 h +1 h +808 m +9434 m +1 h +124 h +9435 m +10 h +262 h +31 h +4 h +195 h +10 h +1 h +278 h +147 h +313 h +65 h +109 h +9436 m +109 h +4 h +4 h +1 h +1 h +976 h +36 h +1 h +6549 m +13 h +9437 m +1 h +10 h +4 h +10 h +1 h +4 h +4 h +9438 m +9439 m +9440 m +9441 m +1 h +1 h +10 h +9442 m +4 h +4320 m +1 h +41 h +9443 m +190 h +1 h +1 h +10 h +3 h +9444 m +12 h +9445 m +4 h +1 h +1 h +9446 m +46 h +9447 m +278 h +10 h +1 h +4 h +9448 m +1 h +1 h +368 h +9449 m +59 h +156 h +5 h +4 h +41 h +9450 m +9451 m +9452 m +9453 m +4 h +4 h +1 h +383 h +45 h +4 h +31 h +10 h +4 h +92 h +4 h +11 h +172 h +9454 m +10 h +9455 m +59 h +1 h +10 h +124 h +9456 m +196 h +8133 m +9457 m +1 h +12 h +41 h +4 h +195 h +757 h +9458 m +40 h +1 h +10 h +9459 m +989 m +1 h +1470 h +4 h +1 h +65 h +25 h +9460 m +31 h +1780 m +83 h +27 h +10 h +1 h +10 h +11 h +1 h +4 h +4 h +479 m +41 h +4 h +1 h +9461 m +10 h +195 h +8 h +9462 m +4 h +4 h +1261 h +1 h +4 h +4 h +10 h +10 h +9463 m +4 h +1535 m +4 h +10 h +1 h +9464 m +9465 m +4 h +4 h +4 h +4 h +1 h +4 h +1 h +9466 m +10 h +4 h +181 h +9467 m +1 h +4 h +9468 m +8 h +11 h +3 h +4 h +124 h +83 h +4 h +4 h +1 h +10 h +1 h +25 h +4 h +139 h +10 h +92 h +1 h +4 h +181 h +9469 m +9470 m +104 h +10 h +9471 m +57 h +9472 m +9473 m +36 h +258 h +10 h +4 h +9474 m +9475 m +1 h +9476 m +8 h +9477 m +1 h +13 h +10 h +9478 m +31 h +1 h +10 h +9479 m +4 h +9480 m +9481 m +83 h +9482 m +4 h +65 h +1 h +114 h +9483 m +57 h +1 h +1 h +4 h +1 h +9484 m +10 h +12 h +1 h +9485 m +4 h +1 h +4 h +83 h +83 h +10 h +238 h +10 h +1780 m +4 h +9486 m +82 h +9487 m +4 h +4 h +31 h +6869 m +1 h +3 h +195 h +279 h +9488 m +1 h +9489 m +569 h +9490 m +10 h +4 h +4 h +1 h +11 h +164 h +9491 m +10 h +59 h +1 h +124 h +11 h +4538 m +10 h +9492 m +1 h +4 h +359 h +124 h +238 h +9493 m +4 h +9494 m +9495 m +9496 m +91 h +82 h +9497 m +1 h +4 h +1 h +65 h +3 h +1 h +73 h +1 h +4 h +9498 m +4 h +146 h +31 h +11 h +10 h +1261 h +124 h +41 h +9499 m +1 h +10 h +83 h +124 h +9500 m +112 h +8533 m +1 h +4 h +1 h +83 h +4 h +55 h +4 h +10 h +4 h +1 h +9501 m +112 h +4 h +1 h +9502 m +1828 m +82 h +2278 m +185 h +11 h +538 h +104 h +4 h +4 h +1 h +9503 m +1642 h +4 h +10 h +518 m +114 h +4 h +4 h +9504 m +4 h +1646 m +103 h +1 h +57 h +1 h +4 h +83 h +41 h +9505 m +4 h +9506 m +4 h +4 h +10 h +4 h +109 h +10 h +2124 m +41 h +4 h +170 h +1 h +36 h +10 h +125 h +1 h +10 h +9507 m +9508 m +1 h +12 h +1 h +276 h +4 h +10 h +74 h +73 h +9509 m +118 h +27 h +113 h +9510 m +4 h +36 h +4 h +25 h +4 h +1 h +266 h +1 h +5225 m +9511 m +4 h +1 h +4 h +9512 m +467 m +9513 m +12 h +2314 m +4 h +10 h +911 m +9514 m +10 h +79 h +68 m +10 h +11 h +2163 m +1 h +4 h +1 h +11 h +9515 m +10 h +1 h +276 h +9516 m +94 h +10 h +196 h +7924 m +83 h +1 h +10 h +278 h +9517 m +4 h +57 h +1 h +11 h +1 h +4 h +74 h +238 h +4 h +4 h +371 h +4 h +9518 m +9519 m +45 h +65 h +1 h +4 h +4 h +4 h +13 h +1 h +1 h +358 h +9520 m +9521 m +9522 m +10 h +9523 m +10 h +164 h +4 h +1 h +10 h +1 h +9524 m +10 h +11 h +10 h +464 h +1083 h +31 h +9525 m +9526 m +11 h +4 h +196 h +11 h +1 h +4 h +106 h +1 h +147 h +79 h +9527 m +1 h +1 h +82 h +10 h +4 h +2172 h +10 h +9528 m +74 h +1 h +9529 m +4 h +1 h +204 h +4 h +9530 m +7253 m +97 h +4 h +9531 m +4 h +9532 m +4 h +1 h +196 h +9533 m +4 h +185 h +1127 m +97 h +1 h +9534 m +4 h +4 h +1 h +1 h +4 h +250 h +4 h +9535 m +9536 m +1 h +10 h +8 h +25 h +11 h +9537 m +1 h +1116 m +97 h +9538 m +4 h +9539 m +11 h +4 h +9540 m +4 h +4 h +9541 m +31 h +10 h +9542 m +1309 m +82 h +1 h +9543 m +9544 m +114 h +8 h +9545 m +9546 m +140 h +170 h +10 h +10 h +4 h +4 h +1 h +4 h +7999 m +123 h +113 h +447 h +1478 h +9547 m +10 h +1 h +9548 m +31 h +1 h +4 h +27 h +4 h +36 h +4 h +4 h +1 h +4 h +1 h +4 h +9549 m +1 h +9550 m +9551 m +1650 h +1 h +25 h +4 h +10 h +10 h +10 h +195 h +169 h +4 h +9552 m +74 h +57 h +1 h +9553 m +25 h +297 h +1 h +5456 m +11 h +4127 m +196 h +4 h +9554 m +1 h +4 h +11 h +8555 m +4 h +1 h +1 h +1 h +9555 m +1 h +9556 m +4 h +8485 m +4 h +10 h +5 h +4 h +10 h +31 h +65 h +9557 m +9558 m +25 h +4 h +9559 m +10 h +9560 m +1 h +4 h +9561 m +10 h +4 h +4 h +9562 m +4 h +1 h +1 h +359 h +4 h +31 h +9563 m +1 h +4257 m +4 h +4 h +172 h +9564 m +3 h +164 h +976 h +4 h +10 h +10 h +9565 m +125 h +22 h +9566 m +1 h +4 h +91 h +10 h +3768 m +4 h +1627 m +578 h +10 h +4 h +1 h +1 h +9567 m +4 h +10 h +2794 m +31 h +41 h +1 h +1 h +1 h +11 h +4 h +124 h +10 h +4 h +3095 m +1 h +91 h +174 h +276 h +4 h +9568 m +4 h +4 h +82 h +56 h +25 h +4 h +10 h +25 h +1 h +10 h +14 m +1 h +12 h +11 h +9569 m +1 h +147 h +59 h +2719 m +1 h +1 h +25 h +4 h +190 h +9570 m +4 h +170 h +4 h +1886 m +9571 m +478 m +9572 m +9573 m +4 h +1027 m +135 h +1 h +1 h +4 h +9574 m +10 h +2558 m +94 h +11 h +83 h +9575 m +4 h +40 h +9576 m +10 h +119 h +9577 m +1 h +258 h +94 h +820 m +7322 m +4 h +4 h +4 h +146 h +83 h +11 h +173 h +55 h +9578 m +41 h +97 h +1 h +104 h +4 h +1 h +10 h +4 h +4 h +874 m +4 h +1 h +9579 m +1 h +1 h +332 h +170 h +10 h +1 h +1 h +74 h +4 h +1493 m +10 h +9580 m +83 h +4 h +265 h +1454 m +9581 m +10 h +25 h +1 h +31 h +4 h +172 h +10 h +9582 m +4 h +195 h +1 h +1261 h +976 h +238 h +82 h +9583 m +4 h +1 h +1 h +9584 m +4 h +3704 m +1 h +4 h +11 h +986 h +1 h +1 h +332 h +11 h +1 h +1 h +93 h +8781 m +10 h +172 h +10 h +4 h +11 h +1 h +4 h +9585 m +9586 m +359 h +4 h +4 h +195 h +135 h +4177 m +4 h +146 h +11 h +11 h +4 h +48 h +9587 m +4 h +601 h +1 h +82 h +10 h +25 h +119 h +9588 m +124 h +9589 m +1201 m +4 h +1 h +1 h +4 h +601 h +11 h +82 h +91 h +6381 m +4 h +4616 m +10 h +4 h +135 h +10 h +119 h +2952 m +172 h +820 m +36 h +9590 m +4 h +9591 m +278 h +9592 m +4 h +1 h +125 h +9593 m +211 m +65 h +1 h +56 h +9594 m +4 h +4 h +1 h +10 h +4 h +4 h +1 h +167 h +9595 m +1 h +9596 m +10 h +1 h +1 h +4 h +9597 m +9598 m +9599 m +164 h +10 h +170 h +4 h +4 h +59 h +9600 m +4 h +11 h +1453 m +3068 m +74 h +4 h +4 h +11 h +1 h +4 h +4 h +1 h +3 h +4 h +124 h +4 h +10 h +4 h +97 h +1 h +557 m +9601 m +4 h +4 h +10 h +192 h +4 h +10 h +3 h +9602 m +9603 m +158 h +1 h +9604 m +4 h +2592 m +9605 m +1 h +11 h +9606 m +9607 m +82 h +307 h +4 h +9608 m +11 h +1 h +10 h +135 h +4 h +9609 m +10 h +1 h +10 h +82 h +9610 m +3 h +332 h +156 h +238 h +9611 m +4 h +196 h +104 h +1 h +4 h +3680 m +4 h +4 h +9612 m +1074 h +10 h +83 h +1 h +4 h +238 h +9613 m +1893 m +1261 h +4648 m +1 h +4 h +4 h +10 h +10 h +4 h +770 m +4 h +4 h +4 h +4 h +1 h +22 h +25 h +1 h +4 h +888 m +1 h +10 h +9614 m +4 h +9615 m +1 h +4 h +82 h +4 h +4 h +9616 m +23 h +4 h +11 h +1 h +1 h +4 h +1 h +8133 m +10 h +9617 m +10 h +3 h +1 h +9618 m +4 h +1 h +9619 m +92 h +9620 m +124 h +41 h +31 h +10 h +2984 m +10 h +4 h +3161 m +31 h +1 h +73 h +4 h +9621 m +10 h +4 h +10 h +10 h +82 h +10 h +146 h +4 h +181 h +4 h +109 h +330 m +1 h +1 h +4 h +1 h +1 h +1 h +41 h +1 h +1 h +10 h +1 h +1220 m +9622 m +11 h +9623 m +4 h +9624 m +1 h +4 h +57 h +4 h +1 h +976 h +1 h +55 h +9625 m +4 h +11 h +9626 m +196 h +9627 m +1 h +10 h +31 h +4 h +1 h +4 h +4 h +278 h +9628 m +1127 m +9629 m +196 h +3 h +3 h +1 h +31 h +4 h +4 h +3 h +11 h +1 h +4 h +1 h +10 h +9630 m +1260 m +403 h +12 h +9631 m +10 h +73 h +4 h +4 h +1 h +2844 m +278 h +124 h +119 h +4 h +31 h +11 h +4 h +9632 m +1 h +125 h +1 h +4 h +167 h +1 h +10 h +9633 m +295 h +1 h +9475 m +4 h +1 h +57 h +4 h +10 h +1 h +4 h +4 h +10 h +9634 m +10 h +1 h +4 h +4 h +4 h +10 h +238 h +57 h +1 h +10 h +1 h +9635 m +9636 m +146 h +1 h +9637 m +9638 m +1 h +10 h +4 h +1 h +4 h +4 h +10 h +9639 m +1 h +9640 m +11 h +258 h +4 h +41 h +3933 m +4 h +297 h +1 h +911 m +9641 m +1 h +11 h +4 h +124 h +3 h +1 h +196 h +4 h +10 h +2379 h +41 h +1 h +575 m +97 h +1 h +1 h +1 h +10 h +10 h +10 h +1 h +1 h +10 h +9642 m +57 h +82 h +4 h +9643 m +1 h +9644 m +810 m +9645 m +83 h +10 h +5470 m +10 h +1884 h +1 h +9646 m +9647 m +82 h +1 h +9648 m +4 h +1074 h +73 h +125 h +1030 h +9649 m +10 h +4 h +10 h +9650 m +3246 m +186 h +1 h +57 h +10 h +1884 h +1 h +1 h +687 h +9651 m +4 h +9652 m +10 h +147 h +4 h +4 h +1 h +97 h +1 h +4 h +5869 m +9653 m +1 h +9654 m +4 h +4 h +170 h +10 h +1 h +4 h +139 h +4 h +1677 m +2418 m +1 h +11 h +69 h +1 h +1250 h +4 h +4 h +69 h +10 h +4 h +1 h +1 h +9655 m +2878 m +4 h +9656 m +9657 m +146 h +4 h +1 h +1 h +125 h +9658 m +9659 m +4 h +4 h +250 h +4 h +1 h +138 h +9660 m +1 h +104 h +9661 m +4 h +9662 m +9663 m +9664 m +1 h +31 h +1 h +9665 m +1 h +1 h +238 h +10 h +11 h +1 h +10 h +156 h +4 h +677 m +1 h +9666 m +57 h +186 h +4 h +1574 m +83 h +9667 m +1 h +13 h +9668 m +9669 m +9670 m +1 h +11 h +10 h +4 h +1 h +9671 m +10 h +9672 m +10 h +11 h +9673 m +55 h +9674 m +987 m +10 h +4 h +9675 m +3847 m +4 h +11 h +9676 m +4 h +195 h +11 h +10 h +9677 m +371 h +9678 m +3 h +83 h +4 h +9679 m +25 h +10 h +9680 m +170 h +4 h +9681 m +4 h +6869 m +3 h +169 h +6599 m +1 h +9682 m +9683 m +4 h +124 h +569 h +4 h +9684 m +1 h +5917 h +79 h +4 h +3820 m +55 h +4 h +170 h +4 h +4 h +74 h +1024 m +9685 m +9686 m +9687 m +9688 m +4 h +1 h +10 h +1 h +11 h +9689 m +447 h +10 h +443 h +9690 m +10 h +9691 m +4 h +9692 m +7395 m +125 h +4 h +4 h +113 h +9693 m +4 h +9694 m +167 h +57 h +9695 m +11 h +9696 m +9697 m +939 m +10 h +9698 m +65 h +10 h +146 h +4 h +4 h +4 h +3 h +9699 m +4 h +13 h +36 h +9700 m +195 h +92 h +74 h +9701 m +124 h +41 h +4 h +1 h +25 h +9702 m +1 h +55 h +1 h +10 h +109 h +4 h +10 h +447 h +82 h +1 h +1 h +433 m +1116 m +264 m +4 h +266 h +99 m +4 h +59 h +4 h +203 m +10 h +146 h +3 h +4 h +9703 m +4 h +9704 m +297 h +1 h +27 h +10 h +10 h +150 m +403 h +6102 m +77 h +4 h +4000 m +2186 m +1 h +135 h +9705 m +1 h +83 h +4 h +1 h +4 h +1 h +72 m +9706 m +4 h +4 h +10 h +3841 m +4 h +56 h +4 h +1201 m +82 h +447 h +6963 m +4 h +1 h +4 h +4 h +172 h +11 h +1 h +124 h +211 m +10 h +4 h +7572 m +1 h +2374 m +1 h +57 h +4 h +11 h +4 h +4 h +10 h +4 h +143 h +1 h +93 h +1 h +4 h +77 h +1 h +4 h +9707 m +4 h +4 h +9708 m +41 h +4 h +1 h +9709 m +4 h +10 h +9710 m +258 h +4 h +9711 m +9712 m +4 h +4 h +94 h +11 h +4 h +10 h +258 h +4 h +4 h +4 h +59 h +9713 m +9714 m +1 h +10 h +1 h +9715 m +4 h +10 h +9716 m +82 h +1 h +9717 m +9718 m +10 h +4 h +538 h +9719 m +1 h +1 h +307 h +114 h +4 h +56 h +12 h +4 h +4 h +70 m +10 h +10 h +9720 m +9721 m +4 h +319 h +4 h +10 h +4 h +1 h +1 h +9722 m +4 h +10 h +2002 m +1 h +10 h +4 h +9723 m +59 h +1 h +9724 m +146 h +64 h +4 h +1 h +10 h +181 h +9725 m +110 h +9726 m +279 h +79 h +83 h +9727 m +10 h +10 h +10 h +1 h +110 h +383 h +9728 m +7394 m +1137 h +9729 m +10 h +9730 m +9731 m +4 h +279 h +295 h +1 h +10 h +11 h +3 h +10 h +10 h +10 h +9732 m +10 h +1 h +73 h +4 h +104 h +2475 m +169 h +10 h +11 h +10 h +9733 m +4 h +9734 m +963 m +4 h +9735 m +358 h +9736 m +9737 m +10 h +1 h +1 h +4 h +4 h +1 h +139 h +83 h +10 h +1 h +1253 m +9738 m +11 h +10 h +82 h +10 h +4 h +190 h +156 h +74 h +4 h +11 h +4 h +533 h +1 h +9739 m +1 h +4 h +4 h +9740 m +3 h +9741 m +146 h +97 h +57 h +74 h +1 h +25 h +31 h +9742 m +10 h +10 h +169 h +4 h +11 h +196 h +97 h +10 h +3494 m +9743 m +4 h +4 h +9744 m +92 h +4 h +4 h +10 h +181 h +11 h +73 h +256 m +9745 m +829 m +10 h +82 h +11 h +4 h +9746 m +112 h +1 h +4 h +82 h +1 h +1 h +9747 m +1 h +9748 m +250 h +4 h +4 h +10 h +10 h +41 h +9749 m +10 h +104 h +9750 m +170 h +195 h +9751 m +3 h +1 h +59 h +4 h +4 h +9752 m +9753 m +4 h +1 h +359 h +9754 m +4 h +169 h +1 h +10 h +4 h +9755 m +4 h +1 h +4 h +114 h +332 h +10 h +4 h +9756 m +1 h +4 h +11 h +185 h +9757 m +1403 m +4 h +4 h +82 h +4 h +10 h +447 h +10 h +258 h +31 h +4 h +109 h +10 h +41 h +77 h +9758 m +83 h +9759 m +186 h +1083 h +4 h +164 h +4 h +219 m +4 h +9760 m +94 h +1 h +221 m +4 h +1 h +443 h +4 h +4 h +4 h +8 h +170 h +25 h +74 h +4 h +1 h +4 h +10 h +12 h +9761 m +10 h +4 h +1 h +1790 h +10 h +4 h +9762 m +79 h +125 h +4 h +190 h +601 h +4 h +9763 m +9764 m +9765 m +1 h +4 h +9766 m +11 h +11 h +1 h +124 h +1884 h +9767 m +1 h +276 h +104 h +9768 m +358 h +10 h +10 h +1 h +9769 m +9770 m +4 h +1 h +4 h +533 h +4 h +83 h +2582 m +1 h +64 h +4 h +4 h +82 h +569 h +1 h +170 h +9771 m +82 h +9772 m +1 h +2418 m +1337 m +109 h +1 h +1 h +135 h +135 h +9773 m +229 h +1 h +2865 m +4 h +1 h +279 h +79 h +1 h +10 h +10 h +4 h +10 h +10 h +4 h +3561 m +4 h +6387 m +1470 h +69 h +4 h +304 m +4 h +687 h +9774 m +4 h +9775 m +9776 m +4 h +4 h +1 h +1 h +4 h +278 h +1 h +31 h +10 h +1 h +7641 m +9777 m +9778 m +1 h +9779 m +123 h +4 h +82 h +144 h +238 h +124 h +4 h +4 h +1 h +4 h +353 m +258 h +4 h +2184 m +265 h +1 h +489 m +1 h +124 h +10 h +10 h +83 h +9780 m +9781 m +11 h +10 h +10 h +1975 m +4 h +10 h +10 h +1 h +1 h +10 h +11 h +4 h +9782 m +109 h +9783 m +4 h +10 h +4 h +9784 m +59 h +4 h +4 h +1 h +9785 m +31 h +9786 m +110 h +9787 m +1 h +4 h +109 h +10 h +10 h +250 h +4 h +4702 m +4 h +5378 m +536 h +59 h +10 h +4 h +11 h +9788 m +1 h +4556 m +4 h +9789 m +9790 m +1 h +332 h +9791 m +4 h +4 h +4 h +9792 m +82 h +1 h +9793 m +1137 h +3 h +9794 m +10 h +9795 m +31 h +10 h +10 h +4 h +108 h +1 h +4 h +195 h +9796 m +1 h +1 h +4 h +143 h +1 h +520 h +9797 m +4 h +9798 m +4 h +986 h +1 h +9799 m +74 h +36 h +4 h +4 h +279 h +3704 m +4 h +4 h +10 h +3 h +9800 m +4 h +4 h +9801 m +4 h +10 h +31 h +9802 m +9803 m +4 h +238 h +10 h +9804 m +4 h +4 h +10 h +1 h +718 h +2101 m +1 h +1 h +2379 h +170 h +4 h +10 h +170 h +9805 m +10 h +147 h +172 h +1 h +9806 m +1 h +83 h +447 h +6197 m +9807 m +10 h +9808 m +4 h +591 m +123 h +4 h +1 h +265 h +9809 m +125 h +124 h +4 h +9810 m +10 h +10 h +119 h +4 h +82 h +1 h +339 m +368 h +403 h +9811 m +1955 m +8626 m +9812 m +4 h +250 h +69 h +4 h +109 h +1 h +82 h +1 h +4 h +289 h +192 h +10 h +278 h +9813 m +195 h +1 h +4 h +10 h +112 h +299 h +1 h +1406 h +10 h +1 h +4 h +73 h +1 h +12 h +9814 m +4 h +104 h +4 h +56 h +4 h +1 h +4 h +4 h +57 h +583 h +9815 m +4 h +65 h +82 h +170 h +4 h +9816 m +10 h +10 h +1 h +109 h +9817 m +22 h +447 h +10 h +4 h +4 h +4 h +10 h +9818 m +10 h +4 h +124 h +1 h +10 h +9819 m +65 h +57 h +1 h +4 h +4 h +1 h +11 h +4553 m +83 h +1 h +3369 m +278 h +196 h +10 h +82 h +10 h +109 h +4932 m +4 h +9820 m +10 h +1 h +1 h +10 h +195 h +10 h +4 h +1 h +4 h +1 h +4 h +9821 m +258 h +1030 h +4 h +4 h +9822 m +10 h +10 h +238 h +9823 m +9824 m +9825 m +1 h +270 h +1 h +10 h +629 m +10 h +109 h +1 h +195 h +10 h +9826 m +265 h +1 h +82 h +9827 m +9828 m +4320 m +60 m +1 h +114 h +4 h +1 h +109 h +10 h +9829 m +569 h +109 h +83 h +11 h +124 h +1 h +65 h +10 h +1 h +1201 h +238 h +22 h +10 h +6941 m +100 m +10 h +1 h +82 h +238 h +338 m +2148 m +1 h +9830 m +147 h +1 h +10 h +9831 m +195 h +31 h +92 h +9832 m +9833 m +10 h +4 h +4 h +196 h +278 h +9834 m +270 h +4 h +124 h +1117 m +278 h +9835 m +1 h +10 h +9836 m +108 h +1 h +1 h +4 h +1 h +11 h +9837 m +79 h +1 h +3 h +4 h +4 h +9838 m +1 h +10 h +4 h +1 h +1 h +1 h +4 h +10 h +4 h +4 h +1 h +57 h +4 h +4 h +1 h +4 h +10 h +9839 m +9840 m +94 h +4 h +83 h +59 h +4 h +114 h +10 h +4 h +9841 m +4 h +3 h +443 h +57 h +4 h +9842 m +1 h +1 h +3303 m +9843 m +1 h +196 h +104 h +9844 m +4 h +4 h +1 h +4 h +10 h +109 h +1 h +258 h +10 h +4 h +9845 m +4 h +10 h +83 h +1 h +10 h +10 h +9846 m +104 h +10 h +1 h +10 h +41 h +11 h +4 h +36 h +4 h +73 h +10 h +109 h +1 h +4 h +4 h +4 h +4 h +10 h +10 h +238 h +9847 m +10 h +10 h +1 h +4 h +57 h +9848 m +1 h +9849 m +10 h +9850 m +9851 m +4 h +10 h +488 h +92 h +10 h +196 h +41 h +9852 m +1 h +9853 m +4 h +1 h +4 h +1 h +399 h +4 h +25 h +10 h +4 h +4 h +4 h +1 h +8 h +1442 m +11 h +939 m +36 h +83 h +273 m +4 h +1 h +1 h +1 h +9854 m +4 h +1 h +1 h +3321 m +48 h +79 h +185 h +125 h +10 h +79 h +757 h +9855 m +173 h +9856 m +4 h +258 h +4 h +359 h +9857 m +4 h +10 h +4 h +10 h +1 h +1 h +4 h +9858 m +9859 m +1 h +82 h +9860 m +143 h +1016 h +9861 m +1 h +9862 m +10 h +9863 m +1835 h +31 h +4 h +9864 m +1 h +10 h +4 h +124 h +4 h +443 h +10 h +9865 m +10 h +10 h +4 h +156 h +238 h +40 h +9866 m +9867 m +4 h +3398 m +692 h +4 h +1 h +10 h +11 h +4 h +9868 m +9869 m +9870 m +1 h +31 h +4 h +9871 m +11 h +4 h +4 h +4 h +9872 m +7306 m +1 h +1 h +9873 m +10 h +55 h +185 h +10 h +1 h +10 h +1 h +1 h +4 h +10 h +10 h +196 h +10 h +73 h +4 h +6107 m +12 h +4 h +9874 m +59 h +8571 m +1 h +46 h +1 h +1 h +77 h +4 h +4 h +9875 m +9876 m +83 h +10 h +9877 m +1 h +1 h +10 h +10 h +9878 m +11 h +10 h +297 h +41 h +829 m +1 h +10 h +1 h +10 h +4 h +10 h +4 h +4 h +83 h +9879 m +8472 m +929 m +143 h +538 h +9880 m +1 h +97 h +12 h +4 h +1 h +3 h +278 h +156 h +13 h +10 h +4 h +1 h +4 h +4 h +238 h +64 h +125 h +1070 m +57 h +1 h +4 h +9881 m +4 h +10 h +11 h +10 h +11 h +108 h +1 h +1 h +9882 m +3111 m +3 h +82 h +9883 m +1068 m +1 h +4 h +9884 m +9885 m +869 h +332 h +4 h +10 h +4 h +9886 m +57 h +10 h +4 h +1 h +9887 m +4 h +69 h +1 h +4 h +9888 m +9889 m +2720 m +10 h +1 h +10 h +9890 m +4 h +307 h +4 h +4 h +1 h +83 h +10 h +10 h +4 h +10 h +3847 m +10 h +110 h +9891 m +73 h +4 h +104 h +1 h +274 h +4 h +9892 m +184 h +10 h +1 h +9893 m +4 h +1 h +241 m +1 h +4 h +4 h +109 h +11 h +9894 m +1 h +9895 m +9896 m +9897 m +9898 m +1 h +9899 m +9900 m +9901 m +9902 m +109 h +9903 m +1 h +4 h +10 h +9904 m +10 h +1 h +1083 h +83 h +4 h +124 h +9905 m +36 h +185 h +578 h +12 h +4 h +10 h +4 h +109 h +1 h +4 h +4 h +1 h +9906 m +1418 m +4 h +2928 m +2313 m +139 h +9907 m +10 h +9908 m +9909 m +1 h +11 h +1 h +94 h +195 h +1 h +9910 m +4 h +45 h +4 h +57 h +185 h +4 h +9911 m +119 h +4 h +64 h +1 h +27 h +9912 m +9913 m +4 h +169 h +4 h +10 h +10 h +9914 m +1 h +4 h +4 h +59 h +358 h +4 h +114 h +10 h +97 h +3 h +4 h +9915 m +1 h +4 h +4 h +4 h +4 h +4 h +10 h +8 h +9916 m +9917 m +9918 m +278 h +83 h +64 h +196 h +1 h +9919 m +4 h +9920 m +4 h +9921 m +386 h +4 h +10 h +4 h +7924 m +1 h +4 h +9922 m +4 h +9923 m +4 h +9924 m +1 h +185 h +4 h +1 h +4 h +10 h +77 h +10 h +9925 m +1 h +9926 m +4 h +1 h +11 h +4 h +4 h +79 h +4 h +631 m +4 h +4 h +9927 m +459 m +25 h +687 h +9928 m +82 h +10 h +57 h +10 h +31 h +1 h +10 h +4 h +4 h +9929 m +1 h +1 h +1835 h +9930 m +4 h +464 h +1 h +9931 m +10 h +112 h +9932 m +185 h +69 h +11 h +4 h +109 h +2041 m +9933 m +9934 m +108 h +9935 m +156 h +9936 m +1 h +4 h +9937 m +10 h +8193 m +195 h +9938 m +4 h +196 h +9939 m +10 h +1 h +4 h +83 h +9940 m +10 h +1 h +9941 m +767 m +10 h +59 h +1 h +143 h +307 h +1 h +9942 m +2751 m +125 h +9943 m +9944 m +10 h +143 h +4867 m +266 h +4 h +3293 m +9945 m +9946 m +1 h +3534 m +5483 m +4 h +1 h +10 h +2710 m +1 h +1 h +9947 m +10 h +10 h +10 h +11 h +4 h +1 h +4 h +9948 m +9949 m +1 h +9950 m +9951 m +1 h +10 h +1 h +73 h +9952 m +11 h +4 h +4 h +1 h +9953 m +10 h +41 h +146 h +4 h +6869 m +1 h +10 h +1 h +31 h +4 h +1 h +1 h +4 h +1 h +4 h +4 h +4 h +57 h +1 h +935 h +9954 m +1847 m +4 h +9955 m +1 h +9956 m +9957 m +11 h +109 h +4 h +9958 m +45 h +9959 m +1 h +10 h +82 h +143 h +1 h +371 h +10 h +4 h +4 h +4 h +4 h +146 h +1 h +56 h +4 h +1 h +10 h +1 h +258 h +10 h +10 h +1 h +65 h +1 h +9960 m +4 h +1 h +1 h +164 h +9961 m +4 h +1 h +1 h +9962 m +10 h +4 h +4 h +4 h +9963 m +1 h +443 h +31 h +9964 m +11 h +143 h +82 h +9965 m +4 h +25 h +4 h +1 h +9966 m +82 h +8 h +238 h +4 h +4 h +9967 m +9968 m +966 m +279 h +10 h +4 h +4 h +9969 m +471 m +9970 m +9971 m +10 h +4 h +12 h +9972 m +11 h +258 h +172 h +400 m +4 h +3155 m +9973 m +1 h +10 h +9974 m +9975 m +11 h +4 h +3025 h +124 h +10 h +9976 m +13 h +146 h +4 h +1 h +9977 m +57 h +9978 m +4 h +4 h +9 h +11 h +258 h +9979 m +74 h +359 h +55 h +371 h +41 h +1 h +307 h +258 h +9980 m +41 h +27 h +1220 m +10 h +1260 m +92 h +10 h +9981 m +10 h +1 h +113 h +9982 m +10 h +3 h +8070 m +4 h +9983 m +9984 m +10 h +195 h +10 h +9985 m +9986 m +9987 m +9988 m +371 h +4 h +9989 m +4 h +4 h +1 h +83 h +4 h +4 h +4 h +9990 m +31 h +1 h +1 h +9991 m +1 h +9992 m +9993 m +9994 m +4 h +4 h +1 h +59 h +65 h +1 h +4 h +4 h +1 h +10 h +1 h +4 h +10 h +10 h +74 h +124 h +1 h +4 h +4 h +9995 m +9996 m +9997 m +4 h +9998 m +1 h +986 h +59 h +3 h +4 h +4 h +9999 m +4 h +48 h +3 h +10 h +167 h +1 h +1 h +4 h +4 h +3 h +9956 m +1 h +4 h +10000 m +1374 m +1504 m +4 h +4 h +10001 m +4 h +4 h +1619 h +169 h +10 h +1 h +6022 m +4 h +1 h +1 h +41 h +278 h +265 h +196 h +489 m +57 h +536 h +74 h +8809 m +4 h +10002 m +307 h +4 h +1886 m +4 h +10003 m +3 h +124 h +167 h +10 h +1 h +4 h +278 h +4 h +143 h +10004 m +10005 m +10 h +1 h +1 h +1 h +1 h +10 h +423 m +11 h +40 h +4 h +4 h +10006 m +4 h +4 h +10007 m +4 h +55 h +4 h +10008 m +82 h +10 h +10 h +1 h +4 h +1250 h +278 h +10009 m +10 h +83 h +1 h +3 h +109 h +10010 m +1 h +185 h +270 h +1122 m +1595 m +1 h +4 h +1 h +1 h +1074 h +4 h +1 h +10 h +1 h +125 h +4 h +1 h +4 h +4 h +3303 m +1 h +4 h +4376 m +4 h +4 h +872 m +4 h +4 h +123 h +4 h +1 h +10011 m +10 h +11 h +57 h +1 h +31 h +10012 m +1 h +83 h +4 h +10013 m +10 h +10 h +1 h +4 h +147 h +112 h +109 h +1 h +138 h +1 h +82 h +1074 h +10014 m +143 h +4 h +4 h +74 h +4 h +10015 m +4 h +4 h +4 h +185 h +1 h +10016 m +4 h +4 h +258 h +2788 m +4 h +4 h +297 h +10 h +1 h +104 h +41 h +1309 m +195 h +1 h +10017 m +138 h +1 h +4 h +31 h +10018 m +124 h +5036 m +4 h +10019 m +10 h +10020 m +1 h +1470 h +10 h +1 h +10021 m +1 h +10 h +10 h +10022 m +10023 m +4 h +4 h +7541 m +3 h +1 h +10 h +73 h +1 h +784 m +763 m +196 h +45 h +4 h +125 h +11 h +1 h +1 h +10024 m +10025 m +4 h +4 h +10026 m +285 m +1 h +1 h +1137 h +10027 m +4 h +82 h +10 h +4 h +1 h +10028 m +1309 m +371 h +4 h +1 h +10 h +4 h +135 h +10029 m +10030 m +83 h +258 h +620 m +1 h +123 h +447 h +10 h +4 h +10031 m +10032 m +4 h +1 h +10 h +10 h +4 h +10033 m +1 h +4 h +4 h +692 h +238 h +10 h +10034 m +79 h +1 h +4 h +10035 m +351 m +10 h +986 h +10036 m +10037 m +59 h +10038 m +1 h +10 h +4 h +4 h +186 h +146 h +4 h +10 h +10 h +408 m +25 h +185 h +1261 h +10 h +109 h +10039 m +1 h +4 h +3 h +10040 m +4 h +10 h +1 h +784 m +1 h +59 h +82 h +1835 h +10 h +10041 m +1 h +1 h +10 h +157 h +10 h +10 h +8 h +1 h +45 h +124 h +10 h +10 h +10042 m +10043 m +1 h +10 h +986 h +10 h +4 h +1 h +1892 m +10044 m +1 h +10 h +10045 m +4 h +10 h +1893 m +10046 m +10047 m +10 h +1 h +10048 m +900 m +1685 h +1 h +10049 m +4 h +1 h +92 h +4 h +10 h +11 h +4 h +4 h +10050 m +642 m +1 h +1 h +79 h +1638 m +57 h +173 h +125 h +57 h +1 h +1 h +10051 m +1 h +4 h +10052 m +4 h +4 h +10 h +295 h +10053 m +10054 m +196 h +4 h +383 h +82 h +1 h +1 h +4 h +10055 m +4 h +10056 m +1 h +1 h +10 h +10057 m +4 h +41 h +10058 m +1 h +10059 m +82 h +11 h +278 h +4 h +10060 m +1 h +10061 m +10 h +4 h +10062 m +1 h +1785 m +4 h +10063 m +11 h +10064 m +4 h +4 h +109 h +113 h +4 h +4 h +10065 m +4 h +4 h +1838 m +1 h +10066 m +82 h +1 h +10 h +73 h +4 h +3 h +1 h +4 h +1185 m +4 h +4 h +10 h +10 h +10 h +4 h +10 h +172 h +4 h +41 h +4 h +1 h +4 h +10067 m +59 h +4 h +4 h +1 h +2379 h +1 h +1 h +10068 m +114 h +1 h +10069 m +10 h +4 h +10070 m +10071 m +4 h +1 h +10072 m +4 h +4 h +4 h +10 h +146 h +1250 h +4 h +10073 m +31 h +10074 m +1 h +1642 h +36 h +59 h +1 h +6766 m +10 h +6124 m +10 h +4 h +10 h +1 h +1 h +156 h +25 h +4 h +1 h +262 h +4 h +4 h +10075 m +4 h +1261 h +371 h +10076 m +10 h +10 h +4 h +4 h +4 h +10 h +1 h +4 h +4 h +1 h +56 h +11 h +10 h +1 h +10 h +4 h +1 h +10 h +124 h +1 h +10077 m +10 h +1 h +4 h +1 h +1 h +489 m +10078 m +3 h +4 h +10 h +4 h +1 h +82 h +4 h +10 h +10079 m +1632 m +2379 h +2733 h +10080 m +10 h +82 h +1 h +4 h +147 h +36 h +10 h +10081 m +4 h +10082 m +10 h +1260 m +11 h +1027 m +1 h +4 h +4 h +4 h +388 m +1 h +4 h +114 h +82 h +214 m +4 h +10083 m +737 m +1 h +10084 m +10 h +230 h +10085 m +12 h +11 h +10086 m +1 h +1 h +4 h +1 h +1 h +10087 m +2840 m +556 h +4 h +10088 m +1 h +4 h +4 h +82 h +1 h +10 h +4 h +4 h +4 h +10089 m +146 h +123 h +10090 m +10 h +4 h +10091 m +1 h +10092 m +1 h +1 h +4 h +10093 m +268 m +4 h +10094 m +4240 m +59 h +10095 m +4 h +10 h +10096 m +10097 m +11 h +174 h +10 h +229 h +976 h +10098 m +13 h +10 h +4 h +10099 m +1137 h +73 h +25 h +274 h +4 h +1 h +601 h +1 h +1 h +4 h +4 h +770 m +1 h +125 h +169 h +1 h +10 h +10 h +1 h +45 h +124 h +10100 m +57 h +4 h +4 h +1 h +1 h +10101 m +74 h +10 h +687 h +1 h +91 h +4 h +10102 m +1 h +83 h +10 h +10 h +4 h +1 h +4 h +1 h +10103 m +4 h +2617 m +10104 m +1 h +4 h +92 h +31 h +7870 m +4 h +10 h +4 h +10105 m +10 h +11 h +4 h +4 h +79 h +2303 m +230 h +11 h +10106 m +412 m +10 h +4 h +10 h +10107 m +1 h +10108 m +10 h +11 h +10109 m +3990 m +1 h +185 h +11 h +57 h +109 h +640 h +74 h +238 h +10 h +64 h +4 h +11 h +390 m +124 h +10110 m +4 h +219 m +135 h +4 h +10 h +1 h +65 h +11 h +4 h +1772 h +4 h +10 h +4 h +4 h +1 h +10 h +4 h +4 h +4 h +41 h +4 h +10 h +1 h +10 h +1 h +1 h +10 h +1 h +229 h +4 h +57 h +97 h +338 m +10 h +147 h +10111 m +1 h +4 h +4 h +10112 m +10 h +10113 m +79 h +10 h +3837 m +463 h +4 h +1 h +10114 m +4 h +10115 m +10116 m +4 h +4 h +59 h +1 h +1370 m +11 h +92 h +55 h +4 h +1 h +4 h +4 h +1 h +1 h +1714 h +1 h +4 h +10 h +10 h +10117 m +11 h +1 h +11 h +10 h +11 h +25 h +4 h +10 h +955 m +10118 m +8 h +1 h +692 h +591 m +10119 m +10120 m +4 h +1 h +4 h +5976 m +1 h +8243 m +10121 m +65 h +4 h +4 h +10122 m +4 h +1650 h +10123 m +4 h +10 h +13 h +64 h +125 h +3 h +10124 m +10 h +124 h +1 h +1 h +1 h +10125 m +1 h +10126 m +4 h +4 h +10127 m +65 h +10 h +4 h +135 h +57 h +10 h +31 h +4 h +22 h +124 h +1 h +10 h +10128 m +4 h +10129 m +1 h +79 h +10 h +4 h +10130 m +10131 m +124 h +124 h +11 h +10132 m +4 h +109 h +260 m +1 h +10 h +4 h +1 h +4 h +10133 m +1 h +55 h +295 h +10134 m +4 h +110 h +4 h +11 h +10 h +10135 m +297 h +1 h +10 h +169 h +629 m +10 h +4 h +82 h +1 h +11 h +520 h +1 h +109 h +10 h +10 h +1 h +4 h +4 h +1766 h +1 h +10136 m +4 h +4 h +1 h +4 h +4 h +109 h +933 m +4 h +83 h +10137 m +10 h +10 h +10 h +1 h +181 h +270 h +4 h +4 h +169 h +4 h +97 h +578 h +10138 m +4 h +1 h +4 h +109 h +1766 h +10139 m +55 h +4 h +2851 m +4 h +33 m +10 h +4 h +5863 m +10140 m +4 h +4 h +4 h +10141 m +4 h +172 h +4 h +25 h +4 h +1 h +2041 m +10 h +4 h +10142 m +4 h +1 h +10143 m +1 h +10 h +1 h +10 h +4 h +4 h +10 h +124 h +4 h +2374 m +4 h +10 h +1199 m +358 h +11 h +4 h +146 h +4 h +10144 m +74 h +57 h +4 h +10 h +55 h +125 h +4 h +10145 m +10 h +4 h +1 h +82 h +10 h +4 h +10 h +266 h +195 h +10 h +1 h +4 h +196 h +10146 m +10147 m +114 h +1 h +359 h +10 h +11 h +3177 m +4 h +10 h +4 h +1 h +10148 m +4 h +4 h +45 h +1157 m +10 h +10 h +4 h +1083 h +10149 m +169 h +1650 h +123 h +4 h +3 h +82 h +10 h +4 h +229 h +1 h +57 h +4 h +1 h +10 h +119 h +4 h +4 h +10 h +92 h +4 h +1 h +4 h +4 h +10 h +399 h +10150 m +83 h +1 h +307 h +4 h +570 h +124 h +4 h +1 h +1 h +10151 m +1 h +124 h +10152 m +4 h +1 h +10 h +64 h +1 h +73 h +4 h +123 h +10153 m +4 h +4 h +10 h +10154 m +10155 m +70 m +1250 h +10 h +1 h +10156 m +3680 m +1 h +97 h +10157 m +4 h +578 h +1016 h +4 h +13 h +4 h +4 h +1 h +779 h +10158 m +4 h +1 h +1 h +10 h +1 h +185 h +172 h +2475 m +1 h +1 h +10 h +4 h +1105 h +4 h +10159 m +1 h +56 h +1 h +10160 m +10 h +10161 m +41 h +1 h +1861 m +1650 h +10 h +83 h +59 h +4 h +4 h +10162 m +56 h +10 h +4 h +10163 m +10164 m +10165 m +4 h +11 h +8 h +41 h +55 h +4 h +10166 m +332 h +1646 m +10167 m +10168 m +11 h +167 h +10 h +1 h +10169 m +4 h +10 h +10 h +1 h +1070 m +10 h +1 h +4 h +10170 m +192 h +10171 m +10 h +459 m +4 h +10 h +4 h +4 h +1089 m +4 h +10 h +10172 m +4 h +55 h +57 h +1 h +10173 m +124 h +1 h +10 h +10 h +7521 m +388 m +1 h +1 h +170 h +10174 m +1 h +4 h +4 h +25 h +4 h +5567 m +109 h +31 h +11 h +10175 m +4 h +4 h +147 h +10176 m +135 h +4 h +10 h +11 h +10 h +4 h +10177 m +10178 m +1 h +1 h +10179 m +1884 h +10 h +4 h +1 h +12 h +12 h +10180 m +25 h +79 h +31 h +10181 m +10 h +167 h +1 h +4 h +10 h +4 h +4 h +12 h +10 h +229 h +10 h +10182 m +114 h +10 h +94 h +4 h +297 h +3 h +10183 m +10184 m +10185 m +698 m +3622 m +31 h +4 h +4 h +1 h +74 h +10 h +10186 m +4 h +1 h +1 h +104 h +10187 m +1796 h +1 h +23 h +167 h +1 h +10188 m +158 h +4 h +10 h +3 h +10189 m +57 h +28 h +109 h +10 h +61 m +1547 m +590 h +10 h +146 h +124 h +10190 m +10191 m +4 h +583 h +10 h +1 h +74 h +158 h +143 h +4 h +1 h +4 h +10 h +45 h +4 h +64 h +4 h +10192 m +313 h +10 h +4 h +4 h +57 h +10193 m +8 h +1 h +10194 m +10 h +10 h +1 h +279 h +4 h +10195 m +4 h +10 h +1 h +4 h +173 h +5863 m +10 h +4 h +1 h +1 h +2475 m +1 h +4 h +1 h +1 h +10196 m +10197 m +9 h +1 h +4 h +2865 m +4 h +4 h +10198 m +1 h +266 h +6851 m +10 h +4 h +1 h +10 h +10 h +258 h +1 h +4 h +10199 m +176 m +10200 m +10 h +185 h +4 h +10 h +1 h +4 h +10201 m +1 h +4 h +10 h +238 h +2760 m +73 h +1 h +2300 m +10202 m +4 h +10203 m +4 h +10204 m +109 h +10205 m +83 h +1 h +1 h +1 h +4 h +10 h +4 h +6586 m +10206 m +144 h +4 h +1 h +1 h +4 h +4 h +4 h +10 h +1 h +11 h +10207 m +4 h +4 h +27 h +4 h +1 h +1 h +11 h +1 h +4 h +4 h +6125 m +4 h +10208 m +13 h +1 h +10 h +4 h +4 h +857 h +1 h +1 h +4 h +1 h +10209 m +358 h +4 h +4 h +4 h +4 h +4 h +1 h +114 h +10210 m +10 h +2041 m +10211 m +10212 m +1 h +10 h +1 h +1 h +10213 m +4 h +4 h +114 h +10 h +74 h +10214 m +10215 m +10216 m +1454 m +4 h +2475 h +4514 m +4 h +11 h +172 h +10217 m +1 h +1 h +1 h +10 h +1 h +10218 m +172 h +10219 m +276 h +1027 m +10 h +10220 m +1 h +10 h +4 h +64 h +10221 m +10222 m +10 h +2788 m +4 h +118 h +10223 m +125 h +10224 m +31 h +11 h +10225 m +2532 m +1 h +4 h +1 h +1 h +1 h +1 h +10 h +1 h +10 h +147 h +10226 m +10227 m +10228 m +256 m +82 h +4 h +4 h +1 h +4 h +4 h +146 h +4 h +258 h +10229 m +10 h +73 h +97 h +10 h +10230 m +25 h +79 h +112 h +4 h +4 h +10231 m +97 h +976 h +10232 m +104 h +97 h +1309 h +13 h +278 h +10233 m +4 h +10234 m +10235 m +10236 m +4 h +4 h +4 h +10237 m +4 h +1 h +4 h +4 h +10238 m +230 h +10239 m +10240 m +4 h +10241 m +4 h +10242 m +146 h +190 h +1016 h +31 h +1 h +11 h +12 h +1 h +10243 m +10 h +10244 m +45 h +92 h +10245 m +82 h +10 h +10 h +447 h +4 h +4 h +7271 m +1 h +4 h +10 h +113 h +83 h +124 h +10 h +238 h +1 h +10246 m +195 h +443 h +10247 m +4 h +196 h +4 h +1766 h +1 h +10 h +73 h +181 h +10248 m +510 m +4 h +1137 h +25 h +10249 m +10250 m +1471 m +10 h +1 h +12 h +4 h +1 h +4 h +10251 m +10252 m +118 h +10 h +4 h +238 h +10253 m +45 h +1 h +10 h +104 h +10254 m +10255 m +10 h +1 h +4263 m +10256 m +1 h +31 h +1619 h +1725 m +11 h +4 h +1 h +10257 m +41 h +59 h +10 h +10 h +4 h +10258 m +8571 m +4 h +125 h +4 h +266 h +10259 m +1 h +196 h +4 h +57 h +1116 m +10260 m +109 h +10261 m +184 h +10262 m +10263 m +36 h +4 h +11 h +4 h +3 h +10 h +10264 m +57 h +10 h +1 h +10265 m +4 h +1 h +4 h +64 h +10 h +1 h +569 h +1 h +4 h +167 h +108 h +10266 m +1 h +801 m +10267 m +10 h +1 h +1 h +1 h +4 h +31 h +4 h +4356 m +278 h +4 h +10268 m +124 h +4 h +31 h +4 h +4 h +124 h +4 h +10269 m +11 h +119 h +4 h +125 h +10 h +11 h +4 h +1 h +4 h +4 h +10270 m +4 h +57 h +25 h +10 h +10 h +4 h +59 h +10271 m +10 h +10272 m +10273 m +2002 m +2607 m +1685 h +73 h +10 h +10274 m +1047 m +4 h +4 h +4 h +4 h +10275 m +10276 m +10 h +209 m +4 h +1 h +1 h +4 h +83 h +114 h +1 h +72 m +4 h +45 h +10277 m +4 h +10 h +10278 m +5243 m +10 h +1 h +55 h +143 h +4 h +10 h +214 m +10279 m +1 h +238 h +1 h +4 h +10 h +11 h +10 h +1 h +10280 m +10 h +4 h +4 h +59 h +11 h +10281 m +687 h +4 h +10282 m +3 h +56 h +110 h +173 h +56 h +1 h +383 h +82 h +8 h +125 h +10283 m +1 h +11 h +113 h +4 h +4 h +10284 m +10 h +119 h +10 h +1 h +4 h +757 h +2379 h +10285 m +692 h +1 h +10286 m +4 h +3909 m +1 h +4 h +4 h +4 h +1 h +4 h +1 h +10287 m +10 h +4 h +1 h +1 h +10288 m +65 h +10289 m +10290 m +10291 m +1 h +3 h +10292 m +692 h +1620 h +10 h +10293 m +4 h +10 h +10294 m +10295 m +1 h +1 h +4 h +10296 m +31 h +172 h +143 h +123 h +10297 m +10 h +4 h +12 h +4 h +92 h +10298 m +4 h +4608 m +25 h +10299 m +4 h +10300 m +1 h +4 h +1 h +1 h +147 h +36 h +4 h +8 h +4 h +164 h +10301 m +109 h +10 h +186 h +4 h +1 h +10302 m +1 h +10303 m +10304 m +1790 h +1 h +1 h +195 h +4 h +10 h +1 h +10305 m +65 h +4 h +3299 m +1 h +4 h +4 h +114 h +10306 m +4 h +4 h +22 h +4 h +1 h +443 h +59 h +4 h +31 h +1 h +12 h +4 h +4 h +190 h +10 h +3 h +10307 m +2591 m +10 h +10 h +10308 m +1 h +10309 m +1027 h +14 m +164 h +1 h +97 h +4 h +4 h +61 m +1 h +6438 m +1 h +82 h +57 h +10310 m +4 h +1 h +1 h +4 h +169 h +4 h +601 h +339 m +83 h +1 h +4 h +73 h +65 h +113 h +278 h +4 h +10311 m +4 h +1 h +41 h +4 h +79 h +10312 m +4 h +4 h +4 h +4 h +69 h +4 h +10313 m +10314 m +10315 m +1 h +1 h +556 h +1 h +82 h +4 h +1 h +4 h +31 h +4 h +59 h +4 h +11 h +10 h +1 h +1 h +173 h +61 h +10316 m +124 h +10 h +10317 m +1 h +82 h +4 h +1 h +229 h +304 m +10318 m +10319 m +10 h +25 h +10 h +113 h +10320 m +10 h +139 h +195 h +1 h +4 h +4 h +139 h +125 h +4 h +1790 h +1835 h +4 h +716 m +125 h +3 h +10321 m +4 h +10 h +59 h +5141 m +10322 m +4 h +1 h +4 h +73 h +10323 m +1 h +4 h +1261 h +10 h +1 h +10 h +733 m +10324 m +10 h +10 h +10 h +1 h +1 h +371 h +10325 m +1 h +4 h +10 h +10 h +10 h +4 h +10326 m +4 h +4 h +4 h +4 h +28 h +1 h +4 h +4 h +10 h +3 h +1 h +10 h +4 h +1 h +1 h +31 h +1 h +10327 m +10328 m +4 h +238 h +10329 m +10 h +10330 m +1 h +4 h +1 h +10 h +10 h +10331 m +4 h +59 h +157 h +4 h +1 h +10332 m +10333 m +1 h +4 h +10334 m +45 h +57 h +10 h +4 h +4 h +11 h +900 m +31 h +4 h +1 h +4 h +10335 m +1677 m +399 h +10 h +10 h +1 h +10 h +1374 m +4 h +10336 m +4 h +1 h +41 h +11 h +10 h +10337 m +3 h +4 h +10 h +4 h +1 h +1 h +448 m +10338 m +4 h +10339 m +1 h +13 h +358 h +164 h +146 h +10340 m +1 h +4 h +10341 m +4 h +10342 m +4 h +10 h +64 h +4 h +10343 m +10344 m +125 h +1 h +10 h +4 h +57 h +4 h +4 h +1722 m +4 h +10345 m +9321 m +146 h +1 h +1 h +10346 m +4 h +332 h +109 h +10347 m +10348 m +65 h +10349 m +10350 m +4 h +82 h +10351 m +10 h +55 h +4 h +4 h +4 h +4 h +1 h +10 h +10352 m +10 h +4 h +10 h +10 h +10353 m +10354 m +10 h +10 h +403 h +4 h +4 h +4 h +4905 m +1 h +10355 m +124 h +82 h +45 h +1 h +4 h +10 h +4 h +1 h +1 h +11 h +4 h +4 h +10 h +158 h +27 h +45 h +1790 h +4 h +556 h +31 h +4 h +10 h +10356 m +57 h +368 h +10357 m +10 h +10358 m +10 h +4 h +10359 m +55 h +31 h +1817 m +10360 m +11 h +1 h +10 h +10361 m +65 h +109 h +10 h +1 h +1953 m +4 h +125 h +10 h +55 h +195 h +10362 m +10363 m +447 h +4 h +1 h +1 h +94 h +1 h +10 h +1894 m +109 h +1 h +4 h +986 h +1 h +4 h +509 m +4 h +4 h +4 h +10364 m +1 h +279 h +4 h +10365 m +10366 m +10 h +41 h +10367 m +74 h +4 h +10368 m +124 h +114 h +10369 m +3 h +83 h +10370 m +109 h +4 h +10 h +4 h +28 h +170 h +272 m +1 h +4 h +10 h +9860 m +1 h +371 h +10 h +1619 h +1 h +1 h +1 h +109 h +1 h +48 h +10371 m +4 h +581 m +10372 m +1835 h +10373 m +1 h +258 h +10 h +94 h +44 m +10374 m +65 h +464 h +10375 m +104 h +10376 m +1 h +8 h +4 h +170 h +10377 m +10 h +10 h +114 h +4 h +4 h +307 h +1 h +1 h +10 h +10378 m +4 h +1 h +82 h +4 h +65 h +10379 m +10 h +1 h +299 h +4 h +10380 m +27 h +1 h +368 h +307 h +4 h +1 h +4 h +129 h +538 h +1478 h +295 h +10 h +4 h +1 h +276 h +104 h +10381 m +4 h +4 h +12 h +10382 m +10 h +4 h +10 h +2532 m +4 h +4 h +1472 m +10 h +109 h +10383 m +1 h +1 h +10384 m +10 h +2496 m +10 h +258 h +4 h +1 h +279 h +1 h +3435 m +1 h +10385 m +1 h +10386 m +1 h +1 h +97 h +12 h +4 h +109 h +1016 h +4 h +10 h +1 h +170 h +138 h +11 h +10387 m +1 h +4 h +10388 m +11 h +1666 m +10 h +10389 m +65 h +10 h +322 m +4 h +1 h +82 h +4 h +82 h +10 h +1 h +10 h +4 h +10 h +601 h +4 h +10390 m +1 h +468 h +10391 m +1293 m +10392 m +10393 m +4 h +10394 m +10395 m +10396 m +10 h +266 h +4 h +10397 m +83 h +1 h +10398 m +11 h +10 h +56 h +297 h +4 h +4 h +10 h +4 h +1 h +10399 m +1 h +144 h +124 h +1 h +109 h +1563 m +10 h +10400 m +4 h +4 h +10401 m +10 h +10402 m +1 h +4 h +82 h +10403 m +4 h +1 h +10404 m +1769 m +520 h +10405 m +4 h +1308 m +10 h +82 h +11 h +1201 h +10406 m +10407 m +4 h +10408 m +10 h +4 h +1948 m +157 h +1796 h +10 h +125 h +4 h +11 h +295 h +4 h +10 h +5 h +45 h +10 h +10 h +192 h +10409 m +172 h +10410 m +2733 h +104 h +10411 m +31 h +4 h +10 h +97 h +1 h +10 h +64 h +1 h +10412 m +104 h +4 h +5982 m +4 h +10 h +4 h +347 m +4 h +109 h +4 h +1 h +4 h +10413 m +1 h +250 h +10414 m +4 h +4 h +119 h +4 h +1 h +10415 m +10 h +4 h +57 h +211 h +65 h +1 h +27 h +57 h +1284 m +10416 m +4 h +75 h +10417 m +10418 m +10419 m +10420 m +10421 m +1089 m +10 h +4 h +97 h +157 h +10422 m +10 h +13 h +11 h +56 h +147 h +109 h +10 h +3274 m +4 h +10423 m +10 h +1952 m +1260 h +4 h +10424 m +4 h +13 h +125 h +1 h +1 h +10 h +7938 m +10425 m +10426 m +113 h +10 h +10427 m +10 h +569 h +10 h +108 h +4 h +59 h +10 h +1 h +10428 m +10429 m +10430 m +10431 m +4 h +4 h +10432 m +4 h +1685 h +10433 m +4 h +1 h +2719 m +10434 m +1 h +125 h +4 h +10435 m +10 h +10436 m +1 h +41 h +1 h +358 h +10437 m +3 h +1 h +10438 m +10439 m +4 h +1 h +10440 m +45 h +4 h +1 h +10441 m +4 h +10 h +266 h +4 h +10442 m +94 h +10443 m +41 h +10 h +1 h +9411 m +1 h +224 h +6185 m +1576 m +4 h +10444 m +10445 m +4 h +224 h +10 h +10446 m +4 h +83 h +4 h +4 h +10447 m +1 h +4 h +109 h +59 h +1 h +1 h +4 h +10448 m +10 h +994 m +229 h +146 h +1 h +4 h +976 h +478 m +4 h +57 h +10449 m +10450 m +10 h +10451 m +10452 m +109 h +124 h +4 h +4 h +3622 m +914 m +4 h +4 h +1 h +488 h +2285 m +56 h +4 h +59 h +1 h +1 h +10453 m +4 h +125 h +1 h +10454 m +4 h +4 h +4 h +109 h +10 h +11 h +41 h +464 h +10 h +4 h +4 h +74 h +1027 h +4 h +10 h +109 h +147 h +4 h +185 h +10 h +1403 h +276 h +1 h +4 h +1 h +266 h +8 h +10455 m +31 h +368 h +8 h +10456 m +1 h +557 m +10457 m +1 h +1 h +195 h +10458 m +4 h +181 h +10459 m +4 h +1 h +59 h +4 h +10 h +10460 m +12 h +146 h +10461 m +10462 m +10 h +10463 m +109 h +4 h +10464 m +10 h +4 h +59 h +27 h +4 h +10465 m +4 h +10466 m +23 h +4 h +4 h +3 h +1 h +10 h +203 m +1 h +4 h +4 h +1 h +4 h +74 h +4215 m +10 h +31 h +138 h +10 h +6022 m +10467 m +10468 m +447 h +92 h +195 h +12 h +4 h +1 h +11 h +649 m +10469 m +4 h +2308 m +4 h +10470 m +11 h +45 h +307 h +10471 m +10472 m +4 h +10 h +10473 m +10 h +1685 h +31 h +124 h +4 h +578 h +4 h +2733 h +25 h +10 h +11 h +4 h +65 h +10 h +140 h +1 h +10474 m +4 h +1 h +4 h +55 h +10475 m +4 h +56 h +10476 m +10477 m +371 h +4 h +1 h +4 h +10 h +1 h +4 h +10 h +1 h +27 h +33 m +10478 m +10479 m +11 h +10 h +1 h +10 h +10480 m +3680 m +4 h +124 h +10481 m +10482 m +11 h +10 h +1 h +10483 m +10484 m +10485 m +10 h +1 h +4 h +1 h +1016 h +443 h +258 h +1 h +4 h +1 h +10486 m +4 h +10487 m +4 h +1 h +4 h +10488 m +1 h +164 h +1 h +1 h +4 h +10 h +10 h +22 h +45 h +4 h +10 h +1 h +10489 m +8 h +23 h +25 h +4 h +83 h +10490 m +4 h +10491 m +4 h +10492 m +186 h +11 h +10 h +10 h +1 h +94 h +10 h +195 h +4 h +2885 m +4 h +59 h +620 m +10493 m +2928 m +10 h +10494 m +4 h +1 h +1 h +10495 m +10 h +97 h +1 h +319 h +10496 m +59 h +4 h +800 m +229 h +4 h +124 h +307 h +1 h +10497 m +10498 m +104 h +10499 m +11 h +10500 m +4857 m +10501 m +4 h +4 h +1 h +74 h +1 h +4 h +330 h +1884 h +1 h +4 h +173 h +4 h +4 h +10502 m +4 h +1 h +10503 m +10504 m +1 h +83 h +1 h +125 h +1 h +10 h +196 h +1 h +135 h +10 h +10505 m +125 h +92 h +4 h +10 h +319 h +31 h +4 h +10506 m +4 h +1 h +4 h +10507 m +4 h +4 h +185 h +1 h +57 h +59 h +195 h +1 h +8497 m +1 h +4 h +190 h +1 h +4 h +10 h +59 h +10508 m +10509 m +4 h +10 h +10 h +4 h +94 h +10 h +10510 m +10511 m +4 h +10512 m +4 h +1 h +5309 m +4 h +4 h +11 h +276 h +10 h +4 h +57 h +190 h +10513 m +196 h +463 h +10 h +4 h +1 h +4 h +10514 m +1 h +1 h +77 h +1 h +295 h +10515 m +10 h +1 h +4 h +124 h +169 h +10516 m +74 h +4 h +1 h +13 h +1 h +4 h +332 h +4522 m +1 h +167 h +11 h +10517 m +12 h +4 h +169 h +10 h +4 h +41 h +4 h +4 h +4 h +56 h +1 h +10518 m +1 h +1056 m +4 h +1 h +109 h +1 h +10 h +1 h +1 h +10519 m +12 h +4 h +10520 m +289 h +10521 m +4 h +1 h +10522 m +10523 m +4 h +1 h +11 h +10524 m +1 h +4 h +124 h +10525 m +195 h +10526 m +10527 m +11 h +10528 m +4 h +74 h +123 h +109 h +1 h +195 h +1 h +10529 m +164 h +1 h +4 h +10530 m +10531 m +10 h +1 h +1 h +124 h +1 h +10532 m +1 h +82 h +82 h +4 h +258 h +11 h +10533 m +93 h +1 h +41 h +10 h +10534 m +4 h +10 h +219 m +1 h +11 h +146 h +10535 m +10 h +1 h +4 h +10 h +1 h +4 h +41 h +114 h +33 m +10536 m +4 h +125 h +478 m +10 h +1045 m +135 h +601 h +10537 m +10538 m +447 h +1 h +45 h +4 h +36 h +10539 m +10540 m +7832 m +4 h +104 h +10 h +10 h +11 h +4 h +843 m +236 m +447 h +4 h +4 h +1 h +4 h +4 h +1 h +10541 m +10542 m +11 h +73 h +1 h +1 h +124 h +450 m +3 h +4 h +10543 m +10 h +10 h +57 h +4 h +238 h +10544 m +10545 m +185 h +10546 m +1 h +4 h +258 h +4 h +82 h +48 h +94 h +109 h +10547 m +1 h +10548 m +10 h +119 h +204 h +692 h +1 h +57 h +1 h +1 h +1 h +4 h +190 h +10 h +10 h +4 h +10549 m +1 h +10550 m +25 h +4 h +196 h +4 h +1454 m +10551 m +4 h +10 h +338 m +10552 m +4 h +10 h +265 h +10553 m +56 h +4 h +10554 m +4 h +10555 m +1796 h +1 h +4 h +82 h +11 h +106 h +10556 m +7352 m +10 h +172 h +83 h +10557 m +10 h +4 h +338 m +1 h +3558 m +164 h +104 h +195 h +536 h +1 h +10558 m +4 h +4 h +10559 m +4 h +4 h +104 h +3 h +4 h +10560 m +10561 m +12 h +10 h +1 h +11 h +10562 m +1 h +12 h +109 h +1 h +10563 m +11 h +10 h +10 h +3 h +147 h +69 h +316 m +10564 m +1861 m +4 h +10565 m +282 m +4 h +54 m +10566 m +4 h +1766 h +4 h +4 h +10567 m +1406 h +57 h +11 h +11 h +74 h +31 h +258 h +109 h +10568 m +1 h +1548 m +83 h +986 h +4 h +10569 m +125 h +10570 m +73 h +4 h +1 h +1 h +1201 h +10571 m +10 h +1 h +10572 m +3 h +4 h +119 h +10573 m +10574 m +4 h +10575 m +9 h +10 h +538 h +2961 m +10 h +10 h +139 h +4542 m +10576 m +10577 m +1 h +190 h +1 h +173 h +10578 m +9040 m +1650 h +4 h +4 h +79 h +279 h +1835 h +1 h +1 h +4 h +13 h +10579 m +3 h +10580 m +1 h +10 h +1 h +1 h +1 h +4 h +10 h +4 h +10581 m +8 h +135 h +1 h +433 m +57 h +41 h +10582 m +10583 m +10 h +1 h +4 h +10 h +10 h +184 h +1 h +10584 m +10 h +104 h +4 h +109 h +146 h +97 h +4 h +1 h +10585 m +10586 m +10 h +1 h +1362 m +55 h +11 h +1 h +5 h +1685 h +10 h +10587 m +36 h +135 h +10 h +10 h +10 h +4 h +185 h +57 h +4 h +10588 m +1 h +1535 m +1 h +8133 m +1278 m +91 h +4 h +459 m +4 h +25 h +10589 m +4 h +109 h +10 h +57 h +10590 m +1 h +41 h +82 h +4 h +64 h +4 h +146 h +1 h +8 h +1 h +1 h +10 h +1 h +10 h +124 h +1 h +2002 m +4 h +82 h +1 h +4 h +8 h +4 h +1 h +4 h +57 h +7915 m +1027 h +4 h +11 h +4 h +12 h +10 h +10 h +56 h +4 h +4 h +10591 m +10 h +195 h +1732 m +1 h +578 h +169 h +626 h +1 h +1 h +4 h +10592 m +3 h +4 h +10 h +2064 m +10593 m +1 h +82 h +10 h +12 h +4 h +1 h +1 h +10 h +10 h +4 h +109 h +489 h +6197 m +1 h +1 h +10594 m +4 h +4 h +4 h +104 h +1137 h +4 h +10 h +55 h +1 h +4 h +10595 m +10 h +10 h +3 h +11 h +119 h +10596 m +4867 m +97 h +82 h +10597 m +112 h +79 h +59 h +4 h +10598 m +25 h +10 h +196 h +10599 m +4 h +11 h +1953 m +2914 m +976 h +4 h +11 h +31 h +56 h +10600 m +11 h +56 h +4 h +10 h +83 h +57 h +10601 m +1 h +10602 m +59 h +10603 m +10604 m +10605 m +2788 m +123 h +10606 m +1 h +10607 m +10 h +1 h +10608 m +10609 m +146 h +10 h +4 h +57 h +10610 m +1 h +4 h +104 h +4 h +12 h +1 h +3240 m +1 h +1975 m +41 h +10 h +45 h +4 h +1 h +4 h +10 h +4 h +10611 m +195 h +10 h +4 h +10612 m +1250 h +124 h +1 h +10 h +12 h +4 h +805 m +4 h +11 h +10613 m +57 h +1 h +4 h +4 h +1 h +4 h +59 h +10 h +10614 m +4 h +10615 m +4 h +10 h +1 h +1 h +10 h +65 h +297 h +74 h +4 h +6399 m +4 h +10616 m +31 h +1 h +10617 m +1 h +4 h +10618 m +4 h +92 h +41 h +82 h +10619 m +1092 m +1 h +4 h +104 h +4 h +10620 m +10 h +36 h +692 h +10621 m +31 h +172 h +4 h +4 h +124 h +172 h +1 h +11 h +1 h +1 h +4 h +10622 m +10623 m +1 h +10 h +4 h +1 h +10624 m +10 h +4 h +164 h +10625 m +1 h +10626 m +57 h +11 h +1 h +4 h +11 h +10627 m +1 h +10628 m +10 h +59 h +1 h +4 h +1 h +10629 m +4 h +10630 m +74 h +4 h +10631 m +4 h +110 h +1137 h +1089 m +4 h +2887 m +1 h +4 h +4 h +10632 m +31 h +430 m +4 h +1 h +4 h +10 h +157 h +4 h +4 h +10633 m +57 h +10 h +1 h +4 h +11 h +4 h +190 h +4349 m +10634 m +4 h +1 h +10635 m +10 h +10 h +124 h +9757 m +10636 m +4 h +4240 m +83 h +33 h +4692 m +1 h +4 h +1261 h +40 h +1 h +295 h +888 m +10637 m +10 h +55 h +10638 m +1 h +1861 m +10639 m +25 h +10640 m +10 h +60 m +114 h +77 h +45 h +10 h +4 h +10641 m +10 h +4 h +4 h +11 h +250 h +10 h +10642 m +114 h +4 h +92 h +10643 m +4 h +4 h +10644 m +1 h +1 h +73 h +124 h +4 h +1 h +10645 m +184 h +779 h +10 h +10 h +4 h +10 h +146 h +1 h +10646 m +114 h +10647 m +41 h +4 h +10648 m +581 m +10649 m +10 h +10650 m +56 h +113 h +10651 m +3 h +125 h +10652 m +4 h +10653 m +4 h +10654 m +4 h +1 h +4 h +97 h +4 h +10655 m +83 h +10656 m +11 h +10657 m +4 h +307 h +4 h +4 h +10 h +10 h +10 h +10658 m +1 h +11 h +1 h +10659 m +4 h +10 h +10660 m +64 h +83 h +4 h +295 h +4 h +4 h +92 h +477 m +10 h +10 h +4 h +1 h +4 h +1 h +4 h +4 h +10661 m +4 h +135 h +10662 m +27 h +4 h +10663 m +10664 m +10665 m +4 h +10666 m +1 h +4 h +2788 m +31 h +10667 m +4 h +4 h +1 h +4 h +4 h +146 h +12 h +4 h +478 h +4 h +146 h +10668 m +1024 m +10 h +82 h +10 h +109 h +10669 m +10 h +7128 m +1 h +10670 m +10671 m +1 h +196 h +125 h +1 h +1 h +57 h +11 h +1 h +135 h +83 h +4 h +135 h +10672 m +1136 m +1 h +82 h +692 h +1535 m +1 h +93 h +4 h +820 h +1 h +1 h +10 h +4 h +4 h +10673 m +4 h +4 h +1 h +10674 m +13 h +104 h +82 h +27 h +4 h +113 h +4 h +172 h +10 h +5008 m +4 h +4 h +10 h +4 h +10675 m +1 h +1 h +4 h +196 h +196 h +11 h +1 h +4 h +74 h +169 h +1 h +1 h +367 m +4 h +4 h +10676 m +10677 m +10 h +82 h +1 h +11 h +1 h +25 h +4 h +4 h +1 h +10678 m +4 h +10679 m +4 h +1 h +10 h +6252 m +4 h +488 h +4 h +10 h +10 h +11 h +104 h +1 h +371 h +109 h +10680 m +4 h +1 h +4 h +203 m +10 h +10 h +1 h +4 h +109 h +10 h +3013 m +104 h +262 h +10681 m +10 h +575 m +10682 m +4 h +1 h +1 h +1 h +1 h +10 h +55 h +11 h +4 h +1 h +4 h +109 h +10683 m +10 h +1 h +4 h +1 h +186 h +536 h +10684 m +4 h +4 h +779 h +10685 m +1 h +1 h +4 h +10 h +10 h +10 h +10686 m +223 m +110 h +4 h +10687 m +4 h +83 h +229 h +10688 m +4 h +124 h +1 h +10689 m +157 h +307 h +10690 m +10 h +10691 m +1 h +10 h +10692 m +10 h +173 h +1 h +10693 m +10 h +109 h +1 h +55 h +266 h +184 h +1 h +4 h +10694 m +109 h +4 h +1 h +4 h +1 h +124 h +238 h +140 h +10 h +82 h +1 h +10 h +82 h +10695 m +4 h +10696 m +1 h +10697 m +10 h +10698 m +4 h +92 h +4 h +4 h +10 h +4 h +1 h +4 h +109 h +1 h +65 h +1403 h +1027 h +10699 m +10 h +383 h +11 h +4 h +4 h +4 h +10700 m +4 h +1 h +1 h +3048 m +4 h +56 h +10 h +4 h +45 h +94 h +10701 m +1 h +1 h +10702 m +129 h +4 h +10703 m +435 m +156 h +164 h +10704 m +911 m +3 h +10705 m +10 h +1 h +146 h +1 h +181 h +109 h +167 h +124 h +10706 m +10 h +4 h +10707 m +10 h +10 h +4 h +4 h +10 h +1 h +10708 m +125 h +125 h +4 h +10 h +10709 m +113 h +10710 m +195 h +1 h +578 h +1 h +4 h +4 h +10711 m +82 h +4 h +65 h +4 h +10712 m +4 h +59 h +1 h +1 h +167 h +4 h +10713 m +74 h +10714 m +4 h +4 h +79 h +4 h +10715 m +4 h +13 h +4 h +10716 m +4 h +802 m +4292 m +83 h +10717 m +11 h +1 h +4 h +4 h +1 h +1 h +10718 m +22 h +1 h +4 h +3909 m +1 h +10 h +3 h +10 h +10719 m +10720 m +1 h +208 m +1 h +1137 h +443 h +12 h +10 h +10 h +10721 m +10722 m +770 m +1 h +1 h +2038 m +1 h +74 h +1 h +124 h +1 h +10 h +1 h +5866 m +195 h +1768 m +4 h +10723 m +10 h +1 h +4 h +10724 m +1 h +10725 m +10726 m +649 m +10727 m +386 h +4 h +1 h +147 h +4 h +1 h +4 h +10728 m +97 h +1 h +10729 m +4 h +1 h +4 h +1 h +82 h +110 h +10 h +82 h +1 h +4 h +2540 m +1 h +4 h +4 h +196 h +125 h +6855 m +10730 m +327 m +124 h +10 h +536 h +1 h +1 h +1 h +10 h +10731 m +869 h +195 h +10 h +4 h +10732 m +10733 m +1 h +10734 m +3 h +10 h +1309 h +125 h +2172 h +1 h +41 h +1 h +65 h +10735 m +4 h +10736 m +10737 m +4 h +10738 m +1 h +57 h +2418 h +4 h +83 h +10739 m +4 h +74 h +97 h +842 m +1 h +27 h +4 h +1 h +10 h +109 h +45 h +4 h +10 h +1 h +11 h +4 h +10740 m +10 h +10 h +536 h +1564 m +4 h +488 h +4 h +10741 m +4 h +1 h +108 h +4 h +2591 m +10742 m +114 h +79 h +11 h +4 h +79 h +10743 m +10744 m +10745 m +4 h +31 h +10685 m +1 h +10746 m +123 h +73 h +10747 m +3303 m +41 h +4 h +4 h +10748 m +10 h +10 h +83 h +8040 m +10 h +10749 m +1 h +938 m +70 m +10 h +10750 m +1 h +10 h +10 h +10751 m +59 h +64 h +10 h +4 h +48 h +109 h +1 h +10 h +1359 m +10752 m +1 h +4 h +108 h +10 h +83 h +64 h +31 h +536 h +4 h +4 h +11 h +939 m +146 h +1 h +10 h +1 h +57 h +10753 m +10 h +56 h +195 h +10754 m +229 h +1 h +10755 m +10756 m +109 h +10757 m +10 h +11 h +147 h +3 h +295 h +196 h +4 h +10758 m +1 h +1 h +1 h +4 h +5470 m +10 h +219 m +4 h +1362 m +109 h +1 h +1 h +509 m +4 h +1 h +1 h +4 h +4 h +10759 m +4 h +185 h +4 h +10760 m +124 h +1 h +4 h +7119 m +10761 m +4 h +69 h +82 h +10762 m +4 h +10763 m +1137 h +97 h +27 h +1 h +4 h +10764 m +10765 m +10 h +11 h +10 h +1 h +7047 m +97 h +238 h +1 h +135 h +1 h +10 h +1 h +3088 m +1788 m +10 h +338 h +278 h +371 h +4 h +10766 m +10 h +4 h +536 h +4 h +238 h +203 m +1 h +10 h +57 h +181 h +25 h +10767 m +1 h +2308 m +4 h +4 h +10768 m +10 h +757 h +10 h +10769 m +10770 m +10 h +10 h +4 h +1 h +1 h +11 h +1 h +447 h +10771 m +135 h +4 h +4 h +10772 m +10 h +59 h +190 h +25 h +10773 m +4 h +1 h +1 h +124 h +10 h +11 h +10 h +4 h +4 h +1137 h +4 h +295 h +4 h +10774 m +4 h +10775 m +4 h +123 h +1 h +11 h +11 h +56 h +10 h +4 h +1 h +4 h +135 h +94 h +11 h +8133 m +1027 h +4 h +4 h +276 h +10 h +10 h +1 h +10776 m +10777 m +4 h +4 h +10778 m +10779 m +281 m +4 h +10780 m +1 h +4 h +1 h +11 h +4 h +3799 m +10781 m +10782 m +4 h +10 h +1 h +1 h +4 h +4 h +1 h +172 h +10783 m +104 h +4 h +10 h +5964 m +1 h +13 h +124 h +4 h +10784 m +4 h +55 h +2265 m +412 m +11 h +10 h +4 h +3 h +10785 m +10786 m +10 h +4 h +10 h +11 h +1 h +109 h +25 h +2379 h +265 h +10787 m +8386 m +976 h +10788 m +73 h +368 h +10789 m +10 h +10 h +10790 m +10 h +2594 m +1 h +4 h +10 h +10 h +10791 m +10792 m +10793 m +4 h +10794 m +4 h +1 h +10 h +10 h +1 h +1 h +1 h +10795 m +10796 m +65 h +4 h +10 h +10797 m +1 h +10 h +10798 m +4 h +146 h +4 h +10799 m +986 h +4 h +4 h +2265 m +4 h +4 h +10 h +1 h +10 h +4 h +10 h +10800 m +4 h +123 h +10 h +3 h +10 h +4 h +1137 h +10801 m +41 h +829 h +1 h +911 h +1 h +109 h +4 h +64 h +169 h +4 h +10 h +4 h +1 h +4 h +135 h +10 h +11 h +83 h +779 h +1 h +31 h +1 h +10802 m +10803 m +196 h +4 h +371 h +196 h +996 m +1 h +4 h +10804 m +307 h +4 h +1 h +8784 m +4 h +4 h +1 h +10805 m +10 h +3396 m +10806 m +2733 h +757 h +10 h +4 h +10807 m +10808 m +1 h +31 h +167 h +10 h +1 h +10809 m +1 h +1 h +11 h +1 h +1201 h +10810 m +1 h +10811 m +224 h +4 h +1 h +10812 m +10813 m +4 h +10814 m +10 h +10815 m +1 h +1 h +1027 h +11 h +203 h +10816 m +10817 m +57 h +190 h +97 h +104 h +45 h +25 h +41 h +4 h +278 h +1 h +10818 m +190 h +3 h +1 h +4 h +196 h +82 h +4 h +6766 m +1 h +45 h +10 h +10819 m +4 h +10 h +10820 m +4 h +4 h +938 m +10 h +10 h +1 h +1 h +3 h +114 h +10821 m +10822 m +10 h +110 h +4 h +10823 m +82 h +1 h +10824 m +1374 m +1 h +1 h +1886 m +4 h +7253 m +1 h +297 h +447 h +10 h +935 h +1 h +4 h +10825 m +124 h +3845 m +10826 m +10827 m +4 h +181 h +57 h +4033 m +10828 m +10829 m +4 h +1 h +12 h +332 h +10830 m +1 h +10831 m +82 h +399 h +10 h +10 h +1 h +10832 m +10833 m +8 h +10 h +4 h +4 h +109 h +11 h +1 h +911 h +4 h +4 h +138 h +57 h +10834 m +4 h +10 h +4 h +10835 m +4 h +1 h +56 h +4 h +109 h +238 h +10 h +1 h +1 h +112 h +113 h +1 h +4 h +4151 m +8332 m +1 h +12 h +25 h +4 h +11 h +55 h +266 h +4 h +10836 m +10837 m +1 h +10 h +109 h +10838 m +10839 m +1 h +1 h +124 h +82 h +10840 m +10 h +10 h +8332 m +186 h +10 h +10 h +4 h +1 h +1710 m +1 h +1 h +10 h +1016 h +4 h +4 h +4 h +10 h +4 h +4 h +108 h +4 h +10841 m +4 h +297 h +11 h +4 h +11 h +10 h +3161 m +10 h +10 h +4 h +1545 m +10 h +10842 m +109 h +10843 m +353 m +10844 m +57 h +4 h +265 h +4 h +10845 m +4 h +4 h +626 h +10 h +1 h +10846 m +10 h +1766 h +1 h +10 h +10847 m +11 h +10848 m +4 h +4 h +1 h +10849 m +83 h +195 h +11 h +185 h +1 h +147 h +4297 m +10850 m +10 h +4 h +229 h +1 h +10 h +1 h +4 h +4 h +4 h +181 h +10 h +10851 m +4 h +1 h +2442 m +10852 m +55 h +1 h +10 h +11 h +158 h +4 h +10853 m +1 h +10 h +1 h +912 m +10854 m +10855 m +10856 m +5445 m +10 h +4 h +195 h +2261 m +4 h +11 h +4 h +82 h +10857 m +4 h +10 h +10858 m +4668 m +10859 m +10860 m +10 h +184 h +11 h +10 h +6808 m +1 h +1410 m +1 h +10 h +4 h +10 h +10 h +1 h +10 h +10 h +170 h +97 h +10861 m +10862 m +1541 m +4 h +83 h +113 h +3 h +700 m +1 h +10863 m +10864 m +258 h +31 h +10865 m +3533 m +11 h +10 h +258 h +10866 m +22 h +1220 m +10867 m +10868 m +170 h +5917 m +11 h +3 h +59 h +109 h +10869 m +59 h +4 h +10870 m +4 h +31 h +1 h +10871 m +10872 m +307 h +1 h +4 h +10 h +4 h +10 h +1 h +3 h +10 h +1 h +270 h +185 h +11 h +4 h +10 h +10 h +1 h +10873 m +10874 m +10875 m +41 h +196 h +169 h +4 h +27 h +10876 m +1 h +4 h +93 h +41 h +10877 m +1 h +31 h +192 h +4 h +75 h +92 h +295 h +31 h +10 h +10 h +4 h +55 h +4 h +28 h +124 h +299 h +1674 m +10878 m +10879 m +1 h +1 h +4 h +10880 m +4 h +10 h +10881 m +10882 m +808 m +4 h +4 h +57 h +59 h +4 h +307 h +4 h +339 m +8386 m +10 h +167 h +10883 m +10884 m +10 h +57 h +10885 m +4 h +443 h +4 h +10 h +1620 h +10886 m +1 h +860 m +1 h +4 h +266 h +83 h +3 h +258 h +109 h +3 h +10 h +10887 m +4 h +25 h +566 m +65 h +10 h +1 h +319 h +265 h +10888 m +74 h +10 h +10889 m +964 m +4 h +4 h +322 m +338 h +4 h +10 h +569 h +1 h +64 h +10 h +692 h +1 h +1 h +11 h +4 h +25 h +1 h +4 h +1 h +4 h +1 h +4 h +10 h +10890 m +10 h +10 h +10891 m +10 h +10892 m +10893 m +1 h +10 h +10894 m +4 h +83 h +10 h +4 h +4 h +4 h +1 h +1 h +4 h +10 h +147 h +4 h +10895 m +806 m +399 h +10896 m +10897 m +3680 m +10898 m +11 h +11 h +1 h +138 h +92 h +1218 m +172 h +10 h +10899 m +4 h +1 h +11 h +4 h +169 h +108 h +83 h +4 h +10900 m +4 h +10 h +10901 m +10902 m +10 h +266 h +10903 m +10904 m +4542 m +83 h +45 h +4 h +1 h +192 h +10905 m +1 h +10906 m +1 h +10907 m +10908 m +10 h +4 h +10 h +10909 m +10 h +10 h +10 h +1 h +10910 m +33 h +10 h +1 h +10911 m +4 h +278 h +506 m +4 h +10912 m +9916 m +10913 m +4 h +74 h +41 h +4 h +74 h +4 h +4 h +10914 m +4 h +11 h +4 h +104 h +479 m +10 h +4 h +4 h +1 h +4 h +10 h +262 h +4 h +4 h +4 h +11 h +124 h +4 h +10 h +10915 m +124 h +10916 m +386 h +4 h +276 h +4 h +1 h +10 h +10 h +57 h +4 h +11 h +10917 m +4 h +386 h +4 h +140 h +10 h +10918 m +10919 m +74 h +1 h +4 h +4 h +25 h +10920 m +626 h +4 h +1 h +10921 m +158 h +692 h +4 h +4 h +2434 m +82 h +10 h +258 h +1 h +4 h +4 h +4 h +4 h +10922 m +10923 m +938 h +4 h +22 h +4 h +10 h +10 h +4 h +27 h +1 h +1 h +986 h +10924 m +10 h +447 h +10925 m +77 h +22 h +41 h +1 h +10 h +8 h +1 h +3 h +1 h +2265 h +4 h +4 h +195 h +3732 m +4 h +195 h +1 h +1 h +10926 m +1 h +4 h +1 h +4 h +156 h +4 h +1 h +10 h +65 h +2265 h +59 h +10927 m +10 h +10 h +82 h +1 h +10 h +1 h +250 h +97 h +229 h +295 h +10928 m +10929 m +1 h +1 h +1 h +9831 m +10 h +123 h +118 h +10930 m +10 h +82 h +83 h +4 h +83 h +4 h +229 h +1 h +4 h +93 h +10 h +556 h +4 h +1 h +83 h +4 h +1 h +4 h +1 h +10931 m +4 h +278 h +4 h +74 h +1 h +4 h +10 h +14 m +10932 m +4 h +4 h +4 h +4 h +10933 m +10 h +10 h +10 h +10934 m +4 h +2625 h +4 h +4 h +10935 m +4 h +4 h +10 h +10 h +10936 m +4 h +4 h +10 h +4 h +1 h +1 h +3 h +3836 m +113 h +10 h +4 h +4 h +82 h +3 h +83 h +109 h +4 h +1 h +278 h +31 h +59 h +885 m +4 h +114 h +1 h +4 h +1 h +4 h +383 h +1 h +57 h +59 h +447 h +4 h +1 h +10937 m +1 h +10 h +1 h +10938 m +11 h +1 h +4 h +10939 m +4 h +1 h +10940 m +10 h +1 h +10 h +4 h +97 h +10941 m +1 h +195 h +4 h +10 h +1 h +10942 m +10943 m +1 h +1 h +3 h +1 h +1 h +10944 m +2205 m +10945 m +7479 m +4 h +31 h +10 h +1 h +4 h +10 h +10 h +6378 m +11 h +48 h +2172 h +3 h +1 h +4 h +1 h +114 h +1 h +10 h +626 h +1 h +4 h +4 h +11 h +4 h +1508 m +332 h +1 h +10 h +4 h +10 h +1 h +4 h +109 h +170 h +33 h +10946 m +10947 m +4 h +10 h +57 h +10948 m +4 h +1 h +4 h +4 h +10949 m +1796 h +12 h +10950 m +383 h +4 h +1 h +1 h +8305 m +4 h +10951 m +10952 m +435 m +196 h +4 h +1 h +1 h +1 h +10 h +4 h +10 h +808 m +4 h +10953 m +10954 m +4 h +4 h +10 h +10955 m +10956 m +10957 m +4 h +10958 m +929 m +1 h +10959 m +31 h +4 h +10960 m +92 h +10961 m +25 h +4 h +4 h +41 h +12 h +276 h +358 h +10962 m +22 h +4 h +4 h +74 h +4 h +74 h +1 h +11 h +1 h +358 h +57 h +4 h +4 h +40 h +10 h +41 h +358 h +10 h +125 h +10963 m +241 m +74 h +10 h +1 h +4 h +386 h +10964 m +1 h +4 h +2374 m +10 h +4 h +10 h +4 h +1 h +10 h +295 h +1 h +146 h +114 h +10965 m +1 h +10 h +295 h +1 h +11 h +10966 m +10967 m +74 h +10968 m +1470 h +1 h +10 h +4 h +4 h +10 h +4 h +10969 m +4 h +4 h +10 h +25 h +4 h +3 h +10 h +4 h +143 h +1470 h +4 h +195 h +601 h +1 h +11 h +10 h +1 h +10970 m +4 h +123 h +4 h +195 h +3026 m +1 h +10971 m +10 h +1 h +169 h +59 h +123 h +25 h +1 h +10 h +10 h +1 h +10972 m +82 h +10 h +124 h +4 h +94 h +4 h +4 h +41 h +164 h +9 h +4 h +12 h +1 h +146 h +1 h +1 h +4 h +10973 m +1 h +83 h +1 h +195 h +1309 h +10974 m +82 h +1499 m +10 h +10975 m +11 h +10 h +1 h +4 h +10976 m +55 h +10977 m +65 h +4 h +10978 m +1 h +10979 m +4 h +36 h +1 h +463 h +1127 m +10980 m +1 h +31 h +104 h +124 h +1 h +4 h +1 h +55 h +10981 m +55 h +3 h +1 h +1 h +10982 m +97 h +1 h +4 h +10 h +297 h +276 h +1 h +10983 m +114 h +114 h +1 h +68 m +4 h +138 h +4 h +10984 m +4 h +10985 m +1 h +4 h +4124 m +10986 m +104 h +2625 h +10 h +10987 m +1 h +4 h +10988 m +10 h +4 h +10 h +1 h +4 h +10989 m +12 h +10 h +1 h +45 h +10990 m +1642 h +1016 h +4 h +10991 m +10 h +900 m +10 h +10992 m +8486 m +10993 m +11 h +4 h +2215 m +4 h +4 h +4 h +4 h +4 h +478 h +1 h +125 h +125 h +5869 m +4 h +4 h +57 h +11 h +4596 m +10 h +1 h +45 h +124 h +1 h +10 h +109 h +167 h +10 h +10994 m +10 h +10995 m +692 h +10996 m +124 h +578 h +10 h +10997 m +10 h +10998 m +10999 m +11000 m +59 h +4 h +4 h +11001 m +10 h +10 h +74 h +11002 m +4 h +10 h +1 h +11003 m +172 h +11004 m +4 h +10 h +10 h +4 h +10 h +313 h +181 h +10 h +1 h +1 h +36 h +4 h +64 h +11005 m +11006 m +4 h +11007 m +172 h +10 h +82 h +11008 m +10 h +113 h +11009 m +11010 m +1 h +83 h +1 h +11 h +1 h +1 h +4 h +1 h +4 h +4 h +4 h +1 h +10 h +1 h +103 m +11011 m +4 h +1083 h +4 h +4 h +4 h +11012 m +1 h +737 m +1 h +57 h +10 h +1 h +928 m +4 h +74 h +4 h +11013 m +383 h +3 h +4 h +119 h +4 h +8179 m +1 h +48 h +1 h +74 h +9228 m +4 h +8 h +190 h +1 h +2887 m +82 h +367 m +10 h +11014 m +10 h +4 h +146 h +11015 m +4 h +1 h +82 h +10324 m +520 h +1 h +4 h +1 h +82 h +139 h +1 h +443 h +11016 m +6668 m +10 h +295 h +11017 m +229 h +11018 m +1 h +10 h +11 h +10 h +4 h +307 h +124 h +57 h +196 h +4 h +7913 m +10 h +4 h +1 h +2484 m +4 h +25 h +4 h +3680 m +196 h +7798 m +279 h +10 h +57 h +82 h +11019 m +31 h +358 h +11020 m +4 h +172 h +11021 m +11022 m +4 h +11023 m +4 h +1 h +11024 m +4 h +4 h +1 h +4 h +11025 m +143 h +4 h +4 h +1 h +79 h +3 h +4 h +11026 m +1 h +11027 m +11028 m +3134 m +4 h +4 h +11029 m +31 h +109 h +74 h +1 h +13 h +11030 m +10 h +4 h +124 h +1 h +124 h +4 h +9065 m +4 h +140 h +10 h +10 h +4 h +10 h +10 h +10 h +59 h +185 h +1 h +3707 m +11 h +11031 m +1 h +11032 m +11033 m +11034 m +11035 m +4 h +124 h +4 h +10 h +4 h +1 h +1406 h +4 h +1 h +11036 m +11037 m +135 h +1 h +146 h +10 h +11038 m +4 h +1 h +83 h +11039 m +11 h +4 h +10 h +4 h +77 h +2412 m +3095 m +4 h +11040 m +1 h +569 h +692 h +1 h +571 m +1 h +3 h +1 h +4 h +4 h +124 h +11041 m +11042 m +10 h +1 h +1 h +11043 m +11044 m +31 h +5 h +27 h +1 h +79 h +135 h +11045 m +10 h +4 h +13 h +25 h +11046 m +10 h +10 h +7253 m +1 h +1 h +11047 m +1 h +4 h +118 h +10 h +4 h +4 h +11048 m +4 h +11049 m +11050 m +41 h +4 h +79 h +1 h +11051 m +4 h +4 h +11052 m +73 h +11053 m +1 h +4 h +1 h +11054 m +8 h +170 h +4 h +10 h +65 h +1 h +4 h +1 h +986 h +4 h +4 h +104 h +148 m +1 h +276 h +11055 m +11056 m +5206 m +1 h +11 h +1953 m +11057 m +4 h +1 h +10 h +41 h +11058 m +10 h +10 h +11059 m +4 h +69 h +11060 m +11061 m +4 h +4 h +1 h +11062 m +114 h +976 h +11063 m +1 h +129 h +10 h +4 h +3 h +10 h +31 h +692 h +10 h +1 h +464 h +83 h +10 h +11064 m +109 h +10 h +55 h +10 h +1 h +4 h +1 h +400 m +1 h +10 h +4 h +4 h +4 h +11065 m +11066 m +4 h +83 h +1 h +4 h +1 h +1 h +4 h +64 h +65 h +10 h +11067 m +10 h +10 h +4 h +1 h +1 h +3177 m +274 h +4 h +83 h +4 h +79 h +82 h +4 h +1 h +1 h +11068 m +11069 m +11070 m +11071 m +10 h +1 h +4 h +104 h +10 h +1359 m +10 h +8 h +265 h +99 m +4 h +4 h +1 h +4 h +11 h +13 h +11072 m +1 h +1 h +4 h +10 h +11073 m +1 h +10 h +2435 m +11074 m +172 h +83 h +82 h +10 h +40 h +1 h +11075 m +11076 m +4 h +11077 m +1 h +12 h +77 h +4 h +4 h +11078 m +124 h +4 h +10 h +1 h +10 h +10 h +192 h +11079 m +11080 m +4 h +124 h +11081 m +1 h +11082 m +10 h +4 h +1 h +295 h +4 h +601 h +1 h +520 h +4 h +386 h +1 h +11083 m +11084 m +11085 m +4 h +25 h +11086 m +11087 m +425 m +11088 m +1677 m +11089 m +181 h +601 h +4 h +4 h +65 h +464 h +157 h +4 h +11090 m +10 h +11091 m +307 h +1 h +1 h +169 h +1 h +10 h +1 h +1 h +11092 m +1650 h +11093 m +1 h +59 h +464 h +10 h +11094 m +57 h +11095 m +2116 m +109 h +11096 m +1 h +4 h +4 h +22 h +109 h +11097 m +4 h +4 h +79 h +173 h +114 h +11098 m +1137 h +1 h +10 h +4 h +10 h +4 h +730 m +11099 m +10689 m +4 h +4 h +11100 m +11101 m +10 h +11102 m +10 h +1981 m +3177 m +4 h +118 h +1 h +1 h +4 h +11103 m +10 h +4 h +146 h +11 h +4 h +10 h +11 h +10 h +4 h +11104 m +11105 m +4 h +1 h +2733 h +264 m +1 h +4 h +1105 h +4 h +36 h +996 m +158 h +1 h +11106 m +1 h +10 h +1 h +2245 m +12 h +124 h +164 h +83 h +196 h +11107 m +11108 m +8114 m +4 h +11109 m +4 h +1 h +11110 m +307 h +195 h +7535 m +11111 m +91 h +25 h +208 m +92 h +1 h +10 h +11112 m +11113 m +11114 m +4 h +4 h +4 h +196 h +359 h +11115 m +31 h +4 h +65 h +10 h +11116 m +167 h +250 h +124 h +4 h +1 h +4 h +10 h +57 h +10 h +11117 m +4 h +4 h +10 h +55 h +11118 m +4 h +1083 h +11119 m +1 h +10 h +110 h +11120 m +4030 m +11 h +11121 m +143 h +1 h +1309 h +976 h +11122 m +1 h +4 h +4 h +10 h +4 h +1 h +10 h +1 h +11123 m +4 h +3 h +10 h +4 h +331 m +11124 m +64 h +135 h +4 h +11125 m +147 h +5225 m +4 h +1 h +11126 m +1 h +4 h +4 h +11127 m +124 h +295 h +4 h +10 h +11128 m +172 h +4 h +11129 m +4 h +73 h +73 h +1261 h +5046 m +4 h +4 h +11130 m +1 h +1 h +10 h +10 h +11131 m +10 h +11132 m +11133 m +10 h +4 h +1 h +601 h +10 h +11134 m +10 h +279 h +4 h +11135 m +109 h +1 h +4 h +146 h +4 h +8035 m +569 h +8767 m +367 m +1 h +4 h +1 h +104 h +10 h +97 h +25 h +10 h +4 h +57 h +1 h +181 h +4 h +56 h +4 h +1 h +11136 m +1261 h +10 h +8571 m +7641 m +181 h +3293 m +109 h +59 h +11137 m +57 h +65 h +11138 m +73 h +10 h +11139 m +1771 m +4 h +4 h +2591 m +11 h +1 h +59 h +11140 m +10 h +4 h +4 h +2887 m +4 h +11141 m +3 h +994 m +11142 m +11143 m +41 h +110 h +4 h +113 h +1 h +4 h +1 h +11144 m +11145 m +4 h +1 h +556 h +11146 m +1 h +4 h +1 h +97 h +10 h +4 h +31 h +109 h +147 h +82 h +83 h +139 h +935 h +11147 m +4 h +10 h +10 h +4 h +1016 h +1 h +1 h +11148 m +4 h +447 h +123 h +1 h +97 h +12 h +10 h +1 h +3 h +338 h +10 h +307 h +1796 h +74 h +4 h +57 h +1 h +4 h +59 h +1 h +1 h +77 h +1 h +4 h +11149 m +82 h +11150 m +5785 m +11151 m +11152 m +104 h +11153 m +25 h +11 h +11154 m +11155 m +74 h +1 h +11156 m +104 h +82 h +258 h +41 h +10 h +10 h +1 h +11157 m +10 h +10 h +4 h +109 h +11158 m +65 h +4 h +59 h +4 h +3 h +1 h +139 h +11159 m +1089 m +10 h +9691 m +1 h +4 h +45 h +4 h +83 h +4 h +11160 m +11161 m +11162 m +190 h +3 h +10 h +1 h +1337 m +10 h +11 h +135 h +28 h +4 h +3 h +1 h +10 h +10 h +31 h +443 h +4 h +1 h +1 h +4 h +11163 m +11164 m +4 h +4 h +10 h +1 h +195 h +4 h +10 h +1 h +22 h +4 h +3 h +146 h +11165 m +256 m +45 h +11166 m +2788 h +1 h +10 h +4 h +779 h +11167 m +1 h +4 h +110 h +1 h +1 h +82 h +2887 h +4 h +4 h +4 h +3455 m +4 h +10 h +1 h +4 h +1 h +10 h +11168 m +3 h +4 h +1 h +10 h +57 h +170 h +10 h +1 h +11 h +10 h +444 m +55 h +1 h +11169 m +11170 m +103 h +4 h +10 h +109 h +11171 m +4 h +11172 m +4 h +94 h +1389 m +4 h +4 h +1 h +4 h +359 h +11173 m +4 h +1 h +10 h +10 h +4 h +79 h +1 h +146 h +10 h +11174 m +10 h +4 h +11175 m +1 h +11176 m +10283 m +11177 m +10 h +4 h +1 h +2840 m +82 h +4 h +4 h +11178 m +10 h +6469 m +10111 m +1 h +1 h +10 h +4 h +4 h +358 h +278 h +10 h +4 h +4 h +10 h +4 h +4 h +1 h +125 h +10 h +4 h +1 h +11 h +1 h +4 h +4 h +11179 m +9372 m +4 h +4 h +11180 m +4 h +3272 m +1201 h +11181 m +383 h +1 h +10 h +3737 m +1 h +11182 m +11183 m +1 h +208 m +11184 m +1 h +11185 m +297 h +1737 m +10 h +11186 m +11187 m +83 h +4 h +11188 m +4 h +73 h +1 h +10 h +11189 m +1092 m +11190 m +4 h +4 h +11191 m +57 h +10 h +10 h +10 h +1 h +4 h +172 h +4 h +4 h +11192 m +83 h +104 h +1 h +4 h +1691 m +1 h +4 h +10 h +1 h +4 h +1 h +59 h +1482 m +11193 m +1 h +1 h +6139 m +73 h +11194 m +4 h +11 h +11 h +59 h +1 h +1 h +10 h +4 h +11195 m +4 h +2374 m +10 h +10 h +4 h +11196 m +10 h +990 m +1 h +64 h +208 h +536 h +83 h +4 h +5567 m +10 h +11197 m +1 h +11198 m +4 h +1 h +1 h +276 h +1 h +11199 m +11200 m +358 h +56 h +4 h +4 h +185 h +10 h +59 h +11201 m +4 h +57 h +939 m +4 h +10 h +4 h +109 h +996 m +4 h +4 h +109 h +185 h +1 h +4 h +11202 m +23 h +11203 m +10 h +11204 m +55 h +11205 m +5976 m +13 h +59 h +4 h +11 h +276 h +11206 m +10 h +11207 m +4 h +569 h +1 h +11208 m +4 h +11 h +11 h +41 h +10 h +10 h +181 h +64 h +11209 m +11 h +41 h +4 h +10 h +11210 m +4 h +4 h +10 h +196 h +1 h +4 h +3 h +3679 m +2883 m +10 h +1 h +4 h +10 h +10 h +322 m +11211 m +1 h +82 h +1016 h +65 h +31 h +48 h +146 h +11 h +1 h +1 h +11 h +4 h +10 h +4857 m +229 h +11212 m +1 h +4 h +1 h +13 h +11213 m +1 h +10 h +488 h +10 h +1 h +4 h +10 h +4 h +1 h +11214 m +4 h +1 h +1 h +2433 m +11215 m +11216 m +11217 m +11218 m +358 h +10 h +11219 m +10 h +10 h +10 h +83 h +4 h +10 h +1 h +10 h +11220 m +4 h +4 h +11221 m +4 h +157 h +1 h +82 h +104 h +4 h +10 h +11222 m +185 h +4 h +3 h +4 h +4 h +195 h +11223 m +146 h +4 h +13 h +11224 m +4 h +4 h +1868 m +3 h +4 h +1 h +11225 m +4 h +10 h +11226 m +10 h +1 h +10 h +4 h +4 h +1 h +82 h +147 h +4 h +1 h +1 h +31 h +10 h +383 h +4 h +4 h +1 h +118 h +4 h +4 h +1 h +443 h +1 h +447 h +169 h +371 h +4 h +1 h +2733 h +4 h +10 h +11227 m +10 h +4 h +4 h +11228 m +69 h +1 h +1 h +1 h +322 m +1 h +1 h +11229 m +146 h +4 h +11 h +4 h +83 h +2309 m +10 h +147 h +11230 m +11 h +4 h +10 h +41 h +59 h +1 h +10 h +4 h +143 h +4 h +4 h +1 h +4 h +9501 m +45 h +1 h +170 h +41 h +138 h +173 h +4 h +1 h +109 h +4 h +4 h +4 h +4 h +114 h +3 h +172 h +12 h +4 h +74 h +10 h +27 h +11231 m +265 h +10 h +109 h +4 h +10 h +1 h +10 h +1 h +10 h +74 h +1 h +1 h +10 h +11232 m +4 h +1253 m +10 h +6855 m +4 h +4 h +57 h +31 h +11233 m +10 h +192 h +125 h +1 h +11234 m +4 h +146 h +10 h +1 h +10 h +170 h +4 h +167 h +4 h +1 h +11 h +1 h +4 h +2846 m +5199 m +11235 m +124 h +11 h +4 h +11236 m +4 h +10 h +4 h +10 h +289 h +11237 m +4 h +4 h +3768 m +11238 m +158 h +119 h +1 h +36 h +4 h +147 h +1 h +338 h +4 h +109 h +83 h +1 h +112 h +11239 m +11240 m +1 h +11241 m +4 h +4 h +4 h +125 h +477 m +109 h +4 h +10 h +1 h +110 h +11242 m +1 h +10 h +10 h +10 h +11243 m +4 h +97 h +82 h +10 h +11 h +13 h +1 h +4 h +435 m +11244 m +1 h +41 h +4 h +181 h +1 h +11245 m +4 h +1 h +4 h +11246 m +1 h +25 h +11247 m +11248 m +1 h +10 h +31 h +2958 m +11249 m +10 h +10 h +11250 m +1 h +4 h +11251 m +10 h +1030 h +125 h +1261 h +1 h +1 h +10 h +40 h +4 h +4 h +2265 h +1330 m +533 h +4 h +1 h +10 h +11252 m +11253 m +1 h +11254 m +57 h +4 h +124 h +1 h +59 h +4292 m +4 h +110 h +1 h +97 h +1 h +11255 m +4 h +59 h +83 h +109 h +10 h +4 h +10 h +1 h +11256 m +1 h +4 h +170 h +4 h +10 h +110 h +4 h +45 h +11257 m +4 h +11258 m +4 h +11259 m +4 h +11 h +1 h +4 h +10 h +4 h +11260 m +1 h +11261 m +403 h +1 h +10 h +41 h +11262 m +1374 m +169 h +11263 m +27 h +45 h +11264 m +583 h +1691 m +4 h +11 h +4 h +10 h +4 h +4 h +11265 m +4 h +69 h +5053 m +11266 m +1 h +1619 h +185 h +1 h +3 h +4 h +11267 m +4 h +258 h +1 h +4 h +1 h +11 h +1 h +4 h +11268 m +11269 m +11270 m +59 h +1 h +11271 m +10 h +4 h +11272 m +4 h +10 h +83 h +11273 m +353 m +124 h +1 h +4 h +11274 m +4 h +94 h +10 h +1 h +1 h +10 h +1880 m +10 h +1 h +1 h +1 h +11275 m +11276 m +1 h +25 h +64 h +1 h +4 h +56 h +1 h +11277 m +4 h +11278 m +4 h +10 h +11279 m +11280 m +147 h +103 h +10 h +83 h +56 h +1 h +10 h +11281 m +11282 m +770 m +5526 m +11283 m +129 h +11284 m +10 h +124 h +195 h +1 h +1714 m +10 h +10 h +125 h +169 h +55 h +9139 m +4 h +11285 m +1 h +1 h +11 h +4 h +4 h +1691 h +4 h +630 m +124 h +4 h +10 h +1 h +4 h +4 h +1 h +11286 m +4 h +4 h +1 h +11 h +1 h +97 h +4 h +1 h +3 h +1 h +1454 m +1 h +3 h +10 h +10 h +4 h +4 h +265 h +41 h +11287 m +4 h +10 h +11288 m +55 h +4 h +1 h +1 h +4 h +1137 h +11289 m +4 h +10 h +25 h +4 h +1 h +10 h +114 h +4 h +1 h +1 h +10 h +10 h +11290 m +4 h +1 h +10 h +10 h +109 h +4 h +11291 m +25 h +3141 m +4 h +3 h +10 h +10 h +4 h +1822 m +119 h +12 h +1 h +3 h +82 h +4 h +36 h +73 h +4 h +692 h +1 h +1 h +11292 m +4 h +11293 m +10 h +1 h +4 h +22 h +124 h +11294 m +10 h +11295 m +11296 m +2794 m +64 h +1 h +4 h +4 h +146 h +195 h +10 h +1 h +10 h +109 h +79 h +169 h +1 h +358 h +11297 m +103 h +4 h +10 h +4 h +4 h +1 h +10 h +4 h +11298 m +1 h +10 h +1 h +1 h +4 h +4 h +10 h +82 h +10 h +262 h +11299 m +10 h +10 h +6129 m +114 h +31 h +11300 m +1 h +57 h +10 h +82 h +4 h +4 h +4 h +1 h +4 h +109 h +443 h +11301 m +11 h +4 h +10 h +25 h +1 h +123 h +1 h +74 h +6770 m +10 h +493 m +11 h +11302 m +4 h +4 h +1 h +10 h +4 h +1 h +11303 m +1470 h +4 h +4 h +11304 m +11305 m +4 h +11 h +4 h +4 h +1 h +11306 m +11 h +11307 m +97 h +1 h +10 h +109 h +4 h +1 h +10 h +4 h +11308 m +10 h +11309 m +11310 m +4 h +25 h +1 h +258 h +10 h +4 h +195 h +74 h +536 h +10 h +10 h +801 m +1 h +2002 m +109 h +10 h +11311 m +4714 m +11312 m +4 h +82 h +4 h +1 h +10 h +1 h +1 h +4 h +11313 m +172 h +1 h +109 h +27 h +1 h +1 h +10 h +4 h +4 h +1 h +4 h +109 h +25 h +11 h +1 h +640 m +11314 m +4 h +10 h +1 h +4 h +4 h +11315 m +4 h +109 h +4 h +4 h +11 h +11316 m +1478 h +4 h +1 h +2235 m +59 h +4 h +4 h +1 h +11 h +11317 w +1372 m +11318 m +11319 m +359 h +11320 m +4 h +1089 m +10 h +73 h +1 h +1 h +1 h +59 h +1 h +11321 m +4 h +1 h +10 h +97 h +10 h +1 h +4 h +4 h +843 m +4 h +4 h +4 h +1 h +4 h +1 h +3 h +601 h +4 h +11322 m +447 h +10 h +4 h +4 h +4 h +10 h +11323 m +1 h +11324 m +1 h +10 h +1 h +123 h +4 h +11325 m +4 h +4 h +10 h +1470 h +3240 m +4 h +11326 m +1 h +10 h +10 h +11327 m +11328 m +4 h +601 h +41 h +4 h +147 h +4 h +13 h +4 h +11329 m +10 h +57 h +258 h +10 h +10 h +208 h +1 h +83 h +4 h +11330 m +11331 m +1 h +11 h +1 h +1 h +359 h +48 h +10 h +4 h +59 h +11332 m +1 h +41 h +1 h +4 h +146 h +4 h +1 h +4 h +4 h +1 h +4338 m +10 h +11333 m +1 h +10 h +10 h +3351 m +31 h +10 h +10 h +4 h +195 h +10 h +10 h +119 h +4 h +10 h +10 h +40 h +4 h +11334 m +25 h +139 h +146 h +97 h +125 h +147 h +1 h +1 h +11335 m +11336 m +10 h +4 h +147 h +11337 m +1 h +103 h +1403 h +10 h +123 h +11 h +11338 m +1 h +4 h +3 h +1 h +11339 m +8879 m +1 h +11340 m +10 h +10 h +11341 m +4 h +74 h +11342 m +1 h +4 h +158 h +4 h +10 h +4 h +4 h +74 h +4 h +4 h +4 h +11343 m +4 h +11344 m +1 h +10 h +272 m +11345 m +4 h +10 h +174 m +4 h +1 h +425 m +1 h +1 h +2205 m +4 h +109 h +10 h +4 h +270 h +1 h +56 h +3 h +11346 m +31 h +109 h +1820 m +1 h +4 h +10 h +1 h +195 h +1 h +10 h +4 h +11347 m +10345 m +1 h +11348 m +1 h +10 h +1 h +11349 m +10 h +258 h +140 h +11350 m +79 h +11351 m +3089 m +11352 m +186 h +1 h +6558 m +4 h +10 h +1 h +4 h +10 h +4 h +1766 h +1 h +11353 m +1 h +109 h +169 h +4 h +11354 m +10 h +97 h +73 h +4 h +1780 m +11355 m +1 h +11 h +3 h +10 h +10 h +4 h +11356 m +11 h +123 h +11357 m +4 h +64 h +10 h +4 h +119 h +4 h +181 h +4 h +3150 m +56 h +1 h +4 h +11358 m +4 h +1722 m +698 m +10 h +11359 m +4 h +443 h +4 h +65 h +6144 m +4 h +10 h +4 h +4 h +13 h +25 h +307 h +4 h +157 h +22 h +1 h +196 h +12 h +371 h +1 h +4 h +1 h +4 h +1 h +4 h +1 h +4 h +11360 m +119 h +36 h +4 h +11361 m +11362 m +4 h +224 h +1 h +109 h +97 h +4 h +10 h +4 h +4 h +12 h +4 h +11363 m +5863 m +4 h +10 h +10 h +1442 m +1 h +10 h +82 h +601 h +1 h +11364 m +11365 m +10 h +1 h +59 h +124 h +10 h +1 h +1 h +698 m +11 h +1 h +4 h +1 h +10 h +11366 m +11367 m +4 h +11 h +1 h +1 h +109 h +10 h +1 h +10 h +1 h +204 h +4 h +11368 m +10 h +4 h +11369 m +4 h +1 h +12 h +57 h +4 h +11370 m +4 h +55 h +11371 m +4 h +146 h +1796 h +83 h +10 h +11372 m +478 h +4 h +4 h +4 h +41 h +11 h +266 h +1 h +155 m +1 h +124 h +642 m +92 h +1847 m +11373 m +147 h +1 h +10 h +4 h +104 h +4 h +104 h +763 m +1 h +10 h +1 h +1 h +914 m +11374 m +4 h +7913 m +1 h +4 h +10 h +22 h +4 h +1 h +1 h +10 h +4 h +1 h +10 h +11375 m +4 h +1 h +3 h +4 h +4 h +94 h +4 h +11376 m +124 h +135 h +4 h +7243 m +1 h +368 h +11377 m +1 h +4 h +582 m +1 h +1 h +11378 m +367 h +4 h +698 h +4 h +4 h +4 h +4 h +1 h +10 h +377 m +11379 m +1 h +10 h +4 h +124 h +57 h +10 h +11380 m +1 h +11381 m +4 h +25 h +1 h +1 h +4 h +10 h +4 h +3 h +10 h +9075 m +119 h +1 h +11382 m +1 h +1685 h +1 h +10 h +11383 m +229 h +104 h +1116 m +4 h +114 h +1 h +4 h +10 h +1 h +4 h +185 h +5145 m +4 h +4 h +4 h +4 h +125 h +3877 m +4 h +4 h +59 h +10 h +270 h +11384 m +125 h +1 h +11385 m +172 h +2090 m +9120 m +692 h +4 h +4 h +11386 m +4 h +258 h +1 h +10 h +1 h +4 h +307 h +1 h +4 h +10 h +512 m +4 h +4 h +125 h +4 h +10 h +4 h +1 h +4 h +4 h +11387 m +12 h +4 h +4 h +124 h +4 h +10 h +11388 m +278 h +10 h +1 h +4 h +4 h +10 h +1 h +11389 m +4 h +1 h +10 h +11390 m +4 h +4 h +11 h +3028 m +10 h +10 h +10 h +10 h +11 h +11391 m +3 h +4 h +125 h +1 h +10 h +4 h +11392 m +1 h +11 h +1 h +11393 m +1 h +164 h +4 h +1 h +1 h +1 h +1 h +146 h +4 h +104 h +1 h +11394 m +172 h +4 h +27 h +4 h +82 h +10 h +4 h +1 h +79 h +1 h +4 h +1 h +4 h +10 h +11395 m +10 h +266 h +295 h +1 h +10 h +10 h +1 h +4 h +10 h +1493 m +1027 h +4 h +11 h +65 h +4 h +1822 m +4 h +10 h +1 h +4 h +4 h +4 h +11396 m +167 h +1 h +181 h +114 h +1 h +1 h +11397 m +10 h +11 h +5122 m +4904 m +1 h +4 h +488 h +1 h +1 h +10 h +11398 m +9669 m +156 h +125 h +10 h +4 h +1 h +4 h +3 h +556 h +383 h +1 h +10 h +601 h +1 h +4 h +11399 m +1 h +4 h +10 h +10 h +10 h +11400 m +1403 h +125 h +1 h +48 h +4 h +4 h +1 h +1089 m +4 h +1089 h +4 h +7243 m +41 h +10 h +1 h +4 h +8 h +11401 m +11402 m +11403 m +164 h +256 m +1 h +10 h +4 h +10 h +626 h +1 h +1 h +278 h +55 h +10 h +4 h +11404 m +31 h +10 h +10 h +4 h +4 h +4 h +9372 m +65 h +10 h +4 h +190 h +4 h +1780 m +1 h +11405 m +11406 m +146 h +4 h +11 h +10 h +55 h +22 h +1 h +1 h +123 h +278 h +4 h +82 h +4 h +11407 m +83 h +1 h +4 h +91 h +4 h +4 h +11408 m +10 h +129 h +11409 m +4 h +104 h +11410 m +4 h +11411 m +70 m +9411 m +10 h +11412 m +2532 m +11413 m +4 h +4 h +2045 m +1 h +11414 m +10 h +1 h +83 h +22 h +10 h +1261 h +4 h +1780 h +4 h +1 h +1 h +11415 m +1 h +11416 m +110 h +4 h +4 h +4 h +4 h +11417 m +4 h +10 h +69 h +1 h +59 h +4 h +1 h +11418 m +83 h +11419 m +642 m +59 h +4 h +25 h +1 h +4 h +83 h +1697 m +10 h +1 h +3680 h +4 h +11420 m +10 h +1 h +36 h +3089 m +79 h +1 h +167 h +10 h +10 h +4 h +10 h +11421 m +124 h +11 h +57 h +109 h +11422 m +11423 m +10 h +10 h +1 h +10 h +11424 m +1 h +124 h +4 h +1053 m +4 h +4 h +1 h +4 h +4 h +488 h +1 h +113 h +57 h +195 h +4 h +1 h +4 h +10 h +1 h +73 h +10 h +1 h +358 h +11425 m +1 h +11426 m +1 h +11427 m +1 h +2887 h +1 h +11428 m +4 h +75 h +4 h +563 m +10 h +1 h +4 h +278 h +65 h +278 h +319 h +22 h +1 h +11429 m +11430 m +1 h +1939 m +10 h +1 h +1 h +1 h +10 h +4 h +4 h +3 h +10 h +11431 m +8212 m +125 h +10 h +1 h +2788 h +4 h +1 h +11432 m +167 h +4 h +4 h +124 h +1 h +626 h +11 h +125 h +4 h +123 h +25 h +125 h +11433 m +4 h +10 h +2508 m +8889 m +1 h +82 h +11434 m +4 h +11435 m +911 h +1 h +4 h +368 h +10 h +65 h +11436 m +4 h +164 h +25 h +4177 m +1 h +11437 m +258 h +4 h +11438 m +4 h +4 h +129 h +4 h +1 h +3396 m +65 h +167 h +3484 m +195 h +1 h +4 h +1 h +10 h +4 h +258 h +3 h +11439 m +11440 m +11441 m +1 h +22 h +9335 m +25 h +4 h +172 h +1038 m +45 h +73 h +170 h +1650 h +578 h +10 h +11442 m +4516 m +1 h +10 h +11443 m +377 m +1619 h +4 h +536 h +10 h +4 h +1646 m +1 h +1 h +10 h +1016 h +4 h +5526 m +82 h +11444 m +94 h +31 h +4 h +185 h +4 h +1184 m +124 h +31 h +11445 m +124 h +4 h +1 h +73 h +11446 m +169 h +4 h +4 h +4 h +11447 m +6129 m +4 h +10 h +4 h +2126 m +158 h +11448 m +1 h +11449 m +22 h +4 h +11450 m +11451 m +1642 h +11 h +10 h +4 h +11452 m +1185 m +11453 m +11454 m +11455 m +23 h +118 h +25 h +1595 m +1 h +91 h +1 h +73 h +4 h +4 h +4 h +4 h +82 h +25 h +1 h +10 h +74 h +104 h +1 h +11456 m +83 h +11457 m +109 h +143 h +109 h +108 h +10 h +10 h +4 h +10 h +5 h +4 h +4 h +4 h +56 h +1 h +79 h +4 h +4 h +4 h +10 h +4 h +11458 m +36 h +5869 m +4 h +12 h +1 h +4 h +4 h +359 h +11459 m +11 h +1 h +11460 m +13 h +11461 m +10 h +1 h +11462 m +4 h +4 h +4 h +105 m +11 h +1 h +4 h +164 h +1 h +11463 m +11464 m +11465 m +1564 m +11466 m +48 h +11467 m +1017 m +10 h +11468 m +109 h +433 m +83 h +4 h +124 h +10 h +10 h +11469 m +1 h +1 h +1 h +10 h +262 h +4 h +97 h +1 h +3 h +11 h +119 h +10 h +11470 m +11471 m +10 h +3768 m +10 h +11472 m +4 h +262 h +10 h +10 h +1 h +82 h +1 h +4 h +1 h +10 h +4 h +4 h +4 h +172 h +11473 m +1 h +4 h +11474 m +11475 m +266 h +7395 m +10 h +10 h +10 h +59 h +1 h +1 h +57 h +1261 h +83 h +4 h +1 h +4 h +123 h +3 h +4 h +11476 m +10 h +4 h +1 h +3184 m +11477 m +1 h +4 h +1 h +1 h +1 h +11478 m +224 h +118 h +4 h +91 h +1 h +1 h +109 h +64 h +82 h +146 h +11 h +57 h +4 h +10 h +4 h +11 h +1 h +10 h +1 h +10 h +1 h +4 h +1977 m +4 h +135 h +10 h +11479 m +10 h +10 h +10 h +4 h +10 h +4 h +4 h +11480 m +124 h +1642 h +1 h +1 h +1 h +214 m +4 h +10 h +4127 m +399 h +1 h +10 h +7938 m +11 h +109 h +4 h +11481 m +82 h +1 h +1556 m +4 h +1 h +1754 m +900 m +11482 m +11483 m +4 h +11484 m +25 h +181 h +124 h +1 h +57 h +2423 m +4 h +11485 m +83 h +4 h +1 h +4 h +41 h +1030 h +10 h +4 h +164 h +10 h +4 h +11486 m +1 h +11487 m +10512 m +10 h +11 h +124 h +4 h +36 h +10 h +27 h +11488 m +4 h +1 h +4 h +10 h +4 h +1 h +11489 m +97 h +10 h +146 h +28 h +1 h +146 h +10 h +124 h +4 h +10 h +143 h +57 h +11490 m +6187 m +74 h +181 h +74 h +1 h +4 h +10 h +1 h +83 h +97 h +2128 m +10 h +1403 h +8610 m +1261 h +190 h +164 h +11491 m +4 h +97 h +4 h +31 h +57 h +10 h +4 h +11492 m +1 h +4 h +170 h +433 m +1 h +11493 m +11494 m +11495 m +158 h +4 h +11496 m +4 h +4 h +1 h +10 h +4 h +1 h +10 h +1 h +11497 m +11498 m +10 h +11499 m +11500 m +1017 m +289 h +3161 m +10 h +56 h +11501 m +11502 m +1 h +4 h +11 h +82 h +4 h +135 h +4 h +1 h +11503 m +190 h +733 m +65 h +601 h +125 h +97 h +11504 m +4 h +186 h +10 h +1 h +11505 m +241 m +83 h +412 m +10 h +125 h +11506 m +4 h +10 h +59 h +83 h +146 h +4 h +3 h +6461 m +190 h +4 h +59 h +11507 m +190 h +8 h +11 h +9156 m +1 h +11508 m +4 h +11509 m +97 h +10 h +4 h +109 h +1 h +41 h +143 h +11510 m +1 h +11511 m +4 h +987 m +10 h +4 h +65 h +1 h +11512 m +124 h +1 h +11513 m +4 h +10 h +307 h +10 h +4 h +808 h +10 h +11514 m +11515 m +4 h +4 h +135 h +124 h +1 h +10 h +185 h +4 h +332 h +1 h +1 h +1 h +358 h +857 m +1 h +10 h +45 h +147 h +11 h +10 h +4 h +4 h +10 h +11516 m +4 h +447 h +285 m +1 h +10 h +10 h +109 h +1260 h +4 h +4 h +104 h +31 h +11517 m +4 h +164 h +219 m +10 h +11518 m +4 h +109 h +10 h +11519 m +4 h +94 h +10 h +1 h +11 h +11520 m +124 h +196 h +22 h +11521 m +4 h +125 h +164 h +11522 m +11141 m +92 h +10 h +10 h +11523 m +1 h +1 h +1 h +10 h +1 h +13 h +4 h +4 h +10 h +1 h +4 h +11524 m +5017 m +6381 m +4 h +10 h +219 m +11525 m +4 h +1 h +4 h +79 h +195 h +10 h +11526 m +82 h +11527 m +164 h +1 h +82 h +11528 m +125 h +10 h +1 h +10 h +1 h +1 h +11529 m +1 h +10 h +4 h +4 h +4 h +4 h +4 h +11530 m +3 h +4 h +123 h +1 h +4 h +911 h +11531 m +3 h +36 h +10 h +11532 m +229 h +383 h +4 h +11533 m +1 h +4 h +371 h +59 h +10 h +1 h +45 h +10 h +4 h +2442 m +4 h +94 h +4 h +55 h +4 h +11534 m +119 h +10 h +4 h +4 h +1 h +4 h +10 h +4 h +9385 m +11535 m +459 m +11536 m +11537 m +1137 h +4 h +10 h +1 h +10 h +4 h +11538 m +1 h +83 h +11539 m +196 h +1 h +1 h +692 h +11540 m +83 h +11541 m +4 h +172 h +190 h +3558 m +82 h +11 h +1 h +4 h +1 h +1 h +11542 m +11543 m +4 h +10 h +114 h +11544 m +4 h +1 h +4 h +11545 m +11546 m +4 h +82 h +11547 m +4 h +10 h +11548 m +11 h +4 h +4 h +359 h +11 h +104 h +11549 m +1 h +4 h +4 h +11 h +4 h +10 h +1 h +4 h +60 m +31 h +965 m +1 h +4 h +1016 h +11550 m +11551 m +11552 m +4 h +1281 m +113 h +4 h +10 h +12 h +11553 m +1 h +57 h +4 h +806 m +4 h +82 h +4 h +11554 m +2840 m +4 h +4 h +4 h +31 h +41 h +10 h +10 h +1 h +4 h +10 h +10 h +10 h +82 h +10 h +1185 m +11555 m +10 h +1 h +11556 m +11557 m +1 h +124 h +4 h +11558 m +65 h +158 h +73 h +11559 m +1 h +190 h +119 h +185 h +11 h +11 h +1 h +10 h +11560 m +1 h +1 h +31 h +4 h +10 h +4 h +74 h +11561 m +4 h +4 h +4256 m +11 h +1250 h +359 h +1 h +11562 m +307 h +11563 m +11564 m +4 h +493 m +4 h +10 h +10 h +94 h +4 h +4 h +2537 m +1 h +250 h +4 h +4 h +84 m +11565 m +1 h +1038 m +1 h +11566 m +41 h +219 h +4 h +196 h +11567 m +4 h +10 h +104 h +1 h +195 h +10 h +11568 m +4 h +11569 m +371 h +11570 m +79 h +1 h +55 h +10 h +196 h +11571 m +1403 h +10 h +10 h +4 h +10 h +1105 h +10 h +11572 m +11573 m +11574 m +11575 m +83 h +11576 m +22 h +4 h +4 h +1 h +11485 m +10 h +11577 m +11578 m +11579 m +125 h +4 h +4 h +109 h +1 h +79 h +10 h +4 h +1 h +1 h +11580 m +195 h +11581 m +332 h +1 h +656 m +4 h +4 h +25 h +11582 m +164 h +10 h +56 h +104 h +156 h +4 h +1 h +93 h +4 h +4 h +3025 m +1 h +11583 m +11584 m +1 h +124 h +10 h +1 h +11585 m +11586 m +4 h +167 h +11587 m +10 h +129 h +1 h +1 h +59 h +11588 m +4 h +36 h +1478 h +4 h +138 h +11589 m +11 h +150 m +4 h +4 h +11590 m +5093 m +1 h +229 h +4 h +1 h +11591 m +11592 m +3 h +1 h +10 h +1 h +74 h +4 h +1030 h +11593 m +1 h +4 h +1 h +4735 m +10 h +1016 h +1 h +1016 h +82 h +757 h +1 h +4 h +5475 m +11594 m +11595 m +4 h +1 h +11596 m +4 h +11597 m +57 h +11598 m +11599 m +74 h +10 h +109 h +11600 m +4 h +1 h +10 h +57 h +10 h +11601 m +11602 m +3 h +11603 m +1 h +10 h +1 h +1261 h +10 h +368 h +272 m +25 h +11604 m +31 h +10 h +59 h +97 h +11605 m +11606 m +1 h +11607 m +4 h +4 h +1027 h +11 h +1 h +1 h +1 h +57 h +493 m +1 h +1 h +10 h +110 h +4 h +4 h +11608 m +27 h +4 h +10 h +4 h +11 h +4 h +119 h +22 h +10 h +45 h +1017 h +11609 m +4 h +278 h +196 h +74 h +4 h +447 h +857 m +1 h +36 h +1444 m +172 h +11610 m +41 h +4 h +195 h +125 h +557 m +11611 m +4 h +238 m +4 h +82 h +4 h +48 h +55 h +4 h +4 h +11 h +8496 m +57 h +11612 m +10 h +1 h +1 h +1 h +1 h +181 h +10 h +4 h +371 h +11613 m +1678 m +11614 m +11615 m +1 h +10 h +1 h +11616 m +533 h +10 h +11617 m +41 h +11618 m +4 h +1 h +1 h +1 h +11619 m +10 h +83 h +8 h +59 h +1 h +1 h +4 h +4 h +135 h +135 h +4 h +11620 m +1 h +195 h +1 h +506 m +59 h +1 h +238 h +278 h +4 h +57 h +4 h +4 h +11621 m +4 h +272 m +59 h +10 h +700 m +57 h +59 h +4 h +4 h +59 h +4 h +11622 m +1278 m +1 h +1 h +10 h +11623 m +10 h +1 h +5650 m +11624 m +10 h +10 h +104 h +1 h +1 h +1 h +41 h +282 m +11625 m +1 h +4 h +4 h +8332 h +31 h +316 m +4 h +238 h +119 h +82 h +11626 m +238 h +4 h +4 h +11627 m +367 h +1 h +59 h +258 h +10 h +10 h +10 h +119 h +266 h +4 h +11628 m +1 h +258 h +2300 m +4 h +4 h +4 h +4 h +10 h +3 h +11 h +25 h +10 h +4 h +31 h +4 h +158 h +4 h +4 h +11629 m +31 h +64 h +139 h +4 h +57 h +4 h +11630 m +10 h +12 h +31 h +1 h +4 h +258 h +1 h +1 h +4 h +10 h +2251 m +3 h +4 h +10 h +97 h +10 h +15 m +11631 m +82 h +25 h +11632 m +2755 m +1 h +10 h +11633 m +4 h +41 h +110 h +4 h +4 h +1835 m +4 h +10 h +1 h +11 h +11634 m +4 h +1 h +186 h +11635 m +124 h +11636 m +1 h +11637 m +1 h +371 h +4 h +3 h +4 h +41 h +4 h +11638 m +4 h +1016 h +4 h +11639 m +79 h +1 h +11640 m +5387 m +4 h +25 h +412 m +11641 m +1 h +104 h +11642 m +11 h +104 h +1 h +156 h +4 h +295 h +11643 m +11644 m +1 h +11645 m +31 h +278 h +10914 m +82 h +4 h +11646 m +125 h +4297 m +11647 m +83 h +1 h +11648 m +4 h +13 h +11649 m +3473 m +11650 m +4 h +11651 m +1 h +238 h +10 h +4 h +167 h +297 h +1 h +4 h +4 h +146 h +4 h +463 h +1 h +11652 m +11653 m +4 h +11654 m +737 m +10 h +4 h +1893 m +1 h +4 h +1 h +1070 m +10 h +11 h +1822 h +109 h +10 h +297 h +230 m +4 h +1 h +143 h +386 h +569 h +4 h +11655 m +4 h +123 h +3 h +65 h +1 h +4 h +10 h +11656 m +10 h +59 h +1 h +124 h +12 h +500 m +1 h +4 h +4 h +138 h +4 h +10 h +11657 m +1 h +1 h +1 h +601 h +4 h +31 h +11 h +1 h +74 h +4 h +8496 m +4 h +1 h +11658 m +4 h +11659 m +64 h +1 h +10 h +97 h +1 h +2925 m +1 h +11660 m +1583 m +25 h +4 h +92 h +4 h +10 h +4 h +11661 m +1 h +10 h +11662 m +4 h +11663 m +1 h +1 h +1 h +1 h +1271 m +2719 m +11664 m +1 h +48 h +11665 m +11666 m +4 h +1074 m +4 h +11667 m +10 h +4 h +11668 m +10 h +371 h +4 h +31 h +640 m +4 h +1 h +10 h +158 h +1 h +4 h +11669 m +1 h +82 h +3 h +4 h +109 h +1 h +10 h +104 h +11670 m +1 h +124 h +4 h +4 h +11671 m +41 h +11672 m +4 h +45 h +4 h +156 h +1 h +11 h +11673 m +181 h +4 h +11674 m +4 h +4 h +11675 m +1 h +1 h +11676 m +371 h +4 h +238 h +1 h +1 h +4 h +1953 m +10 h +4 h +45 h +11 h +1 h +4 h +185 h +10 h +11677 m +11678 m +4 h +139 h +1265 m +4 h +28 h +4 h +10 h +11679 m +4 h +956 m +11680 m +4 h +6784 m +976 h +1 h +6001 m +11681 m +4 h +11682 m +1 h +4 h +1 h +4 h +3 h +4 h +10 h +108 h +11683 m +82 h +31 h +10 h +4 h +1 h +10 h +11 h +59 h +11684 m +4 h +4 h +1764 m +10 h +4 h +109 h +11685 m +11686 m +11687 m +11688 m +41 h +1 h +1 h +1 h +11689 m +4333 m +10 h +266 h +57 h +108 h +124 h +1 h +11690 m +4 h +4 h +808 h +11691 m +11692 m +307 h +1 h +10 h +757 h +172 h +10 h +11693 m +11694 m +4 h +104 h +1 h +119 h +57 h +1 h +196 h +10970 m +1 h +11 h +11695 m +11696 m +92 h +10 h +538 m +10 h +1 h +10 h +83 h +10 h +167 h +4 h +808 h +10 h +4 h +4 h +11697 m +10 h +69 h +48 h +4 h +4 h +11698 m +332 h +278 h +57 h +57 h +2258 m +4 h +41 h +125 h +1 h +4 h +1 h +4 h +11699 m +4 h +172 h +114 h +10 h +11 h +1 h +10 h +1 h +11700 m +1 h +1 h +147 h +10 h +4 h +4 h +195 h +4437 m +10 h +10 h +4 h +1 h +10 h +1 h +1 h +4 h +4 h +45 h +1 h +10 h +10 h +124 h +10 h +10 h +124 h +1 h +1 h +10 h +4 h +5230 m +1 h +10 h +59 h +11701 m +447 h +3 h +939 m +2418 m +4 h +1499 m +1642 h +167 h +10 h +12 h +10 h +79 h +4 h +97 h +59 h +4 h +11702 m +1 h +264 m +371 h +10 h +10 h +4 h +1 h +4 h +10 h +1 h +97 h +4 h +11427 m +10 h +74 h +125 h +4 h +4 h +4 h +124 h +10 h +65 h +1 h +1030 h +4 h +36 h +31 h +4 h +4 h +2028 m +1 h +1470 h +185 h +4542 m +10 h +10 h +11 h +109 h +965 m +11 h +164 h +11 h +10 h +10 h +282 m +109 h +4 h +4 h +83 h +11703 m +4 h +11704 m +229 h +11705 m +5864 m +1 h +10 h +4 h +65 h +11706 m +4 h +41 h +4 h +4297 m +1 h +4 h +1 h +1 h +11707 m +109 h +4 h +10 h +11708 m +11709 m +11710 m +10 h +278 h +109 h +10 h +4 h +11711 m +4 h +13 h +4 h +73 h +3 h +4 h +57 h +11712 m +11713 m +11714 m +11715 m +11716 m +629 m +4 h +104 h +1 h +1 h +1 h +31 h +1 h +109 h +1 h +4 h +10 h +109 h +3 h +11717 m +4 h +4 h +640 m +4 h +1 h +10 h +109 h +1 h +6776 m +11718 m +1 h +4 h +146 h +11 h +11719 m +13 h +1 h +779 h +4 h +10 h +4 h +4929 m +10 h +4 h +4 h +57 h +1 h +1 h +11720 m +4 h +1 h +11721 m +1 h +307 h +11722 m +56 h +11723 m +11724 m +221 m +4 h +11725 m +1 h +4 h +4 h +1 h +4 h +10 h +169 h +123 h +104 h +146 h +10 h +185 h +11726 m +9282 m +11727 m +1 h +110 h +4 h +2300 m +4 h +11728 m +1 h +11729 m +11730 m +1 h +11731 m +135 h +10 h +1 h +1 h +4 h +2594 m +4 h +4 h +1 h +11732 m +196 h +82 h +11733 m +11734 m +10 h +11735 m +1 h +65 h +1 h +4 h +4 h +22 h +4 h +10 h +10 h +11736 m +1024 m +11737 m +4 h +1 h +11738 m +1 h +11739 m +1 h +4 h +10 h +4 h +1 h +1 h +1 h +195 h +4 h +11740 m +10 h +11741 m +45 h +10 h +1 h +10 h +74 h +27 h +4 h +1 h +4 h +11742 m +4188 m +11743 m +4 h +368 h +4 h +10 h +146 h +4 h +45 h +1189 m +11744 m +1 h +1 h +4 h +1 h +1 h +4 h +1 h +11745 m +11 h +11746 m +31 h +1 h +338 h +11747 m +11748 m +1 h +4 h +1 h +10 h +1027 h +83 h +64 h +4 h +41 h +1 h +1 h +11749 m +1 h +4 h +1 h +4 h +4 h +4 h +1 h +55 h +10 h +4 h +41 h +10 h +36 h +358 h +1 h +4 h +11750 m +10918 m +278 h +6311 m +172 h +1128 m +4 h +307 h +279 h +10 h +4 h +1 h +1886 m +278 h +4 h +195 h +11751 m +82 h +3772 m +11752 m +1 h +8188 m +10 h +1 h +1 h +59 h +358 h +186 h +999 m +83 h +3 h +4 h +11753 m +167 h +4 h +11754 m +1 h +10 h +11755 m +359 h +11756 m +124 h +10 h +4 h +10 h +10 h +6747 m +4 h +10 h +113 h +10 h +25 h +4 h +8423 m +11757 m +82 h +4 h +11758 m +190 h +1 h +10 h +10 h +4 h +10070 m +1 h +2438 m +31 h +1 h +11759 m +1 h +4 h +4 h +204 h +4 h +11760 m +124 h +4 h +4 h +11761 m +10 h +4 h +11762 m +10 h +10 h +4 h +11763 m +10 h +459 m +1677 m +11 h +262 h +3 h +11764 m +4 h +135 h +4 h +3112 m +4 h +11765 m +195 h +386 h +4 h +124 h +1 h +170 h +10 h +1 h +10 h +108 h +11766 m +10 h +1 h +125 h +11767 m +4 h +203 h +190 h +11768 m +40 h +4 h +4 h +11769 m +4 h +11770 m +10 h +82 h +3 h +73 h +11771 m +12 h +386 h +4 h +1 h +10 h +10 h +536 h +4 h +918 m +73 h +11772 m +10 h +692 h +4 h +4 h +4 h +10 h +1250 h +935 h +939 m +11773 m +11774 m +4 h +4 h +123 h +1 h +4 h +11 h +4728 m +13 h +10 h +104 h +4 h +10 h +4 h +11 h +10 h +4 h +4 h +358 h +447 h +10 h +11775 m +4 h +238 h +802 m +4 h +4 h +1 h +119 h +1027 h +1 h +11776 m +11777 m +2265 h +10 h +10 h +4 h +185 h +4 h +74 h +10 h +265 h +4 h +11778 m +13 h +801 m +74 h +13 h +371 h +266 h +11779 m +11780 m +14 m +11781 m +11782 m +124 h +4 h +82 h +11783 m +459 m +1 h +4 h +4 h +3 h +687 m +59 h +11784 m +464 h +11785 m +10 h +1 h +10 h +1249 m +4 h +4 h +1 h +10 h +1 h +1 h +4 h +1 h +23 h +4 h +10 h +2891 m +1 h +4 h +10070 m +4 h +1 h +1 h +583 h +4 h +10 h +11786 m +8243 m +1 h +73 h +4 h +4 h +4 h +4 h +238 h +4 h +358 h +10 h +4 h +10 h +11787 m +274 h +4 h +4 h +4 h +4 h +57 h +4520 m +10 h +97 h +338 h +82 h +4 h +3558 m +4 h +65 h +6066 m +119 h +425 m +1619 h +4 h +1 h +4 h +4 h +57 h +10 h +4 h +278 h +195 h +4 h +11 h +4 h +11788 m +3 h +139 h +11789 m +4 h +1454 m +11790 m +195 h +104 h +11791 m +31 h +4 h +181 h +10 h +8 h +1 h +25 h +11792 m +11793 m +4 h +82 h +4 h +3216 m +11794 m +11795 m +10 h +4 h +11796 m +11797 m +41 h +1 h +2266 m +10 h +10 h +83 h +8 h +1 h +4 h +1 h +4 h +129 h +10 h +11798 m +11799 m +4 h +11800 m +196 h +4 h +10 h +718 m +4 h +4 h +935 h +4 h +82 h +1 h +11801 m +4 h +11802 m +4 h +4 h +10 h +4 h +10 h +1261 h +297 h +11803 m +10 h +4 h +4 h +57 h +36 h +4 h +4 h +4 h +11804 m +330 m +144 m +10 h +10 h +4 h +1 h +11805 m +31 h +289 h +1 h +2920 m +181 h +4 h +1406 h +4 h +92 h +4 h +135 h +897 m +10 h +1 h +4 h +478 h +11806 m +11807 m +195 h +1 h +109 h +104 h +11808 m +4 h +11809 m +1 h +103 h +11810 m +601 h +8497 m +169 h +1 h +10 h +10 h +57 h +4 h +10 h +4 h +4 h +4 h +10 h +4 h +41 h +4 h +11 h +11811 m +1 h +10 h +4 h +4 h +11 h +1 h +4 h +83 h +1 h +4 h +4 h +4 h +1 h +11812 m +1 h +11813 m +41 h +4 h +59 h +114 h +41 h +1 h +1 h +1 h +1 h +11 h +103 h +11814 m +73 h +3 h +4 h +147 h +620 m +10 h +599 m +1 h +11815 m +11816 m +278 h +11817 m +1 h +1018 m +13 h +4 h +332 h +10 h +41 h +4 h +10 h +10 h +118 h +258 h +74 h +1 h +4 h +800 m +1 h +4 h +11818 m +353 m +1 h +10 h +1 h +1 h +31 h +79 h +4 h +97 h +10 h +185 h +3 h +4 h +4 h +147 h +11819 m +11820 m +1 h +11821 m +41 h +4 h +386 h +11822 m +1 h +1 h +11 h +8 h +10 h +265 h +10 h +40 h +4 h +4 h +1 h +11823 m +10 h +3 h +4 h +10 h +1 h +4 h +4 h +3 h +114 h +4 h +8 h +109 h +10 h +4 h +4 h +1478 h +11824 m +10 h +11825 m +1 h +11826 m +10 h +11827 m +8974 m +41 h +10 h +4 h +31 h +169 h +1 h +3188 m +1 h +5562 m +4 h +1137 h +1 h +1 h +146 h +173 h +11828 m +4 h +10 h +11829 m +4 h +1 h +10 h +41 h +97 h +11 h +25 h +138 h +986 h +11830 m +11831 m +358 h +11832 m +31 h +4 h +256 m +4 h +11833 m +478 h +55 h +57 h +10 h +1 h +6613 m +692 h +11834 m +397 m +11835 m +1 h +10 h +435 m +1 h +10 h +10 h +4 h +4 h +1 h +110 h +25 h +1 h +146 h +1030 h +11836 m +11837 m +4 h +124 h +4 h +13 h +297 h +11838 m +11839 m +3 h +258 h +256 m +4 h +10 h +59 h +1 h +11840 m +1952 m +1 h +10 h +4 h +11841 m +1 h +4 h +11842 m +4 h +4 h +11843 m +41 h +10 h +10 h +11 h +1 h +4 h +10 h +10 h +28 h +3 h +4966 m +36 h +11844 m +4 h +1 h +1 h +10 h +11845 m +4 h +169 h +195 h +10 h +11846 m +10 h +1 h +11847 m +190 h +11848 m +11849 m +4 h +1 h +4 h +4 h +1 h +4 h +1 h +11850 m +1 h +4 h +4 h +11851 m +11852 m +4 h +1650 h +11853 m +4 h +4 h +4 h +999 m +192 h +11854 m +687 h +146 h +11855 m +94 h +11 h +4 h +11856 m +10 h +11857 m +11858 m +536 h +11859 m +11860 m +10 h +11861 m +4 h +11862 m +733 m +1650 h +33 h +4 h +22 h +1 h +10 h +1 h +55 h +4 h +1 h +190 h +157 h +36 h +1 h +82 h +1445 m +10 h +11 h +307 h +11863 m +184 h +4 h +1 h +464 h +1 h +10 h +4 h +4 h +11864 m +3344 m +124 h +11865 m +11866 m +4 h +11867 m +1 h +48 h +10 h +57 h +172 h +4 h +94 h +11868 m +4 h +1 h +266 h +4 h +4 h +11 h +1 h +31 h +1 h +11869 m +4 h +4 h +4 h +1 h +10 h +1 h +4 h +10 h +108 h +10 h +1 h +1 h +11870 m +4 h +11871 m +4 h +124 h +1 h +1 h +11872 m +569 h +11873 m +1 h +4 h +10 h +11874 m +10 h +10 h +1 h +4 h +196 h +55 h +57 h +1 h +10 h +4 h +1 h +1403 h +11875 m +119 h +170 h +11876 m +1 h +4 h +74 h +10 h +11877 m +146 h +4 h +4 h +4 h +57 h +82 h +10 h +172 h +4 h +124 h +1 h +332 h +2585 m +10 h +11878 m +146 h +4 h +4 h +112 h +109 h +1 h +307 h +4 h +11879 m +4 h +4 h +11880 m +83 h +1 h +11881 m +4 h +10 h +74 h +1 h +4 h +11882 m +10 h +1 h +10 h +1 h +4 h +1 h +11883 m +11884 m +4 h +11885 m +59 h +11886 m +41 h +164 h +4 h +4 h +10 h +1 h +1 h +1 h +129 h +1 h +129 h +1817 m +11887 m +10 h +59 h +1 h +104 h +10 h +1 h +5600 m +57 h +14 m +10 h +11888 m +4 h +11889 m +11890 m +4 h +93 h +144 m +4 h +109 h +4 h +124 h +4 h +124 h +174 m +687 h +4 h +412 h +109 h +4 h +4 h +1 h +11891 m +581 m +40 h +82 h +147 h +11892 m +1 h +129 h +4 h +31 h +8 h +114 h +11893 m +4 h +1 h +536 h +11894 m +4 h +2002 m +2865 m +11 h +270 h +11895 m +10 h +1 h +31 h +11896 m +10 h +11897 m +4 h +59 h +139 h +11898 m +195 h +9482 m +1 h +4 h +4 h +10 h +185 h +4 h +11 h +11899 m +10 h +716 m +1 h +31 h +4 h +10 h +279 h +4 h +4 h +57 h +4 h +10 h +4 h +4 h +59 h +1 h +11900 m +4 h +224 h +11901 m +4 h +10 h +82 h +65 h +10 h +4 h +4 h +11902 m +10 h +83 h +10 h +69 h +10 h +10 h +4 h +11903 m +57 h +4 h +91 h +11904 m +1 h +1 h +1 h +1 h +4 h +10 h +4 h +1 h +1 h +295 h +11905 m +1 h +36 h +4 h +1 h +1 h +104 h +10 h +11906 m +1 h +1 h +1771 m +158 h +11907 m +4 h +11908 m +10 h +4 h +6784 m +196 h +4 h +11909 m +123 h +10 h +4 h +10 h +172 h +196 h +31 h +10 h +1 h +4 h +65 h +11910 m +4 h +11911 m +73 h +11912 m +4 h +4 h +11 h +185 h +10 h +4 h +124 h +114 h +601 h +4 h +10 h +11913 m +147 h +330 m +70 m +173 h +1 h +570 m +1410 m +110 h +10 h +4 h +4 h +1 h +4 h +784 m +4 h +164 h +4 h +185 h +11914 m +169 h +266 h +258 h +119 h +1 h +4 h +10 h +10 h +359 h +124 h +4 h +10 h +11915 m +10 h +1 h +10 h +1 h +15 m +10 h +1 h +27 h +1 h +1 h +1 h +11 h +11916 m +4 h +4849 m +114 h +11917 m +11918 m +11919 m +10 h +538 h +124 h +11920 m +4 h +1 h +110 h +146 h +4 h +1 h +1 h +170 h +3 h +1 h +1 h +11921 m +1 h +10 h +1 h +4 h +113 h +4 h +1 h +1 h +114 h +11922 m +45 h +276 h +10 h +69 h +1470 h +241 m +2623 m +11 h +11923 m +10 h +10 h +11924 m +1 h +11925 m +83 h +1 h +1 h +4 h +10 h +59 h +4 h +4 h +10 h +73 h +11 h +10 h +307 h +1030 h +11926 m +11927 m +25 h +11928 m +11929 m +11930 m +4 h +11931 m +1 h +4 h +11932 m +4 h +10 h +1 h +1 h +4 h +10 h +11933 m +1 h +966 m +10 h +1 h +10 h +10 h +11934 m +4 h +61 m +83 h +93 h +11935 m +31 h +109 h +83 h +11936 m +57 h +4 h +11937 m +11938 m +10 h +104 h +11939 m +11940 m +11941 m +4 h +403 h +11942 m +4 h +1 h +307 h +4 h +10 h +10 h +4 h +3539 m +5505 m +4 h +104 h +4 h +10 h +10 h +31 h +11943 m +10 h +1281 m +11 h +4 h +11944 m +4 h +10 h +109 h +11945 m +4 h +25 h +11 h +1 h +1 h +4 h +11 h +11946 m +11947 m +109 h +10 h +1284 m +10 h +4 h +139 h +10 h +4 h +1 h +11948 m +11949 m +170 h +4 h +11950 m +10 h +4 h +11951 m +11 h +11952 m +143 h +3177 m +97 h +25 h +109 h +11953 m +11954 m +4 h +11955 m +11956 m +4 h +11957 m +4 h +1027 h +1 h +1 h +135 h +4 h +4 h +266 h +4 h +45 h +11958 m +938 h +10 h +196 h +10 h +4 h +11959 m +11960 m +4 h +4 h +11961 m +114 h +11962 m +1710 m +11 h +147 h +1 h +1 h +10 h +274 h +4 h +1 h +11963 m +1 h +1830 m +4 h +11 h +4 h +59 h +10 h +11 h +11964 m +1 h +4 h +1 h +4 h +4 h +1 h +4 h +11965 m +11966 m +11967 m +8 h +11968 m +939 h +1 h +82 h +11 h +1 h +1 h +4 h +4 h +190 h +4 h +147 h +10 h +11969 m +10 h +164 h +692 h +11970 m +1470 h +109 h +104 h +59 h +447 h +82 h +10 h +7585 m +11971 m +10 h +4 h +73 h +1 h +4 h +11972 m +4 h +1 h +11973 m +6784 m +4 h +82 h +4 h +31 h +11 h +41 h +124 h +4 h +31 h +11974 m +10 h +319 h +403 h +124 h +11975 m +11976 m +1 h +1 h +1 h +1632 m +11977 m +11978 m +4 h +82 h +1 h +11 h +74 h +4 h +4 h +4 h +11979 m +11980 m +4 h +1 h +264 m +11 h +11981 m +4 h +1 h +575 m +4 h +11982 m +4 h +4 h +4 h +4 h +236 m +4 h +4 h +57 h +10 h +4 h +10 h +10 h +11983 m +4 h +11984 m +1542 m +4 h +1957 m +11 h +11985 m +10 h +10 h +1 h +135 h +506 m +10 h +1 h +11 h +4 h +4 h +307 h +11986 m +82 h +10 h +11987 m +10 h +4 h +4 h +1 h +1 h +1 h +1 h +11988 m +11989 m +10 h +10 h +10 h +10 h +10 h +11990 m +1 h +1 h +57 h +57 h +11991 m +4 h +11 h +1 h +4 h +11992 m +82 h +83 h +4 h +278 h +4 h +11993 m +1 h +11 h +83 h +1 h +11994 m +10 h +11995 m +11996 m +4 h +1 h +170 h +82 h +11997 m +1 h +11998 m +11999 m +12000 m +1 h +10 h +1 h +10 h +124 h +195 h +2096 m +4 h +125 h +12001 m +185 h +74 h +6015 m +65 h +83 h +266 h +444 m +4 h +123 h +12002 m +10 h +12003 m +12004 m +578 h +4 h +10 h +12005 m +12006 m +1 h +10 h +4 h +276 h +190 h +4 h +11 h +12007 m +4 h +12008 m +1 h +11 h +1697 m +4 h +110 h +12009 m +1 h +12010 m +12011 m +4 h +4 h +10 h +4 h +1 h +12012 m +25 h +1 h +4 h +4 h +4 h +358 h +196 h +57 h +4 h +4 h +1 h +459 h +4 h +1 h +190 h +4 h +10 h +1 h +12013 m +274 h +10 h +12014 m +74 h +83 h +4 h +1 h +10 h +4 h +10 h +12015 m +3 h +966 m +1 h +160 m +45 h +190 h +125 h +1 h +12016 m +12017 m +10 h +1 h +12018 m +377 h +11 h +79 h +41 h +1 h +10 h +10 h +12019 m +65 h +2813 m +12020 m +10 h +1 h +12021 m +10 h +12022 m +4 h +1886 m +1 h +65 h +4 h +4 h +1 h +4 h +1 h +12023 m +12024 m +12025 m +114 h +447 h +10 h +10 h +10 h +1 h +83 h +687 h +4 h +1 h +12026 m +434 m +1 h +10 h +0 m +12027 m +4 h +10 h +2592 m +4 h +156 h +4 h +4 h +1 h +1 h +12028 m +10 h +4 h +4 h +4 h +12029 m +57 h +4 h +4 h +10 h +12030 m +11 h +1 h +1 h +69 h +4 h +1 h +12 h +169 h +136 m +12031 m +1299 m +4 h +12032 m +1 h +10 h +12033 m +12034 m +1 h +12035 m +124 h +4 h +1 h +4 h +10 h +12036 m +97 h +1 h +12037 m +1 h +12038 m +109 h +4471 m +4 h +12039 m +138 h +5964 m +536 h +4 h +4 h +1 h +4 h +10 h +185 h +59 h +10 h +4 h +4 h +181 h +12040 m +12041 m +10 h +1 h +4 h +4 h +1 h +1 h +601 h +520 m +3 h +12042 m +4 h +1189 m +2733 m +10 h +125 h +41 h +36 h +1 h +1 h +4 h +12043 m +4 h +1 h +4 h +192 h +1 h +135 h +12044 m +12045 m +12046 m +10 h +12047 m +11 h +12048 m +4 h +174 m +1 h +4 h +4 h +11 h +10 h +12049 m +1016 h +358 h +25 h +4 h +1 h +10 h +170 h +10 h +10 h +1137 h +11 h +1 h +10 h +10 h +69 h +4 h +110 h +4 h +4 h +65 h +11 h +12050 m +10 h +1 h +4 h +4 h +11 h +4 h +4 h +1089 h +1 h +12051 m +12052 m +4 h +31 h +12053 m +12054 m +12055 m +12056 m +1 h +11 h +4 h +186 h +10 h +11 h +12057 m +170 h +1016 h +4 h +124 h +196 h +229 h +939 h +1 h +1220 m +1751 m +82 h +124 h +10 h +40 h +129 h +4 h +92 h +12058 m +1 h +1 h +0 m +125 h +10 h +1 h +12059 m +1 h +1 h +4 h +4 h +56 h +113 h +4 h +12060 m +1 h +4 h +477 m +45 h +11 h +4 h +4 h +97 h +332 h +12061 m +12062 m +12063 m +11 h +2540 m +4 h +1 h +12064 m +11 h +1 h +55 h +4 h +45 h +4 h +104 h +911 h +4229 m +36 h +4 h +4 h +4 h +10 h +45 h +1 h +4 h +1 h +4 h +13 h +11 h +447 h +10 h +10330 m +10 h +8 h +1 h +12065 m +55 h +4 h +1266 m +12066 m +110 h +12067 m +55 h +4 h +273 m +12068 m +73 h +1 h +1 h +12069 m +1359 m +10 h +1 h +12070 m +4 h +12071 m +6025 m +109 h +12072 m +1 h +4 h +4 h +4 h +12073 m +4 h +12074 m +74 h +10 h +10 h +264 m +12075 m +12076 m +12077 m +276 h +278 h +4 h +4 h +377 h +2786 m +1 h +12078 m +4 h +12079 m +83 h +285 m +83 h +4 h +4 h +383 h +143 h +10 h +3341 m +12080 m +1 h +4 h +146 h +12081 m +278 h +1 h +181 h +1 h +4 h +124 h +12082 m +1 h +1 h +31 h +4 h +12083 m +1 h +11 h +1 h +4 h +4 h +79 h +10 h +12084 m +1017 h +12085 m +1 h +265 h +4 h +12086 m +383 h +59 h +4 h +3321 m +1409 m +12087 m +1 h +13 h +109 h +1 h +4 h +82 h +4 h +4 h +1 h +1 h +1 h +1 h +12088 m +12089 m +82 h +1 h +59 h +4 h +12090 m +1 h +10 h +12091 m +2788 h +1650 h +156 h +629 m +104 h +4 h +109 h +45 h +4 h +6788 m +1 h +167 h +1 h +1 h +2379 m +2116 m +12092 m +12093 m +1 h +10 h +1 h +12094 m +1 h +4 h +794 m +12095 m +12096 m +4744 m +1 h +4 h +10 h +59 h +278 h +1 h +1 h +928 m +11 h +1 h +4 h +4 h +1 h +3 h +12097 m +10 h +954 m +4 h +250 h +10 h +83 h +4 h +190 h +4 h +12098 m +12099 m +1 h +57 h +4 h +10 h +11 h +2665 m +10 h +578 h +4 h +36 h +533 h +12100 m +82 h +1 h +10 h +1574 m +6413 m +1 h +1 h +12101 m +4 h +93 h +12102 m +59 h +4 h +69 h +1 h +4 h +1 h +1250 h +12103 m +12104 m +10 h +2309 m +1 h +4 h +4 h +97 h +12105 m +64 h +41 h +4 h +4 h +1 h +28 h +1 h +1 h +12106 m +59 h +1 h +1 h +430 m +578 h +1 h +4 h +1 h +4 h +10 h +4 h +110 h +4 h +12107 m +1 h +4 h +4 h +4 h +3 h +12108 m +12109 m +10 h +57 h +73 h +10 h +1 h +4 h +4 h +1 h +4 h +12110 m +10 h +4 h +1 h +258 h +4 h +12111 m +25 h +1 h +12112 m +4 h +12113 m +3 h +12114 m +12115 m +3845 m +10 h +10 h +10 h +4 h +307 h +186 h +12116 m +124 h +12117 m +4 h +12118 m +1 h +12119 m +45 h +4106 m +12120 m +10 h +4 h +12121 m +169 h +64 h +124 h +4 h +1 h +4 h +167 h +12122 m +4 h +11 h +1 h +4 h +1 h +12123 m +4 h +10 h +1 h +4 h +12124 m +1 h +10 h +4 h +4 h +10 h +11 h +4 h +27 h +12125 m +4 h +1 h +79 h +10 h +4 h +1 h +8 h +270 h +1 h +41 h +12126 m +172 h +1 h +41 h +12127 m +12128 m +4 h +12129 m +59 h +2733 h +195 h +1359 m +1959 m +12130 m +12131 m +4 h +12132 m +4 h +12133 m +12134 m +12 h +297 h +12135 m +12136 m +1 h +1722 m +10 h +129 h +114 h +4 h +97 h +12137 m +383 h +229 h +10 h +10 h +1 h +6766 m +1 h +1 h +10 h +4 h +4 h +1 h +1 h +4127 m +4 h +12138 m +114 h +11 h +10 h +10 h +173 h +31 h +1 h +11 h +1406 h +10 h +12139 m +1 h +1 h +25 h +10 h +25 h +173 h +279 h +45 h +4 h +4 h +6095 m +83 h +224 h +4 h +114 h +1 h +939 h +4 h +4 h +11 h +57 h +4 h +10 h +10 h +10 h +4 h +687 h +146 h +10 h +12140 m +1 h +12141 m +4 h +25 h +1 h +1 h +1 h +170 h +10 h +4 h +31 h +4 h +83 h +12142 m +12143 m +307 h +12144 m +1 h +12145 m +4 h +1 h +4 h +83 h +10 h +4 h +74 h +13 h +4 h +10 h +10 h +1 h +4 h +258 h +4 h +124 h +10 h +1 h +1 h +12146 m +4 h +1 h +12147 m +4 h +770 m +57 h +266 h +4 h +104 h +1 h +4 h +10 h +167 h +57 h +4 h +1 h +1 h +8206 m +1 h +11 h +4 h +4 h +59 h +1 h +10 h +1138 m +83 h +4 h +241 m +12148 m +1 h +1409 m +1016 h +4 h +4 h +4 h +4 h +1 h +4 h +4 h +1 h +12149 m +11 h +12150 m +112 h +4 h +31 h +12151 m +583 h +10 h +12152 m +10 h +4 h +12153 m +1 h +4 h +4 h +10 h +12154 m +4 h +1556 m +135 h +2923 m +22 h +4 h +65 h +4 h +270 h +2928 m +12155 m +1 h +12156 m +4 h +12157 m +158 h +59 h +278 h +31 h +57 h +7649 m +1 h +10 h +59 h +1666 m +109 h +25 h +5125 m +2951 m +1 h +10 h +4 h +64 h +582 m +4 h +4 h +1 h +12158 m +963 m +12159 m +1 h +12160 m +4 h +2794 m +1 h +2205 m +83 h +4 h +1 h +4 h +57 h +1 h +1 h +1 h +4 h +25 h +1454 m +6437 m +10 h +4 h +10 h +10 h +307 h +25 h +94 h +2041 m +1 h +10 h +12161 m +4 h +25 h +4 h +4 h +3668 m +4 h +4 h +1 h +196 h +195 h +65 h +11 h +31 h +12162 m +4 h +10 h +10 h +12163 m +12164 m +1 h +1486 m +2339 m +4 h +4 h +13 h +12165 m +4 h +12166 m +45 h +12167 m +12168 m +4 h +4 h +10 h +10 h +94 h +1 h +289 h +12169 m +10 h +1 h +1 h +124 h +358 h +12170 m +1 h +10 h +97 h +4 h +25 h +1016 h +4 h +4297 h +888 m +124 h +146 h +59 h +4 h +258 h +12171 m +10 h +4 h +4 h +10 h +4 h +480 m +57 h +400 m +4 h +4 h +1 h +4 h +10 h +10 h +444 m +4 h +4 h +520 m +12172 m +10 h +65 h +4 h +4 h +266 h +4 h +10 h +104 h +104 h +4 h +4 h +270 h +82 h +10 h +12173 m +12131 m +147 h +114 h +1 h +10 h +10 h +12174 m +4 h +12175 m +1 h +12176 m +4 h +196 h +143 h +1 h +10 h +11 h +11 h +64 h +1 h +1 h +4 h +4 h +4 h +4 h +4 h +12 h +74 h +1 h +104 h +1796 m +12177 m +1 h +10 h +1 h +113 h +57 h +4 h +1 h +4 h +4 h +12178 m +1 h +1 h +82 h +4 h +4 h +386 h +12179 m +12180 m +4 h +4 h +4 h +4 h +1 h +1105 h +104 h +12181 m +1 h +10 h +4 h +92 h +4 h +10 h +1 h +1 h +104 h +12182 m +12183 m +4 h +10 h +12184 m +203 h +104 h +10 h +12185 m +4 h +4 h +871 m +12186 m +4 h +65 h +10 h +1 h +147 h +266 h +170 h +11 h +12187 m +124 h +12188 m +10 h +1 h +82 h +4 h +1 h +11 h +12189 m +124 h +1 h +4 h +4 h +25 h +13 h +4 h +4 h +195 h +1 h +64 h +10 h +12190 m +464 h +1 h +31 h +1 h +4 h +1 h +1 h +169 h +4 h +1 h +4 h +447 h +82 h +10 h +73 h +282 h +129 h +12191 m +79 h +4 h +10 h +25 h +1 h +196 h +4 h +12192 m +4 h +8274 m +10 h +10 h +10 h +4 h +12193 m +295 h +1 h +1 h +1 h +12194 m +12195 m +12196 m +10 h +3402 m +125 h +143 h +12197 m +4 h +12 h +10 h +1 h +4 h +12198 m +74 h +4849 m +10 h +4 h +12199 m +12200 m +31 h +4 h +10 h +12201 m +1 h +139 h +4 h +11 h +12202 m +4 h +1 h +12203 m +4 h +4 h +83 h +1261 h +119 h +83 h +4 h +4 h +1 h +4 h +463 h +4 h +118 h +4 h +41 h +4 h +12 h +12204 m +1 h +65 h +1 h +4 h +3 h +1 h +1 h +10 h +3702 m +12205 m +12206 m +4 h +57 h +22 h +4 h +12207 m +12208 m +1 h +1 h +1 h +4 h +4 h +4 h +12209 m +3 h +10 h +1 h +12210 m +10 h +4 h +12211 m +55 h +12212 m +1 h +170 h +156 h +1 h +4 h +4 h +12213 m +1 h +12214 m +4 h +10 h +1 h +10 h +4 h +12215 m +4 h +4 h +11 h +221 m +4 h +196 h +1137 h +4 h +327 m +10 h +12216 m +12217 m +11 h +10 h +4 h +10 h +4 h +10 h +10 h +10 h +56 h +12218 m +1 h +4 h +184 h +1 h +986 h +12219 m +12220 m +12221 m +4 h +73 h +73 h +124 h +10 h +10 h +57 h +170 h +12222 m +55 h +12223 m +4 h +196 h +57 h +12224 m +12225 m +10 h +4 h +1 h +4 h +12226 m +4 h +109 h +276 h +146 h +74 h +575 m +104 h +1 h +12227 m +11 h +4 h +1 h +12228 m +12229 m +10 h +388 m +12230 m +12231 m +82 h +4 h +4 h +185 h +1 h +185 h +1828 m +1 h +4 h +12232 m +10 h +10 h +4 h +1 h +278 h +4 h +12233 m +4 h +12234 m +12235 m +1 h +10 h +4 h +12236 m +112 h +1 h +12237 m +169 h +4 h +1 h +1835 m +74 h +1 h +1 h +10 h +4 h +10 h +4 h +82 h +10 h +10 h +8608 m +12238 m +4 h +12239 m +10659 m +4 h +4 h +12240 m +10 h +3278 m +12241 m +2002 m +3 h +82 h +55 h +4 h +1 h +4 h +25 h +124 h +57 h +172 h +10 h +12242 m +186 h +195 h +10 h +10 h +986 h +82 h +1403 h +4 h +10 h +31 h +1 h +57 h +1 h +12243 m +1 h +1822 h +12244 m +4 h +12245 m +10 h +10 h +83 h +12246 m +82 h +4 h +124 h +4 h +10 h +114 h +12 h +31 h +12247 m +109 h +692 h +4 h +8854 m +1137 h +238 h +12248 m +12249 m +4 h +41 h +31 h +31 h +12250 m +4 h +12251 m +10 h +332 h +13 h +57 h +1 h +109 h +4 h +36 h +12252 m +83 h +4 h +10 h +12253 m +4 h +447 h +12254 m +195 h +4 h +82 h +1 h +1 h +12255 m +10 h +4 h +10 h +57 h +4 h +10 h +4 h +4 h +1 h +10 h +12256 m +110 h +10 h +250 h +12257 m +82 h +4 h +10 h +10 h +258 h +73 h +25 h +59 h +65 h +1470 h +25 h +10 h +12258 m +1379 m +41 h +31 h +1 h +1 h +12259 m +31 h +4 h +12260 m +3 h +4 h +4 h +279 h +196 h +181 h +1 h +12261 m +4 h +266 h +12262 m +10 h +4 h +12263 m +146 h +4 h +25 h +278 h +12264 m +4 h +12265 m +12266 m +12267 m +4 h +12268 m +10 h +10 h +12269 m +12270 m +1 h +1 h +12271 m +104 h +4 h +2923 m +1 h +1 h +11 h +1 h +12272 m +8556 m +4 h +10 h +990 m +1 h +4 h +83 h +4 h +8643 m +536 h +4 h +143 h +12273 m +1 h +57 h +912 m +1 h +4 h +3 h +4 h +11 h +4 h +12274 m +4 h +74 h +386 h +10 h +538 h +91 h +1027 h +40 h +1074 h +1 h +10 h +1 h +4 h +12275 m +4 h +12276 m +4 h +1 h +1 h +4 h +173 h +124 h +124 h +4 h +10 h +4 h +109 h +74 h +1299 m +12277 m +4 h +146 h +1 h +4 h +12278 m +8104 m +4 h +139 h +1 h +1 h +7306 m +12279 m +297 h +135 h +10 h +12280 m +12281 m +1 h +4 h +1201 m +1 h +10 h +11 h +4815 m +10 h +10 h +196 h +10 h +1 h +83 h +4 h +12282 m +12283 m +10 h +11 h +4 h +147 h +1 h +1 h +4 h +2719 m +12284 m +157 h +1 h +11 h +4 h +4 h +4 h +1 h +79 h +1201 h +12285 m +12 h +12286 m +1 h +4 h +12287 m +4 h +1 h +12288 m +4 h +12289 m +10 h +4 h +4 h +12290 m +10 h +1 h +4 h +10745 m +1 h +1 h +1 h +4 h +1698 m +1 h +74 h +11 h +1 h +4 h +4 h +425 m +10 h +10 h +4 h +12291 m +4 h +1 h +12292 m +270 h +10 h +3 h +12293 m +10 h +4 h +12294 m +12295 m +1453 m +4 h +4 h +4 h +1953 m +12296 m +83 h +1 h +488 h +757 h +170 h +4 h +1 h +443 h +12297 m +12298 m +2374 m +1504 m +74 h +1 h +13 h +10 h +144 h +10 h +147 h +399 h +4 h +10 h +12299 m +1 h +4 h +1 h +6503 m +10 h +10 h +82 h +10 h +4 h +1089 h +124 h +538 h +12300 m +1 h +110 h +12301 m +12302 m +4 h +73 h +238 h +4 h +36 h +4 h +185 h +4 h +12303 m +11 h +12304 m +12305 m +730 m +1 h +56 h +10 h +12306 m +578 h +6129 m +12307 m +4 h +124 h +12308 m +12309 m +10 h +4 h +1 h +12310 m +12311 m +110 h +12312 m +13 h +1 h +4 h +1 h +297 h +1 h +1 h +12313 m +11 h +12314 m +4 h +4 h +1 h +12194 m +10 h +10 h +4 h +4701 m +4 h +94 h +12 h +10 h +4 h +4 h +10 h +83 h +13 h +41 h +10 h +10 h +250 h +12315 m +11 h +4 h +12316 m +1 h +135 h +12317 m +1 h +41 h +12318 m +1 h +1 h +4 h +238 h +109 h +10 h +4 h +12319 m +10 h +1 h +109 h +12320 m +12 h +48 h +12321 m +4 h +10 h +83 h +4 h +12322 m +14 h +57 h +124 h +22 h +146 h +12323 m +57 h +4 h +12324 m +1 h +83 h +6457 m +1 h +4 h +1 h +164 h +146 h +10 h +616 m +4 h +97 h +12325 m +4 h +4 h +10 h +4 h +1 h +1 h +12326 m +1 h +1337 m +195 h +1650 h +10 h +10 h +12327 m +13 h +140 m +1016 h +1858 m +4 h +1 h +12328 m +12329 m +12330 m +196 h +4 h +5621 m +12331 m +1 h +4 h +12332 m +12333 m +1 h +10 h +11 h +332 h +82 h +4 h +4 h +4 h +11 h +82 h +4 h +12334 m +2379 m +82 h +10 h +1953 m +4 h +4 h +10 h +1 h +1 h +119 h +10 h +12335 m +4 h +1 h +12336 m +12337 m +12 h +45 h +181 h +25 h +196 h +12338 m +4 h +12339 m +4 h +74 h +1 h +1089 h +443 h +1137 h +4 h +91 h +12340 m +10 h +1 h +12341 m +12342 m +104 h +1 h +157 h +12343 m +113 h +41 h +12344 m +4 h +4 h +192 h +12345 m +1 h +737 m +1 h +59 h +12346 m +12347 m +1 h +170 h +23 h +12348 m +4 h +371 h +31 h +57 h +10 h +6187 m +12349 m +1 h +4 h +184 h +27 h +274 h +4 h +12350 m +274 h +1766 m +57 h +4 h +10 h +3 h +10 h +11 h +1 h +59 h +4 h +7 m +125 h +12351 m +12352 m +4 h +12353 m +12354 m +4 h +10 h +4 h +1 h +12355 m +4 h +4 h +12356 m +196 h +4 h +3 h +56 h +4 h +10 h +56 h +10 h +256 h +1 h +4 h +869 m +4 h +1 h +12 h +124 h +4 h +6784 h +4 h +4 h +230 m +403 h +1 h +266 h +4 h +12357 m +4 h +1 h +57 h +12358 m +4 h +164 h +77 h +1 h +12359 m +92 h +4 h +12360 m +1 h +55 h +2205 m +4 h +3 h +10 h +59 h +83 h +4 h +1 h +57 h +82 h +74 h +174 m +73 h +10 h +27 h +1 h +10 h +4 h +65 h +4 h +4 h +1 h +146 h +12361 m +12362 m +12363 m +10 h +12364 m +359 h +4 h +12365 m +12366 m +538 h +4 h +1 h +12367 m +10 h +2285 m +1 h +4 h +65 h +1 h +11 h +1 h +4 h +1 h +10 h +4 h +106 m +10 h +104 h +1 h +59 h +185 h +125 h +1406 h +4 h +10 h +164 h +1 h +12368 m +41 h +10 h +3 h +114 h +1 h +520 h +1 h +10 h +10 h +386 h +59 h +4 h +556 h +1 h +10 h +12369 m +12370 m +4 h +12371 m +4 h +4 h +976 h +10 h +11 h +1 h +4 h +4 h +23 h +124 h +1 h +4 h +4 h +59 h +196 h +12372 m +4 h +4 h +10 h +10 h +11 h +1 h +1 h +10 h +4240 m +1 h +1 h +4 h +4 h +1 h +4 h +56 h +93 h +12373 m +4 h +74 h +4 h +4 h +125 h +12374 m +1 h +536 h +4 h +31 h +12375 m +4 h +10 h +3 h +2923 h +443 h +12376 m +1 h +258 h +12377 m +10 h +570 m +12378 m +4 h +976 h +640 h +195 h +1089 h +12 h +2172 m +87 m +1 h +737 m +575 m +1137 h +64 h +488 h +12379 m +319 h +104 h +12380 m +4 h +4 h +4 h +10 h +238 h +12381 m +1 h +56 h +1 h +4 h +104 h +3 h +4 h +4 h +1 h +195 h +4 h +4 h +144 h +506 m +4 h +196 h +11 h +2002 m +1 h +27 h +73 h +4 h +4111 m +195 h +266 h +1 h +10 h +4 h +3 h +316 m +10 h +1 h +12382 m +83 h +282 h +12383 m +1 h +91 h +4 h +10 h +4 h +1 h +443 h +143 h +83 h +27 h +1016 h +185 h +12384 m +12385 m +4 h +12386 m +10 h +11 h +12387 m +12388 m +3 h +65 h +1 h +10 h +10 h +12389 m +12390 m +12391 m +12392 m +307 h +1 h +59 h +12393 m +1 h +1 h +4 h +1 h +10 h +12394 m +169 h +12395 m +12396 m +0 h +124 h +82 h +10 h +4 h +270 h +124 h +1 h +10 h +1 h +1 h +146 h +4 h +11 h +1 h +10 h +77 h +93 h +4 h +1 h +12397 m +57 h +12398 m +4 h +4 h +1 h +4 h +601 h +10 h +11 h +1 h +10 h +4 h +10 h +4 h +79 h +10 h +1 h +12399 m +358 h +4 h +10 h +274 h +1 h +4 h +10 h +83 h +1 h +4 h +4 h +10 h +135 h +332 h +12400 m +1 h +1 h +41 h +4229 m +4 h +10 h +4 h +4 h +358 h +1 h +1 h +1 h +12401 m +4 h +4 h +190 h +371 h +109 h +1 h +1 h +219 h +10 h +12402 m +12403 m +1714 m +196 h +12404 m +1 h +1 h +1 h +10 h +1 h +10 h +1822 h +12405 m +1 h +1 h +1955 m +109 h +11 h +1 h +10 h +12406 m +12407 m +258 h +82 h +4966 m +274 h +10 h +10 h +270 h +12408 m +1 h +4 h +48 h +12409 m +27 h +12410 m +4 h +1737 m +12411 m +4 h +4 h +4 h +12412 m +4 h +10 h +4 h +12413 m +73 h +4 h +4 h +1 h +10 h +12414 m +1 h +45 h +123 h +4 h +4 h +12415 m +12416 m +4 h +12417 m +4 h +94 h +169 h +1957 m +4 h +83 h +12418 m +4 h +97 h +4 h +1 h +12419 m +3 h +83 h +4 h +97 h +1 h +4 h +11 h +1 h +1 h +10 h +1 h +1478 h +12420 m +12421 m +4 h +12422 m +173 h +12423 m +1 h +4 h +10 h +1 h +4 h +229 h +4 h +4 h +110 h +12424 m +1 h +4 h +4 h +338 h +190 h +10 h +31 h +10 h +10 h +10 h +1 h +874 m +12425 m +10 h +10 h +4 h +125 h +10 h +104 h +79 h +12426 m +4 h +4 h +238 h +12427 m +1 h +10 h +3 h +11 h +2442 m +12428 m +10 h +59 h +12429 m +10 h +10 h +377 h +12430 m +1 h +1 h +12431 m +1 h +4 h +533 h +4 h +25 h +82 h +10 h +1 h +4 h +10 h +4 h +10 h +4 h +4 h +11 h +4520 m +10 h +4 h +12432 m +64 h +4 h +1 h +10 h +27 h +4 h +12433 m +12434 m +276 h +12 h +4 h +124 h +82 h +1 h +110 h +45 h +12435 m +12436 m +4 h +4 h +11 h +10 h +4 h +823 m +12437 m +3 h +31 h +1 h +125 h +4 h +82 h +12438 m +1 h +4 h +4 h +22 h +4 h +12 h +4 h +1 h +230 m +12439 m +4 h +332 h +885 m +146 h +1 h +12440 m +4 h +4 h +12441 m +11 h +125 h +4 h +4 h +12442 m +82 h +12443 m +4 h +1 h +1 h +10 h +92 h +359 h +1 h +4 h +12444 m +583 h +59 h +2617 m +12445 m +4 h +1 h +41 h +1 h +112 h +12446 m +1437 m +601 h +4 h +83 h +59 h +4 h +6753 m +4 h +12447 m +241 m +10 h +125 h +1 h +157 h +31 h +92 h +358 h +12448 m +82 h +12449 m +1 h +82 h +36 h +27 h +1499 m +12450 m +4 h +12451 m +124 h +125 h +4 h +74 h +12452 m +12453 m +11 h +1261 h +986 h +4 h +1 h +1838 m +4 h +4 h +4 h +4 h +12454 m +1 h +11 h +12455 m +581 m +4 h +12456 m +4 h +25 h +83 h +4 h +12457 m +4 h +4 h +10 h +104 h +10 h +10 h +4 h +27 h +4 h +10 h +41 h +1053 m +1 h +146 h +1 h +10 h +4 h +12458 m +4 h +1642 h +109 h +196 h +1 h +12459 m +1 h +12460 m +10 h +31 h +3299 m +1 h +1 h +1 h +1 h +4 h +1 h +4 h +4 h +4 h +4 h +12461 m +4 h +1 h +10 h +359 h +97 h +4 h +1499 m +55 h +1359 h +12462 m +11 h +10 h +4 h +229 h +1 h +4 h +4 h +10 h +1 h +10 h +27 h +12463 m +1 h +4 h +4 h +9 m +1 h +358 h +12464 m +104 h +167 h +31 h +1 h +11 h +11 h +12465 m +12 h +6469 m +1 h +1 h +1 h +12466 m +59 h +146 h +4 h +1 h +10 h +57 h +10 h +1 h +1308 m +57 h +104 h +25 h +7 m +4 h +10 h +258 h +112 h +1 h +976 h +31 h +4 h +1 h +386 h +1 h +4 h +1 h +1 h +12467 m +332 h +12 h +10 h +4 h +74 h +4 h +1 h +140 h +4 h +57 h +2418 m +124 h +1205 m +4 h +10 h +10 h +1 h +10 h +1 h +3 h +124 h +12468 m +10 h +4 h +1 h +10 h +4 h +1 h +4 h +1 h +12469 m +1 h +601 h +4 h +10 h +4 h +12470 m +1 h +12471 m +1 h +3 h +2465 m +1 h +1 h +124 h +1 h +4 h +12272 m +4 h +12472 m +10 h +12473 m +307 h +64 h +272 m +4 h +12474 m +1535 m +114 h +10 h +4 h +109 h +295 h +10 h +4 h +12475 m +1 h +4 h +4 h +1 h +33 h +123 h +12476 m +31 h +10 h +1 h +1 h +12477 m +181 h +12478 m +97 h +56 h +4 h +12479 m +4 h +1 h +1 h +1 h +1 h +1 h +181 h +12480 m +8 h +4 h +10 h +12481 m +4 h +770 m +170 h +1 h +1 h +4 h +83 h +1 h +1 h +1 h +12482 m +12483 m +4 h +1 h +12484 m +4 h +4 h +10 h +12485 m +1 h +12486 m +12487 m +1299 m +4 h +2927 m +12488 m +322 m +77 h +10 h +10 h +135 h +12489 m +4 h +4 h +1822 h +12490 m +4 h +1 h +1 h +109 h +104 h +4 h +536 h +11490 m +12491 m +12492 m +4 h +1089 h +1 h +1 h +4 h +12493 m +4 h +1 h +12494 m +4 h +10 h +4 h +12495 m +10 h +12496 m +10 h +1 h +31 h +12497 m +10 h +2625 m +79 h +4 h +12498 m +10 h +1 h +125 h +186 h +12 h +1 h +79 h +4 h +1 h +10 h +12499 m +4 h +169 h +935 h +1 h +7572 m +12500 m +718 m +4 h +10 h +4 h +12501 m +12502 m +1 h +12503 m +11 h +3 h +10243 m +1 h +1667 m +4 h +8146 m +12504 m +10 h +4 h +4 h +10 h +4 h +3 h +36 h +82 h +10 h +4 h +12505 m +4 h +1 h +447 h +4 h +1 h +4 h +146 h +146 h +4 h +10 h +640 h +4 h +1 h +1261 h +110 h +1 h +10 h +4 h +4 h +10 h +4 h +12506 m +4 h +13 h +4 h +1 h +25 h +1955 m +10 h +1 h +12507 m +12508 m +4 h +1 h +4 h +1 h +3 h +12509 m +4 h +25 h +1074 h +114 h +1 h +1 h +11 h +4 h +10 h +93 h +4 h +4 h +12510 m +10 h +1 h +1 h +4 h +1024 m +601 h +158 h +1 h +12511 m +82 h +10 h +73 h +12512 m +12513 m +4 h +12514 m +1 h +4 h +12515 m +4 h +1 h +12516 m +10 h +124 h +4 h +1574 m +12 h +1 h +45 h +146 h +6187 m +1 h +83 h +1 h +167 h +12517 m +59 h +4 h +4 h +1 h +10 h +250 h +10 h +10 h +12518 m +4 h +4 h +12519 m +1 h +31 h +3177 m +4 h +1 h +11 h +109 h +1 h +10 h +12520 m +1619 h +12521 m +478 h +1 h +12522 m +7809 m +10 h +10 h +368 h +1 h +1 h +12523 m +1 h +12524 m +82 h +4 h +55 h +1499 h +119 h +4 h +2379 m +1939 m +10 h +12525 m +1504 m +4 h +355 m +538 h +1 h +1 h +1 h +74 h +124 h +12526 m +82 h +77 h +10 h +11 h +1189 m +5348 m +265 h +322 m +1 h +1 h +74 h +1 h +4 h +10 h +806 m +10 h +185 h +10 h +4 h +4 h +1 h +1 h +4815 m +1 h +195 h +1 h +10 h +170 h +986 h +12527 m +4 h +4 h +12528 m +12529 m +447 h +5224 m +124 h +12530 m +91 h +10 h +4 h +1 h +4 h +146 h +4 h +1 h +10 h +1 h +1366 m +1 h +12531 m +1 h +10099 m +12532 m +10 h +12533 m +367 h +114 h +12534 m +12535 m +3704 m +1646 m +1 h +1 h +41 h +447 h +41 h +219 h +109 h +82 h +1 h +219 h +4 h +4 h +55 h +1 h +13 h +3 h +10 h +12536 m +1 h +12537 m +1 h +55 h +3 h +10 h +10 h +1 h +12538 m +1 h +94 h +11 h +4 h +59 h +368 h +4 h +83 h +4 h +12539 m +10 h +10 h +4 h +1 h +4 h +1 h +12540 m +1 h +10 h +1 h +1 h +12541 m +12542 m +307 h +1642 h +4 h +4 h +12543 m +64 h +10 h +4 h +65 h +4 h +4 h +10 h +82 h +4 h +1 h +12544 m +12545 m +12546 m +12547 m +11 h +1389 m +1 h +12548 m +12549 m +10 h +4 h +10 h +4 h +10 h +274 h +12550 m +1 h +1 h +12551 m +4 h +10 h +10 h +124 h +4 h +12552 m +10 h +12553 m +1 h +57 h +1 h +4 h +79 h +82 h +147 h +10 h +12554 m +4 h +15 m +4 h +167 h +1 h +12555 m +79 h +10 h +569 h +4 h +219 h +12556 m +1714 m +3 h +146 h +12557 m +12558 m +12559 m +4 h +601 h +1766 h +1 h +1337 m +12560 m +1 h +4 h +12561 m +11 h +12562 m +82 h +12563 m +12564 m +4 h +55 h +12565 m +195 h +4 h +12566 m +4 h +1 h +13 h +12567 m +538 h +4 h +1 h +1 h +938 h +12568 m +12569 m +258 h +10 h +12570 m +74 h +4 h +10 h +12571 m +12572 m +4 h +307 h +1 h +4 h +1 h +443 h +11 h +1 h +11 h +4 h +4 h +10 h +4 h +10 h +11 h +13 h +12573 m +1 h +4 h +12574 m +12575 m +4 h +12576 m +1 h +10 h +12577 m +4 h +718 m +4 h +144 h +56 h +12578 m +12579 m +12580 m +4 h +12581 m +4 h +12582 m +4 h +94 h +1650 h +4 h +367 h +55 h +4 h +45 h +4 h +12583 m +12584 m +12585 m +1 h +10 h +104 h +1948 m +10 h +1359 h +28 h +12586 m +4 h +10 h +4 h +1919 m +4 h +12587 m +4 h +1309 m +238 h +46 m +1 h +12588 m +12589 m +10 h +8 h +578 h +112 h +83 h +1 h +1056 m +4 h +10 h +10 h +1751 m +1 h +124 h +10 h +11 h +1 h +1 h +12590 m +82 h +4 h +172 h +57 h +12591 m +4 h +1 h +12592 m +4 h +10 h +10 h +12593 m +4 h +12594 m +4 h +4 h +3499 m +4 h +10 h +1 h +4 h +195 h +4 h +45 h +1 h +12595 m +4 h +10 h +12596 m +146 h +4 h +1508 m +1 h +203 h +238 h +2733 h +12597 m +4 h +7901 m +158 h +10 h +9347 m +169 h +888 m +1 h +4 h +1 h +12598 m +1 h +41 h +4 h +12599 m +1 h +12600 m +4 h +10 h +4 h +12 h +274 h +11 h +10 h +4 h +1835 m +10 h +4 h +1 h +4 h +124 h +170 h +118 h +10 h +11 h +1 h +1 h +4 h +12601 m +12602 m +170 h +4 h +295 h +1486 m +92 h +10 h +4 h +11 h +124 h +10 h +4 h +1 h +109 h +4 h +3161 m +12603 m +4 h +10 h +4 h +10 h +12604 m +83 h +59 h +4 h +31 h +747 m +10 h +10 h +11 h +3 h +10 h +12605 m +57 h +4 h +185 h +1 h +12606 m +4 h +12607 m +1 h +12 h +4810 m +82 h +169 h +3 h +10 h +12608 m +10 h +4 h +1020 m +1030 h +12609 m +279 h +10 h +1619 h +4 h +181 h +4 h +12610 m +4 h +10 h +4 h +27 h +4 h +12611 m +12612 m +1 h +11 h +25 h +4 h +11 h +10 h +4 h +156 h +4 h +10 h +25 h +4 h +83 h +1981 m +31 h +976 h +4 h +83 h +10 h +25 h +147 h +12613 m +4 h +299 m +1 h +1 h +4 h +1 h +10 h +4 h +266 h +718 h +12614 m +1 h +1 h +258 h +1 h +358 h +6851 m +12615 m +939 h +147 h +4 h +1 h +12616 m +4 h +4 h +10 h +1 h +1 h +4 h +12617 m +12618 m +12619 m +4 h +4 h +12620 m +4 h +59 h +4 h +12621 m +1 h +4 h +4 h +94 h +307 h +108 h +12622 m +172 h +125 h +12623 m +1886 m +1 h +83 h +1 h +1 h +3342 m +109 h +10 h +4 h +1 h +11 h +1 h +158 h +12624 m +12625 m +1 h +184 h +4 h +1089 h +56 h +4 h +59 h +205 m +1 h +1 h +4 h +10 h +12626 m +295 h +11 h +4 h +4 h +687 h +1 h +10 h +4 h +10 h +4 h +164 h +109 h +279 h +10 h +4 h +4 h +4 h +4 h +1 h +4 h +157 h +82 h +626 m +11 h +196 h +109 h +278 h +8147 m +10 h +4 h +59 h +5 m +1 h +104 h +83 h +3 h +83 h +1 h +82 h +104 h +361 m +4 h +10 h +57 h +12627 m +4 h +10 h +4 h +1 h +4 h +12 h +11 h +4 h +10 h +1 h +9431 m +124 h +4 h +1 h +82 h +4 h +10 h +82 h +109 h +10 h +55 h +4 h +1 h +1 h +4 h +1 h +10 h +196 h +4 h +11 h +238 h +124 h +4 h +1532 m +368 h +1 h +73 h +332 h +1 h +12628 m +124 h +4 h +12629 m +9902 m +113 h +1127 m +10 h +4 h +41 h +1322 m +12 h +108 h +10 h +1235 m +435 m +169 h +92 h +4 h +10 h +12630 m +1 h +12631 m +447 h +10 h +250 h +147 h +196 h +4 h +4 h +4 h +10 h +258 h +1 h +1 h +1 h +12632 m +4 h +4 h +57 h +12633 m +9040 m +10 h +4 h +41 h +1 h +258 h +82 h +12634 m +4 h +27 h +146 h +4 h +41 h +12635 m +146 h +12636 m +4 h +10 h +1 h +125 h +10 h +12637 m +4 h +10 h +82 h +83 h +1 h +12638 m +1 h +12639 m +2072 m +1 h +12640 m +4 h +10 h +36 h +4 h +1 h +3396 m +4 h +10 h +4 h +1 h +83 h +12641 m +74 h +4 h +10 h +1 h +1 h +12642 m +31 h +10 h +4 h +400 m +4 h +1 h +4 h +1 h +1 h +4 h +1 h +12643 m +12644 m +25 h +82 h +238 h +12645 m +173 h +97 h +538 h +12 h +74 h +1576 m +10 h +4 h +10 h +4 h +12646 m +4 h +59 h +4 h +4 h +4 h +12647 m +83 h +4 h +12648 m +4 h +4 h +1 h +4 h +10 h +1 h +818 m +12649 m +10 h +4 h +129 h +869 m +3 h +1 h +4 h +1 h +4 h +12650 m +11 h +4 h +12651 m +1 h +1 h +11 h +4 h +10 h +12652 m +1 h +1 h +4 h +4 h +12653 m +4 h +1 h +4 h +443 h +238 h +1 h +1 h +4 h +278 h +3 h +77 h +4 h +1 h +1 h +4 h +10 h +12654 m +4 h +4 h +687 h +10 h +1 h +118 h +10 h +4 h +109 h +368 h +10 h +10 h +12655 m +430 m +104 h +1 h +1 h +4 h +4 h +12656 m +1 h +139 h +12657 m +12658 m +1 h +10 h +45 h +114 h +10 h +55 h +258 h +1 h +57 h +12659 m +4 h +82 h +1 h +1 h +119 h +368 h +4 h +113 h +12660 m +1 h +4 h +13 h +82 h +4524 m +1 h +12661 m +4 h +4 h +12662 m +4 h +4 h +4 h +12663 m +1 h +1 h +7553 m +12664 m +12665 m +12666 m +109 h +12667 m +281 m +12668 m +1772 m +4 h +1 h +10 h +12669 m +156 h +4 h +1261 h +94 h +196 h +124 h +1655 m +156 h +4 h +4 h +966 h +10 h +1 h +12670 m +25 h +10 h +1 h +12671 m +83 h +4 h +802 m +587 m +1 h +10 h +94 h +147 h +1 h +135 h +3 h +4 h +10 h +258 h +12672 m +4 h +11 h +4 h +57 h +12 h +12673 m +4 h +4 h +12674 m +109 h +12675 m +12676 m +55 h +146 h +1 h +12677 m +10 h +1659 m +3373 m +4 h +1 h +4 h +12678 m +48 h +12679 m +4 h +4 h +1 h +143 h +125 h +169 h +4 h +1271 m +4 h +146 h +4 h +173 h +12680 m +10 h +4 h +10 h +4 h +1 h +1 h +10 h +123 h +135 h +4 h +1 h +156 h +181 h +4 h +1 h +1 h +4 h +12681 m +1 h +4 h +124 h +10 h +10 h +40 h +12682 m +4 h +13 h +11 h +125 h +1 h +4 h +4 h +11 h +196 h +114 h +1 h +10 h +4 h +1250 h +1201 h +4 h +12683 m +10 h +12684 m +12685 m +4 h +4 h +4 h +119 h +13 h +10 h +25 h +82 h +4 h +4 h +4 h +10 h +195 h +1 h +10 h +307 h +74 h +12686 m +1 h +1 h +10 h +4 h +12687 m +1 h +4 h +12688 m +4 h +1 h +4 h +1 h +3188 m +83 h +1 h +125 h +10 h +10 h +1 h +10 h +4905 m +11 h +12689 m +167 h +1 h +10 h +146 h +11 h +92 h +12690 m +9228 m +1 h +4 h +4 h +104 h +4 h +65 h +1 h +10 h +12691 m +79 h +195 h +4 h +57 h +12692 m +4 h +12693 m +4 h +1486 m +13 h +12694 m +10 h +12695 m +10 h +104 h +4 h +4 h +1 h +12696 m +1 h +146 h +4 h +59 h +10 h +4 h +4 h +12697 m +10 h +59 h +1 h +12698 m +4 h +12699 m +82 h +123 h +104 h +10 h +4 h +124 h +784 m +278 h +10 h +767 m +129 h +4 h +4 h +1 h +1542 m +10 h +4 h +4378 m +12700 m +147 h +4 h +10 h +124 h +10 h +181 h +4 h +386 h +10 h +4 h +92 h +82 h +1 h +147 h +10 h +1 h +12701 m +1 h +4 h +79 h +12702 m +4 h +169 h +41 h +172 h +11 h +82 h +4 h +4 h +109 h +4 h +1 h +1 h +106 m +10 h +3 h +12703 m +11358 m +1 h +479 m +885 m +12704 m +266 h +1 h +192 h +10 h +1 h +1 h +12705 m +1261 h +4 h +10 h +399 h +12706 m +4 h +9893 m +1 h +10 h +538 h +1 h +79 h +4 h +4 h +12707 m +10 h +82 h +1 h +12708 m +12709 m +4 h +57 h +12710 m +12711 m +11 h +12712 m +2794 m +4 h +195 h +1 h +12713 m +1 h +1261 h +57 h +4 h +1 h +1 h +10 h +10 h +1 h +4 h +4 h +104 h +10 h +601 h +10 h +4 h +5422 m +1 h +486 m +83 h +10 h +45 h +4 h +4 h +4 h +10 h +12714 m +4 h +4 h +55 h +12715 m +1 h +4 h +48 h +4 h +1359 h +4 h +12716 m +12717 m +10 h +57 h +1 h +11 h +1201 h +1 h +82 h +353 m +12718 m +1089 h +1 h +4 h +31 h +48 h +4 h +1 h +1 h +1 h +143 h +12719 m +1 h +4 h +10 h +10 h +25 h +250 h +4 h +10 h +113 h +10 h +25 h +1 h +4 h +12720 m +110 h +59 h +1 h +10 h +1 h +4 h +4 h +10 h +1 h +1 h +12721 m +1 h +12722 m +4 h +92 h +1366 m +10 h +10 h +4 h +143 h +10 h +1 h +1 h +104 h +12199 m +1185 m +169 h +12723 m +4 h +4 h +601 h +124 h +12724 m +57 h +265 h +3 h +11 h +1 h +12725 m +10 h +12726 m +4 h +10 h +1 h +4 h +4 h +11 h +4 h +59 h +1 h +4 h +1 h +74 h +4 h +196 h +1804 m +1 h +146 h +12727 m +4 h +164 h +11 h +59 h +158 h +46 m +4 h +82 h +4 h +1 h +464 h +1 h +1 h +12728 m +12729 m +97 h +12730 m +1 h +3 h +3 h +11 h +83 h +1 h +59 h +12731 m +12732 m +4 h +4 h +146 h +31 h +4 h +83 h +41 h +10 h +12733 m +10 h +11 h +103 h +109 h +12734 m +4 h +1 h +338 h +4 h +10 h +3067 m +11 h +4 h +1 h +10 h +125 h +10 h +12735 m +4 h +124 h +4 h +135 h +1 h +12736 m +2846 m +12737 m +4 h +12738 m +229 h +12739 m +10 h +11 h +1535 m +1 h +10 h +123 h +124 h +12740 m +1737 m +10 h +82 h +31 h +1 h +10 h +4 h +12741 m +1718 m +4 h +13 h +4 h +4 h +358 h +1 h +11477 m +12742 m +83 h +4 h +10 h +12743 m +12744 m +55 h +125 h +4 h +4 h +12745 m +12746 m +10 h +10 h +996 m +4 h +403 h +1 h +83 h +1 h +1 h +12747 m +1 h +1 h +147 h +4 h +4 h +4 h +2935 m +1 h +4 h +41 h +10 h +124 h +1 h +91 h +12748 m +4 h +10 h +1 h +12749 m +430 m +12750 m +10 h +4 h +6391 m +3 h +10 h +25 h +64 h +1 h +1 h +12751 m +12752 m +83 h +4 h +1 h +4 h +4 h +12753 m +82 h +10 h +4 h +4 h +964 m +12754 m +1 h +1 h +4 h +12755 m +4 h +12756 m +109 h +10177 m +83 h +10 h +1 h +10 h +4 h +10 h +3 h +10 h +1 h +10 h +12757 m +1278 m +12758 m +1 h +12759 m +403 h +83 h +92 h +1 h +4 h +510 m +195 h +124 h +10 h +4 h +10 h +12760 m +1 h +12761 m +12762 m +4 h +10 h +1 h +12 h +6808 m +10 h +4 h +195 h +4 h +4 h +258 h +1691 h +10 h +82 h +4 h +4 h +10 h +10 h +56 h +258 h +1 h +83 h +4 h +12763 m +12764 m +1 h +12765 m +1 h +4 h +10 h +1 h +1 h +57 h +4 h +1 h +4 h +12766 m +135 h +36 h +25 h +12767 m +12768 m +10 h +12769 m +12770 m +10 h +1 h +4 h +4 h +59 h +4 h +12771 m +12772 m +1 h +4 h +4 h +10 h +10 h +82 h +4 h +648 m +733 m +11 h +1304 m +92 h +4 h +181 h +4 h +109 h +12773 m +299 m +3 h +4 h +12774 m +4 h +1 h +1 h +1 h +1 h +196 h +1 h +4 h +939 h +12775 m +4 h +4 h +110 h +1 h +12776 m +109 h +4 h +4 h +4 h +59 h +4 h +97 h +1 h +1 h +1027 h +4 h +10 h +1 h +12777 m +12778 m +1 h +12779 m +25 h +10 h +10 h +135 h +12780 m +185 h +10 h +10 h +4 h +36 h +4 h +4 h +264 h +4 h +11 h +82 h +4 h +368 h +4 h +1 h +4 h +10 h +4 h +4 h +31 h +59 h +10 h +74 h +10 h +4 h +4 h +4 h +1 h +10 h +11 h +1 h +1 h +3 h +4 h +147 h +1 h +10 h +41 h +3070 m +4 h +4718 m +10 h +763 m +295 h +12781 m +1 h +12782 m +4 h +4 h +4 h +4 h +12783 m +4 h +1 h +124 h +1 h +109 h +1 h +41 h +4 h +4 h +12784 m +12785 m +82 h +109 h +10 h +25 h +110 h +10 h +2506 m +10 h +12 h +13 h +1 h +125 h +1 h +10 h +1 h +12786 m +12787 m +124 h +383 h +31 h +4 h +1 h +12788 m +12789 m +4 h +12790 m +4 h +4 h +74 h +1 h +4 h +4 h +1 h +11 h +10 h +443 h +4 h +12791 m +36 h +12792 m +1 h +82 h +12793 m +59 h +12794 m +12795 m +10221 m +12796 m +1027 h +10 h +12797 m +11 h +190 h +12798 m +4 h +10 h +4 h +692 h +31 h +82 h +94 h +59 h +7479 m +1265 m +1 h +124 h +41 h +4 h +1 h +12799 m +109 h +82 h +1535 m +12800 m +114 h +12801 m +4 h +4 h +4 h +124 h +12802 m +1454 m +4 h +69 h +10 h +4 h +10 h +383 h +36 h +4 h +57 h +4 h +1 h +109 h +57 h +4 h +1 h +10 h +4 h +4 h +3 h +1 h +4 h +12803 m +4 h +10 h +73 h +12804 m +4 h +10 h +10 h +1016 h +10 h +1 h +1359 h +4 h +12805 m +12806 m +11 h +359 h +3 h +54 m +1 h +4 h +10 h +4 h +1 h +10 h +964 m +12807 m +4 h +12808 m +4 h +12809 m +1271 m +12810 m +4 h +73 h +273 m +3 h +10 h +1 h +1 h +10 h +1499 h +12811 m +12812 m +4 h +74 h +143 h +12813 m +12814 m +1 h +114 h +12815 m +1016 h +674 m +12816 m +4 h +10 h +1 h +1 h +12817 m +939 h +12818 m +4 h +687 h +10 h +4 h +10 h +1 h +12819 m +1 h +1 h +12820 m +12821 m +4 h +55 h +12822 m +1 h +12823 m +1 h +4 h +4074 m +4 h +12824 m +447 h +82 h +12825 m +1 h +59 h +12826 m +1 h +1 h +10 h +11 h +10 h +1 h +97 h +31 h +4 h +12827 m +10 h +4 h +4 h +12828 m +258 h +10 h +146 h +135 h +12829 m +12830 m +124 h +74 h +10 h +10 h +1 h +1 h +10 h +4 h +12831 m +108 h +478 h +1 h +1 h +1 h +1 h +10 h +10 h +1 h +1 h +332 h +12832 m +12833 m +4723 m +22 h +4 h +4 h +4 h +1619 h +10 h +12834 m +885 m +4 h +1 h +4 h +1 h +83 h +1 h +4 h +4 h +4 h +12835 m +4 h +4 h +1470 h +1 h +1 h +1 h +4 h +10 h +1 h +4 h +10 h +10 h +4 h +4 h +31 h +10 h +1 h +12836 m +73 h +4 h +4 h +74 h +12837 m +92 h +4252 m +1 h +4 h +59 h +12838 m +1016 h +10 h +12839 m +10 h +4 h +12840 m +1 h +4 h +10 h +4 h +103 h +146 h +10 h +4 h +4 h +12841 m +1374 m +229 h +12842 m +12 h +114 h +4 h +12843 m +56 h +12844 m +1 h +885 h +4 h +12845 m +124 h +11 h +10 h +266 h +238 h +1 h +12846 m +181 h +119 h +10272 m +146 h +4 h +73 h +1 h +11 h +195 h +65 h +12847 m +12848 m +41 h +4 h +10 h +12849 m +4 h +79 h +4 h +12850 m +10 h +1 h +41 h +313 m +4 h +1 h +10 h +64 h +1650 h +1 h +1 h +1 h +12851 m +12852 m +13 h +12853 m +4 h +2625 m +278 h +10 h +10 h +570 m +55 h +12854 m +12855 m +10 h +238 h +12856 m +4 h +10 h +353 m +266 h +1 h +12857 m +124 h +1 h +4 h +1 h +4 h +4 h +1 h +12858 m +4 h +12859 m +27 h +114 h +10 h +109 h +125 h +1 h +1 h +41 h +12860 m +12861 m +1 h +10 h +12862 m +1 h +8332 m +8663 m +11 h +338 h +12863 m +25 h +4 h +358 h +1 h +1 h +1 h +97 h +1 h +1 h +11 h +1 h +10 h +12864 m +4 h +147 h +12865 m +57 h +55 h +12866 m +1 h +10 h +297 h +4 h +11 h +1 h +12867 m +12868 m +1 h +1 h +4 h +4 h +10 h +4 h +12869 m +12870 m +10 h +12871 m +140 h +4 h +125 h +2813 m +4 h +10 h +4 h +4 h +1 h +4 h +124 h +124 h +12872 m +1 h +73 h +12873 m +4 h +12874 m +12875 m +12876 m +11 h +4 h +4 h +4 h +1 h +11 h +1 h +57 h +97 h +3 h +10 h +59 h +11 h +12877 m +270 h +12878 m +3 h +1 h +12879 m +2257 m +4 h +6290 m +12880 m +2438 m +1261 h +1 h +12881 m +4 h +1 h +12882 m +4 h +4 h +1 h +12883 m +238 h +250 h +316 m +4 h +1 h +1 h +1 h +59 h +1 h +57 h +12884 m +1 h +174 m +1 h +11 h +1 h +12885 m +990 m +4 h +12886 m +4 h +4 h +12887 m +794 m +3 h +10 h +4 h +399 h +45 h +10 h +12888 m +4 h +12889 m +12890 m +150 m +4 h +147 h +104 h +1 h +4 h +12891 m +1 h +10 h +10 h +109 h +4 h +12892 m +12893 m +12894 m +4 h +12895 m +538 h +258 h +5093 m +119 h +10783 m +1030 h +1 h +12896 m +3 h +806 m +4 h +12897 m +4 h +4 h +4 h +4 h +12898 m +82 h +1 h +27 h +4 h +1 h +10 h +28 h +10 h +7135 m +4 h +170 h +4 h +181 h +10 h +1 h +1 h +73 h +4 h +358 h +1 h +158 h +4 h +4 h +4 h +4 h +10 h +4 h +10 h +4 h +4 h +4 h +1 h +3 h +4 h +4 h +1 h +4 h +181 h +4 h +1 h +12899 m +59 h +12900 m +4 h +110 h +229 h +92 h +55 h +10 h +11 h +119 h +104 h +12901 m +10 h +10 h +4 h +12902 m +57 h +10 h +1 h +1 h +185 h +110 h +1 h +12903 m +1 h +12904 m +1 h +1261 h +10 h +1 h +167 h +190 h +10 h +12905 m +1309 h +4 h +124 h +12906 m +4 h +1 h +27 h +4 h +12907 m +12908 m +763 m +4 h +4 h +25 h +12909 m +12910 m +10 h +1 h +12911 m +11 h +289 h +265 h +12912 m +4 h +1 h +4596 m +147 h +83 h +10 h +10 h +1 h +158 h +1 h +4 h +1 h +4 h +1 h +4 h +12913 m +10 h +12914 m +4 h +12915 m +4 h +4 h +1 h +12916 m +10 h +4 h +12917 m +12918 m +10 h +4 h +36 h +1 h +109 h +10 h +1 h +109 h +10 h +10 h +97 h +10 h +167 h +4 h +185 h +4 h +4 h +10 h +4 h +11 h +12919 m +10 h +1 h +12920 m +1 h +10 h +74 h +1 h +4 h +109 h +3 h +12921 m +59 h +4 h +4 h +114 h +307 h +1 h +4 h +1 h +10 h +11 h +4 h +4 h +4 h +91 h +4 h +12922 m +56 h +1 h +10 h +10 h +4 h +3345 m +12923 m +10 h +4 h +97 h +270 h +4 h +10 h +10 h +4 h +1 h +4 h +12924 m +10 h +4 h +1 h +12925 m +10 h +4 h +1 h +12926 m +147 h +112 h +1 h +1 h +4 h +1 h +4 h +124 h +4 h +64 h +4 h +10 h +4 h +640 h +181 h +359 h +4 h +1642 h +4 h +10 h +109 h +4 h +114 h +1 h +74 h +1409 h +4 h +4 h +57 h +1 h +109 h +10 h +77 h +4 h +12927 m +1 h +11 h +265 h +1 h +4 h +12928 m +10 h +1650 h +25 h +4 h +1 h +1 h +1 h +12929 m +146 h +4 h +12930 m +4 h +4 h +4 h +74 h +1 h +12931 m +83 h +11 h +4 h +12932 m +13 h +12933 m +4 h +4 h +1 h +12934 m +79 h +12935 m +196 h +1017 h +10 h +11 h +12936 m +12 h +172 h +1 h +4 h +41 h +4 h +4 h +4 h +4 h +4 h +12937 m +4 h +27 h +4 h +172 h +10 h +265 h +12938 m +4 h +82 h +4 h +59 h +1 h +10 h +10 h +297 h +82 h +4 h +258 h +1 h +2617 m +10 h +11 h +4 h +11 h +10 h +41 h +12939 m +219 h +4 h +4 h +97 h +12940 m +4 h +10 h +1822 h +12941 m +1 h +4 h +278 h +12942 m +12943 m +4 h +4 h +12944 m +12945 m +12946 m +1713 m +10 h +83 h +332 h +425 m +4 h +12947 m +41 h +125 h +104 h +4 h +3742 m +12948 m +10 h +1 h +12949 m +1 h +447 h +10 h +10 h +258 h +83 h +4 h +181 h +1 h +10 h +172 h +92 h +92 h +56 h +12950 m +186 h +4 h +12951 m +1 h +4 h +10 h +10 h +12952 m +56 h +447 h +10 h +12953 m +12954 m +1 h +274 h +4 h +25 h +4 h +12955 m +4 h +135 h +108 h +1 h +12956 m +10 h +1 h +4 h +2582 m +4 h +12957 m +12958 m +4 h +10 h +1137 h +805 m +59 h +82 h +8133 m +4 h +4 h +10 h +195 h +114 h +1 h +10 h +12959 m +8017 m +4 h +12960 m +4 h +4 h +10 h +11 h +169 h +12961 m +1 h +5141 m +10 h +3988 m +12962 m +8327 m +11 h +12963 m +12964 m +10 h +4 h +1 h +12965 m +172 h +12966 m +238 h +1 h +77 h +10 h +104 h +1 h +10 h +4 h +1 h +692 h +10 h +4177 m +109 h +1 h +4 h +4 h +1 h +1 h +12967 m +12968 m +4 h +195 h +12969 m +1 h +1685 m +10 h +10 h +4 h +12970 m +3742 m +12971 m +1 h +1780 h +4 h +4 h +10 h +10 h +12972 m +10 h +4 h +12973 m +12974 m +4 h +1 h +12975 m +1 h +10 h +124 h +57 h +12976 m +10 h +6187 m +196 h +11 h +79 h +12977 m +12978 m +1003 m +1 h +124 h +4 h +12979 m +1 h +1 h +11 h +82 h +59 h +4 h +4 h +3 h +10 h +5348 m +266 h +4 h +12980 m +1 h +11 h +1 h +109 h +12981 m +94 h +4 h +10 h +1271 h +4 h +4 h +12982 m +10 h +104 h +10 h +12983 m +327 m +41 h +4 h +4 h +1454 m +1 h +1 h +1 h +10 h +25 h +12984 m +10 h +4 h +10 h +10 h +3115 m +4 h +11 h +4 h +10 h +4 h +81 m +31 h +4 h +4 h +258 h +59 h +12985 m +10 h +10 h +114 h +4 h +4 h +4 h +109 h +1 h +4 h +172 h +82 h +2374 m +11 h +64 h +27 h +1 h +10 h +4 h +12986 m +12987 m +12988 m +41 h +1504 m +4 h +12989 m +4 h +4 h +10 h +12990 m +1 h +4 h +266 h +12991 m +4 h +4 h +31 h +12992 m +4 h +12993 m +1 h +274 h +1389 m +10 h +4 h +4 h +3 h +1 h +170 h +11 h +386 h +1 h +10 h +10 h +10 h +10 h +11 h +203 h +12994 m +12995 m +4 h +1 h +12996 m +4 h +146 h +1 h +10 h +27 h +299 m +1454 m +4229 m +258 h +56 h +10 h +1250 h +4 h +94 h +1 h +10 h +4 h +4 h +1 h +10 h +1 h +12 h +146 h +229 h +82 h +5363 m +1 h +25 h +12997 m +4 h +10 h +3680 m +59 h +1 h +10 h +4 h +2592 m +12998 m +10 h +4 h +158 h +4 h +56 h +59 h +55 h +12999 m +10 h +57 h +13000 m +104 h +1 h +2865 m +10 h +4 h +1 h +1 h +11 h +1 h +4 h +135 h +4 h +1 h +1 h +13001 m +4 h +1 h +990 m +79 h +10 h +1003 m +45 h +1 h +10378 m +4 h +1 h +1 h +10 h +225 m +1 h +10 h +10 h +13002 m +41 h +92 h +1 h +2617 m +13003 m +10 h +97 h +1766 h +74 h +10 h +4 h +25 h +3344 m +266 h +1 h +4 h +13004 m +4 h +1 h +10 h +1 h +13005 m +297 h +4 h +143 h +4 h +1 h +10 h +4 h +13006 m +1 h +1 h +4 h +13007 m +1 h +10 h +13008 m +10 h +158 h +10 h +82 h +4 h +4 h +464 h +22 h +4 h +195 h +4 h +5863 m +61 m +4 h +4 h +1 h +10 h +10 h +1 h +702 m +11 h +4 h +13009 m +13010 m +204 m +13011 m +4 h +13012 m +164 h +278 h +4 h +41 h +109 h +13013 m +57 h +13014 m +1 h +13015 m +1 h +9771 m +13016 m +4 h +169 h +368 h +5 m +10 h +4 h +4 h +79 h +10 h +1 h +12 h +1220 m +1 h +4 h +3 h +4 h +266 h +64 h +4 h +4 h +13017 m +13018 m +1 h +10 h +4 h +10 h +113 h +1 h +1 h +13019 m +4 h +250 h +10 h +3 h +11 h +104 h +10 h +4 h +13020 m +1261 h +13021 m +64 h +10 h +13022 m +10 h +1 h +13023 m +10 h +13024 m +4 h +3 h +10 h +4 h +1 h +1 h +41 h +4 h +11 h +4 h +186 h +12 h +13025 m +59 h +1406 h +13026 m +1 h +1 h +10 h +885 h +1 h +4 h +13027 m +22 h +139 h +11 h +4 h +13028 m +4 h +4 h +83 h +976 h +11 h +13029 m +125 h +1 h +10 h +10 h +13030 m +4 h +10 h +10 h +1 h +1 h +59 h +4 h +4 h +13031 m +4 h +1 h +10 h +13032 m +40 h +10 h +1 h +4 h +4 h +13033 m +4 h +59 h +1 h +4 h +1822 h +109 h +45 h +13034 m +4 h +4 h +541 m +10 h +92 h +1 h +55 h +10 h +4 h +13035 m +11 h +10 h +4 h +10 h +11 h +192 h +4 h +57 h +1 h +13036 m +1 h +11 h +4 h +400 m +4 h +65 h +114 h +13037 m +10 h +4 h +4 h +13038 m +124 h +4 h +31 h +135 h +10 h +10 h +8 h +83 h +1 h +5 m +4 h +1 h +1751 m +1 h +1 h +10 h +4 h +1 h +1 h +13039 m +4 h +1 h +13040 m +190 h +3702 m +4 h +371 h +1 h +1 h +4 h +1 h +13041 m +13042 m +1 h +10 h +1 h +25 h +4 h +1 h +1 h +195 h +4 h +4 h +13043 m +1 h +56 h +1 h +4 h +10 h +10700 m +11246 m +1 h +4 h +13044 m +13045 m +4 h +517 m +4 h +13046 m +1 h +1 h +1544 m +2496 m +4 h +13047 m +7862 m +11 h +4 h +113 h +135 h +871 m +10 h +10 h +11 h +986 h +2374 m +4 h +10 h +109 h +2418 m +278 h +13048 m +10 h +1 h +13049 m +4 h +1 h +1 h +13050 m +124 h +1 h +4516 m +167 h +69 h +13 h +278 h +359 h +4 h +4 h +10 h +83 h +1 h +13051 m +7616 m +13052 m +13053 m +13054 m +1 h +13055 m +1 h +358 h +157 h +11990 m +10 h +4 h +11 h +1632 m +11 h +57 h +6863 m +10 h +77 h +36 h +1016 h +11 h +914 m +276 h +4 h +110 h +13056 m +110 h +4 h +4 h +13057 m +25 h +83 h +4 h +10 h +13058 m +4 h +4 h +13059 m +276 h +143 h +4 h +125 h +270 h +13060 m +4 h +1 h +1981 m +1 h +10 h +4 h +83 h +4 h +114 h +45 h +64 h +10 h +31 h +538 h +2885 m +25 h +1 h +1 h +4 h +10 h +83 h +13061 m +1 h +13062 m +13063 m +4 h +1785 m +520 h +4 h +10 h +74 h +186 h +10 h +1 h +1 h +10 h +1 h +4 h +2617 h +4 h +196 h +13064 m +119 h +11 h +13065 m +4 h +13066 m +27 h +1 h +8 h +7243 m +13067 m +13068 m +1 h +13069 m +10 h +911 m +4 h +4 h +1 h +1 h +36 h +4 h +4 h +10 h +11 h +10 h +1 h +13070 m +13071 m +13072 m +4 h +13073 m +13074 m +1 h +4 h +74 h +2433 m +4 h +4 h +125 h +358 h +13075 m +13076 m +13077 m +13078 m +12 h +4 h +97 h +13079 m +1 h +1 h +25 h +4 h +109 h +4 h +4 h +1284 m +13080 m +1 h +13081 m +1 h +13082 m +13083 m +13084 m +172 h +1 h +10 h +173 h +4 h +13085 m +4 h +1 h +10 h +10 h +4 h +10 h +4 h +332 h +114 h +2813 m +4 h +4 h +4 h +167 h +4 h +1 h +13086 m +11 h +25 h +13087 m +4 h +1 h +74 h +1 h +110 h +2865 m +10 h +184 h +41 h +57 h +4 h +13088 m +4 h +13089 m +4 h +83 h +10 h +104 h +4 h +1 h +109 h +10 h +13090 m +3558 m +59 h +285 m +3 h +109 h +1 h +1 h +1 h +10 h +1 h +13091 m +4 h +4 h +4 h +10 h +109 h +4 h +4 h +1 h +297 h +125 h +10 h +4 h +386 h +4 h +13092 m +109 h +10 h +295 h +8 h +1 h +4 h +124 h +13093 m +64 h +10 h +1281 m +1714 m +10 h +13094 m +1 h +10 h +10 h +1 h +4824 m +57 h +10 h +10 h +1 h +4 h +1359 h +3 h +13095 m +1 h +13096 m +146 h +4 h +164 h +10 h +3278 m +13097 m +1 h +3909 m +13098 m +13099 m +4 h +10 h +12 h +3383 m +41 h +119 h +1 h +4 h +4 h +10 h +10 h +124 h +2591 m +1 h +4 h +4 h +13100 m +10 h +5053 m +10 h +267 m +11 h +1 h +65 h +4 h +13101 m +10 h +1 h +57 h +104 h +4 h +1 h +13102 m +13103 m +4 h +13104 m +1 h +3089 m +6821 m +1 h +4 h +10 h +13105 m +1 h +195 h +13106 m +1 h +4 h +4 h +1 h +4 h +91 h +196 h +4 h +123 h +4 h +13107 m +10 h +119 h +1 h +13108 m +4 h +13109 m +13110 m +4 h +4 h +1 h +13111 m +64 h +13112 m +10 h +1 h +10 h +4 h +156 h +73 h +196 h +10 h +4 h +4 h +368 h +10 h +45 h +13113 m +4 h +4 h +167 h +1 h +1118 m +1 h +13114 m +10 h +8 h +10 h +11 h +4 h +1 h +83 h +11 h +118 h +1 h +10 h +4 h +13115 m +13116 m +13117 m +1 h +7 h +4 h +13118 m +8496 m +935 h +13119 m +69 h +1 h +4 h +10 h +3 h +59 h +10 h +1 h +1006 m +13120 m +114 h +31 h +489 m +1 h +109 h +13121 m +164 h +124 h +4 h +4 h +1 h +12218 m +13122 m +1 h +4 h +4 h +13123 m +4 h +97 h +4 h +1403 h +278 h +4 h +10 h +13124 m +278 h +57 h +4 h +4 h +4 h +276 h +13125 m +13126 m +289 h +56 h +1 h +4 h +1766 h +1646 m +10 h +4 h +11 h +13127 m +4 h +13128 m +1 h +1 h +1556 m +129 h +82 h +1 h +13129 m +4 h +4 h +229 h +1 h +359 h +92 h +10 h +11 h +10 h +13130 m +1 h +13131 m +4 h +1030 h +157 h +124 h +13132 m +13133 m +73 h +11 h +4 h +110 h +939 h +3 h +4 h +13134 m +13135 m +10 h +4 h +4 h +10 h +31 h +13136 m +4 h +13137 m +82 h +10 h +4 h +12 h +1 h +4 h +1 h +1528 m +10 h +124 h +4 h +10 h +11 h +4 h +4 h +10 h +41 h +10 h +10 h +1 h +125 h +13138 m +1 h +4 h +109 h +169 h +4 h +13139 m +13140 m +4 h +82 h +4 h +82 h +1 h +885 h +13141 m +12 h +1 h +13142 m +4 h +1 h +4 h +1 h +229 h +4 h +172 h +4 h +13143 m +4 h +11 h +10 h +4 h +10 h +266 h +13144 m +10 h +4 h +4 h +13145 m +1 h +4 h +558 m +4 h +4 h +4 h +569 h +82 h +13146 m +1 h +1 h +140 h +13147 m +10 h +13148 m +10 h +13149 m +13150 m +10 h +4 h +704 m +4 h +97 h +4 h +57 h +13151 m +1 h +4 h +4 h +4 h +1003 h +10 h +10 h +4 h +8 h +4 h +13152 m +4 h +1 h +1 h +13153 m +10 h +278 h +1 h +10 h +4 h +1 h +276 h +1 h +4 h +4 h +13154 m +1796 m +1 h +31 h +13155 m +13156 m +1 h +57 h +4 h +59 h +57 h +4 h +13157 m +488 h +1 h +4 h +9282 m +663 m +119 h +10 h +31 h +4 h +13158 m +4 h +4 h +11 h +10 h +11 h +11 h +4 h +82 h +10 h +4 h +13159 m +10 h +4 h +10 h +4 h +13160 m +13161 m +4 h +11 h +1 h +173 h +109 h +4 h +1 h +4 h +10 h +27 h +10 h +125 h +4 h +4 h +1 h +13162 m +1 h +10324 m +4 h +13163 m +1 h +75 m +13164 m +13165 m +25 h +1 h +4 h +4 h +1 h +10 h +10 h +97 h +10 h +1 h +13166 m +4 h +10 h +1 h +1 h +203 h +59 h +10 h +4 h +13167 m +1 h +1642 h +307 h +13 h +4 h +27 h +1 h +13168 m +10 h +1 h +13169 m +4 h +4 h +83 h +104 h +13170 m +0 h +276 h +1 h +4 h +601 h +230 h +3675 m +3112 m +1 h +464 h +114 h +4 h +13171 m +13172 m +13 h +4 h +266 h +4 h +13173 m +10 h +1 h +13174 m +13175 m +4 h +10 h +1 h +5348 m +4 h +7419 m +135 h +4 h +857 m +1 h +13176 m +219 h +13177 m +13178 m +1 h +2923 h +1 h +10 h +10 h +10 h +13179 m +4 h +4 h +113 h +114 h +276 h +13180 m +13181 m +195 h +377 h +1 h +13182 m +3 h +13183 m +4 h +124 h +1016 h +124 h +13184 m +4623 m +4 h +1 h +13185 m +4 h +109 h +4 h +25 h +5093 m +3 h +4 h +4 h +25 h +13186 m +114 h +1 h +3 h +13187 m +10 h +92 h +4 h +1 h +10 h +1089 h +13188 m +10 h +4 h +10 h +10 h +1 h +10 h +11 h +1 h +10 h +59 h +2924 m +4 h +10 h +13 h +4 h +4 h +1 h +73 h +965 m +157 h +13189 m +4 h +124 h +4 h +1309 h +10 h +338 h +230 h +687 h +4 h +13190 m +83 h +13191 m +11 h +11 h +13192 m +196 h +4 h +1116 m +13193 m +11 h +10 h +10 h +4 h +1 h +12 h +10 h +13194 m +13195 m +10 h +13196 m +13197 m +4 h +4 h +3555 m +10 h +1 h +4 h +97 h +267 m +135 h +1 h +1 h +11 h +10 h +4 h +10 h +1 h +4 h +41 h +1 h +1359 h +13198 m +59 h +4 h +1 h +13199 m +13200 m +13201 m +11 h +4 h +195 h +143 h +1 h +367 h +4 h +124 h +5504 m +1 h +1 h +3398 m +114 h +10 h +4 h +13202 m +13203 m +57 h +10 h +4 h +1 h +4314 m +13204 m +25 h +74 h +68 m +11 h +9400 m +13205 m +4 h +4 h +2510 m +1 h +1189 m +4 h +1 h +83 h +45 h +4 h +10 h +4 h +41 h +41 h +990 h +1 h +1 h +167 h +1886 m +1 h +4 h +13206 m +13207 m +13208 m +147 h +1 h +124 h +3558 m +31 h +1822 h +10 h +10 h +13209 m +13210 m +10 h +94 h +10 h +45 h +13211 m +1 h +13212 m +4 h +119 h +1 h +13213 m +147 h +13214 m +1915 m +4 h +10 h +4 h +1 h +4 h +4 h +383 h +13215 m +4 h +64 h +1 h +1620 m +10 h +5917 m +990 h +11 h +1 h +110 h +4 h +10 h +1 h +4 h +55 h +13216 m +195 h +4 h +4 h +147 h +6663 m +1 h +256 h +13217 m +13218 m +1 h +185 h +1 h +4 h +4 h +386 h +726 m +1 h +412 h +147 h +278 h +45 h +536 h +1 h +4 h +4 h +4 h +119 h +1 h +1 h +13219 m +1 h +25 h +10 h +4 h +13220 m +338 h +4 h +4 h +13221 m +10 h +124 h +204 m +13222 m +4 h +4 h +1 h +4 h +31 h +4 h +12 h +4 h +13223 m +13224 m +4 h +1 h +10 h +169 h +13225 m +10682 m +13226 m +13227 m +258 h +57 h +4 h +109 h +13228 m +307 h +57 h +1 h +59 h +4 h +13229 m +22 h +4 h +12 h +1 h +1 h +13230 m +4 h +13231 m +109 h +12 h +4 h +1 h +10 h +757 m +40 h +170 h +10 h +4 h +10468 m +258 h +1 h +13232 m +22 h +4 h +10 h +13233 m +10862 m +4 h +4 h +4 h +10 h +10 h +13234 m +13235 m +4 h +4 h +12 h +4 h +1 h +4 h +1 h +1 h +13236 m +1 h +1 h +1 h +104 h +119 h +4 h +4802 m +10 h +64 h +4 h +911 h +13237 m +13238 m +13239 m +4 h +1 h +10 h +4 h +1 h +4 h +1 h +1 h +1 h +965 m +536 h +4 h +4 h +1 h +4 h +112 h +4 h +4 h +4 h +13240 m +4 h +73 h +4 h +41 h +10 h +92 h +1 h +3 h +2920 m +146 h +4 h +1 h +13241 m +1 h +1 h +10 h +124 h +1 h +31 h +31 h +10 h +888 m +843 m +109 h +59 h +4 h +203 h +13 h +4 h +4 h +83 h +1 h +4 h +285 m +11 h +73 h +13242 m +124 h +83 h +10 h +4 h +1 h +4 h +10 h +3 h +4 h +4 h +11 h +195 h +59 h +195 h +1 h +10 h +10 h +10 h +13243 m +13244 m +11 h +11 h +4 h +297 h +10 h +4 h +468 m +698 m +10 h +1 h +11 h +4 h +124 h +167 h +4 h +13245 m +4 h +4 h +83 h +278 h +1 h +11 h +1 h +4 h +4 h +1 h +4 h +28 h +10 h +1 h +13246 m +10 h +109 h +13247 m +4 h +4 h +83 h +11 h +1 h +4 h +1619 h +1 h +1 h +73 h +4 h +400 m +112 h +4 h +276 h +13248 m +1 h +57 h +1 h +10 h +13249 m +4 h +4 h +1 h +4 h +3 h +1220 m +1 h +10 h +57 h +270 h +1 h +11485 m +109 h +3 h +1137 h +11 h +1 h +172 h +1 h +4 h +10 h +1 h +10 h +1 h +13250 m +2040 m +6107 m +1 h +65 h +1 h +10 h +10 h +192 h +4 h +1 h +4 h +4 h +238 h +2928 m +144 h +13251 m +1 h +4 h +1 h +196 h +13252 m +10 h +1 h +1 h +1 h +13253 m +185 h +4 h +13254 m +1 h +1 h +10 h +94 h +13255 m +41 h +4 h +4 h +157 h +911 h +1 h +4 h +656 m +13256 m +10 h +13257 m +97 h +143 h +204 m +4 h +169 h +1 h +4 h +13258 m +4 h +1137 h +1 h +6851 m +1 h +125 h +123 h +11 h +129 h +1 h +10 h +181 h +4 h +10 h +332 h +13259 m +10 h +109 h +1 h +4 h +13260 m +10 h +3539 m +4 h +185 h +11 h +13261 m +4 h +13262 m +4 h +1 h +11 h +123 h +4 h +10 h +109 h +4 h +4 h +10 h +74 h +8 h +10 h +1 h +1772 m +147 h +36 h +10 h +4 h +83 h +4 h +1 h +313 m +13263 m +74 h +104 h +124 h +1 h +1 h +1 h +4 h +10 h +83 h +13264 m +10 h +3 h +4 h +4 h +10 h +13265 m +109 h +4 h +4 h +1089 h +57 h +4 h +4 h +1 h +322 h +10 h +10 h +11 h +5567 m +139 h +1 h +1 h +1 h +10 h +4 h +4 h +1 h +4 h +1 h +13266 m +74 h +266 h +1 h +74 h +3558 h +114 h +31 h +10 h +10 h +1039 m +13267 m +10 h +157 h +1 h +4 h +10 h +108 h +1 h +4 h +1 h +13268 m +4 h +13269 m +13270 m +4 h +1835 m +278 h +4 h +13271 m +1 h +82 h +57 h +8 h +4 h +1 h +5557 m +13272 m +10 h +1 h +1 h +4 h +262 m +536 h +119 h +478 h +295 h +4561 m +13273 m +1 h +4 h +1 h +10 h +104 h +4 h +4 h +4 h +124 h +10 h +4 h +4 h +13274 m +4 h +4 h +4 h +4 h +4 h +23 h +4 h +13275 m +1 h +10 h +4145 m +4 h +4 h +1 h +4 h +488 h +1 h +4 h +1 h +65 h +4 h +1 h +1 h +1 h +4 h +1 h +10 h +13276 m +11 h +1137 h +1 h +4 h +4 h +13277 m +1 h +13278 m +10 h +4 h +84 m +295 h +509 m +113 h +10 h +1 h +1 h +45 h +170 h +13279 m +167 h +1 h +4 h +1 h +13280 m +4 h +4 h +3 h +13281 m +13282 m +4 h +13283 m +1 h +13284 m +1 h +10 h +2238 m +1 h +4 h +4 h +13285 m +10 h +4 h +4 h +4 h +10 h +12 h +4 h +83 h +167 h +4 h +4 h +4 h +1 h +4 h +195 h +3 h +4 h +10 h +4 h +1 h +976 h +4 h +13286 m +10 h +1 h +1 h +31 h +1 h +135 h +319 h +13287 m +4 h +4 h +10 h +4 h +4 h +4 h +3 h +1 h +59 h +13288 m +1 h +4 h +1 h +13289 m +4 h +59 h +13290 m +11 h +13291 m +1284 m +10 h +4 h +1 h +10 h +4 h +1 h +11 h +1261 h +1 h +119 h +6985 m +13292 m +6705 m +1 h +1 h +1 h +4 h +4 h +10 h +4 h +82 h +1553 m +1953 m +147 h +64 h +11 h +4 h +4 h +13293 m +1 h +443 h +10 h +4 h +10 h +82 h +4 h +4 h +10 h +109 h +4 h +13294 m +4 h +1 h +10 h +13295 m +41 h +65 h +82 h +5526 m +65 h +1 h +10 h +250 h +1 h +10 h +4 h +737 m +4 h +10 h +147 h +1 h +1 h +3 h +3 h +13296 m +13297 m +4 h +10 h +10 h +13298 m +13299 m +10414 m +11 h +4 h +4 h +4 h +1 h +82 h +4 h +4 h +1 h +4 h +4 h +11 h +4 h +1 h +1 h +4 h +59 h +1 h +11 h +3303 m +4 h +7999 m +73 h +124 h +1016 h +10 h +10 h +4 h +109 h +74 h +143 h +13300 m +4 h +13301 m +57 h +79 h +36 h +13302 m +1 h +13303 m +4509 m +10 h +4 h +11 h +1 h +181 h +1 h +13304 m +1 h +13305 m +1 h +10 h +10 h +13306 m +82 h +1 h +3815 m +10 h +1 h +10 h +10 h +1 h +13307 m +27 h +3 h +11 h +10 h +109 h +1 h +13308 m +8535 m +10 h +1 h +5682 m +10 h +57 h +1 h +4 h +1 h +262 h +1 h +4 h +1 h +10 h +4 h +13309 m +4 h +692 h +109 h +4 h +4 h +109 h +4 h +1 h +377 h +13310 m +146 h +11 h +1 h +295 h +4 h +4 h +4 h +112 h +125 h +4 h +4 h +13311 m +4 h +1619 h +10 h +1 h +1 h +195 h +1 h +412 h +718 h +109 h +1 h +4 h +1 h +692 h +4 h +13312 m +10 h +10 h +64 h +11 h +4 h +124 h +73 h +1 h +4 h +82 h +1 h +169 h +4 h +118 h +190 h +83 h +4 h +48 h +55 h +1 h +10 h +1 h +45 h +9027 m +4 h +1 h +4 h +1 h +11 h +10 h +278 h +10 h +939 h +1 h +55 h +258 h +4 h +10 h +2887 m +10 h +9450 m +13313 m +104 h +10 h +1 h +112 h +4 h +13314 m +4 h +10 h +25 h +10 h +4 h +13315 m +10 h +82 h +1 h +4 h +13316 m +109 h +13317 m +41 h +129 h +97 h +11 h +13318 m +13319 m +74 h +13320 m +1751 m +1 h +4 h +4 h +1 h +1 h +4 h +4 h +13321 m +123 h +11 h +4 h +36 h +1 h +74 h +10 h +620 m +4 h +10 h +10 h +1 h +4 h +1 h +77 h +13322 m +4 h +10 h +1619 h +1 h +147 h +4 h +13323 m +59 h +82 h +1 h +4 h +97 h +57 h +10 h +11 h +4 h +1409 h +1685 m +82 h +4 h +10 h +1 h +13324 m +13325 m +692 h +124 h +190 h +4 h +57 h +307 h +4 h +13326 m +10 h +358 h +601 h +4 h +4 h +295 h +4 h +4 h +4 h +174 m +1650 h +10 h +4 h +124 h +157 h +10 h +4 h +124 h +31 h +1 h +13327 m +4 h +13 h +2374 h +1 h +1619 h +388 m +13328 m +10 h +196 h +4 h +10 h +13329 m +13330 m +13331 m +82 h +55 h +31 h +13332 m +4 h +1 h +332 h +13333 m +139 h +13334 m +3 h +13335 m +25 h +4 h +3 h +1 h +307 h +601 h +10 h +12 h +13336 m +103 h +4 h +10 h +10 h +4 h +13337 m +4 h +1 h +10 h +13338 m +640 h +11 h +238 h +1137 h +359 h +172 h +1 h +112 h +11 h +1 h +4 h +13339 m +4 h +1478 h +4 h +4 h +65 h +10 h +83 h +4 h +1 h +59 h +4 h +10 h +1 h +4 h +230 h +313 m +3 h +10 h +4 h +10 h +4 h +4 h +4 h +1 h +1642 h +13340 m +1470 h +4 h +4 h +4 h +1 h +10 h +1 h +11 h +123 h +146 h +10 h +4 h +4 h +13341 m +4 h +1 h +31 h +4 h +1 h +10 h +41 h +1 h +13342 m +31 h +4 h +172 h +13343 m +104 h +4 h +10 h +45 h +4 h +10 h +118 h +1 h +3 h +3 h +1 h +13344 m +4 h +307 h +83 h +13345 m +13346 m +10 h +13347 m +10 h +1 h +10 h +1 h +11 h +1359 h +1117 m +146 h +4 h +11 h +1016 h +4 h +25 h +164 h +371 h +3177 m +13 h +109 h +124 h +11 h +808 m +1 h +196 h +459 h +4 h +4 h +13348 m +2788 h +1 h +48 h +13349 m +1 h +4 h +4 h +447 h +4 h +4 h +31 h +13350 m +104 h +10 h +13351 m +4 h +13352 m +10 h +82 h +1 h +10 h +13353 m +4 h +13354 m +10 h +57 h +10 h +13355 m +4 h +82 h +3307 m +31 h +4 h +13356 m +1 h +3070 m +13357 m +109 h +25 h +8324 m +536 h +1 h +10 h +4 h +4 h +13358 m +1 h +640 h +1 h +73 h +22 h +1 h +1 h +996 m +1 h +13359 m +11 h +10 h +10 h +1 h +11 h +13360 m +1 h +4 h +4 h +4 h +10 h +1 h +358 h +109 h +157 h +13361 m +757 h +10 h +1 h +4 h +3 h +11 h +4 h +13362 m +10 h +10 h +4 h +1 h +1 h +10 h +3028 m +1 h +1 h +4 h +10 h +13363 m +13364 m +10 h +1 h +8184 m +1 h +13365 m +4 h +4 h +4 h +4 h +4 h +4 h +11 h +13366 m +13367 m +13368 m +13369 m +1 h +4 h +1 h +262 h +779 h +124 h +4 h +13 h +65 h +13370 m +1470 h +125 h +59 h +10 h +25 h +124 h +11 h +10 h +4 h +4 h +31 h +27 h +113 h +10 h +1 h +4 h +10 h +13371 m +4 h +172 h +4 h +13372 m +91 h +4 h +1 h +8105 m +4 h +4 h +82 h +13373 m +3 h +1 h +4 h +4 h +8879 m +13 h +10 h +4 h +112 h +12 h +4 h +4 h +164 h +4 h +13374 m +1 h +135 h +114 h +13375 m +1 h +4 h +13376 m +11 h +4 h +10 h +4 h +1 h +1 h +1 h +82 h +10 h +4 h +25 h +13377 m +4 h +13378 m +79 h +13379 m +13380 m +4 h +10 h +4 h +1 h +83 h +1 h +4 h +143 h +1359 h +276 h +1 h +1 h +1 h +4 h +94 h +258 h +10 h +1 h +13 h +158 h +4 h +4 h +31 h +10 h +10 h +36 h +1 h +13381 m +4 h +1055 m +1 h +6381 m +10 h +13382 m +36 h +143 h +4 h +4 h +25 h +109 h +278 h +4 h +13383 m +1 h +13384 m +1772 m +10 h +1993 m +330 m +57 h +13385 m +338 h +4 h +109 h +10 h +4 h +13386 m +4 h +13387 m +4 h +13388 m +150 m +13389 m +13390 m +4 h +10 h +4 h +1 h +1 h +3 h +687 h +1 h +1 h +13391 m +4 h +27 h +10 h +4 h +1 h +13392 m +4 h +82 h +430 h +10 h +75 m +10 h +10 h +13 h +1 h +2004 m +11 h +1 h +10 h +238 h +31 h +4 h +31 h +124 h +109 h +976 h +4 h +2124 m +1 h +11 h +114 h +190 h +4 h +10 h +3 h +13393 m +4 h +4 h +13394 m +265 h +4 h +4 h +4 h +13395 m +4 h +10 h +41 h +12 h +7074 m +4 h +74 h +4 h +4 h +13396 m +13397 m +13398 m +4 h +125 h +1 h +4 h +125 h +13399 m +1 h +13400 m +13401 m +13402 m +737 h +31 h +10 h +13403 m +1 h +10 h +4 h +4 h +4 h +13404 m +13405 m +1 h +1 h +13406 m +4 h +13407 m +56 h +4 h +10 h +1 h +13408 m +332 h +1017 h +4 h +258 h +124 h +13409 m +10 h +1 h +1 h +109 h +13410 m +13411 m +13412 m +13413 m +4 h +13414 m +10 h +4 h +125 h +4 h +1 h +11 h +13415 m +4 h +169 h +1089 h +4 h +109 h +1309 h +1642 h +204 h +82 h +601 h +59 h +4 h +4 h +601 h +13416 m +11 h +13417 m +4 h +13418 m +4 h +4 h +4 h +10 h +4 h +169 h +1 h +10 h +10 h +11 h +11 h +1 h +250 h +4 h +10 h +10 h +10 h +1 h +10 h +1 h +13419 m +13420 m +4 h +4 h +4 h +1 h +1 h +4 h +11 h +10 h +11 h +57 h +1016 h +13421 m +1 h +1 h +104 h +13422 m +2285 m +1 h +109 h +25 h +10 h +3 h +4 h +4 h +10 h +1 h +13423 m +13424 m +4 h +4 h +13425 m +51 m +332 h +4 h +13426 m +1 h +73 h +4 h +13427 m +265 h +65 h +278 h +4 h +299 m +13428 m +10 h +1 h +332 h +4 h +13429 m +4 h +13430 m +10 h +1 h +1 h +1 h +13431 m +1 h +10 h +8 h +10 h +1127 m +1559 m +1796 m +4 h +4 h +359 h +1 h +57 h +4 h +4966 m +4 h +1 h +97 h +1 h +1 h +10 h +114 h +204 h +2148 m +10 h +13432 m +1074 m +91 h +412 h +13433 m +124 h +10 h +1 h +10 h +13434 m +45 h +57 h +135 h +13435 m +687 h +13436 m +1 h +61 m +82 h +4 h +1 h +104 h +1 h +4 h +4 h +13437 m +13438 m +4 h +4 h +10 h +1 h +13439 m +9397 m +4 h +10 h +4 h +56 h +976 h +10990 m +10 h +4 h +4 h +1 h +195 h +4 h +124 h +13440 m +4 h +3 h +10 h +459 h +520 h +4 h +1016 h +13441 m +4 h +10 h +10 h +10 h +13442 m +4 h +119 h +4 h +13443 m +4 h +13444 m +1 h +1 h +13445 m +13446 m +358 h +13447 m +238 h +1 h +104 h +10 h +40 h +1 h +4 h +13448 m +10 h +13449 m +13450 m +13451 m +1 h +6678 m +143 h +4 h +31 h +13452 m +13453 m +1 h +13454 m +4 h +4 h +156 h +3616 m +25 h +4 h +1504 m +4 h +82 h +1 h +25 h +73 h +4 h +109 h +4 h +1 h +4 h +13455 m +13456 m +10 h +10 h +146 h +156 h +4 h +4 h +172 h +65 h +2788 h +3 h +196 h +1 h +4 h +10 h +13457 m +1 h +13458 m +13459 m +4 h +1 h +510 m +13460 m +11 h +13461 m +1 h +13462 m +13463 m +13464 m +5296 m +1 h +13465 m +423 m +13466 m +164 h +65 h +10 h +4 h +13467 m +1 h +13468 m +1 h +10 h +10 h +13469 m +1 h +368 h +1 h +13470 m +1 h +4245 m +4 h +10 h +13471 m +1 h +11 h +13472 m +83 h +10 h +4 h +10 h +4 h +10 h +13473 m +13474 m +1 h +13475 m +295 h +2308 m +13476 m +4 h +1771 m +1 h +146 h +258 h +123 h +4 h +1764 m +13477 m +13478 m +1 h +59 h +55 h +1 h +4 h +10 h +13479 m +146 h +4 h +13480 m +13481 m +4 h +10 h +10 h +10 h +4 h +181 h +10 h +13482 m +13483 m +1 h +4 h +41 h +4 h +1100 m +10 h +146 h +22 h +1 h +4 h +2733 h +1 h +4 h +1 h +359 h +823 m +1 h +4 h +23 h +124 h +10 h +10 h +4 h +190 h +1 h +13484 m +57 h +10 h +578 h +172 h +1 h +13485 m +31 h +10 h +4 h +57 h +64 h +8346 m +1 h +1 h +258 h +1127 m +4 h +10 h +10 h +1 h +692 h +278 h +265 h +4 h +640 h +229 h +10 h +13486 m +11 h +13487 m +4 h +172 h +104 h +4 h +4 h +4 h +4 h +91 h +13488 m +10 h +10 h +1 h +1 h +10101 m +10 h +82 h +1 h +13489 m +4 h +4 h +4 h +13490 m +13491 m +4 h +5708 m +10 h +41 h +1 h +4 h +170 h +13492 m +65 h +4 h +1 h +13493 m +56 h +4030 m +4 h +124 h +4 h +135 h +2851 m +1993 m +1 h +82 h +146 h +4 h +4 h +1 h +1 h +10 h +4 h +13494 m +13495 m +2022 m +4 h +57 h +1 h +4 h +4 h +13496 m +4 h +332 h +4 h +1 h +1 h +4 h +31 h +1 h +386 h +4 h +1137 h +4 h +40 h +146 h +13497 m +3 h +1 h +823 m +57 h +10 h +124 h +13498 m +74 h +135 h +1 h +10 h +41 h +1 h +13499 m +10 h +54 m +5348 m +13500 m +11 h +1 h +1 h +13501 m +204 h +167 h +13502 m +1595 m +31 h +13503 m +1 h +1 h +687 h +1 h +4 h +1 h +4 h +1 h +147 h +1 h +124 h +1 h +1 h +801 m +1 h +1138 m +13504 m +1 h +11 h +10 h +13505 m +83 h +13506 m +11 h +4 h +4 h +73 h +13507 m +13508 m +3737 m +8 h +109 h +13509 m +258 h +258 h +9692 m +4 h +4 h +10 h +1 h +10 h +13510 m +1 h +13511 m +10 h +4 h +10 h +10 h +4 h +59 h +3396 m +4 h +195 h +4 h +1 h +41 h +109 h +13512 m +13513 m +13514 m +1 h +10 h +91 h +270 h +1 h +1 h +1 h +1 h +56 h +13515 m +4 h +169 h +4 h +13516 m +10 h +1 h +13517 m +13518 m +11 h +4 h +73 h +10 h +469 m +167 h +4 h +13519 m +146 h +4 h +4 h +196 h +403 h +7444 m +13520 m +13521 m +2625 m +4 h +4 h +114 h +146 h +4 h +73 h +13522 m +4 h +82 h +10 h +1 h +4 h +13523 m +1 h +4 h +13524 m +11 h +11 h +59 h +11 h +1403 h +10 h +13525 m +10 h +1 h +4 h +10 h +4 h +83 h +1 h +4 h +986 h +4 h +13526 m +4 h +11 h +270 h +10 h +4 h +4 h +208 m +3 h +13527 m +1 h +4 h +13528 m +10 h +1 h +74 h +10 h +4 h +3 h +297 h +13529 m +10 h +4 h +13530 m +13531 m +13532 m +1 h +13533 m +6852 m +4 h +13534 m +1 h +10 h +1 h +1 h +4 h +1 h +1 h +13535 m +13536 m +10 h +10 h +114 h +4538 m +13537 m +74 h +13538 m +59 h +1 h +82 h +1 h +2374 h +13539 m +125 h +10 h +4 h +1030 h +4 h +1 h +10 h +124 h +13540 m +1 h +13541 m +82 h +1 h +13542 m +13543 m +4 h +10 h +687 h +1 h +10 h +6226 m +4 h +10 h +36 h +174 m +13544 m +10 h +1 h +4 h +146 h +13545 m +6863 m +4 h +104 h +4 h +10 h +1 h +13546 m +13547 m +65 h +36 h +1 h +4867 m +190 h +1 h +4 h +4 h +13548 m +4 h +4 h +13549 m +4 h +4 h +74 h +4 h +4 h +10 h +5632 m +1 h +1 h +13550 m +1 h +1 h +13551 m +1 h +13552 m +1 h +11 h +13553 m +1 h +13554 m +4 h +1 h +1 h +10 h +11 h +13555 m +169 h +13556 m +1 h +13557 m +129 h +272 m +10 h +13558 m +4 h +83 h +1 h +4 h +4 h +1 h +59 h +3768 m +109 h +1 h +41 h +4 h +4 h +10 h +11 h +4 h +10 h +11559 m +13559 m +10 h +4 h +13560 m +94 h +4 h +12898 m +4 h +434 m +297 h +1 h +4 h +143 h +25 h +110 h +10 h +146 h +1 h +278 h +41 h +13561 m +1 h +25 h +488 h +11 h +10 h +1 h +124 h +1 h +4 h +13562 m +1 h +1 h +82 h +1 h +13563 m +4 h +10 h +447 h +1 h +808 m +11 h +13564 m +13565 m +10 h +10 h +4 h +443 h +10 h +143 h +4 h +4 h +443 h +13566 m +13567 m +1 h +10 h +10 h +1 h +1499 h +195 h +4 h +109 h +13 h +1 h +13568 m +104 h +4 h +13569 m +4 h +4 h +13570 m +10 h +13571 m +307 h +1 h +10 h +4 h +13572 m +83 h +13573 m +1 h +1 h +7395 m +13574 m +10 h +327 m +25 h +1 h +57 h +4 h +4 h +10 h +1 h +190 h +10 h +13575 m +4 h +13576 m +10 h +10 h +124 h +4 h +13577 m +4 h +1 h +10 h +1 h +1 h +13578 m +10 h +10 h +10 h +13579 m +93 h +45 h +412 h +360 m +13580 m +4 h +10 h +157 h +1 h +4 h +1 h +13581 m +11 h +4 h +13582 m +112 h +83 h +109 h +4 h +11 h +463 h +4 h +1 h +13583 m +4 h +119 h +367 h +4 h +41 h +1 h +4 h +1 h +718 h +13584 m +11 h +10 h +258 h +4471 m +4 h +41 h +13585 m +13586 m +2625 m +147 h +113 h +1 h +4 h +13587 m +59 h +1 h +59 h +13588 m +4 h +4 h +4 h +13589 m +41 h +1 h +4 h +150 m +250 h +10 h +13590 m +4 h +109 h +13591 m +1 h +13592 m +4 h +4 h +10 h +13593 m +238 h +4 h +359 h +1470 h +4 h +1 h +13173 m +10 h +11 h +13594 m +4 h +4 h +4 h +4 h +4 h +1765 m +4 h +11 h +1 h +125 h +5619 m +13595 m +1 h +13596 m +4 h +4 h +536 h +41 h +4 h +1 h +1620 m +4 h +4 h +146 h +4 h +1 h +28 h +1 h +185 h +1 h +113 h +11 h +1 h +1 h +36 h +91 h +10 h +31 h +79 h +10 h +4 h +1 h +620 m +10 h +13597 m +13598 m +13599 m +83 h +1 h +1 h +11 h +55 h +4 h +184 h +10 h +167 h +10 h +48 h +59 h +82 h +1 h +10 h +11 h +124 h +4 h +10 h +4 h +31 h +13600 m +4 h +13601 m +4 h +4 h +13602 m +779 h +1685 m +13603 m +4 h +1 h +13604 m +36 h +4 h +4 h +92 h +4 h +10 h +13605 m +13606 m +1 h +1 h +92 h +65 h +4874 m +10 h +6851 m +10 h +4 h +170 h +13607 m +4 h +4 h +4 h +1 h +1 h +4 h +12 h +4 h +1 h +10 h +4 h +4 h +13608 m +4 h +1 h +285 m +10 h +1 h +1642 h +1 h +13609 m +5917 m +10 h +2308 m +4 h +169 h +4 h +4 h +13610 m +13611 m +25 h +11 h +69 h +1620 m +1 h +10 h +1 h +986 h +1 h +4 h +4 h +12 h +167 h +2717 m +266 h +10 h +196 h +124 h +13612 m +250 h +4 h +55 h +4 h +4 h +1619 h +1 h +10 h +860 m +109 h +185 h +59 h +1 h +4 h +11 h +1 h +31 h +10 h +1 h +13613 m +25 h +4 h +13614 m +103 h +13615 m +1 h +108 h +307 h +82 h +13616 m +3161 m +1 h +4 h +13617 m +13618 m +1 h +4 h +1 h +13619 m +11 h +119 h +4 h +13620 m +433 m +102 m +181 h +40 h +1016 h +8 h +444 m +124 h +13621 m +124 h +4 h +463 h +41 h +10 h +10 h +109 h +13622 m +12 h +4 h +11 h +57 h +64 h +74 h +143 h +4 h +69 h +1 h +83 h +7535 m +13623 m +4 h +1 h +1201 h +4440 m +1559 m +4 h +13624 m +443 h +13625 m +1 h +10 h +13626 m +195 h +10 h +10 h +10 h +4 h +10 h +13627 m +1411 m +36 h +10 h +83 h +4 h +41 h +6699 m +10 h +13628 m +1454 h +13629 m +13630 m +10 h +1 h +1 h +1403 h +4 h +4 h +1 h +869 m +13631 m +1 h +1 h +1 h +1 h +1 h +4 h +13632 m +4 h +4 h +4 h +4 h +4 h +13633 m +13634 m +13635 m +1 h +108 h +1 h +13636 m +4 h +4 h +13637 m +13638 m +2229 m +59 h +25 h +4 h +1 h +10 h +13639 m +4 h +4 h +1 h +13640 m +13641 m +82 h +1 h +4 h +1 h +4 h +12739 m +4 h +4 h +25 h +468 m +1780 h +4 h +266 h +10 h +41 h +13642 m +1 h +4 h +10 h +359 h +1 h +10 h +4 h +4 h +83 h +94 h +104 h +94 h +4 h +1532 m +4 h +3 h +1 h +4 h +2379 m +147 h +4 h +11 h +10 h +3 h +118 h +146 h +146 h +4 h +59 h +10 h +4 h +1 h +13643 m +4 h +4 h +1 h +1 h +228 m +1 h +31 h +10 h +36 h +338 h +13644 m +13645 m +10 h +13646 m +4 h +4 h +1 h +13647 m +11 h +164 h +13648 m +4 h +4 h +69 h +4 h +143 h +1 h +172 h +11 h +4 h +13649 m +630 m +10 h +4 h +4 h +8486 m +13650 m +1092 m +1 h +737 h +106 m +1 h +65 h +157 h +1 h +41 h +110 h +13651 m +13652 m +4 h +4 h +13653 m +10 h +1 h +4 h +64 h +25 h +13654 m +1 h +1 h +794 m +272 m +4 h +1 h +64 h +10 h +110 h +13655 m +13656 m +4 h +10 h +4 h +1 h +4625 m +4 h +4 h +13657 m +307 h +13658 m +4 h +1 h +1 h +13659 m +332 h +13660 m +146 h +4 h +13661 m +13662 m +10 h +181 h +4 h +4 h +1 h +6370 m +10 h +13663 m +36 h +10177 m +4 h +278 h +13664 m +4 h +13665 m +4 h +4 h +13666 m +57 h +4 h +10 h +4 h +13667 m +4 h +10 h +4 h +124 h +4 h +13668 m +4 h +83 h +4 h +13 h +11 h +1 h +79 h +4 h +10 h +4 h +1 h +1 h +125 h +443 h +13669 m +11 h +10 h +27 h +124 h +36 h +27 h +196 h +1 h +57 h +135 h +1761 m +10 h +1 h +40 h +11 h +4 h +10 h +31 h +72 m +4 h +195 h +4 h +10 h +1 h +3 h +13670 m +22 h +1955 m +4 h +4 h +13671 m +1 h +110 h +13672 m +13673 m +1003 h +10 h +57 h +8497 m +13674 m +164 h +4 h +13675 m +1 h +3143 m +1445 m +65 h +1 h +1 h +1 h +1 h +4 h +9940 m +4 h +10925 m +13676 m +10 h +4 h +13677 m +11 h +13678 m +1 h +13679 m +4 h +10 h +1 h +13680 m +4 h +3 h +104 h +10 h +13681 m +11 h +41 h +1 h +2788 h +11 h +13682 m +13683 m +4 h +1 h +1309 h +4 h +10 h +11 h +1 h +13684 m +195 h +83 h +116 m +13685 m +13686 m +10 h +8040 m +181 h +13687 m +13688 m +94 h +2309 m +13689 m +1 h +10 h +4 h +4 h +4 h +1 h +13690 m +13691 m +1975 m +125 h +1 h +10 h +10 h +10 h +4 h +4 h +1 h +10 h +4 h +7128 m +4 h +1281 m +146 h +10 h +1 h +1 h +83 h +10 h +13692 m +2418 m +4 h +1 h +10 h +10 h +1 h +83 h +82 h +1 h +1 h +4 h +10 h +91 h +13693 m +13694 m +1 h +11 h +4 h +13695 m +56 h +307 h +1 h +4 h +1 h +295 h +15 m +4 h +4 h +10 h +4 h +1 h +11 h +13696 m +4 h +1 h +31 h +299 m +13697 m +83 h +13698 m +36 h +4 h +4975 m +4 h +1 h +13699 m +114 h +13700 m +1 h +92 h +4 h +7630 m +1 h +1 h +13701 m +4 h +109 h +181 h +13702 m +1 h +10 h +1 h +1 h +2720 m +1 h +299 m +119 h +13703 m +36 h +59 h +1 h +433 m +41 h +10 h +13704 m +11 h +110 h +4 h +520 h +448 m +4 h +10 h +4 h +912 m +10 h +1 h +1 h +4 h +3 h +4 h +13705 m +10 h +1 h +13706 m +4 h +2914 m +146 h +135 h +13707 m +4 h +10 h +1 h +109 h +4 h +4 h +74 h +13708 m +4 h +10 h +1822 h +10 h +4 h +1 h +4 h +4 h +1 h +25 h +13709 m +13710 m +13711 m +10 h +59 h +13712 m +4 h +1 h +1 h +478 h +109 h +59 h +156 h +11 h +13713 m +1 h +184 h +13714 m +1 h +4 h +10 h +258 h +13715 m +7814 m +184 h +1 h +65 h +4 h +1 h +13716 m +11 h +11 h +276 h +97 h +4 h +13717 m +1 h +109 h +31 h +1 h +4 h +270 h +1 h +3 h +10272 m +4 h +2308 h +13718 m +4 h +1 h +143 h +4 h +1 h +1100 m +4 h +4 h +4 h +1 h +109 h +10 h +13719 m +779 h +13720 m +1 h +4 h +10 h +57 h +10 h +4 h +1 h +13721 m +59 h +4 h +10 h +1 h +1847 m +13722 m +195 h +10 h +1 h +125 h +13723 m +13724 m +5653 m +4 h +820 m +10 h +10 h +1 h +13725 m +4 h +488 h +1 h +4 h +1 h +4 h +4 h +4 h +4 h +4 h +25 h +13726 m +1 h +105 m +11 h +319 h +73 h +28 h +10 h +1 h +181 h +10 h +4 h +332 h +1685 m +74 h +4 h +4 h +4 h +1 h +13727 m +10 h +1 h +371 h +10 h +82 h +13728 m +250 h +13729 m +1 h +13730 m +4 h +4 h +1 h +31 h +258 h +143 h +13731 m +4 h +11 h +10 h +4 h +1780 h +10 h +10 h +1 h +7253 m +7243 m +4 h +73 h +3622 m +10 h +13732 m +13733 m +4 h +112 h +125 h +124 h +1 h +4 h +13734 m +10 h +2072 m +59 h +83 h +13735 m +1 h +297 h +13736 m +4 h +4 h +83 h +46 m +13737 m +299 h +601 h +10 h +1 h +64 h +1 h +10 h +13738 m +4 h +1261 h +10 h +1 h +10 h +1 h +4 h +13739 m +61 m +1 h +10 h +13740 m +4 h +4 h +1 h +4 h +4521 m +116 m +1564 m +4 h +4 h +82 h +10 h +265 h +1 h +13741 m +4 h +4 h +11 h +10 h +1 h +82 h +1 h +4 h +4 h +857 m +1 h +4 h +13742 m +4 h +4 h +119 h +13743 m +4 h +109 h +10 h +4 h +1 h +1 h +4 h +1 h +13744 m +13745 m +172 h +11 h +56 h +10 h +196 h +1 h +91 h +4 h +1714 m +1 h +4 h +10 h +4 h +1 h +64 h +10 h +4 h +13746 m +1497 m +10 h +64 h +25 h +265 h +83 h +109 h +27 h +109 h +1 h +109 h +4 h +112 h +13747 m +13748 m +11 h +1 h +13749 m +399 h +4 h +1 h +4 h +13750 m +13751 m +13752 m +46 m +112 h +1 h +10 h +13753 m +1 h +22 h +4 h +1 h +10 h +467 m +4 h +1 h +4 h +358 h +13754 m +1 h +2379 m +779 h +1 h +45 h +83 h +13755 m +13756 m +4 h +1337 m +332 h +114 h +4 h +108 h +92 h +124 h +10 h +10 h +13757 m +1 h +1 h +123 h +1 h +4 h +4 h +4 h +1 h +13758 m +59 h +11 h +13759 m +4 h +10 h +4 h +1 h +4 h +359 h +4 h +1 h +4 h +83 h +1 h +1 h +13760 m +649 m +140 h +13761 m +13762 m +4 h +13763 m +1 h +13764 m +124 h +185 h +4 h +241 m +1 h +10 h +4 h +41 h +13765 m +31 h +4 h +11 h +4 h +4 h +65 h +10 h +13766 m +10 h +11 h +11 h +10 h +1 h +57 h +13767 m +4 h +13768 m +3742 h +170 h +4 h +2625 h +1 h +4 h +13769 m +1 h +7585 m +1 h +1 h +4 h +10 h +10612 m +13770 m +4 h +4 h +9 m +1 h +640 h +4 h +13771 m +10 h +10 h +4 h +97 h +4 h +1 h +1 h +4 h +13772 m +4 h +11 h +72 m +4 h +10 h +4 h +10 h +1 h +13773 m +109 h +4 h +104 h +1 h +13774 m +276 h +10 h +10 h +4 h +13775 m +94 h +10 h +173 h +1 h +124 h +4 h +25 h +4 h +4 h +10 h +4 h +13776 m +368 h +11 h +13777 m +109 h +299 h +1 h +1 h +1 h +4986 m +687 h +266 h +1 h +10 h +109 h +11 h +1128 m +4 h +4 h +4 h +10 h +10 h +4 h +10 h +28 h +112 h +13778 m +1 h +4 h +1 h +13779 m +11 h +276 h +297 h +1548 m +13780 m +13781 m +13782 m +13783 m +10 h +1790 m +82 h +8 h +570 m +4 h +13784 m +1655 m +1 h +2041 m +4 h +4 h +1 h +13785 m +13786 m +13787 m +1796 m +698 m +12 h +13788 m +13789 m +1 h +4 h +13790 m +1 h +195 h +383 h +4 h +478 h +4 h +13791 m +13792 m +10588 m +737 h +4 h +4 h +11 h +4 h +211 m +264 h +112 h +4 h +10 h +538 h +124 h +13793 m +4 h +13794 m +1030 h +13795 m +1 h +4 h +1138 m +601 h +124 h +295 h +1 h +4 h +1016 h +4 h +181 h +10 h +4 h +195 h +83 h +10 h +10 h +13796 m +4 h +55 h +4 h +1 h +13797 m +10 h +1 h +41 h +4 h +1 h +4 h +4 h +185 h +13798 m +4 h +278 h +1 h +23 h +109 h +1 h +83 h +274 h +4 h +10 h +10 h +27 h +1 h +13799 m +13800 m +92 h +4 h +4 h +4 h +4 h +104 h +13 h +13801 m +1 h +1 h +4 h +1 h +13802 m +1 h +10 h +13803 m +97 h +25 h +13804 m +11 h +1 h +4 h +97 h +41 h +4 h +3 h +94 h +10 h +10 h +3 h +1714 m +1 h +1 h +13805 m +4 h +10 h +10 h +13806 m +4 h +1 h +172 h +13807 m +1 h +1 h +332 h +13808 m +13809 m +1 h +185 h +332 h +10 h +10 h +83 h +1 h +4 h +4 h +4 h +13810 m +4 h +13811 m +4 h +146 h +278 h +103 h +11 h +1 h +10 h +13812 m +13813 m +10 h +10 h +687 h +10 h +109 h +4 h +64 h +164 h +4 h +4 h +1 h +109 h +4 h +1 h +10 h +4 h +10 h +4 h +13814 m +478 h +10 h +13815 m +13816 m +59 h +13817 m +1 h +1822 h +13818 m +143 h +172 h +4 h +4 h +65 h +13819 m +10 h +13820 m +4 h +4 h +3435 m +4 h +184 h +661 m +11 h +13821 m +3 h +1 h +4 h +1 h +842 m +4 h +13822 m +488 h +82 h +4 h +1 h +1020 m +124 h +59 h +13823 m +2710 m +1 h +4 h +13824 m +3 h +687 h +1 h +82 h +10 h +4 h +371 h +265 h +10 h +4 h +124 h +9397 m +13825 m +8 h +4 h +1 h +4 h +1 h +4 h +4 h +190 h +13826 m +10 h +83 h +4 h +1 h +41 h +11 h +74 h +129 h +167 h +10 h +13827 m +1 h +113 h +185 h +64 h +4 h +4 h +119 h +4 h +11 h +10 h +4 h +570 m +13828 m +13829 m +332 h +4 h +10 h +4 h +10 h +13830 m +4 h +4 h +1 h +1953 m +27 h +13392 m +4 h +10 h +59 h +208 m +13831 m +13832 m +13833 m +1 h +1 h +4 h +10 h +82 h +11 h +1 h +4 h +447 h +114 h +13834 m +3 h +10 h +10 h +3 h +13835 m +94 h +1 h +4 h +1418 m +13836 m +4 h +1 h +13837 m +1 h +4 h +13838 m +13839 m +467 m +146 h +124 h +4 h +4 h +13840 m +4 h +13841 m +13842 m +13843 m +13844 m +4 h +10 h +273 m +10 h +109 h +332 h +4 h +1556 m +10 h +1442 m +1 h +1 h +4 h +13845 m +13846 m +13847 m +13848 m +1 h +185 h +1 h +447 h +1 h +1 h +10 h +124 h +266 h +4 h +4 h +1 h +11 h +110 h +10 h +10 h +12 h +10 h +97 h +13849 m +1 h +13850 m +371 h +1 h +64 h +4 h +13851 m +186 h +4 h +10 h +4 h +3 h +11 h +13852 m +156 h +4 h +4 h +82 h +13853 m +10 h +36 h +371 h +3 h +4 h +10 h +4 h +10 h +4 h +11 h +358 h +10 h +1 h +2920 m +4 h +4 h +1 h +757 h +1 h +13854 m +13855 m +13856 m +1 h +13857 m +4426 m +13858 m +170 h +1 h +4 h +1 h +1 h +4 h +158 h +4 h +10 h +13859 m +4 h +13860 m +10 h +10 h +57 h +13861 m +10 h +1 h +4 h +1 h +13862 m +125 h +83 h +114 h +13863 m +13864 m +4 h +3 h +13865 m +82 h +278 h +59 h +10 h +1 h +1 h +1541 m +1 h +4 h +104 h +13866 m +1 h +1 h +10 h +4 h +147 h +125 h +2508 m +82 h +13867 m +13868 m +40 h +10 h +44 m +146 h +13869 m +13870 m +3 h +172 h +912 m +11 h +104 h +10 h +939 h +73 h +10 h +10 h +4 h +10 h +4 h +10 h +10 h +74 h +13871 m +45 h +3 h +195 h +10 h +13872 m +1 h +1470 h +13873 m +10 h +10 h +1 h +4 h +390 m +1 h +41 h +10 h +1 h +8 h +123 h +368 h +36 h +13874 m +1 h +509 m +4 h +195 h +57 h +1 h +4 h +1 h +1 h +59 h +13875 m +13876 m +4 h +59 h +4 h +4 h +3396 m +1 h +91 h +1 h +57 h +3360 m +13877 m +4 h +119 h +1 h +97 h +113 h +4 h +1 h +1 h +4 h +4 h +10 h +8555 m +13878 m +1 h +4 h +4 h +13879 m +4 h +4 h +79 h +1 h +195 h +31 h +10 h +13880 m +82 h +13881 m +13882 m +1 h +10 h +135 h +4849 m +13883 m +110 h +82 h +27 h +4 h +65 h +1 h +1 h +1 h +266 h +10 h +4 h +4 h +82 h +10 h +1 h +10 h +74 h +196 h +13884 m +10 h +4 h +1 h +13885 m +10 h +1 h +10 h +1 h +10 h +4 h +1 h +4 h +10 h +181 h +25 h +4 h +1 h +124 h +196 h +1 h +41 h +459 h +94 h +13886 m +4 h +124 h +4 h +4 h +83 h +1 h +4 h +10 h +10 h +13887 m +13888 m +11 h +13889 m +73 h +83 h +4 h +4 h +4 h +258 h +4 h +1 h +13890 m +59 h +22 h +36 h +13891 m +1 h +4 h +278 h +13892 m +4 h +4 h +13893 m +4 h +13894 m +13 h +4 h +13895 m +10 h +10 h +794 m +13896 m +1 h +13897 m +1 h +13898 m +109 h +7950 m +11 h +10 h +4 h +2494 m +4 h +575 m +13899 m +59 h +13900 m +4 h +1 h +1914 m +353 m +13901 m +13902 m +4 h +4 h +1 h +295 h +4 h +5374 m +25 h +4 h +918 m +4 h +13903 m +297 h +82 h +13904 m +4 h +13905 m +1 h +13906 m +10 h +12 h +10 h +4 h +4 h +1 h +4 h +1089 h +1 h +10 h +13907 m +1 h +13908 m +4 h +5348 m +1 h +4 h +1 h +1 h +479 m +13909 m +4 h +10 h +12237 m +13910 m +13911 m +1822 h +4 h +13912 m +110 h +299 h +13913 m +13914 m +3704 m +520 h +13915 m +10 h +4 h +59 h +57 h +276 h +13916 m +13917 m +1 h +13918 m +13919 m +195 h +10 h +10 h +1 h +4 h +1 h +13920 m +109 h +13921 m +13922 m +4 h +4 h +4 h +112 h +4 h +124 h +73 h +10 h +13923 m +1 h +31 h +59 h +1 h +10 h +976 h +13924 m +104 h +10 h +135 h +69 h +65 h +4 h +1 h +4 h +109 h +13925 m +4905 m +10044 m +1 h +4 h +4 h +9586 m +97 h +13926 m +578 h +1020 m +4 h +4 h +27 h +4 h +4 h +190 h +1725 m +190 h +13927 m +13928 m +4 h +4 h +1 h +13929 m +13930 m +83 h +1 h +4 h +1 h +4 h +10 h +10 h +10 h +10 h +45 h +169 h +4 h +10 h +104 h +13931 m +4 h +7169 m +13932 m +13933 m +10 h +13 h +91 h +3 h +11 h +185 h +976 h +3 h +156 h +4 h +112 h +10 h +57 h +10 h +13934 m +10 h +4 h +386 h +1650 h +190 h +82 h +1 h +1 h +13935 m +1 h +11 h +11 h +1 h +4 h +13936 m +83 h +1635 m +13937 m +3982 m +4 h +83 h +1 h +0 h +353 m +4 h +4 h +10 h +616 m +4 h +13938 m +10 h +1 h +1 h +570 h +13939 m +2215 m +13940 m +4 h +119 h +4 h +1 h +13941 m +13942 m +4 h +10 h +11639 m +10 h +13943 m +11 h +1 h +1 h +109 h +103 h +55 h +10 h +13944 m +13945 m +10 h +4 h +10 h +1 h +493 m +1016 h +64 h +1 h +13946 m +1 h +1 h +1470 h +31 h +4 h +4 h +1 h +4 h +408 m +1 h +3025 m +12 h +4 h +1 h +13947 m +10 h +109 h +41 h +146 h +13948 m +57 h +10 h +4 h +167 h +4 h +13949 m +1 h +13950 m +1 h +13951 m +4 h +4 h +693 m +13952 m +74 h +125 h +1 h +57 h +1 h +124 h +57 h +4 h +1 h +10 h +1137 h +1 h +10 h +55 h +1 h +266 h +4 h +1 h +124 h +4 h +13953 m +1 h +4 h +13954 m +10 h +4 h +13955 m +10 h +13956 m +4 h +1 h +13957 m +97 h +1 h +7565 m +11 h +1 h +13958 m +4 h +4 h +10 h +36 h +10 h +1 h +11 h +1713 m +1 h +1 h +4 h +4 h +10 h +1 h +4 h +1 h +1 h +135 h +7950 m +332 h +3344 m +10 h +13959 m +4 h +13960 m +10 h +1 h +4 h +10 h +13961 m +4 h +1 h +10 h +4 h +36 h +13962 m +10 h +4 h +158 h +13963 m +13964 m +4 h +1 h +10 h +289 h +4 h +1 h +295 h +13965 m +109 h +1 h +13966 m +4 h +4 h +4 h +11 h +4 h +4 h +295 h +1556 m +11522 m +10 h +59 h +1 h +976 h +258 h +11 h +124 h +13753 m +444 m +4 h +258 h +10 h +10 h +13967 m +13968 m +83 h +4 h +11 h +4 h +1 h +4 h +1 h +13969 m +31 h +13970 m +10 h +307 h +677 m +1 h +3322 m +10 h +109 h +13971 m +4 h +4 h +31 h +1 h +13972 m +4 h +4 h +10 h +1 h +1 h +4 h +13973 m +1 h +36 h +59 h +1 h +260 m +4 h +1 h +13974 m +10 h +258 h +1003 h +11 h +4 h +83 h +1 h +31 h +3508 m +885 h +10 h +10 h +1 h +13975 m +1 h +13976 m +10 h +250 h +13977 m +1 h +13978 m +10 h +11 h +1053 m +4 h +140 h +13979 m +109 h +4 h +4 h +109 h +965 h +10 h +124 h +4 h +13980 m +4 h +13981 m +59 h +1 h +10 h +10 h +10 h +55 h +13982 m +59 h +10 h +59 h +57 h +13983 m +146 h +1 h +1 h +74 h +170 h +10 h +1261 h +59 h +10 h +13 h +13984 m +4 h +1 h +1 h +1 h +1 h +10 h +13985 m +65 h +13986 m +170 h +109 h +1 h +4 h +13987 m +74 h +10 h +13988 m +1 h +1 h +113 h +11 h +10 h +4 h +146 h +1 h +1 h +1 h +4 h +307 h +13989 m +10 h +2733 h +4 h +3 h +41 h +4 h +1 h +3 h +4 h +11117 m +13990 m +4 h +13991 m +1 h +2733 h +109 h +10 h +10 h +124 h +23 h +10 h +11691 m +4 h +1 h +4 h +4 h +104 h +13992 m +11672 m +13993 m +10 h +1 h +4 h +570 h +13994 m +1 h +5197 m +13995 m +10 h +4 h +4 h +1 h +11 h +10 h +1 h +94 h +4 h +4 h +91 h +1 h +73 h +3 h +1 h +4 h +13996 m +1 h +4 h +13997 m +4 h +10 h +1122 m +10 h +4 h +181 h +4 h +82 h +358 h +4 h +13998 m +4 h +13999 m +10 h +4 h +1796 m +10 h +843 m +1 h +125 h +1 h +1105 m +1 h +10 h +359 h +45 h +1 h +1 h +114 h +1 h +4 h +14000 m +1 h +45 h +4 h +185 h +185 h +1 h +1 h +4 h +4 h +114 h +7713 m +125 h +109 h +1038 m +12 h +186 h +3 h +14001 m +5111 m +2623 m +1 h +11 h +4 h +1 h +10 h +2002 m +124 h +4 h +1 h +14002 m +4 h +14003 m +59 h +11 h +14004 m +779 h +10 h +1 h +642 m +12131 m +4 h +14005 m +4 h +4 h +83 h +4 h +181 h +36 h +4 h +4 h +114 h +1 h +2840 m +4 h +1038 m +1 h +11 h +68 m +45 h +14006 m +307 h +36 h +14007 m +1 h +14008 m +1 h +1 h +10 h +4 h +167 h +4 h +14009 m +59 h +488 h +10 h +687 h +1 h +2846 m +1 h +295 h +110 h +10 h +14010 m +1955 m +1250 h +4 h +1 h +4 h +14011 m +14012 m +1 h +4 h +3882 m +1 h +14013 m +4 h +1 h +4 h +9228 m +358 h +14014 m +1 h +2041 m +4 h +11 h +135 h +4 h +8251 m +1 h +4 h +195 h +59 h +14015 m +59 h +10 h +1 h +11 h +56 h +1 h +10 h +1620 h +14016 m +10 h +4 h +1 h +1 h +1 h +266 h +4 h +4 h +196 h +14017 m +4 h +4966 m +1 h +4 h +1 h +11 h +4 h +1 h +1 h +1 h +986 h +4 h +59 h +10 h +1 h +79 h +4 h +4 h +1 h +4 h +1 h +4 h +109 h +1 h +146 h +8882 m +4 h +10 h +4 h +4 h +1619 h +4 h +146 h +4 h +1 h +332 h +4 h +5 m +1 h +538 h +1067 m +4 h +4 h +14018 m +4 h +10 h +14019 m +169 h +10 h +14020 m +65 h +4 h +11 h +4 h +79 h +12 h +10 h +14021 m +14022 m +125 h +4 h +4 h +4 h +14023 m +14024 m +1 h +14025 m +14026 m +10 h +83 h +14027 m +266 h +59 h +1 h +14028 m +4 h +536 h +124 h +10 h +14029 m +10 h +4 h +169 h +14030 m +4 h +14031 m +14032 m +1 h +1 h +196 h +4 h +1 h +83 h +3533 m +4 h +14033 m +1 h +4 h +41 h +12 h +10 h +14034 m +533 m +1 h +59 h +11 h +14035 m +11 h +79 h +4 h +14036 m +1 h +4 h +1 h +109 h +10 h +14037 m +4 h +14038 m +11 h +11 h +1 h +31 h +464 h +1 h +4 h +3 h +64 h +56 h +10 h +14039 m +14040 m +14041 m +14042 m +14043 m +1 h +10 h +10 h +4 h +4 h +4 h +4 h +4 h +4378 m +4 h +4 h +57 h +1 h +4 h +14044 m +1 h +2592 m +11 h +41 h +10 h +27 h +14045 m +229 h +1299 m +1 h +4 h +443 h +10 h +241 m +25 h +1 h +399 h +11 h +14046 m +10 h +10 h +73 h +4 h +10 h +94 h +2281 m +14047 m +266 h +1 h +25 h +1 h +1 h +14048 m +4 h +10 h +4 h +10 h +14049 m +1 h +77 h +1379 m +3 h +10 h +4 h +11 h +4 h +4 h +276 h +1 h +4848 m +4 h +150 m +3 h +14050 m +11 h +14051 m +1 h +14052 m +1 h +10 h +14053 m +964 m +1 h +4 h +57 h +1 h +164 h +10 h +1 h +74 h +14054 m +83 h +3177 m +14055 m +93 h +1 h +4 h +4 h +4 h +14056 m +10 h +11 h +538 h +55 h +14057 m +10 h +93 h +69 h +4 h +258 h +104 h +164 h +11 h +36 h +14058 m +4 h +13 h +1 h +1 h +10 h +59 h +14059 m +1185 m +1 h +5475 m +4 h +541 m +1 h +10 h +10 h +10 h +4 h +104 h +82 h +14060 m +11 h +10 h +4 h +274 h +1 h +4 h +7827 m +1 h +1 h +1 h +14061 m +10 h +4966 m +4 h +4 h +4 h +195 h +4 h +4 h +14062 m +1 h +14063 m +4 h +79 h +1 h +14064 m +59 h +138 m +13435 m +1 h +14065 m +4481 m +4 h +10 h +433 h +14066 m +10 h +11 h +871 m +4 h +14067 m +4 h +185 h +11 h +4 h +4 h +10 h +79 h +4 h +195 h +4 h +4 h +83 h +276 h +14068 m +1027 m +4 h +12655 m +10 h +146 h +156 h +250 h +4 h +55 h +258 h +229 h +1886 m +4 h +1 h +4 h +10 h +1 h +73 h +4 h +7950 h +31 h +10 h +14069 m +14070 m +92 h +1 h +14071 m +45 h +1 h +124 h +14072 m +1 h +14073 m +1 h +1 h +1 h +10 h +14074 m +4 h +1 h +69 h +1 h +185 h +59 h +4 h +4 h +12964 m +10 h +10 h +1 h +1 h +4 h +1 h +1 h +10 h +124 h +59 h +14075 m +14076 m +1 h +196 h +11 h +1 h +11 h +1 h +1 h +14077 m +41 h +1 h +82 h +10 h +4 h +114 h +229 h +4 h +258 h +10 h +4 h +10649 m +13 h +4 h +6882 m +371 h +1 h +10 h +1 h +97 h +14078 m +1 h +25 h +1 h +14079 m +10 h +4 h +74 h +332 h +10 h +14080 m +14081 m +1 h +2607 m +181 h +124 h +14082 m +11 h +1764 m +4 h +4 h +185 h +332 h +69 h +4 h +4 h +4 h +185 h +114 h +1 h +1 h +4645 m +10 h +73 h +173 h +31 h +14083 m +1105 m +1 h +6726 m +10 h +1847 m +674 m +94 h +4 h +4 h +14084 m +45 h +1 h +888 m +10 h +14085 m +4 h +1 h +14086 m +1 h +10 h +4 h +1 h +10 h +4 h +1 h +4 h +4 h +10 h +1 h +10 h +10 h +1 h +10 h +14087 m +4 h +36 h +4 h +10 h +14088 m +14089 m +1 h +1886 m +14090 m +14091 m +14092 m +1 h +14093 m +146 h +1 h +11 h +4 h +125 h +4 h +14094 m +135 h +276 h +4 h +10 h +14095 m +25 h +14096 m +1 h +1 h +1 h +4 h +642 m +4 h +11 h +1 h +4 h +1 h +10 h +11 h +14097 m +10 h +14098 m +10 h +1 h +14099 m +1 h +14100 m +3 h +10 h +124 h +45 h +14101 m +14102 m +104 h +10 h +14103 m +14104 m +4 h +10 h +238 h +1 h +1 h +14105 m +10 h +45 h +2617 h +4 h +14106 m +1 h +65 h +82 h +31 h +4 h +64 h +172 h +14107 m +1 h +124 h +92 h +1796 m +770 m +276 h +1 h +4 h +36 h +4 h +10 h +139 h +82 h +82 h +14108 m +1 h +119 h +1 h +14109 m +94 h +1 h +10 h +5944 m +14110 m +41 h +1884 m +1 h +14111 m +22 h +1 h +4 h +14112 m +1458 m +4 h +14113 m +14114 m +4 h +14115 m +167 h +14116 m +4 h +228 m +14117 m +1 h +135 h +11 h +11779 m +14118 m +1 h +109 h +2824 m +10 h +4 h +4 h +1 h +11 h +4 h +4 h +4 h +147 h +138 m +4 h +704 m +10 h +10 h +74 h +82 h +4 h +10 h +4 h +4 h +4 h +307 h +57 h +4 h +4 h +125 h +4 h +10 h +14119 m +4 h +2535 m +3837 m +14120 m +11 h +83 h +11 h +114 h +14121 m +14122 m +55 h +4 h +10 h +31 h +10 h +57 h +25 h +59 h +14123 m +1 h +1 h +1 h +10 h +10 h +22 h +10 h +4 h +196 h +109 h +1 h +83 h +57 h +4 h +1 h +135 h +14124 m +10 h +1 h +4 h +258 h +1 h +4 h +10 h +14125 m +1 h +195 h +10 h +25 h +124 h +4 h +109 h +74 h +97 h +4 h +14126 m +57 h +358 h +1 h +14127 m +1 h +82 h +14128 m +1886 h +65 h +4 h +1 h +172 h +4 h +4 h +229 h +4 h +1 h +10 h +13 h +14129 m +477 m +4 h +4 h +4 h +4 h +857 m +10 h +4 h +4 h +4 h +14130 m +14131 m +10 h +1 h +14132 m +4 h +2438 m +1 h +481 m +2582 m +5728 m +4 h +10 h +4 h +10 h +14133 m +65 h +1 h +4 h +10 h +1655 m +1 h +10 h +146 h +11 h +10 h +10 h +82 h +1 h +4 h +1 h +1 h +10 h +169 h +1 h +1 h +1 h +1 h +146 h +10 h +4 h +4 h +65 h +14134 m +83 h +82 h +109 h +14135 m +1 h +2412 m +11 h +14136 m +14137 m +10 h +144 h +196 h +14138 m +27 h +14139 m +10 h +1 h +3 h +14140 m +4 h +73 h +10 h +1 h +4 h +1137 h +10 h +92 h +4 h +10 h +1 h +14141 m +4 h +4 h +4 h +258 h +272 h +4 h +4 h +22 h +1 h +587 m +1 h +4 h +1250 h +4 h +5470 m +14142 m +447 h +1957 m +912 m +4 h +10 h +7181 m +1 h +14143 m +1 h +4 h +10 h +14144 m +14145 m +11806 m +185 h +4 h +4 h +4 h +10 h +2928 m +11 h +2056 m +1 h +4 h +10 h +82 h +10 h +10 h +1 h +14146 m +4 h +109 h +14147 m +118 h +1 h +14148 m +10 h +14149 m +1 h +82 h +14150 m +10 h +4 h +45 h +1403 h +4 h +10 h +258 h +1201 h +4 h +266 h +307 h +11 h +1 h +140 h +4 h +1 h +14151 m +2265 m +990 h +64 h +10 h +14152 m +266 h +403 h +4 h +14153 m +4 h +13 h +4 h +125 h +4 h +4 h +1 h +4 h +1016 h +1 h +74 h +1 h +36 h +10 h +10 h +8 h +10 h +14154 m +4 h +14155 m +4 h +386 h +92 h +12 h +4 h +184 h +14156 m +4 h +109 h +1 h +368 h +1 h +11 h +10 h +509 m +10 h +4 h +10 h +10 h +353 h +1 h +4 h +12 h +478 h +4 h +10 h +1 h +10 h +434 m +538 h +556 m +1 h +14157 m +4 h +10 h +4359 m +4 h +1 h +1 h +1 h +10 h +4 h +14158 m +4 h +330 m +169 h +1261 h +630 m +1 h +10 h +10 h +278 h +10 h +45 h +4 h +12 h +4 h +2435 m +1053 m +23 h +4 h +267 m +1 h +10 h +75 m +124 h +57 h +3115 m +7474 m +1 h +14159 m +1 h +14160 m +10 h +4 h +1 h +4 h +14161 m +1 h +125 h +82 h +4 h +14162 m +112 h +14163 m +860 m +386 h +31 h +41 h +4 h +4 h +1 h +1 h +10 h +14164 m +185 h +104 h +14165 m +10 h +297 h +14166 m +14167 m +4 h +31 h +91 h +4 h +7271 m +14168 m +4 h +10 h +4 h +1 h +4 h +869 m +1 h +83 h +10 h +10 h +1 h +4 h +14169 m +14170 m +10 h +10 h +104 h +83 h +4 h +59 h +211 m +10 h +25 h +123 h +1 h +1 h +4 h +4297 m +14171 m +4 h +332 h +173 h +31 h +10 h +1 h +83 h +14172 m +14173 m +14174 m +4 h +10 h +1 h +10 h +1 h +1138 m +4 h +82 h +74 h +14175 m +195 h +57 h +5 h +4 h +10 h +4 h +1 h +82 h +1 h +1685 h +14176 m +14177 m +1017 h +4 h +4 h +935 h +135 h +4 h +14178 m +12301 m +4 h +1 h +1 h +4 h +4 h +14179 m +10 h +40 h +4 h +5504 m +92 h +14180 m +124 h +1 h +139 h +10 h +10 h +1 h +10 h +4 h +1 h +4 h +4 h +4 h +3742 h +1 h +14181 m +10 h +575 m +1 h +1 h +14182 m +143 h +1 h +82 h +10 h +1 h +10 h +4 h +3479 m +10 h +1 h +14183 m +4 h +1 h +4 h +3424 m +1 h +4 h +1 h +14184 m +278 h +388 m +124 h +1 h +2172 m +83 h +4 h +14185 m +1 h +368 h +1 h +11 h +4 h +82 h +1 h +4 h +4 h +965 h +1 h +10 h +14186 m +4 h +4 h +1 h +581 m +65 h +14187 m +843 m +109 h +14188 m +14189 m +83 h +109 h +11 h +241 m +10 h +4 h +986 h +4 h +4256 m +4 h +1 h +4 h +1 h +14190 m +1 h +83 h +10 h +911 h +4 h +295 h +14191 m +4 h +1218 m +83 h +1 h +4 h +4 h +59 h +10 h +4 h +478 h +14192 m +4 h +1 h +82 h +10 h +4 h +14193 m +10 h +3 h +14194 m +192 h +1 h +4 h +12571 m +1 h +1 h +14195 m +10 h +31 h +14196 m +4 h +4 h +82 h +10 h +10 h +57 h +14197 m +14198 m +4 h +4 h +447 h +74 h +91 h +14199 m +14200 m +10 h +1 h +83 h +1 h +1 h +14201 m +4 h +4 h +8890 m +4 h +4 h +1 h +4 h +1 h +4 h +10 h +1 h +196 h +10 h +1045 m +4 h +4 h +10 h +195 h +14202 m +14203 m +2920 m +4 h +1 h +4 h +14204 m +4 h +1 h +1278 m +14205 m +1 h +1 h +4 h +1 h +10 h +14206 m +307 h +97 h +11 h +14207 m +4 h +4 h +4 h +1 h +4 h +1261 h +4 h +14208 m +295 h +14209 m +330 m +14210 m +14211 m +1 h +4 h +13 h +10 h +14212 m +10 h +4 h +10 h +869 m +4 h +14213 m +359 h +10 h +14214 m +6747 m +13 h +4 h +10 h +4 h +169 h +4 h +14215 m +14216 m +1 h +79 h +4 h +2442 m +14217 m +82 h +1 h +4 h +4 h +1 h +14218 m +4 h +48 h +3 h +4 h +11 h +358 h +4 h +4 h +64 h +307 h +4151 m +14219 m +1 h +1 h +1 h +195 h +14220 m +27 h +1 h +4 h +4 h +14221 m +4 h +129 h +4 h +10 h +1 h +139 h +10 h +295 h +14222 m +13 h +601 h +14223 m +10 h +12 h +14224 m +10 h +10 h +1 h +4 h +1 h +10 h +4 h +14225 m +700 m +14226 m +14227 m +4 h +1 h +14228 m +10 h +14229 m +1 h +1 h +22 h +14230 m +4 h +196 h +10 h +14231 m +1 h +14232 m +14233 m +14234 m +10 h +1 h +4 h +83 h +97 h +1 h +10 h +888 m +1 h +4 h +14235 m +10 h +55 h +1 h +692 h +1 h +4 h +1 h +14236 m +1 h +4 h +14237 m +4 h +195 h +556 m +4 h +22 h +1 h +23 h +13 h +2309 m +1299 m +4 h +4 h +10 h +11 h +4 h +10 h +1 h +1 h +4 h +1504 m +10 h +14238 m +3 h +97 h +1 h +4 h +4 h +575 m +8395 m +14239 m +82 h +14240 m +56 h +172 h +1 h +195 h +4 h +14241 m +4 h +4 h +31 h +1027 m +4 h +59 h +4 h +14242 m +14243 m +10 h +10 h +4 h +4 h +1 h +1685 h +4 h +1 h +3177 m +4 h +104 h +125 h +109 h +1 h +4895 m +4 h +4 h +378 m +4 h +10 h +25 h +1 h +4 h +59 h +1 h +125 h +143 h +10 h +14244 m +14245 m +14246 m +22 h +1 h +14247 m +4 h +82 h +4481 m +79 h +11 h +1 h +36 h +1 h +4 h +10 h +14248 m +14078 m +33 m +14249 m +27 h +79 h +1 h +4 h +1 h +4 h +4 h +40 h +4 h +10 h +14250 m +14251 m +14252 m +169 h +109 h +64 h +125 h +155 m +1 h +1454 h +14253 m +4 h +139 h +14254 m +1 h +10 h +14255 m +1 h +123 h +1 h +143 h +82 h +10 h +10 h +82 h +196 h +4 h +4 h +73 h +1454 h +4 h +14256 m +14257 m +1 h +10 h +110 h +10 h +41 h +4 h +4 h +25 h +4 h +57 h +4 h +4 h +1 h +4 h +976 h +10 h +10 h +10 h +14258 m +10 h +158 h +4 h +1 h +4 h +1 h +10 h +82 h +10 h +184 h +4 h +4 h +1 h +295 h +14259 m +172 h +1185 m +1 h +10 h +4 h +14260 m +14261 m +36 h +14262 m +1 h +4 h +10 h +14263 m +4 h +2879 m +4 h +1 h +4 h +4 h +91 h +110 h +4 h +1 h +147 h +4 h +1027 m +1 h +4 h +57 h +463 m +1 h +14264 m +4 h +1 h +125 h +533 m +1 h +109 h +1 h +1 h +4 h +10 h +14265 m +1 h +14266 m +181 h +4 h +1 h +1470 h +4867 m +1 h +14267 m +4 h +124 h +109 h +125 h +10 h +10 h +14268 m +82 h +4 h +4 h +4 h +4 h +14269 m +1 h +4 h +10 h +1 h +4 h +169 h +4 h +10 h +1 h +1 h +3923 m +4 h +14270 m +4 h +14271 m +10 h +10 h +36 h +4 h +11 h +4 h +4 h +14272 m +14273 m +14274 m +10 h +4 h +1 h +59 h +1454 h +14275 m +10 h +14276 m +1 h +3 h +4 h +156 h +6413 m +14277 m +8535 m +4 h +1 h +14278 m +10 h +1 h +11 h +14279 m +14280 m +14281 m +14282 m +10 h +1 h +307 h +4 h +459 h +14283 m +276 h +3 h +1 h +4 h +10 h +4 h +10 h +14284 m +27 h +14285 m +4 h +4 h +459 h +955 m +4 h +14286 m +14287 m +14288 m +10 h +1445 m +4 h +109 h +10 h +1 h +533 m +10 h +3299 m +4 h +14289 m +4 h +4 h +1 h +601 h +14290 m +84 m +14291 m +10 h +14292 m +1 h +57 h +25 h +1 h +170 h +92 h +129 h +1 h +1 h +250 h +14293 m +4 h +4 h +14294 m +4 h +250 h +14295 m +1822 h +14296 m +14297 m +4 h +25 h +1 h +10 h +31 h +14298 m +1 h +14299 m +94 h +10 h +124 h +14300 m +14301 m +12 h +4 h +4 h +1 h +3307 m +4 h +10 h +14302 m +125 h +14303 m +4 h +10 h +4 h +124 h +1070 m +4 h +4 h +1 h +4 h +307 h +10 h +57 h +14304 m +14305 m +10 h +3 h +110 h +4 h +82 h +276 h +83 h +4 h +25 h +4 h +135 h +65 h +4 h +14306 m +14307 m +14308 m +4 h +10 h +45 h +299 h +109 h +4 h +1 h +383 h +5567 m +1 h +14309 m +270 h +8697 m +4 h +14310 m +1 h +3 h +14311 m +10 h +14312 m +186 h +1 h +10 h +11 h +4 h +10 h +557 m +156 h +104 h +1 h +14313 m +4 h +1 h +14314 m +4 h +520 h +14315 m +10 h +14316 m +4 h +45 h +1 h +14317 m +4 h +14318 m +14319 m +14320 m +109 h +1250 h +4 h +59 h +28 h +124 h +1 h +4 h +687 h +13 h +10 h +601 h +1 h +757 h +14321 m +12675 m +4 h +4 h +31 h +620 m +10 h +10 h +14322 m +447 h +11 h +14323 m +1 h +10 h +1 h +10 h +4 h +4 h +4 h +8 h +1 h +14324 m +10267 m +10 h +1 h +1 h +478 h +10 h +10 h +3837 m +4 h +1 h +10 h +14325 m +14326 m +14327 m +83 h +185 h +11 h +41 h +4 h +4 h +4 h +135 h +10 h +11 h +1 h +83 h +82 h +1261 h +1 h +1 h +4 h +113 h +1 h +2308 h +1 h +14328 m +297 h +11 h +4 h +2475 m +14329 m +4 h +25 h +2046 m +4 h +4 h +4 h +14330 m +10 h +12 h +4 h +4 h +2139 m +4 h +4 h +73 h +368 h +59 h +10 h +4 h +1 h +56 h +83 h +14331 m +14332 m +59 h +10 h +129 h +4 h +25 h +4 h +14333 m +124 h +1 h +196 h +4 h +4 h +1 h +4 h +59 h +4 h +1 h +10 h +3 h +4 h +4 h +4 h +114 h +1 h +14334 m +4 h +4 h +2212 m +10 h +10 h +125 h +172 h +109 h +10 h +14335 m +14336 m +278 h +4 h +1 h +158 h +520 h +1 h +14337 m +14338 m +578 h +4 h +1 h +12 h +14339 m +14340 m +92 h +1 h +10 h +14341 m +14342 m +10 h +31 h +718 h +65 h +57 h +358 h +1 h +1 h +14343 m +14344 m +332 h +1 h +4 h +14345 m +10 h +146 h +1 h +1 h +14346 m +10 h +143 h +1 h +135 h +4 h +1 h +7727 m +14 m +14347 m +82 h +1 h +986 h +31 h +14348 m +4 h +14349 m +14350 m +83 h +4 h +4 h +11 h +14351 m +93 h +4 h +119 h +14352 m +4 h +146 h +14353 m +167 h +6057 m +14354 m +4 h +157 h +41 h +112 h +172 h +4 h +11 h +124 h +258 h +109 h +1 h +1 h +383 h +1 h +109 h +14355 m +10 h +14356 m +10 h +6749 m +383 h +1 h +1 h +4 h +69 h +1 h +14357 m +82 h +14358 m +1 h +1 h +109 h +167 h +4 h +1 h +4 h +14359 m +10 h +11 h +14360 m +1 h +14361 m +82 h +41 h +4 h +143 h +14362 m +1 h +14363 m +4 h +1 h +367 m +14364 m +10 h +4 h +41 h +14365 m +14366 m +10 h +119 h +10 h +10 h +10 h +59 h +4 h +4 h +1 h +1642 m +2418 m +1 h +4 h +14367 m +143 h +91 h +10 h +10 h +4 h +104 h +620 m +4 h +14368 m +4 h +25 h +14369 m +1 h +4 h +82 h +14370 m +10 h +996 m +97 h +12570 m +11417 m +1 h +1 h +4 h +1269 m +4 h +4 h +4 h +5205 m +27 h +10 h +4 h +4 h +1 h +56 h +14371 m +1 h +4 h +14372 m +4 h +10 h +1535 m +1 h +1 h +1 h +1 h +4 h +4 h +857 m +1089 h +14373 m +1096 m +10 h +22 h +14374 m +4 h +10 h +10 h +1 h +146 h +94 h +83 h +45 h +5616 m +13 h +10 h +4 h +1 h +4 h +1089 h +97 h +14375 m +10391 m +13 h +4 h +10 h +1 h +14376 m +65 h +82 h +4 h +10 h +10 h +1 h +10 h +1642 h +14377 m +4 h +36 h +4 h +1 h +3 h +14378 m +12020 m +258 h +11 h +4 h +14379 m +14380 m +2770 m +146 h +25 h +14381 m +2028 m +14382 m +1 h +10 h +14383 m +14384 m +4 h +68 m +4 h +14385 m +1 h +14386 m +10 h +4 h +11 h +4 h +4 h +14387 m +238 h +10 h +368 h +14388 m +10 h +4 h +4 h +4 h +4 h +307 h +14389 m +14390 m +14391 m +1 h +4 h +1 h +1 h +4 h +57 h +1 h +297 h +5 h +1 h +4 h +5025 m +11 h +14392 m +11 h +4 h +10 h +4 h +3 h +1 h +74 h +14393 m +10 h +10 h +10 h +3143 m +4 h +4 h +794 m +14394 m +4530 m +110 h +10 h +10 h +13 h +41 h +1 h +104 h +4 h +1 h +10 h +124 h +4 h +36 h +4 h +10 h +14395 m +4 h +1 h +4 h +4 h +4 h +10 h +4 h +10 h +10 h +10 h +371 h +124 h +14396 m +1 h +278 h +1 h +322 m +4 h +3 h +4 h +3293 m +59 h +10 h +4 h +1 h +4 h +41 h +10 h +10 h +109 h +4 h +12047 m +12 h +14397 m +4 h +4 h +14398 m +3704 m +1759 m +1016 h +1766 h +4 h +10 h +4 h +4524 m +10 h +10 h +11 h +10 h +10 h +4 h +2194 m +10 h +4 h +13969 m +1 h +6869 m +1 h +143 h +135 h +14399 m +25 h +1 h +4 h +1137 h +4 h +31 h +1309 h +4 h +65 h +1 h +14400 m +79 h +4030 m +976 h +10 h +1 h +10 h +14401 m +10 h +1 h +689 m +14402 m +181 h +4 h +92 h +104 h +10 h +1 h +4 h +11 h +1 h +11 h +4 h +109 h +1 h +4 h +1309 h +14403 m +4 h +195 h +93 h +1 h +14404 m +91 h +4 h +1 h +14405 m +4 h +2339 m +10 h +1 h +10 h +10 h +1 h +104 h +4 h +1 h +1 h +3 h +4 h +1 h +1 h +1 h +57 h +11 h +1 h +278 h +1 h +1 h +642 h +10 h +1 h +1 h +1 h +59 h +14406 m +4 h +1 h +14407 m +14408 m +4 h +14409 m +4 h +4 h +4 h +4 h +4 h +11 h +4 h +10 h +57 h +4 h +1 h +4 h +3 h +2851 m +83 h +10 h +14410 m +601 h +14411 m +823 m +10 h +1650 h +57 h +176 m +14412 m +3 h +1822 h +1 h +1619 h +14413 m +4 h +1 h +4 h +4 h +14414 m +10 h +14415 m +10 h +195 h +14416 m +1 h +100 m +41 h +4 h +1 h +10 h +4 h +124 h +14417 m +1 h +4 h +14418 m +1 h +3523 m +14419 m +4 h +10 h +808 m +1 h +10 h +1 h +4 h +74 h +4 h +14420 m +4 h +4 h +14421 m +5475 m +14422 m +1 h +27 h +1 h +14423 m +10 h +4 h +14424 m +10 h +4538 m +14425 m +1 h +74 h +1 h +1 h +4 h +4 h +12 h +14426 m +3307 m +8 h +14427 m +14428 m +1 h +1 h +14429 m +211 m +172 h +14430 m +14431 m +14432 m +245 m +14433 m +4 h +10 h +1 h +1 h +14434 m +14435 m +327 m +4 h +57 h +4 h +6726 m +14436 m +4 h +10 h +14437 m +278 h +1 h +4 h +5475 m +11 h +4 h +14438 m +14439 m +14440 m +4 h +4 h +692 h +10 h +14441 m +14442 m +14443 m +10 h +4 h +10 h +10 h +4 h +4 h +25 h +14444 m +4 h +1 h +83 h +1 h +14445 m +4 h +14446 m +1 h +14447 m +8114 m +14448 m +14449 m +1 h +1 h +14450 m +14451 m +59 h +14452 m +4 h +4 h +4 h +4 h +22 h +10 h +1 h +14453 m +10 h +1 h +167 h +14454 m +1 h +109 h +4 h +125 h +14455 m +4 h +1 h +14456 m +4 h +1 h +10 h +4 h +14457 m +143 h +4 h +139 h +10 h +14458 m +4 h +14459 m +4 h +10 h +4 h +1 h +1 h +31 h +1137 h +14460 m +1 h +14461 m +1 h +692 h +64 h +10 h +14462 m +4 h +14463 m +1261 h +1 h +14464 m +1 h +10 h +4 h +1 h +14465 m +4 h +108 h +10 h +297 h +10 h +601 h +952 m +36 h +1 h +1 h +1261 h +124 h +14466 m +10 h +1 h +10 h +3 h +92 h +1027 h +3 h +10 h +14467 m +156 h +124 h +1 h +69 h +1 h +10 h +14468 m +10 h +1 h +125 h +11 h +4 h +14469 m +14470 m +10 h +10 h +14471 m +1 h +10 h +14472 m +4 h +10 h +14473 m +10 h +4 h +143 h +1 h +14474 m +1261 h +69 h +10 h +11147 m +2379 m +14475 m +14476 m +192 h +31 h +41 h +10 h +25 h +10 h +4788 m +4 h +4 h +1 h +1 h +10 h +14477 m +4 h +1 h +4 h +1822 h +41 h +3913 m +14478 m +1 h +4 h +322 m +1 h +14479 m +22 h +14480 m +119 h +11 h +139 h +10 h +1 h +14481 m +14482 m +4 h +125 h +157 h +1 h +9837 m +1 h +3 h +14483 m +4 h +319 h +4 h +4932 m +10 h +4 h +5709 m +13 h +2923 h +14484 m +14485 m +1 h +14486 m +61 m +10 h +109 h +10 h +57 h +4 h +14487 m +4 h +167 h +2710 m +14488 m +990 h +258 h +4 h +11 h +10 h +10 h +14489 m +14490 m +1359 h +4 h +14491 m +41 h +14492 m +82 h +692 h +4 h +1128 m +10 h +1196 m +4 h +170 h +4 h +1 h +4 h +10 h +74 h +536 h +14493 m +14494 m +10 h +143 h +4 h +4 h +1886 h +124 h +109 h +1 h +1 h +718 h +10 h +10 h +4 h +10 h +1 h +1 h +10 h +55 h +147 h +10 h +6784 m +339 m +25 h +11 h +1089 h +94 h +57 h +124 h +14495 m +14496 m +358 h +4 h +22 h +57 h +1 h +4 h +14497 m +10 h +59 h +2265 m +279 m +1 h +82 h +520 h +10 h +4 h +36 h +4 h +195 h +14498 m +14499 m +14500 m +28 h +11 h +1092 m +4 h +10 h +74 h +4 h +10 h +1 h +1 h +11 h +2110 m +104 h +4 h +1 h +1 h +10 h +1 h +11 h +109 h +11 h +14501 m +1 h +4 h +2418 m +1 h +25 h +172 h +1 h +918 m +10 h +1 h +14502 m +10 h +11 h +10 h +1 h +10 h +2534 m +1 h +4 h +10 h +4 h +322 h +4 h +4 h +1 h +274 h +59 h +10 h +4 h +1 h +4 h +1 h +4 h +14503 m +10 h +1 h +4 h +104 h +601 h +11 h +14504 m +14505 m +6399 m +147 h +4 h +146 h +10 h +14506 m +169 h +4 h +124 h +14507 m +1 h +4 h +4 h +4 h +238 h +11 h +4 h +4 h +83 h +4 h +4 h +14508 m +104 h +14509 m +1 h +129 h +1 h +250 h +10 h +57 h +14510 m +1 h +10 h +4 h +13 h +14511 m +1 h +11 h +25 h +4 h +1646 m +14512 m +14513 m +4 h +10 h +1 h +14514 m +10 h +10 h +125 h +10 h +10 h +10 h +97 h +4 h +4 h +27 h +1 h +10 h +14515 m +4 h +4 h +14516 m +10 h +11 h +170 h +10 h +1 h +1 h +10 h +14517 m +147 h +31 h +135 h +14518 m +12170 m +1 h +14519 m +4 h +10 h +11 h +1 h +4 h +4297 m +14520 m +4 h +10 h +4 h +184 h +4 h +1 h +13 h +4 h +4 h +11 h +14521 m +4 h +1 h +1 h +7553 m +1884 m +265 h +14522 m +10 h +1 h +104 h +14523 m +56 h +4 h +14524 m +1 h +14525 m +195 h +1 h +14526 m +1 h +10 h +273 m +538 h +14527 m +3484 m +4 h +256 m +4131 m +307 h +195 h +332 h +4 h +158 h +10 h +359 h +11 h +14528 m +12 h +4 h +10 h +4 h +14529 m +11 h +1 h +1 h +1 h +125 h +4 h +506 m +10 h +297 h +57 h +4 h +10 h +434 m +57 h +4 h +14530 m +14531 m +4 h +4 h +83 h +4 h +4 h +295 h +110 h +135 h +83 h +278 h +1 h +14532 m +13 h +4 h +1 h +114 h +10 h +10 h +14533 m +4 h +14534 m +3607 m +1 h +266 h +4 h +1 h +1 h +14535 m +10 h +4 h +14536 m +4 h +1796 h +14537 m +10615 m +14538 m +10 h +4 h +804 m +185 h +104 h +358 h +14539 m +4 h +14540 m +10 h +10 h +238 h +125 h +12 h +14541 m +10 h +276 h +10 h +4 h +114 h +4 h +10 h +1 h +125 h +4 h +4 h +14542 m +55 h +181 h +181 h +14543 m +4 h +114 h +1 h +14544 m +1 h +1 h +6135 m +10 h +181 h +4 h +4 h +10 h +1 h +4 h +447 h +4 h +164 h +14545 m +10 h +10 h +146 h +14546 m +10 h +10 h +1 h +195 h +104 h +4 h +4 h +4 h +4 h +14547 m +113 h +74 h +367 h +4 h +1 h +10 h +114 h +4 h +119 h +4 h +4 h +123 h +10 h +104 h +11 h +1 h +14548 m +386 h +10 h +14549 m +1 h +79 h +1 h +1 h +25 h +14550 m +12005 m +14551 m +10 h +14552 m +1 h +4 h +5929 m +10 h +1 h +4 h +4 h +11 h +10 h +119 h +10 h +1 h +10 h +10 h +11 h +14553 m +14554 m +1 h +91 h +10 h +4 h +1 h +97 h +14555 m +4 h +25 h +224 m +10 h +279 m +10 h +1 h +185 h +14556 m +4 h +4 h +4 h +14557 m +125 h +167 h +40 h +124 h +1137 h +25 h +14558 m +4 h +123 h +692 h +14559 m +5505 m +1 h +4 h +1 h +447 h +1 h +10 h +172 h +2379 h +4 h +14560 m +14561 m +14562 m +10 h +10 h +4 h +10 h +82 h +14563 m +14564 m +83 h +10 h +64 h +28 h +1 h +14565 m +1677 m +14566 m +11 h +10 h +10 h +14567 m +14568 m +14569 m +4 h +12329 m +10 h +1 h +377 h +4 h +4 h +367 h +190 h +464 h +124 h +1016 h +11 h +104 h +11 h +14570 m +119 h +31 h +4 h +10 h +73 h +4 h +14571 m +4 h +640 h +4 h +14572 m +124 h +11 h +31 h +167 h +1 h +41 h +1 h +11 h +4 h +857 m +11 h +10 h +11 h +14573 m +204 h +192 h +25 h +10028 m +1089 h +4 h +4 h +1 h +73 h +14574 m +196 h +1 h +104 h +10 h +57 h +14575 m +4 h +14576 m +1 h +14577 m +10 h +4 h +4 h +4 h +57 h +1 h +4 h +14578 m +73 h +10 h +14579 m +25 h +59 h +4 h +4 h +1 h +14580 m +12 h +1 h +10 h +2379 h +124 h +1 h +1 h +14581 m +78 m +4 h +14582 m +14583 m +4 h +14584 m +14585 m +1 h +1 h +10 h +4 h +14586 m +14587 m +10 h +82 h +25 h +14588 m +41 h +4 h +10 h +14589 m +10 h +1 h +1 h +1 h +4 h +1 h +10 h +82 h +10 h +4 h +14590 m +14591 m +11 h +219 h +4 h +4 h +4 h +1 h +196 h +11 h +4 h +4 h +55 h +258 h +1 h +10 h +10 h +1 h +14592 m +4 h +169 h +143 h +297 h +1 h +14593 m +14594 m +74 h +82 h +14595 m +241 m +4 h +12 h +123 h +1 h +124 h +4 h +1 h +4 h +83 h +4 h +124 h +1 h +14596 m +9176 m +2447 m +1 h +4 h +4 h +14597 m +14598 m +31 h +4 h +459 h +10 h +4 h +14599 m +10 h +1 h +1 h +10 h +55 h +1 h +11 h +4 h +278 h +146 h +1 h +146 h +1 h +935 h +601 h +10 h +28 h +4 h +14600 m +4 h +10 h +4 h +4 h +14601 m +4 h +4 h +13544 m +4 h +1 h +14602 m +41 h +10 h +1 h +14603 m +14604 m +1697 m +25 h +1 h +14605 m +104 h +14606 m +4 h +1 h +4 h +146 h +82 h +25 h +14607 m +4 h +10 h +14608 m +4 h +4 h +10 h +74 h +14609 m +14610 m +591 m +1 h +1 h +14611 m +10 h +520 h +4 h +4 h +1 h +14612 m +14613 m +4 h +238 h +10 h +2788 m +266 h +4 h +338 m +5 h +1714 m +14614 m +258 h +4 h +1 h +4 h +447 h +55 h +114 h +10 h +181 h +57 h +1 h +4 h +1 h +4 h +196 h +10 h +4 h +823 m +4 h +1 h +1 h +1 h +12 h +11 h +4 h +10 h +129 h +14615 m +10 h +1 h +14616 m +1 h +185 h +14617 m +8950 m +1 h +14618 m +14619 m +10 h +569 m +1 h +14620 m +124 h +4 h +185 h +14621 m +14622 m +1 h +57 h +14623 m +1 h +14624 m +65 h +14625 m +93 h +4 h +14626 m +196 h +3 h +109 h +4 h +1 h +10 h +14627 m +14628 m +4 h +1 h +12131 m +31 h +14629 m +14630 m +1016 h +25 h +14631 m +57 h +4 h +10 h +5478 m +14632 m +109 h +1 h +14633 m +10 h +278 h +10 h +14634 m +10 h +109 h +10 h +4 h +10 h +10 h +14635 m +10 h +83 h +4 h +65 h +14636 m +1137 h +353 h +1 h +1 h +10 h +10 h +14637 m +5 h +109 h +1 h +4 h +4 h +1772 m +10089 m +92 h +10 h +10 h +1 h +1470 h +10 h +1 h +4 h +118 h +1 h +737 h +31 h +14638 m +14639 m +1 h +1595 m +14640 m +299 h +2172 m +14641 m +28 h +4 h +1 h +14642 m +3 h +4 h +10 h +65 h +4 h +4 h +1 h +14643 m +11 h +109 h +10 h +1 h +4 h +4 h +1 h +10 h +14644 m +4 h +4 h +14645 m +692 h +14646 m +14647 m +14648 m +57 h +4 h +125 h +4 h +1 h +2733 h +109 h +14649 m +14650 m +1 h +1 h +4 h +10 h +4 h +258 h +109 h +1 h +14651 m +1 h +536 h +4 h +2494 m +1 h +4 h +10 h +383 h +2367 m +4 h +1 h +109 h +14652 m +59 h +3 h +1838 m +195 h +11 h +10 h +4 h +10 h +4 h +4 h +4 h +10 h +1 h +1 h +10 h +14653 m +14654 m +124 h +92 h +10 h +14655 m +1 h +1 h +4 h +14656 m +1227 m +4 h +4 h +4 h +14657 m +1 h +10 h +4 h +10 h +14658 m +913 m +56 h +10 h +4 h +1 h +135 h +14659 m +10 h +83 h +1 h +14660 m +4 h +1 h +1 h +4 h +31 h +4 h +14661 m +1 h +169 h +14662 m +1 h +14663 m +10 h +64 h +4 h +4 h +4 h +274 h +14664 m +1957 m +14665 m +41 h +10 h +135 h +14666 m +11 h +4 h +10 h +10 h +1 h +13 h +10 h +10 h +14667 m +289 h +195 h +2418 h +190 h +10 h +74 h +14668 m +59 h +14669 m +4 h +4 h +4 h +4 h +10 h +104 h +4 h +11 h +3028 m +4 h +1074 m +10 h +4 h +536 h +10 h +1 h +31 h +3 h +4 h +4 h +57 h +1 h +1 h +10 h +10 h +74 h +1 h +10 h +135 h +6869 m +10 h +4 h +14670 m +14671 m +22 h +195 h +109 h +1 h +1 h +124 h +172 h +10 h +10 h +10 h +4 h +14672 m +307 h +2971 m +10 h +4 h +264 m +1 h +97 h +11 h +1 h +4 h +4 h +124 h +1 h +14673 m +1 h +4 h +14674 m +4 h +4 h +4 h +14675 m +184 h +12 h +1 h +2072 m +1030 m +14676 m +11 h +64 h +4 h +13 h +195 h +83 h +14677 m +1137 h +477 m +1 h +3083 m +4 h +10 h +14678 m +14679 m +4 h +10 h +14680 m +1 h +28 h +14681 m +1 h +1 h +1 h +9861 m +4 h +1 h +4 h +10 h +14682 m +83 h +1 h +1 h +9757 m +10 h +14683 m +14684 m +14685 m +10 h +14686 m +1 h +1 h +11 h +11 h +4 h +10 h +104 h +4 h +10 h +10 h +14687 m +14688 m +4 h +14689 m +860 m +1 h +10 h +11 h +11 h +1 h +14690 m +4 h +338 h +4 h +4 h +4 h +14691 m +10 h +104 h +1 h +11 h +4 h +1039 m +10 h +13361 m +114 h +156 h +146 h +1 h +4 h +9727 m +156 h +14692 m +124 h +4 h +1 h +6567 m +1 h +14693 m +295 h +4 h +4 h +143 h +1 h +4 h +11 h +14694 m +4 h +13 h +4 h +4 h +14695 m +4 h +195 h +4 h +4 h +1419 m +3 h +964 m +1 h +1 h +109 h +60 m +730 m +1 h +1 h +4 h +109 h +562 m +10 h +10 h +124 h +10 h +143 h +14696 m +10 h +1 h +172 h +1 h +14697 m +4 h +14698 m +4 h +143 h +3 h +4900 m +36 h +4 h +14699 m +1 h +4 h +45 h +10 h +10 h +82 h +1 h +55 h +10 h +976 h +4 h +1 h +2359 m +10 h +4 h +59 h +1 h +4 h +74 h +4 h +1697 m +4 h +3 h +36 h +1 h +4 h +1 h +1 h +4 h +14700 m +124 h +4 h +289 h +4 h +1 h +57 h +3 h +10 h +4 h +4 h +110 h +4 h +1 h +1 h +11 h +332 h +4 h +14701 m +14702 m +1 h +112 h +10 h +1 h +4 h +1 h +1 h +10 h +258 h +14703 m +82 h +14704 m +172 h +14705 m +10 h +10 h +10 h +1 h +10 h +1 h +10 h +12 h +640 h +59 h +8 h +4 h +1 h +14706 m +4 h +10 h +125 h +4 h +4 h +10 h +10 h +4 h +57 h +4 h +1 h +1 h +74 h +11 h +1 h +147 h +4 h +1 h +1 h +10464 m +10 h +31 h +14707 m +147 h +10640 m +14708 m +4 h +4 h +4 h +230 h +8938 m +843 m +4 h +83 h +10 h +4 h +4 h +14709 m +4 h +1713 m +4 h +4 h +278 h +64 h +10 h +104 h +6702 m +4 h +1 h +230 h +278 h +14710 m +10 h +57 h +383 h +11 h +4 h +4 h +10 h +4 h +82 h +125 h +1 h +1 h +10 h +10 h +10 h +10 h +4 h +14711 m +520 h +4 h +4 h +4 h +10 h +1 h +1 h +69 h +4 h +1 h +1 h +1 h +1639 m +4 h +569 m +14712 m +986 h +9933 m +4441 m +4 h +1 h +258 h +4 h +14713 m +40 h +4 h +509 m +857 m +4 h +83 h +4 h +14714 m +4 h +4441 m +447 h +1 h +190 h +14715 m +266 h +45 h +1 h +110 h +412 h +146 h +4 h +278 h +1 h +143 h +10 h +10 h +4 h +169 h +1 h +14716 m +258 h +1027 h +1 h +10 h +10 h +82 h +4 h +195 h +2163 m +1 h +14717 m +1 h +25 h +10 h +10 h +104 h +4 h +10 h +164 h +185 h +1337 m +4 h +27 h +10 h +147 h +4 h +147 h +4 h +167 h +4 h +14718 m +4 h +4 h +14719 m +114 h +184 h +359 h +57 h +4 h +1 h +14720 m +238 h +2442 m +4 h +1 h +1 h +4 h +4 h +258 h +10 h +10 h +10 h +1 h +4 h +14721 m +10 h +139 h +1 h +10 h +10 h +1 h +13879 m +10 h +4 h +10 h +13536 m +4 h +146 h +125 h +45 h +14722 m +192 h +1 h +94 h +1 h +10 h +4 h +4 h +4 h +1 h +14723 m +14724 m +10 h +125 h +1 h +10 h +14725 m +536 h +14726 m +4 h +1 h +14727 m +14728 m +125 h +307 h +11 h +238 h +109 h +1 h +14729 m +1 h +5125 m +181 h +91 h +4 h +13 h +4 h +45 h +104 h +4 h +1 h +4 h +4 h +14730 m +4 h +4 h +10 h +1 h +55 h +4 h +14731 m +45 h +59 h +14732 m +1 h +4 h +14733 m +10 h +4 h +59 h +4 h +4 h +12 h +10 h +41 h +1 h +1 h +92 h +14734 m +10 h +41 h +4 h +1 h +1 h +31 h +4 h +14735 m +10 h +14736 m +1 h +14737 m +11 h +14738 m +1 h +195 h +1 h +170 h +4 h +14739 m +59 h +1 h +11 h +4 h +4 h +124 h +181 h +1 h +238 h +14740 m +4 h +10 h +14741 m +14742 m +1337 m +114 h +14743 m +31 h +45 h +4 h +14744 m +8197 m +3 h +14745 m +10 h +79 h +27 h +10 h +4 h +1737 m +386 h +4 h +14746 m +4905 m +1 h +1 h +10 h +229 h +1 h +4 h +4 h +195 h +10 h +11 h +295 h +14747 m +11 h +1 h +2769 m +14748 m +14749 m +14750 m +4 h +92 h +14751 m +1 h +581 m +4 h +4 h +195 h +14752 m +4 h +332 h +278 h +12805 m +4 h +14753 m +14754 m +169 h +10 h +10 h +4177 m +36 h +14755 m +4 h +12218 m +4 h +14756 m +10 h +5557 m +31 h +10 h +4 h +383 h +4 h +14757 m +4 h +1 h +4 h +2591 m +14758 m +1 h +1 h +3995 m +146 h +119 h +108 h +1 h +10 h +14759 m +1 h +265 h +79 h +146 h +4 h +1 h +14760 m +4 h +2490 m +10 h +123 h +109 h +14761 m +1 h +10 h +7479 m +11 h +4 h +14762 m +359 h +104 h +125 h +14763 m +4 h +1 h +41 h +1 h +282 m +14764 m +9933 m +276 h +4 h +4 h +14765 m +4 h +1 h +1 h +14766 m +12 h +10 h +14767 m +104 h +1 h +14768 m +11 h +10 h +14769 m +4 h +1 h +1 h +4 h +4 h +41 h +358 h +1 h +1 h +10 h +4 h +1 h +1 h +4 h +27 h +1 h +92 h +14770 m +14771 m +10 h +124 h +332 h +1 h +4 h +1 h +4 h +14772 m +1 h +10 h +10 h +4528 m +3 h +103 h +31 h +1 h +10 h +10 h +4 h +4 h +10 h +614 m +626 m +173 h +57 h +14773 m +14774 m +10 h +4 h +167 h +4 h +4 h +14775 m +14776 m +1 h +31 h +238 h +1 h +4 h +1 h +10 h +109 h +14777 m +319 h +10 h +4 h +4 h +1 h +83 h +14778 m +11 h +4 h +14779 m +4 h +4 h +10 h +10 h +1 h +196 h +8683 m +443 h +83 h +10 h +124 h +11 h +4 h +4 h +4 h +4 h +14780 m +10 h +14781 m +8890 m +11 h +10 h +1 h +14782 m +10 h +4 h +11 h +1 h +124 h +4 h +4 h +4 h +14783 m +4 h +12338 m +1791 m +1796 h +4 h +10 h +10 h +4 h +82 h +10 h +4 h +4 h +4 h +258 h +14784 m +1 h +124 h +3 h +1 h +1 h +1 h +4 h +14785 m +4 h +14786 m +14787 m +124 h +4 h +14788 m +4 h +4 h +14789 m +113 h +13 h +4 h +14790 m +4 h +4 h +14791 m +14792 m +106 m +1261 h +4188 m +10 h +14793 m +14794 m +4 h +1 h +1 h +10 h +3750 m +4 h +14795 m +1 h +1 h +14796 m +11 h +119 h +14797 m +1 h +10 h +73 h +4 h +10 h +25 h +14798 m +83 h +14477 m +10 h +955 m +1 h +14799 m +4744 m +14800 m +1780 m +14801 m +104 h +14802 m +45 h +14803 m +276 h +10 h +1 h +55 h +14804 m +4 h +14805 m +75 m +14806 m +11 h +1 h +4 h +14807 m +69 h +4 h +399 h +4 h +4 h +14808 m +104 h +733 m +31 h +14809 m +10 h +14810 m +14811 m +124 h +4 h +11 h +14812 m +10 h +1952 m +4 h +14813 m +110 h +11 h +14814 m +83 h +10 h +4 h +1 h +4 h +10 h +4 h +4 h +1 h +4 h +57 h +10 h +57 h +692 h +10 h +114 h +64 h +12551 m +4 h +10 h +4 h +167 h +4 h +4 h +4 h +124 h +1 h +10 h +4 h +1 h +1 h +10 h +181 h +83 h +10 h +4 h +10 h +4 h +10 h +82 h +10 h +10 h +83 h +82 h +3170 m +14815 m +1 h +8 h +14816 m +14817 m +1 h +1 h +7395 m +4 h +270 h +307 h +14818 m +4 h +6599 m +4 h +59 h +1 h +10 h +4 h +11 h +1 h +64 h +10 h +4 h +14819 m +83 h +4 h +10 h +10 h +109 h +1 h +371 h +7592 m +4 h +4 h +14820 m +14821 m +278 h +41 h +1 h +4 h +82 h +11 h +124 h +125 h +14822 m +167 h +10 h +10 h +14823 m +4 h +4 h +10 h +1 h +266 h +83 h +1 h +4 h +4 h +14824 m +1330 m +31 h +14825 m +10 h +4 h +124 h +1574 m +11 h +763 m +41 h +14826 m +4 h +14827 m +1 h +13 h +4 h +14828 m +4 h +10 h +12637 m +10 h +1 h +4 h +14829 m +14830 m +14831 m +4 h +13944 m +642 h +14832 m +4 h +11 h +1 h +10 h +14833 m +10 h +4 h +10 h +1725 m +14834 m +10 h +4 h +4 h +14835 m +82 h +4 h +14836 m +25 h +14837 m +14838 m +10 h +4 h +1 h +68 m +4 h +10 h +1 h +4 h +167 h +144 h +4 h +14839 m +10 h +4 h +4 h +4 h +4 h +4 h +10 h +538 h +10 h +10 h +4 h +10 h +2845 m +36 h +1 h +408 m +14840 m +124 h +330 h +114 h +13 h +1 h +74 h +10 h +3 h +14841 m +11486 m +4 h +10 h +987 m +14842 m +1 h +10 h +14843 m +14844 m +4 h +2885 m +112 h +4 h +146 h +10 h +4 h +10 h +10 h +41 h +14845 m +1 h +57 h +2184 m +1 h +10 h +4 h +14846 m +79 h +1 h +4 h +144 h +27 h +4 h +4 h +10 h +14847 m +4 h +570 h +14848 m +1 h +3 h +4 h +124 h +4 h +14849 m +14850 m +10 h +14851 m +10 h +1 h +125 h +14852 m +1 h +14853 m +1 h +36 h +73 h +69 h +170 h +8 h +83 h +10 h +14854 m +25 h +14855 m +4 h +4 h +1 h +10 h +1 h +57 h +4 h +1 h +10 h +4 h +1 h +4 h +14856 m +4 h +14857 m +164 h +14858 m +1 h +10 h +4 h +1 h +14859 m +869 h +14860 m +82 h +4 h +10 h +266 h +10 h +330 h +1 h +10 h +10 h +83 h +14861 m +10 h +73 h +4 h +45 h +3396 m +1 h +74 h +124 h +4 h +64 h +5544 m +1 h +10 h +196 h +4 h +14862 m +14863 m +59 h +4103 m +14689 m +2699 m +146 h +147 h +299 h +14864 m +147 h +4 h +1 h +1 h +10 h +10 h +82 h +4 h +4 h +4 h +1 h +447 h +299 h +10 h +10 h +83 h +83 h +307 h +279 h +14865 m +1 h +65 h +10 h +1 h +1 h +119 h +8 h +1016 h +14866 m +1 h +238 h +65 h +1 h +1780 m +4 h +74 h +125 h +14867 m +4 h +986 h +4 h +14868 m +14869 m +57 h +4 h +10 h +114 h +4 h +1105 h +4 h +14870 m +14871 m +1 h +1 h +4 h +10 h +4 h +57 h +14872 m +14873 m +10 h +4 h +4 h +10 h +7300 m +14874 m +6668 m +488 h +59 h +14875 m +10 h +10 h +14876 m +59 h +1 h +10 h +196 h +1 h +10 h +14877 m +25 h +123 h +4 h +4 h +4 h +4 h +265 h +1 h +173 h +14878 m +10 h +4 h +1281 m +1 h +10 h +14879 m +147 h +10 h +4 h +4 h +4 h +196 h +2275 m +57 h +14880 m +3 h +2022 m +1 h +3048 m +986 h +8643 m +11 h +7872 m +1642 h +1 h +196 h +14881 m +10 h +10 h +1 h +1 h +14882 m +83 h +4 h +4 h +4 h +258 h +4 h +1 h +4 h +1 h +1 h +4 h +4 h +578 h +167 h +4 h +4 h +1 h +1137 h +1 h +10 h +109 h +10 h +4 h +4 h +265 h +172 h +1 h +1 h +196 h +1 h +73 h +1 h +36 h +57 h +4 h +14883 m +4 h +1 h +4 h +1 h +4 h +885 h +256 m +563 m +4 h +14884 m +14885 m +4 h +10 h +1 h +10 h +14886 m +1 h +27 h +158 h +1 h +1070 m +57 h +10 h +4 h +4 h +196 h +82 h +10 h +4 h +274 h +4 h +74 h +12884 m +4 h +10 h +4 h +156 h +4 h +14887 m +443 h +11 h +14888 m +14889 m +181 h +258 h +14890 m +4 h +4 h +14891 m +4 h +4 h +4 h +14892 m +2444 m +1 h +4 h +4 h +10 h +1 h +1454 h +4 h +135 h +11 h +59 h +4 h +1 h +10 h +124 h +4 h +124 h +11 h +3 h +4 h +1 h +12755 m +10 h +12 h +104 h +11 h +14893 m +5557 m +25 h +10 h +10 h +2846 m +14894 m +10 h +82 h +1 h +4 h +135 h +10 h +4 h +10 h +1 h +27 h +41 h +1 h +4 h +14895 m +1 h +4 h +164 h +569 h +1 h +4253 m +14896 m +57 h +57 h +1 h +4 h +82 h +10 h +10 h +1 h +1 h +1 h +82 h +4 h +1 h +1 h +14897 m +14898 m +1 h +4 h +59 h +10 h +1 h +4 h +146 h +4 h +94 h +14899 m +1 h +41 h +1 h +11 h +83 h +443 h +4 h +82 h +119 h +94 h +1 h +1 h +65 h +10 h +4 h +1 h +10062 m +4844 m +14900 m +10 h +55 h +4 h +10 h +4486 m +1 h +1 h +13349 m +464 h +4 h +1199 m +1 h +4 h +14901 m +14902 m +1 h +7479 m +4 h +1 h +4 h +4 h +4 h +14903 m +4 h +1 h +1 h +14904 m +10 h +125 h +31 h +10 h +1 h +1 h +11 h +10 h +258 h +41 h +3293 m +14905 m +14906 m +4 h +14907 m +195 h +14908 m +1 h +1 h +1 h +3 h +186 h +14909 m +11 h +10 h +10 h +4 h +1 h +1403 h +97 h +4 h +1 h +1 h +113 h +4 h +1 h +1 h +109 h +4 h +4 h +403 h +10 h +31 h +31 h +10 h +1 h +25 h +1 h +12 h +10 h +4 h +10 h +1 h +82 h +83 h +1 h +4 h +1 h +14910 m +14911 m +14912 m +278 h +1 h +14913 m +14914 m +4 h +4 h +1 h +11 h +358 h +65 h +1822 h +143 h +4 h +25 h +14915 m +172 h +2379 h +14916 m +276 h +125 h +4 h +4 h +10 h +14917 m +1 h +196 h +4 h +27 h +4 h +1250 h +1 h +14918 m +185 h +10 h +1 h +4 h +14919 m +4 h +4 h +4 h +14920 m +10 h +4 h +14921 m +109 h +14922 m +270 h +10 h +4 h +1 h +1 h +57 h +270 h +14923 m +14924 m +14925 m +307 h +14926 m +83 h +4 h +1 h +123 h +2594 m +10 h +12131 m +55 h +14927 m +3095 m +3 h +192 h +11 h +10 h +10 h +4 h +4 h +190 h +79 h +1 h +4 h +14928 m +1 h +10 h +1 h +14929 m +279 h +195 h +124 h +4240 m +4 h +4 h +14930 m +10 h +124 h +1 h +386 h +41 h +14931 m +1 h +11 h +4 h +4 h +83 h +4 h +4 h +11766 m +4 h +64 h +1 h +4 h +157 h +4 h +1 h +13 h +14932 m +125 h +1 h +4 h +195 h +4 h +125 h +10 h +94 h +14933 m +4 h +57 h +31 h +14934 m +4 h +10 h +14935 m +4 h +64 h +82 h +11 h +4 h +97 h +14936 m +4 h +73 h +46 m +1 h +57 h +1454 h +1 h +1 h +14937 m +4 h +13 h +1 h +112 h +14938 m +10 h +14939 m +1 h +14940 m +4 h +25 h +4 h +109 h +10 h +14941 m +1 h +169 h +14942 m +10 h +4 h +488 h +4 h +27 h +1 h +4 h +10 h +1 h +14943 m +158 h +10 h +10 h +22 h +124 h +14944 m +2721 m +1 h +143 h +14945 m +14946 m +10 h +73 h +1 h +1697 h +14947 m +41 h +1 h +10 h +4 h +11 h +14948 m +1 h +14949 m +4538 m +10 h +11 h +10 h +4 h +279 h +4 h +8 h +147 h +4 h +14950 m +167 h +10 h +106 m +1 h +1 h +14951 m +1650 h +10 h +1 h +10 h +14952 m +1 h +10 h +1 h +14953 m +195 h +173 h +11 h +3 h +1 h +14954 m +4 h +1 h +4 h +1 h +332 h +10 h +4 h +4 h +10 h +307 h +1284 m +2887 m +2928 m +10 h +2163 m +3 h +196 h +14955 m +10 h +1 h +14838 m +11 h +1 h +146 h +4567 m +4 h +14956 m +3 h +14957 m +4 h +1 h +10 h +73 h +1 h +4 h +4 h +4 h +14958 m +1 h +14959 m +4 h +12 h +14960 m +125 h +4 h +14961 m +14962 m +1915 m +1 h +14963 m +1 h +1 h +640 h +258 h +4 h +14964 m +14965 m +11 h +181 h +4 h +6663 m +14966 m +996 m +4 h +4 h +11 h +4 h +1 h +14967 m +15 m +11 h +278 h +4 h +4 h +14968 m +14969 m +192 h +195 h +14970 m +82 h +31 h +13 h +27 h +8 h +25 h +14971 m +14972 m +1 h +3 h +124 h +57 h +14973 m +1 h +1 h +11 h +4 h +4 h +4 h +4 h +14974 m +1 h +14975 m +9411 m +4 h +10 h +266 h +97 h +1 h +55 h +125 h +266 h +14976 m +14977 m +4 h +14978 m +4 h +10 h +1 h +274 h +57 h +14979 m +840 m +3161 m +14980 m +4 h +1 h +146 h +59 h +73 h +1677 m +124 h +1775 m +196 h +4 h +4 h +14981 m +147 h +779 h +1374 m +4 h +5562 m +1 h +4 h +4 h +1 h +4 h +1 h +65 h +4 h +195 h +4 h +11 h +4 h +14982 m +14983 m +4 h +1 h +4 h +79 h +4 h +1 h +125 h +631 m +14984 m +14985 m +4 h +4 h +8324 m +10 h +14986 m +1 h +10 h +82 h +1 h +1 h +1 h +1 h +1 h +4 h +97 h +4 h +14987 m +10 h +10 h +9176 m +14988 m +190 h +4 h +64 h +31 h +9400 m +10 h +1 h +10 h +14989 m +4 h +1 h +14990 m +538 h +14991 m +4 h +14992 m +1 h +1 h +1714 h +14993 m +109 h +1 h +4 h +4 h +11 h +14994 m +4 h +2064 m +1 h +57 h +1 h +4 h +8188 m +4 h +1 h +10 h +4 h +14995 m +125 h +1685 h +11 h +10 h +1 h +14996 m +538 h +1 h +5541 m +14997 m +10 h +14998 m +10 h +4 h +124 h +14999 m +146 h +15000 m +1 h +2002 m +4 h +4 h +10 h +4 h +10 h +4127 m +83 h +687 h +3 h +4 h +144 h +4 h +10 h +15001 m +10 h +4 h +4 h +481 m +8610 m +15002 m +11 h +5125 m +10 h +15003 m +15004 m +15005 m +10 h +15006 m +1 h +15007 m +15008 m +12 h +332 h +4 h +4 h +65 h +15009 m +3141 m +1 h +4 h +4 h +8 h +15010 m +4 h +15011 m +65 h +6963 m +10 h +10 h +563 m +1 h +15012 m +15013 m +10 h +15014 m +15015 m +1 h +4 h +172 h +4 h +1293 m +94 h +1 h +15016 m +57 h +15017 m +10 h +97 h +4 h +15018 m +1 h +13980 m +4 h +332 h +156 h +4 h +6260 m +4 h +1 h +238 h +55 h +229 h +92 h +4 h +4 h +1 h +4 h +601 h +109 h +229 h +196 h +15019 m +15020 m +1 h +11 h +1 h +11 h +124 h +15021 m +4 h +2054 m +4 h +1 h +4 h +11 h +10 h +15022 m +110 h +113 h +69 h +135 h +15023 m +1359 h +4 h +156 h +15024 m +15025 m +1 h +15026 m +1 h +10 h +15027 m +4089 m +2891 m +10 h +1 h +5422 m +536 h +15028 m +1 h +8 h +57 h +15029 m +1 h +15030 m +4 h +4 h +4 h +4 h +383 h +4 h +363 m +1 h +4 h +10 h +4 h +11 h +1 h +285 m +885 h +3 h +59 h +10 h +15031 m +185 h +41 h +4 h +1 h +9691 m +4 h +4 h +10 h +4 h +10 h +3 h +1 h +170 h +147 h +59 h +15032 m +1070 m +4 h +10 h +82 h +6783 m +4 h +15033 m +15034 m +1 h +15035 m +135 h +1 h +15036 m +1 h +1 h +1 h +48 h +4 h +83 h +1 h +146 h +10 h +1 h +1 h +4 h +10 h +135 h +1953 m +15037 m +1409 m +27 h +10 h +56 h +4 h +10 h +10 h +147 h +857 h +124 h +94 h +4 h +15038 m +10 h +10 h +359 h +4 h +12 h +4 h +1 h +10 h +14570 m +15039 m +15040 m +2303 m +10 h +83 h +10 h +1 h +4 h +64 h +4 h +1 h +10 h +2928 m +10 h +4 h +1 h +196 h +15041 m +1 h +15042 m +10 h +4 h +4 h +1261 h +4 h +57 h +4 h +15043 m +459 h +124 h +15044 m +1 h +55 h +1 h +45 h +10 h +15045 m +10 h +1645 m +4 h +4 h +1 h +1 h +4 h +1 h +124 h +83 h +55 h +649 m +65 h +10 h +4 h +1198 m +15046 m +575 h +1 h +15047 m +195 h +338 h +1 h +1 h +124 h +1205 m +10 h +15048 m +4 h +124 h +147 h +4 h +15049 m +4 h +1 h +15050 m +4 h +82 h +147 h +1 h +31 h +4718 m +186 h +4 h +11 h +3 h +1 h +82 h +15051 m +316 m +278 h +3 h +4 h +888 m +278 h +73 h +1 h +10 h +82 h +15052 m +164 h +15053 m +10 h +146 h +10 h +1 h +82 h +10 h +4 h +1 h +1 h +718 h +6296 m +15054 m +4 h +1 h +4 h +1 h +4 h +578 h +139 h +6501 m +4 h +4 h +3 h +4 h +10 h +3 h +4 h +4 h +1 h +119 h +10 h +1 h +4 h +10 h +10 h +1 h +2495 m +1116 m +1 h +4 h +12 h +4 h +15055 m +1 h +4 h +124 h +11 h +1 h +1 h +425 m +15056 m +4 h +1 h +15057 m +4 h +15058 m +3 h +1 h +59 h +10 h +1 h +1 h +10 h +195 h +15059 m +15060 m +15061 m +15062 m +139 h +4 h +3 h +1 h +4 h +4 h +4 h +4 h +97 h +4 h +10 h +15063 m +15064 m +270 h +1 h +4 h +74 h +10 h +15065 m +5809 m +1 h +15066 m +4 h +10 h +1470 h +1 h +1 h +1 h +4 h +4 h +82 h +4905 m +7 m +4 h +3 h +4 h +15067 m +1 h +181 h +4 h +307 h +1 h +5387 m +124 h +31 h +15068 m +147 h +4372 m +10 h +82 h +196 h +1953 m +15069 m +1 h +109 h +15070 m +104 h +169 h +10 h +4 h +4 h +15071 m +1 h +172 h +10 h +4 h +15072 m +15073 m +1 h +1470 h +4905 m +1 h +15074 m +82 h +10 h +10 h +15075 m +10 h +10 h +13 h +4 h +386 h +10 h +15076 m +4 h +15077 m +1 h +10 h +1 h +181 h +4 h +1 h +10 h +386 h +4 h +4 h +15078 m +10 h +10 h +15079 m +15080 m +4 h +6549 m +164 h +266 h +10 h +83 h +10 h +4 h +59 h +12 h +15081 m +10 h +82 h +15082 m +124 h +15083 m +12 h +4 h +10 h +15084 m +4 h +15085 m +1 h +15086 m +1 h +15087 m +1 h +1 h +1 h +25 h +15088 m +97 h +124 h +10 h +124 h +57 h +124 h +10 h +10 h +10 h +1685 h +4 h +10 h +10 h +146 h +15089 m +10 h +45 h +258 h +276 h +15090 m +15091 m +15092 m +1 h +1 h +1 h +4 h +1 h +10 h +124 h +4 h +3216 m +15093 m +258 h +10 h +15094 m +4 h +4 h +74 h +4 h +1 h +104 h +1835 m +4 h +1137 h +15095 m +82 h +79 h +15096 m +10 h +83 h +10 h +4 h +4 h +4 h +10 h +1 h +12 h +1 h +124 h +124 h +322 h +319 h +332 h +4 h +4 h +15097 m +4857 m +15098 m +15099 m +10 h +15100 m +181 h +10 h +4 h +4 h +4 h +10 h +4 h +22 h +82 h +15101 m +4 h +1 h +4 h +10 h +10 h +15102 m +10 h +15103 m +698 m +15104 m +15105 m +74 h +4 h +10 h +31 h +4 h +4 h +15106 m +139 h +4 h +11 h +4 h +1 h +1 h +15107 m +94 h +15108 m +538 h +15109 m +1 h +15110 m +4132 m +15111 m +1 h +10 h +4 h +10 h +4 h +10 h +1 h +1027 h +109 h +3 h +15112 m +15113 m +4 h +108 h +10 h +12700 m +15114 m +15115 m +65 h +358 h +4 h +15116 m +4 h +10 h +4 h +4 h +4 h +1 h +4 h +4 h +276 h +10 h +10 h +15117 m +15118 m +1 h +533 h +256 m +3841 m +15119 m +10 h +1 h +4 h +135 h +15120 m +190 h +1556 m +97 h +91 h +10 h +15121 m +119 h +15122 m +15123 m +15124 m +4 h +4 h +57 h +4 h +15125 m +3 h +92 h +4 h +172 h +15126 m +15127 m +15128 m +94 h +15129 m +15130 m +1 h +104 h +1137 h +1 h +3 h +15131 m +4 h +5348 m +4 h +11 h +4 h +4 h +10 h +14308 m +15132 m +10 h +23 h +112 h +1 h +135 h +4 h +601 h +4 h +5632 m +10 h +4 h +15133 m +73 h +4 h +14 m +146 h +10 h +15134 m +1 h +195 h +11 h +4 h +31 h +10 h +4 h +109 h +15135 m +10 h +4 h +1 h +15136 m +1 h +15137 m +124 h +15138 m +15139 m +195 h +10 h +4 h +536 h +1576 m +10 h +15140 m +3 h +15141 m +1 h +4 h +1 h +278 h +1817 m +1 h +82 h +112 h +15142 m +195 h +143 h +15143 m +1309 h +169 h +4 h +4 h +4 h +25 h +10 h +4 h +1 h +196 h +15144 m +4 h +1 h +1 h +15145 m +82 h +1 h +4 h +15146 m +124 h +15147 m +15148 m +383 h +4 h +15149 m +1 h +104 h +1 h +238 h +1 h +1 h +4 h +4350 m +83 h +11 h +10 h +10 h +82 h +10 h +1 h +10 h +113 h +15150 m +4 h +74 h +4 h +4 h +125 h +1 h +27 h +4 h +4 h +367 h +15151 m +11 h +1 h +15152 m +139 h +4 h +976 h +124 h +1 h +195 h +15153 m +10 h +15154 m +10 h +59 h +10 h +15155 m +808 m +238 h +10 h +872 m +4 h +36 h +15156 m +4 h +156 h +4 h +10 h +307 h +33 m +15157 m +15158 m +57 h +55 h +1 h +79 h +59 h +297 h +15159 m +31 h +4 h +1 h +10 h +4 h +15160 m +1 h +4 h +1 h +8 h +186 h +1574 m +15161 m +447 h +15162 m +15163 m +15164 m +147 h +79 h +186 h +4 h +3 h +4 h +15165 m +45 h +1 h +1725 m +3702 m +10 h +4 h +1 h +1403 h +15166 m +108 h +1 h +1 h +4 h +11 h +31 h +15167 m +10 h +15168 m +4 h +4 h +1 h +10 h +935 h +15169 m +4 h +10 h +4 h +4 h +15170 m +57 h +4 h +4 h +10 h +4 h +15171 m +3 h +10 h +97 h +4 h +10 h +112 h +10 h +48 h +1 h +1 h +1 h +114 h +83 h +82 h +164 h +15172 m +10 h +114 h +4 h +1116 m +4 h +4 h +10 h +4 h +1 h +4 h +74 h +1 h +4 h +1 h +4 h +4 h +1 h +1 h +15173 m +10 h +10 h +4 h +15174 m +82 h +435 m +59 h +4 h +83 h +10 h +2720 m +15175 m +11 h +1 h +1 h +123 h +13 h +15176 m +83 h +31 h +5545 m +15177 m +82 h +4 h +1766 h +41 h +15178 m +5982 m +15179 m +10 h +1 h +169 h +4 h +4 h +15180 m +443 h +123 h +15181 v +13854 m +11 h +1861 m +1 h +1 h +15182 m +4 h +1 h +36 h +97 h +124 h +10 h +990 h +1 h +195 h +1 h +11 h +4 h +1 h +3 h +1441 m +955 m +1 h +4 h +10 h +12 h +123 h +1261 h +15183 m +91 h +109 h +4 h +4 h +10 h +65 h +124 h +169 h +717 m +10 h +124 h +4 h +4 h +31 h +94 h +359 h +11 h +113 h +2937 m +2285 m +4 h +8 h +15184 m +104 h +4 h +4 h +297 h +10 h +4 h +4 h +15185 m +82 h +15186 m +1 h +1 h +11 h +10 h +45 h +4 h +15187 m +4 h +1 h +4 h +273 m +4 h +15188 m +1045 m +1 h +1089 h +4 h +15189 m +1 h +1 h +10 h +10 h +4301 m +4 h +45 h +83 h +4 h +195 h +64 h +146 h +10 h +4 h +1 h +10 h +10 h +15190 m +2183 m +299 h +4 h +97 h +15191 m +1261 h +1 h +454 m +1 h +15192 m +15193 m +135 h +4 h +195 h +15194 m +10 h +4 h +4 h +1 h +15195 m +13 h +278 h +4 h +5249 m +986 h +82 h +1 h +114 h +10 h +358 h +10 h +4 h +74 h +4 h +15196 m +4 h +15197 m +1 h +15198 m +1 h +4 h +4857 m +15199 m +4 h +1 h +279 h +31 h +10 h +15200 m +1 h +15201 m +1 h +4 h +4 h +4 h +156 h +11 h +1 h +4 h +11 h +10 h +27 h +4 h +3405 m +10 h +3 h +15202 m +4 h +1 h +4 h +1089 h +4 h +1 h +15203 m +4 h +31 h +124 h +57 h +15204 m +1 h +15205 m +1 h +4 h +10 h +41 h +15206 m +15207 m +4 h +4 h +4 h +110 h +79 h +15208 m +15209 m +4 h +4 h +1 h +10 h +4 h +4 h +204 h +4 h +4520 m +4 h +135 h +4 h +10 h +129 h +82 h +15210 m +307 h +10 h +1 h +1015 m +911 h +4 h +125 h +238 h +59 h +11 h +10 h +4 h +238 h +10 h +195 h +4 h +14112 m +170 h +1 h +6290 m +195 h +11 h +1 h +1714 h +97 h +1137 h +10 h +4 h +857 h +56 h +4 h +11 h +4 h +601 h +4 h +15211 m +82 h +4 h +2788 h +15212 m +125 h +31 h +1 h +4 h +65 h +258 h +64 h +4 h +15213 m +10 h +358 h +57 h +1 h +15214 m +10 h +11 h +15215 m +15216 m +1074 m +278 h +4 h +11 h +297 h +4 h +4 h +10 h +256 m +10 h +41 h +146 h +15217 m +15218 m +1 h +4 h +307 h +3 h +15219 m +11 h +97 h +1 h +4 h +4 h +1 h +10 h +289 h +83 h +15220 m +4 h +1 h +36 h +733 m +196 h +1 h +4 h +15221 m +15222 m +1 h +10 h +108 h +4 h +92 h +4 h +258 h +15223 m +15224 m +41 h +1 h +195 h +65 h +316 m +695 m +3287 m +11 h +97 h +15225 m +1 h +4 h +1 h +10 h +1 h +1 h +15226 m +1535 m +1030 h +307 h +4 h +15227 m +358 h +10 h +10 h +124 h +1 h +15228 m +59 h +4 h +1898 m +25 h +4 h +4 h +15229 m +11 h +15230 m +4 h +4 h +10 h +4 h +31 h +15231 m +57 h +4 h +4 h +10 h +156 h +1 h +8 h +3 h +125 h +119 h +443 h +4 h +2418 h +1 h +15232 m +4 h +4 h +4 h +15233 m +56 h +802 m +11 h +1309 h +4 h +59 h +11 h +82 h +4 h +15234 m +4 h +12543 m +41 h +1 h +10 h +10 h +94 h +1 h +4 h +4 h +10 h +4 h +1 h +170 h +4 h +15235 m +10 h +4 h +15236 m +15237 m +1772 m +1 h +10 h +15238 m +1 h +4 h +1016 h +41 h +10 h +1 h +1 h +1822 h +10 h +10 h +41 h +4 h +10 h +57 h +4 h +4 h +10 h +15239 m +10 h +15240 m +4 h +15241 m +4 h +109 h +4 h +11 h +94 h +10 h +4 h +1 h +10 h +15242 m +238 h +4 h +4 h +4 h +4 h +5814 m +144 h +1083 m +3141 m +4 h +10 h +4 h +9691 m +4 h +57 h +4 h +15243 m +114 h +15244 m +10 h +15245 m +1 h +4 h +10 h +15246 m +1 h +4 h +3 h +285 m +10 h +15247 m +447 h +12 h +15248 m +1137 h +4 h +4 h +4 h +4 h +4 h +157 h +59 h +4 h +110 h +10 h +15249 m +266 h +25 h +10 h +45 h +59 h +10 h +4 h +15250 m +15251 m +109 h +15252 m +3 h +4 h +124 h +4 h +4 h +15253 m +31 h +1 h +15254 m +4 h +2923 h +109 h +4966 m +4 h +1260 m +27 h +74 h +332 h +15255 m +386 h +195 h +65 h +124 h +4 h +15256 m +31 h +1 h +332 h +4 h +4 h +4 h +15257 m +10 h +1386 m +15258 m +1 h +6214 m +15259 m +4 h +74 h +7400 m +1403 h +5145 m +4 h +83 h +4 h +1 h +10 h +4 h +289 h +4 h +8477 m +139 h +109 h +4 h +4524 m +15260 m +1 h +4 h +4 h +4030 m +11 h +157 h +1027 h +143 h +10 h +4 h +4 h +4 h +279 h +1 h +15261 m +15262 m +12 h +4 h +59 h +1 h +15263 m +400 m +74 h +1619 h +4 h +10 h +4 h +1 h +125 h +4 h +1 h +114 h +1 h +4 h +1 h +15264 m +15265 m +25 h +10 h +4 h +15266 m +443 h +10 h +25 h +10 h +1 h +330 h +146 h +1 h +15267 m +1 h +4 h +11 h +11 h +45 h +146 h +59 h +10 h +4 h +10 h +3 h +4 h +15268 m +4 h +125 h +74 h +59 h +4 h +10 h +295 h +79 h +1 h +125 h +4 h +4 h +11654 m +10 h +2627 m +10 h +1 h +97 h +4 h +4 h +9300 m +14 m +4 h +59 h +172 h +59 h +4 h +2887 m +1 h +94 h +190 h +103 h +10 h +59 h +15269 m +15270 m +4 h +15271 m +1 h +1 h +25 h +4 h +1 h +1 h +1 h +4 h +4 h +4 h +104 h +4 h +1 h +109 h +3 h +3523 m +169 h +11 h +10 h +27 h +493 m +79 h +4240 m +1 h +1650 h +15272 m +119 h +1751 m +4 h +4 h +15273 m +4 h +412 h +435 m +912 m +15274 m +1 h +1 h +1 h +143 h +27 h +1 h +27 h +4 h +289 h +4 h +10 h +10 h +114 h +10 h +70 m +640 h +15275 m +97 h +15276 m +1 h +1 h +119 h +4 h +28 h +4 h +4 h +11 h +4 h +4 h +104 h +15277 m +15278 m +386 h +15279 m +4 h +15280 m +15281 m +4 h +10 h +10 h +1 h +10 h +10 h +164 h +109 h +1 h +109 h +172 h +4 h +238 h +1 h +83 h +10 h +10 h +10 h +4 h +4 h +1685 h +4 h +1 h +1 h +124 h +1 h +64 h +10 h +11 h +8 h +295 h +10 h +1 h +4 h +1089 h +57 h +10 h +15282 m +109 h +10 h +11109 m +1 h +509 m +15283 m +15284 m +41 h +952 m +15285 m +15286 m +1 h +2733 h +4 h +15287 m +15288 m +15289 m +1 h +1677 m +1 h +4 h +22 h +1 h +10 h +10062 m +307 h +83 h +3477 m +59 h +31 h +97 h +13 h +11 h +135 h +11 h +10 h +11 h +1 h +1 h +10 h +12 h +124 h +10 h +4 h +4 h +4 h +170 h +1137 h +307 h +41 h +113 h +83 h +4 h +4 h +4 h +1 h +10 h +15290 m +270 h +15291 m +1 h +1 h +4 h +1 h +692 h +4 h +109 h +4 h +110 h +10 h +1 h +10 h +10 h +79 h +36 h +15292 m +1 h +1 h +10 h +1 h +443 h +1 h +4 h +15293 m +10 h +1 h +1642 h +1016 h +238 h +4 h +1 h +11 h +4 h +368 h +1 h +1766 h +1 h +399 h +11 h +10 h +4 h +15294 m +1 h +11 h +258 h +72 m +1 h +10 h +31 h +1893 m +74 h +11 h +15295 m +7646 m +1 h +4 h +1 h +4 h +295 h +15296 m +1 h +135 h +10 h +4 h +443 h +11 h +319 h +1 h +12047 m +15297 m +170 h +15298 m +135 h +1 h +1 h +4 h +55 h +9175 m +15299 m +15300 m +11 h +10 h +4 h +15301 m +353 h +478 h +143 h +266 h +15302 m +15303 m +1 h +57 h +124 h +15304 m +10 h +338 h +10 h +15305 m +4 h +536 h +4 h +15306 m +59 h +1 h +1 h +2475 m +1 h +91 h +10 h +1 h +4 h +15307 m +10 h +15308 m +1 h +1 h +10 h +5613 m +10 h +4 h +15309 m +330 h +4 h +10 h +15310 m +31 h +15311 m +1 h +1 h +15312 m +4 h +10 h +15313 m +57 h +4 h +4 h +1 h +15314 m +4 h +59 h +55 h +92 h +15315 m +15316 m +4 h +4 h +4 h +4 h +15317 m +4 h +276 h +929 m +403 h +15318 m +2041 m +4 h +15319 m +4 h +13 h +10 h +83 h +12389 m +4 h +125 h +94 h +185 h +184 h +4 h +10 h +104 h +256 h +15320 m +15321 m +656 m +82 h +1 h +82 h +25 h +1 h +4 h +15322 m +56 h +10 h +1 h +1016 h +10 h +11 h +10 h +4 h +1 h +8346 m +147 h +12 h +295 h +82 h +169 h +1 h +10 h +1 h +1 h +10 h +36 h +3622 m +1 h +4 h +109 h +15323 m +59 h +1 h +4 h +1 h +15324 m +3679 m +31 h +1 h +170 h +4 h +172 h +4 h +10 h +4 h +1 h +4 h +10 h +1881 m +15325 m +1 h +10 h +1650 h +1 h +10 h +83 h +4 h +1 h +704 m +114 h +4 h +1 h +1 h +10 h +15326 m +4 h +15327 m +10 h +4 h +295 h +694 m +14098 m +1 h +4 h +1 h +129 h +3155 m +377 h +15328 m +56 h +1 h +4 h +1 h +1914 m +1595 m +10 h +15329 m +125 h +10 h +1 h +4 h +15154 m +4 h +4 h +208 m +4 h +91 h +196 h +4 h +4 h +77 m +15330 m +1 h +1 h +1 h +4 h +779 h +4 h +297 h +258 h +1 h +4 h +82 h +3344 m +56 h +368 h +11 h +15331 m +1105 h +4 h +1 h +4 h +82 h +4 h +4 h +11 h +15332 m +1137 h +4 h +15333 m +147 h +4 h +124 h +10 h +1 h +4 h +15334 m +1 h +15335 m +195 h +10 h +4 h +15336 m +11 h +15337 m +4 h +4 h +124 h +10 h +1 h +935 h +15338 m +15339 m +25 h +4 h +4 h +4 h +1 h +2865 m +123 h +4 h +167 h +82 h +1 h +119 h +4 h +1 h +512 m +4 h +4 h +124 h +15340 m +92 h +59 h +1 h +857 h +10 h +25 h +4 h +1 h +13 h +4 h +10 h +1 h +10 h +1 h +430 m +15341 m +4 h +5065 m +591 m +15342 m +10 h +157 h +15343 m +4 h +4 h +1 h +11 h +28 h +59 h +10 h +15344 m +10 h +10 h +1 h +264 h +41 h +10 h +15345 m +15346 m +11 h +10 h +146 h +15347 m +4 h +4 h +164 h +97 h +1 h +83 h +4 h +10 h +15348 m +125 h +399 h +4 h +104 h +1 h +1 h +196 h +57 h +10 h +1 h +15349 m +4 h +1 h +1 h +10 h +59 h +1 h +195 h +11 h +15350 m +4 h +65 h +1840 m +15351 m +15352 m +124 h +10 h +104 h +112 h +4 h +15353 m +4 h +15354 m +4 h +5348 m +1 h +1 h +367 h +123 h +4 h +10 h +15355 m +1 h +36 h +11 h +11 h +15356 m +10 h +15357 m +15358 m +15359 m +1 h +1250 h +250 h +15360 m +4 h +48 h +4 h +630 m +11 h +1 h +65 h +1 h +147 h +10 h +4 h +143 h +1 h +6133 m +15361 m +15362 m +1 h +10 h +12 h +1 h +15363 m +4 h +4 h +25 h +5017 m +4 h +4 h +15364 m +1 h +15365 m +92 h +10 h +15366 m +36 h +103 h +4 h +15367 m +4 h +4 h +10 h +4 h +92 h +7 m +358 h +15368 m +4 h +642 h +13099 m +15369 m +1 h +83 h +15370 m +1322 m +15371 m +124 h +4 h +69 h +4 h +1 h +10 h +15372 m +4 h +10 h +1 h +4 h +15373 m +443 h +4441 h +15374 m +279 h +1764 m +10 h +10 h +124 h +15375 m +92 h +97 h +1 h +124 h +4 h +4 h +4 h +10 h +1685 h +1 h +687 h +90 m +4 h +15376 m +4 h +10 h +15377 m +10 h +15378 m +3 h +109 h +25 h +4 h +1 h +139 h +4 h +181 h +11 h +15379 m +4 h +4 h +10 h +258 h +1759 m +4 h +4 h +15380 m +4 h +15381 m +1 h +31 h +15382 m +229 h +10 h +1 h +146 h +11 h +57 h +1 h +4542 m +125 h +15383 m +258 h +4 h +82 h +1 h +10 h +15384 m +109 h +114 h +10 h +10 h +1 h +1 h +15385 m +4 h +41 h +169 h +4 h +124 h +83 h +4 h +25 h +15386 m +15387 m +4 h +935 h +10 h +7381 m +15388 m +10 h +15389 m +125 h +158 h +4 h +1470 h +125 h +10 h +11427 m +538 h +15390 m +4 h +10 h +11381 m +1 h +1 h +146 h +15391 m +4 h +1 h +4 h +157 h +2535 m +1 h +10 h +4 h +4 h +4 h +4 h +12 h +14473 m +704 m +3 h +12 h +966 m +15392 m +13 h +1 h +332 h +15393 m +4 h +15394 m +41 h +1 h +59 h +1 h +15395 m +10564 m +10 h +4 h +4 h +3028 m +11 h +4 h +4 h +2733 h +15396 m +1 h +1074 m +5047 m +124 h +10 h +1 h +104 h +4 h +4 h +10 h +15397 m +4 h +15398 m +1 h +41 h +129 h +4 h +4 h +478 h +1 h +92 h +1 h +5911 m +73 h +4 h +15399 m +4 h +10177 m +4 h +4 h +229 h +74 h +11 h +31 h +1 h +1 h +4 h +1 h +1 h +1 h +757 h +1 h +15400 m +1 h +172 h +15401 m +4 h +1 h +14529 m +15402 m +10 h +4 h +1 h +4 h +15403 m +4 h +4513 m +15404 m +4 h +4 h +10 h +4 h +4 h +1 h +4 h +15405 m +367 h +1137 h +1 h +4 h +6882 m +11 h +1 h +10 h +1 h +83 h +15406 m +383 h +4 h +4 h +250 h +109 h +15407 m +181 h +4 h +1 h +15408 m +4 h +1 h +319 h +138 m +1 h +10 h +10 h +125 h +4 h +1 h +4 h +15409 m +25 h +4 h +332 h +4 h +11778 m +1 h +4 h +15410 m +15411 m +1 h +1 h +4 h +4464 m +15412 m +4 h +4 h +1914 m +11 h +4 h +4 h +4 h +4 h +10 h +1 h +4 h +22 h +83 h +10 h +56 h +1 h +805 m +238 h +15413 m +911 h +4 h +10 h +15414 m +4 h +36 h +1 h +1 h +1 h +976 h +11 h +10 h +57 h +125 h +4 h +1 h +15415 m +15416 m +114 h +4 h +10532 m +1454 h +15417 m +65 h +125 h +15418 m +4 h +1 h +48 h +65 h +97 h +4 h +15419 m +4 h +56 h +97 h +1 h +3657 m +4 h +368 h +4 h +82 h +4 h +10 h +11 h +93 h +4 h +4 h +15420 m +4 h +15421 m +72 m +1 h +1 h +229 h +1 h +569 h +1 h +4 h +15422 m +4815 m +109 h +3 h +1 h +10 h +4 h +4 h +1 h +11 h +4 h +15423 m +15424 m +11 h +4 h +10028 m +104 h +4 h +94 h +4 h +4 h +999 m +73 h +82 h +1 h +59 h +307 h +885 h +238 h +15425 m +4 h +10 h +3 h +4 h +11 h +135 h +4 h +4 h +124 h +31 h +508 m +4 h +82 h +1 h +65 h +82 h +11 h +4 h +1 h +1 h +4 h +15426 m +5377 m +83 h +59 h +15427 m +1 h +186 h +10 h +2002 m +97 h +1 h +15428 m +195 h +4 h +196 h +15429 m +4 h +4 h +1 h +156 h +11 h +4 h +4 h +1403 h +1 h +3 h +4 h +156 h +1 h +10 h +11 h +1 h +4 h +4 h +1 h +4 h +57 h +4 h +31 h +4 h +1838 m +1454 h +4 h +164 h +4 h +4 h +1 h +1 h +25 h +4 h +196 h +10 h +15430 m +15431 m +82 h +4 h +403 h +1 h +4 h +15432 m +4 h +11 h +10 h +1 h +4 h +164 h +114 h +10 h +4 h +229 h +1 h +4 h +4 h +11 h +135 h +1 h +135 h +1 h +15433 m +15434 m +185 h +15435 m +10 h +15436 m +4 h +15437 m +10 h +12873 m +1 h +124 h +15438 m +4 h +4 h +10 h +10 h +10 h +966 m +1 h +1 h +1835 m +3 h +4 h +1893 m +4 h +124 h +125 h +4 h +10 h +57 h +4 h +4 h +15026 m +3 h +10 h +144 h +15439 m +4 h +15440 m +1 h +289 h +4 h +1 h +4 h +10 h +15441 m +4 h +10 h +1 h +11 h +10 h +1 h +4 h +9692 m +11 h +15442 m +353 h +109 h +10 h +1 h +65 h +195 h +4 h +1 h +10 h +4 h +10 h +4 h +4 h +4 h +1 h +1 h +147 h +538 h +4 h +1 h +15443 m +260 m +15444 m +4 h +1 h +15445 m +124 h +1 h +59 h +1 h +10 h +4 h +1 h +10 h +4 h +4 h +15446 m +104 h +1 h +601 h +1 h +4 h +10 h +4 h +169 h +15447 m +6528 m +1278 m +104 h +15448 m +82 h +59 h +15449 m +386 h +1 h +4 h +10 h +1 h +1 h +4 h +10 h +59 h +10 h +4 h +4 h +1 h +1 h +190 h +185 h +10 h +1 h +1 h +143 h +4 h +1 h +1 h +1 h +1074 m +4 h +1 h +11 h +15450 m +15451 m +77 h +1 h +1993 m +4 h +15452 m +82 h +1403 h +45 h +3 h +109 h +10 h +1 h +1989 m +4 h +146 h +169 h +278 h +4 h +104 h +4 h +533 h +1 h +118 h +4 h +368 h +10 h +1 h +143 h +10 h +15453 m +125 h +59 h +469 m +4 h +10 h +1 h +10 h +83 h +15454 m +92 h +57 h +3 h +1198 m +15455 m +22 h +1269 m +4 h +10 h +82 h +59 h +15456 m +4 h +3453 m +278 h +3 h +1 h +109 h +8206 m +113 h +10 h +65 h +12576 m +1 h +15457 m +73 h +15458 m +10 h +3 h +15459 m +4 h +4 h +1 h +4 h +82 h +1 h +10 h +307 h +15460 m +15461 m +10 h +1 h +4 h +10 h +4 h +4 h +4 h +4 h +4 h +4 h +10 h +630 m +4 h +10 h +1382 m +15462 m +4 h +4 h +1 h +10 h +10 h +59 h +1 h +1304 m +4 h +10 h +41 h +15463 m +1 h +2840 m +533 h +4 h +15464 m +1 h +15465 m +4 h +4 h +59 h +31 h +250 h +1 h +1 h +119 h +4 h +4 h +10 h +15466 m +1 h +146 h +25 h +10 h +124 h +4 h +1 h +15467 m +195 h +15468 m +10 h +10 h +10 h +1 h +79 h +10 h +1 h +1 h +4 h +11 h +1 h +156 h +65 h +10 h +15469 m +1595 m +10 h +1 h +1 h +15470 m +10 h +2314 m +1 h +167 h +4 h +1 h +1772 m +5475 m +10 h +15471 m +15472 m +15473 m +10 h +1 h +4 h +25 h +10 h +4 h +11 h +4 h +4 h +4 h +15474 m +31 h +575 h +12 h +4 h +1 h +15475 m +57 h +59 h +4 h +15476 m +4 h +3 h +11 h +12 h +1 h +1 h +113 h +15477 m +4 h +1 h +1 h +10 h +15478 m +181 h +82 h +185 h +15479 m +10 h +15480 m +15481 m +11 h +15482 m +94 h +4 h +538 h +15483 m +4 h +156 h +15484 m +10 h +10 h +1308 m +15485 m +15486 m +986 h +1 h +10 h +7938 m +1 h +1 h +1 h +15487 m +12 h +15488 m +15489 m +124 h +10 h +1 h +15490 m +1 h +15491 m +4 h +190 h +10 h +65 h +4 h +1 h +15492 m +4608 m +10 h +45 h +57 h +1 h +10 h +4 h +15493 m +12 h +3398 m +4 h +10 h +1 h +109 h +6545 m +195 h +31 h +1 h +59 h +1564 m +1 h +1 h +1 h +15494 m +109 h +1 h +10 h +1 h +2041 m +1 h +4 h +10 h +276 h +1 h +4 h +15495 m +15496 m +10 h +1 h +10 h +1 h +4 h +1 h +10 h +1 h +10 h +1 h +15497 m +15498 m +4 h +65 h +4 h +1 h +82 h +4 h +15499 m +15500 m +10 h +1 h +11 h +135 h +15501 m +57 h +195 h +15502 m +4 h +1 h +1 h +10 h +2556 m +4 h +10 h +11128 m +4 h +15503 m +25 h +123 h +4 h +4 h +4 h +15504 m +265 h +195 h +1 h +15505 m +10 h +10 h +1 h +10 h +4 h +118 h +1 h +4 h +15506 m +57 h +73 h +108 h +15507 m +1 h +1 h +1 h +3 h +1 h +57 h +10 h +4 h +11 h +15508 m +4 h +15509 m +4 h +4 h +97 h +1 h +15510 m +109 h +15511 m +27 h +10 h +1 h +1 h +4 h +1 h +4 h +4 h +65 h +59 h +15512 m +4 h +124 h +479 m +4 h +11 h +1 h +661 m +1 h +4 h +15513 m +15514 m +8 h +5407 m +4 h +190 h +12 h +10 h +1 h +91 h +74 h +110 h +4 h +4 h +79 h +4 h +15515 m +1 h +10 h +11 h +1 h +15516 m +185 h +368 h +4 h +4 h +15517 m +15518 m +4 h +1 h +124 h +83 h +3322 m +15519 m +157 h +1 h +299 h +15520 m +4 h +4 h +250 h +57 h +13 h +190 h +4 h +15521 m +119 h +109 h +4 h +4 h +15522 m +4 h +4 h +1 h +4 h +4 h +11 h +4 h +15523 m +1 h +10 h +10 h +10 h +4 h +83 h +4 h +10 h +10 h +15524 m +10 h +97 h +4 h +15525 m +8 h +190 h +1 h +41 h +94 h +13326 m +996 m +1 h +94 h +123 h +1 h +1 h +4 h +59 h +1 h +15526 m +1 h +3 h +911 h +10 h +36 h +447 h +4 h +15527 m +15528 m +6583 m +74 h +15529 m +15530 m +4 h +1 h +65 h +10 h +3 h +5483 m +1 h +358 h +4 h +330 h +15531 m +10 h +15532 m +15533 m +123 h +10 h +4 h +4 h +82 h +108 h +278 h +520 m +15534 m +4 h +1 h +8 h +10 h +15535 m +297 h +15536 m +97 h +25 h +770 m +15537 m +4 h +15538 m +82 h +1 h +10 h +1 h +4 h +1 h +55 h +15539 m +274 h +4 h +65 h +4 h +4 h +10 h +15540 m +1 h +10 h +10 h +15541 m +109 h +10 h +4 h +4 h +15542 m +1 h +1 h +1 h +4 h +1 h +10 h +4 h +1 h +73 h +4 h +59 h +55 h +1 h +1 h +15543 m +1 h +1 h +82 h +1 h +4 h +4 h +4 h +1 h +4 h +4 h +15544 m +4 h +10 h +10 h +10 h +15545 m +4849 m +190 h +4 h +15546 m +4 h +520 h +1 h +1 h +15547 m +4 h +1 h +83 h +4896 m +4 h +15548 m +4 h +57 h +125 h +15549 m +15550 m +1 h +15551 m +10 h +15552 m +1 h +4 h +1030 h +4 h +1737 m +74 h +109 h +1 h +15553 m +10 h +1 h +1 h +1 h +4 h +97 h +15554 m +1 h +4 h +1 h +1 h +59 h +1 h +15555 m +1 h +195 h +15556 m +10 h +195 h +11 h +114 h +1 h +82 h +3 h +1 h +15557 m +4 h +140 h +4 h +125 h +4 h +1678 m +8882 m +368 h +386 h +10 h +15558 m +15559 m +4 h +15560 m +4 h +4 h +15561 m +10 h +10 h +104 h +11 h +15562 m +10 h +4 h +4 h +2308 h +104 h +1 h +15563 m +1 h +79 h +10 h +763 m +4 h +4 h +15564 m +10 h +59 h +15565 m +173 h +4 h +15566 m +158 h +1271 m +4 h +57 h +536 h +4 h +4 h +11 h +15567 m +4 h +170 h +1 h +1 h +6158 m +4 h +1 h +1 h +4 h +1 h +196 h +104 h +82 h +266 h +15568 m +15569 m +15570 m +147 h +15571 m +11 h +10 h +1 h +15572 m +1 h +10 h +15573 m +184 h +258 h +1 h +15574 m +15575 m +4 h +10 h +57 h +1 h +15576 m +10 h +489 m +10 h +1 h +4 h +186 h +15577 m +4 h +1 h +12 h +146 h +15578 m +871 m +757 h +4 h +15579 m +4 h +15580 m +4 h +4 h +15581 m +4 h +15582 m +1 h +10 h +15583 m +4 h +31 h +8767 m +10 h +10 h +13 h +4 h +4 h +15584 m +10 h +15585 m +124 h +82 h +4 h +4 h +4 h +1 h +15586 m +1260 m +10 h +15587 m +110 h +4 h +65 h +12489 m +56 h +10 h +1 h +1 h +57 h +15588 m +10 h +3 h +4 h +4 h +687 h +15589 m +1 h +10 h +15590 m +7839 m +4 h +65 h +1 h +3 h +125 h +10 h +74 h +15591 m +15592 m +15593 m +779 h +15594 m +4 h +10 h +74 h +10 h +808 m +4 h +1308 m +2625 m +1 h +22 h +25 h +1 h +965 h +59 h +15595 m +74 h +15596 m +4 h +10 h +10 h +1 h +11 h +4 h +143 h +1 h +15597 m +5330 m +1 h +125 h +10 h +15598 m +15599 m +1 h +4 h +11 h +1 h +1 h +3 h +125 h +10 h +15600 m +15601 m +3 h +83 h +15602 m +1 h +22 h +167 h +15603 m +687 h +4 h +1 h +55 h +15604 m +1 h +13 h +2931 m +4 h +186 h +1 h +818 m +4 h +1 h +15605 m +1 h +1 h +15606 m +15607 m +4 h +181 h +1685 h +1 h +479 m +1 h +1 h +3 h +15608 m +4 h +11 h +4 h +10 h +15377 m +295 h +4 h +11 h +4 h +353 h +15609 m +4 h +10 h +1 h +10 h +4 h +1027 h +10 h +1 h +10 h +1 h +196 h +3558 m +4 h +10 h +123 h +15610 m +5225 m +109 h +4 h +2591 m +15611 m +82 h +91 h +4 h +1 h +250 h +1478 m +13 h +6941 m +1 h +723 m +70 m +4 h +15612 m +1 h +4 h +4 h +1 h +15613 m +77 h +116 m +4 h +4 h +104 h +10 h +73 h +10 h +10 h +15614 m +10 h +4 h +616 m +6129 m +4 h +10 h +59 h +55 h +82 h +4229 m +11 h +4 h +4 h +10 h +1 h +10 h +10 h +57 h +109 h +15615 m +4 h +1 h +3 h +4 h +258 h +104 h +1220 m +4 h +15616 m +15617 m +65 h +108 h +1 h +1 h +4 h +15618 m +10 h +1 h +1 h +172 h +4 h +10 h +82 h +276 h +10 h +1 h +4 h +10 h +15619 m +1 h +4 h +15620 m +139 h +266 h +109 h +1 h +1 h +92 h +1 h +106 m +15621 m +15622 m +10 h +1780 h +2851 m +45 h +146 h +4 h +1 h +147 h +1 h +1 h +15623 m +125 h +1016 h +4 h +1 h +15624 m +4 h +4 h +64 h +97 h +22 h +1 h +64 h +15625 m +15626 m +10 h +1 h +4 h +15627 m +15628 m +4 h +59 h +4 h +10 h +4 h +11 h +1 h +1796 h +15629 m +1981 m +386 h +15630 m +114 h +15631 m +4 h +9482 m +4 h +15632 m +279 h +1 h +10 h +1 h +448 m +4 h +1 h +36 h +104 h +15633 m +4 h +119 h +41 h +10 h +4 h +1 h +4 h +1 h +4 h +10 h +123 h +4 h +4 h +10 h +10 h +15634 m +4 h +10 h +307 h +10 h +15635 m +1642 h +4 h +1 h +583 m +11 h +15636 m +15637 m +77 h +15638 m +4 h +4 h +55 h +15639 m +1 h +1 h +332 h +15640 m +15641 m +10 h +4 h +25 h +874 m +15642 m +4 h +10 h +15643 m +15644 m +10 h +83 h +10 h +15645 m +4 h +11 h +10 h +10 h +15646 m +4 h +3177 m +1 h +1642 h +10 h +12 h +1444 m +27 h +15647 m +1 h +4 h +4 h +119 h +4 h +10 h +10 h +4 h +4 h +15648 m +15649 m +332 h +124 h +1 h +10 h +224 m +1 h +1 h +208 m +1 h +167 h +15650 m +97 h +1 h +4 h +1 h +4 h +4 h +4 h +4 h +15651 m +9293 m +15652 m +1815 m +4 h +10 h +295 h +4 h +10 h +15653 m +15654 m +1 h +4 h +1 h +626 m +15655 m +10 h +15656 m +15657 m +15658 m +10 h +65 h +169 h +195 h +10 h +4 h +15659 m +45 h +15660 m +4 h +4 h +15661 m +15662 m +10640 m +10 h +4 h +4 h +45 h +1 h +74 h +15663 m +185 h +1822 h +73 h +4 h +10 h +4 h +124 h +1 h +15664 m +1 h +307 h +2172 m +4 h +4 h +538 h +4 h +4 h +2474 m +1 h +15665 m +10 h +1 h +109 h +5809 m +1 h +15666 m +4 h +10 h +25 h +1 h +109 h +167 h +119 h +4 h +11869 m +1 h +4 h +15667 m +15668 m +15669 m +285 m +124 h +1 h +4 h +124 h +94 h +4 h +3 h +1 h +10 h +4 h +4 h +10 h +1 h +15670 m +4 h +4 h +10 h +15671 m +4 h +4 h +4 h +897 m +1 h +10 h +11 h +31 h +10 h +1 h +15672 m +4 h +15673 m +4 h +10 h +587 m +3 h +82 h +10 h +59 h +4 h +4 h +11 h +1 h +10 h +74 h +1 h +15674 m +10 h +1 h +41 h +10 h +15675 m +4 h +15676 m +31 h +4 h +10 h +1 h +224 m +15677 m +238 h +15678 m +10 h +119 h +2438 m +125 h +15679 m +538 h +15680 m +4 h +15681 m +15682 m +22 h +13140 m +1 h +15683 m +1 h +185 h +1 h +11 h +74 h +1 h +11 h +1 h +15684 m +1 h +1 h +1 h +10 h +1 h +15685 m +1 h +1 h +4 h +109 h +82 h +4 h +1 h +10 h +4 h +1030 h +15686 m +2126 m +4 h +15687 m +4 h +10 h +15688 m +56 h +1 h +4 h +36 h +1 h +15689 m +4 h +15690 m +4 h +10 h +10 h +97 h +1 h +1478 m +4 h +10 h +4 h +3 h +15691 m +10 h +4 h +1 h +10 h +97 h +1 h +1 h +15692 m +36 h +4 h +4 h +1 h +27 h +10 h +10 h +10 h +15693 m +1 h +147 h +73 h +10 h +74 h +13 h +15694 m +169 h +4 h +15695 m +297 h +1 h +4 h +4 h +8734 m +1 h +11 h +59 h +10 h +1 h +10 h +1 h +4 h +10 h +4 h +15696 m +258 h +114 h +3562 m +1 h +1 h +10 h +4 h +1948 m +258 h +4 h +3 h +10 h +1 h +15697 m +83 h +10 h +386 h +4 h +10 h +140 h +15698 m +4 h +371 h +8383 m +1 h +4 h +1220 m +359 h +10 h +12372 m +11 h +1038 m +94 h +3732 m +41 h +4 h +15699 m +10 h +3 h +15700 m +10 h +2309 m +15701 m +1 h +1 h +57 h +5863 m +2965 m +25 h +1 h +15702 m +1 h +4 h +4 h +1 h +4 h +1 h +4 h +11779 m +10 h +4 h +119 h +92 h +4 h +15703 m +27 h +15704 m +4524 m +1454 h +15705 m +1 h +4 h +15706 m +15707 m +258 h +4 h +4 h +1 h +15708 m +4 h +109 h +1083 m +229 h +912 m +4 h +4 h +4 h +1955 m +15709 m +1 h +1 h +2851 m +15710 m +10 h +258 h +45 h +4359 m +10 h +4 h +4 h +4 h +15711 m +4 h +2720 m +10 h +278 h +15712 m +1 h +4 h +1 h +4 h +4 h +219 m +1 h +4 h +57 h +15713 m +109 h +4 h +15714 m +82 h +4 h +11 h +31 h +9940 m +10 h +4 h +4 h +4 h +11 h +15715 m +109 h +15716 m +1 h +15717 m +10 h +4 h +4 h +139 h +1 h +4 h +147 h +15718 m +55 h +4 h +10 h +1 h +3 h +3845 m +15719 m +4 h +28 h +15720 m +1 h +22 h +94 h +1 h +4 h +10 h +4 h +4 h +59 h +4 h +11 h +4 h +4 h +1 h +4 h +4 h +1 h +124 h +1 h +4 h +3089 m +435 h +278 h +15721 m +4 h +4 h +1 h +15722 m +15723 m +11 h +976 h +15724 m +1 h +15725 m +3748 m +10 h +11 h +57 h +10 h +1 h +15726 m +1 h +1137 h +1 h +11 h +265 h +15727 m +1722 m +1 h +10 h +172 h +10 h +3 h +10 h +241 m +11 h +4 h +167 h +1 h +123 h +7215 m +4 h +238 h +8 h +15728 m +4 h +4 h +1 h +3 h +4 h +4 h +238 h +1 h +10 h +15729 m +97 h +195 h +83 h +10 h +4 h +4 h +143 h +15730 m +65 h +4 h +278 h +4 h +10 h +10 h +4 h +4 h +3 h +5933 m +4 h +57 h +5 h +4 h +4 h +15731 m +1 h +1 h +238 h +278 h +15732 m +4 h +4 h +11 h +4 h +1 h +82 h +10 h +10 h +158 h +4 h +10 h +4 h +15733 m +25 h +4 h +156 h +15734 m +15735 m +59 h +15736 m +1 h +1492 m +15737 m +1 h +10 h +4 h +4 h +10 h +1 h +1 h +4 h +11 h +4 h +4 h +4 h +15738 m +447 h +15739 m +4 h +114 h +4 h +25 h +15740 m +2494 m +6001 m +1 h +31 h +15741 m +15742 m +4 h +1 h +15743 m +2022 m +4 h +192 h +15744 m +10 h +15745 m +119 h +4 h +4 h +4 h +4 h +1 h +1 h +4 h +4 h +55 h +1 h +55 h +2002 m +15746 m +1 h +4 h +1714 h +4 h +10 h +195 h +1 h +4 h +1 h +1 h +1914 h +4 h +10 h +25 h +57 h +4 h +4 h +36 h +4 h +15747 m +4 h +15748 m +640 h +2183 m +82 h +64 h +4 h +1 h +10 h +11 h +279 h +536 h +10 h +371 h +1 h +11 h +64 h +538 h +3702 m +1 h +1 h +10 h +10 h +1 h +15749 m +270 h +79 h +10 h +10 h +1 h +10 h +4 h +164 h +64 h +64 h +124 h +14050 m +1 h +1 h +520 h +1 h +172 h +10 h +4 h +4 h +15750 m +1780 h +4 h +10 h +15751 m +2116 m +10 h +4 h +10 h +15752 m +15753 m +15754 m +687 h +1 h +15755 m +358 h +15756 m +4714 m +10 h +1 h +4 h +4 h +1 h +1 h +15757 m +10 h +1 h +12 h +5296 m +12005 m +15758 m +83 h +4 h +15759 m +4 h +11 h +4542 m +156 h +757 h +4 h +1 h +10958 m +15760 m +4 h +10 h +4 h +13392 m +10 h +167 h +15761 m +656 m +15762 m +4 h +1948 m +1 h +10 h +4 h +45 h +36 h +172 h +1 h +15763 m +4 h +1 h +4 h +15764 m +57 h +4 h +4 h +55 h +146 h +10 h +36 h +258 h +4 h +109 h +2459 m +15765 m +4 h +15766 m +4 h +1 h +15767 m +15768 m +1 h +4 h +1 h +82 h +4 h +10 h +266 h +4 h +4 h +15769 m +10 h +10 h +15770 m +4 h +15771 m +10 h +31 h +10 h +4 h +135 h +10 h +31 h +1 h +1 h +4 h +4 h +10 h +1 h +1 h +383 h +10 h +1 h +1062 m +4 h +1 h +1677 m +15772 m +15773 m +4 h +146 h +125 h +4 h +10 h +195 h +4 h +4 h +15774 m +15775 m +109 h +1 h +15776 m +15777 m +73 h +156 h +4 h +10 h +25 h +15778 m +3177 m +11 h +10 h +10 h +1 h +10 h +4 h +15779 m +1198 m +1 h +10 h +1 h +172 h +1 h +146 h +15780 m +27 h +4 h +1201 m +1 h +11 h +4 h +15781 m +939 m +125 h +112 h +10 h +25 h +15782 m +4 h +1 h +170 h +10 h +4 h +10 h +4 h +3 h +181 h +10 h +4 h +4308 m +278 h +1 h +15783 m +355 m +109 h +15784 m +59 h +1 h +1 h +10 h +15785 m +1 h +278 h +11 h +1 h +1003 m +4 h +10 h +110 h +4 h +15786 m +4 h +4 h +83 h +10 h +4 h +10 h +1 h +15787 m +1 h +10 h +4 h +591 m +15788 m +138 m +15789 m +4 h +11 h +15790 m +79 h +15791 m +15792 m +6124 m +1 h +4 h +59 h +10 h +4 h +3 h +13 h +10 h +57 h +15793 m +15794 m +1137 h +109 h +15795 m +4 h +1 h +3143 m +1 h +10 h +135 h +82 h +224 h +15796 m +10 h +190 h +65 h +11 h +15797 m +15798 m +10 h +15799 m +10 h +15800 m +1 h +169 h +10 h +41 h +170 h +221 m +15801 m +338 h +10 h +15802 m +1 h +15803 m +56 h +1 h +250 h +4 h +82 h +4 h +97 h +4 h +10 h +11 h +11 h +4 h +15804 m +10 h +1 h +82 h +83 h +1 h +15805 m +4 h +10 h +15806 m +4 h +10 h +4 h +10 h +15807 m +4 h +1 h +10 h +1 h +15808 m +4 h +124 h +4 h +4 h +1 h +4 h +4 h +966 h +1 h +10 h +4 h +11 h +1 h +1 h +31 h +12 h +2148 m +79 h +4 h +59 h +4 h +143 h +1 h +1 h +15809 m +10 h +10 h +15810 m +59 h +10 h +15811 m +1 h +1 h +15812 m +25 h +15813 m +124 h +4 h +1 h +1 h +4 h +3847 m +10 h +10 h +15814 m +3 h +4 h +59 h +104 h +1 h +11 h +15815 m +1 h +347 m +4 h +4 h +1 h +27 h +15816 m +169 h +156 h +10 h +4 h +83 h +1 h +1 h +36 h +10 h +1 h +10 h +124 h +1027 h +15817 m +15818 m +143 h +4 h +2887 m +1 h +1 h +15819 m +1 h +15820 m +1 h +124 h +114 h +1 h +297 h +4 h +6726 m +4 h +3398 m +307 h +15821 m +12 h +1 h +10 h +1 h +4 h +15822 m +4 h +138 m +2096 m +15823 m +4 h +1 h +4 h +10 h +4 h +359 h +3539 m +15824 m +2002 m +4 h +1 h +4 h +59 h +123 h +10 h +1 h +10 h +10 h +7135 m +4 h +15825 m +4 h +1 h +10 h +1 h +10 h +11 h +1 h +15826 m +1 h +1 h +15827 m +13 h +2694 m +6422 m +113 h +15828 m +464 h +1 h +15829 m +83 h +1 h +1 h +4 h +11 h +10 h +15830 m +1122 m +59 h +1 h +13468 m +976 h +15831 m +1 h +15832 m +10 h +1 h +10 h +1 h +4 h +12 h +4 h +15833 m +10 h +4 h +4 h +36 h +65 h +169 h +195 h +1 h +1137 h +11 h +1 h +1359 h +77 h +677 m +15834 m +4 h +15835 m +15836 m +4 h +238 h +2308 h +1 h +27 h +10 h +15837 m +3 h +1 h +3 h +10 h +1 h +4 h +4 h +4 h +15838 m +14345 m +15839 m +109 h +1 h +11 h +4 h +15840 m +12301 m +169 h +1 h +15 m +1117 m +15841 m +15842 m +15843 m +10 h +10 h +15844 m +1 h +397 m +10089 m +4 h +434 m +110 h +13886 m +4 h +4 h +241 m +10 h +1 h +1 h +106 m +10 h +9536 m +1822 h +1 h +4 h +684 m +10 h +5 h +1650 h +1 h +15845 m +15846 m +1 h +15847 m +109 h +15848 m +1 h +109 h +3 h +15849 m +767 m +4 h +1 h +82 h +4 h +1 h +338 h +1 h +10 h +114 h +4 h +25 h +113 h +15850 m +228 m +31 h +4 h +2920 m +386 h +10 h +15851 m +4 h +57 h +15852 m +11 h +1137 h +57 h +73 h +64 h +1 h +4 h +1981 m +15853 m +4 h +4 h +15854 m +15855 m +10 h +15856 m +92 h +10 h +109 h +4 h +2379 h +569 h +10 h +11 h +258 h +41 h +15857 m +4576 m +114 h +1 h +10 h +4 h +10 h +146 h +4 h +27 h +1 h +10 h +10 h +15858 m +4 h +55 h +3 h +4 h +10 h +83 h +10 h +1 h +267 m +55 h +4 h +10 h +4 h +536 h +73 h +12 h +82 h +15859 m +140 h +1 h +4 h +1 h +48 h +4 h +124 h +10 h +4 h +4 h +1 h +1 h +113 h +25 h +15860 m +97 h +15861 m +1 h +1 h +15862 m +4 h +10 h +10 h +36 h +10 h +1 h +74 h +1 h +386 h +1 h +15863 m +4 h +1 h +3025 m +10 h +1 h +1953 h +1 h +31 h +196 h +15864 m +718 h +8809 m +10 h +5863 m +45 h +31 h +4 h +5689 m +1 h +15865 m +10 h +25 h +10 h +15866 m +83 h +15867 m +4 h +31 h +4 h +11 h +10 h +4 h +15868 m +106 m +4 h +97 h +4 h +1 h +718 h +15869 m +386 h +15870 m +15871 m +10 h +4 h +10 h +1 h +57 h +1 h +110 h +1 h +10 h +4 h +447 h +4 h +119 h +15872 m +11 h +15873 m +4 h +4 h +4 h +1 h +367 h +15874 m +15875 m +185 h +1 h +1 h +10 h +15876 m +15877 m +31 h +45 h +4 h +4 h +15878 m +4 h +11 h +110 h +4 h +1 h +4 h +15879 m +36 h +270 h +4 h +25 h +4 h +15880 m +1 h +11 h +15881 m +15882 m +4830 m +114 h +279 h +3707 m +1 h +15883 m +10 h +1650 h +4 h +92 h +82 h +4 h +4 h +10 h +92 h +10 h +4 h +1 h +1 h +15884 m +332 h +4905 h +1 h +15885 m +1 h +1 h +10 h +10 h +15886 m +15887 m +15888 m +119 h +10 h +1 h +4 h +4 h +10 h +56 h +10 h +1 h +15889 m +1 h +56 h +15890 m +1006 m +4 h +10 h +4 h +538 h +4 h +109 h +1 h +1 h +1 h +4 h +15891 m +104 h +10 h +1 h +10 h +15892 m +10 h +73 h +4 h +1 h +15893 m +57 h +1 h +15894 m +10 h +15895 m +15896 m +1 h +954 m +238 h +1 h +135 h +1 h +1666 m +1 h +15897 m +443 h +4 h +4 h +15898 m +82 h +10 h +1 h +1 h +4 h +12363 m +1714 h +15899 m +1 h +4 h +15900 m +15901 m +509 m +10464 m +15902 m +15903 m +15904 m +4 h +146 h +4 h +276 h +15905 m +15906 m +10 h +1 h +1 h +10 h +11 h +164 h +4 h +15907 m +536 h +4 h +15908 m +15909 m +4 h +10 h +4 h +12 h +1 h +1 h +25 h +4 h +4 h +4 h +10 h +4 h +27 h +73 h +36 h +4 h +4 h +1 h +3704 m +15910 m +4 h +15911 m +3 h +12 h +11 h +368 h +4 h +15912 m +11 h +15913 m +1 h +2265 m +1 h +11 h +94 h +10 h +15914 m +3 h +15915 m +124 h +1 h +1 h +31 h +4 h +1 h +4 h +3 h +138 h +4 h +15916 m +4 h +65 h +1 h +4 h +2128 m +103 h +4 h +73 h +10 h +15917 m +4 h +447 h +1 h +4 h +4 h +10 h +4 h +13481 m +1 h +83 h +25 h +11 h +1 h +4 h +1 h +1016 h +1 h +15918 m +4 h +10 h +83 h +1 h +11 h +1374 m +1 h +56 h +74 h +114 h +4 h +4 h +4 h +15919 m +2754 m +10 h +114 h +15920 m +15921 m +15922 m +15923 m +185 h +4 h +172 h +7532 m +1027 h +1822 h +15924 m +1 h +1 h +10 h +15925 m +10 h +82 h +15926 m +4 h +195 h +4 h +1 h +1 h +4 h +1 h +464 h +1 h +4 h +1 h +4 h +3 h +10 h +1 h +1 h +15927 m +4 h +15928 m +7727 m +1 h +4 h +15929 m +4 h +15930 m +11 h +10 h +10 h +10 h +4 h +10 h +1 h +1 h +10 h +15931 m +15932 m +64 h +15933 m +1 h +4 h +92 h +307 h +10 h +4 h +10391 m +4 h +4 h +25 h +25 h +4 h +4 h +109 h +307 h +4 h +79 h +4 h +1 h +1 h +4 h +4 h +1 h +82 h +59 h +1 h +582 m +10 h +616 m +1 h +15934 m +4 h +1359 h +11 h +1321 m +73 h +110 h +11 h +15935 m +15936 m +4 h +1619 h +15937 m +1 h +22 h +4 h +15938 m +15939 m +11 h +1 h +10 h +1 h +13 h +92 h +3 h +3 h +10685 m +1 h +15940 m +10 h +10324 m +4 h +124 h +181 h +4 h +15941 m +843 m +1 h +15942 m +10 h +13662 m +4 h +15943 m +3 h +15944 m +266 h +15945 m +15946 m +15947 m +1 h +15948 m +146 h +10 h +4 h +15949 m +15950 m +10 h +10 h +10 h +1 h +112 h +1713 m +4 h +464 h +10 h +332 h +15951 m +359 h +15952 m +10 h +10 h +10 h +4 h +15953 m +147 h +15954 m +4 h +486 m +10 h +238 h +59 h +74 h +4 h +4 h +3068 m +10 h +57 h +15955 m +4229 m +10 h +4 h +10 h +10 h +4 h +10 h +14050 m +11353 m +10 h +1 h +1074 h +1 h +1 h +10 h +15956 m +4 h +15957 m +640 h +10 h +1 h +15958 m +1 h +1 h +15959 m +4 h +27 h +4 h +1 h +124 h +477 m +4 h +1083 m +1 h +4 h +15960 m +4 h +15961 m +1 h +109 h +15962 m +4 h +4 h +82 h +10 h +109 h +124 h +15963 m +15964 m +15965 m +41 h +11 h +125 h +888 m +4 h +4 h +1053 m +4 h +15966 m +4 h +31 h +15967 m +4 h +10 h +1 h +15968 m +736 m +15969 m +31 h +1 h +10 h +11 h +4 h +4 h +15970 m +4 h +15971 m +1 h +10 h +4 h +4 h +1340 m +3 h +190 h +583 m +4 h +15972 m +10 h +1 h +15973 m +4 h +83 h +15974 m +135 h +13 h +13544 m +15975 m +4 h +15976 m +1 h +4 h +15977 m +4 h +4 h +4 h +4 h +114 h +15978 m +82 h +1 h +15979 m +12526 m +15980 m +4030 m +1 h +2733 h +41 h +860 m +114 h +1250 h +15981 m +15982 m +4 h +15983 m +1089 h +4 h +110 h +15984 m +13 h +399 h +15985 m +15986 m +196 h +10 h +4 h +147 h +10 h +888 m +11 h +15987 m +5141 m +109 h +1 h +229 h +97 h +4 h +677 m +464 h +359 h +4 h +15988 m +4 h +15989 m +104 h +1 h +10 h +12 h +4 h +10 h +820 m +15990 m +5357 m +4 h +15263 m +12 h +3 h +1 h +1 h +94 h +4 h +1 h +11344 m +4 h +1 h +1780 h +15991 m +10 h +1 h +74 h +15992 m +10 h +4 h +4 h +238 h +1 h +4 h +1 h +4 h +82 h +15993 m +1 h +10 h +4 h +15994 m +4 h +12 h +4 h +25 h +10 h +12635 m +4 h +4 h +4 h +147 h +4 h +270 h +267 m +1 h +412 h +10 h +4 h +1 h +279 h +15995 m +65 h +12 h +15996 m +1 h +1 h +1 h +10 h +1 h +4 h +15997 m +25 h +8040 m +1 h +1 h +1 h +1 h +7702 m +10 h +808 m +4 h +1 h +4 h +15998 m +15999 m +10 h +92 h +12 h +195 h +11 h +16000 m +16001 m +92 h +718 h +1 h +16002 m +55 h +10 h +123 h +10 h +4 h +4 h +10 h +13 h +3112 m +16003 m +3 h +562 m +663 m +172 h +10 h +31 h +10 h +10 h +1 h +1 h +4 h +135 h +1 h +16004 m +16005 m +4 h +16006 m +16007 m +10 h +31 h +16008 m +1 h +16009 m +1 h +16010 m +10 h +1083 m +4 h +1 h +4 h +16011 m +4 h +4 h +264 h +10 h +16012 m +16013 m +4 h +4 h +2733 h +57 h +1 h +4 h +16014 m +16015 m +250 h +185 h +4 h +4 h +16016 m +185 h +1 h +1 h +1 h +339 m +10 h +307 h +10 h +10 h +55 h +4 h +12264 m +4 h +3 h +4 h +4 h +4 h +11 h +264 h +16017 m +36 h +5720 m +687 h +167 h +10 h +109 h +4 h +4 h +1 h +4 h +1 h +276 h +146 h +4 h +4 h +1 h +9182 m +10 h +11 h +10 h +5254 m +1 h +4 h +4 h +1 h +73 h +74 h +1 h +1 h +10 h +16018 m +10 h +65 h +258 h +4 h +4 h +10 h +61 m +1 h +1 h +10 h +110 h +16019 m +16020 m +4 h +4 h +10 h +4 h +10 h +27 h +536 h +10 h +16021 m +16022 m +16023 m +40 m +1 h +281 m +4 h +82 h +11621 m +1535 m +16024 m +82 h +16025 m +11 h +229 h +82 h +195 h +143 h +203 m +83 h +4 h +16026 m +11 h +11 h +1 h +16027 m +16028 m +1261 h +59 h +16029 m +16030 m +4 h +10 h +12 h +143 h +4 h +169 h +10 h +1 h +12 h +1 h +104 h +10 h +10 h +10 h +4 h +4 h +16031 m +1 h +59 h +57 h +4 h +16032 m +16033 m +195 h +224 h +41 h +443 h +16034 m +843 m +195 h +297 h +74 h +31 h +1 h +16035 m +10 h +4 h +10 h +471 m +10 h +1 h +10 h +181 h +74 h +1 h +10 h +4 h +12225 m +1 h +4 h +4 h +169 h +2148 m +11810 m +1 h +4 h +11 h +16036 m +4 h +16037 m +1 h +425 m +4 h +11 h +16038 m +16039 m +285 m +16040 m +278 h +16041 m +10 h +1 h +83 h +10 h +16042 m +4 h +16043 m +1 h +104 h +4 h +1 h +3 h +506 m +4 h +10 h +1 h +10 h +4 h +307 h +1 h +4 h +1 h +73 h +16044 m +104 h +169 h +22 h +1 h +1 h +1 h +1 h +79 h +16045 m +10 h +4 h +16046 m +10 h +59 h +16047 m +11 h +16048 m +3 h +4 h +10 h +16049 m +83 h +48 h +16050 m +4 h +10 h +1 h +4 h +1 h +1 h +57 h +82 h +1 h +59 h +16051 m +4 h +1 h +65 h +104 h +104 h +939 m +1 h +1 h +16052 m +1 h +16053 m +1 h +228 m +1 h +8070 m +412 h +16054 m +16055 m +10 h +55 h +1 h +16056 m +10 h +1 h +104 h +16057 m +1 h +443 h +3 h +1 h +4 h +16058 m +16059 m +92 h +16060 m +16061 m +74 h +4 h +4 h +238 h +11 h +2040 m +1 h +82 h +140 h +82 h +73 h +4 h +55 h +1 h +3170 m +10 h +4 h +10 h +578 h +4 h +16062 m +4 h +2308 h +986 h +1 h +4 h +1 h +10 h +1 h +4 h +12372 m +16063 m +4 h +10 h +4 h +16064 m +10 h +196 h +4 h +16065 m +1 h +4 h +4 h +1 h +1642 h +16066 m +4 h +16067 m +208 m +338 h +110 h +97 h +9757 m +16068 m +10 h +10 h +10 h +10 h +4 h +16069 m +16070 m +16071 m +16072 m +1 h +297 h +2851 h +114 h +4 h +16073 m +195 h +97 h +1 h +10 h +109 h +443 h +16074 m +16075 m +1 h +1 h +1 h +83 h +642 h +4 h +10 h +1 h +124 h +4 h +33 m +1 h +16076 m +4 h +4 h +692 h +1 h +59 h +2054 m +10 h +4 h +104 h +13980 m +10 h +806 m +4 h +4 h +16077 m +10 h +4 h +10 h +16078 m +16079 m +16080 m +147 h +4592 m +65 h +16081 m +11 h +16082 m +12 h +1 h +4 h +10 h +1 h +601 h +10 h +73 h +16083 m +1955 m +16084 m +5809 m +10 h +11 h +41 h +4 h +16085 m +1 h +4 h +57 h +2751 m +36 h +16086 m +3435 m +4 h +4 h +181 h +1 h +119 h +4 h +16087 m +4 h +4 h +104 h +143 h +157 h +5387 m +185 h +74 h +4 h +45 h +4 h +10 h +16088 m +464 h +1 h +2625 m +10 h +16089 m +1 h +16090 m +2840 m +10 h +10 h +41 h +4 h +1 h +4 h +1 h +4 h +4 h +16091 m +1 h +1 h +92 h +10 h +10 h +1 h +10 h +16092 m +4 h +4911 m +4 h +358 h +124 h +10 h +4 h +1 h +16093 m +16094 m +146 h +11 h +4 h +106 h +5 h +1 h +143 h +4 h +59 h +174 m +16095 m +1359 h +966 h +16096 m +4 h +156 h +139 h +146 h +4 h +1 h +4 h +4 h +10 h +10 h +3 h +124 h +4 h +82 h +184 h +16097 m +10 h +16098 m +804 m +4 h +1 h +6869 m +4 h +16099 m +59 h +4 h +1 h +2607 m +4 h +16100 m +11 h +4 h +16101 m +1 h +4 h +4 h +25 h +10 h +16102 m +82 h +4 h +16103 m +1 h +1 h +4 h +4 h +109 h +1 h +16104 m +10 h +16105 m +4 h +4 h +1 h +16106 m +28 h +16107 m +2025 m +1 h +10 h +1 h +229 h +13426 m +77 h +1 h +278 h +16108 m +172 h +4 h +10 h +307 h +125 h +4 h +16109 m +11 h +1 h +16110 m +4 h +4 h +4 h +16111 m +4 h +3 h +4 h +25 h +12 h +41 h +297 h +124 h +4 h +10 h +16112 m +16113 m +16114 m +4 h +1 h +16115 m +4 h +57 h +83 h +4 h +1 h +10 h +4 h +74 h +4 h +1 h +41 h +10 h +4 h +16116 m +1 h +1 h +1 h +10 h +10 h +4 h +297 h +459 h +196 h +601 h +4 h +157 h +16117 m +83 h +16118 m +1 h +97 h +4 h +16119 m +10177 m +4 h +181 h +11 h +16120 m +16121 m +4 h +11 h +4 h +10 h +4 h +59 h +41 h +4 h +16122 m +146 h +4 h +16123 m +10 h +4 h +16124 m +7401 m +172 h +109 h +1 h +16125 m +181 h +687 h +4 h +1 h +1 h +16126 m +82 h +4 h +258 h +4 h +4 h +16127 m +10 h +16128 m +147 h +4 h +164 h +1445 m +4 h +1 h +11948 m +10 h +4 h +272 m +124 h +1 h +57 h +59 h +1 h +3 h +403 h +16129 m +83 h +7839 m +4 h +16130 m +4 h +181 h +186 h +150 m +172 h +65 h +10 h +59 h +230 m +4 h +119 h +4 h +59 h +10 h +10 h +4 h +16131 m +16132 m +10 h +4 h +16133 m +1 h +27 h +10958 m +124 h +16134 m +16135 m +16136 m +10 h +82 h +4 h +1 h +1 h +737 m +1 h +3307 m +185 h +97 h +16137 m +57 h +16138 m +4 h +16139 m +10 h +41 h +4 h +124 h +1 h +386 h +203 m +601 h +3988 m +845 m +10 h +4 h +59 h +16140 m +16141 m +4 h +146 h +167 h +124 h +119 h +1 h +10 h +1470 h +4 h +4 h +4 h +156 h +10 h +16142 m +31 h +1 h +4 h +10 h +195 h +16143 m +82 h +966 h +4 h +16144 m +4 h +16145 m +4 h +536 h +1 h +10 h +10 h +16146 m +82 h +1 h +10 h +5348 m +1 h +4 h +693 m +11 h +16147 m +147 h +11 h +1 h +16148 m +10 h +4 h +229 h +10 h +4 h +10 h +147 h +16149 m +4 h +92 h +16150 m +10539 m +10 h +10 h +4 h +4 h +28 h +1 h +10 h +16151 m +10 h +4 h +1 h +1 h +10 h +1478 h +1 h +1 h +16152 m +4 h +4 h +10 h +4 h +55 h +16153 m +10 h +4 h +4 h +4 h +10 h +57 h +16154 m +857 h +4 h +383 h +27 h +16155 m +4 h +10 h +146 h +16156 m +1 h +16157 m +1 h +10 h +10 h +10 h +4 h +4 h +16158 m +124 h +82 h +125 h +97 h +4 h +1 h +4 h +11 h +57 h +16159 m +10 h +332 h +147 h +4 h +3048 m +10 h +185 h +4 h +57 h +10 h +4 h +538 h +1 h +704 h +4 h +276 h +184 h +10 h +4 h +4 h +57 h +4 h +351 m +55 h +4 h +1 h +16160 m +16161 m +16162 m +1 h +4 h +4 h +4 h +11 h +10 h +173 h +4 h +295 h +16163 m +4 h +4 h +16164 m +55 h +4 h +10 h +16165 m +16166 m +11940 m +1 h +11 h +4 h +4 h +12 h +3 h +4 h +1 h +16167 m +10 h +7444 m +97 h +109 h +1 h +10 h +146 h +4 h +4 h +10 h +4 h +1 h +16168 m +266 h +59 h +4 h +16169 m +386 h +1 h +4 h +16170 m +10 h +4 h +1 h +1 h +167 h +4 h +10 h +64 h +10 h +4 h +10414 m +1 h +10 h +73 h +4 h +1 h +190 h +4 h +185 h +156 h +16171 m +4 h +4 h +147 h +16172 m +125 h +4 h +4 h +4 h +143 h +109 h +16173 m +108 h +147 h +13157 m +124 h +1 h +1016 h +4 h +16174 m +3 h +4 h +358 h +16175 m +10 h +4 h +57 h +4 h +4 h +359 h +2923 h +16176 m +73 h +16177 m +1 h +10 h +16178 m +196 h +16179 m +1 h +16180 m +4 h +4 h +11 h +1 h +10 h +276 h +82 h +1 h +59 h +10 h +97 h +3533 m +10 h +10 h +1 h +16181 m +83 h +4 h +16182 m +4 h +4 h +16183 m +16184 m +16185 m +4 h +1201 m +3558 m +10 h +4 h +4 h +16186 m +16187 m +1 h +27 h +118 h +10 h +16188 m +59 h +10 h +16189 m +10 h +536 h +266 h +10 h +16190 m +1 h +16191 m +16192 m +10 h +1 h +4 h +16193 m +295 h +6469 m +16194 m +16195 m +10 h +1 h +109 h +274 h +16196 m +4 h +10 h +10 h +16197 m +4 h +1 h +16198 m +16199 m +4 h +16200 m +16201 m +1 h +4 h +4 h +1 h +4 h +10 h +278 h +4 h +97 h +16202 m +169 h +164 h +4 h +1 h +3 h +1 h +36 h +10 h +10 h +16203 m +11 h +4 h +1 h +82 h +4 h +4 h +4 h +1 h +83 h +4 h +10 h +16204 m +4 h +41 h +16205 m +4 h +1 h +2887 m +954 m +4 h +146 h +4 h +1 h +266 h +10 h +16206 m +4 h +10 h +1 h +1250 h +1470 h +4 h +10 h +109 h +1 h +4 h +1 h +1 h +1 h +16207 m +109 h +11 h +10 h +358 h +1 h +4 h +4 h +3 h +4 h +64 h +1 h +59 h +16208 m +10 h +124 h +1 h +1 h +4 h +1 h +10418 m +4 h +109 h +1 h +16209 m +1 h +10 h +140 h +74 h +4 h +1 h +4 h +8324 m +4 h +11 h +83 h +10 h +10 h +1 h +7541 m +9027 m +10 h +10 h +1 h +16210 m +4 h +1261 h +1 h +4 h +10 h +57 h +1 h +16211 m +16212 m +4 h +1 h +4 h +4 h +4 h +1 h +4 h +1 h +4 h +27 h +1 h +10 h +1 h +10 h +4 h +1 h +10 h +16213 m +10 h +16214 m +4 h +16215 m +1 h +79 h +16216 m +4 h +16217 m +11 h +4 h +10 h +1 h +10 h +1 h +16218 m +10 h +4 h +65 h +1 h +4 h +113 h +10 h +4 h +10 h +59 h +114 h +4 h +25 h +16219 m +4 h +124 h +1 h +109 h +12 h +2788 h +1 h +4 h +1 h +322 h +10 h +16220 m +4 h +10 h +4 h +10 h +1 h +4 h +4 h +4 h +12 h +1 h +4 h +16221 m +110 h +279 h +4 h +10 h +4 h +1 h +41 h +6066 m +125 h +16222 m +16223 m +4 h +955 m +16224 m +493 m +16225 m +4 h +10 h +11 h +4 h +4 h +16226 m +4 h +1 h +1309 h +4 h +918 m +4 h +4 h +1 h +1 h +4535 m +1 h +4 h +1 h +57 h +10 h +124 h +1 h +147 h +4 h +10 h +112 h +10 h +11 h +4 h +16227 m +1 h +4 h +10 h +16228 m +4 h +1 h +147 h +1 h +10 h +16229 m +104 h +9450 m +83 h +1 h +676 m +1 h +4 h +104 h +4 h +4 h +1 h +10 h +4 h +1 h +16230 m +16231 m +10 h +16232 m +16233 m +4 h +16234 m +1535 m +11 h +74 h +11 h +57 h +4 h +0 m +124 h +11 h +10 h +10 h +1 h +10 h +4 h +4 h +11 h +16235 m +10 h +4 h +4 h +59 h +16236 m +4 h +114 h +4 h +4 h +16237 m +16238 m +4 h +1959 m +5567 m +403 h +10 h +1030 h +146 h +16239 m +1 h +59 h +57 h +146 h +1 h +16240 m +74 h +1 h +4 h +1 h +857 h +10 h +10 h +48 h +4 h +83 h +190 h +1 h +10 h +16241 m +1 h +4 h +4 h +1 h +2788 h +1569 m +10 h +10 h +4 h +4 h +4 h +123 h +4 h +10 h +10 h +174 m +1 h +279 h +135 h +1 h +16242 m +10 h +4 h +4 h +16243 m +371 h +1 h +4 h +1 h +1 h +1 h +1 h +11 h +1 h +16244 m +94 h +11 h +1 h +1 h +74 h +267 h +1 h +10 h +464 h +16245 m +94 h +10 h +55 h +4 h +10 h +4 h +16246 m +57 h +4 h +16247 m +16248 m +92 h +4 h +1 h +1 h +147 h +14345 m +25 h +10 h +1 h +3 h +10 h +16249 m +196 h +10 h +4 h +79 h +56 h +1 h +16250 m +11 h +1128 m +4 h +4 h +1 h +195 h +41 h +10 h +4 h +1 h +692 h +4 h +11 h +10 h +1 h +59 h +4 h +4 h +238 h +16251 m +10 h +4 h +1 h +4256 m +4 h +65 h +1 h +16252 m +92 h +1 h +41 h +4 h +1725 m +65 h +1 h +16253 m +1 h +82 h +238 h +4 h +3558 m +1 h +11 h +10 h +1 h +1 h +4 h +2435 m +16254 m +1 h +779 h +16255 m +11 h +11691 m +322 h +4 h +104 h +16256 m +4 h +124 h +4 h +1 h +4 h +45 h +135 h +1 h +104 h +4 h +125 h +16257 m +10 h +10 h +4 h +11 h +16258 m +10 h +238 h +4 h +371 h +718 h +843 h +10 h +16259 m +4 h +82 h +1 h +4 h +4 h +276 h +10 h +1 h +10 h +1713 m +79 h +1 h +1 h +1 h +59 h +2002 h +1 h +1 h +1 h +31 h +57 h +11 h +109 h +3 h +45 h +10 h +16260 m +57 h +332 h +1 h +3 h +57 h +16261 m +2022 m +16262 m +1 h +1 h +8114 m +1 h +10 h +16263 m +16264 m +4 h +16265 m +4 h +10 h +4 h +1 h +224 h +16266 m +1 h +4 h +25 h +16267 m +1 h +270 h +1 h +4 h +4 h +4 h +692 h +12 h +4 h +55 h +140 h +1 h +11 h +1 h +56 h +16268 m +4 h +59 h +57 h +13043 m +16269 m +6784 m +9323 m +297 h +250 h +2928 m +170 h +10 h +1 h +1 h +10 h +1 h +10 h +57 h +16270 m +692 h +4 h +2490 m +4 h +31 h +4 h +181 h +1 h +10 h +11 h +82 h +16271 m +112 h +4 h +16272 m +279 h +1 h +1 h +4 h +4 h +41 h +6296 m +4 h +1 h +59 h +4714 m +538 h +27 h +97 h +4 h +4 h +16273 m +10 h +11 h +45 h +1 h +13 h +4 h +16274 m +1 h +276 h +4 h +16275 m +114 h +1 h +5065 m +195 h +1 h +368 h +12 h +5584 m +4 h +10 h +1 h +10 h +124 h +65 h +16276 m +4 h +2433 m +295 h +4 h +7900 m +1 h +4 h +22 h +203 h +114 h +1 h +4 h +698 m +16277 m +143 h +2865 m +48 h +83 h +16278 m +3 h +3 h +4 h +10 h +1 h +4 h +4 h +4 h +10 h +4 h +192 h +10 h +1 h +4 h +4 h +124 h +16279 m +4 h +16280 m +4 h +135 h +16281 m +16282 m +443 h +16283 m +16284 m +1 h +10 h +16285 m +4 h +1 h +13 h +10 h +16286 m +4 h +6107 m +12041 m +16287 m +1 h +4 h +11 h +1 h +10 h +1 h +16288 m +1 h +1 h +278 h +16289 m +1 h +4 h +195 h +1 h +1 h +147 h +488 m +299 h +4 h +16290 m +16291 m +3344 m +16292 m +464 h +4 h +1 h +16293 m +16294 m +169 h +55 h +4 h +1 h +464 h +10 h +4 h +16295 m +124 h +1 h +16296 m +4 h +1 h +11 h +4 h +11 h +4 h +1780 h +1685 h +4 h +41 h +143 h +186 h +97 h +190 h +4 h +10 h +4 h +1 h +4 h +459 h +3 h +1 h +11 h +16297 m +16298 m +4 h +109 h +1 h +11 h +16299 m +11 h +10 h +1 h +13 h +4 h +1 h +4 h +10 h +4 h +10 h +4 h +1 h +83 h +10 h +4 h +16300 m +31 h +2924 m +16301 m +16302 m +4 h +10 h +4111 m +94 h +4 h +16303 m +1261 h +10 h +1980 m +10 h +4 h +203 h +16304 m +1 h +65 h +16305 m +146 h +25 h +16306 m +112 h +16307 m +16308 m +10 h +36 h +16309 m +10 h +238 h +258 h +16310 m +16311 m +4 h +347 m +578 h +1 h +4 h +16312 m +10 h +1 h +16313 m +74 h +82 h +4 h +16314 m +3 h +170 h +5125 m +1 h +1 h +11 h +1123 m +172 h +109 h +16315 m +1 h +16316 m +297 h +1 h +16317 m +4 h +119 h +1 h +4 h +4 h +10 h +1 h +10 h +1 h +16318 m +17 m +4 h +16319 m +4 h +1 h +196 h +4 h +368 h +16320 m +196 h +10 h +4 h +2494 m +10 h +10 h +10 h +1 h +167 h +10 h +185 h +10 h +36 h +4 h +16321 m +1 h +10 h +1 h +110 h +4 h +16322 m +4 h +1 h +12 h +4 h +1 h +1409 m +1 h +16323 m +10 h +57 h +10 h +4 h +4 h +10 h +16324 m +139 h +1 h +4 h +4 h +1 h +25 h +92 h +83 h +4 h +147 h +3 h +16325 m +16326 m +1 h +4 h +1 h +10 h +16327 m +114 h +1 h +4 h +10 h +4 h +10 h +10 h +4 h +5008 m +25 h +1 h +10 h +16328 m +167 h +4 h +1016 h +1 h +104 h +10 h +3 h +124 h +4 h +1 h +4 h +1 h +4 h +563 m +4 h +10 h +10 h +16329 m +41 h +25 h +113 h +16330 m +124 h +10 h +4 h +238 h +1646 m +4 h +4 h +1 h +4 h +4 h +1 h +3 h +1967 m +16331 m +16332 m +10 h +10 h +1 h +10 h +1 h +124 h +1 h +172 h +16333 m +25 h +4 h +4 h +16334 m +16335 m +1 h +1 h +196 h +4 h +4 h +1 h +16336 m +10 h +4 h +1027 h +16337 m +4 h +1 h +16338 m +1 h +16339 m +4 h +11948 m +4 h +9467 m +16340 m +4 h +1 h +4 h +1 h +25 h +10 h +4 h +1 h +82 h +31 h +1386 m +114 h +64 h +10 h +157 h +143 h +1 h +10 h +1 h +1 h +16341 m +4 h +1 h +1 h +278 h +1 h +10 h +82 h +10 h +4030 m +10 h +146 h +125 h +4 h +16342 m +4 h +447 h +10 h +16343 m +10 h +10 h +16344 m +16345 m +4 h +2931 m +16346 m +10 h +4 h +16347 m +16348 m +196 h +109 h +16349 m +10 h +4 h +1470 h +4 h +16350 m +16351 m +55 h +36 h +172 h +10062 m +11 h +1 h +125 h +1278 m +16352 m +16353 m +4 h +147 h +16354 m +16355 m +1 h +10 h +143 h +1 h +1 h +4 h +4 h +25 h +16356 m +170 h +1 h +11 h +82 h +4 h +170 h +25 h +45 h +4 h +16357 m +10 h +4 h +464 h +2163 m +4 h +10 h +4 h +1 h +82 h +1 h +4 h +147 h +16358 m +16359 m +4 h +16360 m +10 h +16361 m +4 h +109 h +16362 m +4 h +31 h +124 h +16363 m +4 h +4 h +1 h +4 h +10 h +4 h +143 h +16364 m +266 h +82 h +1 h +16365 m +1 h +4 h +83 h +4 h +1279 m +36 h +250 h +143 h +615 m +307 h +92 h +125 h +4 h +16366 m +10 h +11 h +4 h +4 h +12066 m +16367 m +1 h +464 h +9077 m +1 h +4 h +4 h +16368 m +16369 m +10 h +16370 m +825 m +1 h +10 h +1 h +113 h +1 h +1 h +1 h +4 h +12 h +4 h +4 h +1 h +73 h +16371 m +1478 h +976 h +10 h +109 h +125 h +11 h +4 h +94 h +104 h +16372 m +172 h +238 h +258 h +109 h +4 h +4 h +1 h +4 h +16373 m +1 h +16374 m +59 h +10 h +45 h +10 h +41 h +16375 m +1 h +1 h +250 h +57 h +55 h +4 h +73 h +976 h +11 h +1886 m +10 h +4 h +16376 m +164 h +16377 m +11 h +10 h +1 h +190 h +408 m +83 h +10 h +10 h +118 h +10 h +3 h +238 h +4 h +16378 m +307 h +278 h +4 h +16379 m +4 h +11 h +40 m +332 h +16380 m +1 h +1 h +1 h +1470 h +169 h +4 h +1 h +1677 m +4 h +10 h +41 h +156 h +4 h +16381 m +913 m +1 h +6963 m +124 h +10 h +1 h +16382 m +1261 h +16383 m +10 h +4 h +10 h +4 h +1 h +1 h +4 h +330 h +266 h +55 h +94 h +41 h +4 h +16384 m +147 h +3558 h +4 h +10 h +1 h +536 h +1 h +190 h +313 m +125 h +1198 m +1 h +1 h +11 h +10 h +31 h +1 h +297 h +4 h +4 h +16385 m +4 h +190 h +1478 h +358 h +332 h +4 h +1 h +16386 m +4 h +4 h +4 h +4 h +1772 m +25 h +1 h +16387 m +911 h +10 h +109 h +4 h +443 h +10 h +16388 m +4 h +36 h +82 h +4 h +10 h +109 h +4 h +1 h +1 h +16389 m +55 h +435 h +10 h +1957 m +4 h +22 h +1 h +1 h +79 h +147 h +1542 m +1 h +10 h +4 h +256 h +1 h +1576 m +16390 m +1 h +4 h +4030 m +10 h +262 m +4 h +10 h +4 h +4 h +5505 m +11 h +1 h +10 h +14083 m +16391 m +10 h +4 h +4 h +16392 m +1 h +10 h +16393 m +11 h +3680 m +16394 m +1 h +16395 m +4 h +4 h +1 h +10 h +94 h +1 h +16396 m +22 h +10 h +2788 h +143 h +10 h +383 h +4 h +16397 m +103 h +73 h +4 h +1766 h +10 h +4 h +1 h +10 h +1 h +16398 m +11 h +82 h +10 h +3025 m +4 h +16399 m +4 h +4 h +83 h +31 h +109 h +1535 m +4 h +4 h +4 h +10 h +16400 m +10 h +238 h +1 h +16401 m +10 h +4 h +1 h +4 h +1 h +10 h +74 h +1 h +16402 m +16403 m +4 h +10 h +4 h +12 h +10 h +11 h +74 h +124 h +4 h +16404 m +4 h +172 h +1137 h +16405 m +4 h +10 h +4 h +4 h +238 h +1137 h +4 h +4 h +4 h +1 h +4 h +16406 m +29 m +1 h +358 h +10 h +4 h +16407 m +383 h +10 h +4 h +74 h +1 h +16408 m +16409 m +1685 h +224 h +4 h +109 h +16410 m +447 h +73 h +430 m +169 h +16411 m +16412 m +1 h +16413 m +13 h +4 h +1 h +1198 m +16414 m +10 h +1 h +4240 m +4 h +10 h +82 h +16415 m +16416 m +82 h +143 h +8555 m +16417 m +1 h +4 h +965 h +4 h +4 h +109 h +1 h +109 h +16418 m +146 h +4 h +4 h +4 h +4 h +16419 m +57 h +4 h +4 h +15 m +16420 m +1 h +16421 m +4 h +4 h +109 h +25 h +1 h +1 h +10 h +4 h +109 h +1 h +4 h +56 h +4 h +4 h +4 h +25 h +16422 m +4 h +358 h +16423 m +4 h +1403 h +59 h +10 h +4 h +12 h +94 h +10 h +1936 m +4 h +16424 m +1016 h +1 h +1957 m +1936 m +16425 m +536 h +16426 m +11 h +103 h +278 h +10 h +4 h +1 h +1 h +82 h +11 h +4 h +1 h +4 h +144 m +4 h +4 h +10 h +4 h +4 h +4 h +4 h +144 h +4 h +147 h +1470 h +57 h +1 h +10 h +1 h +16427 m +4 h +135 h +82 h +1 h +4 h +129 h +1 h +4 h +11 h +1 h +4 h +10 h +258 h +1 h +1 h +7479 m +28 h +4 h +16428 m +112 h +10 h +1128 m +4 h +181 h +1 h +10 h +4 h +4457 m +135 h +65 h +4 h +347 m +16429 m +1 h +1 h +10 h +4 h +16430 m +4 h +10 h +16431 m +16432 m +83 h +172 h +4 h +4 h +16433 m +14829 m +65 h +1016 h +1 h +57 h +4 h +10 h +10 h +16434 m +16435 m +1 h +1 h +16436 m +4 h +1 h +16437 m +181 h +10 h +16438 m +4 h +10 h +125 h +135 h +1 h +16439 m +185 h +16440 m +4 h +258 h +642 h +11 h +11 h +16441 m +97 h +143 h +172 h +16442 m +195 h +16443 m +16444 m +97 h +146 h +4 h +4 h +94 h +4 h +16445 m +11 h +1 h +4 h +8 h +4 h +4 h +10 h +16446 m +4 h +16447 m +1 h +16448 m +4 h +278 h +16449 m +4 h +270 h +1 h +57 h +82 h +114 h +1 h +1 h +31 h +4 h +1 h +16450 m +74 h +1 h +4 h +4 h +124 h +238 h +1685 h +4 h +1 h +10 h +97 h +1 h +1 h +1 h +4 h +4 h +10 h +97 h +57 h +16451 m +16452 m +119 h +41 h +16453 m +4 h +4 h +1 h +82 h +10 h +4 h +359 h +1 h +1 h +1 h +170 h +266 h +16454 m +4 h +1 h +4 h +109 h +16455 m +10 h +16456 m +16457 m +147 h +169 h +319 h +1 h +16458 m +4 h +1 h +4 h +16459 m +1 h +16460 m +25 h +158 h +10 h +16461 m +55 h +112 h +4 h +48 h +65 h +109 h +109 h +10 h +16462 m +4 h +14569 m +124 h +16463 m +1 h +1770 m +195 h +262 m +16464 m +10 h +8332 m +173 h +687 h +4 h +2788 h +1 h +4 h +307 h +10177 m +10 h +279 h +31 h +16465 m +1 h +10 h +601 h +59 h +4 h +4 h +41 h +94 h +1 h +4 h +1 h +4 h +4 h +4 h +10 h +186 h +1 h +16466 m +124 h +79 h +109 h +10 h +4 h +4 h +10 h +10 h +16467 m +10 h +4 h +4 h +94 h +3720 m +57 h +10 h +4 h +1 h +4 h +10 h +25 h +1 h +4 h +1 h +1 h +181 h +1 h +425 m +4 h +4 h +569 h +10 h +12 h +16468 m +1 h +10 h +4 h +10 h +4 h +31 h +1 h +10 h +114 h +10 h +57 h +10 h +97 h +4 h +4 h +41 h +16469 m +10 h +4 h +7388 m +536 h +1 h +16470 m +1347 m +4 h +4 h +16471 m +1 h +16472 m +4 h +41 h +1 h +4 h +1 h +82 h +4 h +1 h +4 h +10 h +1 h +181 h +4 h +10 h +16473 m +10 h +4 h +6545 m +1 h +92 h +1504 m +57 h +1 h +443 h +4 h +4 h +10 h +12 h +4 h +1 h +4 h +16474 m +1 h +16475 m +1 h +55 h +1105 h +1006 m +59 h +10 h +1 h +4 h +104 h +16476 m +1 h +4895 m +16477 m +468 m +1 h +135 h +1 h +196 h +57 h +16478 m +11 h +4 h +258 h +16479 m +569 h +1 h +10 h +1 h +16480 m +112 h +9912 m +332 h +4089 m +16481 m +195 h +31 h +4 h +1807 m +276 h +79 h +16482 m +1 h +578 h +36 h +4 h +5080 m +1 h +4 h +4 h +840 m +16483 m +10 h +1 h +16484 m +7839 m +1 h +1 h +16485 m +10 h +1 h +1 h +16486 m +3 h +16487 m +172 h +4 h +4 h +10 h +1 h +1511 m +82 h +4 h +4 h +83 h +16488 m +1198 h +16489 m +10 h +10 h +1650 h +4 h +3539 m +4 h +27 h +16490 m +258 h +4 h +10 h +1 h +16491 m +129 h +10 h +4 h +1 h +569 h +1 h +1 h +1 h +10 h +157 h +16492 m +16493 m +1 h +1 h +10 h +1 h +16494 m +4 h +3143 m +4 h +16495 m +4 h +1 h +16496 m +16497 m +9701 m +181 h +4 h +4 h +114 h +1 h +16498 m +1 h +4 h +16499 m +1 h +479 h +10 h +11 h +1 h +4 h +16500 m +10 h +3600 m +4 h +1 h +443 h +16501 m +11 h +10 h +4564 m +4 h +104 h +97 h +10 h +4 h +1 h +16502 m +1 h +7253 m +16503 m +45 h +4 h +4 h +104 h +11 h +10 h +4 h +10 h +1 h +4 h +10 h +4 h +4 h +16504 m +16505 m +1 h +10 h +14388 m +4 h +10 h +16506 m +16507 m +124 h +1 h +109 h +195 h +10 h +16508 m +109 h +1 h +4 h +10 h +692 h +2002 h +16509 m +4 h +10 h +16510 m +41 h +4 h +10 h +41 h +10 h +4 h +31 h +74 h +119 h +65 h +10 h +59 h +10 h +55 h +4 h +45 h +4 h +10 h +196 h +16511 m +11 h +1 h +10 h +4 h +1 h +1 h +4 h +1 h +5230 m +124 h +4 h +16512 m +16513 m +114 h +10 h +1 h +4 h +119 h +4 h +4 h +109 h +4 h +169 h +16514 m +16515 m +1 h +4 h +10 h +4 h +1714 h +1 h +1 h +297 h +31 h +16516 m +73 h +65 h +16517 m +640 h +147 h +3 h +4 h +16518 m +230 m +4 h +195 h +16519 m +4 h +1 h +4 h +40 m +4 h +4 h +10 h +1 h +4 h +4 h +648 m +4 h +69 h +16520 m +114 h +4 h +16521 m +4 h +11 h +13 h +146 h +1714 h +3 h +16522 m +16523 m +16524 m +3 h +10 h +270 h +1 h +10 h +1 h +10 h +1685 h +256 h +1 h +79 h +1 h +4372 m +16525 m +4 h +270 h +10 h +4 h +124 h +4 h +1 h +4 h +109 h +1 h +1 h +16526 m +4 h +16527 m +16528 m +1 h +238 h +228 m +59 h +10 h +1 h +1 h +4 h +65 h +40 h +1 h +629 m +16529 m +4 h +10 h +1 h +1 h +55 h +289 h +12 h +1714 h +157 h +10 h +8497 m +11 h +16530 m +1 h +3398 m +16531 m +13 h +5695 m +10 h +1 h +1 h +4 h +258 h +10 h +11 h +16532 m +4 h +10 h +16533 m +4 h +10 h +16534 m +10 h +125 h +16535 m +82 h +4 h +13 h +1 h +1016 h +10 h +4 h +4 h +4 h +4 h +1 h +10 h +16536 m +16537 m +779 h +1 h +3070 m +10 h +16538 m +16539 m +256 h +4 h +1 h +4 h +10 h +82 h +10 h +16540 m +4 h +1 h +3 h +41 h +1828 m +1 h +1 h +1 h +1 h +170 h +82 h +16541 m +10 h +16542 m +181 h +73 h +3 h +10 h +5 h +16543 m +16544 m +119 h +10942 m +10958 m +4 h +10 h +74 h +1 h +4 h +4 h +16545 m +16546 m +4 h +1 h +4 h +4 h +4 h +125 h +129 h +16547 m +139 h +1 h +4 h +4 h +59 h +4 h +10 h +4 h +57 h +4 h +14642 m +65 h +195 h +10 h +16548 m +10 h +4 h +83 h +4 h +4 h +65 h +1 h +10 h +16549 m +4 h +129 h +10 h +939 m +1939 m +16550 m +4 h +4 h +1 h +11 h +1 h +4 h +1 h +4 h +5348 m +22 h +45 h +10 h +4 h +139 h +114 h +4 h +4 h +1114 m +258 h +4 h +258 h +10 h +16551 m +1 h +157 h +172 h +55 h +4 h +2474 m +75 m +1 h +1 h +468 m +4 h +1 h +4 h +125 h +10 h +16552 m +59 h +1 h +1 h +10 h +4177 m +1 h +57 h +4 h +16553 m +7535 m +16554 m +4 h +16555 m +2265 m +16556 m +966 h +276 h +16147 m +10 h +13084 m +195 h +10 h +125 h +11 h +1 h +4 h +1 h +1 h +1 h +4 h +16557 m +10 h +16558 m +185 h +1 h +11 h +25 h +64 h +23 h +500 m +10 h +10 h +16559 m +1 h +10 h +10 h +1 h +16560 m +601 h +16561 m +64 h +1 h +45 h +16562 m +1 h +10 h +1 h +278 h +10378 m +10 h +27 h +10 h +11 h +10464 m +16563 m +1 h +1 h +3028 m +109 h +4 h +54 m +4 h +109 h +4 h +10 h +31 h +386 h +2923 h +4 h +140 h +10 h +4 h +17 m +27 h +10 h +1 h +1 h +172 h +4 h +97 h +4 h +10 h +10 h +16564 m +97 h +4 h +11141 m +59 h +4 h +1 h +4 h +4 h +16565 m +4 h +13 h +57 h +4 h +10 h +112 h +1766 h +1 h +31 h +1 h +173 h +4 h +1 h +266 h +4 h +25 h +12 h +4 h +82 h +4 h +1 h +36 h +4 h +12 h +16566 m +16567 m +94 h +7135 m +4 h +16568 m +489 m +4 h +82 h +16569 m +104 h +10 h +10 h +10 h +4 h +581 m +1 h +4 h +4 h +1 h +8 h +124 h +10 h +31 h +16570 m +4 h +332 h +4 h +4 h +4 h +359 h +16571 m +41 h +48 h +16572 m +4 h +16573 m +1 h +11 h +10682 m +4 h +4 h +16574 m +10 h +10 h +4 h +16575 m +10 h +1 h +16576 m +4 h +1 h +1 h +94 h +4 h +1 h +16577 m +16578 m +16579 m +16580 m +10 h +1 h +3673 m +4 h +1 h +10 h +4 h +4 h +41 h +113 h +297 h +65 h +16581 m +4 h +4 h +1470 h +4 h +10 h +10 h +1 h +16582 m +1 h +1 h +16583 m +16584 m +143 h +4 h +10 h +3 h +4 h +16585 m +1 h +1 h +4 h +10 h +138 h +10 h +4 h +13958 m +124 h +10 h +16586 m +25 h +57 h +16587 m +4522 m +16588 m +1 h +4 h +1 h +16589 m +1 h +146 h +4 h +1 h +16590 m +16591 m +224 h +181 h +195 h +10 h +4 h +10 h +59 h +10 h +4 h +4 h +1 h +4 h +4 h +4 h +5567 m +2391 m +10 h +16592 m +4 h +1 h +4 h +16593 m +4 h +16594 m +1 h +57 h +4 h +10 h +112 h +10 h +16595 m +4 h +10 h +57 h +4 h +1 h +109 h +1470 h +10 h +16596 m +114 h +1 h +11 h +443 h +10 h +911 h +1 h +10 h +1 h +1 h +16597 m +4 h +1 h +16598 m +16599 m +16600 m +16601 m +1 h +4 h +4 h +4 h +16602 m +16603 m +4 h +16604 m +12 h +6558 m +82 h +155 m +16605 m +11 h +4 h +16606 m +1 h +4 h +1 h +4 h +11 h +10 h +94 h +1 h +1 h +1 h +1 h +16607 m +16608 m +4 h +1249 m +1 h +10 h +4 h +10 h +1 h +9482 m +1 h +57 h +1 h +59 h +1 h +4 h +10 h +3 h +12 h +4 h +10 h +4 h +195 h +11 h +10 h +104 h +158 h +16609 m +4 h +4 h +1 h +16610 m +1 h +97 h +16611 m +10 h +4 h +16612 m +109 h +97 h +976 h +10 h +4 h +147 h +4 h +4 h +4 h +4 h +4 h +258 h +717 m +4 h +190 h +4 h +61 m +1 h +3606 m +1 h +4 h +6473 m +4 h +2480 m +8 h +59 h +4 h +146 h +986 h +10 h +3533 m +10 h +4 h +285 m +1 h +16613 m +16614 m +59 h +10 h +100 m +1 h +5003 m +11 h +7087 m +13 h +4 h +1 h +2887 m +10 h +16615 m +4 h +10 h +1 h +1 h +16616 m +1 h +110 h +590 m +57 h +22 h +12990 m +1642 h +143 h +1 h +4 h +16617 m +16618 m +4 h +4 h +10 h +4 h +1 h +16619 m +8 h +16620 m +45 h +79 h +10 h +16621 m +4 h +4 h +10 h +7382 m +1 h +1 h +16622 m +4 h +1 h +16623 m +10 h +11 h +1 h +16624 m +97 h +443 h +124 h +16625 m +57 h +83 h +55 h +4 h +3025 m +6678 m +4 h +82 h +1 h +1 h +266 h +16626 m +4 h +4 h +10 h +10 h +119 h +10 h +10 h +195 h +4 h +57 h +124 h +825 m +1 h +143 h +1 h +16627 m +16628 m +4 h +109 h +10 h +4 h +4 h +25 h +10 h +4 h +10 h +1 h +82 h +3 h +5230 m +1 h +196 h +4 h +16629 m +16630 m +11334 m +1 h +1 h +4 h +104 h +167 h +1 h +146 h +65 h +1 h +16631 m +4 h +10 h +4 h +10 h +16632 m +10 h +1 h +1 h +16633 m +10 h +16634 m +4 h +16635 m +16636 m +1 h +25 h +1 h +1 h +1766 h +4 h +5 h +73 h +16637 m +4 h +16638 m +195 h +1 h +1 h +1 h +10 h +4 h +104 h +4561 m +4 h +1 h +82 h +16639 m +10 h +1975 m +10 h +123 h +16640 m +25 h +16641 m +57 h +4 h +16642 m +16643 m +16644 m +82 h +124 h +82 h +164 h +4 h +16645 m +11 h +45 h +41 h +4 h +16646 m +4 h +10 h +8511 m +4 h +10 h +4 h +1 h +779 h +11 h +10 h +1 h +976 h +104 h +4 h +4 h +1 h +82 h +1 h +7661 m +4 h +1 h +4 h +10 h +278 h +82 h +41 h +4 h +16647 m +16648 m +65 h +3 h +770 m +339 m +2530 m +1 h +10 h +10 h +1 h +4 h +10 h +4 h +278 h +1 h +1 h +1 h +1 h +2184 m +4 h +1 h +11 h +4 h +4 h +4 h +16649 m +16650 m +4 h +4 h +16651 m +104 h +4 h +10 h +4 h +4 h +1 h +190 h +16652 m +4 h +4 h +4 h +4 h +125 h +45 h +1 h +16653 m +16654 m +1 h +16655 m +10 h +16656 m +109 h +4 h +4 h +4 h +10 h +11 h +10 h +85 m +10 h +536 h +4 h +25 h +16657 m +16658 m +1 h +10 h +4 h +16659 m +27 h +353 h +258 h +82 h +4 h +59 h +4 h +1 h +4 h +4 h +16660 m +83 h +190 h +250 h +4 h +601 h +976 h +4 h +4 h +83 h +1 h +10 h +16661 m +79 h +1 h +4 h +4 h +10 h +1 h +10 h +10 h +113 h +4 h +4 h +10 h +4 h +1 h +16662 m +14476 m +4 h +10 h +1 h +1 h +10 h +40 h +10 h +92 h +307 h +14 m +1685 h +64 h +16663 m +1 h +16664 m +16665 m +1 h +10 h +16666 m +1 h +10 h +10 h +57 h +124 h +1 h +16667 m +156 h +195 h +10 h +10 h +104 h +16668 m +557 m +274 h +3 h +10 h +468 h +4 h +1 h +16669 m +615 m +4 h +307 h +16670 m +386 h +4 h +1 h +1714 h +16671 m +16672 m +4 h +157 h +4 h +16673 m +278 h +1714 h +91 h +857 h +10 h +4 h +4 h +11 h +1 h +1 h +1 h +79 h +4 h +10 h +16674 m +36 h +16675 m +10 h +16676 m +10 h +16677 m +1 h +1 h +16678 m +146 h +10 h +4 h +125 h +45 h +4 h +1366 m +16679 m +4 h +1 h +4 h +16680 m +3 h +143 h +56 h +147 h +16681 m +1 h +190 h +16682 m +383 h +16683 m +477 m +1915 m +135 h +1 h +258 h +16684 m +16685 m +109 h +11 h +1 h +4 h +64 h +4 h +10 h +16686 m +11 h +41 h +1403 h +1 h +45 h +12585 m +1 h +10 h +16687 m +73 h +16688 m +83 h +265 h +258 h +4 h +74 h +10 h +55 h +4 h +10 h +16689 m +1 h +4 h +1003 m +10 h +4 h +10 h +1 h +4 h +16690 m +4 h +16691 m +4 h +16692 m +16693 m +4 h +1 h +36 h +1 h +16694 m +4 h +146 h +157 h +1 h +10 h +16695 m +16696 m +1105 h +11 h +2172 m +16697 m +1 h +22 h +4 h +1 h +1 h +601 h +16698 m +1 h +10 h +1 h +16699 m +1 h +1 h +16700 m +1 h +1 h +1 h +16701 m +1 h +16702 m +10 h +1 h +4 h +185 h +4 h +10 h +82 h +169 h +16703 m +1 h +4 h +41 h +4 h +57 h +16704 m +1 h +10 h +146 h +463 m +16705 m +1 h +1 h +11 h +4 h +1 h +104 h +4 h +103 h +56 h +1 h +16706 m +16707 m +109 h +10 h +16708 m +4 h +4 h +4 h +16709 m +1 h +4 h +1 h +10 h +109 h +11 h +10 h +4 h +1 h +4 h +1 h +800 m +266 h +4 h +493 m +16710 m +4 h +16711 m +3 h +59 h +1 h +16712 m +16713 m +3 h +4 h +10 h +3 h +4 h +83 h +25 h +146 h +4 h +40 h +4 h +16714 m +1 h +124 h +4 h +1 h +113 h +4 h +219 h +1 h +195 h +4 h +1470 h +55 h +16715 m +8 h +4 h +1 h +1 h +1 h +65 h +104 h +157 h +16716 m +16717 m +11 h +16718 m +57 h +4 h +83 h +3 h +16719 m +1 h +307 h +16720 m +4 h +11 h +16721 m +4861 m +16722 m +16723 m +10 h +4297 m +97 h +4 h +104 h +16724 m +1 h +16725 m +4101 m +1766 h +4 h +4 h +169 h +97 h +4 h +59 h +4 h +4 h +11 h +16726 m +250 h +10 h +1 h +4 h +16727 m +1 h +4 h +1 h +16728 m +615 m +16729 m +4 h +478 m +10 h +16730 m +1 h +10 h +16731 m +299 h +4 h +31 h +10 h +1545 m +1 h +4 h +1 h +4 h +114 h +11 h +4 h +1 h +16732 m +4 h +4 h +16733 m +4 h +1 h +10 h +2625 m +82 h +16734 m +16735 m +1 h +332 h +10 h +186 h +4 h +11 h +57 h +195 h +1 h +4 h +25 h +16736 m +57 h +10 h +64 h +1 h +10 h +4 h +1791 m +4 h +1 h +1 h +16737 m +1 h +10 h +1 h +10 h +1 h +1 h +16738 m +82 h +4 h +11 h +4 h +1 h +16739 m +16740 m +16741 m +1 h +65 h +10 h +10 h +4359 m +1 h +1 h +16742 m +45 h +4 h +16743 m +16744 m +10 h +16745 m +16746 m +4 h +3 h +1 h +16747 m +16748 m +10 h +11 h +1 h +1 h +16749 m +4 h +1083 h +16750 m +4 h +31 h +1 h +4 h +11 h +10 h +16751 m +16752 m +10 h +4 h +10 h +181 h +16753 m +2710 m +307 h +16754 m +16755 m +82 h +125 h +4 h +385 m +16756 m +1185 m +10 h +3 h +10 h +4 h +4 h +4 h +4 h +1766 h +16757 m +278 h +4 h +1 h +330 h +1 h +4 h +1 h +4 h +4 h +16758 m +16759 m +16760 m +135 h +10 h +25 h +124 h +601 h +390 m +2339 m +4 h +11 h +16761 m +4 h +355 m +146 h +10 h +16762 m +265 h +1 h +146 h +3533 m +1 h +64 h +10 h +82 h +16763 m +16764 m +4 h +4 h +13 h +59 h +16765 m +46 m +4 h +59 h +4 h +4 h +10 h +2733 h +10 h +16766 m +55 h +2971 m +1 h +10 h +1 h +10 h +109 h +16767 m +1 h +112 h +22 h +1 h +1 h +11 h +4 h +4 h +11 h +16768 m +125 h +1 h +4 h +1 h +10 h +1 h +13 h +16769 m +41 h +109 h +10 h +536 h +1 h +10 h +45 h +1 h +10 h +4 h +1 h +1 h +4 h +4 h +517 m +10 h +4 h +4 h +10 h +16770 m +16771 m +4 h +4 h +1 h +16772 m +10 h +4 h +4 h +2887 m +4441 m +41 h +10 h +16773 m +10 h +2710 m +4 h +109 h +10 h +4 h +1 h +59 h +322 h +16774 m +976 h +16775 m +238 h +4 h +1 h +4 h +1 h +4 h +16776 m +1 h +10 h +10 h +16777 m +10 h +31 h +10 h +4 h +4 h +241 h +10 h +1 h +124 h +55 h +12 h +10 h +1 h +10 h +3 h +4 h +506 m +4 h +16778 m +4 h +16779 m +4 h +1835 m +4 h +4 h +109 h +16780 m +16781 m +11 h +435 h +1 h +4 h +1 h +16782 m +2300 m +16783 m +1 h +976 h +1 h +1 h +1 h +16784 m +2379 h +16785 m +4 h +16786 m +6461 m +1 h +25 h +10 h +11 h +4 h +1 h +1 h +11 h +10 h +82 h +4 h +11 h +278 h +687 h +186 h +73 h +97 h +97 h +469 m +10 h +45 h +195 h +11 h +16787 m +16788 m +1321 m +16789 m +1 h +16790 m +1 h +10 h +4 h +270 h +10 h +4 h +65 h +31 h +10 h +114 h +4 h +125 h +4 h +1 h +8140 m +4 h +4 h +11707 m +4 h +16791 m +10 h +4 h +1 h +1 h +4 h +358 h +1 h +109 h +4 h +16792 m +1 h +4 h +158 h +16793 m +4 h +10 h +10 h +124 h +16794 m +964 m +4 h +1 h +119 h +25 h +16795 m +55 h +109 h +383 h +4 h +82 h +857 h +4724 m +104 h +4 h +16796 m +16797 m +16798 m +31 h +1 h +56 h +4 h +196 h +143 h +4 h +16799 m +82 h +1 h +10 h +4 h +10 h +4 h +11346 m +1 h +16800 m +1 h +16801 m +1 h +4 h +16802 m +10 h +10 h +57 h +10 h +4 h +229 h +16803 m +1 h +16804 m +1403 h +5308 m +16805 m +2851 h +16806 m +10 h +104 h +4 h +4 h +16514 m +358 h +157 h +4 h +10 h +1478 h +94 h +4 h +1 h +74 h +124 h +10 h +8497 m +16807 m +4 h +2040 m +4 h +359 h +31 h +1 h +10 h +1 h +10 h +157 h +4 h +16808 m +16809 m +1 h +1 h +1 h +464 h +1 h +16810 m +10 h +190 h +10 h +4 h +10 h +112 h +57 h +1 h +4905 h +2087 m +16811 m +83 h +55 h +83 h +10 h +99 m +4 h +10 h +146 h +4 h +1478 h +1016 h +16812 m +319 h +262 h +4 h +4 h +4 h +4 h +1 h +4 h +25 h +4 h +16813 m +278 h +4 h +4 h +16814 m +104 h +181 h +1 h +41 h +1714 h +359 h +82 h +4 h +10 h +1 h +16815 m +16816 m +16817 m +4 h +16748 m +1 h +70 m +172 h +16818 m +4 h +124 h +16819 m +4 h +2788 h +4 h +4 h +16820 m +16821 m +1083 h +10 h +4 h +4 h +297 h +2002 h +1666 m +1492 m +97 h +169 h +146 h +4 h +16822 m +1 h +4 h +570 m +4 h +1 h +16823 m +1 h +16824 m +1 h +10 h +16825 m +10 h +16826 m +10 h +75 m +12 h +10 h +4 h +10 h +1261 h +10 h +10 h +238 h +10 h +1 h +16827 m +173 h +16828 m +4 h +1137 h +9912 m +4 h +27 h +73 h +4 h +16829 m +59 h +10901 m +113 h +4 h +4 h +4 h +10 h +4 h +10 h +10 h +124 h +16830 m +10 h +1 h +190 h +16831 m +4 h +4 h +125 h +4 h +4 h +1 h +181 h +4 h +16832 m +82 h +12 h +124 h +1 h +11 h +1 h +1250 h +6266 m +16833 m +1766 h +1 h +74 h +10 h +11 h +13 h +65 h +4 h +1 h +4 h +16834 m +109 h +195 h +4 h +1 h +10 h +4 h +10 h +10 h +4 h +11 h +1 h +4 h +10 h +4 h +4 h +2192 m +1 h +1 h +1 h +4 h +16835 m +266 h +4 h +16836 m +10 h +4 h +4 h +4 h +10 h +4 h +4 h +1 h +55 h +16837 m +332 h +10 h +10 h +16838 m +4 h +4 h +1 h +399 h +16839 m +59 h +10 h +443 h +64 h +4 h +4 h +976 h +10 h +10 h +238 h +4 h +2495 m +11 h +2265 m +4 h +16840 m +1 h +1 h +16841 m +295 h +1 h +16842 m +1 h +10 h +6185 m +57 h +16843 m +4 h +25 h +16844 m +4 h +1 h +4 h +16845 m +124 h +10 h +1 h +16846 m +16847 m +16848 m +4 h +36 h +16849 m +1 h +59 h +1875 m +4 h +1 h +1886 m +11 h +16850 m +40 h +266 h +16851 m +94 h +16852 m +74 h +4 h +10 h +16853 m +4 h +109 h +204 m +4 h +1 h +172 h +1 h +1 h +1 h +4 h +10 h +10 h +794 m +4 h +4 h +1 h +4 h +31 h +4 h +10 h +10 h +4 h +2002 h +1 h +196 h +196 h +4 h +4 h +83 h +16854 m +4 h +125 h +1 h +16855 m +4 h +83 h +1 h +109 h +1 h +1 h +10 h +109 h +16856 m +1 h +10 h +1 h +11 h +4 h +4 h +31 h +16857 m +1 h +16858 m +10 h +4 h +3 h +59 h +14814 m +1 h +4 h +10 h +4 h +16859 m +12 h +1 h +278 h +10 h +4 h +135 h +1 h +16860 m +3 h +16861 m +4 h +4 h +114 h +1 h +1 h +10 h +1 h +7802 m +11 h +4 h +4 h +16862 m +56 h +338 h +59 h +10 h +4 h +4 h +4 h +114 h +888 h +1 h +181 h +1 h +146 h +1105 h +82 h +10 h +16863 m +4 h +5610 m +1564 m +10 h +16864 m +190 h +1 h +2914 m +109 h +10 h +1 h +1137 h +16865 m +16866 m +4 h +8 h +278 h +10 h +276 h +10 h +355 m +1 h +4 h +1 h +16867 m +11 h +118 h +1 h +1 h +4 h +16868 m +4 h +16869 m +4 h +1 h +16870 m +1 h +10 h +16871 m +13392 m +313 m +4 h +4 h +16872 m +4 h +4 h +135 h +10 h +16873 m +16874 m +4 h +195 h +4 h +1 h +16875 m +16876 m +4 h +4 h +4 h +4 h +16877 m +10 h +397 m +65 h +109 h +1437 m +5526 m +1 h +1 h +16878 m +12 h +1 h +10 h +1089 h +185 h +10 h +358 h +16879 m +110 h +4 h +4 h +4 h +10 h +156 h +164 h +190 h +3 h +190 h +41 h +4 h +16880 m +16881 m +4 h +1 h +1 h +16882 m +125 h +1 h +16883 m +737 m +4 h +3155 m +41 h +1 h +1 h +4 h +4 h +1 h +11 h +11 h +1 h +59 h +4 h +10 h +36 h +25 h +108 h +11 h +4 h +7 m +10 h +10 h +16884 m +16885 m +2194 m +124 h +4 h +16886 m +123 h +25 h +1 h +10 h +125 h +4 h +9933 m +718 h +56 h +4 h +4 h +109 h +1 h +279 h +1 h +16887 m +83 h +1137 h +3 h +1 h +4 h +1 h +4 h +1 h +25 h +4 h +4 h +4 h +332 h +16888 m +1 h +4 h +4 h +16889 m +1 h +1 h +4 h +10 h +10 h +4 h +332 h +10 h +4 h +11 h +124 h +147 h +4 h +4 h +16890 m +10 h +1 h +4 h +16891 m +5224 m +5709 m +10 h +1 h +203 h +4 h +16892 m +1 h +16893 m +150 m +16894 m +10 h +4 h +1 h +5357 m +16895 m +79 h +16896 m +56 h +31 h +4 h +4 h +10 h +11 h +170 h +4 h +4 h +1 h +82 h +10 h +4 h +4 h +289 h +444 m +10 h +16897 m +386 h +10 h +1083 h +1 h +16898 m +1 h +11731 m +1 h +10 h +4 h +8 h +16899 m +10 h +10 h +4 h +2233 m +4 h +147 h +4 h +4 h +45 h +10 h +1 h +12 h +5053 m +10 h +16900 m +258 h +16901 m +4 h +1 h +1 h +1886 m +4 h +16902 m +10 h +4 h +83 h +10 h +4 h +16903 m +1 h +1 h +1 h +135 h +1056 m +16904 m +1 h +109 h +16905 m +1 h +10 h +11 h +97 h +16906 m +10 h +104 h +74 h +1 h +16907 m +10 h +16908 m +2846 m +16909 m +10 h +123 h +4 h +4 h +4 h +158 h +74 h +4 h +10 h +1 h +4 h +11715 m +447 h +4848 m +16910 m +124 h +4 h +10 h +16911 m +167 h +10 h +1 h +4 h +1 h +10 h +16912 m +16913 m +16914 m +10 h +16915 m +1 h +620 m +124 h +1 h +1 h +1 h +1 h +36 h +10 h +173 h +11 h +10 h +57 h +16916 m +4 h +124 h +4 h +1 h +10 h +10 h +13 h +4 h +4 h +1 h +1 h +10 h +1 h +4 h +4 h +1 h +16917 m +4 h +4 h +16918 m +16919 m +355 h +4 h +16920 m +4 h +1 h +204 h +4 h +4 h +11 h +1 h +109 h +10 h +4 h +332 h +4608 m +82 h +55 h +16921 m +4 h +279 h +12 h +1406 m +990 m +4 h +3 h +1 h +330 h +1 h +4 h +57 h +41 h +1 h +1 h +1 h +1 h +10 h +338 h +3 h +125 h +36 h +1650 h +4 h +10 h +3 h +16922 m +4 h +353 h +1 h +4 h +169 h +1 h +1 h +11 h +359 h +41 h +16923 m +1 h +4 h +4 h +1504 m +1 h +11 h +4 h +16924 m +1 h +41 h +11825 m +1 h +16925 m +423 m +2436 m +16926 m +16927 m +4 h +10 h +4 h +1 h +10 h +1 h +10 h +16928 m +4 h +757 h +16929 m +1 h +4 h +4 h +7214 m +230 m +1250 h +1 h +1 h +59 h +16930 m +4 h +10 h +4 h +10 h +1337 m +4 h +4 h +10 h +65 h +10 h +2865 m +3 h +16931 m +4538 m +656 m +16932 m +4 h +266 h +16933 m +16934 m +16935 m +12 h +10 h +10 h +4590 m +1 h +16936 m +1 h +1 h +4 h +1 h +1772 m +1 h +228 m +1 h +11 h +16937 m +1642 h +57 h +124 h +16938 m +10 h +16939 m +140 h +74 h +4 h +16940 m +3 h +1 h +1 h +143 h +1 h +4 h +16941 m +0 m +109 h +238 h +94 h +1 h +1 h +16942 m +1 h +124 h +84 m +9411 m +4 h +1 h +55 h +4 h +16943 m +4 h +224 h +15156 m +4 h +16944 m +1 h +196 h +4 h +4 h +10 h +1 h +11 h +1975 m +173 h +1 h +59 h +1 h +10 h +1 h +16945 m +109 h +4 h +16946 m +59 h +601 h +1847 m +16947 m +74 h +10 h +1 h +1650 h +1137 h +1 h +4 h +1 h +125 h +4 h +4 h +16948 m +83 h +1 h +4 h +10 h +4 h +10 h +4 h +16949 m +11 h +3 h +1 h +10 h +4 h +196 h +16950 m +4 h +4 h +169 h +4 h +22 h +119 h +10 h +3236 m +10 h +181 h +1 h +4 h +297 h +10 h +1 h +57 h +114 h +4 h +4 h +10 h +104 h +1114 m +123 h +79 h +238 h +4 h +16951 m +5514 m +4 h +4 h +123 h +1 h +1 h +10 h +135 h +241 h +12 h +4 h +16952 m +4 h +1 h +10 h +2887 m +16953 m +4 h +976 h +65 h +278 h +1 h +1 h +16954 m +104 h +10 h +16955 m +1 h +112 h +41 h +59 h +265 h +59 h +10 h +1 h +4 h +16956 m +1 h +16957 m +59 h +157 h +4 h +10 h +10 h +12131 m +4 h +16958 m +10 h +13 h +276 h +10 h +147 h +4 h +1321 m +4 h +31 h +16959 m +4 h +4 h +1 h +204 h +4 h +16960 m +278 h +4 h +16961 m +16962 m +10 h +4 h +16963 m +73 h +4 h +144 h +16964 m +1 h +73 h +8 h +16965 m +22 h +10 h +1 h +4 h +119 h +1 h +1 h +266 h +976 h +167 h +16966 m +10 h +10 h +4 h +4030 h +17 m +630 m +10 h +109 h +10 h +1 h +1 h +16967 m +1 h +1 h +10 h +224 h +82 h +16968 m +4 h +4 h +1 h +1 h +4 h +1 h +92 h +59 h +4 h +31 h +16969 m +4 h +1 h +1 h +4 h +1 h +10 h +4 h +16970 m +1 h +83 h +447 h +16971 m +1 h +224 h +11 h +4 h +2379 h +16972 m +4 h +167 h +4 h +36 h +4 h +3 h +77 h +10 h +1 h +4 h +16973 m +10 h +4 h +10 h +11 h +55 h +112 h +4 h +146 h +124 h +10 h +16974 m +13833 m +10 h +11 h +10 h +1 h +16975 m +13 h +16976 m +1089 h +1 h +10 h +10 h +4 h +265 h +16977 m +16978 m +1 h +1 h +16979 m +10 h +4 h +57 h +10 h +16980 m +16981 m +4 h +4 h +4 h +557 m +332 h +2054 m +10 h +10 h +10 h +808 m +4 h +569 h +10 h +16982 m +1 h +4 h +4 h +40 h +10 h +41 h +109 h +104 h +10 h +10 h +629 m +45 h +4 h +16983 m +9396 m +229 h +11 h +10 h +1 h +1 h +10 h +16984 m +1 h +13 h +1016 h +4 h +7236 m +4 h +91 h +1 h +11 h +2358 m +230 m +447 h +33 m +10 h +4 h +16985 m +10 h +316 m +241 h +4 h +3 h +4 h +3987 m +143 h +55 h +4 h +463 m +1 h +16986 m +4 h +1 h +1260 m +10 h +1 h +4 h +2314 m +1 h +59 h +13324 m +278 h +1 h +92 h +332 h +2438 m +16987 m +1 h +103 h +1 h +74 h +371 h +684 m +16988 m +10 h +3 h +10 h +16989 m +16990 m +10 h +11 h +164 h +4 h +1 h +399 h +10 h +1 h +16991 m +4 h +169 h +119 h +4 h +4057 m +10 h +16992 m +4 h +1027 h +109 h +1 h +82 h +1 h +4 h +11 h +4 h +4 h +1470 h +266 h +16993 m +4 h +10 h +4 h +16994 m +1 h +4 h +190 h +16995 m +2951 m +1 h +1 h +25 h +1 h +4 h +125 h +4 h +1 h +265 h +4 h +10 h +64 h +1 h +203 h +4 h +10 h +3 h +297 h +16996 m +4 h +1 h +4 h +59 h +16997 m +1 h +10 h +10 h +1 h +1 h +16998 m +16999 m +17000 m +8040 m +82 h +4 h +17001 m +17002 m +11 h +888 h +10 h +169 h +119 h +57 h +4 h +17003 m +17004 m +4 h +1 h +4 h +4 h +31 h +10 h +170 h +4 h +1 h +10 h +10 h +1 h +4 h +41 h +4 h +10 h +112 h +1 h +13907 m +1 h +4 h +1 h +45 h +4 h +167 h +17005 m +135 h +17006 m +4 h +1 h +17007 m +10 h +10 h +82 h +1 h +64 h +57 h +10 h +17008 m +270 h +65 h +124 h +4 h +17009 m +4 h +1 h +169 h +17010 m +1016 h +1 h +196 h +4 h +4 h +4 h +31 h +17011 m +8486 m +1 h +258 h +4 h +17012 m +843 h +10 h +4 h +11 h +1370 m +4 h +45 h +74 h +1 h +1 h +578 h +1 h +55 h +443 h +10 h +97 h +83 h +4 h +17013 m +1 h +1 h +4 h +4 h +17014 m +4 h +17015 m +4 h +17016 m +97 h +1 h +11 h +4 h +17017 m +10 h +10 h +4 h +4 h +4 h +4 h +10 h +164 h +104 h +17018 m +4 h +4 h +779 h +400 m +4 h +10 h +17019 m +48 h +41 h +17020 m +17021 m +10 h +4 h +60 m +17022 m +4 h +347 h +1 h +4 h +17023 m +10 h +4 h +1 h +82 h +316 m +12 h +4 h +1083 h +4 h +1 h +4 h +4 h +4 h +169 h +4 h +10 h +4 h +17024 m +4 h +4240 m +181 h +4 h +17025 m +10 h +4 h +1 h +717 m +196 h +1 h +17026 m +4 h +65 h +64 h +10 h +1 h +6558 m +4 h +1 h +4 h +4 h +10 h +4 h +56 h +17027 m +17028 m +10 h +10 h +425 m +13 h +4 h +31 h +125 h +17029 m +1 h +4 h +4 h +1 h +3286 m +4 h +17030 m +10 h +10 h +109 h +184 h +4332 m +1 h +4 h +2340 m +1 h +185 h +17031 m +125 h +17032 m +17033 m +10 h +4 h +4 h +1 h +17034 m +17035 m +4 h +17036 m +4 h +4 h +4 h +4 h +10 h +17037 m +2887 m +65 h +11 h +6010 m +31 h +22 h +25 h +17038 m +1 h +10 h +1 h +770 m +4 h +4 h +10 h +4 h +195 h +4 h +4 h +17039 m +4 h +4 h +124 h +17040 m +4 h +82 h +17041 m +1 h +17042 m +17043 m +59 h +114 h +9396 m +17044 m +10 h +181 h +4 h +4 h +1 h +4 h +17045 m +1 h +4 h +229 h +9831 m +17046 m +11910 m +928 m +10 h +10 h +10 h +17047 m +1 h +41 h +11 h +1 h +17048 m +10 h +17049 m +17050 m +10 h +11 h +10 h +10 h +4 h +1 h +156 h +4 h +10 h +10 h +976 h +10 h +1 h +17051 m +17052 m +10 h +1 h +11 h +1 h +17053 m +1 h +1 h +1 h +976 h +10 h +4 h +17054 m +250 h +1 h +1 h +17055 m +3 h +195 h +4 h +17056 m +10 h +1105 h +1 h +10 h +36 h +3 h +1027 h +10 h +757 h +17057 m +25 h +10 h +1 h +1 h +368 h +4 h +11 h +125 h +185 h +17058 m +196 h +1 h +79 h +4 h +2172 m +299 h +59 h +17059 m +307 h +13 h +1 h +11 h +173 h +4 h +10 h +181 h +1 h +17060 m +17061 m +17062 m +371 h +1 h +4 h +262 h +10 h +10 h +408 m +17063 m +4 h +17064 m +10 h +1 h +10 h +1 h +4 h +2459 m +140 h +10 h +10 h +4 h +238 h +1 h +4 h +4 h +10 h +1 h +17065 m +4 h +147 h +125 h +4 h +17066 m +10 h +3322 m +17067 m +4 h +1 h +22 h +164 h +31 h +4 h +1 h +8 h +10 h +443 h +649 m +4 h +4 h +274 h +1 h +4 h +10 h +1 h +1 h +1 h +17068 m +4 h +3 h +74 h +4 h +4 h +4 h +1 h +17069 m +4 h +10 h +258 h +4 h +4 h +3 h +82 h +79 h +17070 m +104 h +1 h +195 h +692 h +11 h +11 h +17071 m +10 h +17072 m +1 h +10 h +10 h +17073 m +146 h +1 h +4 h +767 m +4 h +104 h +104 h +1 h +17074 m +4177 m +1 h +17075 m +17076 m +11 h +1 h +1620 m +4 h +17077 m +229 h +857 h +10062 m +170 h +17078 m +17079 m +17080 m +1 h +1 h +46 m +17081 m +45 h +17082 m +4 h +3 h +1 h +124 h +10 h +17083 m +41 h +1 h +4 h +10 h +17084 m +59 h +17085 m +3557 m +195 h +17086 m +17087 m +10 h +17088 m +17089 m +10 h +55 h +82 h +10 h +297 h +17090 m +569 h +17091 m +10 h +1 h +10 h +17092 m +10 h +6946 m +9800 m +1 h +31 h +4 h +124 h +82 h +1 h +17093 m +265 h +10 h +1 h +184 h +55 h +1 h +1 h +4 h +3 h +4 h +31 h +10 h +158 h +10 h +10 h +4 h +1 h +10 h +1 h +12 h +3704 m +1 h +297 h +25 h +57 h +4 h +59 h +1 h +28 h +11 h +36 h +1 h +4 h +5125 m +36 h +4 h +4 h +57 h +1 h +1 h +10 h +10 h +10 h +230 h +1 h +4 h +17094 m +1 h +73 h +358 h +64 h +278 h +4 h +5 h +1 h +4 h +10 h +4 h +10 h +1 h +10 h +17095 m +10 h +1 h +1454 h +17096 m +36 h +3707 m +2017 m +1 h +4 h +4919 m +4 h +10 h +4 h +172 h +11 h +10 h +196 h +10 h +10 h +10 h +1137 h +1677 m +10 h +157 h +4 h +109 h +4 h +109 h +17097 m +65 h +17098 m +124 h +17099 m +4 h +17100 m +4 h +10 h +4 h +124 h +112 h +17101 m +4 h +4 h +192 h +17102 m +4 h +45 h +403 h +1 h +238 h +4 h +1 h +17103 m +4 h +17104 m +4 h +1074 h +4 h +22 h +185 h +3025 m +10 h +125 h +156 h +1 h +92 h +4 h +10 h +5470 m +1 h +4 h +4 h +170 h +17105 m +83 h +5 h +17106 m +97 h +716 m +1062 m +4 h +59 h +110 h +1642 h +1 h +17107 m +1 h +10 h +17108 m +4 h +4 h +4 h +17109 m +124 h +4 h +4 h +31 h +1 h +282 m +1074 h +4 h +164 h +3 h +31 h +4 h +1 h +4 h +1 h +4 h +112 h +10 h +4 h +10 h +17110 m +125 h +11 h +17111 m +1 h +1 h +10 h +1 h +17112 m +17113 m +10 h +10 h +185 h +195 h +83 h +4 h +10 h +368 h +987 m +10 h +359 h +1710 m +17114 m +73 h +4 h +17115 m +17116 m +4 h +10 h +1 h +83 h +17117 m +17118 m +109 h +10 h +4 h +4 h +17119 m +1 h +338 h +10 h +4 h +12131 m +4 h +17120 m +17121 m +587 m +2041 m +4 h +124 h +11 h +954 m +4 h +79 h +4 h +10 h +9585 m +801 m +1 h +11 h +1 h +10 h +10 h +3 h +10 h +11 h +17122 m +13821 m +17123 m +55 h +13 h +238 h +4 h +1 h +520 h +4 h +1 h +17124 m +116 m +1 h +3 h +1 h +1 h +3 h +4 h +941 m +4 h +4 h +4 h +4 h +10 h +4 h +17125 m +17126 m +918 m +601 h +10 h +4 h +4 h +5967 m +4 h +966 h +17127 m +4 h +4 h +17128 m +1 h +1737 m +1 h +1 h +3562 m +17129 m +1 h +65 h +17130 m +4 h +1 h +104 h +10 h +10 h +17131 m +4 h +11 h +109 h +10 h +17132 m +1 h +4 h +1725 m +443 h +17133 m +425 m +17134 m +332 h +124 h +4 h +10 h +17135 m +17136 m +59 h +31 h +17137 m +10 h +172 h +31 h +10 h +157 h +17138 m +4 h +1137 h +17139 m +4 h +4 h +17140 m +276 h +1 h +4 h +10 h +17141 m +4 h +4 h +10739 m +4 h +4 h +17142 m +146 h +1 h +10 h +59 h +1 h +4 h +4 h +1337 m +4 h +3 h +17143 m +1 h +1 h +7900 m +258 h +17144 m +74 h +1 h +4 h +266 h +4 h +10 h +4 h +82 h +124 h +17145 m +1 h +1 h +1 h +4 h +55 h +276 h +31 h +17146 m +1 h +10 h +266 h +143 h +10 h +204 h +13 h +59 h +41 h +17147 m +4 h +10 h +4 h +10 h +124 h +119 h +2617 m +1 h +1 h +45 h +17148 m +10 h +195 h +4 h +10 h +10 h +1 h +17149 m +4 h +74 h +295 h +147 h +41 h +1 h +17150 m +1 h +219 h +4 h +4 h +4 h +1 h +25 h +506 m +73 h +10 h +447 h +150 m +4 h +10 h +1 h +17151 m +4 h +25 h +10 h +1 h +10 h +124 h +4 h +36 h +17152 m +1 h +10 h +17153 m +1 h +4 h +17154 m +4 h +1 h +4 h +167 h +4 h +79 h +8486 m +11 h +17155 m +488 m +97 h +1 h +4 h +57 h +13334 m +4 h +3 h +1 h +184 h +1 h +57 h +4 h +41 h +1 h +1 h +17156 m +276 h +114 h +1 h +10 h +1 h +4 h +195 h +17157 m +1 h +10 h +59 h +124 h +307 h +10 h +10 h +10 h +810 m +1 h +10 h +10 h +27 h +1 h +1 h +4 h +1470 h +4 h +12898 m +4 h +274 h +195 h +1 h +36 h +10 h +119 h +27 h +1 h +403 h +10 h +10 h +1 h +59 h +4 h +10 h +16395 m +10 h +4 h +1 h +17158 m +383 h +387 m +4 h +10 h +1 h +4 h +17159 m +1796 m +1 h +17160 m +11 h +10 h +17161 m +10 h +5 h +1 h +1 h +17162 m +274 h +17163 m +113 h +488 m +4 h +3 h +4 h +196 h +147 h +1 h +56 h +17164 m +270 h +31 h +4 h +3 h +1 h +7 m +3 h +124 h +17165 m +4 h +2788 h +403 h +12 h +1 h +1053 m +17166 m +17167 m +433 m +4 h +1 h +97 h +4 h +1 h +74 h +8 h +17168 m +55 h +25 h +4 h +17169 m +10556 m +17170 m +1 h +13 h +10 h +1304 m +104 h +97 h +10 h +10 h +17171 m +10 h +1 h +4 h +10 h +687 h +4 h +4 h +4 h +82 h +17172 m +17173 m +488 h +10 h +1 h +45 h +11 h +3 h +17174 m +1 h +4 h +4 h +1 h +276 h +10 h +4 h +1 h +59 h +17175 m +113 h +10 h +196 h +164 h +195 h +4 h +17176 m +1 h +3601 m +10 h +135 h +10 h +11 h +59 h +17177 m +10 h +17178 m +17179 m +4 h +114 h +17180 m +888 h +10 h +1 h +17181 m +2558 m +4 h +17182 m +10 h +41 h +10 h +10 h +17183 m +1 h +10 h +17184 m +17185 m +12993 m +10 h +114 h +1 h +2438 m +1 h +1 h +14570 m +763 m +10 h +1 h +872 m +1 h +4256 m +1 h +17186 m +17187 m +1 h +4 h +1556 m +1 h +17188 m +17189 m +195 h +17190 m +17191 m +10 h +1 h +1542 m +1 h +1 h +265 h +4 h +10 h +10 h +125 h +258 h +17192 m +13 h +1 h +1 h +601 h +10 h +114 h +3 h +935 m +1 h +10 h +124 h +1 h +1 h +17193 m +1 h +12244 m +41 h +10 h +17194 m +41 h +4 h +1 h +4 h +84 m +17195 m +17196 m +1 h +4 h +4 h +1322 m +4 h +4 h +4 h +25 h +10 h +74 h +10 h +1 h +17197 m +4 h +82 h +12911 m +10 h +4 h +17198 m +17199 m +147 h +41 h +10 h +4 h +10 h +17200 m +4 h +10 h +146 h +1 h +4 h +174 m +4 h +57 h +25 h +4 h +17201 m +1 h +10 h +3 h +4 h +4 h +1 h +125 h +3 h +4 h +27 h +104 h +1 h +10 h +10 h +4 h +1 h +124 h +31 h +4 h +74 h +1 h +1 h +204 h +83 h +57 h +17202 m +4747 m +124 h +224 h +4 h +10 h +17203 m +10 h +65 h +10 h +4 h +65 h +4 h +79 h +10 h +4 h +10 h +10 h +578 h +17204 m +17205 m +1 h +1 h +265 h +4 h +11 h +478 m +6505 m +1 h +3276 m +4 h +12 h +17206 m +6304 m +10 h +4 h +4 h +4 h +265 h +4 h +1 h +1 h +769 m +17207 m +170 h +17208 m +10 h +10 h +4 h +1 h +17209 m +17210 m +1 h +10 h +1 h +10 h +1 h +17211 m +10 h +10 h +124 h +10 h +17212 m +17213 m +4 h +1 h +91 h +17214 m +11 h +4 h +17215 m +4 h +1 h +17216 m +10 h +10 h +4 h +1 h +1 h +4 h +25 h +10 h +59 h +1 h +17217 m +17218 m +17219 m +10 h +10 h +59 h +1 h +109 h +10 h +4409 m +4 h +4 h +4 h +10 h +4240 m +1 h +10 h +45 h +4 h +10 h +10 h +124 h +4 h +4 h +17220 m +146 h +17221 m +262 h +4 h +17222 m +1 h +10 h +196 h +135 h +12655 m +4240 m +17223 m +1 h +1 h +4 h +4 h +1 h +97 h +74 h +1 h +295 h +4 h +4 h +1 h +22 h +1 h +55 h +779 h +1 h +1 h +4 h +4 h +73 h +4 h +10 h +1 h +17224 m +17225 m +65 h +10 h +4 h +17226 m +1 h +10 h +1 h +3188 m +17227 m +59 h +4 h +181 h +56 h +17228 m +493 m +566 m +17229 m +17230 m +1 h +256 h +10 h +4 h +10 h +3 h +1 h +1 h +10 h +4 h +36 h +570 m +1 h +10 h +10 h +17231 m +17232 m +10 h +31 h +10 h +97 h +5760 m +4 h +12 h +4 h +4 h +3 h +4 h +1 h +4 h +1403 h +10 h +10 h +1 h +17233 m +640 h +4 h +4 h +17234 m +31 h +802 m +17235 m +1 h +25 h +17236 m +1 h +17237 m +1835 m +17238 m +1089 h +1 h +79 h +1 h +124 h +17239 m +4 h +17240 m +4 h +146 h +135 h +319 h +109 h +12 h +295 h +3 h +1 h +4 h +15377 m +11 h +4 h +10 h +10 h +10 h +10 h +1045 m +17241 m +1 h +17242 m +147 h +1 h +31 h +11 h +17243 m +1 h +4 h +1 h +1 h +10 h +10 h +13 h +17244 m +104 h +4 h +10 h +4576 m +4 h +31 h +4 h +124 h +17245 m +687 h +181 h +17246 m +17247 m +196 h +195 h +17248 m +4 h +82 h +25 h +17249 m +17250 m +4 h +4292 m +17251 m +17252 m +11 h +10 h +1 h +13 h +17253 m +10 h +1 h +10 h +1 h +10 h +17254 m +5917 m +601 h +4 h +97 h +4 h +1403 h +17255 m +114 h +82 h +114 h +73 h +17256 m +17257 m +10 h +1 h +7125 m +12 h +10 h +14723 m +10 h +1 h +238 h +108 h +4576 m +4 h +10 h +1955 m +1 h +4 h +4 h +1 h +10 h +10 h +17258 m +109 h +1105 h +17259 m +7938 m +1 h +4 h +11 h +3 h +4 h +36 h +1 h +4 h +17260 m +104 h +17261 m +10 h +4 h +12 h +4 h +1 h +124 h +10 h +82 h +17262 m +17263 m +4 h +41 h +113 h +4 h +10 h +355 h +295 h +17264 m +17265 m +11 h +11 h +17266 m +1 h +412 h +1 h +57 h +11 h +109 h +17267 m +1 h +874 m +195 h +123 h +69 h +4 h +276 h +186 h +4 h +41 h +82 h +1 h +1 h +4 h +1 h +4 h +737 m +172 h +443 h +27 h +8 h +1 h +4 h +17268 m +4 h +1 h +538 h +1 h +1 h +4 h +17269 m +36 h +124 h +4 h +11 h +84 m +195 h +520 h +17270 m +17271 m +371 h +10 h +4 h +17272 m +17273 m +119 h +1 h +17274 m +4 h +258 h +114 h +31 h +4 h +3779 m +1 h +4 h +92 h +17275 m +4 h +5522 m +147 h +1 h +4 h +17276 m +59 h +4 h +1886 h +10 h +4 h +17277 m +4 h +1 h +4229 m +2172 m +11 h +1406 m +17278 m +4 h +17279 m +4 h +4 h +17280 m +10 h +17281 m +1835 m +10 h +83 h +4 h +1 h +10 h +10 h +1 h +1 h +2025 m +843 h +17282 m +1 h +17283 m +3 h +10 h +10 h +4 h +4 h +17284 m +10 h +1 h +17285 m +1 h +1 h +1 h +4 h +4 h +10 h +1 h +4 h +17286 m +4 h +4 h +4 h +4 h +11 h +4 h +1 h +1 h +1 h +4 h +447 h +82 h +4 h +4 h +17287 m +3768 m +17288 m +1 h +4 h +238 h +17289 m +64 h +64 h +10 h +4 h +11 h +1790 m +17290 m +1868 m +4 h +146 h +17291 m +276 h +125 h +10 h +10 h +17292 m +83 h +190 h +1 h +1 h +4 h +109 h +4 h +17293 m +4 h +1 h +1 h +10 h +10 h +1 h +3 h +11 h +4 h +17294 m +1771 m +266 h +1 h +1 h +4 h +4 h +1 h +31 h +4 h +11 h +17295 m +59 h +10 h +10 h +10 h +1 h +124 h +4 h +10 h +41 h +10 h +6135 m +1 h +1 h +125 h +17296 m +57 h +11 h +11 h +94 h +17297 m +4 h +1 h +10 h +46 m +536 h +82 h +4 h +4 h +1 h +10 h +4 h +4 h +358 h +17298 m +17299 m +4 h +17300 m +17301 m +4 h +1 h +1 h +307 h +82 h +4 h +10 h +17302 m +1 h +109 h +17303 m +17304 m +1 h +1 h +1 h +59 h +1 h +10 h +1 h +4 h +1 h +12 h +2627 m +10900 m +124 h +4 h +4 h +36 h +124 h +1 h +4 h +10 h +82 h +66 m +299 h +1 h +1 h +4 h +1796 m +4 h +167 h +10 h +1337 m +135 h +1 h +1 h +17305 m +94 h +4 h +79 h +1 h +10 h +1 h +4 h +190 h +10 h +112 h +4 h +16338 m +65 h +1 h +10 h +17306 m +17307 m +10 h +4 h +10 h +181 h +1 h +11 h +8511 m +4 h +10 h +10 h +146 h +4 h +143 h +10 h +17308 m +1 h +17309 m +17310 m +4 h +41 h +10 h +83 h +25 h +10 h +17311 m +140 h +17312 m +1 h +1 h +17313 m +1 h +83 h +106 h +276 h +10 h +45 h +1 h +59 h +1 h +3 h +10 h +10 h +17314 m +17315 m +1 h +4 h +41 h +1574 m +1027 h +4 h +1 h +17316 m +10 h +17317 m +10 h +10 h +4 h +4 h +4 h +123 h +17318 m +13 h +2002 h +1 h +17319 m +3 h +4 h +1 h +4 h +109 h +125 h +74 h +17320 m +17321 m +4 h +10 h +10 h +1 h +10 h +258 h +17322 m +4 h +10 h +17323 m +4 h +4 h +17324 m +4 h +82 h +59 h +82 h +4 h +59 h +17325 m +17326 m +124 h +1975 m +17327 m +4 h +1 h +10 h +4 h +94 h +79 h +69 h +1 h +17328 m +4 h +1 h +17329 m +1 h +10 h +1 h +10 h +12 h +48 h +124 h +124 h +17330 m +17331 m +1 h +5537 m +113 h +338 h +4 h +1 h +17332 m +4 h +110 h +10 h +17333 m +359 h +17334 m +1 h +1 h +4 h +4 h +10 h +1 h +4 h +17335 m +4 h +10 h +1 h +1 h +10 h +17336 m +1 h +17337 m +4 h +10 h +4 h +17338 m +10 h +170 h +17339 m +17340 m +17341 m +104 h +17342 m +11 h +1 h +1 h +1 h +4 h +1 h +10 h +124 h +10 h +4 h +295 h +4 h +1 h +11 h +4 h +10 h +4 h +278 h +59 h +123 h +1 h +4 h +10 h +10 h +278 h +59 h +17343 m +65 h +1 h +224 h +17344 m +17345 m +536 h +4 h +1 h +10 h +124 h +13 h +258 h +4 h +45 h +17346 m +17347 m +4 h +10 h +11 h +94 h +73 h +4 h +56 h +8767 m +10 h +4 h +73 h +1 h +17348 m +4 h +55 h +17349 m +1 h +15790 m +17350 m +10 h +17351 m +1 h +1 h +270 h +17352 m +65 h +10 h +17353 m +10 h +1 h +105 m +4 h +10 h +4 h +1 h +10 h +1725 m +83 h +59 h +1 h +45 h +266 h +124 h +97 h +4 h +1 h +104 h +4 h +17354 m +195 h +17355 m +190 h +4 h +10 h +106 h +888 h +45 h +1 h +4 h +17356 m +4 h +4 h +41 h +1 h +82 h +110 h +266 h +4 h +276 h +17357 m +170 h +11 h +10 h +10 h +41 h +10 h +1 h +4 h +823 m +1 h +4 h +10 h +10 h +135 h +1 h +229 h +119 h +173 h +1 h +4 h +167 h +10 h +10 h +4 h +800 m +4 h +17358 m +17359 m +17360 m +55 h +4 h +65 h +10 h +4 h +1 h +97 h +17361 m +17362 m +172 h +156 h +4 h +1 h +383 h +17363 m +4 h +1 h +4 h +2720 m +4 h +169 h +10 h +82 h +4 h +4 h +1 h +33 m +6132 m +17364 m +3 h +1 h +4 h +4 h +4 h +11 h +17365 m +82 h +17366 m +4 h +1 h +10 h +4 h +4 h +1 h +918 m +1 h +11 h +10 h +17367 m +41 h +1 h +65 h +1 h +1362 m +17368 m +74 h +1249 m +17369 m +1 h +82 h +57 h +4 h +3 h +1 h +4 h +59 h +125 h +10 h +22 h +83 h +1 h +4 h +4350 m +10 h +1 h +2925 m +10 h +17370 m +59 h +4 h +1 h +986 h +82 h +4 h +1309 h +17371 m +275 m +1 h +55 h +17372 m +170 h +1 h +10 h +1 h +17373 m +17374 m +1 h +4 h +1822 h +1 h +4 h +1 h +17375 m +4 h +17376 m +2418 m +57 h +4 h +17377 m +13879 m +10 h +45 h +4 h +17378 m +1772 m +17379 m +17380 m +4 h +4 h +4 h +763 m +3 h +4 h +31 h +125 h +1 h +10 h +265 h +4 h +57 h +10 h +10 h +1 h +1 h +173 h +10 h +4 h +27 h +4 h +17381 m +113 h +1 h +10 h +4 h +25 h +17382 m +989 m +3 h +4 h +10 h +17383 m +17384 m +156 h +3622 m +4 h +4 h +17385 m +12 h +4 h +10365 m +17386 m +169 h +1 h +4 h +94 h +4 h +368 h +109 h +229 h +17387 m +17388 m +10 h +57 h +17389 m +4 h +1 h +82 h +1 h +1 h +17390 m +82 h +1981 m +10 h +1100 m +1 h +105 m +17391 m +1 h +17392 m +146 h +1 h +11 h +55 h +10 h +64 h +17393 m +17394 m +11 h +4 h +65 h +266 h +17395 m +4 h +92 h +1 h +358 h +262 h +57 h +1 h +2786 m +4 h +1 h +1 h +10099 m +4 h +4 h +4 h +109 h +4 h +4 h +11 h +17396 m +4 h +112 h +17397 m +4 h +1 h +17398 m +82 h +17399 m +17400 m +1 h +4 h +10 h +17401 m +4 h +17402 m +332 h +1 h +1 h +10 h +17403 m +1 h +1 h +986 h +73 h +4 h +17404 m +4 h +125 h +4 h +3 h +73 h +4 h +10 h +1 h +12 h +1 h +59 h +1 h +4 h +17405 m +493 m +4 h +4 h +10 h +73 h +17406 m +17407 m +82 h +17408 m +10937 m +10 h +1 h +10 h +10 h +196 h +7535 m +12 h +4 h +1 h +10 h +4 h +1 h +1 h +17409 m +82 h +1 h +17410 m +124 h +17411 m +1 h +4 h +1 h +10 h +1 h +10 h +3089 m +1 h +11 h +1 h +1 h +1 h +4 h +57 h +1 h +4 h +4 h +4 h +17412 m +1 h +1 h +17413 m +17414 m +94 h +17415 m +4 h +4 h +4 h +40 h +17416 m +265 h +4 h +73 h +601 h +1 h +4145 m +10 h +2887 h +1 h +4 h +3 h +17417 m +10 h +4 h +986 h +10 h +10 h +10 h +464 h +4 h +10 h +17418 m +4 h +17419 m +10 h +123 h +17420 m +17421 m +10 h +17422 m +11 h +17423 m +4 h +4 h +91 h +4 h +4 h +4 h +1 h +10 h +10 h +82 h +17424 m +13 h +144 h +4 h +1 h +17425 m +93 m +12 h +1 h +1 h +338 h +278 h +4 h +190 h +4 h +295 h +642 h +4 h +17426 m +119 h +4 h +10 h +17427 m +1 h +17428 m +10 h +17429 m +59 h +17430 m +1 h +82 h +1 h +4 h +1 h +25 h +4 h +4 h +17431 m +4 h +10 h +17432 m +10 h +4 h +17433 m +45 h +10 h +17434 m +4 h +1 h +17435 m +1 h +17436 m +17437 m +1 h +17438 m +258 h +1 h +1 h +319 h +4 h +4 h +10 h +1 h +17439 m +17440 m +4 h +1 h +17441 m +4 h +10 h +146 h +17442 m +82 h +10 h +4 h +157 h +4 h +25 h +11 h +4 h +10 h +11 h +4 h +692 h +36 h +17443 m +4 h +4 h +1 h +17444 m +1 h +4 h +125 h +1 h +10 h +1337 m +4 h +1 h +4 h +10 h +1 h +1 h +10 h +124 h +10 h +2379 h +4 h +1389 m +4 h +10 h +28 h +17445 m +59 h +1 h +3161 m +17446 m +4 h +4 h +124 h +10 h +83 h +4 h +11 h +82 h +11 h +17447 m +135 h +316 h +196 h +10 h +4 h +1 h +825 m +4 h +74 h +17448 m +10 h +10 h +59 h +447 h +10 h +17449 m +13811 m +45 h +17450 m +4 h +17451 m +17452 m +1 h +4 h +73 h +1 h +184 h +173 h +4 h +276 h +17453 m +1 h +4 h +1 h +900 m +4 h +4 h +4 h +4 h +10 h +1 h +4 h +31 h +4297 m +13 h +270 h +170 h +17454 m +1 h +4 h +4 h +4 h +17455 m +1 h +16871 m +4 h +1 h +698 m +4 h +59 h +79 h +17456 m +4 h +147 h +11 h +358 h +10 h +147 h +109 h +1 h +172 h +10 h +10 h +1 h +4932 m +11 h +1 h +17457 m +2558 m +229 h +238 h +583 m +25 h +195 h +27 h +1 h +17458 m +4 h +1 h +1 h +4 h +10 h +1 h +10 h +4 h +4 h +8318 m +4 h +109 h +4 h +17459 m +4 h +1 h +10 h +4 h +10 h +57 h +7553 m +10 h +94 h +1 h +17460 m +1 h +1185 m +4 h +27 h +11 h +4 h +91 h +4 h +41 h +10 h +1 h +10 h +4 h +17461 m +295 h +181 h +17462 m +4 h +124 h +1 h +17463 m +17464 m +4 h +1 h +285 m +4 h +524 m +195 h +4384 m +4 h +17465 m +1 h +4 h +4 h +4 h +17466 m +17467 m +262 h +17468 m +4 h +17469 m +64 h +195 h +109 h +17470 m +8 h +4 h +4 h +447 h +4 h +59 h +1 h +4 h +1772 m +83 h +10 h +4 h +10 h +1 h +4 h +2205 m +17471 m +13 h +31 h +77 h +1 h +4 h +17472 m +4 h +1 h +1 h +4 h +4 h +59 h +1 h +4 h +1 h +31 h +16512 m +17473 m +1 h +1 h +2865 m +1 h +10 h +4 h +718 h +10 h +1 h +4 h +4 h +17474 m +4359 m +64 h +698 m +45 h +17475 m +4 h +17476 m +1 h +322 h +4 h +36 h +56 h +4 h +307 h +17477 m +59 h +10 h +147 h +1 h +1 h +125 h +5162 m +10 h +10 h +1 h +4 h +4 h +10 h +4 h +4 h +12 h +4 h +1 h +4 h +17478 m +10 h +1 h +17479 m +4 h +17480 m +10 h +11654 m +135 h +4 h +4 h +4 h +1 h +4 h +4 h +4 h +17481 m +10 h +4 h +45 h +17482 m +3 h +1 h +203 h +1 h +1 h +4 h +1 h +1 h +17483 m +4 h +11 h +4 h +10 h +10 h +1 h +10 h +4 h +124 h +11 h +17484 m +4 h +353 h +1 h +55 h +195 h +64 h +1322 m +124 h +4 h +4 h +125 h +17485 m +274 h +1 h +17486 m +3 h +4 h +4 h +1 h +10 h +57 h +10 h +238 h +4 h +6371 m +1 h +11 h +17487 m +17488 m +1556 m +196 h +1 h +316 h +4 h +17489 m +4 h +169 h +17490 m +25 h +17491 m +4 h +1 h +17492 m +10 h +327 m +1 h +383 h +17493 m +1 h +270 h +4 h +17494 m +737 m +10 h +124 h +4 h +109 h +359 h +10 h +143 h +17495 m +12956 m +10 h +3159 m +1 h +17496 m +649 m +4 h +4 h +109 h +4538 m +17497 m +59 h +1 h +4 h +4 h +4 h +1 h +1 h +7243 m +4 h +1 h +82 h +1 h +1 h +1 h +3188 m +17498 m +17499 m +4 h +1 h +4 h +10 h +4 h +4 h +10 h +4 h +17500 m +41 h +4 h +1667 m +1 h +444 m +10 h +4 h +17501 m +17502 m +10 h +4 h +125 h +4 h +3 h +17503 m +1 h +1 h +1 h +17504 m +124 h +4349 m +17505 m +4 h +17506 m +8 h +17507 m +55 h +57 h +1665 m +185 h +10 h +17508 m +1 h +258 h +4 h +1 h +57 h +2733 h +104 h +4 h +170 h +10 h +4 h +1 h +1 h +147 h +1 h +124 h +11 h +17509 m +4 h +17510 m +10 h +10 h +17511 m +25 h +258 h +17512 m +79 h +97 h +3908 m +1 h +1 h +40 h +10 h +10 h +1 h +118 h +2022 m +1 h +27 h +1 h +4 h +1 h +146 h +10 h +17513 m +295 h +17514 m +1 h +4 h +1 h +1 h +4 h +1 h +1 h +17515 m +124 h +2438 m +17516 m +230 h +25 h +17517 m +1 h +1 h +17518 m +25 h +10 h +17519 m +570 m +10 h +4 h +390 m +4 h +11 h +1 h +4 h +4 h +1 h +59 h +4 h +4 h +17520 m +4 h +109 h +4 h +17521 m +4 h +17522 m +10 h +4 h +146 h +935 m +17523 m +1 h +1 h +10 h +1772 h +17524 m +4 h +1 h +4 h +11 h +104 h +64 h +83 h +36 h +1 h +74 h +82 h +4 h +265 h +109 h +338 h +784 m +4 h +5387 m +156 h +4 h +17525 m +4 h +1 h +17526 m +601 h +15455 m +4 h +4 h +17527 m +10 h +4 h +1 h +170 h +8 h +4 h +57 h +1 h +17528 m +17529 m +1619 h +4 h +4 h +17530 m +3 h +185 h +17531 m +12192 m +17532 m +114 h +4 h +125 h +192 h +4 h +1 h +73 h +69 h +82 h +1 h +4 h +11 h +10 h +10 h +11 h +10 h +4 h +4 h +4 h +57 h +1 h +1 h +17533 m +17534 m +17535 m +9256 m +4 h +17536 m +1030 m +167 h +83 h +1 h +1 h +1 h +1 h +56 h +4 h +59 h +4 h +1 h +4 h +10 h +1 h +11 h +1372 m +10 h +124 h +112 h +1 h +4 h +4 h +266 h +1 h +4 h +79 h +181 h +73 h +4 h +17537 m +17538 m +124 h +10 h +4 h +17539 m +1 h +4 h +17540 m +332 h +10383 m +266 h +1 h +1 h +1 h +4 h +4 h +1 h +447 h +4 h +1 h +17541 m +4 h +109 h +17542 m +17543 m +110 h +4 h +4 h +4 h +97 h +10 h +10 h +13 h +10 h +4 h +1 h +59 h +79 h +27 h +10 h +17544 m +10 h +17545 m +258 h +4 h +2607 m +57 h +1 h +157 h +1 h +4 h +10 h +4 h +185 h +10 h +17546 m +4 h +17547 m +17548 m +10 h +17549 m +1 h +17550 m +17551 m +11 h +1 h +4 h +17552 m +1403 h +4 h +17553 m +10 h +4 h +17554 m +307 h +1 h +99 m +4 h +10 h +104 h +164 h +14814 m +119 h +9912 m +4 h +10 h +196 h +25 h +479 h +1 h +536 h +1 h +1 h +4 h +1 h +17555 m +11 h +885 m +97 h +109 h +4 h +4 h +1 h +139 h +4 h +10 h +687 h +12 h +97 h +10 h +4 h +45 h +1 h +1 h +8711 m +4 h +4 h +33 m +17556 m +82 h +4 h +10 h +83 h +4 h +1 h +169 h +10 h +73 h +4 h +11334 m +10 h +10 h +1 h +82 h +1 h +57 h +278 h +17557 m +4 h +5141 m +1 h +17558 m +17559 m +17560 m +10 h +1 h +17561 m +16841 m +630 m +2617 m +9397 m +65 h +83 h +4 h +4 h +4 h +79 h +4 h +4 h +196 h +1 h +17562 m +4 h +4 h +4 h +1691 m +17563 m +110 h +172 h +125 h +83 h +55 h +13 h +10 h +83 h +1 h +195 h +1 h +17564 m +10 h +8503 m +17565 m +3 h +10 h +10 h +10 h +1 h +295 h +1 h +170 h +4 h +4 h +5505 m +1 h +4 h +4 h +17566 m +17567 m +124 h +10 h +1027 h +1 h +17568 m +104 h +10 h +468 h +4 h +2733 h +124 h +4867 m +4 h +2172 m +4 h +4 h +1 h +4 h +8 h +17569 m +146 h +4441 h +41 h +17570 m +4 h +4 h +4 h +1 h +11 h +83 h +4 h +17571 m +4 h +17572 m +10 h +17573 m +10 h +22 h +1 h +10 h +1 h +17574 m +97 h +4350 m +14316 m +794 m +4 h +12020 m +4 h +17575 m +17576 m +10 h +4 h +4 h +17577 m +10 h +17578 m +4 h +230 h +4 h +4 h +109 h +7800 m +17579 m +4 h +10 h +143 h +4 h +1 h +10 h +31 h +17580 m +4 h +4 h +1 h +4 h +10 h +4 h +5348 m +77 h +4 h +1 h +186 h +119 h +4 h +4 h +17581 m +4 h +17582 m +10 h +17583 m +65 h +331 m +4 h +48 h +17584 m +125 h +185 h +170 h +17585 m +17586 m +17587 m +467 m +10 h +1 h +278 h +1074 h +25 h +17588 m +17589 m +17590 m +48 h +17591 m +74 h +464 h +94 h +4 h +1 h +4 h +2532 m +57 h +4 h +4 h +1 h +17592 m +4 h +97 h +17593 m +125 h +1027 h +17594 m +3 h +17595 m +1 h +10 h +692 h +238 h +156 h +11 h +10 h +10 h +4 h +1 h +1 h +1 h +172 h +1 h +109 h +17596 m +4 h +17597 m +1 h +57 h +13790 m +1768 m +4 h +17598 m +4 h +8741 m +17599 m +73 h +1 h +4 h +1 h +167 h +17600 m +185 h +1 h +1 h +1 h +17601 m +10 h +1 h +1 h +10 h +1 h +4 h +1 h +173 h +4 h +4 h +55 h +4 h +1619 h +10 h +1 h +278 h +195 h +1 h +11 h +59 h +10 h +4 h +4904 m +1 h +48 h +4 h +10 h +4 h +17602 m +4 h +59 h +1 h +8 h +4 h +1 h +11 h +4 h +110 h +4 h +4 h +1 h +1045 m +17603 m +97 h +1 h +8 h +1 h +3657 m +1 h +367 m +17604 m +1 h +1 h +2585 m +156 h +4 h +64 h +4 h +17605 m +4 h +10 h +4 h +17606 m +17607 m +17608 m +10 h +4 h +17609 m +4 h +25 h +4 h +332 h +4 h +4 h +12 h +17610 m +3 h +4 h +10 h +17611 m +57 h +1791 m +17612 m +4 h +8 h +1 h +4 h +1 h +10 h +17613 m +1 h +4 h +4 h +1 h +10 h +17614 m +146 h +4 h +1 h +1 h +4 h +4 h +28 h +4 h +57 h +4 h +307 h +112 h +11 h +97 h +11 h +1 h +10 h +22 h +10 h +3 h +17615 m +119 h +806 m +10 h +17616 m +135 h +4 h +79 h +4 h +1261 h +11 h +1 h +4 h +157 h +4 h +82 h +3 h +59 h +45 h +97 h +17617 m +17618 m +1 h +31 h +65 h +11 h +41 h +266 h +1 h +4 h +4 h +4 h +17619 m +10 h +4 h +229 h +1 h +1 h +10 h +1 h +276 h +10 h +27 h +10 h +17620 m +4 h +383 h +59 h +17621 m +581 m +4 h +4 h +1250 h +41 h +92 h +17622 m +990 m +10 h +1 h +17623 m +4 h +1 h +4 h +279 h +601 h +4 h +1 h +4 h +57 h +10 h +1 h +4 h +4 h +6851 m +4 h +157 h +135 h +10 h +1 h +1 h +4 h +1 h +4 h +258 h +14708 m +3 h +17624 m +1 h +146 h +55 h +262 h +158 h +1 h +4 h +1 h +1 h +13007 m +124 h +169 h +17625 m +170 h +4 h +17626 m +911 h +4 h +156 h +4 h +1 h +143 h +17627 m +4 h +41 h +10 h +4 h +17628 m +4 h +56 h +4 h +125 h +11 h +124 h +4292 m +17629 m +97 h +124 h +2733 h +36 h +4 h +17630 m +1 h +1260 m +4 h +10 h +17631 m diff --git a/pebble/internal/cache/value.go b/pebble/internal/cache/value.go new file mode 100644 index 0000000..6d2cae1 --- /dev/null +++ b/pebble/internal/cache/value.go @@ -0,0 +1,46 @@ +// Copyright 2020 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 cache + +// Value holds a reference counted immutable value. +type Value struct { + buf []byte + // Reference count for the value. The value is freed when the reference count + // drops to zero. + ref refcnt +} + +// Buf returns the buffer associated with the value. The contents of the buffer +// should not be changed once the value has been added to the cache. Instead, a +// new Value should be created and added to the cache to replace the existing +// value. +func (v *Value) Buf() []byte { + if v == nil { + return nil + } + return v.buf +} + +// Truncate the buffer to the specified length. The buffer length should not be +// changed once the value has been added to the cache as there may be +// concurrent readers of the Value. Instead, a new Value should be created and +// added to the cache to replace the existing value. +func (v *Value) Truncate(n int) { + v.buf = v.buf[:n] +} + +func (v *Value) refs() int32 { + return v.ref.refs() +} + +func (v *Value) acquire() { + v.ref.acquire() +} + +func (v *Value) release() { + if v != nil && v.ref.release() { + v.free() + } +} diff --git a/pebble/internal/cache/value_invariants.go b/pebble/internal/cache/value_invariants.go new file mode 100644 index 0000000..1e30d27 --- /dev/null +++ b/pebble/internal/cache/value_invariants.go @@ -0,0 +1,55 @@ +// Copyright 2020 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 (invariants && !race) || (tracing && !race) +// +build invariants,!race tracing,!race + +package cache + +import ( + "fmt" + "os" + + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/manual" +) + +// newValue creates a Value with a manually managed buffer of size n. +// +// This definition of newValue is used when either the "invariants" or +// "tracing" build tags are specified. It hooks up a finalizer to the returned +// Value that checks for memory leaks when the GC determines the Value is no +// longer reachable. +func newValue(n int) *Value { + if n == 0 { + return nil + } + b := manual.New(n) + v := &Value{buf: b} + v.ref.init(1) + // Note: this is a no-op if invariants and tracing are disabled or race is + // enabled. + invariants.SetFinalizer(v, func(obj interface{}) { + v := obj.(*Value) + if v.buf != nil { + fmt.Fprintf(os.Stderr, "%p: cache value was not freed: refs=%d\n%s", + v, v.refs(), v.ref.traces()) + os.Exit(1) + } + }) + return v +} + +func (v *Value) free() { + // When "invariants" are enabled set the value contents to 0xff in order to + // cache use-after-free bugs. + for i := range v.buf { + v.buf[i] = 0xff + } + manual.Free(v.buf) + // Setting Value.buf to nil is needed for correctness of the leak checking + // that is performed when the "invariants" or "tracing" build tags are + // enabled. + v.buf = nil +} diff --git a/pebble/internal/cache/value_normal.go b/pebble/internal/cache/value_normal.go new file mode 100644 index 0000000..e03379d --- /dev/null +++ b/pebble/internal/cache/value_normal.go @@ -0,0 +1,57 @@ +// Copyright 2020 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 (!invariants && !tracing) || race +// +build !invariants,!tracing race + +package cache + +import ( + "unsafe" + + "github.com/cockroachdb/pebble/internal/manual" +) + +const valueSize = int(unsafe.Sizeof(Value{})) + +func newValue(n int) *Value { + if n == 0 { + return nil + } + + if !cgoEnabled { + // If Cgo is disabled then all memory is allocated from the Go heap and we + // can't play the trick below to combine the Value and buffer allocation. + v := &Value{buf: make([]byte, n)} + v.ref.init(1) + return v + } + + // When we're not performing leak detection, the lifetime of the returned + // Value is exactly the lifetime of the backing buffer and we can manually + // allocate both. + // + // TODO(peter): It may be better to separate the allocation of the value and + // the buffer in order to reduce internal fragmentation in malloc. If the + // buffer is right at a power of 2, adding valueSize might push the + // allocation over into the next larger size. + b := manual.New(valueSize + n) + v := (*Value)(unsafe.Pointer(&b[0])) + v.buf = b[valueSize:] + v.ref.init(1) + return v +} + +func (v *Value) free() { + if !cgoEnabled { + return + } + + // When we're not performing leak detection, the Value and buffer were + // allocated contiguously. + n := valueSize + cap(v.buf) + buf := (*[manual.MaxArrayLen]byte)(unsafe.Pointer(v))[:n:n] + v.buf = nil + manual.Free(buf) +} diff --git a/pebble/internal/constants/constants.go b/pebble/internal/constants/constants.go new file mode 100644 index 0000000..8d9198c --- /dev/null +++ b/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)<>15|c<<17) + 0xa282ead8 +} diff --git a/pebble/internal/datatest/datatest.go b/pebble/internal/datatest/datatest.go new file mode 100644 index 0000000..40f78d5 --- /dev/null +++ b/pebble/internal/datatest/datatest.go @@ -0,0 +1,140 @@ +// 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 datatest provides common datadriven test commands for use outside of +// the root Pebble package. +package datatest + +import ( + "strings" + "sync" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" +) + +// TODO(jackson): Consider a refactoring that can consolidate this package and +// the datadriven commands defined in pebble/data_test.go. + +// DefineBatch interprets the provided datadriven command as a sequence of write +// operations, one-per-line, to apply to the provided batch. +func DefineBatch(d *datadriven.TestData, b *pebble.Batch) error { + for _, line := range strings.Split(d.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + if parts[1] == `` { + parts[1] = "" + } + var err error + switch parts[0] { + case "set": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + err = b.Set([]byte(parts[1]), []byte(parts[2]), nil) + case "del": + if len(parts) != 2 { + return errors.Errorf("%s expects 1 argument", parts[0]) + } + err = b.Delete([]byte(parts[1]), nil) + case "singledel": + if len(parts) != 2 { + return errors.Errorf("%s expects 1 argument", parts[0]) + } + err = b.SingleDelete([]byte(parts[1]), nil) + case "del-range": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + err = b.DeleteRange([]byte(parts[1]), []byte(parts[2]), nil) + case "merge": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + err = b.Merge([]byte(parts[1]), []byte(parts[2]), nil) + case "range-key-set": + if len(parts) != 5 { + return errors.Errorf("%s expects 4 arguments", parts[0]) + } + err = b.RangeKeySet( + []byte(parts[1]), + []byte(parts[2]), + []byte(parts[3]), + []byte(parts[4]), + nil) + case "range-key-unset": + if len(parts) != 4 { + return errors.Errorf("%s expects 3 arguments", parts[0]) + } + err = b.RangeKeyUnset( + []byte(parts[1]), + []byte(parts[2]), + []byte(parts[3]), + nil) + case "range-key-del": + if len(parts) != 3 { + return errors.Errorf("%s expects 2 arguments", parts[0]) + } + err = b.RangeKeyDelete( + []byte(parts[1]), + []byte(parts[2]), + nil) + default: + return errors.Errorf("unknown op: %s", parts[0]) + } + if err != nil { + return err + } + } + return nil +} + +// CompactionTracker is a listener that tracks the number of compactions. +type CompactionTracker struct { + sync.Cond + count int + attached bool +} + +// NewCompactionTracker setups the necessary options to keep track of the +// compactions that are in flight. +func NewCompactionTracker(options *pebble.Options) *CompactionTracker { + ct := CompactionTracker{} + ct.Cond = sync.Cond{ + L: &sync.Mutex{}, + } + ct.attached = true + el := pebble.EventListener{ + CompactionEnd: func(info pebble.CompactionInfo) { + ct.L.Lock() + ct.count-- + ct.Broadcast() + ct.L.Unlock() + }, + CompactionBegin: func(info pebble.CompactionInfo) { + ct.L.Lock() + ct.count++ + ct.Broadcast() + ct.L.Unlock() + }, + } + + options.AddEventListener(el) + return &ct +} + +// WaitForInflightCompactionsToEqual waits until compactions meet the specified target. +func (cql *CompactionTracker) WaitForInflightCompactionsToEqual(target int) { + cql.L.Lock() + if !cql.attached { + panic("Cannot wait for compactions if listener has not been attached") + } + for cql.count != target { + cql.Wait() + } + cql.L.Unlock() +} diff --git a/pebble/internal/dsl/dsl.go b/pebble/internal/dsl/dsl.go new file mode 100644 index 0000000..ef546fd --- /dev/null +++ b/pebble/internal/dsl/dsl.go @@ -0,0 +1,160 @@ +// 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 dsl provides facilities for parsing lisp-like domain-specific +// languages (DSL). +package dsl + +import ( + "fmt" + "go/scanner" + "go/token" + "strconv" + "strings" + + "github.com/cockroachdb/errors" +) + +// NewParser constructs a new Parser of a lisp-like DSL. +func NewParser[T any]() *Parser[T] { + p := new(Parser[T]) + p.constants = make(map[string]func() T) + p.funcs = make(map[string]func(*Parser[T], *Scanner) T) + return p +} + +// NewPredicateParser constructs a new Parser of a Lisp-like DSL, where the +// resulting type implements Predicate[E]. NewPredicateParser predefines a few +// useful functions: Not, And, Or, OnIndex. +func NewPredicateParser[E any]() *Parser[Predicate[E]] { + p := NewParser[Predicate[E]]() + p.DefineFunc("Not", parseNot[E]) + p.DefineFunc("And", parseAnd[E]) + p.DefineFunc("Or", parseOr[E]) + p.DefineFunc("OnIndex", parseOnIndex[E]) + return p +} + +// A Parser holds the rules and logic for parsing a DSL. +type Parser[T any] struct { + constants map[string]func() T + funcs map[string]func(*Parser[T], *Scanner) T +} + +// DefineConstant adds a new constant to the Parser's supported DSL. Whenever +// the provided identifier is used within a constant context, the provided +// closure is invoked to instantiate an appropriate AST value. +func (p *Parser[T]) DefineConstant(identifier string, instantiate func() T) { + p.constants[identifier] = instantiate +} + +// DefineFunc adds a new func to the Parser's supported DSL. Whenever the +// provided identifier is used within a function invocation context, the +// provided closure is invoked to instantiate an appropriate AST value. +func (p *Parser[T]) DefineFunc(identifier string, parseFunc func(*Parser[T], *Scanner) T) { + p.funcs[identifier] = parseFunc +} + +// Parse parses the provided input string. +func (p *Parser[T]) Parse(d string) (ret T, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + panic(r) + } + } + }() + + fset := token.NewFileSet() + file := fset.AddFile("", -1, len(d)) + var s Scanner + s.Init(file, []byte(strings.TrimSpace(d)), nil /* no error handler */, 0) + tok := s.Scan() + ret = p.ParseFromPos(&s, tok) + tok = s.Scan() + if tok.Kind == token.SEMICOLON { + tok = s.Scan() + } + assertTok(tok, token.EOF) + return ret, err +} + +// ParseFromPos parses from the provided current position and associated +// scanner. If the parser fails to parse, it panics. This function is intended +// to be used when composing Parsers of various types. +func (p *Parser[T]) ParseFromPos(s *Scanner, tok Token) T { + switch tok.Kind { + case token.IDENT: + // A constant without any parens, eg. `Reads`. + p, ok := p.constants[tok.Lit] + if !ok { + panic(errors.Errorf("dsl: unknown constant %q", tok.Lit)) + } + return p() + case token.LPAREN: + // Otherwise it's an expression, eg: (OnIndex 1) + tok = s.Consume(token.IDENT) + fp, ok := p.funcs[tok.Lit] + if !ok { + panic(errors.Errorf("dsl: unknown func %q", tok.Lit)) + } + return fp(p, s) + default: + panic(errors.Errorf("dsl: unexpected token %s; expected IDENT or LPAREN", tok.String())) + } +} + +// A Scanner holds the scanner's internal state while processing a given text. +type Scanner struct { + scanner.Scanner +} + +// Scan scans the next token and returns it. +func (s *Scanner) Scan() Token { + pos, tok, lit := s.Scanner.Scan() + return Token{pos, tok, lit} +} + +// Consume scans the next token. If the token is not of the provided token, it +// panics. It returns the token itself. +func (s *Scanner) Consume(expect token.Token) Token { + t := s.Scan() + assertTok(t, expect) + return t +} + +// ConsumeString scans the next token. It panics if the next token is not a +// string, or if unable to unquote the string. It returns the unquoted string +// contents. +func (s *Scanner) ConsumeString() string { + lit := s.Consume(token.STRING).Lit + str, err := strconv.Unquote(lit) + if err != nil { + panic(errors.Newf("dsl: unquoting %q: %v", lit, err)) + } + return str +} + +// Token is a lexical token scanned from an input text. +type Token struct { + pos token.Pos + Kind token.Token + Lit string +} + +// String implements fmt.Stringer. +func (t *Token) String() string { + if t.Lit != "" { + return fmt.Sprintf("(%s, %q) at pos %v", t.Kind, t.Lit, t.pos) + } + return fmt.Sprintf("%s at pos %v", t.Kind, t.pos) +} + +func assertTok(tok Token, expect token.Token) { + if tok.Kind != expect { + panic(errors.Errorf("dsl: unexpected token %s; expected %s", tok.String(), expect)) + } +} diff --git a/pebble/internal/dsl/predicates.go b/pebble/internal/dsl/predicates.go new file mode 100644 index 0000000..fff0fcd --- /dev/null +++ b/pebble/internal/dsl/predicates.go @@ -0,0 +1,136 @@ +// 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 dsl + +import ( + "fmt" + "go/token" + "strconv" + "strings" + "sync/atomic" + + "github.com/cockroachdb/errors" +) + +// Predicate encodes conditional logic that yields a boolean. +type Predicate[E any] interface { + Evaluate(E) bool + String() string +} + +// Not returns a Predicate that negates the provided predicate. +func Not[E any](p Predicate[E]) Predicate[E] { return not[E]{Predicate: p} } + +// And returns a Predicate that evaluates to true if all its operands evaluate +// to true. +func And[E any](preds ...Predicate[E]) Predicate[E] { return and[E](preds) } + +// Or returns a Predicate that evaluates to true if any of its operands evaluate +// true. +func Or[E any](preds ...Predicate[E]) Predicate[E] { return or[E](preds) } + +// OnIndex returns a Predicate that evaluates to true on its N-th call. +func OnIndex[E any](n int32) *Index[E] { + p := new(Index[E]) + p.Int32.Store(n) + return p +} + +// Index is a Predicate that evaluates to true only on its N-th invocation. +type Index[E any] struct { + atomic.Int32 +} + +// String implements fmt.Stringer. +func (p *Index[E]) String() string { + return fmt.Sprintf("(OnIndex %d)", p.Int32.Load()) +} + +// Evaluate implements Predicate. +func (p *Index[E]) Evaluate(E) bool { return p.Int32.Add(-1) == -1 } + +type not[E any] struct { + Predicate[E] +} + +func (p not[E]) String() string { return fmt.Sprintf("(Not %s)", p.Predicate.String()) } +func (p not[E]) Evaluate(e E) bool { return !p.Predicate.Evaluate(e) } + +type and[E any] []Predicate[E] + +func (p and[E]) String() string { + var sb strings.Builder + sb.WriteString("(And") + for i := 0; i < len(p); i++ { + sb.WriteRune(' ') + sb.WriteString(p[i].String()) + } + sb.WriteRune(')') + return sb.String() +} + +func (p and[E]) Evaluate(e E) bool { + ok := true + for i := range p { + ok = ok && p[i].Evaluate(e) + } + return ok +} + +type or[E any] []Predicate[E] + +func (p or[E]) String() string { + var sb strings.Builder + sb.WriteString("(Or") + for i := 0; i < len(p); i++ { + sb.WriteRune(' ') + sb.WriteString(p[i].String()) + } + sb.WriteRune(')') + return sb.String() +} + +func (p or[E]) Evaluate(e E) bool { + ok := false + for i := range p { + ok = ok || p[i].Evaluate(e) + } + return ok +} + +func parseNot[E any](p *Parser[Predicate[E]], s *Scanner) Predicate[E] { + preds := parseVariadicPredicate(p, s) + if len(preds) != 1 { + panic(errors.Newf("dsl: not accepts exactly 1 argument, given %d", len(preds))) + } + return not[E]{Predicate: preds[0]} +} + +func parseAnd[E any](p *Parser[Predicate[E]], s *Scanner) Predicate[E] { + return And[E](parseVariadicPredicate[E](p, s)...) +} + +func parseOr[E any](p *Parser[Predicate[E]], s *Scanner) Predicate[E] { + return Or[E](parseVariadicPredicate[E](p, s)...) +} + +func parseOnIndex[E any](p *Parser[Predicate[E]], s *Scanner) Predicate[E] { + i, err := strconv.ParseInt(s.Consume(token.INT).Lit, 10, 32) + if err != nil { + panic(err) + } + s.Consume(token.RPAREN) + return OnIndex[E](int32(i)) +} + +func parseVariadicPredicate[E any](p *Parser[Predicate[E]], s *Scanner) (ret []Predicate[E]) { + tok := s.Scan() + for tok.Kind == token.LPAREN || tok.Kind == token.IDENT { + ret = append(ret, p.ParseFromPos(s, tok)) + tok = s.Scan() + } + assertTok(tok, token.RPAREN) + return ret +} diff --git a/pebble/internal/fastrand/fastrand.go b/pebble/internal/fastrand/fastrand.go new file mode 100644 index 0000000..dd3ec9c --- /dev/null +++ b/pebble/internal/fastrand/fastrand.go @@ -0,0 +1,17 @@ +// Copyright 2020 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 fastrand + +import _ "unsafe" // required by go:linkname + +// Uint32 returns a lock free uint32 value. +// +//go:linkname Uint32 runtime.fastrand +func Uint32() uint32 + +// Uint32n returns a lock free uint32 value in the interval [0, n). +// +//go:linkname Uint32n runtime.fastrandn +func Uint32n(n uint32) uint32 diff --git a/pebble/internal/fastrand/fastrand_test.go b/pebble/internal/fastrand/fastrand_test.go new file mode 100644 index 0000000..581c056 --- /dev/null +++ b/pebble/internal/fastrand/fastrand_test.go @@ -0,0 +1,86 @@ +// Copyright 2020 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 fastrand + +import ( + "fmt" + "sync" + "testing" + "time" + + "golang.org/x/exp/rand" +) + +type defaultRand struct { + mu sync.Mutex + src rand.PCGSource +} + +func newDefaultRand() *defaultRand { + r := &defaultRand{} + r.src.Seed(uint64(time.Now().UnixNano())) + return r +} + +func (r *defaultRand) Uint32() uint32 { + r.mu.Lock() + i := uint32(r.src.Uint64()) + r.mu.Unlock() + return i +} + +func BenchmarkFastRand(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + Uint32() + } + }) +} + +func BenchmarkDefaultRand(b *testing.B) { + r := newDefaultRand() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + r.Uint32() + } + }) +} + +// Benchmarks for single-threaded (ST) use of fastrand compared to +// constructing a Rand, which can have heap allocation overhead. + +// Global state to disable elision of benchmark code. +var xg uint32 + +func BenchmarkSTFastRand(b *testing.B) { + var x uint32 + for i := 0; i < b.N; i++ { + // Arbitrary constant. + x = Uint32n(2097152) + } + xg = x +} + +func BenchmarkSTDefaultRand(b *testing.B) { + for _, newPeriod := range []int{0, 10, 100, 1000} { + name := "no-new" + if newPeriod > 0 { + name = fmt.Sprintf("new-period=%d", newPeriod) + } + b.Run(name, func(b *testing.B) { + r := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + b.ResetTimer() + var x uint32 + for i := 0; i < b.N; i++ { + if newPeriod > 0 && i%newPeriod == 0 { + r = rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + } + // Arbitrary constant. + x = uint32(r.Uint64n(2097152)) + } + xg = x + }) + } +} diff --git a/pebble/internal/humanize/humanize.go b/pebble/internal/humanize/humanize.go new file mode 100644 index 0000000..cb82343 --- /dev/null +++ b/pebble/internal/humanize/humanize.go @@ -0,0 +1,68 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package humanize + +import ( + "fmt" + "math" + + "github.com/cockroachdb/redact" +) + +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func humanate(s uint64, base float64, suffixes []string) string { + if s < 10 { + return fmt.Sprintf("%d%s", s, suffixes[0]) + } + e := math.Floor(logn(float64(s), base)) + suffix := suffixes[int(e)] + val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10 + f := "%.0f%s" + if val < 10 { + f = "%.1f%s" + } + + return fmt.Sprintf(f, val, suffix) +} + +type config struct { + base float64 + suffix []string +} + +// Bytes produces human readable representations of byte values in IEC units. +var Bytes = config{1024, []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}} + +// Count produces human readable representations of unitless values in SI units. +var Count = config{1000, []string{"", "K", "M", "G", "T", "P", "E"}} + +// Int64 produces a human readable representation of the value. +func (c *config) Int64(s int64) FormattedString { + if s < 0 { + return FormattedString("-" + humanate(uint64(-s), c.base, c.suffix)) + } + return FormattedString(humanate(uint64(s), c.base, c.suffix)) +} + +// Uint64 produces a human readable representation of the value. +func (c *config) Uint64(s uint64) FormattedString { + return FormattedString(humanate(s, c.base, c.suffix)) +} + +// FormattedString represents a human readable representation of a value. It +// implements the redact.SafeValue interface to signal that it represents a +// a string that does not need to be redacted. +type FormattedString string + +var _ redact.SafeValue = FormattedString("") + +// SafeValue implements redact.SafeValue. +func (fs FormattedString) SafeValue() {} + +// String implements fmt.Stringer. +func (fs FormattedString) String() string { return string(fs) } diff --git a/pebble/internal/humanize/humanize_test.go b/pebble/internal/humanize/humanize_test.go new file mode 100644 index 0000000..a4a42c3 --- /dev/null +++ b/pebble/internal/humanize/humanize_test.go @@ -0,0 +1,38 @@ +// 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 humanize + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" +) + +func TestHumanize(t *testing.T) { + datadriven.RunTest(t, "testdata/humanize", func(t *testing.T, td *datadriven.TestData) string { + var c config + switch td.Cmd { + case "bytes": + c = Bytes + case "count": + c = Count + default: + td.Fatalf(t, "invalid command %q", td.Cmd) + } + var buf bytes.Buffer + for _, row := range strings.Split(td.Input, "\n") { + val, err := strconv.ParseInt(row, 10, 64) + if err != nil { + td.Fatalf(t, "error parsing %q: %v", row, err) + } + fmt.Fprintf(&buf, "%s\n", c.Int64(val)) + } + return buf.String() + }) +} diff --git a/pebble/internal/humanize/testdata/humanize b/pebble/internal/humanize/testdata/humanize new file mode 100644 index 0000000..27f554a --- /dev/null +++ b/pebble/internal/humanize/testdata/humanize @@ -0,0 +1,49 @@ +bytes +0 +1 +9 +99 +123 +123456 +12345678 +1234567890 +1234567890123 +123456789012345 +123456789012345678 +---- +0B +1B +9B +99B +123B +121KB +12MB +1.1GB +1.1TB +112TB +110PB + +count +0 +1 +9 +99 +123 +123456 +12345678 +1234567890 +1234567890123 +123456789012345 +123456789012345678 +---- +0 +1 +9 +99 +123 +124K +12M +1.2G +1.2T +124T +124P diff --git a/pebble/internal/intern/intern.go b/pebble/internal/intern/intern.go new file mode 100644 index 0000000..9f8bad5 --- /dev/null +++ b/pebble/internal/intern/intern.go @@ -0,0 +1,27 @@ +// Copyright 2020 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 intern + +import "sync" + +var pool = sync.Pool{ + New: func() interface{} { + return make(map[string]string) + }, +} + +// Bytes returns b converted to a string, interned. +func Bytes(b []byte) string { + m := pool.Get().(map[string]string) + c, ok := m[string(b)] + if ok { + pool.Put(m) + return c + } + s := string(b) + m[s] = s + pool.Put(m) + return s +} diff --git a/pebble/internal/intern/intern_test.go b/pebble/internal/intern/intern_test.go new file mode 100644 index 0000000..1db6581 --- /dev/null +++ b/pebble/internal/intern/intern_test.go @@ -0,0 +1,30 @@ +// Copyright 2020 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 intern + +import ( + "bytes" + "testing" + + "github.com/cockroachdb/pebble/internal/invariants" +) + +func TestBytes(t *testing.T) { + if invariants.RaceEnabled { + // sync.Pool is a no-op under -race, making this test fail. + t.Skip("not supported under -race") + } + + const abc = "abc" + s := bytes.Repeat([]byte(abc), 100) + n := testing.AllocsPerRun(100, func() { + for i := 0; i < 100; i++ { + _ = Bytes(s[i*len(abc) : (i+1)*len(abc)]) + } + }) + if n > 0 { + t.Fatalf("Bytes allocated %d, want 0", int(n)) + } +} diff --git a/pebble/internal/invalidating/iter.go b/pebble/internal/invalidating/iter.go new file mode 100644 index 0000000..e27db58 --- /dev/null +++ b/pebble/internal/invalidating/iter.go @@ -0,0 +1,168 @@ +// 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 invalidating + +import ( + "context" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/fastrand" + "github.com/cockroachdb/pebble/internal/invariants" +) + +// MaybeWrapIfInvariants wraps some iterators with an invalidating iterator. +// MaybeWrapIfInvariants does nothing in non-invariant builds. +func MaybeWrapIfInvariants(iter base.InternalIterator) base.InternalIterator { + if invariants.Enabled { + if fastrand.Uint32n(10) == 1 { + return NewIter(iter) + } + } + return iter +} + +// iter tests unsafe key/value slice reuse by modifying the last +// returned key/value to all 1s. +type iter struct { + iter base.InternalIterator + lastKey *base.InternalKey + lastValue base.LazyValue + ignoreKinds [base.InternalKeyKindMax + 1]bool + err error +} + +// Option configures the behavior of an invalidating iterator. +type Option interface { + apply(*iter) +} + +type funcOpt func(*iter) + +func (f funcOpt) apply(i *iter) { f(i) } + +// IgnoreKinds constructs an Option that configures an invalidating iterator to +// skip trashing k/v pairs with the provided key kinds. Some iterators provided +// key stability guarantees for specific key kinds. +func IgnoreKinds(kinds ...base.InternalKeyKind) Option { + return funcOpt(func(i *iter) { + for _, kind := range kinds { + i.ignoreKinds[kind] = true + } + }) +} + +// NewIter constructs a new invalidating iterator that wraps the provided +// iterator, trashing buffers for previously returned keys. +func NewIter(originalIterator base.InternalIterator, opts ...Option) base.InternalIterator { + i := &iter{iter: originalIterator} + for _, opt := range opts { + opt.apply(i) + } + return i +} + +func (i *iter) update( + key *base.InternalKey, value base.LazyValue, +) (*base.InternalKey, base.LazyValue) { + i.trashLastKV() + if key == nil { + i.lastKey = nil + i.lastValue = base.LazyValue{} + return nil, base.LazyValue{} + } + + i.lastKey = &base.InternalKey{} + *i.lastKey = key.Clone() + i.lastValue = base.LazyValue{ + ValueOrHandle: append(make([]byte, 0, len(value.ValueOrHandle)), value.ValueOrHandle...), + } + if value.Fetcher != nil { + fetcher := new(base.LazyFetcher) + *fetcher = *value.Fetcher + i.lastValue.Fetcher = fetcher + } + return i.lastKey, i.lastValue +} + +func (i *iter) trashLastKV() { + if i.lastKey == nil { + return + } + if i.ignoreKinds[i.lastKey.Kind()] { + return + } + + if i.lastKey != nil { + for j := range i.lastKey.UserKey { + i.lastKey.UserKey[j] = 0xff + } + i.lastKey.Trailer = 0xffffffffffffffff + } + for j := range i.lastValue.ValueOrHandle { + i.lastValue.ValueOrHandle[j] = 0xff + } + if i.lastValue.Fetcher != nil { + // Not all the LazyFetcher fields are visible, so we zero out the last + // value's Fetcher struct entirely. + *i.lastValue.Fetcher = base.LazyFetcher{} + } +} + +func (i *iter) SeekGE(key []byte, flags base.SeekGEFlags) (*base.InternalKey, base.LazyValue) { + return i.update(i.iter.SeekGE(key, flags)) +} + +func (i *iter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + return i.update(i.iter.SeekPrefixGE(prefix, key, flags)) +} + +func (i *iter) SeekLT(key []byte, flags base.SeekLTFlags) (*base.InternalKey, base.LazyValue) { + return i.update(i.iter.SeekLT(key, flags)) +} + +func (i *iter) First() (*base.InternalKey, base.LazyValue) { + return i.update(i.iter.First()) +} + +func (i *iter) Last() (*base.InternalKey, base.LazyValue) { + return i.update(i.iter.Last()) +} + +func (i *iter) Next() (*base.InternalKey, base.LazyValue) { + return i.update(i.iter.Next()) +} + +func (i *iter) Prev() (*base.InternalKey, base.LazyValue) { + return i.update(i.iter.Prev()) +} + +func (i *iter) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) { + return i.update(i.iter.NextPrefix(succKey)) +} + +func (i *iter) Error() error { + if err := i.iter.Error(); err != nil { + return err + } + return i.err +} + +func (i *iter) Close() error { + return i.iter.Close() +} + +func (i *iter) SetBounds(lower, upper []byte) { + i.iter.SetBounds(lower, upper) +} + +func (i *iter) SetContext(ctx context.Context) { + i.iter.SetContext(ctx) +} + +func (i *iter) String() string { + return i.iter.String() +} diff --git a/pebble/internal/invariants/finalizer_off.go b/pebble/internal/invariants/finalizer_off.go new file mode 100644 index 0000000..d2c600a --- /dev/null +++ b/pebble/internal/invariants/finalizer_off.go @@ -0,0 +1,14 @@ +// Copyright 2021 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 (!invariants && !tracing) || race +// +build !invariants,!tracing race + +package invariants + +// SetFinalizer is a wrapper around runtime.SetFinalizer that is a no-op under +// race builds or if neither the invariants or tracing build tags are +// specified. +func SetFinalizer(obj, finalizer interface{}) { +} diff --git a/pebble/internal/invariants/finalizer_on.go b/pebble/internal/invariants/finalizer_on.go new file mode 100644 index 0000000..da4e307 --- /dev/null +++ b/pebble/internal/invariants/finalizer_on.go @@ -0,0 +1,17 @@ +// Copyright 2021 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 (invariants && !race) || (tracing && !race) +// +build invariants,!race tracing,!race + +package invariants + +import "runtime" + +// SetFinalizer is a wrapper around runtime.SetFinalizer that is a no-op under +// race builds or if neither the invariants or tracing build tags are +// specified. +func SetFinalizer(obj, finalizer interface{}) { + runtime.SetFinalizer(obj, finalizer) +} diff --git a/pebble/internal/invariants/off.go b/pebble/internal/invariants/off.go new file mode 100644 index 0000000..01513f2 --- /dev/null +++ b/pebble/internal/invariants/off.go @@ -0,0 +1,11 @@ +// Copyright 2020 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 !invariants && !race +// +build !invariants,!race + +package invariants + +// Enabled is true if we were built with the "invariants" or "race" build tags. +const Enabled = false diff --git a/pebble/internal/invariants/on.go b/pebble/internal/invariants/on.go new file mode 100644 index 0000000..b418680 --- /dev/null +++ b/pebble/internal/invariants/on.go @@ -0,0 +1,11 @@ +// Copyright 2020 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 invariants || race +// +build invariants race + +package invariants + +// Enabled is true if we were built with the "invariants" or "race" build tags. +const Enabled = true diff --git a/pebble/internal/invariants/race_off.go b/pebble/internal/invariants/race_off.go new file mode 100644 index 0000000..b2b8c5e --- /dev/null +++ b/pebble/internal/invariants/race_off.go @@ -0,0 +1,11 @@ +// Copyright 2018 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 !race +// +build !race + +package invariants + +// RaceEnabled is true if we were built with the "race" build tag. +const RaceEnabled = false diff --git a/pebble/internal/invariants/race_on.go b/pebble/internal/invariants/race_on.go new file mode 100644 index 0000000..46613f7 --- /dev/null +++ b/pebble/internal/invariants/race_on.go @@ -0,0 +1,11 @@ +// Copyright 2018 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 race +// +build race + +package invariants + +// RaceEnabled is true if we were built with the "race" build tag. +const RaceEnabled = true diff --git a/pebble/internal/itertest/datadriven.go b/pebble/internal/itertest/datadriven.go new file mode 100644 index 0000000..6c2feef --- /dev/null +++ b/pebble/internal/itertest/datadriven.go @@ -0,0 +1,196 @@ +// 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 itertest provides facilities for testing internal iterators. +package itertest + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" +) + +type iterCmdOpts struct { + fmtKV func(io.Writer, *base.InternalKey, []byte, base.InternalIterator) + stats *base.InternalIteratorStats +} + +// An IterOpt configures the behavior of RunInternalIterCmd. +type IterOpt func(*iterCmdOpts) + +// Verbose configures RunInternalIterCmd to output verbose results. +func Verbose(opts *iterCmdOpts) { opts.fmtKV = verboseFmt } + +// Condensed configures RunInternalIterCmd to output condensed results without +// values. +func Condensed(opts *iterCmdOpts) { opts.fmtKV = condensedFmt } + +// WithStats configures RunInternalIterCmd to collect iterator stats in the +// struct pointed to by stats. +func WithStats(stats *base.InternalIteratorStats) IterOpt { + return func(opts *iterCmdOpts) { + opts.stats = stats + } +} + +func defaultFmt(w io.Writer, key *base.InternalKey, v []byte, iter base.InternalIterator) { + if key != nil { + fmt.Fprintf(w, "%s:%s\n", key.UserKey, v) + } else if err := iter.Error(); err != nil { + fmt.Fprintf(w, "err=%v\n", err) + } else { + fmt.Fprintf(w, ".\n") + } +} + +func condensedFmt(w io.Writer, key *base.InternalKey, v []byte, iter base.InternalIterator) { + if key != nil { + fmt.Fprintf(w, "<%s:%d>", key.UserKey, key.SeqNum()) + } else if err := iter.Error(); err != nil { + fmt.Fprintf(w, "err=%v", err) + } else { + fmt.Fprint(w, ".") + } +} + +func verboseFmt(w io.Writer, key *base.InternalKey, v []byte, iter base.InternalIterator) { + if key != nil { + fmt.Fprintf(w, "%s:%s\n", key, v) + return + } + defaultFmt(w, key, v, iter) +} + +// RunInternalIterCmd evaluates a datadriven command controlling an internal +// iterator, returning a string with the results of the iterator operations. +func RunInternalIterCmd( + t *testing.T, d *datadriven.TestData, iter base.InternalIterator, opts ...IterOpt, +) string { + var buf bytes.Buffer + RunInternalIterCmdWriter(t, &buf, d, iter, opts...) + return buf.String() +} + +// RunInternalIterCmdWriter evaluates a datadriven command controlling an +// internal iterator, writing the results of the iterator operations to the +// provided Writer. +func RunInternalIterCmdWriter( + t *testing.T, w io.Writer, d *datadriven.TestData, iter base.InternalIterator, opts ...IterOpt, +) { + o := iterCmdOpts{fmtKV: defaultFmt} + for _, opt := range opts { + opt(&o) + } + + getKV := func(key *base.InternalKey, val base.LazyValue) (*base.InternalKey, []byte) { + v, _, err := val.Value(nil) + require.NoError(t, err) + return key, v + } + var prefix []byte + for _, line := range strings.Split(d.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + var key *base.InternalKey + var value []byte + switch parts[0] { + case "seek-ge": + if len(parts) < 2 || len(parts) > 3 { + fmt.Fprint(w, "seek-ge []\n") + return + } + prefix = nil + var flags base.SeekGEFlags + if len(parts) == 3 { + if trySeekUsingNext, err := strconv.ParseBool(parts[2]); err != nil { + fmt.Fprintf(w, "%s", err.Error()) + return + } else if trySeekUsingNext { + flags = flags.EnableTrySeekUsingNext() + } + } + key, value = getKV(iter.SeekGE([]byte(strings.TrimSpace(parts[1])), flags)) + case "seek-prefix-ge": + if len(parts) != 2 && len(parts) != 3 { + fmt.Fprint(w, "seek-prefix-ge []\n") + return + } + prefix = []byte(strings.TrimSpace(parts[1])) + var flags base.SeekGEFlags + if len(parts) == 3 { + if trySeekUsingNext, err := strconv.ParseBool(parts[2]); err != nil { + fmt.Fprintf(w, "%s", err.Error()) + return + } else if trySeekUsingNext { + flags = flags.EnableTrySeekUsingNext() + } + } + key, value = getKV(iter.SeekPrefixGE(prefix, prefix /* key */, flags)) + case "seek-lt": + if len(parts) != 2 { + fmt.Fprint(w, "seek-lt \n") + return + } + prefix = nil + key, value = getKV(iter.SeekLT([]byte(strings.TrimSpace(parts[1])), base.SeekLTFlagsNone)) + case "first": + prefix = nil + key, value = getKV(iter.First()) + case "last": + prefix = nil + key, value = getKV(iter.Last()) + case "next": + key, value = getKV(iter.Next()) + case "prev": + key, value = getKV(iter.Prev()) + case "set-bounds": + if len(parts) <= 1 || len(parts) > 3 { + fmt.Fprint(w, "set-bounds lower= upper=\n") + return + } + var lower []byte + var upper []byte + for _, part := range parts[1:] { + arg := strings.Split(strings.TrimSpace(part), "=") + switch arg[0] { + case "lower": + lower = []byte(arg[1]) + case "upper": + upper = []byte(arg[1]) + default: + fmt.Fprintf(w, "set-bounds: unknown arg: %s", arg) + return + } + } + iter.SetBounds(lower, upper) + continue + case "stats": + if o.stats != nil { + // The timing is non-deterministic, so set to 0. + o.stats.BlockReadDuration = 0 + fmt.Fprintf(w, "%+v\n", *o.stats) + } + continue + case "reset-stats": + if o.stats != nil { + *o.stats = base.InternalIteratorStats{} + } + continue + default: + fmt.Fprintf(w, "unknown op: %s", parts[0]) + return + } + o.fmtKV(w, key, value, iter) + + } +} diff --git a/pebble/internal/keyspan/bounded.go b/pebble/internal/keyspan/bounded.go new file mode 100644 index 0000000..70dd395 --- /dev/null +++ b/pebble/internal/keyspan/bounded.go @@ -0,0 +1,268 @@ +// 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 keyspan + +import "github.com/cockroachdb/pebble/internal/base" + +// TODO(jackson): Consider removing this type and adding bounds enforcement +// directly to the MergingIter. This type is probably too lightweight to warrant +// its own type, but for now we implement it separately for expediency. + +// boundedIterPos records the position of the BoundedIter relative to the +// underlying iterator's position. It's used to avoid Next/Prev-ing the iterator +// if there can't possibly be another span within bounds, because the current +// span overlaps the bound. +// +// Imagine bounds [a,c) and an iterator that seeks to a span [b,d). The span +// [b,d) overlaps some portion of the iterator bounds, so the iterator must +// return it. If the iterator is subsequently Nexted, Next can tell that the +// iterator is exhausted without advancing the underlying iterator because the +// current span's end bound of d is ≥ the upper bound of c. In this case, the +// bounded iterator returns nil and records i.pos as posAtUpperLimit to remember +// that the underlying iterator position does not match the current BoundedIter +// position. +type boundedIterPos int8 + +const ( + posAtLowerLimit boundedIterPos = -1 + posAtIterSpan boundedIterPos = 0 + posAtUpperLimit boundedIterPos = +1 +) + +// BoundedIter implements FragmentIterator and enforces bounds. +// +// Like the point InternalIterator interface, the bounded iterator's forward +// positioning routines (SeekGE, First, and Next) only check the upper bound. +// The reverse positioning routines (SeekLT, Last, and Prev) only check the +// lower bound. It is up to the caller to ensure that the forward positioning +// routines respect the lower bound and the reverse positioning routines respect +// the upper bound (i.e. calling SeekGE instead of First if there is a lower +// bound, and SeekLT instead of Last if there is an upper bound). +// +// When the hasPrefix parameter indicates that the iterator is in prefix +// iteration mode, BoundedIter elides any spans that do not overlap with the +// prefix's keyspace. In prefix iteration mode, reverse iteration is disallowed, +// except for an initial SeekLT with a seek key greater than or equal to the +// prefix. In prefix iteration mode, the first seek must position the iterator +// at or immediately before the first fragment covering a key greater than or +// equal to the prefix. +type BoundedIter struct { + iter FragmentIterator + iterSpan *Span + cmp base.Compare + split base.Split + lower []byte + upper []byte + hasPrefix *bool + prefix *[]byte + pos boundedIterPos +} + +// Init initializes the bounded iterator. +// +// In addition to the iterator bounds, Init takes pointers to a boolean +// indicating whether the iterator is in prefix iteration mode and the prefix +// key if it is. This is used to exclude spans that are outside the iteration +// prefix. +// +// hasPrefix and prefix are allowed to be nil, however if hasPrefix != nil, +// prefix must also not be nil. +func (i *BoundedIter) Init( + cmp base.Compare, + split base.Split, + iter FragmentIterator, + lower, upper []byte, + hasPrefix *bool, + prefix *[]byte, +) { + *i = BoundedIter{ + iter: iter, + cmp: cmp, + split: split, + lower: lower, + upper: upper, + hasPrefix: hasPrefix, + prefix: prefix, + } +} + +var _ FragmentIterator = (*BoundedIter)(nil) + +// Seek calls. +// +// Seek calls check iterator bounds in the direction of the seek. Additionally, +// if the iterator is in prefix iteration mode, seek calls check both start and +// end bounds against the prefix's bounds. We check both bounds for defense in +// depth. This optimization has been a source of various bugs due to various +// other prefix iteration optimizations that can result in seek keys that don't +// respect the prefix bounds. + +// SeekGE implements FragmentIterator. +func (i *BoundedIter) SeekGE(key []byte) *Span { + s := i.iter.SeekGE(key) + s = i.checkPrefixSpanStart(s) + s = i.checkPrefixSpanEnd(s) + return i.checkForwardBound(s) +} + +// SeekLT implements FragmentIterator. +func (i *BoundedIter) SeekLT(key []byte) *Span { + s := i.iter.SeekLT(key) + s = i.checkPrefixSpanStart(s) + s = i.checkPrefixSpanEnd(s) + return i.checkBackwardBound(s) +} + +// First implements FragmentIterator. +func (i *BoundedIter) First() *Span { + s := i.iter.First() + s = i.checkPrefixSpanStart(s) + return i.checkForwardBound(s) +} + +// Last implements FragmentIterator. +func (i *BoundedIter) Last() *Span { + s := i.iter.Last() + s = i.checkPrefixSpanEnd(s) + return i.checkBackwardBound(s) +} + +// Next implements FragmentIterator. +func (i *BoundedIter) Next() *Span { + switch i.pos { + case posAtLowerLimit: + // The BoundedIter had previously returned nil, because it knew from + // i.iterSpan's bounds that there was no previous span. To Next, we only + // need to return the current iter span and reset i.pos to reflect that + // we're no longer positioned at the limit. + i.pos = posAtIterSpan + return i.iterSpan + case posAtIterSpan: + // If the span at the underlying iterator position extends to or beyond the + // upper bound, we can avoid advancing because the next span is necessarily + // out of bounds. + if i.iterSpan != nil && i.upper != nil && i.cmp(i.iterSpan.End, i.upper) >= 0 { + i.pos = posAtUpperLimit + return nil + } + // Similarly, if the span extends to the next prefix and we're in prefix + // iteration mode, we can avoid advancing. + if i.iterSpan != nil && i.hasPrefix != nil && *i.hasPrefix { + ei := i.split(i.iterSpan.End) + if i.cmp(i.iterSpan.End[:ei], *i.prefix) > 0 { + i.pos = posAtUpperLimit + return nil + } + } + return i.checkForwardBound(i.checkPrefixSpanStart(i.iter.Next())) + case posAtUpperLimit: + // Already exhausted. + return nil + default: + panic("unreachable") + } +} + +// Prev implements FragmentIterator. +func (i *BoundedIter) Prev() *Span { + switch i.pos { + case posAtLowerLimit: + // Already exhausted. + return nil + case posAtIterSpan: + // If the span at the underlying iterator position extends to or beyond + // the lower bound, we can avoid advancing because the previous span is + // necessarily out of bounds. + if i.iterSpan != nil && i.lower != nil && i.cmp(i.iterSpan.Start, i.lower) <= 0 { + i.pos = posAtLowerLimit + return nil + } + // Similarly, if the span extends to or beyond the current prefix and + // we're in prefix iteration mode, we can avoid advancing. + if i.iterSpan != nil && i.hasPrefix != nil && *i.hasPrefix { + si := i.split(i.iterSpan.Start) + if i.cmp(i.iterSpan.Start[:si], *i.prefix) < 0 { + i.pos = posAtLowerLimit + return nil + } + } + return i.checkBackwardBound(i.checkPrefixSpanEnd(i.iter.Prev())) + case posAtUpperLimit: + // The BoundedIter had previously returned nil, because it knew from + // i.iterSpan's bounds that there was no next span. To Prev, we only + // need to return the current iter span and reset i.pos to reflect that + // we're no longer positioned at the limit. + i.pos = posAtIterSpan + return i.iterSpan + default: + panic("unreachable") + } +} + +// Error implements FragmentIterator. +func (i *BoundedIter) Error() error { + return i.iter.Error() +} + +// Close implements FragmentIterator. +func (i *BoundedIter) Close() error { + return i.iter.Close() +} + +// SetBounds modifies the FragmentIterator's bounds. +func (i *BoundedIter) SetBounds(lower, upper []byte) { + i.lower, i.upper = lower, upper +} + +func (i *BoundedIter) checkPrefixSpanStart(span *Span) *Span { + // Compare to the prefix's bounds, if in prefix iteration mode. + if span != nil && i.hasPrefix != nil && *i.hasPrefix { + si := i.split(span.Start) + if i.cmp(span.Start[:si], *i.prefix) > 0 { + // This span starts at a prefix that sorts after our current prefix. + span = nil + } + } + return span +} + +// checkForwardBound enforces the upper bound, returning nil if the provided +// span is wholly outside the upper bound. It also updates i.pos and i.iterSpan +// to reflect the new iterator position. +func (i *BoundedIter) checkForwardBound(span *Span) *Span { + // Compare to the upper bound. + if span != nil && i.upper != nil && i.cmp(span.Start, i.upper) >= 0 { + span = nil + } + i.iterSpan = span + if i.pos != posAtIterSpan { + i.pos = posAtIterSpan + } + return span +} + +func (i *BoundedIter) checkPrefixSpanEnd(span *Span) *Span { + // Compare to the prefix's bounds, if in prefix iteration mode. + if span != nil && i.hasPrefix != nil && *i.hasPrefix && i.cmp(span.End, *i.prefix) <= 0 { + // This span ends before the current prefix. + span = nil + } + return span +} + +// checkBackward enforces the lower bound, returning nil if the provided span is +// wholly outside the lower bound. It also updates i.pos and i.iterSpan to +// reflect the new iterator position. +func (i *BoundedIter) checkBackwardBound(span *Span) *Span { + // Compare to the lower bound. + if span != nil && i.lower != nil && i.cmp(span.End, i.lower) <= 0 { + span = nil + } + i.iterSpan = span + if i.pos != posAtIterSpan { + i.pos = posAtIterSpan + } + return span +} diff --git a/pebble/internal/keyspan/bounded_test.go b/pebble/internal/keyspan/bounded_test.go new file mode 100644 index 0000000..edb3b5a --- /dev/null +++ b/pebble/internal/keyspan/bounded_test.go @@ -0,0 +1,69 @@ +// 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 keyspan + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/testkeys" +) + +func TestBoundedIter(t *testing.T) { + getBounds := func(td *datadriven.TestData) (lower, upper []byte) { + for _, cmdArg := range td.CmdArgs { + switch cmdArg.Key { + case "lower": + if len(cmdArg.Vals[0]) > 0 { + lower = []byte(cmdArg.Vals[0]) + } + case "upper": + if len(cmdArg.Vals[0]) > 0 { + upper = []byte(cmdArg.Vals[0]) + } + } + } + return lower, upper + } + + cmp := testkeys.Comparer.Compare + split := testkeys.Comparer.Split + var buf bytes.Buffer + var iter BoundedIter + var hasPrefix bool + var prefix []byte + datadriven.RunTest(t, "testdata/bounded_iter", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + var spans []Span + lines := strings.Split(strings.TrimSpace(td.Input), "\n") + for _, line := range lines { + spans = append(spans, ParseSpan(line)) + } + inner := &invalidatingIter{iter: NewIter(cmp, spans)} + lower, upper := getBounds(td) + iter.Init(cmp, split, inner, lower, upper, &hasPrefix, &prefix) + return "" + case "set-prefix": + hasPrefix = len(td.CmdArgs) > 0 + if hasPrefix { + prefix = []byte(td.CmdArgs[0].String()) + return fmt.Sprintf("set prefix to %q\n", prefix) + } + return "cleared prefix" + case "iter": + buf.Reset() + lower, upper := getBounds(td) + iter.SetBounds(lower, upper) + runIterCmd(t, td, &iter, &buf) + return buf.String() + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} diff --git a/pebble/internal/keyspan/datadriven_test.go b/pebble/internal/keyspan/datadriven_test.go new file mode 100644 index 0000000..5b1d7aa --- /dev/null +++ b/pebble/internal/keyspan/datadriven_test.go @@ -0,0 +1,432 @@ +// 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 ( + "fmt" + "go/token" + "io" + "reflect" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/dsl" +) + +// This file contains testing facilities for Spans and FragmentIterators. It's +// defined here so that it may be used by the keyspan package to test its +// various FragmentIterator implementations. +// +// TODO(jackson): Move keyspan.{Span,Key,FragmentIterator} into internal/base, +// and then move the testing facilities to an independent package, eg +// internal/itertest. + +// probe defines an interface for probes that may inspect or mutate internal +// span iterator behavior. +type probe interface { + // probe inspects, and possibly manipulates, iterator operations' results. + probe(*probeContext) +} + +func parseProbes(probeDSLs ...string) []probe { + probes := make([]probe, len(probeDSLs)) + var err error + for i := range probeDSLs { + probes[i], err = probeParser.Parse(probeDSLs[i]) + if err != nil { + panic(err) + } + } + return probes +} + +func attachProbes(iter FragmentIterator, pctx probeContext, probes ...probe) FragmentIterator { + if pctx.log == nil { + pctx.log = io.Discard + } + for i := range probes { + iter = &probeIterator{ + iter: iter, + probe: probes[i], + probeCtx: pctx, + } + } + return iter +} + +// probeContext provides the context within which a probe is run. It includes +// information about the iterator operation in progress. +type probeContext struct { + op + log io.Writer +} + +type op struct { + Kind OpKind + SeekKey []byte + Span *Span + Err error +} + +// ErrInjected is an error artificially injected for testing. +var ErrInjected = &errorProbe{name: "ErrInjected", err: errors.New("injected error")} + +var probeParser = func() *dsl.Parser[probe] { + valuerParser := dsl.NewParser[valuer]() + valuerParser.DefineConstant("StartKey", func() valuer { return startKey{} }) + valuerParser.DefineFunc("Bytes", + func(p *dsl.Parser[valuer], s *dsl.Scanner) valuer { + v := bytesConstant{bytes: []byte(s.ConsumeString())} + s.Consume(token.RPAREN) + return v + }) + + predicateParser := dsl.NewPredicateParser[*probeContext]() + predicateParser.DefineFunc("Equal", + func(p *dsl.Parser[dsl.Predicate[*probeContext]], s *dsl.Scanner) dsl.Predicate[*probeContext] { + eq := equal{ + valuerParser.ParseFromPos(s, s.Scan()), + valuerParser.ParseFromPos(s, s.Scan()), + } + s.Consume(token.RPAREN) + return eq + }) + for i, name := range opNames { + opKind := OpKind(i) + predicateParser.DefineConstant(name, func() dsl.Predicate[*probeContext] { + // An OpKind implements dsl.Predicate[*probeContext]. + return opKind + }) + } + probeParser := dsl.NewParser[probe]() + probeParser.DefineConstant("ErrInjected", func() probe { return ErrInjected }) + probeParser.DefineConstant("noop", func() probe { return noop{} }) + probeParser.DefineFunc("If", + func(p *dsl.Parser[probe], s *dsl.Scanner) probe { + probe := ifProbe{ + predicateParser.ParseFromPos(s, s.Scan()), + probeParser.ParseFromPos(s, s.Scan()), + probeParser.ParseFromPos(s, s.Scan()), + } + s.Consume(token.RPAREN) + return probe + }) + probeParser.DefineFunc("Return", + func(p *dsl.Parser[probe], s *dsl.Scanner) (ret probe) { + switch tok := s.Scan(); tok.Kind { + case token.STRING: + str, err := strconv.Unquote(tok.Lit) + if err != nil { + panic(err) + } + span := ParseSpan(str) + ret = returnSpan{s: &span} + case token.IDENT: + switch tok.Lit { + case "nil": + ret = returnSpan{s: nil} + default: + panic(errors.Newf("unrecognized return value %q", tok.Lit)) + } + } + s.Consume(token.RPAREN) + return ret + }) + probeParser.DefineFunc("Log", + func(p *dsl.Parser[probe], s *dsl.Scanner) (ret probe) { + ret = loggingProbe{prefix: s.ConsumeString()} + s.Consume(token.RPAREN) + return ret + }) + return probeParser +}() + +// probe implementations + +type errorProbe struct { + name string + err error +} + +func (p *errorProbe) String() string { return p.name } +func (p *errorProbe) Error() error { return p.err } +func (p *errorProbe) probe(pctx *probeContext) { + pctx.op.Err = p.err + pctx.op.Span = nil +} + +// ifProbe is a conditional probe. If its predicate evaluates to true, it probes +// using its Then probe. If its predicate evalutes to false, it probes using its +// Else probe. +type ifProbe struct { + Predicate dsl.Predicate[*probeContext] + Then probe + Else probe +} + +func (p ifProbe) String() string { return fmt.Sprintf("(If %s %s %s)", p.Predicate, p.Then, p.Else) } +func (p ifProbe) probe(pctx *probeContext) { + if p.Predicate.Evaluate(pctx) { + p.Then.probe(pctx) + } else { + p.Else.probe(pctx) + } +} + +type returnSpan struct { + s *Span +} + +func (p returnSpan) String() string { + if p.s == nil { + return "(Return nil)" + } + return fmt.Sprintf("(Return %q)", p.s.String()) +} + +func (p returnSpan) probe(pctx *probeContext) { + pctx.op.Span = p.s + pctx.op.Err = nil +} + +type noop struct{} + +func (noop) String() string { return "Noop" } +func (noop) probe(pctx *probeContext) {} + +type loggingProbe struct { + prefix string +} + +func (lp loggingProbe) String() string { return fmt.Sprintf("(Log %q)", lp.prefix) } +func (lp loggingProbe) probe(pctx *probeContext) { + opStr := strings.TrimPrefix(pctx.op.Kind.String(), "Op") + fmt.Fprintf(pctx.log, "%s%s(", lp.prefix, opStr) + if pctx.op.SeekKey != nil { + fmt.Fprintf(pctx.log, "%q", pctx.op.SeekKey) + } + fmt.Fprint(pctx.log, ") = ") + if pctx.op.Span == nil { + fmt.Fprint(pctx.log, "nil") + if pctx.op.Err != nil { + fmt.Fprintf(pctx.log, " ", pctx.op.Err) + } + } else { + fmt.Fprint(pctx.log, pctx.op.Span.String()) + } + fmt.Fprintln(pctx.log) +} + +// dsl.Predicate[*probeContext] implementations. + +type equal struct { + a, b valuer +} + +func (e equal) String() string { return fmt.Sprintf("(Equal %s %s)", e.a, e.b) } +func (e equal) Evaluate(pctx *probeContext) bool { + return reflect.DeepEqual(e.a.value(pctx), e.b.value(pctx)) +} + +// OpKind indicates the type of iterator operation being performed. +type OpKind int8 + +const ( + OpSeekGE OpKind = iota + OpSeekLT + OpFirst + OpLast + OpNext + OpPrev + OpClose + numOpKinds +) + +func (o OpKind) String() string { return opNames[o] } +func (o OpKind) Evaluate(pctx *probeContext) bool { return pctx.op.Kind == o } + +var opNames = [numOpKinds]string{ + OpSeekGE: "OpSeekGE", + OpSeekLT: "OpSeekLT", + OpFirst: "OpFirst", + OpLast: "OpLast", + OpNext: "OpNext", + OpPrev: "OpPrev", + OpClose: "OpClose", +} + +// valuer implementations + +type valuer interface { + fmt.Stringer + value(pctx *probeContext) any +} + +type bytesConstant struct { + bytes []byte +} + +func (b bytesConstant) String() string { return fmt.Sprintf("%q", string(b.bytes)) } +func (b bytesConstant) value(pctx *probeContext) any { return b.bytes } + +type startKey struct{} + +func (s startKey) String() string { return "StartKey" } +func (s startKey) value(pctx *probeContext) any { + if pctx.op.Span == nil { + return nil + } + return pctx.op.Span.Start +} + +type probeIterator struct { + iter FragmentIterator + err error + probe probe + probeCtx probeContext +} + +// Assert that probeIterator implements the fragment iterator interface. +var _ FragmentIterator = (*probeIterator)(nil) + +func (p *probeIterator) handleOp(preProbeOp op) *Span { + p.probeCtx.op = preProbeOp + if preProbeOp.Span == nil && p.iter != nil { + p.probeCtx.op.Err = p.iter.Error() + } + + p.probe.probe(&p.probeCtx) + p.err = p.probeCtx.op.Err + return p.probeCtx.op.Span +} + +func (p *probeIterator) SeekGE(key []byte) *Span { + op := op{ + Kind: OpSeekGE, + SeekKey: key, + } + if p.iter != nil { + op.Span = p.iter.SeekGE(key) + } + return p.handleOp(op) +} + +func (p *probeIterator) SeekLT(key []byte) *Span { + op := op{ + Kind: OpSeekLT, + SeekKey: key, + } + if p.iter != nil { + op.Span = p.iter.SeekLT(key) + } + return p.handleOp(op) +} + +func (p *probeIterator) First() *Span { + op := op{Kind: OpFirst} + if p.iter != nil { + op.Span = p.iter.First() + } + return p.handleOp(op) +} + +func (p *probeIterator) Last() *Span { + op := op{Kind: OpLast} + if p.iter != nil { + op.Span = p.iter.Last() + } + return p.handleOp(op) +} + +func (p *probeIterator) Next() *Span { + op := op{Kind: OpNext} + if p.iter != nil { + op.Span = p.iter.Next() + } + return p.handleOp(op) +} + +func (p *probeIterator) Prev() *Span { + op := op{Kind: OpPrev} + if p.iter != nil { + op.Span = p.iter.Prev() + } + return p.handleOp(op) +} + +func (p *probeIterator) Error() error { + return p.err +} + +func (p *probeIterator) Close() error { + op := op{Kind: OpClose} + if p.iter != nil { + op.Err = p.iter.Close() + } + + p.probeCtx.op = op + p.probe.probe(&p.probeCtx) + p.err = p.probeCtx.op.Err + return p.err +} + +// runIterCmd evaluates a datadriven command controlling an internal +// keyspan.FragmentIterator, writing the results of the iterator operations to +// the provided writer. +func runIterCmd(t *testing.T, td *datadriven.TestData, iter FragmentIterator, w io.Writer) { + lines := strings.Split(strings.TrimSpace(td.Input), "\n") + for i, line := range lines { + if i > 0 { + fmt.Fprintln(w) + } + line = strings.TrimSpace(line) + i := strings.IndexByte(line, '#') + iterCmd := line + if i > 0 { + iterCmd = string(line[:i]) + } + runIterOp(w, iter, iterCmd) + } +} + +var iterDelim = map[rune]bool{',': true, ' ': true, '(': true, ')': true, '"': true} + +func runIterOp(w io.Writer, it FragmentIterator, op string) { + fields := strings.FieldsFunc(op, func(r rune) bool { return iterDelim[r] }) + var s *Span + switch strings.ToLower(fields[0]) { + case "first": + s = it.First() + case "last": + s = it.Last() + case "seekge", "seek-ge": + if len(fields) == 1 { + panic(fmt.Sprintf("unable to parse iter op %q", op)) + } + s = it.SeekGE([]byte(fields[1])) + case "seeklt", "seek-lt": + if len(fields) == 1 { + panic(fmt.Sprintf("unable to parse iter op %q", op)) + } + s = it.SeekLT([]byte(fields[1])) + case "next": + s = it.Next() + case "prev": + s = it.Prev() + default: + panic(fmt.Sprintf("unrecognized iter op %q", fields[0])) + } + if s == nil { + fmt.Fprint(w, "") + if err := it.Error(); err != nil { + fmt.Fprintf(w, " err=<%s>", it.Error()) + } + return + } + fmt.Fprint(w, s) +} diff --git a/pebble/internal/keyspan/defragment.go b/pebble/internal/keyspan/defragment.go new file mode 100644 index 0000000..d056ef0 --- /dev/null +++ b/pebble/internal/keyspan/defragment.go @@ -0,0 +1,539 @@ +// 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 keyspan + +import ( + "bytes" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/bytealloc" + "github.com/cockroachdb/pebble/internal/invariants" +) + +// bufferReuseMaxCapacity is the maximum capacity of a DefragmentingIter buffer +// that DefragmentingIter will reuse. Buffers larger than this will be +// discarded and reallocated as necessary. +const bufferReuseMaxCapacity = 10 << 10 // 10 KB + +// keysReuseMaxCapacity is the maximum capacity of a []keyspan.Key buffer that +// DefragmentingIter will reuse. Buffers larger than this will be discarded and +// reallocated as necessary. +const keysReuseMaxCapacity = 100 + +// DefragmentMethod configures the defragmentation performed by the +// DefragmentingIter. +type DefragmentMethod interface { + // ShouldDefragment takes two abutting spans and returns whether the two + // spans should be combined into a single, defragmented Span. + ShouldDefragment(equal base.Equal, left, right *Span) bool +} + +// The DefragmentMethodFunc type is an adapter to allow the use of ordinary +// functions as DefragmentMethods. If f is a function with the appropriate +// signature, DefragmentMethodFunc(f) is a DefragmentMethod that calls f. +type DefragmentMethodFunc func(equal base.Equal, left, right *Span) bool + +// ShouldDefragment calls f(equal, left, right). +func (f DefragmentMethodFunc) ShouldDefragment(equal base.Equal, left, right *Span) bool { + return f(equal, left, right) +} + +// DefragmentInternal configures a DefragmentingIter to defragment spans +// only if they have identical keys. It requires spans' keys to be sorted in +// trailer descending order. +// +// This defragmenting method is intended for use in compactions that may see +// internal range keys fragments that may now be joined, because the state that +// required their fragmentation has been dropped. +var DefragmentInternal DefragmentMethod = DefragmentMethodFunc(func(equal base.Equal, a, b *Span) bool { + if a.KeysOrder != ByTrailerDesc || b.KeysOrder != ByTrailerDesc { + panic("pebble: span keys unexpectedly not in trailer descending order") + } + if len(a.Keys) != len(b.Keys) { + return false + } + for i := range a.Keys { + if a.Keys[i].Trailer != b.Keys[i].Trailer { + return false + } + if !equal(a.Keys[i].Suffix, b.Keys[i].Suffix) { + return false + } + if !bytes.Equal(a.Keys[i].Value, b.Keys[i].Value) { + return false + } + } + return true +}) + +// DefragmentReducer merges the current and next Key slices, returning a new Key +// slice. +// +// Implementations should modify and return `cur` to save on allocations, or +// consider allocating a new slice, as the `cur` slice may be retained by the +// DefragmentingIter and mutated. The `next` slice must not be mutated. +// +// The incoming slices are sorted by (SeqNum, Kind) descending. The output slice +// must also have this sort order. +type DefragmentReducer func(cur, next []Key) []Key + +// StaticDefragmentReducer is a no-op DefragmentReducer that simply returns the +// current key slice, effectively retaining the first set of keys encountered +// for a defragmented span. +// +// This reducer can be used, for example, when the set of Keys for each Span +// being reduced is not expected to change, and therefore the keys from the +// first span encountered can be used without considering keys in subsequent +// spans. +var StaticDefragmentReducer DefragmentReducer = func(cur, _ []Key) []Key { + return cur +} + +// iterPos is an enum indicating the position of the defragmenting iter's +// wrapped iter. The defragmenting iter must look ahead or behind when +// defragmenting forward or backwards respectively, and this enum records that +// current position. +type iterPos int8 + +const ( + iterPosPrev iterPos = -1 + iterPosCurr iterPos = 0 + iterPosNext iterPos = +1 +) + +// DefragmentingIter wraps a key span iterator, defragmenting physical +// fragmentation during iteration. +// +// During flushes and compactions, keys applied over a span may be split at +// sstable boundaries. This fragmentation can produce internal key bounds that +// do not match any of the bounds ever supplied to a user operation. This +// physical fragmentation is necessary to avoid excessively wide sstables. +// +// The defragmenting iterator undoes this physical fragmentation, joining spans +// with abutting bounds and equal state. The defragmenting iterator takes a +// DefragmentMethod to determine what is "equal state" for a span. The +// DefragmentMethod is a function type, allowing arbitrary comparisons between +// Span keys. +// +// Seeking (SeekGE, SeekLT) poses an obstacle to defragmentation. A seek may +// land on a physical fragment in the middle of several fragments that must be +// defragmented. A seek that lands in a fragment straddling the seek key must +// first degfragment in the opposite direction of iteration to find the +// beginning of the defragmented span, and then defragments in the iteration +// direction, ensuring it's found a whole defragmented span. +type DefragmentingIter struct { + // DefragmentingBuffers holds buffers used for copying iterator state. + *DefragmentingBuffers + comparer *base.Comparer + equal base.Equal + iter FragmentIterator + iterSpan *Span + iterPos iterPos + + // curr holds the span at the current iterator position. + curr Span + + // method is a comparison function for two spans. method is called when two + // spans are abutting to determine whether they may be defragmented. + // method does not itself check for adjacency for the two spans. + method DefragmentMethod + + // reduce is the reducer function used to collect Keys across all spans that + // constitute a defragmented span. + reduce DefragmentReducer +} + +// DefragmentingBuffers holds buffers used for copying iterator state. +type DefragmentingBuffers struct { + // currBuf is a buffer for use when copying user keys for curr. currBuf is + // cleared between positioning methods. + currBuf bytealloc.A + // keysBuf is a buffer for use when copying Keys for DefragmentingIter.curr. + keysBuf []Key + // keyBuf is a buffer specifically for the defragmented start key when + // defragmenting backwards or the defragmented end key when defragmenting + // forwards. These bounds are overwritten repeatedly during defragmentation, + // and the defragmentation routines overwrite keyBuf repeatedly to store + // these extended bounds. + keyBuf []byte +} + +// PrepareForReuse discards any excessively large buffers. +func (bufs *DefragmentingBuffers) PrepareForReuse() { + if cap(bufs.currBuf) > bufferReuseMaxCapacity { + bufs.currBuf = nil + } + if cap(bufs.keyBuf) > bufferReuseMaxCapacity { + bufs.keyBuf = nil + } + if cap(bufs.keysBuf) > keysReuseMaxCapacity { + bufs.keysBuf = nil + } +} + +// Assert that *DefragmentingIter implements the FragmentIterator interface. +var _ FragmentIterator = (*DefragmentingIter)(nil) + +// Init initializes the defragmenting iter using the provided defragment +// method. +func (i *DefragmentingIter) Init( + comparer *base.Comparer, + iter FragmentIterator, + equal DefragmentMethod, + reducer DefragmentReducer, + bufs *DefragmentingBuffers, +) { + *i = DefragmentingIter{ + DefragmentingBuffers: bufs, + comparer: comparer, + equal: comparer.Equal, + iter: iter, + method: equal, + reduce: reducer, + } +} + +// Error returns any accumulated error. +func (i *DefragmentingIter) Error() error { + return i.iter.Error() +} + +// Close closes the underlying iterators. +func (i *DefragmentingIter) Close() error { + return i.iter.Close() +} + +// SeekGE moves the iterator to the first span covering a key greater than or +// equal to the given key. This is equivalent to seeking to the first span with +// an end key greater than the given key. +func (i *DefragmentingIter) SeekGE(key []byte) *Span { + i.iterSpan = i.iter.SeekGE(key) + if i.iterSpan == nil { + i.iterPos = iterPosCurr + return nil + } else if i.iterSpan.Empty() { + i.iterPos = iterPosCurr + return i.iterSpan + } + // If the span starts strictly after key, we know there mustn't be an + // earlier span that ends at i.iterSpan.Start, otherwise i.iter would've + // returned that span instead. + if i.comparer.Compare(i.iterSpan.Start, key) > 0 { + return i.defragmentForward() + } + + // The span we landed on has a Start bound ≤ key. There may be additional + // fragments before this span. Defragment backward to find the start of the + // defragmented span. + i.defragmentBackward() + + // Defragmenting backward may have stopped because it encountered an error. + // If so, we must not continue so that i.iter.Error() (and thus i.Error()) + // yields the error. + if i.iterSpan == nil && i.iter.Error() != nil { + return nil + } + + if i.iterPos == iterPosPrev { + // Next once back onto the span. + i.iterSpan = i.iter.Next() + } + // Defragment the full span from its start. + return i.defragmentForward() +} + +// SeekLT moves the iterator to the last span covering a key less than the +// given key. This is equivalent to seeking to the last span with a start +// key less than the given key. +func (i *DefragmentingIter) SeekLT(key []byte) *Span { + i.iterSpan = i.iter.SeekLT(key) + if i.iterSpan == nil { + i.iterPos = iterPosCurr + return nil + } else if i.iterSpan.Empty() { + i.iterPos = iterPosCurr + return i.iterSpan + } + // If the span ends strictly before key, we know there mustn't be a later + // span that starts at i.iterSpan.End, otherwise i.iter would've returned + // that span instead. + if i.comparer.Compare(i.iterSpan.End, key) < 0 { + return i.defragmentBackward() + } + + // The span we landed on has a End bound ≥ key. There may be additional + // fragments after this span. Defragment forward to find the end of the + // defragmented span. + i.defragmentForward() + + // Defragmenting forward may have stopped because it encountered an error. + // If so, we must not continue so that i.iter.Error() (and thus i.Error()) + // yields the error. + if i.iterSpan == nil && i.iter.Error() != nil { + return nil + } + + if i.iterPos == iterPosNext { + // Prev once back onto the span. + i.iterSpan = i.iter.Prev() + } + // Defragment the full span from its end. + return i.defragmentBackward() +} + +// First seeks the iterator to the first span and returns it. +func (i *DefragmentingIter) First() *Span { + i.iterSpan = i.iter.First() + if i.iterSpan == nil { + i.iterPos = iterPosCurr + return nil + } + return i.defragmentForward() +} + +// Last seeks the iterator to the last span and returns it. +func (i *DefragmentingIter) Last() *Span { + i.iterSpan = i.iter.Last() + if i.iterSpan == nil { + i.iterPos = iterPosCurr + return nil + } + return i.defragmentBackward() +} + +// Next advances to the next span and returns it. +func (i *DefragmentingIter) Next() *Span { + switch i.iterPos { + case iterPosPrev: + // Switching directions; The iterator is currently positioned over the + // last span of the previous set of fragments. In the below diagram, + // the iterator is positioned over the last span that contributes to + // the defragmented x position. We want to be positioned over the first + // span that contributes to the z position. + // + // x x x y y y z z z + // ^ ^ + // old new + // + // Next once to move onto y, defragment forward to land on the first z + // position. + i.iterSpan = i.iter.Next() + if invariants.Enabled && i.iterSpan == nil && i.iter.Error() == nil { + panic("pebble: invariant violation: no next span while switching directions") + } + // We're now positioned on the first span that was defragmented into the + // current iterator position. Skip over the rest of the current iterator + // position's constitutent fragments. In the above example, this would + // land on the first 'z'. + i.defragmentForward() + if i.iterSpan == nil { + i.iterPos = iterPosCurr + return nil + } + + // Now that we're positioned over the first of the next set of + // fragments, defragment forward. + return i.defragmentForward() + case iterPosCurr: + // iterPosCurr is only used when the iter is exhausted or when the iterator + // is at an empty span. + if invariants.Enabled && i.iterSpan != nil && !i.iterSpan.Empty() { + panic("pebble: invariant violation: iterPosCurr with valid iterSpan") + } + + i.iterSpan = i.iter.Next() + if i.iterSpan == nil { + return nil + } + return i.defragmentForward() + case iterPosNext: + // Already at the next span. + if i.iterSpan == nil { + i.iterPos = iterPosCurr + return nil + } + return i.defragmentForward() + default: + panic("unreachable") + } +} + +// Prev steps back to the previous span and returns it. +func (i *DefragmentingIter) Prev() *Span { + switch i.iterPos { + case iterPosPrev: + // Already at the previous span. + if i.iterSpan == nil { + i.iterPos = iterPosCurr + return nil + } + return i.defragmentBackward() + case iterPosCurr: + // iterPosCurr is only used when the iter is exhausted or when the iterator + // is at an empty span. + if invariants.Enabled && i.iterSpan != nil && !i.iterSpan.Empty() { + panic("pebble: invariant violation: iterPosCurr with valid iterSpan") + } + + i.iterSpan = i.iter.Prev() + if i.iterSpan == nil { + return nil + } + return i.defragmentBackward() + case iterPosNext: + // Switching directions; The iterator is currently positioned over the + // first fragment of the next set of fragments. In the below diagram, + // the iterator is positioned over the first span that contributes to + // the defragmented z position. We want to be positioned over the last + // span that contributes to the x position. + // + // x x x y y y z z z + // ^ ^ + // new old + // + // Prev once to move onto y, defragment backward to land on the last x + // position. + i.iterSpan = i.iter.Prev() + if invariants.Enabled && i.iterSpan == nil && i.iter.Error() == nil { + panic("pebble: invariant violation: no previous span while switching directions") + } + // We're now positioned on the last span that was defragmented into the + // current iterator position. Skip over the rest of the current iterator + // position's constitutent fragments. In the above example, this would + // land on the last 'x'. + i.defragmentBackward() + + // Now that we're positioned over the last of the prev set of + // fragments, defragment backward. + if i.iterSpan == nil { + i.iterPos = iterPosCurr + return nil + } + return i.defragmentBackward() + default: + panic("unreachable") + } +} + +// checkEqual checks the two spans for logical equivalence. It uses the passed-in +// DefragmentMethod and ensures both spans are NOT empty; not defragmenting empty +// spans is an optimization that lets us load fewer sstable blocks. +func (i *DefragmentingIter) checkEqual(left, right *Span) bool { + return (!left.Empty() && !right.Empty()) && i.method.ShouldDefragment(i.equal, i.iterSpan, &i.curr) +} + +// defragmentForward defragments spans in the forward direction, starting from +// i.iter's current position. The span at the current position must be non-nil, +// but may be Empty(). +func (i *DefragmentingIter) defragmentForward() *Span { + if i.iterSpan.Empty() { + // An empty span will never be equal to another span; see checkEqual for + // why. To avoid loading non-empty range keys further ahead by calling Next, + // return early. + i.iterPos = iterPosCurr + return i.iterSpan + } + i.saveCurrent() + + i.iterPos = iterPosNext + i.iterSpan = i.iter.Next() + for i.iterSpan != nil { + if !i.equal(i.curr.End, i.iterSpan.Start) { + // Not a continuation. + break + } + if !i.checkEqual(i.iterSpan, &i.curr) { + // Not a continuation. + break + } + i.keyBuf = append(i.keyBuf[:0], i.iterSpan.End...) + i.curr.End = i.keyBuf + i.keysBuf = i.reduce(i.keysBuf, i.iterSpan.Keys) + i.iterSpan = i.iter.Next() + } + // i.iterSpan == nil + // + // The inner iterator may return nil when it encounters an error. If there + // was an error, we don't know whether there is another span we should + // defragment or not. Return nil so that the caller knows they should check + // Error(). + if i.iter.Error() != nil { + return nil + } + i.curr.Keys = i.keysBuf + return &i.curr +} + +// defragmentBackward defragments spans in the backward direction, starting from +// i.iter's current position. The span at the current position must be non-nil, +// but may be Empty(). +func (i *DefragmentingIter) defragmentBackward() *Span { + if i.iterSpan.Empty() { + // An empty span will never be equal to another span; see checkEqual for + // why. To avoid loading non-empty range keys further ahead by calling Next, + // return early. + i.iterPos = iterPosCurr + return i.iterSpan + } + i.saveCurrent() + + i.iterPos = iterPosPrev + i.iterSpan = i.iter.Prev() + for i.iterSpan != nil { + if !i.equal(i.curr.Start, i.iterSpan.End) { + // Not a continuation. + break + } + if !i.checkEqual(i.iterSpan, &i.curr) { + // Not a continuation. + break + } + i.keyBuf = append(i.keyBuf[:0], i.iterSpan.Start...) + i.curr.Start = i.keyBuf + i.keysBuf = i.reduce(i.keysBuf, i.iterSpan.Keys) + i.iterSpan = i.iter.Prev() + } + // i.iterSpan == nil + // + // The inner iterator may return nil when it encounters an error. If there + // was an error, we don't know whether there is another span we should + // defragment or not. Return nil so that the caller knows they should check + // Error(). + if i.iter.Error() != nil { + return nil + } + i.curr.Keys = i.keysBuf + return &i.curr +} + +func (i *DefragmentingIter) saveCurrent() { + i.currBuf.Reset() + i.keysBuf = i.keysBuf[:0] + i.keyBuf = i.keyBuf[:0] + if i.iterSpan == nil { + return + } + i.curr = Span{ + Start: i.saveBytes(i.iterSpan.Start), + End: i.saveBytes(i.iterSpan.End), + KeysOrder: i.iterSpan.KeysOrder, + } + for j := range i.iterSpan.Keys { + i.keysBuf = append(i.keysBuf, Key{ + Trailer: i.iterSpan.Keys[j].Trailer, + Suffix: i.saveBytes(i.iterSpan.Keys[j].Suffix), + Value: i.saveBytes(i.iterSpan.Keys[j].Value), + }) + } + i.curr.Keys = i.keysBuf +} + +func (i *DefragmentingIter) saveBytes(b []byte) []byte { + if b == nil { + return nil + } + i.currBuf, b = i.currBuf.Copy(b) + return b +} diff --git a/pebble/internal/keyspan/defragment_test.go b/pebble/internal/keyspan/defragment_test.go new file mode 100644 index 0000000..b9856da --- /dev/null +++ b/pebble/internal/keyspan/defragment_test.go @@ -0,0 +1,271 @@ +// 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 keyspan + +import ( + "bytes" + "fmt" + "math/rand" + "sort" + "strings" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/pmezard/go-difflib/difflib" +) + +func TestDefragmentingIter(t *testing.T) { + comparer := testkeys.Comparer + cmp := comparer.Compare + internalEqual := DefragmentInternal + alwaysEqual := DefragmentMethodFunc(func(_ base.Equal, _, _ *Span) bool { return true }) + staticReducer := StaticDefragmentReducer + collectReducer := func(cur, next []Key) []Key { + c := keysBySeqNumKind(append(cur, next...)) + sort.Sort(&c) + return c + } + + var buf bytes.Buffer + var spans []Span + datadriven.RunTest(t, "testdata/defragmenting_iter", func(t *testing.T, td *datadriven.TestData) string { + buf.Reset() + switch td.Cmd { + case "define": + spans = spans[:0] + lines := strings.Split(strings.TrimSpace(td.Input), "\n") + for _, line := range lines { + spans = append(spans, ParseSpan(line)) + } + return "" + case "iter": + equal := internalEqual + reducer := staticReducer + var probes []probe + for _, cmdArg := range td.CmdArgs { + switch cmd := cmdArg.Key; cmd { + case "equal": + if len(cmdArg.Vals) != 1 { + return fmt.Sprintf("only one equal func expected; got %d", len(cmdArg.Vals)) + } + switch val := cmdArg.Vals[0]; val { + case "internal": + equal = internalEqual + case "always": + equal = alwaysEqual + default: + return fmt.Sprintf("unknown reducer %s", val) + } + case "reducer": + if len(cmdArg.Vals) != 1 { + return fmt.Sprintf("only one reducer expected; got %d", len(cmdArg.Vals)) + } + switch val := cmdArg.Vals[0]; val { + case "collect": + reducer = collectReducer + case "static": + reducer = staticReducer + default: + return fmt.Sprintf("unknown reducer %s", val) + } + case "probes": + probes = parseProbes(cmdArg.Vals...) + default: + return fmt.Sprintf("unknown command: %s", cmd) + } + } + var miter MergingIter + miter.Init(cmp, noopTransform, new(MergingBuffers), NewIter(cmp, spans)) + innerIter := attachProbes(&miter, probeContext{log: &buf}, probes...) + var iter DefragmentingIter + iter.Init(comparer, innerIter, equal, reducer, new(DefragmentingBuffers)) + for _, line := range strings.Split(td.Input, "\n") { + runIterOp(&buf, &iter, line) + fmt.Fprintln(&buf) + } + return strings.TrimSpace(buf.String()) + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +func TestDefragmentingIter_Randomized(t *testing.T) { + seed := time.Now().UnixNano() + for i := int64(0); i < 100; i++ { + testDefragmentingIteRandomizedOnce(t, seed+i) + } +} + +func TestDefragmentingIter_RandomizedFixedSeed(t *testing.T) { + const seed = 1648173101214881000 + testDefragmentingIteRandomizedOnce(t, seed) +} + +func testDefragmentingIteRandomizedOnce(t *testing.T, seed int64) { + comparer := testkeys.Comparer + cmp := comparer.Compare + formatKey := comparer.FormatKey + + rng := rand.New(rand.NewSource(seed)) + t.Logf("seed = %d", seed) + + // Use a key space of alphanumeric strings, with a random max length between + // 1-2. Repeat keys are more common at the lower max lengths. + ks := testkeys.Alpha(rng.Intn(2) + 1) + + // Generate between 1-15 range keys. + const maxRangeKeys = 15 + var original, fragmented []Span + numRangeKeys := 1 + rng.Intn(maxRangeKeys) + for i := 0; i < numRangeKeys; i++ { + startIdx := rng.Int63n(ks.Count()) + endIdx := rng.Int63n(ks.Count()) + for startIdx == endIdx { + endIdx = rng.Int63n(ks.Count()) + } + if startIdx > endIdx { + startIdx, endIdx = endIdx, startIdx + } + + key := Key{ + Trailer: base.MakeTrailer(uint64(i), base.InternalKeyKindRangeKeySet), + Value: []byte(fmt.Sprintf("v%d", rng.Intn(3))), + } + // Generate suffixes 0, 1, 2, or 3 with 0 indicating none. + if suffix := rng.Int63n(4); suffix > 0 { + key.Suffix = testkeys.Suffix(suffix) + } + original = append(original, Span{ + Start: testkeys.Key(ks, startIdx), + End: testkeys.Key(ks, endIdx), + Keys: []Key{key}, + }) + + for startIdx < endIdx { + width := rng.Int63n(endIdx-startIdx) + 1 + fragmented = append(fragmented, Span{ + Start: testkeys.Key(ks, startIdx), + End: testkeys.Key(ks, startIdx+width), + Keys: []Key{key}, + }) + startIdx += width + } + } + + // Both the original and the deliberately fragmented spans may contain + // overlaps, so we need to sort and fragment them. + original = fragment(cmp, formatKey, original) + fragmented = fragment(cmp, formatKey, fragmented) + + var originalInner MergingIter + originalInner.Init(cmp, noopTransform, new(MergingBuffers), NewIter(cmp, original)) + var fragmentedInner MergingIter + fragmentedInner.Init(cmp, noopTransform, new(MergingBuffers), NewIter(cmp, fragmented)) + + var referenceIter, fragmentedIter DefragmentingIter + referenceIter.Init(comparer, &originalInner, DefragmentInternal, StaticDefragmentReducer, new(DefragmentingBuffers)) + fragmentedIter.Init(comparer, &fragmentedInner, DefragmentInternal, StaticDefragmentReducer, new(DefragmentingBuffers)) + + // Generate 100 random operations and run them against both iterators. + const numIterOps = 100 + type opKind struct { + weight int + fn func() string + } + ops := []opKind{ + {weight: 2, fn: func() string { return "first" }}, + {weight: 2, fn: func() string { return "last" }}, + {weight: 50, fn: func() string { return "next" }}, + {weight: 50, fn: func() string { return "prev" }}, + {weight: 5, fn: func() string { + k := testkeys.Key(ks, rng.Int63n(ks.Count())) + return fmt.Sprintf("seekge(%s)", k) + }}, + {weight: 5, fn: func() string { + k := testkeys.Key(ks, rng.Int63n(ks.Count())) + return fmt.Sprintf("seeklt(%s)", k) + }}, + } + var totalWeight int + for _, op := range ops { + totalWeight += op.weight + } + var referenceHistory, fragmentedHistory bytes.Buffer + for i := 0; i < numIterOps; i++ { + p := rng.Intn(totalWeight) + opIndex := 0 + if i == 0 { + // First op is always a First(). + } else { + for i, op := range ops { + if p < op.weight { + opIndex = i + break + } + p -= op.weight + } + } + op := ops[opIndex].fn() + runIterOp(&referenceHistory, &referenceIter, op) + runIterOp(&fragmentedHistory, &fragmentedIter, op) + if !bytes.Equal(referenceHistory.Bytes(), fragmentedHistory.Bytes()) { + t.Fatal(debugContext(cmp, formatKey, original, fragmented, + referenceHistory.String(), fragmentedHistory.String())) + } + fmt.Fprintln(&referenceHistory) + fmt.Fprintln(&fragmentedHistory) + } +} + +func fragment(cmp base.Compare, formatKey base.FormatKey, spans []Span) []Span { + Sort(cmp, spans) + var fragments []Span + f := Fragmenter{ + Cmp: cmp, + Format: formatKey, + Emit: func(f Span) { + fragments = append(fragments, f) + }, + } + for _, s := range spans { + f.Add(s) + } + f.Finish() + return fragments +} + +func debugContext( + cmp base.Compare, + formatKey base.FormatKey, + original, fragmented []Span, + refHistory, fragHistory string, +) string { + var buf bytes.Buffer + fmt.Fprintln(&buf, "Reference:") + for _, s := range original { + fmt.Fprintln(&buf, s) + } + fmt.Fprintln(&buf) + fmt.Fprintln(&buf, "Fragmented:") + for _, s := range fragmented { + fmt.Fprintln(&buf, s) + } + fmt.Fprintln(&buf) + fmt.Fprintln(&buf, "\nOperations diff:") + diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(refHistory), + B: difflib.SplitLines(fragHistory), + Context: 5, + }) + if err != nil { + panic(err) + } + fmt.Fprintln(&buf, diff) + return buf.String() +} diff --git a/pebble/internal/keyspan/doc.go b/pebble/internal/keyspan/doc.go new file mode 100644 index 0000000..e05aad2 --- /dev/null +++ b/pebble/internal/keyspan/doc.go @@ -0,0 +1,13 @@ +// Package keyspan provides facilities for sorting, fragmenting and +// iterating over spans of user keys. +// +// A Span represents a range of user key space with an inclusive start +// key and exclusive end key. A span may hold any number of Keys which are +// applied over the entirety of the span's keyspace. +// +// Spans are used within Pebble as an in-memory representation of range +// deletion tombstones, and range key sets, unsets and deletes. Spans +// are fragmented at overlapping key boundaries by the Fragmenter type. +// This package's various iteration facilities require these +// non-overlapping fragmented spans. +package keyspan diff --git a/pebble/internal/keyspan/filter.go b/pebble/internal/keyspan/filter.go new file mode 100644 index 0000000..a63a43c --- /dev/null +++ b/pebble/internal/keyspan/filter.go @@ -0,0 +1,115 @@ +// 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 keyspan + +import "github.com/cockroachdb/pebble/internal/base" + +// FilterFunc defines a transform from the input Span into the output Span. The +// function returns true if the Span should be returned by the iterator, and +// false if the Span should be skipped. The FilterFunc is permitted to mutate +// the output Span, for example, to elice certain keys, or update the Span's +// bounds if so desired. The output Span's Keys slice may be reused to reduce +// allocations. +type FilterFunc func(in *Span, out *Span) (keep bool) + +// filteringIter is a FragmentIterator that uses a FilterFunc to select which +// Spans from the input iterator are returned in the output. +// +// A note on Span lifetimes: as the FilterFunc reuses a Span with a mutable +// slice of Keys to reduce allocations, Spans returned by this iterator are only +// valid until the next relative or absolute positioning method is called. +type filteringIter struct { + iter FragmentIterator + filterFn FilterFunc + cmp base.Compare + + // span is a mutable Span passed to the filterFn. The filterFn is free to + // mutate this Span. The slice of Keys in the Span is reused with every call + // to the filterFn. + span Span +} + +var _ FragmentIterator = (*filteringIter)(nil) + +// Filter returns a new filteringIter that will filter the Spans from the +// provided child iterator using the provided FilterFunc. +func Filter(iter FragmentIterator, filter FilterFunc, cmp base.Compare) FragmentIterator { + return &filteringIter{iter: iter, filterFn: filter, cmp: cmp} +} + +// SeekGE implements FragmentIterator. +func (i *filteringIter) SeekGE(key []byte) *Span { + span := i.filter(i.iter.SeekGE(key), +1) + // i.filter could return a span that's less than key, _if_ the filterFunc + // (which has no knowledge of the seek key) mutated the span to end at a key + // less than or equal to `key`. Detect this case and next/invalidate the iter. + if span != nil && i.cmp(span.End, key) <= 0 { + return i.Next() + } + return span +} + +// SeekLT implements FragmentIterator. +func (i *filteringIter) SeekLT(key []byte) *Span { + span := i.filter(i.iter.SeekLT(key), -1) + // i.filter could return a span that's >= 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. +func (i *filteringIter) First() *Span { + return i.filter(i.iter.First(), +1) +} + +// Last implements FragmentIterator. +func (i *filteringIter) Last() *Span { + return i.filter(i.iter.Last(), -1) +} + +// Next implements FragmentIterator. +func (i *filteringIter) Next() *Span { + return i.filter(i.iter.Next(), +1) +} + +// Prev implements FragmentIterator. +func (i *filteringIter) Prev() *Span { + return i.filter(i.iter.Prev(), -1) +} + +// Error implements FragmentIterator. +func (i *filteringIter) Error() error { + return i.iter.Error() +} + +// Close implements FragmentIterator. +func (i *filteringIter) Close() error { + return i.iter.Close() +} + +// filter uses the filterFn (if configured) to filter and possibly mutate the +// given Span. If the current Span is to be skipped, the iterator continues +// iterating in the given direction until it lands on a Span that should be +// returned, or the iterator becomes invalid. +func (i *filteringIter) filter(span *Span, dir int8) *Span { + if i.filterFn == nil { + return span + } + for i.Error() == nil && span != nil { + if keep := i.filterFn(span, &i.span); keep { + return &i.span + } + if dir == +1 { + span = i.iter.Next() + } else { + span = i.iter.Prev() + } + } + return span +} diff --git a/pebble/internal/keyspan/filter_test.go b/pebble/internal/keyspan/filter_test.go new file mode 100644 index 0000000..beb4de8 --- /dev/null +++ b/pebble/internal/keyspan/filter_test.go @@ -0,0 +1,79 @@ +// 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 keyspan + +import ( + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" +) + +func TestFilteringIter(t *testing.T) { + // makeFilter returns a FilterFunc that will filter out all keys in a Span + // that are not of the given kind. Empty spans are skipped. + makeFilter := func(kind base.InternalKeyKind) FilterFunc { + return func(in *Span, out *Span) (keep bool) { + out.Start, out.End = in.Start, in.End + out.Keys = out.Keys[:0] + for _, k := range in.Keys { + if k.Kind() != kind { + continue + } + out.Keys = append(out.Keys, k) + } + return len(out.Keys) > 0 + } + } + + cmp := testkeys.Comparer.Compare + var spans []Span + datadriven.RunTest(t, "testdata/filtering_iter", func(t *testing.T, td *datadriven.TestData) string { + switch cmd := td.Cmd; cmd { + case "define": + spans = spans[:0] + lines := strings.Split(strings.TrimSpace(td.Input), "\n") + for _, line := range lines { + spans = append(spans, ParseSpan(line)) + } + return "" + + case "iter": + var filter FilterFunc + for _, cmdArg := range td.CmdArgs { + switch cmdArg.Key { + case "filter": + for _, s := range cmdArg.Vals { + switch s { + case "no-op": + filter = nil + case "key-kind-set": + filter = makeFilter(base.InternalKeyKindRangeKeySet) + case "key-kind-unset": + filter = makeFilter(base.InternalKeyKindRangeKeyUnset) + case "key-kind-del": + filter = makeFilter(base.InternalKeyKindRangeKeyDelete) + default: + return fmt.Sprintf("unknown filter: %s", s) + } + } + default: + return fmt.Sprintf("unknown command: %s", cmdArg.Key) + } + } + innerIter := NewIter(cmp, spans) + iter := Filter(innerIter, filter, cmp) + defer iter.Close() + s := runFragmentIteratorCmd(iter, td.Input, nil) + return s + + default: + return fmt.Sprintf("unknown command: %s", cmd) + } + }) +} diff --git a/pebble/internal/keyspan/fragmenter.go b/pebble/internal/keyspan/fragmenter.go new file mode 100644 index 0000000..d4a410d --- /dev/null +++ b/pebble/internal/keyspan/fragmenter.go @@ -0,0 +1,483 @@ +// Copyright 2018 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 ( + "fmt" + "sort" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" +) + +type spansByStartKey struct { + cmp base.Compare + buf []Span +} + +func (v *spansByStartKey) Len() int { return len(v.buf) } +func (v *spansByStartKey) Less(i, j int) bool { + return v.cmp(v.buf[i].Start, v.buf[j].Start) < 0 +} +func (v *spansByStartKey) Swap(i, j int) { + v.buf[i], v.buf[j] = v.buf[j], v.buf[i] +} + +type spansByEndKey struct { + cmp base.Compare + buf []Span +} + +func (v *spansByEndKey) Len() int { return len(v.buf) } +func (v *spansByEndKey) Less(i, j int) bool { + return v.cmp(v.buf[i].End, v.buf[j].End) < 0 +} +func (v *spansByEndKey) Swap(i, j int) { + v.buf[i], v.buf[j] = v.buf[j], v.buf[i] +} + +// keysBySeqNumKind sorts spans by the start key's sequence number in +// descending order. If two spans have equal sequence number, they're compared +// by key kind in descending order. This ordering matches the ordering of +// base.InternalCompare among keys with matching user keys. +type keysBySeqNumKind []Key + +func (v *keysBySeqNumKind) Len() int { return len(*v) } +func (v *keysBySeqNumKind) Less(i, j int) bool { return (*v)[i].Trailer > (*v)[j].Trailer } +func (v *keysBySeqNumKind) Swap(i, j int) { (*v)[i], (*v)[j] = (*v)[j], (*v)[i] } + +// Sort the spans by start key. This is the ordering required by the +// Fragmenter. Usually spans are naturally sorted by their start key, +// but that isn't true for range deletion tombstones in the legacy +// range-del-v1 block format. +func Sort(cmp base.Compare, spans []Span) { + sorter := spansByStartKey{ + cmp: cmp, + buf: spans, + } + sort.Sort(&sorter) +} + +// Fragmenter fragments a set of spans such that overlapping spans are +// split at their overlap points. The fragmented spans are output to the +// supplied Output function. +type Fragmenter struct { + Cmp base.Compare + Format base.FormatKey + // Emit is called to emit a fragmented span and its keys. Every key defined + // within the emitted Span applies to the entirety of the Span's key span. + // Keys are ordered in decreasing order of their sequence numbers, and if + // equal, decreasing order of key kind. + Emit func(Span) + // pending contains the list of pending fragments that have not been + // flushed to the block writer. Note that the spans have not been + // fragmented on the end keys yet. That happens as the spans are + // flushed. All pending spans have the same Start. + pending []Span + // doneBuf is used to buffer completed span fragments when flushing to a + // specific key (e.g. TruncateAndFlushTo). It is cached in the Fragmenter to + // allow reuse. + doneBuf []Span + // sortBuf is used to sort fragments by end key when flushing. + sortBuf spansByEndKey + // flushBuf is used to sort keys by (seqnum,kind) before emitting. + flushBuf keysBySeqNumKind + // flushedKey is the key that fragments have been flushed up to. Any + // additional spans added to the fragmenter must have a start key >= + // flushedKey. A nil value indicates flushedKey has not been set. + flushedKey []byte + finished bool +} + +func (f *Fragmenter) checkInvariants(buf []Span) { + for i := 1; i < len(buf); i++ { + if f.Cmp(buf[i].Start, buf[i].End) >= 0 { + panic(fmt.Sprintf("pebble: empty pending span invariant violated: %s", buf[i])) + } + if f.Cmp(buf[i-1].Start, buf[i].Start) != 0 { + panic(fmt.Sprintf("pebble: pending span invariant violated: %s %s", + f.Format(buf[i-1].Start), f.Format(buf[i].Start))) + } + } +} + +// Add adds a span to the fragmenter. Spans may overlap and the +// fragmenter will internally split them. The spans must be presented in +// increasing start key order. That is, Add must be called with a series +// of spans like: +// +// a---e +// c---g +// c-----i +// j---n +// j-l +// +// We need to fragment the spans at overlap points. In the above +// example, we'd create: +// +// a-c-e +// c-e-g +// c-e-g-i +// j-l-n +// j-l +// +// The fragments need to be output sorted by start key, and for equal start +// keys, sorted by descending sequence number. This last part requires a mild +// bit of care as the fragments are not created in descending sequence number +// order. +// +// Once a start key has been seen, we know that we'll never see a smaller +// start key and can thus flush all of the fragments that lie before that +// start key. +// +// Walking through the example above, we start with: +// +// a---e +// +// Next we add [c,g) resulting in: +// +// a-c-e +// c---g +// +// The fragment [a,c) is flushed leaving the pending spans as: +// +// c-e +// c---g +// +// The next span is [c,i): +// +// c-e +// c---g +// c-----i +// +// No fragments are flushed. The next span is [j,n): +// +// c-e +// c---g +// c-----i +// j---n +// +// The fragments [c,e), [c,g) and [c,i) are flushed. We sort these fragments +// by their end key, then split the fragments on the end keys: +// +// c-e +// c-e-g +// c-e---i +// +// The [c,e) fragments all get flushed leaving: +// +// e-g +// e---i +// +// This process continues until there are no more fragments to flush. +// +// WARNING: the slices backing Start, End, Keys, Key.Suffix and Key.Value are +// all retained after this method returns and should not be modified. This is +// safe for spans that are added from a memtable or batch. It is partially +// unsafe for a span read from an sstable. Specifically, the Keys slice of a +// Span returned during sstable iteration is only valid until the next iterator +// operation. The stability of the user keys depend on whether the block is +// prefix compressed, and in practice Pebble never prefix compresses range +// deletion and range key blocks, so these keys are stable. Because of this key +// stability, typically callers only need to perform a shallow clone of the Span +// before Add-ing it to the fragmenter. +// +// Add requires the provided span's keys are sorted in Trailer descending order. +func (f *Fragmenter) Add(s Span) { + if f.finished { + panic("pebble: span fragmenter already finished") + } else if s.KeysOrder != ByTrailerDesc { + panic("pebble: span keys unexpectedly not in trailer descending order") + } + if f.flushedKey != nil { + switch c := f.Cmp(s.Start, f.flushedKey); { + case c < 0: + panic(fmt.Sprintf("pebble: start key (%s) < flushed key (%s)", + f.Format(s.Start), f.Format(f.flushedKey))) + } + } + if f.Cmp(s.Start, s.End) >= 0 { + // An empty span, we can ignore it. + return + } + if invariants.RaceEnabled { + f.checkInvariants(f.pending) + defer func() { f.checkInvariants(f.pending) }() + } + + if len(f.pending) > 0 { + // Since all of the pending spans have the same start key, we only need + // to compare against the first one. + switch c := f.Cmp(f.pending[0].Start, s.Start); { + case c > 0: + panic(fmt.Sprintf("pebble: keys must be added in order: %s > %s", + f.Format(f.pending[0].Start), f.Format(s.Start))) + case c == 0: + // The new span has the same start key as the existing pending + // spans. Add it to the pending buffer. + f.pending = append(f.pending, s) + return + } + + // At this point we know that the new start key is greater than the pending + // spans start keys. + f.truncateAndFlush(s.Start) + } + + f.pending = append(f.pending, s) +} + +// Cover is returned by Framenter.Covers and describes a span's relationship to +// a key at a particular snapshot. +type Cover int8 + +const ( + // NoCover indicates the tested key does not fall within the span's bounds, + // or the span contains no keys with sequence numbers higher than the key's. + NoCover Cover = iota + // CoversInvisibly indicates the tested key does fall within the span's + // bounds and the span contains at least one key with a higher sequence + // number, but none visible at the provided snapshot. + CoversInvisibly + // CoversVisibly indicates the tested key does fall within the span's + // bounds, and the span constains at least one key with a sequence number + // higher than the key's sequence number that is visible at the provided + // snapshot. + CoversVisibly +) + +// Covers returns an enum indicating whether the specified key is covered by one +// of the pending keys. The provided key must be consistent with the ordering of +// the spans. That is, it is invalid to specify a key here that is out of order +// with the span start keys passed to Add. +func (f *Fragmenter) Covers(key base.InternalKey, snapshot uint64) Cover { + if f.finished { + panic("pebble: span fragmenter already finished") + } + if len(f.pending) == 0 { + return NoCover + } + + if f.Cmp(f.pending[0].Start, key.UserKey) > 0 { + panic(fmt.Sprintf("pebble: keys must be in order: %s > %s", + f.Format(f.pending[0].Start), key.Pretty(f.Format))) + } + + cover := NoCover + seqNum := key.SeqNum() + for _, s := range f.pending { + if f.Cmp(key.UserKey, s.End) < 0 { + // NB: A range deletion tombstone does not delete a point operation + // at the same sequence number, and broadly a span is not considered + // to cover a point operation at the same sequence number. + + for i := range s.Keys { + if kseq := s.Keys[i].SeqNum(); kseq > seqNum { + // This key from the span has a higher sequence number than + // `key`. It covers `key`, although the span's key might not + // be visible if its snapshot is too high. + // + // Batch keys are always be visible. + if kseq < snapshot || kseq&base.InternalKeySeqNumBatch != 0 { + return CoversVisibly + } + // s.Keys[i] is not visible. + cover = CoversInvisibly + } + } + } + } + return cover +} + +// Empty returns true if all fragments added so far have finished flushing. +func (f *Fragmenter) Empty() bool { + return f.finished || len(f.pending) == 0 +} + +// TruncateAndFlushTo flushes all of the fragments with a start key <= key, +// truncating spans to the specified end key. Used during compaction to force +// emitting of spans which straddle an sstable boundary. Consider +// the scenario: +// +// a---------k#10 +// f#8 +// f#7 +// +// Let's say the next user key after f is g. Calling TruncateAndFlushTo(g) will +// flush this span: +// +// a-------g#10 +// f#8 +// f#7 +// +// And leave this one in f.pending: +// +// g----k#10 +// +// WARNING: The fragmenter could hold on to the specified end key. Ensure it's +// a safe byte slice that could outlast the current sstable output, and one +// that will never be modified. +func (f *Fragmenter) TruncateAndFlushTo(key []byte) { + if f.finished { + panic("pebble: span fragmenter already finished") + } + if f.flushedKey != nil { + switch c := f.Cmp(key, f.flushedKey); { + case c < 0: + panic(fmt.Sprintf("pebble: start key (%s) < flushed key (%s)", + f.Format(key), f.Format(f.flushedKey))) + } + } + if invariants.RaceEnabled { + f.checkInvariants(f.pending) + defer func() { f.checkInvariants(f.pending) }() + } + if len(f.pending) > 0 { + // Since all of the pending spans have the same start key, we only need + // to compare against the first one. + switch c := f.Cmp(f.pending[0].Start, key); { + case c > 0: + panic(fmt.Sprintf("pebble: keys must be added in order: %s > %s", + f.Format(f.pending[0].Start), f.Format(key))) + case c == 0: + return + } + } + f.truncateAndFlush(key) +} + +// Start returns the start key of the first span in the pending buffer, or nil +// if there are no pending spans. The start key of all pending spans is the same +// as that of the first one. +func (f *Fragmenter) Start() []byte { + if len(f.pending) > 0 { + return f.pending[0].Start + } + return nil +} + +// Flushes all pending spans up to key (exclusive). +// +// WARNING: The specified key is stored without making a copy, so all callers +// must ensure it is safe. +func (f *Fragmenter) truncateAndFlush(key []byte) { + f.flushedKey = append(f.flushedKey[:0], key...) + done := f.doneBuf[:0] + pending := f.pending + f.pending = f.pending[:0] + + // pending and f.pending share the same underlying storage. As we iterate + // over pending we append to f.pending, but only one entry is appended in + // each iteration, after we have read the entry being overwritten. + for _, s := range pending { + if f.Cmp(key, s.End) < 0 { + // s: a--+--e + // new: c------ + if f.Cmp(s.Start, key) < 0 { + done = append(done, Span{ + Start: s.Start, + End: key, + Keys: s.Keys, + }) + } + f.pending = append(f.pending, Span{ + Start: key, + End: s.End, + Keys: s.Keys, + }) + } else { + // s: a-----e + // new: e---- + done = append(done, s) + } + } + + f.doneBuf = done[:0] + f.flush(done, nil) +} + +// flush a group of range spans to the block. The spans are required to all have +// the same start key. We flush all span fragments until startKey > lastKey. If +// lastKey is nil, all span fragments are flushed. The specification of a +// non-nil lastKey occurs for range deletion tombstones during compaction where +// we want to flush (but not truncate) all range tombstones that start at or +// before the first key in the next sstable. Consider: +// +// a---e#10 +// a------h#9 +// +// If a compaction splits the sstables at key c we want the first sstable to +// contain the tombstones [a,e)#10 and [a,e)#9. Fragmentation would naturally +// produce a tombstone [e,h)#9, but we don't need to output that tombstone to +// the first sstable. +func (f *Fragmenter) flush(buf []Span, lastKey []byte) { + if invariants.RaceEnabled { + f.checkInvariants(buf) + } + + // Sort the spans by end key. This will allow us to walk over the spans and + // easily determine the next split point (the smallest end-key). + f.sortBuf.cmp = f.Cmp + f.sortBuf.buf = buf + sort.Sort(&f.sortBuf) + + // Loop over the spans, splitting by end key. + for len(buf) > 0 { + // A prefix of spans will end at split. remove represents the count of + // that prefix. + remove := 1 + split := buf[0].End + f.flushBuf = append(f.flushBuf[:0], buf[0].Keys...) + + for i := 1; i < len(buf); i++ { + if f.Cmp(split, buf[i].End) == 0 { + remove++ + } + f.flushBuf = append(f.flushBuf, buf[i].Keys...) + } + + sort.Sort(&f.flushBuf) + + f.Emit(Span{ + Start: buf[0].Start, + End: split, + // Copy the sorted keys to a new slice. + // + // This allocation is an unfortunate side effect of the Fragmenter and + // the expectation that the spans it produces are available in-memory + // indefinitely. + // + // Eventually, we should be able to replace the fragmenter with the + // keyspan.MergingIter which will perform just-in-time + // fragmentation, and only guaranteeing the memory lifetime for the + // current span. The MergingIter fragments while only needing to + // access one Span per level. It only accesses the Span at the + // current position for each level. During compactions, we can write + // these spans to sstables without retaining previous Spans. + Keys: append([]Key(nil), f.flushBuf...), + }) + + if lastKey != nil && f.Cmp(split, lastKey) > 0 { + break + } + + // Adjust the start key for every remaining span. + buf = buf[remove:] + for i := range buf { + buf[i].Start = split + } + } +} + +// Finish flushes any remaining fragments to the output. It is an error to call +// this if any other spans will be added. +func (f *Fragmenter) Finish() { + if f.finished { + panic("pebble: span fragmenter already finished") + } + f.flush(f.pending, nil) + f.finished = true +} diff --git a/pebble/internal/keyspan/fragmenter_test.go b/pebble/internal/keyspan/fragmenter_test.go new file mode 100644 index 0000000..6916f15 --- /dev/null +++ b/pebble/internal/keyspan/fragmenter_test.go @@ -0,0 +1,320 @@ +// Copyright 2018 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 ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" +) + +var spanRe = regexp.MustCompile(`(\d+):\s*(\w+)-*(\w+)\w*([^\n]*)`) + +func parseSpanSingleKey(t *testing.T, s string, kind base.InternalKeyKind) Span { + m := spanRe.FindStringSubmatch(s) + if len(m) != 5 { + t.Fatalf("expected 5 components, but found %d: %s", len(m), s) + } + seqNum, err := strconv.Atoi(m[1]) + require.NoError(t, err) + return Span{ + Start: []byte(m[2]), + End: []byte(m[3]), + Keys: []Key{ + { + Trailer: base.MakeTrailer(uint64(seqNum), kind), + Value: []byte(strings.TrimSpace(m[4])), + }, + }, + } +} + +func buildSpans( + t *testing.T, cmp base.Compare, formatKey base.FormatKey, s string, kind base.InternalKeyKind, +) []Span { + var spans []Span + f := &Fragmenter{ + Cmp: cmp, + Format: formatKey, + Emit: func(fragmented Span) { + spans = append(spans, fragmented) + }, + } + for _, line := range strings.Split(s, "\n") { + if strings.HasPrefix(line, "truncate-and-flush-to ") { + parts := strings.Split(line, " ") + if len(parts) != 2 { + t.Fatalf("expected 2 components, but found %d: %s", len(parts), line) + } + f.TruncateAndFlushTo([]byte(parts[1])) + continue + } + + f.Add(parseSpanSingleKey(t, line, kind)) + } + f.Finish() + return spans +} + +func formatAlphabeticSpans(spans []Span) string { + isLetter := func(b []byte) bool { + if len(b) != 1 { + return false + } + return b[0] >= 'a' && b[0] <= 'z' + } + + var buf bytes.Buffer + for _, v := range spans { + switch { + case !v.Valid(): + fmt.Fprintf(&buf, "\n") + case v.Empty(): + fmt.Fprintf(&buf, "\n") + case !isLetter(v.Start) || !isLetter(v.End) || v.Start[0] == v.End[0]: + for _, k := range v.Keys { + fmt.Fprintf(&buf, "%d: %s-%s", k.SeqNum(), v.Start, v.End) + if len(k.Value) > 0 { + buf.WriteString(strings.Repeat(" ", int('z'-v.End[0]+1))) + buf.WriteString(string(k.Value)) + } + fmt.Fprintln(&buf) + } + default: + for _, k := range v.Keys { + fmt.Fprintf(&buf, "%d: %s%s%s%s", + k.SeqNum(), + strings.Repeat(" ", int(v.Start[0]-'a')), + v.Start, + strings.Repeat("-", int(v.End[0]-v.Start[0]-1)), + v.End) + if len(k.Value) > 0 { + buf.WriteString(strings.Repeat(" ", int('z'-v.End[0]+1))) + buf.WriteString(string(k.Value)) + } + fmt.Fprintln(&buf) + } + } + } + return buf.String() +} + +func TestFragmenter(t *testing.T) { + cmp := base.DefaultComparer.Compare + fmtKey := base.DefaultComparer.FormatKey + + var getRe = regexp.MustCompile(`(\w+)#(\d+)`) + + parseGet := func(t *testing.T, s string) (string, int) { + m := getRe.FindStringSubmatch(s) + if len(m) != 3 { + t.Fatalf("expected 3 components, but found %d", len(m)) + } + seq, err := strconv.Atoi(m[2]) + require.NoError(t, err) + return m[1], seq + } + + var iter FragmentIterator + + // Returns true if the specified pair is deleted at the specified + // read sequence number. Get ignores spans newer than the read sequence + // number. This is a simple version of what full processing of range + // tombstones looks like. + deleted := func(key []byte, seq, readSeq uint64) bool { + s := Get(cmp, iter, key) + return s != nil && s.CoversAt(readSeq, seq) + } + + datadriven.RunTest(t, "testdata/fragmenter", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "build": + return func() (result string) { + defer func() { + if r := recover(); r != nil { + result = fmt.Sprint(r) + } + }() + + spans := buildSpans(t, cmp, fmtKey, d.Input, base.InternalKeyKindRangeDelete) + iter = NewIter(cmp, spans) + return formatAlphabeticSpans(spans) + }() + + case "get": + if len(d.CmdArgs) != 1 { + return fmt.Sprintf("expected 1 argument, but found %s", d.CmdArgs) + } + if d.CmdArgs[0].Key != "t" { + return fmt.Sprintf("expected timestamp argument, but found %s", d.CmdArgs[0]) + } + readSeq, err := strconv.Atoi(d.CmdArgs[0].Vals[0]) + require.NoError(t, err) + + var results []string + for _, p := range strings.Split(d.Input, " ") { + key, seq := parseGet(t, p) + if deleted([]byte(key), uint64(seq), uint64(readSeq)) { + results = append(results, "deleted") + } else { + results = append(results, "alive") + } + } + return strings.Join(results, " ") + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestFragmenterCovers(t *testing.T) { + datadriven.RunTest(t, "testdata/fragmenter_covers", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "build": + f := &Fragmenter{ + Cmp: base.DefaultComparer.Compare, + Format: base.DefaultComparer.FormatKey, + Emit: func(fragmented Span) { + }, + } + var buf bytes.Buffer + for _, line := range strings.Split(d.Input, "\n") { + switch { + case strings.HasPrefix(line, "add "): + t := parseSpanSingleKey(t, strings.TrimPrefix(line, "add "), base.InternalKeyKindRangeDelete) + f.Add(t) + case strings.HasPrefix(line, "deleted "): + fields := strings.Fields(strings.TrimPrefix(line, "deleted ")) + key := base.ParseInternalKey(fields[0]) + snapshot, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + return err.Error() + } + func() { + defer func() { + if r := recover(); r != nil { + fmt.Fprintf(&buf, "%s: %s\n", key, r) + } + }() + switch f.Covers(key, snapshot) { + case NoCover: + fmt.Fprintf(&buf, "%s: none\n", key) + case CoversInvisibly: + fmt.Fprintf(&buf, "%s: invisibly\n", key) + case CoversVisibly: + fmt.Fprintf(&buf, "%s: visibly\n", key) + } + }() + } + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestFragmenterTruncateAndFlushTo(t *testing.T) { + cmp := base.DefaultComparer.Compare + fmtKey := base.DefaultComparer.FormatKey + + datadriven.RunTest(t, "testdata/fragmenter_truncate_and_flush_to", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "build": + return func() (result string) { + defer func() { + if r := recover(); r != nil { + result = fmt.Sprint(r) + } + }() + + spans := buildSpans(t, cmp, fmtKey, d.Input, base.InternalKeyKindRangeDelete) + return formatAlphabeticSpans(spans) + }() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestFragmenter_Values(t *testing.T) { + cmp := base.DefaultComparer.Compare + fmtKey := base.DefaultComparer.FormatKey + + datadriven.RunTest(t, "testdata/fragmenter_values", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "build": + return func() (result string) { + defer func() { + if r := recover(); r != nil { + result = fmt.Sprint(r) + } + }() + + spans := buildSpans(t, cmp, fmtKey, d.Input, base.InternalKeyKindRangeKeySet) + return formatAlphabeticSpans(spans) + }() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestFragmenter_EmitOrder(t *testing.T) { + var buf bytes.Buffer + + datadriven.RunTest(t, "testdata/fragmenter_emit_order", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "build": + buf.Reset() + f := Fragmenter{ + Cmp: base.DefaultComparer.Compare, + Format: base.DefaultComparer.FormatKey, + Emit: func(span Span) { + fmt.Fprintf(&buf, "%s %s:", + base.DefaultComparer.FormatKey(span.Start), + base.DefaultComparer.FormatKey(span.End)) + for i, k := range span.Keys { + if i == 0 { + fmt.Fprint(&buf, " ") + } else { + fmt.Fprint(&buf, ", ") + } + fmt.Fprintf(&buf, "#%d,%s", k.SeqNum(), k.Kind()) + } + fmt.Fprintln(&buf, "\n-") + }, + } + for _, line := range strings.Split(d.Input, "\n") { + fields := strings.Fields(line) + if len(fields) != 2 { + panic(fmt.Sprintf("datadriven test: expect 2 fields, found %d", len(fields))) + } + k := base.ParseInternalKey(fields[0]) + f.Add(Span{ + Start: k.UserKey, + End: []byte(fields[1]), + Keys: []Key{{Trailer: k.Trailer}}, + }) + } + + f.Finish() + return buf.String() + default: + panic(fmt.Sprintf("unrecognized command %q", d.Cmd)) + } + }) +} diff --git a/pebble/internal/keyspan/get.go b/pebble/internal/keyspan/get.go new file mode 100644 index 0000000..c07f8c8 --- /dev/null +++ b/pebble/internal/keyspan/get.go @@ -0,0 +1,53 @@ +// Copyright 2018 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" + +// Get returns the newest span that contains the target key. If no span +// contains the target key, an empty span is returned. The snapshot +// parameter controls the visibility of spans (only spans older than the +// snapshot sequence number are visible). The iterator must contain +// fragmented spans: no span may overlap another. +func Get(cmp base.Compare, iter FragmentIterator, key []byte) *Span { + // NB: We use SeekLT in order to land on the proper span for a search + // key that resides in the middle of a span. Consider the scenario: + // + // a---e + // e---i + // + // The spans are indexed by their start keys `a` and `e`. If the + // search key is `c` we want to land on the span [a,e). If we were + // to use SeekGE then the search key `c` would land on the span + // [e,i) and we'd have to backtrack. The one complexity here is what + // happens for the search key `e`. In that case SeekLT will land us + // on the span [a,e) and we'll have to move forward. + iterSpan := iter.SeekLT(key) + if iterSpan == nil { + iterSpan = iter.Next() + if iterSpan == nil { + // The iterator is empty. + return nil + } + if cmp(key, iterSpan.Start) < 0 { + // The search key lies before the first span. + return nil + } + } + + // Invariant: key > iterSpan.Start + if cmp(key, iterSpan.End) >= 0 { + // The current span lies before the search key. Advance the iterator + // once to potentially land on a key with a start key exactly equal to + // key. (See the comment at the beginning of this function.) + iterSpan = iter.Next() + if iterSpan == nil || cmp(key, iterSpan.Start) < 0 { + // We've run out of spans or we've moved on to a span which + // starts after our search key. + return nil + } + } + return iterSpan +} diff --git a/pebble/internal/keyspan/interleaving_iter.go b/pebble/internal/keyspan/interleaving_iter.go new file mode 100644 index 0000000..e1fd600 --- /dev/null +++ b/pebble/internal/keyspan/interleaving_iter.go @@ -0,0 +1,1149 @@ +// Copyright 2021 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 ( + "context" + "fmt" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" +) + +// A SpanMask may be used to configure an interleaving iterator to skip point +// keys that fall within the bounds of some spans. +type SpanMask interface { + // SpanChanged is invoked by an interleaving iterator whenever the current + // span changes. As the iterator passes into or out of a Span, it invokes + // SpanChanged, passing the new Span. When the iterator passes out of a + // span's boundaries and is no longer covered by any span, SpanChanged is + // invoked with a nil span. + // + // SpanChanged is invoked before SkipPoint, and callers may use SpanChanged + // to recalculate state used by SkipPoint for masking. + // + // SpanChanged may be invoked consecutively with identical spans under some + // circumstances, such as repeatedly absolutely positioning an iterator to + // positions covered by the same span, or while changing directions. + SpanChanged(*Span) + // SkipPoint is invoked by the interleaving iterator whenever the iterator + // encounters a point key covered by a Span. If SkipPoint returns true, the + // interleaving iterator skips the point key and all larger keys with the + // same prefix. This is used during range key iteration to skip over point + // keys 'masked' by range keys. + SkipPoint(userKey []byte) bool +} + +// InterleavingIter combines an iterator over point keys with an iterator over +// key spans. +// +// Throughout Pebble, some keys apply at single discrete points within the user +// keyspace. Other keys apply over continuous spans of the user key space. +// Internally, iterators over point keys adhere to the base.InternalIterator +// interface, and iterators over spans adhere to the keyspan.FragmentIterator +// interface. The InterleavingIterator wraps a point iterator and span iterator, +// providing access to all the elements of both iterators. +// +// The InterleavingIterator implements the point base.InternalIterator +// interface. After any of the iterator's methods return a key, a caller may +// call Span to retrieve the span covering the returned key, if any. A span is +// considered to 'cover' a returned key if the span's [start, end) bounds +// include the key's user key. +// +// In addition to tracking the current covering span, InterleavingIter returns a +// special InternalKey at span start boundaries. Start boundaries are surfaced +// as a synthetic span marker: an InternalKey with the boundary as the user key, +// the infinite sequence number and a key kind selected from an arbitrary key +// the infinite sequence number and an arbitrary contained key's kind. Since +// which of the Span's key's kind is surfaced is undefined, the caller should +// not use the InternalKey's kind. The caller should only rely on the `Span` +// method for retrieving information about spanning keys. The interleaved +// synthetic keys have the infinite sequence number so that they're interleaved +// before any point keys with the same user key when iterating forward and after +// when iterating backward. +// +// Interleaving the synthetic start key boundaries at the maximum sequence +// number provides an opportunity for the higher-level, public Iterator to +// observe the Span, even if no live points keys exist within the boudns of the +// Span. +// +// When returning a synthetic marker key for a start boundary, InterleavingIter +// will truncate the span's start bound to the SeekGE or SeekPrefixGE search +// key. For example, a SeekGE("d") that finds a span [a, z) may return a +// synthetic span marker key `d#72057594037927935,21`. +// +// If bounds have been applied to the iterator through SetBounds, +// InterleavingIter will truncate the bounds of spans returned through Span to +// the set bounds. The bounds returned through Span are not truncated by a +// SeekGE or SeekPrefixGE search key. Consider, for example SetBounds('c', 'e'), +// with an iterator containing the Span [a,z): +// +// First() = `c#72057594037927935,21` Span() = [c,e) +// SeekGE('d') = `d#72057594037927935,21` Span() = [c,e) +// +// InterleavedIter does not interleave synthetic markers for spans that do not +// contain any keys. +// +// # SpanMask +// +// InterelavingIter takes a SpanMask parameter that may be used to configure the +// behavior of the iterator. See the documentation on the SpanMask type. +// +// All spans containing keys are exposed during iteration. +type InterleavingIter struct { + cmp base.Compare + comparer *base.Comparer + pointIter base.InternalIterator + keyspanIter FragmentIterator + mask SpanMask + + // lower and upper hold the iteration bounds set through SetBounds. + lower, upper []byte + // keyBuf is used to copy SeekGE or SeekPrefixGE arguments when they're used + // to truncate a span. The byte slices backing a SeekGE/SeekPrefixGE search + // keys can come directly from the end user, so they're copied into keyBuf + // to ensure key stability. + keyBuf []byte + // nextPrefixBuf is used during SeekPrefixGE calls to store the truncated + // upper bound of the returned spans. SeekPrefixGE truncates the returned + // spans to an upper bound of the seeked prefix's immediate successor. + 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 + // to be truncated to the iterator bounds, then truncated is set to true and + // Span() must return a pointer to truncatedSpan. + span *Span + // spanMarker holds the synthetic key that is returned when the iterator + // passes over a key span's start bound. + spanMarker base.InternalKey + // truncated indicates whether or not the span at the current position + // needed to be truncated. If it did, truncatedSpan holds the truncated + // span that should be returned. + truncatedSpan Span + truncated bool + + // Keeping all of the bools/uint8s together reduces the sizeof the struct. + + // 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. + spanMarkerTruncated bool + // maskSpanChangedCalled records whether or not the last call to + // SpanMask.SpanChanged provided the current span (i.span) or not. + maskSpanChangedCalled 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 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, + opts InterleavingIterOpts, +) { + *i = InterleavingIter{ + cmp: comparer.Compare, + comparer: comparer, + pointIter: pointIter, + keyspanIter: keyspanIter, + mask: opts.Mask, + lower: opts.LowerBound, + upper: opts.UpperBound, + } +} + +// InitSeekGE may be called after Init but before any positioning method. +// InitSeekGE initializes the current position of the point iterator and then +// performs a SeekGE on the keyspan iterator using the provided key. InitSeekGE +// returns whichever point or keyspan key is smaller. After InitSeekGE, the +// iterator is positioned and may be repositioned using relative positioning +// methods. +// +// This method is used specifically for lazily constructing combined iterators. +// It allows for seeding the iterator with the current position of the point +// iterator. +func (i *InterleavingIter) InitSeekGE( + prefix, key []byte, pointKey *base.InternalKey, pointValue base.LazyValue, +) (*base.InternalKey, base.LazyValue) { + i.dir = +1 + i.clearMask() + 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) + i.computeSmallestPos() + return i.yieldPosition(key, i.nextPos) +} + +// InitSeekLT may be called after Init but before any positioning method. +// InitSeekLT initializes the current position of the point iterator and then +// performs a SeekLT on the keyspan iterator using the provided key. InitSeekLT +// returns whichever point or keyspan key is larger. After InitSeekLT, the +// iterator is positioned and may be repositioned using relative positioning +// methods. +// +// This method is used specifically for lazily constructing combined iterators. +// It allows for seeding the iterator with the current position of the point +// iterator. +func (i *InterleavingIter) InitSeekLT( + key []byte, pointKey *base.InternalKey, pointValue base.LazyValue, +) (*base.InternalKey, base.LazyValue) { + i.dir = -1 + i.clearMask() + i.savePoint(pointKey, pointValue) + i.keyspanSeekLT(key) + i.computeLargestPos() + return i.yieldPosition(i.lower, i.prevPos) +} + +// SeekGE implements (base.InternalIterator).SeekGE. +// +// If there exists a span with a start key ≤ the first matching point key, +// SeekGE will return a synthetic span marker key for the span. If this span's +// start key is less than key, the returned marker will be truncated to key. +// Note that this search-key truncation of the marker's key is not applied to +// the span returned by Span. +// +// NB: In accordance with the base.InternalIterator contract: +// +// i.lower ≤ key +func (i *InterleavingIter) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + i.err = nil + i.clearMask() + i.disablePrefixMode() + 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 + // seek key falls within the existing span's bounds. + 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.saveSpanForward(i.span) + i.savedKeyspan() + } else { + i.keyspanSeekGE(key, nil /* prefix */) + } + + i.dir = +1 + i.computeSmallestPos() + return i.yieldPosition(key, i.nextPos) +} + +// SeekPrefixGE implements (base.InternalIterator).SeekPrefixGE. +// +// If there exists a span with a start key ≤ the first matching point key, +// SeekPrefixGE will return a synthetic span marker key for the span. If this +// span's start key is less than key, the returned marker will be truncated to +// key. Note that this search-key truncation of the marker's key is not applied +// to the span returned by Span. +// +// NB: In accordance with the base.InternalIterator contract: +// +// i.lower ≤ key +func (i *InterleavingIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + i.err = nil + i.clearMask() + 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 + // entire seek prefix key falls within the existing span's bounds. + // + // During a SeekPrefixGE, Pebble defragments range keys within the bounds of + // the prefix. For example, a SeekPrefixGE('c', 'c@8') must defragment the + // any overlapping range keys within the bounds of [c,c\00). + // + // If range keys are fragmented within a prefix (eg, because a version + // within a prefix was chosen as an sstable boundary), then it's possible + // the seek key falls into the current i.span, but the current i.span does + // not wholly cover the seek prefix. + // + // For example, a SeekPrefixGE('d@5') may only defragment a range key to + // the bounds of [c@2,e). A subsequent SeekPrefixGE('c@0') must re-seek the + // keyspan iterator, because although 'c@0' is contained within [c@2,e), the + // full span of the prefix is not. + // + // Similarly, a SeekPrefixGE('a@3') may only defragment a range key to the + // bounds [a,c@8). A subsequent SeekPrefixGE('c@10') must re-seek the + // keyspan iterator, because although 'c@10' is contained within [a,c@8), + // the full span of the prefix is not. + seekKeyspanIter := true + if i.span != nil && i.cmp(prefix, i.span.Start) >= 0 { + 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.saveSpanForward(i.span) + i.savedKeyspan() + seekKeyspanIter = false + } + } + if seekKeyspanIter { + i.keyspanSeekGE(key, prefix) + } + + i.dir = +1 + 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.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 + // seek key falls within the existing span's bounds. + 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.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, 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). + // + // This problem is a consequence of the SeekLT's exclusive search key + // and the fact that we don't perform bounds truncation at every leaf + // iterator. + if i.span != nil && i.truncated && i.cmp(i.truncatedSpan.Start, key) >= 0 { + i.span = nil + } + i.savedKeyspan() + } else { + i.keyspanSeekLT(key) + } + + i.dir = -1 + 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.savePoint(i.pointIter.First()) + i.saveSpanForward(i.keyspanIter.First()) + i.savedKeyspan() + i.dir = +1 + 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.savePoint(i.pointIter.Last()) + i.saveSpanBackward(i.keyspanIter.Last()) + i.savedKeyspan() + i.dir = -1 + i.computeLargestPos() + return i.yieldPosition(i.lower, i.prevPos) +} + +// Next implements (base.InternalIterator).Next. +func (i *InterleavingIter) Next() (*base.InternalKey, base.LazyValue) { + if i.dir == -1 { + // Switching directions. + i.dir = +1 + + if i.mask != nil { + // Clear the mask while we reposition the point iterator. While + // switching directions, we may move the point iterator outside of + // i.span's bounds. + i.clearMask() + } + + // 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. + } + i.nextPos() + return i.yieldPosition(i.lower, i.nextPos) +} + +// NextPrefix implements (base.InternalIterator).NextPrefix. +func (i *InterleavingIter) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) { + if i.dir == -1 { + panic("pebble: cannot switch directions with NextPrefix") + } + + 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() + } + return i.yieldPosition(i.lower, i.nextPos) +} + +// Prev implements (base.InternalIterator).Prev. +func (i *InterleavingIter) Prev() (*base.InternalKey, base.LazyValue) { + if i.dir == +1 { + // Switching directions. + i.dir = -1 + + if i.mask != nil { + // Clear the mask while we reposition the point iterator. While + // switching directions, we may move the point iterator outside of + // i.span's bounds. + i.clearMask() + } + + // 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 + // + // 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. + 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 + } + 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() + } + // Fallthrough to calling i.prevPos. + } + i.prevPos() + return i.yieldPosition(i.lower, i.prevPos) +} + +// 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 +} + +// 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: + panic("i.withinSpan=true and i.span=nil") + case i.pointKey == nil: + // Since i.withinSpan=true, we step onto the end boundary of the + // keyspan. + i.pos = posKeyspanEnd + default: + // 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)) + } +} + +// 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 + } + + 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)) + } +} + +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 + // return nil. + // + // NB: The above is not just an optimization. During + // prefix-iteration mode, the internal iterator contract + // prohibits us from Next-ing beyond the first key + // beyond the iteration prefix. If we didn't already + // stop early, we would need to check if this masked + // point is already beyond the prefix. + return i.yieldNil() + } + // 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. + + // Advance beyond the masked point key. + advance() + continue + } + } + 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() { + advance() + continue + } + return i.yieldSyntheticSpanMarker(lowerBound) + default: + 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.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.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, 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). + // + // This problem is a consequence of the SeekLT's exclusive search key and + // the fact that we don't perform bounds truncation at every leaf iterator. + if i.span != nil && i.truncated && i.cmp(i.truncatedSpan.Start, k) >= 0 { + i.span = nil + } + i.savedKeyspan() +} + +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 + return + } + + // TODO(jackson): The key comparisons below truncate bounds whenever the + // keyspan iterator is repositioned. We could perform this lazily, and do it + // the first time the user actually asks for this span's bounds in + // SpanBounds. This would reduce work in the case where there's no span + // covering the point and the keyspan iterator is non-empty. + + // NB: These truncations don't require setting `keyspanMarkerTruncated`: + // That flag only applies to truncated span marker keys. + if i.lower != nil && i.cmp(i.span.Start, i.lower) < 0 { + i.truncated = true + i.truncatedSpan = *i.span + i.truncatedSpan.Start = i.lower + } + if i.upper != nil && i.cmp(i.upper, i.span.End) < 0 { + if !i.truncated { + i.truncated = true + i.truncatedSpan = *i.span + } + i.truncatedSpan.End = i.upper + } + // If this is a part of a SeekPrefixGE call, we may also need to truncate to + // the prefix's bounds. + if i.prefix != nil { + if !i.truncated { + i.truncated = true + i.truncatedSpan = *i.span + } + if i.cmp(i.prefix, i.truncatedSpan.Start) > 0 { + i.truncatedSpan.Start = i.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 + } + } + + if i.truncated && i.comparer.Equal(i.truncatedSpan.Start, i.truncatedSpan.End) { + i.span = nil + } +} + +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 + return + } + + // TODO(jackson): The key comparisons below truncate bounds whenever the + // keyspan iterator is repositioned. We could perform this lazily, and do it + // the first time the user actually asks for this span's bounds in + // SpanBounds. This would reduce work in the case where there's no span + // covering the point and the keyspan iterator is non-empty. + + // NB: These truncations don't require setting `keyspanMarkerTruncated`: + // That flag only applies to truncated span marker keys. + if i.lower != nil && i.cmp(i.span.Start, i.lower) < 0 { + i.truncated = true + i.truncatedSpan = *i.span + i.truncatedSpan.Start = i.lower + } + if i.upper != nil && i.cmp(i.upper, i.span.End) < 0 { + if !i.truncated { + i.truncated = true + i.truncatedSpan = *i.span + } + i.truncatedSpan.End = i.upper + } + if i.truncated && i.comparer.Equal(i.truncatedSpan.Start, i.truncatedSpan.End) { + i.span = nil + } +} + +func (i *InterleavingIter) yieldNil() (*base.InternalKey, base.LazyValue) { + i.withinSpan = false + i.clearMask() + return i.verify(nil, base.LazyValue{}) +} + +func (i *InterleavingIter) yieldPointKey() (*base.InternalKey, base.LazyValue) { + return i.verify(i.pointKey, i.pointVal) +} + +func (i *InterleavingIter) yieldSyntheticSpanMarker( + lowerBound []byte, +) (*base.InternalKey, base.LazyValue) { + i.spanMarker.UserKey = i.startKey() + i.spanMarker.Trailer = base.MakeTrailer(base.InternalKeySeqNumMax, i.span.Keys[0].Kind()) + + // 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 + // argument is guaranteed to be ≥ i.lower. It may be equal to the SetBounds + // lower bound, or it could come from a SeekGE or SeekPrefixGE search key. + if lowerBound != nil && i.cmp(lowerBound, i.startKey()) > 0 { + // Truncating to the lower bound may violate the upper bound if + // lowerBound == i.upper. For example, a SeekGE(k) uses k as a lower + // bound for truncating a span. The span a-z will be truncated to [k, + // z). If i.upper == k, we'd mistakenly try to return a span [k, k), an + // invariant violation. + if i.comparer.Equal(lowerBound, i.upper) { + return i.yieldNil() + } + + // If the lowerBound argument came from a SeekGE or SeekPrefixGE + // call, and it may be backed by a user-provided byte slice that is not + // guaranteed to be stable. + // + // If the lowerBound argument is the lower bound set by SetBounds, + // Pebble owns the slice's memory. However, consider two successive + // calls to SetBounds(). The second may overwrite the lower bound. + // Although the external contract requires a seek after a SetBounds, + // Pebble's tests don't always. For this reason and to simplify + // reasoning around lifetimes, always copy the bound into keyBuf when + // truncating. + i.keyBuf = append(i.keyBuf[:0], lowerBound...) + i.spanMarker.UserKey = i.keyBuf + i.spanMarkerTruncated = true + } + i.maybeUpdateMask() + return i.verify(&i.spanMarker, base.LazyValue{}) +} + +func (i *InterleavingIter) disablePrefixMode() { + 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 + } +} + +func (i *InterleavingIter) verify( + k *base.InternalKey, v base.LazyValue, +) (*base.InternalKey, base.LazyValue) { + // Wrap the entire function body in the invariants build tag, so that + // production builds elide this entire function. + if invariants.Enabled { + switch { + 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.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.spanMarkerTruncated = false + i.maskSpanChangedCalled = false +} + +// 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 + } +} + +// clearMask clears the current mask, if a mask is configured and no mask should +// be active. +func (i *InterleavingIter) clearMask() { + if i.mask != nil { + i.maskSpanChangedCalled = false + i.mask.SpanChanged(nil) + } +} + +func (i *InterleavingIter) startKey() []byte { + if i.truncated { + return i.truncatedSpan.Start + } + 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 +// responsible for copying if stability is required. +// +// Span will never return an invalid or empty span. +func (i *InterleavingIter) Span() *Span { + if !i.withinSpan || len(i.span.Keys) == 0 { + return nil + } else if i.truncated { + return &i.truncatedSpan + } + return i.span +} + +// SetBounds implements (base.InternalIterator).SetBounds. +func (i *InterleavingIter) SetBounds(lower, upper []byte) { + i.lower, i.upper = lower, upper + i.pointIter.SetBounds(lower, upper) + i.Invalidate() +} + +// SetContext implements (base.InternalIterator).SetContext. +func (i *InterleavingIter) SetContext(ctx context.Context) { + i.pointIter.SetContext(ctx) +} + +// Invalidate invalidates the interleaving iterator's current position, clearing +// its state. This prevents optimizations such as reusing the current span on +// seek. +func (i *InterleavingIter) Invalidate() { + i.span = nil + i.pointKey = nil + i.pointVal = base.LazyValue{} +} + +// Error implements (base.InternalIterator).Error. +func (i *InterleavingIter) Error() error { + return i.err +} + +// Close implements (base.InternalIterator).Close. +func (i *InterleavingIter) Close() error { + perr := i.pointIter.Close() + rerr := i.keyspanIter.Close() + return firstError(perr, rerr) +} + +// String implements (base.InternalIterator).String. +func (i *InterleavingIter) String() string { + return fmt.Sprintf("keyspan-interleaving(%q)", i.pointIter.String()) +} + +func firstError(err0, err1 error) error { + if err0 != nil { + return err0 + } + return err1 +} diff --git a/pebble/internal/keyspan/interleaving_iter_test.go b/pebble/internal/keyspan/interleaving_iter_test.go new file mode 100644 index 0000000..116f037 --- /dev/null +++ b/pebble/internal/keyspan/interleaving_iter_test.go @@ -0,0 +1,291 @@ +// Copyright 2021 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 ( + "bytes" + "context" + "fmt" + "io" + "sort" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/stretchr/testify/require" +) + +func TestInterleavingIter(t *testing.T) { + runInterleavingIterTest(t, "testdata/interleaving_iter") +} + +func TestInterleavingIter_Masking(t *testing.T) { + runInterleavingIterTest(t, "testdata/interleaving_iter_masking") +} + +type maskingHooks struct { + log io.Writer + cmp base.Compare + split base.Split + threshold []byte + maskSuffix []byte +} + +func (m *maskingHooks) SpanChanged(s *Span) { + if m.log != nil { + if s == nil { + fmt.Fprintln(m.log, "-- SpanChanged(nil)") + } else { + fmt.Fprintf(m.log, "-- SpanChanged(%s)\n", s) + } + } + + // Find the smallest suffix of a key contained within the Span, excluding + // suffixes less than m.threshold. + m.maskSuffix = nil + if s == nil || m.threshold == nil || len(s.Keys) == 0 { + return + } + for i := range s.Keys { + if s.Keys[i].Suffix == nil { + continue + } + if m.cmp(s.Keys[i].Suffix, m.threshold) < 0 { + continue + } + if m.maskSuffix == nil || m.cmp(m.maskSuffix, s.Keys[i].Suffix) > 0 { + m.maskSuffix = s.Keys[i].Suffix + } + } +} + +func (m *maskingHooks) SkipPoint(userKey []byte) bool { + pointSuffix := userKey[m.split(userKey):] + return m.maskSuffix != nil && len(pointSuffix) > 0 && m.cmp(m.maskSuffix, pointSuffix) < 0 +} + +func runInterleavingIterTest(t *testing.T, filename string) { + cmp := testkeys.Comparer.Compare + var keyspanIter MergingIter + var pointIter pointIterator + var iter InterleavingIter + var buf bytes.Buffer + hooks := maskingHooks{ + log: &buf, + cmp: testkeys.Comparer.Compare, + split: testkeys.Comparer.Split, + } + + var prevKey *base.InternalKey + formatKey := func(k *base.InternalKey, _ base.LazyValue) { + if k == nil { + fmt.Fprint(&buf, ".") + return + } + prevKey = k + s := iter.Span() + fmt.Fprintf(&buf, "PointKey: %s\n", k.String()) + if s != nil { + fmt.Fprintf(&buf, "Span: %s\n-", s) + } else { + fmt.Fprintf(&buf, "Span: %s\n-", Span{}) + } + } + + datadriven.RunTest(t, filename, func(t *testing.T, td *datadriven.TestData) string { + buf.Reset() + switch td.Cmd { + case "set-masking-threshold": + hooks.threshold = []byte(strings.TrimSpace(td.Input)) + return "OK" + case "define-rangekeys": + var spans []Span + lines := strings.Split(strings.TrimSpace(td.Input), "\n") + for _, line := range lines { + spans = append(spans, ParseSpan(line)) + } + keyspanIter.Init(cmp, noopTransform, new(MergingBuffers), NewIter(cmp, spans)) + hooks.maskSuffix = nil + iter.Init(testkeys.Comparer, &pointIter, &keyspanIter, + InterleavingIterOpts{Mask: &hooks}) + return "OK" + case "define-pointkeys": + var points []base.InternalKey + lines := strings.Split(strings.TrimSpace(td.Input), "\n") + for _, line := range lines { + points = append(points, base.ParseInternalKey(line)) + } + pointIter = pointIterator{cmp: cmp, keys: points} + hooks.maskSuffix = nil + iter.Init(testkeys.Comparer, &pointIter, &keyspanIter, + InterleavingIterOpts{Mask: &hooks}) + return "OK" + case "iter": + buf.Reset() + // Clear any previous bounds. + iter.SetBounds(nil, nil) + prevKey = nil + lines := strings.Split(strings.TrimSpace(td.Input), "\n") + for _, line := range lines { + bufLen := buf.Len() + line = strings.TrimSpace(line) + i := strings.IndexByte(line, ' ') + iterCmd := line + if i > 0 { + iterCmd = string(line[:i]) + } + switch iterCmd { + case "first": + formatKey(iter.First()) + case "last": + formatKey(iter.Last()) + case "next": + formatKey(iter.Next()) + case "next-prefix": + succKey := testkeys.Comparer.ImmediateSuccessor(nil, prevKey.UserKey[:testkeys.Comparer.Split(prevKey.UserKey)]) + formatKey(iter.NextPrefix(succKey)) + case "prev": + formatKey(iter.Prev()) + case "seek-ge": + formatKey(iter.SeekGE([]byte(strings.TrimSpace(line[i:])), base.SeekGEFlagsNone)) + case "seek-prefix-ge": + key := []byte(strings.TrimSpace(line[i:])) + prefix := key[:testkeys.Comparer.Split(key)] + formatKey(iter.SeekPrefixGE(prefix, key, base.SeekGEFlagsNone)) + case "seek-lt": + formatKey(iter.SeekLT([]byte(strings.TrimSpace(line[i:])), base.SeekLTFlagsNone)) + case "set-bounds": + bounds := strings.Fields(line[i:]) + if len(bounds) != 2 { + return fmt.Sprintf("set-bounds expects 2 bounds, got %d", len(bounds)) + } + l, u := []byte(bounds[0]), []byte(bounds[1]) + if bounds[0] == "." { + l = nil + } + if bounds[1] == "." { + u = nil + } + iter.SetBounds(l, u) + default: + return fmt.Sprintf("unrecognized iter command %q", iterCmd) + } + require.NoError(t, iter.Error()) + if buf.Len() > bufLen { + fmt.Fprintln(&buf) + } + } + return strings.TrimSpace(buf.String()) + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) + require.NoError(t, iter.Close()) +} + +type pointIterator struct { + cmp base.Compare + keys []base.InternalKey + lower []byte + upper []byte + index int +} + +var _ base.InternalIterator = &pointIterator{} + +func (i *pointIterator) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + i.index = sort.Search(len(i.keys), func(j int) bool { + return i.cmp(i.keys[j].UserKey, key) >= 0 + }) + if i.index < 0 || i.index >= len(i.keys) { + return nil, base.LazyValue{} + } + if i.upper != nil && i.cmp(i.keys[i.index].UserKey, i.upper) >= 0 { + return nil, base.LazyValue{} + } + return &i.keys[i.index], base.LazyValue{} +} + +func (i *pointIterator) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + return i.SeekGE(key, flags) +} + +func (i *pointIterator) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*base.InternalKey, base.LazyValue) { + i.index = sort.Search(len(i.keys), func(j int) bool { + return i.cmp(i.keys[j].UserKey, key) >= 0 + }) + i.index-- + if i.index < 0 || i.index >= len(i.keys) { + return nil, base.LazyValue{} + } + if i.lower != nil && i.cmp(i.keys[i.index].UserKey, i.lower) < 0 { + return nil, base.LazyValue{} + } + return &i.keys[i.index], base.LazyValue{} +} + +func (i *pointIterator) First() (*base.InternalKey, base.LazyValue) { + i.index = 0 + if i.index < 0 || i.index >= len(i.keys) { + return nil, base.LazyValue{} + } + if i.upper != nil && i.cmp(i.keys[i.index].UserKey, i.upper) >= 0 { + return nil, base.LazyValue{} + } + return &i.keys[i.index], base.LazyValue{} +} + +func (i *pointIterator) Last() (*base.InternalKey, base.LazyValue) { + i.index = len(i.keys) - 1 + if i.index < 0 || i.index >= len(i.keys) { + return nil, base.LazyValue{} + } + if i.lower != nil && i.cmp(i.keys[i.index].UserKey, i.lower) < 0 { + return nil, base.LazyValue{} + } + return &i.keys[i.index], base.LazyValue{} +} + +func (i *pointIterator) Next() (*base.InternalKey, base.LazyValue) { + i.index++ + if i.index < 0 || i.index >= len(i.keys) { + return nil, base.LazyValue{} + } + if i.upper != nil && i.cmp(i.keys[i.index].UserKey, i.upper) >= 0 { + return nil, base.LazyValue{} + } + return &i.keys[i.index], base.LazyValue{} +} + +func (i *pointIterator) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) { + return i.SeekGE(succKey, base.SeekGEFlagsNone) +} + +func (i *pointIterator) Prev() (*base.InternalKey, base.LazyValue) { + i.index-- + if i.index < 0 || i.index >= len(i.keys) { + return nil, base.LazyValue{} + } + if i.lower != nil && i.cmp(i.keys[i.index].UserKey, i.lower) < 0 { + return nil, base.LazyValue{} + } + return &i.keys[i.index], base.LazyValue{} +} + +func (i *pointIterator) Close() error { return nil } +func (i *pointIterator) Error() error { return nil } +func (i *pointIterator) String() string { return "test-point-iterator" } +func (i *pointIterator) SetBounds(lower, upper []byte) { + i.lower, i.upper = lower, upper +} +func (i *pointIterator) SetContext(_ context.Context) {} diff --git a/pebble/internal/keyspan/internal_iter_shim.go b/pebble/internal/keyspan/internal_iter_shim.go new file mode 100644 index 0000000..bb9e37b --- /dev/null +++ b/pebble/internal/keyspan/internal_iter_shim.go @@ -0,0 +1,125 @@ +// 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 keyspan + +import ( + "context" + + "github.com/cockroachdb/pebble/internal/base" +) + +// InternalIteratorShim is a temporary iterator type used as a shim between +// keyspan.MergingIter and base.InternalIterator. It's used temporarily for +// range deletions during compactions, allowing range deletions to be +// interleaved by a compaction input iterator. +// +// TODO(jackson): This type should be removed, and the usages converted to using +// an InterleavingIterator type that interleaves keyspan.Spans from a +// keyspan.FragmentIterator with point keys. +type InternalIteratorShim struct { + miter MergingIter + mbufs MergingBuffers + span *Span + iterKey base.InternalKey +} + +// Assert that InternalIteratorShim implements InternalIterator. +var _ base.InternalIterator = &InternalIteratorShim{} + +// Init initializes the internal iterator shim to merge the provided fragment +// iterators. +func (i *InternalIteratorShim) Init(cmp base.Compare, iters ...FragmentIterator) { + i.miter.Init(cmp, noopTransform, &i.mbufs, iters...) +} + +// Span returns the span containing the full set of keys over the key span at +// the current iterator position. +func (i *InternalIteratorShim) Span() *Span { + return i.span +} + +// SeekGE implements (base.InternalIterator).SeekGE. +func (i *InternalIteratorShim) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +// SeekPrefixGE implements (base.InternalIterator).SeekPrefixGE. +func (i *InternalIteratorShim) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +// SeekLT implements (base.InternalIterator).SeekLT. +func (i *InternalIteratorShim) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +// First implements (base.InternalIterator).First. +func (i *InternalIteratorShim) First() (*base.InternalKey, base.LazyValue) { + i.span = i.miter.First() + for i.span != nil && i.span.Empty() { + i.span = i.miter.Next() + } + if i.span == nil { + return nil, base.LazyValue{} + } + i.iterKey = base.InternalKey{UserKey: i.span.Start, Trailer: i.span.Keys[0].Trailer} + return &i.iterKey, base.MakeInPlaceValue(i.span.End) +} + +// Last implements (base.InternalIterator).Last. +func (i *InternalIteratorShim) Last() (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +// Next implements (base.InternalIterator).Next. +func (i *InternalIteratorShim) Next() (*base.InternalKey, base.LazyValue) { + i.span = i.miter.Next() + for i.span != nil && i.span.Empty() { + i.span = i.miter.Next() + } + if i.span == nil { + return nil, base.LazyValue{} + } + i.iterKey = base.InternalKey{UserKey: i.span.Start, Trailer: i.span.Keys[0].Trailer} + return &i.iterKey, base.MakeInPlaceValue(i.span.End) +} + +// NextPrefix implements (base.InternalIterator).NextPrefix. +func (i *InternalIteratorShim) NextPrefix([]byte) (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +// Prev implements (base.InternalIterator).Prev. +func (i *InternalIteratorShim) Prev() (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +// Error implements (base.InternalIterator).Error. +func (i *InternalIteratorShim) Error() error { + return i.miter.Error() +} + +// Close implements (base.InternalIterator).Close. +func (i *InternalIteratorShim) Close() error { + return i.miter.Close() +} + +// SetBounds implements (base.InternalIterator).SetBounds. +func (i *InternalIteratorShim) SetBounds(lower, upper []byte) { +} + +// SetContext implements (base.InternalIterator).SetContext. +func (i *InternalIteratorShim) SetContext(_ context.Context) {} + +// String implements fmt.Stringer. +func (i *InternalIteratorShim) String() string { + return i.miter.String() +} diff --git a/pebble/internal/keyspan/iter.go b/pebble/internal/keyspan/iter.go new file mode 100644 index 0000000..7f8ceb8 --- /dev/null +++ b/pebble/internal/keyspan/iter.go @@ -0,0 +1,220 @@ +// Copyright 2018 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" + "github.com/cockroachdb/pebble/internal/manifest" +) + +// FragmentIterator defines an iterator interface over spans. The spans +// surfaced by a FragmentIterator must be non-overlapping. This is achieved by +// fragmenting spans at overlap points (see Fragmenter). +// +// A Span returned by a FragmentIterator is only valid until the next +// positioning method. Some implementations (eg, keyspan.Iter) may provide +// longer lifetimes but implementations need only guarantee stability until the +// next positioning method. +type FragmentIterator interface { + // SeekGE moves the iterator to the first span covering a key greater than + // or equal to the given key. This is equivalent to seeking to the first + // span with an end key greater than the given key. + SeekGE(key []byte) *Span + + // SeekLT moves the iterator to the last span covering a key less than the + // given key. This is equivalent to seeking to the last span with a start + // key less than the given key. + SeekLT(key []byte) *Span + + // First moves the iterator to the first span. + First() *Span + + // Last moves the iterator to the last span. + Last() *Span + + // Next moves the iterator to the next span. + // + // It is valid to call Next when the iterator is positioned before the first + // key/value pair due to either a prior call to SeekLT or Prev which + // returned an invalid span. It is not allowed to call Next when the + // previous call to SeekGE, SeekPrefixGE or Next returned an invalid span. + Next() *Span + + // Prev moves the iterator to the previous span. + // + // It is valid to call Prev when the iterator is positioned after the last + // key/value pair due to either a prior call to SeekGE or Next which + // returned an invalid span. It is not allowed to call Prev when the + // previous call to SeekLT or Prev returned an invalid span. + 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 + // the iterator is not considered to be an error. It is valid to call Close + // multiple times. Other methods should not be called after the iterator has + // been closed. + Close() error +} + +// TableNewSpanIter creates a new iterator for range key spans for the given +// file. +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. +type SpanIterOptions struct { + // RangeKeyFilters can be used to avoid scanning tables and blocks in tables + // when iterating over range keys. + RangeKeyFilters []base.BlockPropertyFilter +} + +// Iter is an iterator over a set of fragmented spans. +type Iter struct { + cmp base.Compare + spans []Span + index int +} + +// Iter implements the FragmentIterator interface. +var _ FragmentIterator = (*Iter)(nil) + +// NewIter returns a new iterator over a set of fragmented spans. +func NewIter(cmp base.Compare, spans []Span) *Iter { + i := &Iter{} + i.Init(cmp, spans) + return i +} + +// Count returns the number of spans contained by Iter. +func (i *Iter) Count() int { + return len(i.spans) +} + +// Init initializes an Iter with the provided spans. +func (i *Iter) Init(cmp base.Compare, spans []Span) { + *i = Iter{ + cmp: cmp, + spans: spans, + index: -1, + } +} + +// SeekGE implements FragmentIterator.SeekGE. +func (i *Iter) SeekGE(key []byte) *Span { + // NB: manually inlined sort.Search is ~5% faster. + // + // Define f(j) = false iff the span i.spans[j] is strictly before `key` + // (equivalently, i.spans[j].End ≤ key.) + // + // Define f(-1) == false and f(n) == true. + // Invariant: f(index-1) == false, f(upper) == true. + i.index = 0 + upper := len(i.spans) + for i.index < upper { + h := int(uint(i.index+upper) >> 1) // avoid overflow when computing h + // i.index ≤ h < upper + if i.cmp(key, i.spans[h].End) >= 0 { + i.index = h + 1 // preserves f(i-1) == false + } else { + upper = h // preserves f(j) == true + } + } + + // i.index == upper, f(i.index-1) == false, and f(upper) (= f(i.index)) == + // true => answer is i.index. + if i.index >= len(i.spans) { + return nil + } + return &i.spans[i.index] +} + +// SeekLT implements FragmentIterator.SeekLT. +func (i *Iter) SeekLT(key []byte) *Span { + // NB: manually inlined sort.Search is ~5% faster. + // + // Define f(-1) == false and f(n) == true. + // Invariant: f(index-1) == false, f(upper) == true. + i.index = 0 + upper := len(i.spans) + for i.index < upper { + h := int(uint(i.index+upper) >> 1) // avoid overflow when computing h + // i.index ≤ h < upper + if i.cmp(key, i.spans[h].Start) > 0 { + i.index = h + 1 // preserves f(i-1) == false + } else { + upper = h // preserves f(j) == true + } + } + // i.index == upper, f(i.index-1) == false, and f(upper) (= f(i.index)) == + // true => answer is i.index. + + // Since keys are strictly increasing, if i.index > 0 then i.index-1 will be + // the largest whose key is < the key sought. + i.index-- + if i.index < 0 { + return nil + } + return &i.spans[i.index] +} + +// First implements FragmentIterator.First. +func (i *Iter) First() *Span { + if len(i.spans) == 0 { + return nil + } + i.index = 0 + return &i.spans[i.index] +} + +// Last implements FragmentIterator.Last. +func (i *Iter) Last() *Span { + if len(i.spans) == 0 { + return nil + } + i.index = len(i.spans) - 1 + return &i.spans[i.index] +} + +// Next implements FragmentIterator.Next. +func (i *Iter) Next() *Span { + if i.index >= len(i.spans) { + return nil + } + i.index++ + if i.index >= len(i.spans) { + return nil + } + return &i.spans[i.index] +} + +// Prev implements FragmentIterator.Prev. +func (i *Iter) Prev() *Span { + if i.index < 0 { + return nil + } + i.index-- + if i.index < 0 { + return nil + } + return &i.spans[i.index] +} + +// Error implements FragmentIterator.Error. +func (i *Iter) Error() error { + return nil +} + +// Close implements FragmentIterator.Close. +func (i *Iter) Close() error { + return nil +} + +func (i *Iter) String() string { + return "fragmented-spans" +} diff --git a/pebble/internal/keyspan/iter_test.go b/pebble/internal/keyspan/iter_test.go new file mode 100644 index 0000000..c269f3b --- /dev/null +++ b/pebble/internal/keyspan/iter_test.go @@ -0,0 +1,147 @@ +// Copyright 2018 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 ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" +) + +func runFragmentIteratorCmd(iter FragmentIterator, input string, extraInfo func() string) string { + var b bytes.Buffer + for _, line := range strings.Split(input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + var span *Span + switch parts[0] { + case "seek-ge": + if len(parts) != 2 { + return "seek-ge \n" + } + span = iter.SeekGE([]byte(strings.TrimSpace(parts[1]))) + case "seek-lt": + if len(parts) != 2 { + return "seek-lt \n" + } + span = iter.SeekLT([]byte(strings.TrimSpace(parts[1]))) + case "first": + span = iter.First() + case "last": + span = iter.Last() + case "next": + span = iter.Next() + case "prev": + span = iter.Prev() + default: + return fmt.Sprintf("unknown op: %s", parts[0]) + } + if span != nil { + fmt.Fprintf(&b, "%s", span) + if extraInfo != nil { + fmt.Fprintf(&b, " (%s)", extraInfo()) + } + b.WriteByte('\n') + } else if err := iter.Error(); err != nil { + fmt.Fprintf(&b, "err=%v\n", err) + } else { + fmt.Fprintf(&b, ".\n") + } + } + return b.String() +} + +func TestIter(t *testing.T) { + var spans []Span + datadriven.RunTest(t, "testdata/iter", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + spans = nil + for _, line := range strings.Split(d.Input, "\n") { + spans = append(spans, ParseSpan(line)) + } + return "" + + case "iter": + iter := NewIter(base.DefaultComparer.Compare, spans) + defer iter.Close() + return runFragmentIteratorCmd(iter, d.Input, nil) + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +// invalidatingIter wraps a FragmentIterator and implements FragmentIterator +// itself. Spans surfaced by the inner iterator are copied to buffers that are +// zeroed by sbubsequent iterator positioning calls. This is intended to help +// surface bugs in improper lifetime expectations of Spans. +type invalidatingIter struct { + iter FragmentIterator + bufs [][]byte + keys []Key + span Span +} + +// invalidatingIter implements FragmentIterator. +var _ FragmentIterator = (*invalidatingIter)(nil) + +func (i *invalidatingIter) invalidate(s *Span) *Span { + // Zero the entirety of the byte bufs and the keys slice. + for j := range i.bufs { + for k := range i.bufs[j] { + i.bufs[j][k] = 0x00 + } + i.bufs[j] = nil + } + for j := range i.keys { + i.keys[j] = Key{} + } + if s == nil { + return nil + } + + // Copy all of the span's slices into slices owned by the invalidating iter + // that we can invalidate on a subsequent positioning method. + i.bufs = i.bufs[:0] + i.keys = i.keys[:0] + i.span = Span{ + Start: i.saveBytes(s.Start), + End: i.saveBytes(s.End), + } + for j := range s.Keys { + i.keys = append(i.keys, Key{ + Trailer: s.Keys[j].Trailer, + Suffix: i.saveBytes(s.Keys[j].Suffix), + Value: i.saveBytes(s.Keys[j].Value), + }) + } + i.span.Keys = i.keys + return &i.span +} + +func (i *invalidatingIter) saveBytes(b []byte) []byte { + if b == nil { + return nil + } + saved := append([]byte(nil), b...) + i.bufs = append(i.bufs, saved) + return saved +} + +func (i *invalidatingIter) SeekGE(key []byte) *Span { return i.invalidate(i.iter.SeekGE(key)) } +func (i *invalidatingIter) SeekLT(key []byte) *Span { return i.invalidate(i.iter.SeekLT(key)) } +func (i *invalidatingIter) First() *Span { return i.invalidate(i.iter.First()) } +func (i *invalidatingIter) Last() *Span { return i.invalidate(i.iter.Last()) } +func (i *invalidatingIter) Next() *Span { return i.invalidate(i.iter.Next()) } +func (i *invalidatingIter) Prev() *Span { return i.invalidate(i.iter.Prev()) } +func (i *invalidatingIter) Close() error { return i.iter.Close() } +func (i *invalidatingIter) Error() error { return i.iter.Error() } diff --git a/pebble/internal/keyspan/level_iter.go b/pebble/internal/keyspan/level_iter.go new file mode 100644 index 0000000..6dd7ac6 --- /dev/null +++ b/pebble/internal/keyspan/level_iter.go @@ -0,0 +1,521 @@ +// 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 keyspan + +import ( + "fmt" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/manifest" +) + +// LevelIter provides a merged view of spans from sstables in a level. +// It takes advantage of level invariants to only have one sstable span block +// open at one time, opened using the newIter function passed in. +type LevelIter struct { + cmp base.Compare + // Denotes the kind of key the level iterator should read. If the key type + // is KeyTypePoint, the level iterator will read range tombstones (which + // only affect point keys). If the key type is KeyTypeRange, the level + // iterator will read range keys. It is invalid to configure an iterator + // with the KeyTypePointAndRange key type. + // + // If key type is KeyTypePoint, no straddle spans are emitted between files, + // and point key bounds are used to find files instead of range key bounds. + // + // TODO(bilal): Straddle spans can safely be produced in rangedel mode once + // we can guarantee that we will never read sstables in a level that split + // user keys across them. This might be guaranteed in a future release, but + // as of CockroachDB 22.2 it is not guaranteed, so to be safe disable it when + // keyType == KeyTypePoint + keyType manifest.KeyType + // The LSM level this LevelIter is initialized for. Used in logging. + level manifest.Level + // The below fields are used to fill in gaps between adjacent files' range + // key spaces. This is an optimization to avoid unnecessarily loading files + // in cases where range keys are sparse and rare. dir is set by every + // positioning operation, straddleDir is set to dir whenever a straddling + // Span is synthesized and the last positioning operation returned a + // synthesized straddle span. + // + // Note that when a straddle span is initialized, iterFile is modified to + // point to the next file in the straddleDir direction. A change of direction + // on a straddle key therefore necessitates the value of iterFile to be + // reverted. + dir int + straddle Span + straddleDir int + // The iter for the current file (iterFile). It is nil under any of the + // following conditions: + // - files.Current() == nil + // - err != nil + // - straddleDir != 0, in which case iterFile is not nil and points to the + // next file (in the straddleDir direction). + // - some other constraint, like the bounds in opts, caused the file at index to not + // be relevant to the iteration. + iter FragmentIterator + // iterFile holds the current file. + // INVARIANT: iterFile = files.Current() + iterFile *manifest.FileMetadata + newIter TableNewSpanIter + files manifest.LevelIterator + err error + + // The options that were passed in. + tableOpts SpanIterOptions + + // TODO(bilal): Add InternalIteratorStats. +} + +// LevelIter implements the keyspan.FragmentIterator interface. +var _ FragmentIterator = (*LevelIter)(nil) + +// NewLevelIter returns a LevelIter. +func NewLevelIter( + opts SpanIterOptions, + cmp base.Compare, + newIter TableNewSpanIter, + files manifest.LevelIterator, + level manifest.Level, + keyType manifest.KeyType, +) *LevelIter { + l := &LevelIter{} + l.Init(opts, cmp, newIter, files, level, keyType) + return l +} + +// Init initializes a LevelIter. +func (l *LevelIter) Init( + opts SpanIterOptions, + cmp base.Compare, + newIter TableNewSpanIter, + files manifest.LevelIterator, + level manifest.Level, + keyType manifest.KeyType, +) { + l.err = nil + l.level = level + l.tableOpts = opts + l.cmp = cmp + l.iterFile = nil + l.newIter = newIter + switch keyType { + case manifest.KeyTypePoint: + l.keyType = keyType + l.files = files.Filter(keyType) + case manifest.KeyTypeRange: + l.keyType = keyType + l.files = files.Filter(keyType) + default: + panic(fmt.Sprintf("unsupported key type: %v", keyType)) + } +} + +func (l *LevelIter) findFileGE(key []byte) *manifest.FileMetadata { + // Find the earliest file whose largest key is >= key. + // + // If the earliest file has its largest key == key and that largest key is a + // range deletion sentinel, we know that we manufactured this sentinel to convert + // the exclusive range deletion end key into an inclusive key (reminder: [start, end)#seqnum + // is the form of a range deletion sentinel which can contribute a largest key = end#sentinel). + // In this case we don't return this as the earliest file since there is nothing actually + // equal to key in it. + + m := l.files.SeekGE(l.cmp, key) + for m != nil { + largestKey := m.LargestRangeKey + if l.keyType == manifest.KeyTypePoint { + largestKey = m.LargestPointKey + } + if !largestKey.IsExclusiveSentinel() || l.cmp(largestKey.UserKey, key) != 0 { + break + } + m = l.files.Next() + } + return m +} + +func (l *LevelIter) findFileLT(key []byte) *manifest.FileMetadata { + // Find the last file whose smallest key is < key. + return l.files.SeekLT(l.cmp, key) +} + +type loadFileReturnIndicator int8 + +const ( + noFileLoaded loadFileReturnIndicator = iota + fileAlreadyLoaded + newFileLoaded +) + +func (l *LevelIter) loadFile(file *manifest.FileMetadata, dir int) loadFileReturnIndicator { + indicator := noFileLoaded + if l.iterFile == file { + if l.err != nil { + return noFileLoaded + } + if l.iter != nil { + // We are already at the file, but we would need to check for bounds. + // Set indicator accordingly. + indicator = fileAlreadyLoaded + } + // We were already at file, but don't have an iterator, probably because the file was + // beyond the iteration bounds. It may still be, but it is also possible that the bounds + // have changed. We handle that below. + } + + // Note that LevelIter.Close() can be called multiple times. + if indicator != fileAlreadyLoaded { + if err := l.Close(); err != nil { + return noFileLoaded + } + } + + l.iterFile = file + if file == nil { + return noFileLoaded + } + if indicator != fileAlreadyLoaded { + l.iter, l.err = l.newIter(file, l.tableOpts) + indicator = newFileLoaded + } + if l.err != nil { + return noFileLoaded + } + return indicator +} + +// SeekGE implements keyspan.FragmentIterator. +func (l *LevelIter) SeekGE(key []byte) *Span { + l.dir = +1 + l.straddle = Span{} + l.straddleDir = 0 + l.err = nil // clear cached iteration error + + f := l.findFileGE(key) + if f != nil && l.keyType == manifest.KeyTypeRange && l.cmp(key, f.SmallestRangeKey.UserKey) < 0 { + // Peek at the previous file. + prevFile := l.files.Prev() + l.files.Next() + if prevFile != nil { + // We could unconditionally return an empty span between the seek key and + // f.SmallestRangeKey, however if this span is to the left of all range + // keys on this level, it could lead to inconsistent behaviour in relative + // positioning operations. Consider this example, with a b-c range key: + // + // SeekGE(a) -> a-b:{} + // Next() -> b-c{(#5,RANGEKEYSET,@4,foo)} + // Prev() -> nil + // + // Iterators higher up in the iterator stack rely on this sort of relative + // positioning consistency. + // + // TODO(bilal): Investigate ways to be able to return straddle spans in + // cases similar to the above, while still retaining correctness. + // Return a straddling key instead of loading the file. + l.iterFile = f + if err := l.Close(); err != nil { + return l.verify(nil) + } + l.straddleDir = +1 + l.straddle = Span{ + Start: prevFile.LargestRangeKey.UserKey, + End: f.SmallestRangeKey.UserKey, + Keys: nil, + } + return l.verify(&l.straddle) + } + } + loadFileIndicator := l.loadFile(f, +1) + if loadFileIndicator == noFileLoaded { + return l.verify(nil) + } + if span := l.iter.SeekGE(key); span != nil { + return l.verify(span) + } + return l.skipEmptyFileForward() +} + +// SeekLT implements keyspan.FragmentIterator. +func (l *LevelIter) SeekLT(key []byte) *Span { + l.dir = -1 + l.straddle = Span{} + l.straddleDir = 0 + l.err = nil // clear cached iteration error + + f := l.findFileLT(key) + if f != nil && l.keyType == manifest.KeyTypeRange && l.cmp(f.LargestRangeKey.UserKey, key) < 0 { + // Peek at the next file. + nextFile := l.files.Next() + l.files.Prev() + if nextFile != nil { + // We could unconditionally return an empty span between f.LargestRangeKey + // and the seek key, however if this span is to the right of all range keys + // on this level, it could lead to inconsistent behaviour in relative + // positioning operations. Consider this example, with a b-c range key: + // + // SeekLT(d) -> c-d:{} + // Prev() -> b-c{(#5,RANGEKEYSET,@4,foo)} + // Next() -> nil + // + // Iterators higher up in the iterator stack rely on this sort of relative + // positioning consistency. + // + // TODO(bilal): Investigate ways to be able to return straddle spans in + // cases similar to the above, while still retaining correctness. + // Return a straddling key instead of loading the file. + l.iterFile = f + if err := l.Close(); err != nil { + return l.verify(nil) + } + l.straddleDir = -1 + l.straddle = Span{ + Start: f.LargestRangeKey.UserKey, + End: nextFile.SmallestRangeKey.UserKey, + Keys: nil, + } + return l.verify(&l.straddle) + } + } + if l.loadFile(f, -1) == noFileLoaded { + return l.verify(nil) + } + if span := l.iter.SeekLT(key); span != nil { + return l.verify(span) + } + return l.skipEmptyFileBackward() +} + +// First implements keyspan.FragmentIterator. +func (l *LevelIter) First() *Span { + l.dir = +1 + l.straddle = Span{} + l.straddleDir = 0 + l.err = nil // clear cached iteration error + + if l.loadFile(l.files.First(), +1) == noFileLoaded { + return l.verify(nil) + } + if span := l.iter.First(); span != nil { + return l.verify(span) + } + return l.skipEmptyFileForward() +} + +// Last implements keyspan.FragmentIterator. +func (l *LevelIter) Last() *Span { + l.dir = -1 + l.straddle = Span{} + l.straddleDir = 0 + l.err = nil // clear cached iteration error + + if l.loadFile(l.files.Last(), -1) == noFileLoaded { + return l.verify(nil) + } + if span := l.iter.Last(); span != nil { + return l.verify(span) + } + return l.skipEmptyFileBackward() +} + +// Next implements keyspan.FragmentIterator. +func (l *LevelIter) Next() *Span { + if l.err != nil || (l.iter == nil && l.iterFile == nil && l.dir > 0) { + return l.verify(nil) + } + if l.iter == nil && l.iterFile == nil { + // l.dir <= 0 + return l.First() + } + l.dir = +1 + + if l.iter != nil { + if span := l.iter.Next(); span != nil { + return l.verify(span) + } + } + return l.skipEmptyFileForward() +} + +// Prev implements keyspan.FragmentIterator. +func (l *LevelIter) Prev() *Span { + if l.err != nil || (l.iter == nil && l.iterFile == nil && l.dir < 0) { + return l.verify(nil) + } + if l.iter == nil && l.iterFile == nil { + // l.dir >= 0 + return l.Last() + } + l.dir = -1 + + if l.iter != nil { + if span := l.iter.Prev(); span != nil { + return l.verify(span) + } + } + return l.skipEmptyFileBackward() +} + +func (l *LevelIter) skipEmptyFileForward() *Span { + if l.straddleDir == 0 && l.keyType == manifest.KeyTypeRange && + l.iterFile != nil && l.iter != nil { + // We were at a file that had spans. Check if the next file that has + // spans is not directly adjacent to the current file i.e. there is a + // gap in the span keyspace between the two files. In that case, synthesize + // a "straddle span" in l.straddle and return that. + // + // Straddle spans are not created in rangedel mode. + if err := l.Close(); err != nil { + l.err = err + return l.verify(nil) + } + startKey := l.iterFile.LargestRangeKey.UserKey + // Resetting l.iterFile without loading the file into l.iter is okay and + // does not change the logic in loadFile() as long as l.iter is also nil; + // which it should be due to the Close() call above. + l.iterFile = l.files.Next() + if l.iterFile == nil { + return l.verify(nil) + } + endKey := l.iterFile.SmallestRangeKey.UserKey + if l.cmp(startKey, endKey) < 0 { + // There is a gap between the two files. Synthesize a straddling span + // to avoid unnecessarily loading the next file. + l.straddle = Span{ + Start: startKey, + End: endKey, + } + l.straddleDir = +1 + return l.verify(&l.straddle) + } + } else if l.straddleDir < 0 { + // We were at a straddle key, but are now changing directions. l.iterFile + // was already moved backward by skipEmptyFileBackward, so advance it + // forward. + l.iterFile = l.files.Next() + } + l.straddle = Span{} + l.straddleDir = 0 + var span *Span + for span.Empty() { + fileToLoad := l.iterFile + if l.keyType == manifest.KeyTypePoint { + // We haven't iterated to the next file yet if we're in point key + // (rangedel) mode. + fileToLoad = l.files.Next() + } + if l.loadFile(fileToLoad, +1) == noFileLoaded { + return l.verify(nil) + } + span = l.iter.First() + // In rangedel mode, we can expect to get empty files that we'd need to + // skip over, but not in range key mode. + if l.keyType == manifest.KeyTypeRange { + break + } + } + return l.verify(span) +} + +func (l *LevelIter) skipEmptyFileBackward() *Span { + // We were at a file that had spans. Check if the previous file that has + // spans is not directly adjacent to the current file i.e. there is a + // gap in the span keyspace between the two files. In that case, synthesize + // a "straddle span" in l.straddle and return that. + // + // Straddle spans are not created in rangedel mode. + if l.straddleDir == 0 && l.keyType == manifest.KeyTypeRange && + l.iterFile != nil && l.iter != nil { + if err := l.Close(); err != nil { + l.err = err + return l.verify(nil) + } + endKey := l.iterFile.SmallestRangeKey.UserKey + // Resetting l.iterFile without loading the file into l.iter is okay and + // does not change the logic in loadFile() as long as l.iter is also nil; + // which it should be due to the Close() call above. + l.iterFile = l.files.Prev() + if l.iterFile == nil { + return l.verify(nil) + } + startKey := l.iterFile.LargestRangeKey.UserKey + if l.cmp(startKey, endKey) < 0 { + // There is a gap between the two files. Synthesize a straddling span + // to avoid unnecessarily loading the next file. + l.straddle = Span{ + Start: startKey, + End: endKey, + } + l.straddleDir = -1 + return l.verify(&l.straddle) + } + } else if l.straddleDir > 0 { + // We were at a straddle key, but are now changing directions. l.iterFile + // was already advanced forward by skipEmptyFileForward, so move it + // backward. + l.iterFile = l.files.Prev() + } + l.straddle = Span{} + l.straddleDir = 0 + var span *Span + for span.Empty() { + fileToLoad := l.iterFile + if l.keyType == manifest.KeyTypePoint { + fileToLoad = l.files.Prev() + } + if l.loadFile(fileToLoad, -1) == noFileLoaded { + return l.verify(nil) + } + span = l.iter.Last() + // In rangedel mode, we can expect to get empty files that we'd need to + // skip over, but not in range key mode as the filter on the FileMetadata + // should guarantee we always get a non-empty file. + if l.keyType == manifest.KeyTypeRange { + break + } + } + return l.verify(span) +} + +// verify is invoked whenever a span is returned from an iterator positioning +// method to a caller. During invariant builds, it asserts invariants to the +// caller. +func (l *LevelIter) verify(s *Span) *Span { + // NB: Do not add any logic outside the invariants.Enabled conditional to + // ensure that verify is always compiled away in production builds. + if invariants.Enabled { + if f := l.files.Current(); f != l.iterFile { + panic(fmt.Sprintf("LevelIter.files.Current (%s) and l.iterFile (%s) diverged", + f, l.iterFile)) + } + } + return s +} + +// Error implements keyspan.FragmentIterator. +func (l *LevelIter) Error() error { + if l.err != nil || l.iter == nil { + return l.err + } + return l.iter.Error() +} + +// Close implements keyspan.FragmentIterator. +func (l *LevelIter) Close() error { + if l.iter != nil { + l.err = l.iter.Close() + l.iter = nil + } + return l.err +} + +// String implements keyspan.FragmentIterator. +func (l *LevelIter) String() string { + if l.iterFile != nil { + return fmt.Sprintf("%s: fileNum=%s", l.level, l.iterFile.FileNum) + } + return fmt.Sprintf("%s: fileNum=", l.level) +} diff --git a/pebble/internal/keyspan/level_iter_test.go b/pebble/internal/keyspan/level_iter_test.go new file mode 100644 index 0000000..6e30396 --- /dev/null +++ b/pebble/internal/keyspan/level_iter_test.go @@ -0,0 +1,472 @@ +// 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 keyspan + +import ( + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/stretchr/testify/require" +) + +func TestLevelIterEquivalence(t *testing.T) { + type level [][]Span + testCases := []struct { + name string + levels []level + }{ + { + "single level, no gaps, no overlaps", + []level{ + { + { + Span{ + Start: []byte("a"), + End: []byte("b"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("b"), + End: []byte("c"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("c"), + End: []byte("d"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + }, + { + Span{ + Start: []byte("d"), + End: []byte("e"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("e"), + End: []byte("f"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("f"), + End: []byte("g"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + }, + }, + }, + }, + { + "single level, overlapping fragments", + []level{ + { + { + Span{ + Start: []byte("a"), + End: []byte("b"), + Keys: []Key{ + { + Trailer: base.MakeTrailer(4, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("bar"), + }, + { + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }, + }, + }, + Span{ + Start: []byte("b"), + End: []byte("c"), + Keys: []Key{ + { + Trailer: base.MakeTrailer(4, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("bar"), + }, + { + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }, + }, + }, + Span{ + Start: []byte("c"), + End: []byte("d"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + }, + { + Span{ + Start: []byte("d"), + End: []byte("e"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("e"), + End: []byte("f"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("f"), + End: []byte("g"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + }, + }, + }, + }, + { + "single level, gaps between files and range keys", + []level{ + { + { + Span{ + Start: []byte("a"), + End: []byte("b"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("c"), + End: []byte("d"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("e"), + End: []byte("f"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + }, + { + Span{ + Start: []byte("g"), + End: []byte("h"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("i"), + End: []byte("j"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + Span{ + Start: []byte("k"), + End: []byte("l"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + }, + }, + }, + }, + { + "two levels, one with overlapping unset", + []level{ + { + { + Span{ + Start: []byte("a"), + End: []byte("h"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + }, + { + Span{ + Start: []byte("l"), + End: []byte("u"), + Keys: []Key{{ + Trailer: base.MakeTrailer(2, base.InternalKeyKindRangeKeyUnset), + Suffix: nil, + Value: nil, + }}, + }, + }, + }, + { + { + Span{ + Start: []byte("e"), + End: []byte("r"), + Keys: []Key{{ + Trailer: base.MakeTrailer(1, base.InternalKeyKindRangeKeySet), + Suffix: nil, + Value: []byte("foo"), + }}, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + var fileIters []FragmentIterator + var levelIters []FragmentIterator + var iter1, iter2 MergingIter + for j, level := range tc.levels { + j := j // Copy for use in closures down below. + var levelIter LevelIter + var metas []*manifest.FileMetadata + for k, file := range level { + fileIters = append(fileIters, NewIter(base.DefaultComparer.Compare, file)) + meta := &manifest.FileMetadata{ + FileNum: base.FileNum(k + 1), + Size: 1024, + SmallestSeqNum: 2, + LargestSeqNum: 2, + SmallestRangeKey: base.MakeInternalKey(file[0].Start, file[0].SmallestKey().SeqNum(), file[0].SmallestKey().Kind()), + LargestRangeKey: base.MakeExclusiveSentinelKey(file[len(file)-1].LargestKey().Kind(), file[len(file)-1].End), + HasPointKeys: false, + HasRangeKeys: true, + } + meta.InitPhysicalBacking() + meta.ExtendRangeKeyBounds(base.DefaultComparer.Compare, meta.SmallestRangeKey, meta.LargestRangeKey) + metas = append(metas, meta) + } + + tableNewIters := func(file *manifest.FileMetadata, iterOptions SpanIterOptions) (FragmentIterator, error) { + return NewIter(base.DefaultComparer.Compare, tc.levels[j][file.FileNum-1]), nil + } + // Add all the fileMetadatas to L6. + b := &manifest.BulkVersionEdit{} + amap := make(map[base.FileNum]*manifest.FileMetadata) + for i := range metas { + amap[metas[i].FileNum] = metas[i] + } + b.Added[6] = amap + v, err := b.Apply(nil, base.DefaultComparer.Compare, base.DefaultFormatter, 0, 0, nil, manifest.ProhibitSplitUserKeys) + require.NoError(t, err) + levelIter.Init( + SpanIterOptions{}, base.DefaultComparer.Compare, tableNewIters, + v.Levels[6].Iter(), 0, manifest.KeyTypeRange, + ) + levelIters = append(levelIters, &levelIter) + } + + iter1.Init(base.DefaultComparer.Compare, VisibleTransform(base.InternalKeySeqNumMax), new(MergingBuffers), fileIters...) + iter2.Init(base.DefaultComparer.Compare, VisibleTransform(base.InternalKeySeqNumMax), new(MergingBuffers), levelIters...) + // Check iter1 and iter2 for equivalence. + + require.Equal(t, iter1.First(), iter2.First(), "failed on test case %q", tc.name) + valid := true + for valid { + f1 := iter1.Next() + var f2 *Span + for { + f2 = iter2.Next() + // The level iter could produce empty spans that straddle between + // files. Ignore those. + if f2 == nil || !f2.Empty() { + break + } + } + + require.Equal(t, f1, f2, "failed on test case %q", tc.name) + valid = f1 != nil && f2 != nil + } + } +} + +func TestLevelIter(t *testing.T) { + var level [][]Span + var rangedels [][]Span + var metas []*manifest.FileMetadata + var iter FragmentIterator + var extraInfo func() string + + datadriven.RunTest(t, "testdata/level_iter", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + level = level[:0] + metas = metas[:0] + rangedels = rangedels[:0] + if iter != nil { + iter.Close() + iter = nil + } + var pointKeys []base.InternalKey + var currentRangeDels []Span + var currentFile []Span + for _, key := range strings.Split(d.Input, "\n") { + if strings.HasPrefix(key, "file") { + // Skip the very first file creation. + if len(level) != 0 || len(currentFile) != 0 { + meta := &manifest.FileMetadata{ + FileNum: base.FileNum(len(level) + 1), + } + if len(currentFile) > 0 { + smallest := base.MakeInternalKey(currentFile[0].Start, currentFile[0].SmallestKey().SeqNum(), currentFile[0].SmallestKey().Kind()) + largest := base.MakeExclusiveSentinelKey(currentFile[len(currentFile)-1].LargestKey().Kind(), currentFile[len(currentFile)-1].End) + meta.ExtendRangeKeyBounds(base.DefaultComparer.Compare, smallest, largest) + } + if len(pointKeys) != 0 { + meta.ExtendPointKeyBounds(base.DefaultComparer.Compare, pointKeys[0], pointKeys[len(pointKeys)-1]) + } + meta.InitPhysicalBacking() + level = append(level, currentFile) + metas = append(metas, meta) + rangedels = append(rangedels, currentRangeDels) + currentRangeDels = nil + currentFile = nil + pointKeys = nil + } + continue + } + key = strings.TrimSpace(key) + if strings.HasPrefix(key, "point:") { + key = strings.TrimPrefix(key, "point:") + j := strings.Index(key, ":") + ikey := base.ParseInternalKey(key[:j]) + pointKeys = append(pointKeys, ikey) + if ikey.Kind() == base.InternalKeyKindRangeDelete { + currentRangeDels = append(currentRangeDels, Span{ + Start: ikey.UserKey, End: []byte(key[j+1:]), Keys: []Key{{Trailer: ikey.Trailer}}}) + } + continue + } + span := ParseSpan(key) + currentFile = append(currentFile, span) + } + meta := &manifest.FileMetadata{ + FileNum: base.FileNum(len(level) + 1), + } + meta.InitPhysicalBacking() + level = append(level, currentFile) + rangedels = append(rangedels, currentRangeDels) + if len(currentFile) > 0 { + smallest := base.MakeInternalKey(currentFile[0].Start, currentFile[0].SmallestKey().SeqNum(), currentFile[0].SmallestKey().Kind()) + largest := base.MakeExclusiveSentinelKey(currentFile[len(currentFile)-1].LargestKey().Kind(), currentFile[len(currentFile)-1].End) + meta.ExtendRangeKeyBounds(base.DefaultComparer.Compare, smallest, largest) + } + if len(pointKeys) != 0 { + meta.ExtendPointKeyBounds(base.DefaultComparer.Compare, pointKeys[0], pointKeys[len(pointKeys)-1]) + } + metas = append(metas, meta) + return "" + case "num-files": + return fmt.Sprintf("%d", len(level)) + case "close-iter": + _ = iter.Close() + iter = nil + return "ok" + case "iter": + keyType := manifest.KeyTypeRange + for _, arg := range d.CmdArgs { + if strings.Contains(arg.Key, "rangedel") { + keyType = manifest.KeyTypePoint + } + } + if iter == nil { + var lastFileNum base.FileNum + tableNewIters := func(file *manifest.FileMetadata, _ SpanIterOptions) (FragmentIterator, error) { + keyType := keyType + spans := level[file.FileNum-1] + if keyType == manifest.KeyTypePoint { + spans = rangedels[file.FileNum-1] + } + lastFileNum = file.FileNum + return NewIter(base.DefaultComparer.Compare, spans), nil + } + b := &manifest.BulkVersionEdit{} + amap := make(map[base.FileNum]*manifest.FileMetadata) + for i := range metas { + amap[metas[i].FileNum] = metas[i] + } + b.Added[6] = amap + v, err := b.Apply(nil, base.DefaultComparer.Compare, base.DefaultFormatter, 0, 0, nil, manifest.ProhibitSplitUserKeys) + require.NoError(t, err) + iter = NewLevelIter( + SpanIterOptions{}, base.DefaultComparer.Compare, + tableNewIters, v.Levels[6].Iter(), 6, keyType, + ) + extraInfo = func() string { + return fmt.Sprintf("file = %s.sst", lastFileNum) + } + } + + return runFragmentIteratorCmd(iter, d.Input, extraInfo) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) + + if iter != nil { + iter.Close() + } +} diff --git a/pebble/internal/keyspan/merging_iter.go b/pebble/internal/keyspan/merging_iter.go new file mode 100644 index 0000000..c73ba59 --- /dev/null +++ b/pebble/internal/keyspan/merging_iter.go @@ -0,0 +1,1209 @@ +// 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 keyspan + +import ( + "bytes" + "fmt" + "sort" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/manifest" +) + +// TODO(jackson): Consider implementing an optimization to seek lower levels +// past higher levels' RANGEKEYDELs. This would be analaogous to the +// optimization pebble.mergingIter performs for RANGEDELs during point key +// seeks. It may not be worth it, because range keys are rare and cascading +// seeks would require introducing key comparisons to switchTo{Min,Max}Heap +// where there currently are none. + +// TODO(jackson): There are several opportunities to use base.Equal in the +// MergingIter implementation, but will require a bit of plumbing to thread the +// Equal function. + +// MergingIter merges spans across levels of the LSM, exposing an iterator over +// spans that yields sets of spans fragmented at unique user key boundaries. +// +// A MergingIter is initialized with an arbitrary number of child iterators over +// fragmented spans. Each child iterator exposes fragmented key spans, such that +// overlapping keys are surfaced in a single Span. Key spans from one child +// iterator may overlap key spans from another child iterator arbitrarily. +// +// The spans combined by MergingIter will return spans with keys sorted by +// trailer descending. If the MergingIter is configured with a Transformer, it's +// permitted to modify the ordering of the spans' keys returned by MergingIter. +// +// # Algorithm +// +// The merging iterator wraps child iterators, merging and fragmenting spans +// across levels. The high-level algorithm is: +// +// 1. Initialize the heap with bound keys from child iterators' spans. +// 2. Find the next [or previous] two unique user keys' from bounds. +// 3. Consider the span formed between the two unique user keys a candidate +// span. +// 4. Determine if any of the child iterators' spans overlap the candidate +// span. +// 4a. If any of the child iterator's current bounds are end keys +// (during forward iteration) or start keys (during reverse +// iteration), then all the spans with that bound overlap the +// candidate span. +// 4b. Apply the configured transform, which may remove keys. +// 4c. If no spans overlap, forget the smallest (forward iteration) +// or largest (reverse iteration) unique user key and advance +// the iterators to the next unique user key. Start again from 3. +// +// # Detailed algorithm +// +// Each level (i0, i1, ...) has a user-provided input FragmentIterator. The +// merging iterator steps through individual boundaries of the underlying +// spans separately. If the underlying FragmentIterator has fragments +// [a,b){#2,#1} [b,c){#1} the mergingIterLevel.{next,prev} step through: +// +// (a, start), (b, end), (b, start), (c, end) +// +// Note that (a, start) and (b, end) are observed ONCE each, despite two keys +// sharing those bounds. Also note that (b, end) and (b, start) are two distinct +// iterator positions of a mergingIterLevel. +// +// The merging iterator maintains a heap (min during forward iteration, max +// during reverse iteration) containing the boundKeys. Each boundKey is a +// 3-tuple holding the bound user key, whether the bound is a start or end key +// and the set of keys from that level that have that bound. The heap orders +// based on the boundKey's user key only. +// +// The merging iterator is responsible for merging spans across levels to +// determine which span is next, but it's also responsible for fragmenting +// overlapping spans. Consider the example: +// +// i0: b---d e-----h +// i1: a---c h-----k +// i2: a------------------------------p +// +// fragments: a-b-c-d-e-----h-----k----------p +// +// None of the individual child iterators contain a span with the exact bounds +// [c,d), but the merging iterator must produce a span [c,d). To accomplish +// this, the merging iterator visits every span between unique boundary user +// keys. In the above example, this is: +// +// [a,b), [b,c), [c,d), [d,e), [e, h), [h, k), [k, p) +// +// The merging iterator first initializes the heap to prepare for iteration. +// The description below discusses the mechanics of forward iteration after a +// call to First, but the mechanics are similar for reverse iteration and +// other positioning methods. +// +// During a call to First, the heap is initialized by seeking every +// mergingIterLevel to the first bound of the first fragment. In the above +// example, this seeks the child iterators to: +// +// i0: (b, boundKindFragmentStart, [ [b,d) ]) +// i1: (a, boundKindFragmentStart, [ [a,c) ]) +// i2: (a, boundKindFragmentStart, [ [a,p) ]) +// +// After fixing up the heap, the root of the heap is a boundKey with the +// smallest user key ('a' in the example). Once the heap is setup for iteration +// in the appropriate direction and location, the merging iterator uses +// find{Next,Prev}FragmentSet to find the next/previous span bounds. +// +// During forward iteration, the root of the heap's user key is the start key +// key of next merged span. findNextFragmentSet sets m.start to this user +// key. The heap may contain other boundKeys with the same user key if another +// level has a fragment starting or ending at the same key, so the +// findNextFragmentSet method pulls from the heap until it finds the first key +// greater than m.start. This key is used as the end key. +// +// In the above example, this results in m.start = 'a', m.end = 'b' and child +// iterators in the following positions: +// +// i0: (b, boundKindFragmentStart, [ [b,d) ]) +// i1: (c, boundKindFragmentEnd, [ [a,c) ]) +// i2: (p, boundKindFragmentEnd, [ [a,p) ]) +// +// With the user key bounds of the next merged span established, +// findNextFragmentSet must determine which, if any, fragments overlap the span. +// During forward iteration any child iterator that is now positioned at an end +// boundary has an overlapping span. (Justification: The child iterator's end +// boundary is ≥ m.end. The corresponding start boundary must be ≤ m.start since +// there were no other user keys between m.start and m.end. So the fragments +// associated with the iterator's current end boundary have start and end bounds +// such that start ≤ m.start < m.end ≤ end). +// +// findNextFragmentSet iterates over the levels, collecting keys from any child +// iterators positioned at end boundaries. In the above example, i1 and i2 are +// positioned at end boundaries, so findNextFragmentSet collects the keys of +// [a,c) and [a,p). These spans contain the merging iterator's [m.start, m.end) +// span, but they may also extend beyond the m.start and m.end. The merging +// iterator returns the keys with the merging iter's m.start and m.end bounds, +// preserving the underlying keys' sequence numbers, key kinds and values. +// +// A MergingIter is configured with a Transform that's applied to the span +// before surfacing it to the iterator user. A Transform may remove keys +// arbitrarily, but it may not modify the values themselves. +// +// It may be the case that findNextFragmentSet finds no levels positioned at end +// boundaries, or that there are no spans remaining after applying a transform, +// in which case the span [m.start, m.end) overlaps with nothing. In this case +// findNextFragmentSet loops, repeating the above process again until it finds a +// span that does contain keys. +// +// # Memory safety +// +// The FragmentIterator interface only guarantees stability of a Span and its +// associated slices until the next positioning method is called. Adjacent Spans +// may be contained in different sstables, requring the FragmentIterator +// implementation to close one sstable, releasing its memory, before opening the +// next. Most of the state used by the MergingIter is derived from spans at +// current child iterator positions only, ensuring state is stable. The one +// exception is the start bound during forward iteration and the end bound +// during reverse iteration. +// +// If the heap root originates from an end boundary when findNextFragmentSet +// begins, a Next on the heap root level may invalidate the end boundary. To +// accommodate this, find{Next,Prev}FragmentSet copy the initial boundary if the +// subsequent Next/Prev would move to the next span. +type MergingIter struct { + *MergingBuffers + // start and end hold the bounds for the span currently under the + // iterator position. + // + // Invariant: None of the levels' iterators contain spans with a bound + // between start and end. For all bounds b, b ≤ start || b ≥ end. + start, end []byte + + // transformer defines a transformation to be applied to a span before it's + // yielded to the user. Transforming may filter individual keys contained + // within the span. + transformer Transformer + // span holds the iterator's current span. This span is used as the + // destination for transforms. Every tranformed span overwrites the + // previous. + span Span + err error + dir int8 + + // alloc preallocates mergingIterLevel and mergingIterItems for use by the + // merging iterator. As long as the merging iterator is used with + // manifest.NumLevels+3 and fewer fragment iterators, the merging iterator + // will not need to allocate upon initialization. The value NumLevels+3 + // mirrors the preallocated levels in iterAlloc used for point iterators. + // Invariant: cap(levels) == cap(items) + alloc struct { + levels [manifest.NumLevels + 3]mergingIterLevel + items [manifest.NumLevels + 3]mergingIterItem + } +} + +// MergingBuffers holds buffers used while merging keyspans. +type MergingBuffers struct { + // keys holds all of the keys across all levels that overlap the key span + // [start, end), sorted by Trailer descending. This slice is reconstituted + // in synthesizeKeys from each mergingIterLevel's keys every time the + // [start, end) bounds change. + // + // Each element points into a child iterator's memory, so the keys may not + // be directly modified. + keys keysBySeqNumKind + // levels holds levels allocated by MergingIter.init. The MergingIter will + // prefer use of its `manifest.NumLevels+3` array, so this slice will be + // longer if set. + levels []mergingIterLevel + // heap holds a slice for the merging iterator heap allocated by + // MergingIter.init. The MergingIter will prefer use of its + // `manifest.NumLevels+3` items array, so this slice will be longer if set. + heap mergingIterHeap + // buf is a buffer used to save [start, end) boundary keys. + buf []byte +} + +// PrepareForReuse discards any excessively large buffers. +func (bufs *MergingBuffers) PrepareForReuse() { + if cap(bufs.buf) > bufferReuseMaxCapacity { + bufs.buf = nil + } +} + +// MergingIter implements the FragmentIterator interface. +var _ FragmentIterator = (*MergingIter)(nil) + +type mergingIterLevel struct { + iter FragmentIterator + + // heapKey holds the current key at this level for use within the heap. + heapKey boundKey +} + +func (l *mergingIterLevel) next() { + if l.heapKey.kind == boundKindFragmentStart { + l.heapKey = boundKey{ + kind: boundKindFragmentEnd, + key: l.heapKey.span.End, + span: l.heapKey.span, + } + return + } + if s := l.iter.Next(); s == nil { + l.heapKey = boundKey{kind: boundKindInvalid} + } else { + l.heapKey = boundKey{ + kind: boundKindFragmentStart, + key: s.Start, + span: s, + } + } +} + +func (l *mergingIterLevel) prev() { + if l.heapKey.kind == boundKindFragmentEnd { + l.heapKey = boundKey{ + kind: boundKindFragmentStart, + key: l.heapKey.span.Start, + span: l.heapKey.span, + } + return + } + if s := l.iter.Prev(); s == nil { + l.heapKey = boundKey{kind: boundKindInvalid} + } else { + l.heapKey = boundKey{ + kind: boundKindFragmentEnd, + key: s.End, + span: s, + } + } +} + +// Init initializes the merging iterator with the provided fragment iterators. +func (m *MergingIter) Init( + cmp base.Compare, transformer Transformer, bufs *MergingBuffers, iters ...FragmentIterator, +) { + *m = MergingIter{ + MergingBuffers: bufs, + transformer: transformer, + } + m.heap.cmp = cmp + levels, items := m.levels, m.heap.items + + // Invariant: cap(levels) >= cap(items) + // Invariant: cap(alloc.levels) == cap(alloc.items) + if len(iters) <= len(m.alloc.levels) { + // The slices allocated on the MergingIter struct are large enough. + m.levels = m.alloc.levels[:len(iters)] + m.heap.items = m.alloc.items[:0] + } else if len(iters) <= cap(levels) { + // The existing heap-allocated slices are large enough, so reuse them. + m.levels = levels[:len(iters)] + m.heap.items = items[:0] + } else { + // Heap allocate new slices. + m.levels = make([]mergingIterLevel, len(iters)) + m.heap.items = make([]mergingIterItem, 0, len(iters)) + } + for i := range m.levels { + m.levels[i] = mergingIterLevel{iter: iters[i]} + } +} + +// AddLevel adds a new level to the bottom of the merging iterator. AddLevel +// must be called after Init and before any other method. +func (m *MergingIter) AddLevel(iter FragmentIterator) { + m.levels = append(m.levels, mergingIterLevel{iter: iter}) +} + +// SeekGE moves the iterator to the first span covering a key greater than +// or equal to the given key. This is equivalent to seeking to the first +// span with an end key greater than the given key. +func (m *MergingIter) SeekGE(key []byte) *Span { + m.invalidate() // clear state about current position + + // SeekGE(k) seeks to the first span with an end key greater than the given + // key. The merged span M that we're searching for might straddle the seek + // `key`. In this case, the M.Start may be a key ≤ the seek key. + // + // Consider a SeekGE(dog) in the following example. + // + // i0: b---d e-----h + // i1: a---c h-----k + // i2: a------------------------------p + // merged: a-b-c-d-e-----h-----k----------p + // + // The merged span M containing 'dog' is [d,e). The 'd' of the merged span + // comes from i0's [b,d)'s end boundary. The [b,d) span does not cover any + // key >= dog, so we cannot find the span by positioning the child iterators + // using a SeekGE(dog). + // + // Instead, if we take all the child iterators' spans bounds: + // a b c d e h k p + // We want to partition them into keys ≤ `key` and keys > `key`. + // dog + // │ + // a b c d│e h k p + // │ + // The largest key on the left of the partition forms the merged span's + // start key, and the smallest key on the right of the partition forms the + // merged span's end key. Recharacterized: + // + // M.Start: the largest boundary ≤ k of any child span + // M.End: the smallest boundary > k of any child span + // + // The FragmentIterator interface doesn't implement seeking by all bounds, + // it implements seeking by containment. A SeekGE(k) will ensure we observe + // all start boundaries ≥ k and all end boundaries > k but does not ensure + // we observe end boundaries = k or any boundaries < k. A SeekLT(k) will + // ensure we observe all start boundaries < k and all end boundaries ≤ k but + // does not ensure we observe any start boundaries = k or any boundaries > + // k. This forces us to seek in one direction and step in the other. + // + // In a SeekGE, we want to end up oriented in the forward direction when + // complete, so we begin with searching for M.Start by SeekLT-ing every + // child iterator to `k`. For every child span found, we determine the + // largest bound ≤ `k` and use it to initialize our max heap. The resulting + // root of the max heap is a preliminary value for `M.Start`. + for i := range m.levels { + l := &m.levels[i] + s := l.iter.SeekLT(key) + if s == nil { + l.heapKey = boundKey{kind: boundKindInvalid} + } else if m.cmp(s.End, key) <= 0 { + l.heapKey = boundKey{ + kind: boundKindFragmentEnd, + key: s.End, + span: s, + } + } else { + // s.End > key && s.Start < key + // We need to use this span's start bound, since that's the largest + // bound ≤ key. + l.heapKey = boundKey{ + kind: boundKindFragmentStart, + key: s.Start, + span: s, + } + } + } + m.initMaxHeap() + if m.err != nil { + return nil + } else if len(m.heap.items) == 0 { + // There are no spans covering any key < `key`. There is no span that + // straddles the seek key. Reorient the heap into a min heap and return + // the first span we find in the forward direction. + m.switchToMinHeap() + return m.findNextFragmentSet() + } + + // The heap root is now the largest boundary key b such that: + // 1. b < k + // 2. b = k, and b is an end boundary + // There's a third case that we will need to consider later, after we've + // switched to a min heap: + // 3. there exists a start boundary key b such that b = k. + // A start boundary key equal to k would not be surfaced when we seeked all + // the levels using SeekLT(k), since no key `key`, which will serve as our candidate end + // bound. + m.switchToMinHeap() + if m.err != nil { + return nil + } else if len(m.heap.items) == 0 { + return nil + } + + // Check for the case 3 described above. It's possible that when we switch + // heap directions, we discover a start boundary of some child span that is + // equal to the seek key `key`. In this case, we want this key to be our + // start boundary. + if m.heap.items[0].boundKey.kind == boundKindFragmentStart && + m.cmp(m.heap.items[0].boundKey.key, key) == 0 { + // Call findNextFragmentSet, which will set m.start to the heap root and + // proceed forward. + return m.findNextFragmentSet() + } + + m.end = m.heap.items[0].boundKey.key + if found, s := m.synthesizeKeys(+1); found && s != nil { + return s + } + return m.findNextFragmentSet() + +} + +// SeekLT moves the iterator to the last span covering a key less than the +// given key. This is equivalent to seeking to the last span with a start +// key less than the given key. +func (m *MergingIter) SeekLT(key []byte) *Span { + m.invalidate() // clear state about current position + + // SeekLT(k) seeks to the last span with a start key less than the given + // key. The merged span M that we're searching for might straddle the seek + // `key`. In this case, the M.End may be a key ≥ the seek key. + // + // Consider a SeekLT(dog) in the following example. + // + // i0: b---d e-----h + // i1: a---c h-----k + // i2: a------------------------------p + // merged: a-b-c-d-e-----h-----k----------p + // + // The merged span M containing the largest key <'dog' is [d,e). The 'e' of + // the merged span comes from i0's [e,h)'s start boundary. The [e,h) span + // does not cover any key < dog, so we cannot find the span by positioning + // the child iterators using a SeekLT(dog). + // + // Instead, if we take all the child iterators' spans bounds: + // a b c d e h k p + // We want to partition them into keys < `key` and keys ≥ `key`. + // dog + // │ + // a b c d│e h k p + // │ + // The largest key on the left of the partition forms the merged span's + // start key, and the smallest key on the right of the partition forms the + // merged span's end key. Recharacterized: + // + // M.Start: the largest boundary < k of any child span + // M.End: the smallest boundary ≥ k of any child span + // + // The FragmentIterator interface doesn't implement seeking by all bounds, + // it implements seeking by containment. A SeekGE(k) will ensure we observe + // all start boundaries ≥ k and all end boundaries > k but does not ensure + // we observe end boundaries = k or any boundaries < k. A SeekLT(k) will + // ensure we observe all start boundaries < k and all end boundaries ≤ k but + // does not ensure we observe any start boundaries = k or any boundaries > + // k. This forces us to seek in one direction and step in the other. + // + // In a SeekLT, we want to end up oriented in the backward direction when + // complete, so we begin with searching for M.End by SeekGE-ing every + // child iterator to `k`. For every child span found, we determine the + // smallest bound ≥ `k` and use it to initialize our min heap. The resulting + // root of the min heap is a preliminary value for `M.End`. + for i := range m.levels { + l := &m.levels[i] + s := l.iter.SeekGE(key) + if s == nil { + l.heapKey = boundKey{kind: boundKindInvalid} + } else if m.cmp(s.Start, key) >= 0 { + l.heapKey = boundKey{ + kind: boundKindFragmentStart, + key: s.Start, + span: s, + } + } else { + // s.Start < key + // We need to use this span's end bound, since that's the smallest + // bound > key. + l.heapKey = boundKey{ + kind: boundKindFragmentEnd, + key: s.End, + span: s, + } + } + } + m.initMinHeap() + if m.err != nil { + return nil + } else if len(m.heap.items) == 0 { + // There are no spans covering any key ≥ `key`. There is no span that + // straddles the seek key. Reorient the heap into a max heap and return + // the first span we find in the reverse direction. + m.switchToMaxHeap() + return m.findPrevFragmentSet() + } + + // The heap root is now the smallest boundary key b such that: + // 1. b > k + // 2. b = k, and b is a start boundary + // There's a third case that we will need to consider later, after we've + // switched to a max heap: + // 3. there exists an end boundary key b such that b = k. + // An end boundary key equal to k would not be surfaced when we seeked all + // the levels using SeekGE(k), since k would not be contained within the + // exclusive end boundary. + // + // Assume that the tightest boundary ≥ k is the current heap root (cases 1 & + // 2). After we switch to a max heap, we'll check for the third case and + // adjust the end boundary if necessary. + m.end = m.heap.items[0].boundKey.key + + // Before switching the direction of the heap, save a copy of the end + // boundary if it's the start boundary of some child span. Prev-ing the + // child iterator might switch files and invalidate the memory of the bound. + if m.heap.items[0].boundKey.kind == boundKindFragmentStart { + m.buf = append(m.buf[:0], m.end...) + m.end = m.buf + } + + // Switch to a max heap. This will move each level to the previous bound in + // every level, and then establish a max heap. This allows us to obtain the + // largest boundary key < `key`, which will serve as our candidate start + // bound. + m.switchToMaxHeap() + if m.err != nil { + return nil + } else if len(m.heap.items) == 0 { + return nil + } + // Check for the case 3 described above. It's possible that when we switch + // heap directions, we discover an end boundary of some child span that is + // equal to the seek key `key`. In this case, we want this key to be our end + // boundary. + if m.heap.items[0].boundKey.kind == boundKindFragmentEnd && + m.cmp(m.heap.items[0].boundKey.key, key) == 0 { + // Call findPrevFragmentSet, which will set m.end to the heap root and + // proceed backwards. + return m.findPrevFragmentSet() + } + + m.start = m.heap.items[0].boundKey.key + if found, s := m.synthesizeKeys(-1); found && s != nil { + return s + } + return m.findPrevFragmentSet() +} + +// First seeks the iterator to the first span. +func (m *MergingIter) First() *Span { + m.invalidate() // clear state about current position + for i := range m.levels { + if s := m.levels[i].iter.First(); s == nil { + m.levels[i].heapKey = boundKey{kind: boundKindInvalid} + } else { + m.levels[i].heapKey = boundKey{ + kind: boundKindFragmentStart, + key: s.Start, + span: s, + } + } + } + m.initMinHeap() + return m.findNextFragmentSet() +} + +// Last seeks the iterator to the last span. +func (m *MergingIter) Last() *Span { + m.invalidate() // clear state about current position + for i := range m.levels { + if s := m.levels[i].iter.Last(); s == nil { + m.levels[i].heapKey = boundKey{kind: boundKindInvalid} + } else { + m.levels[i].heapKey = boundKey{ + kind: boundKindFragmentEnd, + key: s.End, + span: s, + } + } + } + m.initMaxHeap() + return m.findPrevFragmentSet() +} + +// Next advances the iterator to the next span. +func (m *MergingIter) Next() *Span { + if m.err != nil { + return nil + } + if m.dir == +1 && (m.end == nil || m.start == nil) { + return nil + } + if m.dir != +1 { + m.switchToMinHeap() + } + return m.findNextFragmentSet() +} + +// Prev advances the iterator to the previous span. +func (m *MergingIter) Prev() *Span { + if m.err != nil { + return nil + } + if m.dir == -1 && (m.end == nil || m.start == nil) { + return nil + } + if m.dir != -1 { + m.switchToMaxHeap() + } + return m.findPrevFragmentSet() +} + +// Error returns any accumulated error. +func (m *MergingIter) Error() error { + if m.heap.len() == 0 || m.err != nil { + return m.err + } + return m.levels[m.heap.items[0].index].iter.Error() +} + +// Close closes the iterator, releasing all acquired resources. +func (m *MergingIter) Close() error { + for i := range m.levels { + if err := m.levels[i].iter.Close(); err != nil && m.err == nil { + m.err = err + } + } + m.levels = nil + m.heap.items = m.heap.items[:0] + return m.err +} + +// String implements fmt.Stringer. +func (m *MergingIter) String() string { + return "merging-keyspan" +} + +func (m *MergingIter) initMinHeap() { + m.dir = +1 + m.heap.reverse = false + m.initHeap() +} + +func (m *MergingIter) initMaxHeap() { + m.dir = -1 + m.heap.reverse = true + m.initHeap() +} + +func (m *MergingIter) initHeap() { + m.heap.items = m.heap.items[:0] + for i := range m.levels { + if l := &m.levels[i]; l.heapKey.kind != boundKindInvalid { + m.heap.items = append(m.heap.items, mergingIterItem{ + index: i, + boundKey: &l.heapKey, + }) + } else { + m.err = firstError(m.err, l.iter.Error()) + if m.err != nil { + return + } + } + } + m.heap.init() +} + +func (m *MergingIter) switchToMinHeap() { + // switchToMinHeap reorients the heap for forward iteration, without moving + // the current MergingIter position. + + // The iterator is currently positioned at the span [m.start, m.end), + // oriented in the reverse direction, so each level's iterator is positioned + // to the largest key ≤ m.start. To reorient in the forward direction, we + // must advance each level's iterator to the smallest key ≥ m.end. Consider + // this three-level example. + // + // i0: b---d e-----h + // i1: a---c h-----k + // i2: a------------------------------p + // + // merged: a-b-c-d-e-----h-----k----------p + // + // If currently positioned at the merged span [c,d), then the level + // iterators' heap keys are: + // + // i0: (b, [b, d)) i1: (c, [a,c)) i2: (a, [a,p)) + // + // Reversing the heap should not move the merging iterator and should not + // change the current [m.start, m.end) bounds. It should only prepare for + // forward iteration by updating the child iterators' heap keys to: + // + // i0: (d, [b, d)) i1: (h, [h,k)) i2: (p, [a,p)) + // + // In every level the first key ≥ m.end is the next in the iterator. + // Justification: Suppose not and a level iterator's next key was some key k + // such that k < m.end. The max-heap invariant dictates that the current + // iterator position is the largest entry with a user key ≥ m.start. This + // means k > m.start. We started with the assumption that k < m.end, so + // m.start < k < m.end. But then k is between our current span bounds, + // and reverse iteration would have constructed the current interval to be + // [k, m.end) not [m.start, m.end). + + if invariants.Enabled { + for i := range m.levels { + l := &m.levels[i] + if l.heapKey.kind != boundKindInvalid && m.cmp(l.heapKey.key, m.start) > 0 { + panic("pebble: invariant violation: max-heap key > m.start") + } + } + } + + for i := range m.levels { + m.levels[i].next() + } + m.initMinHeap() +} + +func (m *MergingIter) switchToMaxHeap() { + // switchToMaxHeap reorients the heap for reverse iteration, without moving + // the current MergingIter position. + + // The iterator is currently positioned at the span [m.start, m.end), + // oriented in the forward direction. Each level's iterator is positioned at + // the smallest bound ≥ m.end. To reorient in the reverse direction, we must + // move each level's iterator to the largest key ≤ m.start. Consider this + // three-level example. + // + // i0: b---d e-----h + // i1: a---c h-----k + // i2: a------------------------------p + // + // merged: a-b-c-d-e-----h-----k----------p + // + // If currently positioned at the merged span [c,d), then the level + // iterators' heap keys are: + // + // i0: (d, [b, d)) i1: (h, [h,k)) i2: (p, [a,p)) + // + // Reversing the heap should not move the merging iterator and should not + // change the current [m.start, m.end) bounds. It should only prepare for + // reverse iteration by updating the child iterators' heap keys to: + // + // i0: (b, [b, d)) i1: (c, [a,c)) i2: (a, [a,p)) + // + // In every level the largest key ≤ m.start is the prev in the iterator. + // Justification: Suppose not and a level iterator's prev key was some key k + // such that k > m.start. The min-heap invariant dictates that the current + // iterator position is the smallest entry with a user key ≥ m.end. This + // means k < m.end, otherwise the iterator would be positioned at k. We + // started with the assumption that k > m.start, so m.start < k < m.end. But + // then k is between our current span bounds, and reverse iteration + // would have constructed the current interval to be [m.start, k) not + // [m.start, m.end). + + if invariants.Enabled { + for i := range m.levels { + l := &m.levels[i] + if l.heapKey.kind != boundKindInvalid && m.cmp(l.heapKey.key, m.end) < 0 { + panic("pebble: invariant violation: min-heap key < m.end") + } + } + } + + for i := range m.levels { + m.levels[i].prev() + } + m.initMaxHeap() +} + +func (m *MergingIter) cmp(a, b []byte) int { + return m.heap.cmp(a, b) +} + +func (m *MergingIter) findNextFragmentSet() *Span { + // Each iteration of this loop considers a new merged span between unique + // user keys. An iteration may find that there exists no overlap for a given + // span, (eg, if the spans [a,b), [d, e) exist within level iterators, the + // below loop will still consider [b,d) before continuing to [d, e)). It + // returns when it finds a span that is covered by at least one key. + + for m.heap.len() > 0 && m.err == nil { + // Initialize the next span's start bound. SeekGE and First prepare the + // heap without advancing. Next leaves the heap in a state such that the + // root is the smallest bound key equal to the returned span's end key, + // so the heap is already positioned at the next merged span's start key. + + // NB: m.heapRoot() might be either an end boundary OR a start boundary + // of a level's span. Both end and start boundaries may still be a start + // key of a span in the set of fragmented spans returned by MergingIter. + // Consider the scenario: + // a----------l #1 + // b-----------m #2 + // + // The merged, fully-fragmented spans that MergingIter exposes to the caller + // have bounds: + // a-b #1 + // b--------l #1 + // b--------l #2 + // l-m #2 + // + // When advancing to l-m#2, we must set m.start to 'l', which originated + // from [a,l)#1's end boundary. + m.start = m.heap.items[0].boundKey.key + + // Before calling nextEntry, consider whether it might invalidate our + // start boundary. If the start boundary key originated from an end + // boundary, then we need to copy the start key before advancing the + // underlying iterator to the next Span. + if m.heap.items[0].boundKey.kind == boundKindFragmentEnd { + m.buf = append(m.buf[:0], m.start...) + m.start = m.buf + } + + // There may be many entries all with the same user key. Spans in other + // levels may also start or end at this same user key. For eg: + // L1: [a, c) [c, d) + // L2: [c, e) + // If we're positioned at L1's end(c) end boundary, we want to advance + // to the first bound > c. + m.nextEntry() + for len(m.heap.items) > 0 && m.err == nil && m.cmp(m.heapRoot(), m.start) == 0 { + m.nextEntry() + } + if len(m.heap.items) == 0 || m.err != nil { + break + } + + // The current entry at the top of the heap is the first key > m.start. + // It must become the end bound for the span we will return to the user. + // In the above example, the root of the heap is L1's end(d). + m.end = m.heap.items[0].boundKey.key + + // Each level within m.levels may have a span that overlaps the + // fragmented key span [m.start, m.end). Update m.keys to point to them + // and sort them by kind, sequence number. There may not be any keys + // defined over [m.start, m.end) if we're between the end of one span + // and the start of the next, OR if the configured transform filters any + // keys out. We allow empty spans that were emitted by child iterators, but + // we elide empty spans created by the mergingIter itself that don't overlap + // with any child iterator returned spans (i.e. empty spans that bridge two + // distinct child-iterator-defined spans). + if found, s := m.synthesizeKeys(+1); found && s != nil { + return s + } + } + // Exhausted. + m.clear() + return nil +} + +func (m *MergingIter) findPrevFragmentSet() *Span { + // Each iteration of this loop considers a new merged span between unique + // user keys. An iteration may find that there exists no overlap for a given + // span, (eg, if the spans [a,b), [d, e) exist within level iterators, the + // below loop will still consider [b,d) before continuing to [a, b)). It + // returns when it finds a span that is covered by at least one key. + + for m.heap.len() > 0 && m.err == nil { + // Initialize the next span's end bound. SeekLT and Last prepare the + // heap without advancing. Prev leaves the heap in a state such that the + // root is the largest bound key equal to the returned span's start key, + // so the heap is already positioned at the next merged span's end key. + + // NB: m.heapRoot() might be either an end boundary OR a start boundary + // of a level's span. Both end and start boundaries may still be a start + // key of a span returned by MergingIter. Consider the scenario: + // a----------l #2 + // b-----------m #1 + // + // The merged, fully-fragmented spans that MergingIter exposes to the caller + // have bounds: + // a-b #2 + // b--------l #2 + // b--------l #1 + // l-m #1 + // + // When Preving to a-b#2, we must set m.end to 'b', which originated + // from [b,m)#1's start boundary. + m.end = m.heap.items[0].boundKey.key + + // Before calling prevEntry, consider whether it might invalidate our + // end boundary. If the end boundary key originated from a start + // boundary, then we need to copy the end key before advancing the + // underlying iterator to the previous Span. + if m.heap.items[0].boundKey.kind == boundKindFragmentStart { + m.buf = append(m.buf[:0], m.end...) + m.end = m.buf + } + + // There may be many entries all with the same user key. Spans in other + // levels may also start or end at this same user key. For eg: + // L1: [a, c) [c, d) + // L2: [c, e) + // If we're positioned at L1's start(c) start boundary, we want to prev + // to move to the first bound < c. + m.prevEntry() + for len(m.heap.items) > 0 && m.err == nil && m.cmp(m.heapRoot(), m.end) == 0 { + m.prevEntry() + } + if len(m.heap.items) == 0 || m.err != nil { + break + } + + // The current entry at the top of the heap is the first key < m.end. + // It must become the start bound for the span we will return to the + // user. In the above example, the root of the heap is L1's start(a). + m.start = m.heap.items[0].boundKey.key + + // Each level within m.levels may have a set of keys that overlap the + // fragmented key span [m.start, m.end). Update m.keys to point to them + // and sort them by kind, sequence number. There may not be any keys + // spanning [m.start, m.end) if we're between the end of one span and + // the start of the next, OR if the configured transform filters any + // keys out. We allow empty spans that were emitted by child iterators, but + // we elide empty spans created by the mergingIter itself that don't overlap + // with any child iterator returned spans (i.e. empty spans that bridge two + // distinct child-iterator-defined spans). + if found, s := m.synthesizeKeys(-1); found && s != nil { + return s + } + } + // Exhausted. + m.clear() + return nil +} + +func (m *MergingIter) heapRoot() []byte { + return m.heap.items[0].boundKey.key +} + +// synthesizeKeys is called by find{Next,Prev}FragmentSet to populate and +// sort the set of keys overlapping [m.start, m.end). +// +// During forward iteration, if the current heap item is a fragment end, +// then the fragment's start must be ≤ m.start and the fragment overlaps the +// current iterator position of [m.start, m.end). +// +// During reverse iteration, if the current heap item is a fragment start, +// then the fragment's end must be ≥ m.end and the fragment overlaps the +// current iteration position of [m.start, m.end). +// +// The boolean return value, `found`, is true if the returned span overlaps +// with a span returned by a child iterator. +func (m *MergingIter) synthesizeKeys(dir int8) (bool, *Span) { + if invariants.Enabled { + if m.cmp(m.start, m.end) >= 0 { + panic(fmt.Sprintf("pebble: invariant violation: span start ≥ end: %s >= %s", m.start, m.end)) + } + } + + m.keys = m.keys[:0] + found := false + for i := range m.levels { + if dir == +1 && m.levels[i].heapKey.kind == boundKindFragmentEnd || + dir == -1 && m.levels[i].heapKey.kind == boundKindFragmentStart { + m.keys = append(m.keys, m.levels[i].heapKey.span.Keys...) + found = true + } + } + // TODO(jackson): We should be able to remove this sort and instead + // guarantee that we'll return keys in the order of the levels they're from. + // With careful iterator construction, this would guarantee that they're + // sorted by trailer descending for the range key iteration use case. + sort.Sort(&m.keys) + + // Apply the configured transform. See VisibleTransform. + m.span = Span{ + Start: m.start, + End: m.end, + Keys: m.keys, + KeysOrder: ByTrailerDesc, + } + // NB: m.heap.cmp is a base.Compare, whereas m.cmp is a method on + // MergingIter. + if err := m.transformer.Transform(m.heap.cmp, m.span, &m.span); err != nil { + m.err = err + return false, nil + } + return found, &m.span +} + +func (m *MergingIter) invalidate() { + m.err = nil +} + +func (m *MergingIter) clear() { + for fi := range m.keys { + m.keys[fi] = Key{} + } + m.keys = m.keys[:0] +} + +// nextEntry steps to the next entry. +func (m *MergingIter) nextEntry() { + l := &m.levels[m.heap.items[0].index] + l.next() + if !l.heapKey.valid() { + // l.iter is exhausted. + m.err = l.iter.Error() + if m.err == nil { + m.heap.pop() + } + return + } + + if m.heap.len() > 1 { + m.heap.fix(0) + } +} + +// prevEntry steps to the previous entry. +func (m *MergingIter) prevEntry() { + l := &m.levels[m.heap.items[0].index] + l.prev() + if !l.heapKey.valid() { + // l.iter is exhausted. + m.err = l.iter.Error() + if m.err == nil { + m.heap.pop() + } + return + } + + if m.heap.len() > 1 { + m.heap.fix(0) + } +} + +// DebugString returns a string representing the current internal state of the +// merging iterator and its heap for debugging purposes. +func (m *MergingIter) DebugString() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "Current bounds: [%q, %q)\n", m.start, m.end) + for i := range m.levels { + fmt.Fprintf(&buf, "%d: heap key %s\n", i, m.levels[i].heapKey) + } + return buf.String() +} + +type mergingIterItem struct { + // boundKey points to the corresponding mergingIterLevel's `iterKey`. + *boundKey + // index is the index of this level within the MergingIter's levels field. + index int +} + +// mergingIterHeap is copied from mergingIterHeap defined in the root pebble +// package for use with point keys. + +type mergingIterHeap struct { + cmp base.Compare + reverse bool + items []mergingIterItem +} + +func (h *mergingIterHeap) len() int { + return len(h.items) +} + +func (h *mergingIterHeap) less(i, j int) bool { + // This key comparison only uses the user key and not the boundKind. Bound + // kind doesn't matter because when stepping over a user key, + // findNextFragmentSet and findPrevFragmentSet skip past all heap items with + // that user key, and makes no assumptions on ordering. All other heap + // examinations only consider the user key. + ik, jk := h.items[i].key, h.items[j].key + c := h.cmp(ik, jk) + if h.reverse { + return c > 0 + } + return c < 0 +} + +func (h *mergingIterHeap) swap(i, j int) { + h.items[i], h.items[j] = h.items[j], h.items[i] +} + +// init, fix, up and down are copied from the go stdlib. +func (h *mergingIterHeap) init() { + // heapify + n := h.len() + for i := n/2 - 1; i >= 0; i-- { + h.down(i, n) + } +} + +func (h *mergingIterHeap) fix(i int) { + if !h.down(i, h.len()) { + h.up(i) + } +} + +func (h *mergingIterHeap) pop() *mergingIterItem { + n := h.len() - 1 + h.swap(0, n) + h.down(0, n) + item := &h.items[n] + h.items = h.items[:n] + return item +} + +func (h *mergingIterHeap) up(j int) { + for { + i := (j - 1) / 2 // parent + if i == j || !h.less(j, i) { + break + } + h.swap(i, j) + j = i + } +} + +func (h *mergingIterHeap) down(i0, n int) bool { + i := i0 + for { + j1 := 2*i + 1 + if j1 >= n || j1 < 0 { // j1 < 0 after int overflow + break + } + j := j1 // left child + if j2 := j1 + 1; j2 < n && h.less(j2, j1) { + j = j2 // = 2*i + 2 // right child + } + if !h.less(j, i) { + break + } + h.swap(i, j) + i = j + } + return i > i0 +} + +type boundKind int8 + +const ( + boundKindInvalid boundKind = iota + boundKindFragmentStart + boundKindFragmentEnd +) + +type boundKey struct { + kind boundKind + key []byte + // span holds the span the bound key comes from. + // + // If kind is boundKindFragmentStart, then key is span.Start. If kind is + // boundKindFragmentEnd, then key is span.End. + span *Span +} + +func (k boundKey) valid() bool { + return k.kind != boundKindInvalid +} + +func (k boundKey) String() string { + var buf bytes.Buffer + switch k.kind { + case boundKindInvalid: + fmt.Fprint(&buf, "invalid") + case boundKindFragmentStart: + fmt.Fprint(&buf, "fragment-start") + case boundKindFragmentEnd: + fmt.Fprint(&buf, "fragment-end ") + default: + fmt.Fprintf(&buf, "unknown-kind(%d)", k.kind) + } + fmt.Fprintf(&buf, " %s [", k.key) + fmt.Fprintf(&buf, "%s", k.span) + fmt.Fprint(&buf, "]") + return buf.String() +} diff --git a/pebble/internal/keyspan/merging_iter_test.go b/pebble/internal/keyspan/merging_iter_test.go new file mode 100644 index 0000000..18650bb --- /dev/null +++ b/pebble/internal/keyspan/merging_iter_test.go @@ -0,0 +1,252 @@ +// 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 keyspan + +import ( + "bytes" + "fmt" + "math/rand" + "slices" + "strconv" + "strings" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/stretchr/testify/require" +) + +func TestMergingIter(t *testing.T) { + cmp := base.DefaultComparer.Compare + + var definedIters []FragmentIterator + var buf bytes.Buffer + datadriven.RunTest(t, "testdata/merging_iter", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + definedIters = definedIters[:0] + lines := strings.Split(strings.TrimSpace(td.Input), "\n") + var spans []Span + for _, line := range lines { + if line == "--" { + definedIters = append(definedIters, &invalidatingIter{iter: NewIter(cmp, spans)}) + spans = nil + continue + } + spans = append(spans, ParseSpan(line)) + } + if len(spans) > 0 { + definedIters = append(definedIters, &invalidatingIter{iter: NewIter(cmp, spans)}) + } + return fmt.Sprintf("%d levels", len(definedIters)) + case "iter": + buf.Reset() + pctx := probeContext{log: &buf} + snapshot := base.InternalKeySeqNumMax + iters := slices.Clone(definedIters) + for _, cmdArg := range td.CmdArgs { + switch cmdArg.Key { + case "snapshot": + var err error + snapshot, err = strconv.ParseUint(cmdArg.Vals[0], 10, 64) + require.NoError(t, err) + case "probes": + // The first value indicates which of the merging iterator's + // child iterators is the target. + i, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return err.Error() + } + // The remaining values define probes to attach. + iters[i] = attachProbes(iters[i], pctx, parseProbes(cmdArg.Vals[1:]...)...) + default: + return fmt.Sprintf("unrecognized arg %q", cmdArg.Key) + } + } + var iter MergingIter + iter.Init(cmp, VisibleTransform(snapshot), new(MergingBuffers), iters...) + runIterCmd(t, td, &iter, &buf) + return buf.String() + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +// TestMergingIter_FragmenterEquivalence tests for equivalence between the +// fragmentation performed on-the-fly by the MergingIter and the fragmentation +// performed by the Fragmenter. +// +// It does this by producing 1-10 levels of well-formed fragments. Generated +// fragments may overlap other levels arbitrarily, but within their level +// generated fragments may only overlap other fragments that share the same user +// key bounds. +// +// The test then feeds all the fragments, across all levels, into a Fragmenter +// and produces a Iter over those fragments. The test also constructs a +// MergingIter with a separate Iter for each level. It runs a random +// series of operations, applying each operation to both. It asserts that each +// operation has identical results on both iterators. +func TestMergingIter_FragmenterEquivalence(t *testing.T) { + seed := time.Now().UnixNano() + for i := int64(0); i < 10; i++ { + testFragmenterEquivalenceOnce(t, seed+i) + } +} + +func TestMergingIter_FragmenterEquivalence_Seed(t *testing.T) { + // This test uses a fixed seed. It's useful to manually edit its seed when + // debugging a test failure of the variable-seed test. + const seed = 1644517830186873000 + testFragmenterEquivalenceOnce(t, seed) +} + +func testFragmenterEquivalenceOnce(t *testing.T, seed int64) { + cmp := testkeys.Comparer.Compare + rng := rand.New(rand.NewSource(seed)) + t.Logf("seed = %d", seed) + + // Use a key space of alphanumeric strings, with a random max length between + // 1-3. Repeat keys are more common at the lower max lengths. + ks := testkeys.Alpha(rng.Intn(3) + 1) + + // Generate between 1 and 10 levels of fragment iterators. + levels := make([][]Span, rng.Intn(10)+1) + iters := make([]FragmentIterator, len(levels)) + var allSpans []Span + var buf bytes.Buffer + for l := 0; l < len(levels); l++ { + fmt.Fprintf(&buf, "level %d: ", l) + for keyspaceStartIdx := int64(0); keyspaceStartIdx < ks.Count(); { + // Generate spans of lengths of up to a third of the keyspace. + spanStartIdx := keyspaceStartIdx + rng.Int63n(ks.Count()/3) + spanEndIdx := spanStartIdx + rng.Int63n(ks.Count()/3) + 1 + + if spanEndIdx < ks.Count() { + keyCount := uint64(rng.Intn(3) + 1) + s := Span{ + Start: testkeys.Key(ks, spanStartIdx), + End: testkeys.Key(ks, spanEndIdx), + Keys: make([]Key, 0, keyCount), + } + for k := keyCount; k > 0; k-- { + seqNum := uint64((len(levels)-l)*3) + k + s.Keys = append(s.Keys, Key{ + Trailer: base.MakeTrailer(seqNum, base.InternalKeyKindRangeKeySet), + }) + } + if len(levels[l]) > 0 { + fmt.Fprint(&buf, ", ") + } + fmt.Fprintf(&buf, "%s", s) + + levels[l] = append(levels[l], s) + allSpans = append(allSpans, s) + } + keyspaceStartIdx = spanEndIdx + } + iters[l] = &invalidatingIter{iter: NewIter(cmp, levels[l])} + fmt.Fprintln(&buf) + } + + // Fragment the spans across the levels. + var allFragmented []Span + f := Fragmenter{ + Cmp: cmp, + Format: testkeys.Comparer.FormatKey, + Emit: func(span Span) { + allFragmented = append(allFragmented, span) + }, + } + Sort(f.Cmp, allSpans) + for _, s := range allSpans { + f.Add(s) + } + f.Finish() + + // Log all the levels and their fragments, as well as the fully-fragmented + // spans produced by the Fragmenter. + fmt.Fprintln(&buf, "Fragmenter fragments:") + for i, s := range allFragmented { + if i > 0 { + fmt.Fprint(&buf, ", ") + } + fmt.Fprint(&buf, s) + } + t.Logf("%d levels:\n%s\n", len(levels), buf.String()) + + fragmenterIter := NewIter(f.Cmp, allFragmented) + mergingIter := &MergingIter{} + mergingIter.Init(f.Cmp, VisibleTransform(base.InternalKeySeqNumMax), new(MergingBuffers), iters...) + + // Position both so that it's okay to perform relative positioning + // operations immediately. + mergingIter.First() + fragmenterIter.First() + + type opKind struct { + weight int + fn func() (str string, f *Span, m *Span) + } + ops := []opKind{ + {weight: 2, fn: func() (string, *Span, *Span) { + return "First()", fragmenterIter.First(), mergingIter.First() + }}, + {weight: 2, fn: func() (string, *Span, *Span) { + return "Last()", fragmenterIter.Last(), mergingIter.Last() + }}, + {weight: 5, fn: func() (string, *Span, *Span) { + k := testkeys.Key(ks, rng.Int63n(ks.Count())) + return fmt.Sprintf("SeekGE(%q)", k), + fragmenterIter.SeekGE(k), + mergingIter.SeekGE(k) + }}, + {weight: 5, fn: func() (string, *Span, *Span) { + k := testkeys.Key(ks, rng.Int63n(ks.Count())) + return fmt.Sprintf("SeekLT(%q)", k), + fragmenterIter.SeekLT(k), + mergingIter.SeekLT(k) + }}, + {weight: 50, fn: func() (string, *Span, *Span) { + return "Next()", fragmenterIter.Next(), mergingIter.Next() + }}, + {weight: 50, fn: func() (string, *Span, *Span) { + return "Prev()", fragmenterIter.Prev(), mergingIter.Prev() + }}, + } + var totalWeight int + for _, op := range ops { + totalWeight += op.weight + } + + var fragmenterBuf bytes.Buffer + var mergingBuf bytes.Buffer + opCount := rng.Intn(200) + 50 + for i := 0; i < opCount; i++ { + p := rng.Intn(totalWeight) + opIndex := 0 + for i, op := range ops { + if p < op.weight { + opIndex = i + break + } + p -= op.weight + } + + opString, fs, ms := ops[opIndex].fn() + + fragmenterBuf.Reset() + mergingBuf.Reset() + fmt.Fprint(&fragmenterBuf, fs) + fmt.Fprint(&mergingBuf, ms) + if fragmenterBuf.String() != mergingBuf.String() { + t.Fatalf("seed %d, op %d: %s = %s, fragmenter iterator returned %s", + seed, i, opString, mergingBuf.String(), fragmenterBuf.String()) + } + t.Logf("op %d: %s = %s", i, opString, fragmenterBuf.String()) + } +} diff --git a/pebble/internal/keyspan/seek.go b/pebble/internal/keyspan/seek.go new file mode 100644 index 0000000..efcf682 --- /dev/null +++ b/pebble/internal/keyspan/seek.go @@ -0,0 +1,48 @@ +// Copyright 2018 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" + +// SeekLE seeks to the span that contains or is before the target key. +func SeekLE(cmp base.Compare, iter FragmentIterator, key []byte) *Span { + // NB: We use SeekLT in order to land on the proper span for a search + // key that resides in the middle of a span. Consider the scenario: + // + // a---e + // e---i + // + // The spans are indexed by their start keys `a` and `e`. If the + // search key is `c` we want to land on the span [a,e). If we were to + // use SeekGE then the search key `c` would land on the span [e,i) and + // we'd have to backtrack. The one complexity here is what happens for the + // search key `e`. In that case SeekLT will land us on the span [a,e) + // and we'll have to move forward. + iterSpan := iter.SeekLT(key) + + if iterSpan == nil { + // Advance the iterator once to see if the next span has a start key + // equal to key. + iterSpan = iter.Next() + if iterSpan == nil || cmp(key, iterSpan.Start) < 0 { + // The iterator is exhausted or we've hit the next span. + return nil + } + } else { + // Invariant: key > iterSpan.Start + if cmp(key, iterSpan.End) >= 0 { + // The current span lies entirely before the search key. Check to see if + // the next span contains the search key. If it doesn't, we'll backup + // and return to our earlier candidate. + iterSpan = iter.Next() + if iterSpan == nil || cmp(key, iterSpan.Start) < 0 { + // The next span is past our search key or there is no next span. Go + // back. + iterSpan = iter.Prev() + } + } + } + return iterSpan +} diff --git a/pebble/internal/keyspan/seek_test.go b/pebble/internal/keyspan/seek_test.go new file mode 100644 index 0000000..aa1d643 --- /dev/null +++ b/pebble/internal/keyspan/seek_test.go @@ -0,0 +1,63 @@ +// Copyright 2018 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 ( + "bytes" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" +) + +func TestSeek(t *testing.T) { + cmp := base.DefaultComparer.Compare + fmtKey := base.DefaultComparer.FormatKey + var iter FragmentIterator + var buf bytes.Buffer + + datadriven.RunTest(t, "testdata/seek", func(t *testing.T, d *datadriven.TestData) string { + buf.Reset() + switch d.Cmd { + case "build": + spans := buildSpans(t, cmp, fmtKey, d.Input, base.InternalKeyKindRangeDelete) + for _, s := range spans { + fmt.Fprintln(&buf, s) + } + iter = NewIter(cmp, spans) + return buf.String() + case "seek-ge", "seek-le": + seek := SeekLE + if d.Cmd == "seek-ge" { + seek = func(_ base.Compare, iter FragmentIterator, key []byte) *Span { + return iter.SeekGE(key) + } + } + + for _, line := range strings.Split(d.Input, "\n") { + parts := strings.Fields(line) + if len(parts) != 2 { + return fmt.Sprintf("malformed input: %s", line) + } + seq, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return err.Error() + } + span := seek(cmp, iter, []byte(parts[0])) + if span != nil { + visible := span.Visible(seq) + span = &visible + } + fmt.Fprintln(&buf, span) + } + return buf.String() + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} diff --git a/pebble/internal/keyspan/span.go b/pebble/internal/keyspan/span.go new file mode 100644 index 0000000..257b373 --- /dev/null +++ b/pebble/internal/keyspan/span.go @@ -0,0 +1,467 @@ +// Copyright 2018 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/keyspan" + +import ( + "bytes" + "fmt" + "sort" + "strconv" + "strings" + "unicode" + + "github.com/cockroachdb/pebble/internal/base" +) + +// Span represents a set of keys over a span of user key space. All of the keys +// within a Span are applied across the span's key span indicated by Start and +// End. Each internal key applied over the user key span appears as a separate +// Key, with its own kind and sequence number. Optionally, each Key may also +// have a Suffix and/or Value. +// +// Note that the start user key is inclusive and the end user key is exclusive. +// +// Currently the only supported key kinds are: +// +// RANGEDEL, RANGEKEYSET, RANGEKEYUNSET, RANGEKEYDEL. +type Span struct { + // Start and End encode the user key range of all the contained items, with + // an inclusive start key and exclusive end key. Both Start and End must be + // non-nil, or both nil if representing an invalid Span. + Start, End []byte + // Keys holds the set of keys applied over the [Start, End) user key range. + // Keys is sorted by (SeqNum, Kind) descending, unless otherwise specified + // by the context. If SeqNum and Kind are equal, the order of Keys is + // undefined. Keys may be empty, even if Start and End are non-nil. + // + // Keys are a decoded representation of the internal keys stored in batches + // or sstable blocks. A single internal key in a range key block may produce + // several decoded Keys. + Keys []Key + KeysOrder KeysOrder +} + +// KeysOrder describes the ordering of Keys within a Span. +type KeysOrder int8 + +const ( + // ByTrailerDesc indicates a Span's keys are sorted by Trailer descending. + // This is the default ordering, and the ordering used during physical + // storage. + ByTrailerDesc KeysOrder = iota + // BySuffixAsc indicates a Span's keys are sorted by Suffix ascending. This + // ordering is used during user iteration of range keys. + BySuffixAsc +) + +// Key represents a single key applied over a span of user keys. A Key is +// contained by a Span which specifies the span of user keys over which the Key +// is applied. +type Key struct { + // Trailer contains the key kind and sequence number. + Trailer uint64 + // Suffix holds an optional suffix associated with the key. This is only + // non-nil for RANGEKEYSET and RANGEKEYUNSET keys. + Suffix []byte + // Value holds a logical value associated with the Key. It is NOT the + // internal value stored in a range key or range deletion block. This is + // only non-nil for RANGEKEYSET keys. + Value []byte +} + +// SeqNum returns the sequence number component of the key. +func (k Key) SeqNum() uint64 { + return k.Trailer >> 8 +} + +// VisibleAt returns true if the provided key is visible at the provided +// snapshot sequence number. It interprets batch sequence numbers as always +// visible, because non-visible batch span keys are filtered when they're +// fragmented. +func (k Key) VisibleAt(snapshot uint64) bool { + seq := k.SeqNum() + return seq < snapshot || seq&base.InternalKeySeqNumBatch != 0 +} + +// Kind returns the kind component of the key. +func (k Key) Kind() base.InternalKeyKind { + return base.InternalKeyKind(k.Trailer & 0xff) +} + +// Equal returns true if this Key is equal to the given key. Two keys are said +// to be equal if the two Keys have equal trailers, suffix and value. Suffix +// comparison uses the provided base.Compare func. Value comparison is bytewise. +func (k Key) Equal(equal base.Equal, b Key) bool { + return k.Trailer == b.Trailer && + equal(k.Suffix, b.Suffix) && + bytes.Equal(k.Value, b.Value) +} + +// Valid returns true if the span is defined. +func (s *Span) Valid() bool { + return s.Start != nil && s.End != nil +} + +// Empty returns true if the span does not contain any keys. An empty span may +// still be Valid. A non-empty span must be Valid. +// +// An Empty span may be produced by Visible, or be produced by iterators in +// order to surface the gaps between keys. +func (s *Span) Empty() bool { + return s == nil || len(s.Keys) == 0 +} + +// SmallestKey returns the smallest internal key defined by the span's keys. +// It requires the Span's keys be in ByTrailerDesc order. It panics if the span +// contains no keys or its keys are sorted in a different order. +func (s *Span) SmallestKey() base.InternalKey { + if len(s.Keys) == 0 { + panic("pebble: Span contains no keys") + } else if s.KeysOrder != ByTrailerDesc { + panic("pebble: span's keys unexpectedly not in trailer order") + } + // The first key has the highest (sequence number,kind) tuple. + return base.InternalKey{ + UserKey: s.Start, + Trailer: s.Keys[0].Trailer, + } +} + +// LargestKey returns the largest internal key defined by the span's keys. The +// returned key will always be a "sentinel key" at the end boundary. The +// "sentinel key" models the exclusive end boundary by returning an InternalKey +// with the maximal sequence number, ensuring all InternalKeys with the same +// user key sort after the sentinel key. +// +// It requires the Span's keys be in ByTrailerDesc order. It panics if the span +// contains no keys or its keys are sorted in a different order. +func (s *Span) LargestKey() base.InternalKey { + if len(s.Keys) == 0 { + panic("pebble: Span contains no keys") + } else if s.KeysOrder != ByTrailerDesc { + panic("pebble: span's keys unexpectedly not in trailer order") + } + // The last key has the lowest (sequence number,kind) tuple. + kind := s.Keys[len(s.Keys)-1].Kind() + return base.MakeExclusiveSentinelKey(kind, s.End) +} + +// SmallestSeqNum returns the smallest sequence number of a key contained within +// the span. It requires the Span's keys be in ByTrailerDesc order. It panics if +// the span contains no keys or its keys are sorted in a different order. +func (s *Span) SmallestSeqNum() uint64 { + if len(s.Keys) == 0 { + panic("pebble: Span contains no keys") + } else if s.KeysOrder != ByTrailerDesc { + panic("pebble: span's keys unexpectedly not in trailer order") + } + + return s.Keys[len(s.Keys)-1].SeqNum() +} + +// LargestSeqNum returns the largest sequence number of a key contained within +// the span. It requires the Span's keys be in ByTrailerDesc order. It panics if +// the span contains no keys or its keys are sorted in a different order. +func (s *Span) LargestSeqNum() uint64 { + if len(s.Keys) == 0 { + panic("pebble: Span contains no keys") + } else if s.KeysOrder != ByTrailerDesc { + panic("pebble: span's keys unexpectedly not in trailer order") + } + return s.Keys[0].SeqNum() +} + +// TODO(jackson): Replace most of the calls to Visible with more targeted calls +// that avoid the need to construct a new Span. + +// Visible returns a span with the subset of keys visible at the provided +// sequence number. It requires the Span's keys be in ByTrailerDesc order. It +// panics if the span's keys are sorted in a different order. +// +// Visible may incur an allocation, so callers should prefer targeted, +// non-allocating methods when possible. +func (s Span) Visible(snapshot uint64) Span { + if s.KeysOrder != ByTrailerDesc { + panic("pebble: span's keys unexpectedly not in trailer order") + } + + ret := Span{Start: s.Start, End: s.End} + if len(s.Keys) == 0 { + return ret + } + + // Keys from indexed batches may force an allocation. The Keys slice is + // ordered by sequence number, so ordinarily we can return the trailing + // subslice containing keys with sequence numbers less than `seqNum`. + // + // However, batch keys are special. Only visible batch keys are included + // when an Iterator's batch spans are fragmented. They must always be + // visible. + // + // Batch keys can create a sandwich of visible batch keys at the beginning + // of the slice and visible committed keys at the end of the slice, forcing + // us to allocate a new slice and copy the contents. + // + // Care is taking to only incur an allocation only when batch keys and + // visible keys actually sandwich non-visible keys. + + // lastBatchIdx and lastNonVisibleIdx are set to the last index of a batch + // key and a non-visible key respectively. + lastBatchIdx := -1 + lastNonVisibleIdx := -1 + for i := range s.Keys { + if seqNum := s.Keys[i].SeqNum(); seqNum&base.InternalKeySeqNumBatch != 0 { + // Batch key. Always visible. + lastBatchIdx = i + } else if seqNum >= snapshot { + // This key is not visible. + lastNonVisibleIdx = i + } + } + + // In the following comments: b = batch, h = hidden, v = visible (committed). + switch { + case lastNonVisibleIdx == -1: + // All keys are visible. + // + // [b b b], [v v v] and [b b b v v v] + ret.Keys = s.Keys + case lastBatchIdx == -1: + // There are no batch keys, so we can return the continuous subslice + // starting after the last non-visible Key. + // + // h h h [v v v] + ret.Keys = s.Keys[lastNonVisibleIdx+1:] + case lastNonVisibleIdx == len(s.Keys)-1: + // While we have a batch key and non-visible keys, there are no + // committed visible keys. The 'sandwich' is missing the bottom layer, + // so we can return the continuous sublice at the beginning. + // + // [b b b] h h h + ret.Keys = s.Keys[0 : lastBatchIdx+1] + default: + // This is the problematic sandwich case. Allocate a new slice, copying + // the batch keys and the visible keys into it. + // + // [b b b] h h h [v v v] + ret.Keys = make([]Key, (lastBatchIdx+1)+(len(s.Keys)-lastNonVisibleIdx-1)) + copy(ret.Keys, s.Keys[:lastBatchIdx+1]) + copy(ret.Keys[lastBatchIdx+1:], s.Keys[lastNonVisibleIdx+1:]) + } + return ret +} + +// VisibleAt returns true if the span contains a key visible at the provided +// snapshot. Keys with sequence numbers with the batch bit set are treated as +// always visible. +// +// VisibleAt requires the Span's keys be in ByTrailerDesc order. It panics if +// the span's keys are sorted in a different order. +func (s *Span) VisibleAt(snapshot uint64) bool { + if s.KeysOrder != ByTrailerDesc { + panic("pebble: span's keys unexpectedly not in trailer order") + } + if len(s.Keys) == 0 { + return false + } else if first := s.Keys[0].SeqNum(); first&base.InternalKeySeqNumBatch != 0 { + // Only visible batch keys are included when an Iterator's batch spans + // are fragmented. They must always be visible. + return true + } else { + // Otherwise we check the last key. Since keys are ordered decreasing in + // sequence number, the last key has the lowest sequence number of any + // of the span's keys. If any of the keys are visible, the last key must + // be visible. Or put differently: if the last key is not visible, then + // no key is visible. + return s.Keys[len(s.Keys)-1].SeqNum() < snapshot + } +} + +// ShallowClone returns the span with a Keys slice owned by the span itself. +// None of the key byte slices are cloned (see Span.DeepClone). +func (s *Span) ShallowClone() Span { + c := Span{ + Start: s.Start, + End: s.End, + Keys: make([]Key, len(s.Keys)), + KeysOrder: s.KeysOrder, + } + copy(c.Keys, s.Keys) + return c +} + +// DeepClone clones the span, creating copies of all contained slices. DeepClone +// is intended for non-production code paths like tests, the level checker, etc +// because it is allocation heavy. +func (s *Span) DeepClone() Span { + c := Span{ + Start: make([]byte, len(s.Start)), + End: make([]byte, len(s.End)), + Keys: make([]Key, len(s.Keys)), + KeysOrder: s.KeysOrder, + } + copy(c.Start, s.Start) + copy(c.End, s.End) + for i := range s.Keys { + c.Keys[i].Trailer = s.Keys[i].Trailer + if len(s.Keys[i].Suffix) > 0 { + c.Keys[i].Suffix = make([]byte, len(s.Keys[i].Suffix)) + copy(c.Keys[i].Suffix, s.Keys[i].Suffix) + } + if len(s.Keys[i].Value) > 0 { + c.Keys[i].Value = make([]byte, len(s.Keys[i].Value)) + copy(c.Keys[i].Value, s.Keys[i].Value) + } + } + return c +} + +// Contains returns true if the specified key resides within the span's bounds. +func (s *Span) Contains(cmp base.Compare, key []byte) bool { + return cmp(s.Start, key) <= 0 && cmp(key, s.End) < 0 +} + +// Covers returns true if the span covers keys at seqNum. +// +// Covers requires the Span's keys be in ByTrailerDesc order. It panics if the +// span's keys are sorted in a different order. +func (s Span) Covers(seqNum uint64) bool { + if s.KeysOrder != ByTrailerDesc { + panic("pebble: span's keys unexpectedly not in trailer order") + } + return !s.Empty() && s.Keys[0].SeqNum() > seqNum +} + +// CoversAt returns true if the span contains a key that is visible at the +// provided snapshot sequence number, and that key's sequence number is higher +// than seqNum. +// +// Keys with sequence numbers with the batch bit set are treated as always +// visible. +// +// CoversAt requires the Span's keys be in ByTrailerDesc order. It panics if the +// span's keys are sorted in a different order. +func (s *Span) CoversAt(snapshot, seqNum uint64) bool { + if s.KeysOrder != ByTrailerDesc { + panic("pebble: span's keys unexpectedly not in trailer order") + } + // NB: A key is visible at `snapshot` if its sequence number is strictly + // less than `snapshot`. See base.Visible. + for i := range s.Keys { + if kseq := s.Keys[i].SeqNum(); kseq&base.InternalKeySeqNumBatch != 0 { + // Only visible batch keys are included when an Iterator's batch spans + // are fragmented. They must always be visible. + return kseq > seqNum + } else if kseq < snapshot { + return kseq > seqNum + } + } + return false +} + +// String returns a string representation of the span. +func (s Span) String() string { + return fmt.Sprint(prettySpan{Span: s, formatKey: base.DefaultFormatter}) +} + +// Pretty returns a formatter for the span. +func (s Span) Pretty(f base.FormatKey) fmt.Formatter { + // TODO(jackson): Take a base.FormatValue to format Key.Value too. + return prettySpan{s, f} +} + +type prettySpan struct { + Span + formatKey base.FormatKey +} + +func (s prettySpan) Format(fs fmt.State, c rune) { + if !s.Valid() { + fmt.Fprintf(fs, "") + return + } + fmt.Fprintf(fs, "%s-%s:{", s.formatKey(s.Start), s.formatKey(s.End)) + for i, k := range s.Keys { + if i > 0 { + fmt.Fprint(fs, " ") + } + fmt.Fprintf(fs, "(#%d,%s", k.SeqNum(), k.Kind()) + if len(k.Suffix) > 0 || len(k.Value) > 0 { + fmt.Fprintf(fs, ",%s", k.Suffix) + } + if len(k.Value) > 0 { + fmt.Fprintf(fs, ",%s", k.Value) + } + fmt.Fprint(fs, ")") + } + fmt.Fprintf(fs, "}") +} + +// SortKeysByTrailer sorts a keys slice by trailer. +func SortKeysByTrailer(keys *[]Key) { + // NB: keys is a pointer to a slice instead of a slice to avoid `sorted` + // escaping to the heap. + sorted := (*keysBySeqNumKind)(keys) + sort.Sort(sorted) +} + +// KeysBySuffix implements sort.Interface, sorting its member Keys slice to by +// Suffix in the order dictated by Cmp. +type KeysBySuffix struct { + Cmp base.Compare + Keys []Key +} + +func (s *KeysBySuffix) Len() int { return len(s.Keys) } +func (s *KeysBySuffix) Less(i, j int) bool { return s.Cmp(s.Keys[i].Suffix, s.Keys[j].Suffix) < 0 } +func (s *KeysBySuffix) Swap(i, j int) { s.Keys[i], s.Keys[j] = s.Keys[j], s.Keys[i] } + +// ParseSpan parses the string representation of a Span. It's intended for +// tests. ParseSpan panics if passed a malformed span representation. +func ParseSpan(input string) Span { + var s Span + parts := strings.FieldsFunc(input, func(r rune) bool { + switch r { + case '-', ':', '{', '}': + return true + default: + return unicode.IsSpace(r) + } + }) + s.Start, s.End = []byte(parts[0]), []byte(parts[1]) + + // Each of the remaining parts represents a single Key. + s.Keys = make([]Key, 0, len(parts)-2) + for _, p := range parts[2:] { + keyFields := strings.FieldsFunc(p, func(r rune) bool { + switch r { + case '#', ',', '(', ')': + return true + default: + return unicode.IsSpace(r) + } + }) + + var k Key + // Parse the sequence number. + seqNum, err := strconv.ParseUint(keyFields[0], 10, 64) + if err != nil { + panic(fmt.Sprintf("invalid sequence number: %q: %s", keyFields[0], err)) + } + // Parse the key kind. + kind := base.ParseKind(keyFields[1]) + k.Trailer = base.MakeTrailer(seqNum, kind) + // Parse the optional suffix. + if len(keyFields) >= 3 { + k.Suffix = []byte(keyFields[2]) + } + // Parse the optional value. + if len(keyFields) >= 4 { + k.Value = []byte(keyFields[3]) + } + s.Keys = append(s.Keys, k) + } + return s +} diff --git a/pebble/internal/keyspan/span_test.go b/pebble/internal/keyspan/span_test.go new file mode 100644 index 0000000..29651fb --- /dev/null +++ b/pebble/internal/keyspan/span_test.go @@ -0,0 +1,98 @@ +// 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 keyspan + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/stretchr/testify/require" +) + +// TODO(jackson): Add unit tests for all of the various Span methods. + +func TestSpan_ParseRoundtrip(t *testing.T) { + spans := []string{ + "a-c:{(#5,RANGEDEL)}", + "a-c:{(#5,RANGEDEL) (#2,RANGEDEL)}", + "h-z:{(#20,RANGEKEYSET,@5,foo) (#15,RANGEKEYUNSET,@9) (#2,RANGEKEYDEL)}", + } + for _, input := range spans { + got := ParseSpan(input).String() + if got != input { + t.Errorf("ParseSpan(%q).String() = %q", input, got) + } + } +} + +func TestSpan_Visible(t *testing.T) { + var s Span + datadriven.RunTest(t, "testdata/visible", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + s = ParseSpan(d.Input) + return fmt.Sprint(s) + case "visible": + var buf bytes.Buffer + for _, line := range strings.Split(d.Input, "\n") { + snapshot, err := strconv.ParseUint(line, 10, 64) + require.NoError(t, err) + fmt.Fprintf(&buf, "%-2d: %s\n", snapshot, s.Visible(snapshot)) + } + return buf.String() + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestSpan_VisibleAt(t *testing.T) { + var s Span + datadriven.RunTest(t, "testdata/visible_at", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + s = ParseSpan(d.Input) + return fmt.Sprint(s) + case "visible-at": + var buf bytes.Buffer + for _, line := range strings.Split(d.Input, "\n") { + snapshot, err := strconv.ParseUint(line, 10, 64) + require.NoError(t, err) + fmt.Fprintf(&buf, "%-2d: %t\n", snapshot, s.VisibleAt(snapshot)) + } + return buf.String() + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestSpan_CoversAt(t *testing.T) { + var s Span + datadriven.RunTest(t, "testdata/covers_at", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + s = ParseSpan(d.Input) + return fmt.Sprint(s) + case "covers-at": + var buf bytes.Buffer + for _, line := range strings.Split(d.Input, "\n") { + fields := strings.Fields(line) + snapshot, err := strconv.ParseUint(fields[0], 10, 64) + require.NoError(t, err) + seqNum, err := strconv.ParseUint(fields[1], 10, 64) + require.NoError(t, err) + fmt.Fprintf(&buf, "%d %d : %t\n", snapshot, seqNum, s.CoversAt(snapshot, seqNum)) + } + return buf.String() + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} diff --git a/pebble/internal/keyspan/testdata/bounded_iter b/pebble/internal/keyspan/testdata/bounded_iter new file mode 100644 index 0000000..8532f62 --- /dev/null +++ b/pebble/internal/keyspan/testdata/bounded_iter @@ -0,0 +1,251 @@ +define +a-b:{(#10,RANGEKEYSET,@5,apples)} +d-e:{(#4,RANGEKEYSET,@3,coconut)} +g-h:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +---- + +# Nothing out of bounds. + +iter lower=a upper=z +first +next +next +next +last +prev +prev +prev +---- +a-b:{(#10,RANGEKEYSET,@5,apples)} +d-e:{(#4,RANGEKEYSET,@3,coconut)} +g-h:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} + +g-h:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +d-e:{(#4,RANGEKEYSET,@3,coconut)} +a-b:{(#10,RANGEKEYSET,@5,apples)} + + +# Test out of upper bound, but undiscovered until we Next. + +iter lower=a upper=f +first +next +next +prev +---- +a-b:{(#10,RANGEKEYSET,@5,apples)} +d-e:{(#4,RANGEKEYSET,@3,coconut)} + +d-e:{(#4,RANGEKEYSET,@3,coconut)} + +# Test out of upper bound, but discovered before we Next. + +iter lower=a upper=dog +first +next +next +prev +---- +a-b:{(#10,RANGEKEYSET,@5,apples)} +d-e:{(#4,RANGEKEYSET,@3,coconut)} + +d-e:{(#4,RANGEKEYSET,@3,coconut)} + +# Test out of lower bound, but undiscovered until we Prev. + +iter lower=c upper=z +last +prev +prev +next +---- +g-h:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +d-e:{(#4,RANGEKEYSET,@3,coconut)} + +d-e:{(#4,RANGEKEYSET,@3,coconut)} + +# Test out of lower bound, but discovered before we Prev. + +iter lower=d upper=z +last +prev +prev +next +---- +g-h:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +d-e:{(#4,RANGEKEYSET,@3,coconut)} + +d-e:{(#4,RANGEKEYSET,@3,coconut)} + +# Test a single span ([b-g)) within the bounds, overlapping on both ends. + +define +a-b:{(#10,RANGEKEYSET,@5)} +b-g:{(#4,RANGEKEYSET,@3)} +g-h:{(#20,RANGEKEYSET,@5)} +---- + +iter lower=c upper=f +seek-ge b +next +next +seek-ge b +prev +prev +seek-lt f +prev +prev +seek-lt f +next +next +prev +prev +---- +b-g:{(#4,RANGEKEYSET,@3)} + + +b-g:{(#4,RANGEKEYSET,@3)} + + +b-g:{(#4,RANGEKEYSET,@3)} + + +b-g:{(#4,RANGEKEYSET,@3)} + + +b-g:{(#4,RANGEKEYSET,@3)} + + +set-prefix bar +---- +set prefix to "bar" + +# Test seeking to a portion of the keyspace that contains no range keys with +# start bounds ≥ the seek key such that the range key also overlaps the current +# prefix. + +iter lower=a upper=z +seek-ge bar +prev +prev +---- +b-g:{(#4,RANGEKEYSET,@3)} + + + +# Test seeking to a portion of the keyspace that contains a range key with a +# start bound < the seek key, and the range key also overlaps the current +# prefix. + +iter lower=a upper=z +seek-lt bar +next +prev +prev +---- +b-g:{(#4,RANGEKEYSET,@3)} + +b-g:{(#4,RANGEKEYSET,@3)} + + +# Test seeking with bounds narrower than the range of the seek prefix. This is +# possible in practice because the bounded iterator iterates over fragments, not +# pre-defragmented range keys. + +iter lower=bar@9 upper=bar@3 +seek-lt bar +next +prev +prev +---- +b-g:{(#4,RANGEKEYSET,@3)} + +b-g:{(#4,RANGEKEYSET,@3)} + + +# Test a similar scenario but on the start prefix of a key. + +iter lower=b@9 upper=b@3 +seek-lt b +next +next +prev +prev +---- + +b-g:{(#4,RANGEKEYSET,@3)} + +b-g:{(#4,RANGEKEYSET,@3)} + + +# Test a scenario where the prefix overlaps a span, but the bounds exclude it. + +iter lower=z@9 upper=z@3 +seek-lt z@3 +next +---- + + + +# Test many spans matching the prefix, due to fragmentation within a prefix. + +define +b-boo:{(#1,RANGEKEYSET,@1)} +c@9-c@8:{(#1,RANGEKEYSET,@1)} +c@8-c@7:{(#1,RANGEKEYSET,@1)} +c@7-c@6:{(#1,RANGEKEYSET,@1)} +c@6-c@5:{(#1,RANGEKEYSET,@1)} +c@5-c@4:{(#1,RANGEKEYSET,@1)} +---- + +set-prefix c +---- +set prefix to "c" + +iter +seek-lt c +next +next +next +next +next +next +---- + +c@9-c@8:{(#1,RANGEKEYSET,@1)} +c@8-c@7:{(#1,RANGEKEYSET,@1)} +c@7-c@6:{(#1,RANGEKEYSET,@1)} +c@6-c@5:{(#1,RANGEKEYSET,@1)} +c@5-c@4:{(#1,RANGEKEYSET,@1)} + + +# Test the same scenario with bounds limiting iteration to a subset of the +# keys. + +iter lower=c@7 upper=c@5 +seek-lt c@7 +next +next +next +---- + +c@7-c@6:{(#1,RANGEKEYSET,@1)} +c@6-c@5:{(#1,RANGEKEYSET,@1)} + + +define +a@7-a@5:{(#1,RANGEKEYSET,@1)} +b-boo:{(#1,RANGEKEYSET,@1)} +c@9-c@8:{(#1,RANGEKEYSET,@1)} +---- + +set-prefix b +---- +set prefix to "b" + +iter +seek-lt c@8 +seek-ge a@9 +---- + + diff --git a/pebble/internal/keyspan/testdata/covers_at b/pebble/internal/keyspan/testdata/covers_at new file mode 100644 index 0000000..c32f7ed --- /dev/null +++ b/pebble/internal/keyspan/testdata/covers_at @@ -0,0 +1,91 @@ +define +a-b:{(#5,RANGEDEL) (#3,RANGEDEL)} +---- +a-b:{(#5,RANGEDEL) (#3,RANGEDEL)} + +covers-at +6 6 +6 5 +6 4 +6 2 +6 3 +5 5 +5 4 +5 3 +5 2 +4 5 +4 1 +3 9 +3 2 +3 1 +3 0 +2 0 +1 0 +---- +6 6 : false +6 5 : false +6 4 : true +6 2 : true +6 3 : true +5 5 : false +5 4 : false +5 3 : false +5 2 : true +4 5 : false +4 1 : true +3 9 : false +3 2 : false +3 1 : false +3 0 : false +2 0 : false +1 0 : false + +# The below sequence number is the minimal batch sequence number (eg, a RANGEDEL +# written right at the beginning of the batch.) In the tests below, all other +# batch sequence numbers are not covered by it. + +define +a-c:{(#36028797018963968,RANGEDEL)} +---- +a-c:{(#36028797018963968,RANGEDEL)} + +covers-at +100 90000 +100 90 +0 0 +33 36028797018964068 +33 36028797018963968 +---- +100 90000 : true +100 90 : true +0 0 : true +33 36028797018964068 : false +33 36028797018963968 : false + +# The below sequence number is a batch sequence number for offset 100. + +define +a-c:{(#36028797018964068,RANGEDEL)} +---- +a-c:{(#36028797018964068,RANGEDEL)} + +covers-at +10 10 +---- +10 10 : true + +# The below sequence number is a batch sequence number for offset 200. It should +# not be covered. + +covers-at +100 36028797018964168 +---- +100 36028797018964168 : false + +# The below sequence number is a batch sequence number for offset 0. It should +# be covered. + +covers-at +100 36028797018963968 +---- +100 36028797018963968 : true diff --git a/pebble/internal/keyspan/testdata/defragmenting_iter b/pebble/internal/keyspan/testdata/defragmenting_iter new file mode 100644 index 0000000..f81ca9b --- /dev/null +++ b/pebble/internal/keyspan/testdata/defragmenting_iter @@ -0,0 +1,395 @@ +# Test a scenario that should NOT result in defragmentation. + +define +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +---- + +iter +first +next +next +last +prev +prev +---- +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} + +iter +first +next +next +next +last +prev +prev +prev +---- +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} + +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} + + +# Test a scenario that SHOULD result in internal defragmentation ([a,c) and +# [c,d) should be merged. + +define +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +d-e:{(#1,RANGEKEYSET,@3,bananas)} +---- + +iter +first +next +next +---- +a-d:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +d-e:{(#1,RANGEKEYSET,@3,bananas)} + + +# Test defragmenting in both directions at seek keys. + +define +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +f-h:{(#3,RANGEKEYSET,@3,bananas)} +h-p:{(#3,RANGEKEYSET,@3,bananas)} +p-t:{(#3,RANGEKEYSET,@3,bananas)} +---- + +iter +seekge b +prev +seekge b +next +seeklt d +next +seeklt d +prev +---- +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} + +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} + + +iter +seeklt d +next +prev +---- +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} + +# Test next-ing and prev-ing around seek keys. + +define +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +f-h:{(#3,RANGEKEYSET,@3,bananas)} +h-p:{(#3,RANGEKEYSET,@3,bananas)} +p-t:{(#3,RANGEKEYSET,@3,bananas)} +t-z:{(#4,RANGEKEYSET,@2,oranges)} +---- + +iter +seekge r +prev +next +next +---- +f-t:{(#3,RANGEKEYSET,@3,bananas)} +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +t-z:{(#4,RANGEKEYSET,@2,oranges)} + +iter +seekge f +seekge h +seekge p +seekge t +seekge u +seekge v +seekge z +---- +f-t:{(#3,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +t-z:{(#4,RANGEKEYSET,@2,oranges)} +t-z:{(#4,RANGEKEYSET,@2,oranges)} +t-z:{(#4,RANGEKEYSET,@2,oranges)} + + +iter +seeklt f +seeklt h +seeklt p +seeklt t +seeklt u +seeklt z +---- +a-f:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +f-t:{(#3,RANGEKEYSET,@3,bananas)} +t-z:{(#4,RANGEKEYSET,@2,oranges)} +t-z:{(#4,RANGEKEYSET,@2,oranges)} + +# Test iteration with a reducer that collects keys across all spans that +# constitute a defragmented span. Abutting spans are always combined. + +define +a-b:{(#3,RANGEDEL) (#2,RANGEDEL)} +b-c:{(#4,RANGEDEL) (#1,RANGEDEL)} +c-d:{(#5,RANGEDEL)} +e-f:{(#1,RANGEDEL)} +f-g:{(#2,RANGEDEL)} +---- + +iter equal=always reducer=collect +first +next +next +last +prev +prev +---- +a-d:{(#5,RANGEDEL) (#4,RANGEDEL) (#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +e-g:{(#2,RANGEDEL) (#1,RANGEDEL)} + +e-g:{(#2,RANGEDEL) (#1,RANGEDEL)} +a-d:{(#5,RANGEDEL) (#4,RANGEDEL) (#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} + + +# Test defragmentation of non-empty (i.e. more than one value) fragments, while +# empty fragments are left untouched. + +define +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +d-e:{} +e-f:{} +g-h:{(#1,RANGEKEYSET,@3,bananas)} +---- + +iter +first +next +next +next +next +---- +a-d:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +d-e:{} +e-f:{} +g-h:{(#1,RANGEKEYSET,@3,bananas)} + + +iter +last +prev +prev +prev +prev +---- +g-h:{(#1,RANGEKEYSET,@3,bananas)} +e-f:{} +d-e:{} +a-d:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} + + +iter +seekge d +next +prev +seekge e +next +prev +prev +prev +---- +d-e:{} +e-f:{} +d-e:{} +e-f:{} +g-h:{(#1,RANGEKEYSET,@3,bananas)} +e-f:{} +d-e:{} +a-d:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} + +iter +seeklt e +next +prev +seeklt f +next +prev +prev +prev +---- +d-e:{} +e-f:{} +d-e:{} +e-f:{} +g-h:{(#1,RANGEKEYSET,@3,bananas)} +e-f:{} +d-e:{} +a-d:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} + +# Test that the defragmenting iterator does yield errors in cases that do not +# need to defragment. + +define +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +---- + +iter probes=(ErrInjected) +seek-ge b +seek-lt d +first +last +---- + err= + err= + err= + err= + +# Next and Prev may only be called on positioned iterators, so to test +# propagation of errors on Next or Prev, we must use a probe that injects errors +# on Next or Prev but leaves seeks untouched. +# +# The situation is complicated by the fact that a seek on the defragmenting +# iterator will result in Next/Prevs on the embedded iterator (in order to peek +# ahead to see if anything needs to be defragmented). +# +# First we test the seeks too result in injected errors when they Next/Prev +# ahead to determine if there's anything to defragment. + +iter probes=((If (Or OpNext OpPrev) ErrInjected noop), (Log "# inner.")) +seek-ge b +next +seek-lt cat +prev +---- +# inner.SeekGE("b") = a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +# inner.Prev() = nil + err= +# inner.Next() = nil + err= +# inner.SeekLT("cat") = c-d:{(#4,RANGEKEYSET,@3,bananas)} +# inner.Next() = nil + err= +# inner.Prev() = nil + err= + +# Use a probe that injects errors whenever we otherwise would've returned the +# c-d span. First and Last calls should both return errors because during +# defragmenting they'll step the internal iterator on to the error position. + +iter probes=((If (Equal StartKey (Bytes "c")) ErrInjected noop), (Log "# inner.")) +first +last +---- +# inner.First() = a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +# inner.Next() = nil + err= +# inner.Last() = d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +# inner.Prev() = nil + err= + +# In order to test that errors are injected when Next-ing the top-level +# iterator, define test data that includes 5 spans. + +define +a-b:{(#3,RANGEKEYUNSET,@5)} +b-c:{(#4,RANGEKEYSET,@5,apples)} +c-d:{(#5,RANGEKEYSET,@3,bananas)} +d-e:{(#6,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +e-f:{(#4,RANGEKEYSET,@1,pineapple)} +---- + +# Use a probe that injects errors whenever we would've otherwise returned the +# c-d span. Our initial First/Last seeks should not step on to the error +# position and should not error. The subsequent Next/Prev however should. + +iter probes=((If (Equal StartKey (Bytes "c")) ErrInjected noop), (Log "# inner.")) +first +next +last +prev +---- +# inner.First() = a-b:{(#3,RANGEKEYUNSET,@5)} +# inner.Next() = b-c:{(#4,RANGEKEYSET,@5,apples)} +a-b:{(#3,RANGEKEYUNSET,@5)} +# inner.Next() = nil + err= +# inner.Last() = e-f:{(#4,RANGEKEYSET,@1,pineapple)} +# inner.Prev() = d-e:{(#6,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +e-f:{(#4,RANGEKEYSET,@1,pineapple)} +# inner.Prev() = nil + err= + +# When seeking, the defragmenting iterator needs to defragment in both +# directions. A forward seek first defragments in the reverse direction, and +# then in the forward direction. A backward seek does the inverse. If an error +# is encountered while performing the first defragment scan, it must be +# surfaced. +# +# To test this scenario we again inject errors instead of the c-d span. +# - The SeekGE('d') should land on d-e, try to defragment backward first and +# encounter the error. +# - The SeekLT('c') should land on b-c, try to defragment forward first and +# encounter the error. +iter probes=((If (Equal StartKey (Bytes "c")) ErrInjected noop), (Log "# inner.")) +seek-ge d +seek-lt c +---- +# inner.SeekGE("d") = d-e:{(#6,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +# inner.Prev() = nil + err= +# inner.SeekLT("c") = b-c:{(#4,RANGEKEYSET,@5,apples)} +# inner.Next() = nil + err= + +# When changing directions in some circumstances we step an iterator and then +# defragment twice; once to skip over the current span and once to construct the +# next defragmented span in the new iteration direction. If the first step of +# the iterator surfaces an error, ensure that it's still propagated. +iter probes=((If (And OpPrev (Equal StartKey (Bytes "c"))) ErrInjected noop), (Log "# inner.")) +seek-ge c +prev +---- +# inner.SeekGE("c") = c-d:{(#5,RANGEKEYSET,@3,bananas)} +# inner.Prev() = b-c:{(#4,RANGEKEYSET,@5,apples)} +# inner.Next() = c-d:{(#5,RANGEKEYSET,@3,bananas)} +# inner.Next() = d-e:{(#6,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +c-d:{(#5,RANGEKEYSET,@3,bananas)} +# inner.Prev() = nil + err= + +iter probes=((If (And OpNext (Equal StartKey (Bytes "c"))) ErrInjected noop), (Log "# inner.")) +seek-lt d +next +---- +# inner.SeekLT("d") = c-d:{(#5,RANGEKEYSET,@3,bananas)} +# inner.Next() = d-e:{(#6,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +# inner.Prev() = c-d:{(#5,RANGEKEYSET,@3,bananas)} +# inner.Prev() = b-c:{(#4,RANGEKEYSET,@5,apples)} +c-d:{(#5,RANGEKEYSET,@3,bananas)} +# inner.Next() = nil + err= diff --git a/pebble/internal/keyspan/testdata/filtering_iter b/pebble/internal/keyspan/testdata/filtering_iter new file mode 100644 index 0000000..3299254 --- /dev/null +++ b/pebble/internal/keyspan/testdata/filtering_iter @@ -0,0 +1,84 @@ +# The following filters are available: +# - no-op: passes through all spans. +# - key-kind-{set,unset,del}: filters keys in spans with the given key kind. + +define +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas) (#3,RANGEKEYDEL)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +---- + +iter filter=no-op +first +next +next +next +---- +a-c:{(#3,RANGEKEYUNSET,@5) (#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas) (#3,RANGEKEYDEL)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +. + +iter filter=key-kind-set +first +next +next +next +---- +a-c:{(#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +. + +iter filter=key-kind-set +last +prev +prev +prev +---- +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +a-c:{(#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +. + +iter filter=key-kind-set +seek-ge a +seek-ge c +next +seek-lt b +prev +next +seek-lt z +next +---- +a-c:{(#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +a-c:{(#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +. +a-c:{(#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} +. + +iter filter=key-kind-set +first +next +next +---- +a-c:{(#2,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,bananas)} +d-e:{(#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@1,pineapple)} + +iter filter=key-kind-unset +first +next +---- +a-c:{(#3,RANGEKEYUNSET,@5)} +. + +iter filter=key-kind-del +first +next +---- +c-d:{(#3,RANGEKEYDEL)} +. diff --git a/pebble/internal/keyspan/testdata/fragmenter b/pebble/internal/keyspan/testdata/fragmenter new file mode 100644 index 0000000..a064b00 --- /dev/null +++ b/pebble/internal/keyspan/testdata/fragmenter @@ -0,0 +1,951 @@ +build +3: a-----------m +2: f------------s +1: j---------------z +---- +3: a----f +3: f---j +2: f---j +3: j--m +2: j--m +1: j--m +2: m-----s +1: m-----s +1: s------z + +# Building is idempotent. +build +3: a----f +3: f---j +2: f---j +3: j--m +2: j--m +1: j--m +2: m-----s +1: m-----s +1: s------z +---- +3: a----f +3: f---j +2: f---j +3: j--m +2: j--m +1: j--m +2: m-----s +1: m-----s +1: s------z + +# An empty tombstone will not get emitted. +build +1: a-a +---- + +build +2: c-e +1: a-c +---- +pebble: keys must be added in order: c > a + +build +3: a-a +3: a-b +2: a-b +1: a-a +---- +3: ab +2: ab + +build +1: a---e +3: b-d +---- +1: ab +3: b-d +1: b-d +1: de + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive alive alive deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive deleted + +get t=2 +a#1 a#0 +---- +alive deleted + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive alive alive deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive deleted + +get t=2 +d#1 d#0 +---- +alive deleted + + +build +3: a---e +1: b-d +---- +3: ab +3: b-d +1: b-d +3: de + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive deleted deleted deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive alive + +get t=2 +a#1 a#0 +---- +alive alive + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive deleted deleted deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive alive + +get t=2 +d#1 d#0 +---- +alive alive + + +build +3: a--d +1: b--e +---- +3: ab +3: b-d +1: b-d +1: de + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive deleted deleted deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive alive + +get t=2 +a#1 a#0 +---- +alive alive + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive alive alive deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive deleted + +get t=2 +d#1 d#0 +---- +alive deleted + + +build +1: a--d +3: b--e +---- +1: ab +3: b-d +1: b-d +3: de + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive alive alive deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive deleted + +get t=2 +a#1 a#0 +---- +alive deleted + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive deleted deleted deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive alive + +get t=2 +d#1 d#0 +---- +alive alive + + +build +3: a--d +1: a---e +---- +3: a--d +1: a--d +1: de + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive deleted deleted deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive deleted + +get t=2 +a#1 a#0 +---- +alive deleted + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive alive alive deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive deleted + +get t=2 +d#1 d#0 +---- +alive deleted + + +build +3: a---e +1: a--d +---- +3: a--d +1: a--d +3: de + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive deleted deleted deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive deleted + +get t=2 +a#1 a#0 +---- +alive deleted + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive deleted deleted deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive alive + +get t=2 +d#1 d#0 +---- +alive alive + + +build +1: a---e +3: b--e +---- +1: ab +3: b--e +1: b--e + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive alive alive deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive deleted + +get t=2 +a#1 a#0 +---- +alive deleted + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive deleted deleted deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive deleted + +get t=2 +d#1 d#0 +---- +alive deleted + + +build +3: a---e +1: b--e +---- +3: ab +3: b--e +1: b--e + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive deleted deleted deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive alive + +get t=2 +a#1 a#0 +---- +alive alive + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive deleted deleted deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive deleted + +get t=2 +d#1 d#0 +---- +alive deleted + + +build +3: a---e +1: a---e +---- +3: a---e +1: a---e + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive deleted deleted deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive deleted + +get t=2 +a#1 a#0 +---- +alive deleted + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive deleted deleted deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive deleted + +get t=2 +d#1 d#0 +---- +alive deleted + + +build +1: a-c +3: c-e +---- +1: a-c +3: c-e + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive alive alive deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive deleted + +get t=2 +a#1 a#0 +---- +alive deleted + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive alive alive deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive deleted deleted deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive alive + +get t=2 +c#1 c#0 +---- +alive alive + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive deleted deleted deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive alive + +get t=2 +d#1 d#0 +---- +alive alive + + +build +3: a-c +1: c-e +---- +3: a-c +1: c-e + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive deleted deleted deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive alive + +get t=2 +a#1 a#0 +---- +alive alive + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive alive + +get t=2 +b#1 b#0 +---- +alive alive + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive alive alive deleted + +get t=3 +c#2 c#1 c#0 +---- +alive alive deleted + +get t=2 +c#1 c#0 +---- +alive deleted + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive alive alive deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive deleted + +get t=2 +d#1 d#0 +---- +alive deleted + + +build +1: a-c +3: de +---- +1: a-c +3: de + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive alive alive deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive deleted + +get t=2 +a#1 a#0 +---- +alive deleted + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive alive alive deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive deleted + +get t=2 +b#1 b#0 +---- +alive deleted + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive alive alive alive + +get t=3 +c#2 c#1 c#0 +---- +alive alive alive + +get t=2 +c#1 c#0 +---- +alive alive + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive deleted deleted deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive alive + +get t=2 +d#1 d#0 +---- +alive alive + + +build +3: a-c +1: de +---- +3: a-c +1: de + +get t=4 +a#3 a#2 a#1 a#0 +---- +alive deleted deleted deleted + +get t=3 +a#2 a#1 a#0 +---- +alive alive alive + +get t=2 +a#1 a#0 +---- +alive alive + +get t=4 +b#3 b#2 b#1 b#0 +---- +alive deleted deleted deleted + +get t=3 +b#2 b#1 b#0 +---- +alive alive alive + +get t=2 +b#1 b#0 +---- +alive alive + +get t=4 +c#3 c#2 c#1 c#0 +---- +alive alive alive alive + +get t=3 +c#2 c#1 c#0 +---- +alive alive alive + +get t=2 +c#1 c#0 +---- +alive alive + +get t=4 +d#3 d#2 d#1 d#0 +---- +alive alive alive deleted + +get t=3 +d#2 d#1 d#0 +---- +alive alive deleted + +get t=2 +d#1 d#0 +---- +alive deleted diff --git a/pebble/internal/keyspan/testdata/fragmenter_covers b/pebble/internal/keyspan/testdata/fragmenter_covers new file mode 100644 index 0000000..abd505d --- /dev/null +++ b/pebble/internal/keyspan/testdata/fragmenter_covers @@ -0,0 +1,58 @@ +# This datadriven test uses a single command 'build' that illustrates a sequence +# of calls to a fragmenter. +# +# 'add' lines add a new span with the provided sequence number and the provided +# bounds. 'add' outputs nothing. +# +# 'deleted' lines test whether the provided key is deleted by a RANGEDEL in the +# fragmenter when read at the trailing snapshot sequence number. + +build +deleted a.SET.0 5 +add 3: a-----------m +deleted a.SET.0 5 +deleted a.SET.1 5 +deleted a.SET.1 2 +deleted a.SET.2 5 +deleted a.SET.3 5 +deleted l.SET.3 5 +add 2: f------------s +deleted e.SET.3 5 +deleted f.SET.2 5 +deleted l.SET.2 5 +deleted m.SET.2 5 +add 1: j---------------z +deleted j.SET.1 5 +deleted j.SET.1 1 +deleted j.SET.2 5 +deleted j.SET.3 5 +deleted l.SET.2 5 +deleted m.SET.2 5 +deleted r.SET.1 5 +deleted r.SET.1 1 +deleted s.SET.1 5 +deleted y.SET.0 5 +deleted z.SET.0 5 +---- +a#0,1: none +a#0,1: visibly +a#1,1: visibly +a#1,1: invisibly +a#2,1: visibly +a#3,1: none +l#3,1: none +e#3,1: pebble: keys must be in order: f > e#3,SET +f#2,1: visibly +l#2,1: visibly +m#2,1: none +j#1,1: visibly +j#1,1: invisibly +j#2,1: visibly +j#3,1: none +l#2,1: visibly +m#2,1: none +r#1,1: visibly +r#1,1: invisibly +s#1,1: none +y#0,1: visibly +z#0,1: none diff --git a/pebble/internal/keyspan/testdata/fragmenter_emit_order b/pebble/internal/keyspan/testdata/fragmenter_emit_order new file mode 100644 index 0000000..9af2c42 --- /dev/null +++ b/pebble/internal/keyspan/testdata/fragmenter_emit_order @@ -0,0 +1,21 @@ +build +a.RANGEKEYSET.5 b +a.RANGEKEYSET.4 b +a.RANGEKEYUNSET.6 b +---- +a b: #6,RANGEKEYUNSET, #5,RANGEKEYSET, #4,RANGEKEYSET +- + +# Test that keys emitted together that share the same sequence number are +# ordered by key kind, descending. +# NB: RANGEKEYSET > RANGEKEYUNSET > RANGEKEYDEL + +build +b.RANGEKEYSET.5 c +b.RANGEKEYUNSET.5 d +b.RANGEKEYDEL.5 c +---- +b c: #5,RANGEKEYSET, #5,RANGEKEYUNSET, #5,RANGEKEYDEL +- +c d: #5,RANGEKEYUNSET +- diff --git a/pebble/internal/keyspan/testdata/fragmenter_truncate_and_flush_to b/pebble/internal/keyspan/testdata/fragmenter_truncate_and_flush_to new file mode 100644 index 0000000..9b7ecce --- /dev/null +++ b/pebble/internal/keyspan/testdata/fragmenter_truncate_and_flush_to @@ -0,0 +1,113 @@ +build +2: a--c +1: b--d +truncate-and-flush-to c +---- +2: ab +2: bc +1: bc +1: cd + +build +truncate-and-flush-to c +1: b--d +---- +pebble: start key (b) < flushed key (c) + +build +truncate-and-flush-to c +truncate-and-flush-to b +---- +pebble: start key (b) < flushed key (c) + +# Call out of order + +build +3: a--d +2: d--g +truncate-and-flush-to c +---- +pebble: start key (c) < flushed key (d) + +build +3: a--d +truncate-and-flush-to a +---- +3: a--d + +build +3: a--d +2: d--g +truncate-and-flush-to d +---- +3: a--d +2: d--g + +build +2: a----f +truncate-and-flush-to c +---- +2: a-c +2: c--f + +build +2: a----f +truncate-and-flush-to f +---- +2: a----f + +build +2: a----f +truncate-and-flush-to g +---- +2: a----f + +build +3: a-c +1: a-----g +truncate-and-flush-to d +---- +3: a-c +1: a-c +1: cd +1: d--g + +build +2: a---e +1: a------h +truncate-and-flush-to c +---- +2: a-c +1: a-c +2: c-e +1: c-e +1: e--h + +build +3: a-c +2: a---e +1: a-----g +truncate-and-flush-to d +3: d----i +---- +3: a-c +2: a-c +1: a-c +2: cd +1: cd +3: de +2: de +1: de +3: e-g +1: e-g +3: g-i + +build +3: a-c +2: a-----g +truncate-and-flush-to e +---- +3: a-c +2: a-c +2: c-e +2: e-g diff --git a/pebble/internal/keyspan/testdata/fragmenter_values b/pebble/internal/keyspan/testdata/fragmenter_values new file mode 100644 index 0000000..7462ae8 --- /dev/null +++ b/pebble/internal/keyspan/testdata/fragmenter_values @@ -0,0 +1,65 @@ +build +3: a-----------m apples +2: f------------s bananas +1: j---------------z coconuts +---- +3: a----f apples +3: f---j apples +2: f---j bananas +3: j--m apples +2: j--m bananas +1: j--m coconuts +2: m-----s bananas +1: m-----s coconuts +1: s------z coconuts + +# Building is idempotent. +build +3: a----f a +3: f---j b +2: f---j c +3: j--m d +2: j--m e +1: j--m f +2: m-----s g +1: m-----s h +1: s------z i +---- +3: a----f a +3: f---j b +2: f---j c +3: j--m d +2: j--m e +1: j--m f +2: m-----s g +1: m-----s h +1: s------z i + +build +2: a--c apple +1: b--d banana +truncate-and-flush-to c +---- +2: ab apple +2: bc apple +1: bc banana +1: cd banana + +build +3: a-c apple +2: a---e banana +1: a-----g coconut +truncate-and-flush-to d +3: d----i orange +---- +3: a-c apple +2: a-c banana +1: a-c coconut +2: cd banana +1: cd coconut +3: de orange +2: de banana +1: de coconut +3: e-g orange +1: e-g coconut +3: g-i orange diff --git a/pebble/internal/keyspan/testdata/interleaving_iter b/pebble/internal/keyspan/testdata/interleaving_iter new file mode 100644 index 0000000..b49db93 --- /dev/null +++ b/pebble/internal/keyspan/testdata/interleaving_iter @@ -0,0 +1,998 @@ +define-rangekeys +a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +c-d:{(#4,RANGEKEYSET,@3,coconut)} +e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +h-j:{(#22,RANGEKEYDEL) (#21,RANGEKEYSET,@5,peaches) (#21,RANGEKEYSET,@3,starfruit)} +l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)} +q-z:{(#14,RANGEKEYSET,@9,mangos)} +---- +OK + +define-pointkeys +artichoke.SET.10 +artichoke.SET.8 +carrot.SET.13 +cauliflower.DEL.9 +parsnip.SET.3 +tomato.SET.2 +zucchini.MERGE.12 +---- +OK + +iter +first +next +next +next +next +next +next +next +next +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +PointKey: artichoke#10,1 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +PointKey: artichoke#8,1 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +-- SpanChanged(c-d:{(#4,RANGEKEYSET,@3,coconut)}) +PointKey: c#72057594037927935,21 +Span: c-d:{(#4,RANGEKEYSET,@3,coconut)} +- +PointKey: carrot#13,1 +Span: c-d:{(#4,RANGEKEYSET,@3,coconut)} +- +PointKey: cauliflower#9,0 +Span: c-d:{(#4,RANGEKEYSET,@3,coconut)} +- +-- SpanChanged(e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)}) +PointKey: e#72057594037927935,21 +Span: e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +- +-- SpanChanged(h-j:{(#22,RANGEKEYDEL) (#21,RANGEKEYSET,@5,peaches) (#21,RANGEKEYSET,@3,starfruit)}) +PointKey: h#72057594037927935,19 +Span: h-j:{(#22,RANGEKEYDEL) (#21,RANGEKEYSET,@5,peaches) (#21,RANGEKEYSET,@3,starfruit)} +- +-- SpanChanged(l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)}) +PointKey: l#72057594037927935,20 +Span: l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)} +- +-- SpanChanged(nil) +PointKey: parsnip#3,1 +Span: +- +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: q#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +PointKey: tomato#2,1 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- + +# Test set-bounds passes through to the underlying point iterator and truncates +# a range key's end. + +iter +set-bounds b carrot +seek-ge b +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(b-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)}) +PointKey: b#72057594037927935,21 +Span: b-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +-- SpanChanged(c-carrot:{(#4,RANGEKEYSET,@3,coconut)}) +PointKey: c#72057594037927935,21 +Span: c-carrot:{(#4,RANGEKEYSET,@3,coconut)} +- +-- SpanChanged(nil) +. + + +# Test set-bounds passes through to the underlying point iterator and truncates +# a range key's start. + +iter +set-bounds b carrot +seek-lt carrot +prev +prev +---- +-- SpanChanged(nil) +-- SpanChanged(c-carrot:{(#4,RANGEKEYSET,@3,coconut)}) +PointKey: c#72057594037927935,21 +Span: c-carrot:{(#4,RANGEKEYSET,@3,coconut)} +- +-- SpanChanged(b-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)}) +PointKey: b#72057594037927935,21 +Span: b-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +-- SpanChanged(nil) +. + +# Test seek-ge. +# NB: The `seek-ge yyy` case demonstrates truncation to the search key. + +iter +first +seek-ge a +seek-ge p +seek-ge yyy +seek-ge z +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: parsnip#3,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: yyy#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: zucchini#12,2 +Span: +- + +iter +last +prev +prev +prev +prev +next +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: zucchini#12,2 +Span: +- +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: tomato#2,1 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +PointKey: q#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +-- SpanChanged(nil) +PointKey: parsnip#3,1 +Span: +- +-- SpanChanged(l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)}) +PointKey: l#72057594037927935,20 +Span: l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: parsnip#3,1 +Span: +- +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: q#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +PointKey: tomato#2,1 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +-- SpanChanged(nil) +PointKey: zucchini#12,2 +Span: +- + +iter +seek-ge tomato +next +seek-ge q +seek-ge parsnip +next +---- +-- SpanChanged(nil) +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: tomato#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +PointKey: tomato#2,1 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +-- SpanChanged(nil) +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: q#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: parsnip#3,1 +Span: +- +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: q#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- + +iter +seek-lt tomato +prev +seek-lt a +seek-lt tomato +seek-lt tomago +---- +-- SpanChanged(nil) +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: q#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +-- SpanChanged(nil) +PointKey: parsnip#3,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(nil) +. +-- SpanChanged(nil) +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: q#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- +-- SpanChanged(nil) +-- SpanChanged(q-z:{(#14,RANGEKEYSET,@9,mangos)}) +PointKey: q#72057594037927935,21 +Span: q-z:{(#14,RANGEKEYSET,@9,mangos)} +- + +define-rangekeys +a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +c-d:{(#4,RANGEKEYSET,@3,coconut)} +e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +h-j:{(#22,RANGEKEYDEL) (#21,RANGEKEYSET,@5,peaches) (#21,RANGEKEYSET,@3,starfruit)} +l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)} +q-z:{(#14,RANGEKEYSET,@9,mangos)} +---- +OK + +define-pointkeys +a.SET.10 +a.SET.8 +b.SET.13 +c.DEL.9 +d.SET.3 +e.SET.2 +---- +OK + +iter +seek-ge a +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +PointKey: a#10,1 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +PointKey: a#8,1 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +PointKey: b#13,1 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- + +iter +seek-lt a +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +. + +iter +seek-ge ab +next +next +next +next +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)}) +PointKey: ab#72057594037927935,21 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +PointKey: b#13,1 +Span: a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas) (#4,RANGEKEYSET,@2,oranges)} +- +-- SpanChanged(c-d:{(#4,RANGEKEYSET,@3,coconut)}) +PointKey: c#72057594037927935,21 +Span: c-d:{(#4,RANGEKEYSET,@3,coconut)} +- +PointKey: c#9,0 +Span: c-d:{(#4,RANGEKEYSET,@3,coconut)} +- +-- SpanChanged(nil) +PointKey: d#3,1 +Span: +- +-- SpanChanged(e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)}) +PointKey: e#72057594037927935,21 +Span: e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +- +PointKey: e#2,1 +Span: e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +- +-- SpanChanged(h-j:{(#22,RANGEKEYDEL) (#21,RANGEKEYSET,@5,peaches) (#21,RANGEKEYSET,@3,starfruit)}) +PointKey: h#72057594037927935,19 +Span: h-j:{(#22,RANGEKEYDEL) (#21,RANGEKEYSET,@5,peaches) (#21,RANGEKEYSET,@3,starfruit)} +- + +define-rangekeys +a-z:{(#5,RANGEKEYSET,@5,apples)} +---- +OK + +define-pointkeys +a.SET.10 +a.SET.8 +b.SET.13 +c.DEL.9 +d.SET.3 +e.SET.2 +---- +OK + +iter +first +next +next +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: a#72057594037927935,21 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: a#10,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: a#8,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: b#13,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: c#9,0 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: d#3,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- + +# Switch to reverse within a range key. +# NB: The seek-ge b should truncate the range key a-z to b. + +iter +seek-ge b +prev +---- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: b#72057594037927935,21 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: a#8,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- + +# Switch to reverse after a seek-ge. Reverse iteration should not revisit the +# interleaved range-key start at the seek-ge bound: The range-key start should +# be interleaved at its true start key. + +iter +seek-ge b +next +prev +prev +prev +---- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: b#72057594037927935,21 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: b#13,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: a#8,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: a#10,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: a#72057594037927935,21 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- + +# Switch to forward iteration after a seek-lt. + +iter +seek-lt c +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: b#13,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: c#9,0 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- + +iter +seek-lt c +prev +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: b#13,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +PointKey: a#8,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@5,apples)}) +PointKey: b#13,1 +Span: a-z:{(#5,RANGEKEYSET,@5,apples)} +- + +# Test sparse range keys. + +define-rangekeys +ace-bat:{(#5,RANGEKEYSET,@5,v5)} +x-z:{(#6,RANGEKEYSET,@6,v5)} +---- +OK + +define-pointkeys +a.SET.9 +b.SET.13 +c.DEL.9 +d.SET.18 +m.SET.4 +o.MERGE.3 +r.SET.22 +y.SET.3 +z.SET.3 +---- +OK + +iter +first +next +next +prev +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: a#9,1 +Span: +- +-- SpanChanged(ace-bat:{(#5,RANGEKEYSET,@5,v5)}) +PointKey: ace#72057594037927935,21 +Span: ace-bat:{(#5,RANGEKEYSET,@5,v5)} +- +PointKey: b#13,1 +Span: ace-bat:{(#5,RANGEKEYSET,@5,v5)} +- +-- SpanChanged(nil) +-- SpanChanged(ace-bat:{(#5,RANGEKEYSET,@5,v5)}) +PointKey: ace#72057594037927935,21 +Span: ace-bat:{(#5,RANGEKEYSET,@5,v5)} +- +-- SpanChanged(nil) +-- SpanChanged(ace-bat:{(#5,RANGEKEYSET,@5,v5)}) +PointKey: b#13,1 +Span: ace-bat:{(#5,RANGEKEYSET,@5,v5)} +- +-- SpanChanged(nil) +PointKey: c#9,0 +Span: +- + +iter +seek-lt ace +seek-lt zoo +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: a#9,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: z#3,1 +Span: +- + +iter +last +prev +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: z#3,1 +Span: +- +-- SpanChanged(x-z:{(#6,RANGEKEYSET,@6,v5)}) +PointKey: y#3,1 +Span: x-z:{(#6,RANGEKEYSET,@6,v5)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: z#3,1 +Span: +- +-- SpanChanged(nil) +. + +iter +seek-lt m +next +seek-ge m +prev +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: d#18,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: m#4,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: m#4,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: d#18,1 +Span: +- + +# First, Last, SeekLT and SeekGE elide spans without Sets. + +define-rangekeys +b-d:{(#5,RANGEKEYDEL)} +f-g:{(#6,RANGEKEYDEL)} +---- +OK + +define-pointkeys +c.SET.8 +---- +OK + +iter +first +last +seek-ge a +seek-lt d +---- +-- SpanChanged(nil) +-- SpanChanged(b-d:{(#5,RANGEKEYDEL)}) +PointKey: b#72057594037927935,19 +Span: b-d:{(#5,RANGEKEYDEL)} +- +-- SpanChanged(nil) +-- SpanChanged(f-g:{(#6,RANGEKEYDEL)}) +PointKey: f#72057594037927935,19 +Span: f-g:{(#6,RANGEKEYDEL)} +- +-- SpanChanged(nil) +-- SpanChanged(b-d:{(#5,RANGEKEYDEL)}) +PointKey: b#72057594037927935,19 +Span: b-d:{(#5,RANGEKEYDEL)} +- +-- SpanChanged(nil) +-- SpanChanged(b-d:{(#5,RANGEKEYDEL)}) +PointKey: c#8,1 +Span: b-d:{(#5,RANGEKEYDEL)} +- + +# Test a scenario where Next is out of point keys, the current range key has +# already been interleaved, and there are no more range keys. + +define-rangekeys +w-y:{(#5,RANGEKEYSET,@1,v1)} +y-z:{(#5,RANGEKEYDEL)} +---- +OK + +define-pointkeys +x.SET.8 +---- +OK + +iter +first +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(w-y:{(#5,RANGEKEYSET,@1,v1)}) +PointKey: w#72057594037927935,21 +Span: w-y:{(#5,RANGEKEYSET,@1,v1)} +- +PointKey: x#8,1 +Span: w-y:{(#5,RANGEKEYSET,@1,v1)} +- +-- SpanChanged(y-z:{(#5,RANGEKEYDEL)}) +PointKey: y#72057594037927935,19 +Span: y-z:{(#5,RANGEKEYDEL)} +- + +# Test a scenario where we change direction on a synthetic range key boundary +# key. +iter +first +prev +---- +-- SpanChanged(nil) +-- SpanChanged(w-y:{(#5,RANGEKEYSET,@1,v1)}) +PointKey: w#72057594037927935,21 +Span: w-y:{(#5,RANGEKEYSET,@1,v1)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +. + +define-rangekeys +a-z:{(#5,RANGEKEYSET,@1,v1)} +---- +OK + +define-pointkeys +z.SET.8 +---- +OK + +iter +seek-ge c +prev +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@1,v1)}) +PointKey: c#72057594037927935,21 +Span: a-z:{(#5,RANGEKEYSET,@1,v1)} +- +-- SpanChanged(nil) +-- SpanChanged(a-z:{(#5,RANGEKEYSET,@1,v1)}) +PointKey: a#72057594037927935,21 +Span: a-z:{(#5,RANGEKEYSET,@1,v1)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: z#8,1 +Span: +- + +iter +set-bounds . c +first +set-bounds c . +last +prev +prev +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#5,RANGEKEYSET,@1,v1)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#5,RANGEKEYSET,@1,v1)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: z#8,1 +Span: +- +-- SpanChanged(c-z:{(#5,RANGEKEYSET,@1,v1)}) +PointKey: c#72057594037927935,21 +Span: c-z:{(#5,RANGEKEYSET,@1,v1)} +- +-- SpanChanged(nil) +. + +# Test switching directions after exhausting a range key iterator. +# Switching reverse to forward iteration. + +define-rangekeys +j-l:{(#3,RANGEKEYSET,@1,v0)} +---- +OK + +define-pointkeys +g.SET.1 +s.SET.1 +v.SET.2 +v.SET.1 +z.SET.1 +---- +OK + +iter +last +prev +prev +prev +prev +prev +next +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: z#1,1 +Span: +- +-- SpanChanged(nil) +PointKey: v#1,1 +Span: +- +-- SpanChanged(nil) +PointKey: v#2,1 +Span: +- +-- SpanChanged(nil) +PointKey: s#1,1 +Span: +- +-- SpanChanged(j-l:{(#3,RANGEKEYSET,@1,v0)}) +PointKey: j#72057594037927935,21 +Span: j-l:{(#3,RANGEKEYSET,@1,v0)} +- +-- SpanChanged(nil) +PointKey: g#1,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(j-l:{(#3,RANGEKEYSET,@1,v0)}) +PointKey: j#72057594037927935,21 +Span: j-l:{(#3,RANGEKEYSET,@1,v0)} +- + +# Test switching directions after exhausting a range key iterator. +# Switching forward to reverse iteration. + +define-rangekeys +j-l:{(#3,RANGEKEYSET,@1,v0)} +---- +OK + +define-pointkeys +a.SET.1 +k.SET.1 +m.SET.1 +---- +OK + +iter +first +next +next +next +prev +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: a#1,1 +Span: +- +-- SpanChanged(j-l:{(#3,RANGEKEYSET,@1,v0)}) +PointKey: j#72057594037927935,21 +Span: j-l:{(#3,RANGEKEYSET,@1,v0)} +- +PointKey: k#1,1 +Span: j-l:{(#3,RANGEKEYSET,@1,v0)} +- +-- SpanChanged(nil) +PointKey: m#1,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(j-l:{(#3,RANGEKEYSET,@1,v0)}) +PointKey: k#1,1 +Span: j-l:{(#3,RANGEKEYSET,@1,v0)} +- + +# Test a seek that moves the lower bound beyond the upper bound. + +define-rangekeys +a-d:{(#10,RANGEKEYSET,@5,apples)} +---- +OK + +define-pointkeys +b.SET.8 +---- +OK + + +iter +set-bounds a c +seek-ge c +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +. + +iter +set-bounds a c +seek-lt a +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +. + +# Test a SeekLT that searches a keyspace exclusive with the iterator's bounds. +# Previously, there was a bug that would incorrectly surface the span with the +# iterator's bounds, despite the fact the SeekLT search key is exclusive. See +# the comment in keyspanSeekLT. + +define-rangekeys +b-f:{(#1,RANGEKEYSET,@1,foo)} +---- +OK + +define-pointkeys +f.SET.3 +---- +OK + +iter +set-bounds d e +seek-lt d +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +. + +# Test seek-prefix-ge and its truncation of bounds to the prefix's bounds. + +define-rangekeys +b-d:{(#5,RANGEKEYSET,@1,foo)} +f-g:{(#6,RANGEKEYSET,@1,foo)} +---- +OK + +define-pointkeys +c.SET.8 +---- +OK + +iter +seek-prefix-ge b +next +seek-prefix-ge c +next +seek-ge c +---- +-- SpanChanged(nil) +-- SpanChanged(b-b\x00:{(#5,RANGEKEYSET,@1,foo)}) +PointKey: b#72057594037927935,21 +Span: b-b\x00:{(#5,RANGEKEYSET,@1,foo)} +- +PointKey: c#8,1 +Span: b-b\x00:{(#5,RANGEKEYSET,@1,foo)} +- +-- SpanChanged(nil) +-- SpanChanged(c-c\x00:{(#5,RANGEKEYSET,@1,foo)}) +PointKey: c#72057594037927935,21 +Span: c-c\x00:{(#5,RANGEKEYSET,@1,foo)} +- +PointKey: c#8,1 +Span: c-c\x00:{(#5,RANGEKEYSET,@1,foo)} +- +-- SpanChanged(nil) +-- SpanChanged(b-d:{(#5,RANGEKEYSET,@1,foo)}) +PointKey: c#72057594037927935,21 +Span: b-d:{(#5,RANGEKEYSET,@1,foo)} +- + +# Test NextPrefix + +define-rangekeys +b-e:{(#5,RANGEKEYSET,@9,foo)} +f-g:{(#6,RANGEKEYSET,@9,foo)} +---- +OK + +define-pointkeys +a@4.SET.8 +c@11.SET.8 +c@3.SET.8 +c@1.SET.4 +d@5.SET.3 +e@9.SET.2 +---- +OK + +iter +first +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: a@4#8,1 +Span: +- +-- SpanChanged(b-e:{(#5,RANGEKEYSET,@9,foo)}) +PointKey: b#72057594037927935,21 +Span: b-e:{(#5,RANGEKEYSET,@9,foo)} +- +PointKey: c@11#8,1 +Span: b-e:{(#5,RANGEKEYSET,@9,foo)} +- +PointKey: d@5#3,1 +Span: b-e:{(#5,RANGEKEYSET,@9,foo)} +- +-- SpanChanged(nil) +PointKey: e@9#2,1 +Span: +- +-- SpanChanged(f-g:{(#6,RANGEKEYSET,@9,foo)}) +PointKey: f#72057594037927935,21 +Span: f-g:{(#6,RANGEKEYSET,@9,foo)} +- +-- SpanChanged(nil) +. +. diff --git a/pebble/internal/keyspan/testdata/interleaving_iter_masking b/pebble/internal/keyspan/testdata/interleaving_iter_masking new file mode 100644 index 0000000..8ad8fb3 --- /dev/null +++ b/pebble/internal/keyspan/testdata/interleaving_iter_masking @@ -0,0 +1,501 @@ +# Test the scenario illustrated in the below visualization. +# +# ^ +# @9 | •―――――――――――――――○ [e,m)@9 +# s 8 | • l@8 +# u 7 |------------------------------------ @7 masking +# f 6 | [h,q)@6 •―――――――――――――――――○ threshold +# f 5 | • h@5 +# f 4 | • n@4 +# i 3 | •―――――――――――○ [f,l)@3 +# x 2 | • b@2 +# 1 | +# 0 |___________________________________ +# a b c d e f g h i j k l m n o p q +# + +define-rangekeys +e-f:{(#1,RANGEKEYSET,@9,foo)} +f-h:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@3,bar)} +h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)} +l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +m-q:{(#1,RANGEKEYSET,@6,bax)} +---- +OK + +define-pointkeys +b@2.SET.1 +h@5.SET.1 +l@8.SET.1 +n@4.SET.1 +---- +OK + +set-masking-threshold +@7 +---- +OK + +iter +first +next +next +next +next +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: b@2#1,1 +Span: +- +-- SpanChanged(e-f:{(#1,RANGEKEYSET,@9,foo)}) +PointKey: e#72057594037927935,21 +Span: e-f:{(#1,RANGEKEYSET,@9,foo)} +- +-- SpanChanged(f-h:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@3,bar)}) +PointKey: f#72057594037927935,21 +Span: f-h:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@3,bar)} +- +-- SpanChanged(h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)}) +PointKey: h#72057594037927935,21 +Span: h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)} +- +-- SpanChanged(l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)}) +PointKey: l#72057594037927935,21 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +PointKey: l@8#1,1 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(m-q:{(#1,RANGEKEYSET,@6,bax)}) +PointKey: m#72057594037927935,21 +Span: m-q:{(#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(nil) +. + +iter +last +prev +prev +prev +prev +prev +prev +prev +---- +-- SpanChanged(nil) +-- SpanChanged(m-q:{(#1,RANGEKEYSET,@6,bax)}) +PointKey: m#72057594037927935,21 +Span: m-q:{(#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)}) +PointKey: l@8#1,1 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +PointKey: l#72057594037927935,21 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)}) +PointKey: h#72057594037927935,21 +Span: h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)} +- +-- SpanChanged(f-h:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@3,bar)}) +PointKey: f#72057594037927935,21 +Span: f-h:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@3,bar)} +- +-- SpanChanged(e-f:{(#1,RANGEKEYSET,@9,foo)}) +PointKey: e#72057594037927935,21 +Span: e-f:{(#1,RANGEKEYSET,@9,foo)} +- +-- SpanChanged(nil) +PointKey: b@2#1,1 +Span: +- +-- SpanChanged(nil) +. + +iter +seek-ge a +seek-ge c +seek-ge h +seek-ge i +seek-ge l +next +seek-ge m +seek-ge r +---- +-- SpanChanged(nil) +-- SpanChanged(nil) +PointKey: b@2#1,1 +Span: +- +-- SpanChanged(nil) +-- SpanChanged(e-f:{(#1,RANGEKEYSET,@9,foo)}) +PointKey: e#72057594037927935,21 +Span: e-f:{(#1,RANGEKEYSET,@9,foo)} +- +-- SpanChanged(nil) +-- SpanChanged(h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)}) +PointKey: h#72057594037927935,21 +Span: h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)} +- +-- SpanChanged(nil) +-- SpanChanged(h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)}) +PointKey: i#72057594037927935,21 +Span: h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)} +- +-- SpanChanged(nil) +-- SpanChanged(l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)}) +PointKey: l#72057594037927935,21 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +PointKey: l@8#1,1 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(nil) +-- SpanChanged(m-q:{(#1,RANGEKEYSET,@6,bax)}) +PointKey: m#72057594037927935,21 +Span: m-q:{(#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(nil) +-- SpanChanged(nil) +. + +# Setting the masking threshold to @9 should result in l@8 being masked by +# [e,m)@9. + +set-masking-threshold +@9 +---- +OK + +iter +seek-ge l +next +seek-lt l +seek-lt ll +prev +---- +-- SpanChanged(nil) +-- SpanChanged(l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)}) +PointKey: l#72057594037927935,21 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(m-q:{(#1,RANGEKEYSET,@6,bax)}) +PointKey: m#72057594037927935,21 +Span: m-q:{(#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(nil) +-- SpanChanged(h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)}) +PointKey: h#72057594037927935,21 +Span: h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)} +- +-- SpanChanged(nil) +-- SpanChanged(l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)}) +PointKey: l#72057594037927935,21 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)}) +PointKey: h#72057594037927935,21 +Span: h-l:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax) (#1,RANGEKEYSET,@3,bar)} +- + +iter +seek-ge l +next +---- +-- SpanChanged(nil) +-- SpanChanged(l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)}) +PointKey: l#72057594037927935,21 +Span: l-m:{(#1,RANGEKEYSET,@9,foo) (#1,RANGEKEYSET,@6,bax)} +- +-- SpanChanged(m-q:{(#1,RANGEKEYSET,@6,bax)}) +PointKey: m#72057594037927935,21 +Span: m-q:{(#1,RANGEKEYSET,@6,bax)} +- + +define-rangekeys +a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)} +---- +OK + +define-pointkeys +a.SET.1 +a@3.SET.1 +a@12.SET.1 +b@2.SET.1 +---- +OK + +set-masking-threshold +@10 +---- +OK + +# Test that both a@3 and b@2 are masked by the rangekey. +# The unsuffixed point key 'a' and the point key at a higher timestamp 'a@12' +# are not masked. + +iter +first +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)} +- +PointKey: a#1,1 +Span: a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)} +- +PointKey: a@12#1,1 +Span: a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)} +- +-- SpanChanged(nil) +. + +iter +last +prev +prev +prev +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)}) +PointKey: a@12#1,1 +Span: a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)} +- +PointKey: a#1,1 +Span: a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)} +- +PointKey: a#72057594037927935,21 +Span: a-c:{(#1,RANGEKEYSET,@5,apples) (#1,RANGEKEYSET,@2,bananas)} +- +-- SpanChanged(nil) +. + +# Try the same test, but with a range key that sorts before the masking +# threshold (eg, higher MVCC timestamp). Nothing should be masked. + +define-rangekeys +a-c:{(#2,RANGEKEYSET,@20,apples)} +---- +OK + +iter +first +next +next +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#2,RANGEKEYSET,@20,apples)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +PointKey: a#1,1 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +PointKey: a@3#1,1 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +PointKey: a@12#1,1 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +PointKey: b@2#1,1 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +-- SpanChanged(nil) +. + +iter +last +prev +prev +prev +prev +prev +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#2,RANGEKEYSET,@20,apples)}) +PointKey: b@2#1,1 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +PointKey: a@12#1,1 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +PointKey: a@3#1,1 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +PointKey: a#1,1 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +PointKey: a#72057594037927935,21 +Span: a-c:{(#2,RANGEKEYSET,@20,apples)} +- +-- SpanChanged(nil) +. + +# Try the original test, but with an internal range key containing just an +# Unset, and no Set. Nothing should be masked. No range keys should be surfaced, +# because there are none. + +define-rangekeys +a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)} +---- +OK + +iter +first +next +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)}) +PointKey: a#72057594037927935,20 +Span: a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)} +- +PointKey: a#1,1 +Span: a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)} +- +PointKey: a@12#1,1 +Span: a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)} +- +-- SpanChanged(nil) +. +-- SpanChanged(nil) +. + +iter +last +prev +prev +prev +prev +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)}) +PointKey: a@12#1,1 +Span: a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)} +- +PointKey: a#1,1 +Span: a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)} +- +PointKey: a#72057594037927935,20 +Span: a-c:{(#1,RANGEKEYUNSET,@5) (#1,RANGEKEYUNSET,@2)} +- +-- SpanChanged(nil) +. +-- SpanChanged(nil) +. + +# Test a scenario where a point key is masked in the forward direction, which in +# turn requires nexting to the next range key as well. + +define-rangekeys +a-c:{(#1,RANGEKEYSET,@5,apples)} +c-z:{(#1,RANGEKEYSET,@10,bananas)} +---- +OK + +define-pointkeys +b@3.SET.2 +d@9.SET.4 +j@11.SET.3 +---- +OK + +set-masking-threshold +@20 +---- +OK + +iter +first +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#1,RANGEKEYSET,@5,apples)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#1,RANGEKEYSET,@5,apples)} +- +-- SpanChanged(c-z:{(#1,RANGEKEYSET,@10,bananas)}) +PointKey: c#72057594037927935,21 +Span: c-z:{(#1,RANGEKEYSET,@10,bananas)} +- +PointKey: j@11#3,1 +Span: c-z:{(#1,RANGEKEYSET,@10,bananas)} +- + +iter +last +prev +prev +---- +-- SpanChanged(nil) +-- SpanChanged(c-z:{(#1,RANGEKEYSET,@10,bananas)}) +PointKey: j@11#3,1 +Span: c-z:{(#1,RANGEKEYSET,@10,bananas)} +- +PointKey: c#72057594037927935,21 +Span: c-z:{(#1,RANGEKEYSET,@10,bananas)} +- +-- SpanChanged(a-c:{(#1,RANGEKEYSET,@5,apples)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#1,RANGEKEYSET,@5,apples)} +- + +# Test a scenario where a there's an empty range key, requiring the interleaving +# iter to call SpanChanged(nil) which should clear the previous mask. + +define-rangekeys +a-c:{(#1,RANGEKEYSET,@10,apples)} +c-e:{} +e-f:{(#1,RANGEKEYSET,@5,bananas)} +---- +OK + +define-pointkeys +a@2.SET.4 +b@9.SET.2 +d@9.SET.3 +---- +OK + +set-masking-threshold +@20 +---- +OK + +iter +seek-ge a +next +next +next +---- +-- SpanChanged(nil) +-- SpanChanged(a-c:{(#1,RANGEKEYSET,@10,apples)}) +PointKey: a#72057594037927935,21 +Span: a-c:{(#1,RANGEKEYSET,@10,apples)} +- +-- SpanChanged(nil) +PointKey: d@9#3,1 +Span: +- +-- SpanChanged(e-f:{(#1,RANGEKEYSET,@5,bananas)}) +PointKey: e#72057594037927935,21 +Span: e-f:{(#1,RANGEKEYSET,@5,bananas)} +- +-- SpanChanged(nil) +. diff --git a/pebble/internal/keyspan/testdata/iter b/pebble/internal/keyspan/testdata/iter new file mode 100644 index 0000000..5a1c451 --- /dev/null +++ b/pebble/internal/keyspan/testdata/iter @@ -0,0 +1,55 @@ +define +a-b:{(#2,SET) (#1,SET)} +b-c:{(#2,SET) (#1,SET)} +c-d:{(#2,SET) (#1,SET)} +---- + +iter +seek-ge a +seek-ge b +seek-ge c +seek-ge cat +seek-ge d +seek-lt a +seek-lt b +seek-lt c +seek-lt cat +seek-lt d +seek-lt e +---- +a-b:{(#2,SET) (#1,SET)} +b-c:{(#2,SET) (#1,SET)} +c-d:{(#2,SET) (#1,SET)} +c-d:{(#2,SET) (#1,SET)} +. +. +a-b:{(#2,SET) (#1,SET)} +b-c:{(#2,SET) (#1,SET)} +c-d:{(#2,SET) (#1,SET)} +c-d:{(#2,SET) (#1,SET)} +c-d:{(#2,SET) (#1,SET)} + +iter +first +next +prev +prev +next +next +next +prev +next +next +prev +---- +a-b:{(#2,SET) (#1,SET)} +b-c:{(#2,SET) (#1,SET)} +a-b:{(#2,SET) (#1,SET)} +. +a-b:{(#2,SET) (#1,SET)} +b-c:{(#2,SET) (#1,SET)} +c-d:{(#2,SET) (#1,SET)} +b-c:{(#2,SET) (#1,SET)} +c-d:{(#2,SET) (#1,SET)} +. +c-d:{(#2,SET) (#1,SET)} diff --git a/pebble/internal/keyspan/testdata/level_iter b/pebble/internal/keyspan/testdata/level_iter new file mode 100644 index 0000000..3919819 --- /dev/null +++ b/pebble/internal/keyspan/testdata/level_iter @@ -0,0 +1,475 @@ + +# Simple case. + +define +file + a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} + c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +---- + +iter +seek-ge a +seek-ge apple +seek-ge b +seek-ge banana +seek-ge c +seek-ge cantalope +seek-ge d +seek-ge dragonfruit +---- +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +. +. + +iter +seek-lt a +seek-lt apple +seek-lt b +seek-lt banana +seek-lt c +seek-lt cantalope +seek-lt d +seek-lt dragonfruit +prev +---- +. +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) + +iter +seek-ge a +prev +seek-lt d +next +---- +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +. +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +. + +iter +first +next +next +next +---- +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +. + +iter +last +prev +prev +prev +---- +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +. + +# Set some bounds + +iter +seek-ge a +seek-ge b +seek-ge c +seek-ge d +seek-lt a +seek-lt b +seek-lt c +seek-lt d +---- +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +. +. +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) + + +iter +seek-lt cc +prev +prev +prev +---- +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +. + +# Test skipping over empty/point-key-only files in both directions. + +define +file + a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + point:b.SET.1:foo +file + c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} + d-e:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +---- + +num-files +---- +3 + +iter +first +next +next +next +---- +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{} (file = 000001.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +d-e:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) + +iter +last +prev +prev +prev +---- +d-e:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +b-c:{} (file = 000003.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) + +# Test straddle keys between files. + +define +file + a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +---- + +iter +first +next +next +next +next +next +next +next +---- +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{} (file = 000001.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +d-e:{} (file = 000002.sst) +e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +f-g:{} (file = 000003.sst) +g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000004.sst) +. + +iter +last +prev +prev +prev +prev +prev +prev +prev +---- +g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000004.sst) +f-g:{} (file = 000004.sst) +e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +d-e:{} (file = 000003.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{} (file = 000002.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +. + +# The below case seeks into a file straddle, then iterates forward and back to +# it, and confirms that changing iterator directions on a straddle does the +# right thing. + +iter +seek-ge bb +next +prev +next +prev +prev +---- +b-c:{} (file = 000001.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{} (file = 000002.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) + +# The same case as above, but with inverted directions. + +iter +seek-lt dd +prev +next +prev +next +next +---- +d-e:{} (file = 000001.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +d-e:{} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +d-e:{} (file = 000002.sst) +e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) + +iter +seek-lt dd +prev +next +prev +next +next +---- +d-e:{} (file = 000003.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +d-e:{} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +d-e:{} (file = 000002.sst) +e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) + +# Seeks right at the bound should return nothing. + +iter +seek-lt bb +---- +b-c:{} (file = 000003.sst) + +iter +seek-ge dd +---- +d-e:{} (file = 000003.sst) + +iter +seek-lt d +prev +next +prev +prev +prev +next +next +---- +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{} (file = 000002.sst) +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +b-c:{} (file = 000002.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +. +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{} (file = 000001.sst) + +# A bunch of files with point keys only should not fragment straddles. + +define +file + a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + point:c.SET.1:foo +file + point:d.SET.1:foo +file + e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + point:g.SET.1:foo +file + h-i:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +---- + +iter +first +next +next +next +next +next +---- +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-e:{} (file = 000001.sst) +e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000004.sst) +f-h:{} (file = 000004.sst) +h-i:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000006.sst) +. + +iter +last +prev +prev +prev +prev +prev +---- +h-i:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000006.sst) +f-h:{} (file = 000006.sst) +e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000004.sst) +b-e:{} (file = 000004.sst) +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +. + +# Test files with range keys and rangedels + +define +file + a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} + point:a.SET.1:foo + point:b.SET.1:foo +file + c-e:{(#3,RANGEKEYSET,@3,baz) (#3,RANGEKEYSET,@1,bar)} + point:c.RANGEDEL.2:f + point:d.SET.1:foo +file + g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} + i-j:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} + point:f.RANGEDEL.2:g +---- + +iter rangedel +first +next +next +next +---- +c-f:{(#2,RANGEDEL)} (file = 000002.sst) +f-g:{(#2,RANGEDEL)} (file = 000003.sst) +. +. + +iter rangedel +last +prev +prev +prev +---- +f-g:{(#2,RANGEDEL)} (file = 000003.sst) +c-f:{(#2,RANGEDEL)} (file = 000002.sst) +. +. + +iter rangedel +seek-ge c +next +next +---- +c-f:{(#2,RANGEDEL)} (file = 000002.sst) +f-g:{(#2,RANGEDEL)} (file = 000003.sst) +. + +iter rangedel +seek-lt ff +prev +next +prev +prev +---- +f-g:{(#2,RANGEDEL)} (file = 000003.sst) +c-f:{(#2,RANGEDEL)} (file = 000002.sst) +f-g:{(#2,RANGEDEL)} (file = 000003.sst) +c-f:{(#2,RANGEDEL)} (file = 000002.sst) +. + +close-iter +---- +ok + +# Test that a regular LevelIter ignores rangedels and emits straddle spans. + +iter +first +next +next +next +next +next +---- +a-b:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +b-c:{} (file = 000001.sst) +c-e:{(#3,RANGEKEYSET,@3,baz) (#3,RANGEKEYSET,@1,bar)} (file = 000002.sst) +e-g:{} (file = 000002.sst) +g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +i-j:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) + +iter +seek-ge c +next +next +next +next +---- +c-e:{(#3,RANGEKEYSET,@3,baz) (#3,RANGEKEYSET,@1,bar)} (file = 000002.sst) +e-g:{} (file = 000002.sst) +g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +i-j:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +. + +# Test seeking outside of bounds with straddles. + +define +file + c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +file + g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} +---- + +iter +seek-lt j +next +prev +prev +---- +g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +. +g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +f-g:{} (file = 000003.sst) + +iter +seek-lt j +prev +prev +next +next +---- +g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) +f-g:{} (file = 000003.sst) +e-f:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000002.sst) +f-g:{} (file = 000002.sst) +g-h:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000003.sst) + +iter +seek-ge a +prev +next +next +---- +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +. +c-d:{(#2,RANGEKEYSET,@3,foo) (#1,RANGEKEYSET,@1,bar)} (file = 000001.sst) +d-e:{} (file = 000001.sst) diff --git a/pebble/internal/keyspan/testdata/merging_iter b/pebble/internal/keyspan/testdata/merging_iter new file mode 100644 index 0000000..aa309e2 --- /dev/null +++ b/pebble/internal/keyspan/testdata/merging_iter @@ -0,0 +1,758 @@ +# Test a single level. + +define +a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,coconut)} +e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +h-j:{(#22,RANGEKEYDEL) (#21,RANGEKEYSET,@5,peaches) (#21,RANGEKEYSET,@3,starfruit)} +l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)} +q-z:{(#14,RANGEKEYSET,@9,mangos)} +---- +1 levels + +iter +first +next +next +next +next +next +next +---- +a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,coconut)} +e-f:{(#20,RANGEKEYSET,@5,pineapple) (#20,RANGEKEYSET,@3,guava)} +h-j:{(#22,RANGEKEYDEL) (#21,RANGEKEYSET,@5,peaches) (#21,RANGEKEYSET,@3,starfruit)} +l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)} +q-z:{(#14,RANGEKEYSET,@9,mangos)} + + +# Test snapshot filtering. + +iter snapshot=12 +first +next +next +next +next +next +next +---- +a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas)} +c-d:{(#4,RANGEKEYSET,@3,coconut)} +e-f:{} +h-j:{} +l-m:{(#2,RANGEKEYUNSET,@9) (#2,RANGEKEYUNSET,@5)} +q-z:{} + + +# Test error handling on seeks. + +iter probes=(0,ErrInjected,(Log "# inner.")) +first +last +seek-ge boo +seek-lt lemon +---- +# inner.First() = nil + err= +# inner.Last() = nil + err= +# inner.SeekLT("boo") = nil + err= +# inner.SeekGE("lemon") = nil + err= + +# Test error handling on steps. + +iter probes=(0,(If (Or OpNext OpPrev) ErrInjected noop),(Log "# inner.")) +first +next +last +prev +---- +# inner.First() = a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas)} +a-c:{(#10,RANGEKEYSET,@5,apples) (#10,RANGEKEYDEL) (#8,RANGEKEYUNSET,@1) (#4,RANGEKEYSET,@3,bananas)} +# inner.Next() = nil + err= +# inner.Last() = q-z:{(#14,RANGEKEYSET,@9,mangos)} +q-z:{(#14,RANGEKEYSET,@9,mangos)} +# inner.Prev() = nil + err= + +define +b-d:{#10,RANGEKEYSET,@1,apples} +e-h:{#8,RANGEKEYDEL} +-- +a-c:{#3,RANGEKEYUNSET,@1} +h-k:{#5,RANGEKEYDEL} +---- +2 levels + +iter +first +next +next +next +next +next +---- +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +e-h:{(#8,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} + + +iter +last +prev +prev +prev +prev +prev +---- +h-k:{(#5,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} + + +# Test changing directions at each iterator position, reverse to forward. +iter +last +next +last +prev +next +---- +h-k:{(#5,RANGEKEYDEL)} + +h-k:{(#5,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} + +iter +last +prev +prev +next +---- +h-k:{(#5,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +e-h:{(#8,RANGEKEYDEL)} + +iter +last +prev +prev +prev +next +---- +h-k:{(#5,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +c-d:{(#10,RANGEKEYSET,@1,apples)} + +iter +last +prev +prev +prev +prev +next +---- +h-k:{(#5,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} + +iter +last +prev +prev +prev +prev +prev +next +---- +h-k:{(#5,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} + +a-b:{(#3,RANGEKEYUNSET,@1)} + +# Test changing directions at each iterator position, forward to reverse. + +iter +first +prev +first +next +prev +---- +a-b:{(#3,RANGEKEYUNSET,@1)} + +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} + +iter +first +next +next +prev +---- +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} + +iter +first +next +next +next +prev +---- +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +e-h:{(#8,RANGEKEYDEL)} +c-d:{(#10,RANGEKEYSET,@1,apples)} + +iter +first +next +next +next +next +next +prev +---- +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +e-h:{(#8,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} + +h-k:{(#5,RANGEKEYDEL)} + +iter +first +next +next +next +next +prev +---- +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +e-h:{(#8,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} + +# Test SeekGE. Note that MergingIter's SeekGE implements the FragmentIterator's +# SeekGE semantics. It returns the first fragment that covers a key ≥ the search +# key. + +iter +seek-ge cc +---- +c-d:{(#10,RANGEKEYSET,@1,apples)} + +iter +seek-ge 1 +seek-ge a +seek-ge b +seek-ge bb +---- +a-b:{(#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} + +iter +seek-ge c +seek-ge cc +seek-ge e +seek-ge f +---- +c-d:{(#10,RANGEKEYSET,@1,apples)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +e-h:{(#8,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} + +iter +seek-ge h +seek-ge i +seek-ge k +seek-ge l +---- +h-k:{(#5,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} + + + +# Test SeekLT. Note that MergingIter's SeekLT implements the FragmentIterator's +# SeekLT semantics. It returns the first fragment with a Start key < the search +# key, NOT the first fragment that covers a key < the search key. +# +# NB: seek-lt bb finds b-c#3.RANGEKEYUNSET (the last fragment with the bounds +# [b,c), unlike the above seek-ge b which finds the first). + +iter +seek-lt b +---- +a-b:{(#3,RANGEKEYUNSET,@1)} + +iter +seek-lt 1 +seek-lt a +seek-lt aa +seek-lt b +seek-lt bb +seek-lt c +---- + + +a-b:{(#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} + +iter +seek-lt cc +seek-lt d +seek-lt dd +seek-lt e +seek-lt ee +seek-lt h +seek-lt hh +seek-lt k +seek-lt z +---- +c-d:{(#10,RANGEKEYSET,@1,apples)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +e-h:{(#8,RANGEKEYDEL)} +e-h:{(#8,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} + +# Test error handling with multiple levels. Inject errors in all operations on +# the first iterator, and none of the second iterator. + +iter probes=(0,ErrInjected,(Log "# a.")) probes=(1,(Log "# b.")) +seek-ge a +seek-ge b +seek-ge c +seek-ge d +seek-ge e +seek-ge f +seek-ge g +seek-ge h +seek-ge i +seek-ge j +seek-ge k +seek-ge z +---- +# a.SeekLT("a") = nil +# b.SeekLT("a") = nil + err= +# a.SeekLT("b") = nil +# b.SeekLT("b") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekLT("c") = nil +# b.SeekLT("c") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekLT("d") = nil +# b.SeekLT("d") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekLT("e") = nil +# b.SeekLT("e") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekLT("f") = nil +# b.SeekLT("f") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekLT("g") = nil +# b.SeekLT("g") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekLT("h") = nil +# b.SeekLT("h") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekLT("i") = nil +# b.SeekLT("i") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekLT("j") = nil +# b.SeekLT("j") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekLT("k") = nil +# b.SeekLT("k") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekLT("z") = nil +# b.SeekLT("z") = h-k:{(#5,RANGEKEYDEL)} + err= + +# Test the same as above, but with errors injected on the second iterator. + +iter probes=(0,(Log "# a.")) probes=(1,ErrInjected,(Log "# b.")) +seek-ge a +seek-ge b +seek-ge c +seek-ge d +seek-ge e +seek-ge f +seek-ge g +seek-ge h +seek-ge i +seek-ge j +seek-ge k +seek-ge z +---- +# a.SeekLT("a") = nil +# b.SeekLT("a") = nil + err= +# a.SeekLT("b") = nil +# b.SeekLT("b") = nil + err= +# a.SeekLT("c") = b-d:{(#10,RANGEKEYSET,@1,apples)} +# b.SeekLT("c") = nil + err= +# a.SeekLT("d") = b-d:{(#10,RANGEKEYSET,@1,apples)} +# b.SeekLT("d") = nil + err= +# a.SeekLT("e") = b-d:{(#10,RANGEKEYSET,@1,apples)} +# b.SeekLT("e") = nil + err= +# a.SeekLT("f") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekLT("f") = nil + err= +# a.SeekLT("g") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekLT("g") = nil + err= +# a.SeekLT("h") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekLT("h") = nil + err= +# a.SeekLT("i") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekLT("i") = nil + err= +# a.SeekLT("j") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekLT("j") = nil + err= +# a.SeekLT("k") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekLT("k") = nil + err= +# a.SeekLT("z") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekLT("z") = nil + err= + +# Test SeekLTs with errors injected on the first iterator. + +iter probes=(0,ErrInjected,(Log "# a.")) probes=(1,(Log "# b.")) +seek-lt a +seek-lt b +seek-lt c +seek-lt d +seek-lt e +seek-lt f +seek-lt g +seek-lt h +seek-lt i +seek-lt j +seek-lt k +seek-lt z +---- +# a.SeekGE("a") = nil +# b.SeekGE("a") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekGE("b") = nil +# b.SeekGE("b") = a-c:{(#3,RANGEKEYUNSET,@1)} + err= +# a.SeekGE("c") = nil +# b.SeekGE("c") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekGE("d") = nil +# b.SeekGE("d") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekGE("e") = nil +# b.SeekGE("e") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekGE("f") = nil +# b.SeekGE("f") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekGE("g") = nil +# b.SeekGE("g") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekGE("h") = nil +# b.SeekGE("h") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekGE("i") = nil +# b.SeekGE("i") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekGE("j") = nil +# b.SeekGE("j") = h-k:{(#5,RANGEKEYDEL)} + err= +# a.SeekGE("k") = nil +# b.SeekGE("k") = nil + err= +# a.SeekGE("z") = nil +# b.SeekGE("z") = nil + err= + +# Test SeekLTs with errors injected on the second iterator. + +iter probes=(0,(Log "# a.")) probes=(1,ErrInjected,(Log "# b.")) +seek-lt a +seek-lt b +seek-lt c +seek-lt d +seek-lt e +seek-lt f +seek-lt g +seek-lt h +seek-lt i +seek-lt j +seek-lt k +seek-lt z +---- +# a.SeekGE("a") = b-d:{(#10,RANGEKEYSET,@1,apples)} +# b.SeekGE("a") = nil + err= +# a.SeekGE("b") = b-d:{(#10,RANGEKEYSET,@1,apples)} +# b.SeekGE("b") = nil + err= +# a.SeekGE("c") = b-d:{(#10,RANGEKEYSET,@1,apples)} +# b.SeekGE("c") = nil + err= +# a.SeekGE("d") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekGE("d") = nil + err= +# a.SeekGE("e") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekGE("e") = nil + err= +# a.SeekGE("f") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekGE("f") = nil + err= +# a.SeekGE("g") = e-h:{(#8,RANGEKEYDEL)} +# b.SeekGE("g") = nil + err= +# a.SeekGE("h") = nil +# b.SeekGE("h") = nil + err= +# a.SeekGE("i") = nil +# b.SeekGE("i") = nil + err= +# a.SeekGE("j") = nil +# b.SeekGE("j") = nil + err= +# a.SeekGE("k") = nil +# b.SeekGE("k") = nil + err= +# a.SeekGE("z") = nil +# b.SeekGE("z") = nil + err= + +# Test error handling during Next. + +iter probes=(0,(If OpNext ErrInjected noop),(Log "# a.")) probes=(1,(Log "# b.")) +first +next +next +next +---- +# a.First() = b-d:{(#10,RANGEKEYSET,@1,apples)} +# b.First() = a-c:{(#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +# b.Next() = h-k:{(#5,RANGEKEYDEL)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +# a.Next() = nil + err= + +iter probes=(0,(Log "# a.")) probes=(1,(If OpNext ErrInjected noop),(Log "# b.")) +first +next +next +---- +# a.First() = b-d:{(#10,RANGEKEYSET,@1,apples)} +# b.First() = a-c:{(#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +# b.Next() = nil + err= + +# Test error handling during Prev. + +iter probes=(0,(If OpPrev ErrInjected noop),(Log "# a.")) probes=(1,(Log "# b.")) +last +prev +prev +---- +# a.Last() = e-h:{(#8,RANGEKEYDEL)} +# b.Last() = h-k:{(#5,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} +# b.Prev() = a-c:{(#3,RANGEKEYUNSET,@1)} +e-h:{(#8,RANGEKEYDEL)} +# a.Prev() = nil + err= + +iter probes=(0,(Log "# a.")) probes=(1,(If OpPrev ErrInjected noop),(Log "# b.")) +last +prev +---- +# a.Last() = e-h:{(#8,RANGEKEYDEL)} +# b.Last() = h-k:{(#5,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} +# b.Prev() = nil + err= + +define +a-f:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL)} +k-s:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL)} +---- +1 levels + +iter +first +prev +next +---- +a-f:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL)} + +a-f:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL)} + +iter +last +next +prev +---- +k-s:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL)} + +k-s:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL)} + +define +w-x:{(#5,RANGEKEYDEL) (#3,RANGEKEYDEL)} +x-z:{(#5,RANGEKEYDEL)} +-- +w-y:{(#4,RANGEKEYDEL) (#1,RANGEKEYDEL)} +---- +2 levels + +iter +last +next +prev +first +prev +next +---- +y-z:{(#5,RANGEKEYDEL)} + +y-z:{(#5,RANGEKEYDEL)} +w-x:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL) (#3,RANGEKEYDEL) (#1,RANGEKEYDEL)} + +w-x:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL) (#3,RANGEKEYDEL) (#1,RANGEKEYDEL)} + +iter +seek-ge x +prev +seek-ge xray +prev +---- +x-y:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL) (#1,RANGEKEYDEL)} +w-x:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL) (#3,RANGEKEYDEL) (#1,RANGEKEYDEL)} +x-y:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL) (#1,RANGEKEYDEL)} +w-x:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL) (#3,RANGEKEYDEL) (#1,RANGEKEYDEL)} + +define +il-qb:{(#10,RANGEKEYDEL)} +sn-wn:{(#10,RANGEKEYDEL)} +-- +qt-kh:{(#9,RANGEKEYDEL) (#8,RANGEKEYDEL) (#7,RANGEKEYDEL)} +ky-sv:{(#8,RANGEKEYDEL) (#7,RANGEKEYDEL)} +-- +as-fz:{(#5,RANGEKEYDEL) (#4,RANGEKEYDEL)} +hh-ir:{(#4,RANGEKEYDEL)} +rf-yx:{(#4,RANGEKEYDEL)} +---- +3 levels + +iter +seek-ge qp +next +next +next +next +next +seek-ge yz +prev +---- +qb-rf:{(#8,RANGEKEYDEL) (#7,RANGEKEYDEL)} +rf-sn:{(#8,RANGEKEYDEL) (#7,RANGEKEYDEL) (#4,RANGEKEYDEL)} +sn-sv:{(#10,RANGEKEYDEL) (#8,RANGEKEYDEL) (#7,RANGEKEYDEL) (#4,RANGEKEYDEL)} +sv-wn:{(#10,RANGEKEYDEL) (#4,RANGEKEYDEL)} +wn-yx:{(#4,RANGEKEYDEL)} + + +wn-yx:{(#4,RANGEKEYDEL)} + +# Test that empty spans from child iterators are preserved +define +b-d:{#10,RANGEKEYSET,@1,apples} +e-f:{} +g-h:{#8,RANGEKEYDEL} +-- +a-c:{#3,RANGEKEYUNSET,@1} +h-k:{#5,RANGEKEYDEL} +k-m:{} +---- +2 levels + +iter +first +next +next +next +next +next +next +next +---- +a-b:{(#3,RANGEKEYUNSET,@1)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +c-d:{(#10,RANGEKEYSET,@1,apples)} +e-f:{} +g-h:{(#8,RANGEKEYDEL)} +h-k:{(#5,RANGEKEYDEL)} +k-m:{} + + +iter +last +prev +prev +prev +prev +prev +prev +prev +---- +k-m:{} +h-k:{(#5,RANGEKEYDEL)} +g-h:{(#8,RANGEKEYDEL)} +e-f:{} +c-d:{(#10,RANGEKEYSET,@1,apples)} +b-c:{(#10,RANGEKEYSET,@1,apples) (#3,RANGEKEYUNSET,@1)} +a-b:{(#3,RANGEKEYUNSET,@1)} + diff --git a/pebble/internal/keyspan/testdata/seek b/pebble/internal/keyspan/testdata/seek new file mode 100644 index 0000000..e75a65c --- /dev/null +++ b/pebble/internal/keyspan/testdata/seek @@ -0,0 +1,309 @@ +build +1: b-d +---- +b-d:{(#1,RANGEDEL)} + +seek-ge +a 2 +b 2 +b 1 +d 2 +---- +b-d:{(#1,RANGEDEL)} +b-d:{(#1,RANGEDEL)} +b-d:{} + + +seek-le +a 2 +b 2 +b 1 +d 2 +---- + +b-d:{(#1,RANGEDEL)} +b-d:{} +b-d:{(#1,RANGEDEL)} + +build +3: b-d +2: b-d +1: b-d +---- +b-d:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} + +seek-ge +a 4 +b 4 +b 3 +b 2 +b 1 +d 4 +---- +b-d:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +b-d:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +b-d:{(#2,RANGEDEL) (#1,RANGEDEL)} +b-d:{(#1,RANGEDEL)} +b-d:{} + + +seek-le +a 4 +b 4 +b 3 +b 2 +b 1 +d 4 +---- + +b-d:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +b-d:{(#2,RANGEDEL) (#1,RANGEDEL)} +b-d:{(#1,RANGEDEL)} +b-d:{} +b-d:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} + +build +1: b-d +2: d-f +---- +b-d:{(#1,RANGEDEL)} +d-f:{(#2,RANGEDEL)} + +seek-ge +b 2 +d 2 +d 3 +e 3 +---- +b-d:{(#1,RANGEDEL)} +d-f:{} +d-f:{(#2,RANGEDEL)} +d-f:{(#2,RANGEDEL)} + +seek-le +a 3 +b 2 +d 2 +d 3 +e 3 +f 3 +---- + +b-d:{(#1,RANGEDEL)} +d-f:{} +d-f:{(#2,RANGEDEL)} +d-f:{(#2,RANGEDEL)} +d-f:{(#2,RANGEDEL)} + +build +3: a-----------m +2: f------------s +1: j---------------z +---- +a-f:{(#3,RANGEDEL)} +f-j:{(#3,RANGEDEL) (#2,RANGEDEL)} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +m-s:{(#2,RANGEDEL) (#1,RANGEDEL)} +s-z:{(#1,RANGEDEL)} + +seek-ge +a 4 +a 3 +a 2 +a 1 +f 4 +f 3 +f 2 +f 1 +j 4 +j 3 +j 2 +j 1 +m 3 +m 2 +m 1 +s 2 +s 1 +z 2 +---- +a-f:{(#3,RANGEDEL)} +a-f:{} +a-f:{} +a-f:{} +f-j:{(#3,RANGEDEL) (#2,RANGEDEL)} +f-j:{(#2,RANGEDEL)} +f-j:{} +f-j:{} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#1,RANGEDEL)} +j-m:{} +m-s:{(#2,RANGEDEL) (#1,RANGEDEL)} +m-s:{(#1,RANGEDEL)} +m-s:{} +s-z:{(#1,RANGEDEL)} +s-z:{} + + +seek-le +a 4 +a 3 +a 2 +a 1 +f 4 +f 3 +f 2 +f 1 +j 4 +j 3 +j 2 +j 1 +m 3 +m 2 +m 1 +s 2 +s 1 +z 2 +---- +a-f:{(#3,RANGEDEL)} +a-f:{} +a-f:{} +a-f:{} +f-j:{(#3,RANGEDEL) (#2,RANGEDEL)} +f-j:{(#2,RANGEDEL)} +f-j:{} +f-j:{} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#1,RANGEDEL)} +j-m:{} +m-s:{(#2,RANGEDEL) (#1,RANGEDEL)} +m-s:{(#1,RANGEDEL)} +m-s:{} +s-z:{(#1,RANGEDEL)} +s-z:{} +s-z:{(#1,RANGEDEL)} + +build +1: a-----------m +2: f------------s +3: j---------------z +---- +a-f:{(#1,RANGEDEL)} +f-j:{(#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +m-s:{(#3,RANGEDEL) (#2,RANGEDEL)} +s-z:{(#3,RANGEDEL)} + +seek-ge +a 2 +a 1 +f 3 +f 2 +f 1 +j 4 +j 3 +j 2 +j 1 +m 4 +m 3 +m 2 +m 1 +s 4 +s 3 +s 2 +s 1 +z 4 +---- +a-f:{(#1,RANGEDEL)} +a-f:{} +f-j:{(#2,RANGEDEL) (#1,RANGEDEL)} +f-j:{(#1,RANGEDEL)} +f-j:{} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#1,RANGEDEL)} +j-m:{} +m-s:{(#3,RANGEDEL) (#2,RANGEDEL)} +m-s:{(#2,RANGEDEL)} +m-s:{} +m-s:{} +s-z:{(#3,RANGEDEL)} +s-z:{} +s-z:{} +s-z:{} + + +seek-le +a 2 +a 1 +f 3 +f 2 +f 1 +j 4 +j 3 +j 2 +j 1 +m 4 +m 3 +m 2 +m 1 +s 4 +s 3 +s 2 +s 1 +z 4 +z 3 +z 2 +---- +a-f:{(#1,RANGEDEL)} +a-f:{} +f-j:{(#2,RANGEDEL) (#1,RANGEDEL)} +f-j:{(#1,RANGEDEL)} +f-j:{} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#2,RANGEDEL) (#1,RANGEDEL)} +j-m:{(#1,RANGEDEL)} +j-m:{} +m-s:{(#3,RANGEDEL) (#2,RANGEDEL)} +m-s:{(#2,RANGEDEL)} +m-s:{} +m-s:{} +s-z:{(#3,RANGEDEL)} +s-z:{} +s-z:{} +s-z:{} +s-z:{(#3,RANGEDEL)} +s-z:{} +s-z:{} + +build +1: a-c +3: a-c +5: a-c +5: c-e +---- +a-c:{(#5,RANGEDEL) (#3,RANGEDEL) (#1,RANGEDEL)} +c-e:{(#5,RANGEDEL)} + +# Regression test for a bug where seek-le was failing to find the most recent +# version of a tombstone. The bug existed when seek-{ge,le} performed snapshot +# filtering, and the problematic case was "seek-le c 4". The seeking code was +# finding the tombstone c-e#5, determining it wasn't visible and then return the +# immediately preceding tombstone a-c#1. Now we return c-e:{} immediately, +# because the span c-e covers c and contains no visible keys. + +seek-le +c 1 +c 2 +c 3 +c 4 +c 5 +c 6 +---- +c-e:{} +c-e:{} +c-e:{} +c-e:{} +c-e:{} +c-e:{(#5,RANGEDEL)} diff --git a/pebble/internal/keyspan/testdata/truncate b/pebble/internal/keyspan/testdata/truncate new file mode 100644 index 0000000..33ab3a5 --- /dev/null +++ b/pebble/internal/keyspan/testdata/truncate @@ -0,0 +1,318 @@ +build +1: b-d +2: d-f +3: f-h +---- +1: b-d +2: d-f +3: f-h + + +truncate a-b +---- + +truncate a-c +---- +1: bc + +truncate a-d +---- +1: b-d + +truncate a-e +---- +1: b-d +2: de + +# The second range tombstone should be elided, as it starts after the +# specified file end key. + +truncate a-e endKey=(d.SET.3) +---- +1: b-d + +# The second range tombstone should be back in the below example, as the +# specified end key has a trailer (RANGEDEL.2) exactly matching that of the +# rangedel tombstone's start key. + +truncate a-e endKey=(d.RANGEDEL.2) +---- +1: b-d +2: de + +truncate a-e endKey=(d.SET.1) +---- +1: b-d +2: de + +# Similarly, truncate range tombstones that end before the start key. + +truncate a-e startKey=(d.SET.3) +---- +2: de + +truncate a-e startKey=(c.SET.3) +---- +1: b-d +2: de + +truncate a-f +---- +1: b-d +2: d-f + +truncate a-g +---- +1: b-d +2: d-f +3: fg + +truncate a-h +---- +1: b-d +2: d-f +3: f-h + + +truncate b-b +---- + +truncate b-c +---- +1: bc + +truncate b-d +---- +1: b-d + +truncate b-e +---- +1: b-d +2: de + +truncate b-f +---- +1: b-d +2: d-f + +truncate b-g +---- +1: b-d +2: d-f +3: fg + +truncate b-h +---- +1: b-d +2: d-f +3: f-h + + +truncate c-c +---- + +truncate c-d +---- +1: cd + +truncate c-e +---- +1: cd +2: de + +truncate c-f +---- +1: cd +2: d-f + +truncate c-g +---- +1: cd +2: d-f +3: fg + +truncate c-h +---- +1: cd +2: d-f +3: f-h + + +truncate d-d +---- + +truncate d-e +---- +2: de + +truncate d-f +---- +2: d-f + +truncate d-g +---- +2: d-f +3: fg + +truncate d-h +---- +2: d-f +3: f-h + + +truncate e-e +---- + +truncate e-f +---- +2: ef + +truncate e-g +---- +2: ef +3: fg + +truncate e-h +---- +2: ef +3: f-h + + +truncate f-f +---- + +truncate f-g +---- +3: fg + +truncate f-h +---- +3: f-h + + +truncate g-g +---- + +truncate g-h +---- +3: gh + +# Regression test for https://github.com/cockroachdb/cockroach/issues/113973. + +truncate-and-save-iter a-dd +---- +ok + +saved-iter +first +next +next +next +---- +b-d:{(#1,RANGEDEL)} +d-dd:{(#2,RANGEDEL)} + + + +saved-iter +seek-ge e +next +next +---- + + + + +saved-iter +seek-ge e +prev +prev +---- + +d-dd:{(#2,RANGEDEL)} +b-d:{(#1,RANGEDEL)} + +saved-iter +seek-lt e +prev +prev +---- +d-dd:{(#2,RANGEDEL)} +b-d:{(#1,RANGEDEL)} + + +saved-iter +seek-lt e +next +next +---- +d-dd:{(#2,RANGEDEL)} + + + +truncate-and-save-iter ee-h +---- +ok + +saved-iter +first +next +next +next +---- +ee-f:{(#2,RANGEDEL)} +f-h:{(#3,RANGEDEL)} + + + +saved-iter +seek-ge e +next +next +---- +ee-f:{(#2,RANGEDEL)} +f-h:{(#3,RANGEDEL)} + + +saved-iter +seek-ge e +prev +prev +---- +ee-f:{(#2,RANGEDEL)} + + + +saved-iter +seek-lt e +prev +prev +---- + + + + +saved-iter +seek-lt e +next +next +---- + +ee-f:{(#2,RANGEDEL)} +f-h:{(#3,RANGEDEL)} + + +truncate-and-save-iter a-g +---- +ok + +saved-iter +seek-ge h +prev +seek-lt h +next +---- + +f-g:{(#3,RANGEDEL)} +f-g:{(#3,RANGEDEL)} + diff --git a/pebble/internal/keyspan/testdata/visible b/pebble/internal/keyspan/testdata/visible new file mode 100644 index 0000000..6a3b14b --- /dev/null +++ b/pebble/internal/keyspan/testdata/visible @@ -0,0 +1,58 @@ +define +a-b:{(#5,RANGEKEYSET) (#3,RANGEKEYSET)} +---- +a-b:{(#5,RANGEKEYSET) (#3,RANGEKEYSET)} + +visible +6 +5 +4 +3 +2 +1 +---- +6 : a-b:{(#5,RANGEKEYSET) (#3,RANGEKEYSET)} +5 : a-b:{(#3,RANGEKEYSET)} +4 : a-b:{(#3,RANGEKEYSET)} +3 : a-b:{} +2 : a-b:{} +1 : a-b:{} + +define +a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET)} +---- +a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET)} + +visible +5 +1 +---- +5 : a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET)} +1 : a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET)} + +define +a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#10,RANGEKEYSET) (#9,RANGEKEYSET) (#4,RANGEKEYSET) (#1,RANGEKEYSET)} +---- +a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#10,RANGEKEYSET) (#9,RANGEKEYSET) (#4,RANGEKEYSET) (#1,RANGEKEYSET)} + +# Test 'sandwich cases'. Eg, at snapshot=7 the keys at #10 and #9 are invisible, +# but the batch keys and the keys at #4 and #1 are visible. + +visible +12 +10 +8 +7 +4 +3 +2 +1 +---- +12: a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#10,RANGEKEYSET) (#9,RANGEKEYSET) (#4,RANGEKEYSET) (#1,RANGEKEYSET)} +10: a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#9,RANGEKEYSET) (#4,RANGEKEYSET) (#1,RANGEKEYSET)} +8 : a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#4,RANGEKEYSET) (#1,RANGEKEYSET)} +7 : a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#4,RANGEKEYSET) (#1,RANGEKEYSET)} +4 : a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#1,RANGEKEYSET)} +3 : a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#1,RANGEKEYSET)} +2 : a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#1,RANGEKEYSET)} +1 : a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET)} diff --git a/pebble/internal/keyspan/testdata/visible_at b/pebble/internal/keyspan/testdata/visible_at new file mode 100644 index 0000000..6c8d56b --- /dev/null +++ b/pebble/internal/keyspan/testdata/visible_at @@ -0,0 +1,58 @@ +define +a-b:{(#5,RANGEKEYSET) (#3,RANGEKEYSET)} +---- +a-b:{(#5,RANGEKEYSET) (#3,RANGEKEYSET)} + +visible-at +6 +5 +4 +3 +2 +1 +---- +6 : true +5 : true +4 : true +3 : false +2 : false +1 : false + +# NB: #36028797018963996 and #36028797018963995 are sequence numbers with the +# batch bit set. These keys should always be visible. + +define +a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET)} +---- +a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET)} + +visible-at +5 +1 +---- +5 : true +1 : true + +define +a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#10,RANGEKEYSET) (#9,RANGEKEYSET) (#4,RANGEKEYSET) (#1,RANGEKEYSET)} +---- +a-c:{(#36028797018963996,RANGEKEYSET) (#36028797018963995,RANGEKEYSET) (#10,RANGEKEYSET) (#9,RANGEKEYSET) (#4,RANGEKEYSET) (#1,RANGEKEYSET)} + +visible-at +12 +10 +8 +7 +4 +3 +2 +1 +---- +12: true +10: true +8 : true +7 : true +4 : true +3 : true +2 : true +1 : true diff --git a/pebble/internal/keyspan/transformer.go b/pebble/internal/keyspan/transformer.go new file mode 100644 index 0000000..b5e8735 --- /dev/null +++ b/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/pebble/internal/keyspan/truncate.go b/pebble/internal/keyspan/truncate.go new file mode 100644 index 0000000..c0e609b --- /dev/null +++ b/pebble/internal/keyspan/truncate.go @@ -0,0 +1,73 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package keyspan + +import "github.com/cockroachdb/pebble/internal/base" + +// Truncate creates a new iterator where every span in the supplied iterator is +// 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, + 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. 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 + // is sufficient. + if start != nil && cmp(in.End, start.UserKey) <= 0 { + return false + } + if end != nil { + v := cmp(in.Start, end.UserKey) + switch { + case v > 0: + // Wholly outside the end bound. Skip it. + return false + case v == 0: + // This span begins at the same user key as `end`. Whether or + // not any of the keys contained within the span are relevant is + // dependent on Trailers. Any keys contained within the span + // with trailers larger than end cover the small sliver of + // keyspace between [k#inf, k#]. Since keys are + // sorted descending by Trailer within the span, we need to find + // the prefix of keys with larger trailers. + for i := range in.Keys { + if in.Keys[i].Trailer < end.Trailer { + out.Keys = out.Keys[:i] + break + } + } + default: + // 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/pebble/internal/keyspan/truncate_test.go b/pebble/internal/keyspan/truncate_test.go new file mode 100644 index 0000000..f2b2793 --- /dev/null +++ b/pebble/internal/keyspan/truncate_test.go @@ -0,0 +1,94 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package keyspan + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" +) + +func TestTruncate(t *testing.T) { + cmp := base.DefaultComparer.Compare + fmtKey := base.DefaultComparer.FormatKey + var iter FragmentIterator + var savedIter FragmentIterator + defer func() { + if savedIter != nil { + savedIter.Close() + savedIter = nil + } + }() + + datadriven.RunTest(t, "testdata/truncate", func(t *testing.T, d *datadriven.TestData) string { + doTruncate := func() FragmentIterator { + if len(d.Input) > 0 { + t.Fatalf("unexpected input: %s", d.Input) + } + if len(d.CmdArgs) < 1 || len(d.CmdArgs) > 3 { + t.Fatalf("expected 1-3 arguments: %s", d.CmdArgs) + } + parts := strings.Split(d.CmdArgs[0].String(), "-") + var startKey, endKey *base.InternalKey + if len(d.CmdArgs) > 1 { + for _, arg := range d.CmdArgs[1:] { + switch arg.Key { + case "startKey": + startKey = &base.InternalKey{} + *startKey = base.ParseInternalKey(arg.Vals[0]) + case "endKey": + endKey = &base.InternalKey{} + *endKey = base.ParseInternalKey(arg.Vals[0]) + } + } + } + if len(parts) != 2 { + t.Fatalf("malformed arg: %s", d.CmdArgs[0]) + } + lower := []byte(parts[0]) + upper := []byte(parts[1]) + + tIter := Truncate( + cmp, iter, lower, upper, startKey, endKey, false, + ) + return tIter + } + + switch d.Cmd { + case "build": + tombstones := buildSpans(t, cmp, fmtKey, d.Input, base.InternalKeyKindRangeDelete) + iter = NewIter(cmp, tombstones) + return formatAlphabeticSpans(tombstones) + + case "truncate": + tIter := doTruncate() + defer tIter.Close() + var truncated []Span + for s := tIter.First(); s != nil; s = tIter.Next() { + truncated = append(truncated, s.ShallowClone()) + } + return formatAlphabeticSpans(truncated) + + case "truncate-and-save-iter": + if savedIter != nil { + savedIter.Close() + } + savedIter = doTruncate() + return "ok" + + case "saved-iter": + var buf bytes.Buffer + runIterCmd(t, d, savedIter, &buf) + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} diff --git a/pebble/internal/lint/lint.go b/pebble/internal/lint/lint.go new file mode 100644 index 0000000..338a34a --- /dev/null +++ b/pebble/internal/lint/lint.go @@ -0,0 +1,5 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package lint diff --git a/pebble/internal/lint/lint_test.go b/pebble/internal/lint/lint_test.go new file mode 100644 index 0000000..e088d69 --- /dev/null +++ b/pebble/internal/lint/lint_test.go @@ -0,0 +1,301 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package lint + +import ( + "bytes" + "fmt" + "go/build" + "os/exec" + "regexp" + "runtime" + "strings" + "testing" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/ghemawat/stream" + "github.com/stretchr/testify/require" +) + +const ( + cmdGo = "go" + golint = "golang.org/x/lint/golint@6edffad5e6160f5949cdefc81710b2706fbcd4f6" + staticcheck = "honnef.co/go/tools/cmd/staticcheck@2023.1" + crlfmt = "github.com/cockroachdb/crlfmt@44a36ec7" +) + +func dirCmd(t *testing.T, dir string, name string, args ...string) stream.Filter { + cmd := exec.Command(name, args...) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + switch err.(type) { + case nil: + case *exec.ExitError: + // Non-zero exit is expected. + default: + require.NoError(t, err) + } + return stream.ReadLines(bytes.NewReader(out)) +} + +func ignoreGoMod() stream.Filter { + return stream.GrepNot(`^go: (finding|extracting|downloading)`) +} + +func TestLint(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("lint checks skipped on Windows") + } + if invariants.RaceEnabled { + // We are not interested in race-testing the linters themselves. + t.Skip("lint checks skipped on race builds") + } + + const root = "github.com/cockroachdb/pebble" + + pkg, err := build.Import(root, "../..", 0) + require.NoError(t, err) + + var pkgs []string + if err := stream.ForEach( + stream.Sequence( + dirCmd(t, pkg.Dir, "go", "list", "./..."), + ignoreGoMod(), + ), func(s string) { + pkgs = append(pkgs, s) + }); err != nil { + require.NoError(t, err) + } + + t.Run("TestGolint", func(t *testing.T) { + t.Parallel() + + args := []string{"run", golint} + args = append(args, pkgs...) + + // This is overkill right now, but provides a structure for filtering out + // lint errors we don't care about. + if err := stream.ForEach( + stream.Sequence( + dirCmd(t, pkg.Dir, cmdGo, args...), + stream.GrepNot("go: downloading"), + ), func(s string) { + t.Errorf("\n%s", s) + }); err != nil { + t.Error(err) + } + }) + + t.Run("TestStaticcheck", func(t *testing.T) { + t.Parallel() + + args := []string{"run", staticcheck} + args = append(args, pkgs...) + + if err := stream.ForEach( + stream.Sequence( + dirCmd(t, pkg.Dir, cmdGo, args...), + stream.GrepNot("go: downloading"), + ), func(s string) { + t.Errorf("\n%s", s) + }); err != nil { + t.Error(err) + } + }) + + t.Run("TestGoVet", func(t *testing.T) { + t.Parallel() + + if err := stream.ForEach( + stream.Sequence( + dirCmd(t, pkg.Dir, "go", "vet", "-all", "./..."), + stream.GrepNot(`^#`), // ignore comment lines + ignoreGoMod(), + ), func(s string) { + t.Errorf("\n%s", s) + }); err != nil { + t.Error(err) + } + }) + + t.Run("TestFmtErrorf", func(t *testing.T) { + t.Parallel() + + if err := stream.ForEach( + dirCmd(t, pkg.Dir, "git", "grep", "fmt\\.Errorf("), + func(s string) { + t.Errorf("\n%s <- please use \"errors.Errorf\" instead", s) + }); err != nil { + t.Error(err) + } + }) + + t.Run("TestOSIsErr", func(t *testing.T) { + t.Parallel() + + if err := stream.ForEach( + dirCmd(t, pkg.Dir, "git", "grep", "os\\.Is"), + func(s string) { + t.Errorf("\n%s <- please use the \"oserror\" equivalent instead", s) + }); err != nil { + t.Error(err) + } + }) + + t.Run("TestSetFinalizer", func(t *testing.T) { + t.Parallel() + + if err := stream.ForEach( + stream.Sequence( + dirCmd(t, pkg.Dir, "git", "grep", "-B1", "runtime\\.SetFinalizer("), + lintIgnore("lint:ignore SetFinalizer"), + stream.GrepNot(`^internal/invariants/finalizer_on.go`), + ), func(s string) { + t.Errorf("\n%s <- please use the \"invariants.SetFinalizer\" equivalent instead", s) + }); err != nil { + t.Error(err) + } + }) + + // Disallow "raw" atomics; wrappers like atomic.Int32 provide much better + // safety and alignment guarantees. + t.Run("TestRawAtomics", func(t *testing.T) { + t.Parallel() + if err := stream.ForEach( + stream.Sequence( + dirCmd(t, pkg.Dir, "git", "grep", `atomic\.\(Load\|Store\|Add\|Swap\|Compare\)`), + lintIgnore("lint:ignore RawAtomics"), + ), func(s string) { + t.Errorf("\n%s <- please use atomic wrappers (like atomic.Int32) instead", s) + }); err != nil { + t.Error(err) + } + }) + + t.Run("TestForbiddenImports", func(t *testing.T) { + t.Parallel() + + // Forbidden-import-pkg -> permitted-replacement-pkg + forbiddenImports := map[string]string{ + "errors": "github.com/cockroachdb/errors", + "pkg/errors": "github.com/cockroachdb/errors", + } + + // grepBuf creates a grep string that matches any forbidden import pkgs. + var grepBuf bytes.Buffer + grepBuf.WriteByte('(') + for forbiddenPkg := range forbiddenImports { + grepBuf.WriteByte('|') + grepBuf.WriteString(regexp.QuoteMeta(forbiddenPkg)) + } + grepBuf.WriteString(")$") + + filter := stream.FilterFunc(func(arg stream.Arg) error { + for _, path := range pkgs { + buildContext := build.Default + buildContext.UseAllFiles = true + importPkg, err := buildContext.Import(path, pkg.Dir, 0) + if _, ok := err.(*build.MultiplePackageError); ok { + buildContext.UseAllFiles = false + importPkg, err = buildContext.Import(path, pkg.Dir, 0) + } + + switch err.(type) { + case nil: + for _, s := range importPkg.Imports { + arg.Out <- importPkg.ImportPath + ": " + s + } + for _, s := range importPkg.TestImports { + arg.Out <- importPkg.ImportPath + ": " + s + } + for _, s := range importPkg.XTestImports { + arg.Out <- importPkg.ImportPath + ": " + s + } + case *build.NoGoError: + default: + return errors.Wrapf(err, "error loading package %s", path) + } + } + return nil + }) + if err := stream.ForEach(stream.Sequence( + filter, + stream.Sort(), + stream.Uniq(), + stream.Grep(grepBuf.String()), + ), func(s string) { + pkgStr := strings.Split(s, ": ") + importedPkg := pkgStr[1] + + // Test that a disallowed package is not imported. + if replPkg, ok := forbiddenImports[importedPkg]; ok { + t.Errorf("\n%s <- please use %q instead of %q", s, replPkg, importedPkg) + } + }); err != nil { + t.Error(err) + } + }) + + t.Run("TestCrlfmt", func(t *testing.T) { + t.Parallel() + + args := []string{"run", crlfmt, "-fast", "-tab", "2", "."} + var buf bytes.Buffer + if err := stream.ForEach( + stream.Sequence( + dirCmd(t, pkg.Dir, cmdGo, args...), + stream.GrepNot("go: downloading"), + ), + func(s string) { + fmt.Fprintln(&buf, s) + }); err != nil { + t.Error(err) + } + errs := buf.String() + if len(errs) > 0 { + t.Errorf("\n%s", errs) + } + + if t.Failed() { + reWriteCmd := []string{crlfmt, "-w"} + reWriteCmd = append(reWriteCmd, args...) + t.Logf("run the following to fix your formatting:\n"+ + "\n%s\n\n"+ + "Don't forget to add amend the result to the correct commits.", + strings.Join(reWriteCmd, " "), + ) + } + }) +} + +// lintIgnore is a stream.FilterFunc that filters out lines that are preceded by +// the given ignore directive. The function assumes the input stream receives a +// sequence of strings that are to be considered as pairs. If the first string +// in the sequence matches the ignore directive, the following string is +// dropped, else it is emitted. +// +// For example, given the sequence "foo", "bar", "baz", "bam", and an ignore +// directive "foo", the sequence "baz", "bam" would be emitted. If the directive +// was "baz", the sequence "foo", "bar" would be emitted. +func lintIgnore(ignore string) stream.FilterFunc { + return func(arg stream.Arg) error { + var prev string + var i int + for s := range arg.In { + if i%2 == 0 { + // Fist string in the pair is used as the filter. Store it. + prev = s + } else { + // Second string is emitted only if it _does not_ match the directive. + if !strings.Contains(prev, ignore) { + arg.Out <- s + } + } + i++ + } + return nil + } +} diff --git a/pebble/internal/manifest/btree.go b/pebble/internal/manifest/btree.go new file mode 100644 index 0000000..dd17834 --- /dev/null +++ b/pebble/internal/manifest/btree.go @@ -0,0 +1,1304 @@ +// Copyright 2020 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 manifest + +import ( + "bytes" + "fmt" + "strings" + "sync/atomic" + "unsafe" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/invariants" + stdcmp "github.com/cockroachdb/pebble/shims/cmp" +) + +// The Annotator type defined below is used by other packages to lazily +// compute a value over a B-Tree. Each node of the B-Tree stores one +// `annotation` per annotator, containing the result of the computation over +// the node's subtree. +// +// An annotation is marked as valid if it's current with the current subtree +// state. Annotations are marked as invalid whenever a node will be mutated +// (in mut). Annotators may also return `false` from `Accumulate` to signal +// that a computation for a file is not stable and may change in the future. +// Annotations that include these unstable values are also marked as invalid +// on the node, ensuring that future queries for the annotation will recompute +// the value. + +// An Annotator defines a computation over a level's FileMetadata. If the +// computation is stable and uses inputs that are fixed for the lifetime of +// a FileMetadata, the LevelMetadata's internal data structures are annotated +// with the intermediary computations. This allows the computation to be +// computed incrementally as edits are applied to a level. +type Annotator interface { + // Zero returns the zero value of an annotation. This value is returned + // when a LevelMetadata is empty. The dst argument, if non-nil, is an + // obsolete value previously returned by this Annotator and may be + // overwritten and reused to avoid a memory allocation. + Zero(dst interface{}) (v interface{}) + + // Accumulate computes the annotation for a single file in a level's + // metadata. It merges the file's value into dst and returns a bool flag + // indicating whether or not the value is stable and okay to cache as an + // annotation. If the file's value may change over the life of the file, + // the annotator must return false. + // + // Implementations may modify dst and return it to avoid an allocation. + Accumulate(m *FileMetadata, dst interface{}) (v interface{}, cacheOK bool) + + // Merge combines two values src and dst, returning the result. + // Implementations may modify dst and return it to avoid an allocation. + Merge(src interface{}, dst interface{}) interface{} +} + +type btreeCmp func(*FileMetadata, *FileMetadata) int + +func btreeCmpSeqNum(a, b *FileMetadata) int { + return a.cmpSeqNum(b) +} + +func btreeCmpSmallestKey(cmp Compare) btreeCmp { + return func(a, b *FileMetadata) int { + return a.cmpSmallestKey(b, cmp) + } +} + +// btreeCmpSpecificOrder is used in tests to construct a B-Tree with a +// specific ordering of FileMetadata within the tree. It's typically used to +// test consistency checking code that needs to construct a malformed B-Tree. +func btreeCmpSpecificOrder(files []*FileMetadata) btreeCmp { + m := map[*FileMetadata]int{} + for i, f := range files { + m[f] = i + } + return func(a, b *FileMetadata) int { + ai, aok := m[a] + bi, bok := m[b] + if !aok || !bok { + panic("btreeCmpSliceOrder called with unknown files") + } + return stdcmp.Compare(ai, bi) + } +} + +const ( + degree = 16 + maxItems = 2*degree - 1 + minItems = degree - 1 +) + +type annotation struct { + annotator Annotator + // v is an annotation value, the output of either + // annotator.Value or annotator.Merge. + v interface{} + // valid indicates whether future reads of the annotation may use v as-is. + // If false, v will be zeroed and recalculated. + valid bool +} + +type leafNode struct { + ref atomic.Int32 + count int16 + leaf bool + // subtreeCount holds the count of files in the entire subtree formed by + // this node. For leaf nodes, subtreeCount is always equal to count. For + // non-leaf nodes, it's the sum of count plus all the children's + // subtreeCounts. + // + // NB: We could move this field to the end of the node struct, since leaf => + // count=subtreeCount, however the unsafe casting [leafToNode] performs make + // it risky and cumbersome. + subtreeCount int + items [maxItems]*FileMetadata + // annot contains one annotation per annotator, merged over the entire + // node's files (and all descendants for non-leaf nodes). + annot []annotation +} + +type node struct { + leafNode + children [maxItems + 1]*node +} + +//go:nocheckptr casts a ptr to a smaller struct to a ptr to a larger struct. +func leafToNode(ln *leafNode) *node { + return (*node)(unsafe.Pointer(ln)) +} + +func newLeafNode() *node { + n := leafToNode(new(leafNode)) + n.leaf = true + n.ref.Store(1) + return n +} + +func newNode() *node { + n := new(node) + n.ref.Store(1) + return n +} + +// mut creates and returns a mutable node reference. If the node is not shared +// with any other trees then it can be modified in place. Otherwise, it must be +// cloned to ensure unique ownership. In this way, we enforce a copy-on-write +// policy which transparently incorporates the idea of local mutations, like +// Clojure's transients or Haskell's ST monad, where nodes are only copied +// during the first time that they are modified between Clone operations. +// +// When a node is cloned, the provided pointer will be redirected to the new +// mutable node. +func mut(n **node) *node { + if (*n).ref.Load() == 1 { + // Exclusive ownership. Can mutate in place. + + // Whenever a node will be mutated, reset its annotations to be marked + // as uncached. This ensures any future calls to (*node).annotation + // will recompute annotations on the modified subtree. + for i := range (*n).annot { + (*n).annot[i].valid = false + } + return *n + } + // If we do not have unique ownership over the node then we + // clone it to gain unique ownership. After doing so, we can + // release our reference to the old node. We pass recursive + // as true because even though we just observed the node's + // reference count to be greater than 1, we might be racing + // with another call to decRef on this node. + c := (*n).clone() + (*n).decRef(true /* contentsToo */, nil) + *n = c + // NB: We don't need to clear annotations, because (*node).clone does not + // copy them. + return *n +} + +// incRef acquires a reference to the node. +func (n *node) incRef() { + n.ref.Add(1) +} + +// decRef releases a reference to the node. If requested, the method will unref +// its items and recurse into child nodes and decrease their refcounts as well. +// Some internal codepaths that manually copy the node's items or children to +// 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 *[]*FileBacking) { + if n.ref.Add(-1) > 0 { + // Other references remain. Can't free. + return + } + + // Dereference the node's metadata and release child references if + // requested. Some internal callers may not want to propagate the deref + // because they're manually copying the filemetadata and children to other + // nodes, and they want to preserve the existing reference count. + if contentsToo { + for _, f := range n.items[:n.count] { + 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 + // non-nil. + if obsolete == nil { + panic(fmt.Sprintf("file metadata %s dereferenced to zero during tree mutation", f.FileNum)) + } + // 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 { + for i := int16(0); i <= n.count; i++ { + n.children[i].decRef(true /* contentsToo */, obsolete) + } + } + } +} + +// clone creates a clone of the receiver with a single reference count. +func (n *node) clone() *node { + var c *node + if n.leaf { + c = newLeafNode() + } else { + c = newNode() + } + // NB: copy field-by-field without touching n.ref to avoid + // triggering the race detector and looking like a data race. + c.count = n.count + c.items = n.items + c.subtreeCount = n.subtreeCount + // Increase the refcount of each contained item. + for _, f := range n.items[:n.count] { + f.Ref() + } + if !c.leaf { + // Copy children and increase each refcount. + c.children = n.children + for i := int16(0); i <= c.count; i++ { + c.children[i].incRef() + } + } + return c +} + +// insertAt inserts the provided file and node at the provided index. This +// function is for use only as a helper function for internal B-Tree code. +// Clients should not invoke it directly. +func (n *node) insertAt(index int, item *FileMetadata, nd *node) { + if index < int(n.count) { + copy(n.items[index+1:n.count+1], n.items[index:n.count]) + if !n.leaf { + copy(n.children[index+2:n.count+2], n.children[index+1:n.count+1]) + } + } + n.items[index] = item + if !n.leaf { + n.children[index+1] = nd + } + n.count++ +} + +// pushBack inserts the provided file and node at the tail of the node's items. +// This function is for use only as a helper function for internal B-Tree code. +// Clients should not invoke it directly. +func (n *node) pushBack(item *FileMetadata, nd *node) { + n.items[n.count] = item + if !n.leaf { + n.children[n.count+1] = nd + } + n.count++ +} + +// pushFront inserts the provided file and node at the head of the +// node's items. This function is for use only as a helper function for internal B-Tree +// code. Clients should not invoke it directly. +func (n *node) pushFront(item *FileMetadata, nd *node) { + if !n.leaf { + copy(n.children[1:n.count+2], n.children[:n.count+1]) + n.children[0] = nd + } + copy(n.items[1:n.count+1], n.items[:n.count]) + n.items[0] = item + n.count++ +} + +// removeAt removes a value at a given index, pulling all subsequent values +// back. This function is for use only as a helper function for internal B-Tree +// code. Clients should not invoke it directly. +func (n *node) removeAt(index int) (*FileMetadata, *node) { + var child *node + if !n.leaf { + child = n.children[index+1] + copy(n.children[index+1:n.count], n.children[index+2:n.count+1]) + n.children[n.count] = nil + } + n.count-- + out := n.items[index] + copy(n.items[index:n.count], n.items[index+1:n.count+1]) + n.items[n.count] = nil + return out, child +} + +// popBack removes and returns the last element in the list. This function is +// for use only as a helper function for internal B-Tree code. Clients should +// not invoke it directly. +func (n *node) popBack() (*FileMetadata, *node) { + n.count-- + out := n.items[n.count] + n.items[n.count] = nil + if n.leaf { + return out, nil + } + child := n.children[n.count+1] + n.children[n.count+1] = nil + return out, child +} + +// popFront removes and returns the first element in the list. This function is +// for use only as a helper function for internal B-Tree code. Clients should +// not invoke it directly. +func (n *node) popFront() (*FileMetadata, *node) { + n.count-- + var child *node + if !n.leaf { + child = n.children[0] + copy(n.children[:n.count+1], n.children[1:n.count+2]) + n.children[n.count+1] = nil + } + out := n.items[0] + copy(n.items[:n.count], n.items[1:n.count+1]) + n.items[n.count] = nil + return out, child +} + +// find returns the index where the given item should be inserted into this +// list. 'found' is true if the item already exists in the list at the given +// index. +// +// This function is for use only as a helper function for internal B-Tree code. +// Clients should not invoke it directly. +func (n *node) find(cmp btreeCmp, item *FileMetadata) (index int, found bool) { + // Logic copied from sort.Search. Inlining this gave + // an 11% speedup on BenchmarkBTreeDeleteInsert. + i, j := 0, int(n.count) + for i < j { + h := int(uint(i+j) >> 1) // avoid overflow when computing h + // i ≤ h < j + v := cmp(item, n.items[h]) + if v == 0 { + return h, true + } else if v > 0 { + i = h + 1 + } else { + j = h + } + } + return i, false +} + +// split splits the given node at the given index. The current node shrinks, +// and this function returns the item that existed at that index and a new +// node containing all items/children after it. +// +// split is called when we want to perform a transformation like the one +// depicted in the following diagram. +// +// Before: +// +-----------+ +// n *node | x y z | +// +--/-/-\-\--+ +// +// After: +// +-----------+ +// | y | n's parent +// +----/-\----+ +// / \ +// v v +// +-----------+ +-----------+ +// n *node | x | | z | next *node +// +-----------+ +-----------+ +// +// split does not perform the complete transformation; the caller is responsible +// for updating the parent appropriately. split splits `n` into two nodes, `n` +// and `next`, returning `next` and the file that separates them. In the diagram +// above, `n.split` removes y and z from `n`, returning y in the first return +// value and `next` in the second return value. The caller is responsible for +// updating n's parent to now contain `y` as the separator between nodes `n` and +// `next`. +// +// This function is for use only as a helper function for internal B-Tree code. +// Clients should not invoke it directly. +func (n *node) split(i int) (*FileMetadata, *node) { + out := n.items[i] + var next *node + if n.leaf { + next = newLeafNode() + } else { + next = newNode() + } + next.count = n.count - int16(i+1) + copy(next.items[:], n.items[i+1:n.count]) + for j := int16(i); j < n.count; j++ { + n.items[j] = nil + } + if !n.leaf { + copy(next.children[:], n.children[i+1:n.count+1]) + descendantsMoved := 0 + for j := int16(i + 1); j <= n.count; j++ { + descendantsMoved += n.children[j].subtreeCount + n.children[j] = nil + } + n.subtreeCount -= descendantsMoved + next.subtreeCount += descendantsMoved + } + n.count = int16(i) + // NB: We subtract one more than `next.count` from n's subtreeCount because + // the item at index `i` was removed from `n.items`. We'll return the item + // at index `i`, and the caller is responsible for updating the subtree + // count of whichever node adopts it. + n.subtreeCount -= int(next.count) + 1 + next.subtreeCount += int(next.count) + return out, next +} + +// Insert inserts a item into the subtree rooted at this node, making sure no +// nodes in the subtree exceed maxItems items. +func (n *node) Insert(cmp btreeCmp, item *FileMetadata) error { + i, found := n.find(cmp, item) + if found { + // cmp provides a total ordering of the files within a level. + // If we're inserting a metadata that's equal to an existing item + // in the tree, we're inserting a file into a level twice. + return errors.Errorf("files %s and %s collided on sort keys", + errors.Safe(item.FileNum), errors.Safe(n.items[i].FileNum)) + } + if n.leaf { + n.insertAt(i, item, nil) + n.subtreeCount++ + return nil + } + if n.children[i].count >= maxItems { + splitLa, splitNode := mut(&n.children[i]).split(maxItems / 2) + n.insertAt(i, splitLa, splitNode) + + switch cmp := cmp(item, n.items[i]); { + case cmp < 0: + // no change, we want first split node + case cmp > 0: + i++ // we want second split node + default: + // cmp provides a total ordering of the files within a level. + // If we're inserting a metadata that's equal to an existing item + // in the tree, we're inserting a file into a level twice. + return errors.Errorf("files %s and %s collided on sort keys", + errors.Safe(item.FileNum), errors.Safe(n.items[i].FileNum)) + } + } + + err := mut(&n.children[i]).Insert(cmp, item) + if err == nil { + n.subtreeCount++ + } + return err +} + +// removeMax removes and returns the maximum item from the subtree rooted at +// this node. This function is for use only as a helper function for internal +// B-Tree code. Clients should not invoke it directly. +func (n *node) removeMax() *FileMetadata { + if n.leaf { + n.count-- + n.subtreeCount-- + out := n.items[n.count] + n.items[n.count] = nil + return out + } + child := mut(&n.children[n.count]) + if child.count <= minItems { + n.rebalanceOrMerge(int(n.count)) + return n.removeMax() + } + n.subtreeCount-- + return child.removeMax() +} + +// Remove removes a item from the subtree rooted at this node. Returns +// the item that was removed or nil if no matching item was found. +func (n *node) Remove(cmp btreeCmp, item *FileMetadata) (out *FileMetadata) { + i, found := n.find(cmp, item) + if n.leaf { + if found { + out, _ = n.removeAt(i) + n.subtreeCount-- + return out + } + return nil + } + if n.children[i].count <= minItems { + // Child not large enough to remove from. + n.rebalanceOrMerge(i) + return n.Remove(cmp, item) + } + child := mut(&n.children[i]) + if found { + // Replace the item being removed with the max item in our left child. + out = n.items[i] + n.items[i] = child.removeMax() + n.subtreeCount-- + return out + } + // File is not in this node and child is large enough to remove from. + out = child.Remove(cmp, item) + if out != nil { + n.subtreeCount-- + } + return out +} + +// rebalanceOrMerge grows child 'i' to ensure it has sufficient room to remove a +// item from it while keeping it at or above minItems. This function is for use +// only as a helper function for internal B-Tree code. Clients should not invoke +// it directly. +func (n *node) rebalanceOrMerge(i int) { + switch { + case i > 0 && n.children[i-1].count > minItems: + // Rebalance from left sibling. + // + // +-----------+ + // | y | + // +----/-\----+ + // / \ + // v v + // +-----------+ +-----------+ + // | x | | | + // +----------\+ +-----------+ + // \ + // v + // a + // + // After: + // + // +-----------+ + // | x | + // +----/-\----+ + // / \ + // v v + // +-----------+ +-----------+ + // | | | y | + // +-----------+ +/----------+ + // / + // v + // a + // + left := mut(&n.children[i-1]) + child := mut(&n.children[i]) + xLa, grandChild := left.popBack() + yLa := n.items[i-1] + child.pushFront(yLa, grandChild) + n.items[i-1] = xLa + child.subtreeCount++ + left.subtreeCount-- + if grandChild != nil { + child.subtreeCount += grandChild.subtreeCount + left.subtreeCount -= grandChild.subtreeCount + } + + case i < int(n.count) && n.children[i+1].count > minItems: + // Rebalance from right sibling. + // + // +-----------+ + // | y | + // +----/-\----+ + // / \ + // v v + // +-----------+ +-----------+ + // | | | x | + // +-----------+ +/----------+ + // / + // v + // a + // + // After: + // + // +-----------+ + // | x | + // +----/-\----+ + // / \ + // v v + // +-----------+ +-----------+ + // | y | | | + // +----------\+ +-----------+ + // \ + // v + // a + // + right := mut(&n.children[i+1]) + child := mut(&n.children[i]) + xLa, grandChild := right.popFront() + yLa := n.items[i] + child.pushBack(yLa, grandChild) + child.subtreeCount++ + right.subtreeCount-- + if grandChild != nil { + child.subtreeCount += grandChild.subtreeCount + right.subtreeCount -= grandChild.subtreeCount + } + n.items[i] = xLa + + default: + // Merge with either the left or right sibling. + // + // +-----------+ + // | u y v | + // +----/-\----+ + // / \ + // v v + // +-----------+ +-----------+ + // | x | | z | + // +-----------+ +-----------+ + // + // After: + // + // +-----------+ + // | u v | + // +-----|-----+ + // | + // v + // +-----------+ + // | x y z | + // +-----------+ + // + if i >= int(n.count) { + i = int(n.count - 1) + } + child := mut(&n.children[i]) + // Make mergeChild mutable, bumping the refcounts on its children if necessary. + _ = mut(&n.children[i+1]) + mergeLa, mergeChild := n.removeAt(i) + child.items[child.count] = mergeLa + copy(child.items[child.count+1:], mergeChild.items[:mergeChild.count]) + if !child.leaf { + copy(child.children[child.count+1:], mergeChild.children[:mergeChild.count+1]) + } + child.count += mergeChild.count + 1 + child.subtreeCount += mergeChild.subtreeCount + 1 + + mergeChild.decRef(false /* contentsToo */, nil) + } +} + +// InvalidateAnnotation removes any existing cached annotations for the provided +// annotator from this node's subtree. +func (n *node) InvalidateAnnotation(a Annotator) { + // Find this annotator's annotation on this node. + var annot *annotation + for i := range n.annot { + if n.annot[i].annotator == a { + annot = &n.annot[i] + } + } + + if annot != nil && annot.valid { + annot.valid = false + annot.v = a.Zero(annot.v) + } + if !n.leaf { + for i := int16(0); i <= n.count; i++ { + n.children[i].InvalidateAnnotation(a) + } + } +} + +// Annotation retrieves, computing if not already computed, the provided +// annotator's annotation of this node. The second return value indicates +// whether the future reads of this annotation may use the first return value +// as-is. If false, the annotation is not stable and may change on a subsequent +// computation. +func (n *node) Annotation(a Annotator) (interface{}, bool) { + // Find this annotator's annotation on this node. + var annot *annotation + for i := range n.annot { + if n.annot[i].annotator == a { + annot = &n.annot[i] + } + } + + // If it exists and is marked as valid, we can return it without + // recomputing anything. + if annot != nil && annot.valid { + return annot.v, true + } + + if annot == nil { + // This is n's first time being annotated by a. + // Create a new zeroed annotation. + n.annot = append(n.annot, annotation{ + annotator: a, + v: a.Zero(nil), + }) + annot = &n.annot[len(n.annot)-1] + } else { + // There's an existing annotation that must be recomputed. + // Zero its value. + annot.v = a.Zero(annot.v) + } + + annot.valid = true + for i := int16(0); i <= n.count; i++ { + if !n.leaf { + v, ok := n.children[i].Annotation(a) + annot.v = a.Merge(v, annot.v) + annot.valid = annot.valid && ok + } + if i < n.count { + v, ok := a.Accumulate(n.items[i], annot.v) + annot.v = v + annot.valid = annot.valid && ok + } + } + return annot.v, annot.valid +} + +func (n *node) verifyInvariants() { + recomputedSubtreeCount := int(n.count) + if !n.leaf { + for i := int16(0); i <= n.count; i++ { + n.children[i].verifyInvariants() + recomputedSubtreeCount += n.children[i].subtreeCount + } + } + if recomputedSubtreeCount != n.subtreeCount { + panic(fmt.Sprintf("recomputed subtree count (%d) ≠ n.subtreeCount (%d)", + recomputedSubtreeCount, n.subtreeCount)) + } +} + +// btree is an implementation of a B-Tree. +// +// btree stores FileMetadata in an ordered structure, allowing easy insertion, +// removal, and iteration. The B-Tree stores items in order based on cmp. The +// first level of the LSM uses a cmp function that compares sequence numbers. +// All other levels compare using the FileMetadata.Smallest. +// +// Write operations are not safe for concurrent mutation by multiple +// goroutines, but Read operations are. +type btree struct { + root *node + cmp btreeCmp +} + +// 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 backing files, if any. +func (t *btree) Release() (obsolete []*FileBacking) { + if t.root != nil { + t.root.decRef(true /* contentsToo */, &obsolete) + t.root = nil + } + return obsolete +} + +// Clone clones the btree, lazily. It does so in constant time. +func (t *btree) Clone() btree { + c := *t + if c.root != nil { + // Incrementing the reference count on the root node is sufficient to + // ensure that no node in the cloned tree can be mutated by an actor + // holding a reference to the original tree and vice versa. This + // property is upheld because the root node in the receiver btree and + // the returned btree will both necessarily have a reference count of at + // least 2 when this method returns. All tree mutations recursively + // acquire mutable node references (see mut) as they traverse down the + // tree. The act of acquiring a mutable node reference performs a clone + // if a node's reference count is greater than one. Cloning a node (see + // clone) increases the reference count on each of its children, + // ensuring that they have a reference count of at least 2. This, in + // turn, ensures that any of the child nodes that are modified will also + // be copied-on-write, recursively ensuring the immutability property + // over the entire tree. + c.root.incRef() + } + return c +} + +// Delete removes the provided file from the tree. +// It returns true if the file now has a zero reference count. +func (t *btree) Delete(item *FileMetadata) (obsolete bool) { + if t.root == nil || t.root.count == 0 { + return false + } + if out := mut(&t.root).Remove(t.cmp, item); out != nil { + obsolete = out.Unref() == 0 + } + if invariants.Enabled { + t.root.verifyInvariants() + } + if t.root.count == 0 { + old := t.root + if t.root.leaf { + t.root = nil + } else { + t.root = t.root.children[0] + } + old.decRef(false /* contentsToo */, nil) + } + return obsolete +} + +// Insert adds the given item to the tree. If a item in the tree already +// equals the given one, Insert panics. +func (t *btree) Insert(item *FileMetadata) error { + if t.root == nil { + t.root = newLeafNode() + } else if t.root.count >= maxItems { + splitLa, splitNode := mut(&t.root).split(maxItems / 2) + newRoot := newNode() + newRoot.count = 1 + newRoot.items[0] = splitLa + newRoot.children[0] = t.root + newRoot.children[1] = splitNode + newRoot.subtreeCount = t.root.subtreeCount + splitNode.subtreeCount + 1 + t.root = newRoot + } + item.Ref() + err := mut(&t.root).Insert(t.cmp, item) + if invariants.Enabled { + t.root.verifyInvariants() + } + return err +} + +// Iter returns a new iterator object. It is not safe to continue using an +// iterator after modifications are made to the tree. If modifications are made, +// create a new iterator. +func (t *btree) Iter() iterator { + return iterator{r: t.root, pos: -1, cmp: t.cmp} +} + +// Count returns the number of files contained within the B-Tree. +func (t *btree) Count() int { + if t.root == nil { + return 0 + } + return t.root.subtreeCount +} + +// String returns a string description of the tree. The format is +// similar to the https://en.wikipedia.org/wiki/Newick_format. +func (t *btree) String() string { + if t.Count() == 0 { + return ";" + } + var b strings.Builder + t.root.writeString(&b) + return b.String() +} + +func (n *node) writeString(b *strings.Builder) { + if n.leaf { + for i := int16(0); i < n.count; i++ { + if i != 0 { + b.WriteString(",") + } + b.WriteString(n.items[i].String()) + } + return + } + for i := int16(0); i <= n.count; i++ { + b.WriteString("(") + n.children[i].writeString(b) + b.WriteString(")") + if i < n.count { + b.WriteString(n.items[i].String()) + } + } +} + +// iterStack represents a stack of (node, pos) tuples, which captures +// iteration state as an iterator descends a btree. +type iterStack struct { + // a contains aLen stack frames when an iterator stack is short enough. + // If the iterator stack overflows the capacity of iterStackArr, the stack + // is moved to s and aLen is set to -1. + a iterStackArr + aLen int16 // -1 when using s + s []iterFrame +} + +// Used to avoid allocations for stacks below a certain size. +type iterStackArr [3]iterFrame + +type iterFrame struct { + n *node + pos int16 +} + +func (is *iterStack) push(f iterFrame) { + if is.aLen == -1 { + is.s = append(is.s, f) + } else if int(is.aLen) == len(is.a) { + is.s = make([]iterFrame, int(is.aLen)+1, 2*int(is.aLen)) + copy(is.s, is.a[:]) + is.s[int(is.aLen)] = f + is.aLen = -1 + } else { + is.a[is.aLen] = f + is.aLen++ + } +} + +func (is *iterStack) pop() iterFrame { + if is.aLen == -1 { + f := is.s[len(is.s)-1] + is.s = is.s[:len(is.s)-1] + return f + } + is.aLen-- + return is.a[is.aLen] +} + +func (is *iterStack) len() int { + if is.aLen == -1 { + return len(is.s) + } + return int(is.aLen) +} + +func (is *iterStack) clone() iterStack { + // If the iterator is using the embedded iterStackArr, we only need to + // copy the struct itself. + if is.s == nil { + return *is + } + clone := *is + clone.s = make([]iterFrame, len(is.s)) + copy(clone.s, is.s) + return clone +} + +func (is *iterStack) nth(n int) (f iterFrame, ok bool) { + if is.aLen == -1 { + if n >= len(is.s) { + return f, false + } + return is.s[n], true + } + if int16(n) >= is.aLen { + return f, false + } + return is.a[n], true +} + +func (is *iterStack) reset() { + if is.aLen == -1 { + is.s = is.s[:0] + } else { + is.aLen = 0 + } +} + +// iterator is responsible for search and traversal within a btree. +type iterator struct { + // the root node of the B-Tree. + r *node + // n and pos make up the current position of the iterator. + // If valid, n.items[pos] is the current value of the iterator. + // + // n may be nil iff i.r is nil. + n *node + pos int16 + // cmp dictates the ordering of the FileMetadata. + cmp func(*FileMetadata, *FileMetadata) int + // a stack of n's ancestors within the B-Tree, alongside the position + // taken to arrive at n. If non-empty, the bottommost frame of the stack + // will always contain the B-Tree root. + s iterStack +} + +// countLeft returns the count of files that are to the left of the current +// iterator position. +func (i *iterator) countLeft() int { + if i.r == nil { + return 0 + } + + // Each iterator has a stack of frames marking the path from the root node + // to the current iterator position. All files (n.items) and all subtrees + // (n.children) with indexes less than [pos] are to the left of the current + // iterator position. + // + // +------------------------+ - + // | Root pos:5 | | + // +------------------------+ | stack + // | Root/5 pos:3 | | frames + // +------------------------+ | [i.s] + // | Root/5/3 pos:9 | | + // +========================+ - + // | | + // | i.n: Root/5/3/9 i.pos:2| + // +------------------------+ + // + var count int + // Walk all the ancestors in the iterator stack [i.s], tallying up all the + // files and subtrees to the left of the stack frame's position. + f, ok := i.s.nth(0) + for fi := 0; ok; fi++ { + // There are [f.pos] files contained within [f.n.items] that sort to the + // left of the subtree the iterator has descended. + count += int(f.pos) + // Any subtrees that fall before the stack frame's position are entirely + // to the left of the iterator's current position. + for j := int16(0); j < f.pos; j++ { + count += f.n.children[j].subtreeCount + } + f, ok = i.s.nth(fi + 1) + } + + // The bottommost stack frame is inlined within the iterator struct. Again, + // [i.pos] files fall to the left of the current iterator position. + count += int(i.pos) + if !i.n.leaf { + // NB: Unlike above, we use a `<= i.pos` comparison. The iterator is + // positioned at item `i.n.items[i.pos]`, which sorts after everything + // in the subtree at `i.n.children[i.pos]`. + for j := int16(0); j <= i.pos; j++ { + count += i.n.children[j].subtreeCount + } + } + return count +} + +func (i *iterator) clone() iterator { + c := *i + c.s = i.s.clone() + return c +} + +func (i *iterator) reset() { + i.n = i.r + i.pos = -1 + i.s.reset() +} + +func (i iterator) String() string { + var buf bytes.Buffer + for n := 0; ; n++ { + f, ok := i.s.nth(n) + if !ok { + break + } + fmt.Fprintf(&buf, "%p: %02d/%02d\n", f.n, f.pos, f.n.count) + } + if i.r == nil { + fmt.Fprintf(&buf, ": %02d", i.pos) + } else { + fmt.Fprintf(&buf, "%p: %02d/%02d", i.n, i.pos, i.n.count) + } + return buf.String() +} + +func cmpIter(a, b iterator) int { + if a.r != b.r { + panic("compared iterators from different btrees") + } + + // Each iterator has a stack of frames marking the path from the root node + // to the current iterator position. We walk both paths formed by the + // iterators' stacks simultaneously, descending from the shared root node, + // always comparing nodes at the same level in the tree. + // + // If the iterators' paths ever diverge and point to different nodes, the + // iterators are not equal and we use the node positions to evaluate the + // comparison. + // + // If an iterator's stack ends, we stop descending and use its current + // node and position for the final comparison. One iterator's stack may + // end before another's if one iterator is positioned deeper in the tree. + // + // a b + // +------------------------+ +--------------------------+ - + // | Root pos:5 | = | Root pos:5 | | + // +------------------------+ +--------------------------+ | stack + // | Root/5 pos:3 | = | Root/5 pos:3 | | frames + // +------------------------+ +--------------------------+ | + // | Root/5/3 pos:9 | > | Root/5/3 pos:1 | | + // +========================+ +==========================+ - + // | | | | + // | a.n: Root/5/3/9 a.pos:2| | b.n: Root/5/3/1, b.pos:5 | + // +------------------------+ +--------------------------+ + + // Initialize with the iterator's current node and position. These are + // conceptually the most-recent/current frame of the iterator stack. + an, apos := a.n, a.pos + bn, bpos := b.n, b.pos + + // aok, bok are set while traversing the iterator's path down the B-Tree. + // They're declared in the outer scope because they help distinguish the + // sentinel case when both iterators' first frame points to the last child + // of the root. If an iterator has no other frames in its stack, it's the + // end sentinel state which sorts after everything else. + var aok, bok bool + for i := 0; ; i++ { + var af, bf iterFrame + af, aok = a.s.nth(i) + bf, bok = b.s.nth(i) + if !aok || !bok { + if aok { + // Iterator a, unlike iterator b, still has a frame. Set an, + // apos so we compare using the frame from the stack. + an, apos = af.n, af.pos + } + if bok { + // Iterator b, unlike iterator a, still has a frame. Set bn, + // bpos so we compare using the frame from the stack. + bn, bpos = bf.n, bf.pos + } + break + } + + // aok && bok + if af.n != bf.n { + panic("nonmatching nodes during btree iterator comparison") + } + if v := stdcmp.Compare(af.pos, bf.pos); v != 0 { + return v + } + // Otherwise continue up both iterators' stacks (equivalently, down the + // B-Tree away from the root). + } + + if aok && bok { + panic("expected one or more stacks to have been exhausted") + } + if an != bn { + panic("nonmatching nodes during btree iterator comparison") + } + if v := stdcmp.Compare(apos, bpos); v != 0 { + return v + } + switch { + case aok: + // a is positioned at a leaf child at this position and b is at an + // end sentinel state. + return -1 + case bok: + // b is positioned at a leaf child at this position and a is at an + // end sentinel state. + return +1 + default: + return 0 + } +} + +func (i *iterator) descend(n *node, pos int16) { + i.s.push(iterFrame{n: n, pos: pos}) + i.n = n.children[pos] + i.pos = 0 +} + +// ascend ascends up to the current node's parent and resets the position +// to the one previously set for this parent node. +func (i *iterator) ascend() { + f := i.s.pop() + i.n = f.n + i.pos = f.pos +} + +// seek repositions the iterator over the first file for which fn returns +// true, mirroring the semantics of the standard library's sort.Search +// function. Like sort.Search, seek requires the iterator's B-Tree to be +// ordered such that fn returns false for some (possibly empty) prefix of the +// tree's files, and then true for the (possibly empty) remainder. +func (i *iterator) seek(fn func(*FileMetadata) bool) { + i.reset() + if i.r == nil { + return + } + + for { + // Logic copied from sort.Search. + j, k := 0, int(i.n.count) + for j < k { + h := int(uint(j+k) >> 1) // avoid overflow when computing h + + // j ≤ h < k + if !fn(i.n.items[h]) { + j = h + 1 // preserves f(j-1) == false + } else { + k = h // preserves f(k) == true + } + } + + i.pos = int16(j) + if i.n.leaf { + if i.pos == i.n.count { + i.next() + } + return + } + i.descend(i.n, i.pos) + } +} + +// first seeks to the first item in the btree. +func (i *iterator) first() { + i.reset() + if i.r == nil { + return + } + for !i.n.leaf { + i.descend(i.n, 0) + } + i.pos = 0 +} + +// last seeks to the last item in the btree. +func (i *iterator) last() { + i.reset() + if i.r == nil { + return + } + for !i.n.leaf { + i.descend(i.n, i.n.count) + } + i.pos = i.n.count - 1 +} + +// next positions the iterator to the item immediately following +// its current position. +func (i *iterator) next() { + if i.r == nil { + return + } + + if i.n.leaf { + if i.pos < i.n.count { + i.pos++ + } + if i.pos < i.n.count { + return + } + for i.s.len() > 0 && i.pos >= i.n.count { + i.ascend() + } + return + } + + i.descend(i.n, i.pos+1) + for !i.n.leaf { + i.descend(i.n, 0) + } + i.pos = 0 +} + +// prev positions the iterator to the item immediately preceding +// its current position. +func (i *iterator) prev() { + if i.r == nil { + return + } + + if i.n.leaf { + i.pos-- + if i.pos >= 0 { + return + } + for i.s.len() > 0 && i.pos < 0 { + i.ascend() + i.pos-- + } + return + } + + i.descend(i.n, i.pos) + for !i.n.leaf { + i.descend(i.n, i.n.count) + } + i.pos = i.n.count - 1 +} + +// valid returns whether the iterator is positioned at a valid position. +func (i *iterator) valid() bool { + return i.r != nil && i.pos >= 0 && i.pos < i.n.count +} + +// cur returns the item at the iterator's current position. It is illegal +// to call cur if the iterator is not valid. +func (i *iterator) cur() *FileMetadata { + if invariants.Enabled && !i.valid() { + panic("btree iterator.cur invoked on invalid iterator") + } + return i.n.items[i.pos] +} diff --git a/pebble/internal/manifest/btree_test.go b/pebble/internal/manifest/btree_test.go new file mode 100644 index 0000000..cce22a2 --- /dev/null +++ b/pebble/internal/manifest/btree_test.go @@ -0,0 +1,991 @@ +// Copyright 2020 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 manifest + +import ( + stdcmp "cmp" + "fmt" + "math/rand" + "reflect" + "slices" + "sync" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/stretchr/testify/require" +) + +func newItem(k InternalKey) *FileMetadata { + m := (&FileMetadata{}).ExtendPointKeyBounds( + base.DefaultComparer.Compare, k, k, + ) + m.InitPhysicalBacking() + return m +} + +func cmp(a, b *FileMetadata) int { + return cmpKey(a.Smallest, b.Smallest) +} + +func cmpKey(a, b InternalKey) int { + return base.InternalCompare(base.DefaultComparer.Compare, a, b) +} + +////////////////////////////////////////// +// Invariant verification // +////////////////////////////////////////// + +// Verify asserts that the tree's structural invariants all hold. +func (t *btree) Verify(tt *testing.T) { + if t.Count() == 0 { + require.Nil(tt, t.root) + return + } + t.verifyLeafSameDepth(tt) + t.verifyCountAllowed(tt) + t.isSorted(tt) + t.root.verifyInvariants() +} + +func (t *btree) verifyLeafSameDepth(tt *testing.T) { + h := t.height() + t.root.verifyDepthEqualToHeight(tt, 1, h) +} + +func (n *node) verifyDepthEqualToHeight(t *testing.T, depth, height int) { + if n.leaf { + require.Equal(t, height, depth, "all leaves should have the same depth as the tree height") + } + n.recurse(func(child *node, _ int16) { + child.verifyDepthEqualToHeight(t, depth+1, height) + }) +} + +func (t *btree) verifyCountAllowed(tt *testing.T) { + t.root.verifyCountAllowed(tt, true) +} + +// height returns the height of the tree. +func (t *btree) height() int { + if t.root == nil { + return 0 + } + h := 1 + n := t.root + for !n.leaf { + n = n.children[0] + h++ + } + return h +} + +func (n *node) verifyCountAllowed(t *testing.T, root bool) { + if !root { + require.GreaterOrEqual(t, n.count, int16(minItems), "item count %d must be in range [%d,%d]", n.count, minItems, maxItems) + require.LessOrEqual(t, n.count, int16(maxItems), "item count %d must be in range [%d,%d]", n.count, minItems, maxItems) + } + for i, item := range n.items { + if i < int(n.count) { + require.NotNil(t, item, "item below count") + } else { + require.Nil(t, item, "item above count") + } + } + if !n.leaf { + for i, child := range n.children { + if i <= int(n.count) { + require.NotNil(t, child, "node below count") + } else { + require.Nil(t, child, "node above count") + } + } + } + n.recurse(func(child *node, _ int16) { + child.verifyCountAllowed(t, false) + }) +} + +func (t *btree) isSorted(tt *testing.T) { + t.root.isSorted(tt, t.cmp) +} + +func (n *node) isSorted(t *testing.T, cmp func(*FileMetadata, *FileMetadata) int) { + for i := int16(1); i < n.count; i++ { + require.LessOrEqual(t, cmp(n.items[i-1], n.items[i]), 0) + } + if !n.leaf { + for i := int16(0); i < n.count; i++ { + prev := n.children[i] + next := n.children[i+1] + + require.LessOrEqual(t, cmp(prev.items[prev.count-1], n.items[i]), 0) + require.LessOrEqual(t, cmp(n.items[i], next.items[0]), 0) + } + } + n.recurse(func(child *node, _ int16) { + child.isSorted(t, cmp) + }) +} + +func (n *node) recurse(f func(child *node, pos int16)) { + if !n.leaf { + for i := int16(0); i <= n.count; i++ { + f(n.children[i], i) + } + } +} + +////////////////////////////////////////// +// Unit Tests // +////////////////////////////////////////// + +func key(i int) InternalKey { + if i < 0 || i > 99999 { + panic("key out of bounds") + } + return base.MakeInternalKey([]byte(fmt.Sprintf("%05d", i)), 0, base.InternalKeyKindSet) +} + +func keyWithMemo(i int, memo map[int]InternalKey) InternalKey { + if s, ok := memo[i]; ok { + return s + } + s := key(i) + memo[i] = s + return s +} + +func checkIterRelative(t *testing.T, it *iterator, start, end int, keyMemo map[int]InternalKey) { + t.Helper() + i := start + for ; it.valid(); it.next() { + item := it.cur() + expected := keyWithMemo(i, keyMemo) + if cmpKey(expected, item.Smallest) != 0 { + t.Fatalf("expected %s, but found %s", expected, item.Smallest) + } + i++ + } + if i != end { + t.Fatalf("expected %d, but at %d", end, i) + } +} + +func checkIter(t *testing.T, it iterator, start, end int, keyMemo map[int]InternalKey) { + t.Helper() + i := start + for it.first(); it.valid(); it.next() { + item := it.cur() + expected := keyWithMemo(i, keyMemo) + if cmpKey(expected, item.Smallest) != 0 { + t.Fatalf("expected %s, but found %s", expected, item.Smallest) + } + require.Equal(t, i-start, it.countLeft()) + i++ + } + if i != end { + t.Fatalf("expected %d, but at %d", end, i) + } + + for it.last(); it.valid(); it.prev() { + i-- + item := it.cur() + expected := keyWithMemo(i, keyMemo) + if cmpKey(expected, item.Smallest) != 0 { + t.Fatalf("expected %s, but found %s", expected, item.Smallest) + } + require.Equal(t, i-start, it.countLeft()) + } + if i != start { + t.Fatalf("expected %d, but at %d: %+v", start, i, it) + } +} + +// TestBTree tests basic btree operations. +func TestBTree(t *testing.T) { + var tr btree + tr.cmp = cmp + keyMemo := make(map[int]InternalKey) + + // With degree == 16 (max-items/node == 31) we need 513 items in order for + // there to be 3 levels in the tree. The count here is comfortably above + // that. + const count = 768 + items := rang(0, count-1) + + // Add keys in sorted order. + for i := 0; i < count; i++ { + require.NoError(t, tr.Insert(items[i])) + tr.Verify(t) + if e := i + 1; e != tr.Count() { + t.Fatalf("expected length %d, but found %d", e, tr.Count()) + } + checkIter(t, tr.Iter(), 0, i+1, keyMemo) + } + + // delete keys in sorted order. + for i := 0; i < count; i++ { + obsolete := tr.Delete(items[i]) + if !obsolete { + t.Fatalf("expected item %d to be obsolete", i) + } + tr.Verify(t) + if e := count - (i + 1); e != tr.Count() { + t.Fatalf("expected length %d, but found %d", e, tr.Count()) + } + checkIter(t, tr.Iter(), i+1, count, keyMemo) + } + + // Add keys in reverse sorted order. + for i := 1; i <= count; i++ { + require.NoError(t, tr.Insert(items[count-i])) + tr.Verify(t) + if i != tr.Count() { + t.Fatalf("expected length %d, but found %d", i, tr.Count()) + } + checkIter(t, tr.Iter(), count-i, count, keyMemo) + } + + // delete keys in reverse sorted order. + for i := 1; i <= count; i++ { + obsolete := tr.Delete(items[count-i]) + if !obsolete { + t.Fatalf("expected item %d to be obsolete", i) + } + tr.Verify(t) + if e := count - i; e != tr.Count() { + t.Fatalf("expected length %d, but found %d", e, tr.Count()) + } + checkIter(t, tr.Iter(), 0, count-i, keyMemo) + } +} + +func TestIterClone(t *testing.T) { + const count = 65536 + + var tr btree + tr.cmp = cmp + keyMemo := make(map[int]InternalKey) + + for i := 0; i < count; i++ { + require.NoError(t, tr.Insert(newItem(key(i)))) + } + + it := tr.Iter() + i := 0 + for it.first(); it.valid(); it.next() { + if i%500 == 0 { + c := it.clone() + + require.Equal(t, 0, cmpIter(it, c)) + checkIterRelative(t, &c, i, count, keyMemo) + if i < count { + require.Equal(t, -1, cmpIter(it, c)) + require.Equal(t, +1, cmpIter(c, it)) + } + } + i++ + } +} + +func TestIterCmpEdgeCases(t *testing.T) { + var tr btree + tr.cmp = cmp + t.Run("empty", func(t *testing.T) { + a := tr.Iter() + b := tr.Iter() + require.Equal(t, 0, cmpIter(a, b)) + }) + require.NoError(t, tr.Insert(newItem(key(5)))) + t.Run("exhausted_next", func(t *testing.T) { + a := tr.Iter() + b := tr.Iter() + a.first() + b.first() + require.Equal(t, 0, cmpIter(a, b)) + b.next() + require.False(t, b.valid()) + require.Equal(t, -1, cmpIter(a, b)) + }) + t.Run("exhausted_prev", func(t *testing.T) { + a := tr.Iter() + b := tr.Iter() + a.first() + b.first() + b.prev() + require.False(t, b.valid()) + require.Equal(t, 1, cmpIter(a, b)) + b.next() + require.Equal(t, 0, cmpIter(a, b)) + }) +} + +func TestIterCmpRand(t *testing.T) { + const itemCount = 65536 + const iterCount = 1000 + + var tr btree + tr.cmp = cmp + for i := 0; i < itemCount; i++ { + require.NoError(t, tr.Insert(newItem(key(i)))) + } + + seed := time.Now().UnixNano() + rng := rand.New(rand.NewSource(seed)) + iters1 := make([]*LevelIterator, iterCount) + iters2 := make([]*LevelIterator, iterCount) + for i := 0; i < iterCount; i++ { + k := rng.Intn(itemCount) + iter := LevelIterator{iter: tr.Iter()} + iter.SeekGE(base.DefaultComparer.Compare, key(k).UserKey) + iters1[i] = &iter + iters2[i] = &iter + } + + // All the iterators should be positioned, so sorting them by items and by + // iterator comparisons should equal identical orderings. + slices.SortStableFunc(iters1, func(a, b *LevelIterator) int { return cmpIter(a.iter, b.iter) }) + slices.SortStableFunc(iters2, func(a, b *LevelIterator) int { return cmp(a.iter.cur(), b.iter.cur()) }) + for i := 0; i < iterCount; i++ { + if iters1[i] != iters2[i] { + t.Fatalf("seed %d: iters out of order at index %d:\n%s\n\n%s", + seed, i, iters1[i], iters2[i]) + } + } +} + +// TestBTreeSeek tests basic btree iterator operations on an iterator wrapped +// by a LevelIterator. +func TestBTreeSeek(t *testing.T) { + const count = 513 + + var tr btree + tr.cmp = cmp + for i := 0; i < count; i++ { + require.NoError(t, tr.Insert(newItem(key(i*2)))) + } + + it := LevelIterator{iter: tr.Iter()} + for i := 0; i < 2*count-1; i++ { + item := it.SeekGE(base.DefaultComparer.Compare, key(i).UserKey) + if item == nil { + t.Fatalf("%d: expected valid iterator", i) + } + expected := key(2 * ((i + 1) / 2)) + if cmpKey(expected, item.Smallest) != 0 { + t.Fatalf("%d: expected %s, but found %s", i, expected, item.Smallest) + } + } + it.SeekGE(base.DefaultComparer.Compare, key(2*count-1).UserKey) + if it.iter.valid() { + t.Fatalf("expected invalid iterator") + } + + for i := 1; i < 2*count; i++ { + item := it.SeekLT(base.DefaultComparer.Compare, key(i).UserKey) + if item == nil { + t.Fatalf("%d: expected valid iterator", i) + } + expected := key(2 * ((i - 1) / 2)) + if cmpKey(expected, item.Smallest) != 0 { + t.Fatalf("%d: expected %s, but found %s", i, expected, item.Smallest) + } + } + it.SeekLT(base.DefaultComparer.Compare, key(0).UserKey) + if it.iter.valid() { + t.Fatalf("expected invalid iterator") + } +} + +func TestBTreeInsertDuplicateError(t *testing.T) { + var tr btree + tr.cmp = cmp + require.NoError(t, tr.Insert(newItem(key(1)))) + require.NoError(t, tr.Insert(newItem(key(2)))) + require.NoError(t, tr.Insert(newItem(key(3)))) + wantErr := errors.Errorf("files %s and %s collided on sort keys", + errors.Safe(base.FileNum(000000)), errors.Safe(base.FileNum(000000))) + require.Error(t, wantErr, tr.Insert(newItem(key(2)))) +} + +// TestBTreeCloneConcurrentOperations tests that cloning a btree returns a new +// btree instance which is an exact logical copy of the original but that can be +// modified independently going forward. +func TestBTreeCloneConcurrentOperations(t *testing.T) { + const cloneTestSize = 1000 + p := perm(cloneTestSize) + + var trees []*btree + treeC, treeDone := make(chan *btree), make(chan struct{}) + go func() { + for b := range treeC { + trees = append(trees, b) + } + close(treeDone) + }() + + var wg sync.WaitGroup + var populate func(tr *btree, start int) + populate = func(tr *btree, start int) { + t.Logf("Starting new clone at %v", start) + treeC <- tr + for i := start; i < cloneTestSize; i++ { + require.NoError(t, tr.Insert(p[i])) + if i%(cloneTestSize/5) == 0 { + wg.Add(1) + c := tr.Clone() + go populate(&c, i+1) + } + } + wg.Done() + } + + wg.Add(1) + var tr btree + tr.cmp = cmp + go populate(&tr, 0) + wg.Wait() + close(treeC) + <-treeDone + + t.Logf("Starting equality checks on %d trees", len(trees)) + want := rang(0, cloneTestSize-1) + for i, tree := range trees { + if got := all(tree); !reflect.DeepEqual(strReprs(got), strReprs(want)) { + t.Errorf("tree %v mismatch", i) + } + } + + t.Log("Removing half of items from first half") + toRemove := want[cloneTestSize/2:] + for i := 0; i < len(trees)/2; i++ { + tree := trees[i] + wg.Add(1) + go func() { + for _, item := range toRemove { + tree.Delete(item) + } + wg.Done() + }() + } + wg.Wait() + + t.Log("Checking all values again") + for i, tree := range trees { + var wantpart []*FileMetadata + if i < len(trees)/2 { + wantpart = want[:cloneTestSize/2] + } else { + wantpart = want + } + if got := all(tree); !reflect.DeepEqual(strReprs(got), strReprs(wantpart)) { + t.Errorf("tree %v mismatch, want %#v got %#v", i, strReprs(wantpart), strReprs(got)) + } + } + + var obsolete []*FileBacking + for i := range trees { + obsolete = append(obsolete, trees[i].Release()...) + } + if len(obsolete) != len(p) { + t.Errorf("got %d obsolete trees, expected %d", len(obsolete), len(p)) + } +} + +// TestIterStack tests the interface of the iterStack type. +func TestIterStack(t *testing.T) { + f := func(i int) iterFrame { return iterFrame{pos: int16(i)} } + var is iterStack + for i := 1; i <= 2*len(iterStackArr{}); i++ { + var j int + for j = 0; j < i; j++ { + is.push(f(j)) + } + require.Equal(t, j, is.len()) + for j--; j >= 0; j-- { + require.Equal(t, f(j), is.pop()) + } + is.reset() + } +} + +func TestIterEndSentinel(t *testing.T) { + var tr btree + tr.cmp = cmp + require.NoError(t, tr.Insert(newItem(key(1)))) + require.NoError(t, tr.Insert(newItem(key(2)))) + require.NoError(t, tr.Insert(newItem(key(3)))) + iter := LevelIterator{iter: tr.Iter()} + iter.SeekGE(base.DefaultComparer.Compare, key(3).UserKey) + require.True(t, iter.iter.valid()) + iter.Next() + require.False(t, iter.iter.valid()) + + // If we seek into the end sentinel, prev should return us to a valid + // position. + iter.SeekGE(base.DefaultComparer.Compare, key(4).UserKey) + require.False(t, iter.iter.valid()) + iter.Prev() + require.True(t, iter.iter.valid()) +} + +type orderStatistic struct{} + +func (o orderStatistic) Zero(dst interface{}) interface{} { + if dst == nil { + return new(int) + } + v := dst.(*int) + *v = 0 + return v +} + +func (o orderStatistic) Accumulate(meta *FileMetadata, dst interface{}) (interface{}, bool) { + v := dst.(*int) + *v++ + return v, true +} + +func (o orderStatistic) Merge(src interface{}, dst interface{}) interface{} { + srcv := src.(*int) + dstv := dst.(*int) + *dstv = *dstv + *srcv + return dstv +} + +func TestAnnotationOrderStatistic(t *testing.T) { + const count = 1000 + ann := orderStatistic{} + + var tr btree + tr.cmp = cmp + for i := 1; i <= count; i++ { + require.NoError(t, tr.Insert(newItem(key(i)))) + + v, ok := tr.root.Annotation(ann) + require.True(t, ok) + vtyped := v.(*int) + require.Equal(t, i, *vtyped) + } + + v, ok := tr.root.Annotation(ann) + require.True(t, ok) + vtyped := v.(*int) + require.Equal(t, count, *vtyped) + + v, ok = tr.root.Annotation(ann) + vtyped = v.(*int) + require.True(t, ok) + require.Equal(t, count, *vtyped) +} + +// TestRandomizedBTree tests a random set of Insert, Delete and iteration +// operations, checking for equivalence with a map of filenums. +func TestRandomizedBTree(t *testing.T) { + const maxFileNum = 50_000 + + seed := time.Now().UnixNano() + t.Log("seed", seed) + rng := rand.New(rand.NewSource(seed)) + + var numOps int + if invariants.RaceEnabled { + // Reduce the number of ops in race mode so the test doesn't take very long. + numOps = 1_000 + rng.Intn(4_000) + } else { + numOps = 10_000 + rng.Intn(40_000) + } + + var metadataAlloc [maxFileNum]FileMetadata + for i := 0; i < len(metadataAlloc); i++ { + metadataAlloc[i].FileNum = base.FileNum(i) + metadataAlloc[i].InitPhysicalBacking() + } + + // Use a btree comparator that sorts by file number to make it easier to + // prevent duplicates or overlaps. + tree := btree{ + cmp: func(a *FileMetadata, b *FileMetadata) int { + return stdcmp.Compare(a.FileNum, b.FileNum) + }, + } + + type opDecl struct { + fn func() + weight int + } + ref := map[base.FileNum]bool{} + ops := []opDecl{ + { + // Insert + fn: func() { + f := &metadataAlloc[rng.Intn(maxFileNum)] + err := tree.Insert(f) + if ref[f.FileNum] { + require.Error(t, err, "btree.Insert should error if file already exists") + } else { + ref[f.FileNum] = true + require.NoError(t, err) + } + }, + weight: 20, + }, + { + // Delete + fn: func() { + f := &metadataAlloc[rng.Intn(maxFileNum)] + tree.Delete(f) + delete(ref, f.FileNum) + }, + weight: 10, + }, + { + // Iterate + fn: func() { + iter := tree.Iter() + count := 0 + var prev base.FileNum + for iter.first(); iter.valid(); iter.next() { + fn := iter.cur().FileNum + require.True(t, ref[fn]) + if count > 0 { + require.Less(t, prev, fn) + } + count++ + } + require.Equal(t, count, len(ref)) + }, + weight: 1, + }, + } + weightSum := 0 + for i := range ops { + weightSum += ops[i].weight + } + + for i := 0; i < numOps; i++ { + w := rng.Intn(weightSum) + for j := range ops { + w -= ops[j].weight + if w < 0 { + ops[j].fn() + break + } + } + } +} + +////////////////////////////////////////// +// Benchmarks // +////////////////////////////////////////// + +// perm returns a random permutation of items with keys in the range [0, n). +func perm(n int) (out []*FileMetadata) { + for _, i := range rand.Perm(n) { + out = append(out, newItem(key(i))) + } + return out +} + +// rang returns an ordered list of items with keys in the range [m, n]. +func rang(m, n int) (out []*FileMetadata) { + for i := m; i <= n; i++ { + out = append(out, newItem(key(i))) + } + return out +} + +func strReprs(items []*FileMetadata) []string { + s := make([]string, len(items)) + for i := range items { + s[i] = items[i].String() + } + return s +} + +// all extracts all items from a tree in order as a slice. +func all(tr *btree) (out []*FileMetadata) { + it := tr.Iter() + it.first() + for it.valid() { + out = append(out, it.cur()) + it.next() + } + return out +} + +func forBenchmarkSizes(b *testing.B, f func(b *testing.B, count int)) { + for _, count := range []int{16, 128, 1024, 8192, 65536} { + b.Run(fmt.Sprintf("count=%d", count), func(b *testing.B) { + f(b, count) + }) + } +} + +// BenchmarkBTreeInsert measures btree insertion performance. +func BenchmarkBTreeInsert(b *testing.B) { + forBenchmarkSizes(b, func(b *testing.B, count int) { + insertP := perm(count) + b.ResetTimer() + for i := 0; i < b.N; { + var tr btree + tr.cmp = cmp + for _, item := range insertP { + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + i++ + if i >= b.N { + return + } + } + } + }) +} + +// BenchmarkBTreeDelete measures btree deletion performance. +func BenchmarkBTreeDelete(b *testing.B) { + forBenchmarkSizes(b, func(b *testing.B, count int) { + insertP, removeP := perm(count), perm(count) + b.ResetTimer() + for i := 0; i < b.N; { + b.StopTimer() + var tr btree + tr.cmp = cmp + for _, item := range insertP { + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + b.StartTimer() + for _, item := range removeP { + tr.Delete(item) + i++ + if i >= b.N { + return + } + } + if tr.Count() > 0 { + b.Fatalf("tree not empty: %s", &tr) + } + } + }) +} + +// BenchmarkBTreeDeleteInsert measures btree deletion and insertion performance. +func BenchmarkBTreeDeleteInsert(b *testing.B) { + forBenchmarkSizes(b, func(b *testing.B, count int) { + insertP := perm(count) + var tr btree + tr.cmp = cmp + for _, item := range insertP { + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + item := insertP[i%count] + tr.Delete(item) + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + }) +} + +// BenchmarkBTreeDeleteInsertCloneOnce measures btree deletion and insertion +// performance after the tree has been copy-on-write cloned once. +func BenchmarkBTreeDeleteInsertCloneOnce(b *testing.B) { + forBenchmarkSizes(b, func(b *testing.B, count int) { + insertP := perm(count) + var tr btree + tr.cmp = cmp + for _, item := range insertP { + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + tr = tr.Clone() + b.ResetTimer() + for i := 0; i < b.N; i++ { + item := insertP[i%count] + tr.Delete(item) + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + }) +} + +// BenchmarkBTreeDeleteInsertCloneEachTime measures btree deletion and insertion +// performance while the tree is repeatedly copy-on-write cloned. +func BenchmarkBTreeDeleteInsertCloneEachTime(b *testing.B) { + for _, release := range []bool{false, true} { + b.Run(fmt.Sprintf("release=%t", release), func(b *testing.B) { + forBenchmarkSizes(b, func(b *testing.B, count int) { + insertP := perm(count) + var tr, trRelease btree + tr.cmp = cmp + trRelease.cmp = cmp + for _, item := range insertP { + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + item := insertP[i%count] + if release { + trRelease.Release() + trRelease = tr + } + tr = tr.Clone() + tr.Delete(item) + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + }) + }) + } +} + +// BenchmarkBTreeIter measures the cost of creating a btree iterator. +func BenchmarkBTreeIter(b *testing.B) { + var tr btree + tr.cmp = cmp + for i := 0; i < b.N; i++ { + it := tr.Iter() + it.first() + } +} + +// BenchmarkBTreeIterSeekGE measures the cost of seeking a btree iterator +// forward. +func BenchmarkBTreeIterSeekGE(b *testing.B) { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + forBenchmarkSizes(b, func(b *testing.B, count int) { + var keys []InternalKey + var tr btree + tr.cmp = cmp + + for i := 0; i < count; i++ { + s := key(i) + keys = append(keys, s) + if err := tr.Insert(newItem(s)); err != nil { + b.Fatal(err) + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + k := keys[rng.Intn(len(keys))] + it := LevelIterator{iter: tr.Iter()} + f := it.SeekGE(base.DefaultComparer.Compare, k.UserKey) + if testing.Verbose() { + if f == nil { + b.Fatal("expected to find key") + } + if cmpKey(k, f.Smallest) != 0 { + b.Fatalf("expected %s, but found %s", k, f.Smallest) + } + } + } + }) +} + +// BenchmarkBTreeIterSeekLT measures the cost of seeking a btree iterator +// backward. +func BenchmarkBTreeIterSeekLT(b *testing.B) { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + forBenchmarkSizes(b, func(b *testing.B, count int) { + var keys []InternalKey + var tr btree + tr.cmp = cmp + + for i := 0; i < count; i++ { + k := key(i) + keys = append(keys, k) + if err := tr.Insert(newItem(k)); err != nil { + b.Fatal(err) + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + j := rng.Intn(len(keys)) + k := keys[j] + it := LevelIterator{iter: tr.Iter()} + f := it.SeekLT(base.DefaultComparer.Compare, k.UserKey) + if testing.Verbose() { + if j == 0 { + if f != nil { + b.Fatal("unexpected key") + } + } else { + if f == nil { + b.Fatal("expected to find key") + } + k := keys[j-1] + if cmpKey(k, f.Smallest) != 0 { + b.Fatalf("expected %s, but found %s", k, f.Smallest) + } + } + } + } + }) +} + +// BenchmarkBTreeIterNext measures the cost of seeking a btree iterator to the +// next item in the tree. +func BenchmarkBTreeIterNext(b *testing.B) { + var tr btree + tr.cmp = cmp + + const count = 8 << 10 + for i := 0; i < count; i++ { + item := newItem(key(i)) + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + + it := tr.Iter() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if !it.valid() { + it.first() + } + it.next() + } +} + +// BenchmarkBTreeIterPrev measures the cost of seeking a btree iterator to the +// previous item in the tree. +func BenchmarkBTreeIterPrev(b *testing.B) { + var tr btree + tr.cmp = cmp + + const count = 8 << 10 + for i := 0; i < count; i++ { + item := newItem(key(i)) + if err := tr.Insert(item); err != nil { + b.Fatal(err) + } + } + + it := tr.Iter() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if !it.valid() { + it.first() + } + it.prev() + } +} diff --git a/pebble/internal/manifest/l0_sublevels.go b/pebble/internal/manifest/l0_sublevels.go new file mode 100644 index 0000000..3857045 --- /dev/null +++ b/pebble/internal/manifest/l0_sublevels.go @@ -0,0 +1,2042 @@ +// Copyright 2020 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 manifest + +import ( + "bytes" + "fmt" + "math" + "sort" + "strings" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + stdcmp "github.com/cockroachdb/pebble/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" +) + +// errInvalidL0SublevelsOpt is for use in AddL0Files when the incremental +// sublevel generation optimization failed, and NewL0Sublevels must be called. +var errInvalidL0SublevelsOpt = errors.New("pebble: L0 sublevel generation optimization cannot be used") + +// Intervals are of the form [start, end) with no gap between intervals. Each +// file overlaps perfectly with a sequence of intervals. This perfect overlap +// occurs because the union of file boundary keys is used to pick intervals. +// However the largest key in a file is inclusive, so when it is used as +// an interval, the actual key is ImmediateSuccessor(key). We don't have the +// ImmediateSuccessor function to do this computation, so we instead keep an +// isLargest bool to remind the code about this fact. This is used for +// comparisons in the following manner: +// - intervalKey{k, false} < intervalKey{k, true} +// - k1 < k2 -> intervalKey{k1, _} < intervalKey{k2, _}. +// +// Note that the file's largest key is exclusive if the internal key +// has a trailer matching the rangedel sentinel key. In this case, we set +// isLargest to false for end interval computation. +// +// For example, consider three files with bounds [a,e], [b,g], and [e,j]. The +// interval keys produced would be intervalKey{a, false}, intervalKey{b, false}, +// intervalKey{e, false}, intervalKey{e, true}, intervalKey{g, true} and +// intervalKey{j, true}, resulting in intervals +// [a, b), [b, (e, false)), [(e,false), (e, true)), [(e, true), (g, true)) and +// [(g, true), (j, true)). The first file overlaps with the first three +// perfectly, the second file overlaps with the second through to fourth +// intervals, and the third file overlaps with the last three. +// +// The intervals are indexed starting from 0, with the index of the interval +// being the index of the start key of the interval. +// +// In addition to helping with compaction picking, we use interval indices +// to assign each file an interval range once. Subsequent operations, say +// picking overlapping files for a compaction, only need to use the index +// numbers and so avoid expensive byte slice comparisons. +type intervalKey struct { + key []byte + isLargest bool +} + +// intervalKeyTemp is used in the sortAndSweep step. It contains additional metadata +// which is used to generate the {min,max}IntervalIndex for files. +type intervalKeyTemp struct { + intervalKey intervalKey + fileMeta *FileMetadata + isEndKey bool +} + +func (i *intervalKeyTemp) setFileIntervalIndex(idx int) { + if i.isEndKey { + // This is the right endpoint of some file interval, so the + // file.maxIntervalIndex must be j - 1 as maxIntervalIndex is + // inclusive. + i.fileMeta.maxIntervalIndex = idx - 1 + return + } + // This is the left endpoint for some file interval, so the + // file.minIntervalIndex must be j. + i.fileMeta.minIntervalIndex = idx +} + +func intervalKeyCompare(cmp Compare, a, b intervalKey) int { + rv := cmp(a.key, b.key) + if rv == 0 { + if a.isLargest && !b.isLargest { + return +1 + } + if !a.isLargest && b.isLargest { + return -1 + } + } + return rv +} + +type intervalKeySorter struct { + keys []intervalKeyTemp + cmp Compare +} + +func (s intervalKeySorter) Len() int { return len(s.keys) } +func (s intervalKeySorter) Less(i, j int) bool { + return intervalKeyCompare(s.cmp, s.keys[i].intervalKey, s.keys[j].intervalKey) < 0 +} +func (s intervalKeySorter) Swap(i, j int) { + s.keys[i], s.keys[j] = s.keys[j], s.keys[i] +} + +// sortAndSweep will sort the intervalKeys using intervalKeySorter, remove the +// duplicate fileIntervals, and set the {min, max}IntervalIndex for the files. +func sortAndSweep(keys []intervalKeyTemp, cmp Compare) []intervalKeyTemp { + if len(keys) == 0 { + return nil + } + sorter := intervalKeySorter{keys: keys, cmp: cmp} + sort.Sort(sorter) + + // intervalKeys are generated using the file bounds. Specifically, there are + // 2 intervalKeys for each file, and len(keys) = 2 * number of files. Each + // `intervalKeyTemp` stores information about which file it was generated + // from, and whether the key represents the end key of the file. So, as + // we're deduplicating the `keys` slice, we're guaranteed to iterate over + // the interval keys belonging to each of the files. Since the + // file.{min,max}IntervalIndex points to the position of the files bounds in + // the deduplicated `keys` slice, we can determine + // file.{min,max}IntervalIndex during the iteration. + i := 0 + j := 0 + for i < len(keys) { + // loop invariant: j <= i + currKey := keys[i] + keys[j] = keys[i] + + for { + keys[i].setFileIntervalIndex(j) + i++ + if i >= len(keys) || intervalKeyCompare(cmp, currKey.intervalKey, keys[i].intervalKey) != 0 { + break + } + } + j++ + } + return keys[:j] +} + +// A key interval of the form [start, end). The end is not represented here +// since it is implicit in the start of the next interval. The last interval is +// an exception but we don't need to ever lookup the end of that interval; the +// last fileInterval will only act as an end key marker. The set of intervals +// is const after initialization. +type fileInterval struct { + index int + startKey intervalKey + + // True iff some file in this interval is compacting to base. Such intervals + // cannot have any files participate in L0 -> Lbase compactions. + isBaseCompacting bool + + // The min and max intervals index across all the files that overlap with + // this interval. Inclusive on both sides. + filesMinIntervalIndex int + filesMaxIntervalIndex int + + // True if another interval that has a file extending into this interval is + // undergoing a compaction into Lbase. In other words, this bool is true if + // any interval in [filesMinIntervalIndex, filesMaxIntervalIndex] has + // isBaseCompacting set to true. This lets the compaction picker + // de-prioritize this interval for picking compactions, since there's a high + // chance that a base compaction with a sufficient height of sublevels + // rooted at this interval could not be chosen due to the ongoing base + // compaction in the other interval. If the file straddling the two + // intervals is at a sufficiently high sublevel (with enough compactible + // files below it to satisfy minCompactionDepth), this is not an issue, but + // to optimize for quickly picking base compactions far away from other base + // compactions, this bool is used as a heuristic (but not as a complete + // disqualifier). + intervalRangeIsBaseCompacting bool + + // All files in this interval, in increasing sublevel order. + files []*FileMetadata + + // len(files) - compactingFileCount is the stack depth that requires + // starting new compactions. This metric is not precise since the + // compactingFileCount can include files that are part of N (where N > 1) + // intra-L0 compactions, so the stack depth after those complete will be + // len(files) - compactingFileCount + N. We ignore this imprecision since we + // don't want to track which files are part of which intra-L0 compaction. + compactingFileCount int + + // Interpolated from files in this interval. For files spanning multiple + // intervals, we assume an equal distribution of bytes across all those + // intervals. + estimatedBytes uint64 +} + +// Helper type for any cases requiring a bool slice. +type bitSet []bool + +func newBitSet(n int) bitSet { + return make([]bool, n) +} + +func (b *bitSet) markBit(i int) { + (*b)[i] = true +} + +func (b *bitSet) markBits(start, end int) { + for i := start; i < end; i++ { + (*b)[i] = true + } +} + +func (b *bitSet) clearAllBits() { + for i := range *b { + (*b)[i] = false + } +} + +// L0Compaction describes an active compaction with inputs from L0. +type L0Compaction struct { + Smallest InternalKey + Largest InternalKey + IsIntraL0 bool +} + +// L0Sublevels represents a sublevel view of SSTables in L0. Tables in one +// sublevel are non-overlapping in key ranges, and keys in higher-indexed +// sublevels shadow older versions in lower-indexed sublevels. These invariants +// are similar to the regular level invariants, except with higher indexed +// sublevels having newer keys as opposed to lower indexed levels. +// +// There is no limit to the number of sublevels that can exist in L0 at any +// time, however read and compaction performance is best when there are as few +// sublevels as possible. +type L0Sublevels struct { + // Levels are ordered from oldest sublevel to youngest sublevel in the + // outer slice, and the inner slice contains non-overlapping files for + // that sublevel in increasing key order. Levels is constructed from + // levelFiles and is used by callers that require a LevelSlice. The below two + // fields are treated as immutable once created in NewL0Sublevels. + Levels []LevelSlice + levelFiles [][]*FileMetadata + + cmp Compare + formatKey base.FormatKey + + fileBytes uint64 + // All the L0 files, ordered from oldest to youngest. + levelMetadata *LevelMetadata + + // The file intervals in increasing key order. + orderedIntervals []fileInterval + + // Keys to break flushes at. + flushSplitUserKeys [][]byte + + // Only used to check invariants. + addL0FilesCalled bool +} + +type sublevelSorter []*FileMetadata + +// Len implements sort.Interface. +func (sl sublevelSorter) Len() int { + return len(sl) +} + +// Less implements sort.Interface. +func (sl sublevelSorter) Less(i, j int) bool { + return sl[i].minIntervalIndex < sl[j].minIntervalIndex +} + +// Swap implements sort.Interface. +func (sl sublevelSorter) Swap(i, j int) { + sl[i], sl[j] = sl[j], sl[i] +} + +// NewL0Sublevels creates an L0Sublevels instance for a given set of L0 files. +// These files must all be in L0 and must be sorted by seqnum (see +// SortBySeqNum). During interval iteration, when flushSplitMaxBytes bytes are +// exceeded in the range of intervals since the last flush split key, a flush +// split key is added. +// +// This method can be called without DB.mu being held, so any DB.mu protected +// fields in FileMetadata cannot be accessed here, such as Compacting and +// IsIntraL0Compacting. Those fields are accessed in InitCompactingFileInfo +// instead. +func NewL0Sublevels( + levelMetadata *LevelMetadata, cmp Compare, formatKey base.FormatKey, flushSplitMaxBytes int64, +) (*L0Sublevels, error) { + s := &L0Sublevels{cmp: cmp, formatKey: formatKey} + s.levelMetadata = levelMetadata + keys := make([]intervalKeyTemp, 0, 2*s.levelMetadata.Len()) + iter := levelMetadata.Iter() + for i, f := 0, iter.First(); f != nil; i, f = i+1, iter.Next() { + f.L0Index = i + keys = append(keys, intervalKeyTemp{ + intervalKey: intervalKey{key: f.Smallest.UserKey}, + fileMeta: f, + isEndKey: false, + }) + keys = append(keys, intervalKeyTemp{ + intervalKey: intervalKey{ + key: f.Largest.UserKey, + isLargest: !f.Largest.IsExclusiveSentinel(), + }, + fileMeta: f, + isEndKey: true, + }) + } + keys = sortAndSweep(keys, cmp) + // All interval indices reference s.orderedIntervals. + s.orderedIntervals = make([]fileInterval, len(keys)) + for i := range keys { + s.orderedIntervals[i] = fileInterval{ + index: i, + startKey: keys[i].intervalKey, + filesMinIntervalIndex: i, + filesMaxIntervalIndex: i, + } + } + // Initialize minIntervalIndex and maxIntervalIndex for each file, and use that + // to update intervals. + for f := iter.First(); f != nil; f = iter.Next() { + if err := s.addFileToSublevels(f, false /* checkInvariant */); err != nil { + return nil, err + } + } + // Sort each sublevel in increasing key order. + for i := range s.levelFiles { + sort.Sort(sublevelSorter(s.levelFiles[i])) + } + + // Construct a parallel slice of sublevel B-Trees. + // TODO(jackson): Consolidate and only use the B-Trees. + for _, sublevelFiles := range s.levelFiles { + tr, ls := makeBTree(btreeCmpSmallestKey(cmp), sublevelFiles) + s.Levels = append(s.Levels, ls) + tr.Release() + } + + s.calculateFlushSplitKeys(flushSplitMaxBytes) + return s, nil +} + +// Helper function to merge new intervalKeys into an existing slice of old +// fileIntervals, into result. Returns the new result and a slice of ints +// mapping old interval indices to new ones. The added intervalKeys do not need +// to be sorted; they get sorted and deduped in this function. +func mergeIntervals( + old, result []fileInterval, added []intervalKeyTemp, compare Compare, +) ([]fileInterval, []int) { + sorter := intervalKeySorter{keys: added, cmp: compare} + sort.Sort(sorter) + + oldToNewMap := make([]int, len(old)) + i := 0 + j := 0 + + for i < len(old) || j < len(added) { + for j > 0 && j < len(added) && intervalKeyCompare(compare, added[j-1].intervalKey, added[j].intervalKey) == 0 { + added[j].setFileIntervalIndex(len(result) - 1) + j++ + } + if i >= len(old) && j >= len(added) { + break + } + var cmp int + if i >= len(old) { + cmp = +1 + } + if j >= len(added) { + cmp = -1 + } + if cmp == 0 { + cmp = intervalKeyCompare(compare, old[i].startKey, added[j].intervalKey) + } + switch { + case cmp <= 0: + // Shallow-copy the existing interval. + newInterval := old[i] + result = append(result, newInterval) + oldToNewMap[i] = len(result) - 1 + i++ + if cmp == 0 { + added[j].setFileIntervalIndex(len(result) - 1) + j++ + } + case cmp > 0: + var prevInterval fileInterval + // Insert a new interval for a newly-added file. prevInterval, if + // non-zero, will be "inherited"; we copy its files as those extend + // into this interval. + if len(result) > 0 { + prevInterval = result[len(result)-1] + } + newInterval := fileInterval{ + index: len(result), + startKey: added[j].intervalKey, + filesMinIntervalIndex: len(result), + filesMaxIntervalIndex: len(result), + + // estimatedBytes gets recalculated later on, as the number of intervals + // the file bytes are interpolated over has changed. + estimatedBytes: 0, + // Copy the below attributes from prevInterval. + files: append([]*FileMetadata(nil), prevInterval.files...), + isBaseCompacting: prevInterval.isBaseCompacting, + intervalRangeIsBaseCompacting: prevInterval.intervalRangeIsBaseCompacting, + compactingFileCount: prevInterval.compactingFileCount, + } + result = append(result, newInterval) + added[j].setFileIntervalIndex(len(result) - 1) + j++ + } + } + return result, oldToNewMap +} + +// AddL0Files incrementally builds a new L0Sublevels for when the only change +// since the receiver L0Sublevels was an addition of the specified files, with +// no L0 deletions. The common case of this is an ingestion or a flush. These +// files can "sit on top" of existing sublevels, creating at most one new +// sublevel for a flush (and possibly multiple for an ingestion), and at most +// 2*len(files) additions to s.orderedIntervals. No files must have been deleted +// from L0, and the added files must all be newer in sequence numbers than +// existing files in L0Sublevels. The files parameter must be sorted in seqnum +// order. The levelMetadata parameter corresponds to the new L0 post addition of +// files. This method is meant to be significantly more performant than +// NewL0Sublevels. +// +// Note that this function can only be called once on a given receiver; it +// appends to some slices in s which is only safe when done once. This is okay, +// as the common case (generating a new L0Sublevels after a flush/ingestion) is +// only going to necessitate one call of this method on a given receiver. The +// returned value, if non-nil, can then have [*L0Sublevels.AddL0Files] called on +// it again, and so on. If [errInvalidL0SublevelsOpt] is returned as an error, +// it likely means the optimization could not be applied (i.e. files added were +// older than files already in the sublevels, which is possible around +// ingestions and in tests). Eg. it can happen when an ingested file was +// ingested without queueing a flush since it did not actually overlap with any +// keys in the memtable. Later on the memtable was flushed, and the memtable had +// keys spanning around the ingested file, producing a flushed file that +// overlapped with the ingested file in file bounds but not in keys. It's +// possible for that flushed file to have a lower LargestSeqNum than the +// ingested file if all the additions after the ingestion were to another +// flushed file that was split into a separate sstable during flush. Any other +// non-nil error means [L0Sublevels] generation failed in the same way as +// [NewL0Sublevels] would likely fail. +func (s *L0Sublevels) AddL0Files( + files []*FileMetadata, flushSplitMaxBytes int64, levelMetadata *LevelMetadata, +) (*L0Sublevels, error) { + if invariants.Enabled && s.addL0FilesCalled { + panic("AddL0Files called twice on the same receiver") + } + s.addL0FilesCalled = true + + // Start with a shallow copy of s. + newVal := &L0Sublevels{} + *newVal = *s + + newVal.addL0FilesCalled = false + newVal.levelMetadata = levelMetadata + // Deep copy levelFiles and Levels, as they are mutated and sorted below. + // Shallow copies of slices that we just append to, are okay. + newVal.levelFiles = make([][]*FileMetadata, len(s.levelFiles)) + for i := range s.levelFiles { + newVal.levelFiles[i] = make([]*FileMetadata, len(s.levelFiles[i])) + copy(newVal.levelFiles[i], s.levelFiles[i]) + } + newVal.Levels = make([]LevelSlice, len(s.Levels)) + copy(newVal.Levels, s.Levels) + + fileKeys := make([]intervalKeyTemp, 0, 2*len(files)) + for _, f := range files { + left := intervalKeyTemp{ + intervalKey: intervalKey{key: f.Smallest.UserKey}, + fileMeta: f, + } + right := intervalKeyTemp{ + intervalKey: intervalKey{ + key: f.Largest.UserKey, + isLargest: !f.Largest.IsExclusiveSentinel(), + }, + fileMeta: f, + isEndKey: true, + } + fileKeys = append(fileKeys, left, right) + } + keys := make([]fileInterval, 0, 2*levelMetadata.Len()) + var oldToNewMap []int + // We can avoid the sortAndSweep step on the combined length of + // s.orderedIntervals and fileKeys by treating this as a merge of two sorted + // runs, fileKeys and s.orderedIntervals, into `keys` which will form + // newVal.orderedIntervals. + keys, oldToNewMap = mergeIntervals(s.orderedIntervals, keys, fileKeys, s.cmp) + if invariants.Enabled { + for i := 1; i < len(keys); i++ { + if intervalKeyCompare(newVal.cmp, keys[i-1].startKey, keys[i].startKey) >= 0 { + panic("keys not sorted correctly") + } + } + } + newVal.orderedIntervals = keys + // Update indices in s.orderedIntervals for fileIntervals we retained. + for _, newIdx := range oldToNewMap { + newInterval := &keys[newIdx] + newInterval.index = newIdx + // This code, and related code in the for loop below, adjusts + // files{Min,Max}IntervalIndex just for interval indices shifting due to + // new intervals, and not for any of the new files being added to the + // same intervals. The goal is to produce a state of the system that's + // accurate for all existing files, and has all the new intervals to + // support new files. Once that's done, we can just call + // addFileToSublevel to adjust all relevant intervals for new files. + newInterval.filesMinIntervalIndex = oldToNewMap[newInterval.filesMinIntervalIndex] + // maxIntervalIndexes are special. Since it's an inclusive end bound, we + // actually have to map it to the _next_ old interval's new previous + // interval. This logic is easier to understand if you see + // [f.minIntervalIndex, f.maxIntervalIndex] as [f.minIntervalIndex, + // f.maxIntervalIndex+1). The other case to remember is when the + // interval is completely empty (i.e. len(newInterval.files) == 0); in + // that case we want to refer back to ourselves regardless of additions + // to the right of us. + if newInterval.filesMaxIntervalIndex < len(oldToNewMap)-1 && len(newInterval.files) > 0 { + newInterval.filesMaxIntervalIndex = oldToNewMap[newInterval.filesMaxIntervalIndex+1] - 1 + } else { + // newInterval.filesMaxIntervalIndex == len(oldToNewMap)-1. + newInterval.filesMaxIntervalIndex = oldToNewMap[newInterval.filesMaxIntervalIndex] + } + } + // Loop through all instances of new intervals added between two old + // intervals and expand [filesMinIntervalIndex, filesMaxIntervalIndex] of + // new intervals to reflect that of adjacent old intervals. + { + // We can skip cases where new intervals were added to the left of all + // existing intervals (eg. if the first entry in oldToNewMap is + // oldToNewMap[0] >= 1). Those intervals will only contain newly added + // files and will have their parameters adjusted down in + // addFileToSublevels. The same can also be said about new intervals + // that are to the right of all existing intervals. + lastIdx := 0 + for _, newIdx := range oldToNewMap { + for i := lastIdx + 1; i < newIdx; i++ { + minIntervalIndex := i + maxIntervalIndex := i + if keys[lastIdx].filesMaxIntervalIndex != lastIdx { + // Last old interval has files extending into keys[i]. + minIntervalIndex = keys[lastIdx].filesMinIntervalIndex + maxIntervalIndex = keys[lastIdx].filesMaxIntervalIndex + } + + keys[i].filesMinIntervalIndex = minIntervalIndex + keys[i].filesMaxIntervalIndex = maxIntervalIndex + } + lastIdx = newIdx + } + } + // Go through old files and update interval indices. + // + // TODO(bilal): This is the only place in this method where we loop through + // all existing files, which could be much more in number than newly added + // files. See if we can avoid the need for this, either by getting rid of + // f.minIntervalIndex and f.maxIntervalIndex and calculating them on the fly + // with a binary search, or by only looping through files to the right of + // the first interval touched by this method. + for sublevel := range s.Levels { + s.Levels[sublevel].Each(func(f *FileMetadata) { + oldIntervalDelta := f.maxIntervalIndex - f.minIntervalIndex + 1 + oldMinIntervalIndex := f.minIntervalIndex + f.minIntervalIndex = oldToNewMap[f.minIntervalIndex] + // maxIntervalIndex is special. Since it's an inclusive end bound, + // we actually have to map it to the _next_ old interval's new + // previous interval. This logic is easier to understand if you see + // [f.minIntervalIndex, f.maxIntervalIndex] as [f.minIntervalIndex, + // f.maxIntervalIndex+1). + f.maxIntervalIndex = oldToNewMap[f.maxIntervalIndex+1] - 1 + newIntervalDelta := f.maxIntervalIndex - f.minIntervalIndex + 1 + // Recalculate estimatedBytes for all old files across new + // intervals, but only if new intervals were added in between. + if oldIntervalDelta != newIntervalDelta { + // j is incremented so that oldToNewMap[j] points to the next + // old interval. This is used to distinguish between old + // intervals (i.e. ones where we need to subtract + // f.Size/oldIntervalDelta) from new ones (where we don't need + // to subtract). In both cases we need to add + // f.Size/newIntervalDelta. + j := oldMinIntervalIndex + for i := f.minIntervalIndex; i <= f.maxIntervalIndex; i++ { + if oldToNewMap[j] == i { + newVal.orderedIntervals[i].estimatedBytes -= f.Size / uint64(oldIntervalDelta) + j++ + } + newVal.orderedIntervals[i].estimatedBytes += f.Size / uint64(newIntervalDelta) + } + } + }) + } + updatedSublevels := make([]int, 0) + // Update interval indices for new files. + for i, f := range files { + f.L0Index = s.levelMetadata.Len() + i + if err := newVal.addFileToSublevels(f, true /* checkInvariant */); err != nil { + return nil, err + } + updatedSublevels = append(updatedSublevels, f.SubLevel) + } + + // Sort and deduplicate updatedSublevels. + sort.Ints(updatedSublevels) + { + j := 0 + for i := 1; i < len(updatedSublevels); i++ { + if updatedSublevels[i] != updatedSublevels[j] { + j++ + updatedSublevels[j] = updatedSublevels[i] + } + } + updatedSublevels = updatedSublevels[:j+1] + } + + // Sort each updated sublevel in increasing key order. + for _, sublevel := range updatedSublevels { + sort.Sort(sublevelSorter(newVal.levelFiles[sublevel])) + } + + // Construct a parallel slice of sublevel B-Trees. + // TODO(jackson): Consolidate and only use the B-Trees. + for _, sublevel := range updatedSublevels { + tr, ls := makeBTree(btreeCmpSmallestKey(newVal.cmp), newVal.levelFiles[sublevel]) + if sublevel == len(newVal.Levels) { + newVal.Levels = append(newVal.Levels, ls) + } else { + // sublevel < len(s.Levels). If this panics, updatedSublevels was not + // populated correctly. + newVal.Levels[sublevel] = ls + } + tr.Release() + } + + newVal.flushSplitUserKeys = nil + newVal.calculateFlushSplitKeys(flushSplitMaxBytes) + return newVal, nil +} + +// addFileToSublevels is called during L0Sublevels generation, and adds f to the +// correct sublevel's levelFiles, the relevant intervals' files slices, and sets +// interval indices on f. This method, if called successively on multiple files, +// _must_ be called on successively newer files (by seqnum). If checkInvariant +// is true, it could check for this in some cases and return +// [errInvalidL0SublevelsOpt] if that invariant isn't held. +func (s *L0Sublevels) addFileToSublevels(f *FileMetadata, checkInvariant bool) error { + // This is a simple and not very accurate estimate of the number of + // bytes this SSTable contributes to the intervals it is a part of. + // + // TODO(bilal): Call EstimateDiskUsage in sstable.Reader with interval + // bounds to get a better estimate for each interval. + interpolatedBytes := f.Size / uint64(f.maxIntervalIndex-f.minIntervalIndex+1) + s.fileBytes += f.Size + subLevel := 0 + // Update state in every fileInterval for this file. + for i := f.minIntervalIndex; i <= f.maxIntervalIndex; i++ { + interval := &s.orderedIntervals[i] + if len(interval.files) > 0 { + if checkInvariant && interval.files[len(interval.files)-1].LargestSeqNum > f.LargestSeqNum { + // We are sliding this file "underneath" an existing file. Throw away + // and start over in NewL0Sublevels. + return errInvalidL0SublevelsOpt + } + // interval.files is sorted by sublevels, from lowest to highest. + // AddL0Files can only add files at sublevels higher than existing files + // in the same key intervals. + if maxSublevel := interval.files[len(interval.files)-1].SubLevel; subLevel <= maxSublevel { + subLevel = maxSublevel + 1 + } + } + interval.estimatedBytes += interpolatedBytes + if f.minIntervalIndex < interval.filesMinIntervalIndex { + interval.filesMinIntervalIndex = f.minIntervalIndex + } + if f.maxIntervalIndex > interval.filesMaxIntervalIndex { + interval.filesMaxIntervalIndex = f.maxIntervalIndex + } + interval.files = append(interval.files, f) + } + f.SubLevel = subLevel + if subLevel > len(s.levelFiles) { + return errors.Errorf("chose a sublevel beyond allowed range of sublevels: %d vs 0-%d", subLevel, len(s.levelFiles)) + } + if subLevel == len(s.levelFiles) { + s.levelFiles = append(s.levelFiles, []*FileMetadata{f}) + } else { + s.levelFiles[subLevel] = append(s.levelFiles[subLevel], f) + } + return nil +} + +func (s *L0Sublevels) calculateFlushSplitKeys(flushSplitMaxBytes int64) { + var cumulativeBytes uint64 + // Multiply flushSplitMaxBytes by the number of sublevels. This prevents + // excessive flush splitting when the number of sublevels increases. + flushSplitMaxBytes *= int64(len(s.levelFiles)) + for i := 0; i < len(s.orderedIntervals); i++ { + interval := &s.orderedIntervals[i] + if flushSplitMaxBytes > 0 && cumulativeBytes > uint64(flushSplitMaxBytes) && + (len(s.flushSplitUserKeys) == 0 || + !bytes.Equal(interval.startKey.key, s.flushSplitUserKeys[len(s.flushSplitUserKeys)-1])) { + s.flushSplitUserKeys = append(s.flushSplitUserKeys, interval.startKey.key) + cumulativeBytes = 0 + } + cumulativeBytes += s.orderedIntervals[i].estimatedBytes + } +} + +// InitCompactingFileInfo initializes internal flags relating to compacting +// files. Must be called after sublevel initialization. +// +// Requires DB.mu *and* the manifest lock to be held. +func (s *L0Sublevels) InitCompactingFileInfo(inProgress []L0Compaction) { + for i := range s.orderedIntervals { + s.orderedIntervals[i].compactingFileCount = 0 + s.orderedIntervals[i].isBaseCompacting = false + s.orderedIntervals[i].intervalRangeIsBaseCompacting = false + } + + iter := s.levelMetadata.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if invariants.Enabled { + if !bytes.Equal(s.orderedIntervals[f.minIntervalIndex].startKey.key, f.Smallest.UserKey) { + panic(fmt.Sprintf("f.minIntervalIndex in FileMetadata out of sync with intervals in L0Sublevels: %s != %s", + s.formatKey(s.orderedIntervals[f.minIntervalIndex].startKey.key), s.formatKey(f.Smallest.UserKey))) + } + if !bytes.Equal(s.orderedIntervals[f.maxIntervalIndex+1].startKey.key, f.Largest.UserKey) { + panic(fmt.Sprintf("f.maxIntervalIndex in FileMetadata out of sync with intervals in L0Sublevels: %s != %s", + s.formatKey(s.orderedIntervals[f.maxIntervalIndex+1].startKey.key), s.formatKey(f.Smallest.UserKey))) + } + } + if !f.IsCompacting() { + continue + } + if invariants.Enabled { + if s.cmp(s.orderedIntervals[f.minIntervalIndex].startKey.key, f.Smallest.UserKey) != 0 || s.cmp(s.orderedIntervals[f.maxIntervalIndex+1].startKey.key, f.Largest.UserKey) != 0 { + panic(fmt.Sprintf("file %s has inconsistent L0 Sublevel interval bounds: %s-%s, %s-%s", f.FileNum, + s.orderedIntervals[f.minIntervalIndex].startKey.key, s.orderedIntervals[f.maxIntervalIndex+1].startKey.key, + f.Smallest.UserKey, f.Largest.UserKey)) + } + } + for i := f.minIntervalIndex; i <= f.maxIntervalIndex; i++ { + interval := &s.orderedIntervals[i] + interval.compactingFileCount++ + if !f.IsIntraL0Compacting { + // If f.Compacting && !f.IsIntraL0Compacting, this file is + // being compacted to Lbase. + interval.isBaseCompacting = true + } + } + } + + // Some intervals may be base compacting without the files contained within + // those intervals being marked as compacting. This is possible if the files + // were added after the compaction initiated, and the active compaction + // files straddle the input file. Mark these intervals as base compacting. + for _, c := range inProgress { + startIK := intervalKey{key: c.Smallest.UserKey, isLargest: false} + endIK := intervalKey{key: c.Largest.UserKey, isLargest: !c.Largest.IsExclusiveSentinel()} + start, _ := slices.BinarySearchFunc(s.orderedIntervals, startIK, func(a fileInterval, b intervalKey) int { + return intervalKeyCompare(s.cmp, a.startKey, b) + }) + end, _ := slices.BinarySearchFunc(s.orderedIntervals, endIK, func(a fileInterval, b intervalKey) int { + return intervalKeyCompare(s.cmp, a.startKey, b) + }) + for i := start; i < end && i < len(s.orderedIntervals); i++ { + interval := &s.orderedIntervals[i] + if !c.IsIntraL0 { + interval.isBaseCompacting = true + } + } + } + + min := 0 + for i := range s.orderedIntervals { + interval := &s.orderedIntervals[i] + if interval.isBaseCompacting { + minIndex := interval.filesMinIntervalIndex + if minIndex < min { + minIndex = min + } + for j := minIndex; j <= interval.filesMaxIntervalIndex; j++ { + min = j + s.orderedIntervals[j].intervalRangeIsBaseCompacting = true + } + } + } +} + +// String produces a string containing useful debug information. Useful in test +// code and debugging. +func (s *L0Sublevels) String() string { + return s.describe(false) +} + +func (s *L0Sublevels) describe(verbose bool) string { + var buf strings.Builder + fmt.Fprintf(&buf, "file count: %d, sublevels: %d, intervals: %d\nflush split keys(%d): [", + s.levelMetadata.Len(), len(s.levelFiles), len(s.orderedIntervals), len(s.flushSplitUserKeys)) + for i := range s.flushSplitUserKeys { + fmt.Fprintf(&buf, "%s", s.formatKey(s.flushSplitUserKeys[i])) + if i < len(s.flushSplitUserKeys)-1 { + fmt.Fprintf(&buf, ", ") + } + } + fmt.Fprintln(&buf, "]") + numCompactingFiles := 0 + for i := len(s.levelFiles) - 1; i >= 0; i-- { + maxIntervals := 0 + sumIntervals := 0 + var totalBytes uint64 + for _, f := range s.levelFiles[i] { + intervals := f.maxIntervalIndex - f.minIntervalIndex + 1 + if intervals > maxIntervals { + maxIntervals = intervals + } + sumIntervals += intervals + totalBytes += f.Size + if f.IsCompacting() { + numCompactingFiles++ + } + } + fmt.Fprintf(&buf, "0.%d: file count: %d, bytes: %d, width (mean, max): %0.1f, %d, interval range: [%d, %d]\n", + i, len(s.levelFiles[i]), totalBytes, float64(sumIntervals)/float64(len(s.levelFiles[i])), maxIntervals, s.levelFiles[i][0].minIntervalIndex, + s.levelFiles[i][len(s.levelFiles[i])-1].maxIntervalIndex) + for _, f := range s.levelFiles[i] { + intervals := f.maxIntervalIndex - f.minIntervalIndex + 1 + if verbose { + fmt.Fprintf(&buf, "\t%s\n", f) + } + if s.levelMetadata.Len() > 50 && intervals*3 > len(s.orderedIntervals) { + var intervalsBytes uint64 + for k := f.minIntervalIndex; k <= f.maxIntervalIndex; k++ { + intervalsBytes += s.orderedIntervals[k].estimatedBytes + } + fmt.Fprintf(&buf, "wide file: %d, [%d, %d], byte fraction: %f\n", + f.FileNum, f.minIntervalIndex, f.maxIntervalIndex, + float64(intervalsBytes)/float64(s.fileBytes)) + } + } + } + + lastCompactingIntervalStart := -1 + fmt.Fprintf(&buf, "compacting file count: %d, base compacting intervals: ", numCompactingFiles) + i := 0 + foundBaseCompactingIntervals := false + for ; i < len(s.orderedIntervals); i++ { + interval := &s.orderedIntervals[i] + if len(interval.files) == 0 { + continue + } + if !interval.isBaseCompacting { + if lastCompactingIntervalStart != -1 { + if foundBaseCompactingIntervals { + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "[%d, %d]", lastCompactingIntervalStart, i-1) + foundBaseCompactingIntervals = true + } + lastCompactingIntervalStart = -1 + } else { + if lastCompactingIntervalStart == -1 { + lastCompactingIntervalStart = i + } + } + } + if lastCompactingIntervalStart != -1 { + if foundBaseCompactingIntervals { + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "[%d, %d]", lastCompactingIntervalStart, i-1) + } else if !foundBaseCompactingIntervals { + fmt.Fprintf(&buf, "none") + } + fmt.Fprintln(&buf, "") + return buf.String() +} + +// ReadAmplification returns the contribution of L0Sublevels to the read +// amplification for any particular point key. It is the maximum height of any +// tracked fileInterval. This is always less than or equal to the number of +// sublevels. +func (s *L0Sublevels) ReadAmplification() int { + amp := 0 + for i := range s.orderedIntervals { + interval := &s.orderedIntervals[i] + fileCount := len(interval.files) + if amp < fileCount { + amp = fileCount + } + } + return amp +} + +// UserKeyRange encodes a key range in user key space. A UserKeyRange's Start +// and End boundaries are both inclusive. +type UserKeyRange struct { + Start, End []byte +} + +// InUseKeyRanges returns the merged table bounds of L0 files overlapping the +// provided user key range. The returned key ranges are sorted and +// nonoverlapping. +func (s *L0Sublevels) InUseKeyRanges(smallest, largest []byte) []UserKeyRange { + // Binary search to find the provided keys within the intervals. + startIK := intervalKey{key: smallest, isLargest: false} + endIK := intervalKey{key: largest, isLargest: true} + start := sort.Search(len(s.orderedIntervals), func(i int) bool { + return intervalKeyCompare(s.cmp, s.orderedIntervals[i].startKey, startIK) > 0 + }) + if start > 0 { + // Back up to the first interval with a start key <= startIK. + start-- + } + end := sort.Search(len(s.orderedIntervals), func(i int) bool { + return intervalKeyCompare(s.cmp, s.orderedIntervals[i].startKey, endIK) > 0 + }) + + var keyRanges []UserKeyRange + var curr *UserKeyRange + for i := start; i < end; { + // Intervals with no files are not in use and can be skipped, once we + // end the current UserKeyRange. + if len(s.orderedIntervals[i].files) == 0 { + curr = nil + i++ + continue + } + + // If curr is nil, start a new in-use key range. + if curr == nil { + keyRanges = append(keyRanges, UserKeyRange{ + Start: s.orderedIntervals[i].startKey.key, + }) + curr = &keyRanges[len(keyRanges)-1] + } + + // If the filesMaxIntervalIndex is not the current index, we can jump to + // the max index, knowing that all intermediary intervals are overlapped + // by some file. + if maxIdx := s.orderedIntervals[i].filesMaxIntervalIndex; maxIdx != i { + // Note that end may be less than or equal to maxIdx if we're + // concerned with a key range that ends before the interval at + // maxIdx starts. We must set curr.End now, before making that leap, + // because this iteration may be the last. + i = maxIdx + curr.End = s.orderedIntervals[i+1].startKey.key + continue + } + + // No files overlapping with this interval overlap with the next + // interval. Update the current end to be the next interval's start key. + // Note that curr is not necessarily finished, because there may be an + // abutting non-empty interval. + curr.End = s.orderedIntervals[i+1].startKey.key + i++ + } + return keyRanges +} + +// FlushSplitKeys returns a slice of user keys to split flushes at. Used by +// flushes to avoid writing sstables that straddle these split keys. These +// should be interpreted as the keys to start the next sstable (not the last key +// to include in the prev sstable). These are user keys so that range tombstones +// can be properly truncated (untruncated range tombstones are not permitted for +// L0 files). +func (s *L0Sublevels) FlushSplitKeys() [][]byte { + return s.flushSplitUserKeys +} + +// MaxDepthAfterOngoingCompactions returns an estimate of maximum depth of +// sublevels after all ongoing compactions run to completion. Used by compaction +// picker to decide compaction score for L0. There is no scoring for intra-L0 +// compactions -- they only run if L0 score is high but we're unable to pick an +// L0 -> Lbase compaction. +func (s *L0Sublevels) MaxDepthAfterOngoingCompactions() int { + depth := 0 + for i := range s.orderedIntervals { + interval := &s.orderedIntervals[i] + intervalDepth := len(interval.files) - interval.compactingFileCount + if depth < intervalDepth { + depth = intervalDepth + } + } + return depth +} + +// Only for temporary debugging in the absence of proper tests. +// +// TODO(bilal): Simplify away the debugging statements in this method, and make +// this a pure sanity checker. +// +//lint:ignore U1000 - useful for debugging +func (s *L0Sublevels) checkCompaction(c *L0CompactionFiles) error { + includedFiles := newBitSet(s.levelMetadata.Len()) + fileIntervalsByLevel := make([]struct { + min int + max int + }, len(s.levelFiles)) + for i := range fileIntervalsByLevel { + fileIntervalsByLevel[i].min = math.MaxInt32 + fileIntervalsByLevel[i].max = 0 + } + var topLevel int + var increment int + var limitReached func(int) bool + if c.isIntraL0 { + topLevel = len(s.levelFiles) - 1 + increment = +1 + limitReached = func(level int) bool { + return level == len(s.levelFiles) + } + } else { + topLevel = 0 + increment = -1 + limitReached = func(level int) bool { + return level < 0 + } + } + for _, f := range c.Files { + if fileIntervalsByLevel[f.SubLevel].min > f.minIntervalIndex { + fileIntervalsByLevel[f.SubLevel].min = f.minIntervalIndex + } + if fileIntervalsByLevel[f.SubLevel].max < f.maxIntervalIndex { + fileIntervalsByLevel[f.SubLevel].max = f.maxIntervalIndex + } + includedFiles.markBit(f.L0Index) + if c.isIntraL0 { + if topLevel > f.SubLevel { + topLevel = f.SubLevel + } + } else { + if topLevel < f.SubLevel { + topLevel = f.SubLevel + } + } + } + min := fileIntervalsByLevel[topLevel].min + max := fileIntervalsByLevel[topLevel].max + for level := topLevel; !limitReached(level); level += increment { + if fileIntervalsByLevel[level].min < min { + min = fileIntervalsByLevel[level].min + } + if fileIntervalsByLevel[level].max > max { + max = fileIntervalsByLevel[level].max + } + index, _ := slices.BinarySearchFunc(s.levelFiles[level], min, func(a *FileMetadata, b int) int { + return stdcmp.Compare(a.maxIntervalIndex, b) + }) + // start := index + for ; index < len(s.levelFiles[level]); index++ { + f := s.levelFiles[level][index] + if f.minIntervalIndex > max { + break + } + if c.isIntraL0 && f.LargestSeqNum >= c.earliestUnflushedSeqNum { + return errors.Errorf( + "sstable %s in compaction has sequence numbers higher than the earliest unflushed seqnum %d: %d-%d", + f.FileNum, c.earliestUnflushedSeqNum, f.SmallestSeqNum, + f.LargestSeqNum) + } + if !includedFiles[f.L0Index] { + var buf strings.Builder + fmt.Fprintf(&buf, "bug %t, seed interval: %d: level %d, sl index %d, f.index %d, min %d, max %d, pre-min %d, pre-max %d, f.min %d, f.max %d, filenum: %d, isCompacting: %t\n%s\n", + c.isIntraL0, c.seedInterval, level, index, f.L0Index, min, max, c.preExtensionMinInterval, c.preExtensionMaxInterval, + f.minIntervalIndex, f.maxIntervalIndex, + f.FileNum, f.IsCompacting(), s) + fmt.Fprintf(&buf, "files included:\n") + for _, f := range c.Files { + fmt.Fprintf(&buf, "filenum: %d, sl: %d, index: %d, [%d, %d]\n", + f.FileNum, f.SubLevel, f.L0Index, f.minIntervalIndex, f.maxIntervalIndex) + } + fmt.Fprintf(&buf, "files added:\n") + for _, f := range c.filesAdded { + fmt.Fprintf(&buf, "filenum: %d, sl: %d, index: %d, [%d, %d]\n", + f.FileNum, f.SubLevel, f.L0Index, f.minIntervalIndex, f.maxIntervalIndex) + } + return errors.New(buf.String()) + } + } + } + return nil +} + +// UpdateStateForStartedCompaction updates internal L0Sublevels state for a +// recently started compaction. isBase specifies if this is a base compaction; +// if false, this is assumed to be an intra-L0 compaction. The specified +// compaction must be involving L0 SSTables. It's assumed that the Compacting +// and IsIntraL0Compacting fields are already set on all [FileMetadata]s passed +// in. +func (s *L0Sublevels) UpdateStateForStartedCompaction(inputs []LevelSlice, isBase bool) error { + minIntervalIndex := -1 + maxIntervalIndex := 0 + for i := range inputs { + iter := inputs[i].Iter() + for f := iter.First(); f != nil; f = iter.Next() { + for i := f.minIntervalIndex; i <= f.maxIntervalIndex; i++ { + interval := &s.orderedIntervals[i] + interval.compactingFileCount++ + } + if f.minIntervalIndex < minIntervalIndex || minIntervalIndex == -1 { + minIntervalIndex = f.minIntervalIndex + } + if f.maxIntervalIndex > maxIntervalIndex { + maxIntervalIndex = f.maxIntervalIndex + } + } + } + if isBase { + for i := minIntervalIndex; i <= maxIntervalIndex; i++ { + interval := &s.orderedIntervals[i] + interval.isBaseCompacting = isBase + for j := interval.filesMinIntervalIndex; j <= interval.filesMaxIntervalIndex; j++ { + s.orderedIntervals[j].intervalRangeIsBaseCompacting = true + } + } + } + return nil +} + +// L0CompactionFiles represents a candidate set of L0 files for compaction. Also +// referred to as "lcf". Contains state information useful for generating the +// compaction (such as Files), as well as for picking between candidate +// compactions (eg. fileBytes and seedIntervalStackDepthReduction). +type L0CompactionFiles struct { + Files []*FileMetadata + + FilesIncluded bitSet + // A "seed interval" is an interval with a high stack depth that was chosen + // to bootstrap this compaction candidate. seedIntervalStackDepthReduction + // is the number of sublevels that have a file in the seed interval that is + // a part of this compaction. + seedIntervalStackDepthReduction int + // For base compactions, seedIntervalMinLevel is 0, and for intra-L0 + // compactions, seedIntervalMaxLevel is len(s.Files)-1 i.e. the highest + // sublevel. + seedIntervalMinLevel int + seedIntervalMaxLevel int + // Index of the seed interval. + seedInterval int + // Sum of file sizes for all files in this compaction. + fileBytes uint64 + // Intervals with index [minIntervalIndex, maxIntervalIndex] are + // participating in this compaction; it's the union set of all intervals + // overlapped by participating files. + minIntervalIndex int + maxIntervalIndex int + + // Set for intra-L0 compactions. SSTables with sequence numbers greater + // than earliestUnflushedSeqNum cannot be a part of intra-L0 compactions. + isIntraL0 bool + earliestUnflushedSeqNum uint64 + + // For debugging purposes only. Used in checkCompaction(). + preExtensionMinInterval int + preExtensionMaxInterval int + filesAdded []*FileMetadata +} + +// Clone allocates a new L0CompactionFiles, with the same underlying data. Note +// that the two fileMetadata slices contain values that point to the same +// underlying fileMetadata object. This is safe because these objects are read +// only. +func (l *L0CompactionFiles) Clone() *L0CompactionFiles { + oldLcf := *l + return &oldLcf +} + +// String merely prints the starting address of the first file, if it exists. +func (l *L0CompactionFiles) String() string { + if len(l.Files) > 0 { + return fmt.Sprintf("First File Address: %p", &l.Files[0]) + } + return "" +} + +// addFile adds the specified file to the LCF. +func (l *L0CompactionFiles) addFile(f *FileMetadata) { + if l.FilesIncluded[f.L0Index] { + return + } + l.FilesIncluded.markBit(f.L0Index) + l.Files = append(l.Files, f) + l.filesAdded = append(l.filesAdded, f) + l.fileBytes += f.Size + if f.minIntervalIndex < l.minIntervalIndex { + l.minIntervalIndex = f.minIntervalIndex + } + if f.maxIntervalIndex > l.maxIntervalIndex { + l.maxIntervalIndex = f.maxIntervalIndex + } +} + +// Helper to order intervals being considered for compaction. +type intervalAndScore struct { + interval int + score int +} +type intervalSorterByDecreasingScore []intervalAndScore + +func (is intervalSorterByDecreasingScore) Len() int { return len(is) } +func (is intervalSorterByDecreasingScore) Less(i, j int) bool { + return is[i].score > is[j].score +} +func (is intervalSorterByDecreasingScore) Swap(i, j int) { + is[i], is[j] = is[j], is[i] +} + +// Compactions: +// +// The sub-levels and intervals can be visualized in 2 dimensions as the X axis +// containing intervals in increasing order and the Y axis containing sub-levels +// (older to younger). The intervals can be sparse wrt sub-levels. We observe +// that the system is typically under severe pressure in L0 during large numbers +// of ingestions where most files added to L0 are narrow and non-overlapping. +// +// L0.1 d---g +// L0.0 c--e g--j o--s u--x +// +// As opposed to a case with a lot of wide, overlapping L0 files: +// +// L0.3 d-----------r +// L0.2 c--------o +// L0.1 b-----------q +// L0.0 a----------------x +// +// In that case we expect the rectangle represented in the good visualization +// above (i.e. the first one) to be wide and short, and not too sparse (most +// intervals will have fileCount close to the sub-level count), which would make +// it amenable to concurrent L0 -> Lbase compactions. +// +// L0 -> Lbase: The high-level goal of a L0 -> Lbase compaction is to reduce +// stack depth, by compacting files in the intervals with the highest (fileCount +// - compactingCount). Additionally, we would like compactions to not involve a +// huge number of files, so that they finish quickly, and to allow for +// concurrent L0 -> Lbase compactions when needed. In order to achieve these +// goals we would like compactions to visualize as capturing thin and tall +// rectangles. The approach below is to consider intervals in some order and +// then try to construct a compaction using the interval. The first interval we +// can construct a compaction for is the compaction that is started. There can +// be multiple heuristics in choosing the ordering of the intervals -- the code +// uses one heuristic that worked well for a large ingestion stemming from a +// cockroachdb import, but additional experimentation is necessary to pick a +// general heuristic. Additionally, the compaction that gets picked may be not +// as desirable as one that could be constructed later in terms of reducing +// stack depth (since adding more files to the compaction can get blocked by +// needing to encompass files that are already being compacted). So an +// alternative would be to try to construct more than one compaction and pick +// the best one. +// +// Here's a visualization of an ideal L0->LBase compaction selection: +// +// L0.3 a--d g-j +// L0.2 f--j r-t +// L0.1 b-d e---j +// L0.0 a--d f--j l--o p-----x +// +// Lbase a--------i m---------w +// +// The [g,j] interval has the highest stack depth, so it would have the highest +// priority for selecting a base compaction candidate. Assuming none of the +// files are already compacting, this is the compaction that will be chosen: +// +// _______ +// L0.3 a--d | g-j| +// L0.2 | f--j| r-t +// L0.1 b-d |e---j| +// L0.0 a--d | f--j| l--o p-----x +// +// Lbase a--------i m---------w +// +// Note that running this compaction will mark the a--i file in Lbase as +// compacting, and when ExtendL0ForBaseCompactionTo is called with the bounds of +// that base file, it'll expand the compaction to also include all L0 files in +// the a-d interval. The resultant compaction would then be: +// +// _____________ +// L0.3 |a--d g-j| +// L0.2 | f--j| r-t +// L0.1 | b-d e---j| +// L0.0 |a--d f--j| l--o p-----x +// +// Lbase a--------i m---------w +// +// The next best interval for base compaction would therefore be the one +// including r--t in L0.2 and p--x in L0.0, and both this compaction and the one +// picked earlier can run in parallel. This is assuming minCompactionDepth >= 2, +// otherwise the second compaction has too little depth to pick. +// +// _____________ +// L0.3 |a--d g-j| _________ +// L0.2 | f--j| | r-t | +// L0.1 | b-d e---j| | | +// L0.0 |a--d f--j| l--o |p-----x| +// +// Lbase a--------i m---------w +// +// Note that when ExtendL0ForBaseCompactionTo is called, the compaction expands +// to the following, given that the [l,o] file can be added without including +// additional files in Lbase: +// +// _____________ +// L0.3 |a--d g-j| _________ +// L0.2 | f--j| | r-t | +// L0.1 | b-d e---j|______| | +// L0.0 |a--d f--j||l--o p-----x| +// +// Lbase a--------i m---------w +// +// If an additional file existed in LBase that overlapped with [l,o], it would +// be excluded from the compaction. Concretely: +// +// _____________ +// L0.3 |a--d g-j| _________ +// L0.2 | f--j| | r-t | +// L0.1 | b-d e---j| | | +// L0.0 |a--d f--j| l--o |p-----x| +// +// Lbase a--------ij--lm---------w +// +// Intra-L0: If the L0 score is high, but PickBaseCompaction() is unable to pick +// a compaction, PickIntraL0Compaction will be used to pick an intra-L0 +// compaction. Similar to L0 -> Lbase compactions, we want to allow for multiple +// intra-L0 compactions and not generate wide output files that hinder later +// concurrency of L0 -> Lbase compactions. Also compactions that produce wide +// files don't reduce stack depth -- they represent wide rectangles in our +// visualization, which means many intervals have their depth reduced by a small +// amount. Typically, L0 files have non-overlapping sequence numbers, and +// sticking to that invariant would require us to consider intra-L0 compactions +// that proceed from youngest to oldest files, which could result in the +// aforementioned undesirable wide rectangle shape. But this non-overlapping +// sequence number is already relaxed in RocksDB -- sstables are primarily +// ordered by their largest sequence number. So we can arrange for intra-L0 +// compactions to capture thin and tall rectangles starting with the top of the +// stack (youngest files). Like the L0 -> Lbase case we order the intervals +// using a heuristic and consider each in turn. The same comment about better L0 +// -> Lbase heuristics and not being greedy applies here. +// +// Going back to a modified version of our example from earlier, let's say these +// are the base compactions in progress: +// _______ +// L0.3 a--d | g-j| _________ +// L0.2 | f--j| | r-t | +// L0.1 b-d |e---j| | | +// L0.0 a--d | f--j| l--o |p-----x| +// +// Lbase a---------i m---------w +// +// Since both LBase files are compacting, the only L0 compaction that can be +// picked is an intra-L0 compaction. For this, the b--d interval has the highest +// stack depth (3), and starting with a--d in L0.3 as the seed file, we can +// iterate downward and build this compaction, assuming all files in that +// interval are not compacting and have a highest sequence number less than +// earliestUnflushedSeqNum: +// +// _______ +// L0.3 |a--d| | g-j| _________ +// L0.2 | | | f--j| | r-t | +// L0.1 | b-d| |e---j| | | +// L0.0 |a--d| | f--j| l--o |p-----x| +// ------ +// Lbase a---------i m---------w +// + +// PickBaseCompaction picks a base compaction based on the above specified +// heuristics, for the specified Lbase files and a minimum depth of overlapping +// files that can be selected for compaction. Returns nil if no compaction is +// possible. +func (s *L0Sublevels) PickBaseCompaction( + minCompactionDepth int, baseFiles LevelSlice, +) (*L0CompactionFiles, error) { + // For LBase compactions, we consider intervals in a greedy manner in the + // following order: + // - Intervals that are unlikely to be blocked due + // to ongoing L0 -> Lbase compactions. These are the ones with + // !isBaseCompacting && !intervalRangeIsBaseCompacting. + // - Intervals that are !isBaseCompacting && intervalRangeIsBaseCompacting. + // + // The ordering heuristic exists just to avoid wasted work. Ideally, + // we would consider all intervals with isBaseCompacting = false and + // construct a compaction for it and compare the constructed compactions + // and pick the best one. If microbenchmarks show that we can afford + // this cost we can eliminate this heuristic. + scoredIntervals := make([]intervalAndScore, 0, len(s.orderedIntervals)) + sublevelCount := len(s.levelFiles) + for i := range s.orderedIntervals { + interval := &s.orderedIntervals[i] + depth := len(interval.files) - interval.compactingFileCount + if interval.isBaseCompacting || minCompactionDepth > depth { + continue + } + if interval.intervalRangeIsBaseCompacting { + scoredIntervals = append(scoredIntervals, intervalAndScore{interval: i, score: depth}) + } else { + // Prioritize this interval by incrementing the score by the number + // of sublevels. + scoredIntervals = append(scoredIntervals, intervalAndScore{interval: i, score: depth + sublevelCount}) + } + } + sort.Sort(intervalSorterByDecreasingScore(scoredIntervals)) + + // Optimization to avoid considering different intervals that + // are likely to choose the same seed file. Again this is just + // to reduce wasted work. + consideredIntervals := newBitSet(len(s.orderedIntervals)) + for _, scoredInterval := range scoredIntervals { + interval := &s.orderedIntervals[scoredInterval.interval] + if consideredIntervals[interval.index] { + continue + } + + // Pick the seed file for the interval as the file + // in the lowest sub-level. + f := interval.files[0] + // Don't bother considering the intervals that are covered by the seed + // file since they are likely nearby. Note that it is possible that + // those intervals have seed files at lower sub-levels so could be + // viable for compaction. + if f == nil { + return nil, errors.New("no seed file found in sublevel intervals") + } + consideredIntervals.markBits(f.minIntervalIndex, f.maxIntervalIndex+1) + if f.IsCompacting() { + if f.IsIntraL0Compacting { + // If we're picking a base compaction and we came across a seed + // file candidate that's being intra-L0 compacted, skip the + // interval instead of erroring out. + continue + } + // We chose a compaction seed file that should not be compacting. + // Usually means the score is not accurately accounting for files + // already compacting, or internal state is inconsistent. + return nil, errors.Errorf("file %s chosen as seed file for compaction should not be compacting", f.FileNum) + } + + c := s.baseCompactionUsingSeed(f, interval.index, minCompactionDepth) + if c != nil { + // Check if the chosen compaction overlaps with any files in Lbase + // that have Compacting = true. If that's the case, this compaction + // cannot be chosen. + baseIter := baseFiles.Iter() + // An interval starting at ImmediateSuccessor(key) can never be the + // first interval of a compaction since no file can start at that + // interval. + m := baseIter.SeekGE(s.cmp, s.orderedIntervals[c.minIntervalIndex].startKey.key) + + var baseCompacting bool + for ; m != nil && !baseCompacting; m = baseIter.Next() { + cmp := s.cmp(m.Smallest.UserKey, s.orderedIntervals[c.maxIntervalIndex+1].startKey.key) + // Compaction is ending at exclusive bound of c.maxIntervalIndex+1 + if cmp > 0 || (cmp == 0 && !s.orderedIntervals[c.maxIntervalIndex+1].startKey.isLargest) { + break + } + baseCompacting = baseCompacting || m.IsCompacting() + } + if baseCompacting { + continue + } + return c, nil + } + } + return nil, nil +} + +// Helper function for building an L0 -> Lbase compaction using a seed interval +// and seed file in that seed interval. +func (s *L0Sublevels) baseCompactionUsingSeed( + f *FileMetadata, intervalIndex int, minCompactionDepth int, +) *L0CompactionFiles { + c := &L0CompactionFiles{ + FilesIncluded: newBitSet(s.levelMetadata.Len()), + seedInterval: intervalIndex, + seedIntervalMinLevel: 0, + minIntervalIndex: f.minIntervalIndex, + maxIntervalIndex: f.maxIntervalIndex, + } + c.addFile(f) + + // The first iteration of this loop builds the compaction at the seed file's + // sublevel. Future iterations expand on this compaction by stacking more + // files from intervalIndex and repeating. This is an optional activity so + // when it fails we can fallback to the last successful candidate. + var lastCandidate *L0CompactionFiles + interval := &s.orderedIntervals[intervalIndex] + + for i := 0; i < len(interval.files); i++ { + f2 := interval.files[i] + sl := f2.SubLevel + c.seedIntervalStackDepthReduction++ + c.seedIntervalMaxLevel = sl + c.addFile(f2) + // The seed file is in the lowest sublevel in the seed interval, but it + // may overlap with other files in even lower sublevels. For correctness + // we need to grow our interval to include those files, and capture all + // files in the next level that fall in this extended interval and so + // on. This can result in a triangular shape like the following where + // again the X axis is the key intervals and the Y axis is oldest to + // youngest. Note that it is not necessary for correctness to fill out + // the shape at the higher sub-levels to make it more rectangular since + // the invariant only requires that younger versions of a key not be + // moved to Lbase while leaving behind older versions. + // - + // --- + // ----- + // It may be better for performance to have a more rectangular shape + // since the files being left behind will overlap with the same Lbase + // key range as that of this compaction. But there is also the danger + // that in trying to construct a more rectangular shape we will be + // forced to pull in a file that is already compacting. We expect + // extendCandidateToRectangle to eventually be called on this compaction + // if it's chosen, at which point we would iterate backward and choose + // those files. This logic is similar to compaction.grow for non-L0 + // compactions. + done := false + for currLevel := sl - 1; currLevel >= 0; currLevel-- { + if !s.extendFiles(currLevel, math.MaxUint64, c) { + // Failed to extend due to ongoing compaction. + done = true + break + } + } + if done { + break + } + // Observed some compactions using > 1GB from L0 in an import + // experiment. Very long running compactions are not great as they + // reduce concurrency while they run, and take a while to produce + // results, though they're sometimes unavoidable. There is a tradeoff + // here in that adding more depth is more efficient in reducing stack + // depth, but long running compactions reduce flexibility in what can + // run concurrently in L0 and even Lbase -> Lbase+1. An increase more + // than 150% in bytes since the last candidate compaction (along with a + // total compaction size in excess of 100mb), or a total compaction size + // beyond a hard limit of 500mb, is criteria for rejecting this + // candidate. This lets us prefer slow growths as we add files, while + // still having a hard limit. Note that if this is the first compaction + // candidate to reach a stack depth reduction of minCompactionDepth or + // higher, this candidate will be chosen regardless. + if lastCandidate == nil { + lastCandidate = &L0CompactionFiles{} + } else if lastCandidate.seedIntervalStackDepthReduction >= minCompactionDepth && + c.fileBytes > 100<<20 && + (float64(c.fileBytes)/float64(lastCandidate.fileBytes) > 1.5 || c.fileBytes > 500<<20) { + break + } + *lastCandidate = *c + } + if lastCandidate != nil && lastCandidate.seedIntervalStackDepthReduction >= minCompactionDepth { + lastCandidate.FilesIncluded.clearAllBits() + for _, f := range lastCandidate.Files { + lastCandidate.FilesIncluded.markBit(f.L0Index) + } + return lastCandidate + } + return nil +} + +// Expands fields in the provided L0CompactionFiles instance (cFiles) to +// include overlapping files in the specified sublevel. Returns true if the +// compaction is possible (i.e. does not conflict with any base/intra-L0 +// compacting files). +func (s *L0Sublevels) extendFiles( + sl int, earliestUnflushedSeqNum uint64, cFiles *L0CompactionFiles, +) bool { + index, _ := slices.BinarySearchFunc(s.levelFiles[sl], cFiles.minIntervalIndex, func(a *FileMetadata, b int) int { + return stdcmp.Compare(a.maxIntervalIndex, b) + }) + for ; index < len(s.levelFiles[sl]); index++ { + f := s.levelFiles[sl][index] + if f.minIntervalIndex > cFiles.maxIntervalIndex { + break + } + if f.IsCompacting() { + return false + } + // Skip over files that are newer than earliestUnflushedSeqNum. This is + // okay because this compaction can just pretend these files are not in + // L0 yet. These files must be in higher sublevels than any overlapping + // files with f.LargestSeqNum < earliestUnflushedSeqNum, and the output + // of the compaction will also go in a lower (older) sublevel than this + // file by definition. + if f.LargestSeqNum >= earliestUnflushedSeqNum { + continue + } + cFiles.addFile(f) + } + return true +} + +// PickIntraL0Compaction picks an intra-L0 compaction for files in this +// sublevel. This method is only called when a base compaction cannot be chosen. +// See comment above [PickBaseCompaction] for heuristics involved in this +// selection. +func (s *L0Sublevels) PickIntraL0Compaction( + earliestUnflushedSeqNum uint64, minCompactionDepth int, +) (*L0CompactionFiles, error) { + scoredIntervals := make([]intervalAndScore, len(s.orderedIntervals)) + for i := range s.orderedIntervals { + interval := &s.orderedIntervals[i] + depth := len(interval.files) - interval.compactingFileCount + if minCompactionDepth > depth { + continue + } + scoredIntervals[i] = intervalAndScore{interval: i, score: depth} + } + sort.Sort(intervalSorterByDecreasingScore(scoredIntervals)) + + // Optimization to avoid considering different intervals that are likely to + // choose the same seed file. Again this is just to reduce wasted work. + consideredIntervals := newBitSet(len(s.orderedIntervals)) + for _, scoredInterval := range scoredIntervals { + interval := &s.orderedIntervals[scoredInterval.interval] + if consideredIntervals[interval.index] { + continue + } + + var f *FileMetadata + // Pick the seed file for the interval as the file in the highest + // sub-level. + stackDepthReduction := scoredInterval.score + for i := len(interval.files) - 1; i >= 0; i-- { + f = interval.files[i] + if f.IsCompacting() { + break + } + consideredIntervals.markBits(f.minIntervalIndex, f.maxIntervalIndex+1) + // Can this be the seed file? Files with newer sequence numbers than + // earliestUnflushedSeqNum cannot be in the compaction. + if f.LargestSeqNum >= earliestUnflushedSeqNum { + stackDepthReduction-- + if stackDepthReduction == 0 { + break + } + } else { + break + } + } + if stackDepthReduction < minCompactionDepth { + // Can't use this interval. + continue + } + + if f == nil { + return nil, errors.New("no seed file found in sublevel intervals") + } + if f.IsCompacting() { + // This file could be in a concurrent intra-L0 or base compaction. + // Try another interval. + continue + } + + // We have a seed file. Build a compaction off of that seed. + c := s.intraL0CompactionUsingSeed( + f, interval.index, earliestUnflushedSeqNum, minCompactionDepth) + if c != nil { + return c, nil + } + } + return nil, nil +} + +func (s *L0Sublevels) intraL0CompactionUsingSeed( + f *FileMetadata, intervalIndex int, earliestUnflushedSeqNum uint64, minCompactionDepth int, +) *L0CompactionFiles { + // We know that all the files that overlap with intervalIndex have + // LargestSeqNum < earliestUnflushedSeqNum, but for other intervals + // we need to exclude files >= earliestUnflushedSeqNum + + c := &L0CompactionFiles{ + FilesIncluded: newBitSet(s.levelMetadata.Len()), + seedInterval: intervalIndex, + seedIntervalMaxLevel: len(s.levelFiles) - 1, + minIntervalIndex: f.minIntervalIndex, + maxIntervalIndex: f.maxIntervalIndex, + isIntraL0: true, + earliestUnflushedSeqNum: earliestUnflushedSeqNum, + } + c.addFile(f) + + var lastCandidate *L0CompactionFiles + interval := &s.orderedIntervals[intervalIndex] + slIndex := len(interval.files) - 1 + for { + if interval.files[slIndex] == f { + break + } + slIndex-- + } + // The first iteration of this loop produces an intra-L0 compaction at the + // seed level. Iterations after that optionally add to the compaction by + // stacking more files from intervalIndex and repeating. This is an optional + // activity so when it fails we can fallback to the last successful + // candidate. The code stops adding when it can't add more, or when + // fileBytes grows too large. + for ; slIndex >= 0; slIndex-- { + f2 := interval.files[slIndex] + sl := f2.SubLevel + if f2.IsCompacting() { + break + } + c.seedIntervalStackDepthReduction++ + c.seedIntervalMinLevel = sl + c.addFile(f2) + // The seed file captures all files in the higher level that fall in the + // range of intervals. That may extend the range of intervals so for + // correctness we need to capture all files in the next higher level + // that fall in this extended interval and so on. This can result in an + // inverted triangular shape like the following where again the X axis + // is the key intervals and the Y axis is oldest to youngest. Note that + // it is not necessary for correctness to fill out the shape at lower + // sub-levels to make it more rectangular since the invariant only + // requires that if we move an older seqnum for key k into a file that + // has a higher seqnum, we also move all younger seqnums for that key k + // into that file. + // ----- + // --- + // - + // It may be better for performance to have a more rectangular shape + // since it will reduce the stack depth for more intervals. But there is + // also the danger that in explicitly trying to construct a more + // rectangular shape we will be forced to pull in a file that is already + // compacting. We assume that the performance concern is not a practical + // issue. + done := false + for currLevel := sl + 1; currLevel < len(s.levelFiles); currLevel++ { + if !s.extendFiles(currLevel, earliestUnflushedSeqNum, c) { + // Failed to extend due to ongoing compaction. + done = true + break + } + } + if done { + break + } + if lastCandidate == nil { + lastCandidate = &L0CompactionFiles{} + } else if lastCandidate.seedIntervalStackDepthReduction >= minCompactionDepth && + c.fileBytes > 100<<20 && + (float64(c.fileBytes)/float64(lastCandidate.fileBytes) > 1.5 || c.fileBytes > 500<<20) { + break + } + *lastCandidate = *c + } + if lastCandidate != nil && lastCandidate.seedIntervalStackDepthReduction >= minCompactionDepth { + lastCandidate.FilesIncluded.clearAllBits() + for _, f := range lastCandidate.Files { + lastCandidate.FilesIncluded.markBit(f.L0Index) + } + s.extendCandidateToRectangle( + lastCandidate.minIntervalIndex, lastCandidate.maxIntervalIndex, lastCandidate, false) + return lastCandidate + } + return nil +} + +// ExtendL0ForBaseCompactionTo extends the specified base compaction candidate +// L0CompactionFiles to optionally cover more files in L0 without "touching" any +// of the passed-in keys (i.e. the smallest/largest bounds are exclusive), as +// including any user keys for those internal keys could require choosing more +// files in LBase which is undesirable. Unbounded start/end keys are indicated +// by passing in the InvalidInternalKey. +func (s *L0Sublevels) ExtendL0ForBaseCompactionTo( + smallest, largest InternalKey, candidate *L0CompactionFiles, +) bool { + firstIntervalIndex := 0 + lastIntervalIndex := len(s.orderedIntervals) - 1 + if smallest.Kind() != base.InternalKeyKindInvalid { + if smallest.Trailer == base.InternalKeyRangeDeleteSentinel { + // Starting at smallest.UserKey == interval.startKey is okay. + firstIntervalIndex = sort.Search(len(s.orderedIntervals), func(i int) bool { + return s.cmp(smallest.UserKey, s.orderedIntervals[i].startKey.key) <= 0 + }) + } else { + firstIntervalIndex = sort.Search(len(s.orderedIntervals), func(i int) bool { + // Need to start at >= smallest since if we widen too much we may miss + // an Lbase file that overlaps with an L0 file that will get picked in + // this widening, which would be bad. This interval will not start with + // an immediate successor key. + return s.cmp(smallest.UserKey, s.orderedIntervals[i].startKey.key) < 0 + }) + } + } + if largest.Kind() != base.InternalKeyKindInvalid { + // First interval that starts at or beyond the largest. This interval will not + // start with an immediate successor key. + lastIntervalIndex = sort.Search(len(s.orderedIntervals), func(i int) bool { + return s.cmp(largest.UserKey, s.orderedIntervals[i].startKey.key) <= 0 + }) + // Right now, lastIntervalIndex has a startKey that extends beyond largest. + // The previous interval, by definition, has an end key higher than largest. + // Iterate back twice to get the last interval that's completely within + // (smallest, largest). Except in the case where we went past the end of the + // list; in that case, the last interval to include is the very last + // interval in the list. + if lastIntervalIndex < len(s.orderedIntervals) { + lastIntervalIndex-- + } + lastIntervalIndex-- + } + if lastIntervalIndex < firstIntervalIndex { + return false + } + return s.extendCandidateToRectangle(firstIntervalIndex, lastIntervalIndex, candidate, true) +} + +// Best-effort attempt to make the compaction include more files in the +// rectangle defined by [minIntervalIndex, maxIntervalIndex] on the X axis and +// bounded on the Y axis by seedIntervalMinLevel and seedIntervalMaxLevel. +// +// This is strictly an optional extension; at any point where we can't feasibly +// add more files, the sublevel iteration can be halted early and candidate will +// still be a correct compaction candidate. +// +// Consider this scenario (original candidate is inside the rectangle), with +// isBase = true and interval bounds a-j (from the union of base file bounds and +// that of compaction candidate): +// +// _______ +// L0.3 a--d | g-j| +// L0.2 | f--j| r-t +// L0.1 b-d |e---j| +// L0.0 a--d | f--j| l--o p-----x +// +// Lbase a--------i m---------w +// +// This method will iterate from the bottom up. At L0.0, it will add a--d since +// it's in the bounds, then add b-d, then a--d, and so on, to produce this: +// +// _____________ +// L0.3 |a--d g-j| +// L0.2 | f--j| r-t +// L0.1 | b-d e---j| +// L0.0 |a--d f--j| l--o p-----x +// +// Lbase a-------i m---------w +// +// Let's assume that, instead of a--d in the top sublevel, we had 3 files, a-b, +// bb-c, and cc-d, of which bb-c is compacting. Let's also add another sublevel +// L0.4 with some files, all of which aren't compacting: +// +// L0.4 a------c ca--d _______ +// L0.3 a-b bb-c cc-d | g-j| +// L0.2 | f--j| r-t +// L0.1 b----------d |e---j| +// L0.0 a------------d | f--j| l--o p-----x +// +// Lbase a------------------i m---------w +// +// This method then needs to choose between the left side of L0.3 bb-c (i.e. +// a-b), or the right side (i.e. cc-d and g-j) for inclusion in this compaction. +// Since the right side has more files as well as one file that has already been +// picked, it gets chosen at that sublevel, resulting in this intermediate +// compaction: +// +// L0.4 a------c ca--d +// ______________ +// L0.3 a-b bb-c| cc-d g-j| +// L0.2 _________| f--j| r-t +// L0.1 | b----------d e---j| +// L0.0 |a------------d f--j| l--o p-----x +// +// Lbase a------------------i m---------w +// +// Since bb-c had to be excluded at L0.3, the interval bounds for L0.4 are +// actually ca-j, since ca is the next interval start key after the end interval +// of bb-c. This would result in only ca-d being chosen at that sublevel, even +// though a--c is also not compacting. This is the final result: +// +// ______________ +// L0.4 a------c|ca--d | +// L0.3 a-b bb-c| cc-d g-j| +// L0.2 _________| f--j| r-t +// L0.1 | b----------d e---j| +// L0.0 |a------------d f--j| l--o p-----x +// +// Lbase a------------------i m---------w +// +// TODO(bilal): Add more targeted tests for this method, through +// ExtendL0ForBaseCompactionTo and intraL0CompactionUsingSeed. +func (s *L0Sublevels) extendCandidateToRectangle( + minIntervalIndex int, maxIntervalIndex int, candidate *L0CompactionFiles, isBase bool, +) bool { + candidate.preExtensionMinInterval = candidate.minIntervalIndex + candidate.preExtensionMaxInterval = candidate.maxIntervalIndex + // Extend {min,max}IntervalIndex to include all of the candidate's current + // bounds. + if minIntervalIndex > candidate.minIntervalIndex { + minIntervalIndex = candidate.minIntervalIndex + } + if maxIntervalIndex < candidate.maxIntervalIndex { + maxIntervalIndex = candidate.maxIntervalIndex + } + var startLevel, increment, endLevel int + if isBase { + startLevel = 0 + increment = +1 + // seedIntervalMaxLevel is inclusive, while endLevel is exclusive. + endLevel = candidate.seedIntervalMaxLevel + 1 + } else { + startLevel = len(s.levelFiles) - 1 + increment = -1 + // seedIntervalMinLevel is inclusive, while endLevel is exclusive. + endLevel = candidate.seedIntervalMinLevel - 1 + } + // Stats for files. + addedCount := 0 + // Iterate from the oldest sub-level for L0 -> Lbase and youngest sub-level + // for intra-L0. The idea here is that anything that can't be included from + // that level constrains what can be included from the next level. This + // change in constraint is directly incorporated into minIntervalIndex, + // maxIntervalIndex. + for sl := startLevel; sl != endLevel; sl += increment { + files := s.levelFiles[sl] + // Find the first file that overlaps with minIntervalIndex. + index := sort.Search(len(files), func(i int) bool { + return minIntervalIndex <= files[i].maxIntervalIndex + }) + // Track the files that are fully within the current constraint of + // [minIntervalIndex, maxIntervalIndex]. + firstIndex := -1 + lastIndex := -1 + for ; index < len(files); index++ { + f := files[index] + if f.minIntervalIndex > maxIntervalIndex { + break + } + include := true + // Extends out on the left so can't be included. This narrows what + // we can included in the next level. + if f.minIntervalIndex < minIntervalIndex { + include = false + minIntervalIndex = f.maxIntervalIndex + 1 + } + // Extends out on the right so can't be included. + if f.maxIntervalIndex > maxIntervalIndex { + include = false + maxIntervalIndex = f.minIntervalIndex - 1 + } + if !include { + continue + } + if firstIndex == -1 { + firstIndex = index + } + lastIndex = index + } + if minIntervalIndex > maxIntervalIndex { + // We excluded files that prevent continuation. + break + } + if firstIndex < 0 { + // No files to add in this sub-level. + continue + } + // We have the files in [firstIndex, lastIndex] as potential for + // inclusion. Some of these may already have been picked. Some of them + // may be already compacting. The latter is tricky since we have to + // decide whether to contract minIntervalIndex or maxIntervalIndex when + // we encounter an already compacting file. We pick the longest sequence + // between firstIndex and lastIndex of non-compacting files -- this is + // represented by [candidateNonCompactingFirst, + // candidateNonCompactingLast]. + nonCompactingFirst := -1 + currentRunHasAlreadyPickedFiles := false + candidateNonCompactingFirst := -1 + candidateNonCompactingLast := -1 + candidateHasAlreadyPickedFiles := false + for index = firstIndex; index <= lastIndex; index++ { + f := files[index] + if f.IsCompacting() { + if nonCompactingFirst != -1 { + last := index - 1 + // Prioritize runs of consecutive non-compacting files that + // have files that have already been picked. That is to say, + // if candidateHasAlreadyPickedFiles == true, we stick with + // it, and if currentRunHasAlreadyPickedfiles == true, we + // pick that run even if it contains fewer files than the + // previous candidate. + if !candidateHasAlreadyPickedFiles && (candidateNonCompactingFirst == -1 || + currentRunHasAlreadyPickedFiles || + (last-nonCompactingFirst) > (candidateNonCompactingLast-candidateNonCompactingFirst)) { + candidateNonCompactingFirst = nonCompactingFirst + candidateNonCompactingLast = last + candidateHasAlreadyPickedFiles = currentRunHasAlreadyPickedFiles + } + } + nonCompactingFirst = -1 + currentRunHasAlreadyPickedFiles = false + continue + } + if nonCompactingFirst == -1 { + nonCompactingFirst = index + } + if candidate.FilesIncluded[f.L0Index] { + currentRunHasAlreadyPickedFiles = true + } + } + // Logic duplicated from inside the for loop above. + if nonCompactingFirst != -1 { + last := index - 1 + if !candidateHasAlreadyPickedFiles && (candidateNonCompactingFirst == -1 || + currentRunHasAlreadyPickedFiles || + (last-nonCompactingFirst) > (candidateNonCompactingLast-candidateNonCompactingFirst)) { + candidateNonCompactingFirst = nonCompactingFirst + candidateNonCompactingLast = last + } + } + if candidateNonCompactingFirst == -1 { + // All files are compacting. There will be gaps that we could + // exploit to continue, but don't bother. + break + } + // May need to shrink [minIntervalIndex, maxIntervalIndex] for the next level. + if candidateNonCompactingFirst > firstIndex { + minIntervalIndex = files[candidateNonCompactingFirst-1].maxIntervalIndex + 1 + } + if candidateNonCompactingLast < lastIndex { + maxIntervalIndex = files[candidateNonCompactingLast+1].minIntervalIndex - 1 + } + for index := candidateNonCompactingFirst; index <= candidateNonCompactingLast; index++ { + f := files[index] + if f.IsCompacting() { + // TODO(bilal): Do a logger.Fatalf instead of a panic, for + // cleaner unwinding and error messages. + panic(fmt.Sprintf("expected %s to not be compacting", f.FileNum)) + } + if candidate.isIntraL0 && f.LargestSeqNum >= candidate.earliestUnflushedSeqNum { + continue + } + if !candidate.FilesIncluded[f.L0Index] { + addedCount++ + candidate.addFile(f) + } + } + } + return addedCount > 0 +} diff --git a/pebble/internal/manifest/l0_sublevels_test.go b/pebble/internal/manifest/l0_sublevels_test.go new file mode 100644 index 0000000..8cedb87 --- /dev/null +++ b/pebble/internal/manifest/l0_sublevels_test.go @@ -0,0 +1,620 @@ +// Copyright 2020 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 manifest + +import ( + "bytes" + "fmt" + "io" + "math" + "os" + "slices" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/record" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func readManifest(filename string) (*Version, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + rr := record.NewReader(f, 0 /* logNum */) + var v *Version + addedByFileNum := make(map[base.FileNum]*FileMetadata) + for { + r, err := rr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + var ve VersionEdit + if err = ve.Decode(r); err != nil { + return nil, err + } + var bve BulkVersionEdit + bve.AddedByFileNum = addedByFileNum + if err := bve.Accumulate(&ve); err != nil { + return nil, err + } + if v, err = bve.Apply(v, base.DefaultComparer.Compare, base.DefaultFormatter, 10<<20, 32000, nil, ProhibitSplitUserKeys); err != nil { + return nil, err + } + } + return v, nil +} + +func visualizeSublevels( + s *L0Sublevels, compactionFiles bitSet, otherLevels [][]*FileMetadata, +) string { + var buf strings.Builder + if compactionFiles == nil { + compactionFiles = newBitSet(s.levelMetadata.Len()) + } + largestChar := byte('a') + printLevel := func(files []*FileMetadata, level string, isL0 bool) { + lastChar := byte('a') + fmt.Fprintf(&buf, "L%s:", level) + for i := 0; i < 5-len(level); i++ { + buf.WriteByte(' ') + } + for j, f := range files { + for lastChar < f.Smallest.UserKey[0] { + buf.WriteString(" ") + lastChar++ + } + buf.WriteByte(f.Smallest.UserKey[0]) + middleChar := byte('-') + if isL0 { + if compactionFiles[f.L0Index] { + middleChar = '+' + } else if f.IsCompacting() { + if f.IsIntraL0Compacting { + middleChar = '^' + } else { + middleChar = 'v' + } + } + } else if f.IsCompacting() { + middleChar = '=' + } + if largestChar < f.Largest.UserKey[0] { + largestChar = f.Largest.UserKey[0] + } + if f.Smallest.UserKey[0] == f.Largest.UserKey[0] { + buf.WriteByte(f.Largest.UserKey[0]) + if compactionFiles[f.L0Index] { + buf.WriteByte('+') + } else if j < len(files)-1 { + buf.WriteByte(' ') + } + lastChar++ + continue + } + buf.WriteByte(middleChar) + buf.WriteByte(middleChar) + lastChar++ + for lastChar < f.Largest.UserKey[0] { + buf.WriteByte(middleChar) + buf.WriteByte(middleChar) + buf.WriteByte(middleChar) + lastChar++ + } + if f.Largest.IsExclusiveSentinel() && + j < len(files)-1 && files[j+1].Smallest.UserKey[0] == f.Largest.UserKey[0] { + // This case happens where two successive files have + // matching end/start user keys but where the left-side file + // has the sentinel key as its end key trailer. In this case + // we print the sstables as: + // + // a------d------g + // + continue + } + buf.WriteByte(middleChar) + buf.WriteByte(f.Largest.UserKey[0]) + if j < len(files)-1 { + buf.WriteByte(' ') + } + lastChar++ + } + fmt.Fprintf(&buf, "\n") + } + for i := len(s.levelFiles) - 1; i >= 0; i-- { + printLevel(s.levelFiles[i], fmt.Sprintf("0.%d", i), true) + } + for i := range otherLevels { + if len(otherLevels[i]) == 0 { + continue + } + printLevel(otherLevels[i], strconv.Itoa(i+1), false) + } + buf.WriteString(" ") + for b := byte('a'); b <= largestChar; b++ { + buf.WriteByte(b) + buf.WriteByte(b) + if b < largestChar { + buf.WriteByte(' ') + } + } + buf.WriteByte('\n') + return buf.String() +} + +func TestL0Sublevels(t *testing.T) { + parseMeta := func(s string) (*FileMetadata, error) { + parts := strings.Split(s, ":") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %s", s) + } + fileNum, err := strconv.Atoi(strings.TrimSpace(parts[0])) + if err != nil { + return nil, err + } + fields := strings.Fields(parts[1]) + keyRange := strings.Split(strings.TrimSpace(fields[0]), "-") + m := (&FileMetadata{}).ExtendPointKeyBounds( + base.DefaultComparer.Compare, + base.ParseInternalKey(strings.TrimSpace(keyRange[0])), + base.ParseInternalKey(strings.TrimSpace(keyRange[1])), + ) + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + if m.Largest.IsExclusiveSentinel() { + m.LargestSeqNum = m.SmallestSeqNum + } + m.FileNum = base.FileNum(fileNum) + m.Size = uint64(256) + m.InitPhysicalBacking() + if len(fields) > 1 { + for _, field := range fields[1:] { + parts := strings.Split(field, "=") + switch parts[0] { + case "base_compacting": + m.IsIntraL0Compacting = false + m.CompactionState = CompactionStateCompacting + case "intra_l0_compacting": + m.IsIntraL0Compacting = true + m.CompactionState = CompactionStateCompacting + case "compacting": + m.CompactionState = CompactionStateCompacting + case "size": + sizeInt, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + m.Size = uint64(sizeInt) + } + } + } + + return m, nil + } + + var err error + var fileMetas [NumLevels][]*FileMetadata + var explicitSublevels [][]*FileMetadata + var activeCompactions []L0Compaction + var sublevels *L0Sublevels + baseLevel := NumLevels - 1 + + datadriven.RunTest(t, "testdata/l0_sublevels", func(t *testing.T, td *datadriven.TestData) string { + pickBaseCompaction := false + level := 0 + addL0FilesOpt := false + switch td.Cmd { + case "add-l0-files": + addL0FilesOpt = true + level = 0 + fallthrough + case "define": + if !addL0FilesOpt { + fileMetas = [NumLevels][]*FileMetadata{} + baseLevel = NumLevels - 1 + activeCompactions = nil + } + explicitSublevels = [][]*FileMetadata{} + sublevel := -1 + addedL0Files := make([]*FileMetadata, 0) + for _, data := range strings.Split(td.Input, "\n") { + data = strings.TrimSpace(data) + switch data[:2] { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err = strconv.Atoi(data[1:2]) + if err != nil { + return err.Error() + } + if level == 0 && len(data) > 3 { + // Sublevel was specified. + sublevel, err = strconv.Atoi(data[3:]) + if err != nil { + return err.Error() + } + } else { + sublevel = -1 + } + default: + meta, err := parseMeta(data) + if err != nil { + return err.Error() + } + if level != 0 && level < baseLevel { + baseLevel = level + } + fileMetas[level] = append(fileMetas[level], meta) + if level == 0 { + addedL0Files = append(addedL0Files, meta) + } + if sublevel != -1 { + for len(explicitSublevels) <= sublevel { + explicitSublevels = append(explicitSublevels, []*FileMetadata{}) + } + explicitSublevels[sublevel] = append(explicitSublevels[sublevel], meta) + } + } + } + + flushSplitMaxBytes := 64 + initialize := true + for _, arg := range td.CmdArgs { + switch arg.Key { + case "flush_split_max_bytes": + flushSplitMaxBytes, err = strconv.Atoi(arg.Vals[0]) + if err != nil { + t.Fatal(err) + } + case "no_initialize": + // This case is for use with explicitly-specified sublevels + // only. + initialize = false + } + } + SortBySeqNum(fileMetas[0]) + for i := 1; i < NumLevels; i++ { + SortBySmallest(fileMetas[i], base.DefaultComparer.Compare) + } + + levelMetadata := makeLevelMetadata(base.DefaultComparer.Compare, 0, fileMetas[0]) + if initialize { + if addL0FilesOpt { + SortBySeqNum(addedL0Files) + sublevels, err = sublevels.AddL0Files(addedL0Files, int64(flushSplitMaxBytes), &levelMetadata) + // Check if the output matches a full initialization. + sublevels2, _ := NewL0Sublevels(&levelMetadata, base.DefaultComparer.Compare, base.DefaultFormatter, int64(flushSplitMaxBytes)) + if sublevels != nil && sublevels2 != nil { + require.Equal(t, sublevels.flushSplitUserKeys, sublevels2.flushSplitUserKeys) + require.Equal(t, sublevels.levelFiles, sublevels2.levelFiles) + } + } else { + sublevels, err = NewL0Sublevels( + &levelMetadata, + base.DefaultComparer.Compare, + base.DefaultFormatter, + int64(flushSplitMaxBytes)) + } + if err != nil { + return err.Error() + } + sublevels.InitCompactingFileInfo(nil) + } else { + // This case is for use with explicitly-specified sublevels + // only. + sublevels = &L0Sublevels{ + levelFiles: explicitSublevels, + cmp: base.DefaultComparer.Compare, + formatKey: base.DefaultFormatter, + levelMetadata: &levelMetadata, + } + for _, files := range explicitSublevels { + sublevels.Levels = append(sublevels.Levels, NewLevelSliceSpecificOrder(files)) + } + } + + if err != nil { + t.Fatal(err) + } + + var builder strings.Builder + builder.WriteString(sublevels.describe(true)) + builder.WriteString(visualizeSublevels(sublevels, nil, fileMetas[1:])) + return builder.String() + case "pick-base-compaction": + pickBaseCompaction = true + fallthrough + case "pick-intra-l0-compaction": + minCompactionDepth := 3 + earliestUnflushedSeqNum := uint64(math.MaxUint64) + for _, arg := range td.CmdArgs { + switch arg.Key { + case "min_depth": + minCompactionDepth, err = strconv.Atoi(arg.Vals[0]) + if err != nil { + t.Fatal(err) + } + case "earliest_unflushed_seqnum": + eusnInt, err := strconv.Atoi(arg.Vals[0]) + if err != nil { + t.Fatal(err) + } + earliestUnflushedSeqNum = uint64(eusnInt) + } + } + + var lcf *L0CompactionFiles + if pickBaseCompaction { + baseFiles := NewLevelSliceKeySorted(base.DefaultComparer.Compare, fileMetas[baseLevel]) + lcf, err = sublevels.PickBaseCompaction(minCompactionDepth, baseFiles) + if err == nil && lcf != nil { + // Try to extend the base compaction into a more rectangular + // shape, using the smallest/largest keys of the files before + // and after overlapping base files. This mimics the logic + // the compactor is expected to implement. + baseFiles := fileMetas[baseLevel] + firstFile := sort.Search(len(baseFiles), func(i int) bool { + return sublevels.cmp(baseFiles[i].Largest.UserKey, sublevels.orderedIntervals[lcf.minIntervalIndex].startKey.key) >= 0 + }) + lastFile := sort.Search(len(baseFiles), func(i int) bool { + return sublevels.cmp(baseFiles[i].Smallest.UserKey, sublevels.orderedIntervals[lcf.maxIntervalIndex+1].startKey.key) >= 0 + }) + startKey := base.InvalidInternalKey + endKey := base.InvalidInternalKey + if firstFile > 0 { + startKey = baseFiles[firstFile-1].Largest + } + if lastFile < len(baseFiles) { + endKey = baseFiles[lastFile].Smallest + } + sublevels.ExtendL0ForBaseCompactionTo( + startKey, + endKey, + lcf) + } + } else { + lcf, err = sublevels.PickIntraL0Compaction(earliestUnflushedSeqNum, minCompactionDepth) + } + if err != nil { + return fmt.Sprintf("error: %s", err.Error()) + } + if lcf == nil { + return "no compaction picked" + } + var builder strings.Builder + builder.WriteString(fmt.Sprintf("compaction picked with stack depth reduction %d\n", lcf.seedIntervalStackDepthReduction)) + for i, file := range lcf.Files { + builder.WriteString(file.FileNum.String()) + if i < len(lcf.Files)-1 { + builder.WriteByte(',') + } + } + startKey := sublevels.orderedIntervals[lcf.seedInterval].startKey + endKey := sublevels.orderedIntervals[lcf.seedInterval+1].startKey + builder.WriteString(fmt.Sprintf("\nseed interval: %s-%s\n", startKey.key, endKey.key)) + builder.WriteString(visualizeSublevels(sublevels, lcf.FilesIncluded, fileMetas[1:])) + + return builder.String() + case "read-amp": + return strconv.Itoa(sublevels.ReadAmplification()) + case "in-use-key-ranges": + var buf bytes.Buffer + for _, data := range strings.Split(strings.TrimSpace(td.Input), "\n") { + keyRange := strings.Split(strings.TrimSpace(data), "-") + smallest := []byte(strings.TrimSpace(keyRange[0])) + largest := []byte(strings.TrimSpace(keyRange[1])) + + keyRanges := sublevels.InUseKeyRanges(smallest, largest) + for i, r := range keyRanges { + fmt.Fprintf(&buf, "%s-%s", sublevels.formatKey(r.Start), sublevels.formatKey(r.End)) + if i < len(keyRanges)-1 { + fmt.Fprint(&buf, ", ") + } + } + if len(keyRanges) == 0 { + fmt.Fprint(&buf, ".") + } + fmt.Fprintln(&buf) + } + return buf.String() + case "flush-split-keys": + var builder strings.Builder + builder.WriteString("flush user split keys: ") + flushSplitKeys := sublevels.FlushSplitKeys() + for i, key := range flushSplitKeys { + builder.Write(key) + if i < len(flushSplitKeys)-1 { + builder.WriteString(", ") + } + } + if len(flushSplitKeys) == 0 { + builder.WriteString("none") + } + return builder.String() + case "max-depth-after-ongoing-compactions": + return strconv.Itoa(sublevels.MaxDepthAfterOngoingCompactions()) + case "l0-check-ordering": + for sublevel, files := range sublevels.levelFiles { + slice := NewLevelSliceSpecificOrder(files) + err := CheckOrdering(base.DefaultComparer.Compare, base.DefaultFormatter, + L0Sublevel(sublevel), slice.Iter(), ProhibitSplitUserKeys) + if err != nil { + return err.Error() + } + } + return "OK" + case "update-state-for-compaction": + var fileNums []base.FileNum + for _, arg := range td.CmdArgs { + switch arg.Key { + case "files": + for _, val := range arg.Vals { + fileNum, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return err.Error() + } + fileNums = append(fileNums, base.FileNum(fileNum)) + } + } + } + files := make([]*FileMetadata, 0, len(fileNums)) + for _, num := range fileNums { + for _, f := range fileMetas[0] { + if f.FileNum == num { + f.CompactionState = CompactionStateCompacting + files = append(files, f) + break + } + } + } + slice := NewLevelSliceSeqSorted(files) + sm, la := KeyRange(base.DefaultComparer.Compare, slice.Iter()) + activeCompactions = append(activeCompactions, L0Compaction{Smallest: sm, Largest: la}) + if err := sublevels.UpdateStateForStartedCompaction([]LevelSlice{slice}, true); err != nil { + return err.Error() + } + return "OK" + case "describe": + var builder strings.Builder + builder.WriteString(sublevels.describe(true)) + builder.WriteString(visualizeSublevels(sublevels, nil, fileMetas[1:])) + return builder.String() + } + return fmt.Sprintf("unrecognized command: %s", td.Cmd) + }) +} + +func TestAddL0FilesEquivalence(t *testing.T) { + seed := uint64(time.Now().UnixNano()) + rng := rand.New(rand.NewSource(seed)) + t.Logf("seed: %d", seed) + + var inUseKeys [][]byte + const keyReusePct = 0.15 + var fileMetas []*FileMetadata + var s, s2 *L0Sublevels + keySpace := testkeys.Alpha(8) + + flushSplitMaxBytes := rng.Int63n(1 << 20) + + // The outer loop runs once for each version edit. The inner loop(s) run + // once for each file, or each file bound. + for i := 0; i < 100; i++ { + var filesToAdd []*FileMetadata + numFiles := 1 + rng.Intn(9) + keys := make([][]byte, 0, 2*numFiles) + for j := 0; j < 2*numFiles; j++ { + if rng.Float64() <= keyReusePct && len(inUseKeys) > 0 { + keys = append(keys, inUseKeys[rng.Intn(len(inUseKeys))]) + } else { + newKey := testkeys.Key(keySpace, rng.Int63n(keySpace.Count())) + inUseKeys = append(inUseKeys, newKey) + keys = append(keys, newKey) + } + } + slices.SortFunc(keys, bytes.Compare) + for j := 0; j < numFiles; j++ { + startKey := keys[j*2] + endKey := keys[j*2+1] + if bytes.Equal(startKey, endKey) { + continue + } + meta := (&FileMetadata{ + FileNum: base.FileNum(i*10 + j + 1), + Size: rng.Uint64n(1 << 20), + SmallestSeqNum: uint64(2*i + 1), + LargestSeqNum: uint64(2*i + 2), + }).ExtendPointKeyBounds( + base.DefaultComparer.Compare, + base.MakeInternalKey(startKey, uint64(2*i+1), base.InternalKeyKindSet), + base.MakeRangeDeleteSentinelKey(endKey), + ) + meta.InitPhysicalBacking() + fileMetas = append(fileMetas, meta) + filesToAdd = append(filesToAdd, meta) + } + if len(filesToAdd) == 0 { + continue + } + + levelMetadata := makeLevelMetadata(testkeys.Comparer.Compare, 0, fileMetas) + var err error + + if s2 == nil { + s2, err = NewL0Sublevels(&levelMetadata, testkeys.Comparer.Compare, testkeys.Comparer.FormatKey, flushSplitMaxBytes) + require.NoError(t, err) + } else { + // AddL0Files relies on the indices in FileMetadatas pointing to that of + // the previous L0Sublevels. So it must be called before NewL0Sublevels; + // calling it the other way around results in out-of-bounds panics. + SortBySeqNum(filesToAdd) + s2, err = s2.AddL0Files(filesToAdd, flushSplitMaxBytes, &levelMetadata) + require.NoError(t, err) + } + + s, err = NewL0Sublevels(&levelMetadata, testkeys.Comparer.Compare, testkeys.Comparer.FormatKey, flushSplitMaxBytes) + require.NoError(t, err) + + // Check for equivalence. + require.Equal(t, s.flushSplitUserKeys, s2.flushSplitUserKeys) + require.Equal(t, s.orderedIntervals, s2.orderedIntervals) + require.Equal(t, s.levelFiles, s2.levelFiles) + } +} + +func BenchmarkManifestApplyWithL0Sublevels(b *testing.B) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + v, err := readManifest("testdata/MANIFEST_import") + require.NotNil(b, v) + require.NoError(b, err) + } +} + +func BenchmarkL0SublevelsInit(b *testing.B) { + v, err := readManifest("testdata/MANIFEST_import") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for n := 0; n < b.N; n++ { + sl, err := NewL0Sublevels(&v.Levels[0], + base.DefaultComparer.Compare, base.DefaultFormatter, 5<<20) + require.NoError(b, err) + if sl == nil { + b.Fatal("expected non-nil L0Sublevels to be generated") + } + } +} + +func BenchmarkL0SublevelsInitAndPick(b *testing.B) { + v, err := readManifest("testdata/MANIFEST_import") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for n := 0; n < b.N; n++ { + sl, err := NewL0Sublevels(&v.Levels[0], + base.DefaultComparer.Compare, base.DefaultFormatter, 5<<20) + require.NoError(b, err) + if sl == nil { + b.Fatal("expected non-nil L0Sublevels to be generated") + } + c, err := sl.PickBaseCompaction(2, LevelSlice{}) + require.NoError(b, err) + if c == nil { + b.Fatal("expected non-nil compaction to be generated") + } + } +} diff --git a/pebble/internal/manifest/level.go b/pebble/internal/manifest/level.go new file mode 100644 index 0000000..1a971f6 --- /dev/null +++ b/pebble/internal/manifest/level.go @@ -0,0 +1,46 @@ +// Copyright 2020 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 manifest + +import "fmt" + +const ( + // 3 bits are necessary to represent level values from 0-6. + levelBits = 3 + levelMask = (1 << levelBits) - 1 + // invalidSublevel denotes an invalid or non-applicable sublevel. + invalidSublevel = -1 +) + +// Level encodes a level and optional sublevel for use in log and error +// messages. The encoding has the property that Level(0) == +// L0Sublevel(invalidSublevel). +type Level uint32 + +func makeLevel(level, sublevel int) Level { + return Level(((sublevel + 1) << levelBits) | level) +} + +// LevelToInt returns the int representation of a Level +func LevelToInt(l Level) int { + return int(l) & levelMask +} + +// L0Sublevel returns a Level representing the specified L0 sublevel. +func L0Sublevel(sublevel int) Level { + if sublevel < 0 { + panic(fmt.Sprintf("invalid L0 sublevel: %d", sublevel)) + } + return makeLevel(0, sublevel) +} + +func (l Level) String() string { + level := int(l) & levelMask + sublevel := (int(l) >> levelBits) - 1 + if sublevel != invalidSublevel { + return fmt.Sprintf("L%d.%d", level, sublevel) + } + return fmt.Sprintf("L%d", level) +} diff --git a/pebble/internal/manifest/level_metadata.go b/pebble/internal/manifest/level_metadata.go new file mode 100644 index 0000000..d48e277 --- /dev/null +++ b/pebble/internal/manifest/level_metadata.go @@ -0,0 +1,748 @@ +// Copyright 2020 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 manifest + +import ( + "bytes" + "fmt" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" +) + +// LevelMetadata contains metadata for all of the files within +// a level of the LSM. +type LevelMetadata struct { + 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, + totalSize: lm.totalSize, + NumVirtual: lm.NumVirtual, + VirtualSize: lm.VirtualSize, + tree: lm.tree.Clone(), + } +} + +func (lm *LevelMetadata) release() (obsolete []*FileBacking) { + return lm.tree.Release() +} + +func makeLevelMetadata(cmp Compare, level int, files []*FileMetadata) LevelMetadata { + bcmp := btreeCmpSeqNum + if level > 0 { + bcmp = btreeCmpSmallestKey(cmp) + } + 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 +} + +func makeBTree(cmp btreeCmp, files []*FileMetadata) (btree, LevelSlice) { + var t btree + t.cmp = cmp + for _, f := range files { + t.Insert(f) + } + 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 +} + +// Len returns the number of files within the level. +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()} +} + +// Slice constructs a slice containing the entire level. +func (lm *LevelMetadata) Slice() LevelSlice { + return newLevelSlice(lm.tree.Iter()) +} + +// Find finds the provided file in the level if it exists. +func (lm *LevelMetadata) Find(cmp base.Compare, m *FileMetadata) *LevelFile { + iter := lm.Iter() + if lm.level != 0 { + // If lm holds files for levels >0, we can narrow our search by binary + // searching by bounds. + o := overlaps(iter, cmp, m.Smallest.UserKey, + m.Largest.UserKey, m.Largest.IsExclusiveSentinel()) + iter = o.Iter() + } + for f := iter.First(); f != nil; f = iter.Next() { + if f == m { + lf := iter.Take() + return &lf + } + } + return nil +} + +// Annotation lazily calculates and returns the annotation defined by +// Annotator. The Annotator is used as the key for pre-calculated +// values, so equal Annotators must be used to avoid duplicate computations +// and cached annotations. Annotation must not be called concurrently, and in +// practice this is achieved by requiring callers to hold DB.mu. +func (lm *LevelMetadata) Annotation(annotator Annotator) interface{} { + if lm.Empty() { + return annotator.Zero(nil) + } + v, _ := lm.tree.root.Annotation(annotator) + return v +} + +// InvalidateAnnotation clears any cached annotations defined by Annotator. The +// Annotator is used as the key for pre-calculated values, so equal Annotators +// must be used to clear the appropriate cached annotation. InvalidateAnnotation +// must not be called concurrently, and in practice this is achieved by +// requiring callers to hold DB.mu. +func (lm *LevelMetadata) InvalidateAnnotation(annotator Annotator) { + if lm.Empty() { + return + } + lm.tree.root.InvalidateAnnotation(annotator) +} + +// LevelFile holds a file's metadata along with its position +// within a level of the LSM. +type LevelFile struct { + *FileMetadata + slice LevelSlice +} + +// Slice constructs a LevelSlice containing only this file. +func (lf LevelFile) Slice() LevelSlice { + return lf.slice +} + +// NewLevelSliceSeqSorted constructs a LevelSlice over the provided files, +// sorted by the L0 sequence number sort order. +// TODO(jackson): Can we improve this interface or avoid needing to export +// a slice constructor like this? +func NewLevelSliceSeqSorted(files []*FileMetadata) LevelSlice { + tr, slice := makeBTree(btreeCmpSeqNum, files) + tr.Release() + slice.verifyInvariants() + return slice +} + +// NewLevelSliceKeySorted constructs a LevelSlice over the provided files, +// sorted by the files smallest keys. +// TODO(jackson): Can we improve this interface or avoid needing to export +// a slice constructor like this? +func NewLevelSliceKeySorted(cmp base.Compare, files []*FileMetadata) LevelSlice { + tr, slice := makeBTree(btreeCmpSmallestKey(cmp), files) + tr.Release() + slice.verifyInvariants() + return slice +} + +// NewLevelSliceSpecificOrder constructs a LevelSlice over the provided files, +// ordering the files by their order in the provided slice. It's used in +// tests. +// TODO(jackson): Update tests to avoid requiring this and remove it. +func NewLevelSliceSpecificOrder(files []*FileMetadata) LevelSlice { + tr, slice := makeBTree(btreeCmpSpecificOrder(files), files) + tr.Release() + slice.verifyInvariants() + return slice +} + +// newLevelSlice constructs a new LevelSlice backed by iter. +func newLevelSlice(iter iterator) LevelSlice { + s := LevelSlice{iter: iter} + if iter.r != nil { + s.length = iter.r.subtreeCount + } + s.verifyInvariants() + return s +} + +// newBoundedLevelSlice constructs a new LevelSlice backed by iter and bounded +// by the provided start and end bounds. The provided startBound and endBound +// iterators must be iterators over the same B-Tree. Both start and end bounds +// are inclusive. +func newBoundedLevelSlice(iter iterator, startBound, endBound *iterator) LevelSlice { + s := LevelSlice{ + iter: iter, + start: startBound, + end: endBound, + } + if iter.valid() { + s.length = endBound.countLeft() - startBound.countLeft() + // NB: The +1 is a consequence of the end bound being inclusive. + if endBound.valid() { + s.length++ + } + // NB: A slice that's empty due to its bounds may have an endBound + // positioned before the startBound due to the inclusive bounds. + // TODO(jackson): Consider refactoring the end boundary to be exclusive; + // it would simplify some areas (eg, here) and complicate others (eg, + // Reslice-ing to grow compactions). + if s.length < 0 { + s.length = 0 + } + } + s.verifyInvariants() + return s +} + +// LevelSlice contains a slice of the files within a level of the LSM. +// A LevelSlice is immutable once created, but may be used to construct a +// mutable LevelIterator over the slice's files. +// +// LevelSlices should be constructed through one of the existing constructors, +// not manually initialized. +type LevelSlice struct { + iter iterator + length int + // start and end form the inclusive bounds of a slice of files within a + // level of the LSM. They may be nil if the entire B-Tree backing iter is + // accessible. + start *iterator + end *iterator +} + +func (ls LevelSlice) verifyInvariants() { + if invariants.Enabled { + i := ls.Iter() + var length int + for f := i.First(); f != nil; f = i.Next() { + length++ + } + if ls.length != length { + panic(fmt.Sprintf("LevelSlice %s has length %d value; actual length is %d", ls, ls.length, length)) + } + } +} + +// Each invokes fn for each element in the slice. +func (ls LevelSlice) Each(fn func(*FileMetadata)) { + iter := ls.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + fn(f) + } +} + +// String implements fmt.Stringer. +func (ls LevelSlice) String() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "%d files: ", ls.length) + ls.Each(func(f *FileMetadata) { + if buf.Len() > 0 { + fmt.Fprintf(&buf, " ") + } + fmt.Fprint(&buf, f) + }) + return buf.String() +} + +// Empty indicates whether the slice contains any files. +func (ls *LevelSlice) Empty() bool { + return emptyWithBounds(ls.iter, ls.start, ls.end) +} + +// Iter constructs a LevelIterator that iterates over the slice. +func (ls *LevelSlice) Iter() LevelIterator { + return LevelIterator{ + start: ls.start, + end: ls.end, + iter: ls.iter.clone(), + } +} + +// Len returns the number of files in the slice. Its runtime is constant. +func (ls *LevelSlice) Len() int { + return ls.length +} + +// SizeSum sums the size of all files in the slice. Its runtime is linear in +// the length of the slice. +func (ls *LevelSlice) SizeSum() uint64 { + var sum uint64 + iter := ls.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + sum += f.Size + } + 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 +// positioned to i's inclusive end. The resliceFunc may move either iterator +// forward or backwards, including beyond the callee's original bounds to +// capture additional files from the underlying level. Reslice constructs and +// returns a new LevelSlice with the final bounds of the iterators after +// calling resliceFunc. +func (ls LevelSlice) Reslice(resliceFunc func(start, end *LevelIterator)) LevelSlice { + if ls.iter.r == nil { + return ls + } + var start, end LevelIterator + if ls.start == nil { + start.iter = ls.iter.clone() + start.iter.first() + } else { + start.iter = ls.start.clone() + } + if ls.end == nil { + end.iter = ls.iter.clone() + end.iter.last() + } else { + end.iter = ls.end.clone() + } + resliceFunc(&start, &end) + return newBoundedLevelSlice(start.iter.clone(), &start.iter, &end.iter) +} + +// KeyType is used to specify the type of keys we're looking for in +// LevelIterator positioning operations. Files not containing any keys of the +// desired type are skipped. +type KeyType int8 + +const ( + // KeyTypePointAndRange denotes a search among the entire keyspace, including + // both point keys and range keys. No sstables are skipped. + KeyTypePointAndRange KeyType = iota + // KeyTypePoint denotes a search among the point keyspace. SSTables with no + // point keys will be skipped. Note that the point keyspace includes rangedels. + KeyTypePoint + // KeyTypeRange denotes a search among the range keyspace. SSTables with no + // range keys will be skipped. + KeyTypeRange +) + +type keyTypeAnnotator struct{} + +var _ Annotator = keyTypeAnnotator{} + +func (k keyTypeAnnotator) Zero(dst interface{}) interface{} { + var val *KeyType + if dst != nil { + val = dst.(*KeyType) + } else { + val = new(KeyType) + } + *val = KeyTypePoint + return val +} + +func (k keyTypeAnnotator) Accumulate(m *FileMetadata, dst interface{}) (interface{}, bool) { + v := dst.(*KeyType) + switch *v { + case KeyTypePoint: + if m.HasRangeKeys { + *v = KeyTypePointAndRange + } + case KeyTypePointAndRange: + // Do nothing. + default: + panic("unexpected key type") + } + return v, true +} + +func (k keyTypeAnnotator) Merge(src interface{}, dst interface{}) interface{} { + v := dst.(*KeyType) + srcVal := src.(*KeyType) + switch *v { + case KeyTypePoint: + if *srcVal == KeyTypePointAndRange { + *v = KeyTypePointAndRange + } + case KeyTypePointAndRange: + // Do nothing. + default: + panic("unexpected key type") + } + return v +} + +// LevelIterator iterates over a set of files' metadata. Its zero value is an +// empty iterator. +type LevelIterator struct { + iter iterator + start *iterator + end *iterator + filter KeyType +} + +func (i LevelIterator) String() string { + var buf bytes.Buffer + iter := i.iter.clone() + iter.first() + iter.prev() + if i.iter.pos == -1 { + fmt.Fprint(&buf, "()*") + } + iter.next() + for ; iter.valid(); iter.next() { + if buf.Len() > 0 { + fmt.Fprint(&buf, " ") + } + + if i.start != nil && cmpIter(iter, *i.start) == 0 { + fmt.Fprintf(&buf, " [ ") + } + isCurrentPos := cmpIter(iter, i.iter) == 0 + if isCurrentPos { + fmt.Fprint(&buf, " ( ") + } + fmt.Fprint(&buf, iter.cur().String()) + if isCurrentPos { + fmt.Fprint(&buf, " )*") + } + if i.end != nil && cmpIter(iter, *i.end) == 0 { + fmt.Fprintf(&buf, " ]") + } + } + if i.iter.n != nil && i.iter.pos >= i.iter.n.count { + if buf.Len() > 0 { + fmt.Fprint(&buf, " ") + } + fmt.Fprint(&buf, "()*") + } + return buf.String() +} + +// Clone copies the iterator, returning an independent iterator at the same +// position. +func (i *LevelIterator) Clone() LevelIterator { + if i.iter.r == nil { + return *i + } + // The start and end iterators are not cloned and are treated as + // immutable. + return LevelIterator{ + iter: i.iter.clone(), + start: i.start, + end: i.end, + filter: i.filter, + } +} + +// Current returns the item at the current iterator position. +// +// Current is deprecated. Callers should instead use the return value of a +// positioning operation. +func (i *LevelIterator) Current() *FileMetadata { + if !i.iter.valid() || + (i.end != nil && cmpIter(i.iter, *i.end) > 0) || + (i.start != nil && cmpIter(i.iter, *i.start) < 0) { + return nil + } + return i.iter.cur() +} + +func (i *LevelIterator) empty() bool { + return emptyWithBounds(i.iter, i.start, i.end) +} + +// Filter clones the iterator and sets the desired KeyType as the key to filter +// files on. +func (i *LevelIterator) Filter(keyType KeyType) LevelIterator { + l := i.Clone() + l.filter = keyType + return l +} + +func emptyWithBounds(i iterator, start, end *iterator) bool { + // If i.r is nil, the iterator was constructed from an empty btree. + // If the end bound is before the start bound, the bounds represent an + // empty slice of the B-Tree. + return i.r == nil || (start != nil && end != nil && cmpIter(*end, *start) < 0) +} + +// First seeks to the first file in the iterator and returns it. +func (i *LevelIterator) First() *FileMetadata { + if i.empty() { + return nil + } + if i.start != nil { + i.iter = i.start.clone() + } else { + i.iter.first() + } + if !i.iter.valid() { + return nil + } + return i.skipFilteredForward(i.iter.cur()) +} + +// Last seeks to the last file in the iterator and returns it. +func (i *LevelIterator) Last() *FileMetadata { + if i.empty() { + return nil + } + if i.end != nil { + i.iter = i.end.clone() + } else { + i.iter.last() + } + if !i.iter.valid() { + return nil + } + return i.skipFilteredBackward(i.iter.cur()) +} + +// Next advances the iterator to the next file and returns it. +func (i *LevelIterator) Next() *FileMetadata { + if i.iter.r == nil { + return nil + } + if invariants.Enabled && (i.iter.pos >= i.iter.n.count || (i.end != nil && cmpIter(i.iter, *i.end) > 0)) { + panic("pebble: cannot next forward-exhausted iterator") + } + i.iter.next() + if !i.iter.valid() { + return nil + } + return i.skipFilteredForward(i.iter.cur()) +} + +// Prev moves the iterator the previous file and returns it. +func (i *LevelIterator) Prev() *FileMetadata { + if i.iter.r == nil { + return nil + } + if invariants.Enabled && (i.iter.pos < 0 || (i.start != nil && cmpIter(i.iter, *i.start) < 0)) { + panic("pebble: cannot prev backward-exhausted iterator") + } + i.iter.prev() + if !i.iter.valid() { + return nil + } + return i.skipFilteredBackward(i.iter.cur()) +} + +// SeekGE seeks to the first file in the iterator's file set with a largest +// user key greater than or equal to the provided user key. The iterator must +// have been constructed from L1+, because it requires the underlying files to +// be sorted by user keys and non-overlapping. +func (i *LevelIterator) SeekGE(cmp Compare, userKey []byte) *FileMetadata { + // TODO(jackson): Assert that i.iter.cmp == btreeCmpSmallestKey. + if i.iter.r == nil { + return nil + } + m := i.seek(func(m *FileMetadata) bool { + return cmp(m.Largest.UserKey, userKey) >= 0 + }) + if i.filter != KeyTypePointAndRange && m != nil { + b, ok := m.LargestBound(i.filter) + if !ok { + m = i.Next() + } else if c := cmp(b.UserKey, userKey); c < 0 || c == 0 && b.IsExclusiveSentinel() { + // This file does not contain any keys of the type ≥ lower. It + // should be filtered, even though it does contain point keys. + m = i.Next() + } + } + return i.skipFilteredForward(m) +} + +// SeekLT seeks to the last file in the iterator's file set with a smallest +// user key less than the provided user key. The iterator must have been +// constructed from L1+, because it requires the underlying files to be sorted +// by user keys and non-overlapping. +func (i *LevelIterator) SeekLT(cmp Compare, userKey []byte) *FileMetadata { + // TODO(jackson): Assert that i.iter.cmp == btreeCmpSmallestKey. + if i.iter.r == nil { + return nil + } + i.seek(func(m *FileMetadata) bool { + return cmp(m.Smallest.UserKey, userKey) >= 0 + }) + m := i.Prev() + // Although i.Prev() guarantees that the current file contains keys of the + // relevant type, it doesn't guarantee that the keys of the relevant type + // are < userKey. + if i.filter != KeyTypePointAndRange && m != nil { + b, ok := m.SmallestBound(i.filter) + if !ok { + panic("unreachable") + } + if c := cmp(b.UserKey, userKey); c >= 0 { + // This file does not contain any keys of the type ≥ lower. It + // should be filtered, even though it does contain point keys. + m = i.Prev() + } + } + return i.skipFilteredBackward(m) +} + +// skipFilteredForward takes the file metadata at the iterator's current +// position, and skips forward if the current key-type filter (i.filter) +// excludes the file. It skips until it finds an unfiltered file or exhausts the +// level. If lower is != nil, skipFilteredForward skips any files that do not +// contain keys with the provided key-type ≥ lower. +// +// skipFilteredForward also enforces the upper bound, returning nil if at any +// point the upper bound is exceeded. +func (i *LevelIterator) skipFilteredForward(meta *FileMetadata) *FileMetadata { + for meta != nil && !meta.ContainsKeyType(i.filter) { + i.iter.next() + if !i.iter.valid() { + meta = nil + } else { + meta = i.iter.cur() + } + } + if meta != nil && i.end != nil && cmpIter(i.iter, *i.end) > 0 { + // Exceeded upper bound. + meta = nil + } + return meta +} + +// skipFilteredBackward takes the file metadata at the iterator's current +// position, and skips backward if the current key-type filter (i.filter) +// excludes the file. It skips until it finds an unfiltered file or exhausts the +// level. If upper is != nil, skipFilteredBackward skips any files that do not +// contain keys with the provided key-type < upper. +// +// skipFilteredBackward also enforces the lower bound, returning nil if at any +// point the lower bound is exceeded. +func (i *LevelIterator) skipFilteredBackward(meta *FileMetadata) *FileMetadata { + for meta != nil && !meta.ContainsKeyType(i.filter) { + i.iter.prev() + if !i.iter.valid() { + meta = nil + } else { + meta = i.iter.cur() + } + } + if meta != nil && i.start != nil && cmpIter(i.iter, *i.start) < 0 { + // Exceeded lower bound. + meta = nil + } + return meta +} + +func (i *LevelIterator) seek(fn func(*FileMetadata) bool) *FileMetadata { + i.iter.seek(fn) + + // i.iter.seek seeked in the unbounded underlying B-Tree. If the iterator + // has start or end bounds, we may have exceeded them. Reset to the bounds + // if necessary. + // + // NB: The LevelIterator and LevelSlice semantics require that a bounded + // LevelIterator/LevelSlice containing files x0, x1, ..., xn behave + // identically to an unbounded LevelIterator/LevelSlice of a B-Tree + // containing x0, x1, ..., xn. In other words, any files outside the + // LevelIterator's bounds should not influence the iterator's behavior. + // When seeking, this means a SeekGE that seeks beyond the end bound, + // followed by a Prev should return the last element within bounds. + if i.end != nil && cmpIter(i.iter, *i.end) > 0 { + i.iter = i.end.clone() + // Since seek(fn) positioned beyond i.end, we know there is nothing to + // return within bounds. + i.iter.next() + return nil + } else if i.start != nil && cmpIter(i.iter, *i.start) < 0 { + i.iter = i.start.clone() + } + if !i.iter.valid() { + return nil + } + return i.iter.cur() +} + +// Take constructs a LevelFile containing the file at the iterator's current +// position. Take panics if the iterator is not currently positioned over a +// file. +func (i *LevelIterator) Take() LevelFile { + m := i.Current() + if m == nil { + panic("Take called on invalid LevelIterator") + } + // LevelSlice's start and end fields are immutable and are positioned to + // the same position for a LevelFile because they're inclusive, so we can + // share one iterator stack between the two bounds. + boundsIter := i.iter.clone() + s := newBoundedLevelSlice(i.iter.clone(), &boundsIter, &boundsIter) + return LevelFile{ + FileMetadata: m, + slice: s, + } +} diff --git a/pebble/internal/manifest/level_metadata_test.go b/pebble/internal/manifest/level_metadata_test.go new file mode 100644 index 0000000..95ef91a --- /dev/null +++ b/pebble/internal/manifest/level_metadata_test.go @@ -0,0 +1,144 @@ +// Copyright 2020 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 manifest + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" +) + +func TestLevelIterator(t *testing.T) { + var level LevelSlice + datadriven.RunTest(t, "testdata/level_iterator", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + var files []*FileMetadata + var startReslice int + var endReslice int + for _, metaStr := range strings.Split(d.Input, " ") { + switch metaStr { + case "[": + startReslice = len(files) + continue + case "]": + endReslice = len(files) + continue + case " ", "": + continue + default: + parts := strings.Split(metaStr, "-") + if len(parts) != 2 { + t.Fatalf("malformed table spec: %q", metaStr) + } + m := &FileMetadata{FileNum: base.FileNum(len(files) + 1)} + m.ExtendPointKeyBounds( + base.DefaultComparer.Compare, + base.ParseInternalKey(strings.TrimSpace(parts[0])), + base.ParseInternalKey(strings.TrimSpace(parts[1])), + ) + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + m.InitPhysicalBacking() + files = append(files, m) + } + } + level = NewLevelSliceKeySorted(base.DefaultComparer.Compare, files) + level = level.Reslice(func(start, end *LevelIterator) { + for i := 0; i < startReslice; i++ { + start.Next() + } + for i := len(files); i > endReslice; i-- { + end.Prev() + } + }) + return "" + + case "iter": + return runIterCmd(t, d, level.Iter(), false /* verbose */) + + default: + return fmt.Sprintf("unknown command %q", d.Cmd) + } + }) +} + +func TestLevelIteratorFiltered(t *testing.T) { + var level LevelSlice + datadriven.RunTest(t, "testdata/level_iterator_filtered", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + var files []*FileMetadata + for _, metaStr := range strings.Split(d.Input, "\n") { + m, err := ParseFileMetadataDebug(metaStr) + require.NoError(t, err) + files = append(files, m) + } + level = NewLevelSliceKeySorted(base.DefaultComparer.Compare, files) + return "" + + case "iter": + var keyType string + d.ScanArgs(t, "key-type", &keyType) + iter := level.Iter() + switch keyType { + case "both": + // noop + case "points": + iter = iter.Filter(KeyTypePoint) + case "ranges": + iter = iter.Filter(KeyTypeRange) + } + return runIterCmd(t, d, iter, true /* verbose */) + + default: + return fmt.Sprintf("unknown command %q", d.Cmd) + } + }) +} + +func runIterCmd(t *testing.T, d *datadriven.TestData, iter LevelIterator, verbose bool) string { + var buf bytes.Buffer + for _, line := range strings.Split(d.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + var m *FileMetadata + switch parts[0] { + case "first": + m = iter.First() + case "last": + m = iter.Last() + case "next": + m = iter.Next() + case "prev": + m = iter.Prev() + case "seek-ge": + m = iter.SeekGE(base.DefaultComparer.Compare, []byte(parts[1])) + case "seek-lt": + m = iter.SeekLT(base.DefaultComparer.Compare, []byte(parts[1])) + default: + return fmt.Sprintf("unknown command %q", parts[0]) + } + if m == nil { + fmt.Fprintln(&buf, ".") + } else { + if verbose { + fmt.Fprintln(&buf, m.DebugString(base.DefaultComparer.FormatKey, verbose)) + } else { + fmt.Fprintln(&buf, m) + } + } + } + return buf.String() +} diff --git a/pebble/internal/manifest/level_test.go b/pebble/internal/manifest/level_test.go new file mode 100644 index 0000000..0b9aa7f --- /dev/null +++ b/pebble/internal/manifest/level_test.go @@ -0,0 +1,64 @@ +// Copyright 2020 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 manifest + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLevel(t *testing.T) { + testCases := []struct { + level int + expected string + }{ + {0, "L0"}, + {1, "L1"}, + {2, "L2"}, + {3, "L3"}, + {4, "L4"}, + {5, "L5"}, + {6, "L6"}, + {7, "L7"}, + } + + for _, c := range testCases { + t.Run("", func(t *testing.T) { + s := Level(c.level).String() + require.EqualValues(t, c.expected, s) + }) + } +} + +func TestL0Sublevel(t *testing.T) { + testCases := []struct { + level int + sublevel int + expected string + }{ + {0, 0, "L0.0"}, + {0, 1, "L0.1"}, + {0, 2, "L0.2"}, + {0, 1000, "L0.1000"}, + {0, -1, "invalid L0 sublevel: -1"}, + {0, -2, "invalid L0 sublevel: -2"}, + } + + for _, c := range testCases { + t.Run("", func(t *testing.T) { + s := func() (result string) { + defer func() { + if r := recover(); r != nil { + result = fmt.Sprint(r) + } + }() + return L0Sublevel(c.sublevel).String() + }() + require.EqualValues(t, c.expected, s) + }) + } +} diff --git a/pebble/internal/manifest/testdata/MANIFEST_import b/pebble/internal/manifest/testdata/MANIFEST_import new file mode 100644 index 0000000000000000000000000000000000000000..3c5a0101556efe57ac8743aef73787caf0040d46 GIT binary patch literal 7457190 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%0A2M9#tH(!x@fcj)w3`bai52Bocv%H0|r`%n} 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 z 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

r!>z@30vIl1yK9%IXGCV_nJk$As*G)5so3F!(%J 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%0ApDu`gnP|?PX=F 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 z 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

rM5r*S0(-D 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


#CTFd#x7m!qB_`V!Cw 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≥W4hZG93SaH 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 z zDT2?;t}}yl(T%=n&o7Dg(nMdh*#-t$Tx`_jVf$CN&aQ{POaXn+TpT^RL{tWUWvk*t zSjMN6?cnVnlpH=DE>f?o4={p)Cj8SkXTC}$lKEJ5f_f7%kE1vxgwe}ca=5;D^n2>0 z8TRH@xN~-ieus#fzGMuSNN8pTX#Q99O^Nj4^VYc?W)0^ah|d_rC$@V;4aZ6|rVLOB zHrV`1UVO|6(|>fA=L2kjpho4>z2MPJM?YGm{&1dWU?U)Cg3o^c`acOj68JpA zV>@|Az3)*}B!m;tPsMvoHXhRx_S-$dHbBq>|IY9+X0fbsz_(j~*o=*+N&NBV<=cEk zxSi0G^{R@#*su)@G@)lK?sgd!NusyMuS0t%6Ok|YvKbXgWa!f3RJ>^Bl-0Maws(ft zNRw3bx*{PwDxwCT8|4+r1ZdU-X#Q8`m{jzDt1Iu@NWq^3|JNWo;fcshS&r*5eftMC zevQ?)j}@t>oLL|n0YQt4Eij3Beg0LSQesX4KA(a=IihlSo#j=x`GTKc68tq^`hw3k zFwlfPXwdflsp9XyL zLg2BT9I25Y{B0Ai_^9Og`goD5<2*9YMnKR6-{Ps;?x)n54*UfCI+RVRgZ_;RPalEQ zK}sEyqI2<{eow#WoS2u?i1fBtl21}64XM+nnYYGY0yNcZ|5@XuPKs{e^zEeL`IiBD zeOvGD6_qGFbKw683eW6_I*coZCi)bDwl@D~FFxk@`h-pi1=s*Vlkgl#d)h4JHW&Dg zi;z0l&Yl+D*M8T0RmV4*TLn4o~$=M;Ok(vfq^FUFK>wqyaH2%)u2zsuS2W5Vqo zB}>jnjYd(pHz6RMmLS|4BdYc3UFJ=YjSu?5-Kiwpo0s{*%{DO5;$rujR6Mk5`aH^& zZJ;j#J-jud*74UGk<2qI+5RoEQn5yna_@Vy5fC)Nubx+P0pYg;zXteNZ08`>Mm4pD z5FKp(8u1a_10@T3G%k!@!0ic~>c~<2;(gm* zUvjhU%71>4t}w~sbwxsWeMHR|Ho_~#zd++$7hui*>RSW!qW{3VGip$HcEkVe6rNoX z)o~cl<`DtsJa)Sk{U?jm)6P{0vDqjyacwwT?Rb1GZrUOrci<4+)PRJ=q9b6DpGZSxAANQ1WoXTZ*Ct)q0a+; zB;m20sa-iU3Gs)G7-@O`v`%qq1ls^X6MX5v*5?vFANc)*kK$BBZp(pi%BJtPynnh# zefU^_5fC)Nztp?xi-i9N_;$;IFTfRw89etU#N#&Nich&~5&LI!?{fjRe zf#%i#jTu+Jme!ZNZ5w?9)f-m{VFc!IdONQOljfZOjR$;Pw{i}tXCE?)CQA|aa(&@D zOY2e*K7@euM7(1TM$~}pX6A~}w${Fpbd!WTS?~PVH_CEyKJr_A4~bW6Z4i zU(qqZ{iYJs>-|(wJ@|hL{N?{x;{PD;q&U zi;MO6KMd}6l%jtc{EpzqU_&DpcpnJa`TtERJv`kI**(!0{f?du3^bu%*Xy}N8cSzE z?+tqFnTXmU^I>vuwC#Ow!DB2vt3N#*U;_kA@Jrr$x&q;wMR7NW@aHk%kgq0y;N0RL zYsCg%Gp_b88_z~S(BfhbnAnetexF0JX#xB?icRyVO5sZgyC?ZFbDcP!tTX(d+F%

zm4*WXYp*3o7K2-(`--IxV#YQO#7azl~=+@4ePwr~(Nim+c zdSeV9_rkR&xy=77G|o8=YrLY+UHAUXj?Nd}%I`7}_7W2pnxkrOUR$og(0m@C@qin( zs%{%gin^Hyd%%|-eeW9T%?=1iCiNyF(>{rl1s�_8WQOHa-1(k$TNJEoNgzVI4Gy zcePVDo*;ZD;13bLV^mGu$A?rxl(juF7}W~{2xwLX+`+qz|SE(wzEWzt{}{^>2oaaYvSOwC%^~@n&6MFioK6U z?FisE10RF!9MXr)$#=m}U+6cB^T~SON50Ur4Gc7)KQZrzx5)XCphw7gY$gleS2TG^ z4rbU6Mg))8#Ot4)39tczCiqITx|_#T#sNR{1n^^{YU5eHRtCbeHsQ`cO0qt_NIm1+ z%wZ!SXo9~zt6N+0egg1mgddNC_f9UDgmCUJRP*>I>uc#Boa=OK0|ZU*FK$YCkB)zY zpN3zDQ|b73f+`QnLqZtEs6}znxoC}8qsvvaclGgW%tssKaOKqq+}nw&`NcqJZVS+u zqVroB{Y`h<=$mB=aCB7V_G#lC|47p#K;r@5(C<3eNn1v^AhFa6a*wi zBNrz}RhNFv%{j=vkA1nABH>Qf2cObYqV}fQ1_oMO>|ryIMqk?K532FgL2q^z;YK*( z!q^3Tji4*Sjc%ABXK24(G&)!7_netu$Gh+$a_~mXp1l|5F$@W@BDs*pv5fn6u&x-3t zt)Oh54gO5G4gr*VggQY>91vpb7rL=QdtW__@HZApD%DN|#H* zAUJcU6_)q4i`4Z!?M<^05H!J`=)TcBQnnEIUgwZI3!-Wef3Ni5Grrs@`(K_2C+ov) ze07X%V4%gt9x=Id#o@#k$oWN}cW+h~m&;M(@kLP+dvdUs?O^F=!2 z@wQ@wbJzGj%lnu0=kMFzvkee5!Pma6@)cCaRsyf_Yf60FDWYQ5tQ!Uv2HbeLMtSjZ zP)E0LZkv`|sj$XDLKrD@s3%{X`3;%_wu`Ls%1%#RxwUPamhD{7_6Al$7=anSJc;WV zG|nwD)_A~;y46%0SmL3nV_vq5*z;srs$;7WkQUABCd90Ys%_J}v-!hqeYw~|!kw%O zH~4aqZD62DxEI!V{Rc{xwV-zbJ$6l0^-SZl1aw#f+rLg$vbhpno=c>x;HMANU4YvJtNO$T|9F-=>E^y zvEq9xa!B<$8gEkI9H(#~*LY~2577Lt%rUv`0cT&<^bg9M9q>Q71){S(s`{>&Z=!QN z?2FD7&5Ns;{>&F0wt;~b7h7nmSns-bd_kGB3-lqNhj&KRtifCd(6tXcHwf+!yy@Cw zuiJaZHbBq>e{1WWzmW5LfS(9_?Cz-A*_jjb6@AQ^a6M?z>*&YY+jO>pfhP1{mlT=D z4>LhuN}p--cg@+IK_RBRAv1L&hWRF&qxc1987yG)vd&a94YC0?okR5f$7%g*ed(mJV? z%{#b895g&H9!s|NJQa^{7sq4F0z?5nPG5Q*z5!lslpa_nrqt;1#+B>wn`dt+QsJ*+ zN}c!q8@=Z)w7A$YbEhATRZmT6WgZGHKy-If=jBJ$G|3J0Pbu4%ouBNz6aM7ZM)ZIVb0jgm}T`7h1W2C$OALglq!@P4FXLc=8mD%6-6hYK3vL zH>%Fg&M+nAr2iIKsTRd0<&6KrKHI=R6Z&712bm9B9sqqW=&}2wYV8~z^YG8P{`i$g zH%b3$k-F1)l#h*opb7r_=N`F?#?2Anj}!hdCPrKMU_^+Gc5HqU?=fy((_c7oV;dl7 zf}iqoyW1%?g}^5zA~x7g?*i}S_sl20nvx{+WPScN-w6lXz(5oFtchunnV=s7eIR}v zTE86wvPBpCXh%O*v_euMMaS;&zKObHzrMwJH6y=)`G6%|=-*ifS!?g>ts~5JrTiJU z?=7q$Tg2M|QPpX18}m72k~kOkSz>Sz{i1Wh@XII6SOBh1*XITIYiy--ivB6=z)lL! z$*9VoaK@z0`P+Tr*(sfiuVAVpzQYk40YQt4J!Y!bHM0_q6aEbFXMqo&j;iJ3xhSG@ zuXJYpXC?fmb0a_7!C)I8Xo7!v*Hz|v%+LRY_a>qlo}&GK=-4D^`uI2;xB_vVnQ&=;IC9^RC&@HM{Ru(3x{T{Pj>T>VoQ zO3mirk0kH0q0xhQV8O!)wukpP-XHrXzy=1I(5FSO`fYL)5M<@MNrA3N#C z@s0S7-I!(g7YY%+q=$dSY@!ePwI}Ol`dvTz-E{h~iwx(;&}|d`cMbjT68c}f8i#K+ z(vMbT=u2P7!x|_*fzRNi(2ga)`Hv3mxRSb?_-V1HVTbaDd$0v|DPqDCs zaGzeO8~)ec)Y1dSJ3nq0UPwFdc}J3F0TP3ITgN-6x}OX)t9xlG`$w0XZ`&TO?Rdv8%Z2_y&z@u)Bntv0g!}A4 zoi@(49iMCF8seSc_F+8(_KqY@?O=(){i*x6vT>X!e%zrvmCI`ge+EfnfW+XI>PF5o zM82NoS5fFKxB=X=pFy(4#<7HOpI@tMZMAK_o)ugXUh(IiCCRn`iNV#^)$X!wzMd5v z-s4)xJNr2#cLzueZm2F_WZQf_E4UVTGEKaLA%SfBVJ4l?o zvwAk+QJw14V_(k-u3bdUVjM}Fr{`Hh-X%2GZRXgX`$~Y|I!4rK!9g-NKw@yKbi=D{ z+lIqZ0><@4f#O#Q=xv&D9J>Tat_hG3?u*ho-kCZy^GZOUb5$H%&xqQ~gN!6)Z5&Gs z?gL%rTiZ6@D*=97AG|`uJNpGB-vvkvZls=ct!=wyo*A=#9A5u8#?|->NUjTz7+j$q zkZ;>&dBt6D`~Y)kSVVQ=LG}eC1pyMmeOW`db!K?uyyEW1jgF`!?h2AP*Th(2a7nt6 zb5hMc>&K0csGjWImykGjEm&f3>wO=1^!2RlES|{k$*X*P35j#N)9Tr;ZqZef>_C*d z&J=g?jvr5@r_5!|bdosN>sUhGef6m>@9gYYuecxPyc684l0iliXJ=VraN~4(W!t+Z zUZVJMcno_4cg0tbR0)t6oYwu1*tQ$JWj+MQk5`5k;}HZtR(u6XVSt2iUq7o`pSEq> z>werS6sRKaNJbLpNgFFszHX-*G_-L)A2B7sk6Vj})YvH(%(9uh^UN^{n7_MARs* zC?u&9ATi!e)B~LI`Bbps8fQ2#3`RFF}O3j?Pd0+R(M~t9M!z4gTqss z9Xap5f#mW43E{r2sT;m$+mf~MPH_A@Txc)6R0s`Ee&AsteNSx>A zSwgrb_vp0cHm)R599%a%*26fGtO$@8-1oYFJKI*0CMw&`oST6x>% z>sh|L9U2u?=Q;04QXxP>v1{5#m)~UD3X6O86s57?@CZkH!9n7j*|5ama&`I;8~5-6 zGrybMqN;;S$4x82K{7NzLb&fM>(u(T?XF8r`Sj!PM8Y199Z8&fqAW4EUv!(=Htu)t zw9Z7ZsNDS_6U7@RO<8Jcf7UK#hU#a*366aQd)w4gO>WS~zIA725 z9kYrVrZy29Z2E(_gO zJjjXiBP7n@g(Zah=~`XoBYRUNiQ?dJv#c2}m`sw710)7lPfv8tdP;iM!R18NV$Qpt zAaTxmSYmKP^Z@5<`??}(RuDOnPPC$Ju2eHLy1D>+3|aACX!E{t!*rR&hd6}XpjwiA9#-i8}0xZAQ~o_9l~ z`WL#aGpks3>e3keM~+nAsH-}GecAg+ec!&NCucn_*U1yYxc9Mt`xJ9Sg(Oa3SwgrP z_vjZZ+WWPnp1F4u9c1HvT%6dHF6t-3_dcz0QNLBbI@+`NqW&@GnB2#@sQ-q3+!?HF z0|PBC_PDv9a(>p}2;EO<4f+(KC*`XS@;(^|&eU*<+>yZTls9nm(|P=gjewvD{_iWR zK1z2y+5o>4_!w*_ulrQ*jz_qZ?~cb(;p28tTs_|%4>p2=Cj7?dp5IFDw*`MCxsMHP zlV?TA#RNOPE3N3fscSiN0=5ByCiqhKFa4PErakbj+u+fVv@S@Q@$GTBf6^M{4Q{lY zn&suqt9A7)PKDWhrZ{h29i=Nc$CU}snwjeCVWqpLJW-N2*ozjKhwv_6vL=ahTw#f^ zZPTGI?EQM@q`48-w`CcF^LqA^=W|b_!S@dAd#K|s_}*+L+-{k#GS_{JHLiE_RT@PbXQy!_|!{svk?$9 z>Cqo+R5kA%bpbw=@Yv3tY`!^6h;6oqw|ac@z~Wo_1EdqVM1My=y-O%m`-!nbm zCpOY$o%;$ReEc?p^Fm%Zw%#k0kbU6$l(x9z+&f<_md9|AF)v+a!f~6aJI!Bgg3Q(GUEs;KyP^ zM`d-oxAfNDw)a~t@88z9IJ0=Rfq^FUy(jxGC3=6*qeRDMmdI!YVW&-xI`rtR0X8tu zgnlIDr=t{`!Jzj`Mr;Pg4RjH?2R4 zI_c$6LO3;F_0ID?R!Nf9_WrZP;Fjs6=WSctcg*SPeh0Vv%4SW;_aX3o8hj5WErIXn zTH!~lMEH)O)_t?*`_(x*sjUr6Y*Os|HIM6f=j>|Ho956I8FS;J5t!15?*sBxYIm<3 zkfghfV+r|w&DT1#*tQksn9Aegn%17apR#5ce4mGYjUAe=_80Lh7;o#SGbwu7>sC{P z-_iFqvgvFC11&CAn;Kc8gM8{_K%PObu=;I@8!WS0(UH!OoLc=yN(1e~m_f9ik8V7pUcF6d#`D$OzChz!h z^|ii??`maysT5zvvk??D;Xir2#@$rR(#U)8V<+UR;UoFz3J;xAiUPr7y7w*~t#K}A zu@Mk7!T(VuGMw<~z_)G>JhpRU5g%O%;auLjG^ynInfG)ZXS%~SK+pu=dcgXsR1YQs z-xK&4Y-iJ)C0=w&b@fH(CJ%3(pSkXouU4`V6tuY5B9l2M7d<(d9=;-eFZ?=`Ok)bE z9~Vwe#E-5turT2Etn>!fMMpv>-PF1FTk$0`=wu{#xr!CA@>P1qWOJ@?4I~jeeOO`! z);ZnBxmPsDJ6CXVk1bhIlL9moJCIKSnx3!LonPw(=(^Ls03}!fy75O}fY=BMngr;J z+= zfIb@Z7;HxGvYc9vd3bfxU-^H`^Z0 za=hQ>lvB2Wffg5g(hRGki>sC*`clwSJA%F>UkzweOyBmQO~18uNl~b;%Q@$0Yy$&L z=v{vt_7TNq8R!#0kHKb6x8q4dnG|1as(N@+6w2=Qudu9P3T>+w%tJVRiNh*ePzCi9^^w|eI0e?r=M8#_jM)b z>K)s_Koj~+BQ~w0*sKA4WGBRCbv{-L~4I$z$f5fn7xS9|i;!y z8L=t4=AE`}&3(>@^|JEx<)h8Scs+byh}gug%U7-B{V7D(xpuP9iTnxYbs4sSfhLi; zbkh$XP`_>feG}2KnXPkp;p6x9iH5d+n=E<*U1Mp04Gc7)mpk@)ed^cEpr5CH-IT9p z&ES45cfGG)&s+Ukp{B22*$4_+TE& zUuaV@WE znO~smIk%d9$11OP@eWVE>anw@IYBN3$vV5oSVFjK|Ef_b7-oF*}QY5nT?>J3IDI#7cZjn`w#dt zQjm##C==1SxJF4yhz!l-@_W&4eo7~8utRm)J3n;08Lu5+vX|y%&9#th43L zw{4ZbGv!xsUcYvaNipAa*$dz2r6BUyi8FXJ+0+0;_e-1bRNImY^MTHv7+{;5QWs4k zbKP(6KTQ3)AM|ZR-)}nm{;RaF{IU@gG~qwh?ze9#Z;pZAw=4KZ^Hp*hpXdCmj4yBcTKqfz z^6}XS3R+z388fEd`R$4XDv%odS>T6Hhd@3@XT_;Gy2s48z5+c@Av1Q zz34QG@XPRPN*v4zs5Z{Uk5K9o-o zx)u`WIXIS3ZeADC^_+Uk)y~DWZSm=RiqA>xz-GiJwkTgMlQnfd)T3Xs9oj76$64}+ zMe3)=0*rv5Nqm|#O3x?!Y2YJ-Kb5a$%C|-c@wknMSmFOj&sZ5?0|ZU*d!M*x5@pX> z;8VIGd(PlgtaoRe z-Afgj=xg!o&|s>_NY1$MLaN9Z5(jx&aIrMKcAviGZ+l~x9XAtVNmFkaV{xi{H8h8_ z9+LkCNKDq((dC>|^FUnJh7DU#WZJ^_o&6A*HU(;`EP9H)cP=5;?^sfiKhfKrqcGdR zK$FOP{zA&VL{A1iN_1@I*lw=KAUL;fKDX$f>YE?7_ls>{pb7n%+iox?Ivqjp)*m_F zp+N1=?UoI%49)2}^stTPkukZZ2CN}71MlW%nR zn(9Fcd|yhj=~AFF+wv^rUjNZ#sTG?_ANpn?Yy<@@F7}+s`L2I9{h4~UJNUc74|gk2 zC))9m{1bi3Ig#4!^lV=zW7!4Cw);D7FEDCisu) zTz{Hk-xv5%z{g-aTPE^}TBVPCd3mpgH+fmPyRR0r5fn7xf41i4A>{r5@S6>Q`>6$L zE!Ofg^$;F9x4xPQ9?|&>hnYS0uCWmiG{HB{8fspp9Rz$=!VfG^(Ft6O36WtVx?0is zT+jS9zy=7K;A=m(us20#2=L>8j~QH`&Q9a#RPN!6&Ny)&_jjtU^hJk_pr8r=*q_hE zQgnubKbPFch6>K`#T9t?o9*FT!6Q1K7pcTI0*rv53I5vc54}Mrf`s3SUx!9fQNY-Z z3$MjIC%lC!11d(>_1>A={}{^eJGYm86BDTn7-TEC&|KG3zkkqn(mHdy?tt#Q+_w4V z;NHkh2=_-3p4Hu)2$Ez)fP@Oo_4nz1HsQ1aH4N8>CA^pXZX=$wdiD$5zJGuX z5HtzR%Pj^DrqE9Uekp~1Vu5P5iO;7VyvbJwYFeRxxVJC#Yy<^O_=^V2?M3cS27e1> z4mOk}_w3+ds_o(1f=39`Fk1=ej4!m2tTzz?P|->A;bWi z|FspJFLl3r0&IYw3BFUaa080YOyFk@LUd*ns3DX1Z1CYezUa&p_mP;7DqodkBPeLX zZ?Jkr4|0DN_-nwA#fGx*N@O2Juy`9bmX9K?( zzYe8Q$|F7F!Z}nW_feI^pqS0qw=SBkc*;A)cIT%5n7h?(wkCw96sUy-6U^D*^^kN8 zkWlj9aG8GLR@>&Aj(Oete9K1W0gyS^fvprD?8)vf+`SZon{32Zt9!rFV+IG<06~-Z zlo`HYF@=9V@L7bPSD@y~3OFD*cR;eN@PAdLes+SvMnKR6|Ln~bOH=kN1b*gVWY2;E z)k@9=AHBqvJx_Gu(=}wzkzKLJKS(Bfj7 zo1q)`^3VtGD|bnV@W=4$(2kKP$`SgJJpixwZ(e|Z={@*Y(jxrpG@e)rZH6H!w=$=Z#6w}5_Zq8|t7M{nZIpsk&pi+^>X z-%S~YAH(TigUFgflvA{HK?g!*%*T(G#9K**9+1UkkQ-fK2xVYhg3u-yLR-nYo0chv zkaqJb{4R;0o$1F3vq@U}U^KL$xuh6qwoCsSMgM9>e|L~PXimRtPd~=tN6aE}<2-(Z z7SZqe)8DnBzbm32L+MZUF2uhU(((u+>37TNcV}p86Ugz6^t*ld5i^ng_v}#oNTEND zl3^zO@w$2VF`p<$>3`cd$G;BK-<=(SAFXJUOX{016gPg88%XcPS78pr{5LPhK7)=C;jhg!p}E5O%5HR|6LCM zLK{fijXYRHKbGM~%r3(3qP{#wze^)P2K`+I{cksdcA$T?8-yPRskhbrH<*iT%wI7ns%NC5-_`I`% zTgW?`mZ5g@&K`nfsO0*3KTjN2fdU@=6WN)JO2*!;MznW}#)4@Z(410)8wPY-ZTuZB!A zJuA4S>|MW1wU~GI1SD73IF=Aj_tNF|+P3$jCU(GO6(CUrH#}1<;Fr)y;@lQz3E_%f z&=ocY;C8KcacP-q4<`yqHU&rw?yzrB72cHKmhsL`%2dO-q7*@L@&dS%{d9R}`g{T0 zyiB#7-x?!H*$d!K)zUSG*|s$wo38MCw<1#==A9+U@Bj(z?5ShAf|GX_@NRRa8o@hD z5-0Dht~fnVm#<}S%7CbFsU098@6NoUYdZJOeLE|-NT%A&qk<&PrFxba zT#>G|z~0p4A0>9p_ag6Buy+SD)s6*TML7e>!T<^3&JNY_P8u_=`MT;3?sTTw#5j^T zY0MJBoqJ8kJLj4gz$Ip>*&I8PIJsrx&gk0CmGcYWx@4(;=6O4N4wBdl?CklGx|Va3 zq6Bw>Vz-iG*EdTg@APmaan4UzVlMv1yrs+K+S_WLZ)yO7=kSJRsbiD-c#n6ICNDta z0nh0H%WUJ%Q%vXjf#b5&v@rsNW_f_d0M#fxKm`~DxQh39W|rDAo`IyfBtT<;mwc`( zI1`0$3d}B#9Cx4DWdSbDQq9sBNE+wr5^Fr*1YPR~duzY`WOf{t32|q`e_5(ArRQ3c0 zlIHIKjR$O}t2lQ+L*9o({J_0gYUNk~LgU=?WQ_;htZQFsZ_O9OHN49@OC?G#hoHGC zKx2U63OXsnHu_>Hz!ur6y~L0-nE@IPn4rTIZKE%S0!+%rde^+mVQ8FrCu=-lnyyl4 z1AQ?RV3%yQR$>^2=4gP%1D@24ook3!c5u>WEyu8Hwi+lg3`65wG-Qnd#y+Q8cec0Y zi=hDfXRBrcBu$q9jR$P6!&$b`7efJ#%2qoi4`ZRp4$yeOExM{R{qx0e9q)2_wwf!w z91D#z|FiQj?pEF8QG08?7z%J^wwfrrOq$06G{(cY1fAv_J4-b+rydw7X$Mdo1UNri zZ72pp;~YF$;{nrktaCeQsyCLW`hbhF)sS(VhjGw27tUGZ0Z-{_Z`$s6@W!$L*K-Wl zW~=$dKxp0y&=}yQ&+8^p+xT%ulZSrbj%+nYc9}HJV`!}LfcU(KQ;IkqeqaP6MTXv` z(EQhS!j9pkTXnfrwlVdHIRprB1MhNSw%RYH3oQj8{c&b_AoWKx=9=H@O-v9Ac;+yCokmT6>W4)=Mph*O@JGDm)qp1 zZIjqT(m2-$Y!9!D*X2*!Tiae}VhG^OW0;8uuxF0Skxn7anE;L1GccKPZ;o!UOY=m$>9Q7fb#lBPp|#sIJBulr96F#3V>a?}PnrjW+D>}7k1 zcT#FO_0Jy|oJf^K#S)DGsD*5TG$0-q=mI zb}C1|uBKwAExB*-K#XPC<%2ounB*a8oG`J*17>J<&4zo1dK1p&9&jtiun@&T(&k2J z5^N7yV}Paa)>SXFjW4hC4x>Kc$s9FQc9}HJZLJLjk7bsx!qvXub;2cpi4wjh(UVi{WUk8V{JE z(>~{|mFb>!`M)0so6moCOmsS1xC3%kD>+t`fz-(!))?+h_vr9WdoO&U6yT6tHA}LG zG`j*c9)A6R6y2*{U(HBYqPR&)rB^^mqIzVGQ zEF02kPWJd>D8SjdYK$D7N#kUX9mBG9bnJun)_gJC$-BHbS4E_=N%K&E#`Ca;u6n{Y z`eG=+mAPuB>~dLXbb!VKX6h!1w(+0hA>DQ`s+a&b=BmlXKxmQzGzNGx?g89k8z&{1 zJS<#(p8@XTUEZCmIth?8cLr!Y;P<+mQ$RlTR_pWwGji2jiQ&!A{AvSP;{j*sYR-Y| z_f>%&=H;q=(#tnPn7*yt#J(f9v;e7NitGMb3Q=h0qf}WKWw8fhPye2 zI#(6SF5d#pp8*;V*wZ(6_r*|vr*qX<8G5%swBavMe;Q3rN zx)=z}$N-J;u-v`6sdKvGi=hA$^VA+W5|GB3TC&Ciey=B1w72Gq;U11*a-K?-PALbC zbLoLK9&olU|CtR;ck@tyo$}QAVjwh9n@v$>jRBUA)$z_F7QPq?uv?y*BqN11&O;Tf z@ql%8qI21_LN{}GmQKmwUG9~q4oIhzhbGqEE7o|xo_gW{+vtm-0Q=>Y)Wh=73=GhC zK&(~c%#3|86yV@I)kUT_<)LwA#&!%V+^6H6>k_^g3UGLy8YClyG!twO?HE@0PM6zc z8=Kx>68l8-?iRyP4>LK2WAfC7Vjwh|12krrD~#7wp0kZd*O+SK2c{#jrP`3@`2dXv zJgBR+3NZSClk?P^Vh^ElsysVkfVWoBwVaD1EmE9a&f;C3o~O2nhoo^Xjluw4N$y43rmH=r!3eb4K@AO3H zkoaC3Cv60{Ax{-Zr&NUI-!_mn9&myVm$i-6N1L+?@ld`(vMo<7F9t$$bAZMJ9@6DY z*+$<;5#UaInnOCJA~e?qXbkYSs=7)`+vpo90?f`+v&2Kv1Pulc*jQIv`hNgM@>ITf zcpEg!0yG{FHz20iM&C%0&xh>ACorU9-v-Up0F4Jcq~CYWA$=o7fCp?3p>fV3?U8bO zRo&zpduzUtBEZ5tbxN)jkmlO}jq&hb_v-Sm*v9F1OL166^-w<3q4U%~a;1PYbpkX7 zc*niEW}0nW=2e^N>uNZ_(|KywY(5JhO?rUF0Pnn4*Lv4B*6U_G1aQYl%fsdobx44u zan3tgV}Sp@PnX+j19_JPC?D%cjHqP-B+a$}jRD?upRUk6z~~1iN7UFoyvw9%5uh=^ zyYJQI-m;Bd?=`!;Ze*35H0-i?*gm3G%3+i=ZwF`$@Sb~hg_;4zuA6E&z|Qyrh@=f^ z;sZ1Wc<;SBex+^vxR>!zfbxZsZV@#|fTVFQFtf%0??Zr-Z6NQmAJ{9Ry2$k)(zFZE z7+@s?_>%ymAJ{jdTF>N8Ad zE9r7C+gqz%$p8yB)#$v#0uIHON@lQ!q;ZBTYYebTC0)U(*le7&)dR}cKt>{M#6!|J z725_@t)%0fBUoo|m;Jyo5j9CVg)|@Ad&L^#VYN!SwsTy*Yn<6-0Eh3yE{lf~BC1f* zhBVG`nKcIZ!2Q0-W7Yc&aKd&Mm>yBn#Y58kV|&ON1AOp)9q$xa-kJdAnVyDE z;}lri!-wwIwV$=Ob^+kbh&nDn(l~QU8~AW#UH*U#^zE{EI5(me$y9_i2Lm)_m;X~) z$2(`1^~XyLckV`~2yjtEWl5)y#<_XU8UuW!vabD(y)`zDIa0#_<;yBdB5H?tNE)Zg z+rUSw_->JmdCYj|2d>07E+mGeIcqz?8sp((RdjsN*8IRV5w%}DB#m=Su|0geimrXV zy|oMM^16r`FrCLTX>JJ67!RMQs>`>vjd#B!yWD3FIz_%=vMHiAY;Ny8gF+gohO)*0 ztN%;ac1~~mc)dKL+mjA(OGKR$53Apy%T*6>!T@XBq02kh4|!{T;LeCzEVuVa<6J+q zJ$&*GUDLVue*q77M^rPJijc;++R7T^;Zt|$TF!C#wTUK%@Nj*e6~nBEipp64X`JIS zYYgz|J9T+yp|a0jHo!;^4=A5*iA2<#ZQLoOaWaE72KdZPzE0t-`GNTnwN4WInX(BhcDc$%R5)&`@Uj2rEhN!C|_MUhQ!|0!5b;0`P%l7 zH3siRoyvCRH7x1tsqLxdikY@LVJdD3a$KPukFW}+X zi0UOVB+Y#R8ner_uF253BBC*5bUZG6F7pWY8li>mZuAT&z?G#+rSuIYH#Y?Zm(=Lb$iViyCU zaXe&=2Rxv|FW6hVW{bI0_RW4}F~?9qpwQ{H+=SG&aCny*s?xEY@alTIN`C_v+R_>oTk-8L?r z>GZOEgJ&DQawEI^7BqhZXgpwNUC+7w(dtGgu?4s*s`^T&yakOjw_%M3oTqPbV(9A> z0cJ;4=VA|`abn0C4|qT)In &)+{u^;tNfRU)`Sqy~6xo*oE1AM!@Z-Jb8nWi`l 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⁣-oZ zZ{HevM2F}X@UxXpfr;n|qgWlK)}M7lhR{$J{f=H!P)Zgd6y`pLTipI>;&?$_l&S4d z=Kn`b9>2A-4LRlvOpo0Q$DGFU0ZS`>Cl?oQm=voyW(2#(7!IWl42W_}e#K`F(@fzk z==<@rdn~0jWHcP6p_u~G-3a+AoRGO~3hQm$`U@J`rEffl%^`#EF)U>2&-t!zvW$647M8Z;j7(`QOjO{{kN%Qi*O?s z3q#(rXgwp}-ZolxpE19cIxry0+>ZUf<3ssH&+ zMrV^cFd&Nl>F0g=lI@p3A5U~tb55Q#@`G?)r$1uQ&l>9rsRIL|=nMQcy+pqPIs@Hv zIp2z(QJl(tYN^w^*>t3uJY!8Hbzndgy>NQ12WgZddawP6n}ZZGt0^O(g~QzE^KmR( z&hG1mYUX-nShTTJi5CZs%w`TFCuL1d5phFhjK-5?c-+)`l0E9zugx)T=Fk0d0(s{u z?2p|K-v{!o^Xl_4BiUJFsuXK`$3Kd_@`O&8IxrwU?^2#B-KG92NAzpdvP4HUd*$0{ z$iI_x!g!k=@gRH9xJe^*U_cao=1UtI6a6~qlZlRME~qDxVP6%UKG~+j_A5IabYMUf zePznZRNqR5{#M?-;rt#3*lAN_^rK9Q zLm3hMKd_fZ>&kByt2r9angTfRYM>c;rx)z^AAomy7Fd_%epduM&|D`xdA>NWL3h#U zTT%xGM0uwuvL7F9dxJik=%{9+%$h`)tkYlA=nu2kjk^g_2L?pZJC#1fN`aSj7`MJx0)Jisl~`aCP7ApacY>@H^hBzLfC&$avtrQ3Y0S%H`B;KW3rH?89#=3jO}Hq0|TPypIvw=oz4Xj{W5-b`>FGy z-{P2HzBONd+$APd=XJ5Db^2A<=lb~eg^YI(8aF;LA6~&c21ShhXIbRm%y9&+`;^_I!l(+)x+9o;cCchTw7I}*7aIeVf8mn5V0|cVH68FgKF_a7lpNyZaEtCw< zV}0(klnjt0;?zm`dLjF69~NUQ9)pHLl?-AFtYz~i%VdBuuj=L-L!qBFFzmOFL-1rE zLb6hgnlDf?7zF#bl6wXgSnJg`AmUY8=~HS zsyQV0iilwBczmhR$Fh50(`%MGFd&MaS82;hq7McAU?J#33aoWwZ#v69tzS2ranD~pD} z7m^FCK1=+vWl_es*DlM5G=GG(7_0;Bd$(H4ZjW5=BJYfV{b@(wo#9BtSc>z>F+9*i zC!}fK8ON%mIq1NEDDPyo4iBRPSwvropWP{x4bXFac-1bJJ;)O0{84;757~$RWW9Up zl?5$(gm1R|II<^Q$|zK}f-VKM4!bN5-@)8;ov^~VlyjpTbYMVy-sLj%@_7@z5cP12AL**Inh(}m{Hy!`Q#o`1J@hE#}x`Pf3 zh@xA~yN@8>CxTu`^oa%5?n%=3AQP; zkHP1Y3amn`nv1WzvTo5p+v)!CY_74lDRqEAlvjFmWc=};$-pNN9`)p_L;oNcL6M+2 zeSE;xV}xF{R00A~_^`%NA5+9k0X~cHsHgC{Xj8;o;bonWWg8z+i9P*{gANRc&%1)> zvFRt;^rVQH3i^oSpr>FSg*89-4H-C6N8ER*INbdc*vkJn=m3Ez{H-26N|N!@fS*fv z)H7A>y@Fueo35eJw`_?UbGK%QS@H6EWykC+2+@j5e{j?v2JT&iL z<(qmpS&VV)E@(0(W7C~bU@a;JqKt9eOP@^Df0woVNxuj?-|!(<0nxJQE0SdbLz#9C zWkdq|fprS#yX!3-q`XpwBF@j;m!dLeALHi?itn$4eb_+ocW+h@z(z+%8M>*`N<6 zI;z>0F9vNQ7%?$iqd&%;HFmU(p7OSpQA7wV^cIva&`jIT& z*wMG|qw=n#fb$Ei5zCWgS3((M3N6csuH7NW9xu5WhfS&}s7xkoFlc^oe_M}dz zV&fwo3%Gifa8M?nY!6ZRvJbF2gkKDNI^j{zXq+oAw$L#D4LfA~6Is$(9WQl&KotJ; zG=A#e zz^^EL| zN^f`2fdNtUk?*kvOHt0V5S z@%|*X%ILOI2MEOHT_u*5m2=jS@oRw}LwMA4QcmLG-GX~{`a?E7;tBR}GY1_Q5JmqX zqB?(+Wj*La&VatIz?!TUt{{A_6NVV{BgW2<)PVs}^s%i!Ye6%z4WK9DXUgz+pE>>M zPh-gmnvtaySQphMNa(EpJBxi?x5rM>JnM@j9Y#GYUL6dpk1}sKlo45fC3`edR|ehb zQ9!u<`07MCjG&B>OY~mOkgj2W^$F7aVRwCL%z>tm=zH^D2D$mbgitdTfT$hUBx zU`LG6f0E|&D(sm%b#tW-42bgiwnG~pp;g&t&}Wm+(?Pv{R?uWaREH0;Vh?5E5^-v!KFS!AB4e`gzZ;c@odEuvetIXRupHRm z^DO2D*;p6I(KX_UfXn;0?ok)BlS~zs@{xlM5QxvanrDHJH~RRaN%_FX1Mkf%uu@cp zL>@85rblhMKbe&_mV{C_;cR<|qW^mGd;YP(0??-t9n~aUk#kiLuITnXZP4?FIq1NE zD0+2ws~5h8YqoOujxC4zp-o=@Vf|)dh*m0b1=|YRexjS{gd!|Xk&LzDgl8g{ON~Y z>p|A<2L2@BQO__OCgigkvd}oX($02HL{*lSr(c@X0RmC@ZI@>bu}WC(Jr%P4yqQ|U zgIdO(L(*ABNeB7HM_D4wgJd*cezPKE=2^fd6zFw6{X9P(nYXY4-w%>~*Mm-0aB4Gu zJS-MPj0uJ;@^9vSqH7&IJU5#>v=6qgKL-!(EwEChhq#lC4bb(rhx~q)Y=o}V0RmAT zx|>zpNE@JpKTobf;=u+e4S!c?_(Q^5Q#|~I%tPl`Y=7N3&x!Ynse=wGvE^bsoxNBl zHk2{eHL?sJ{u(^V5|VXbVex5Hp4fzY^4__&j@)wq_TMJ=>@TqTt0OG%fU!!xt;MH5 z;QD!zj+aV6Aj&;O&$$l$_(Q-a5+3!O$Bk;9UdTef&aW)^d;?%IOMTx#2M9#r6Hmy|n|APVoDKlN8~&N1Nk5gzsIoG5bx`PW#{@3Zj{PqKBN=y<6E1fuYnU1nFO zdCm#oj}!hl_E%Km1mRPia9pE5#mX2#D|KK%6#e0YU%W*9>LlnV@UwNDCJYFEZ28^5 zjRo|#D{6P5e#ma{9D8YsZpy;q6A#-4;ozC*2KZhrHqRF=68)+H%A`1y5&i1F?2h|% zrN5@wmmB{!2a2|J|8->u9r`;3`{OR)!NuOk@G$LW%9}`UIP`~U{%|>23z6gE?q#!# zC~GpCzr^$CrSd#wO6xsngbg=m^T`?&si)f~i%`mpdy2(8uj}p!=PU1PDHVpDucLf_ zb5?vkYS}ZeJ?8>i_H==DNxgaot!L~?Hoa=kb)I4cn{~R>fdNtU(yiVbO7yd!?;|>@ zS-D2ek3ra?6IyEYYV1K{-YRuqKomV-~M4!Y+Y)|zS)X9|1CAp9ukJX=h` z%cqUipi}|^QTU`z3;E&h3&7tZi)2*&Yhzh-?(!1bwd`%EeUf%v>@c)~gT(B8__sV)J(8b4b-m!Ll( z@_p`j>Qv}#bJWAeq5Hs(EbeaoYCQ5CB1Rv;F~0(9`mzk!sZhpnuq-1w)nxWaX&qRx zv3PXC0Ji>=ts(a?*gxqK+;atcTzEe#Z$NT^aW}B)wc^P1Ph}(S(D70S2t>JOexa+} zHi@5upFOK%U3lj%{mG_3x%lH=lulD0`sY&mvnm$*Ug+ek?_g|H#jtoEn_|rAqA!%8 zcUF*z@pw~N-%h$2oiB1_^pO`Y9DPv*L6`^4zvkFlJo_Zebap7iD=W=XVs+iQOPmMT zxf9@tR?pmX>lgA`0A5Rl*F11vo_aX)DZKRVoQ`U_KckcUJz{gz%9`zhmz9$Qfd=zKnBdW8hW%R104iJdK-*;V&&(d_;AuoQ!)wQV9q|;SZN>`YgrH|6}bvprb0@|9^6~Aa+qi z>|#L_Q2`YTqKMLqH0gHNf(1k+mXDBL2qA$$AP^uBIs{07(0dKNr2?TR^gc_?|NTtK z&g{9a{?7M*&YT?B=ge!K`?Q%mclK^5@RJCSabk<*`i(S<()l$6pQpt%HvSG986aqG z*IHh!pMCcE5rhu|ej4F1&U*C_h%`KVfVbY3))8ke*+&oPsXBL#ty`vWcrg%e>)uvo)Ue$%MRI=Cop?R=Sa=wrA>pGKQL!|L9nUz}goiNI>o z`rGytSc9N{k{6zj%eB^GeMcq;ChvZuBdT04pGJe9V;dUT$N)ieyVmizUH?o=ex_zH z@G}S>pKB%Ul?yx&-qQ&$X!PgV-D7NIV4x}b*XO3ElkthZ3LjfZWPDi2?Z_bG!zqR8 zhPz2XTRqC^Ch5c3+6t%ma;-h<=LzFCMIWQ+>m{%0SFGt1IxsQQSi=bGL=;zj?kQ}V z4~70`p}%uG<-i)s0YoVl6qEz2>CaaBlTCjX(w}9N7csfk>a+1;r4cBQ@g?4h#(L1( zR0Xov7swtbLSV*)_*;1JQy*giCHwHKsls|1+eP)n&+-A5H57>Jpc7XQtN}C1aqY)7tVMSUNifv~1dc;-r2&5R} z+>w-lf#!Ct=V7|9>x^TR1;as)x`HqrmTUEwXG4RZXEEz^dX%8^40#?u2WsG@5D+wl zUz**O--wI=ehT4}bFD13V@wExKSk5N78}`AA6v=*K~wmL{|fnv(tz-r@Ub`6%>AZQBjdF3s>Cp`}MV}u`@YhA#h9-&BDIBA~-yqEA8XHBjw&>$GL>8EL5+Z)ttr#`e40)pmtZQzMjJTI9~kA%O8kF9hH zO~enTdJ0X1Y>}$aO#?;}zN~boKHT(c;!Tw@l0+Wkg@U9b8sf)>S z(SibJJTa36ju*cN*eT;)x8+B?_@VM`nu+2kFUyJGO5J$Wsk#_<1F{R3J;-Xe)~|R| zH;w~q)BJMW*L6lcLf_7vh>I%5MOo9yDX^S}!eD>7KLVO>CmR@Oxgy-}air&3;ZsH+ zTJciVrs(s}%awoehX5`dbXzG+15+rTu3;LOoNKL8=c8-0tHzvlQ%i;y*&l~>!$=tz zXewGC4+?8T$uJf4`0Gdp3=^eJ4}fq)C){<@SBPH3Plp=!2vP_Ln!-<<{{x?8rURc! z_-VOT!3p`+CPEmC_(uhw7owL~?8o}pQU(Z`!WW%*y*Et*Gk{->kF7c6Rye@zSVg%8 zci$-YZ6H@PyPMTBRzrTdrhJR*2%C~Dd`o?d)sXDN9pCI3*4H>1;g@RyxNF_kFUb0{ zpnud2m?s0%fI9vF3w)wmAWF0TOWvSPhIynA5Hz=IBQHfiZod2p?dT9b0UtXvDS_uv z0&k)O9zp*dMSmvIpQ-d`MjTQZxqs-YTxTJ_jNNdVUwqBBE!xQP%oRT^8Jn^8mY&SI(+f={;nRl8 zm_;9B(?VamH2aE08~bxd+{V_v0A>|l;O|(S5B;NW!aVaZeP+qonM|-vH$kN`-|bT! zHr0q(DFXyenJ1y^6-k@%?^`WH@5Hy7!b}VHY zRg8teZ^FlT@ed3S6CowY`tVAIda%+gps}LaD|PkZ-oLJ9=d!_gu%fKL0L$vR!=&}m zr=Cq8Q86a6RAYl%J+K_rfCF=QN`I5MHJDO*G4$UH{TH`AY;^k+By$)!KV z^ye_8G-^Q1Ksh@j%Z!%<%Piw5{kS)%*3Y_*AAiqN5c#*i04XT1%L)H1Ke@Djy}eMSoRT4KgQg~8ZS(~AkuL9TRlJFCkR(U|FkmX#0o6=)DNP<0>&n7nwArF zy+QRK(@iIZfS@V!?C;lzKUY`<{AA#r7$;0UG@8a9@aTj|K79H#Z%`ehU`XMlvhUCo zKJu~1IaE(q1HXi{$2f!42^K;a`}<3L&P|`rel^zHQU(Z`!dIVvjPG);0e&svG0sY? zk9bia13a#)zt)EjuE+k2w2=XVrtn`qGhh>GzZUpn!eg9tbwU6N8ruTJnsKJ%aaf6t zmqI|$6#n<;8uudn2H;os?}c9g&b5ZAmoY)XD4o!_!b9p|N^m`IP?J;}As}dO*Ct*r zj^+)>qR8I}{2F|0ZKKFXc)A^V6#2-A1L{7xS)f2|?qW^W=)?JTceLu{aje6QclJ;b z`P9eQDUf}5N>R( z_Mx(qK7&QHwUGgWrtrIOd9*X(Hv@l^@EGUlOc^_5;W0YmD<3}i6*kbAd!-BzG=-o4 z?XcB^-vazO!eg9;>igzU&{#V4_Bl6whBxTL4*Jki2nd?OM}Hf^-|V^-_$!3RIAdjK zl7jc?gkT>&_!V!^M-STw0YOvv2}7KhN&9WUXAJ-znYtS9a_JS$h2ckDJ{eO-Wg&jR8ao(+PP)qb7z-8IhYPVm z{De`I9*^Ka(4q&G1FfFFXOiu9z;;8zV7u+P){rpy?Hib9i>~jzR(V>d&%{Hq7j1-q zpefr$7WH)#ei!gl2)`3~Tp%C45W>i;nu5>MB7+UIlL3OJ@N;jrOd$Mj;AapX<7`(8 zYzm%7bPZqd;e%ggWAklffS@USzex{HCwwmOO9;OQ>vtUd;R|dKj3s&lpK*d;#i76& zI$jC^K~wmp*^V+gmz@v%6?|;%p_+$E=cej}-$tauhl04Le4Q7t@LBAY`TCfC>pW4X zJml%Ks**NGpKO~xqVT=X`ex|DljXZGypk$lUaoa=$q-pd(Px%T9|82RN1oAzzQY4r zC4Kyp`i;B5==)%m;o-eJ&b_(T2E1v2XEQ>k`cfS++-LRl3~$hrBW#4Apt)U}`AoCB z?zmCp)cxSk0^fy!B0?pf5XQmp3}4Ko&tjf4I$p{$!h4}9{Dbu`EGK+1@C!(NjFX2) zrKCR9vycTkVu24I{2J?N+-^!4AZQBTo^@IX>Hk)V-e#$db$$HqW`w`-+upg>&@}nBvgYXXzD*xd zi6WTWSn~STbiA}FAdH`}JP}604rz1rF&1&Mj}YR>+6($zCb=L zg;hc#Ve}HrC-SX9WLM+V2l>letzh&Z+W%Ck>6-yU&LYex3c*M#oDTAZQ97`=^yi_|w3b z5gy~L$B96091z;-gfh)IuVZukOB*2|XbS(}u<`uYOwRxx8U;MYnX6uBh3hZV36|5h zj+%pQKqIWAFeIuMn!^8Dd)Nb{{dwSL5&j%(kFPQF=pe*#o&S>H)1{?nvgA!RGC

xhZ4N2mAm87KG++&F!2BLoCZ;k(v)uN7@IT>`!gA3KU@It+`)tQSjv;^|K!{TW7o z#?YU!@t6)#x=V)+7Eje_PBErtIj4{jQ(t03m+NZ0JC@I1m~2)oFQ0F4h85?E^5l~s zbXj53g)Ewi&ByMObYY`3&I4@aYVbrtEc)S&Jyb&(E^991C3X{$Q^fHQjG{s7l^h+?uIFG0oAZvaYA>BTC*{_JOh9^t;g(7lD`PVOm_B zAZsZ47!$W~qkW7usjH9X1M(UwfOVfvXh*Sh6;_!UjrG+PJad)5(waVp?SDrn%=E=l zdZst%zJoSGK+xQ-t-K0N-JH^$@YjJ~2E6N9u61mHoEHgkNJlL5WkPxu>r%%?1_+wM ze{k~?J_FnYK8Nr(a;+n(On_iqe~zYo7JhutSYAmXAZQA|c2eLxdMD3oH<{$XI}&S$B{^2=|=RnCQ(qUTN*k93=(kFoaG_Ya%@vvc{3E3AJc|Go&BNdFwm5BmaP5l7}bgXd3c=yK6Y-U zO0a|e6w;r)_~R(1+OdTGyNv#9qB?=Xa{atqHdF3S)`yb0%j-lvmTA=Tr13njk<>}+ z%JbUeMC@K#cuYJoMi--Y%Pzc5;CA89WBOQ?(>V{Y#EsxdqgBr1rw~GMSz}-{S4f^! zD1UG!o#MuL67iVNYUy*?*G9>cGCWI^ksqa+KU4|Y#{Ih^Q>&VotN955D)4I zr`uO88n7RXw+2cXAZQ9-=keK{sg@Ex9v?d{(k;wcDyXP>I6aETpULWGBVg&f$UZPu z559R#D}5dZoV+1(nyjVhV=N!^iLylv)~u6$ZMML>H$B&YoEs1Qhm&~*np;Ra4SDjFLPc||@&=lVBTc;NZ zp9K62!eg9d`Fdm`7`M1HH0`s!K~;>kn-l_qrtn|g9o3$KhwvNmv2}ss1|D`h7%kfn zRD;WJeFOUXB{s%bR`^ZCA{Fqn!g<#E?5QGnTA+`ykkBXM79Q5q*xK003wkL~jrW^gf3j8o3eVJ|T zA{K3=+tkWjws>{by`{1{|L9cynBLoCZ;aB~6vY6JOV}XwV-idJ%)l1*sX8o_~ga}RfhU}dY zHZm~K6#e*wXFnz7(?E|Ql;5TE8$EZ>W2%0iajk=>k>1-(BkCSEsNGbxHJTQI7)^(3jYp zCAuba#20l`0hPvarsv@Y1HR@Z7d@8R^x(3lEaeGZmJwQsp8bP1XFI;yBWUq+S5Be8 znF7_%k!2?5Sx3}YS?KCJ>WFh%;JnK|o@OHh1Wn=p7rX2L;im$BmGBs6BOZ_Qum@qf zPPnQW{#`5+jVJ$72nd?nwS!mtzAxldCj1QG6B2-*o@eFYg_(T0M2K}dKj9YNO0yCB z@o5_wAZQ9-x5_#GQ&EJUf{(5KOAzshP5grH4J;Or4ztuAPm6$2I)#lfYJ}fnL4`fc zv~1O68TRNiOjprZEF5D|{d8f~ifUV%FBXJ(+OQKXXt6LG`X7Y;jy)8Yh(>&SjQ*UW z;6%`FjQkJ5`7E1TtdDrtC-=&5;>p*>~JDIqEi{yYvasaI4c0`Z^;FzUqPpm~idmUv>hU&xl)g56kk7HJz6}@JX zbO8DoPScnAEo-pRaXRp`E$X{U*M{Kmo&aV&^V$f~e;)MDPJ{yn-9X)pql>^uI9Qoy ztx~Ja76FH=&c<8`Fx*a_4mf<6XH8i(QRv?ieT+F#s>thROAi|}#RhnOVFRwe0G_Cq z9zlgI8~X1g{R=37;9|G)JjD(ID`b#}oq*1s!@`Y?>gZ1VYt|@5+}Tol=lnb?eaM$Q zJkZ0Vt0#MK*#;JAJgxT8cwyVcZIRvgtqd~#BB*`>s#}Rp?*m)v zW7gEzYkeb&d)l-VjsWIa`_`sQ)1%M5y85z@F#Tj!@mXECSUkR0K%xJGzZAVQ@E@$>W4e2LBh+k76$43GX{o_!?iCUJT}OF3H0?r~30(2CYMv3|&9j zW3+8;9b07lrk3K6tib;MefT0eqMhodoWOW7K2@nS{_4wUXQ`jFqk!t{ERDdJJQI{ zu-{=--U41*tNYlv`*maa`B@ojU7mGrOq4Jd^{Hyphg-7Mv+U*Oy71K{e124cvxi$h zWf%LI^xp*iy`=vn(jSB1w-!i$T-OP;lN5Nk4_Px~Gr-T;VmOZ@C(jy`9x3%lA7e8> zcXq3x|ZpRFoAE#2H z!6zeC?6eAKsyH@otgeb*>?qrB#X2WFLfRgE#@X}{u``)9{Y4l0IWw2X&ZY1hcTl2j zhyIsI{}W_;n8@uMKqq?OlTjPz@I(t}tXNjGQTxP{p-*96M1^XNF5-p^q^i$UeeYC9KC^y8d^~ zEp_5&ZkGT)+BM~ATBq-U{>j6zPT!qp^*SH`xl6|;&T0hJB7>oFJ4OJb=x`s2#LjR223)@p37eW8A!x2lTDV7jHc&!7) z5@L)E6nPvlmO|J=D|GE!rr#--=sfYH_Rjn~D{jKG;L?;k-hYS2c5ZvIlUl!1ZfcIET9`SY_H{pdkH(R1*zl|^xbXu-if`m=;qv?Ly;7mlOhmrY>XJt#SOkz)|2=5gy~5QKtY2 z5w7#!6MR1Q`>gFo8yO&I3ctDT_x$@hrND0@d`X_QP(FMO2Eq6`P)kkw_pxPjQ^!jo zAZQAoo|IES#yJ7}_z}P##{|+}ZUhseOh=5@wEuv44DF>15Hz=|falfXg`N4?^;2XV z!k^5u;t$Gl55i`hzDm>n18>ls$8CgwpecNP%XOO4mhh|bv6W3}i?qV8CDWfZl(xv} z4XWk`&a(Gdb7TH-HB?g~&oYq|9tS*qLEW^pMxPqGp0ba~vRL+#aj%|LzVFG1NbCx1%t&)MPv=>a7Io+I1bTF_=|Qp$w=>s!HZrvdDO9h2 zWUG@LdKs$E8wH0hBZtD^ZU>Bn?^TjR;q4r?7!Pd!zge15HrtoqG;;fkG4XCaJoGNF z!{}p_P1#4-KZo`BhgE?Ayny;OcYt&#`uy{%poFCw-vRa&cD^bQ8a8=z%U-I(WR>}& zFtc8wdrw)xK4AA6*ZA6d z7)9g*gMPZDPM0z;(A=&by`BcDj{L_*kPiWpzSrWMH5v z`d82Q+D1Y9LmVhX{PS#*vFJRHeY^RIV(LSe6S|VJ3s0w(EW&t# z>06R%ILH3U$z=SSxPXado*Qs2j>>V@Hf1-AHR43gJk4063i`-W1_qikPw3Yz_zRnf zJ{KQbYbaTfOl}7f0LKdP@L8>wS_e$4#jLrp|L<3LMI?DBhOSQ>B{p|jqmQwxViewg zPAi_L0{CKWPY0U&`sZU$maJ2T$J;9KVSyms0@=bk{CPq%Z_ph^Et5h((3E-7I`!qZ zI-$TXO9eh8-#UiKFGm>X{eqEACzf7yo@;Wjmy`%_)$+co9R73OiieoF_M1S6sU zLFn)3Uy9sCcDeDm9%*|Wi}?gO2?oluek`JyKA2ygN$s8C`PTk(U3g}qM{}DVTo%Ww z8FPW}9-nVm3)MFi{XK-JMZ_`vW{m}`3@abwG^j|Ut(^YJ~6@{;F@v76D z;aqryD%blZKJ)5{DwTqkXmQ_y-S?)A3=A~4YcG%ci1zVsQB8{jeHZ9143mx*m2)}> zZ|Q_x27Uhk8yOgAiavHj`yYuu81%zLkI%PG$sZpIZo&2&`^kqjdQ0}?2AwWtV4x}b zTb;)~M4n9mefd}<)fP%Bm>&n?DXHM`acX55*b%*1<~)5kzob&09h7gKojO8#7JcU1 z^btulg*`G*7y5Zt*uGQKJC~DnhCu%V9A&0|QOb$J}DAqsg4;#rW7c4&S+R zgiuvLEpSr?dz`2iI|l!Us_->?d51n?{a^SxTutWE{7yvC-kI@yGT(qMJ8in~vap$j z8>=k8A|s3+J?zd$$UMVw0Vl{j!}6^|*)s59f+jlsJHtFT<7{MLpt)W9cv?Bm{5F#4 zBSFs_2l|M7Yo7Y@O)GYDkWR?c=&f1f_ibcgpegzacB&51Q$RmJbPN+(A}1>l4EG%{ z=;w`~k}@#R6y4EocpFM9qMyUZj)PPycT#R40dVpz-`b^a7}^BPf|;zvRQ+Q73Z`&` zhg?vWku1{+eT?G|M!_s)b%y9bzdJx-`^yJf9HX6(QP4jm4f;n=R3dorBD8#K|`jsDXSsq7fzIAZ^ zaA_y>F*b$tIlXO7)?~bHC*MvxpI8LYyQAMAilQ{=zcCFH%eZ`Nl>AlH;MT0xL7mY2 z9$)!s!(4ya$iP5zyY}-8Sa)gd4Wg%mo)5ZfLcTRu{yKXw2>;UwT@3oRWi~P}&=mdd zZ|Aor`XtZ~5q)C5wN%{)wPD+gyOGZ|dRum%aStbDV4x}bM?==EqkUtdhmJ>an?S`a zioysHx$}UvbHr~mq z8843TEq6Fh*ga_Zd#x!dr$F~@(A_Ge#6{@fUVjzar5gQ1_SEmX?otK@ znxY3sI;sLgLz0)b!;2rXGfQr5FNwBoRt$82*#SWlR>{`Y|%;?7-)+A zN#!fmDFujLLWV+!;%DEfFz=_rjMO--j+X_F*w0y(@s!E0Fsl^EfaO;t$rM1J;kt2* z!aRlbG`4_!DKKgno;(QaG+z7;|L$ux^q-iH!aN_PQ@zBs9cx-o$4?a2;rF8L@T-r; z?UfV)f~N4T=5%?BZgm#`KM!~p#yPi8Zc-D%xZj$mS-(A7aZVpw$^b!gyAE*Q&OQHh z2lDMg;CB)p<4ja93~Q=SbQHbj4WK-B|P6x&kM^uqIzIRQeWuj5WXP!`J+6OIhFQ`n7%eAz#IgTGBxC zZIjmy@}mWdpnoa!cO+60BX#k9M@nKO_nH{7WDYnNv}R3f>4WvG$h`Ji9dCDp)E@GrVVBUDFXvddA8QBzkeqBa?ocH9m5<}?@R*0D8Dah^p32G;YcY115MFCYk0&< z^cA4*B07c{IA7`xg5lN<2EEvL=pbcapegzpXRSFDl`BEtjgKj6+>QY^QE^JC;viaM z%PUU6ASz;W@6}D!PHgX}AQCBt*VthXVC@hYmFQ!v3uGT2l^@n*(Y1Bp%3F=76u?D^ zkriobSq1&aO@jVIDJl^>Zs%NzNI6Qu1Pr6Cthagh(mDO&!V4%5O2YLCqe?iS>G9`TnKDN$Lfq=a{ z0#F!?7MGyuL}+oPge4!MYhM zn6!1qa#qHsb0ccb$Pam0K@a0wO|l1<{Y$I}oAjAqyfzzJZ-&;H(Ar|;P1qg3tZ)MFj_NPEFCZOLm z?LWe=3>tSaQV0l|!pF?t@EjGLoxo>I!Ce*t$L(A|w^}KsC_0GLf_)KuyJus7K+3tB zIa})2;x~5*Pdxlsi&zRI8Y~NgS=T9$;B73UDUc9gh3aPg!+?%YWIc>P@~aIhkYKc# zlYSL}gf2!P>C1)>r!v=GUH!RjjRGw^ow+XSZgRyQsK0y)T(LXfn!HM`*~r{?>xku= zr#rKiSvE32(3C6UN7r&v>B$5B5aDz4ttkWL>2e;+M*KB!X~Kr=K+FJ6^Ssfq|yzA!Ao>B+nLtzI`e@8@d!uh550zBG1C( zx#|qxhXFmYl{I@vAMUg`kgif%cmi*6%C{Cx`$c#bT^_dSB811;Ew|`GpJy*!#~C%j z>)&wkU^32LsJ~|_j8l|v?NhHA?ZS4vrV~0<@hz&lvipCuk%57x=)HTLdyVM(KrbLV zhKVbcH(DSV-y7~~&`%kQSt$bp&FwnO>(7vsA=BxpP%-Ei@UbJ6<`m>Deu$2i4ZHHK z`SD^g8!&_4W|78|irL?(Wy3K#8X*nu#6=_}wx-D=2R)1(Guea7+OnUz>V|pXZADwT z1K-aQsvn*B@Z>vjG~y6cpEM0o6Gu^lSaFkoQGqCQ5K^nu1B8DI9oHy*OrNXy+Fymv z{(S3NIdCg;jkf8-W2aqJ=6=pbsA5rEgvW6(-%6gR@{8@e3ilJgr7%m3FQ-b z4!XqaO3E%gVH236k1l(t{A3iziQwrrsmm9XuxD@qCvgE*A|)&m3eP9#PZ}jGvUx`N zEFLhw?qI2(>tp&Y-&B5;VDnM!mbOEmFKqgV1nb43^K_xS&q9Ln3Lq}R<3RKb$^N;# zOTJAPA&h5H#p!O9u$!1X1n=m|zxbPQ&cP_BDQM0jVb$;PP+)A=`9Eke*@La> zYa;^$P2u-CE4QcF<0A0=X99mA-`XJG84(PEahktERbRySWEE%WbSVP^&FwnM%R$Z7 z_ufME%b+I`{ZhWg)FvPZvvopcgP!@WjSLJlMW25%_%pgCCwdA#wo)iw;7>RCz(aAg zKt06@?Adm#rEz*ETAX9__w0FWVNM++ZpquBk8$2b_W3v0@=y`h37zzy2 zcdn;}@np(c+TZQVgYBjj?%Lz7!Us`Emyau>8cF)>A<@% z&I0vzd_p{{Bc^Nd|1nFNU?T$rP2s22$?Zq@{sq|OB|OHNu1=eSkggNvY1)5`-hXJ273;56iYd208a03v+SlU#1z*ieSVd)R+V@nwzXbN9@_@NH; z1di~-@v&n%1wLXPzjsB^kF2;EC03RJqkj_XeMBG37q2|}MZ9{PFj7e5QNB%@gDyvH zy8KJ{bDVH&vBIi?z&Lo%+#u_ch#KCS@yeZTQv70&CispM*KkrISq;Asoyq8jEos zk1yKP9?%8 z0Y4xMF*>xsT8Yxn!<-O3bwtpEzS{gbi`Zo&0|d?OD&;X+tL?logdYZc0^u=Eq1r40 zVYg1WN6`7upJP%pZt@72x)xIRtjE3&l$uy%d>B<+p2jl9y>x4fw%YWew z`n0o+5D+wFn%}P6HH2=l2p=^US+$I^3c2K__{VQ?QEDOOLjPN8eZiuPeOccf3}3vd z)Iu1oF9)JeD_u|7N95KjmTD|a`)}pXJXNj=8{ynlffYaH$MPyNOtX_BVx61{F0>`dm>T>2%56_&#!LhuWd^M zzKrnW3akwEQhgANd@0jh`lUChw=thcAs}dO*KwY!BfOhhk@n+(kDdoS#ulrS%A^bsG=-0RHKz|1eZmjN$By}wOo(~6a}OmGk}NtCZ^govUBKv1 zWV83^X7nri!V^F2d6EBO(gZn}wVJ{c;e*|}0!)zbP>$MMYacL>rm(ukBFu03uL5ao zfwgr^j9B=zN1sP^6=k1)izFWE!WYLnKEhuhIR*NkgfE{^TS5?y*4TgaLYXPwXeX|I>l0rB=^f>VJ-YbC?>_p(bD|rV;oS> z6S>2i%)LRsHs7SHng929pXWa=JPrDX&4+n#As5u)ELh+%oe-v3--GwU8XK!p2nd=o zPtV`};BPR@0De8;XBJq?)!Y0DQA_8q*R20Fi%hkV0fMIR58SwrPt(>c;5Xo7$0Eu! z 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

=pMh^f>_Oyt(mN6u_>!5-hz<8 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

VP0661#EdXI<_B~7`= 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%0D?5M>JCb0&|0z)lm{;y)yhLZ$O1t!J{$p- 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

^@(UWnE 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�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

t}7_UAxRk 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

iZB>J%U7CQLBKiyCK8aaHqV=`+wi9N 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%0Dvk}(e6=hMf(RIAn8^s+Db+vY#N}( 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 vA0LU5Rw&vxJ)8Y{K67X`2!j*>tfTssDy-`49J LBase compaction. + +define +L0.3 + 000011:a.SET.18-d.SET.19 + 000012:g.SET.20-j.SET.21 +L0.2 + 000009:f.SET.14-j.SET.15 + 000010:r.SET.16-t.SET.17 +L0.1 + 000007:b.SET.10-d.SET.11 + 000008:e.SET.12-j.SET.13 +L0.0 + 000003:a.SET.2-d.SET.3 + 000004:f.SET.4-j.SET.5 + 000005:l.SET.6-o.SET.7 + 000006:p.SET.8-x.SET.9 +L6 + 000001:a.SET.0-i.SET.0 + 000002:m.SET.0-w.SET.0 +---- +file count: 10, sublevels: 4, intervals: 13 +flush split keys(5): [d, g, j, r, t] +0.3: file count: 1, bytes: 256, width (mean, max): 1.0, 1, interval range: [5, 5] + 000012:[g#20,1-j#21,1] +0.2: file count: 2, bytes: 512, width (mean, max): 2.0, 2, interval range: [0, 5] + 000011:[a#18,1-d#19,1] + 000009:[f#14,1-j#15,1] +0.1: file count: 3, bytes: 768, width (mean, max): 1.7, 3, interval range: [1, 10] + 000007:[b#10,1-d#11,1] + 000008:[e#12,1-j#13,1] + 000010:[r#16,1-t#17,1] +0.0: file count: 4, bytes: 1024, width (mean, max): 2.0, 3, interval range: [0, 11] + 000003:[a#2,1-d#3,1] + 000004:[f#4,1-j#5,1] + 000005:[l#6,1-o#7,1] + 000006:[p#8,1-x#9,1] +compacting file count: 0, base compacting intervals: none +L0.3: g---------j +L0.2: a---------d f------------j +L0.1: b------d e---------------j r------t +L0.0: a---------d f------------j l---------o p------------------------x +L6: a------------------------i m------------------------------w + aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx + +pick-base-compaction min_depth=3 +---- +compaction picked with stack depth reduction 4 +000004,000008,000009,000012,000003,000007,000011 +seed interval: g-j +L0.3: g+++++++++j +L0.2: a+++++++++d f++++++++++++j +L0.1: b++++++d e+++++++++++++++j r------t +L0.0: a+++++++++d f++++++++++++j l---------o p------------------------x +L6: a------------------------i m------------------------------w + aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx + +# Example 2. Left half of the keyspace compacting. Select the "next best" +# compaction. + +define +L0.3 + 000011:a.SET.18-d.SET.19 base_compacting + 000012:g.SET.20-j.SET.21 base_compacting +L0.2 + 000009:f.SET.14-j.SET.15 base_compacting + 000010:r.SET.16-t.SET.17 +L0.1 + 000007:b.SET.10-d.SET.11 base_compacting + 000008:e.SET.12-j.SET.13 base_compacting +L0.0 + 000003:a.SET.2-d.SET.3 base_compacting + 000004:f.SET.4-j.SET.5 base_compacting + 000005:l.SET.6-o.SET.7 + 000006:p.SET.8-x.SET.9 +L6 + 000001:a.SET.0-i.SET.0 + 000002:m.SET.0-w.SET.0 +---- +file count: 10, sublevels: 4, intervals: 13 +flush split keys(5): [d, g, j, r, t] +0.3: file count: 1, bytes: 256, width (mean, max): 1.0, 1, interval range: [5, 5] + 000012:[g#20,1-j#21,1] +0.2: file count: 2, bytes: 512, width (mean, max): 2.0, 2, interval range: [0, 5] + 000011:[a#18,1-d#19,1] + 000009:[f#14,1-j#15,1] +0.1: file count: 3, bytes: 768, width (mean, max): 1.7, 3, interval range: [1, 10] + 000007:[b#10,1-d#11,1] + 000008:[e#12,1-j#13,1] + 000010:[r#16,1-t#17,1] +0.0: file count: 4, bytes: 1024, width (mean, max): 2.0, 3, interval range: [0, 11] + 000003:[a#2,1-d#3,1] + 000004:[f#4,1-j#5,1] + 000005:[l#6,1-o#7,1] + 000006:[p#8,1-x#9,1] +compacting file count: 7, base compacting intervals: [0, 6] +L0.3: gvvvvvvvvvj +L0.2: avvvvvvvvvd fvvvvvvvvvvvvj +L0.1: bvvvvvvd evvvvvvvvvvvvvvvj r------t +L0.0: avvvvvvvvvd fvvvvvvvvvvvvj l---------o p------------------------x +L6: a------------------------i m------------------------------w + aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx + +pick-base-compaction min_depth=3 +---- +no compaction picked + +pick-base-compaction min_depth=2 +---- +compaction picked with stack depth reduction 2 +000006,000010,000005 +seed interval: r-t +L0.3: gvvvvvvvvvj +L0.2: avvvvvvvvvd fvvvvvvvvvvvvj +L0.1: bvvvvvvd evvvvvvvvvvvvvvvj r++++++t +L0.0: avvvvvvvvvd fvvvvvvvvvvvvj l+++++++++o p++++++++++++++++++++++++x +L6: a------------------------i m------------------------------w + aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx + +# Example 3. The same as Example 2, except there is now an additional file in +# LBase that overlaps with the [l,o] file in L0.0. + +define +L0.3 + 000011:a.SET.18-d.SET.19 base_compacting + 000012:g.SET.20-j.SET.21 base_compacting +L0.2 + 000009:f.SET.14-j.SET.15 base_compacting + 000010:r.SET.16-t.SET.17 +L0.1 + 000007:b.SET.10-d.SET.11 base_compacting + 000008:e.SET.12-j.SET.13 base_compacting +L0.0 + 000003:a.SET.2-d.SET.3 base_compacting + 000004:f.SET.4-j.SET.5 base_compacting + 000005:l.SET.6-o.SET.7 + 000006:p.SET.8-x.SET.9 +L6 + 000001:a.SET.0-i.SET.0 + 000013:j.SET.0-l.SET.0 + 000002:m.SET.0-w.SET.0 +---- +file count: 10, sublevels: 4, intervals: 13 +flush split keys(5): [d, g, j, r, t] +0.3: file count: 1, bytes: 256, width (mean, max): 1.0, 1, interval range: [5, 5] + 000012:[g#20,1-j#21,1] +0.2: file count: 2, bytes: 512, width (mean, max): 2.0, 2, interval range: [0, 5] + 000011:[a#18,1-d#19,1] + 000009:[f#14,1-j#15,1] +0.1: file count: 3, bytes: 768, width (mean, max): 1.7, 3, interval range: [1, 10] + 000007:[b#10,1-d#11,1] + 000008:[e#12,1-j#13,1] + 000010:[r#16,1-t#17,1] +0.0: file count: 4, bytes: 1024, width (mean, max): 2.0, 3, interval range: [0, 11] + 000003:[a#2,1-d#3,1] + 000004:[f#4,1-j#5,1] + 000005:[l#6,1-o#7,1] + 000006:[p#8,1-x#9,1] +compacting file count: 7, base compacting intervals: [0, 6] +L0.3: gvvvvvvvvvj +L0.2: avvvvvvvvvd fvvvvvvvvvvvvj +L0.1: bvvvvvvd evvvvvvvvvvvvvvvj r------t +L0.0: avvvvvvvvvd fvvvvvvvvvvvvj l---------o p------------------------x +L6: a------------------------i j------l m------------------------------w + aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx + +pick-base-compaction min_depth=2 +---- +compaction picked with stack depth reduction 2 +000006,000010 +seed interval: r-t +L0.3: gvvvvvvvvvj +L0.2: avvvvvvvvvd fvvvvvvvvvvvvj +L0.1: bvvvvvvd evvvvvvvvvvvvvvvj r++++++t +L0.0: avvvvvvvvvd fvvvvvvvvvvvvj l---------o p++++++++++++++++++++++++x +L6: a------------------------i j------l m------------------------------w + aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx + +# Example 4. Intra-L0 compactions. + +define +L0.3 + 000011:a.SET.18-d.SET.19 + 000012:g.SET.20-j.SET.21 base_compacting +L0.2 + 000009:f.SET.14-j.SET.15 base_compacting + 000010:r.SET.16-t.SET.17 base_compacting +L0.1 + 000007:b.SET.10-d.SET.11 + 000008:e.SET.12-j.SET.13 base_compacting +L0.0 + 000003:a.SET.2-d.SET.3 + 000004:f.SET.4-j.SET.5 base_compacting + 000005:l.SET.6-o.SET.7 + 000006:p.SET.8-x.SET.9 base_compacting +L6 + 000001:a.SET.0-i.SET.0 + 000002:m.SET.0-w.SET.0 +---- +file count: 10, sublevels: 4, intervals: 13 +flush split keys(5): [d, g, j, r, t] +0.3: file count: 1, bytes: 256, width (mean, max): 1.0, 1, interval range: [5, 5] + 000012:[g#20,1-j#21,1] +0.2: file count: 2, bytes: 512, width (mean, max): 2.0, 2, interval range: [0, 5] + 000011:[a#18,1-d#19,1] + 000009:[f#14,1-j#15,1] +0.1: file count: 3, bytes: 768, width (mean, max): 1.7, 3, interval range: [1, 10] + 000007:[b#10,1-d#11,1] + 000008:[e#12,1-j#13,1] + 000010:[r#16,1-t#17,1] +0.0: file count: 4, bytes: 1024, width (mean, max): 2.0, 3, interval range: [0, 11] + 000003:[a#2,1-d#3,1] + 000004:[f#4,1-j#5,1] + 000005:[l#6,1-o#7,1] + 000006:[p#8,1-x#9,1] +compacting file count: 6, base compacting intervals: [3, 6], [9, 12] +L0.3: gvvvvvvvvvj +L0.2: a---------d fvvvvvvvvvvvvj +L0.1: b------d evvvvvvvvvvvvvvvj rvvvvvvt +L0.0: a---------d fvvvvvvvvvvvvj l---------o pvvvvvvvvvvvvvvvvvvvvvvvvx +L6: a------------------------i m------------------------------w + aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx + +pick-intra-l0-compaction min_depth=2 +---- +compaction picked with stack depth reduction 3 +000011,000007,000003 +seed interval: b-d +L0.3: gvvvvvvvvvj +L0.2: a+++++++++d fvvvvvvvvvvvvj +L0.1: b++++++d evvvvvvvvvvvvvvvj rvvvvvvt +L0.0: a+++++++++d fvvvvvvvvvvvvj l---------o pvvvvvvvvvvvvvvvvvvvvvvvvx +L6: a------------------------i m------------------------------w + aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx + +# Regression test for cockroachdb/cockroach#101896. We must return +# errInvalidL0SublevelOpt in any case where a new L0 file is being AddL0File'd +# with a largest sequence number below an existing file in the same interval. + +define +L0 + 000004:a.SET.2-e.SET.3 + 000006:a.SET.7-b.SET.8 + 000007:d.SET.12-f.SET.12 +---- +file count: 3, sublevels: 2, intervals: 5 +flush split keys(2): [b, e] +0.1: file count: 2, bytes: 512, width (mean, max): 1.5, 2, interval range: [0, 3] + 000006:[a#7,1-b#8,1] + 000007:[d#12,1-f#12,1] +0.0: file count: 1, bytes: 256, width (mean, max): 3.0, 3, interval range: [0, 2] + 000004:[a#2,1-e#3,1] +compacting file count: 0, base compacting intervals: none +L0.1: a---b d------f +L0.0: a------------e + aa bb cc dd ee ff + +# Note that 000006 will bump the sublevel for the incoming file to 2. We +# should still realize that it's slotting below 000007 and return an error. + +add-l0-files + 000015:a.SET.9-g.SET.10 +---- +pebble: L0 sublevel generation optimization cannot be used + +# Fully-regenerated L0 sublevels allow us to pick an intra-L0 compaction that +# does not violate sublevel ordering. + +define +L0 + 000004:a.SET.2-e.SET.3 + 000006:a.SET.7-b.SET.8 + 000007:d.SET.12-f.SET.12 + 000015:a.SET.9-g.SET.10 +---- +file count: 4, sublevels: 4, intervals: 6 +flush split keys(2): [b, e] +0.3: file count: 1, bytes: 256, width (mean, max): 2.0, 2, interval range: [2, 3] + 000007:[d#12,1-f#12,1] +0.2: file count: 1, bytes: 256, width (mean, max): 5.0, 5, interval range: [0, 4] + 000015:[a#9,1-g#10,1] +0.1: file count: 1, bytes: 256, width (mean, max): 1.0, 1, interval range: [0, 0] + 000006:[a#7,1-b#8,1] +0.0: file count: 1, bytes: 256, width (mean, max): 3.0, 3, interval range: [0, 2] + 000004:[a#2,1-e#3,1] +compacting file count: 0, base compacting intervals: none +L0.3: d------f +L0.2: a------------------g +L0.1: a---b +L0.0: a------------e + aa bb cc dd ee ff gg + +# Exclude the d-f file through earliest_unflushed_seqnum. + +pick-intra-l0-compaction min_depth=2 earliest_unflushed_seqnum=11 +---- +compaction picked with stack depth reduction 3 +000015,000006,000004 +seed interval: a-b +L0.3: d------f +L0.2: a++++++++++++++++++g +L0.1: a+++b +L0.0: a++++++++++++e + aa bb cc dd ee ff gg + +pick-intra-l0-compaction min_depth=2 +---- +compaction picked with stack depth reduction 3 +000015,000007,000006,000004 +seed interval: a-b +L0.3: d++++++f +L0.2: a++++++++++++++++++g +L0.1: a+++b +L0.0: a++++++++++++e + aa bb cc dd ee ff gg diff --git a/pebble/internal/manifest/testdata/level_iterator b/pebble/internal/manifest/testdata/level_iterator new file mode 100644 index 0000000..dca3e8e --- /dev/null +++ b/pebble/internal/manifest/testdata/level_iterator @@ -0,0 +1,128 @@ +define +[ ] +---- + +iter +first +last +seek-lt a +seek-lt z +seek-ge a +seek-ge z +---- +. +. +. +. +. +. + +define +[ a.SET.1-b.SET.2 ] +---- + +iter +last +---- +000001:[a#1,1-b#2,1] + +iter +first +next +prev +prev +---- +000001:[a#1,1-b#2,1] +. +000001:[a#1,1-b#2,1] +. + +iter +seek-ge a +seek-ge b +seek-ge c +---- +000001:[a#1,1-b#2,1] +000001:[a#1,1-b#2,1] +. + +iter +seek-lt a +seek-lt b +seek-lt z +---- +. +000001:[a#1,1-b#2,1] +000001:[a#1,1-b#2,1] + +define +[ b.SET.1-c.SET.2 ] +---- + +iter +seek-ge a +seek-ge d +seek-lt a +seek-lt z +---- +000001:[b#1,1-c#2,1] +. +. +000001:[b#1,1-c#2,1] + + +define +a.SET.1-b.SET.2 [ c.SET.3-d.SET.4 e.SET.5-f.SET.6 ] g.SET.7-h.SET.8 +---- + +iter +first +prev +last +next +---- +000002:[c#3,1-d#4,1] +. +000003:[e#5,1-f#6,1] +. + +iter +seek-ge a +seek-ge b +seek-ge c +seek-ge h +prev +---- +000002:[c#3,1-d#4,1] +000002:[c#3,1-d#4,1] +000002:[c#3,1-d#4,1] +. +000003:[e#5,1-f#6,1] + +iter +seek-lt b +next +seek-lt a +next +seek-lt z +---- +. +000002:[c#3,1-d#4,1] +. +000002:[c#3,1-d#4,1] +000003:[e#5,1-f#6,1] + +define +a.SET.1-b.SET.2 c.SET.3-d.SET.4 e.SET.5-f.SET.6 g.SET.7-h.SET.8 [ ] +---- + +iter +seek-ge cat +seek-lt cat +first +last +---- +. +. +. +. diff --git a/pebble/internal/manifest/testdata/level_iterator_filtered b/pebble/internal/manifest/testdata/level_iterator_filtered new file mode 100644 index 0000000..066207c --- /dev/null +++ b/pebble/internal/manifest/testdata/level_iterator_filtered @@ -0,0 +1,537 @@ +define +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +---- + +iter key-type=points +seek-ge a +seek-ge m +seek-ge n +seek-ge o +seek-ge p +---- +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +. +. + +iter key-type=ranges +seek-ge a +seek-ge m +seek-ge n +seek-ge o +seek-ge p +---- +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +. +. +. +. + +iter key-type=points +seek-lt a +seek-lt b +seek-lt c +seek-lt j +seek-lt k +seek-lt l +seek-lt m +seek-lt n +seek-lt o +seek-lt p +---- +. +. +. +. +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] + +iter key-type=ranges +seek-lt a +seek-lt b +seek-lt c +seek-lt j +seek-lt k +seek-lt l +seek-lt m +seek-lt n +seek-lt o +seek-lt p +---- +. +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] + +iter key-type=points +seek-lt a +next +next +seek-ge o +prev +prev +---- +. +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +. +. +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +. + +iter key-type=ranges +seek-lt a +next +next +seek-ge m +prev +prev +---- +. +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +. +. +000000:[a#42,RANGEKEYSET-o#inf,RANGEDEL] seqnums:[0-0] points:[j#0,SET-o#inf,RANGEDEL] ranges:[a#42,RANGEKEYSET-m#inf,RANGEKEYSET] +. + +define +000000:[a#9,SET-b#2,DEL] points:[a#9,SET-b#2,DEL] +000001:[c#9,SET-d#2,DEL] points:[c#9,SET-d#2,DEL] +000002:[e#9,SET-f#2,DEL] points:[e#9,SET-f#2,DEL] +000003:[g#9,SET-g#2,DEL] points:[g#9,SET-g#2,DEL] +000004:[i#9,SET-j#2,DEL] points:[i#9,SET-j#2,DEL] +000005:[k#9,SET-k#2,DEL] points:[k#9,SET-k#2,DEL] +---- + +iter key-type=points +seek-ge a +seek-ge apple +seek-ge b +seek-ge banana +seek-ge c +seek-ge cantalope +seek-ge d +seek-ge dragonfruit +---- +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000001:[c#9,SET-d#2,DEL] seqnums:[0-0] points:[c#9,SET-d#2,DEL] +000001:[c#9,SET-d#2,DEL] seqnums:[0-0] points:[c#9,SET-d#2,DEL] +000001:[c#9,SET-d#2,DEL] seqnums:[0-0] points:[c#9,SET-d#2,DEL] +000001:[c#9,SET-d#2,DEL] seqnums:[0-0] points:[c#9,SET-d#2,DEL] +000002:[e#9,SET-f#2,DEL] seqnums:[0-0] points:[e#9,SET-f#2,DEL] + +iter key-type=points +seek-lt a +seek-lt apple +seek-lt b +seek-lt banana +seek-lt c +seek-lt cantalope +seek-lt d +seek-lt dragonfruit +---- +. +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000001:[c#9,SET-d#2,DEL] seqnums:[0-0] points:[c#9,SET-d#2,DEL] +000001:[c#9,SET-d#2,DEL] seqnums:[0-0] points:[c#9,SET-d#2,DEL] +000001:[c#9,SET-d#2,DEL] seqnums:[0-0] points:[c#9,SET-d#2,DEL] + +iter key-type=ranges +seek-ge a +seek-ge apple +seek-ge b +seek-ge banana +seek-ge c +seek-ge cantalope +seek-ge d +seek-ge dragonfruit +---- +. +. +. +. +. +. +. +. + +iter key-type=ranges +seek-lt a +seek-lt apple +seek-lt b +seek-lt banana +seek-lt c +seek-lt cantalope +seek-lt d +seek-lt dragonfruit +---- +. +. +. +. +. +. +. +. + +define +000000:[a#9,SET-b#2,DEL] points:[a#9,SET-b#2,DEL] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000002:[e#9,SET-f#inf,RANGEKEYDEL] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000003:[g#9,SET-g#2,DEL] points:[g#9,SET-g#2,DEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000005:[k#9,SET-k#2,DEL] points:[k#9,SET-k#2,DEL] +---- + +iter key-type=both +seek-ge a +seek-ge apple +seek-ge b +seek-ge banana +seek-ge c +seek-ge cantalope +seek-ge d +seek-ge dragonfruit +seek-ge e +seek-ge elderberry +seek-ge f +seek-ge figs +seek-ge g +seek-ge guava +seek-ge h +seek-ge huckleberry +seek-ge i +seek-ge incaberry +seek-ge j +seek-ge jujube +seek-ge k +seek-ge kiwi +seek-ge l +---- +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +. +. + +iter key-type=both +seek-lt a +seek-lt apple +seek-lt b +seek-lt banana +seek-lt c +seek-lt cantalope +seek-lt d +seek-lt dragonfruit +seek-lt e +seek-lt elderberry +seek-lt f +seek-lt figs +seek-lt g +seek-lt guava +seek-lt h +seek-lt huckleberry +seek-lt i +seek-lt incaberry +seek-lt j +seek-lt jujube +seek-lt k +seek-lt kiwi +seek-lt l +---- +. +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] + + +iter key-type=points +seek-ge a +seek-ge apple +seek-ge b +seek-ge banana +seek-ge c +seek-ge cantalope +seek-ge d +seek-ge dragonfruit +seek-ge e +seek-ge elderberry +seek-ge f +seek-ge figs +seek-ge g +seek-ge guava +seek-ge h +seek-ge huckleberry +seek-ge i +seek-ge incaberry +seek-ge j +seek-ge jujube +seek-ge k +seek-ge kiwi +seek-ge l +---- +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +. +. + +iter key-type=points +seek-lt a +seek-lt apple +seek-lt b +seek-lt banana +seek-lt c +seek-lt cantalope +seek-lt d +seek-lt dragonfruit +seek-lt e +seek-lt elderberry +seek-lt f +seek-lt figs +seek-lt g +seek-lt guava +seek-lt h +seek-lt huckleberry +seek-lt i +seek-lt incaberry +seek-lt j +seek-lt jujube +seek-lt k +seek-lt kiwi +seek-lt l +---- +. +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] + +iter key-type=ranges +seek-ge a +seek-ge apple +seek-ge b +seek-ge banana +seek-ge c +seek-ge cantalope +seek-ge d +seek-ge dragonfruit +seek-ge e +seek-ge elderberry +seek-ge f +seek-ge figs +seek-ge g +seek-ge guava +seek-ge h +seek-ge huckleberry +seek-ge i +seek-ge incaberry +seek-ge j +seek-ge jujube +seek-ge k +seek-ge kiwi +seek-ge l +---- +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +. +. +. +. + +iter key-type=ranges +seek-lt a +seek-lt apple +seek-lt b +seek-lt banana +seek-lt c +seek-lt cantalope +seek-lt d +seek-lt dragonfruit +seek-lt e +seek-lt elderberry +seek-lt f +seek-lt figs +seek-lt g +seek-lt guava +seek-lt h +seek-lt huckleberry +seek-lt i +seek-lt incaberry +seek-lt j +seek-lt jujube +seek-lt k +seek-lt kiwi +seek-lt l +---- +. +. +. +. +. +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] + +iter key-type=both +first +next +next +next +next +next +next +---- +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +. + +iter key-type=points +first +next +next +next +next +next +---- +000000:[a#9,SET-b#2,DEL] seqnums:[0-0] points:[a#9,SET-b#2,DEL] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000003:[g#9,SET-g#2,DEL] seqnums:[0-0] points:[g#9,SET-g#2,DEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +000005:[k#9,SET-k#2,DEL] seqnums:[0-0] points:[k#9,SET-k#2,DEL] +. + +iter key-type=ranges +first +next +next +next +---- +000001:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#9,RANGEKEYSET-d#inf,RANGEKEYSET] +000002:[e#9,SET-f#inf,RANGEKEYDEL] seqnums:[0-0] points:[e#9,SET-elderberry#2,DEL] ranges:[e#3,RANGEKEYSET-f#inf,RANGEKEYDEL] +000004:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] seqnums:[0-0] points:[incaberry#9,SET-incaettry#9,SET] ranges:[i#9,RANGEKEYSET-j#2,RANGEKEYSET] +. diff --git a/pebble/internal/manifest/testdata/overlaps b/pebble/internal/manifest/testdata/overlaps new file mode 100644 index 0000000..e584991 --- /dev/null +++ b/pebble/internal/manifest/testdata/overlaps @@ -0,0 +1,566 @@ +define +0: + 000700:[b#7008,SET-e#7009,SET] + 000701:[c#7018,SET-f#7019,SET] + 000702:[f#7028,SET-g#7029,SET] + 000703:[x#7038,SET-y#7039,SET] + 000704:[n#7048,SET-p#7049,SET] + 000705:[p#7058,SET-p#7059,SET] + 000706:[p#7068,SET-u#7069,SET] + 000707:[r#7078,SET-s#7079,SET] +1: + 000710:[a#7140,SET-d#inf,RANGEDEL] + 000711:[d#7108,SET-g#7109,SET] + 000712:[g#7118,SET-j#7119,SET] + 000713:[n#7128,SET-p#7129,SET] + 000714:[p#7148,SET-p#7149,SET] + 000715:[p#7138,SET-u#7139,SET] +---- +0.3: + 000704:[n#7048,SET-p#7049,SET] +0.2: + 000700:[b#7008,SET-e#7009,SET] + 000705:[p#7058,SET-p#7059,SET] +0.1: + 000701:[c#7018,SET-f#7019,SET] + 000706:[p#7068,SET-u#7069,SET] +0.0: + 000702:[f#7028,SET-g#7029,SET] + 000707:[r#7078,SET-s#7079,SET] + 000703:[x#7038,SET-y#7039,SET] +1: + 000710:[a#7140,SET-d#inf,RANGEDEL] + 000711:[d#7108,SET-g#7109,SET] + 000712:[g#7118,SET-j#7119,SET] + 000713:[n#7128,SET-p#7129,SET] + 000714:[p#7148,SET-p#7149,SET] + 000715:[p#7138,SET-u#7139,SET] + +# Level 0 + +overlaps level=0 start=a end=a exclusive-end=false +---- +0 files: + +overlaps level=0 start=a end=b exclusive-end=false +---- +3 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] + +overlaps level=0 start=a end=d exclusive-end=false +---- +3 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] + +overlaps level=0 start=a end=e exclusive-end=false +---- +3 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] + +overlaps level=0 start=a end=g exclusive-end=false +---- +3 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] + +overlaps level=0 start=a end=z exclusive-end=false +---- +8 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] +000703:[x#7038,SET-y#7039,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=c end=e exclusive-end=false +---- +3 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] + +overlaps level=0 start=d end=d exclusive-end=false +---- +3 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] + +# The below case relies on exclusive-end changing to false after picking some file. + +overlaps level=0 start=b end=f exclusive-end=true +---- +3 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] + +overlaps level=0 start=g end=n exclusive-end=false +---- +7 files: +000700:[b#7008,SET-e#7009,SET] +000701:[c#7018,SET-f#7019,SET] +000702:[f#7028,SET-g#7029,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=h end=i exclusive-end=false +---- +0 files: + +overlaps level=0 start=h end=o exclusive-end=false +---- +4 files: +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=h end=u exclusive-end=false +---- +4 files: +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=k end=l exclusive-end=false +---- +0 files: + +overlaps level=0 start=k end=o exclusive-end=false +---- +4 files: +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=k end=p exclusive-end=false +---- +4 files: +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=n end=o exclusive-end=false +---- +4 files: +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=n end=z exclusive-end=false +---- +5 files: +000703:[x#7038,SET-y#7039,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=o end=z exclusive-end=false +---- +5 files: +000703:[x#7038,SET-y#7039,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=p end=z exclusive-end=false +---- +5 files: +000703:[x#7038,SET-y#7039,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=q end=z exclusive-end=false +---- +5 files: +000703:[x#7038,SET-y#7039,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=r end=s exclusive-end=false +---- +4 files: +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=r end=z exclusive-end=false +---- +5 files: +000703:[x#7038,SET-y#7039,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=s end=z exclusive-end=false +---- +5 files: +000703:[x#7038,SET-y#7039,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=u end=z exclusive-end=false +---- +5 files: +000703:[x#7038,SET-y#7039,SET] +000704:[n#7048,SET-p#7049,SET] +000705:[p#7058,SET-p#7059,SET] +000706:[p#7068,SET-u#7069,SET] +000707:[r#7078,SET-s#7079,SET] + +overlaps level=0 start=y end=z exclusive-end=false +---- +1 files: +000703:[x#7038,SET-y#7039,SET] + +overlaps level=0 start=z end=z exclusive-end=false +---- +0 files: + +# Level 1 + +overlaps level=1 start=a end=a exclusive-end=false +---- +1 files: +000710:[a#7140,SET-d#inf,RANGEDEL] + +overlaps level=1 start=a end=b exclusive-end=false +---- +1 files: +000710:[a#7140,SET-d#inf,RANGEDEL] + +overlaps level=1 start=a end=d exclusive-end=false +---- +2 files: +000710:[a#7140,SET-d#inf,RANGEDEL] +000711:[d#7108,SET-g#7109,SET] + +overlaps level=1 start=a end=e exclusive-end=false +---- +2 files: +000710:[a#7140,SET-d#inf,RANGEDEL] +000711:[d#7108,SET-g#7109,SET] + +overlaps level=1 start=a end=g exclusive-end=false +---- +3 files: +000710:[a#7140,SET-d#inf,RANGEDEL] +000711:[d#7108,SET-g#7109,SET] +000712:[g#7118,SET-j#7119,SET] + +overlaps level=1 start=a end=g exclusive-end=true +---- +2 files: +000710:[a#7140,SET-d#inf,RANGEDEL] +000711:[d#7108,SET-g#7109,SET] + +overlaps level=1 start=a end=z exclusive-end=false +---- +6 files: +000710:[a#7140,SET-d#inf,RANGEDEL] +000711:[d#7108,SET-g#7109,SET] +000712:[g#7118,SET-j#7119,SET] +000713:[n#7128,SET-p#7129,SET] +000714:[p#7148,SET-p#7149,SET] +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=a end=z exclusive-end=true +---- +6 files: +000710:[a#7140,SET-d#inf,RANGEDEL] +000711:[d#7108,SET-g#7109,SET] +000712:[g#7118,SET-j#7119,SET] +000713:[n#7128,SET-p#7129,SET] +000714:[p#7148,SET-p#7149,SET] +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=c end=e exclusive-end=false +---- +2 files: +000710:[a#7140,SET-d#inf,RANGEDEL] +000711:[d#7108,SET-g#7109,SET] + +overlaps level=1 start=d end=d exclusive-end=false +---- +1 files: +000711:[d#7108,SET-g#7109,SET] + +overlaps level=1 start=g end=n exclusive-end=false +---- +3 files: +000711:[d#7108,SET-g#7109,SET] +000712:[g#7118,SET-j#7119,SET] +000713:[n#7128,SET-p#7129,SET] + +overlaps level=1 start=h end=i exclusive-end=false +---- +1 files: +000712:[g#7118,SET-j#7119,SET] + +overlaps level=1 start=h end=n exclusive-end=true +---- +1 files: +000712:[g#7118,SET-j#7119,SET] + +overlaps level=1 start=h end=n exclusive-end=false +---- +2 files: +000712:[g#7118,SET-j#7119,SET] +000713:[n#7128,SET-p#7129,SET] + +overlaps level=1 start=h end=o exclusive-end=false +---- +2 files: +000712:[g#7118,SET-j#7119,SET] +000713:[n#7128,SET-p#7129,SET] + +overlaps level=1 start=h end=u exclusive-end=false +---- +4 files: +000712:[g#7118,SET-j#7119,SET] +000713:[n#7128,SET-p#7129,SET] +000714:[p#7148,SET-p#7149,SET] +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=k end=l exclusive-end=false +---- +0 files: + +overlaps level=1 start=k end=o exclusive-end=false +---- +1 files: +000713:[n#7128,SET-p#7129,SET] + +overlaps level=1 start=k end=p exclusive-end=false +---- +3 files: +000713:[n#7128,SET-p#7129,SET] +000714:[p#7148,SET-p#7149,SET] +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=k end=p exclusive-end=true +---- +1 files: +000713:[n#7128,SET-p#7129,SET] + +overlaps level=1 start=n end=o exclusive-end=false +---- +1 files: +000713:[n#7128,SET-p#7129,SET] + +overlaps level=1 start=n end=z exclusive-end=false +---- +3 files: +000713:[n#7128,SET-p#7129,SET] +000714:[p#7148,SET-p#7149,SET] +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=o end=z exclusive-end=false +---- +3 files: +000713:[n#7128,SET-p#7129,SET] +000714:[p#7148,SET-p#7149,SET] +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=p end=z exclusive-end=false +---- +3 files: +000713:[n#7128,SET-p#7129,SET] +000714:[p#7148,SET-p#7149,SET] +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=q end=z exclusive-end=false +---- +1 files: +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=r end=s exclusive-end=false +---- +1 files: +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=r end=z exclusive-end=false +---- +1 files: +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=s end=z exclusive-end=false +---- +1 files: +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=u end=z exclusive-end=false +---- +1 files: +000715:[p#7138,SET-u#7139,SET] + +overlaps level=1 start=y end=z exclusive-end=false +---- +0 files: + +overlaps level=1 start=z end=z exclusive-end=false +---- +0 files: + +# Level 2 is empty. + +overlaps level=2 start=a end=z exclusive-end=false +---- +0 files: + +# Test a scenario where an originally exclusive-end must be promoted to +# inclusive during the iterative expansion of L0 overlaps. +# +# 000003 with the f largest bound must be included. + +define +0: + 000001:[a#1,SET-d#2,SET] + 000002:[c#3,SET-f#4,SET] + 000003:[f#5,SET-f#5,SET] +---- +0.2: + 000001:[a#1,SET-d#2,SET] +0.1: + 000002:[c#3,SET-f#4,SET] +0.0: + 000003:[f#5,SET-f#5,SET] + +overlaps level=0 start=a end=b exclusive-end=true +---- +3 files: +000001:[a#1,SET-d#2,SET] +000002:[c#3,SET-f#4,SET] +000003:[f#5,SET-f#5,SET] + +# The below is a verbatim reproduction of the case detected by the +# metamorphic tests in pebble#1459: The above case is already a +# simplified version of the same condition. The verbatim reproduction is +# included for completeness. + +define +0.4: + 000987:[aiinjp@20#4667,SET-fcklu@5#inf,RANGEDEL] + 000988:[fcklu@5#4668,MERGE-glpw@1#inf,RANGEDEL] + 000989:[glpw@1#4662,RANGEDEL-mlgxnog@19#inf,RANGEDEL] + 000990:[mlgxnog@19#4662,RANGEDEL-nwnmqtyvjt@5#inf,RANGEDEL] + 000991:[nwnmqtyvjt@5#4662,RANGEDEL-wmkrrxp@6#inf,RANGEDEL] +0.3: + 000978:[dygfdczcax@15#4609,DEL-vtocgpw@18#4609,DEL] + 000992:[wmkrrxp@6#4657,MERGE-yyquzcd@21#4624,SET] + 000993:[zslykqao@12#4636,SINGLEDEL-zzqwavxgrec@12#4627,DEL] +0.2: + 000981:[fhcykuix@5#4601,MERGE-kiati@10#4595,MERGE] + 000977:[mgksrvk@15#4598,DEL-mgksrvk@15#4598,DEL] + 000982:[nirnrarzktp@12#4600,MERGE-zaowx@3#4602,SET] + 000828:[zzqwavxgrec@12#4092,SINGLEDEL-zzqwavxgrec@12#4092,SINGLEDEL] +0.1: + 000980:[dusu@10#4603,SET-duyeldgvnll@21#4605,SET] + 000973:[ewqqtp@15#4591,RANGEDEL-zaygjmy@1#inf,RANGEDEL] + 000605:[zzqwavxgrec@12#2894,SET-zzqwavxgrec@12#2894,SET] +0.0: + 000910:[abddymplk@20#4370,MERGE-abddymplk@20#4370,MERGE] + 000939:[abvukibeofb@13#4439,SET-abvukibeofb@13#4439,SET] + 000975:[ajoqjxr@16#4578,MERGE-zjyqka@1#4544,DEL] + 000983:[znnoar@20#4604,SINGLEDEL-znnoar@20#4604,SINGLEDEL] + 000535:[zzqwavxgrec@12#2657,SINGLEDEL-zzqwavxgrec@12#2526,SET] +5: + 000971:[acutc@6#4227,SET-zzhra@12#inf,RANGEDEL] +6: + 000806:[gourk@18#0,SET-zzhra@2#0,SET] +---- +0.4: + 000987:[aiinjp@20#4667,SET-fcklu@5#inf,RANGEDEL] + 000988:[fcklu@5#4668,MERGE-glpw@1#inf,RANGEDEL] + 000989:[glpw@1#4662,RANGEDEL-mlgxnog@19#inf,RANGEDEL] + 000990:[mlgxnog@19#4662,RANGEDEL-nwnmqtyvjt@5#inf,RANGEDEL] + 000991:[nwnmqtyvjt@5#4662,RANGEDEL-wmkrrxp@6#inf,RANGEDEL] +0.3: + 000978:[dygfdczcax@15#4609,DEL-vtocgpw@18#4609,DEL] + 000992:[wmkrrxp@6#4657,MERGE-yyquzcd@21#4624,SET] + 000993:[zslykqao@12#4636,SINGLEDEL-zzqwavxgrec@12#4627,DEL] +0.2: + 000981:[fhcykuix@5#4601,MERGE-kiati@10#4595,MERGE] + 000977:[mgksrvk@15#4598,DEL-mgksrvk@15#4598,DEL] + 000982:[nirnrarzktp@12#4600,MERGE-zaowx@3#4602,SET] + 000828:[zzqwavxgrec@12#4092,SINGLEDEL-zzqwavxgrec@12#4092,SINGLEDEL] +0.1: + 000980:[dusu@10#4603,SET-duyeldgvnll@21#4605,SET] + 000973:[ewqqtp@15#4591,RANGEDEL-zaygjmy@1#inf,RANGEDEL] + 000605:[zzqwavxgrec@12#2894,SET-zzqwavxgrec@12#2894,SET] +0.0: + 000910:[abddymplk@20#4370,MERGE-abddymplk@20#4370,MERGE] + 000939:[abvukibeofb@13#4439,SET-abvukibeofb@13#4439,SET] + 000975:[ajoqjxr@16#4578,MERGE-zjyqka@1#4544,DEL] + 000983:[znnoar@20#4604,SINGLEDEL-znnoar@20#4604,SINGLEDEL] + 000535:[zzqwavxgrec@12#2657,SINGLEDEL-zzqwavxgrec@12#2526,SET] +5: + 000971:[acutc@6#4227,SET-zzhra@12#inf,RANGEDEL] +6: + 000806:[gourk@18#0,SET-zzhra@2#0,SET] + +overlaps level=0 start=heacptnep@12 end=kiicbzwtpe@16 exclusive-end=false +---- +13 files: +000973:[ewqqtp@15#4591,RANGEDEL-zaygjmy@1#inf,RANGEDEL] +000975:[ajoqjxr@16#4578,MERGE-zjyqka@1#4544,DEL] +000977:[mgksrvk@15#4598,DEL-mgksrvk@15#4598,DEL] +000978:[dygfdczcax@15#4609,DEL-vtocgpw@18#4609,DEL] +000980:[dusu@10#4603,SET-duyeldgvnll@21#4605,SET] +000981:[fhcykuix@5#4601,MERGE-kiati@10#4595,MERGE] +000982:[nirnrarzktp@12#4600,MERGE-zaowx@3#4602,SET] +000987:[aiinjp@20#4667,SET-fcklu@5#inf,RANGEDEL] +000988:[fcklu@5#4668,MERGE-glpw@1#inf,RANGEDEL] +000989:[glpw@1#4662,RANGEDEL-mlgxnog@19#inf,RANGEDEL] +000990:[mlgxnog@19#4662,RANGEDEL-nwnmqtyvjt@5#inf,RANGEDEL] +000991:[nwnmqtyvjt@5#4662,RANGEDEL-wmkrrxp@6#inf,RANGEDEL] +000992:[wmkrrxp@6#4657,MERGE-yyquzcd@21#4624,SET] + +overlaps level=0 start=acutc@6 end=zzhra@12 exclusive-end=true +---- +18 files: +000535:[zzqwavxgrec@12#2657,SINGLEDEL-zzqwavxgrec@12#2526,SET] +000605:[zzqwavxgrec@12#2894,SET-zzqwavxgrec@12#2894,SET] +000828:[zzqwavxgrec@12#4092,SINGLEDEL-zzqwavxgrec@12#4092,SINGLEDEL] +000973:[ewqqtp@15#4591,RANGEDEL-zaygjmy@1#inf,RANGEDEL] +000975:[ajoqjxr@16#4578,MERGE-zjyqka@1#4544,DEL] +000977:[mgksrvk@15#4598,DEL-mgksrvk@15#4598,DEL] +000978:[dygfdczcax@15#4609,DEL-vtocgpw@18#4609,DEL] +000980:[dusu@10#4603,SET-duyeldgvnll@21#4605,SET] +000981:[fhcykuix@5#4601,MERGE-kiati@10#4595,MERGE] +000982:[nirnrarzktp@12#4600,MERGE-zaowx@3#4602,SET] +000983:[znnoar@20#4604,SINGLEDEL-znnoar@20#4604,SINGLEDEL] +000987:[aiinjp@20#4667,SET-fcklu@5#inf,RANGEDEL] +000988:[fcklu@5#4668,MERGE-glpw@1#inf,RANGEDEL] +000989:[glpw@1#4662,RANGEDEL-mlgxnog@19#inf,RANGEDEL] +000990:[mlgxnog@19#4662,RANGEDEL-nwnmqtyvjt@5#inf,RANGEDEL] +000991:[nwnmqtyvjt@5#4662,RANGEDEL-wmkrrxp@6#inf,RANGEDEL] +000992:[wmkrrxp@6#4657,MERGE-yyquzcd@21#4624,SET] +000993:[zslykqao@12#4636,SINGLEDEL-zzqwavxgrec@12#4627,DEL] diff --git a/pebble/internal/manifest/testdata/version_check_ordering b/pebble/internal/manifest/testdata/version_check_ordering new file mode 100644 index 0000000..9f8f710 --- /dev/null +++ b/pebble/internal/manifest/testdata/version_check_ordering @@ -0,0 +1,302 @@ +# Note: when specifying test cases with tables in L0, the L0 files should be +# specified in seqnum descending order, as the test case input is parsed as the +# inverse of `(*FileMetadata).DebugString`. + +check-ordering +0: + 000001:[a#1,SET-b#2,SET] +---- +OK + +check-ordering +0: + 000002:[c#3,SET-d#4,SET] + 000001:[a#1,SET-b#2,SET] +---- +OK + +check-ordering +0: + 000002:[a#1,SET-b#2,SET] + 000001:[c#3,SET-d#4,SET] +---- +L0 files 000001 and 000002 are not properly ordered: <#3-#4> vs <#1-#2> +0.0: + 000002:[a#1,SET-b#2,SET] seqnums:[1-2] points:[a#1,SET-b#2,SET] + 000001:[c#3,SET-d#4,SET] seqnums:[3-4] points:[c#3,SET-d#4,SET] + +check-ordering +0: + 000008:[k#16,SET-n#19,SET] + 000007:[a#14,SET-j#17,SET] + 000006:[b#15,SET-d#15,SET] + 000005:[i#8,SET-j#13,SET] + 000004:[g#6,SET-h#12,SET] + 000003:[e#2,SET-f#7,SET] + 000002:[a#1,SET-b#5,SET] + 000001:[c#3,SET-d#4,SET] +---- +OK + +# Add some ingested SSTables around the 14-19 seqnum cases. +check-ordering +0: + 000010:[m#20,SET-n#20,SET] + 000009:[k#16,SET-n#19,SET] + 000008:[m#18,SET-n#18,SET] + 000007:[a#14,SET-j#17,SET] + 000006:[b#15,SET-d#15,SET] + 000005:[i#8,SET-j#13,SET] + 000004:[g#6,SET-h#12,SET] + 000003:[e#2,SET-f#7,SET] + 000002:[a#1,SET-b#5,SET] + 000001:[c#3,SET-d#4,SET] +---- +OK + +# Coincident sequence numbers around sstables with overlapping sequence numbers +# are possible due to flush splitting, so this is acceptable. +check-ordering +0: + 000010:[m#20,SET-n#20,SET] + 000009:[k#16,SET-n#19,SET] + 000008:[m#18,SET-n#18,SET] + 000007:[a#15,SET-j#17,SET] + 000006:[b#15,SET-d#15,SET] + 000005:[i#8,SET-j#13,SET] + 000004:[g#6,SET-h#12,SET] + 000003:[e#2,SET-f#7,SET] + 000002:[a#1,SET-b#5,SET] + 000001:[c#3,SET-d#4,SET] +---- +OK + +# Ensure that sstables passed in a non-sorted order are detected. +check-ordering +0: + 000002:[a#1,SET-b#2,SET] + 000001:[a#3,SET-d#3,SET] +---- +L0 files 000001 and 000002 are not properly ordered: <#3-#3> vs <#1-#2> +0.1: + 000002:[a#1,SET-b#2,SET] seqnums:[1-2] points:[a#1,SET-b#2,SET] +0.0: + 000001:[a#3,SET-d#3,SET] seqnums:[3-3] points:[a#3,SET-d#3,SET] + +check-ordering +0: + 000002:[a#3,SET-b#3,SET] + 000001:[a#2,SET-d#4,SET] +---- +L0 files 000001 and 000002 are not properly ordered: <#2-#4> vs <#3-#3> +0.1: + 000002:[a#3,SET-b#3,SET] seqnums:[3-3] points:[a#3,SET-b#3,SET] +0.0: + 000001:[a#2,SET-d#4,SET] seqnums:[2-4] points:[a#2,SET-d#4,SET] + +check-ordering +0: + 000002:[a#3,SET-b#3,SET] + 000001:[a#3,SET-d#3,SET] +---- +OK + +check-ordering +0: + 000002:[a#3,SET-d#5,SET] + 000001:[a#3,SET-d#3,SET] +---- +OK + +check-ordering +0: + 000002:[a#3,SET-d#5,SET] + 000001:[a#4,SET-d#4,SET] +---- +OK + +check-ordering +0: + 000002:[a#5,SET-d#5,SET] + 000001:[a#3,SET-d#5,SET] +---- +OK + +check-ordering +0: + 000003:[a#4,SET-d#6,SET] + 000002:[a#5,SET-d#5,SET] + 000001:[a#4,SET-d#4,SET] +---- +OK + +check-ordering +0: + 000003:[a#0,SET-d#3,SET] + 000002:[a#0,SET-d#0,SET] + 000001:[a#0,SET-d#0,SET] +---- +OK + +check-ordering +1: + 000001:[a#1,SET-b#2,SET] +---- +OK + +check-ordering +1: + 000001:[b#1,SET-a#2,SET] +---- +L1 : file 000001 has inconsistent bounds: b#1,SET vs a#2,SET +1: + 000001:[b#1,SET-a#2,SET] seqnums:[0-0] points:[b#1,SET-a#2,SET] + +check-ordering +1: + 000001:[a#1,SET-b#2,SET] + 000002:[c#3,SET-d#4,SET] +---- +OK + +check-ordering +1: + 000001:[a#1,SET-b#2,SET] + 000002:[d#3,SET-c#4,SET] +---- +L1 : file 000002 has inconsistent bounds: d#3,SET vs c#4,SET +1: + 000001:[a#1,SET-b#2,SET] seqnums:[0-0] points:[a#1,SET-b#2,SET] + 000002:[d#3,SET-c#4,SET] seqnums:[0-0] points:[d#3,SET-c#4,SET] + +check-ordering +1: + 000001:[a#1,SET-b#2,SET] + 000002:[b#1,SET-d#4,SET] +---- +L1 files 000001 and 000002 have overlapping ranges: [a#1,SET-b#2,SET] vs [b#1,SET-d#4,SET] +1: + 000001:[a#1,SET-b#2,SET] seqnums:[0-0] points:[a#1,SET-b#2,SET] + 000002:[b#1,SET-d#4,SET] seqnums:[0-0] points:[b#1,SET-d#4,SET] + +check-ordering allow-split-user-keys +1: + 000001:[a#1,SET-b#2,SET] + 000002:[b#1,SET-d#4,SET] +---- +OK + +check-ordering +1: + 000001:[a#1,SET-b#2,SET] + 000002:[b#2,SET-d#4,SET] +---- +L1 files 000001 and 000002 have overlapping ranges: [a#1,SET-b#2,SET] vs [b#2,SET-d#4,SET] +1: + 000001:[a#1,SET-b#2,SET] seqnums:[0-0] points:[a#1,SET-b#2,SET] + 000002:[b#2,SET-d#4,SET] seqnums:[0-0] points:[b#2,SET-d#4,SET] + +check-ordering +1: + 000001:[a#1,SET-c#2,SET] + 000002:[b#3,SET-d#4,SET] +---- +L1 files 000001 and 000002 have overlapping ranges: [a#1,SET-c#2,SET] vs [b#3,SET-d#4,SET] +1: + 000001:[a#1,SET-c#2,SET] seqnums:[0-0] points:[a#1,SET-c#2,SET] + 000002:[b#3,SET-d#4,SET] seqnums:[0-0] points:[b#3,SET-d#4,SET] + +check-ordering +1: + 000001:[a#1,SET-c#2,SET] +2: + 000002:[b#3,SET-d#4,SET] +---- +OK + +check-ordering +1: + 000001:[a#1,SET-c#2,SET] +2: + 000002:[b#3,SET-d#4,SET] + 000003:[c#5,SET-e#6,SET] +---- +L2 files 000002 and 000003 have overlapping ranges: [b#3,SET-d#4,SET] vs [c#5,SET-e#6,SET] +1: + 000001:[a#1,SET-c#2,SET] seqnums:[0-0] points:[a#1,SET-c#2,SET] +2: + 000002:[b#3,SET-d#4,SET] seqnums:[0-0] points:[b#3,SET-d#4,SET] + 000003:[c#5,SET-e#6,SET] seqnums:[0-0] points:[c#5,SET-e#6,SET] + +# Ordering considers tables with just range keys. + +check-ordering +0: + 000002:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] ranges:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] + 000001:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] +---- +OK + +check-ordering +0: + 000002:[c#1,RANGEKEYSET-d#inf,RANGEKEYSET] ranges:[c#1,RANGEKEYSET-d#inf,RANGEKEYSET] + 000001:[a#3,RANGEKEYSET-b#inf,RANGEKEYSET] ranges:[a#3,RANGEKEYSET-b#inf,RANGEKEYSET] +---- +L0 files 000001 and 000002 are not properly ordered: <#3-#72057594037927935> vs <#1-#72057594037927935> +0.0: + 000001:[a#3,RANGEKEYSET-b#inf,RANGEKEYSET] seqnums:[3-72057594037927935] ranges:[a#3,RANGEKEYSET-b#inf,RANGEKEYSET] + 000002:[c#1,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[1-72057594037927935] ranges:[c#1,RANGEKEYSET-d#inf,RANGEKEYSET] + +check-ordering +1: + 000001:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] + 000002:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] ranges:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] +---- +OK + +check-ordering +1: + 000001:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] ranges:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] + 000002:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] +---- +L1 files 000001 and 000002 are not properly ordered: [c#3,RANGEKEYSET-d#inf,RANGEKEYSET] vs [a#1,RANGEKEYSET-b#inf,RANGEKEYSET] +1: + 000001:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] seqnums:[0-0] ranges:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] + 000002:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] seqnums:[0-0] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] + +# Ordering considers tables with both point and range keys. + +check-ordering +0: + 000002:[c#1,RANGEKEYSET-e#4,SET] points:[d#3,SET-e#4,SET] ranges:[c#1,RANGEKEYSET-d#inf,RANGEKEYSET] + 000001:[a#1,RANGEKEYSET-c#2,SET] points:[b#1,SET-c#2,SET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] +---- +OK + +check-ordering +0: + 000002:[c#1,RANGEKEYSET-e#2,SET] points:[d#3,SET-e#2,SET] ranges:[c#1,RANGEKEYSET-d#inf,RANGEKEYSET] + 000001:[a#1,RANGEKEYSET-c#4,SET] points:[b#1,SET-c#4,SET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] +---- +L0 files 000001 and 000002 are not properly ordered: <#1-#4> vs <#1-#2> +0.1: + 000002:[c#1,RANGEKEYSET-e#2,SET] seqnums:[1-2] points:[d#3,SET-e#2,SET] ranges:[c#1,RANGEKEYSET-d#inf,RANGEKEYSET] +0.0: + 000001:[a#1,RANGEKEYSET-c#4,SET] seqnums:[1-4] points:[b#1,SET-c#4,SET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] + +check-ordering +1: + 000001:[a#1,RANGEKEYSET-c#2,SET] points:[b#1,SET-c#2,SET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] + 000002:[d#3,RANGEKEYSET-f#4,SET] points:[e#3,SET-f#4,SET] ranges:[d#3,RANGEKEYSET-e#inf,RANGEKEYSET] +---- +OK + +check-ordering +1: + 000001:[a#1,RANGEKEYSET-c#2,SET] points:[b#1,SET-c#2,SET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] + 000002:[c#3,RANGEKEYSET-f#4,SET] points:[e#3,SET-f#4,SET] ranges:[c#3,RANGEKEYSET-e#inf,RANGEKEYSET] +---- +L1 files 000001 and 000002 have overlapping ranges: [a#1,RANGEKEYSET-c#2,SET] vs [c#3,RANGEKEYSET-f#4,SET] +1: + 000001:[a#1,RANGEKEYSET-c#2,SET] seqnums:[0-0] points:[b#1,SET-c#2,SET] ranges:[a#1,RANGEKEYSET-b#inf,RANGEKEYSET] + 000002:[c#3,RANGEKEYSET-f#4,SET] seqnums:[0-0] points:[e#3,SET-f#4,SET] ranges:[c#3,RANGEKEYSET-e#inf,RANGEKEYSET] diff --git a/pebble/internal/manifest/testdata/version_edit_apply b/pebble/internal/manifest/testdata/version_edit_apply new file mode 100644 index 0000000..94df012 --- /dev/null +++ b/pebble/internal/manifest/testdata/version_edit_apply @@ -0,0 +1,191 @@ +apply + L0 + 1:[a#1,SET-b#2,SET] + 2:[c#3,SET-d#4,SET] +edit + delete + L0 + 1 + add + L2 + 1:[a#1,SET-b#2,SET] + 4:[c#3,SET-d#4,SET] +---- +0.0: + 000002:[c#3,SET-d#4,SET] +2: + 000001:[a#1,SET-b#2,SET] + 000004:[c#3,SET-d#4,SET] +zombies [] + +apply + L0 + 1:[a#1,SET-b#2,SET] + 2:[c#3,SET-d#4,SET] +edit + delete + L1 + 1 +---- +pebble: internal error: No current or added files but have deleted files: 1 + +apply + L0 + 1:[a#1,SET-c#2,SET] + 2:[c#3,SET-d#4,SET] +edit + delete + L0 + 1 + add + L2 + 1:[a#1,SET-c#2,SET] + 4:[b#3,SET-d#4,SET] +---- +pebble: internal error: L2 files 000001 and 000004 have overlapping ranges: [a#1,SET-c#2,SET] vs [b#3,SET-d#4,SET] + +apply + L0 + 1:[a#1,SET-c#2,SET] + 2:[c#3,SET-d#4,SET] +edit + add + L0 + 4:[b#3,SET-d#5,SET] +---- +0.2: + 000004:[b#3,SET-d#5,SET] +0.1: + 000002:[c#3,SET-d#4,SET] +0.0: + 000001:[a#1,SET-c#2,SET] +zombies [] + +apply + L0 + 1:[a#1,SET-c#2,SET] + 2:[c#3,SET-d#4,SET] +edit + add + L0 + 4:[b#0,SET-d#0,SET] +---- +0.2: + 000002:[c#3,SET-d#4,SET] +0.1: + 000001:[a#1,SET-c#2,SET] +0.0: + 000004:[b#0,SET-d#0,SET] +zombies [] + + +apply +edit + add + L0 + 1:[a#1,SET-c#2,SET] + 4:[b#3,SET-d#5,SET] +---- +0.1: + 000004:[b#3,SET-d#5,SET] +0.0: + 000001:[a#1,SET-c#2,SET] +zombies [] + +apply + L0 + 1:[a#1,SET-c#2,SET] +---- +0.0: + 000001:[a#1,SET-c#2,SET] +zombies [] + +apply + L2 + 3:[b#1,SET-c#2,SET] + 4:[d#3,SET-f#4,SET] + 5:[h#3,SET-h#2,SET] + 2:[n#5,SET-q#3,SET] + 1:[r#2,SET-t#1,SET] +edit + delete + L2 + 4 + 1 + add + L2 + 6:[a#10,SET-a#7,SET] + 7:[e#1,SET-g#2,SET] + 10:[j#3,SET-m#2,SET] +---- +2: + 000006:[a#10,SET-a#7,SET] + 000003:[b#1,SET-c#2,SET] + 000007:[e#1,SET-g#2,SET] + 000005:[h#3,SET-h#2,SET] + 000010:[j#3,SET-m#2,SET] + 000002:[n#5,SET-q#3,SET] +zombies [1 4] + +apply +edit + add + L2 + 10:[j#3,SET-m#2,SET] + 6:[a#10,SET-a#7,SET] +---- +2: + 000006:[a#10,SET-a#7,SET] + 000010:[j#3,SET-m#2,SET] +zombies [] + +# Verify that the zombies map is populated correctly. + +apply + L0 + 1:[a#1,SET-b#2,SET] + L1 + 2:[c#3,SET-d#2,SET] +edit + delete + L0 + 1 + L1 + 2 +---- +zombies [1 2] + +# Deletion of a non-existent table results in an error. + +apply + L0 + 1:[a#1,SET-b#2,SET] +edit + delete + L0 + 2 +---- +pebble: file deleted L0.000002 before it was inserted + +apply + L0 + 1:[a#1,SET-b#2,SET] +edit + delete + L0 + 1 + add + L2 + 1:[a#1,SET-b#2,SET] + 4:[c#3,SET-d#4,SET] + 5:[s#3,SET-z#4,SET] +edit + delete + L2 + 1 + L2 + 4 +---- +2: + 000005:[s#3,SET-z#4,SET] +zombies [] diff --git a/pebble/internal/manifest/version.go b/pebble/internal/manifest/version.go new file mode 100644 index 0000000..549aa22 --- /dev/null +++ b/pebble/internal/manifest/version.go @@ -0,0 +1,1561 @@ +// Copyright 2012 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 manifest + +import ( + "bytes" + "fmt" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "unicode" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + stdcmp "github.com/cockroachdb/pebble/shims/cmp" +) + +// Compare exports the base.Compare type. +type Compare = base.Compare + +// InternalKey exports the base.InternalKey type. +type InternalKey = base.InternalKey + +// TableInfo contains the common information for table related events. +type TableInfo struct { + // FileNum is the internal DB identifier for the table. + FileNum base.FileNum + // Size is the size of the file in bytes. + Size uint64 + // Smallest is the smallest internal key in the table. + Smallest InternalKey + // Largest is the largest internal key in the table. + Largest InternalKey + // SmallestSeqNum is the smallest sequence number in the table. + SmallestSeqNum uint64 + // LargestSeqNum is the largest sequence number in the table. + LargestSeqNum uint64 +} + +// TableStats contains statistics on a table used for compaction heuristics, +// and export via Metrics. +type TableStats struct { + // The total number of entries in the table. + NumEntries uint64 + // 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. + PointDeletionsBytesEstimate uint64 + // Estimate of the total disk space that may be dropped by this table's + // range deletions by compacting them. This estimate is at data-block + // granularity and is not updated if compactions beneath the table reduce + // the amount of reclaimable disk space. It also does not account for + // 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. 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 +} + +// boundType represents the type of key (point or range) present as the smallest +// and largest keys. +type boundType uint8 + +const ( + boundTypePointKey boundType = iota + 1 + boundTypeRangeKey +) + +// CompactionState is the compaction state of a file. +// +// The following shows the valid state transitions: +// +// NotCompacting --> Compacting --> Compacted +// ^ | +// | | +// +-------<-------+ +// +// Input files to a compaction transition to Compacting when a compaction is +// picked. A file that has finished compacting typically transitions into the +// Compacted state, at which point it is effectively obsolete ("zombied") and +// will eventually be removed from the LSM. A file that has been move-compacted +// will transition from Compacting back into the NotCompacting state, signaling +// that the file may be selected for a subsequent compaction. A failed +// compaction will result in all input tables transitioning from Compacting to +// NotCompacting. +// +// This state is in-memory only. It is not persisted to the manifest. +type CompactionState uint8 + +// CompactionStates. +const ( + CompactionStateNotCompacting CompactionState = iota + CompactionStateCompacting + CompactionStateCompacted +) + +// String implements fmt.Stringer. +func (s CompactionState) String() string { + switch s { + case CompactionStateNotCompacting: + return "NotCompacting" + case CompactionStateCompacting: + return "Compacting" + case CompactionStateCompacted: + return "Compacted" + default: + panic(fmt.Sprintf("pebble: unknown compaction state %d", s)) + } +} + +// 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 { + // 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 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 + // 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 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. For virtual sstables, this corresponds to the wall clock time + // when the FileMetadata for the virtual sstable was first created. + CreationTime int64 + // 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 + // internal point keys stored in the table. This includes RANGEDELs, which + // alter point keys. + // NB: these field should be set using ExtendPointKeyBounds. They are left + // exported for reads as an optimization. + SmallestPointKey InternalKey + LargestPointKey InternalKey + // SmallestRangeKey and LargestRangeKey are the inclusive bounds for the + // internal range keys stored in the table. + // NB: these field should be set using ExtendRangeKeyBounds. They are left + // exported for reads as an optimization. + SmallestRangeKey InternalKey + LargestRangeKey InternalKey + // Smallest and Largest are the inclusive bounds for the internal keys stored + // in the table, across both point and range keys. + // NB: these fields are derived from their point and range key equivalents, + // and are updated via the MaybeExtend{Point,Range}KeyBounds methods. + 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 + maxIntervalIndex int + + // NB: the alignment of this struct is 8 bytes. We pack all the bools to + // ensure an optimal packing. + + // 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 + // file must be part of a compaction to Lbase. + IsIntraL0Compacting bool + CompactionState CompactionState + // True if compaction of this file has been explicitly requested. + // Previously, RocksDB and earlier versions of Pebble allowed this + // flag to be set by a user table property collector. Some earlier + // versions of Pebble respected this flag, while other more recent + // versions ignored this flag. + // + // More recently this flag has been repurposed to facilitate the + // compaction of 'atomic compaction units'. Files marked for + // compaction are compacted in a rewrite compaction at the lowest + // possible compaction priority. + // + // NB: A count of files marked for compaction is maintained on + // Version, and compaction picking reads cached annotations + // determined by this field. + // + // Protected by DB.mu. + MarkedForCompaction bool + // HasPointKeys tracks whether the table contains point keys (including + // RANGEDELs). If a table contains only range deletions, HasPointsKeys is + // still true. + HasPointKeys bool + // HasRangeKeys tracks whether the table contains any range keys. + HasRangeKeys bool + // smallestSet and largestSet track whether the overall bounds have been set. + boundsSet bool + // boundTypeSmallest and boundTypeLargest provide an indication as to which + // 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 +// state. Protected by DB.mu. +func (m *FileMetadata) SetCompactionState(to CompactionState) { + if invariants.Enabled { + transitionErr := func() error { + return errors.Newf("pebble: invalid compaction state transition: %s -> %s", m.CompactionState, to) + } + switch m.CompactionState { + case CompactionStateNotCompacting: + if to != CompactionStateCompacting { + panic(transitionErr()) + } + case CompactionStateCompacting: + if to != CompactionStateCompacted && to != CompactionStateNotCompacting { + panic(transitionErr()) + } + case CompactionStateCompacted: + panic(transitionErr()) + default: + panic(fmt.Sprintf("pebble: unknown compaction state: %d", m.CompactionState)) + } + } + m.CompactionState = to +} + +// IsCompacting returns true if this file's compaction state is +// CompactionStateCompacting. Protected by DB.mu. +func (m *FileMetadata) IsCompacting() bool { + return m.CompactionState == CompactionStateCompacting +} + +// StatsValid returns true if the table stats have been populated. If StatValid +// returns true, the Stats field may be read (with or without holding the +// database mutex). +func (m *FileMetadata) StatsValid() bool { + 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() { + m.statsValid.Store(true) +} + +// ExtendPointKeyBounds attempts to extend the lower and upper point key bounds +// and overall table bounds with the given smallest and largest keys. The +// smallest and largest bounds may not be extended if the table already has a +// bound that is smaller or larger, respectively. The receiver is returned. +// NB: calling this method should be preferred to manually setting the bounds by +// manipulating the fields directly, to maintain certain invariants. +func (m *FileMetadata) ExtendPointKeyBounds( + cmp Compare, smallest, largest InternalKey, +) *FileMetadata { + // Update the point key bounds. + if !m.HasPointKeys { + m.SmallestPointKey, m.LargestPointKey = smallest, largest + m.HasPointKeys = true + } else { + if base.InternalCompare(cmp, smallest, m.SmallestPointKey) < 0 { + m.SmallestPointKey = smallest + } + if base.InternalCompare(cmp, largest, m.LargestPointKey) > 0 { + m.LargestPointKey = largest + } + } + // Update the overall bounds. + m.extendOverallBounds(cmp, m.SmallestPointKey, m.LargestPointKey, boundTypePointKey) + return m +} + +// ExtendRangeKeyBounds attempts to extend the lower and upper range key bounds +// and overall table bounds with the given smallest and largest keys. The +// smallest and largest bounds may not be extended if the table already has a +// bound that is smaller or larger, respectively. The receiver is returned. +// NB: calling this method should be preferred to manually setting the bounds by +// manipulating the fields directly, to maintain certain invariants. +func (m *FileMetadata) ExtendRangeKeyBounds( + cmp Compare, smallest, largest InternalKey, +) *FileMetadata { + // Update the range key bounds. + if !m.HasRangeKeys { + m.SmallestRangeKey, m.LargestRangeKey = smallest, largest + m.HasRangeKeys = true + } else { + if base.InternalCompare(cmp, smallest, m.SmallestRangeKey) < 0 { + m.SmallestRangeKey = smallest + } + if base.InternalCompare(cmp, largest, m.LargestRangeKey) > 0 { + m.LargestRangeKey = largest + } + } + // Update the overall bounds. + m.extendOverallBounds(cmp, m.SmallestRangeKey, m.LargestRangeKey, boundTypeRangeKey) + return m +} + +// extendOverallBounds attempts to extend the overall table lower and upper +// bounds. The given bounds may not be used if a lower or upper bound already +// exists that is smaller or larger than the given keys, respectively. The given +// boundType will be used if the bounds are updated. +func (m *FileMetadata) extendOverallBounds( + cmp Compare, smallest, largest InternalKey, bTyp boundType, +) { + if !m.boundsSet { + m.Smallest, m.Largest = smallest, largest + m.boundsSet = true + m.boundTypeSmallest, m.boundTypeLargest = bTyp, bTyp + } else { + if base.InternalCompare(cmp, smallest, m.Smallest) < 0 { + m.Smallest = smallest + m.boundTypeSmallest = bTyp + } + if base.InternalCompare(cmp, largest, m.Largest) > 0 { + m.Largest = largest + m.boundTypeLargest = bTyp + } + } +} + +// Overlaps returns true if the file key range overlaps with the given range. +func (m *FileMetadata) Overlaps(cmp Compare, start []byte, end []byte, exclusiveEnd bool) bool { + if c := cmp(m.Largest.UserKey, start); c < 0 || (c == 0 && m.Largest.IsExclusiveSentinel()) { + // f is completely before the specified range; no overlap. + return false + } + if c := cmp(m.Smallest.UserKey, end); c > 0 || (c == 0 && exclusiveEnd) { + // f is completely after the specified range; no overlap. + return false + } + 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 { + switch kt { + case KeyTypePointAndRange: + return true + case KeyTypePoint: + return m.HasPointKeys + case KeyTypeRange: + return m.HasRangeKeys + default: + panic("unrecognized key type") + } +} + +// SmallestBound returns the file's smallest bound of the key type. It returns a +// false second return value if the file does not contain any keys of the key +// type. +func (m *FileMetadata) SmallestBound(kt KeyType) (*InternalKey, bool) { + switch kt { + case KeyTypePointAndRange: + return &m.Smallest, true + case KeyTypePoint: + return &m.SmallestPointKey, m.HasPointKeys + case KeyTypeRange: + return &m.SmallestRangeKey, m.HasRangeKeys + default: + panic("unrecognized key type") + } +} + +// LargestBound returns the file's largest bound of the key type. It returns a +// false second return value if the file does not contain any keys of the key +// type. +func (m *FileMetadata) LargestBound(kt KeyType) (*InternalKey, bool) { + switch kt { + case KeyTypePointAndRange: + return &m.Largest, true + case KeyTypePoint: + return &m.LargestPointKey, m.HasPointKeys + case KeyTypeRange: + return &m.LargestRangeKey, m.HasRangeKeys + default: + panic("unrecognized key type") + } +} + +const ( + maskContainsPointKeys = 1 << 0 + maskSmallest = 1 << 1 + maskLargest = 1 << 2 +) + +// boundsMarker returns a marker byte whose bits encode the following +// information (in order from least significant bit): +// - if the table contains point keys +// - if the table's smallest key is a point key +// - if the table's largest key is a point key +func (m *FileMetadata) boundsMarker() (sentinel uint8, err error) { + if m.HasPointKeys { + sentinel |= maskContainsPointKeys + } + switch m.boundTypeSmallest { + case boundTypePointKey: + sentinel |= maskSmallest + case boundTypeRangeKey: + // No op - leave bit unset. + default: + return 0, base.CorruptionErrorf("file %s has neither point nor range key as smallest key", m.FileNum) + } + switch m.boundTypeLargest { + case boundTypePointKey: + sentinel |= maskLargest + case boundTypeRangeKey: + // No op - leave bit unset. + default: + return 0, base.CorruptionErrorf("file %s has neither point nor range key as largest key", m.FileNum) + } + return +} + +// String implements fmt.Stringer, printing the file number and the overall +// table bounds. +func (m *FileMetadata) String() string { + return fmt.Sprintf("%s:[%s-%s]", m.FileNum, m.Smallest, m.Largest) +} + +// DebugString returns a verbose representation of FileMetadata, typically for +// use in tests and debugging, returning the file number and the point, range +// and overall bounds for the table. +func (m *FileMetadata) DebugString(format base.FormatKey, verbose bool) string { + var b bytes.Buffer + fmt.Fprintf(&b, "%s:[%s-%s]", + m.FileNum, m.Smallest.Pretty(format), m.Largest.Pretty(format)) + 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)) + } + if m.HasRangeKeys { + fmt.Fprintf(&b, " ranges:[%s-%s]", + m.SmallestRangeKey.Pretty(format), m.LargestRangeKey.Pretty(format)) + } + return b.String() +} + +// ParseFileMetadataDebug parses a FileMetadata from its DebugString +// representation. +func ParseFileMetadataDebug(s string) (*FileMetadata, error) { + // Split lines of the form: + // 000000:[a#0,SET-z#0,SET] seqnums:[5-5] points:[...] ranges:[...] + fields := strings.FieldsFunc(s, func(c rune) bool { + switch c { + case ':', '[', '-', ']': + return true + default: + return unicode.IsSpace(c) // NB: also trim whitespace padding. + } + }) + if len(fields)%3 != 0 { + 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 { + case "points": + m.SmallestPointKey, m.LargestPointKey = smallest, largest + m.HasPointKeys = true + case "ranges": + m.SmallestRangeKey, m.LargestRangeKey = smallest, largest + m.HasRangeKeys = true + default: + fileNum, err := strconv.ParseUint(prefix, 10, 64) + if err != nil { + return m, errors.Newf("malformed input: %s: %s", s, err) + } + m.FileNum = base.FileNum(fileNum) + m.Smallest, m.Largest = smallest, largest + m.boundsSet = true + } + fields = fields[3:] + } + // By default, when the parser sees just the overall bounds, we set the point + // keys. This preserves backwards compatability with existing test cases that + // specify only the overall bounds. + if !m.HasPointKeys && !m.HasRangeKeys { + m.SmallestPointKey, m.LargestPointKey = m.Smallest, m.Largest + m.HasPointKeys = true + } + m.InitPhysicalBacking() + return m, nil +} + +// Validate validates the metadata for consistency with itself, returning an +// error if inconsistent. +func (m *FileMetadata) Validate(cmp Compare, formatKey base.FormatKey) error { + // Combined range and point key validation. + + if !m.HasPointKeys && !m.HasRangeKeys { + return base.CorruptionErrorf("file %s has neither point nor range keys", + errors.Safe(m.FileNum)) + } + if base.InternalCompare(cmp, m.Smallest, m.Largest) > 0 { + return base.CorruptionErrorf("file %s has inconsistent bounds: %s vs %s", + errors.Safe(m.FileNum), m.Smallest.Pretty(formatKey), + m.Largest.Pretty(formatKey)) + } + if m.SmallestSeqNum > m.LargestSeqNum { + return base.CorruptionErrorf("file %s has inconsistent seqnum bounds: %d vs %d", + errors.Safe(m.FileNum), m.SmallestSeqNum, m.LargestSeqNum) + } + + // Point key validation. + + if m.HasPointKeys { + if base.InternalCompare(cmp, m.SmallestPointKey, m.LargestPointKey) > 0 { + return base.CorruptionErrorf("file %s has inconsistent point key bounds: %s vs %s", + errors.Safe(m.FileNum), m.SmallestPointKey.Pretty(formatKey), + m.LargestPointKey.Pretty(formatKey)) + } + if base.InternalCompare(cmp, m.SmallestPointKey, m.Smallest) < 0 || + base.InternalCompare(cmp, m.LargestPointKey, m.Largest) > 0 { + return base.CorruptionErrorf( + "file %s has inconsistent point key bounds relative to overall bounds: "+ + "overall = [%s-%s], point keys = [%s-%s]", + errors.Safe(m.FileNum), + m.Smallest.Pretty(formatKey), m.Largest.Pretty(formatKey), + m.SmallestPointKey.Pretty(formatKey), m.LargestPointKey.Pretty(formatKey), + ) + } + } + + // Range key validation. + + if m.HasRangeKeys { + if base.InternalCompare(cmp, m.SmallestRangeKey, m.LargestRangeKey) > 0 { + return base.CorruptionErrorf("file %s has inconsistent range key bounds: %s vs %s", + errors.Safe(m.FileNum), m.SmallestRangeKey.Pretty(formatKey), + m.LargestRangeKey.Pretty(formatKey)) + } + if base.InternalCompare(cmp, m.SmallestRangeKey, m.Smallest) < 0 || + base.InternalCompare(cmp, m.LargestRangeKey, m.Largest) > 0 { + return base.CorruptionErrorf( + "file %s has inconsistent range key bounds relative to overall bounds: "+ + "overall = [%s-%s], range keys = [%s-%s]", + errors.Safe(m.FileNum), + m.Smallest.Pretty(formatKey), m.Largest.Pretty(formatKey), + m.SmallestRangeKey.Pretty(formatKey), m.LargestRangeKey.Pretty(formatKey), + ) + } + } + + // Ensure that FileMetadata.Init was called. + if m.FileBacking == nil { + return base.CorruptionErrorf("file metadata FileBacking not set") + } + + return nil +} + +// TableInfo returns a subset of the FileMetadata state formatted as a +// TableInfo. +func (m *FileMetadata) TableInfo() TableInfo { + return TableInfo{ + FileNum: m.FileNum, + Size: m.Size, + Smallest: m.Smallest, + Largest: m.Largest, + SmallestSeqNum: m.SmallestSeqNum, + LargestSeqNum: m.LargestSeqNum, + } +} + +func (m *FileMetadata) cmpSeqNum(b *FileMetadata) int { + // NB: This is the same ordering that RocksDB uses for L0 files. + + // Sort first by largest sequence number. + if v := stdcmp.Compare(m.LargestSeqNum, b.LargestSeqNum); v != 0 { + return v + } + // Then by smallest sequence number. + if v := stdcmp.Compare(m.SmallestSeqNum, b.SmallestSeqNum); v != 0 { + return v + } + // Break ties by file number. + return stdcmp.Compare(m.FileNum, b.FileNum) +} + +func (m *FileMetadata) lessSeqNum(b *FileMetadata) bool { + return m.cmpSeqNum(b) < 0 +} + +func (m *FileMetadata) cmpSmallestKey(b *FileMetadata, cmp Compare) int { + return base.InternalCompare(cmp, m.Smallest, b.Smallest) +} + +// KeyRange returns the minimum smallest and maximum largest internalKey for +// all the FileMetadata in iters. +func KeyRange(ucmp Compare, iters ...LevelIterator) (smallest, largest InternalKey) { + first := true + for _, iter := range iters { + for meta := iter.First(); meta != nil; meta = iter.Next() { + if first { + first = false + smallest, largest = meta.Smallest, meta.Largest + continue + } + if base.InternalCompare(ucmp, smallest, meta.Smallest) >= 0 { + smallest = meta.Smallest + } + if base.InternalCompare(ucmp, largest, meta.Largest) <= 0 { + largest = meta.Largest + } + } + } + return smallest, largest +} + +type bySeqNum []*FileMetadata + +func (b bySeqNum) Len() int { return len(b) } +func (b bySeqNum) Less(i, j int) bool { + return b[i].lessSeqNum(b[j]) +} +func (b bySeqNum) Swap(i, j int) { b[i], b[j] = b[j], b[i] } + +// SortBySeqNum sorts the specified files by increasing sequence number. +func SortBySeqNum(files []*FileMetadata) { + sort.Sort(bySeqNum(files)) +} + +type bySmallest struct { + files []*FileMetadata + cmp Compare +} + +func (b bySmallest) Len() int { return len(b.files) } +func (b bySmallest) Less(i, j int) bool { + return b.files[i].cmpSmallestKey(b.files[j], b.cmp) < 0 +} +func (b bySmallest) Swap(i, j int) { b.files[i], b.files[j] = b.files[j], b.files[i] } + +// SortBySmallest sorts the specified files by smallest key using the supplied +// comparison function to order user keys. +func SortBySmallest(files []*FileMetadata, cmp Compare) { + sort.Sort(bySmallest{files, cmp}) +} + +func overlaps(iter LevelIterator, cmp Compare, start, end []byte, exclusiveEnd bool) LevelSlice { + startIter := iter.Clone() + { + startIterFile := startIter.SeekGE(cmp, start) + // SeekGE compares user keys. The user key `start` may be equal to the + // f.Largest because f.Largest is a range deletion sentinel, indicating + // that the user key `start` is NOT contained within the file f. If + // that's the case, we can narrow the overlapping bounds to exclude the + // file with the sentinel. + if startIterFile != nil && startIterFile.Largest.IsExclusiveSentinel() && + cmp(startIterFile.Largest.UserKey, start) == 0 { + startIterFile = startIter.Next() + } + _ = startIterFile // Ignore unused assignment. + } + + endIter := iter.Clone() + { + endIterFile := endIter.SeekGE(cmp, end) + + if !exclusiveEnd { + // endIter is now pointing at the *first* file with a largest key >= end. + // If there are multiple files including the user key `end`, we want all + // of them, so move forward. + for endIterFile != nil && cmp(endIterFile.Largest.UserKey, end) == 0 { + endIterFile = endIter.Next() + } + } + + // LevelSlice uses inclusive bounds, so if we seeked to the end sentinel + // or nexted too far because Largest.UserKey equaled `end`, go back. + // + // Consider !exclusiveEnd and end = 'f', with the following file bounds: + // + // [b,d] [e, f] [f, f] [g, h] + // + // the above for loop will Next until it arrives at [g, h]. We need to + // observe that g > f, and Prev to the file with bounds [f, f]. + if endIterFile == nil { + endIterFile = endIter.Prev() + } else if c := cmp(endIterFile.Smallest.UserKey, end); c > 0 || c == 0 && exclusiveEnd { + endIterFile = endIter.Prev() + } + _ = endIterFile // Ignore unused assignment. + } + return newBoundedLevelSlice(startIter.Clone().iter, &startIter.iter, &endIter.iter) +} + +// NumLevels is the number of levels a Version contains. +const NumLevels = 7 + +// NewVersion constructs a new Version with the provided files. It requires +// the provided files are already well-ordered. It's intended for testing. +func NewVersion( + cmp Compare, formatKey base.FormatKey, flushSplitBytes int64, files [NumLevels][]*FileMetadata, +) *Version { + var v Version + for l := range files { + // NB: We specifically insert `files` into the B-Tree in the order + // they appear within `files`. Some tests depend on this behavior in + // order to test consistency checking, etc. Once we've constructed the + // initial B-Tree, we swap out the btreeCmp for the correct one. + // TODO(jackson): Adjust or remove the tests and remove this. + v.Levels[l].tree, _ = makeBTree(btreeCmpSpecificOrder(files[l]), files[l]) + v.Levels[l].level = l + if l == 0 { + v.Levels[l].tree.cmp = btreeCmpSeqNum + } 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) + } + return &v +} + +// Version is a collection of file metadata for on-disk tables at various +// levels. In-memory DBs are written to level-0 tables, and compactions +// migrate data from level N to level N+1. The tables map internal keys (which +// are a user key, a delete or set bit, and a sequence number) to user values. +// +// The tables at level 0 are sorted by largest sequence number. Due to file +// ingestion, there may be overlap in the ranges of sequence numbers contain in +// level 0 sstables. In particular, it is valid for one level 0 sstable to have +// the seqnum range [1,100] while an adjacent sstable has the seqnum range +// [50,50]. This occurs when the [50,50] table was ingested and given a global +// seqnum. The ingestion code will have ensured that the [50,50] sstable will +// not have any keys that overlap with the [1,100] in the seqnum range +// [1,49]. The range of internal keys [fileMetadata.smallest, +// fileMetadata.largest] in each level 0 table may overlap. +// +// The tables at any non-0 level are sorted by their internal key range and any +// two tables at the same non-0 level do not overlap. +// +// The internal key ranges of two tables at different levels X and Y may +// overlap, for any X != Y. +// +// Finally, for every internal key in a table at level X, there is no internal +// key in a higher level table that has both the same user key and a higher +// sequence number. +type Version struct { + 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 + // higher level table that has both the same user key and a higher sequence + // number. Within a sublevel, tables are sorted by their internal key range + // and any two tables at the same sublevel do not overlap. Unlike the normal + // levels, sublevel n contains older tables (lower sequence numbers) than + // sublevel n+1. + // + // The L0Sublevels struct is mostly used for compaction picking. As most + // internal data structures in it are only necessary for compaction picking + // and not for iterator creation, the reference to L0Sublevels is nil'd + // after this version becomes the non-newest version, to reduce memory + // usage. + // + // L0Sublevels.Levels contains L0 files ordered by sublevels. All the files + // 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 + L0SublevelFiles []LevelSlice + + Levels [NumLevels]LevelMetadata + + // RangeKeyLevels holds a subset of the same files as Levels that contain range + // keys (i.e. fileMeta.HasRangeKeys == true). The memory amplification of this + // duplication should be minimal, as range keys are expected to be rare. + RangeKeyLevels [NumLevels]LevelMetadata + + // The callback to invoke when the last reference to a version is + // removed. Will be called with list.mu held. + Deleted func(obsolete []*FileBacking) + + // Stats holds aggregated stats about the version maintained from + // version to version. + Stats struct { + // MarkedForCompaction records the count of files marked for + // compaction within the version. + MarkedForCompaction int + } + + // The list the version is linked into. + list *VersionList + + // The next/prev link for the versionList doubly-linked list of versions. + prev, next *Version +} + +// String implements fmt.Stringer, printing the FileMetadata for each level in +// the Version. +func (v *Version) String() string { + return v.string(base.DefaultFormatter, false) +} + +// DebugString returns an alternative format to String() which includes sequence +// number and kind information for the sstable boundaries. +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 { + fmt.Fprintf(&buf, "%s", describeSublevels(format, verbose, v.L0SublevelFiles)) + } + for level := 1; level < NumLevels; level++ { + if v.Levels[level].Empty() { + continue + } + fmt.Fprintf(&buf, "%d:\n", level) + iter := v.Levels[level].Iter() + for f := iter.First(); f != nil; f = iter.Next() { + fmt.Fprintf(&buf, " %s\n", f.DebugString(format, verbose)) + } + } + return buf.String() +} + +// ParseVersionDebug parses a Version from its DebugString output. +func ParseVersionDebug( + cmp Compare, formatKey base.FormatKey, flushSplitBytes int64, s string, +) (*Version, error) { + var level int + var files [NumLevels][]*FileMetadata + for _, l := range strings.Split(s, "\n") { + l = strings.TrimSpace(l) + + switch l[:2] { + case "0.", "0:", "1:", "2:", "3:", "4:", "5:", "6:": + var err error + level, err = strconv.Atoi(l[:1]) + if err != nil { + return nil, err + } + default: + m, err := ParseFileMetadataDebug(l) + if err != nil { + return nil, err + } + // If we only parsed overall bounds, default to setting the point bounds. + if !m.HasPointKeys && !m.HasRangeKeys { + m.SmallestPointKey, m.LargestPointKey = m.Smallest, m.Largest + m.HasPointKeys = true + } + files[level] = append(files[level], m) + } + } + // Reverse the order of L0 files. This ensures we construct the same + // sublevels. (They're printed from higher sublevel to lower, which means in + // a partial order that represents newest to oldest). + for i := 0; i < len(files[0])/2; i++ { + files[0][i], files[0][len(files[0])-i-1] = files[0][len(files[0])-i-1], files[0][i] + } + return NewVersion(cmp, formatKey, flushSplitBytes, files), nil +} + +// Refs returns the number of references to the version. +func (v *Version) Refs() int32 { + return v.refs.Load() +} + +// Ref increments the version refcount. +func (v *Version) Ref() { + v.refs.Add(1) +} + +// Unref decrements the version refcount. If the last reference to the version +// was removed, the version is removed from the list of versions and the +// Deleted callback is invoked. Requires that the VersionList mutex is NOT +// locked. +func (v *Version) Unref() { + if v.refs.Add(-1) == 0 { + l := v.list + l.mu.Lock() + l.Remove(v) + v.Deleted(v.unrefFiles()) + l.mu.Unlock() + } +} + +// UnrefLocked decrements the version refcount. If the last reference to the +// version was removed, the version is removed from the list of versions and +// the Deleted callback is invoked. Requires that the VersionList mutex is +// already locked. +func (v *Version) UnrefLocked() { + if v.refs.Add(-1) == 0 { + v.list.Remove(v) + v.Deleted(v.unrefFiles()) + } +} + +func (v *Version) unrefFiles() []*FileBacking { + var obsolete []*FileBacking + for _, lm := range v.Levels { + obsolete = append(obsolete, lm.release()...) + } + for _, lm := range v.RangeKeyLevels { + obsolete = append(obsolete, lm.release()...) + } + return obsolete +} + +// Next returns the next version in the list of versions. +func (v *Version) Next() *Version { + return v.next +} + +// InitL0Sublevels initializes the L0Sublevels +func (v *Version) InitL0Sublevels( + cmp Compare, formatKey base.FormatKey, flushSplitBytes int64, +) error { + var err error + v.L0Sublevels, err = NewL0Sublevels(&v.Levels[0], cmp, formatKey, flushSplitBytes) + if err == nil && v.L0Sublevels != nil { + v.L0SublevelFiles = v.L0Sublevels.Levels + } + return err +} + +// Contains returns a boolean indicating whether the provided file exists in +// the version at the given level. If level is non-zero then Contains binary +// searches among the files. If level is zero, Contains scans the entire +// level. +func (v *Version) Contains(level int, cmp Compare, m *FileMetadata) bool { + iter := v.Levels[level].Iter() + if level > 0 { + overlaps := v.Overlaps(level, cmp, m.Smallest.UserKey, m.Largest.UserKey, + m.Largest.IsExclusiveSentinel()) + iter = overlaps.Iter() + } + for f := iter.First(); f != nil; f = iter.Next() { + if f == m { + return true + } + } + return false +} + +// Overlaps returns all elements of v.files[level] whose user key range +// intersects the given range. If level is non-zero then the user key ranges of +// v.files[level] are assumed to not overlap (although they may touch). If level +// is zero then that assumption cannot be made, and the [start, end] range is +// expanded to the union of those matching ranges so far and the computation is +// repeated until [start, end] stabilizes. +// The returned files are a subsequence of the input files, i.e., the ordering +// is not changed. +func (v *Version) Overlaps( + level int, cmp Compare, start, end []byte, exclusiveEnd bool, +) LevelSlice { + if level == 0 { + // Indices that have been selected as overlapping. + l0 := v.Levels[level] + l0Iter := l0.Iter() + selectedIndices := make([]bool, l0.Len()) + numSelected := 0 + var slice LevelSlice + for { + restart := false + for i, meta := 0, l0Iter.First(); meta != nil; i, meta = i+1, l0Iter.Next() { + selected := selectedIndices[i] + if selected { + continue + } + if !meta.Overlaps(cmp, start, end, exclusiveEnd) { + // meta is completely outside the specified range; skip it. + continue + } + // Overlaps. + selectedIndices[i] = true + numSelected++ + + smallest := meta.Smallest.UserKey + largest := meta.Largest.UserKey + // Since level == 0, check if the newly added fileMetadata has + // expanded the range. We expand the range immediately for files + // we have remaining to check in this loop. All already checked + // and unselected files will need to be rechecked via the + // restart below. + if cmp(smallest, start) < 0 { + start = smallest + restart = true + } + if v := cmp(largest, end); v > 0 { + end = largest + exclusiveEnd = meta.Largest.IsExclusiveSentinel() + restart = true + } else if v == 0 && exclusiveEnd && !meta.Largest.IsExclusiveSentinel() { + // Only update the exclusivity of our existing `end` + // bound. + exclusiveEnd = false + restart = true + } + } + + if !restart { + // Construct a B-Tree containing only the matching items. + var tr btree + tr.cmp = v.Levels[level].tree.cmp + for i, meta := 0, l0Iter.First(); meta != nil; i, meta = i+1, l0Iter.Next() { + if selectedIndices[i] { + err := tr.Insert(meta) + if err != nil { + panic(err) + } + } + } + slice = newLevelSlice(tr.Iter()) + // TODO(jackson): Avoid the oddity of constructing and + // immediately releasing a B-Tree. Make LevelSlice an + // interface? + tr.Release() + break + } + // Continue looping to retry the files that were not selected. + } + return slice + } + + return overlaps(v.Levels[level].Iter(), cmp, start, end, exclusiveEnd) +} + +// 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, order OrderingInvariants, +) error { + for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- { + sublevelIter := v.L0SublevelFiles[sublevel].Iter() + // 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(), order); err != nil { + return base.CorruptionErrorf("%s\n%s", err, v.DebugString(format)) + } + } + return nil +} + +// VersionList holds a list of versions. The versions are ordered from oldest +// to newest. +type VersionList struct { + mu *sync.Mutex + root Version +} + +// Init initializes the version list. +func (l *VersionList) Init(mu *sync.Mutex) { + l.mu = mu + l.root.next = &l.root + l.root.prev = &l.root +} + +// Empty returns true if the list is empty, and false otherwise. +func (l *VersionList) Empty() bool { + return l.root.next == &l.root +} + +// Front returns the oldest version in the list. Note that this version is only +// valid if Empty() returns true. +func (l *VersionList) Front() *Version { + return l.root.next +} + +// Back returns the newest version in the list. Note that this version is only +// valid if Empty() returns true. +func (l *VersionList) Back() *Version { + return l.root.prev +} + +// PushBack adds a new version to the back of the list. This new version +// becomes the "newest" version in the list. +func (l *VersionList) PushBack(v *Version) { + if v.list != nil || v.prev != nil || v.next != nil { + panic("pebble: version list is inconsistent") + } + v.prev = l.root.prev + v.prev.next = v + v.next = &l.root + v.next.prev = v + v.list = l + // Let L0Sublevels on the second newest version get GC'd, as it is no longer + // necessary. See the comment in Version. + v.prev.L0Sublevels = nil +} + +// Remove removes the specified version from the list. +func (l *VersionList) Remove(v *Version) { + if v == &l.root { + panic("pebble: cannot remove version list root node") + } + if v.list != l { + panic("pebble: version list is inconsistent") + } + v.prev.next = v.next + v.next.prev = v.prev + v.next = nil // avoid memory leaks + v.prev = nil // avoid memory leaks + 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). +// +// 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 + // checks, defined in the detailed comment below. + if level == Level(0) { + // We have 2 kinds of files: + // - Files with exactly one sequence number: these could be either ingested files + // or flushed files. We cannot tell the difference between them based on FileMetadata, + // so our consistency checking here uses the weaker checks assuming it is a narrow + // flushed file. We cannot error on ingested files having sequence numbers coincident + // with flushed files as the seemingly ingested file could just be a flushed file + // with just one key in it which is a truncated range tombstone sharing sequence numbers + // with other files in the same flush. + // - Files with multiple sequence numbers: these are necessarily flushed files. + // + // Three cases of overlapping sequence numbers: + // Case 1: + // An ingested file contained in the sequence numbers of the flushed file -- it must be + // fully contained (not coincident with either end of the flushed file) since the memtable + // must have been at [a, b-1] (where b > a) when the ingested file was assigned sequence + // num b, and the memtable got a subsequent update that was given sequence num b+1, before + // being flushed. + // + // So a sequence [1000, 1000] [1002, 1002] [1000, 2000] is invalid since the first and + // third file are inconsistent with each other. So comparing adjacent files is insufficient + // for consistency checking. + // + // Visually we have something like + // x------y x-----------yx-------------y (flushed files where x, y are the endpoints) + // y y y y (y's represent ingested files) + // And these are ordered in increasing order of y. Note that y's must be unique. + // + // Case 2: + // A flushed file that did not overlap in keys with any file in any level, but does overlap + // in the file key intervals. This file is placed in L0 since it overlaps in the file + // key intervals but since it has no overlapping data, it is assigned a sequence number + // of 0 in RocksDB. We handle this case for compatibility with RocksDB. + // + // Case 3: + // A sequence of flushed files that overlap in sequence numbers with one another, + // but do not overlap in keys inside the sstables. These files correspond to + // partitioned flushes or the results of intra-L0 compactions of partitioned + // flushes. + // + // Since these types of SSTables violate most other sequence number + // overlap invariants, and handling this case is important for compatibility + // with future versions of pebble, this method relaxes most L0 invariant + // checks. + + var prev *FileMetadata + for f := files.First(); f != nil; f, prev = files.Next(), f { + if prev == nil { + continue + } + // Validate that the sorting is sane. + if prev.LargestSeqNum == 0 && f.LargestSeqNum == prev.LargestSeqNum { + // Multiple files satisfying case 2 mentioned above. + } else if !prev.lessSeqNum(f) { + return base.CorruptionErrorf("L0 files %s and %s are not properly ordered: <#%d-#%d> vs <#%d-#%d>", + errors.Safe(prev.FileNum), errors.Safe(f.FileNum), + errors.Safe(prev.SmallestSeqNum), errors.Safe(prev.LargestSeqNum), + errors.Safe(f.SmallestSeqNum), errors.Safe(f.LargestSeqNum)) + } + } + } else { + var prev *FileMetadata + for f := files.First(); f != nil; f, prev = files.Next(), f { + if err := f.Validate(cmp, format); err != nil { + return errors.Wrapf(err, "%s ", level) + } + if prev != nil { + if prev.cmpSmallestKey(f, cmp) >= 0 { + return base.CorruptionErrorf("%s files %s and %s are not properly ordered: [%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") + } + } + } + } + return nil +} diff --git a/pebble/internal/manifest/version_edit.go b/pebble/internal/manifest/version_edit.go new file mode 100644 index 0000000..ee3a919 --- /dev/null +++ b/pebble/internal/manifest/version_edit.go @@ -0,0 +1,1122 @@ +// Copyright 2012 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 manifest + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "io" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + stdcmp "github.com/cockroachdb/pebble/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" +) + +// TODO(peter): describe the MANIFEST file format, independently of the C++ +// project. + +var errCorruptManifest = base.CorruptionErrorf("pebble: corrupt manifest") + +type byteReader interface { + io.ByteReader + io.Reader +} + +// Tags for the versionEdit disk format. +// Tag 8 is no longer used. +const ( + // LevelDB tags. + tagComparator = 1 + tagLogNumber = 2 + tagNextFileNumber = 3 + tagLastSequence = 4 + tagCompactPointer = 5 + tagDeletedFile = 6 + tagNewFile = 7 + tagPrevLogNumber = 9 + + // RocksDB tags. + tagNewFile2 = 100 + tagNewFile3 = 102 + tagNewFile4 = 103 + tagColumnFamily = 200 + tagColumnFamilyAdd = 201 + tagColumnFamilyDrop = 202 + tagMaxColumnFamily = 203 + + // Pebble tags. + tagNewFile5 = 104 // Range keys. + tagCreatedBackingTable = 105 + tagRemovedBackingTable = 106 + + // The custom tags sub-format used by tagNewFile4 and above. + customTagTerminate = 1 + customTagNeedsCompaction = 2 + customTagCreationTime = 6 + customTagPathID = 65 + customTagNonSafeIgnoreMask = 1 << 6 + customTagVirtual = 66 +) + +// DeletedFileEntry holds the state for a file deletion from a level. The file +// itself might still be referenced by another level. +type DeletedFileEntry struct { + Level int + FileNum base.FileNum +} + +// NewFileEntry holds the state for a new file or one moved from a different +// level. +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 +// on-disk state (log numbers, next file number, and the last sequence number). +type VersionEdit struct { + // ComparerName is the value of Options.Comparer.Name. This is only set in + // the first VersionEdit in a manifest (either when the DB is created, or + // when a new manifest is created) and is used to verify that the comparer + // specified at Open matches the comparer that was previously used. + ComparerName string + + // MinUnflushedLogNum is the smallest WAL log file number corresponding to + // mutations that have not been flushed to an sstable. + // + // This is an optional field, and 0 represents it is not set. + MinUnflushedLogNum base.DiskFileNum + + // ObsoletePrevLogNum is a historic artifact from LevelDB that is not used by + // Pebble, RocksDB, or even LevelDB. Its use in LevelDB was deprecated in + // 6/2011. We keep it around purely for informational purposes when + // displaying MANIFEST contents. + ObsoletePrevLogNum uint64 + + // The next file number. A single counter is used to assign file numbers + // for the WAL, MANIFEST, sstable, and OPTIONS files. + NextFileNum uint64 + + // LastSeqNum is an upper bound on the sequence numbers that have been + // assigned in flushed WALs. Unflushed WALs (that will be replayed during + // recovery) may contain sequence numbers greater than this value. + LastSeqNum uint64 + + // A file num may be present in both deleted files and new files when it + // is moved from a lower level to a higher level (when the compaction + // 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 { + br = bufio.NewReader(r) + } + d := versionEditDecoder{br} + for { + tag, err := binary.ReadUvarint(br) + if err == io.EOF { + break + } + if err != nil { + return err + } + switch tag { + case tagComparator: + s, err := d.readBytes() + if err != nil { + return err + } + v.ComparerName = string(s) + + case tagLogNumber: + n, err := d.readUvarint() + if err != nil { + return err + } + v.MinUnflushedLogNum = base.DiskFileNum(n) + + case tagNextFileNumber: + n, err := d.readUvarint() + if err != nil { + return err + } + v.NextFileNum = n + + case tagLastSequence: + n, err := d.readUvarint() + if err != nil { + return err + } + v.LastSeqNum = n + + case tagCompactPointer: + if _, err := d.readLevel(); err != nil { + return err + } + if _, err := d.readBytes(); err != nil { + return err + } + // 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 { + return err + } + fileNum, err := d.readFileNum() + if err != nil { + return err + } + if v.DeletedFiles == nil { + v.DeletedFiles = make(map[DeletedFileEntry]*FileMetadata) + } + v.DeletedFiles[DeletedFileEntry{level, fileNum}] = nil + + case tagNewFile, tagNewFile2, tagNewFile3, tagNewFile4, tagNewFile5: + level, err := d.readLevel() + if err != nil { + return err + } + fileNum, err := d.readFileNum() + if err != nil { + return err + } + if tag == tagNewFile3 { + // The pathID field appears unused in RocksDB. + _ /* pathID */, err := d.readUvarint() + if err != nil { + return err + } + } + size, err := d.readUvarint() + if err != nil { + return err + } + // We read the smallest / largest key bounds differently depending on + // whether we have point, range or both types of keys present in the + // table. + var ( + smallestPointKey, largestPointKey []byte + smallestRangeKey, largestRangeKey []byte + parsedPointBounds bool + boundsMarker byte + ) + if tag != tagNewFile5 { + // Range keys not present in the table. Parse the point key bounds. + smallestPointKey, err = d.readBytes() + if err != nil { + return err + } + largestPointKey, err = d.readBytes() + if err != nil { + return err + } + } else { + // Range keys are present in the table. Determine whether we have point + // keys to parse, in addition to the bounds. + boundsMarker, err = d.ReadByte() + if err != nil { + return err + } + // Parse point key bounds, if present. + if boundsMarker&maskContainsPointKeys > 0 { + smallestPointKey, err = d.readBytes() + if err != nil { + return err + } + largestPointKey, err = d.readBytes() + if err != nil { + return err + } + parsedPointBounds = true + } else { + // The table does not have point keys. + // Sanity check: the bounds must be range keys. + if boundsMarker&maskSmallest != 0 || boundsMarker&maskLargest != 0 { + return base.CorruptionErrorf( + "new-file-4-range-keys: table without point keys has point key bounds: marker=%x", + boundsMarker, + ) + } + } + // Parse range key bounds. + smallestRangeKey, err = d.readBytes() + if err != nil { + return err + } + largestRangeKey, err = d.readBytes() + if err != nil { + return err + } + } + var smallestSeqNum uint64 + var largestSeqNum uint64 + if tag != tagNewFile { + smallestSeqNum, err = d.readUvarint() + if err != nil { + return err + } + largestSeqNum, err = d.readUvarint() + if err != nil { + return err + } + } + var markedForCompaction bool + var creationTime uint64 + virtualState := struct { + virtual bool + backingFileNum uint64 + }{} + if tag == tagNewFile4 || tag == tagNewFile5 { + for { + customTag, err := d.readUvarint() + if err != nil { + return err + } + 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 + } + switch customTag { + case customTagNeedsCompaction: + if len(field) != 1 { + return base.CorruptionErrorf("new-file4: need-compaction field wrong size") + } + markedForCompaction = (field[0] == 1) + + case customTagCreationTime: + var n int + creationTime, n = binary.Uvarint(field) + if n != len(field) { + return base.CorruptionErrorf("new-file4: invalid file creation time") + } + + case customTagPathID: + return base.CorruptionErrorf("new-file4: path-id field not supported") + + default: + if (customTag & customTagNonSafeIgnoreMask) != 0 { + return base.CorruptionErrorf("new-file4: custom field not supported: %d", customTag) + } + } + } + } + m := &FileMetadata{ + FileNum: fileNum, + Size: size, + CreationTime: int64(creationTime), + SmallestSeqNum: smallestSeqNum, + LargestSeqNum: largestSeqNum, + MarkedForCompaction: markedForCompaction, + Virtual: virtualState.virtual, + } + if tag != tagNewFile5 { // no range keys present + m.SmallestPointKey = base.DecodeInternalKey(smallestPointKey) + m.LargestPointKey = base.DecodeInternalKey(largestPointKey) + m.HasPointKeys = true + m.Smallest, m.Largest = m.SmallestPointKey, m.LargestPointKey + m.boundTypeSmallest, m.boundTypeLargest = boundTypePointKey, boundTypePointKey + } else { // range keys present + // Set point key bounds, if parsed. + if parsedPointBounds { + m.SmallestPointKey = base.DecodeInternalKey(smallestPointKey) + m.LargestPointKey = base.DecodeInternalKey(largestPointKey) + m.HasPointKeys = true + } + // Set range key bounds. + m.SmallestRangeKey = base.DecodeInternalKey(smallestRangeKey) + m.LargestRangeKey = base.DecodeInternalKey(largestRangeKey) + m.HasRangeKeys = true + // Set overall bounds (by default assume range keys). + m.Smallest, m.Largest = m.SmallestRangeKey, m.LargestRangeKey + m.boundTypeSmallest, m.boundTypeLargest = boundTypeRangeKey, boundTypeRangeKey + if boundsMarker&maskSmallest == maskSmallest { + m.Smallest = m.SmallestPointKey + m.boundTypeSmallest = boundTypePointKey + } + if boundsMarker&maskLargest == maskLargest { + m.Largest = m.LargestPointKey + m.boundTypeLargest = boundTypePointKey + } + } + m.boundsSet = true + 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() + if err != nil { + return err + } + v.ObsoletePrevLogNum = n + + case tagColumnFamily, tagColumnFamilyAdd, tagColumnFamilyDrop, tagMaxColumnFamily: + return base.CorruptionErrorf("column families are not supported") + + default: + return errCorruptManifest + } + } + return nil +} + +func (v *VersionEdit) string(verbose bool, fmtKey base.FormatKey) string { + var buf bytes.Buffer + if v.ComparerName != "" { + fmt.Fprintf(&buf, " comparer: %s", v.ComparerName) + } + if v.MinUnflushedLogNum != 0 { + fmt.Fprintf(&buf, " log-num: %d\n", v.MinUnflushedLogNum) + } + if v.ObsoletePrevLogNum != 0 { + fmt.Fprintf(&buf, " prev-log-num: %d\n", v.ObsoletePrevLogNum) + } + if v.NextFileNum != 0 { + fmt.Fprintf(&buf, " next-file-num: %d\n", v.NextFileNum) + } + if v.LastSeqNum != 0 { + fmt.Fprintf(&buf, " last-seq-num: %d\n", v.LastSeqNum) + } + entries := make([]DeletedFileEntry, 0, len(v.DeletedFiles)) + for df := range v.DeletedFiles { + entries = append(entries, df) + } + slices.SortFunc(entries, func(a, b DeletedFileEntry) int { + if v := stdcmp.Compare(a.Level, b.Level); v != 0 { + return v + } + return stdcmp.Compare(a.FileNum, b.FileNum) + }) + for _, df := range entries { + fmt.Fprintf(&buf, " deleted: L%d %s\n", df.Level, df.FileNum) + } + for _, nf := range v.NewFiles { + 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)) + } + fmt.Fprintln(&buf) + } + 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)} + + if v.ComparerName != "" { + e.writeUvarint(tagComparator) + e.writeString(v.ComparerName) + } + if v.MinUnflushedLogNum != 0 { + e.writeUvarint(tagLogNumber) + e.writeUvarint(uint64(v.MinUnflushedLogNum)) + } + if v.ObsoletePrevLogNum != 0 { + e.writeUvarint(tagPrevLogNumber) + e.writeUvarint(v.ObsoletePrevLogNum) + } + if v.NextFileNum != 0 { + 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. + if v.LastSeqNum != 0 || v.ComparerName != "" { + e.writeUvarint(tagLastSequence) + e.writeUvarint(v.LastSeqNum) + } + for x := range v.DeletedFiles { + e.writeUvarint(tagDeletedFile) + e.writeUvarint(uint64(x.Level)) + e.writeUvarint(uint64(x.FileNum)) + } + for _, x := range v.NewFiles { + customFields := x.Meta.MarkedForCompaction || x.Meta.CreationTime != 0 || x.Meta.Virtual + var tag uint64 + switch { + case x.Meta.HasRangeKeys: + tag = tagNewFile5 + case customFields: + tag = tagNewFile4 + default: + tag = tagNewFile2 + } + e.writeUvarint(tag) + e.writeUvarint(uint64(x.Level)) + e.writeUvarint(uint64(x.Meta.FileNum)) + e.writeUvarint(x.Meta.Size) + if !x.Meta.HasRangeKeys { + // If we have no range keys, preserve the original format and write the + // smallest and largest point keys. + e.writeKey(x.Meta.SmallestPointKey) + e.writeKey(x.Meta.LargestPointKey) + } else { + // When range keys are present, we first write a marker byte that + // indicates if the table also contains point keys, in addition to how the + // overall bounds for the table should be reconstructed. This byte is + // followed by the keys themselves. + b, err := x.Meta.boundsMarker() + if err != nil { + return err + } + if err = e.WriteByte(b); err != nil { + return err + } + // Write point key bounds (if present). + if x.Meta.HasPointKeys { + e.writeKey(x.Meta.SmallestPointKey) + e.writeKey(x.Meta.LargestPointKey) + } + // Write range key bounds. + e.writeKey(x.Meta.SmallestRangeKey) + e.writeKey(x.Meta.LargestRangeKey) + } + e.writeUvarint(x.Meta.SmallestSeqNum) + e.writeUvarint(x.Meta.LargestSeqNum) + if customFields { + if x.Meta.CreationTime != 0 { + e.writeUvarint(customTagCreationTime) + var buf [binary.MaxVarintLen64]byte + n := binary.PutUvarint(buf[:], uint64(x.Meta.CreationTime)) + e.writeBytes(buf[:n]) + } + if x.Meta.MarkedForCompaction { + 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) + } + } + _, err := w.Write(e.Bytes()) + return err +} + +// versionEditDecoder should be used to decode version edits. +type versionEditDecoder struct { + byteReader +} + +func (d versionEditDecoder) readBytes() ([]byte, error) { + n, err := d.readUvarint() + if err != nil { + return nil, err + } + s := make([]byte, n) + _, err = io.ReadFull(d, s) + if err != nil { + if err == io.ErrUnexpectedEOF { + return nil, errCorruptManifest + } + return nil, err + } + return s, nil +} + +func (d versionEditDecoder) readLevel() (int, error) { + u, err := d.readUvarint() + if err != nil { + return 0, err + } + if u >= NumLevels { + return 0, errCorruptManifest + } + return int(u), nil +} + +func (d versionEditDecoder) readFileNum() (base.FileNum, error) { + u, err := d.readUvarint() + if err != nil { + return 0, err + } + return base.FileNum(u), nil +} + +func (d versionEditDecoder) readUvarint() (uint64, error) { + u, err := binary.ReadUvarint(d) + if err != nil { + if err == io.EOF { + return 0, errCorruptManifest + } + return 0, err + } + return u, nil +} + +type versionEditEncoder struct { + *bytes.Buffer +} + +func (e versionEditEncoder) writeBytes(p []byte) { + e.writeUvarint(uint64(len(p))) + e.Write(p) +} + +func (e versionEditEncoder) writeKey(k InternalKey) { + e.writeUvarint(uint64(k.Size())) + e.Write(k.UserKey) + buf := k.EncodeTrailer() + e.Write(buf[:]) +} + +func (e versionEditEncoder) writeString(s string) { + e.writeUvarint(uint64(len(s))) + e.WriteString(s) +} + +func (e versionEditEncoder) writeUvarint(u uint64) { + var buf [binary.MaxVarintLen64]byte + n := binary.PutUvarint(buf[:], u) + e.Write(buf[:n]) +} + +// 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]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 + // version edits read from a MANIFEST (as opposed to VersionEdits + // constructed in-memory). While replaying a MANIFEST file, + // VersionEdit.DeletedFiles map entries have nil values, because the + // on-disk deletion record encodes only the file number. Accumulate + // uses AddedByFileNum to correctly populate the BulkVersionEdit's Deleted + // field with non-nil *FileMetadata. + AddedByFileNum map[base.FileNum]*FileMetadata + + // MarkedForCompactionCountDiff holds the aggregated count of files + // marked for compaction added or removed. + MarkedForCompactionCountDiff int +} + +// 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] + if dmap == nil { + dmap = make(map[base.FileNum]*FileMetadata) + b.Deleted[df.Level] = dmap + } + + if m == nil { + // m is nil only when replaying a MANIFEST. + if b.AddedByFileNum == nil { + return errors.Errorf("deleted file L%d.%s's metadata is absent and bve.AddedByFileNum is nil", df.Level, df.FileNum) + } + m = b.AddedByFileNum[df.FileNum] + if m == nil { + return base.CorruptionErrorf("pebble: file deleted L%d.%s before it was inserted", df.Level, df.FileNum) + } + } + if m.MarkedForCompaction { + b.MarkedForCompactionCountDiff-- + } + 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 { + // A new file should not have been deleted in this or a preceding + // VersionEdit at the same level (though files can move across levels). + if dmap := b.Deleted[nf.Level]; dmap != nil { + if _, ok := dmap[nf.Meta.FileNum]; ok { + return base.CorruptionErrorf("pebble: file deleted L%d.%s before it was inserted", nf.Level, nf.Meta.FileNum) + } + } + 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 + } + if nf.Meta.MarkedForCompaction { + 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, 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, + zombies map[base.DiskFileNum]uint64, + orderingInvariants OrderingInvariants, +) (*Version, error) { + addZombie := func(state *FileBacking) { + if zombies != nil { + zombies[state.DiskFileNum] = state.Size + } + } + removeZombie := func(state *FileBacking) { + if zombies != nil { + delete(zombies, state.DiskFileNum) + } + } + + v := new(Version) + + // Adjust the count of files marked for compaction. + if curr != nil { + v.Stats.MarkedForCompaction = curr.Stats.MarkedForCompaction + } + v.Stats.MarkedForCompaction += b.MarkedForCompactionCountDiff + if v.Stats.MarkedForCompaction < 0 { + return nil, base.CorruptionErrorf("pebble: version marked for compaction count negative") + } + + for level := range v.Levels { + if curr == nil || curr.Levels[level].tree.root == nil { + v.Levels[level] = makeLevelMetadata(cmp, level, nil /* files */) + } else { + v.Levels[level] = curr.Levels[level].clone() + } + if curr == nil || curr.RangeKeyLevels[level].tree.root == nil { + v.RangeKeyLevels[level] = makeLevelMetadata(cmp, level, nil /* files */) + } else { + v.RangeKeyLevels[level] = curr.RangeKeyLevels[level].clone() + } + + if len(b.Added[level]) == 0 && len(b.Deleted[level]) == 0 { + // There are no edits on this level. + if level == 0 { + // Initialize L0Sublevels. + if curr == nil || curr.L0Sublevels == nil { + if err := v.InitL0Sublevels(cmp, formatKey, flushSplitBytes); err != nil { + return nil, errors.Wrap(err, "pebble: internal error") + } + } else { + v.L0Sublevels = curr.L0Sublevels + v.L0SublevelFiles = v.L0Sublevels.Levels + } + } + continue + } + + // Some edits on this level. + lm := &v.Levels[level] + lmRange := &v.RangeKeyLevels[level] + + 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(deletedFilesMap))) + } + + // 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 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, err + } + if f.HasRangeKeys { + 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, 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. + slices.SortFunc(addedFiles, func(a, b *FileMetadata) int { + return stdcmp.Compare(a.FileNum, b.FileNum) + }) + + var sm, la *FileMetadata + for _, f := range addedFiles { + // NB: allowedSeeks is used for read triggered compactions. It is set using + // Options.Experimental.ReadCompactionRate which defaults to 32KB. + var allowedSeeks int64 + if readCompactionRate != 0 { + allowedSeeks = int64(f.Size) / readCompactionRate + } + if allowedSeeks < 100 { + allowedSeeks = 100 + } + f.AllowedSeeks.Store(allowedSeeks) + f.InitAllowedSeeks = allowedSeeks + + 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, errors.Wrap(err, "pebble") + } + if f.HasRangeKeys { + err = lmRange.insert(f) + if err != nil { + return nil, errors.Wrap(err, "pebble") + } + } + 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 { + sm = f + } + if la == nil || base.InternalCompare(cmp, la.Largest, f.Largest) < 0 { + la = f + } + } + + if level == 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. + 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, errors.Wrap(err, "pebble: internal error") + } + v.L0SublevelFiles = v.L0Sublevels.Levels + } else if err := v.InitL0Sublevels(cmp, formatKey, flushSplitBytes); err != nil { + return 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 + } + + // Check consistency of the level in the vicinity of our edits. + if sm != nil && la != nil { + overlap := overlaps(v.Levels[level].Iter(), cmp, sm.Smallest.UserKey, + la.Largest.UserKey, la.Largest.IsExclusiveSentinel()) + // overlap contains all of the added files. We want to ensure that + // the added files are consistent with neighboring existing files + // too, so reslice the overlap to pull in a neighbor on each side. + check := overlap.Reslice(func(start, end *LevelIterator) { + if m := start.Prev(); m == nil { + start.Next() + } + if m := end.Next(); m == nil { + end.Prev() + } + }) + if err := CheckOrdering(cmp, formatKey, Level(level), check.Iter(), orderingInvariants); err != nil { + return nil, errors.Wrap(err, "pebble: internal error") + } + } + } + return v, nil +} diff --git a/pebble/internal/manifest/version_edit_test.go b/pebble/internal/manifest/version_edit_test.go new file mode 100644 index 0000000..6d09153 --- /dev/null +++ b/pebble/internal/manifest/version_edit_test.go @@ -0,0 +1,545 @@ +// Copyright 2012 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 manifest + +import ( + "bytes" + "fmt" + "io" + "os" + "reflect" + "slices" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/record" + "github.com/kr/pretty" + "github.com/stretchr/testify/require" +) + +func checkRoundTrip(e0 VersionEdit) error { + var e1 VersionEdit + buf := new(bytes.Buffer) + if err := e0.Encode(buf); err != nil { + return errors.Wrap(err, "encode") + } + if err := e1.Decode(buf); err != nil { + return errors.Wrap(err, "decode") + } + if diff := pretty.Diff(e0, e1); diff != nil { + return errors.Errorf("%s", strings.Join(diff, "\n")) + } + return nil +} + +// Version edits with virtual sstables will not be the same after a round trip +// as the Decode function will not set the FileBacking for a virtual sstable. +// We test round trip + bve accumulation here, after which the virtual sstable +// FileBacking should be set. +func TestVERoundTripAndAccumulate(t *testing.T) { + cmp := base.DefaultComparer.Compare + m1 := (&FileMetadata{ + FileNum: 810, + Size: 8090, + CreationTime: 809060, + SmallestSeqNum: 9, + LargestSeqNum: 11, + }).ExtendPointKeyBounds( + cmp, + base.MakeInternalKey([]byte("a"), 0, base.InternalKeyKindSet), + base.MakeInternalKey([]byte("m"), 0, base.InternalKeyKindSet), + ).ExtendRangeKeyBounds( + cmp, + base.MakeInternalKey([]byte("l"), 0, base.InternalKeyKindRangeKeySet), + base.MakeExclusiveSentinelKey(base.InternalKeyKindRangeKeySet, []byte("z")), + ) + m1.InitPhysicalBacking() + + m2 := (&FileMetadata{ + FileNum: 812, + Size: 8090, + CreationTime: 809060, + SmallestSeqNum: 9, + LargestSeqNum: 11, + Virtual: true, + FileBacking: m1.FileBacking, + }).ExtendPointKeyBounds( + cmp, + base.MakeInternalKey([]byte("a"), 0, base.InternalKeyKindSet), + base.MakeInternalKey([]byte("c"), 0, base.InternalKeyKindSet), + ) + + ve1 := VersionEdit{ + ComparerName: "11", + MinUnflushedLogNum: 22, + ObsoletePrevLogNum: 33, + NextFileNum: 44, + LastSeqNum: 55, + CreatedBackingTables: []*FileBacking{m1.FileBacking}, + NewFiles: []NewFileEntry{ + { + Level: 4, + Meta: m2, + // Only set for the test. + BackingFileNum: m2.FileBacking.DiskFileNum, + }, + }, + } + var err error + buf := new(bytes.Buffer) + if err = ve1.Encode(buf); err != nil { + t.Error(err) + } + var ve2 VersionEdit + if err = ve2.Decode(buf); err != nil { + t.Error(err) + } + // Perform accumulation to set the FileBacking on the files in the Decoded + // version edit. + var bve BulkVersionEdit + require.NoError(t, bve.Accumulate(&ve2)) + if diff := pretty.Diff(ve1, ve2); diff != nil { + t.Error(errors.Errorf("%s", strings.Join(diff, "\n"))) + } +} + +func TestVersionEditRoundTrip(t *testing.T) { + cmp := base.DefaultComparer.Compare + m1 := (&FileMetadata{ + FileNum: 805, + Size: 8050, + CreationTime: 805030, + }).ExtendPointKeyBounds( + cmp, + base.DecodeInternalKey([]byte("abc\x00\x01\x02\x03\x04\x05\x06\x07")), + base.DecodeInternalKey([]byte("xyz\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")), + ) + m1.InitPhysicalBacking() + + m2 := (&FileMetadata{ + FileNum: 806, + Size: 8060, + CreationTime: 806040, + SmallestSeqNum: 3, + LargestSeqNum: 5, + MarkedForCompaction: true, + }).ExtendPointKeyBounds( + cmp, + base.DecodeInternalKey([]byte("A\x00\x01\x02\x03\x04\x05\x06\x07")), + base.DecodeInternalKey([]byte("Z\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")), + ) + m2.InitPhysicalBacking() + + m3 := (&FileMetadata{ + FileNum: 807, + Size: 8070, + CreationTime: 807050, + }).ExtendRangeKeyBounds( + cmp, + base.MakeInternalKey([]byte("aaa"), 0, base.InternalKeyKindRangeKeySet), + base.MakeExclusiveSentinelKey(base.InternalKeyKindRangeKeySet, []byte("zzz")), + ) + m3.InitPhysicalBacking() + + m4 := (&FileMetadata{ + FileNum: 809, + Size: 8090, + CreationTime: 809060, + SmallestSeqNum: 9, + LargestSeqNum: 11, + }).ExtendPointKeyBounds( + cmp, + base.MakeInternalKey([]byte("a"), 0, base.InternalKeyKindSet), + base.MakeInternalKey([]byte("m"), 0, base.InternalKeyKindSet), + ).ExtendRangeKeyBounds( + cmp, + base.MakeInternalKey([]byte("l"), 0, base.InternalKeyKindRangeKeySet), + base.MakeExclusiveSentinelKey(base.InternalKeyKindRangeKeySet, []byte("z")), + ) + m4.InitPhysicalBacking() + + m5 := (&FileMetadata{ + FileNum: 810, + Size: 8090, + CreationTime: 809060, + SmallestSeqNum: 9, + LargestSeqNum: 11, + }).ExtendPointKeyBounds( + cmp, + base.MakeInternalKey([]byte("a"), 0, base.InternalKeyKindSet), + base.MakeInternalKey([]byte("m"), 0, base.InternalKeyKindSet), + ).ExtendRangeKeyBounds( + cmp, + base.MakeInternalKey([]byte("l"), 0, base.InternalKeyKindRangeKeySet), + base.MakeExclusiveSentinelKey(base.InternalKeyKindRangeKeySet, []byte("z")), + ) + m5.InitPhysicalBacking() + + m6 := (&FileMetadata{ + FileNum: 811, + Size: 8090, + CreationTime: 809060, + SmallestSeqNum: 9, + LargestSeqNum: 11, + }).ExtendPointKeyBounds( + cmp, + base.MakeInternalKey([]byte("a"), 0, base.InternalKeyKindSet), + base.MakeInternalKey([]byte("m"), 0, base.InternalKeyKindSet), + ).ExtendRangeKeyBounds( + cmp, + base.MakeInternalKey([]byte("l"), 0, base.InternalKeyKindRangeKeySet), + base.MakeExclusiveSentinelKey(base.InternalKeyKindRangeKeySet, []byte("z")), + ) + m6.InitPhysicalBacking() + + testCases := []VersionEdit{ + // An empty version edit. + {}, + // A complete version edit. + { + ComparerName: "11", + MinUnflushedLogNum: 22, + ObsoletePrevLogNum: 33, + NextFileNum: 44, + LastSeqNum: 55, + RemovedBackingTables: []base.DiskFileNum{ + base.FileNum(10).DiskFileNum(), base.FileNum(11).DiskFileNum(), + }, + CreatedBackingTables: []*FileBacking{m5.FileBacking, m6.FileBacking}, + DeletedFiles: map[DeletedFileEntry]*FileMetadata{ + { + Level: 3, + FileNum: 703, + }: nil, + { + Level: 4, + FileNum: 704, + }: nil, + }, + NewFiles: []NewFileEntry{ + { + Level: 4, + Meta: m1, + }, + { + Level: 5, + Meta: m2, + }, + { + Level: 6, + Meta: m3, + }, + { + Level: 6, + Meta: m4, + }, + }, + }, + } + for _, tc := range testCases { + if err := checkRoundTrip(tc); err != nil { + t.Error(err) + } + } +} + +func TestVersionEditDecode(t *testing.T) { + // TODO(radu): these should be datadriven tests that output the encoded and + // decoded edits. + cmp := base.DefaultComparer.Compare + m := (&FileMetadata{ + FileNum: 4, + Size: 709, + SmallestSeqNum: 12, + LargestSeqNum: 14, + CreationTime: 1701712644, + }).ExtendPointKeyBounds( + cmp, + base.MakeInternalKey([]byte("bar"), 14, base.InternalKeyKindDelete), + base.MakeInternalKey([]byte("foo"), 13, base.InternalKeyKindSet), + ) + m.InitPhysicalBacking() + + testCases := []struct { + filename string + encodedEdits []string + edits []VersionEdit + }{ + // db-stage-1 and db-stage-2 have the same manifest. + { + filename: "db-stage-1/MANIFEST-000001", + encodedEdits: []string{ + "\x01\x1aleveldb.BytewiseComparator\x03\x02\x04\x00", + "\x02\x02\x03\x03\x04\t", + }, + edits: []VersionEdit{ + { + ComparerName: "leveldb.BytewiseComparator", + NextFileNum: 2, + }, + { + MinUnflushedLogNum: 0x2, + NextFileNum: 0x3, + LastSeqNum: 0x9, + }, + }, + }, + // db-stage-3 and db-stage-4 have the same manifest. + { + filename: "db-stage-3/MANIFEST-000006", + encodedEdits: []string{ + "\x01\x1aleveldb.BytewiseComparator\x02\x02\x03\a\x04\x00", + "\x02\x05\x03\x06\x04\x0eg\x00\x04\xc5\x05\vbar\x00\x0e\x00\x00\x00\x00\x00\x00\vfoo\x01\r\x00\x00\x00\x00\x00\x00\f\x0e\x06\x05\x84\xa6\xb8\xab\x06\x01", + }, + edits: []VersionEdit{ + { + ComparerName: "leveldb.BytewiseComparator", + MinUnflushedLogNum: 0x2, + NextFileNum: 0x7, + }, + { + MinUnflushedLogNum: 0x5, + NextFileNum: 0x6, + LastSeqNum: 0xe, + NewFiles: []NewFileEntry{ + { + Level: 0, + Meta: m, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + f, err := os.Open("../../testdata/" + tc.filename) + if err != nil { + t.Fatalf("filename=%q: open error: %v", tc.filename, err) + } + defer f.Close() + i, r := 0, record.NewReader(f, 0 /* logNum */) + for { + rr, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("filename=%q i=%d: record reader error: %v", tc.filename, i, err) + } + if i >= len(tc.edits) { + t.Fatalf("filename=%q i=%d: too many version edits", tc.filename, i+1) + } + + encodedEdit, err := io.ReadAll(rr) + if err != nil { + t.Fatalf("filename=%q i=%d: read error: %v", tc.filename, i, err) + } + if s := string(encodedEdit); s != tc.encodedEdits[i] { + t.Fatalf("filename=%q i=%d: got encoded %q, want %q", tc.filename, i, s, tc.encodedEdits[i]) + } + + var edit VersionEdit + err = edit.Decode(bytes.NewReader(encodedEdit)) + if err != nil { + t.Fatalf("filename=%q i=%d: decode error: %v", tc.filename, i, err) + } + if !reflect.DeepEqual(edit, tc.edits[i]) { + t.Fatalf("filename=%q i=%d: decode\n\tgot %#v\n\twant %#v\n%s", tc.filename, i, edit, tc.edits[i], + strings.Join(pretty.Diff(edit, tc.edits[i]), "\n")) + } + if err := checkRoundTrip(edit); err != nil { + t.Fatalf("filename=%q i=%d: round trip: %v", tc.filename, i, err) + } + + i++ + } + if i != len(tc.edits) { + t.Fatalf("filename=%q: got %d edits, want %d", tc.filename, i, len(tc.edits)) + } + }) + } +} + +func TestVersionEditEncodeLastSeqNum(t *testing.T) { + testCases := []struct { + edit VersionEdit + encoded string + }{ + // If ComparerName is unset, LastSeqNum is only encoded if non-zero. + {VersionEdit{LastSeqNum: 0}, ""}, + {VersionEdit{LastSeqNum: 1}, "\x04\x01"}, + // For compatibility with RocksDB, if ComparerName is set we always encode + // LastSeqNum. + {VersionEdit{ComparerName: "foo", LastSeqNum: 0}, "\x01\x03\x66\x6f\x6f\x04\x00"}, + {VersionEdit{ComparerName: "foo", LastSeqNum: 1}, "\x01\x03\x66\x6f\x6f\x04\x01"}, + } + for _, c := range testCases { + t.Run("", func(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, c.edit.Encode(&buf)) + if result := buf.String(); c.encoded != result { + t.Fatalf("expected %x, but found %x", c.encoded, result) + } + + if c.edit.ComparerName != "" { + // Manually decode the version edit so that we can verify the contents + // even if the LastSeqNum decodes to 0. + d := versionEditDecoder{strings.NewReader(c.encoded)} + + // Decode ComparerName. + tag, err := d.readUvarint() + require.NoError(t, err) + if tag != tagComparator { + t.Fatalf("expected %d, but found %d", tagComparator, tag) + } + s, err := d.readBytes() + require.NoError(t, err) + if c.edit.ComparerName != string(s) { + t.Fatalf("expected %q, but found %q", c.edit.ComparerName, s) + } + + // Decode LastSeqNum. + tag, err = d.readUvarint() + require.NoError(t, err) + if tag != tagLastSequence { + t.Fatalf("expected %d, but found %d", tagLastSequence, tag) + } + val, err := d.readUvarint() + require.NoError(t, err) + if c.edit.LastSeqNum != val { + t.Fatalf("expected %d, but found %d", c.edit.LastSeqNum, val) + } + } + }) + } +} + +func TestVersionEditApply(t *testing.T) { + parseMeta := func(s string) (*FileMetadata, error) { + m, err := ParseFileMetadataDebug(s) + if err != nil { + return nil, err + } + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + if m.SmallestSeqNum > m.LargestSeqNum { + m.SmallestSeqNum, m.LargestSeqNum = m.LargestSeqNum, m.SmallestSeqNum + } + m.InitPhysicalBacking() + return m, nil + } + + // TODO(bananabrick): Improve the parsing logic in this test. + datadriven.RunTest(t, "testdata/version_edit_apply", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "apply": + // TODO(sumeer): move this Version parsing code to utils, to + // avoid repeating it, and make it the inverse of + // Version.DebugString(). + var v *Version + var veList []*VersionEdit + isVersion := true + isDelete := true + var level int + var err error + versionFiles := map[base.FileNum]*FileMetadata{} + for _, data := range strings.Split(d.Input, "\n") { + data = strings.TrimSpace(data) + switch data { + case "edit": + isVersion = false + veList = append(veList, &VersionEdit{}) + case "delete": + isVersion = false + isDelete = true + case "add": + isVersion = false + isDelete = false + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err = strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + default: + var ve *VersionEdit + if len(veList) > 0 { + ve = veList[len(veList)-1] + } + if isVersion || !isDelete { + meta, err := parseMeta(data) + if err != nil { + return err.Error() + } + if isVersion { + if v == nil { + v = new(Version) + for l := 0; l < NumLevels; l++ { + v.Levels[l] = makeLevelMetadata(base.DefaultComparer.Compare, l, nil /* files */) + } + } + versionFiles[meta.FileNum] = meta + v.Levels[level].insert(meta) + meta.LatestRef() + } else { + ve.NewFiles = + append(ve.NewFiles, NewFileEntry{Level: level, Meta: meta}) + } + } else { + fileNum, err := strconv.Atoi(data) + if err != nil { + return err.Error() + } + dfe := DeletedFileEntry{Level: level, FileNum: base.FileNum(fileNum)} + if ve.DeletedFiles == nil { + ve.DeletedFiles = make(map[DeletedFileEntry]*FileMetadata) + } + ve.DeletedFiles[dfe] = versionFiles[dfe.FileNum] + } + } + } + + if v != nil { + if err := v.InitL0Sublevels(base.DefaultComparer.Compare, base.DefaultFormatter, 10<<20); err != nil { + return err.Error() + } + } + + bve := BulkVersionEdit{} + bve.AddedByFileNum = make(map[base.FileNum]*FileMetadata) + for _, ve := range veList { + if err := bve.Accumulate(ve); err != nil { + return err.Error() + } + } + zombies := make(map[base.DiskFileNum]uint64) + newv, err := bve.Apply(v, base.DefaultComparer.Compare, base.DefaultFormatter, 10<<20, 32000, zombies, ProhibitSplitUserKeys) + if err != nil { + return err.Error() + } + + zombieFileNums := make([]base.DiskFileNum, 0, len(zombies)) + if len(veList) == 1 { + // Only care about zombies if a single version edit was + // being applied. + for fileNum := range zombies { + zombieFileNums = append(zombieFileNums, fileNum) + } + slices.Sort(zombieFileNums) + } + + return fmt.Sprintf("%szombies %d\n", newv, zombieFileNums) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} diff --git a/pebble/internal/manifest/version_test.go b/pebble/internal/manifest/version_test.go new file mode 100644 index 0000000..abde613 --- /dev/null +++ b/pebble/internal/manifest/version_test.go @@ -0,0 +1,429 @@ +// Copyright 2012 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 manifest + +import ( + "bytes" + "fmt" + "strings" + "sync" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/stretchr/testify/require" +) + +func levelMetadata(level int, files ...*FileMetadata) LevelMetadata { + return makeLevelMetadata(base.DefaultComparer.Compare, level, files) +} + +func ikey(s string) InternalKey { + return base.MakeInternalKey([]byte(s), 0, base.InternalKeyKindSet) +} + +func TestIkeyRange(t *testing.T) { + cmp := base.DefaultComparer.Compare + testCases := []struct { + input, want string + }{ + { + "", + "-", + }, + { + "a-e", + "a-e", + }, + { + "a-e a-e", + "a-e", + }, + { + "c-g a-e", + "a-g", + }, + { + "a-e c-g a-e", + "a-g", + }, + { + "b-d f-g", + "b-g", + }, + { + "d-e b-d", + "b-e", + }, + { + "e-e", + "e-e", + }, + { + "f-g e-e d-e c-g b-d a-e", + "a-g", + }, + } + for _, tc := range testCases { + var f []*FileMetadata + if tc.input != "" { + for i, s := range strings.Split(tc.input, " ") { + m := (&FileMetadata{ + FileNum: base.FileNum(i), + }).ExtendPointKeyBounds(cmp, ikey(s[0:1]), ikey(s[2:3])) + m.InitPhysicalBacking() + f = append(f, m) + } + } + levelMetadata := makeLevelMetadata(base.DefaultComparer.Compare, 0, f) + + sm, la := KeyRange(base.DefaultComparer.Compare, levelMetadata.Iter()) + got := string(sm.UserKey) + "-" + string(la.UserKey) + if got != tc.want { + t.Errorf("KeyRange(%q) = %q, %q", tc.input, got, tc.want) + } + } +} + +func TestOverlaps(t *testing.T) { + var v *Version + cmp := testkeys.Comparer.Compare + fmtKey := testkeys.Comparer.FormatKey + datadriven.RunTest(t, "testdata/overlaps", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + var err error + v, err = ParseVersionDebug(cmp, fmtKey, 64>>10 /* flush split bytes */, d.Input) + if err != nil { + return err.Error() + } + return v.String() + case "overlaps": + var level int + var start, end string + var exclusiveEnd bool + d.ScanArgs(t, "level", &level) + d.ScanArgs(t, "start", &start) + d.ScanArgs(t, "end", &end) + d.ScanArgs(t, "exclusive-end", &exclusiveEnd) + overlaps := v.Overlaps(level, testkeys.Comparer.Compare, []byte(start), []byte(end), exclusiveEnd) + var buf bytes.Buffer + fmt.Fprintf(&buf, "%d files:\n", overlaps.Len()) + overlaps.Each(func(f *FileMetadata) { + fmt.Fprintf(&buf, "%s\n", f.DebugString(base.DefaultFormatter, false)) + }) + return buf.String() + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestContains(t *testing.T) { + cmp := base.DefaultComparer.Compare + newFileMeta := func(fileNum base.FileNum, size uint64, smallest, largest base.InternalKey) *FileMetadata { + m := (&FileMetadata{ + FileNum: fileNum, + Size: size, + }).ExtendPointKeyBounds(cmp, smallest, largest) + m.InitPhysicalBacking() + return m + } + m00 := newFileMeta( + 700, + 1, + base.ParseInternalKey("b.SET.7008"), + base.ParseInternalKey("e.SET.7009"), + ) + m01 := newFileMeta( + 701, + 1, + base.ParseInternalKey("c.SET.7018"), + base.ParseInternalKey("f.SET.7019"), + ) + m02 := newFileMeta( + 702, + 1, + base.ParseInternalKey("f.SET.7028"), + base.ParseInternalKey("g.SET.7029"), + ) + m03 := newFileMeta( + 703, + 1, + base.ParseInternalKey("x.SET.7038"), + base.ParseInternalKey("y.SET.7039"), + ) + m04 := newFileMeta( + 704, + 1, + base.ParseInternalKey("n.SET.7048"), + base.ParseInternalKey("p.SET.7049"), + ) + m05 := newFileMeta( + 705, + 1, + base.ParseInternalKey("p.SET.7058"), + base.ParseInternalKey("p.SET.7059"), + ) + m06 := newFileMeta( + 706, + 1, + base.ParseInternalKey("p.SET.7068"), + base.ParseInternalKey("u.SET.7069"), + ) + m07 := newFileMeta( + 707, + 1, + base.ParseInternalKey("r.SET.7078"), + base.ParseInternalKey("s.SET.7079"), + ) + + m10 := newFileMeta( + 710, + 1, + base.ParseInternalKey("d.SET.7108"), + base.ParseInternalKey("g.SET.7109"), + ) + m11 := newFileMeta( + 711, + 1, + base.ParseInternalKey("g.SET.7118"), + base.ParseInternalKey("j.SET.7119"), + ) + m12 := newFileMeta( + 712, + 1, + base.ParseInternalKey("n.SET.7128"), + base.ParseInternalKey("p.SET.7129"), + ) + m13 := newFileMeta( + 713, + 1, + base.ParseInternalKey("p.SET.7148"), + base.ParseInternalKey("p.SET.7149"), + ) + m14 := newFileMeta( + 714, + 1, + base.ParseInternalKey("p.SET.7138"), + base.ParseInternalKey("u.SET.7139"), + ) + + v := Version{ + Levels: [NumLevels]LevelMetadata{ + 0: levelMetadata(0, m00, m01, m02, m03, m04, m05, m06, m07), + 1: levelMetadata(1, m10, m11, m12, m13, m14), + }, + } + + testCases := []struct { + level int + file *FileMetadata + want bool + }{ + // Level 0: m00=b-e, m01=c-f, m02=f-g, m03=x-y, m04=n-p, m05=p-p, m06=p-u, m07=r-s. + // Note that: + // - the slice isn't sorted (e.g. m02=f-g, m03=x-y, m04=n-p), + // - m00 and m01 overlap (not just touch), + // - m06 contains m07, + // - m00, m01 and m02 transitively overlap/touch each other, and + // - m04, m05, m06 and m07 transitively overlap/touch each other. + {0, m00, true}, + {0, m01, true}, + {0, m02, true}, + {0, m03, true}, + {0, m04, true}, + {0, m05, true}, + {0, m06, true}, + {0, m07, true}, + {0, m10, false}, + {0, m11, false}, + {0, m12, false}, + {0, m13, false}, + {0, m14, false}, + {1, m00, false}, + {1, m01, false}, + {1, m02, false}, + {1, m03, false}, + {1, m04, false}, + {1, m05, false}, + {1, m06, false}, + {1, m07, false}, + {1, m10, true}, + {1, m11, true}, + {1, m12, true}, + {1, m13, true}, + {1, m14, true}, + + // Level 2: empty. + {2, m00, false}, + {2, m14, false}, + } + + for _, tc := range testCases { + got := v.Contains(tc.level, cmp, tc.file) + if got != tc.want { + t.Errorf("level=%d, file=%s\ngot %t\nwant %t", tc.level, tc.file, got, tc.want) + } + } +} + +func TestVersionUnref(t *testing.T) { + list := &VersionList{} + list.Init(&sync.Mutex{}) + v := &Version{Deleted: func([]*FileBacking) {}} + v.Ref() + list.PushBack(v) + v.Unref() + if !list.Empty() { + t.Fatalf("expected version list to be empty") + } +} + +func TestCheckOrdering(t *testing.T) { + cmp := base.DefaultComparer.Compare + fmtKey := base.DefaultComparer.FormatKey + datadriven.RunTest(t, "testdata/version_check_ordering", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "check-ordering": + orderingInvariants := ProhibitSplitUserKeys + if d.HasArg("allow-split-user-keys") { + orderingInvariants = AllowSplitUserKeys + } + v, err := ParseVersionDebug(cmp, fmtKey, 10<<20, d.Input) + if err != nil { + return err.Error() + } + // L0 files compare on sequence numbers. Use the seqnums from the + // smallest / largest bounds for the table. + v.Levels[0].Slice().Each(func(m *FileMetadata) { + m.SmallestSeqNum = m.Smallest.SeqNum() + m.LargestSeqNum = m.Largest.SeqNum() + }) + if err = v.CheckOrdering(cmp, base.DefaultFormatter, orderingInvariants); err != nil { + return err.Error() + } + return "OK" + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestExtendBounds(t *testing.T) { + cmp := base.DefaultComparer.Compare + parseBounds := func(line string) (lower, upper InternalKey) { + parts := strings.Split(line, "-") + if len(parts) == 1 { + parts = strings.Split(parts[0], ":") + start, end := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + lower = base.ParseInternalKey(start) + switch k := lower.Kind(); k { + case base.InternalKeyKindRangeDelete: + upper = base.MakeRangeDeleteSentinelKey([]byte(end)) + case base.InternalKeyKindRangeKeySet, base.InternalKeyKindRangeKeyUnset, base.InternalKeyKindRangeKeyDelete: + upper = base.MakeExclusiveSentinelKey(k, []byte(end)) + default: + panic(fmt.Sprintf("unknown kind %s with end key", k)) + } + } else { + l, u := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + lower, upper = base.ParseInternalKey(l), base.ParseInternalKey(u) + } + return + } + format := func(m *FileMetadata) string { + var b bytes.Buffer + var smallest, largest string + switch m.boundTypeSmallest { + case boundTypePointKey: + smallest = "point" + case boundTypeRangeKey: + smallest = "range" + default: + return fmt.Sprintf("unknown bound type %d", m.boundTypeSmallest) + } + switch m.boundTypeLargest { + case boundTypePointKey: + largest = "point" + case boundTypeRangeKey: + largest = "range" + default: + return fmt.Sprintf("unknown bound type %d", m.boundTypeLargest) + } + bounds, err := m.boundsMarker() + if err != nil { + panic(err) + } + fmt.Fprintf(&b, "%s\n", m.DebugString(base.DefaultFormatter, true)) + fmt.Fprintf(&b, " bounds: (smallest=%s,largest=%s) (0x%08b)\n", smallest, largest, bounds) + return b.String() + } + m := &FileMetadata{} + datadriven.RunTest(t, "testdata/file_metadata_bounds", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "reset": + m = &FileMetadata{} + return "" + case "extend-point-key-bounds": + u, l := parseBounds(d.Input) + m.ExtendPointKeyBounds(cmp, u, l) + return format(m) + case "extend-range-key-bounds": + u, l := parseBounds(d.Input) + m.ExtendRangeKeyBounds(cmp, u, l) + return format(m) + default: + return fmt.Sprintf("unknown command %s\n", d.Cmd) + } + }) +} + +func TestFileMetadata_ParseRoundTrip(t *testing.T) { + testCases := []struct { + name string + input string + output string + }{ + { + name: "point keys only", + input: "000001:[a#0,SET-z#0,DEL] seqnums:[0-0] points:[a#0,SET-z#0,DEL]", + }, + { + name: "range keys only", + input: "000001:[a#0,RANGEKEYSET-z#0,RANGEKEYDEL] seqnums:[0-0] ranges:[a#0,RANGEKEYSET-z#0,RANGEKEYDEL]", + }, + { + name: "point and range keys", + input: "000001:[a#0,RANGEKEYSET-d#0,DEL] seqnums:[0-0] points:[b#0,SET-d#0,DEL] ranges:[a#0,RANGEKEYSET-c#0,RANGEKEYDEL]", + }, + { + name: "point and range keys with nonzero senums", + input: "000001:[a#3,RANGEKEYSET-d#4,DEL] seqnums:[3-7] points:[b#3,SET-d#4,DEL] ranges:[a#3,RANGEKEYSET-c#5,RANGEKEYDEL]", + }, + { + name: "whitespace", + input: " 000001 : [ a#0,SET - z#0,DEL] points : [ a#0,SET - z#0,DEL] ", + output: "000001:[a#0,SET-z#0,DEL] seqnums:[0-0] points:[a#0,SET-z#0,DEL]", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m, err := ParseFileMetadataDebug(tc.input) + require.NoError(t, err) + err = m.Validate(base.DefaultComparer.Compare, base.DefaultFormatter) + require.NoError(t, err) + got := m.DebugString(base.DefaultFormatter, true) + want := tc.input + if tc.output != "" { + want = tc.output + } + require.Equal(t, want, got) + }) + } +} diff --git a/pebble/internal/manual/manual.go b/pebble/internal/manual/manual.go new file mode 100644 index 0000000..640816a --- /dev/null +++ b/pebble/internal/manual/manual.go @@ -0,0 +1,60 @@ +// Copyright 2020 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 manual + +// #include +import "C" +import "unsafe" + +// The go:linkname directives provides backdoor access to private functions in +// the runtime. Below we're accessing the throw function. + +//go:linkname throw runtime.throw +func throw(s string) + +// TODO(peter): Rather than relying an C malloc/free, we could fork the Go +// runtime page allocator and allocate large chunks of memory using mmap or +// similar. + +// New allocates a slice of size n. The returned slice is from manually managed +// memory and MUST be released by calling Free. Failure to do so will result in +// a memory leak. +func New(n int) []byte { + if n == 0 { + return make([]byte, 0) + } + // We need to be conscious of the Cgo pointer passing rules: + // + // https://golang.org/cmd/cgo/#hdr-Passing_pointers + // + // ... + // Note: the current implementation has a bug. While Go code is permitted + // to write nil or a C pointer (but not a Go pointer) to C memory, the + // current implementation may sometimes cause a runtime error if the + // contents of the C memory appear to be a Go pointer. Therefore, avoid + // passing uninitialized C memory to Go code if the Go code is going to + // store pointer values in it. Zero out the memory in C before passing it + // to Go. + ptr := C.calloc(C.size_t(n), 1) + if ptr == nil { + // NB: throw is like panic, except it guarantees the process will be + // terminated. The call below is exactly what the Go runtime invokes when + // it cannot allocate memory. + throw("out of memory") + } + // Interpret the C pointer as a pointer to a Go array, then slice. + return (*[MaxArrayLen]byte)(unsafe.Pointer(ptr))[:n:n] +} + +// Free frees the specified slice. +func Free(b []byte) { + if cap(b) != 0 { + if len(b) == 0 { + b = b[:cap(b)] + } + ptr := unsafe.Pointer(&b[0]) + C.free(ptr) + } +} diff --git a/pebble/internal/manual/manual_32bit.go b/pebble/internal/manual/manual_32bit.go new file mode 100644 index 0000000..19369fa --- /dev/null +++ b/pebble/internal/manual/manual_32bit.go @@ -0,0 +1,13 @@ +// Copyright 2020 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 386 || amd64p32 || arm || armbe || ppc || sparc +// +build 386 amd64p32 arm armbe ppc sparc + +package manual + +const ( + // MaxArrayLen is a safe maximum length for slices on this architecture. + MaxArrayLen = 1<<31 - 1 +) diff --git a/pebble/internal/manual/manual_64bit.go b/pebble/internal/manual/manual_64bit.go new file mode 100644 index 0000000..8c08232 --- /dev/null +++ b/pebble/internal/manual/manual_64bit.go @@ -0,0 +1,13 @@ +// Copyright 2020 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 amd64 || arm64 || arm64be || ppc64 || ppc64le || mips64 || mips64le || s390x || sparc64 || riscv64 +// +build amd64 arm64 arm64be ppc64 ppc64le mips64 mips64le s390x sparc64 riscv64 + +package manual + +const ( + // MaxArrayLen is a safe maximum length for slices on this architecture. + MaxArrayLen = 1<<50 - 1 +) diff --git a/pebble/internal/manual/manual_mips.go b/pebble/internal/manual/manual_mips.go new file mode 100644 index 0000000..08bb880 --- /dev/null +++ b/pebble/internal/manual/manual_mips.go @@ -0,0 +1,13 @@ +// Copyright 2020 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 mips || mipsle || mips64p32 || mips64p32le +// +build mips mipsle mips64p32 mips64p32le + +package manual + +const ( + // MaxArrayLen is a safe maximum length for slices on this architecture. + MaxArrayLen = 1 << 30 +) diff --git a/pebble/internal/manual/manual_nocgo.go b/pebble/internal/manual/manual_nocgo.go new file mode 100644 index 0000000..74befbd --- /dev/null +++ b/pebble/internal/manual/manual_nocgo.go @@ -0,0 +1,20 @@ +// Copyright 2020 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 !cgo +// +build !cgo + +package manual + +// Provides versions of New and Free when cgo is not available (e.g. cross +// compilation). + +// New allocates a slice of size n. +func New(n int) []byte { + return make([]byte, n) +} + +// Free frees the specified slice. +func Free(b []byte) { +} diff --git a/pebble/internal/metamorphic/.gitignore b/pebble/internal/metamorphic/.gitignore new file mode 100644 index 0000000..33a7810 --- /dev/null +++ b/pebble/internal/metamorphic/.gitignore @@ -0,0 +1,2 @@ +_meta/ +*.test diff --git a/pebble/internal/metamorphic/crossversion/crossversion_test.go b/pebble/internal/metamorphic/crossversion/crossversion_test.go new file mode 100644 index 0000000..192140e --- /dev/null +++ b/pebble/internal/metamorphic/crossversion/crossversion_test.go @@ -0,0 +1,409 @@ +// 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 crossversion builds on the metamorphic testing implemented in +// internal/metamorphic, performing metamorphic testing across versions of +// Pebble. This improves test coverage of upgrade and migration code paths. +package crossversion + +import ( + "bytes" + "context" + "flag" + "fmt" + "io" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + "time" + "unicode" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/metamorphic" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +var ( + factor int + seed int64 + versions pebbleVersions + artifactsDir string + streamOutput bool +) + +func init() { + // NB: If you add new command-line flags, you should update the + // reproductionCommand function. + flag.Int64Var(&seed, "seed", 0, + `a pseudorandom number generator seed`) + flag.IntVar(&factor, "factor", 10, + `the number of data directories to carry forward +from one version's run to the subsequent version's runs.`) + flag.Var(&versions, "version", + `a comma-separated 3-tuple defining a Pebble version to test. +The expected format is

/ops. These will be + // read by the child processes when performing a test run. + km := newKeyManager(runOpts.numInstances) + cfg := presetConfigs[rng.Intn(len(presetConfigs))] + if runOpts.previousOpsPath != "" { + // During cross-version testing, we load keys from an `ops` file + // produced by a metamorphic test run of an earlier Pebble version. + // Seeding the keys ensure we generate interesting operations, including + // ones with key shadowing, merging, etc. + opsPath := filepath.Join(filepath.Dir(filepath.Clean(runOpts.previousOpsPath)), "ops") + opsData, err := os.ReadFile(opsPath) + require.NoError(t, err) + ops, err := parse(opsData, parserOpts{}) + require.NoError(t, err) + loadPrecedingKeys(t, ops, &cfg, km) + } + if runOpts.numInstances > 1 { + // The multi-instance variant does not support all operations yet. + // + // TODO(bilal): Address this and use the default configs. + cfg = multiInstancePresetConfig + cfg.numInstances = runOpts.numInstances + } + ops := generate(rng, opCount, cfg, km) + opsPath := filepath.Join(metaDir, "ops") + formattedOps := formatOps(ops) + require.NoError(t, os.WriteFile(opsPath, []byte(formattedOps), 0644)) + + // runOptions performs a particular test run with the specified options. The + // options are written to /OPTIONS and a child process is created to + // actually execute the test. + runOptions := func(t *testing.T, opts *TestOptions) { + if opts.Opts.Cache != nil { + defer opts.Opts.Cache.Unref() + } + for _, fn := range runOpts.mutateTestOptions { + fn(opts) + } + runDir := filepath.Join(metaDir, path.Base(t.Name())) + require.NoError(t, os.MkdirAll(runDir, 0755)) + + optionsPath := filepath.Join(runDir, "OPTIONS") + optionsStr := optionsToString(opts) + require.NoError(t, os.WriteFile(optionsPath, []byte(optionsStr), 0644)) + + args := []string{ + "-keep=" + fmt.Sprint(runOpts.keep), + "-run-dir=" + runDir, + "-test.run=" + t.Name() + "$", + } + if runOpts.numInstances > 1 { + args = append(args, "--num-instances="+strconv.Itoa(runOpts.numInstances)) + } + if runOpts.traceFile != "" { + args = append(args, "-test.trace="+filepath.Join(runDir, runOpts.traceFile)) + } + + binary := os.Args[0] + if runOpts.innerBinary != "" { + binary = runOpts.innerBinary + } + cmd := exec.Command(binary, args...) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf(` +===== SEED ===== +%d +===== ERR ===== +%v +===== OUT ===== +%s +===== OPTIONS ===== +%s +===== OPS ===== +%s +===== HISTORY ===== +%s`, runOpts.seed, err, out, optionsStr, formattedOps, readFile(filepath.Join(runDir, "history"))) + } + } + + var names []string + options := map[string]*TestOptions{} + + // Create the standard options. + for i, opts := range standardOptions() { + name := fmt.Sprintf("standard-%03d", i) + names = append(names, name) + options[name] = opts + } + + // Create the custom option runs, if any. + for name, customOptsStr := range runOpts.customRuns { + options[name] = defaultTestOptions() + if err := parseOptions(options[name], customOptsStr, runOpts.customOptionParsers); err != nil { + t.Fatalf("custom opts %q: %s", name, err) + } + } + // Sort the custom options names for determinism (they're currently in + // random order from map iteration). + sort.Strings(names[len(names)-len(runOpts.customRuns):]) + + // Create random options. We make an arbitrary choice to run with as many + // random options as we have standard options. + nOpts := len(options) + for i := 0; i < nOpts; i++ { + name := fmt.Sprintf("random-%03d", i) + names = append(names, name) + opts := randomOptions(rng, runOpts.customOptionParsers) + options[name] = opts + } + + // If the user provided the path to an initial database state to use, update + // all the options to pull from it. + if runOpts.initialStatePath != "" { + for _, o := range options { + var err error + o.initialStatePath, err = filepath.Abs(runOpts.initialStatePath) + require.NoError(t, err) + o.initialStateDesc = runOpts.initialStateDesc + } + } + + // Run the options. + t.Run("execution", func(t *testing.T) { + for _, name := range names { + name := name + t.Run(name, func(t *testing.T) { + t.Parallel() + runOptions(t, options[name]) + }) + } + }) + // NB: The above 'execution' subtest will not complete until all of the + // individual execution/ subtests have completed. The grouping within the + // `execution` subtest ensures all the histories are available when we + // proceed to comparing against the base history. + + // Don't bother comparing output if we've already failed. + if t.Failed() { + return + } + + t.Run("compare", func(t *testing.T) { + getHistoryPath := func(name string) string { + return filepath.Join(metaDir, name, "history") + } + + base := readHistory(t, getHistoryPath(names[0])) + base = reorderHistory(base) + for i := 1; i < len(names); i++ { + t.Run(names[i], func(t *testing.T) { + lines := readHistory(t, getHistoryPath(names[i])) + lines = reorderHistory(lines) + diff := difflib.UnifiedDiff{ + A: base, + B: lines, + Context: 5, + } + text, err := difflib.GetUnifiedDiffString(diff) + require.NoError(t, err) + if text != "" { + // NB: We force an exit rather than using t.Fatal because the latter + // will run another instance of the test if -count is specified, while + // we're happy to exit on the first failure. + optionsStrA := optionsToString(options[names[0]]) + optionsStrB := optionsToString(options[names[i]]) + + fmt.Printf(` + ===== SEED ===== + %d + ===== DIFF ===== + %s/{%s,%s} + %s + ===== OPTIONS %s ===== + %s + ===== OPTIONS %s ===== + %s + ===== OPS ===== + %s + `, runOpts.seed, metaDir, names[0], names[i], text, names[0], optionsStrA, names[i], optionsStrB, formattedOps) + os.Exit(1) + } + }) + } + }) +} + +type runOnceOptions struct { + keep bool + maxThreads int + errorRate float64 + failRegexp *regexp.Regexp + numInstances int + customOptionParsers map[string]func(string) (CustomOption, bool) +} + +// A RunOnceOption configures the behavior of a single run of the metamorphic +// tests. +type RunOnceOption interface { + applyOnce(*runOnceOptions) +} + +// KeepData keeps the database directory, even on successful runs. If the test +// used an in-memory filesystem, the in-memory filesystem will be persisted to +// the run directory. +type KeepData struct{} + +func (KeepData) apply(ro *runAndCompareOptions) { ro.keep = true } +func (KeepData) applyOnce(ro *runOnceOptions) { ro.keep = true } + +// InjectErrorsRate configures the run to inject errors into read-only +// filesystem operations and retry injected errors. +type InjectErrorsRate float64 + +func (r InjectErrorsRate) apply(ro *runAndCompareOptions) { ro.errorRate = float64(r) } +func (r InjectErrorsRate) applyOnce(ro *runOnceOptions) { ro.errorRate = float64(r) } + +// MaxThreads sets an upper bound on the number of parallel execution threads +// during replay. +type MaxThreads int + +func (m MaxThreads) apply(ro *runAndCompareOptions) { ro.maxThreads = int(m) } +func (m MaxThreads) applyOnce(ro *runOnceOptions) { ro.maxThreads = int(m) } + +// FailOnMatch configures the run to fail immediately if the history matches the +// provided regular expression. +type FailOnMatch struct { + *regexp.Regexp +} + +func (f FailOnMatch) apply(ro *runAndCompareOptions) { ro.failRegexp = f.Regexp } +func (f FailOnMatch) applyOnce(ro *runOnceOptions) { ro.failRegexp = f.Regexp } + +// MultiInstance configures the number of pebble instances to create. +type MultiInstance int + +func (m MultiInstance) apply(ro *runAndCompareOptions) { ro.numInstances = int(m) } +func (m MultiInstance) applyOnce(ro *runOnceOptions) { ro.numInstances = int(m) } + +// RunOnce performs one run of the metamorphic tests. RunOnce expects the +// directory named by `runDir` to already exist and contain an `OPTIONS` file +// containing the test run's configuration. The history of the run is persisted +// to a file at the path `historyPath`. +// +// The `seed` parameter is not functional; it's used for context in logging. +func RunOnce(t TestingT, runDir string, seed uint64, historyPath string, rOpts ...RunOnceOption) { + runOpts := runOnceOptions{ + customOptionParsers: map[string]func(string) (CustomOption, bool){}, + } + for _, o := range rOpts { + o.applyOnce(&runOpts) + } + + opsPath := filepath.Join(filepath.Dir(filepath.Clean(runDir)), "ops") + opsData, err := os.ReadFile(opsPath) + require.NoError(t, err) + + ops, err := parse(opsData, parserOpts{}) + require.NoError(t, err) + _ = ops + + optionsPath := filepath.Join(runDir, "OPTIONS") + optionsData, err := os.ReadFile(optionsPath) + require.NoError(t, err) + + // NB: It's important to use defaultTestOptions() here as the base into + // which we parse the serialized options. It contains the relevant defaults, + // like the appropriate block-property collectors. + testOpts := defaultTestOptions() + opts := testOpts.Opts + require.NoError(t, parseOptions(testOpts, string(optionsData), runOpts.customOptionParsers)) + + // Always use our custom comparer which provides a Split method, splitting + // keys at the trailing '@'. + opts.Comparer = testkeys.Comparer + // Use an archive cleaner to ease post-mortem debugging. + opts.Cleaner = base.ArchiveCleaner{} + + // Set up the filesystem to use for the test. Note that by default we use an + // in-memory FS. + if testOpts.useDisk { + opts.FS = vfs.Default + require.NoError(t, os.RemoveAll(opts.FS.PathJoin(runDir, "data"))) + } else { + opts.Cleaner = base.ArchiveCleaner{} + if testOpts.strictFS { + opts.FS = vfs.NewStrictMem() + } else { + opts.FS = vfs.NewMem() + } + } + opts.WithFSDefaults() + + threads := testOpts.threads + if runOpts.maxThreads < threads { + threads = runOpts.maxThreads + } + + dir := opts.FS.PathJoin(runDir, "data") + // Set up the initial database state if configured to start from a non-empty + // database. By default tests start from an empty database, but split + // version testing may configure a previous metamorphic tests's database + // state as the initial state. + if testOpts.initialStatePath != "" { + require.NoError(t, setupInitialState(dir, testOpts)) + } + + // Wrap the filesystem with one that will inject errors into read + // operations with *errorRate probability. + opts.FS = errorfs.Wrap(opts.FS, errorfs.ErrInjected.If( + dsl.And[errorfs.Op](errorfs.Reads, errorfs.Randomly(runOpts.errorRate, int64(seed))), + )) + + if opts.WALDir != "" { + if runOpts.numInstances > 1 { + // TODO(bilal): Allow opts to diverge on a per-instance basis, and use + // that to set unique WAL dirs for all instances in multi-instance mode. + opts.WALDir = "" + } else { + opts.WALDir = opts.FS.PathJoin(runDir, opts.WALDir) + } + } + + historyFile, err := os.Create(historyPath) + require.NoError(t, err) + defer historyFile.Close() + writers := []io.Writer{historyFile} + + if testing.Verbose() { + writers = append(writers, os.Stdout) + } + h := newHistory(runOpts.failRegexp, writers...) + + m := newTest(ops) + require.NoError(t, m.init(h, dir, testOpts, runOpts.numInstances)) + + if threads <= 1 { + for m.step(h) { + if err := h.Error(); err != nil { + fmt.Fprintf(os.Stderr, "Seed: %d\n", seed) + fmt.Fprintln(os.Stderr, err) + m.maybeSaveData() + os.Exit(1) + } + } + } else { + eg, ctx := errgroup.WithContext(context.Background()) + for t := 0; t < threads; t++ { + t := t // bind loop var to scope + eg.Go(func() error { + for idx := 0; idx < len(m.ops); idx++ { + // Skip any operations whose receiver object hashes to a + // different thread. All operations with the same receiver + // are performed from the same thread. This goroutine is + // only responsible for executing operations that hash to + // `t`. + if hashThread(m.ops[idx].receiver(), threads) != t { + continue + } + + // Some operations have additional synchronization + // dependencies. If this operation has any, wait for its + // dependencies to complete before executing. + for _, waitOnIdx := range m.opsWaitOn[idx] { + select { + case <-ctx.Done(): + // Exit if some other thread already errored out. + return ctx.Err() + case <-m.opsDone[waitOnIdx]: + } + } + + m.ops[idx].run(m, h.recorder(t, idx)) + + // If this operation has a done channel, close it so that + // other operations that synchronize on this operation know + // that it's been completed. + if ch := m.opsDone[idx]; ch != nil { + close(ch) + } + + if err := h.Error(); err != nil { + return err + } + } + return nil + }) + } + if err := eg.Wait(); err != nil { + fmt.Fprintf(os.Stderr, "Seed: %d\n", seed) + fmt.Fprintln(os.Stderr, err) + m.maybeSaveData() + os.Exit(1) + } + } + + if runOpts.keep && !testOpts.useDisk { + m.maybeSaveData() + } +} + +func hashThread(objID objID, numThreads int) int { + // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ + return int((11400714819323198485 * uint64(objID)) % uint64(numThreads)) +} + +// Compare runs the metamorphic tests in the provided runDirs and compares their +// histories. +func Compare(t TestingT, rootDir string, seed uint64, runDirs []string, rOpts ...RunOnceOption) { + historyPaths := make([]string, len(runDirs)) + for i := 0; i < len(runDirs); i++ { + historyPath := filepath.Join(rootDir, runDirs[i]+"-"+time.Now().Format("060102-150405.000")) + runDirs[i] = filepath.Join(rootDir, runDirs[i]) + _ = os.Remove(historyPath) + historyPaths[i] = historyPath + } + defer func() { + for _, path := range historyPaths { + _ = os.Remove(path) + } + }() + + for i, runDir := range runDirs { + RunOnce(t, runDir, seed, historyPaths[i], rOpts...) + } + + if t.Failed() { + return + } + + i, diff := CompareHistories(t, historyPaths) + if i != 0 { + fmt.Printf(` +===== DIFF ===== +%s/{%s,%s} +%s +`, rootDir, runDirs[0], runDirs[i], diff) + os.Exit(1) + } +} + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + require.TestingT + Failed() bool +} + +func readFile(path string) string { + history, err := os.ReadFile(path) + if err != nil { + return fmt.Sprintf("err: %v", err) + } + + return string(history) +} diff --git a/pebble/metamorphic/ops.go b/pebble/metamorphic/ops.go new file mode 100644 index 0000000..3743b8a --- /dev/null +++ b/pebble/metamorphic/ops.go @@ -0,0 +1,1557 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package metamorphic + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/binary" + "fmt" + "io" + "path" + "path/filepath" + "strings" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/private" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs/errorfs" +) + +// op defines the interface for a single operation, such as creating a batch, +// or advancing an iterator. +type op interface { + String() string + run(t *test, h historyRecorder) + + // receiver returns the object ID of the object the operation is performed + // on. Every operation has a receiver (eg, batch0.Set(...) has `batch0` as + // its receiver). Receivers are used for synchronization when running with + // concurrency. + receiver() objID + + // syncObjs returns an additional set of object IDs—excluding the + // receiver—that the operation must synchronize with. At execution time, + // the operation will run serially with respect to all other operations + // that return these objects from their own syncObjs or receiver methods. + syncObjs() objIDSlice +} + +// initOp performs test initialization +type initOp struct { + dbSlots uint32 + batchSlots uint32 + iterSlots uint32 + snapshotSlots uint32 +} + +func (o *initOp) run(t *test, h historyRecorder) { + t.batches = make([]*pebble.Batch, o.batchSlots) + t.iters = make([]*retryableIter, o.iterSlots) + t.snapshots = make([]readerCloser, o.snapshotSlots) + h.Recordf("%s", o) +} + +func (o *initOp) String() string { + return fmt.Sprintf("Init(%d /* dbs */, %d /* batches */, %d /* iters */, %d /* snapshots */)", + o.dbSlots, o.batchSlots, o.iterSlots, o.snapshotSlots) +} + +func (o *initOp) receiver() objID { return makeObjID(dbTag, 1) } +func (o *initOp) syncObjs() objIDSlice { + syncObjs := make([]objID, 0) + // Add any additional DBs to syncObjs. + for i := uint32(2); i < o.dbSlots+1; i++ { + syncObjs = append(syncObjs, makeObjID(dbTag, i)) + } + return syncObjs +} + +// applyOp models a Writer.Apply operation. +type applyOp struct { + writerID objID + batchID objID +} + +func (o *applyOp) run(t *test, h historyRecorder) { + b := t.getBatch(o.batchID) + w := t.getWriter(o.writerID) + var err error + if o.writerID.tag() == dbTag && t.testOpts.asyncApplyToDB && t.writeOpts.Sync { + err = w.(*pebble.DB).ApplyNoSyncWait(b, t.writeOpts) + if err == nil { + err = b.SyncWait() + } + } else { + err = w.Apply(b, t.writeOpts) + } + h.Recordf("%s // %v", o, err) + // batch will be closed by a closeOp which is guaranteed to be generated +} + +func (o *applyOp) String() string { return fmt.Sprintf("%s.Apply(%s)", o.writerID, o.batchID) } +func (o *applyOp) receiver() objID { return o.writerID } +func (o *applyOp) syncObjs() objIDSlice { + // Apply should not be concurrent with operations that are mutating the + // batch. + return []objID{o.batchID} +} + +// checkpointOp models a DB.Checkpoint operation. +type checkpointOp struct { + dbID objID + // If non-empty, the checkpoint is restricted to these spans. + spans []pebble.CheckpointSpan +} + +func (o *checkpointOp) run(t *test, h historyRecorder) { + // TODO(josh): db.Checkpoint does not work with shared storage yet. + // It would be better to filter out ahead of calling run on the op, + // by setting the weight that generator.go uses to zero, or similar. + // But IIUC the ops are shared for ALL the metamorphic test runs, so + // not sure how to do that easily: + // https://github.com/cockroachdb/pebble/blob/master/metamorphic/meta.go#L177 + if t.testOpts.sharedStorageEnabled { + h.Recordf("%s // %v", o, nil) + return + } + var opts []pebble.CheckpointOption + if len(o.spans) > 0 { + opts = append(opts, pebble.WithRestrictToSpans(o.spans)) + } + db := t.getDB(o.dbID) + err := withRetries(func() error { + return db.Checkpoint(o.dir(t.dir, h.op), opts...) + }) + h.Recordf("%s // %v", o, err) +} + +func (o *checkpointOp) dir(dataDir string, idx int) string { + return filepath.Join(dataDir, "checkpoints", fmt.Sprintf("op-%06d", idx)) +} + +func (o *checkpointOp) String() string { + var spanStr bytes.Buffer + for i, span := range o.spans { + if i > 0 { + spanStr.WriteString(",") + } + fmt.Fprintf(&spanStr, "%q,%q", span.Start, span.End) + } + return fmt.Sprintf("%s.Checkpoint(%s)", o.dbID, spanStr.String()) +} + +func (o *checkpointOp) receiver() objID { return o.dbID } +func (o *checkpointOp) syncObjs() objIDSlice { return nil } + +// closeOp models a {Batch,Iterator,Snapshot}.Close operation. +type closeOp struct { + objID objID + derivedDBID objID +} + +func (o *closeOp) run(t *test, h historyRecorder) { + c := t.getCloser(o.objID) + if o.objID.tag() == dbTag && t.opts.DisableWAL { + // Special case: If WAL is disabled, do a flush right before DB Close. This + // allows us to reuse this run's data directory as initial state for + // future runs without losing any mutations. + _ = t.getDB(o.objID).Flush() + } + t.clearObj(o.objID) + err := c.Close() + h.Recordf("%s // %v", o, err) +} + +func (o *closeOp) String() string { return fmt.Sprintf("%s.Close()", o.objID) } +func (o *closeOp) receiver() objID { return o.objID } +func (o *closeOp) syncObjs() objIDSlice { + // Synchronize on the database so that we don't close the database before + // all its iterators, snapshots and batches are closed. + // TODO(jackson): It would be nice to relax this so that Close calls can + // execute in parallel. + if o.objID.tag() == dbTag { + return nil + } + if o.derivedDBID != 0 { + return []objID{o.derivedDBID} + } + return nil +} + +// compactOp models a DB.Compact operation. +type compactOp struct { + dbID objID + start []byte + end []byte + parallelize bool +} + +func (o *compactOp) run(t *test, h historyRecorder) { + err := withRetries(func() error { + return t.getDB(o.dbID).Compact(o.start, o.end, o.parallelize) + }) + h.Recordf("%s // %v", o, err) +} + +func (o *compactOp) String() string { + return fmt.Sprintf("%s.Compact(%q, %q, %t /* parallelize */)", o.dbID, o.start, o.end, o.parallelize) +} + +func (o *compactOp) receiver() objID { return o.dbID } +func (o *compactOp) syncObjs() objIDSlice { return nil } + +// deleteOp models a Write.Delete operation. +type deleteOp struct { + writerID objID + key []byte + + derivedDBID objID +} + +func (o *deleteOp) run(t *test, h historyRecorder) { + w := t.getWriter(o.writerID) + var err error + if t.testOpts.deleteSized && t.isFMV(o.derivedDBID, pebble.FormatDeleteSizedAndObsolete) { + // Call DeleteSized with a deterministic size derived from the index. + // The size does not need to be accurate for correctness. + err = w.DeleteSized(o.key, hashSize(t.idx), t.writeOpts) + } else { + err = w.Delete(o.key, t.writeOpts) + } + h.Recordf("%s // %v", o, err) +} + +func hashSize(index int) uint32 { + // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ + return uint32((11400714819323198485 * uint64(index)) % maxValueSize) +} + +func (o *deleteOp) String() string { + return fmt.Sprintf("%s.Delete(%q)", o.writerID, o.key) +} +func (o *deleteOp) receiver() objID { return o.writerID } +func (o *deleteOp) syncObjs() objIDSlice { return nil } + +// singleDeleteOp models a Write.SingleDelete operation. +type singleDeleteOp struct { + writerID objID + key []byte + maybeReplaceDelete bool +} + +func (o *singleDeleteOp) run(t *test, h historyRecorder) { + w := t.getWriter(o.writerID) + var err error + if t.testOpts.replaceSingleDelete && o.maybeReplaceDelete { + err = w.Delete(o.key, t.writeOpts) + } else { + err = w.SingleDelete(o.key, t.writeOpts) + } + // NOTE: even if the SINGLEDEL was replaced with a DELETE, we must still + // write the former to the history log. The log line will indicate whether + // or not the delete *could* have been replaced. The OPTIONS file should + // also be consulted to determine what happened at runtime (i.e. by taking + // the logical AND). + h.Recordf("%s // %v", o, err) +} + +func (o *singleDeleteOp) String() string { + return fmt.Sprintf("%s.SingleDelete(%q, %v /* maybeReplaceDelete */)", o.writerID, o.key, o.maybeReplaceDelete) +} + +func (o *singleDeleteOp) receiver() objID { return o.writerID } +func (o *singleDeleteOp) syncObjs() objIDSlice { return nil } + +// deleteRangeOp models a Write.DeleteRange operation. +type deleteRangeOp struct { + writerID objID + start []byte + end []byte +} + +func (o *deleteRangeOp) run(t *test, h historyRecorder) { + w := t.getWriter(o.writerID) + err := w.DeleteRange(o.start, o.end, t.writeOpts) + h.Recordf("%s // %v", o, err) +} + +func (o *deleteRangeOp) String() string { + return fmt.Sprintf("%s.DeleteRange(%q, %q)", o.writerID, o.start, o.end) +} + +func (o *deleteRangeOp) receiver() objID { return o.writerID } +func (o *deleteRangeOp) syncObjs() objIDSlice { return nil } + +// flushOp models a DB.Flush operation. +type flushOp struct { + db objID +} + +func (o *flushOp) run(t *test, h historyRecorder) { + db := t.getDB(o.db) + err := db.Flush() + h.Recordf("%s // %v", o, err) +} + +func (o *flushOp) String() string { return fmt.Sprintf("%s.Flush()", o.db) } +func (o *flushOp) receiver() objID { return o.db } +func (o *flushOp) syncObjs() objIDSlice { return nil } + +// mergeOp models a Write.Merge operation. +type mergeOp struct { + writerID objID + key []byte + value []byte +} + +func (o *mergeOp) run(t *test, h historyRecorder) { + w := t.getWriter(o.writerID) + err := w.Merge(o.key, o.value, t.writeOpts) + h.Recordf("%s // %v", o, err) +} + +func (o *mergeOp) String() string { return fmt.Sprintf("%s.Merge(%q, %q)", o.writerID, o.key, o.value) } +func (o *mergeOp) receiver() objID { return o.writerID } +func (o *mergeOp) syncObjs() objIDSlice { return nil } + +// setOp models a Write.Set operation. +type setOp struct { + writerID objID + key []byte + value []byte +} + +func (o *setOp) run(t *test, h historyRecorder) { + w := t.getWriter(o.writerID) + err := w.Set(o.key, o.value, t.writeOpts) + h.Recordf("%s // %v", o, err) +} + +func (o *setOp) String() string { return fmt.Sprintf("%s.Set(%q, %q)", o.writerID, o.key, o.value) } +func (o *setOp) receiver() objID { return o.writerID } +func (o *setOp) syncObjs() objIDSlice { return nil } + +// rangeKeyDeleteOp models a Write.RangeKeyDelete operation. +type rangeKeyDeleteOp struct { + writerID objID + start []byte + end []byte +} + +func (o *rangeKeyDeleteOp) run(t *test, h historyRecorder) { + w := t.getWriter(o.writerID) + err := w.RangeKeyDelete(o.start, o.end, t.writeOpts) + h.Recordf("%s // %v", o, err) +} + +func (o *rangeKeyDeleteOp) String() string { + return fmt.Sprintf("%s.RangeKeyDelete(%q, %q)", o.writerID, o.start, o.end) +} + +func (o *rangeKeyDeleteOp) receiver() objID { return o.writerID } +func (o *rangeKeyDeleteOp) syncObjs() objIDSlice { return nil } + +// rangeKeySetOp models a Write.RangeKeySet operation. +type rangeKeySetOp struct { + writerID objID + start []byte + end []byte + suffix []byte + value []byte +} + +func (o *rangeKeySetOp) run(t *test, h historyRecorder) { + w := t.getWriter(o.writerID) + err := w.RangeKeySet(o.start, o.end, o.suffix, o.value, t.writeOpts) + h.Recordf("%s // %v", o, err) +} + +func (o *rangeKeySetOp) String() string { + return fmt.Sprintf("%s.RangeKeySet(%q, %q, %q, %q)", + o.writerID, o.start, o.end, o.suffix, o.value) +} + +func (o *rangeKeySetOp) receiver() objID { return o.writerID } +func (o *rangeKeySetOp) syncObjs() objIDSlice { return nil } + +// rangeKeyUnsetOp models a Write.RangeKeyUnset operation. +type rangeKeyUnsetOp struct { + writerID objID + start []byte + end []byte + suffix []byte +} + +func (o *rangeKeyUnsetOp) run(t *test, h historyRecorder) { + w := t.getWriter(o.writerID) + err := w.RangeKeyUnset(o.start, o.end, o.suffix, t.writeOpts) + h.Recordf("%s // %v", o, err) +} + +func (o *rangeKeyUnsetOp) String() string { + return fmt.Sprintf("%s.RangeKeyUnset(%q, %q, %q)", + o.writerID, o.start, o.end, o.suffix) +} + +func (o *rangeKeyUnsetOp) receiver() objID { return o.writerID } +func (o *rangeKeyUnsetOp) syncObjs() objIDSlice { return nil } + +// newBatchOp models a Write.NewBatch operation. +type newBatchOp struct { + dbID objID + batchID objID +} + +func (o *newBatchOp) run(t *test, h historyRecorder) { + b := t.getDB(o.dbID).NewBatch() + t.setBatch(o.batchID, b) + h.Recordf("%s", o) +} + +func (o *newBatchOp) String() string { return fmt.Sprintf("%s = %s.NewBatch()", o.batchID, o.dbID) } +func (o *newBatchOp) receiver() objID { return o.dbID } +func (o *newBatchOp) syncObjs() objIDSlice { + // NewBatch should not be concurrent with operations that interact with that + // same batch. + return []objID{o.batchID} +} + +// newIndexedBatchOp models a Write.NewIndexedBatch operation. +type newIndexedBatchOp struct { + dbID objID + batchID objID +} + +func (o *newIndexedBatchOp) run(t *test, h historyRecorder) { + b := t.getDB(o.dbID).NewIndexedBatch() + t.setBatch(o.batchID, b) + h.Recordf("%s", o) +} + +func (o *newIndexedBatchOp) String() string { + return fmt.Sprintf("%s = %s.NewIndexedBatch()", o.batchID, o.dbID) +} +func (o *newIndexedBatchOp) receiver() objID { return o.dbID } +func (o *newIndexedBatchOp) syncObjs() objIDSlice { + // NewIndexedBatch should not be concurrent with operations that interact + // with that same batch. + return []objID{o.batchID} +} + +// batchCommitOp models a Batch.Commit operation. +type batchCommitOp struct { + dbID objID + batchID objID +} + +func (o *batchCommitOp) run(t *test, h historyRecorder) { + b := t.getBatch(o.batchID) + err := b.Commit(t.writeOpts) + h.Recordf("%s // %v", o, err) +} + +func (o *batchCommitOp) String() string { return fmt.Sprintf("%s.Commit()", o.batchID) } +func (o *batchCommitOp) receiver() objID { return o.batchID } +func (o *batchCommitOp) syncObjs() objIDSlice { + // Synchronize on the database so that NewIters wait for the commit. + return []objID{o.dbID} +} + +// ingestOp models a DB.Ingest operation. +type ingestOp struct { + dbID objID + batchIDs []objID + + derivedDBIDs []objID +} + +func (o *ingestOp) run(t *test, h historyRecorder) { + // We can only use apply as an alternative for ingestion if we are ingesting + // a single batch. If we are ingesting multiple batches, the batches may + // overlap which would cause ingestion to fail but apply would succeed. + if t.testOpts.ingestUsingApply && len(o.batchIDs) == 1 && o.derivedDBIDs[0] == o.dbID { + id := o.batchIDs[0] + b := t.getBatch(id) + iter, rangeDelIter, rangeKeyIter := private.BatchSort(b) + db := t.getDB(o.dbID) + c, err := o.collapseBatch(t, db, iter, rangeDelIter, rangeKeyIter, b) + if err == nil { + err = db.Apply(c, t.writeOpts) + } + _ = b.Close() + _ = c.Close() + t.clearObj(id) + h.Recordf("%s // %v", o, err) + return + } + + var paths []string + var err error + for i, id := range o.batchIDs { + b := t.getBatch(id) + t.clearObj(id) + path, err2 := o.build(t, h, b, i) + if err2 != nil { + h.Recordf("Build(%s) // %v", id, err2) + } + err = firstError(err, err2) + if err2 == nil { + paths = append(paths, path) + } + err = firstError(err, b.Close()) + } + + err = firstError(err, withRetries(func() error { + return t.getDB(o.dbID).Ingest(paths) + })) + + h.Recordf("%s // %v", o, err) +} + +func (o *ingestOp) build(t *test, h historyRecorder, b *pebble.Batch, i int) (string, error) { + path := t.opts.FS.PathJoin(t.tmpDir, fmt.Sprintf("ext%d-%d", o.dbID.slot(), i)) + f, err := t.opts.FS.Create(path) + if err != nil { + return "", err + } + db := t.getDB(o.dbID) + + iter, rangeDelIter, rangeKeyIter := private.BatchSort(b) + defer closeIters(iter, rangeDelIter, rangeKeyIter) + + equal := t.opts.Comparer.Equal + tableFormat := db.FormatMajorVersion().MaxTableFormat() + w := sstable.NewWriter( + objstorageprovider.NewFileWritable(f), + t.opts.MakeWriterOptions(0, tableFormat), + ) + + var lastUserKey []byte + for key, value := iter.First(); key != nil; key, value = iter.Next() { + // Ignore duplicate keys. + if equal(lastUserKey, key.UserKey) { + continue + } + // NB: We don't have to copy the key or value since we're reading from a + // batch which doesn't do prefix compression. + lastUserKey = key.UserKey + + key.SetSeqNum(base.SeqNumZero) + // It's possible that we wrote the key on a batch from a db that supported + // DeleteSized, but are now ingesting into a db that does not. Detect + // this case and translate the key to an InternalKeyKindDelete. + if key.Kind() == pebble.InternalKeyKindDeleteSized && !t.isFMV(o.dbID, pebble.FormatDeleteSizedAndObsolete) { + value = pebble.LazyValue{} + key.SetKind(pebble.InternalKeyKindDelete) + } + if err := w.Add(*key, value.InPlaceValue()); err != nil { + return "", err + } + } + if err := iter.Close(); err != nil { + return "", err + } + iter = nil + + if rangeDelIter != nil { + // NB: The range tombstones have already been fragmented by the Batch. + for t := rangeDelIter.First(); t != nil; t = rangeDelIter.Next() { + // NB: We don't have to copy the key or value since we're reading from a + // batch which doesn't do prefix compression. + if err := w.DeleteRange(t.Start, t.End); err != nil { + return "", err + } + } + if err := rangeDelIter.Close(); err != nil { + return "", err + } + rangeDelIter = nil + } + + if rangeKeyIter != nil { + for span := rangeKeyIter.First(); span != nil; span = rangeKeyIter.Next() { + // Coalesce the keys of this span and then zero the sequence + // numbers. This is necessary in order to make the range keys within + // the ingested sstable internally consistent at the sequence number + // it's ingested at. The individual keys within a batch are + // committed at unique sequence numbers, whereas all the keys of an + // ingested sstable are given the same sequence number. A span + // contaning keys that both set and unset the same suffix at the + // same sequence number is nonsensical, so we "coalesce" or collapse + // the keys. + collapsed := keyspan.Span{ + Start: span.Start, + End: span.End, + Keys: make([]keyspan.Key, 0, len(span.Keys)), + } + err = rangekey.Coalesce(t.opts.Comparer.Compare, equal, span.Keys, &collapsed.Keys) + if err != nil { + return "", err + } + for i := range collapsed.Keys { + collapsed.Keys[i].Trailer = base.MakeTrailer(0, collapsed.Keys[i].Kind()) + } + keyspan.SortKeysByTrailer(&collapsed.Keys) + if err := rangekey.Encode(&collapsed, w.AddRangeKey); err != nil { + return "", err + } + } + if err := rangeKeyIter.Error(); err != nil { + return "", err + } + if err := rangeKeyIter.Close(); err != nil { + return "", err + } + rangeKeyIter = nil + } + + if err := w.Close(); err != nil { + return "", err + } + return path, nil +} + +func (o *ingestOp) receiver() objID { return o.dbID } +func (o *ingestOp) syncObjs() objIDSlice { + // Ingest should not be concurrent with mutating the batches that will be + // ingested as sstables. + objs := make([]objID, 0, len(o.batchIDs)+1) + objs = append(objs, o.batchIDs...) + addedDBs := make(map[objID]struct{}) + for i := range o.derivedDBIDs { + _, ok := addedDBs[o.derivedDBIDs[i]] + if !ok && o.derivedDBIDs[i] != o.dbID { + objs = append(objs, o.derivedDBIDs[i]) + addedDBs[o.derivedDBIDs[i]] = struct{}{} + } + } + return objs +} + +func closeIters( + pointIter base.InternalIterator, + rangeDelIter keyspan.FragmentIterator, + rangeKeyIter keyspan.FragmentIterator, +) { + if pointIter != nil { + pointIter.Close() + } + if rangeDelIter != nil { + rangeDelIter.Close() + } + if rangeKeyIter != nil { + rangeKeyIter.Close() + } +} + +// collapseBatch collapses the mutations in a batch to be equivalent to an +// sstable ingesting those mutations. Duplicate updates to a key are collapsed +// so that only the latest update is performed. All range deletions are +// performed first in the batch to match the semantics of ingestion where a +// range deletion does not delete a point record contained in the sstable. +func (o *ingestOp) collapseBatch( + t *test, + db *pebble.DB, + pointIter base.InternalIterator, + rangeDelIter, rangeKeyIter keyspan.FragmentIterator, + b *pebble.Batch, +) (*pebble.Batch, error) { + defer closeIters(pointIter, rangeDelIter, rangeKeyIter) + equal := t.opts.Comparer.Equal + collapsed := db.NewBatch() + + if rangeDelIter != nil { + // NB: The range tombstones have already been fragmented by the Batch. + for t := rangeDelIter.First(); t != nil; t = rangeDelIter.Next() { + // NB: We don't have to copy the key or value since we're reading from a + // batch which doesn't do prefix compression. + if err := collapsed.DeleteRange(t.Start, t.End, nil); err != nil { + return nil, err + } + } + if err := rangeDelIter.Close(); err != nil { + return nil, err + } + rangeDelIter = nil + } + + if pointIter != nil { + var lastUserKey []byte + for key, value := pointIter.First(); key != nil; key, value = pointIter.Next() { + // Ignore duplicate keys. + // + // Note: this is necessary due to MERGE keys, otherwise it would be + // fine to include all the keys in the batch and let the normal + // sequence number precedence determine which of the keys "wins". + // But the code to build the ingested sstable will only keep the + // most recent internal key and will not merge across internal keys. + if equal(lastUserKey, key.UserKey) { + continue + } + // NB: We don't have to copy the key or value since we're reading from a + // batch which doesn't do prefix compression. + lastUserKey = key.UserKey + + var err error + switch key.Kind() { + case pebble.InternalKeyKindDelete: + err = collapsed.Delete(key.UserKey, nil) + case pebble.InternalKeyKindDeleteSized: + v, _ := binary.Uvarint(value.InPlaceValue()) + // Batch.DeleteSized takes just the length of the value being + // deleted and adds the key's length to derive the overall entry + // size of the value being deleted. This has already been done + // to the key we're reading from the batch, so we must subtract + // the key length from the encoded value before calling + // collapsed.DeleteSized, which will again add the key length + // before encoding. + err = collapsed.DeleteSized(key.UserKey, uint32(v-uint64(len(key.UserKey))), nil) + case pebble.InternalKeyKindSingleDelete: + err = collapsed.SingleDelete(key.UserKey, nil) + case pebble.InternalKeyKindSet: + err = collapsed.Set(key.UserKey, value.InPlaceValue(), nil) + case pebble.InternalKeyKindMerge: + err = collapsed.Merge(key.UserKey, value.InPlaceValue(), nil) + case pebble.InternalKeyKindLogData: + err = collapsed.LogData(key.UserKey, nil) + default: + err = errors.Errorf("unknown batch record kind: %d", key.Kind()) + } + if err != nil { + return nil, err + } + } + if err := pointIter.Close(); err != nil { + return nil, err + } + pointIter = nil + } + + // There's no equivalent of a MERGE operator for range keys, so there's no + // need to collapse the range keys here. Rather than reading the range keys + // from `rangeKeyIter`, which will already be fragmented, read the range + // keys from the batch and copy them verbatim. This marginally improves our + // test coverage over the alternative approach of pre-fragmenting and + // pre-coalescing before writing to the batch. + // + // The `rangeKeyIter` is used only to determine if there are any range keys + // in the batch at all, and only because we already have it handy from + // private.BatchSort. + if rangeKeyIter != nil { + for r := b.Reader(); ; { + kind, key, value, ok, err := r.Next() + if !ok { + if err != nil { + return nil, err + } + break + } else if !rangekey.IsRangeKey(kind) { + continue + } + ik := base.MakeInternalKey(key, 0, kind) + if err := collapsed.AddInternalKey(&ik, value, nil); err != nil { + return nil, err + } + } + if err := rangeKeyIter.Close(); err != nil { + return nil, err + } + rangeKeyIter = nil + } + + return collapsed, nil +} + +func (o *ingestOp) String() string { + var buf strings.Builder + buf.WriteString(o.dbID.String()) + buf.WriteString(".Ingest(") + for i, id := range o.batchIDs { + if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(id.String()) + } + buf.WriteString(")") + return buf.String() +} + +// getOp models a Reader.Get operation. +type getOp struct { + readerID objID + key []byte + derivedDBID objID +} + +func (o *getOp) run(t *test, h historyRecorder) { + r := t.getReader(o.readerID) + var val []byte + var closer io.Closer + err := withRetries(func() (err error) { + val, closer, err = r.Get(o.key) + return err + }) + h.Recordf("%s // [%q] %v", o, val, err) + if closer != nil { + closer.Close() + } +} + +func (o *getOp) String() string { return fmt.Sprintf("%s.Get(%q)", o.readerID, o.key) } +func (o *getOp) receiver() objID { return o.readerID } +func (o *getOp) syncObjs() objIDSlice { + if o.readerID.tag() == dbTag { + return nil + } + // batch.Get reads through to the current database state. + if o.derivedDBID != 0 { + return []objID{o.derivedDBID} + } + return nil +} + +// newIterOp models a Reader.NewIter operation. +type newIterOp struct { + readerID objID + iterID objID + iterOpts + derivedDBID objID +} + +func (o *newIterOp) run(t *test, h historyRecorder) { + r := t.getReader(o.readerID) + opts := iterOptions(o.iterOpts) + + var i *pebble.Iterator + for { + i, _ = r.NewIter(opts) + if err := i.Error(); !errors.Is(err, errorfs.ErrInjected) { + break + } + // close this iter and retry NewIter + _ = i.Close() + } + t.setIter(o.iterID, i) + + // Trash the bounds to ensure that Pebble doesn't rely on the stability of + // the user-provided bounds. + if opts != nil { + rand.Read(opts.LowerBound[:]) + rand.Read(opts.UpperBound[:]) + } + h.Recordf("%s // %v", o, i.Error()) +} + +func (o *newIterOp) String() string { + return fmt.Sprintf("%s = %s.NewIter(%q, %q, %d /* key types */, %d, %d, %t /* use L6 filters */, %q /* masking suffix */)", + o.iterID, o.readerID, o.lower, o.upper, o.keyTypes, o.filterMin, o.filterMax, o.useL6Filters, o.maskSuffix) +} + +func (o *newIterOp) receiver() objID { return o.readerID } +func (o *newIterOp) syncObjs() objIDSlice { + // Prevent o.iterID ops from running before it exists. + objs := []objID{o.iterID} + // If reading through a batch or snapshot, the new iterator will also observe database + // state, and we must synchronize on the database state for a consistent + // view. + if o.readerID.tag() == batchTag || o.readerID.tag() == snapTag { + objs = append(objs, o.derivedDBID) + } + return objs +} + +// newIterUsingCloneOp models a Iterator.Clone operation. +type newIterUsingCloneOp struct { + existingIterID objID + iterID objID + refreshBatch bool + iterOpts + + // derivedReaderID is the ID of the underlying reader that backs both the + // existing iterator and the new iterator. The derivedReaderID is NOT + // serialized by String and is derived from other operations during parse. + derivedReaderID objID +} + +func (o *newIterUsingCloneOp) run(t *test, h historyRecorder) { + iter := t.getIter(o.existingIterID) + cloneOpts := pebble.CloneOptions{ + IterOptions: iterOptions(o.iterOpts), + RefreshBatchView: o.refreshBatch, + } + i, err := iter.iter.Clone(cloneOpts) + if err != nil { + panic(err) + } + t.setIter(o.iterID, i) + h.Recordf("%s // %v", o, i.Error()) +} + +func (o *newIterUsingCloneOp) String() string { + return fmt.Sprintf("%s = %s.Clone(%t, %q, %q, %d /* key types */, %d, %d, %t /* use L6 filters */, %q /* masking suffix */)", + o.iterID, o.existingIterID, o.refreshBatch, o.lower, o.upper, + o.keyTypes, o.filterMin, o.filterMax, o.useL6Filters, o.maskSuffix) +} + +func (o *newIterUsingCloneOp) receiver() objID { return o.existingIterID } + +func (o *newIterUsingCloneOp) syncObjs() objIDSlice { + objIDs := []objID{o.iterID} + // If the underlying reader is a batch, we must synchronize with the batch. + // If refreshBatch=true, synchronizing is necessary to observe all the + // mutations up to until this op and no more. Even when refreshBatch=false, + // we must synchronize because iterator construction may access state cached + // on the indexed batch to avoid refragmenting range tombstones or range + // keys. + if o.derivedReaderID.tag() == batchTag { + objIDs = append(objIDs, o.derivedReaderID) + } + return objIDs +} + +// iterSetBoundsOp models an Iterator.SetBounds operation. +type iterSetBoundsOp struct { + iterID objID + lower []byte + upper []byte +} + +func (o *iterSetBoundsOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + var lower, upper []byte + if o.lower != nil { + lower = append(lower, o.lower...) + } + if o.upper != nil { + upper = append(upper, o.upper...) + } + i.SetBounds(lower, upper) + + // Trash the bounds to ensure that Pebble doesn't rely on the stability of + // the user-provided bounds. + rand.Read(lower[:]) + rand.Read(upper[:]) + + h.Recordf("%s // %v", o, i.Error()) +} + +func (o *iterSetBoundsOp) String() string { + return fmt.Sprintf("%s.SetBounds(%q, %q)", o.iterID, o.lower, o.upper) +} + +func (o *iterSetBoundsOp) receiver() objID { return o.iterID } +func (o *iterSetBoundsOp) syncObjs() objIDSlice { return nil } + +// iterSetOptionsOp models an Iterator.SetOptions operation. +type iterSetOptionsOp struct { + iterID objID + iterOpts + + // derivedReaderID is the ID of the underlying reader that backs the + // iterator. The derivedReaderID is NOT serialized by String and is derived + // from other operations during parse. + derivedReaderID objID +} + +func (o *iterSetOptionsOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + + opts := iterOptions(o.iterOpts) + if opts == nil { + opts = &pebble.IterOptions{} + } + i.SetOptions(opts) + + // Trash the bounds to ensure that Pebble doesn't rely on the stability of + // the user-provided bounds. + rand.Read(opts.LowerBound[:]) + rand.Read(opts.UpperBound[:]) + + h.Recordf("%s // %v", o, i.Error()) +} + +func (o *iterSetOptionsOp) String() string { + return fmt.Sprintf("%s.SetOptions(%q, %q, %d /* key types */, %d, %d, %t /* use L6 filters */, %q /* masking suffix */)", + o.iterID, o.lower, o.upper, o.keyTypes, o.filterMin, o.filterMax, o.useL6Filters, o.maskSuffix) +} + +func iterOptions(o iterOpts) *pebble.IterOptions { + if o.IsZero() { + return nil + } + var lower, upper []byte + if o.lower != nil { + lower = append(lower, o.lower...) + } + if o.upper != nil { + upper = append(upper, o.upper...) + } + opts := &pebble.IterOptions{ + LowerBound: lower, + UpperBound: upper, + KeyTypes: pebble.IterKeyType(o.keyTypes), + RangeKeyMasking: pebble.RangeKeyMasking{ + Suffix: o.maskSuffix, + }, + UseL6Filters: o.useL6Filters, + } + if opts.RangeKeyMasking.Suffix != nil { + opts.RangeKeyMasking.Filter = func() pebble.BlockPropertyFilterMask { + return sstable.NewTestKeysMaskingFilter() + } + } + if o.filterMax > 0 { + opts.PointKeyFilters = []pebble.BlockPropertyFilter{ + sstable.NewTestKeysBlockPropertyFilter(o.filterMin, o.filterMax), + } + // Enforce the timestamp bounds in SkipPoint, so that the iterator never + // returns a key outside the filterMin, filterMax bounds. This provides + // deterministic iteration. + opts.SkipPoint = func(k []byte) (skip bool) { + n := testkeys.Comparer.Split(k) + if n == len(k) { + // No suffix, don't skip it. + return false + } + v, err := testkeys.ParseSuffix(k[n:]) + if err != nil { + panic(err) + } + ts := uint64(v) + return ts < o.filterMin || ts >= o.filterMax + } + } + return opts +} + +func (o *iterSetOptionsOp) receiver() objID { return o.iterID } + +func (o *iterSetOptionsOp) syncObjs() objIDSlice { + if o.derivedReaderID.tag() == batchTag { + // If the underlying reader is a batch, we must synchronize with the + // batch so that we observe all the mutations up until this operation + // and no more. + return []objID{o.derivedReaderID} + } + return nil +} + +// iterSeekGEOp models an Iterator.SeekGE[WithLimit] operation. +type iterSeekGEOp struct { + iterID objID + key []byte + limit []byte + + derivedReaderID objID +} + +func iteratorPos(i *retryableIter) string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "%q", i.Key()) + hasPoint, hasRange := i.HasPointAndRange() + if hasPoint { + fmt.Fprintf(&buf, ",%q", i.Value()) + } else { + fmt.Fprint(&buf, ",") + } + if hasRange { + start, end := i.RangeBounds() + fmt.Fprintf(&buf, ",[%q,%q)=>{", start, end) + for i, rk := range i.RangeKeys() { + if i > 0 { + fmt.Fprint(&buf, ",") + } + fmt.Fprintf(&buf, "%q=%q", rk.Suffix, rk.Value) + } + fmt.Fprint(&buf, "}") + } else { + fmt.Fprint(&buf, ",") + } + if i.RangeKeyChanged() { + fmt.Fprint(&buf, "*") + } + return buf.String() +} + +func validBoolToStr(valid bool) string { + return fmt.Sprintf("%t", valid) +} + +func validityStateToStr(validity pebble.IterValidityState) (bool, string) { + // We can't distinguish between IterExhausted and IterAtLimit in a + // deterministic manner. + switch validity { + case pebble.IterExhausted, pebble.IterAtLimit: + return false, "invalid" + case pebble.IterValid: + return true, "valid" + default: + panic("unknown validity") + } +} + +func (o *iterSeekGEOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + var valid bool + var validStr string + if o.limit == nil { + valid = i.SeekGE(o.key) + validStr = validBoolToStr(valid) + } else { + valid, validStr = validityStateToStr(i.SeekGEWithLimit(o.key, o.limit)) + } + if valid { + h.Recordf("%s // [%s,%s] %v", o, validStr, iteratorPos(i), i.Error()) + } else { + h.Recordf("%s // [%s] %v", o, validStr, i.Error()) + } +} + +func (o *iterSeekGEOp) String() string { + return fmt.Sprintf("%s.SeekGE(%q, %q)", o.iterID, o.key, o.limit) +} +func (o *iterSeekGEOp) receiver() objID { return o.iterID } +func (o *iterSeekGEOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +func onlyBatchIDs(ids ...objID) objIDSlice { + var ret objIDSlice + for _, id := range ids { + if id.tag() == batchTag { + ret = append(ret, id) + } + } + return ret +} + +// iterSeekPrefixGEOp models an Iterator.SeekPrefixGE operation. +type iterSeekPrefixGEOp struct { + iterID objID + key []byte + + derivedReaderID objID +} + +func (o *iterSeekPrefixGEOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + valid := i.SeekPrefixGE(o.key) + if valid { + h.Recordf("%s // [%t,%s] %v", o, valid, iteratorPos(i), i.Error()) + } else { + h.Recordf("%s // [%t] %v", o, valid, i.Error()) + } +} + +func (o *iterSeekPrefixGEOp) String() string { + return fmt.Sprintf("%s.SeekPrefixGE(%q)", o.iterID, o.key) +} +func (o *iterSeekPrefixGEOp) receiver() objID { return o.iterID } +func (o *iterSeekPrefixGEOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +// iterSeekLTOp models an Iterator.SeekLT[WithLimit] operation. +type iterSeekLTOp struct { + iterID objID + key []byte + limit []byte + + derivedReaderID objID +} + +func (o *iterSeekLTOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + var valid bool + var validStr string + if o.limit == nil { + valid = i.SeekLT(o.key) + validStr = validBoolToStr(valid) + } else { + valid, validStr = validityStateToStr(i.SeekLTWithLimit(o.key, o.limit)) + } + if valid { + h.Recordf("%s // [%s,%s] %v", o, validStr, iteratorPos(i), i.Error()) + } else { + h.Recordf("%s // [%s] %v", o, validStr, i.Error()) + } +} + +func (o *iterSeekLTOp) String() string { + return fmt.Sprintf("%s.SeekLT(%q, %q)", o.iterID, o.key, o.limit) +} + +func (o *iterSeekLTOp) receiver() objID { return o.iterID } +func (o *iterSeekLTOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +// iterFirstOp models an Iterator.First operation. +type iterFirstOp struct { + iterID objID + + derivedReaderID objID +} + +func (o *iterFirstOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + valid := i.First() + if valid { + h.Recordf("%s // [%t,%s] %v", o, valid, iteratorPos(i), i.Error()) + } else { + h.Recordf("%s // [%t] %v", o, valid, i.Error()) + } +} + +func (o *iterFirstOp) String() string { return fmt.Sprintf("%s.First()", o.iterID) } +func (o *iterFirstOp) receiver() objID { return o.iterID } +func (o *iterFirstOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +// iterLastOp models an Iterator.Last operation. +type iterLastOp struct { + iterID objID + + derivedReaderID objID +} + +func (o *iterLastOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + valid := i.Last() + if valid { + h.Recordf("%s // [%t,%s] %v", o, valid, iteratorPos(i), i.Error()) + } else { + h.Recordf("%s // [%t] %v", o, valid, i.Error()) + } +} + +func (o *iterLastOp) String() string { return fmt.Sprintf("%s.Last()", o.iterID) } +func (o *iterLastOp) receiver() objID { return o.iterID } +func (o *iterLastOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +// iterNextOp models an Iterator.Next[WithLimit] operation. +type iterNextOp struct { + iterID objID + limit []byte + + derivedReaderID objID +} + +func (o *iterNextOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + var valid bool + var validStr string + if o.limit == nil { + valid = i.Next() + validStr = validBoolToStr(valid) + } else { + valid, validStr = validityStateToStr(i.NextWithLimit(o.limit)) + } + if valid { + h.Recordf("%s // [%s,%s] %v", o, validStr, iteratorPos(i), i.Error()) + } else { + h.Recordf("%s // [%s] %v", o, validStr, i.Error()) + } +} + +func (o *iterNextOp) String() string { return fmt.Sprintf("%s.Next(%q)", o.iterID, o.limit) } +func (o *iterNextOp) receiver() objID { return o.iterID } +func (o *iterNextOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +// iterNextPrefixOp models an Iterator.NextPrefix operation. +type iterNextPrefixOp struct { + iterID objID + + derivedReaderID objID +} + +func (o *iterNextPrefixOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + valid := i.NextPrefix() + validStr := validBoolToStr(valid) + if valid { + h.Recordf("%s // [%s,%s] %v", o, validStr, iteratorPos(i), i.Error()) + } else { + h.Recordf("%s // [%s] %v", o, validStr, i.Error()) + } +} + +func (o *iterNextPrefixOp) String() string { return fmt.Sprintf("%s.NextPrefix()", o.iterID) } +func (o *iterNextPrefixOp) receiver() objID { return o.iterID } +func (o *iterNextPrefixOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +// iterCanSingleDelOp models a call to CanDeterministicallySingleDelete with an +// Iterator. +type iterCanSingleDelOp struct { + iterID objID + + derivedReaderID objID +} + +func (o *iterCanSingleDelOp) run(t *test, h historyRecorder) { + // TODO(jackson): When we perform error injection, we'll need to rethink + // this. + _, err := pebble.CanDeterministicallySingleDelete(t.getIter(o.iterID).iter) + // The return value of CanDeterministicallySingleDelete is dependent on + // internal LSM state and non-deterministic, so we don't record it. + // Including the operation within the metamorphic test at all helps ensure + // that it does not change the result of any other Iterator operation that + // should be deterministic, regardless of its own outcome. + // + // We still record the value of the error because it's deterministic, at + // least for now. The possible error cases are: + // - The iterator was already in an error state when the operation ran. + // - The operation is deterministically invalid (like using an InternalNext + // to change directions.) + h.Recordf("%s // %v", o, err) +} + +func (o *iterCanSingleDelOp) String() string { return fmt.Sprintf("%s.InternalNext()", o.iterID) } +func (o *iterCanSingleDelOp) receiver() objID { return o.iterID } +func (o *iterCanSingleDelOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +// iterPrevOp models an Iterator.Prev[WithLimit] operation. +type iterPrevOp struct { + iterID objID + limit []byte + + derivedReaderID objID +} + +func (o *iterPrevOp) run(t *test, h historyRecorder) { + i := t.getIter(o.iterID) + var valid bool + var validStr string + if o.limit == nil { + valid = i.Prev() + validStr = validBoolToStr(valid) + } else { + valid, validStr = validityStateToStr(i.PrevWithLimit(o.limit)) + } + if valid { + h.Recordf("%s // [%s,%s] %v", o, validStr, iteratorPos(i), i.Error()) + } else { + h.Recordf("%s // [%s] %v", o, validStr, i.Error()) + } +} + +func (o *iterPrevOp) String() string { return fmt.Sprintf("%s.Prev(%q)", o.iterID, o.limit) } +func (o *iterPrevOp) receiver() objID { return o.iterID } +func (o *iterPrevOp) syncObjs() objIDSlice { return onlyBatchIDs(o.derivedReaderID) } + +// newSnapshotOp models a DB.NewSnapshot operation. +type newSnapshotOp struct { + dbID objID + snapID objID + // If nonempty, this snapshot must not be used to read any keys outside of + // the provided bounds. This allows some implementations to use 'Eventually + // file-only snapshots,' which require bounds. + bounds []pebble.KeyRange +} + +func (o *newSnapshotOp) run(t *test, h historyRecorder) { + // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ + if len(t.dbs) > 1 || (len(o.bounds) > 0 && ((11400714819323198485*uint64(t.idx)*t.testOpts.seedEFOS)>>63) == 1) { + s := t.getDB(o.dbID).NewEventuallyFileOnlySnapshot(o.bounds) + t.setSnapshot(o.snapID, s) + } else { + s := t.getDB(o.dbID).NewSnapshot() + t.setSnapshot(o.snapID, s) + } + h.Recordf("%s", o) +} + +func (o *newSnapshotOp) String() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "%s = %s.NewSnapshot(", o.snapID, o.dbID) + for i := range o.bounds { + if i > 0 { + fmt.Fprint(&buf, ", ") + } + fmt.Fprintf(&buf, "%q, %q", o.bounds[i].Start, o.bounds[i].End) + } + fmt.Fprint(&buf, ")") + return buf.String() +} +func (o *newSnapshotOp) receiver() objID { return o.dbID } +func (o *newSnapshotOp) syncObjs() objIDSlice { return []objID{o.snapID} } + +type dbRatchetFormatMajorVersionOp struct { + dbID objID + vers pebble.FormatMajorVersion +} + +func (o *dbRatchetFormatMajorVersionOp) run(t *test, h historyRecorder) { + var err error + // NB: We no-op the operation if we're already at or above the provided + // format major version. Different runs start at different format major + // versions, making the presence of an error and the error message itself + // non-deterministic if we attempt to upgrade to an older version. + // + //Regardless, subsequent operations should behave identically, which is what + //we're really aiming to test by including this format major version ratchet + //operation. + if t.getDB(o.dbID).FormatMajorVersion() < o.vers { + err = t.getDB(o.dbID).RatchetFormatMajorVersion(o.vers) + } + h.Recordf("%s // %v", o, err) +} + +func (o *dbRatchetFormatMajorVersionOp) String() string { + return fmt.Sprintf("%s.RatchetFormatMajorVersion(%s)", o.dbID, o.vers) +} +func (o *dbRatchetFormatMajorVersionOp) receiver() objID { return o.dbID } +func (o *dbRatchetFormatMajorVersionOp) syncObjs() objIDSlice { return nil } + +type dbRestartOp struct { + dbID objID +} + +func (o *dbRestartOp) run(t *test, h historyRecorder) { + if err := t.restartDB(o.dbID); err != nil { + h.Recordf("%s // %v", o, err) + h.history.err.Store(errors.Wrap(err, "dbRestartOp")) + } else { + h.Recordf("%s", o) + } +} + +func (o *dbRestartOp) String() string { return fmt.Sprintf("%s.Restart()", o.dbID) } +func (o *dbRestartOp) receiver() objID { return o.dbID } +func (o *dbRestartOp) syncObjs() objIDSlice { return nil } + +func formatOps(ops []op) string { + var buf strings.Builder + for _, op := range ops { + fmt.Fprintf(&buf, "%s\n", op) + } + return buf.String() +} + +// replicateOp models an operation that could copy keys from one db to +// another through either an IngestAndExcise, or an Ingest. +type replicateOp struct { + source, dest objID + start, end []byte +} + +func (r *replicateOp) runSharedReplicate( + t *test, h historyRecorder, source, dest *pebble.DB, w *sstable.Writer, sstPath string, +) { + var sharedSSTs []pebble.SharedSSTMeta + var err error + err = source.ScanInternal(context.TODO(), sstable.CategoryAndQoS{}, r.start, r.end, + func(key *pebble.InternalKey, value pebble.LazyValue, _ pebble.IteratorLevel) error { + val, _, err := value.Value(nil) + if err != nil { + panic(err) + } + return w.Add(base.MakeInternalKey(key.UserKey, 0, key.Kind()), val) + }, + func(start, end []byte, seqNum uint64) error { + return w.DeleteRange(start, end) + }, + func(start, end []byte, keys []keyspan.Key) error { + s := keyspan.Span{ + Start: start, + End: end, + Keys: keys, + KeysOrder: 0, + } + return rangekey.Encode(&s, func(k base.InternalKey, v []byte) error { + return w.AddRangeKey(base.MakeInternalKey(k.UserKey, 0, k.Kind()), v) + }) + }, + func(sst *pebble.SharedSSTMeta) error { + sharedSSTs = append(sharedSSTs, *sst) + return nil + }, + ) + if err != nil { + h.Recordf("%s // %v", r, err) + return + } + + _, err = dest.IngestAndExcise([]string{sstPath}, sharedSSTs, pebble.KeyRange{Start: r.start, End: r.end}) + h.Recordf("%s // %v", r, err) +} + +func (r *replicateOp) run(t *test, h historyRecorder) { + // Shared replication only works if shared storage is enabled. + useSharedIngest := t.testOpts.useSharedReplicate + if !t.testOpts.sharedStorageEnabled { + useSharedIngest = false + } + + source := t.getDB(r.source) + dest := t.getDB(r.dest) + sstPath := path.Join(t.tmpDir, fmt.Sprintf("ext-replicate%d.sst", t.idx)) + f, err := t.opts.FS.Create(sstPath) + if err != nil { + h.Recordf("%s // %v", r, err) + return + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), t.opts.MakeWriterOptions(0, dest.FormatMajorVersion().MaxTableFormat())) + + if useSharedIngest { + r.runSharedReplicate(t, h, source, dest, w, sstPath) + return + } + + iter, err := source.NewIter(&pebble.IterOptions{ + LowerBound: r.start, + UpperBound: r.end, + KeyTypes: pebble.IterKeyTypePointsAndRanges, + }) + if err != nil { + panic(err) + } + defer iter.Close() + + // Write rangedels and rangekeydels for the range. This mimics the Excise + // that runSharedReplicate would do. + if err := w.DeleteRange(r.start, r.end); err != nil { + panic(err) + } + if err := w.RangeKeyDelete(r.start, r.end); err != nil { + panic(err) + } + + for ok := iter.SeekGE(r.start); ok && iter.Error() != nil; ok = iter.Next() { + hasPoint, hasRange := iter.HasPointAndRange() + if hasPoint { + val, err := iter.ValueAndErr() + if err != nil { + panic(err) + } + if err := w.Set(iter.Key(), val); err != nil { + panic(err) + } + } + if hasRange && iter.RangeKeyChanged() { + rangeKeys := iter.RangeKeys() + rkStart, rkEnd := iter.RangeBounds() + for i := range rangeKeys { + if err := w.RangeKeySet(rkStart, rkEnd, rangeKeys[i].Suffix, rangeKeys[i].Value); err != nil { + panic(err) + } + } + } + } + if err := w.Close(); err != nil { + panic(err) + } + + err = dest.Ingest([]string{sstPath}) + h.Recordf("%s // %v", r, err) +} + +func (r *replicateOp) String() string { + return fmt.Sprintf("%s.Replicate(%s, %q, %q)", r.source, r.dest, r.start, r.end) +} + +func (r *replicateOp) receiver() objID { return r.source } +func (r *replicateOp) syncObjs() objIDSlice { return objIDSlice{r.dest} } diff --git a/pebble/metamorphic/options.go b/pebble/metamorphic/options.go new file mode 100644 index 0000000..b288c44 --- /dev/null +++ b/pebble/metamorphic/options.go @@ -0,0 +1,676 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package metamorphic + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "golang.org/x/exp/rand" +) + +const ( + // The metamorphic test exercises range keys, so we cannot use an older + // FormatMajorVersion than pebble.FormatRangeKeys. + minimumFormatMajorVersion = pebble.FormatRangeKeys + // The format major version to use in the default options configurations. We + // default to the last format major version of Cockroach 22.2 so we exercise + // the runtime version ratcheting that a cluster upgrading to 23.1 would + // experience. The randomized options may still use format major versions + // that are less than defaultFormatMajorVersion but are at least + // minimumFormatMajorVersion. + defaultFormatMajorVersion = pebble.FormatPrePebblev1Marked + // newestFormatMajorVersionToTest is the most recent format major version + // the metamorphic tests should use. This may be greater than + // pebble.FormatNewest when some format major versions are marked as + // experimental. + newestFormatMajorVersionToTest = pebble.FormatNewest +) + +func parseOptions( + opts *TestOptions, data string, customOptionParsers map[string]func(string) (CustomOption, bool), +) error { + hooks := &pebble.ParseHooks{ + NewCache: pebble.NewCache, + NewFilterPolicy: filterPolicyFromName, + SkipUnknown: func(name, value string) bool { + switch name { + case "TestOptions": + return true + case "TestOptions.strictfs": + opts.strictFS = true + return true + case "TestOptions.ingest_using_apply": + opts.ingestUsingApply = true + return true + case "TestOptions.delete_sized": + opts.deleteSized = true + return true + case "TestOptions.replace_single_delete": + opts.replaceSingleDelete = true + return true + case "TestOptions.use_disk": + opts.useDisk = true + return true + case "TestOptions.initial_state_desc": + opts.initialStateDesc = value + return true + case "TestOptions.initial_state_path": + opts.initialStatePath = value + return true + case "TestOptions.threads": + v, err := strconv.Atoi(value) + if err != nil { + panic(err) + } + opts.threads = v + return true + case "TestOptions.disable_block_property_collector": + v, err := strconv.ParseBool(value) + if err != nil { + panic(err) + } + opts.disableBlockPropertyCollector = v + if v { + opts.Opts.BlockPropertyCollectors = nil + } + return true + case "TestOptions.enable_value_blocks": + opts.enableValueBlocks = true + opts.Opts.Experimental.EnableValueBlocks = func() bool { return true } + return true + case "TestOptions.async_apply_to_db": + opts.asyncApplyToDB = true + return true + case "TestOptions.shared_storage_enabled": + opts.sharedStorageEnabled = true + opts.Opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": remote.NewInMem(), + }) + if opts.Opts.Experimental.CreateOnShared == remote.CreateOnSharedNone { + opts.Opts.Experimental.CreateOnShared = remote.CreateOnSharedAll + } + return true + case "TestOptions.secondary_cache_enabled": + opts.secondaryCacheEnabled = true + opts.Opts.Experimental.SecondaryCacheSizeBytes = 1024 * 1024 * 32 // 32 MBs + return true + case "TestOptions.seed_efos": + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + panic(err) + } + opts.seedEFOS = v + return true + case "TestOptions.ingest_split": + opts.ingestSplit = true + opts.Opts.Experimental.IngestSplit = func() bool { + return true + } + return true + default: + if customOptionParsers == nil { + return false + } + name = strings.TrimPrefix(name, "TestOptions.") + if p, ok := customOptionParsers[name]; ok { + if customOpt, ok := p(value); ok { + opts.CustomOpts = append(opts.CustomOpts, customOpt) + return true + } + } + return false + } + }, + } + err := opts.Opts.Parse(data, hooks) + opts.Opts.EnsureDefaults() + return err +} + +func optionsToString(opts *TestOptions) string { + var buf bytes.Buffer + if opts.strictFS { + fmt.Fprint(&buf, " strictfs=true\n") + } + if opts.ingestUsingApply { + fmt.Fprint(&buf, " ingest_using_apply=true\n") + } + if opts.deleteSized { + fmt.Fprint(&buf, " delete_sized=true\n") + } + if opts.replaceSingleDelete { + fmt.Fprint(&buf, " replace_single_delete=true\n") + } + if opts.useDisk { + fmt.Fprint(&buf, " use_disk=true\n") + } + if opts.initialStatePath != "" { + fmt.Fprintf(&buf, " initial_state_path=%s\n", opts.initialStatePath) + } + if opts.initialStateDesc != "" { + fmt.Fprintf(&buf, " initial_state_desc=%s\n", opts.initialStateDesc) + } + if opts.threads != 0 { + fmt.Fprintf(&buf, " threads=%d\n", opts.threads) + } + if opts.disableBlockPropertyCollector { + fmt.Fprintf(&buf, " disable_block_property_collector=%t\n", opts.disableBlockPropertyCollector) + } + if opts.enableValueBlocks { + fmt.Fprintf(&buf, " enable_value_blocks=%t\n", opts.enableValueBlocks) + } + if opts.asyncApplyToDB { + fmt.Fprint(&buf, " async_apply_to_db=true\n") + } + if opts.sharedStorageEnabled { + fmt.Fprint(&buf, " shared_storage_enabled=true\n") + } + if opts.secondaryCacheEnabled { + fmt.Fprint(&buf, " secondary_cache_enabled=true\n") + } + if opts.seedEFOS != 0 { + fmt.Fprintf(&buf, " seed_efos=%d\n", opts.seedEFOS) + } + if opts.ingestSplit { + fmt.Fprintf(&buf, " ingest_split=%v\n", opts.ingestSplit) + } + for _, customOpt := range opts.CustomOpts { + fmt.Fprintf(&buf, " %s=%s\n", customOpt.Name(), customOpt.Value()) + } + + s := opts.Opts.String() + if buf.Len() == 0 { + return s + } + return s + "\n[TestOptions]\n" + buf.String() +} + +func defaultTestOptions() *TestOptions { + return &TestOptions{ + Opts: defaultOptions(), + threads: 16, + } +} + +func defaultOptions() *pebble.Options { + opts := &pebble.Options{ + Comparer: testkeys.Comparer, + FS: vfs.NewMem(), + FormatMajorVersion: defaultFormatMajorVersion, + Levels: []pebble.LevelOptions{{ + FilterPolicy: bloom.FilterPolicy(10), + }}, + BlockPropertyCollectors: blockPropertyCollectorConstructors, + } + return opts +} + +// TestOptions describes the options configuring an individual run of the +// metamorphic tests. +type TestOptions struct { + // Opts holds the *pebble.Options for the test. + Opts *pebble.Options + // CustomOptions holds custom test options that are defined outside of this + // package. + CustomOpts []CustomOption + useDisk bool + strictFS bool + threads int + // Use Batch.Apply rather than DB.Ingest. + ingestUsingApply bool + // Use Batch.DeleteSized rather than Batch.Delete. + deleteSized bool + // Replace a SINGLEDEL with a DELETE. + replaceSingleDelete bool + // The path on the local filesystem where the initial state of the database + // exists. Empty if the test run begins from an empty database state. + initialStatePath string + // A human-readable string describing the initial state of the database. + // Empty if the test run begins from an empty database state. + initialStateDesc string + // Disable the block property collector, which may be used by block property + // filters. + disableBlockPropertyCollector bool + // Enable the use of value blocks. + enableValueBlocks bool + // Use DB.ApplyNoSyncWait for applies that want to sync the WAL. + asyncApplyToDB bool + // Enable the use of shared storage. + sharedStorageEnabled bool + // Enables the use of shared replication in TestOptions. + useSharedReplicate bool + // Enable the secondary cache. Only effective if sharedStorageEnabled is + // also true. + secondaryCacheEnabled bool + // If nonzero, enables the use of EventuallyFileOnlySnapshots for + // newSnapshotOps that are keyspan-bounded. The set of which newSnapshotOps + // are actually created as EventuallyFileOnlySnapshots is deterministically + // derived from the seed and the operation index. + seedEFOS uint64 + // Enables ingest splits. Saved here for serialization as Options does not + // serialize this. + ingestSplit bool +} + +// CustomOption defines a custom option that configures the behavior of an +// individual test run. Like all test options, custom options are serialized to +// the OPTIONS file even if they're not options ordinarily understood by Pebble. +type CustomOption interface { + // Name returns the name of the custom option. This is the key under which + // the option appears in the OPTIONS file, within the [TestOptions] stanza. + Name() string + // Value returns the value of the custom option, serialized as it should + // appear within the OPTIONS file. + Value() string + // Close is run after the test database has been closed at the end of the + // test as well as during restart operations within the test sequence. It's + // passed a copy of the *pebble.Options. If the custom options hold on to + // any resources outside, Close should release them. + Close(*pebble.Options) error + // Open is run before the test runs and during a restart operation after the + // test database has been closed and Close has been called. It's passed a + // copy of the *pebble.Options. If the custom options must acquire any + // resources before the test continues, it should reacquire them. + Open(*pebble.Options) error + + // TODO(jackson): provide additional hooks for custom options changing the + // behavior of a run. +} + +func standardOptions() []*TestOptions { + // The index labels are not strictly necessary, but they make it easier to + // find which options correspond to a failure. + stdOpts := []string{ + 0: "", // default options + 1: ` +[Options] + cache_size=1 +`, + 2: ` +[Options] + disable_wal=true +`, + 3: ` +[Options] + l0_compaction_threshold=1 +`, + 4: ` +[Options] + l0_compaction_threshold=1 + l0_stop_writes_threshold=1 +`, + 5: ` +[Options] + lbase_max_bytes=1 +`, + 6: ` +[Options] + max_manifest_file_size=1 +`, + 7: ` +[Options] + max_open_files=1 +`, + 8: ` +[Options] + mem_table_size=2000 +`, + 9: ` +[Options] + mem_table_stop_writes_threshold=2 +`, + 10: ` +[Options] + wal_dir=data/wal +`, + 11: ` +[Level "0"] + block_restart_interval=1 +`, + 12: ` +[Level "0"] + block_size=1 +`, + 13: ` +[Level "0"] + compression=NoCompression +`, + 14: ` +[Level "0"] + index_block_size=1 +`, + 15: ` +[Level "0"] + target_file_size=1 +`, + 16: ` +[Level "0"] + filter_policy=none +`, + // 1GB + 17: ` +[Options] + bytes_per_sync=1073741824 +[TestOptions] + strictfs=true +`, + 18: ` +[Options] + max_concurrent_compactions=2 +`, + 19: ` +[TestOptions] + ingest_using_apply=true +`, + 20: ` +[TestOptions] + replace_single_delete=true +`, + 21: ` +[TestOptions] + use_disk=true +`, + 22: ` +[Options] + max_writer_concurrency=2 + force_writer_parallelism=true +`, + 23: ` +[TestOptions] + disable_block_property_collector=true +`, + 24: ` +[TestOptions] + threads=1 +`, + 25: ` +[TestOptions] + enable_value_blocks=true +`, + 26: fmt.Sprintf(` +[Options] + format_major_version=%s +`, newestFormatMajorVersionToTest), + 27: ` +[TestOptions] + shared_storage_enabled=true + secondary_cache_enabled=true +`, + } + + opts := make([]*TestOptions, len(stdOpts)) + for i := range opts { + opts[i] = defaultTestOptions() + // NB: The standard options by definition can never include custom + // options, so no need to propagate custom option parsers. + if err := parseOptions(opts[i], stdOpts[i], nil /* custom option parsers */); err != nil { + panic(err) + } + } + return opts +} + +func randomOptions( + rng *rand.Rand, customOptionParsers map[string]func(string) (CustomOption, bool), +) *TestOptions { + testOpts := defaultTestOptions() + opts := testOpts.Opts + + // There are some private options, which we don't want users to fiddle with. + // There's no way to set it through the public interface. The only method is + // through Parse. + { + var privateOpts bytes.Buffer + fmt.Fprintln(&privateOpts, `[Options]`) + if rng.Intn(3) == 0 /* 33% */ { + fmt.Fprintln(&privateOpts, ` disable_delete_only_compactions=true`) + } + if rng.Intn(3) == 0 /* 33% */ { + fmt.Fprintln(&privateOpts, ` disable_elision_only_compactions=true`) + } + if rng.Intn(5) == 0 /* 20% */ { + fmt.Fprintln(&privateOpts, ` disable_lazy_combined_iteration=true`) + } + if privateOptsStr := privateOpts.String(); privateOptsStr != `[Options]\n` { + parseOptions(testOpts, privateOptsStr, customOptionParsers) + } + } + + opts.BytesPerSync = 1 << uint(rng.Intn(28)) // 1B - 256MB + opts.Cache = cache.New(1 << uint(rng.Intn(30))) // 1B - 1GB + opts.DisableWAL = rng.Intn(2) == 0 + opts.FlushDelayDeleteRange = time.Millisecond * time.Duration(5*rng.Intn(245)) // 5-250ms + opts.FlushDelayRangeKey = time.Millisecond * time.Duration(5*rng.Intn(245)) // 5-250ms + opts.FlushSplitBytes = 1 << rng.Intn(20) // 1B - 1MB + opts.FormatMajorVersion = minimumFormatMajorVersion + n := int(newestFormatMajorVersionToTest - opts.FormatMajorVersion) + opts.FormatMajorVersion += pebble.FormatMajorVersion(rng.Intn(n + 1)) + opts.Experimental.L0CompactionConcurrency = 1 + rng.Intn(4) // 1-4 + opts.Experimental.LevelMultiplier = 5 << rng.Intn(7) // 5 - 320 + opts.TargetByteDeletionRate = 1 << uint(20+rng.Intn(10)) // 1MB - 1GB + opts.Experimental.ValidateOnIngest = rng.Intn(2) != 0 + opts.L0CompactionThreshold = 1 + rng.Intn(100) // 1 - 100 + opts.L0CompactionFileThreshold = 1 << rng.Intn(11) // 1 - 1024 + opts.L0StopWritesThreshold = 1 + rng.Intn(100) // 1 - 100 + if opts.L0StopWritesThreshold < opts.L0CompactionThreshold { + opts.L0StopWritesThreshold = opts.L0CompactionThreshold + } + opts.LBaseMaxBytes = 1 << uint(rng.Intn(30)) // 1B - 1GB + maxConcurrentCompactions := rng.Intn(3) + 1 // 1-3 + opts.MaxConcurrentCompactions = func() int { + return maxConcurrentCompactions + } + opts.MaxManifestFileSize = 1 << uint(rng.Intn(30)) // 1B - 1GB + opts.MemTableSize = 2 << (10 + uint(rng.Intn(16))) // 2KB - 256MB + opts.MemTableStopWritesThreshold = 2 + rng.Intn(5) // 2 - 5 + if rng.Intn(2) == 0 { + opts.WALDir = "data/wal" + } + if rng.Intn(4) == 0 { + // Enable Writer parallelism for 25% of the random options. Setting + // MaxWriterConcurrency to any value greater than or equal to 1 has the + // same effect currently. + opts.Experimental.MaxWriterConcurrency = 2 + opts.Experimental.ForceWriterParallelism = true + } + if rng.Intn(2) == 0 { + opts.Experimental.DisableIngestAsFlushable = func() bool { return true } + } + var lopts pebble.LevelOptions + lopts.BlockRestartInterval = 1 + rng.Intn(64) // 1 - 64 + lopts.BlockSize = 1 << uint(rng.Intn(24)) // 1 - 16MB + lopts.BlockSizeThreshold = 50 + rng.Intn(50) // 50 - 100 + lopts.IndexBlockSize = 1 << uint(rng.Intn(24)) // 1 - 16MB + lopts.TargetFileSize = 1 << uint(rng.Intn(28)) // 1 - 256MB + + // We either use no bloom filter, the default filter, or a filter with + // randomized bits-per-key setting. We zero out the Filters map. It'll get + // repopulated on EnsureDefaults accordingly. + opts.Filters = nil + switch rng.Intn(3) { + case 0: + lopts.FilterPolicy = nil + case 1: + lopts.FilterPolicy = bloom.FilterPolicy(10) + default: + lopts.FilterPolicy = newTestingFilterPolicy(1 << rng.Intn(5)) + } + + // We use either no compression, snappy compression or zstd compression. + switch rng.Intn(3) { + case 0: + lopts.Compression = pebble.NoCompression + case 1: + lopts.Compression = pebble.ZstdCompression + default: + lopts.Compression = pebble.SnappyCompression + } + opts.Levels = []pebble.LevelOptions{lopts} + + // Explicitly disable disk-backed FS's for the random configurations. The + // single standard test configuration that uses a disk-backed FS is + // sufficient. + testOpts.useDisk = false + testOpts.strictFS = rng.Intn(2) != 0 // Only relevant for MemFS. + testOpts.threads = rng.Intn(runtime.GOMAXPROCS(0)) + 1 + if testOpts.strictFS { + opts.DisableWAL = false + } + testOpts.ingestUsingApply = rng.Intn(2) != 0 + testOpts.deleteSized = rng.Intn(2) != 0 + testOpts.replaceSingleDelete = rng.Intn(2) != 0 + testOpts.disableBlockPropertyCollector = rng.Intn(2) == 1 + if testOpts.disableBlockPropertyCollector { + testOpts.Opts.BlockPropertyCollectors = nil + } + testOpts.enableValueBlocks = opts.FormatMajorVersion >= pebble.FormatSSTableValueBlocks && + rng.Intn(2) != 0 + if testOpts.enableValueBlocks { + testOpts.Opts.Experimental.EnableValueBlocks = func() bool { return true } + } + testOpts.asyncApplyToDB = rng.Intn(2) != 0 + // 20% of time, enable shared storage. + if rng.Intn(5) == 0 { + testOpts.sharedStorageEnabled = true + inMemShared := remote.NewInMem() + testOpts.Opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": inMemShared, + }) + // If shared storage is enabled, pick between writing all files on shared + // vs. lower levels only, 50% of the time. + testOpts.Opts.Experimental.CreateOnShared = remote.CreateOnSharedAll + if rng.Intn(2) == 0 { + testOpts.Opts.Experimental.CreateOnShared = remote.CreateOnSharedLower + } + // If shared storage is enabled, enable secondary cache 50% of time. + if rng.Intn(2) == 0 { + testOpts.secondaryCacheEnabled = true + // TODO(josh): Randomize various secondary cache settings. + testOpts.Opts.Experimental.SecondaryCacheSizeBytes = 1024 * 1024 * 32 // 32 MBs + } + // 50% of the time, enable shared replication. + testOpts.useSharedReplicate = rng.Intn(2) == 0 + } + testOpts.seedEFOS = rng.Uint64() + testOpts.ingestSplit = rng.Intn(2) == 0 + opts.Experimental.IngestSplit = func() bool { return testOpts.ingestSplit } + testOpts.Opts.EnsureDefaults() + return testOpts +} + +func setupInitialState(dataDir string, testOpts *TestOptions) error { + // Copy (vfs.Default,/data) to (testOpts.opts.FS,). + ok, err := vfs.Clone( + vfs.Default, + testOpts.Opts.FS, + vfs.Default.PathJoin(testOpts.initialStatePath, "data"), + dataDir, + vfs.CloneSync, + vfs.CloneSkip(func(filename string) bool { + // Skip the archive of historical files, any checkpoints created by + // operations and files staged for ingest in tmp. + b := filepath.Base(filename) + return b == "archive" || b == "checkpoints" || b == "tmp" + })) + if err != nil { + return err + } else if !ok { + return os.ErrNotExist + } + + // Tests with wal_dir set store their WALs in a `wal` directory. The source + // database (initialStatePath) could've had wal_dir set, or the current test + // options (testOpts) could have wal_dir set, or both. + fs := testOpts.Opts.FS + walDir := fs.PathJoin(dataDir, "wal") + if err := fs.MkdirAll(walDir, os.ModePerm); err != nil { + return err + } + + // Copy /wal/*.log -> . + src, dst := walDir, dataDir + if testOpts.Opts.WALDir != "" { + // Copy /*.log -> /wal. + src, dst = dst, src + } + return moveLogs(fs, src, dst) +} + +func moveLogs(fs vfs.FS, srcDir, dstDir string) error { + ls, err := fs.List(srcDir) + if err != nil { + return err + } + for _, f := range ls { + if filepath.Ext(f) != ".log" { + continue + } + src := fs.PathJoin(srcDir, f) + dst := fs.PathJoin(dstDir, f) + if err := fs.Rename(src, dst); err != nil { + return err + } + } + return nil +} + +var blockPropertyCollectorConstructors = []func() pebble.BlockPropertyCollector{ + sstable.NewTestKeysBlockPropertyCollector, +} + +// testingFilterPolicy is used to allow bloom filter policies with non-default +// bits-per-key setting. It is necessary because the name of the production +// filter policy is fixed (see bloom.FilterPolicy.Name()); we need to output a +// custom policy name to the OPTIONS file that the test can then parse. +type testingFilterPolicy struct { + bloom.FilterPolicy +} + +var _ pebble.FilterPolicy = (*testingFilterPolicy)(nil) + +func newTestingFilterPolicy(bitsPerKey int) *testingFilterPolicy { + return &testingFilterPolicy{ + FilterPolicy: bloom.FilterPolicy(bitsPerKey), + } +} + +const testingFilterPolicyFmt = "testing_bloom_filter/bits_per_key=%d" + +// Name implements the pebble.FilterPolicy interface. +func (t *testingFilterPolicy) Name() string { + if t.FilterPolicy == 10 { + return "rocksdb.BuiltinBloomFilter" + } + return fmt.Sprintf(testingFilterPolicyFmt, t.FilterPolicy) +} + +func filterPolicyFromName(name string) (pebble.FilterPolicy, error) { + switch name { + case "none": + return nil, nil + case "rocksdb.BuiltinBloomFilter": + return bloom.FilterPolicy(10), nil + } + var bitsPerKey int + if _, err := fmt.Sscanf(name, testingFilterPolicyFmt, &bitsPerKey); err != nil { + return nil, errors.Errorf("Invalid filter policy name '%s'", name) + } + return newTestingFilterPolicy(bitsPerKey), nil +} diff --git a/pebble/metamorphic/options_test.go b/pebble/metamorphic/options_test.go new file mode 100644 index 0000000..cc86ca9 --- /dev/null +++ b/pebble/metamorphic/options_test.go @@ -0,0 +1,234 @@ +// 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 metamorphic + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/kr/pretty" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func TestSetupInitialState(t *testing.T) { + // Construct a small database in the test's TempDir. + initialStatePath := t.TempDir() + initialDataPath := vfs.Default.PathJoin(initialStatePath, "data") + { + d, err := pebble.Open(initialDataPath, &pebble.Options{}) + require.NoError(t, err) + const maxKeyLen = 2 + ks := testkeys.Alpha(maxKeyLen) + var key [maxKeyLen]byte + for i := int64(0); i < ks.Count(); i++ { + n := testkeys.WriteKey(key[:], ks, i) + require.NoError(t, d.Set(key[:n], key[:n], pebble.NoSync)) + if i%100 == 0 { + require.NoError(t, d.Flush()) + } + } + require.NoError(t, d.Close()) + } + ls, err := vfs.Default.List(initialStatePath) + require.NoError(t, err) + + // setupInitialState with an initial state path set to the test's TempDir + // should populate opts.opts.FS with the directory's contents. + opts := &TestOptions{ + Opts: defaultOptions(), + initialStatePath: initialStatePath, + initialStateDesc: "test", + } + require.NoError(t, setupInitialState("data", opts)) + copied, err := opts.Opts.FS.List("") + require.NoError(t, err) + require.ElementsMatch(t, ls, copied) +} + +func TestOptionsRoundtrip(t *testing.T) { + // Some fields must be ignored to avoid spurious diffs. + ignorePrefixes := []string{ + // Pointers + "Cache:", + "Cache.", + "FS:", + "TableCache:", + // Function pointers + "BlockPropertyCollectors:", + "EventListener:", + "MaxConcurrentCompactions:", + "Experimental.EnableValueBlocks:", + "Experimental.DisableIngestAsFlushable:", + "Experimental.RemoteStorage:", + "Experimental.IngestSplit:", + // Floating points + "Experimental.PointTombstoneWeight:", + } + + // Ensure that we unref any caches created, so invariants builds don't + // complain about the leaked ref counts. + maybeUnref := func(o *TestOptions) { + if o.Opts.Cache != nil { + o.Opts.Cache.Unref() + } + } + + checkOptions := func(t *testing.T, o *TestOptions) { + s := optionsToString(o) + t.Logf("Serialized options:\n%s\n", s) + + parsed := defaultTestOptions() + require.NoError(t, parseOptions(parsed, s, nil)) + maybeUnref(parsed) + got := optionsToString(parsed) + require.Equal(t, s, got) + t.Logf("Re-serialized options:\n%s\n", got) + + // In some options, the closure obscures the underlying value. Check + // that the return values are equal. + require.Equal(t, o.Opts.Experimental.EnableValueBlocks == nil, parsed.Opts.Experimental.EnableValueBlocks == nil) + if o.Opts.Experimental.EnableValueBlocks != nil { + require.Equal(t, o.Opts.Experimental.EnableValueBlocks(), parsed.Opts.Experimental.EnableValueBlocks()) + } + require.Equal(t, o.Opts.Experimental.DisableIngestAsFlushable == nil, parsed.Opts.Experimental.DisableIngestAsFlushable == nil) + if o.Opts.Experimental.DisableIngestAsFlushable != nil { + require.Equal(t, o.Opts.Experimental.DisableIngestAsFlushable(), parsed.Opts.Experimental.DisableIngestAsFlushable()) + } + if o.Opts.Experimental.IngestSplit != nil && o.Opts.Experimental.IngestSplit() { + require.Equal(t, o.Opts.Experimental.IngestSplit(), parsed.Opts.Experimental.IngestSplit()) + } + require.Equal(t, o.Opts.MaxConcurrentCompactions(), parsed.Opts.MaxConcurrentCompactions()) + require.Equal(t, len(o.Opts.BlockPropertyCollectors), len(parsed.Opts.BlockPropertyCollectors)) + + diff := pretty.Diff(o.Opts, parsed.Opts) + cleaned := diff[:0] + for _, d := range diff { + var ignored bool + for _, prefix := range ignorePrefixes { + if strings.HasPrefix(d, prefix) { + ignored = true + break + } + } + if !ignored { + cleaned = append(cleaned, d) + } + } + require.Equal(t, diff[:0], cleaned) + } + + standard := standardOptions() + for i := range standard { + t.Run(fmt.Sprintf("standard-%03d", i), func(t *testing.T) { + defer maybeUnref(standard[i]) + checkOptions(t, standard[i]) + }) + } + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + for i := 0; i < 100; i++ { + t.Run(fmt.Sprintf("random-%03d", i), func(t *testing.T) { + o := randomOptions(rng, nil) + defer maybeUnref(o) + checkOptions(t, o) + }) + } +} + +// TestBlockPropertiesParse ensures that the testkeys block property collector +// is in use by default. It runs a single OPTIONS run of the metamorphic tests +// and scans the resulting data directory to ensure there's at least one sstable +// with the property. It runs the test with the archive cleaner to avoid any +// flakiness from small working sets of keys. +func TestBlockPropertiesParse(t *testing.T) { + const fixedSeed = 1 + const numOps = 10_000 + metaDir := t.TempDir() + + rng := rand.New(rand.NewSource(fixedSeed)) + ops := generate(rng, numOps, presetConfigs[0], newKeyManager(1 /* numInstances */)) + opsPath := filepath.Join(metaDir, "ops") + formattedOps := formatOps(ops) + require.NoError(t, os.WriteFile(opsPath, []byte(formattedOps), 0644)) + + runDir := filepath.Join(metaDir, "run") + require.NoError(t, os.MkdirAll(runDir, os.ModePerm)) + optionsPath := filepath.Join(runDir, "OPTIONS") + opts := defaultTestOptions() + opts.Opts.EnsureDefaults() + opts.Opts.Cleaner = pebble.ArchiveCleaner{} + optionsStr := optionsToString(opts) + require.NoError(t, os.WriteFile(optionsPath, []byte(optionsStr), 0644)) + + RunOnce(t, runDir, fixedSeed, filepath.Join(runDir, "history"), KeepData{}) + var foundTableBlockProperty bool + require.NoError(t, filepath.Walk(filepath.Join(runDir, "data"), + func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if filepath.Ext(path) != ".sst" { + return nil + } + f, err := vfs.Default.Open(path) + if err != nil { + return err + } + readable, err := sstable.NewSimpleReadable(f) + if err != nil { + return err + } + r, err := sstable.NewReader(readable, opts.Opts.MakeReaderOptions()) + if err != nil { + return err + } + _, ok := r.Properties.UserProperties[opts.Opts.BlockPropertyCollectors[0]().Name()] + foundTableBlockProperty = foundTableBlockProperty || ok + return r.Close() + })) + require.True(t, foundTableBlockProperty) +} + +func TestCustomOptionParser(t *testing.T) { + customOptionParsers := map[string]func(string) (CustomOption, bool){ + "foo": func(value string) (CustomOption, bool) { + return testCustomOption{name: "foo", value: value}, true + }, + } + + o1 := defaultTestOptions() + o2 := defaultTestOptions() + + require.NoError(t, parseOptions(o1, ` +[TestOptions] + foo=bar +`, customOptionParsers)) + require.NoError(t, parseOptions(o2, optionsToString(o1), customOptionParsers)) + defer o2.Opts.Cache.Unref() + + for _, o := range []*TestOptions{o1, o2} { + require.Equal(t, 1, len(o.CustomOpts)) + require.Equal(t, "foo", o.CustomOpts[0].Name()) + require.Equal(t, "bar", o.CustomOpts[0].Value()) + } +} + +type testCustomOption struct { + name, value string +} + +func (o testCustomOption) Name() string { return o.name } +func (o testCustomOption) Value() string { return o.value } +func (o testCustomOption) Close(*pebble.Options) error { return nil } +func (o testCustomOption) Open(*pebble.Options) error { return nil } diff --git a/pebble/metamorphic/parser.go b/pebble/metamorphic/parser.go new file mode 100644 index 0000000..dcc19e2 --- /dev/null +++ b/pebble/metamorphic/parser.go @@ -0,0 +1,599 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package metamorphic + +import ( + "fmt" + "go/scanner" + "go/token" + "reflect" + "strconv" + "strings" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" +) + +type methodInfo struct { + constructor func() op + validTags uint32 +} + +func makeMethod(i interface{}, tags ...objTag) *methodInfo { + var validTags uint32 + for _, tag := range tags { + validTags |= 1 << tag + } + + t := reflect.TypeOf(i) + return &methodInfo{ + constructor: func() op { + return reflect.New(t).Interface().(op) + }, + validTags: validTags, + } +} + +// args returns the receiverID, targetID and arguments for the op. The +// receiverID is the ID of the object the op will be applied to. The targetID +// is the ID of the object for assignment. If the method does not return a new +// object, then targetID will be nil. The argument list is just what it sounds +// like: the list of arguments for the operation. +func opArgs(op op) (receiverID *objID, targetID *objID, args []interface{}) { + switch t := op.(type) { + case *applyOp: + return &t.writerID, nil, []interface{}{&t.batchID} + case *checkpointOp: + return &t.dbID, nil, []interface{}{&t.spans} + case *closeOp: + return &t.objID, nil, nil + case *compactOp: + return &t.dbID, nil, []interface{}{&t.start, &t.end, &t.parallelize} + case *batchCommitOp: + return &t.batchID, nil, nil + case *dbRatchetFormatMajorVersionOp: + return &t.dbID, nil, []interface{}{&t.vers} + case *dbRestartOp: + return &t.dbID, nil, nil + case *deleteOp: + return &t.writerID, nil, []interface{}{&t.key} + case *deleteRangeOp: + return &t.writerID, nil, []interface{}{&t.start, &t.end} + case *iterFirstOp: + return &t.iterID, nil, nil + case *flushOp: + return &t.db, nil, nil + case *getOp: + return &t.readerID, nil, []interface{}{&t.key} + case *ingestOp: + return &t.dbID, nil, []interface{}{&t.batchIDs} + case *initOp: + return nil, nil, []interface{}{&t.dbSlots, &t.batchSlots, &t.iterSlots, &t.snapshotSlots} + case *iterLastOp: + return &t.iterID, nil, nil + case *mergeOp: + return &t.writerID, nil, []interface{}{&t.key, &t.value} + case *newBatchOp: + return &t.dbID, &t.batchID, nil + case *newIndexedBatchOp: + return &t.dbID, &t.batchID, nil + case *newIterOp: + return &t.readerID, &t.iterID, []interface{}{&t.lower, &t.upper, &t.keyTypes, &t.filterMin, &t.filterMax, &t.useL6Filters, &t.maskSuffix} + case *newIterUsingCloneOp: + return &t.existingIterID, &t.iterID, []interface{}{&t.refreshBatch, &t.lower, &t.upper, &t.keyTypes, &t.filterMin, &t.filterMax, &t.useL6Filters, &t.maskSuffix} + case *newSnapshotOp: + return &t.dbID, &t.snapID, []interface{}{&t.bounds} + case *iterNextOp: + return &t.iterID, nil, []interface{}{&t.limit} + case *iterNextPrefixOp: + return &t.iterID, nil, nil + case *iterCanSingleDelOp: + return &t.iterID, nil, []interface{}{} + case *iterPrevOp: + return &t.iterID, nil, []interface{}{&t.limit} + case *iterSeekLTOp: + return &t.iterID, nil, []interface{}{&t.key, &t.limit} + case *iterSeekGEOp: + return &t.iterID, nil, []interface{}{&t.key, &t.limit} + case *iterSeekPrefixGEOp: + return &t.iterID, nil, []interface{}{&t.key} + case *setOp: + return &t.writerID, nil, []interface{}{&t.key, &t.value} + case *iterSetBoundsOp: + return &t.iterID, nil, []interface{}{&t.lower, &t.upper} + case *iterSetOptionsOp: + return &t.iterID, nil, []interface{}{&t.lower, &t.upper, &t.keyTypes, &t.filterMin, &t.filterMax, &t.useL6Filters, &t.maskSuffix} + case *singleDeleteOp: + return &t.writerID, nil, []interface{}{&t.key, &t.maybeReplaceDelete} + case *rangeKeyDeleteOp: + return &t.writerID, nil, []interface{}{&t.start, &t.end} + case *rangeKeySetOp: + return &t.writerID, nil, []interface{}{&t.start, &t.end, &t.suffix, &t.value} + case *rangeKeyUnsetOp: + return &t.writerID, nil, []interface{}{&t.start, &t.end, &t.suffix} + case *replicateOp: + return &t.source, nil, []interface{}{&t.dest, &t.start, &t.end} + } + panic(fmt.Sprintf("unsupported op type: %T", op)) +} + +var methods = map[string]*methodInfo{ + "Apply": makeMethod(applyOp{}, dbTag, batchTag), + "Checkpoint": makeMethod(checkpointOp{}, dbTag), + "Clone": makeMethod(newIterUsingCloneOp{}, iterTag), + "Close": makeMethod(closeOp{}, dbTag, batchTag, iterTag, snapTag), + "Commit": makeMethod(batchCommitOp{}, batchTag), + "Compact": makeMethod(compactOp{}, dbTag), + "Delete": makeMethod(deleteOp{}, dbTag, batchTag), + "DeleteRange": makeMethod(deleteRangeOp{}, dbTag, batchTag), + "First": makeMethod(iterFirstOp{}, iterTag), + "Flush": makeMethod(flushOp{}, dbTag), + "Get": makeMethod(getOp{}, dbTag, batchTag, snapTag), + "Ingest": makeMethod(ingestOp{}, dbTag), + "Init": makeMethod(initOp{}, dbTag), + "Last": makeMethod(iterLastOp{}, iterTag), + "Merge": makeMethod(mergeOp{}, dbTag, batchTag), + "NewBatch": makeMethod(newBatchOp{}, dbTag), + "NewIndexedBatch": makeMethod(newIndexedBatchOp{}, dbTag), + "NewIter": makeMethod(newIterOp{}, dbTag, batchTag, snapTag), + "NewSnapshot": makeMethod(newSnapshotOp{}, dbTag), + "Next": makeMethod(iterNextOp{}, iterTag), + "NextPrefix": makeMethod(iterNextPrefixOp{}, iterTag), + "InternalNext": makeMethod(iterCanSingleDelOp{}, iterTag), + "Prev": makeMethod(iterPrevOp{}, iterTag), + "RangeKeyDelete": makeMethod(rangeKeyDeleteOp{}, dbTag, batchTag), + "RangeKeySet": makeMethod(rangeKeySetOp{}, dbTag, batchTag), + "RangeKeyUnset": makeMethod(rangeKeyUnsetOp{}, dbTag, batchTag), + "RatchetFormatMajorVersion": makeMethod(dbRatchetFormatMajorVersionOp{}, dbTag), + "Replicate": makeMethod(replicateOp{}, dbTag), + "Restart": makeMethod(dbRestartOp{}, dbTag), + "SeekGE": makeMethod(iterSeekGEOp{}, iterTag), + "SeekLT": makeMethod(iterSeekLTOp{}, iterTag), + "SeekPrefixGE": makeMethod(iterSeekPrefixGEOp{}, iterTag), + "Set": makeMethod(setOp{}, dbTag, batchTag), + "SetBounds": makeMethod(iterSetBoundsOp{}, iterTag), + "SetOptions": makeMethod(iterSetOptionsOp{}, iterTag), + "SingleDelete": makeMethod(singleDeleteOp{}, dbTag, batchTag), +} + +type parser struct { + opts parserOpts + fset *token.FileSet + s scanner.Scanner + objs map[objID]bool +} + +type parserOpts struct { + allowUndefinedObjs bool +} + +func parse(src []byte, opts parserOpts) (_ []op, err error) { + // Various bits of magic incantation to set up a scanner for Go compatible + // syntax. We arranged for the textual format of ops (e.g. op.String()) to + // look like Go which allows us to use the Go scanner for parsing. + p := &parser{ + opts: opts, + fset: token.NewFileSet(), + objs: map[objID]bool{makeObjID(dbTag, 1): true, makeObjID(dbTag, 2): true}, + } + file := p.fset.AddFile("", -1, len(src)) + p.s.Init(file, src, nil /* no error handler */, 0) + return p.parse() +} + +func (p *parser) parse() (_ []op, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + if err, ok = r.(error); ok { + return + } + err = errors.Errorf("%v", r) + } + }() + + var ops []op + for { + op := p.parseOp() + if op == nil { + computeDerivedFields(ops) + return ops, nil + } + ops = append(ops, op) + } +} + +func (p *parser) parseOp() op { + destPos, destTok, destLit := p.s.Scan() + if destTok == token.EOF { + return nil + } + if destTok != token.IDENT { + panic(p.errorf(destPos, "unexpected token: %s %q", destTok, destLit)) + } + if destLit == "Init" { + // () + return p.makeOp(destLit, makeObjID(dbTag, 1), 0, destPos) + } + + destID := p.parseObjID(destPos, destLit) + + pos, tok, lit := p.s.Scan() + switch tok { + case token.PERIOD: + // .() + if !p.objs[destID] { + if p.opts.allowUndefinedObjs { + p.objs[destID] = true + } else { + panic(p.errorf(destPos, "unknown object: %s", destID)) + } + } + _, methodLit := p.scanToken(token.IDENT) + return p.makeOp(methodLit, destID, 0, destPos) + + case token.ASSIGN: + // = .() + srcPos, srcLit := p.scanToken(token.IDENT) + srcID := p.parseObjID(srcPos, srcLit) + if !p.objs[srcID] { + if p.opts.allowUndefinedObjs { + p.objs[srcID] = true + } else { + panic(p.errorf(srcPos, "unknown object %q", srcLit)) + } + } + p.scanToken(token.PERIOD) + _, methodLit := p.scanToken(token.IDENT) + p.objs[destID] = true + return p.makeOp(methodLit, srcID, destID, srcPos) + } + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) +} + +func parseObjID(str string) (objID, error) { + var tag objTag + switch { + case strings.HasPrefix(str, "db"): + tag, str = dbTag, str[2:] + if str == "" { + str = "1" + } + case strings.HasPrefix(str, "batch"): + tag, str = batchTag, str[5:] + case strings.HasPrefix(str, "iter"): + tag, str = iterTag, str[4:] + case strings.HasPrefix(str, "snap"): + tag, str = snapTag, str[4:] + default: + return 0, errors.Newf("unable to parse objectID: %q", str) + } + id, err := strconv.ParseInt(str, 10, 32) + if err != nil { + return 0, err + } + return makeObjID(tag, uint32(id)), nil +} + +func (p *parser) parseObjID(pos token.Pos, str string) objID { + id, err := parseObjID(str) + if err != nil { + panic(p.errorf(pos, "%s", err)) + } + return id +} + +func unquoteBytes(lit string) []byte { + s, err := strconv.Unquote(lit) + if err != nil { + panic(err) + } + if len(s) == 0 { + return nil + } + return []byte(s) +} + +func (p *parser) parseArgs(op op, methodName string, args []interface{}) { + pos, _ := p.scanToken(token.LPAREN) + for i := range args { + if i > 0 { + pos, _ = p.scanToken(token.COMMA) + } + + switch t := args[i].(type) { + case *uint32: + _, lit := p.scanToken(token.INT) + val, err := strconv.ParseUint(lit, 10, 32) + if err != nil { + panic(err) + } + *t = uint32(val) + + case *uint64: + _, lit := p.scanToken(token.INT) + val, err := strconv.ParseUint(lit, 10, 64) + if err != nil { + panic(err) + } + *t = uint64(val) + + case *[]byte: + _, lit := p.scanToken(token.STRING) + *t = unquoteBytes(lit) + + case *bool: + _, lit := p.scanToken(token.IDENT) + b, err := strconv.ParseBool(lit) + if err != nil { + panic(err) + } + *t = b + + case *objID: + pos, lit := p.scanToken(token.IDENT) + *t = p.parseObjID(pos, lit) + + case *[]pebble.KeyRange: + var pending pebble.KeyRange + for { + pos, tok, lit := p.s.Scan() + switch tok { + case token.STRING: + x := unquoteBytes(lit) + if pending.Start == nil { + pending.Start = x + } else { + pending.End = x + *t = append(*t, pending) + pending = pebble.KeyRange{} + } + pos, tok, lit := p.s.Scan() + switch tok { + case token.COMMA: + continue + case token.RPAREN: + p.scanToken(token.SEMICOLON) + return + default: + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) + } + case token.RPAREN: + p.scanToken(token.SEMICOLON) + return + default: + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) + } + } + + case *[]objID: + for { + pos, tok, lit := p.s.Scan() + switch tok { + case token.IDENT: + *t = append(*t, p.parseObjID(pos, lit)) + pos, tok, lit := p.s.Scan() + switch tok { + case token.COMMA: + continue + case token.RPAREN: + p.scanToken(token.SEMICOLON) + return + default: + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) + } + case token.RPAREN: + p.scanToken(token.SEMICOLON) + return + default: + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) + } + } + + case *[]pebble.CheckpointSpan: + pos, tok, lit := p.s.Scan() + switch tok { + case token.RPAREN: + // No spans. + *t = nil + p.scanToken(token.SEMICOLON) + return + + case token.STRING: + var keys [][]byte + for { + s, err := strconv.Unquote(lit) + if err != nil { + panic(p.errorf(pos, "unquoting %q: %v", lit, err)) + } + keys = append(keys, []byte(s)) + + pos, tok, lit = p.s.Scan() + switch tok { + case token.COMMA: + pos, tok, lit = p.s.Scan() + if tok != token.STRING { + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) + } + continue + + case token.RPAREN: + p.scanToken(token.SEMICOLON) + if len(keys)%2 == 1 { + panic(p.errorf(pos, "expected even number of keys")) + } + *t = make([]pebble.CheckpointSpan, len(keys)/2) + for i := range *t { + (*t)[i] = pebble.CheckpointSpan{ + Start: keys[i*2], + End: keys[i*2+1], + } + } + return + + default: + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) + } + } + + default: + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) + } + + case *pebble.FormatMajorVersion: + _, lit := p.scanToken(token.INT) + val, err := strconv.ParseUint(lit, 10, 64) + if err != nil { + panic(err) + } + *t = pebble.FormatMajorVersion(val) + + default: + panic(p.errorf(pos, "%s: unsupported arg[%d] type: %T", methodName, i, args[i])) + } + } + p.scanToken(token.RPAREN) + p.scanToken(token.SEMICOLON) +} + +func (p *parser) scanToken(expected token.Token) (pos token.Pos, lit string) { + pos, tok, lit := p.s.Scan() + if tok != expected { + panic(p.errorf(pos, "unexpected token: %q", p.tokenf(tok, lit))) + } + return pos, lit +} + +func (p *parser) makeOp(methodName string, receiverID, targetID objID, pos token.Pos) op { + info := methods[methodName] + if info == nil { + panic(p.errorf(pos, "unknown op %s.%s", receiverID, methodName)) + } + if info.validTags&(1< op indexes + opsDone []chan struct{} // op index -> done channel + idx int + dir string + opts *pebble.Options + testOpts *TestOptions + writeOpts *pebble.WriteOptions + tmpDir string + // The DBs the test is run on. + dbs []*pebble.DB + // The slots for the batches, iterators, and snapshots. These are read and + // written by the ops to pass state from one op to another. + batches []*pebble.Batch + iters []*retryableIter + snapshots []readerCloser +} + +func newTest(ops []op) *test { + return &test{ + ops: ops, + } +} + +func (t *test) init(h *history, dir string, testOpts *TestOptions, numInstances int) error { + t.dir = dir + t.testOpts = testOpts + t.writeOpts = pebble.NoSync + if testOpts.strictFS { + t.writeOpts = pebble.Sync + } + t.opts = testOpts.Opts.EnsureDefaults() + t.opts.Logger = h + lel := pebble.MakeLoggingEventListener(t.opts.Logger) + t.opts.EventListener = &lel + t.opts.DebugCheck = func(db *pebble.DB) error { + // Wrap the ordinary DebugCheckLevels with retrying + // of injected errors. + return withRetries(func() error { + return pebble.DebugCheckLevels(db) + }) + } + if numInstances < 1 { + numInstances = 1 + } + + t.opsWaitOn, t.opsDone = computeSynchronizationPoints(t.ops) + + defer t.opts.Cache.Unref() + + // If an error occurs and we were using an in-memory FS, attempt to clone to + // on-disk in order to allow post-mortem debugging. Note that always using + // the on-disk FS isn't desirable because there is a large performance + // difference between in-memory and on-disk which causes different code paths + // and timings to be exercised. + maybeExit := func(err error) { + if err == nil || errors.Is(err, errorfs.ErrInjected) || errors.Is(err, pebble.ErrCancelledCompaction) { + return + } + t.maybeSaveData() + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // Exit early on any error from a background operation. + t.opts.EventListener.BackgroundError = func(err error) { + t.opts.Logger.Infof("background error: %s", err) + maybeExit(err) + } + t.opts.EventListener.CompactionEnd = func(info pebble.CompactionInfo) { + t.opts.Logger.Infof("%s", info) + maybeExit(info.Err) + } + t.opts.EventListener.FlushEnd = func(info pebble.FlushInfo) { + t.opts.Logger.Infof("%s", info) + if info.Err != nil && !strings.Contains(info.Err.Error(), "pebble: empty table") { + maybeExit(info.Err) + } + } + t.opts.EventListener.ManifestCreated = func(info pebble.ManifestCreateInfo) { + t.opts.Logger.Infof("%s", info) + maybeExit(info.Err) + } + t.opts.EventListener.ManifestDeleted = func(info pebble.ManifestDeleteInfo) { + t.opts.Logger.Infof("%s", info) + maybeExit(info.Err) + } + t.opts.EventListener.TableDeleted = func(info pebble.TableDeleteInfo) { + t.opts.Logger.Infof("%s", info) + maybeExit(info.Err) + } + t.opts.EventListener.TableIngested = func(info pebble.TableIngestInfo) { + t.opts.Logger.Infof("%s", info) + maybeExit(info.Err) + } + t.opts.EventListener.WALCreated = func(info pebble.WALCreateInfo) { + t.opts.Logger.Infof("%s", info) + maybeExit(info.Err) + } + t.opts.EventListener.WALDeleted = func(info pebble.WALDeleteInfo) { + t.opts.Logger.Infof("%s", info) + maybeExit(info.Err) + } + + for i := range t.testOpts.CustomOpts { + if err := t.testOpts.CustomOpts[i].Open(t.opts); err != nil { + return err + } + } + + t.dbs = make([]*pebble.DB, numInstances) + for i := range t.dbs { + var db *pebble.DB + var err error + if len(t.dbs) > 1 { + dir = path.Join(t.dir, fmt.Sprintf("db%d", i+1)) + } + err = withRetries(func() error { + db, err = pebble.Open(dir, t.opts) + return err + }) + if err != nil { + return err + } + t.dbs[i] = db + h.log.Printf("// db%d.Open() %v", i+1, err) + + if t.testOpts.sharedStorageEnabled { + err = withRetries(func() error { + return db.SetCreatorID(uint64(i + 1)) + }) + if err != nil { + return err + } + h.log.Printf("// db%d.SetCreatorID() %v", i+1, err) + } + } + + var err error + t.tmpDir = t.opts.FS.PathJoin(t.dir, "tmp") + if err = t.opts.FS.MkdirAll(t.tmpDir, 0755); err != nil { + return err + } + if t.testOpts.strictFS { + // Sync the whole directory path for the tmpDir, since restartDB() is executed during + // the test. That would reset MemFS to the synced state, which would make an unsynced + // directory disappear in the middle of the test. It is the responsibility of the test + // (not Pebble) to ensure that it can write the ssts that it will subsequently ingest + // into Pebble. + for { + f, err := t.opts.FS.OpenDir(dir) + if err != nil { + return err + } + if err = f.Sync(); err != nil { + return err + } + if err = f.Close(); err != nil { + return err + } + if len(dir) == 1 { + break + } + dir = t.opts.FS.PathDir(dir) + // TODO(sbhola): PathDir returns ".", which OpenDir() complains about. Fix. + if len(dir) == 1 { + dir = "/" + } + } + } + + return nil +} + +func (t *test) isFMV(dbID objID, fmv pebble.FormatMajorVersion) bool { + db := t.getDB(dbID) + return db.FormatMajorVersion() >= fmv +} + +func (t *test) restartDB(dbID objID) error { + db := t.getDB(dbID) + if !t.testOpts.strictFS { + return nil + } + t.opts.Cache.Ref() + // The fs isn't necessarily a MemFS. + fs, ok := vfs.Root(t.opts.FS).(*vfs.MemFS) + if ok { + fs.SetIgnoreSyncs(true) + } + if err := db.Close(); err != nil { + return err + } + // Release any resources held by custom options. This may be used, for + // example, by the encryption-at-rest custom option (within the Cockroach + // repository) to close the file registry. + for i := range t.testOpts.CustomOpts { + if err := t.testOpts.CustomOpts[i].Close(t.opts); err != nil { + return err + } + } + if ok { + fs.ResetToSyncedState() + fs.SetIgnoreSyncs(false) + } + + // TODO(jackson): Audit errorRate and ensure custom options' hooks semantics + // are well defined within the context of retries. + err := withRetries(func() (err error) { + // Reacquire any resources required by custom options. This may be used, for + // example, by the encryption-at-rest custom option (within the Cockroach + // repository) to reopen the file registry. + for i := range t.testOpts.CustomOpts { + if err := t.testOpts.CustomOpts[i].Open(t.opts); err != nil { + return err + } + } + dir := t.dir + if len(t.dbs) > 1 { + dir = path.Join(dir, fmt.Sprintf("db%d", dbID.slot())) + } + t.dbs[dbID.slot()-1], err = pebble.Open(dir, t.opts) + if err != nil { + return err + } + return err + }) + t.opts.Cache.Unref() + return err +} + +// If an in-memory FS is being used, save the contents to disk. +func (t *test) maybeSaveData() { + rootFS := vfs.Root(t.opts.FS) + if rootFS == vfs.Default { + return + } + _ = os.RemoveAll(t.dir) + if _, err := vfs.Clone(rootFS, vfs.Default, t.dir, t.dir); err != nil { + t.opts.Logger.Infof("unable to clone: %s: %v", t.dir, err) + } +} + +func (t *test) step(h *history) bool { + if t.idx >= len(t.ops) { + return false + } + t.ops[t.idx].run(t, h.recorder(-1 /* thread */, t.idx)) + t.idx++ + return true +} + +func (t *test) setBatch(id objID, b *pebble.Batch) { + if id.tag() != batchTag { + panic(fmt.Sprintf("invalid batch ID: %s", id)) + } + t.batches[id.slot()] = b +} + +func (t *test) setIter(id objID, i *pebble.Iterator) { + if id.tag() != iterTag { + panic(fmt.Sprintf("invalid iter ID: %s", id)) + } + t.iters[id.slot()] = &retryableIter{ + iter: i, + lastKey: nil, + } +} + +type readerCloser interface { + pebble.Reader + io.Closer +} + +func (t *test) setSnapshot(id objID, s readerCloser) { + if id.tag() != snapTag { + panic(fmt.Sprintf("invalid snapshot ID: %s", id)) + } + t.snapshots[id.slot()] = s +} + +func (t *test) clearObj(id objID) { + switch id.tag() { + case dbTag: + t.dbs[id.slot()-1] = nil + case batchTag: + t.batches[id.slot()] = nil + case iterTag: + t.iters[id.slot()] = nil + case snapTag: + t.snapshots[id.slot()] = nil + } +} + +func (t *test) getBatch(id objID) *pebble.Batch { + if id.tag() != batchTag { + panic(fmt.Sprintf("invalid batch ID: %s", id)) + } + return t.batches[id.slot()] +} + +func (t *test) getCloser(id objID) io.Closer { + switch id.tag() { + case dbTag: + return t.dbs[id.slot()-1] + case batchTag: + return t.batches[id.slot()] + case iterTag: + return t.iters[id.slot()] + case snapTag: + return t.snapshots[id.slot()] + } + panic(fmt.Sprintf("cannot close ID: %s", id)) +} + +func (t *test) getIter(id objID) *retryableIter { + if id.tag() != iterTag { + panic(fmt.Sprintf("invalid iter ID: %s", id)) + } + return t.iters[id.slot()] +} + +func (t *test) getReader(id objID) pebble.Reader { + switch id.tag() { + case dbTag: + return t.dbs[id.slot()-1] + case batchTag: + return t.batches[id.slot()] + case snapTag: + return t.snapshots[id.slot()] + } + panic(fmt.Sprintf("invalid reader ID: %s", id)) +} + +func (t *test) getWriter(id objID) pebble.Writer { + switch id.tag() { + case dbTag: + return t.dbs[id.slot()-1] + case batchTag: + return t.batches[id.slot()] + } + panic(fmt.Sprintf("invalid writer ID: %s", id)) +} + +func (t *test) getDB(id objID) *pebble.DB { + switch id.tag() { + case dbTag: + return t.dbs[id.slot()-1] + default: + panic(fmt.Sprintf("invalid writer tag: %v", id.tag())) + } +} + +// Compute the synchronization points between operations. When operating +// with more than 1 thread, operations must synchronize access to shared +// objects. Compute two slices the same length as ops. +// +// opsWaitOn: the value v at index i indicates that operation i must wait +// for the operation at index v to finish before it may run. NB: v < i +// +// opsDone: the channel at index i must be closed when the operation at index i +// completes. This slice is sparse. Operations that are never used as +// synchronization points may have a nil channel. +func computeSynchronizationPoints(ops []op) (opsWaitOn [][]int, opsDone []chan struct{}) { + opsDone = make([]chan struct{}, len(ops)) // operation index -> done channel + opsWaitOn = make([][]int, len(ops)) // operation index -> operation index + lastOpReference := make(map[objID]int) // objID -> operation index + for i, o := range ops { + // Find the last operation that involved the same receiver object. We at + // least need to wait on that operation. + receiver := o.receiver() + waitIndex, ok := lastOpReference[receiver] + lastOpReference[receiver] = i + if !ok { + // Only valid for i=0. For all other operations, the receiver should + // have been referenced by some other operation before it's used as + // a receiver. + if i != 0 && receiver.tag() != dbTag { + panic(fmt.Sprintf("op %s on receiver %s; first reference of %s", ops[i].String(), receiver, receiver)) + } + // The initOp is a little special. We do want to store the objects it's + // syncing on, in `lastOpReference`. + if i != 0 { + continue + } + } + + // The last operation that referenced `receiver` is the one at index + // `waitIndex`. All operations with the same receiver are performed on + // the same thread. We only need to synchronize on the operation at + // `waitIndex` if `receiver` isn't also the receiver on that operation + // too. + if ops[waitIndex].receiver() != receiver { + opsWaitOn[i] = append(opsWaitOn[i], waitIndex) + } + + // In additional to synchronizing on the operation's receiver operation, + // we may need to synchronize on additional objects. For example, + // batch0.Commit() must synchronize its receiver, batch0, but also on + // the DB since it mutates database state. + for _, syncObjID := range o.syncObjs() { + if vi, vok := lastOpReference[syncObjID]; vok { + opsWaitOn[i] = append(opsWaitOn[i], vi) + } + lastOpReference[syncObjID] = i + } + + waitIndexes := opsWaitOn[i] + sort.Ints(waitIndexes) + for _, waitIndex := range waitIndexes { + // If this is the first operation that must wait on the operation at + // `waitIndex`, then there will be no channel for the operation yet. + // Create one. + if opsDone[waitIndex] == nil { + opsDone[waitIndex] = make(chan struct{}) + } + } + } + return opsWaitOn, opsDone +} diff --git a/pebble/metamorphic/testdata/key_manager b/pebble/metamorphic/testdata/key_manager new file mode 100644 index 0000000..8b73f0d --- /dev/null +++ b/pebble/metamorphic/testdata/key_manager @@ -0,0 +1,288 @@ +# run subcommands +# +# add-new-key +# read-keys +# write-keys +# singledel-keys +# op + +run +add-new-key foo +add-new-key foo +---- +"foo" is new +"foo" already tracked + +# Test SET; SINGLEDEL on DB. + +run +read-keys +write-keys +singledel-keys db1 db1 +singledel-keys batch1 db1 +op db1.Set("foo", "foo") +read-keys +write-keys +singledel-keys db1 db1 +singledel-keys batch1 db1 +op db1.SingleDelete("foo", false) +read-keys +write-keys +singledel-keys db1 db1 +---- +read keys: "foo" +write keys: "foo" +singledel keys: (none) +singledel keys: (none) +[db1.Set("foo", "foo")] +read keys: "foo" +write keys: "foo" +singledel keys: "foo" +singledel keys: "foo" +[db1.SingleDelete("foo", false /* maybeReplaceDelete */)] +read keys: "foo" +write keys: "foo" +singledel keys: (none) + + +# Test SET; SINGLEDEL on batch on separate key. + +run +add-new-key bar +op batch1.Set("bar", "bar") +read-keys +write-keys +singledel-keys db1 db1 +singledel-keys batch1 db1 +singledel-keys batch2 db1 +op batch1.SingleDelete("bar", false) +read-keys +write-keys +singledel-keys db1 db1 +singledel-keys batch1 db1 +op db1.Apply(batch1) +write-keys +singledel-keys db1 db1 +---- +"bar" is new +[batch1.Set("bar", "bar")] +read keys: "bar", "foo" +write keys: "bar", "foo" +singledel keys: (none) +singledel keys: "bar" +singledel keys: (none) +[batch1.SingleDelete("bar", false /* maybeReplaceDelete */)] +read keys: "bar", "foo" +write keys: "foo" +singledel keys: (none) +singledel keys: (none) +[db1.Apply(batch1)] +write keys: "bar", "foo" +singledel keys: (none) + +# Test SET on db; SINGLEDEL on batch. + +reset +---- + +run +add-new-key foo +op db1.Set("foo", "foo") +write-keys +singledel-keys db1 db1 +singledel-keys batch1 db1 +op batch1.SingleDelete("foo", false) +write-keys +singledel-keys db1 db1 +singledel-keys batch1 db1 +op db1.Apply(batch1) +write-keys +singledel-keys db1 db1 +op db1.Set("foo", "foo") +singledel-keys db1 db1 +singledel-keys batch1 db1 +---- +"foo" is new +[db1.Set("foo", "foo")] +write keys: "foo" +singledel keys: "foo" +singledel keys: "foo" +[batch1.SingleDelete("foo", false /* maybeReplaceDelete */)] +write keys: (none) +singledel keys: (none) +singledel keys: (none) +[db1.Apply(batch1)] +write keys: "foo" +singledel keys: (none) +[db1.Set("foo", "foo")] +singledel keys: "foo" +singledel keys: "foo" + +# Test SET; DEL; SET; SingleDelete on db. + +reset +---- + +run +add-new-key foo +op db1.Set("foo", "foo") +op db1.Delete("foo") +write-keys +singledel-keys db1 db1 +op db1.Set("foo", "foo") +write-keys +singledel-keys db1 db1 +op db1.SingleDelete("foo", false) +write-keys +singledel-keys db1 db1 +---- +"foo" is new +[db1.Set("foo", "foo")] +[db1.Delete("foo")] +write keys: "foo" +singledel keys: (none) +[db1.Set("foo", "foo")] +write keys: "foo" +singledel keys: "foo" +[db1.SingleDelete("foo", false /* maybeReplaceDelete */)] +write keys: "foo" +singledel keys: (none) + +# Test SET; DEL; SET; DEL on batches. + +reset +---- + +run +add-new-key foo +op batch1.Set("foo", "foo") +op batch1.Delete("foo") +op batch1.Set("foo", "foo") +write-keys +singledel-keys batch1 db1 +op db1.Apply(batch1) +write-keys +---- +"foo" is new +[batch1.Set("foo", "foo")] +[batch1.Delete("foo")] +[batch1.Set("foo", "foo")] +write keys: "foo" +singledel keys: (none) +[db1.Apply(batch1)] +write keys: "foo" + +# "foo" should not be eliible for single delete because set count is 2. + +run +singledel-keys db1 db1 +---- +singledel keys: (none) + +run +op db1.Set("foo", "foo") +---- +[db1.Set("foo", "foo")] + +# "foo" should still not be eliible for single delete because set count is 3. + +run +singledel-keys db1 db1 +---- +singledel keys: (none) + + +run +op batch2.Delete("foo") +op db1.Apply(batch2) +singledel-keys db1 db1 +op db1.Set("foo", "foo") +singledel-keys db1 db1 +---- +[batch2.Delete("foo")] +[db1.Apply(batch2)] +singledel keys: (none) +[db1.Set("foo", "foo")] +singledel keys: "foo" + +# Test SET; MERGE; DEL; SINGLEDEL on DB. + +reset +---- + +run +add-new-key foo +op db.Set("foo", "foo") +singledel-keys db1 db1 +op db1.Merge("foo", "foo") +singledel-keys db1 db1 +op db1.Delete("foo") +write-keys +singledel-keys db1 db1 +op db1.Set("foo", "foo") +write-keys +singledel-keys db1 db1 +op db1.SingleDelete("foo", false) +write-keys +singledel-keys db1 db1 +---- +"foo" is new +[db1.Set("foo", "foo")] +singledel keys: "foo" +[db1.Merge("foo", "foo")] +singledel keys: (none) +[db1.Delete("foo")] +write keys: "foo" +singledel keys: (none) +[db1.Set("foo", "foo")] +write keys: "foo" +singledel keys: "foo" +[db1.SingleDelete("foo", false /* maybeReplaceDelete */)] +write keys: "foo" +singledel keys: (none) + +# Test SET; DEL (db); SET; SINGLEDEL (batch) + +reset +---- + +run +add-new-key foo +op db1.Set("foo", "foo") +singledel-keys db1 db1 +op db1.Delete("foo") +write-keys +singledel-keys db1 db1 +op db1.Set("foo", "foo") +write-keys +singledel-keys db1 db1 +singledel-keys batch1 db1 +op batch1.SingleDelete("foo", false) +write-keys +singledel-keys db1 db1 +singledel-keys batch1 db1 +op db1.Apply(batch1) +write-keys +singledel-keys db1 db1 +op db1.Set("foo", "foo") +singledel-keys db1 db1 +---- +"foo" is new +[db1.Set("foo", "foo")] +singledel keys: "foo" +[db1.Delete("foo")] +write keys: "foo" +singledel keys: (none) +[db1.Set("foo", "foo")] +write keys: "foo" +singledel keys: "foo" +singledel keys: "foo" +[batch1.SingleDelete("foo", false /* maybeReplaceDelete */)] +write keys: (none) +singledel keys: (none) +singledel keys: (none) +[db1.Apply(batch1)] +write keys: "foo" +singledel keys: (none) +[db1.Set("foo", "foo")] +singledel keys: "foo" diff --git a/pebble/metamorphic/testdata/parser b/pebble/metamorphic/testdata/parser new file mode 100644 index 0000000..0b29a88 --- /dev/null +++ b/pebble/metamorphic/testdata/parser @@ -0,0 +1,40 @@ +parse +foo +---- +1:1: unable to parse objectID: "foo" + +parse +"foo" +---- +1:1: unexpected token: STRING "\"foo\"" + +parse +db.bar() +---- +1:1: unknown op db1.bar + +parse +db.Apply() +---- +1:10: unexpected token: ")" + +parse +db.Apply(hello) +---- +1:10: unable to parse objectID: "hello" + +parse +db.NewBatch() +---- +1:1: assignment expected for db1.NewBatch + +parse +batch0 = db.Apply() +---- +1:10: cannot use db1.Apply in assignment + +parse +batch0 = db.NewBatch() +batch0.First() +---- +2:1: batch0.First: First is not a method on batch0 diff --git a/pebble/metamorphic/testdata/reorder_history b/pebble/metamorphic/testdata/reorder_history new file mode 100644 index 0000000..663f7f7 --- /dev/null +++ b/pebble/metamorphic/testdata/reorder_history @@ -0,0 +1,33 @@ +reorder +Init(49 /* batches */, 66 /* iters */, 46 /* snapshots */) #0 +db.Set("ynnjczfq", "rvfk") // #1 +db.Get("ynnjczfq") // ["rvfk"] #2 +db.DeleteRange("ynnjczfq", "ynnjczfq") // #3 +db.SingleDelete("ynnjczfq", false /* maybeReplaceDelete */) // #4 +db.Restart() #5 +db.DeleteRange("ynnjczfq", "ynnjczfq") // #6 +---- +Init(49 /* batches */, 66 /* iters */, 46 /* snapshots */) #0 +db.Set("ynnjczfq", "rvfk") // #1 +db.Get("ynnjczfq") // ["rvfk"] #2 +db.DeleteRange("ynnjczfq", "ynnjczfq") // #3 +db.SingleDelete("ynnjczfq", false /* maybeReplaceDelete */) // #4 +db.Restart() #5 +db.DeleteRange("ynnjczfq", "ynnjczfq") // #6 + +reorder +db.DeleteRange("ynnjczfq", "ynnjczfq") // #6 +db.Restart() #5 +db.SingleDelete("ynnjczfq", false /* maybeReplaceDelete */) // #4 +db.DeleteRange("ynnjczfq", "ynnjczfq") // #3 +db.Get("ynnjczfq") // ["rvfk"] #2 +db.Set("ynnjczfq", "rvfk") // #1 +Init(49 /* batches */, 66 /* iters */, 46 /* snapshots */) #0 +---- +Init(49 /* batches */, 66 /* iters */, 46 /* snapshots */) #0 +db.Set("ynnjczfq", "rvfk") // #1 +db.Get("ynnjczfq") // ["rvfk"] #2 +db.DeleteRange("ynnjczfq", "ynnjczfq") // #3 +db.SingleDelete("ynnjczfq", false /* maybeReplaceDelete */) // #4 +db.Restart() #5 +db.DeleteRange("ynnjczfq", "ynnjczfq") // #6 diff --git a/pebble/metamorphic/utils.go b/pebble/metamorphic/utils.go new file mode 100644 index 0000000..f5c0a83 --- /dev/null +++ b/pebble/metamorphic/utils.go @@ -0,0 +1,104 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package metamorphic + +import ( + "fmt" + "sort" + + "golang.org/x/exp/rand" +) + +// objTag identifies the type for an object: DB, Batch, Iter, or Snapshot. +type objTag uint8 + +const ( + dbTag objTag = iota + 1 + batchTag + iterTag + snapTag +) + +// objID identifies a particular object. The top 4-bits store the tag +// identifying the type of object, while the bottom 28-bits store the slot used +// to index with the test.{batches,iters,snapshots} slices. +type objID uint32 + +func makeObjID(t objTag, slot uint32) objID { + return objID((uint32(t) << 28) | slot) +} + +func (i objID) tag() objTag { + return objTag(i >> 28) +} + +func (i objID) slot() uint32 { + return uint32(i) & ((1 << 28) - 1) +} + +func (i objID) String() string { + switch i.tag() { + case dbTag: + return fmt.Sprintf("db%d", i.slot()) + case batchTag: + return fmt.Sprintf("batch%d", i.slot()) + case iterTag: + return fmt.Sprintf("iter%d", i.slot()) + case snapTag: + return fmt.Sprintf("snap%d", i.slot()) + } + return fmt.Sprintf("unknown%d", i.slot()) +} + +// objIDSlice is an unordered set of integers used when random selection of an +// element is required. +type objIDSlice []objID + +func (s objIDSlice) Len() int { return len(s) } +func (s objIDSlice) Less(i, j int) bool { return s[i] < s[j] } +func (s objIDSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Remove removes the specified integer from the set. +// +// TODO(peter): If this proves slow, we can replace this implementation with a +// map and a slice. The slice would provide for random selection of an element, +// while the map would provide quick location of an element to remove. +func (s *objIDSlice) remove(id objID) { + n := len(*s) + for j := 0; j < n; j++ { + if (*s)[j] == id { + (*s)[j], (*s)[n-1] = (*s)[n-1], (*s)[j] + (*s) = (*s)[:n-1] + break + } + } +} + +func (s *objIDSlice) rand(rng *rand.Rand) objID { + return (*s)[rng.Intn(len(*s))] +} + +// objIDSet is an unordered set of object IDs. +type objIDSet map[objID]struct{} + +// sortedKeys returns a sorted slice of the set's keys for deterministic +// iteration. +func (s objIDSet) sorted() []objID { + keys := make(objIDSlice, 0, len(s)) + for id := range s { + keys = append(keys, id) + } + sort.Sort(keys) + return keys +} + +// firstError returns the first non-nil error of err0 and err1, or nil if both +// are nil. +func firstError(err0, err1 error) error { + if err0 != nil { + return err0 + } + return err1 +} diff --git a/pebble/metrics.go b/pebble/metrics.go new file mode 100644 index 0000000..0f1e32b --- /dev/null +++ b/pebble/metrics.go @@ -0,0 +1,627 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "fmt" + "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" + "github.com/prometheus/client_golang/prometheus" +) + +// CacheMetrics holds metrics for the block and table cache. +type CacheMetrics = cache.Metrics + +// FilterMetrics holds metrics for the filter policy +type FilterMetrics = sstable.FilterMetrics + +// ThroughputMetric is a cumulative throughput metric. See the detailed +// comment in base. +type ThroughputMetric = base.ThroughputMetric + +// 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. +type LevelMetrics struct { + // The number of sublevels within the level. The sublevel count corresponds + // to the read amplification for the level. An empty level will have a + // sublevel count of 0, implying no read amplification. Only L0 will have + // a sublevel count other than 0 or 1. + 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 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 + // the bytes written to the WAL. + BytesIn uint64 + // The number of bytes ingested. The sibling metric for tables is + // TablesIngested. + BytesIngested uint64 + // The number of bytes moved into the level by a "move" compaction. The + // sibling metric for tables is TablesMoved. + BytesMoved uint64 + // The number of bytes read for compactions at the level. This includes bytes + // read from other levels (BytesIn), as well as bytes read for the level. + BytesRead uint64 + // The number of bytes written during compactions. The sibling + // metric for tables is TablesCompacted. This metric may be summed + // with BytesFlushed to compute the total bytes written for the level. + BytesCompacted uint64 + // The number of bytes written during flushes. The sibling + // metrics for tables is TablesFlushed. This metric is always + // zero for all levels other than L0. + BytesFlushed uint64 + // The number of sstables compacted to this level. + TablesCompacted uint64 + // The number of sstables flushed to this level. + TablesFlushed uint64 + // The number of sstables ingested into the level. + 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 + // level. Printed by LevelMetrics.format iff there is at least one level + // with a non-zero value. + ValueBlocksSize uint64 + // Cumulative metrics about bytes written to data blocks and value blocks, + // via compactions (except move compactions) or flushes. Not printed by + // LevelMetrics.format, but are available to sophisticated clients. + BytesWrittenDataBlocks uint64 + BytesWrittenValueBlocks uint64 + } +} + +// 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 + m.BytesMoved += u.BytesMoved + m.BytesRead += u.BytesRead + m.BytesCompacted += u.BytesCompacted + m.BytesFlushed += u.BytesFlushed + m.TablesCompacted += u.TablesCompacted + 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 +} + +// WriteAmp computes the write amplification for compactions at this +// level. Computed as (BytesFlushed + BytesCompacted) / BytesIn. +func (m *LevelMetrics) WriteAmp() float64 { + if m.BytesIn == 0 { + return 0 + } + return float64(m.BytesFlushed+m.BytesCompacted) / float64(m.BytesIn) +} + +// Metrics holds metrics for various subsystems of the DB such as the Cache, +// Compactions, WAL, and per-Level metrics. +// +// TODO(peter): The testing of these metrics is relatively weak. There should +// be testing that performs various operations on a DB and verifies that the +// metrics reflect those operations. +type Metrics struct { + BlockCache CacheMetrics + + 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 + CounterLevelCount int64 + // An estimate of the number of bytes that need to be compacted for the LSM + // to reach a stable state. + EstimatedDebt 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. + InProgressBytes int64 + // Number of compactions that are in-progress. + NumInProgress int64 + // MarkedFiles is a count of files that are marked for + // 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 { + // The total number of flushes. + Count int64 + WriteThroughput ThroughputMetric + // Number of flushes that are in-progress. In the current implementation + // this will always be zero or one. + NumInProgress int64 + // AsIngestCount is a monotonically increasing counter of flush operations + // handling ingested tables. + AsIngestCount uint64 + // AsIngestCount is a monotonically increasing counter of tables ingested as + // flushables. + AsIngestTableCount uint64 + // AsIngestBytes is a monotonically increasing counter of the bytes flushed + // for flushables that originated as ingestion operations. + AsIngestBytes uint64 + } + + Filter FilterMetrics + + Levels [numLevels]LevelMetrics + + MemTable struct { + // The number of bytes allocated by memtables and large (flushable) + // batches. + Size uint64 + // The count of memtables. + Count int64 + // The number of bytes present in zombie memtables which are no longer + // 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 + } + + Keys struct { + // The approximate count of internal range key set keys in the database. + RangeKeySetsCount uint64 + // 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 { + // The number of currently open snapshots. + Count int + // The sequence number of the earliest, currently open snapshot. + EarliestSeqNum uint64 + // A running tally of keys written to sstables during flushes or + // compactions that would've been elided if it weren't for open + // snapshots. + PinnedKeys uint64 + // A running cumulative sum of the size of keys and values written to + // sstables during flushes or compactions that would've been elided if + // it weren't for open snapshots. + PinnedSize uint64 + } + + Table struct { + // The number of bytes present in obsolete tables which are no longer + // referenced by the current DB state or any open iterators. + ObsoleteSize uint64 + // The count of obsolete tables. + ObsoleteCount int64 + // The number of bytes present in zombie tables which are no longer + // referenced by the current DB state but are still in use by an iterator. + 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. + Files int64 + // Number of obsolete WAL files. + ObsoleteFiles int64 + // Physical size of the obsolete WAL files. + ObsoletePhysicalSize uint64 + // Size of the live data in the WAL files. Note that with WAL file + // recycling this is less than the actual on-disk size of the WAL files. + Size uint64 + // Physical size of the WAL files on-disk. With WAL file recycling, + // this is greater than the live data in WAL files. + PhysicalSize uint64 + // Number of logical bytes written to the WAL. + BytesIn uint64 + // Number of bytes written to the WAL. + BytesWritten uint64 + } + + LogWriter struct { + FsyncLatency prometheus.Histogram + record.LogWriterMetrics + } + + CategoryStats []sstable.CategoryStatsAggregate + + SecondaryCacheMetrics SecondaryCacheMetrics + + private struct { + optionsFileSize uint64 + manifestFileSize uint64 + } +} + +var ( + // FsyncLatencyBuckets are prometheus histogram buckets suitable for a histogram + // that records latencies for fsyncs. + FsyncLatencyBuckets = append( + 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, +// including live and obsolete files. +func (m *Metrics) DiskSpaceUsage() uint64 { + var usageBytes uint64 + usageBytes += m.WAL.PhysicalSize + usageBytes += m.WAL.ObsoletePhysicalSize + for _, lm := range m.Levels { + usageBytes += uint64(lm.Size) + } + usageBytes += m.Table.ObsoleteSize + usageBytes += m.Table.ZombieSize + usageBytes += m.private.optionsFileSize + usageBytes += m.private.manifestFileSize + usageBytes += uint64(m.Compact.InProgressBytes) + return usageBytes +} + +// 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 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. +// It's computed as the number of sublevels in L0 + the number of non-empty +// levels below L0. +func (m *Metrics) ReadAmp() int { + var ramp int32 + for _, l := range m.Levels { + ramp += l.Sublevels + } + return int(ramp) +} + +// Total returns the sum of the per-level metrics and WAL metrics. +func (m *Metrics) Total() LevelMetrics { + var total LevelMetrics + for level := 0; level < numLevels; level++ { + l := &m.Levels[level] + total.Add(l) + total.Sublevels += l.Sublevels + } + // Compute total bytes-in as the bytes written to the WAL + bytes ingested. + total.BytesIn = m.WAL.BytesWritten + total.BytesIngested + // Add the total bytes-in to the total bytes-flushed. This is to account for + // the bytes written to the log and bytes written externally and then + // ingested. + total.BytesFlushed += total.BytesIn + return total +} + +// String pretty-prints the metrics as below: +// +// | | | | 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) +} + +var _ redact.SafeFormatter = &Metrics{} + +// SafeFormat implements redact.SafeFormatter. +func (m *Metrics) SafeFormat(w redact.SafePrinter, _ rune) { + // NB: Pebble does not make any assumptions as to which Go primitive types + // have been registered as safe with redact.RegisterSafeType and does not + // register any types itself. Some of the calls to `redact.Safe`, etc are + // superfluous in the context of CockroachDB, which registers all the Go + // numeric types as safe. + + // TODO(jackson): There are a few places where we use redact.SafeValue + // instead of redact.RedactableString. This is necessary because of a bug + // whereby formatting a redact.RedactableString argument does not respect + // width specifiers. When the issue is fixed, we can convert these to + // RedactableStrings. https://github.com/cockroachdb/redact/issues/17 + + 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 + for level := 0; level < numLevels; level++ { + l := &m.Levels[level] + w.Printf("%5d ", redact.Safe(level)) + + // Format the score. + score := math.NaN() + if level < numLevels-1 { + score = l.Score + } + formatRow(l, score) + total.Add(l) + total.Sublevels += l.Sublevels + } + // Compute total bytes-in as the bytes written to the WAL + bytes ingested. + total.BytesIn = m.WAL.BytesWritten + total.BytesIngested + // Add the total bytes-in to the total bytes-flushed. This is to account for + // the bytes written to the log and bytes written externally and then + // ingested. + total.BytesFlushed += total.BytesIn + w.SafeString("total ") + formatRow(&total, math.NaN()) + + 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.Bytes.Uint64(m.Compact.EstimatedDebt), + redact.Safe(m.Compact.NumInProgress), + 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)) + + w.Printf("MemTables: %d (%s) zombie: %d (%s)\n", + redact.Safe(m.MemTable.Count), + humanize.Bytes.Uint64(m.MemTable.Size), + redact.Safe(m.MemTable.ZombieCount), + humanize.Bytes.Uint64(m.MemTable.ZombieSize)) + + w.Printf("Zombie tables: %d (%s)\n", + redact.Safe(m.Table.ZombieCount), + 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), + redact.Safe(m.Snapshots.EarliestSeqNum)) + + 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 { + return percent(hits, hits+misses) +} + +func percent(numerator, denominator int64) float64 { + if denominator == 0 { + return 0 + } + 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/pebble/metrics_test.go b/pebble/metrics_test.go new file mode 100644 index 0000000..a2485d6 --- /dev/null +++ b/pebble/metrics_test.go @@ -0,0 +1,375 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/redact" + "github.com/stretchr/testify/require" +) + +func exampleMetrics() Metrics { + var m Metrics + m.BlockCache.Size = 1 + m.BlockCache.Count = 2 + m.BlockCache.Hits = 3 + m.BlockCache.Misses = 4 + m.Compact.Count = 5 + m.Compact.DefaultCount = 27 + m.Compact.DeleteOnlyCount = 28 + m.Compact.ElisionOnlyCount = 29 + m.Compact.MoveCount = 30 + m.Compact.ReadCount = 31 + m.Compact.RewriteCount = 32 + m.Compact.MultiLevelCount = 33 + m.Compact.EstimatedDebt = 6 + m.Compact.InProgressBytes = 7 + m.Compact.NumInProgress = 2 + m.Flush.Count = 8 + m.Flush.AsIngestBytes = 34 + m.Flush.AsIngestTableCount = 35 + m.Flush.AsIngestCount = 36 + m.Filter.Hits = 9 + m.Filter.Misses = 10 + m.MemTable.Size = 11 + m.MemTable.Count = 12 + m.MemTable.ZombieSize = 13 + m.MemTable.ZombieCount = 14 + m.Snapshots.Count = 4 + m.Snapshots.EarliestSeqNum = 1024 + m.Table.ZombieSize = 15 + m.Table.BackingTableCount = 1 + m.Table.BackingTableSize = 2 << 20 + m.Table.ZombieCount = 16 + m.TableCache.Size = 17 + m.TableCache.Count = 18 + m.TableCache.Hits = 19 + m.TableCache.Misses = 20 + m.TableIters = 21 + m.WAL.Files = 22 + m.WAL.ObsoleteFiles = 23 + m.WAL.Size = 24 + m.WAL.BytesIn = 25 + m.WAL.BytesWritten = 26 + m.Ingest.Count = 27 + + for i := range m.Levels { + l := &m.Levels[i] + base := uint64((i + 1) * 100) + l.Sublevels = int32(i + 1) + l.NumFiles = int64(base) + 1 + l.NumVirtualFiles = uint64(base) + 1 + l.VirtualSize = base + 3 + l.Size = int64(base) + 2 + l.Score = float64(base) + 3 + l.BytesIn = base + 4 + l.BytesIngested = base + 4 + l.BytesMoved = base + 6 + l.BytesRead = base + 7 + l.BytesCompacted = base + 8 + l.BytesFlushed = base + 9 + l.TablesCompacted = base + 10 + l.TablesFlushed = base + 11 + l.TablesIngested = base + 12 + l.TablesMoved = base + 13 + l.MultiLevel.BytesInTop = base + 4 + l.MultiLevel.BytesIn = base + 4 + l.MultiLevel.BytesRead = base + 4 + } + return m +} + +func TestMetrics(t *testing.T) { + c := cache.New(cacheDefaultSize) + defer c.Unref() + opts := &Options{ + Cache: c, + Comparer: testkeys.Comparer, + FormatMajorVersion: FormatNewest, + FS: vfs.NewMem(), + L0CompactionThreshold: 8, + // Large value for determinism. + MaxOpenFiles: 10000, + } + opts.Experimental.EnableValueBlocks = func() bool { return true } + opts.Levels = append(opts.Levels, LevelOptions{TargetFileSize: 50}) + + // Prevent foreground flushes and compactions from triggering asynchronous + // follow-up compactions. This avoids asynchronously-scheduled work from + // interfering with the expected metrics output and reduces test flakiness. + opts.DisableAutomaticCompactions = true + + // Increase the threshold for memtable stalls to allow for more flushable + // ingests. + opts.MemTableStopWritesThreshold = 4 + + d, err := Open("", opts) + require.NoError(t, err) + defer func() { + require.NoError(t, d.Close()) + }() + + iters := make(map[string]*Iterator) + defer func() { + for _, i := range iters { + require.NoError(t, i.Close()) + } + }() + + datadriven.RunTest(t, "testdata/metrics", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "example": + m := exampleMetrics() + res := m.String() + + // Nothing in the metrics should be redacted. + redacted := string(redact.Sprintf("%s", &m).Redact()) + if redacted != res { + td.Fatalf(t, "redacted metrics don't match\nunredacted:\n%s\nredacted:%s\n", res, redacted) + } + return res + + case "batch": + b := d.NewBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + b.Commit(nil) + return "" + + case "build": + if err := runBuildCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + return "" + + case "compact": + if err := runCompactCmd(td, d); err != nil { + return err.Error() + } + + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "delay-flush": + d.mu.Lock() + defer d.mu.Unlock() + switch td.Input { + case "enable": + d.mu.compact.flushing = true + case "disable": + d.mu.compact.flushing = false + default: + return fmt.Sprintf("unknown directive %q (expected 'enable'/'disable')", td.Input) + } + return "" + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "ingest": + if err := runIngestCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + return "" + + case "lsm": + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "ingest-and-excise": + if err := runIngestAndExciseCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + return "" + + case "iter-close": + if len(td.CmdArgs) != 1 { + return "iter-close " + } + name := td.CmdArgs[0].String() + if iter := iters[name]; iter != nil { + if err := iter.Close(); err != nil { + return err.Error() + } + delete(iters, name) + } else { + return fmt.Sprintf("%s: not found", name) + } + + // The deletion of obsolete files happens asynchronously when an iterator + // is closed. Wait for the obsolete tables to be deleted. + d.cleanupManager.Wait() + return "" + + case "iter-new": + if len(td.CmdArgs) < 1 { + return "iter-new " + } + name := td.CmdArgs[0].String() + if iter := iters[name]; iter != nil { + if err := iter.Close(); err != nil { + return err.Error() + } + } + var categoryAndQoS sstable.CategoryAndQoS + if td.HasArg("category") { + var s string + td.ScanArgs(t, "category", &s) + categoryAndQoS.Category = sstable.Category(s) + } + if td.HasArg("qos") { + var qos string + td.ScanArgs(t, "qos", &qos) + categoryAndQoS.QoSLevel = sstable.StringToQoSForTesting(qos) + } + iter, _ := d.NewIter(&IterOptions{CategoryAndQoS: categoryAndQoS}) + // Some iterators (eg. levelIter) do not instantiate the underlying + // iterator until the first positioning call. Position the iterator + // so that levelIters will have loaded an sstable. + iter.First() + iters[name] = iter + return "" + + case "metrics": + // The asynchronous loading of table stats can change metrics, so + // wait for all the tables' stats to be loaded. + d.mu.Lock() + d.waitTableStats() + d.mu.Unlock() + + m := d.Metrics() + if td.HasArg("zero-cache-hits-misses") { + // Avoid non-determinism. + m.TableCache.Hits = 0 + m.TableCache.Misses = 0 + m.BlockCache.Hits = 0 + m.BlockCache.Misses = 0 + // Empirically, the unknown stats are also non-deterministic. + if len(m.CategoryStats) > 0 && m.CategoryStats[0].Category == "_unknown" { + m.CategoryStats[0].CategoryStats = sstable.CategoryStats{} + } + } + var buf strings.Builder + fmt.Fprintf(&buf, "%s", m.StringForTests()) + if len(m.CategoryStats) > 0 { + fmt.Fprintf(&buf, "Iter category stats:\n") + for _, stats := range m.CategoryStats { + fmt.Fprintf(&buf, "%20s, %11s: %+v\n", stats.Category, + redact.StringWithoutMarkers(stats.QoSLevel), stats.CategoryStats) + } + } + return buf.String() + + case "metrics-value": + // metrics-value confirms the value of a given metric. Note that there + // are some metrics which aren't deterministic and behave differently + // for invariant/non-invariant builds. An example of this is cache + // hit rates. Under invariant builds, the excising code will try + // to create iterators and confirm that the virtual sstable bounds + // are accurate. Reads on these iterators will change the cache hit + // rates. + lines := strings.Split(td.Input, "\n") + m := d.Metrics() + // TODO(bananabrick): Use reflection to pull the values associated + // with the metrics fields. + var buf bytes.Buffer + for i := range lines { + line := lines[i] + if line == "num-backing" { + buf.WriteString(fmt.Sprintf("%d\n", m.Table.BackingTableCount)) + } else if line == "backing-size" { + buf.WriteString(fmt.Sprintf("%s\n", humanize.Bytes.Uint64(m.Table.BackingTableSize))) + } else if line == "virtual-size" { + buf.WriteString(fmt.Sprintf("%s\n", humanize.Bytes.Uint64(m.VirtualSize()))) + } else if strings.HasPrefix(line, "num-virtual") { + splits := strings.Split(line, " ") + if len(splits) == 1 { + buf.WriteString(fmt.Sprintf("%d\n", m.NumVirtual())) + continue + } + // Level is specified. + l, err := strconv.Atoi(splits[1]) + if err != nil { + panic(err) + } + if l >= numLevels { + panic(fmt.Sprintf("invalid level %d", l)) + } + buf.WriteString(fmt.Sprintf("%d\n", m.Levels[l].NumVirtualFiles)) + } else { + panic(fmt.Sprintf("invalid field: %s", line)) + } + } + return buf.String() + + case "disk-usage": + return humanize.Bytes.Uint64(d.Metrics().DiskSpaceUsage()).String() + + case "additional-metrics": + // The asynchronous loading of table stats can change metrics, so + // wait for all the tables' stats to be loaded. + d.mu.Lock() + d.waitTableStats() + d.mu.Unlock() + + m := d.Metrics() + var b strings.Builder + fmt.Fprintf(&b, "block bytes written:\n") + fmt.Fprintf(&b, " __level___data-block__value-block\n") + for i := range m.Levels { + fmt.Fprintf(&b, "%7d ", i) + fmt.Fprintf(&b, "%12s %12s\n", + humanize.Bytes.Uint64(m.Levels[i].Additional.BytesWrittenDataBlocks), + humanize.Bytes.Uint64(m.Levels[i].Additional.BytesWrittenValueBlocks)) + } + return b.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestMetricsWAmpDisableWAL(t *testing.T) { + d, err := Open("", &Options{FS: vfs.NewMem(), DisableWAL: true}) + require.NoError(t, err) + ks := testkeys.Alpha(2) + wo := WriteOptions{Sync: false} + for i := 0; i < 5; i++ { + v := []byte(strconv.Itoa(i)) + for j := int64(0); j < ks.Count(); j++ { + require.NoError(t, d.Set(testkeys.Key(ks, j), v, &wo)) + } + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact([]byte("a"), []byte("z"), false /* parallelize */)) + } + m := d.Metrics() + tot := m.Total() + require.Greater(t, tot.WriteAmp(), 1.0) + require.NoError(t, d.Close()) +} diff --git a/pebble/objstorage/noop_readahead.go b/pebble/objstorage/noop_readahead.go new file mode 100644 index 0000000..72c0a85 --- /dev/null +++ b/pebble/objstorage/noop_readahead.go @@ -0,0 +1,34 @@ +// 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" + +// NoopReadHandle can be used by Readable implementations that don't +// support read-ahead. +type NoopReadHandle struct { + readable Readable +} + +// MakeNoopReadHandle initializes a NoopReadHandle. +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 } + +// SetupForCompaction is part of the ReadHandle interface. +func (*NoopReadHandle) SetupForCompaction() {} + +// RecordCacheHit is part of the ReadHandle interface. +func (*NoopReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {} diff --git a/pebble/objstorage/objstorage.go b/pebble/objstorage/objstorage.go new file mode 100644 index 0000000..a4408d1 --- /dev/null +++ b/pebble/objstorage/objstorage.go @@ -0,0 +1,330 @@ +// 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" + "github.com/cockroachdb/redact" +) + +// 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) } + +// SafeFormat implements redact.SafeFormatter. +func (c CreatorID) SafeFormat(w redact.SafePrinter, _ rune) { + w.Printf("%d", redact.SafeUint(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/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing.go b/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing.go new file mode 100644 index 0000000..2672636 --- /dev/null +++ b/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/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_off.go b/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_off.go new file mode 100644 index 0000000..a4923ab --- /dev/null +++ b/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/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_on.go b/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_on.go new file mode 100644 index 0000000..0680b34 --- /dev/null +++ b/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/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_test.go b/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_test.go new file mode 100644 index 0000000..5a79b57 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_test.go @@ -0,0 +1,130 @@ +// 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_test + +import ( + "io" + "strings" + "testing" + "unsafe" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +type Event = objiotracing.Event + +const eventSize = int(unsafe.Sizeof(Event{})) + +func TestTracing(t *testing.T) { + if !objiotracing.Enabled { + t.Skipf("test can only be run under pebble_obj_io_tracing build tag") + } + fs := vfs.NewMem() + d, err := pebble.Open("", &pebble.Options{FS: fs}) + require.NoError(t, err) + + require.NoError(t, d.Set([]byte("a"), []byte("aaa"), nil)) + require.NoError(t, d.Set([]byte("b"), []byte("bbb"), nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("c"), []byte("ccc"), nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact([]byte("a"), []byte("z"), false /* parallelize */)) + require.NoError(t, d.Set([]byte("b"), []byte("bbb2"), nil)) + require.NoError(t, d.Set([]byte("c"), []byte("ccc2"), nil)) + require.NoError(t, d.Set([]byte("d"), []byte("ddd"), nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact([]byte("a"), []byte("z"), false /* parallelize */)) + require.NoError(t, d.Close()) + + collectEvents := func() []Event { + t.Helper() + + list, err := fs.List("") + require.NoError(t, err) + + var events []Event + for _, f := range list { + if strings.HasPrefix(f, "IOTRACES-") { + file, err := fs.Open(f) + require.NoError(t, err) + data, err := io.ReadAll(file) + file.Close() + require.NoError(t, err) + // Remove the file so we don't read these events again later. + fs.Remove(f) + if len(data) == 0 { + continue + } + require.Equal(t, len(data)%eventSize, 0) + p := unsafe.Pointer(&data[0]) + asEvents := unsafe.Slice((*Event)(p), len(data)/eventSize) + events = append(events, asEvents...) + } + } + if testing.Verbose() { + t.Logf("collected events:") + for _, e := range events { + t.Logf(" %#v", e) + } + } + return events + } + events := collectEvents() + num := func(check func(e Event) bool) int { + res := 0 + for _, e := range events { + if check(e) { + res += 1 + } + } + return res + } + // Check that we saw at least a few reads and writes. + // TODO(radu): check more fields when they are populated. + require.Greater(t, num(func(e Event) bool { return e.Op == objiotracing.ReadOp }), 5) + require.Greater(t, num(func(e Event) bool { return e.Op == objiotracing.WriteOp }), 5) + + // We should see writes at L0 and L7. + require.Greater(t, num(func(e Event) bool { return e.Op == objiotracing.WriteOp && e.LevelPlusOne == 1 }), 0) + require.Greater(t, num(func(e Event) bool { return e.Op == objiotracing.WriteOp && e.LevelPlusOne == 7 }), 0) + + // Check that we saw writes for flushing and for compaction. + require.Greater(t, num(func(e Event) bool { return e.Reason == objiotracing.ForFlush }), 0) + require.Greater(t, num(func(e Event) bool { return e.Reason == objiotracing.ForCompaction }), 0) + + // Check that offset is set on reads & writes as expected. + require.Greater(t, num(func(e Event) bool { return e.Op == objiotracing.ReadOp && e.Offset > 0 }), 0) + require.Greater(t, num(func(e Event) bool { return e.Op == objiotracing.WriteOp && e.Offset > 0 }), 0) + + // Check that the FileNums are set and that we see at least two different files. + fileNums := make(map[base.FileNum]int) + for _, e := range events { + require.NotZero(t, e.FileNum) + fileNums[e.FileNum] += 1 + } + require.GreaterOrEqual(t, len(fileNums), 2) + + // Open again and do some reads. + d, err = pebble.Open("", &pebble.Options{FS: fs}) + require.NoError(t, err) + for _, k := range []string{"0", "a", "d", "ccc", "b"} { + _, closer, err := d.Get([]byte(k)) + if err == pebble.ErrNotFound { + continue + } + require.NoError(t, err) + closer.Close() + } + require.NoError(t, d.Close()) + events = collectEvents() + // Expect L6 data block reads. + require.Greater(t, num(func(e Event) bool { + return e.Op == objiotracing.ReadOp && e.BlockType == objiotracing.DataBlock && e.LevelPlusOne == 7 + }), 0) +} diff --git a/pebble/objstorage/objstorageprovider/provider.go b/pebble/objstorage/objstorageprovider/provider.go new file mode 100644 index 0000000..6e27f8d --- /dev/null +++ b/pebble/objstorage/objstorageprovider/provider.go @@ -0,0 +1,524 @@ +// 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" + "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/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" + "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", 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", 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", + fileNum, errors.Safe(fileType), + ) + } + if meta.FileType != fileType { + return objstorage.ObjectMetadata{}, errors.AssertionFailedf( + "file %s type mismatch (known type %d, expected type %d)", + 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) + } + slices.SortFunc(res, func(a, b objstorage.ObjectMetadata) int { + return cmp.Compare(a.DiskFileNum, b.DiskFileNum) + }) + 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, + CustomObjectName: meta.Remote.CustomObjectName, + }) + } 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/pebble/objstorage/objstorageprovider/provider_test.go b/pebble/objstorage/objstorageprovider/provider_test.go new file mode 100644 index 0000000..2badf34 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/provider_test.go @@ -0,0 +1,601 @@ +// 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" + "math/rand" + "strings" + "sync" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestProvider(t *testing.T) { + datadriven.Walk(t, "testdata/provider", func(t *testing.T, path string) { + var log base.InMemLogger + fs := vfs.WithLogging(vfs.NewMem(), func(fmt string, args ...interface{}) { + log.Infof(" "+fmt, args...) + }) + sharedStore := remote.WithLogging(remote.NewInMem(), func(fmt string, args ...interface{}) { + log.Infof(" "+fmt, args...) + }) + sharedFactory := remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": sharedStore, + }) + tmpFileCounter := 0 + + providers := make(map[string]objstorage.Provider) + // We maintain both backings and backing handles to allow tests to use the + // backings after the handles have been closed. + backings := make(map[string]objstorage.RemoteObjectBacking) + backingHandles := make(map[string]objstorage.RemoteObjectBackingHandle) + var curProvider objstorage.Provider + datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { + scanArgs := func(desc string, args ...interface{}) { + t.Helper() + if len(d.CmdArgs) != len(args) { + d.Fatalf(t, "usage: %s %s", d.Cmd, desc) + } + for i := range args { + _, err := fmt.Sscan(d.CmdArgs[i].String(), args[i]) + if err != nil { + d.Fatalf(t, "%s: error parsing argument '%s'", d.Cmd, d.CmdArgs[i]) + } + } + } + ctx := context.Background() + + log.Reset() + switch d.Cmd { + case "open": + var fsDir string + var creatorID objstorage.CreatorID + scanArgs(" ", &fsDir, &creatorID) + + st := DefaultSettings(fs, fsDir) + if creatorID != 0 { + st.Remote.StorageFactory = sharedFactory + st.Remote.CreateOnShared = remote.CreateOnSharedAll + st.Remote.CreateOnSharedLocator = "" + } + require.NoError(t, fs.MkdirAll(fsDir, 0755)) + p, err := Open(st) + require.NoError(t, err) + if creatorID != 0 { + require.NoError(t, p.SetCreatorID(creatorID)) + } + // Checking refs on open affects the test output. We don't want tests to + // only pass when the `invariants` tag is used, so unconditionally + // enable ref checking on open. + p.(*provider).remote.shared.checkRefsOnOpen = true + providers[fsDir] = p + curProvider = p + + return log.String() + + case "switch": + var fsDir string + scanArgs("", &fsDir) + curProvider = providers[fsDir] + if curProvider == nil { + t.Fatalf("unknown provider %s", fsDir) + } + + return "" + + case "close": + require.NoError(t, curProvider.Sync()) + require.NoError(t, curProvider.Close()) + delete(providers, curProvider.(*provider).st.FSDirName) + curProvider = nil + + return log.String() + + case "create": + opts := objstorage.CreateOptions{ + SharedCleanupMethod: objstorage.SharedRefTracking, + } + if len(d.CmdArgs) == 5 && d.CmdArgs[4].Key == "no-ref-tracking" { + d.CmdArgs = d.CmdArgs[:4] + opts.SharedCleanupMethod = objstorage.SharedNoCleanup + } + var fileNum base.FileNum + var typ string + var salt, size int + scanArgs(" [no-ref-tracking]", &fileNum, &typ, &salt, &size) + switch typ { + case "local": + case "shared": + opts.PreferSharedStorage = true + default: + d.Fatalf(t, "'%s' should be 'local' or 'shared'", typ) + } + w, _, err := curProvider.Create(ctx, base.FileTypeTable, fileNum.DiskFileNum(), opts) + if err != nil { + return err.Error() + } + data := make([]byte, size) + // TODO(radu): write in chunks? + genData(byte(salt), 0, data) + require.NoError(t, w.Write(data)) + require.NoError(t, w.Finish()) + + return log.String() + + case "link-or-copy": + opts := objstorage.CreateOptions{ + SharedCleanupMethod: objstorage.SharedRefTracking, + } + if len(d.CmdArgs) == 5 && d.CmdArgs[4].Key == "no-ref-tracking" { + d.CmdArgs = d.CmdArgs[:4] + opts.SharedCleanupMethod = objstorage.SharedNoCleanup + } + var fileNum base.FileNum + var typ string + var salt, size int + scanArgs(" [no-ref-tracking]", &fileNum, &typ, &salt, &size) + switch typ { + case "local": + case "shared": + opts.PreferSharedStorage = true + default: + d.Fatalf(t, "'%s' should be 'local' or 'shared'", typ) + } + + tmpFileCounter++ + tmpFilename := fmt.Sprintf("temp-file-%d", tmpFileCounter) + f, err := fs.Create(tmpFilename) + require.NoError(t, err) + data := make([]byte, size) + genData(byte(salt), 0, data) + n, err := f.Write(data) + require.Equal(t, len(data), n) + require.NoError(t, err) + require.NoError(t, f.Close()) + + _, err = curProvider.LinkOrCopyFromLocal( + ctx, fs, tmpFilename, base.FileTypeTable, fileNum.DiskFileNum(), opts, + ) + require.NoError(t, err) + return log.String() + + case "read": + forCompaction := false + if len(d.CmdArgs) == 2 && d.CmdArgs[1].Key == "for-compaction" { + d.CmdArgs = d.CmdArgs[:1] + forCompaction = true + } + var fileNum base.FileNum + scanArgs(" [for-compaction]", &fileNum) + r, err := curProvider.OpenForReading(ctx, base.FileTypeTable, fileNum.DiskFileNum(), objstorage.OpenOptions{}) + if err != nil { + return err.Error() + } + rh := r.NewReadHandle(ctx) + if forCompaction { + rh.SetupForCompaction() + } + log.Infof("size: %d", r.Size()) + for _, l := range strings.Split(d.Input, "\n") { + var offset, size int + fmt.Sscanf(l, "%d %d", &offset, &size) + data := make([]byte, size) + err := rh.ReadAt(ctx, data, int64(offset)) + if err != nil { + log.Infof("%d %d: %v", offset, size, err) + } else { + salt := checkData(t, offset, data) + log.Infof("%d %d: ok (salt %d)", offset, size, salt) + } + } + require.NoError(t, rh.Close()) + require.NoError(t, r.Close()) + return log.String() + + case "remove": + var fileNum base.FileNum + scanArgs("", &fileNum) + if err := curProvider.Remove(base.FileTypeTable, fileNum.DiskFileNum()); err != nil { + return err.Error() + } + return log.String() + + case "list": + for _, meta := range curProvider.List() { + log.Infof("%s -> %s", meta.DiskFileNum, curProvider.Path(meta)) + } + return log.String() + + case "save-backing": + var key string + var fileNum base.FileNum + scanArgs(" ", &key, &fileNum) + meta, err := curProvider.Lookup(base.FileTypeTable, fileNum.DiskFileNum()) + require.NoError(t, err) + handle, err := curProvider.RemoteObjectBacking(&meta) + if err != nil { + return err.Error() + } + backing, err := handle.Get() + require.NoError(t, err) + backings[key] = backing + backingHandles[key] = handle + return log.String() + + case "close-backing": + var key string + scanArgs("", &key) + backingHandles[key].Close() + return "" + + case "attach": + lines := strings.Split(d.Input, "\n") + if len(lines) == 0 { + d.Fatalf(t, "at least one row expected; format: ") + } + var objs []objstorage.RemoteObjectToAttach + for _, l := range lines { + var key string + var fileNum base.FileNum + _, err := fmt.Sscan(l, &key, &fileNum) + require.NoError(t, err) + b, ok := backings[key] + if !ok { + d.Fatalf(t, "unknown backing key %q", key) + } + objs = append(objs, objstorage.RemoteObjectToAttach{ + FileType: base.FileTypeTable, + FileNum: fileNum.DiskFileNum(), + Backing: b, + }) + } + metas, err := curProvider.AttachRemoteObjects(objs) + if err != nil { + return log.String() + "error: " + err.Error() + } + for _, meta := range metas { + log.Infof("%s -> %s", meta.DiskFileNum, curProvider.Path(meta)) + } + return log.String() + + default: + d.Fatalf(t, "unknown command %s", d.Cmd) + return "" + } + }) + }) +} + +func TestSharedMultipleLocators(t *testing.T) { + ctx := context.Background() + stores := map[remote.Locator]remote.Storage{ + "foo": remote.NewInMem(), + "bar": remote.NewInMem(), + } + sharedFactory := remote.MakeSimpleFactory(stores) + + st1 := DefaultSettings(vfs.NewMem(), "") + st1.Remote.StorageFactory = sharedFactory + st1.Remote.CreateOnShared = remote.CreateOnSharedAll + st1.Remote.CreateOnSharedLocator = "foo" + p1, err := Open(st1) + require.NoError(t, err) + require.NoError(t, p1.SetCreatorID(1)) + + st2 := DefaultSettings(vfs.NewMem(), "") + st2.Remote.StorageFactory = sharedFactory + st2.Remote.CreateOnShared = remote.CreateOnSharedAll + st2.Remote.CreateOnSharedLocator = "bar" + p2, err := Open(st2) + require.NoError(t, err) + require.NoError(t, p2.SetCreatorID(2)) + + file1 := base.FileNum(1).DiskFileNum() + file2 := base.FileNum(2).DiskFileNum() + + for i, provider := range []objstorage.Provider{p1, p2} { + w, _, err := provider.Create(ctx, base.FileTypeTable, file1, objstorage.CreateOptions{ + PreferSharedStorage: true, + }) + require.NoError(t, err) + data := make([]byte, 100) + genData(byte(i), 0, data) + require.NoError(t, w.Write(data)) + require.NoError(t, w.Finish()) + } + + // checkObjects reads the given object and verifies the data matches the salt. + checkObject := func(p objstorage.Provider, fileNum base.DiskFileNum, salt byte) { + t.Helper() + r, err := p.OpenForReading(ctx, base.FileTypeTable, fileNum, objstorage.OpenOptions{}) + require.NoError(t, err) + data := make([]byte, r.Size()) + require.NoError(t, r.ReadAt(ctx, data, 0)) + r.Close() + require.Equal(t, salt, checkData(t, 0, data)) + } + + // Now attach p1's object (in the "foo" store) to p2. + meta1, err := p1.Lookup(base.FileTypeTable, file1) + require.NoError(t, err) + h1, err := p1.RemoteObjectBacking(&meta1) + require.NoError(t, err) + b1, err := h1.Get() + require.NoError(t, err) + + _, err = p2.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ + FileNum: file2, + FileType: base.FileTypeTable, + Backing: b1, + }}) + require.NoError(t, err) + // Close the handle from which we obtained b1. + h1.Close() + checkObject(p2, file2, 0) + + // Now attach p2's object (in the "bar" store) to p1. + meta2, err := p2.Lookup(base.FileTypeTable, file1) + require.NoError(t, err) + h2, err := p2.RemoteObjectBacking(&meta2) + require.NoError(t, err) + b2, err := h2.Get() + require.NoError(t, err) + _, err = p1.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ + FileNum: file2, + FileType: base.FileTypeTable, + Backing: b2, + }}) + require.NoError(t, err) + // Close the handle from which we obtained b2. + h2.Close() + checkObject(p1, file2, 1) + + // Check that the object still works after close/reopen. + require.NoError(t, p1.Close()) + p1, err = Open(st1) + require.NoError(t, err) + checkObject(p1, file2, 1) + require.NoError(t, p1.Close()) + + require.NoError(t, p2.Close()) + + // Try to attach an object to a provider that doesn't recognize the locator. + st3 := DefaultSettings(vfs.NewMem(), "") + st3.Remote.StorageFactory = remote.MakeSimpleFactory(nil) + p3, err := Open(st3) + require.NoError(t, err) + require.NoError(t, p3.SetCreatorID(3)) + _, err = p3.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ + FileNum: file2, + FileType: base.FileTypeTable, + Backing: b2, + }}) + require.Error(t, err) + require.NoError(t, p3.Close()) +} + +func TestAttachCustomObject(t *testing.T) { + ctx := context.Background() + storage := remote.NewInMem() + sharedFactory := remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "foo": storage, + }) + + st1 := DefaultSettings(vfs.NewMem(), "") + st1.Remote.StorageFactory = sharedFactory + p1, err := Open(st1) + require.NoError(t, err) + defer p1.Close() + require.NoError(t, p1.SetCreatorID(1)) + + w, err := storage.CreateObject("some-obj-name") + require.NoError(t, err) + data := make([]byte, 100) + genData(123, 0, data) + _, err = w.Write(data) + require.NoError(t, err) + require.NoError(t, w.Close()) + + backing, err := p1.CreateExternalObjectBacking("foo", "some-obj-name") + require.NoError(t, err) + + _, err = p1.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ + FileNum: base.FileNum(1).DiskFileNum(), + FileType: base.FileTypeTable, + Backing: backing, + }}) + require.NoError(t, err) + + // Verify the provider can read the object. + r, err := p1.OpenForReading(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.OpenOptions{}) + require.NoError(t, err) + require.Equal(t, int64(len(data)), r.Size()) + buf := make([]byte, r.Size()) + require.NoError(t, r.ReadAt(ctx, buf, 0)) + require.Equal(t, byte(123), checkData(t, 0, buf)) + require.NoError(t, r.Close()) + + // Verify that we can extract a correct backing from this provider and attach + // the object to another provider. + meta, err := p1.Lookup(base.FileTypeTable, base.FileNum(1).DiskFileNum()) + require.NoError(t, err) + handle, err := p1.RemoteObjectBacking(&meta) + require.NoError(t, err) + defer handle.Close() + backing, err = handle.Get() + require.NoError(t, err) + + st2 := DefaultSettings(vfs.NewMem(), "") + st2.Remote.StorageFactory = sharedFactory + p2, err := Open(st2) + require.NoError(t, err) + defer p2.Close() + require.NoError(t, p2.SetCreatorID(2)) + + _, err = p2.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ + FileNum: base.FileNum(10).DiskFileNum(), + FileType: base.FileTypeTable, + Backing: backing, + }}) + require.NoError(t, err) + + // Verify the provider can read the object. + r, err = p2.OpenForReading(ctx, base.FileTypeTable, base.FileNum(10).DiskFileNum(), objstorage.OpenOptions{}) + require.NoError(t, err) + require.Equal(t, int64(len(data)), r.Size()) + buf = make([]byte, r.Size()) + require.NoError(t, r.ReadAt(ctx, buf, 0)) + require.Equal(t, byte(123), checkData(t, 0, buf)) + require.NoError(t, r.Close()) +} + +func TestNotExistError(t *testing.T) { + fs := vfs.NewMem() + st := DefaultSettings(fs, "") + sharedStorage := remote.NewInMem() + st.Remote.StorageFactory = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": sharedStorage, + }) + st.Remote.CreateOnShared = remote.CreateOnSharedAll + st.Remote.CreateOnSharedLocator = "" + provider, err := Open(st) + require.NoError(t, err) + require.NoError(t, provider.SetCreatorID(1)) + + for i, shared := range []bool{false, true} { + fileNum := base.FileNum(1 + i).DiskFileNum() + name := "local" + if shared { + name = "remote" + } + t.Run(name, func(t *testing.T) { + // Removing or opening an object that the provider doesn't know anything + // about should return a not-exist error. + err := provider.Remove(base.FileTypeTable, fileNum) + require.True(t, provider.IsNotExistError(err)) + _, err = provider.OpenForReading(context.Background(), base.FileTypeTable, fileNum, objstorage.OpenOptions{}) + require.True(t, provider.IsNotExistError(err)) + + w, _, err := provider.Create(context.Background(), base.FileTypeTable, fileNum, objstorage.CreateOptions{ + PreferSharedStorage: shared, + }) + require.NoError(t, err) + require.NoError(t, w.Write([]byte("foo"))) + require.NoError(t, w.Finish()) + + // Remove the underlying file or object. + if !shared { + require.NoError(t, fs.Remove(base.MakeFilename(base.FileTypeTable, fileNum))) + } else { + meta, err := provider.Lookup(base.FileTypeTable, fileNum) + require.NoError(t, err) + require.NoError(t, sharedStorage.Delete(remoteObjectName(meta))) + } + + _, err = provider.OpenForReading(context.Background(), base.FileTypeTable, fileNum, objstorage.OpenOptions{}) + require.True(t, provider.IsNotExistError(err)) + + // It's acceptable for Remove to return a not-exist error, or no error at all. + if err := provider.Remove(base.FileTypeTable, fileNum); err != nil { + require.True(t, provider.IsNotExistError(err)) + } + }) + } +} + +// genData generates object data that can be checked later with checkData. +func genData(salt byte, offset int, p []byte) { + for i := range p { + p[i] = salt ^ xor(offset+i) + } +} + +func checkData(t *testing.T, offset int, p []byte) (salt byte) { + t.Helper() + salt = p[0] ^ xor(offset) + for i := range p { + if p[i]^xor(offset+i) != salt { + t.Fatalf("invalid data") + } + } + return salt +} + +// xor returns the XOR of all bytes representing the integer. +func xor(n int) byte { + v := uint64(n) + v ^= v >> 32 + v ^= v >> 16 + v ^= v >> 8 + return byte(v) +} + +// TestParallelSync checks that multiple goroutines can create and delete +// objects and sync in parallel. +func TestParallelSync(t *testing.T) { + for _, shared := range []bool{false, true} { + name := "local" + if shared { + name = "shared" + } + t.Run(name, func(t *testing.T) { + st := DefaultSettings(vfs.NewMem(), "") + st.Remote.StorageFactory = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": remote.NewInMem(), + }) + + st.Remote.CreateOnShared = remote.CreateOnSharedAll + st.Remote.CreateOnSharedLocator = "" + p, err := Open(st) + require.NoError(t, err) + require.NoError(t, p.SetCreatorID(1)) + + const numGoroutines = 4 + const numOps = 100 + var wg sync.WaitGroup + for n := 0; n < numGoroutines; n++ { + wg.Add(1) + go func(startNum int, shared bool) { + defer wg.Done() + rng := rand.New(rand.NewSource(int64(startNum))) + for i := 0; i < numOps; i++ { + num := base.FileNum(startNum + i).DiskFileNum() + w, _, err := p.Create(context.Background(), base.FileTypeTable, num, objstorage.CreateOptions{ + PreferSharedStorage: shared, + }) + if err != nil { + panic(err) + } + if err := w.Finish(); err != nil { + panic(err) + } + if rng.Intn(2) == 0 { + if err := p.Sync(); err != nil { + panic(err) + } + } + if err := p.Remove(base.FileTypeTable, num); err != nil { + panic(err) + } + if rng.Intn(2) == 0 { + if err := p.Sync(); err != nil { + panic(err) + } + } + } + }(numOps*(n+1), shared) + } + wg.Wait() + }) + } +} diff --git a/pebble/objstorage/objstorageprovider/readahead.go b/pebble/objstorage/objstorageprovider/readahead.go new file mode 100644 index 0000000..e07017c --- /dev/null +++ b/pebble/objstorage/objstorageprovider/readahead.go @@ -0,0 +1,201 @@ +// 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 + +const ( + // Constants for dynamic readahead of data blocks. Note that the size values + // make sense as some multiple of the default block size; and they should + // both be larger than the default block size. + 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 */ +) + +// readaheadState contains state variables related to readahead. Updated on +// file reads. +type readaheadState struct { + // Number of sequential reads. + numReads int64 + maxReadaheadSize int64 + // Size issued to the next call to Prefetch. Starts at or above + // initialReadaheadSize and grows exponentially until maxReadaheadSize. + size int64 + // prevSize is the size used in the last Prefetch call. + prevSize int64 + // The byte offset up to which the OS has been asked to read ahead / cached. + // When reading ahead, reads up to this limit should not incur an IO + // operation. Reads after this limit can benefit from a new call to + // Prefetch. + limit int64 +} + +func makeReadaheadState(maxReadaheadSize int64) readaheadState { + return readaheadState{ + 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+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+rs.maxReadaheadSize { + // We read too far away from rs.limit to benefit from readahead in + // any scenario. Reset all variables. + rs.numReads = 1 + rs.limit = currentReadEnd + rs.size = initialReadaheadSize + rs.prevSize = 0 + return + } + // Reads in the range [rs.limit - rs.prevSize, rs.limit] end up + // here. This is a read that is potentially benefitting from a past + // readahead. + return + } + if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize { + // Blocks are being read sequentially and would benefit from readahead + // down the line. + rs.numReads++ + return + } + // 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.numReads = 1 + rs.limit = currentReadEnd + rs.size = initialReadaheadSize + rs.prevSize = 0 +} + +// maybeReadahead updates state and determines whether to issue a readahead / +// prefetch call for a block read at offset for blockLength bytes. +// Returns a size value (greater than 0) that should be prefetched if readahead +// would be beneficial. +func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 { + currentReadEnd := offset + blockLength + if rs.numReads >= minFileReadsForReadahead { + // The minimum threshold of sequential reads to justify reading ahead + // has been reached. + // There are two intervals: the interval being read: + // [offset, currentReadEnd] + // 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 + 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+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 + rs.maxReadaheadSize], and ---- is the range + // being read. + // + // 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 + rs.maxReadaheadSize] and [offset, + // currentReadEnd]: + // + // rs.limit rs.limit + rs.maxReadaheadSize + // ++++++++++|===========================| + // + // |-------------| + // offset currentReadEnd + // + // + rs.numReads++ + rs.limit = offset + rs.size + rs.prevSize = rs.size + // Increase rs.size for the next read. + rs.size *= 2 + if rs.size > rs.maxReadaheadSize { + rs.size = rs.maxReadaheadSize + } + return rs.prevSize + } + 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 + rs.maxReadaheadSize) + // |+++++++++++++|=============| + // + // |-------------| + // offset currentReadEnd + // + // Or too far behind: + // + // (rs.limit - rs.prevSize) (rs.limit) (rs.limit + rs.maxReadaheadSize) + // |+++++++++++++|=============| + // + // |-------------| + // offset currentReadEnd + // + rs.numReads = 1 + rs.limit = currentReadEnd + rs.size = initialReadaheadSize + rs.prevSize = 0 + + return 0 + } + // Reads in the range [rs.limit - rs.prevSize, rs.limit] end up + // here. This is a read that is potentially benefitting from a past + // readahead, but there's no reason to issue a readahead call at the + // moment. + // + // (rs.limit - rs.prevSize) (rs.limit + rs.maxReadaheadSize) + // |+++++++++++++|===============| + // (rs.limit) + // + // |-------| + // offset currentReadEnd + // + rs.numReads++ + return 0 + } + 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 + rs.maxReadaheadSize) + // |=============| + // + // |-------| + // offset currentReadEnd + // + rs.numReads++ + return 0 + } + // 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 - rs.maxReadaheadSize) (rs.limit) (rs.limit + rs.maxReadaheadSize) + // |+++++++++++++|=============| + // + // |-------| + // offset currentReadEnd + // + rs.numReads = 1 + rs.limit = currentReadEnd + rs.size = initialReadaheadSize + rs.prevSize = 0 + return 0 +} diff --git a/pebble/objstorage/objstorageprovider/readahead_test.go b/pebble/objstorage/objstorageprovider/readahead_test.go new file mode 100644 index 0000000..a44d78a --- /dev/null +++ b/pebble/objstorage/objstorageprovider/readahead_test.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. + +package objstorageprovider + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/stretchr/testify/require" +) + +func TestMaybeReadahead(t *testing.T) { + rs := makeReadaheadState(256 * 1024) + datadriven.RunTest(t, "testdata/readahead", func(t *testing.T, d *datadriven.TestData) string { + cacheHit := false + switch d.Cmd { + case "reset": + rs.size = initialReadaheadSize + rs.limit = 0 + rs.numReads = 0 + return "" + + case "cache-read": + cacheHit = true + fallthrough + case "read": + args := strings.Split(d.Input, ",") + if len(args) != 2 { + return "expected 2 args: offset, size" + } + + offset, err := strconv.ParseInt(strings.TrimSpace(args[0]), 10, 64) + require.NoError(t, err) + size, err := strconv.ParseInt(strings.TrimSpace(args[1]), 10, 64) + require.NoError(t, err) + var raSize int64 + if cacheHit { + rs.recordCacheHit(offset, size) + } else { + raSize = rs.maybeReadahead(offset, size) + } + + var buf strings.Builder + fmt.Fprintf(&buf, "readahead: %d\n", raSize) + fmt.Fprintf(&buf, "numReads: %d\n", rs.numReads) + fmt.Fprintf(&buf, "size: %d\n", rs.size) + fmt.Fprintf(&buf, "prevSize: %d\n", rs.prevSize) + fmt.Fprintf(&buf, "limit: %d", rs.limit) + return buf.String() + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} diff --git a/pebble/objstorage/objstorageprovider/remote.go b/pebble/objstorage/objstorageprovider/remote.go new file mode 100644 index 0000000..70fdfc7 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/remote.go @@ -0,0 +1,377 @@ +// 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" + "github.com/cockroachdb/redact" +) + +// 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", errors.Safe(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", errors.Safe(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", errors.Safe(refName)) + // TODO(radu): maybe list references for the object. + } + return nil, errors.Errorf("marker object %q does not exist", errors.Safe(refName)) + } + return nil, errors.Wrapf(err, "checking marker object %q", errors.Safe(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", redact.SafeString(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/pebble/objstorage/objstorageprovider/remote_backing.go b/pebble/objstorage/objstorageprovider/remote_backing.go new file mode 100644 index 0000000..c5526e5 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/remote_backing.go @@ -0,0 +1,304 @@ +// 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, + CustomObjectName: d.meta.Remote.CustomObjectName, + }) + } + }() + 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/pebble/objstorage/objstorageprovider/remote_backing_test.go b/pebble/objstorage/objstorageprovider/remote_backing_test.go new file mode 100644 index 0000000..805c087 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/remote_backing_test.go @@ -0,0 +1,158 @@ +// 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 ( + "encoding/binary" + "testing" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestSharedObjectBacking(t *testing.T) { + for _, cleanup := range []objstorage.SharedCleanupMethod{objstorage.SharedRefTracking, objstorage.SharedNoCleanup} { + name := "ref-tracking" + if cleanup == objstorage.SharedNoCleanup { + name = "no-cleanup" + } + t.Run(name, func(t *testing.T) { + st := DefaultSettings(vfs.NewMem(), "") + sharedStorage := remote.NewInMem() + st.Remote.StorageFactory = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "foo": sharedStorage, + }) + p, err := Open(st) + require.NoError(t, err) + defer p.Close() + + const creatorID = objstorage.CreatorID(99) + require.NoError(t, p.SetCreatorID(creatorID)) + meta := objstorage.ObjectMetadata{ + DiskFileNum: base.FileNum(1).DiskFileNum(), + FileType: base.FileTypeTable, + } + meta.Remote.CreatorID = 100 + meta.Remote.CreatorFileNum = base.FileNum(200).DiskFileNum() + meta.Remote.CleanupMethod = cleanup + meta.Remote.Locator = "foo" + meta.Remote.CustomObjectName = "obj-name" + meta.Remote.Storage = sharedStorage + + h, err := p.RemoteObjectBacking(&meta) + require.NoError(t, err) + buf, err := h.Get() + require.NoError(t, err) + h.Close() + _, err = h.Get() + require.Error(t, err) + + d1, err := decodeRemoteObjectBacking(base.FileTypeTable, base.FileNum(100).DiskFileNum(), buf) + require.NoError(t, err) + require.Equal(t, uint64(100), uint64(d1.meta.DiskFileNum.FileNum())) + require.Equal(t, base.FileTypeTable, d1.meta.FileType) + d1.meta.Remote.Storage = sharedStorage + require.Equal(t, meta.Remote, d1.meta.Remote) + if cleanup == objstorage.SharedRefTracking { + require.Equal(t, creatorID, d1.refToCheck.creatorID) + require.Equal(t, base.FileNum(1).DiskFileNum(), d1.refToCheck.fileNum) + } else { + require.Equal(t, objstorage.CreatorID(0), d1.refToCheck.creatorID) + require.Equal(t, base.FileNum(0).DiskFileNum(), d1.refToCheck.fileNum) + } + + t.Run("unknown-tags", func(t *testing.T) { + // Append a tag that is safe to ignore. + buf2 := buf + buf2 = binary.AppendUvarint(buf2, 13) + buf2 = binary.AppendUvarint(buf2, 2) + buf2 = append(buf2, 1, 1) + + d2, err := decodeRemoteObjectBacking(base.FileTypeTable, base.FileNum(100).DiskFileNum(), buf2) + require.NoError(t, err) + require.Equal(t, uint64(100), uint64(d2.meta.DiskFileNum.FileNum())) + require.Equal(t, base.FileTypeTable, d2.meta.FileType) + d2.meta.Remote.Storage = sharedStorage + require.Equal(t, meta.Remote, d2.meta.Remote) + if cleanup == objstorage.SharedRefTracking { + require.Equal(t, creatorID, d2.refToCheck.creatorID) + require.Equal(t, base.FileNum(1).DiskFileNum(), d2.refToCheck.fileNum) + } else { + require.Equal(t, objstorage.CreatorID(0), d2.refToCheck.creatorID) + require.Equal(t, base.FileNum(0).DiskFileNum(), d2.refToCheck.fileNum) + } + + buf3 := buf2 + buf3 = binary.AppendUvarint(buf3, tagNotSafeToIgnoreMask+5) + _, err = decodeRemoteObjectBacking(meta.FileType, meta.DiskFileNum, buf3) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown tag") + }) + }) + } +} + +func TestCreateSharedObjectBacking(t *testing.T) { + st := DefaultSettings(vfs.NewMem(), "") + sharedStorage := remote.NewInMem() + st.Remote.StorageFactory = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "foo": sharedStorage, + }) + p, err := Open(st) + require.NoError(t, err) + defer p.Close() + + require.NoError(t, p.SetCreatorID(1)) + + backing, err := p.CreateExternalObjectBacking("foo", "custom-obj-name") + require.NoError(t, err) + d, err := decodeRemoteObjectBacking(base.FileTypeTable, base.FileNum(100).DiskFileNum(), backing) + require.NoError(t, err) + require.Equal(t, uint64(100), uint64(d.meta.DiskFileNum.FileNum())) + require.Equal(t, base.FileTypeTable, d.meta.FileType) + require.Equal(t, remote.Locator("foo"), d.meta.Remote.Locator) + require.Equal(t, "custom-obj-name", d.meta.Remote.CustomObjectName) + require.Equal(t, objstorage.SharedNoCleanup, d.meta.Remote.CleanupMethod) +} + +func TestAttachRemoteObjects(t *testing.T) { + st := DefaultSettings(vfs.NewMem(), "") + sharedStorage := remote.NewInMem() + st.Remote.StorageFactory = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "foo": sharedStorage, + }) + p, err := Open(st) + require.NoError(t, err) + defer p.Close() + require.NoError(t, p.SetCreatorID(1)) + backing, err := p.CreateExternalObjectBacking("foo", "custom-obj-name") + require.NoError(t, err) + _, err = p.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ + FileType: base.FileTypeTable, + FileNum: base.FileNum(100).DiskFileNum(), + Backing: backing, + }}) + require.NoError(t, err) + + // Sync, close, and reopen the provider and expect that we see + // our object. + require.NoError(t, p.Sync()) + require.NoError(t, p.Close()) + + p, err = Open(st) + require.NoError(t, err) + defer p.Close() + require.NoError(t, p.SetCreatorID(1)) + objs := p.List() + require.Len(t, objs, 1) + o := objs[0] + require.Equal(t, remote.Locator("foo"), o.Remote.Locator) + require.Equal(t, "custom-obj-name", o.Remote.CustomObjectName) + require.Equal(t, uint64(100), uint64(o.DiskFileNum.FileNum())) + require.Equal(t, base.FileTypeTable, o.FileType) +} diff --git a/pebble/objstorage/objstorageprovider/remote_obj_name.go b/pebble/objstorage/objstorageprovider/remote_obj_name.go new file mode 100644 index 0000000..b33a908 --- /dev/null +++ b/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/pebble/objstorage/objstorageprovider/remote_obj_name_test.go b/pebble/objstorage/objstorageprovider/remote_obj_name_test.go new file mode 100644 index 0000000..abe6c4d --- /dev/null +++ b/pebble/objstorage/objstorageprovider/remote_obj_name_test.go @@ -0,0 +1,64 @@ +// 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" + "math/rand" + "testing" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/stretchr/testify/require" +) + +func TestSharedObjectNames(t *testing.T) { + t.Run("crosscheck", func(t *testing.T) { + supportedFileTypes := []base.FileType{ + base.FileTypeTable, + } + for it := 0; it < 100; it++ { + var meta objstorage.ObjectMetadata + meta.DiskFileNum = base.FileNum(rand.Intn(100000)).DiskFileNum() + meta.FileType = supportedFileTypes[rand.Int()%len(supportedFileTypes)] + meta.Remote.CreatorID = objstorage.CreatorID(rand.Int63()) + meta.Remote.CreatorFileNum = base.FileNum(rand.Intn(100000)).DiskFileNum() + if rand.Intn(4) == 0 { + meta.Remote.CustomObjectName = fmt.Sprintf("foo-%d.sst", rand.Intn(10000)) + } + + obj := remoteObjectName(meta) + // Cross-check against cleaner implementations. + expObj := meta.Remote.CustomObjectName + if expObj == "" { + expObj = fmt.Sprintf("%04x-%s-%s", objHash(meta), meta.Remote.CreatorID, base.MakeFilename(meta.FileType, meta.Remote.CreatorFileNum)) + } + require.Equal(t, expObj, obj) + + require.Equal(t, expObj+".ref.", sharedObjectRefPrefix(meta)) + + refCreatorID := objstorage.CreatorID(rand.Int63()) + refObj := sharedObjectRefName(meta, refCreatorID, meta.DiskFileNum) + expRefObj := fmt.Sprintf("%s.ref.%s.%s", expObj, refCreatorID, meta.DiskFileNum) + require.Equal(t, refObj, expRefObj) + } + }) + + t.Run("example", func(t *testing.T) { + var meta objstorage.ObjectMetadata + meta.DiskFileNum = base.FileNum(123).DiskFileNum() + meta.FileType = base.FileTypeTable + meta.Remote.CreatorID = objstorage.CreatorID(456) + meta.Remote.CreatorFileNum = base.FileNum(789).DiskFileNum() + require.Equal(t, remoteObjectName(meta), "0e17-456-000789.sst") + require.Equal(t, sharedObjectRefPrefix(meta), "0e17-456-000789.sst.ref.") + + refCreatorID := objstorage.CreatorID(101112) + require.Equal( + t, sharedObjectRefName(meta, refCreatorID, meta.DiskFileNum), + "0e17-456-000789.sst.ref.101112.000123", + ) + }) +} diff --git a/pebble/objstorage/objstorageprovider/remote_readable.go b/pebble/objstorage/objstorageprovider/remote_readable.go new file mode 100644 index 0000000..991a1ba --- /dev/null +++ b/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/pebble/objstorage/objstorageprovider/remoteobjcat/catalog.go b/pebble/objstorage/objstorageprovider/remoteobjcat/catalog.go new file mode 100644 index 0000000..aa73e93 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/remoteobjcat/catalog.go @@ -0,0 +1,388 @@ +// 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 ( + "fmt" + "io" + "sync" + + "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/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/atomicfs" +) + +// 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. +type Catalog struct { + fs vfs.FS + dirname string + mu struct { + sync.Mutex + + creatorID objstorage.CreatorID + objects map[base.DiskFileNum]RemoteObjectMetadata + + marker *atomicfs.Marker + + catalogFile vfs.File + catalogRecWriter *record.Writer + + rotationHelper record.RotationHelper + + // catalogFilename is the filename of catalogFile when catalogFile != nil, otherwise + // it is the filename of the last catalog file. + catalogFilename string + } +} + +// 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.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 objstorage.CreatorID + // CreatorFileNum is the identifier for the object within the context of the + // DB instance that originally created the object. + 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 = "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 remote objects in the catalog. +type CatalogContents struct { + // CreatorID, if it is set. + CreatorID objstorage.CreatorID + Objects []RemoteObjectMetadata +} + +// Open creates a Catalog and loads any existing catalog file, returning the +// creator ID (if it is set) and the contents. +func Open(fs vfs.FS, dirname string) (*Catalog, CatalogContents, error) { + c := &Catalog{ + fs: fs, + dirname: dirname, + } + c.mu.objects = make(map[base.DiskFileNum]RemoteObjectMetadata) + + var err error + c.mu.marker, c.mu.catalogFilename, err = atomicfs.LocateMarker(fs, dirname, catalogMarkerName) + if err != nil { + return nil, CatalogContents{}, err + } + // If the filename is empty, there is no existing catalog. + if c.mu.catalogFilename != "" { + if err := c.loadFromCatalogFile(c.mu.catalogFilename); err != nil { + return nil, CatalogContents{}, err + } + if err := c.mu.marker.RemoveObsolete(); err != nil { + return nil, CatalogContents{}, err + } + // TODO(radu): remove obsolete catalog files. + } + res := CatalogContents{ + CreatorID: c.mu.creatorID, + 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. + slices.SortFunc(res.Objects, func(a, b RemoteObjectMetadata) int { + return cmp.Compare(a.FileNum, b.FileNum) + }) + return c, res, nil +} + +// SetCreatorID sets the creator ID. If it is already set, it must match. +func (c *Catalog) SetCreatorID(id objstorage.CreatorID) error { + if !id.IsSet() { + return errors.AssertionFailedf("attempt to unset CreatorID") + } + + c.mu.Lock() + defer c.mu.Unlock() + + if c.mu.creatorID.IsSet() { + if c.mu.creatorID != id { + return errors.AssertionFailedf("attempt to change CreatorID from %s to %s", c.mu.creatorID, id) + } + return nil + } + + ve := VersionEdit{CreatorID: id} + if err := c.writeToCatalogFileLocked(&ve); err != nil { + return errors.Wrapf(err, "pebble: could not write to remote object catalog") + } + c.mu.creatorID = id + return nil +} + +// Close any open files. +func (c *Catalog) Close() error { + return c.closeCatalogFile() +} + +func (c *Catalog) closeCatalogFile() error { + if c.mu.catalogFile == nil { + return nil + } + err1 := c.mu.catalogRecWriter.Close() + err2 := c.mu.catalogFile.Close() + c.mu.catalogRecWriter = nil + c.mu.catalogFile = nil + if err1 != nil { + return err1 + } + return err2 +} + +// Batch is used to perform multiple object additions/deletions at once. +type Batch struct { + 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 RemoteObjectMetadata) { + b.ve.NewObjects = append(b.ve.NewObjects, meta) +} + +// DeleteObject adds an object removal to the batch. +func (b *Batch) DeleteObject(fileNum base.DiskFileNum) { + b.ve.DeletedObjects = append(b.ve.DeletedObjects, fileNum) +} + +// Reset clears the batch. +func (b *Batch) Reset() { + b.ve.NewObjects = b.ve.NewObjects[:0] + b.ve.DeletedObjects = b.ve.DeletedObjects[:0] +} + +// IsEmpty returns true if the batch is empty. +func (b *Batch) IsEmpty() bool { + return len(b.ve.NewObjects) == 0 && len(b.ve.DeletedObjects) == 0 +} + +// Copy returns a copy of the Batch. +func (b *Batch) Copy() Batch { + var res Batch + if len(b.ve.NewObjects) > 0 { + 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.DiskFileNum, len(b.ve.DeletedObjects)) + copy(res.ve.DeletedObjects, b.ve.DeletedObjects) + } + return res +} + +// Append merges two batches. +func (b *Batch) Append(other Batch) { + b.ve.NewObjects = append(b.ve.NewObjects, other.ve.NewObjects...) + b.ve.DeletedObjects = append(b.ve.DeletedObjects, other.ve.DeletedObjects...) +} + +// ApplyBatch applies a batch of updates; returns after the change is stably +// recorded on storage. +func (c *Catalog) ApplyBatch(b Batch) error { + c.mu.Lock() + defer c.mu.Unlock() + + // 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(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 remote object catalog") + } + + // 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 + } + for _, n := range b.ve.DeletedObjects { + delete(c.mu.objects, n) + } + + return nil +} + +func (c *Catalog) loadFromCatalogFile(filename string) error { + catalogPath := c.fs.PathJoin(c.dirname, filename) + f, err := c.fs.Open(catalogPath) + if err != nil { + return errors.Wrapf( + err, "pebble: could not open remote object catalog file %q for DB %q", + errors.Safe(filename), c.dirname, + ) + } + defer f.Close() + rr := record.NewReader(f, 0 /* logNum */) + for { + r, err := rr.Next() + if err == io.EOF || record.IsInvalidRecord(err) { + break + } + if err != nil { + return errors.Wrapf(err, "pebble: error when loading remote object catalog file %q", + errors.Safe(filename)) + } + 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 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. +// Creates a new file if this is the first write. +func (c *Catalog) writeToCatalogFileLocked(ve *VersionEdit) error { + c.mu.rotationHelper.AddRecord(int64(len(ve.NewObjects) + len(ve.DeletedObjects))) + snapshotSize := int64(len(c.mu.objects)) + + var shouldRotate bool + if c.mu.catalogFile == nil { + shouldRotate = true + } else if c.mu.catalogRecWriter.Size() >= rotateFileSize { + shouldRotate = c.mu.rotationHelper.ShouldRotate(snapshotSize) + } + + if shouldRotate { + if c.mu.catalogFile != nil { + if err := c.closeCatalogFile(); err != nil { + return err + } + } + if err := c.createNewCatalogFileLocked(); err != nil { + return err + } + c.mu.rotationHelper.Rotate(snapshotSize) + } + return writeRecord(ve, c.mu.catalogFile, c.mu.catalogRecWriter) +} + +func makeCatalogFilename(iter uint64) string { + return fmt.Sprintf("%s-%06d", catalogFilenameBase, iter) +} + +// createNewCatalogFileLocked creates a new catalog file, populates it with the +// current catalog and sets c.mu.catalogFile and c.mu.catalogRecWriter. +func (c *Catalog) createNewCatalogFileLocked() (outErr error) { + if c.mu.catalogFile != nil { + return errors.AssertionFailedf("catalogFile already open") + } + filename := makeCatalogFilename(c.mu.marker.NextIter()) + filepath := c.fs.PathJoin(c.dirname, filename) + file, err := c.fs.Create(filepath) + if err != nil { + return err + } + 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.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) + } + if err := writeRecord(&ve, file, recWriter); err != nil { + return err + } + + // Move the marker to the new filename. Move handles syncing the data + // directory as well. + if err := c.mu.marker.Move(filename); err != nil { + return errors.Wrap(err, "moving marker") + } + + return nil + }() + + if err != nil { + _ = recWriter.Close() + _ = file.Close() + _ = c.fs.Remove(filepath) + return err + } + + // Remove any previous file (ignoring any error). + if c.mu.catalogFilename != "" { + _ = c.fs.Remove(c.fs.PathJoin(c.dirname, c.mu.catalogFilename)) + } + + c.mu.catalogFile = file + c.mu.catalogRecWriter = recWriter + c.mu.catalogFilename = filename + return nil +} + +func writeRecord(ve *VersionEdit, file vfs.File, recWriter *record.Writer) error { + w, err := recWriter.Next() + if err != nil { + return err + } + if err := ve.Encode(w); err != nil { + return err + } + if err := recWriter.Flush(); err != nil { + return err + } + return file.Sync() +} diff --git a/pebble/objstorage/objstorageprovider/remoteobjcat/catalog_test.go b/pebble/objstorage/objstorageprovider/remoteobjcat/catalog_test.go new file mode 100644 index 0000000..37a0c3c --- /dev/null +++ b/pebble/objstorage/objstorageprovider/remoteobjcat/catalog_test.go @@ -0,0 +1,183 @@ +// 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_test + +import ( + "fmt" + "math/rand" + "sort" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat" + "github.com/cockroachdb/pebble/vfs" +) + +func TestCatalog(t *testing.T) { + mem := vfs.NewMem() + var memLog base.InMemLogger + + var cat *remoteobjcat.Catalog + datadriven.RunTest(t, "testdata/catalog", func(t *testing.T, td *datadriven.TestData) string { + toUInt64 := func(args ...string) []uint64 { + t.Helper() + var res []uint64 + for _, arg := range args { + n, err := strconv.Atoi(arg) + if err != nil { + td.Fatalf(t, "error parsing arg %s as integer: %v", arg, err) + } + res = append(res, uint64(n)) + } + return res + } + + parseAdd := func(args []string) remoteobjcat.RemoteObjectMetadata { + t.Helper() + if len(args) != 3 { + td.Fatalf(t, "add ") + } + vals := toUInt64(args...) + return remoteobjcat.RemoteObjectMetadata{ + FileNum: base.FileNum(vals[0]).DiskFileNum(), + // When we support other file types, we should let the test determine this. + FileType: base.FileTypeTable, + CreatorID: objstorage.CreatorID(vals[1]), + CreatorFileNum: base.FileNum(vals[2]).DiskFileNum(), + } + } + + parseDel := func(args []string) base.DiskFileNum { + t.Helper() + if len(args) != 1 { + td.Fatalf(t, "delete ") + } + return base.FileNum(toUInt64(args[0])[0]).DiskFileNum() + } + + memLog.Reset() + switch td.Cmd { + case "open": + if len(td.CmdArgs) != 1 { + td.Fatalf(t, "open ") + } + dirname := td.CmdArgs[0].String() + err := mem.MkdirAll(dirname, 0755) + if err != nil { + td.Fatalf(t, "%v", err) + } + var contents remoteobjcat.CatalogContents + cat, contents, err = remoteobjcat.Open(vfs.WithLogging(mem, memLog.Infof), dirname) + if err != nil { + return err.Error() + } + var buf strings.Builder + if contents.CreatorID.IsSet() { + fmt.Fprintf(&buf, "creator-id: %s\n", contents.CreatorID) + } + for _, meta := range contents.Objects { + fmt.Fprintf(&buf, "%s: %d/%s\n", meta.FileNum, meta.CreatorID, meta.CreatorFileNum) + } + + return buf.String() + + case "set-creator-id": + if len(td.CmdArgs) != 1 { + td.Fatalf(t, "set-creator-id ") + } + id := objstorage.CreatorID(toUInt64(td.CmdArgs[0].String())[0]) + if err := cat.SetCreatorID(id); err != nil { + return fmt.Sprintf("error setting creator ID: %v", err) + } + return memLog.String() + + case "batch": + var b remoteobjcat.Batch + for _, cmd := range strings.Split(td.Input, "\n") { + tokens := strings.Split(cmd, " ") + if len(tokens) == 0 { + td.Fatalf(t, "empty batch line") + } + switch tokens[0] { + case "add": + b.AddObject(parseAdd(tokens[1:])) + case "delete": + b.DeleteObject(parseDel(tokens[1:])) + default: + td.Fatalf(t, "unknown batch command: %s", tokens[0]) + } + } + if err := cat.ApplyBatch(b); err != nil { + return fmt.Sprintf("error applying batch: %v", err) + } + b.Reset() + return memLog.String() + + case "random-batches": + n := 1 + size := 1000 + for _, arg := range td.CmdArgs { + if len(arg.Vals) != 1 { + td.Fatalf(t, "random-batches n= size=") + } + val := toUInt64(arg.Vals[0])[0] + switch arg.Key { + case "n": + n = int(val) + case "size": + size = int(val) + default: + td.Fatalf(t, "random-batches n= size=") + } + } + var b remoteobjcat.Batch + for batchIdx := 0; batchIdx < n; batchIdx++ { + for i := 0; i < size; i++ { + b.AddObject(remoteobjcat.RemoteObjectMetadata{ + FileNum: base.FileNum(rand.Uint64()).DiskFileNum(), + // When we support other file types, we should let the test determine this. + FileType: base.FileTypeTable, + CreatorID: objstorage.CreatorID(rand.Uint64()), + CreatorFileNum: base.FileNum(rand.Uint64()).DiskFileNum(), + }) + } + if err := cat.ApplyBatch(b); err != nil { + td.Fatalf(t, "error applying batch: %v", err) + } + b.Reset() + } + return memLog.String() + + case "close": + if cat == nil { + return "nil catalog" + } + err := cat.Close() + cat = nil + if err != nil { + return fmt.Sprintf("%v", err) + } + return memLog.String() + + case "list": + if len(td.CmdArgs) != 1 { + td.Fatalf(t, "open ") + } + paths, err := mem.List(td.CmdArgs[0].String()) + if err != nil { + return err.Error() + } + sort.Strings(paths) + return strings.Join(paths, "\n") + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} diff --git a/pebble/objstorage/objstorageprovider/remoteobjcat/testdata/catalog b/pebble/objstorage/objstorageprovider/remoteobjcat/testdata/catalog new file mode 100644 index 0000000..b67dff2 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/remoteobjcat/testdata/catalog @@ -0,0 +1,275 @@ +open test +---- + +list test +---- + +batch +add 1 10 100 +---- +create: test/REMOTE-OBJ-CATALOG-000001 +sync: test/REMOTE-OBJ-CATALOG-000001 +create: test/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 +close: test/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 +sync: test +sync: test/REMOTE-OBJ-CATALOG-000001 + +list test +---- +REMOTE-OBJ-CATALOG-000001 +marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + +batch +add 2 20 200 +add 3 30 300 +---- +sync: test/REMOTE-OBJ-CATALOG-000001 + +batch +delete 1 +---- +sync: test/REMOTE-OBJ-CATALOG-000001 + +list test +---- +REMOTE-OBJ-CATALOG-000001 +marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + +set-creator-id 5 +---- +sync: test/REMOTE-OBJ-CATALOG-000001 + +set-creator-id 5 +---- + +set-creator-id 6 +---- +error setting creator ID: attempt to change CreatorID from 5 to 6 + +# Bad batches. +batch +add 3 1 1 +---- +error applying batch: adding existing object 000003 + +batch +delete 1000 +---- +error applying batch: deleting non-existent object 001000 + +close +---- +close: test/REMOTE-OBJ-CATALOG-000001 + +open test +---- +creator-id: 5 +000002: 20/000200 +000003: 30/000300 + +set-creator-id 6 +---- +error setting creator ID: attempt to change CreatorID from 5 to 6 + +batch +add 4 40 40 +delete 3 +add 8 80 80 +---- +create: test/REMOTE-OBJ-CATALOG-000002 +sync: test/REMOTE-OBJ-CATALOG-000002 +create: test/marker.remote-obj-catalog.000002.REMOTE-OBJ-CATALOG-000002 +close: test/marker.remote-obj-catalog.000002.REMOTE-OBJ-CATALOG-000002 +remove: test/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 +sync: test +remove: test/REMOTE-OBJ-CATALOG-000001 +sync: test/REMOTE-OBJ-CATALOG-000002 + +list test +---- +REMOTE-OBJ-CATALOG-000002 +marker.remote-obj-catalog.000002.REMOTE-OBJ-CATALOG-000002 + +close +---- +close: test/REMOTE-OBJ-CATALOG-000002 + +open test +---- +creator-id: 5 +000002: 20/000200 +000004: 40/000040 +000008: 80/000080 + +close +---- + +open other-path +---- + +batch +add 5 50 500 +---- +create: other-path/REMOTE-OBJ-CATALOG-000001 +sync: other-path/REMOTE-OBJ-CATALOG-000001 +create: other-path/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 +close: other-path/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 +sync: other-path +sync: other-path/REMOTE-OBJ-CATALOG-000001 + +# Adding and deleting objects in the same batch is allowed. + +batch +add 9 50 501 +delete 9 +---- +sync: other-path/REMOTE-OBJ-CATALOG-000001 + +list other-path +---- +REMOTE-OBJ-CATALOG-000001 +marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + +list test +---- +REMOTE-OBJ-CATALOG-000002 +marker.remote-obj-catalog.000002.REMOTE-OBJ-CATALOG-000002 + +close +---- +close: other-path/REMOTE-OBJ-CATALOG-000001 + +open test +---- +creator-id: 5 +000002: 20/000200 +000004: 40/000040 +000008: 80/000080 + +# Test rotation. +list test +---- +REMOTE-OBJ-CATALOG-000002 +marker.remote-obj-catalog.000002.REMOTE-OBJ-CATALOG-000002 + +random-batches n=20 size=2000 +---- +create: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +create: test/marker.remote-obj-catalog.000003.REMOTE-OBJ-CATALOG-000003 +close: test/marker.remote-obj-catalog.000003.REMOTE-OBJ-CATALOG-000003 +remove: test/marker.remote-obj-catalog.000002.REMOTE-OBJ-CATALOG-000002 +sync: test +remove: test/REMOTE-OBJ-CATALOG-000002 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000003 +close: test/REMOTE-OBJ-CATALOG-000003 +create: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +create: test/marker.remote-obj-catalog.000004.REMOTE-OBJ-CATALOG-000004 +close: test/marker.remote-obj-catalog.000004.REMOTE-OBJ-CATALOG-000004 +remove: test/marker.remote-obj-catalog.000003.REMOTE-OBJ-CATALOG-000003 +sync: test +remove: test/REMOTE-OBJ-CATALOG-000003 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 + +list test +---- +REMOTE-OBJ-CATALOG-000004 +marker.remote-obj-catalog.000004.REMOTE-OBJ-CATALOG-000004 + +random-batches n=20 size=2000 +---- +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000004 +close: test/REMOTE-OBJ-CATALOG-000004 +create: test/REMOTE-OBJ-CATALOG-000005 +sync: test/REMOTE-OBJ-CATALOG-000005 +create: test/marker.remote-obj-catalog.000005.REMOTE-OBJ-CATALOG-000005 +close: test/marker.remote-obj-catalog.000005.REMOTE-OBJ-CATALOG-000005 +remove: test/marker.remote-obj-catalog.000004.REMOTE-OBJ-CATALOG-000004 +sync: test +remove: test/REMOTE-OBJ-CATALOG-000004 +sync: test/REMOTE-OBJ-CATALOG-000005 +sync: test/REMOTE-OBJ-CATALOG-000005 +sync: test/REMOTE-OBJ-CATALOG-000005 +sync: test/REMOTE-OBJ-CATALOG-000005 +sync: test/REMOTE-OBJ-CATALOG-000005 +sync: test/REMOTE-OBJ-CATALOG-000005 + +list test +---- +REMOTE-OBJ-CATALOG-000005 +marker.remote-obj-catalog.000005.REMOTE-OBJ-CATALOG-000005 + +# Even with huge batches, we don't rotate on each batch. +random-batches n=10 size=50000 +---- +sync: test/REMOTE-OBJ-CATALOG-000005 +close: test/REMOTE-OBJ-CATALOG-000005 +create: test/REMOTE-OBJ-CATALOG-000006 +sync: test/REMOTE-OBJ-CATALOG-000006 +create: test/marker.remote-obj-catalog.000006.REMOTE-OBJ-CATALOG-000006 +close: test/marker.remote-obj-catalog.000006.REMOTE-OBJ-CATALOG-000006 +remove: test/marker.remote-obj-catalog.000005.REMOTE-OBJ-CATALOG-000005 +sync: test +remove: test/REMOTE-OBJ-CATALOG-000005 +sync: test/REMOTE-OBJ-CATALOG-000006 +sync: test/REMOTE-OBJ-CATALOG-000006 +close: test/REMOTE-OBJ-CATALOG-000006 +create: test/REMOTE-OBJ-CATALOG-000007 +sync: test/REMOTE-OBJ-CATALOG-000007 +create: test/marker.remote-obj-catalog.000007.REMOTE-OBJ-CATALOG-000007 +close: test/marker.remote-obj-catalog.000007.REMOTE-OBJ-CATALOG-000007 +remove: test/marker.remote-obj-catalog.000006.REMOTE-OBJ-CATALOG-000006 +sync: test +remove: test/REMOTE-OBJ-CATALOG-000006 +sync: test/REMOTE-OBJ-CATALOG-000007 +sync: test/REMOTE-OBJ-CATALOG-000007 +sync: test/REMOTE-OBJ-CATALOG-000007 +sync: test/REMOTE-OBJ-CATALOG-000007 +close: test/REMOTE-OBJ-CATALOG-000007 +create: test/REMOTE-OBJ-CATALOG-000008 +sync: test/REMOTE-OBJ-CATALOG-000008 +create: test/marker.remote-obj-catalog.000008.REMOTE-OBJ-CATALOG-000008 +close: test/marker.remote-obj-catalog.000008.REMOTE-OBJ-CATALOG-000008 +remove: test/marker.remote-obj-catalog.000007.REMOTE-OBJ-CATALOG-000007 +sync: test +remove: test/REMOTE-OBJ-CATALOG-000007 +sync: test/REMOTE-OBJ-CATALOG-000008 +sync: test/REMOTE-OBJ-CATALOG-000008 +sync: test/REMOTE-OBJ-CATALOG-000008 + +list test +---- +REMOTE-OBJ-CATALOG-000008 +marker.remote-obj-catalog.000008.REMOTE-OBJ-CATALOG-000008 diff --git a/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit.go b/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit.go new file mode 100644 index 0000000..44552f5 --- /dev/null +++ b/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/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit_test.go b/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit_test.go new file mode 100644 index 0000000..85d6c45 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit_test.go @@ -0,0 +1,92 @@ +// 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 ( + "bytes" + "strings" + "testing" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/kr/pretty" +) + +func TestVersionEditRoundTrip(t *testing.T) { + for _, ve := range []VersionEdit{ + {}, + { + CreatorID: 12345, + }, + { + NewObjects: []RemoteObjectMetadata{ + { + FileNum: base.FileNum(1).DiskFileNum(), + FileType: base.FileTypeTable, + CreatorID: 12, + CreatorFileNum: base.FileNum(123).DiskFileNum(), + CleanupMethod: objstorage.SharedNoCleanup, + Locator: "", + CustomObjectName: "foo", + }, + }, + }, + { + DeletedObjects: []base.DiskFileNum{base.FileNum(1).DiskFileNum()}, + }, + { + CreatorID: 12345, + NewObjects: []RemoteObjectMetadata{ + { + FileNum: base.FileNum(1).DiskFileNum(), + FileType: base.FileTypeTable, + CreatorID: 12, + CreatorFileNum: base.FileNum(123).DiskFileNum(), + CleanupMethod: objstorage.SharedRefTracking, + Locator: "foo", + CustomObjectName: "", + }, + { + FileNum: base.FileNum(2).DiskFileNum(), + FileType: base.FileTypeTable, + CreatorID: 22, + CreatorFileNum: base.FileNum(223).DiskFileNum(), + Locator: "bar", + CustomObjectName: "obj1", + }, + { + FileNum: base.FileNum(3).DiskFileNum(), + FileType: base.FileTypeTable, + CreatorID: 32, + CreatorFileNum: base.FileNum(323).DiskFileNum(), + CleanupMethod: objstorage.SharedRefTracking, + Locator: "baz", + CustomObjectName: "obj2", + }, + }, + DeletedObjects: []base.DiskFileNum{base.FileNum(4).DiskFileNum(), base.FileNum(5).DiskFileNum()}, + }, + } { + if err := checkRoundTrip(ve); err != nil { + t.Fatalf("%+v did not roundtrip: %v", ve, err) + } + } +} + +func checkRoundTrip(e0 VersionEdit) error { + var e1 VersionEdit + buf := new(bytes.Buffer) + if err := e0.Encode(buf); err != nil { + return errors.Wrap(err, "encode") + } + if err := e1.Decode(buf); err != nil { + return errors.Wrap(err, "decode") + } + if diff := pretty.Diff(e0, e1); diff != nil { + return errors.Errorf("%s", strings.Join(diff, "\n")) + } + return nil +} diff --git a/pebble/objstorage/objstorageprovider/shared_writable.go b/pebble/objstorage/objstorageprovider/shared_writable.go new file mode 100644 index 0000000..5e8d45a --- /dev/null +++ b/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/pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go b/pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go new file mode 100644 index 0000000..112e362 --- /dev/null +++ b/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.Errorf("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/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_helpers_test.go b/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_helpers_test.go new file mode 100644 index 0000000..8f44b66 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_helpers_test.go @@ -0,0 +1,7 @@ +package sharedcache + +func (c *Cache) WaitForWritesToComplete() { + close(c.writeWorkers.tasksCh) + c.writeWorkers.doneWaitGroup.Wait() + c.writeWorkers.Start(c, c.writeWorkers.numWorkers) +} diff --git a/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_internal_test.go b/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_internal_test.go new file mode 100644 index 0000000..1598ff0 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_internal_test.go @@ -0,0 +1,90 @@ +// 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 ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSharedCacheLruList(t *testing.T) { + var s shard + s.mu.blocks = make([]cacheBlockState, 100) + expect := func(vals ...int) { + t.Helper() + if s.mu.lruHead == invalidBlockIndex { + if len(vals) != 0 { + t.Fatalf("expected non-empty list") + } + return + } + var list []int + prev := s.lruPrev(s.mu.lruHead) + b := s.mu.lruHead + for { + list = append(list, int(b)) + if s.lruPrev(b) != prev { + t.Fatalf("back link broken: %d:next=%d,prev=%d %d:next=%d,prev=%d", + prev, s.lruNext(prev), s.lruPrev(prev), + b, s.lruNext(b), s.lruPrev(b), + ) + } + prev = b + b = s.lruNext(b) + if b == s.mu.lruHead { + break + } + } + if !reflect.DeepEqual(vals, list) { + t.Fatalf("expected %v, got %v", vals, list) + } + } + + s.mu.lruHead = invalidBlockIndex + expect() + s.lruInsertFront(1) + expect(1) + s.lruInsertFront(10) + expect(10, 1) + s.lruInsertFront(5) + expect(5, 10, 1) + s.lruUnlink(5) + expect(10, 1) + s.lruUnlink(1) + expect(10) + s.lruUnlink(10) + expect() +} + +func TestSharedCacheFreeList(t *testing.T) { + var s shard + s.mu.blocks = make([]cacheBlockState, 100) + expect := func(vals ...int) { + t.Helper() + var list []int + for b := s.mu.freeHead; b != invalidBlockIndex; b = s.mu.blocks[b].next { + list = append(list, int(b)) + } + if !reflect.DeepEqual(vals, list) { + t.Fatalf("expected %v, got %v", vals, list) + } + } + s.mu.freeHead = invalidBlockIndex + expect() + s.freePush(1) + expect(1) + s.freePush(10) + expect(10, 1) + s.freePush(20) + expect(20, 10, 1) + require.Equal(t, cacheBlockIndex(20), s.freePop()) + expect(10, 1) + require.Equal(t, cacheBlockIndex(10), s.freePop()) + expect(1) + require.Equal(t, cacheBlockIndex(1), s.freePop()) + expect() +} diff --git a/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_test.go b/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_test.go new file mode 100644 index 0000000..19a988a --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/shared_cache_test.go @@ -0,0 +1,265 @@ +package sharedcache_test + +import ( + "bytes" + "context" + "fmt" + "strconv" + "sync" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "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/sharedcache" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func TestSharedCache(t *testing.T) { + ctx := context.Background() + + datadriven.Walk(t, "testdata/cache", func(t *testing.T, path string) { + var log base.InMemLogger + fs := vfs.WithLogging(vfs.NewMem(), func(fmt string, args ...interface{}) { + log.Infof(" "+fmt, args...) + }) + + provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(fs, "")) + require.NoError(t, err) + + var cache *sharedcache.Cache + defer func() { + if cache != nil { + cache.Close() + } + }() + + var objData []byte + datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { + log.Reset() + switch d.Cmd { + case "init": + blockSize := parseBytesArg(t, d, "block-size", 32*1024) + shardingBlockSize := parseBytesArg(t, d, "sharding-block-size", 1024*1024) + numShards := parseBytesArg(t, d, "num-shards", 32) + size := parseBytesArg(t, d, "size", numShards*shardingBlockSize) + if size%(numShards*shardingBlockSize) != 0 { + d.Fatalf(t, "size (%d) must be a multiple of numShards (%d) * shardingBlockSize(%d)", + size, numShards, shardingBlockSize, + ) + } + cache, err = sharedcache.Open( + fs, base.DefaultLogger, "", blockSize, int64(shardingBlockSize), int64(size), numShards, + ) + require.NoError(t, err) + return fmt.Sprintf("initialized with block-size=%d size=%d num-shards=%d", blockSize, size, numShards) + + case "write": + size := mustParseBytesArg(t, d, "size") + + writable, _, err := provider.Create(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.CreateOptions{}) + require.NoError(t, err) + defer writable.Finish() + + // With invariants on, Write will modify its input buffer. + objData = make([]byte, size) + wrote := make([]byte, size) + for i := 0; i < size; i++ { + objData[i] = byte(i) + wrote[i] = byte(i) + } + err = writable.Write(wrote) + // Writing a file is test setup, and it always is expected to succeed, so we assert + // within the test, rather than returning n and/or err here. + require.NoError(t, err) + + return "" + case "read", "read-for-compaction": + missesBefore := cache.Metrics().ReadsWithPartialHit + cache.Metrics().ReadsWithNoHit + offset := mustParseBytesArg(t, d, "offset") + size := mustParseBytesArg(t, d, "size") + + readable, err := provider.OpenForReading(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.OpenOptions{}) + require.NoError(t, err) + defer readable.Close() + + got := make([]byte, size) + flags := sharedcache.ReadFlags{ + ReadOnly: d.Cmd == "read-for-compaction", + } + err = cache.ReadAt(ctx, base.FileNum(1).DiskFileNum(), got, int64(offset), readable, readable.Size(), flags) + // We always expect cache.ReadAt to succeed. + require.NoError(t, err) + // It is easier to assert this condition programmatically, rather than returning + // got, which may be very large. + require.True(t, bytes.Equal(objData[int(offset):int(offset)+size], got), "incorrect data returned") + + // In order to ensure we get a hit on the next read, we must wait for writing to + // the cache to complete. + cache.WaitForWritesToComplete() + + // TODO(josh): Not tracing out filesystem activity here, since logging_fs.go + // doesn't trace calls to ReadAt or WriteAt. We should consider changing this. + missesAfter := cache.Metrics().ReadsWithPartialHit + cache.Metrics().ReadsWithNoHit + return fmt.Sprintf("misses=%d", missesAfter-missesBefore) + default: + d.Fatalf(t, "unknown command %s", d.Cmd) + return "" + } + }) + }) +} + +func TestSharedCacheRandomized(t *testing.T) { + ctx := context.Background() + + var log base.InMemLogger + fs := vfs.WithLogging(vfs.NewMem(), func(fmt string, args ...interface{}) { + log.Infof(" "+fmt, args...) + }) + + provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(fs, "")) + require.NoError(t, err) + + seed := uint64(time.Now().UnixNano()) + fmt.Printf("seed: %v\n", seed) + rand.Seed(seed) + + helper := func( + blockSize int, + shardingBlockSize int64) func(t *testing.T) { + return func(t *testing.T) { + for _, concurrentReads := range []bool{false, true} { + t.Run(fmt.Sprintf("concurrentReads=%v", concurrentReads), func(t *testing.T) { + maxShards := 32 + if invariants.RaceEnabled { + maxShards = 8 + } + numShards := rand.Intn(maxShards) + 1 + cacheSize := shardingBlockSize * int64(numShards) // minimum allowed cache size + + cache, err := sharedcache.Open(fs, base.DefaultLogger, "", blockSize, shardingBlockSize, cacheSize, numShards) + require.NoError(t, err) + defer cache.Close() + + writable, _, err := provider.Create(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.CreateOptions{}) + require.NoError(t, err) + + // With invariants on, Write will modify its input buffer. + // If size == 0, we can see panics below, so force a nonzero size. + size := rand.Int63n(cacheSize-1) + 1 + objData := make([]byte, size) + wrote := make([]byte, size) + for i := 0; i < int(size); i++ { + objData[i] = byte(i) + wrote[i] = byte(i) + } + + require.NoError(t, writable.Write(wrote)) + require.NoError(t, writable.Finish()) + + readable, err := provider.OpenForReading(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.OpenOptions{}) + require.NoError(t, err) + defer readable.Close() + + const numDistinctReads = 100 + wg := sync.WaitGroup{} + for i := 0; i < numDistinctReads; i++ { + wg.Add(1) + go func() { + defer wg.Done() + offset := rand.Int63n(size) + + got := make([]byte, size-offset) + err := cache.ReadAt(ctx, base.FileNum(1).DiskFileNum(), got, offset, readable, readable.Size(), sharedcache.ReadFlags{}) + require.NoError(t, err) + require.Equal(t, objData[int(offset):], got) + + got = make([]byte, size-offset) + err = cache.ReadAt(ctx, base.FileNum(1).DiskFileNum(), got, offset, readable, readable.Size(), sharedcache.ReadFlags{}) + require.NoError(t, err) + require.Equal(t, objData[int(offset):], got) + }() + // If concurrent reads, only wait 50% of loop iterations on average. + if concurrentReads && rand.Intn(2) == 0 { + wg.Wait() + } + if !concurrentReads { + wg.Wait() + } + } + wg.Wait() + }) + } + } + } + t.Run("32 KB block size", helper(32*1024, 1024*1024)) + t.Run("1 MB block size", helper(1024*1024, 1024*1024)) + + if !invariants.RaceEnabled { + for i := 0; i < 5; i++ { + exp := rand.Intn(11) + 10 // [10, 20] + randomBlockSize := 1 << exp // [1 KB, 1 MB] + + factor := rand.Intn(4) + 1 // [1, 4] + randomShardingBlockSize := int64(randomBlockSize * factor) // [1 KB, 4 MB] + + t.Run("random block and sharding block size", helper(randomBlockSize, randomShardingBlockSize)) + } + } +} + +// parseBytesArg parses an optional argument that specifies a byte size; if the +// argument is not specified the default value is used. K/M/G suffixes are +// supported. +func parseBytesArg(t testing.TB, d *datadriven.TestData, argName string, defaultValue int) int { + res, ok := tryParseBytesArg(t, d, argName) + if !ok { + return defaultValue + } + return res +} + +// parseBytesArg parses a mandatory argument that specifies a byte size; K/M/G +// suffixes are supported. +func mustParseBytesArg(t testing.TB, d *datadriven.TestData, argName string) int { + res, ok := tryParseBytesArg(t, d, argName) + if !ok { + t.Fatalf("argument '%s' missing", argName) + } + return res +} + +func tryParseBytesArg(t testing.TB, d *datadriven.TestData, argName string) (val int, ok bool) { + arg, ok := d.Arg(argName) + if !ok { + return 0, false + } + if len(arg.Vals) != 1 { + t.Fatalf("expected 1 value for '%s'", argName) + } + v := arg.Vals[0] + factor := 1 + switch v[len(v)-1] { + case 'k', 'K': + factor = 1024 + case 'm', 'M': + factor = 1024 * 1024 + case 'g', 'G': + factor = 1024 * 1024 * 1024 + } + if factor > 1 { + v = v[:len(v)-1] + } + res, err := strconv.Atoi(v) + if err != nil { + t.Fatalf("could not parse value '%s' for '%s'", arg.Vals[0], argName) + } + + return res * factor, true +} diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/compaction_reads b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/compaction_reads new file mode 100644 index 0000000..fc9a2e0 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/compaction_reads @@ -0,0 +1,26 @@ +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=200000 +---- + +read offset=1024 size=10000 +---- +misses=1 + +# This should be in the cache. +read-for-compaction offset=4096 size=2000 +---- +misses=0 + +# This should miss the cache. +read-for-compaction offset=4096 size=100000 +---- +misses=1 + +# This should miss the cache again - we don't populate the cache when doing +# compaction reads. +read-for-compaction offset=4096 size=100000 +---- +misses=1 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/eof_handling b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/eof_handling new file mode 100644 index 0000000..2f55de8 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/eof_handling @@ -0,0 +1,14 @@ +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=40000 +---- + +read offset=35000 size=4000 +---- +misses=1 + +read offset=35000 size=4000 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/lru b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/lru new file mode 100644 index 0000000..3044550 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/lru @@ -0,0 +1,33 @@ +init num-shards=1 size=1M +---- +initialized with block-size=32768 size=1048576 num-shards=1 + +write size=1500000 +---- + +read offset=0 size=32K +---- +misses=1 + +read offset=32K size=32K +---- +misses=1 + +read offset=64K size=960K +---- +misses=1 + +# The cache should now be full with the first MB. Read a new block. +read offset=1M size=32K +---- +misses=1 + +# The block that was evicted should have been the one at offset 0. +read offset=0 size=32K +---- +misses=1 + +# The block that was evicted should have been the one at offset 32768. +read offset=32K size=32K +---- +misses=1 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_larger_than_two_cache_shards b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_larger_than_two_cache_shards new file mode 100644 index 0000000..01205ef --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_larger_than_two_cache_shards @@ -0,0 +1,16 @@ +# Read larger than two cache shards. + +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=3145728 +---- + +read offset=57 size=3145671 +---- +misses=1 + +read offset=57 size=3145671 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks new file mode 100644 index 0000000..824ce0a --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks @@ -0,0 +1,20 @@ +# Large read that hits two cache blocks. + +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=32773 +---- + +read offset=0 size=32773 +---- +misses=1 + +read offset=0 size=32773 +---- +misses=0 + +read offset=57 size=32716 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks_with_first_read_at_big_offset b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks_with_first_read_at_big_offset new file mode 100644 index 0000000..20d4e1d --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks_with_first_read_at_big_offset @@ -0,0 +1,16 @@ +# Large read that hits two cache blocks, with first read at big offset. + +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=32773 +---- + +read offset=32768 size=5 +---- +misses=1 + +read offset=32768 size=5 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks_with_first_read_at_offset b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks_with_first_read_at_offset new file mode 100644 index 0000000..3298ec1 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_blocks_with_first_read_at_offset @@ -0,0 +1,16 @@ +# Large read that hits two cache blocks, with first read at offset. + +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=32773 +---- + +read offset=57 size=32716 +---- +misses=1 + +read offset=57 size=32716 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_shards b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_shards new file mode 100644 index 0000000..b447751 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_shards @@ -0,0 +1,20 @@ +# Large read that hits two cache shards. + +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=1048776 +---- + +read offset=0 size=1048776 +---- +misses=1 + +read offset=0 size=1048776 +---- +misses=0 + +read offset=57 size=1048719 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_shards_with_first_read_at_offset b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_shards_with_first_read_at_offset new file mode 100644 index 0000000..9721f68 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/read_that_hits_two_cache_shards_with_first_read_at_offset @@ -0,0 +1,16 @@ +# Large read that hits two cache shards, with first read at offset. + +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=1048776 +---- + +read offset=57 size=1048719 +---- +misses=1 + +read offset=57 size=1048719 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/small_read b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/small_read new file mode 100644 index 0000000..695017e --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/small_read @@ -0,0 +1,20 @@ +# Small read, with one miss then two hits. + +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=10 +---- + +read offset=0 size=10 +---- +misses=1 + +read offset=0 size=10 +---- +misses=0 + +read offset=4 size=6 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/small_read_with_first_read_at_offset b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/small_read_with_first_read_at_offset new file mode 100644 index 0000000..2908e38 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/sharedcache/testdata/cache/small_read_with_first_read_at_offset @@ -0,0 +1,20 @@ +# Small read, with first read at offset. + +init +---- +initialized with block-size=32768 size=33554432 num-shards=32 + +write size=10 +---- + +read offset=4 size=6 +---- +misses=1 + +read offset=4 size=6 +---- +misses=0 + +read offset=0 size=10 +---- +misses=0 diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/local b/pebble/objstorage/objstorageprovider/testdata/provider/local new file mode 100644 index 0000000..a2f4ce3 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/local @@ -0,0 +1,100 @@ +# Basic provider tests without shared storage. + +open p0 0 +---- + mkdir-all: p0 0755 + open-dir: p0 + +create 1 local 1 1024 +foo +---- + create: p0/000001.sst + sync-data: p0/000001.sst + close: p0/000001.sst + +read 1 +0 512 +0 1024 +512 1024 +---- + open: p0/000001.sst +size: 1024 + read-at(0, 512): p0/000001.sst +0 512: ok (salt 1) + read-at(0, 1024): p0/000001.sst +0 1024: ok (salt 1) + prefetch(512, 65536): p0/000001.sst + read-at(512, 1024): p0/000001.sst +512 1024: EOF + close: p0/000001.sst + +# A provider without shared storage creates object with shared preference +# locally. +create 2 shared 2 1024 +---- + create: p0/000002.sst + sync-data: p0/000002.sst + close: p0/000002.sst + +read 2 +0 512 +0 1024 +512 1024 +---- + open: p0/000002.sst +size: 1024 + read-at(0, 512): p0/000002.sst +0 512: ok (salt 2) + read-at(0, 1024): p0/000002.sst +0 1024: ok (salt 2) + prefetch(512, 65536): p0/000002.sst + read-at(512, 1024): p0/000002.sst +512 1024: EOF + close: p0/000002.sst + +remove 1 +---- + remove: p0/000001.sst + +list +---- +000002 -> p0/000002.sst + +read 1 +---- +file 000001 (type 2) unknown to the objstorage provider: file does not exist + +link-or-copy 3 local 3 100 +---- + create: temp-file-1 + close: temp-file-1 + link: temp-file-1 -> p0/000003.sst + +read 3 +0 100 +---- + open: p0/000003.sst +size: 100 + read-at(0, 100): p0/000003.sst +0 100: ok (salt 3) + close: p0/000003.sst + +link-or-copy 4 shared 4 1234 +---- + create: temp-file-2 + close: temp-file-2 + link: temp-file-2 -> p0/000004.sst + +read 4 +0 1234 +---- + open: p0/000004.sst +size: 1234 + read-at(0, 1234): p0/000004.sst +0 1234: ok (salt 4) + close: p0/000004.sst + +close +---- + sync: p0 + close: p0 diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/local_readahead b/pebble/objstorage/objstorageprovider/testdata/provider/local_readahead new file mode 100644 index 0000000..87266cc --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/local_readahead @@ -0,0 +1,51 @@ +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + create: p1/REMOTE-OBJ-CATALOG-000001 + sync: p1/REMOTE-OBJ-CATALOG-000001 + create: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + +create 1 local 1 2000000 +---- + create: p1/000001.sst + sync-data: p1/000001.sst + sync-data: p1/000001.sst + close: p1/000001.sst + +# We should see prefetch calls, and eventually a reopen +# (with sequential reads option). +read 1 +0 1000 +1000 15000 +16000 30000 +46000 10000 +56000 50000 +106000 30000 +140000 80000 +---- + open: p1/000001.sst +size: 2000000 + read-at(0, 1000): p1/000001.sst +0 1000: ok (salt 1) + read-at(1000, 15000): p1/000001.sst +1000 15000: ok (salt 1) + prefetch(16000, 65536): p1/000001.sst + read-at(16000, 30000): p1/000001.sst +16000 30000: ok (salt 1) + read-at(46000, 10000): p1/000001.sst +46000 10000: ok (salt 1) + prefetch(56000, 131072): p1/000001.sst + read-at(56000, 50000): p1/000001.sst +56000 50000: ok (salt 1) + read-at(106000, 30000): p1/000001.sst +106000 30000: ok (salt 1) + open: p1/000001.sst + read-at(140000, 80000): p1/000001.sst +140000 80000: ok (salt 1) + close: p1/000001.sst + close: p1/000001.sst diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach b/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach new file mode 100644 index 0000000..fac25a8 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach @@ -0,0 +1,150 @@ +# Basic tests for obtaining the backing of shared objects and attaching them to +# another provider. + +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + create: p1/REMOTE-OBJ-CATALOG-000001 + sync: p1/REMOTE-OBJ-CATALOG-000001 + create: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + +create 1 shared 1 100 +---- + create object "61a6-1-000001.sst" + close writer for "61a6-1-000001.sst" after 100 bytes + create object "61a6-1-000001.sst.ref.1.000001" + close writer for "61a6-1-000001.sst.ref.1.000001" after 0 bytes + +create 2 shared 2 200 +---- + create object "a629-1-000002.sst" + close writer for "a629-1-000002.sst" after 200 bytes + create object "a629-1-000002.sst.ref.1.000002" + close writer for "a629-1-000002.sst.ref.1.000002" after 0 bytes + +create 3 shared 3 300 +---- + create object "eaac-1-000003.sst" + close writer for "eaac-1-000003.sst" after 300 bytes + create object "eaac-1-000003.sst.ref.1.000003" + close writer for "eaac-1-000003.sst.ref.1.000003" after 0 bytes + +create 100 local 100 15 +---- + create: p1/000100.sst + sync-data: p1/000100.sst + close: p1/000100.sst + +list +---- +000001 -> remote://61a6-1-000001.sst +000002 -> remote://a629-1-000002.sst +000003 -> remote://eaac-1-000003.sst +000100 -> p1/000100.sst + +# Can't get backing of local object. +save-backing foo 100 +---- +object 000100 not on remote storage + +save-backing b1 1 +---- + +save-backing b2 2 +---- + +save-backing b3 3 +---- + +close +---- + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + close: p1/REMOTE-OBJ-CATALOG-000001 + close: p1 + +open p2 2 +---- + mkdir-all: p2 0755 + open-dir: p2 + open-dir: p2 + create: p2/REMOTE-OBJ-CATALOG-000001 + sync: p2/REMOTE-OBJ-CATALOG-000001 + create: p2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p2 + sync: p2/REMOTE-OBJ-CATALOG-000001 + +create 100 shared 100 15 +---- + create object "fd72-2-000100.sst" + close writer for "fd72-2-000100.sst" after 15 bytes + create object "fd72-2-000100.sst.ref.2.000100" + close writer for "fd72-2-000100.sst.ref.2.000100" after 0 bytes + +attach +b1 101 +b2 102 +b3 103 +---- + create object "61a6-1-000001.sst.ref.2.000101" + close writer for "61a6-1-000001.sst.ref.2.000101" after 0 bytes + size of object "61a6-1-000001.sst.ref.1.000001": 0 + create object "a629-1-000002.sst.ref.2.000102" + close writer for "a629-1-000002.sst.ref.2.000102" after 0 bytes + size of object "a629-1-000002.sst.ref.1.000002": 0 + create object "eaac-1-000003.sst.ref.2.000103" + close writer for "eaac-1-000003.sst.ref.2.000103" after 0 bytes + size of object "eaac-1-000003.sst.ref.1.000003": 0 + sync: p2/REMOTE-OBJ-CATALOG-000001 +000101 -> remote://61a6-1-000001.sst +000102 -> remote://a629-1-000002.sst +000103 -> remote://eaac-1-000003.sst + +list +---- +000100 -> remote://fd72-2-000100.sst +000101 -> remote://61a6-1-000001.sst +000102 -> remote://a629-1-000002.sst +000103 -> remote://eaac-1-000003.sst + +read 101 +0 100 +15 10 +---- + size of object "61a6-1-000001.sst.ref.2.000101": 0 + create reader for object "61a6-1-000001.sst": 100 bytes +size: 100 + read object "61a6-1-000001.sst" at 0 (length 100) +0 100: ok (salt 1) + read object "61a6-1-000001.sst" at 15 (length 10) +15 10: ok (salt 1) + close reader for "61a6-1-000001.sst" + +read 102 +0 200 +90 100 +---- + size of object "a629-1-000002.sst.ref.2.000102": 0 + create reader for object "a629-1-000002.sst": 200 bytes +size: 200 + read object "a629-1-000002.sst" at 0 (length 200) +0 200: ok (salt 2) + read object "a629-1-000002.sst" at 90 (length 100) +90 100: ok (salt 2) + close reader for "a629-1-000002.sst" + +read 103 +0 300 +---- + size of object "eaac-1-000003.sst.ref.2.000103": 0 + create reader for object "eaac-1-000003.sst": 300 bytes +size: 300 + read object "eaac-1-000003.sst" at 0 (length 300) +0 300: ok (salt 3) + close reader for "eaac-1-000003.sst" diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach_after_unref b/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach_after_unref new file mode 100644 index 0000000..719737c --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach_after_unref @@ -0,0 +1,91 @@ +# Tests when an object is unrefed before it is attached to another provider. + +open p5 5 +---- + mkdir-all: p5 0755 + open-dir: p5 + open-dir: p5 + create: p5/REMOTE-OBJ-CATALOG-000001 + sync: p5/REMOTE-OBJ-CATALOG-000001 + create: p5/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p5/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p5 + sync: p5/REMOTE-OBJ-CATALOG-000001 + +create 1 shared 1 100 +---- + create object "d632-5-000001.sst" + close writer for "d632-5-000001.sst" after 100 bytes + create object "d632-5-000001.sst.ref.5.000001" + close writer for "d632-5-000001.sst.ref.5.000001" after 0 bytes + +save-backing p5b1 1 +---- + +# This should do nothing. +remove 1 +---- + +open p6 6 +---- + mkdir-all: p6 0755 + open-dir: p6 + open-dir: p6 + create: p6/REMOTE-OBJ-CATALOG-000001 + sync: p6/REMOTE-OBJ-CATALOG-000001 + create: p6/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p6/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p6 + sync: p6/REMOTE-OBJ-CATALOG-000001 + +# Attach should succeed. +attach +p5b1 101 +---- + create object "d632-5-000001.sst.ref.6.000101" + close writer for "d632-5-000001.sst.ref.6.000101" after 0 bytes + size of object "d632-5-000001.sst.ref.5.000001": 0 + sync: p6/REMOTE-OBJ-CATALOG-000001 +000101 -> remote://d632-5-000001.sst + +switch p5 +---- + +# TODO(radu): after we close the backing, the unref should happen. +close-backing p5b1 +---- + +create 2 shared 2 100 +---- + create object "1ab5-5-000002.sst" + close writer for "1ab5-5-000002.sst" after 100 bytes + create object "1ab5-5-000002.sst.ref.5.000002" + close writer for "1ab5-5-000002.sst.ref.5.000002" after 0 bytes + +save-backing p5b2 2 +---- + +# Close the backing, then unref the object. +close-backing p5b2 +---- + +remove 2 +---- + delete object "1ab5-5-000002.sst.ref.5.000002" + list (prefix="1ab5-5-000002.sst.ref.", delimiter="") + delete object "1ab5-5-000002.sst" + +switch p6 +---- + +# Attach should error out because it can't find p5's ref. +attach +p5b2 102 +---- + create object "1ab5-5-000002.sst.ref.6.000102" + close writer for "1ab5-5-000002.sst.ref.6.000102" after 0 bytes + size of object "1ab5-5-000002.sst.ref.5.000002": error: file does not exist + delete object "1ab5-5-000002.sst.ref.6.000102" + list (prefix="1ab5-5-000002.sst.ref.", delimiter="") + delete object "1ab5-5-000002.sst" +error: origin marker object "1ab5-5-000002.sst.ref.5.000002" does not exist; object probably removed from the provider which created the backing diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach_multi b/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach_multi new file mode 100644 index 0000000..93baa31 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/shared_attach_multi @@ -0,0 +1,92 @@ +# Tests with the same shared object attached as multiple objects. + +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + create: p1/REMOTE-OBJ-CATALOG-000001 + sync: p1/REMOTE-OBJ-CATALOG-000001 + create: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + +create 1 shared 1 100 +---- + create object "61a6-1-000001.sst" + close writer for "61a6-1-000001.sst" after 100 bytes + create object "61a6-1-000001.sst.ref.1.000001" + close writer for "61a6-1-000001.sst.ref.1.000001" after 0 bytes + +save-backing b1 1 +---- + +open p2 2 +---- + mkdir-all: p2 0755 + open-dir: p2 + open-dir: p2 + create: p2/REMOTE-OBJ-CATALOG-000001 + sync: p2/REMOTE-OBJ-CATALOG-000001 + create: p2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p2 + sync: p2/REMOTE-OBJ-CATALOG-000001 + +# We should create three ref markers to allow independent removal. +attach +b1 101 +b1 102 +b1 103 +---- + create object "61a6-1-000001.sst.ref.2.000101" + close writer for "61a6-1-000001.sst.ref.2.000101" after 0 bytes + size of object "61a6-1-000001.sst.ref.1.000001": 0 + create object "61a6-1-000001.sst.ref.2.000102" + close writer for "61a6-1-000001.sst.ref.2.000102" after 0 bytes + size of object "61a6-1-000001.sst.ref.1.000001": 0 + create object "61a6-1-000001.sst.ref.2.000103" + close writer for "61a6-1-000001.sst.ref.2.000103" after 0 bytes + size of object "61a6-1-000001.sst.ref.1.000001": 0 + sync: p2/REMOTE-OBJ-CATALOG-000001 +000101 -> remote://61a6-1-000001.sst +000102 -> remote://61a6-1-000001.sst +000103 -> remote://61a6-1-000001.sst + +close-backing b1 +---- + +# Remove original object. +switch p1 +---- + +remove 1 +---- + delete object "61a6-1-000001.sst.ref.1.000001" + list (prefix="61a6-1-000001.sst.ref.", delimiter="") + - 61a6-1-000001.sst.ref.2.000101 + - 61a6-1-000001.sst.ref.2.000102 + - 61a6-1-000001.sst.ref.2.000103 + +switch p2 +---- + +remove 101 +---- + delete object "61a6-1-000001.sst.ref.2.000101" + list (prefix="61a6-1-000001.sst.ref.", delimiter="") + - 61a6-1-000001.sst.ref.2.000102 + - 61a6-1-000001.sst.ref.2.000103 + +remove 103 +---- + delete object "61a6-1-000001.sst.ref.2.000103" + list (prefix="61a6-1-000001.sst.ref.", delimiter="") + - 61a6-1-000001.sst.ref.2.000102 + +remove 102 +---- + delete object "61a6-1-000001.sst.ref.2.000102" + list (prefix="61a6-1-000001.sst.ref.", delimiter="") + delete object "61a6-1-000001.sst" diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/shared_basic b/pebble/objstorage/objstorageprovider/testdata/provider/shared_basic new file mode 100644 index 0000000..540c54c --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/shared_basic @@ -0,0 +1,132 @@ +# Basic provider tests with shared storage. + +# open +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + create: p1/REMOTE-OBJ-CATALOG-000001 + sync: p1/REMOTE-OBJ-CATALOG-000001 + create: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + +create 1 local 1 100 +---- + create: p1/000001.sst + sync-data: p1/000001.sst + close: p1/000001.sst + +read 1 +0 100 +---- + open: p1/000001.sst +size: 100 + read-at(0, 100): p1/000001.sst +0 100: ok (salt 1) + close: p1/000001.sst + +create 2 shared 2 100 +---- + create object "a629-1-000002.sst" + close writer for "a629-1-000002.sst" after 100 bytes + create object "a629-1-000002.sst.ref.1.000002" + close writer for "a629-1-000002.sst.ref.1.000002" after 0 bytes + +read 2 +0 100 +---- + size of object "a629-1-000002.sst.ref.1.000002": 0 + create reader for object "a629-1-000002.sst": 100 bytes +size: 100 + read object "a629-1-000002.sst" at 0 (length 100) +0 100: ok (salt 2) + close reader for "a629-1-000002.sst" + +list +---- +000001 -> p1/000001.sst +000002 -> remote://a629-1-000002.sst + +close +---- + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + close: p1/REMOTE-OBJ-CATALOG-000001 + close: p1 + +# Test that the objects are there on re-open. +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + open: p1/REMOTE-OBJ-CATALOG-000001 + close: p1/REMOTE-OBJ-CATALOG-000001 + +list +---- +000001 -> p1/000001.sst +000002 -> remote://a629-1-000002.sst + +remove 1 +---- + remove: p1/000001.sst + +remove 2 +---- + delete object "a629-1-000002.sst.ref.1.000002" + list (prefix="a629-1-000002.sst.ref.", delimiter="") + delete object "a629-1-000002.sst" + +link-or-copy 3 local 3 100 +---- + create: temp-file-1 + close: temp-file-1 + link: temp-file-1 -> p1/000003.sst + +read 3 +0 100 +---- + open: p1/000003.sst +size: 100 + read-at(0, 100): p1/000003.sst +0 100: ok (salt 3) + close: p1/000003.sst + +link-or-copy 4 shared 4 100 +---- + create: temp-file-2 + close: temp-file-2 + create object "2f2f-1-000004.sst" + open: temp-file-2 + close writer for "2f2f-1-000004.sst" after 100 bytes + create object "2f2f-1-000004.sst.ref.1.000004" + close writer for "2f2f-1-000004.sst.ref.1.000004" after 0 bytes + close: temp-file-2 + +read 4 +0 100 +---- + size of object "2f2f-1-000004.sst.ref.1.000004": 0 + create reader for object "2f2f-1-000004.sst": 100 bytes +size: 100 + read object "2f2f-1-000004.sst" at 0 (length 100) +0 100: ok (salt 4) + close reader for "2f2f-1-000004.sst" + +close +---- + sync: p1 + create: p1/REMOTE-OBJ-CATALOG-000002 + sync: p1/REMOTE-OBJ-CATALOG-000002 + create: p1/marker.remote-obj-catalog.000002.REMOTE-OBJ-CATALOG-000002 + close: p1/marker.remote-obj-catalog.000002.REMOTE-OBJ-CATALOG-000002 + remove: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p1 + remove: p1/REMOTE-OBJ-CATALOG-000001 + sync: p1/REMOTE-OBJ-CATALOG-000002 + close: p1/REMOTE-OBJ-CATALOG-000002 + close: p1 diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/shared_no_ref b/pebble/objstorage/objstorageprovider/testdata/provider/shared_no_ref new file mode 100644 index 0000000..6c16f9a --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/shared_no_ref @@ -0,0 +1,178 @@ +# Tests with shared storage when ref tracking is disabled. + +# open +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + create: p1/REMOTE-OBJ-CATALOG-000001 + sync: p1/REMOTE-OBJ-CATALOG-000001 + create: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + +create 1 shared 1 100 no-ref-tracking +---- + create object "61a6-1-000001.sst" + close writer for "61a6-1-000001.sst" after 100 bytes + +read 1 +0 100 +---- + create reader for object "61a6-1-000001.sst": 100 bytes +size: 100 + read object "61a6-1-000001.sst" at 0 (length 100) +0 100: ok (salt 1) + close reader for "61a6-1-000001.sst" + +create 2 shared 2 100 no-ref-tracking +---- + create object "a629-1-000002.sst" + close writer for "a629-1-000002.sst" after 100 bytes + +read 2 +0 100 +---- + create reader for object "a629-1-000002.sst": 100 bytes +size: 100 + read object "a629-1-000002.sst" at 0 (length 100) +0 100: ok (salt 2) + close reader for "a629-1-000002.sst" + +list +---- +000001 -> remote://61a6-1-000001.sst +000002 -> remote://a629-1-000002.sst + +link-or-copy 3 shared 3 100 no-ref-tracking +---- + create: temp-file-1 + close: temp-file-1 + create object "eaac-1-000003.sst" + open: temp-file-1 + close writer for "eaac-1-000003.sst" after 100 bytes + close: temp-file-1 + +read 3 +0 100 +---- + create reader for object "eaac-1-000003.sst": 100 bytes +size: 100 + read object "eaac-1-000003.sst" at 0 (length 100) +0 100: ok (salt 3) + close reader for "eaac-1-000003.sst" + +close +---- + sync: p1/REMOTE-OBJ-CATALOG-000001 + close: p1/REMOTE-OBJ-CATALOG-000001 + close: p1 + +# Test that the objects are there on re-open. +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + open: p1/REMOTE-OBJ-CATALOG-000001 + close: p1/REMOTE-OBJ-CATALOG-000001 + +list +---- +000001 -> remote://61a6-1-000001.sst +000002 -> remote://a629-1-000002.sst +000003 -> remote://eaac-1-000003.sst + +read 1 +0 100 +---- + create reader for object "61a6-1-000001.sst": 100 bytes +size: 100 + read object "61a6-1-000001.sst" at 0 (length 100) +0 100: ok (salt 1) + close reader for "61a6-1-000001.sst" + +read 2 +0 100 +---- + create reader for object "a629-1-000002.sst": 100 bytes +size: 100 + read object "a629-1-000002.sst" at 0 (length 100) +0 100: ok (salt 2) + close reader for "a629-1-000002.sst" + +read 3 +0 100 +---- + create reader for object "eaac-1-000003.sst": 100 bytes +size: 100 + read object "eaac-1-000003.sst" at 0 (length 100) +0 100: ok (salt 3) + close reader for "eaac-1-000003.sst" + +save-backing b1 1 +---- + +save-backing b2 1 +---- + +open p2 2 +---- + mkdir-all: p2 0755 + open-dir: p2 + open-dir: p2 + create: p2/REMOTE-OBJ-CATALOG-000001 + sync: p2/REMOTE-OBJ-CATALOG-000001 + create: p2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p2 + sync: p2/REMOTE-OBJ-CATALOG-000001 + +attach +b1 101 +b2 102 +---- + sync: p2/REMOTE-OBJ-CATALOG-000001 +000101 -> remote://61a6-1-000001.sst +000102 -> remote://61a6-1-000001.sst + +list +---- +000101 -> remote://61a6-1-000001.sst +000102 -> remote://61a6-1-000001.sst + +read 101 +0 100 +---- + create reader for object "61a6-1-000001.sst": 100 bytes +size: 100 + read object "61a6-1-000001.sst" at 0 (length 100) +0 100: ok (salt 1) + close reader for "61a6-1-000001.sst" + +read 102 +0 100 +---- + create reader for object "61a6-1-000001.sst": 100 bytes +size: 100 + read object "61a6-1-000001.sst" at 0 (length 100) +0 100: ok (salt 1) + close reader for "61a6-1-000001.sst" + +# In this mode, all removes should be no-ops on the shared backend. +remove 101 +---- + +remove 102 +---- + +switch p1 +---- + +remove 1 +---- + +remove 2 +---- diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/shared_readahead b/pebble/objstorage/objstorageprovider/testdata/provider/shared_readahead new file mode 100644 index 0000000..45e1c49 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/shared_readahead @@ -0,0 +1,87 @@ +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + create: p1/REMOTE-OBJ-CATALOG-000001 + sync: p1/REMOTE-OBJ-CATALOG-000001 + create: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + +create 1 shared 1 2000000 +---- + create object "61a6-1-000001.sst" + close writer for "61a6-1-000001.sst" after 2000000 bytes + create object "61a6-1-000001.sst.ref.1.000001" + close writer for "61a6-1-000001.sst.ref.1.000001" after 0 bytes + +# We should be seeing larger and larger reads. But the last read should be +# capped to the object size. +read 1 +0 1000 +1000 15000 +16000 30000 +46000 10000 +56000 50000 +106000 30000 +150000 20000 +180000 10000 +210000 30000 +300000 10000 +500000 1000 +800000 1000 +1500000 10000 +---- + size of object "61a6-1-000001.sst.ref.1.000001": 0 + create reader for object "61a6-1-000001.sst": 2000000 bytes +size: 2000000 + read object "61a6-1-000001.sst" at 0 (length 1000) +0 1000: ok (salt 1) + read object "61a6-1-000001.sst" at 1000 (length 15000) +1000 15000: ok (salt 1) + read object "61a6-1-000001.sst" at 16000 (length 65536) +16000 30000: ok (salt 1) +46000 10000: ok (salt 1) + read object "61a6-1-000001.sst" at 81536 (length 105536) +56000 50000: ok (salt 1) +106000 30000: ok (salt 1) +150000 20000: ok (salt 1) + read object "61a6-1-000001.sst" at 187072 (length 255072) +180000 10000: ok (salt 1) +210000 30000: ok (salt 1) +300000 10000: ok (salt 1) + read object "61a6-1-000001.sst" at 500000 (length 524288) +500000 1000: ok (salt 1) +800000 1000: ok (salt 1) + read object "61a6-1-000001.sst" at 1500000 (length 500000) +1500000 10000: ok (salt 1) + close reader for "61a6-1-000001.sst" + +# When reading for a compaction, we should be doing large reads from the start. +read 1 for-compaction +0 1000 +1000 15000 +16000 30000 +46000 10000 +56000 50000 +106000 30000 +150000 20000 +180000 10000 +210000 30000 +---- + size of object "61a6-1-000001.sst.ref.1.000001": 0 + create reader for object "61a6-1-000001.sst": 2000000 bytes +size: 2000000 + read object "61a6-1-000001.sst" at 0 (length 1048576) +0 1000: ok (salt 1) +1000 15000: ok (salt 1) +16000 30000: ok (salt 1) +46000 10000: ok (salt 1) +56000 50000: ok (salt 1) +106000 30000: ok (salt 1) +150000 20000: ok (salt 1) +180000 10000: ok (salt 1) +210000 30000: ok (salt 1) + close reader for "61a6-1-000001.sst" diff --git a/pebble/objstorage/objstorageprovider/testdata/provider/shared_remove b/pebble/objstorage/objstorageprovider/testdata/provider/shared_remove new file mode 100644 index 0000000..f9c52a1 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/provider/shared_remove @@ -0,0 +1,105 @@ +open p1 1 +---- + mkdir-all: p1 0755 + open-dir: p1 + open-dir: p1 + create: p1/REMOTE-OBJ-CATALOG-000001 + sync: p1/REMOTE-OBJ-CATALOG-000001 + create: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p1 + sync: p1/REMOTE-OBJ-CATALOG-000001 + +create 1 shared 1 100 +---- + create object "61a6-1-000001.sst" + close writer for "61a6-1-000001.sst" after 100 bytes + create object "61a6-1-000001.sst.ref.1.000001" + close writer for "61a6-1-000001.sst.ref.1.000001" after 0 bytes + +create 2 shared 2 100 +---- + create object "a629-1-000002.sst" + close writer for "a629-1-000002.sst" after 100 bytes + create object "a629-1-000002.sst.ref.1.000002" + close writer for "a629-1-000002.sst.ref.1.000002" after 0 bytes + +create 3 shared 3 100 +---- + create object "eaac-1-000003.sst" + close writer for "eaac-1-000003.sst" after 100 bytes + create object "eaac-1-000003.sst.ref.1.000003" + close writer for "eaac-1-000003.sst.ref.1.000003" after 0 bytes + +save-backing b1 1 +---- + +save-backing b2 2 +---- + +open p2 2 +---- + mkdir-all: p2 0755 + open-dir: p2 + open-dir: p2 + create: p2/REMOTE-OBJ-CATALOG-000001 + sync: p2/REMOTE-OBJ-CATALOG-000001 + create: p2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + close: p2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001 + sync: p2 + sync: p2/REMOTE-OBJ-CATALOG-000001 + +create 4 shared 4 100 +---- + create object "4c52-2-000004.sst" + close writer for "4c52-2-000004.sst" after 100 bytes + create object "4c52-2-000004.sst.ref.2.000004" + close writer for "4c52-2-000004.sst.ref.2.000004" after 0 bytes + +attach +b1 101 +b2 102 +---- + create object "61a6-1-000001.sst.ref.2.000101" + close writer for "61a6-1-000001.sst.ref.2.000101" after 0 bytes + size of object "61a6-1-000001.sst.ref.1.000001": 0 + create object "a629-1-000002.sst.ref.2.000102" + close writer for "a629-1-000002.sst.ref.2.000102" after 0 bytes + size of object "a629-1-000002.sst.ref.1.000002": 0 + sync: p2/REMOTE-OBJ-CATALOG-000001 +000101 -> remote://61a6-1-000001.sst +000102 -> remote://a629-1-000002.sst + +# Remove of object with no other refs; backing object should be removed. +remove 4 +---- + delete object "4c52-2-000004.sst.ref.2.000004" + list (prefix="4c52-2-000004.sst.ref.", delimiter="") + delete object "4c52-2-000004.sst" + +# Object shared with p2; backing object should not be removed. +remove 101 +---- + delete object "61a6-1-000001.sst.ref.2.000101" + list (prefix="61a6-1-000001.sst.ref.", delimiter="") + - 61a6-1-000001.sst.ref.1.000001 + +switch p1 +---- + +# Object no longer shared with p1; backing object should be removed. +remove 1 +---- + +# Object shared with p1; backing object should not be removed. +remove 2 +---- + +switch p2 +---- + +remove 102 +---- + delete object "a629-1-000002.sst.ref.2.000102" + list (prefix="a629-1-000002.sst.ref.", delimiter="") + - a629-1-000002.sst.ref.1.000002 diff --git a/pebble/objstorage/objstorageprovider/testdata/readahead b/pebble/objstorage/objstorageprovider/testdata/readahead new file mode 100644 index 0000000..d2bb217 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/testdata/readahead @@ -0,0 +1,221 @@ +reset +---- + +read +2048, 16 +---- +readahead: 0 +numReads: 1 +size: 65536 +prevSize: 0 +limit: 0 + +read +2096, 16 +---- +readahead: 0 +numReads: 2 +size: 65536 +prevSize: 0 +limit: 0 + +read +2112, 16 +---- +readahead: 65536 +numReads: 3 +size: 131072 +prevSize: 65536 +limit: 67648 + +read +8000, 16 +---- +readahead: 0 +numReads: 4 +size: 131072 +prevSize: 65536 +limit: 67648 + +read +8016, 16 +---- +readahead: 0 +numReads: 5 +size: 131072 +prevSize: 65536 +limit: 67648 + +# The new limit is 2112 + 65536 = 67648. +# Since the next read will end at 67646 + 1 = 67647, +# it doesn't yet trigger a readahead. + +read +67646, 1 +---- +readahead: 0 +numReads: 6 +size: 131072 +prevSize: 65536 +limit: 67648 + +read +67646, 2 +---- +readahead: 131072 +numReads: 7 +size: 262144 +prevSize: 131072 +limit: 198718 + +read +16192, 16 +---- +readahead: 0 +numReads: 1 +size: 65536 +prevSize: 0 +limit: 16208 + +read +16193, 16 +---- +readahead: 0 +numReads: 2 +size: 65536 +prevSize: 0 +limit: 16208 + +# The next read is too far ahead to benefit from readahead +# (i.e. 540497 > 16208 (limit) + (512 << 10) (maxReadaheadSize)) +# numReads should get reset to 1. + +read +540497, 16 +---- +readahead: 0 +numReads: 1 +size: 65536 +prevSize: 0 +limit: 540513 + +read +7980, 16 +---- +readahead: 0 +numReads: 1 +size: 65536 +prevSize: 0 +limit: 7996 + +read +0, 16 +---- +readahead: 0 +numReads: 1 +size: 65536 +prevSize: 0 +limit: 16 + +# Sizes should start from initial (64kb) again. + +read +7780, 16 +---- +readahead: 0 +numReads: 2 +size: 65536 +prevSize: 0 +limit: 16 + +read +7680, 16 +---- +readahead: 65536 +numReads: 3 +size: 131072 +prevSize: 65536 +limit: 73216 + +read +7780, 16 +--- +readahead: 0 +numReads: 4 +size: 131072 +prevSize: 65536 +limit: 73216 + +read +7880, 16 +---- +expected 2 args: offset, size + +read +7980, 16 +---- +readahead: 0 +numReads: 4 +size: 131072 +prevSize: 65536 +limit: 73216 + +read +73416, 16 +---- +readahead: 131072 +numReads: 5 +size: 262144 +prevSize: 131072 +limit: 204488 + +read +204488, 16 +---- +readahead: 262144 +numReads: 6 +size: 262144 +prevSize: 262144 +limit: 466632 + +# The readahead size should not increase beyond the max (256kb) + +read +466632, 16 +---- +readahead: 262144 +numReads: 7 +size: 262144 +prevSize: 262144 +limit: 728776 + +# A cache read pushes the limit further ahead without issuing a readahead. + +cache-read +728770, 16 +---- +readahead: 0 +numReads: 7 +size: 262144 +prevSize: 262144 +limit: 728786 + +read +728780, 16 +---- +readahead: 262144 +numReads: 8 +size: 262144 +prevSize: 262144 +limit: 990924 + +# An out-of-order cache read still resets readahead state. + +cache-read +1200, 16 +---- +readahead: 0 +numReads: 1 +size: 65536 +prevSize: 0 +limit: 1216 diff --git a/pebble/objstorage/objstorageprovider/vfs.go b/pebble/objstorage/objstorageprovider/vfs.go new file mode 100644 index 0000000..5b20251 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/vfs.go @@ -0,0 +1,109 @@ +// 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" + + "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.DiskFileNum) string { + return base.MakeFilepath(p.st.FS, p.st.FSDirName, fileType, fileNum) +} + +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 opts.MustExist { + base.MustExist(p.st.FS, filename, p.st.Logger, err) + } + return nil, err + } + return newFileReadable(file, p.st.FS, filename) +} + +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, objstorage.ObjectMetadata{}, err + } + file = vfs.NewSyncingFile(file, vfs.SyncingFileOptions{ + NoSyncOnClose: p.st.NoSyncOnClose, + BytesPerSync: p.st.BytesPerSync, + }) + meta := objstorage.ObjectMetadata{ + DiskFileNum: fileNum, + FileType: fileType, + } + return newFileBufferedWritable(file), meta, nil +} + +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 { + listing := p.st.FSDirInitialListing + if listing == nil { + var err error + listing, err = p.st.FS.List(p.st.FSDirName) + if err != nil { + return errors.Wrapf(err, "pebble: could not list store directory") + } + } + + for _, filename := range listing { + fileType, fileNum, ok := base.ParseFilename(p.st.FS, filename) + if ok && fileType == base.FileTypeTable { + o := objstorage.ObjectMetadata{ + FileType: fileType, + DiskFileNum: fileNum, + } + p.mu.knownObjects[o.DiskFileNum] = o + } + } + return nil +} + +func (p *provider) vfsSync() error { + p.mu.Lock() + shouldSync := p.mu.localObjectsChanged + p.mu.localObjectsChanged = false + p.mu.Unlock() + + if !shouldSync { + return nil + } + if err := p.fsDir.Sync(); err != nil { + p.mu.Lock() + defer p.mu.Unlock() + p.mu.localObjectsChanged = true + return err + } + return nil +} + +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 { + return 0, err + } + return stat.Size(), nil +} diff --git a/pebble/objstorage/objstorageprovider/vfs_readable.go b/pebble/objstorage/objstorageprovider/vfs_readable.go new file mode 100644 index 0000000..9362464 --- /dev/null +++ b/pebble/objstorage/objstorageprovider/vfs_readable.go @@ -0,0 +1,216 @@ +// 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" + "os" + "sync" + + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/vfs" +) + +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 + + // The following fields are used to possibly open the file again using the + // sequential reads option (see vfsReadHandle). + filename string + fs vfs.FS +} + +var _ objstorage.Readable = (*fileReadable)(nil) + +func newFileReadable(file vfs.File, fs vfs.FS, filename string) (*fileReadable, error) { + info, err := file.Stat() + if err != nil { + return nil, err + } + r := &fileReadable{ + file: file, + size: info.Size(), + filename: filename, + fs: fs, + } + invariants.SetFinalizer(r, func(obj interface{}) { + if obj.(*fileReadable).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 *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. +func (r *fileReadable) Close() error { + defer func() { r.file = nil }() + return r.file.Close() +} + +// Size is part of the objstorage.Readable interface. +func (r *fileReadable) Size() int64 { + return r.size +} + +// NewReadHandle is part of the objstorage.Readable interface. +func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle { + rh := readHandlePool.Get().(*vfsReadHandle) + rh.r = r + rh.rs = makeReadaheadState(fileMaxReadaheadSize) + return rh +} + +type vfsReadHandle struct { + r *fileReadable + rs readaheadState + + // sequentialFile holds a file descriptor to the same underlying File, + // except with fadvise(FADV_SEQUENTIAL) called on it to take advantage of + // OS-level readahead. Once this is non-nil, the other variables in + // readaheadState don't matter much as we defer to OS-level readahead. + sequentialFile vfs.File +} + +var _ objstorage.ReadHandle = (*vfsReadHandle)(nil) + +var readHandlePool = sync.Pool{ + New: func() interface{} { + i := &vfsReadHandle{} + // Note: this is a no-op if invariants are disabled or race is enabled. + invariants.SetFinalizer(i, func(obj interface{}) { + if obj.(*vfsReadHandle).r != nil { + fmt.Fprintf(os.Stderr, "ReadHandle was not closed") + os.Exit(1) + } + }) + return i + }, +} + +// Close is part of the objstorage.ReadHandle interface. +func (rh *vfsReadHandle) Close() error { + var err error + if rh.sequentialFile != nil { + err = rh.sequentialFile.Close() + } + *rh = vfsReadHandle{} + readHandlePool.Put(rh) + return err +} + +// ReadAt is part of the objstorage.ReadHandle interface. +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. + 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) + } + if invariants.Enabled && err == nil && n != len(p) { + panic("short read") + } + return err +} + +// SetupForCompaction is part of the objstorage.ReadHandle interface. +func (rh *vfsReadHandle) SetupForCompaction() { + rh.switchToOSReadahead() +} + +func (rh *vfsReadHandle) switchToOSReadahead() { + if rh.sequentialFile != nil { + return + } + + // TODO(radu): we could share the reopened file descriptor across multiple + // handles. + f, err := rh.r.fs.Open(rh.r.filename, vfs.SequentialReadsOption) + if err == nil { + rh.sequentialFile = f + } +} + +// RecordCacheHit is part of the objstorage.ReadHandle interface. +func (rh *vfsReadHandle) RecordCacheHit(_ context.Context, offset, size int64) { + if rh.sequentialFile != nil { + // Using OS-level readahead, so do nothing. + return + } + rh.rs.recordCacheHit(offset, size) +} + +// TestingCheckMaxReadahead returns true if the ReadHandle has switched to +// OS-level read-ahead. +func TestingCheckMaxReadahead(rh objstorage.ReadHandle) bool { + switch rh := rh.(type) { + case *vfsReadHandle: + return rh.sequentialFile != nil + case *PreallocatedReadHandle: + return rh.sequentialFile != nil + default: + panic("unknown ReadHandle type") + } +} + +// PreallocatedReadHandle is used to avoid an allocation in NewReadHandle; see +// UsePreallocatedReadHandle. +type PreallocatedReadHandle struct { + vfsReadHandle +} + +// Close is part of the objstorage.ReadHandle interface. +func (rh *PreallocatedReadHandle) Close() error { + var err error + if rh.sequentialFile != nil { + err = rh.sequentialFile.Close() + } + rh.vfsReadHandle = vfsReadHandle{} + return err +} + +// UsePreallocatedReadHandle is equivalent to calling readable.NewReadHandle() +// 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( + 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(ctx) +} diff --git a/pebble/objstorage/objstorageprovider/vfs_writable.go b/pebble/objstorage/objstorageprovider/vfs_writable.go new file mode 100644 index 0000000..c9b207c --- /dev/null +++ b/pebble/objstorage/objstorageprovider/vfs_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 ( + "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) objstorage.Writable { + return newFileBufferedWritable(file) +} + +type fileBufferedWritable struct { + file vfs.File + bw *bufio.Writer +} + +var _ objstorage.Writable = (*fileBufferedWritable)(nil) + +func newFileBufferedWritable(file vfs.File) *fileBufferedWritable { + return &fileBufferedWritable{ + file: file, + bw: bufio.NewWriter(file), + } +} + +// Write is part of the objstorage.Writable interface. +func (w *fileBufferedWritable) Write(p []byte) error { + // Ignoring the length written since bufio.Writer.Write is guaranteed to + // return an error if the length written is < len(p). + _, err := w.bw.Write(p) + return err +} + +// Finish is part of the objstorage.Writable interface. +func (w *fileBufferedWritable) Finish() error { + err := w.bw.Flush() + if err == nil { + err = w.file.Sync() + } + err = firstError(err, w.file.Close()) + w.bw = nil + w.file = nil + return err +} + +// Abort is part of the objstorage.Writable interface. +func (w *fileBufferedWritable) Abort() { + _ = w.file.Close() + w.bw = nil + w.file = nil +} + +func firstError(err0, err1 error) error { + if err0 != nil { + return err0 + } + return err1 +} diff --git a/pebble/objstorage/remote/factory.go b/pebble/objstorage/remote/factory.go new file mode 100644 index 0000000..620dbba --- /dev/null +++ b/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/pebble/objstorage/remote/localfs.go b/pebble/objstorage/remote/localfs.go new file mode 100644 index 0000000..539ecb1 --- /dev/null +++ b/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/pebble/objstorage/remote/logging.go b/pebble/objstorage/remote/logging.go new file mode 100644 index 0000000..7a9cd77 --- /dev/null +++ b/pebble/objstorage/remote/logging.go @@ -0,0 +1,139 @@ +// 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" + "fmt" + "io" + "sort" +) + +// WithLogging wraps the given Storage implementation and emits logs for various +// operations. +func WithLogging(wrapped Storage, logf func(fmt string, args ...interface{})) Storage { + return &loggingStore{ + logf: logf, + wrapped: wrapped, + } +} + +// loggingStore wraps a remote.Storage implementation and emits logs of the +// operations. +type loggingStore struct { + logf func(fmt string, args ...interface{}) + wrapped Storage +} + +var _ Storage = (*loggingStore)(nil) + +func (l *loggingStore) Close() error { + l.logf("close") + return l.wrapped.Close() +} + +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: objName, + wrapped: r, + }, size, nil +} + +type loggingReader struct { + 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.wrapped.Close() +} + +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: objName, + WriteCloser: writer, + }, nil +} + +type loggingWriter struct { + l *loggingStore + name string + bytesWritten int64 + io.WriteCloser +} + +func (l *loggingWriter) Write(p []byte) (n int, err error) { + n, err = l.WriteCloser.Write(p) + l.bytesWritten += int64(n) + return n, err +} + +func (l *loggingWriter) Close() error { + l.l.logf("close writer for %q after %d bytes", l.name, l.bytesWritten) + return l.WriteCloser.Close() +} + +func (l *loggingStore) List(prefix, delimiter string) ([]string, error) { + l.logf("list (prefix=%q, delimiter=%q)", prefix, delimiter) + res, err := l.wrapped.List(prefix, delimiter) + if err != nil { + return nil, err + } + sorted := append([]string(nil), res...) + sort.Strings(sorted) + for _, s := range sorted { + l.logf(" - %s", s) + } + return res, nil +} + +func (l *loggingStore) Delete(objName string) error { + l.logf("delete object %q", objName) + return l.wrapped.Delete(objName) +} + +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 +} + +func errOrPrintf(err error, format string, args ...interface{}) string { + if err != nil { + return fmt.Sprintf("error: %v", err) + } + return fmt.Sprintf(format, args...) +} + +func (l *loggingStore) IsNotExistError(err error) bool { + return l.wrapped.IsNotExistError(err) +} diff --git a/pebble/objstorage/remote/mem.go b/pebble/objstorage/remote/mem.go new file mode 100644 index 0000000..9bbda71 --- /dev/null +++ b/pebble/objstorage/remote/mem.go @@ -0,0 +1,161 @@ +// 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 ( + "bytes" + "context" + "io" + "os" + "strings" + "sync" +) + +// NewInMem returns an in-memory implementation of the remote.Storage +// interface (for testing). +func NewInMem() Storage { + store := &inMemStore{} + store.mu.objects = make(map[string]*inMemObj) + return store +} + +// inMemStore is an in-memory implementation of the remote.Storage interface +// (for testing). +type inMemStore struct { + mu struct { + sync.Mutex + objects map[string]*inMemObj + } +} + +var _ Storage = (*inMemStore)(nil) + +type inMemObj struct { + name string + data []byte +} + +func (s *inMemStore) Close() error { + *s = inMemStore{} + return nil +} + +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 + } + return &inMemReader{data: obj.data}, int64(len(obj.data)), nil +} + +type inMemReader struct { + data []byte +} + +var _ ObjectReader = (*inMemReader)(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 (r *inMemReader) Close() error { + r.data = nil + return nil +} + +func (s *inMemStore) CreateObject(objName string) (io.WriteCloser, error) { + return &inMemWriter{ + store: s, + name: objName, + }, nil +} + +type inMemWriter struct { + store *inMemStore + name string + buf bytes.Buffer +} + +var _ io.WriteCloser = (*inMemWriter)(nil) + +func (o *inMemWriter) Write(p []byte) (n int, err error) { + if o.store == nil { + panic("Write after Close") + } + return o.buf.Write(p) +} + +func (o *inMemWriter) Close() error { + if o.store != nil { + o.store.addObj(&inMemObj{ + name: o.name, + data: o.buf.Bytes(), + }) + o.store = nil + } + return nil +} + +func (s *inMemStore) List(prefix, delimiter string) ([]string, error) { + if delimiter != "" { + panic("delimiter unimplemented") + } + + s.mu.Lock() + defer s.mu.Unlock() + res := make([]string, 0, len(s.mu.objects)) + for name := range s.mu.objects { + if strings.HasPrefix(name, prefix) { + res = append(res, name) + } + } + return res, nil +} + +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(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() + obj, ok := s.mu.objects[name] + if !ok { + return nil, os.ErrNotExist + } + return obj, nil +} + +func (s *inMemStore) addObj(o *inMemObj) { + s.mu.Lock() + defer s.mu.Unlock() + s.mu.objects[o.name] = o +} + +func (s *inMemStore) rmObj(name string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.mu.objects, name) +} diff --git a/pebble/objstorage/remote/storage.go b/pebble/objstorage/remote/storage.go new file mode 100644 index 0000000..8918764 --- /dev/null +++ b/pebble/objstorage/remote/storage.go @@ -0,0 +1,133 @@ +// 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" + + "github.com/cockroachdb/redact" +) + +// 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 + +// SafeFormat implements redact.SafeFormatter. +func (l Locator) SafeFormat(w redact.SafePrinter, _ rune) { + w.Printf("%s", redact.SafeString(l)) +} + +// 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/pebble/open.go b/pebble/open.go new file mode 100644 index 0000000..9183ee1 --- /dev/null +++ b/pebble/open.go @@ -0,0 +1,1191 @@ +// Copyright 2012 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 ( + "bytes" + "context" + "encoding/binary" + "fmt" + "io" + "math" + "os" + "sync/atomic" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "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/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + initialMemTableSize = 256 << 10 // 256 KB + + // The max batch size is limited by the uint32 offsets stored in + // internal/batchskl.node, DeferredBatchOp, and flushableBatchEntry. + // + // 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. + // + // 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 +// cache size for a single db, given the maximum open +// files which can be used by a table cache which is +// only used by a single db. +func TableCacheSize(maxOpenFiles int) int { + tableCacheSize := maxOpenFiles - numNonTableCacheFiles + if tableCacheSize < minTableCacheSize { + tableCacheSize = minTableCacheSize + } + return tableCacheSize +} + +// Open opens a DB whose files live in the given directory. +func Open(dirname string, opts *Options) (db *DB, _ error) { + // Make a copy of the options so that we don't mutate the passed in options. + opts = opts.Clone() + opts = opts.EnsureDefaults() + if err := opts.Validate(); err != nil { + return nil, err + } + if opts.LoggerAndTracer == nil { + opts.LoggerAndTracer = &base.LoggerWithNoopTracer{Logger: opts.Logger} + } else { + opts.Logger = opts.LoggerAndTracer + } + + // In all error cases, we return db = nil; this is used by various + // deferred cleanups. + + // Open the database and WAL directories first. + walDirname, dataDir, walDir, err := prepareAndOpenDirs(dirname, opts) + if err != nil { + return nil, errors.Wrapf(err, "error opening database at %q", dirname) + } + defer func() { + if db == nil { + if walDir != dataDir { + walDir.Close() + } + dataDir.Close() + } + }() + + // Lock the database directory. + var fileLock *Lock + if opts.Lock != nil { + // The caller already acquired the database lock. Ensure that the + // directory matches. + if dirname != opts.Lock.dirname { + return nil, errors.Newf("pebble: opts.Lock acquired in %q not %q", opts.Lock.dirname, dirname) + } + if err := opts.Lock.refForOpen(); err != nil { + return nil, err + } + fileLock = opts.Lock + } else { + fileLock, err = LockDirectory(dirname, opts.FS) + if err != nil { + return nil, err + } + } + defer func() { + if db == nil { + fileLock.Close() + } + }() + + // Establish the format major version. + formatVersion, formatVersionMarker, err := lookupFormatMajorVersion(opts.FS, dirname) + if err != nil { + return nil, err + } + defer func() { + if db == nil { + formatVersionMarker.Close() + } + }() + + // Find the currently active manifest, if there is one. + manifestMarker, manifestFileNum, manifestExists, err := findCurrentManifest(formatVersion, opts.FS, dirname) + if err != nil { + return nil, errors.Wrapf(err, "pebble: database %q", dirname) + } + defer func() { + if db == nil { + manifestMarker.Close() + } + }() + + // Atomic markers may leave behind obsolete files if there's a crash + // mid-update. Clean these up if we're not in read-only mode. + if !opts.ReadOnly { + if err := formatVersionMarker.RemoveObsolete(); err != nil { + return nil, err + } + if err := manifestMarker.RemoveObsolete(); err != nil { + return nil, err + } + } + + if opts.Cache == nil { + opts.Cache = cache.New(cacheDefaultSize) + } else { + opts.Cache.Ref() + } + + d := &DB{ + cacheID: opts.Cache.NewID(), + dirname: dirname, + walDirname: walDirname, + opts: opts, + cmp: opts.Comparer.Compare, + equal: opts.equal(), + merge: opts.Merger.Merge, + split: opts.Comparer.Split, + abbreviatedKey: opts.Comparer.AbbreviatedKey, + largeBatchThreshold: (opts.MemTableSize - uint64(memTableEmptySize)) / 2, + fileLock: fileLock, + dataDir: dataDir, + walDir: walDir, + logRecycler: logRecycler{limit: opts.MemTableStopWritesThreshold + 1}, + closed: new(atomic.Value), + closedCh: make(chan struct{}), + } + d.mu.versions = &versionSet{} + d.diskAvailBytes.Store(math.MaxUint64) + + defer func() { + // If an error or panic occurs during open, attempt to release the manually + // allocated memory resources. Note that rather than look for an error, we + // look for the return of a nil DB pointer. + if r := recover(); db == nil { + // Release our references to the Cache. Note that both the DB, and + // tableCache have a reference. When we release the reference to + // the tableCache, and if there are no other references to + // the tableCache, then the tableCache will also release its + // reference to the cache. + opts.Cache.Unref() + + if d.tableCache != nil { + _ = d.tableCache.close() + } + + for _, mem := range d.mu.mem.queue { + switch t := mem.flushable.(type) { + case *memTable: + manual.Free(t.arenaBuf) + t.arenaBuf = nil + } + } + if d.cleanupManager != nil { + d.cleanupManager.Close() + } + if d.objProvider != nil { + d.objProvider.Close() + } + if r != nil { + panic(r) + } + } + }() + + d.commit = newCommitPipeline(commitEnv{ + logSeqNum: &d.mu.versions.logSeqNum, + visibleSeqNum: &d.mu.versions.visibleSeqNum, + apply: d.commitApply, + write: d.commitWrite, + }) + d.mu.nextJobID = 1 + d.mu.mem.nextSize = opts.MemTableSize + if d.mu.mem.nextSize > initialMemTableSize { + d.mu.mem.nextSize = initialMemTableSize + } + 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 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() + + jobID := d.mu.nextJobID + d.mu.nextJobID++ + + setCurrent := setCurrentFunc(d.FormatMajorVersion(), manifestMarker, opts.FS, dirname, d.dataDir) + + if !manifestExists { + // DB does not exist. + if d.opts.ErrorIfNotExists || d.opts.ReadOnly { + return nil, errors.Wrapf(ErrDBDoesNotExist, "dirname=%q", dirname) + } + + // Create the DB. + if err := d.mu.versions.create(jobID, dirname, opts, manifestMarker, setCurrent, d.FormatMajorVersion, &d.mu.Mutex); err != nil { + return nil, err + } + } else { + if opts.ErrorIfExists { + return nil, errors.Wrapf(ErrDBAlreadyExists, "dirname=%q", dirname) + } + // Load the version set. + if err := d.mu.versions.load(dirname, opts, manifestFileNum, manifestMarker, setCurrent, d.FormatMajorVersion, &d.mu.Mutex); err != nil { + return nil, err + } + if opts.ErrorIfNotPristine { + liveFileNums := make(map[base.DiskFileNum]struct{}) + d.mu.versions.addLiveFileNums(liveFileNums) + if len(liveFileNums) != 0 { + return nil, errors.Wrapf(ErrDBNotPristine, "dirname=%q", dirname) + } + } + } + + // In read-only mode, we replay directly into the mutable memtable but never + // flush it. We need to delay creation of the memtable until we know the + // 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.logSeqNum.Load()) + d.mu.mem.queue = append(d.mu.mem.queue, entry) + } + + // List the objects + ls, err := opts.FS.List(d.walDirname) + if err != nil { + return nil, err + } + if d.dirname != d.walDirname { + ls2, err := opts.FS.List(d.dirname) + if err != nil { + return nil, err + } + ls = append(ls, ls2...) + } + providerSettings := objstorageprovider.Settings{ + Logger: opts.Logger, + FS: opts.FS, + FSDirName: dirname, + FSDirInitialListing: ls, + FSCleaner: opts.Cleaner, + NoSyncOnClose: opts.NoSyncOnClose, + BytesPerSync: opts.BytesPerSync, + } + 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 = 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 { + return nil, err + } + } + + tableCacheSize := TableCacheSize(opts.MaxOpenFiles) + d.tableCache = newTableCacheContainer( + opts.TableCache, d.cacheID, d.objProvider, d.opts, tableCacheSize, + &sstable.CategoryStatsCollector{}) + d.newIters = d.tableCache.newIters + d.tableNewRangeKeyIter = d.tableCache.newRangeKeyIter + + // Replay any newer log files than the ones named in the manifest. + type fileNumAndName struct { + num base.DiskFileNum + name string + } + var logFiles []fileNumAndName + var previousOptionsFileNum FileNum + var previousOptionsFilename string + for _, filename := range ls { + ft, fn, ok := base.ParseFilename(opts.FS, filename) + if !ok { + continue + } + + // Don't reuse any obsolete file numbers to avoid modifying an + // ingested sstable's original external file. + if d.mu.versions.nextFileNum <= uint64(fn.FileNum()) { + d.mu.versions.nextFileNum = uint64(fn.FileNum()) + 1 + } + + switch ft { + case fileTypeLog: + if fn >= d.mu.versions.minUnflushedLogNum { + logFiles = append(logFiles, fileNumAndName{fn, filename}) + } + if d.logRecycler.minRecycleLogNum <= fn.FileNum() { + d.logRecycler.minRecycleLogNum = fn.FileNum() + 1 + } + case fileTypeOptions: + if previousOptionsFileNum < fn.FileNum() { + previousOptionsFileNum = fn.FileNum() + previousOptionsFilename = filename + } + case fileTypeTemp, fileTypeOldTemp: + if !d.opts.ReadOnly { + // Some codepaths write to a temporary file and then + // rename it to its final location when complete. A + // temp file is leftover if a process exits before the + // rename. Remove it. + err := opts.FS.Remove(opts.FS.PathJoin(dirname, filename)) + if err != nil { + return nil, err + } + } + } + } + + // 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 <= uint64(obj.DiskFileNum) { + d.mu.versions.nextFileNum = uint64(obj.DiskFileNum) + 1 + } + } + + // Validate the most-recent OPTIONS file, if there is one. + var strictWALTail bool + if previousOptionsFilename != "" { + path := opts.FS.PathJoin(dirname, previousOptionsFilename) + strictWALTail, err = checkOptions(opts, path) + if err != nil { + return nil, err + } + } + + slices.SortFunc(logFiles, func(a, b fileNumAndName) int { + return cmp.Compare(a.num, b.num) + }) + + var ve versionEdit + var toFlush flushableList + for i, lf := range logFiles { + lastWAL := i == len(logFiles)-1 + flush, maxSeqNum, err := d.replayWAL(jobID, &ve, opts.FS, + opts.FS.PathJoin(d.walDirname, lf.name), lf.num, strictWALTail && !lastWAL) + if err != nil { + return nil, err + } + toFlush = append(toFlush, flush...) + d.mu.versions.markFileNumUsed(lf.num) + if d.mu.versions.logSeqNum.Load() < maxSeqNum { + d.mu.versions.logSeqNum.Store(maxSeqNum) + } + } + d.mu.versions.visibleSeqNum.Store(d.mu.versions.logSeqNum.Load()) + + if !d.opts.ReadOnly { + // Create an empty .log file. + newLogNum := d.mu.versions.getNextDiskFileNum() + + // This logic is slightly different than RocksDB's. Specifically, RocksDB + // sets MinUnflushedLogNum to max-recovered-log-num + 1. We set it to the + // newLogNum. There should be no difference in using either value. + ve.MinUnflushedLogNum = newLogNum + + // Create the manifest with the updated MinUnflushedLogNum before + // creating the new log file. If we created the log file first, a + // crash before the manifest is synced could leave two WALs with + // unclean tails. + d.mu.versions.logLock() + if err := d.mu.versions.logAndApply(jobID, &ve, newFileMetrics(ve.NewFiles), false /* forceRotation */, func() []compactionInfo { + return nil + }); err != nil { + return nil, err + } + + for _, entry := range toFlush { + 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}) + logFile, err := opts.FS.Create(newLogName) + if err != nil { + return nil, err + } + if err := d.walDir.Sync(); err != nil { + return nil, err + } + d.opts.EventListener.WALCreated(WALCreateInfo{ + JobID: jobID, + Path: newLogName, + FileNum: newLogNum, + }) + // This isn't strictly necessary as we don't use the log number for + // memtables being flushed, only for the next unflushed memtable. + d.mu.mem.queue[len(d.mu.mem.queue)-1].logNum = newLogNum + + logFile = vfs.NewSyncingFile(logFile, vfs.SyncingFileOptions{ + NoSyncOnClose: d.opts.NoSyncOnClose, + BytesPerSync: d.opts.WALBytesPerSync, + PreallocateSize: d.walPreallocateSize(), + }) + d.mu.log.metrics.fsyncLatency = prometheus.NewHistogram(prometheus.HistogramOpts{ + Buckets: FsyncLatencyBuckets, + }) + + logWriterConfig := record.LogWriterConfig{ + WALMinSyncInterval: d.opts.WALMinSyncInterval, + WALFsyncLatency: d.mu.log.metrics.fsyncLatency, + QueueSemChan: d.commit.logSyncQSem, + } + d.mu.log.LogWriter = record.NewLogWriter(logFile, newLogNum, logWriterConfig) + d.mu.versions.metrics.WAL.Files++ + } + d.updateReadStateLocked(d.opts.DebugCheck) + + // If the Options specify a format major version higher than the + // loaded database's, upgrade it. If this is a new database, this + // code path also performs an initial upgrade from the starting + // implicit MostCompatible version. + // + // We ratchet the version this far into Open so that migrations have a read + // state available. + if !d.opts.ReadOnly && opts.FormatMajorVersion > d.FormatMajorVersion() { + if err := d.ratchetFormatMajorVersionLocked(opts.FormatMajorVersion); err != nil { + return nil, err + } + } + + if !d.opts.ReadOnly { + // Write the current options to disk. + d.optionsFileNum = d.mu.versions.getNextDiskFileNum() + tmpPath := base.MakeFilepath(opts.FS, dirname, fileTypeTemp, d.optionsFileNum) + optionsPath := base.MakeFilepath(opts.FS, dirname, fileTypeOptions, d.optionsFileNum) + + // Write them to a temporary file first, in case we crash before + // we're done. A corrupt options file prevents opening the + // database. + optionsFile, err := opts.FS.Create(tmpPath) + if err != nil { + return nil, err + } + serializedOpts := []byte(opts.String()) + if _, err := optionsFile.Write(serializedOpts); err != nil { + return nil, errors.CombineErrors(err, optionsFile.Close()) + } + d.optionsFileSize = uint64(len(serializedOpts)) + if err := optionsFile.Sync(); err != nil { + return nil, errors.CombineErrors(err, optionsFile.Close()) + } + if err := optionsFile.Close(); err != nil { + return nil, err + } + // Atomically rename to the OPTIONS-XXXXXX path. This rename is + // guaranteed to be atomic because the destination path does not + // exist. + if err := opts.FS.Rename(tmpPath, optionsPath); err != nil { + return nil, err + } + if err := d.dataDir.Sync(); err != nil { + return nil, err + } + } + + if !d.opts.ReadOnly { + d.scanObsoleteFiles(ls) + d.deleteObsoleteFiles(jobID) + } else { + // All the log files are obsolete. + d.mu.versions.metrics.WAL.Files = int64(len(logFiles)) + } + d.mu.tableStats.cond.L = &d.mu.Mutex + d.mu.tableValidation.cond.L = &d.mu.Mutex + if !d.opts.ReadOnly { + d.maybeCollectTableStatsLocked() + } + d.calculateDiskAvailableBytes() + + d.maybeScheduleFlush() + d.maybeScheduleCompaction() + + // Note: this is a no-op if invariants are disabled or race is enabled. + // + // Setting a finalizer on *DB causes *DB to never be reclaimed and the + // finalizer to never be run. The problem is due to this limitation of + // finalizers mention in the SetFinalizer docs: + // + // If a cyclic structure includes a block with a finalizer, that cycle is + // not guaranteed to be garbage collected and the finalizer is not + // guaranteed to run, because there is no ordering that respects the + // dependencies. + // + // DB has cycles with several of its internal structures: readState, + // newIters, tableCache, versions, etc. Each of this individually cause a + // cycle and prevent the finalizer from being run. But we can workaround this + // finializer limitation by setting a finalizer on another object that is + // tied to the lifetime of DB: the DB.closed atomic.Value. + dPtr := fmt.Sprintf("%p", d) + invariants.SetFinalizer(d.closed, func(obj interface{}) { + v := obj.(*atomic.Value) + if err := v.Load(); err == nil { + fmt.Fprintf(os.Stderr, "%s: unreferenced DB not closed\n", dPtr) + os.Exit(1) + } + }) + + return d, nil +} + +// prepareAndOpenDirs opens the directories for the store (and creates them if +// necessary). +// +// Returns an error if ReadOnly is set and the directories don't exist. +func prepareAndOpenDirs( + dirname string, opts *Options, +) (walDirname string, dataDir vfs.File, walDir vfs.File, err error) { + walDirname = opts.WALDir + if opts.WALDir == "" { + walDirname = dirname + } + + // Create directories if needed. + if !opts.ReadOnly { + if err := opts.FS.MkdirAll(dirname, 0755); err != nil { + return "", nil, nil, err + } + if walDirname != dirname { + if err := opts.FS.MkdirAll(walDirname, 0755); err != nil { + return "", nil, nil, err + } + } + } + + dataDir, err = opts.FS.OpenDir(dirname) + if err != nil { + if opts.ReadOnly && oserror.IsNotExist(err) { + return "", nil, nil, errors.Errorf("pebble: database %q does not exist", dirname) + } + return "", nil, nil, err + } + + if walDirname == dirname { + walDir = dataDir + } else { + walDir, err = opts.FS.OpenDir(walDirname) + if err != nil { + dataDir.Close() + return "", nil, nil, err + } + } + return walDirname, dataDir, walDir, nil +} + +// GetVersion returns the engine version string from the latest options +// file present in dir. Used to check what Pebble or RocksDB version was last +// used to write to the database stored in this directory. An empty string is +// returned if no valid OPTIONS file with a version key was found. +func GetVersion(dir string, fs vfs.FS) (string, error) { + ls, err := fs.List(dir) + if err != nil { + return "", err + } + var version string + lastOptionsSeen := FileNum(0) + for _, filename := range ls { + ft, fn, ok := base.ParseFilename(fs, filename) + if !ok { + continue + } + switch ft { + case fileTypeOptions: + // If this file has a higher number than the last options file + // processed, reset version. This is because rocksdb often + // writes multiple options files without deleting previous ones. + // Otherwise, skip parsing this options file. + if fn.FileNum() > lastOptionsSeen { + version = "" + lastOptionsSeen = fn.FileNum() + } else { + continue + } + f, err := fs.Open(fs.PathJoin(dir, filename)) + if err != nil { + return "", err + } + data, err := io.ReadAll(f) + f.Close() + + if err != nil { + return "", err + } + err = parseOptions(string(data), func(section, key, value string) error { + switch { + case section == "Version": + switch key { + case "pebble_version": + version = value + case "rocksdb_version": + version = fmt.Sprintf("rocksdb v%s", value) + } + } + return nil + }) + if err != nil { + return "", err + } + } + } + return version, nil +} + +// replayWAL replays the edits in the specified log file. If the DB is in +// read only mode, then the WALs are replayed into memtables and not flushed. If +// the DB is not in read only mode, then the contents of the WAL are guaranteed +// to be flushed. +// +// The toFlush return value is a list of flushables associated with the WAL +// being replayed which will be flushed. Once the version edit has been applied +// to the manifest, it is up to the caller of replayWAL to unreference the +// toFlush flushables returned by replayWAL. +// +// 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) replayWAL( + jobID int, + ve *versionEdit, + fs vfs.FS, + filename string, + logNum base.DiskFileNum, + strictWALTail bool, +) (toFlush flushableList, maxSeqNum uint64, err error) { + file, err := fs.Open(filename) + if err != nil { + return nil, 0, err + } + defer file.Close() + var ( + b Batch + buf bytes.Buffer + mem *memTable + entry *flushableEntry + 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. + mem = d.mu.mem.mutable + if mem != nil { + entry = d.mu.mem.queue[len(d.mu.mem.queue)-1] + } + } + + // Flushes the current memtable, if not nil. + flushMem := func() { + if mem == nil { + return + } + var logSize uint64 + if offset >= lastFlushOffset { + logSize = uint64(offset - lastFlushOffset) + } + // Else, this was the initial memtable in the read-only case which must have + // been empty, but we need to flush it since we don't want to add to it later. + lastFlushOffset = offset + entry.logSize = logSize + if !d.opts.ReadOnly { + toFlush = append(toFlush, entry) + } + mem, entry = nil, nil + } + // Creates a new memtable if there is no current memtable. + ensureMem := func(seqNum uint64) { + if mem != nil { + return + } + mem, entry = d.newMemTable(logNum, seqNum) + if d.opts.ReadOnly { + d.mu.mem.mutable = mem + d.mu.mem.queue = append(d.mu.mem.queue, entry) + } + } + + // updateVE is used to update ve with information about new files created + // during the flush of any flushable not of type ingestedFlushable. For the + // flushable of type ingestedFlushable we use custom handling below. + updateVE := func() error { + // 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, d.timeNow()) + newVE, _, _, err := d.runCompaction(jobID, c) + if err != nil { + return errors.Wrapf(err, "running compaction during WAL replay") + } + 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() + r, err := rr.Next() + if err == nil { + _, err = io.Copy(&buf, r) + } + if err != nil { + // It is common to encounter a zeroed or invalid chunk due to WAL + // preallocation and WAL recycling. We need to distinguish these + // errors from EOF in order to recognize that the record was + // truncated and to avoid replaying subsequent WALs, but want + // to otherwise treat them like EOF. + if err == io.EOF { + break + } else if record.IsInvalidRecord(err) && !strictWALTail { + break + } + return nil, 0, errors.Wrap(err, "pebble: error when replaying WAL") + } + + if buf.Len() < batchHeaderLen { + return nil, 0, base.CorruptionErrorf("pebble: corrupt log file %q (num %s)", + filename, errors.Safe(logNum)) + } + + if d.opts.ErrorIfNotPristine { + return nil, 0, errors.WithDetailf(ErrDBNotPristine, "location: %q", d.dirname) + } + + // Specify Batch.db so that Batch.SetRepr will compute Batch.memTableSize + // which is used below. + 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, _, 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).DiskFileNum()) + } + addFileNum(encodedFileNum) + + for i := 1; i < int(b.Count()); i++ { + kind, encodedFileNum, _, ok, err := br.Next() + if err != nil { + return nil, 0, err + } + if kind != InternalKeyKindIngestSST { + panic("pebble: invalid batch key kind.") + } + if !ok { + panic("pebble: invalid batch count.") + } + addFileNum(encodedFileNum) + } + + if _, _, _, ok, err := br.Next(); err != nil { + return nil, 0, err + } else if ok { + panic("pebble: invalid number of entries in batch.") + } + + meta := make([]*fileMetadata, len(fileNums)) + for i, n := range fileNums { + 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 + } + + 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() { + panic("pebble: couldn't load all files in WAL entry.") + } + + entry, err = d.newIngestedFlushableEntry( + meta, seqNum, logNum, + ) + if err != nil { + return nil, 0, err + } + + if d.opts.ReadOnly { + d.mu.mem.queue = append(d.mu.mem.queue, entry) + // We added the IngestSST flushable to the queue. But there + // must be at least one WAL entry waiting to be replayed. We + // have to ensure this newer WAL entry isn't replayed into + // the current value of d.mu.mem.mutable because the current + // mutable memtable exists before this flushable entry in + // the memtable queue. To ensure this, we just need to unset + // d.mu.mem.mutable. When a newer WAL is replayed, we will + // set d.mu.mem.mutable to a newer value. + d.mu.mem.mutable = nil + } else { + toFlush = append(toFlush, entry) + // During WAL replay, the lsm only has L0, hence, the + // baseLevel is 1. For the sake of simplicity, we place the + // ingested files in L0 here, instead of finding their + // target levels. This is a simplification for the sake of + // simpler code. It is expected that WAL replay should be + // rare, and that flushables of type ingestedFlushable + // should also be rare. So, placing the ingested files in L0 + // is alright. + // + // TODO(bananabrick): Maybe refactor this function to allow + // us to easily place ingested files in levels as low as + // possible during WAL replay. It would require breaking up + // the application of ve to the manifest into chunks and is + // not pretty w/o a refactor to this function and how it's + // used. + c := newFlush( + 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.FileMetadata}) + } + } + return toFlush, maxSeqNum, nil + } + } + + if b.memTableSize >= uint64(d.largeBatchThreshold) { + flushMem() + // Make a copy of the data slice since it is currently owned by buf and will + // be reused in the next iteration. + b.data = slices.Clone(b.data) + 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.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. + // But there must be at least one WAL entry waiting to be + // replayed. We have to ensure this newer WAL entry isn't + // replayed into the current value of d.mu.mem.mutable because + // the current mutable memtable exists before this flushable + // entry in the memtable queue. To ensure this, we just need to + // unset d.mu.mem.mutable. When a newer WAL is replayed, we will + // set d.mu.mem.mutable to a newer value. + d.mu.mem.mutable = nil + } else { + toFlush = append(toFlush, entry) + } + } else { + ensureMem(seqNum) + if err = mem.prepare(&b); err != nil && err != arenaskl.ErrArenaFull { + return nil, 0, err + } + // We loop since DB.newMemTable() slowly grows the size of allocated memtables, so the + // batch may not initially fit, but will eventually fit (since it is smaller than + // largeBatchThreshold). + for err == arenaskl.ErrArenaFull { + flushMem() + ensureMem(seqNum) + err = mem.prepare(&b) + if err != nil && err != arenaskl.ErrArenaFull { + return nil, 0, err + } + } + if err = mem.apply(&b, seqNum); err != nil { + return nil, 0, err + } + mem.writerUnref() + } + 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() + if err != nil { + return nil, 0, err + } + } + return toFlush, maxSeqNum, err +} + +func checkOptions(opts *Options, path string) (strictWALTail bool, err error) { + f, err := opts.FS.Open(path) + if err != nil { + return false, err + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return false, err + } + return opts.checkOptions(string(data)) +} + +// DBDesc briefly describes high-level state about a database. +type DBDesc struct { + // Exists is true if an existing database was found. + Exists bool + // FormatMajorVersion indicates the database's current format + // version. + FormatMajorVersion FormatMajorVersion + // ManifestFilename is the filename of the current active manifest, + // if the database exists. + ManifestFilename string +} + +// Peek looks for an existing database in dirname on the provided FS. It +// returns a brief description of the database. Peek is read-only and +// does not open the database +func Peek(dirname string, fs vfs.FS) (*DBDesc, error) { + vers, versMarker, err := lookupFormatMajorVersion(fs, dirname) + if err != nil { + return nil, err + } + // TODO(jackson): Immediately closing the marker is clunky. Add a + // PeekMarker variant that avoids opening the directory. + if err := versMarker.Close(); err != nil { + return nil, err + } + + // Find the currently active manifest, if there is one. + manifestMarker, manifestFileNum, exists, err := findCurrentManifest(vers, fs, dirname) + if err != nil { + return nil, err + } + // TODO(jackson): Immediately closing the marker is clunky. Add a + // PeekMarker variant that avoids opening the directory. + if err := manifestMarker.Close(); err != nil { + return nil, err + } + + desc := &DBDesc{ + Exists: exists, + FormatMajorVersion: vers, + } + if exists { + desc.ManifestFilename = base.MakeFilepath(fs, dirname, fileTypeManifest, manifestFileNum) + } + return desc, nil +} + +// LockDirectory acquires the database directory lock in the named directory, +// preventing another process from opening the database. LockDirectory returns a +// handle to the held lock that may be passed to Open through Options.Lock to +// subsequently open the database, skipping lock acquistion during Open. +// +// 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, base.FileNum(0).DiskFileNum())) + if err != nil { + return nil, err + } + l := &Lock{dirname: dirname, fileLock: fileLock} + l.refs.Store(1) + invariants.SetFinalizer(l, func(obj interface{}) { + if refs := obj.(*Lock).refs.Load(); refs > 0 { + panic(errors.AssertionFailedf("lock for %q finalized with %d refs", dirname, refs)) + } + }) + return l, nil +} + +// Lock represents a file lock on a directory. It may be passed to Open through +// Options.Lock to elide lock aquisition during Open. +type Lock struct { + dirname string + fileLock io.Closer + // refs is a count of the number of handles on the lock. refs must be 0, 1 + // or 2. + // + // When acquired by the client and passed to Open, refs = 1 and the Open + // call increments it to 2. When the database is closed, it's decremented to + // 1. Finally when the original caller, calls Close on the Lock, it's + // drecemented to zero and the underlying file lock is released. + // + // When Open acquires the file lock, refs remains at 1 until the database is + // closed. + refs atomic.Int32 +} + +func (l *Lock) refForOpen() error { + // During Open, when a user passed in a lock, the reference count must be + // exactly 1. If it's zero, the lock is no longer held and is invalid. If + // it's 2, the lock is already in use by another database within the + // process. + if !l.refs.CompareAndSwap(1, 2) { + return errors.Errorf("pebble: unexpected Lock reference count; is the lock already in use?") + } + return nil +} + +// Close releases the lock, permitting another process to lock and open the +// database. Close must not be called until after a database using the Lock has +// been closed. +func (l *Lock) Close() error { + if l.refs.Add(-1) > 0 { + return nil + } + defer func() { l.fileLock = nil }() + return l.fileLock.Close() +} + +// ErrDBDoesNotExist is generated when ErrorIfNotExists is set and the database +// does not exist. +// +// Note that errors can be wrapped with more details; use errors.Is(). +var ErrDBDoesNotExist = errors.New("pebble: database does not exist") + +// ErrDBAlreadyExists is generated when ErrorIfExists is set and the database +// already exists. +// +// Note that errors can be wrapped with more details; use errors.Is(). +var ErrDBAlreadyExists = errors.New("pebble: database already exists") + +// ErrDBNotPristine is generated when ErrorIfNotPristine is set and the database +// already exists and is not pristine. +// +// Note that errors can be wrapped with more details; use errors.Is(). +var ErrDBNotPristine = errors.New("pebble: database already exists and is not pristine") + +// IsCorruptionError returns true if the given error indicates database +// corruption. +func IsCorruptionError(err error) bool { + return errors.Is(err, base.ErrCorruption) +} + +func checkConsistency(v *manifest.Version, dirname string, objProvider objstorage.Provider) error { + var errs []error + dedup := make(map[base.DiskFileNum]struct{}) + for level, files := range v.Levels { + iter := files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + backingState := f.FileBacking + if _, ok := dedup[backingState.DiskFileNum]; ok { + continue + } + dedup[backingState.DiskFileNum] = struct{}{} + fileNum := backingState.DiskFileNum + fileSize := backingState.Size + // We skip over remote objects; those are instead checked asynchronously + // by the table stats loading job. + meta, err := objProvider.Lookup(base.FileTypeTable, fileNum) + var size int64 + if err == nil { + if meta.IsRemote() { + continue + } + size, err = objProvider.Size(meta) + } + if err != nil { + errs = append(errs, errors.Wrapf(err, "L%d: %s", errors.Safe(level), fileNum)) + continue + } + + if size != int64(fileSize) { + errs = append(errs, errors.Errorf( + "L%d: %s: object size mismatch (%s): %d (disk) != %d (MANIFEST)", + errors.Safe(level), fileNum, objProvider.Path(meta), + errors.Safe(size), errors.Safe(fileSize))) + continue + } + } + } + return errors.Join(errs...) +} diff --git a/pebble/open_test.go b/pebble/open_test.go new file mode 100644 index 0000000..fae3237 --- /dev/null +++ b/pebble/open_test.go @@ -0,0 +1,1390 @@ +// Copyright 2012 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 ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "runtime/debug" + "sort" + "strconv" + "strings" + "sync/atomic" + "syscall" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/atomicfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/cockroachdb/redact" + "github.com/kr/pretty" + "github.com/stretchr/testify/require" +) + +func TestOpenSharedTableCache(t *testing.T) { + c := cache.New(cacheDefaultSize) + tc := NewTableCache(c, 16, 100) + defer tc.Unref() + defer c.Unref() + + d0, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + Cache: c, + TableCache: tc, + })) + if err != nil { + t.Errorf("d0 Open: %s", err.Error()) + } + defer d0.Close() + + d1, err := Open("", testingRandomized(t, &Options{ + FS: vfs.NewMem(), + Cache: c, + TableCache: tc, + })) + if err != nil { + t.Errorf("d1 Open: %s", err.Error()) + } + defer d1.Close() + + // Make sure that the Open function is using the passed in table cache + // when the TableCache option is set. + require.Equalf( + t, d0.tableCache.tableCache, d1.tableCache.tableCache, + "expected tableCache for both d0 and d1 to be the same", + ) +} + +func TestErrorIfExists(t *testing.T) { + opts := testingRandomized(t, &Options{ + FS: vfs.NewMem(), + ErrorIfExists: true, + }) + defer ensureFilesClosed(t, opts)() + + d0, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d0.Close()) + + if _, err := Open("", opts); !errors.Is(err, ErrDBAlreadyExists) { + t.Fatalf("expected db-already-exists error, got %v", err) + } + + opts.ErrorIfExists = false + d1, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d1.Close()) +} + +func TestErrorIfNotExists(t *testing.T) { + opts := testingRandomized(t, &Options{ + FS: vfs.NewMem(), + ErrorIfNotExists: true, + }) + defer ensureFilesClosed(t, opts)() + + _, err := Open("", opts) + if !errors.Is(err, ErrDBDoesNotExist) { + t.Fatalf("expected db-does-not-exist error, got %v", err) + } + + // Create the DB and try again. + opts.ErrorIfNotExists = false + d0, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d0.Close()) + + opts.ErrorIfNotExists = true + d1, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d1.Close()) +} + +func TestErrorIfNotPristine(t *testing.T) { + opts := testingRandomized(t, &Options{ + FS: vfs.NewMem(), + ErrorIfNotPristine: true, + }) + defer ensureFilesClosed(t, opts)() + + d0, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d0.Close()) + + // Store is pristine; ok to open. + d1, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d1.Set([]byte("foo"), []byte("bar"), Sync)) + require.NoError(t, d1.Close()) + + if _, err := Open("", opts); !errors.Is(err, ErrDBNotPristine) { + t.Fatalf("expected db-not-pristine error, got %v", err) + } + + // Run compaction and make sure we're still not allowed to open. + opts.ErrorIfNotPristine = false + d2, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d2.Compact([]byte("a"), []byte("z"), false /* parallelize */)) + require.NoError(t, d2.Close()) + + opts.ErrorIfNotPristine = true + if _, err := Open("", opts); !errors.Is(err, ErrDBNotPristine) { + t.Fatalf("expected db-already-exists error, got %v", err) + } +} + +func TestOpenAlreadyLocked(t *testing.T) { + runTest := func(t *testing.T, dirname string, fs vfs.FS) { + opts := testingRandomized(t, &Options{FS: fs}) + var err error + opts.Lock, err = LockDirectory(dirname, fs) + require.NoError(t, err) + + d, err := Open(dirname, opts) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("foo"), []byte("bar"), Sync)) + + // Try to open the same database reusing the Options containing the same + // Lock. It should error when it observes that it's already referenced. + _, err = Open(dirname, opts) + require.Error(t, err) + + // Close the database. + require.NoError(t, d.Close()) + + // Now Opening should succeed again. + d, err = Open(dirname, opts) + require.NoError(t, err) + require.NoError(t, d.Close()) + + require.NoError(t, opts.Lock.Close()) + // There should be no more remaining references. + require.Equal(t, int32(0), opts.Lock.refs.Load()) + } + t.Run("memfs", func(t *testing.T) { + runTest(t, "", vfs.NewMem()) + }) + t.Run("disk", func(t *testing.T) { + runTest(t, t.TempDir(), vfs.Default) + }) +} + +func TestNewDBFilenames(t *testing.T) { + versions := map[FormatMajorVersion][]string{ + FormatMostCompatible: { + "000002.log", + "CURRENT", + "LOCK", + "MANIFEST-000001", + "OPTIONS-000003", + }, + internalFormatNewest: { + "000002.log", + "CURRENT", + "LOCK", + "MANIFEST-000001", + "OPTIONS-000003", + "marker.format-version.000015.016", + "marker.manifest.000001.MANIFEST-000001", + }, + } + + for formatVers, want := range versions { + t.Run(fmt.Sprintf("vers=%s", formatVers), func(t *testing.T) { + mem := vfs.NewMem() + fooBar := mem.PathJoin("foo", "bar") + d, err := Open(fooBar, &Options{ + FS: mem, + FormatMajorVersion: formatVers, + }) + if err != nil { + t.Fatalf("Open: %v", err) + } + if err := d.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + got, err := mem.List(fooBar) + if err != nil { + t.Fatalf("List: %v", err) + } + sort.Strings(got) + if !reflect.DeepEqual(got, want) { + t.Errorf("\ngot %v\nwant %v", got, want) + } + }) + } +} + +func testOpenCloseOpenClose(t *testing.T, fs vfs.FS, root string) { + opts := testingRandomized(t, &Options{FS: fs}) + + for _, startFromEmpty := range []bool{false, true} { + for _, walDirname := range []string{"", "wal"} { + for _, length := range []int{-1, 0, 1, 1000, 10000, 100000} { + dirname := "sharedDatabase" + walDirname + if startFromEmpty { + dirname = "startFromEmpty" + walDirname + strconv.Itoa(length) + } + dirname = fs.PathJoin(root, dirname) + if walDirname == "" { + opts.WALDir = "" + } else { + opts.WALDir = fs.PathJoin(dirname, walDirname) + } + + got, xxx := []byte(nil), "" + if length >= 0 { + xxx = strings.Repeat("x", length) + } + + d0, err := Open(dirname, opts) + if err != nil { + t.Fatalf("sfe=%t, length=%d: Open #0: %v", + startFromEmpty, length, err) + continue + } + if length >= 0 { + err = d0.Set([]byte("key"), []byte(xxx), nil) + if err != nil { + t.Errorf("sfe=%t, length=%d: Set: %v", + startFromEmpty, length, err) + continue + } + } + err = d0.Close() + if err != nil { + t.Errorf("sfe=%t, length=%d: Close #0: %v", + startFromEmpty, length, err) + continue + } + + d1, err := Open(dirname, opts) + if err != nil { + t.Errorf("sfe=%t, length=%d: Open #1: %v", + startFromEmpty, length, err) + continue + } + if length >= 0 { + var closer io.Closer + got, closer, err = d1.Get([]byte("key")) + if err != nil { + t.Errorf("sfe=%t, length=%d: Get: %v", + startFromEmpty, length, err) + continue + } + got = append([]byte(nil), got...) + closer.Close() + } + err = d1.Close() + if err != nil { + t.Errorf("sfe=%t, length=%d: Close #1: %v", + startFromEmpty, length, err) + continue + } + + if length >= 0 && string(got) != xxx { + t.Errorf("sfe=%t, length=%d: got value differs from set value", + startFromEmpty, length) + continue + } + + { + got, err := opts.FS.List(dirname) + if err != nil { + t.Fatalf("List: %v", err) + } + var optionsCount int + for _, s := range got { + if t, _, ok := base.ParseFilename(opts.FS, s); ok && t == fileTypeOptions { + optionsCount++ + } + } + if optionsCount != 1 { + t.Fatalf("expected 1 OPTIONS file, but found %d", optionsCount) + } + } + } + } + } +} + +func TestOpenCloseOpenClose(t *testing.T) { + for _, fstype := range []string{"disk", "mem"} { + t.Run(fstype, func(t *testing.T) { + var fs vfs.FS + var dir string + switch fstype { + case "disk": + var err error + dir, err = os.MkdirTemp("", "open-close") + require.NoError(t, err) + defer func() { + _ = os.RemoveAll(dir) + }() + fs = vfs.Default + case "mem": + dir = "" + fs = vfs.NewMem() + } + testOpenCloseOpenClose(t, fs, dir) + }) + } +} + +func TestOpenOptionsCheck(t *testing.T) { + mem := vfs.NewMem() + opts := &Options{FS: mem} + + d, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, d.Close()) + + opts = &Options{ + Comparer: &Comparer{Name: "foo"}, + FS: mem, + } + _, err = Open("", opts) + require.Regexp(t, `comparer name from file.*!=.*`, err) + + opts = &Options{ + Merger: &Merger{Name: "bar"}, + FS: mem, + } + _, err = Open("", opts) + require.Regexp(t, `merger name from file.*!=.*`, err) +} + +func TestOpenCrashWritingOptions(t *testing.T) { + memFS := vfs.NewMem() + + d, err := Open("", &Options{FS: memFS}) + require.NoError(t, err) + require.NoError(t, d.Close()) + + // Open the database again, this time with a mocked filesystem that + // will only succeed in partially writing the OPTIONS file. + fs := optionsTornWriteFS{FS: memFS} + _, err = Open("", &Options{FS: fs}) + require.Error(t, err) + + // Re-opening the database must succeed. + d, err = Open("", &Options{FS: memFS}) + require.NoError(t, err) + require.NoError(t, d.Close()) +} + +type optionsTornWriteFS struct { + vfs.FS +} + +func (fs optionsTornWriteFS) Create(name string) (vfs.File, error) { + file, err := fs.FS.Create(name) + if file != nil { + file = optionsTornWriteFile{File: file} + } + return file, err +} + +type optionsTornWriteFile struct { + vfs.File +} + +func (f optionsTornWriteFile) Write(b []byte) (int, error) { + // Look for the OPTIONS-XXXXXX file's `comparer=` field. + comparerKey := []byte("comparer=") + i := bytes.Index(b, comparerKey) + if i == -1 { + return f.File.Write(b) + } + // Write only the contents through `comparer=` and return an error. + n, _ := f.File.Write(b[:i+len(comparerKey)]) + return n, syscall.EIO +} + +func TestOpenReadOnly(t *testing.T) { + mem := vfs.NewMem() + + { + // Opening a non-existent DB in read-only mode should result in no mutable + // filesystem operations. + var memLog base.InMemLogger + _, err := Open("non-existent", testingRandomized(t, &Options{ + FS: vfs.WithLogging(mem, memLog.Infof), + ReadOnly: true, + WALDir: "non-existent-waldir", + })) + if err == nil { + t.Fatalf("expected error, but found success") + } + const expected = `open-dir: non-existent` + if trimmed := strings.TrimSpace(memLog.String()); expected != trimmed { + t.Fatalf("expected %q, but found %q", expected, trimmed) + } + } + + { + // Opening a DB with a non-existent WAL dir in read-only mode should result + // in no mutable filesystem operations other than the LOCK. + var memLog base.InMemLogger + _, err := Open("", testingRandomized(t, &Options{ + FS: vfs.WithLogging(mem, memLog.Infof), + ReadOnly: true, + WALDir: "non-existent-waldir", + })) + if err == nil { + t.Fatalf("expected error, but found success") + } + const expected = "open-dir: \nopen-dir: non-existent-waldir\nclose:" + if trimmed := strings.TrimSpace(memLog.String()); expected != trimmed { + t.Fatalf("expected %q, but found %q", expected, trimmed) + } + } + + var contents []string + { + // Create a new DB and populate it with a small amount of data. + d, err := Open("", testingRandomized(t, &Options{ + FS: mem, + })) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("test"), nil, nil)) + require.NoError(t, d.Close()) + contents, err = mem.List("") + require.NoError(t, err) + sort.Strings(contents) + } + + { + // Re-open the DB read-only. The directory contents should be unchanged. + d, err := Open("", testingRandomized(t, &Options{ + FS: mem, + ReadOnly: true, + })) + require.NoError(t, err) + + // Verify various write operations fail in read-only mode. + require.EqualValues(t, ErrReadOnly, d.Compact(nil, []byte("\xff"), false)) + require.EqualValues(t, ErrReadOnly, d.Flush()) + require.EqualValues(t, ErrReadOnly, func() error { _, err := d.AsyncFlush(); return err }()) + + require.EqualValues(t, ErrReadOnly, d.Delete(nil, nil)) + require.EqualValues(t, ErrReadOnly, d.DeleteRange(nil, nil, nil)) + require.EqualValues(t, ErrReadOnly, d.Ingest(nil)) + require.EqualValues(t, ErrReadOnly, d.LogData(nil, nil)) + require.EqualValues(t, ErrReadOnly, d.Merge(nil, nil, nil)) + require.EqualValues(t, ErrReadOnly, d.Set(nil, nil, nil)) + + // Verify we can still read in read-only mode. + require.NoError(t, func() error { + _, closer, err := d.Get([]byte("test")) + if closer != nil { + closer.Close() + } + return err + }()) + + checkIter := func(iter *Iterator, err error) { + t.Helper() + + var keys []string + for valid := iter.First(); valid; valid = iter.Next() { + keys = append(keys, string(iter.Key())) + } + require.NoError(t, iter.Close()) + expectedKeys := []string{"test"} + if diff := pretty.Diff(keys, expectedKeys); diff != nil { + t.Fatalf("%s\n%s", strings.Join(diff, "\n"), keys) + } + } + + checkIter(d.NewIter(nil)) + + b := d.NewIndexedBatch() + checkIter(b.NewIter(nil)) + require.EqualValues(t, ErrReadOnly, b.Commit(nil)) + require.EqualValues(t, ErrReadOnly, d.Apply(b, nil)) + + s := d.NewSnapshot() + checkIter(s.NewIter(nil)) + require.NoError(t, s.Close()) + + require.NoError(t, d.Close()) + + newContents, err := mem.List("") + require.NoError(t, err) + + sort.Strings(newContents) + if diff := pretty.Diff(contents, newContents); diff != nil { + t.Fatalf("%s", strings.Join(diff, "\n")) + } + } +} + +func TestOpenWALReplay(t *testing.T) { + largeValue := []byte(strings.Repeat("a", 100<<10)) + hugeValue := []byte(strings.Repeat("b", 10<<20)) + checkIter := func(iter *Iterator, err error) { + t.Helper() + + var keys []string + for valid := iter.First(); valid; valid = iter.Next() { + keys = append(keys, string(iter.Key())) + } + require.NoError(t, iter.Close()) + expectedKeys := []string{"1", "2", "3", "4", "5"} + if diff := pretty.Diff(keys, expectedKeys); diff != nil { + t.Fatalf("%s\n%s", strings.Join(diff, "\n"), keys) + } + } + + for _, readOnly := range []bool{false, true} { + t.Run(fmt.Sprintf("read-only=%t", readOnly), func(t *testing.T) { + // Create a new DB and populate it with some data. + const dir = "" + mem := vfs.NewMem() + d, err := Open(dir, testingRandomized(t, &Options{ + FS: mem, + MemTableSize: 32 << 20, + })) + require.NoError(t, err) + // All these values will fit in a single memtable, so on closing the db there + // will be no sst and all the data is in a single WAL. + require.NoError(t, d.Set([]byte("1"), largeValue, nil)) + require.NoError(t, d.Set([]byte("2"), largeValue, nil)) + require.NoError(t, d.Set([]byte("3"), largeValue, nil)) + require.NoError(t, d.Set([]byte("4"), hugeValue, nil)) + require.NoError(t, d.Set([]byte("5"), largeValue, nil)) + checkIter(d.NewIter(nil)) + require.NoError(t, d.Close()) + files, err := mem.List(dir) + require.NoError(t, err) + sort.Strings(files) + logCount, sstCount := 0, 0 + for _, fname := range files { + if strings.HasSuffix(fname, ".sst") { + sstCount++ + } + if strings.HasSuffix(fname, ".log") { + logCount++ + } + } + require.Equal(t, 0, sstCount) + // The memtable size starts at 256KB and doubles up to 32MB so we expect 5 + // logs (one for each doubling). + require.Equal(t, 7, logCount) + + // Re-open the DB with a smaller memtable. Values for 1, 2 will fit in the first memtable; + // value for 3 will go in the next memtable; value for 4 will be in a flushable batch + // which will cause the previous memtable to be flushed; value for 5 will go in the next + // memtable + d, err = Open(dir, testingRandomized(t, &Options{ + FS: mem, + MemTableSize: 300 << 10, + ReadOnly: readOnly, + })) + require.NoError(t, err) + + if readOnly { + m := d.Metrics() + require.Equal(t, int64(logCount), m.WAL.Files) + d.mu.Lock() + require.NotNil(t, d.mu.mem.mutable) + d.mu.Unlock() + } + checkIter(d.NewIter(nil)) + require.NoError(t, d.Close()) + }) + } +} + +// Reproduction for https://github.com/cockroachdb/pebble/issues/2234. +func TestWALReplaySequenceNumBug(t *testing.T) { + mem := vfs.NewMem() + d, err := Open("", testingRandomized(t, &Options{ + FS: mem, + })) + require.NoError(t, err) + + d.mu.Lock() + // Disable any flushes. + d.mu.compact.flushing = true + d.mu.Unlock() + + require.NoError(t, d.Set([]byte("1"), nil, nil)) + require.NoError(t, d.Set([]byte("2"), nil, nil)) + + // Write a large batch. This should go to a separate memtable. + largeValue := []byte(strings.Repeat("a", int(d.largeBatchThreshold))) + require.NoError(t, d.Set([]byte("1"), largeValue, nil)) + + // This write should go the mutable memtable after the large batch in the + // memtable queue. + d.Set([]byte("1"), nil, nil) + + d.mu.Lock() + d.mu.compact.flushing = false + d.mu.Unlock() + + // Make sure none of the flushables have been flushed. + require.Equal(t, 3, len(d.mu.mem.queue)) + + // Close the db. This doesn't cause a flush of the memtables, so they'll + // have to be replayed when the db is reopened. + require.NoError(t, d.Close()) + + files, err := mem.List("") + require.NoError(t, err) + sort.Strings(files) + sstCount := 0 + for _, fname := range files { + if strings.HasSuffix(fname, ".sst") { + sstCount++ + } + } + require.Equal(t, 0, sstCount) + + // Reopen db in read only mode to force read only wal replay. + d, err = Open("", &Options{ + FS: mem, + ReadOnly: true, + }) + require.NoError(t, err) + val, c, _ := d.Get([]byte("1")) + require.Equal(t, []byte{}, val) + c.Close() + require.NoError(t, d.Close()) +} + +// Similar to TestOpenWALReplay, except we test replay behavior after a +// memtable has been flushed. We test all 3 reasons for flushing: forced, size, +// and large-batch. +func TestOpenWALReplay2(t *testing.T) { + for _, readOnly := range []bool{false, true} { + t.Run(fmt.Sprintf("read-only=%t", readOnly), func(t *testing.T) { + for _, reason := range []string{"forced", "size", "large-batch"} { + t.Run(reason, func(t *testing.T) { + mem := vfs.NewMem() + d, err := Open("", testingRandomized(t, &Options{ + FS: mem, + MemTableSize: 256 << 10, + })) + require.NoError(t, err) + + switch reason { + case "forced": + require.NoError(t, d.Set([]byte("1"), nil, nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("2"), nil, nil)) + case "size": + largeValue := []byte(strings.Repeat("a", 100<<10)) + require.NoError(t, d.Set([]byte("1"), largeValue, nil)) + require.NoError(t, d.Set([]byte("2"), largeValue, nil)) + require.NoError(t, d.Set([]byte("3"), largeValue, nil)) + case "large-batch": + largeValue := []byte(strings.Repeat("a", int(d.largeBatchThreshold))) + require.NoError(t, d.Set([]byte("1"), nil, nil)) + require.NoError(t, d.Set([]byte("2"), largeValue, nil)) + require.NoError(t, d.Set([]byte("3"), nil, nil)) + } + require.NoError(t, d.Close()) + + files, err := mem.List("") + require.NoError(t, err) + sort.Strings(files) + sstCount := 0 + for _, fname := range files { + if strings.HasSuffix(fname, ".sst") { + sstCount++ + } + } + require.Equal(t, 1, sstCount) + + // Re-open the DB with a smaller memtable. Values for 1, 2 will fit in the first memtable; + // value for 3 will go in the next memtable; value for 4 will be in a flushable batch + // which will cause the previous memtable to be flushed; value for 5 will go in the next + // memtable + d, err = Open("", testingRandomized(t, &Options{ + FS: mem, + MemTableSize: 300 << 10, + ReadOnly: readOnly, + })) + require.NoError(t, err) + require.NoError(t, d.Close()) + }) + } + }) + } +} + +// TestTwoWALReplayCorrupt tests WAL-replay behavior when the first of the two +// WALs is corrupted with an sstable checksum error. Replay must stop at the +// first WAL because otherwise we may violate point-in-time recovery +// semantics. See #864. +func TestTwoWALReplayCorrupt(t *testing.T) { + // Use the real filesystem so that we can seek and overwrite WAL data + // easily. + dir, err := os.MkdirTemp("", "wal-replay") + require.NoError(t, err) + defer os.RemoveAll(dir) + + d, err := Open(dir, testingRandomized(t, &Options{ + MemTableStopWritesThreshold: 4, + MemTableSize: 2048, + })) + require.NoError(t, err) + d.mu.Lock() + d.mu.compact.flushing = true + d.mu.Unlock() + require.NoError(t, d.Set([]byte("1"), []byte(strings.Repeat("a", 1024)), nil)) + require.NoError(t, d.Set([]byte("2"), nil, nil)) + d.mu.Lock() + d.mu.compact.flushing = false + d.mu.Unlock() + require.NoError(t, d.Close()) + + // We should have two WALs. + var logs []string + ls, err := vfs.Default.List(dir) + require.NoError(t, err) + for _, name := range ls { + if filepath.Ext(name) == ".log" { + logs = append(logs, name) + } + } + sort.Strings(logs) + if len(logs) < 2 { + t.Fatalf("expected at least two log files, found %d", len(logs)) + } + + // Corrupt the (n-1)th WAL by zeroing four bytes, 100 bytes from the end + // of the file. + f, err := os.OpenFile(filepath.Join(dir, logs[len(logs)-2]), os.O_RDWR, os.ModePerm) + require.NoError(t, err) + off, err := f.Seek(-100, 2) + require.NoError(t, err) + _, err = f.Write([]byte{0, 0, 0, 0}) + require.NoError(t, err) + require.NoError(t, f.Close()) + t.Logf("zeored four bytes in %s at offset %d\n", logs[len(logs)-2], off) + + // Re-opening the database should detect and report the corruption. + _, err = Open(dir, nil) + require.Error(t, err, "pebble: corruption") +} + +// TestTwoWALReplayCorrupt tests WAL-replay behavior when the first of the two +// WALs is corrupted with an sstable checksum error and the OPTIONS file does +// not enable the private strict_wal_tail option, indicating that the WAL was +// produced by a database that did not guarantee clean WAL tails. See #864. +func TestTwoWALReplayPermissive(t *testing.T) { + // Use the real filesystem so that we can seek and overwrite WAL data + // easily. + dir, err := os.MkdirTemp("", "wal-replay") + require.NoError(t, err) + defer os.RemoveAll(dir) + + opts := &Options{ + MemTableStopWritesThreshold: 4, + MemTableSize: 2048, + } + opts.testingRandomized(t) + opts.EnsureDefaults() + d, err := Open(dir, opts) + require.NoError(t, err) + d.mu.Lock() + d.mu.compact.flushing = true + d.mu.Unlock() + require.NoError(t, d.Set([]byte("1"), []byte(strings.Repeat("a", 1024)), nil)) + require.NoError(t, d.Set([]byte("2"), nil, nil)) + d.mu.Lock() + d.mu.compact.flushing = false + d.mu.Unlock() + require.NoError(t, d.Close()) + + // We should have two WALs. + var logs []string + var optionFilename string + ls, err := vfs.Default.List(dir) + require.NoError(t, err) + for _, name := range ls { + if filepath.Ext(name) == ".log" { + logs = append(logs, name) + } + if strings.HasPrefix(filepath.Base(name), "OPTIONS") { + optionFilename = name + } + } + sort.Strings(logs) + if len(logs) < 2 { + t.Fatalf("expected at least two log files, found %d", len(logs)) + } + + // Corrupt the (n-1)th WAL by zeroing four bytes, 100 bytes from the end + // of the file. + f, err := os.OpenFile(filepath.Join(dir, logs[len(logs)-2]), os.O_RDWR, os.ModePerm) + require.NoError(t, err) + off, err := f.Seek(-100, 2) + require.NoError(t, err) + _, err = f.Write([]byte{0, 0, 0, 0}) + require.NoError(t, err) + require.NoError(t, f.Close()) + t.Logf("zeored four bytes in %s at offset %d\n", logs[len(logs)-2], off) + + // Remove the OPTIONS file containing the strict_wal_tail option. + require.NoError(t, vfs.Default.Remove(filepath.Join(dir, optionFilename))) + + // Re-opening the database should not report the corruption. + d, err = Open(dir, nil) + require.NoError(t, err) + require.NoError(t, d.Close()) +} + +// TestCrashOpenCrashAfterWALCreation tests a database that exits +// ungracefully, begins recovery, creates the new WAL but promptly exits +// ungracefully again. +// +// This sequence has the potential to be problematic with the strict_wal_tail +// behavior because the first crash's WAL has an unclean tail. By the time the +// new WAL is created, the current manifest's MinUnflushedLogNum must be +// higher than the previous WAL. +func TestCrashOpenCrashAfterWALCreation(t *testing.T) { + fs := vfs.NewStrictMem() + + getLogs := func() (logs []string) { + ls, err := fs.List("") + require.NoError(t, err) + for _, name := range ls { + if filepath.Ext(name) == ".log" { + logs = append(logs, name) + } + } + return logs + } + + { + d, err := Open("", testingRandomized(t, &Options{FS: fs})) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("abc"), nil, Sync)) + + // Ignore syncs during close to simulate a crash. This will leave the WAL + // without an EOF trailer. It won't be an 'unclean tail' yet since the + // log file was not recycled, but we'll fix that down below. + fs.SetIgnoreSyncs(true) + require.NoError(t, d.Close()) + fs.ResetToSyncedState() + fs.SetIgnoreSyncs(false) + } + + // There should be one WAL. + logs := getLogs() + if len(logs) != 1 { + t.Fatalf("expected one log file, found %d", len(logs)) + } + + // The one WAL file doesn't have an EOF trailer, but since it wasn't + // recycled it won't have garbage at the end. Rewrite it so that it has + // the same contents it currently has, followed by garbage. + { + f, err := fs.Open(logs[0]) + require.NoError(t, err) + b, err := io.ReadAll(f) + require.NoError(t, err) + require.NoError(t, f.Close()) + f, err = fs.Create(logs[0]) + require.NoError(t, err) + _, err = f.Write(b) + require.NoError(t, err) + _, err = f.Write([]byte{0xde, 0xad, 0xbe, 0xef}) + require.NoError(t, err) + require.NoError(t, f.Sync()) + require.NoError(t, f.Close()) + dir, err := fs.OpenDir("") + require.NoError(t, err) + require.NoError(t, dir.Sync()) + require.NoError(t, dir.Close()) + } + + // Open the database again (with syncs respected again). Wrap the + // filesystem with an errorfs that will turn off syncs after a new .log + // file is created and after a subsequent directory sync occurs. This + // simulates a crash after the new log file is created and synced. + { + var walCreated, dirSynced atomic.Bool + d, err := Open("", &Options{ + FS: errorfs.Wrap(fs, errorfs.InjectorFunc(func(op errorfs.Op) error { + if dirSynced.Load() { + fs.SetIgnoreSyncs(true) + } + if op.Kind == errorfs.OpCreate && filepath.Ext(op.Path) == ".log" { + walCreated.Store(true) + } + // Record when there's a sync of the data directory after the + // WAL was created. The data directory will have an empty + // path because that's what we passed into Open. + if op.Kind == errorfs.OpFileSync && op.Path == "" && walCreated.Load() { + dirSynced.Store(true) + } + return nil + })), + }) + require.NoError(t, err) + require.NoError(t, d.Close()) + } + + fs.ResetToSyncedState() + fs.SetIgnoreSyncs(false) + + if n := len(getLogs()); n != 2 { + t.Fatalf("expected two logs, found %d\n", n) + } + + // Finally, open the database with syncs enabled. + d, err := Open("", testingRandomized(t, &Options{FS: fs})) + require.NoError(t, err) + require.NoError(t, d.Close()) +} + +// TestOpenWALReplayReadOnlySeqNums tests opening a database: +// - in read-only mode +// - with multiple unflushed log files that must replayed +// - a MANIFEST that sets the last sequence number to a number greater than +// the unflushed log files +// +// See cockroachdb/cockroach#48660. +func TestOpenWALReplayReadOnlySeqNums(t *testing.T) { + const root = "" + mem := vfs.NewMem() + + copyFiles := func(srcDir, dstDir string) { + files, err := mem.List(srcDir) + require.NoError(t, err) + for _, f := range files { + require.NoError(t, vfs.Copy(mem, mem.PathJoin(srcDir, f), mem.PathJoin(dstDir, f))) + } + } + + // Create a new database under `/original` with a couple sstables. + dir := mem.PathJoin(root, "original") + d, err := Open(dir, testingRandomized(t, &Options{FS: mem})) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("a"), nil, nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("a"), nil, nil)) + require.NoError(t, d.Flush()) + + // Prevent flushes so that multiple unflushed log files build up. + d.mu.Lock() + d.mu.compact.flushing = true + d.mu.Unlock() + + require.NoError(t, d.Set([]byte("b"), nil, nil)) + d.AsyncFlush() + require.NoError(t, d.Set([]byte("c"), nil, nil)) + d.AsyncFlush() + require.NoError(t, d.Set([]byte("e"), nil, nil)) + + // Manually compact some of the key space so that the latest `logSeqNum` is + // written to the MANIFEST. This produces a MANIFEST where the `logSeqNum` + // is greater than the sequence numbers contained in the + // `minUnflushedLogNum` log file + require.NoError(t, d.Compact([]byte("a"), []byte("a\x00"), false)) + d.mu.Lock() + for d.mu.compact.compactingCount > 0 { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + + d.TestOnlyWaitForCleaning() + // While the MANIFEST is still in this state, copy all the files in the + // database to a new directory. + replayDir := mem.PathJoin(root, "replay") + require.NoError(t, mem.MkdirAll(replayDir, os.ModePerm)) + copyFiles(dir, replayDir) + + d.mu.Lock() + d.mu.compact.flushing = false + d.mu.Unlock() + require.NoError(t, d.Close()) + + // Open the copy of the database in read-only mode. Since we copied all + // the files before the flushes were allowed to complete, there should be + // multiple unflushed log files that need to replay. Since the manual + // compaction completed, the `logSeqNum` read from the manifest should be + // greater than the unflushed log files' sequence numbers. + d, err = Open(replayDir, testingRandomized(t, &Options{ + FS: mem, + ReadOnly: true, + })) + require.NoError(t, err) + require.NoError(t, d.Close()) +} + +func TestOpenWALReplayMemtableGrowth(t *testing.T) { + mem := vfs.NewMem() + const memTableSize = 64 * 1024 * 1024 + opts := &Options{ + MemTableSize: memTableSize, + FS: mem, + } + opts.testingRandomized(t) + func() { + db, err := Open("", opts) + require.NoError(t, err) + defer db.Close() + b := db.NewBatch() + defer b.Close() + key := make([]byte, 8) + val := make([]byte, 16*1024*1024) + b.Set(key, val, nil) + require.NoError(t, db.Apply(b, Sync)) + }() + db, err := Open("", opts) + require.NoError(t, err) + db.Close() +} + +func TestGetVersion(t *testing.T) { + mem := vfs.NewMem() + opts := &Options{ + FS: mem, + } + opts.testingRandomized(t) + + // Case 1: No options file. + version, err := GetVersion("", mem) + require.NoError(t, err) + require.Empty(t, version) + + // Case 2: Pebble created file. + db, err := Open("", opts) + require.NoError(t, err) + require.NoError(t, db.Close()) + version, err = GetVersion("", mem) + require.NoError(t, err) + require.Equal(t, "0.1", version) + + // Case 3: Manually created OPTIONS file with a higher number. + highestOptionsNum := FileNum(0) + ls, err := mem.List("") + require.NoError(t, err) + for _, filename := range ls { + ft, fn, ok := base.ParseFilename(mem, filename) + if !ok { + continue + } + switch ft { + case fileTypeOptions: + if fn.FileNum() > highestOptionsNum { + highestOptionsNum = fn.FileNum() + } + } + } + f, _ := mem.Create(fmt.Sprintf("OPTIONS-%d", highestOptionsNum+1)) + _, err = f.Write([]byte("[Version]\n pebble_version=0.2\n")) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + version, err = GetVersion("", mem) + require.NoError(t, err) + require.Equal(t, "0.2", version) + + // Case 4: Manually created OPTIONS file with a RocksDB number. + f, _ = mem.Create(fmt.Sprintf("OPTIONS-%d", highestOptionsNum+2)) + _, err = f.Write([]byte("[Version]\n rocksdb_version=6.2.1\n")) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + version, err = GetVersion("", mem) + require.NoError(t, err) + require.Equal(t, "rocksdb v6.2.1", version) +} + +func TestRocksDBNoFlushManifest(t *testing.T) { + mem := vfs.NewMem() + // Have the comparer and merger names match what's in the testdata + // directory. + comparer := *DefaultComparer + merger := *DefaultMerger + comparer.Name = "cockroach_comparator" + merger.Name = "cockroach_merge_operator" + opts := &Options{ + FS: mem, + Comparer: &comparer, + Merger: &merger, + } + + // rocksdb-ingest-only is a RocksDB-generated db directory that has not had + // a single flush yet, only ingestion operations. The manifest contains + // a next-log-num but no log-num entry. Ensure that pebble can read these + // directories without an issue. + _, err := vfs.Clone(vfs.Default, mem, "testdata/rocksdb-ingest-only", "testdata") + require.NoError(t, err) + + db, err := Open("testdata", opts) + require.NoError(t, err) + defer db.Close() + + val, closer, err := db.Get([]byte("ajulxeiombjiyw\x00\x00\x00\x00\x00\x00\x00\x01\x12\x09")) + require.NoError(t, err) + require.NotEmpty(t, val) + require.NoError(t, closer.Close()) +} + +func TestOpen_ErrorIfUnknownFormatVersion(t *testing.T) { + fs := vfs.NewMem() + d, err := Open("", &Options{ + FS: fs, + FormatMajorVersion: FormatVersioned, + }) + require.NoError(t, err) + require.NoError(t, d.Close()) + + // Move the marker to a version that does not exist. + m, _, err := atomicfs.LocateMarker(fs, "", formatVersionMarkerName) + require.NoError(t, err) + require.NoError(t, m.Move("999999")) + require.NoError(t, m.Close()) + + _, err = Open("", &Options{ + FS: fs, + FormatMajorVersion: FormatVersioned, + }) + require.Error(t, err) + require.EqualError(t, err, `pebble: database "" written in format major version 999999`) +} + +// ensureFilesClosed updates the provided Options to wrap the filesystem. It +// returns a closure that when invoked fails the test if any files opened by the +// filesystem are not closed. +// +// This function is intended to be used in tests with defer. +// +// opts := &Options{FS: vfs.NewMem()} +// defer ensureFilesClosed(t, opts)() +// /* test code */ +func ensureFilesClosed(t *testing.T, o *Options) func() { + fs := &closeTrackingFS{ + FS: o.FS, + files: map[*closeTrackingFile]struct{}{}, + } + o.FS = fs + return func() { + // fs.files should be empty if all the files were closed. + for f := range fs.files { + t.Errorf("An open file was never closed. Opened at:\n%s", f.stack) + } + } +} + +type closeTrackingFS struct { + vfs.FS + files map[*closeTrackingFile]struct{} +} + +func (fs *closeTrackingFS) wrap(file vfs.File, err error) (vfs.File, error) { + if err != nil { + return nil, err + } + f := &closeTrackingFile{ + File: file, + fs: fs, + stack: debug.Stack(), + } + fs.files[f] = struct{}{} + return f, err +} + +func (fs *closeTrackingFS) Create(name string) (vfs.File, error) { + return fs.wrap(fs.FS.Create(name)) +} + +func (fs *closeTrackingFS) Open(name string, opts ...vfs.OpenOption) (vfs.File, error) { + return fs.wrap(fs.FS.Open(name)) +} + +func (fs *closeTrackingFS) OpenDir(name string) (vfs.File, error) { + return fs.wrap(fs.FS.OpenDir(name)) +} + +func (fs *closeTrackingFS) ReuseForWrite(oldname, newname string) (vfs.File, error) { + return fs.wrap(fs.FS.ReuseForWrite(oldname, newname)) +} + +type closeTrackingFile struct { + vfs.File + fs *closeTrackingFS + stack []byte +} + +func (f *closeTrackingFile) Close() error { + delete(f.fs.files, f) + return f.File.Close() +} + +func TestCheckConsistency(t *testing.T) { + const dir = "./test" + mem := vfs.NewMem() + mem.MkdirAll(dir, 0755) + + provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(mem, dir)) + require.NoError(t, err) + defer provider.Close() + + cmp := base.DefaultComparer.Compare + fmtKey := base.DefaultComparer.FormatKey + parseMeta := func(s string) (*manifest.FileMetadata, error) { + if len(s) == 0 { + return nil, nil + } + parts := strings.Split(s, ":") + if len(parts) != 2 { + return nil, errors.Errorf("malformed table spec: %q", s) + } + fileNum, err := strconv.Atoi(strings.TrimSpace(parts[0])) + if err != nil { + return nil, err + } + size, err := strconv.Atoi(strings.TrimSpace(parts[1])) + if err != nil { + return nil, err + } + m := &manifest.FileMetadata{ + FileNum: base.FileNum(fileNum), + Size: uint64(size), + } + m.InitPhysicalBacking() + return m, nil + } + + datadriven.RunTest(t, "testdata/version_check_consistency", + func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "check-consistency": + var filesByLevel [manifest.NumLevels][]*manifest.FileMetadata + var files *[]*manifest.FileMetadata + + for _, data := range strings.Split(d.Input, "\n") { + switch data { + case "L0", "L1", "L2", "L3", "L4", "L5", "L6": + level, err := strconv.Atoi(data[1:]) + if err != nil { + return err.Error() + } + files = &filesByLevel[level] + + default: + m, err := parseMeta(data) + if err != nil { + return err.Error() + } + if m != nil { + *files = append(*files, m) + } + } + } + + redactErr := false + for _, arg := range d.CmdArgs { + switch v := arg.String(); v { + case "redact": + redactErr = true + default: + return fmt.Sprintf("unknown argument: %q", v) + } + } + + v := manifest.NewVersion(cmp, fmtKey, 0, filesByLevel) + err := checkConsistency(v, dir, provider) + if err != nil { + if redactErr { + redacted := redact.Sprint(err).Redact() + return string(redacted) + } + return err.Error() + } + return "OK" + + case "build": + for _, data := range strings.Split(d.Input, "\n") { + m, err := parseMeta(data) + if err != nil { + return err.Error() + } + path := base.MakeFilepath(mem, dir, base.FileTypeTable, m.FileBacking.DiskFileNum) + _ = mem.Remove(path) + f, err := mem.Create(path) + if err != nil { + return err.Error() + } + _, err = f.Write(make([]byte, m.Size)) + if err != nil { + return err.Error() + } + f.Close() + } + return "" + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} + +func TestOpenRatchetsNextFileNum(t *testing.T) { + mem := vfs.NewMem() + memShared := remote.NewInMem() + + opts := &Options{FS: mem} + opts.Experimental.CreateOnShared = remote.CreateOnSharedAll + opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": memShared, + }) + d, err := Open("", opts) + require.NoError(t, err) + d.SetCreatorID(1) + + require.NoError(t, d.Set([]byte("foo"), []byte("value"), nil)) + require.NoError(t, d.Set([]byte("bar"), []byte("value"), nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact([]byte("a"), []byte("z"), false)) + + // Create a shared file with the newest file num and then close the db. + d.mu.Lock() + nextFileNum := d.mu.versions.getNextFileNum() + w, _, err := d.objProvider.Create(context.TODO(), fileTypeTable, nextFileNum.DiskFileNum(), objstorage.CreateOptions{PreferSharedStorage: true}) + require.NoError(t, err) + require.NoError(t, w.Write([]byte("foobar"))) + require.NoError(t, w.Finish()) + require.NoError(t, d.objProvider.Sync()) + d.mu.Unlock() + + // Write one key and then close the db. This write will stay in the memtable, + // forcing the reopen to do a compaction on open. + require.NoError(t, d.Set([]byte("foo1"), []byte("value"), nil)) + require.NoError(t, d.Close()) + + // Reopen db. Compactions should happen without error. + d, err = Open("", opts) + require.NoError(t, err) + require.NoError(t, d.Set([]byte("foo2"), []byte("value"), nil)) + require.NoError(t, d.Set([]byte("bar2"), []byte("value"), nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact([]byte("a"), []byte("z"), false)) + +} diff --git a/pebble/options.go b/pebble/options.go new file mode 100644 index 0000000..8a2c609 --- /dev/null +++ b/pebble/options.go @@ -0,0 +1,1737 @@ +// 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 pebble + +import ( + "bytes" + "fmt" + "io" + "runtime" + "strconv" + "strings" + "time" + + "github.com/cockroachdb/errors" + "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/remote" + "github.com/cockroachdb/pebble/rangekey" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" +) + +const ( + cacheDefaultSize = 8 << 20 // 8 MB + defaultLevelMultiplier = 10 +) + +// Compression exports the base.Compression type. +type Compression = sstable.Compression + +// Exported Compression constants. +const ( + DefaultCompression = sstable.DefaultCompression + NoCompression = sstable.NoCompression + SnappyCompression = sstable.SnappyCompression + ZstdCompression = sstable.ZstdCompression +) + +// FilterType exports the base.FilterType type. +type FilterType = base.FilterType + +// Exported TableFilter constants. +const ( + TableFilter = base.TableFilter +) + +// FilterWriter exports the base.FilterWriter type. +type FilterWriter = base.FilterWriter + +// FilterPolicy exports the base.FilterPolicy type. +type FilterPolicy = base.FilterPolicy + +// TablePropertyCollector exports the sstable.TablePropertyCollector type. +type TablePropertyCollector = sstable.TablePropertyCollector + +// BlockPropertyCollector exports the sstable.BlockPropertyCollector type. +type BlockPropertyCollector = sstable.BlockPropertyCollector + +// BlockPropertyFilter exports the sstable.BlockPropertyFilter type. +type BlockPropertyFilter = base.BlockPropertyFilter + +// ShortAttributeExtractor exports the base.ShortAttributeExtractor type. +type ShortAttributeExtractor = base.ShortAttributeExtractor + +// UserKeyPrefixBound exports the sstable.UserKeyPrefixBound type. +type UserKeyPrefixBound = sstable.UserKeyPrefixBound + +// IterKeyType configures which types of keys an iterator should surface. +type IterKeyType int8 + +const ( + // IterKeyTypePointsOnly configures an iterator to iterate over point keys + // only. + IterKeyTypePointsOnly IterKeyType = iota + // IterKeyTypeRangesOnly configures an iterator to iterate over range keys + // only. + IterKeyTypeRangesOnly + // IterKeyTypePointsAndRanges configures an iterator iterate over both point + // keys and range keys simultaneously. + IterKeyTypePointsAndRanges +) + +// String implements fmt.Stringer. +func (t IterKeyType) String() string { + switch t { + case IterKeyTypePointsOnly: + return "points-only" + case IterKeyTypeRangesOnly: + return "ranges-only" + case IterKeyTypePointsAndRanges: + return "points-and-ranges" + default: + panic(fmt.Sprintf("unknown key type %d", t)) + } +} + +// IterOptions hold the optional per-query parameters for NewIter. +// +// Like Options, a nil *IterOptions is valid and means to use the default +// values. +type IterOptions struct { + // LowerBound specifies the smallest key (inclusive) that the iterator will + // return during iteration. If the iterator is seeked or iterated past this + // boundary the iterator will return Valid()==false. Setting LowerBound + // effectively truncates the key space visible to the iterator. + LowerBound []byte + // UpperBound specifies the largest key (exclusive) that the iterator will + // return during iteration. If the iterator is seeked or iterated past this + // boundary the iterator will return Valid()==false. Setting UpperBound + // effectively truncates the key space visible to the iterator. + UpperBound []byte + // TableFilter can be used to filter the tables that are scanned during + // iteration based on the user properties. Return true to scan the table and + // 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. 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 + // PointKeyFilters apply here too. + RangeKeyFilters []BlockPropertyFilter + // KeyTypes configures which types of keys to iterate over: point keys, + // range keys, or both. + KeyTypes IterKeyType + // RangeKeyMasking can be used to enable automatic masking of point keys by + // range keys. Range key masking is only supported during combined range key + // and point key iteration mode (IterKeyTypePointsAndRanges). + RangeKeyMasking RangeKeyMasking + + // OnlyReadGuaranteedDurable is an advanced option that is only supported by + // the Reader implemented by DB. When set to true, only the guaranteed to be + // durable state is visible in the iterator. + // - This definition is made under the assumption that the FS implementation + // is providing a durability guarantee when data is synced. + // - The visible state represents a consistent point in the history of the + // DB. + // - The implementation is free to choose a conservative definition of what + // is guaranteed durable. For simplicity, the current implementation + // ignores memtables. A more sophisticated implementation could track the + // highest seqnum that is synced to the WAL and published and use that as + // the visible seqnum for an iterator. Note that the latter approach is + // not strictly better than the former since we can have DBs that are (a) + // synced more rarely than memtable flushes, (b) have no WAL. (a) is + // likely to be true in a future CockroachDB context where the DB + // containing the state machine may be rarely synced. + // NB: this current implementation relies on the fact that memtables are + // flushed in seqnum order, and any ingested sstables that happen to have a + // lower seqnum than a non-flushed memtable don't have any overlapping keys. + // This is the fundamental level invariant used in other code too, like when + // merging iterators. + // + // Semantically, using this option provides the caller a "snapshot" as of + // the time the most recent memtable was flushed. An alternate interface + // would be to add a NewSnapshot variant. Creating a snapshot is heavier + // weight than creating an iterator, so we have opted to support this + // iterator option. + OnlyReadGuaranteedDurable bool + // UseL6Filters allows the caller to opt into reading filter blocks for L6 + // sstables. Helpful if a lot of SeekPrefixGEs are expected in quick + // succession, that are also likely to not yield a single key. Filter blocks in + // L6 can be relatively large, often larger than data blocks, so the benefit of + // loading them in the cache is minimized if the probability of the key + // existing is not low or if we just expect a one-time Seek (where loading the + // data block directly is better). + UseL6Filters bool + // CategoryAndQoS is used for categorized iterator stats. This should not be + // changed by calling SetOptions. + sstable.CategoryAndQoS + + // Internal options. + + logger Logger + // Level corresponding to this file. Only passed in if constructed by a + // levelIter. + 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. +} + +// GetLowerBound returns the LowerBound or nil if the receiver is nil. +func (o *IterOptions) GetLowerBound() []byte { + if o == nil { + return nil + } + return o.LowerBound +} + +// GetUpperBound returns the UpperBound or nil if the receiver is nil. +func (o *IterOptions) GetUpperBound() []byte { + if o == nil { + return nil + } + return o.UpperBound +} + +func (o *IterOptions) pointKeys() bool { + if o == nil { + return true + } + return o.KeyTypes == IterKeyTypePointsOnly || o.KeyTypes == IterKeyTypePointsAndRanges +} + +func (o *IterOptions) rangeKeys() bool { + if o == nil { + return false + } + return o.KeyTypes == IterKeyTypeRangesOnly || o.KeyTypes == IterKeyTypePointsAndRanges +} + +func (o *IterOptions) getLogger() Logger { + if o == nil || o.logger == nil { + return DefaultLogger + } + 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 { + sstable.CategoryAndQoS + 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 +// masking range key's bounds and have suffixes greater than the range key's +// suffix are automatically skipped. +// +// Specifically, when configured with a RangeKeyMasking.Suffix _s_, and there +// exists a range key with suffix _r_ covering a point key with suffix _p_, and +// +// _s_ ≤ _r_ < _p_ +// +// then the point key is elided. +// +// Range-key masking may only be used when iterating over both point keys and +// range keys with IterKeyTypePointsAndRanges. +type RangeKeyMasking struct { + // Suffix configures which range keys may mask point keys. Only range keys + // that are defined at suffixes greater than or equal to Suffix will mask + // point keys. + Suffix []byte + // Filter is an optional field that may be used to improve performance of + // range-key masking through a block-property filter defined over key + // suffixes. If non-nil, Filter is called by Pebble to construct a + // block-property filter mask at iterator creation. The filter is used to + // skip whole point-key blocks containing point keys with suffixes greater + // than a covering range-key's suffix. + // + // To use this functionality, the caller must create and configure (through + // Options.BlockPropertyCollectors) a block-property collector that records + // the maxmimum suffix contained within a block. The caller then must write + // and provide a BlockPropertyFilterMask implementation on that same + // property. See the BlockPropertyFilterMask type for more information. + Filter func() BlockPropertyFilterMask +} + +// BlockPropertyFilterMask extends the BlockPropertyFilter interface for use +// with range-key masking. Unlike an ordinary block property filter, a +// BlockPropertyFilterMask's filtering criteria is allowed to change when Pebble +// invokes its SetSuffix method. +// +// When a Pebble iterator steps into a range key's bounds and the range key has +// a suffix greater than or equal to RangeKeyMasking.Suffix, the range key acts +// as a mask. The masking range key hides all point keys that fall within the +// range key's bounds and have suffixes > the range key's suffix. Without a +// filter mask configured, Pebble performs this hiding by stepping through point +// keys and comparing suffixes. If large numbers of point keys are masked, this +// requires Pebble to load, iterate through and discard a large number of +// sstable blocks containing masked point keys. +// +// If a block-property collector and a filter mask are configured, Pebble may +// skip loading some point-key blocks altogether. If a block's keys are known to +// all fall within the bounds of the masking range key and the block was +// annotated by a block-property collector with the maximal suffix, Pebble can +// ask the filter mask to compare the property to the current masking range +// key's suffix. If the mask reports no intersection, the block may be skipped. +// +// If unsuffixed and suffixed keys are written to the database, care must be +// taken to avoid unintentionally masking un-suffixed keys located in the same +// block as suffixed keys. One solution is to interpret unsuffixed keys as +// containing the maximal suffix value, ensuring that blocks containing +// unsuffixed keys are always loaded. +type BlockPropertyFilterMask interface { + BlockPropertyFilter + + // SetSuffix configures the mask with the suffix of a range key. The filter + // should return false from Intersects whenever it's provided with a + // property encoding a block's minimum suffix that's greater (according to + // Compare) than the provided suffix. + SetSuffix(suffix []byte) error +} + +// WriteOptions hold the optional per-query parameters for Set and Delete +// operations. +// +// Like Options, a nil *WriteOptions is valid and means to use the default +// values. +type WriteOptions struct { + // Sync is whether to sync writes through the OS buffer cache and down onto + // the actual disk, if applicable. Setting Sync is required for durability of + // individual write operations but can result in slower writes. + // + // If false, and the process or machine crashes, then a recent write may be + // lost. This is due to the recently written data being buffered inside the + // process running Pebble. This differs from the semantics of a write system + // call in which the data is buffered in the OS buffer cache and would thus + // survive a process crash. + // + // The default value is true. + Sync bool +} + +// Sync specifies the default write options for writes which synchronize to +// disk. +var Sync = &WriteOptions{Sync: true} + +// NoSync specifies the default write options for writes which do not +// synchronize to disk. +var NoSync = &WriteOptions{Sync: false} + +// GetSync returns the Sync value or true if the receiver is nil. +func (o *WriteOptions) GetSync() bool { + return o == nil || o.Sync +} + +// LevelOptions holds the optional per-level parameters. +type LevelOptions struct { + // BlockRestartInterval is the number of keys between restart points + // for delta encoding of keys. + // + // The default value is 16. + BlockRestartInterval int + + // BlockSize is the target uncompressed size in bytes of each table block. + // + // The default value is 4096. + BlockSize int + + // BlockSizeThreshold finishes a block if the block size is larger than the + // specified percentage of the target block size and adding the next entry + // would cause the block to be larger than the target block size. + // + // The default value is 90 + BlockSizeThreshold int + + // Compression defines the per-block compression to use. + // + // The default value (DefaultCompression) uses snappy compression. + Compression Compression + + // FilterPolicy defines a filter algorithm (such as a Bloom filter) that can + // reduce disk reads for Get calls. + // + // One such implementation is bloom.FilterPolicy(10) from the pebble/bloom + // package. + // + // The default value means to use no filter. + FilterPolicy FilterPolicy + + // FilterType defines whether an existing filter policy is applied at a + // block-level or table-level. Block-level filters use less memory to create, + // but are slower to access as a check for the key in the index must first be + // performed to locate the filter block. A table-level filter will require + // memory proportional to the number of keys in an sstable to create, but + // avoids the index lookup when determining if a key is present. Table-level + // filters should be preferred except under constrained memory situations. + FilterType FilterType + + // IndexBlockSize is the target uncompressed size in bytes of each index + // block. When the index block size is larger than this target, two-level + // indexes are automatically enabled. Setting this option to a large value + // (such as math.MaxInt32) disables the automatic creation of two-level + // indexes. + // + // The default value is the value of BlockSize. + IndexBlockSize int + + // The target file size for the level. + TargetFileSize int64 +} + +// EnsureDefaults ensures that the default values for all of the options have +// been initialized. It is valid to call EnsureDefaults on a nil receiver. A +// non-nil result will always be returned. +func (o *LevelOptions) EnsureDefaults() *LevelOptions { + if o == nil { + o = &LevelOptions{} + } + if o.BlockRestartInterval <= 0 { + o.BlockRestartInterval = base.DefaultBlockRestartInterval + } + if o.BlockSize <= 0 { + o.BlockSize = base.DefaultBlockSize + } else if o.BlockSize > sstable.MaximumBlockSize { + panic(errors.Errorf("BlockSize %d exceeds MaximumBlockSize", o.BlockSize)) + } + if o.BlockSizeThreshold <= 0 { + o.BlockSizeThreshold = base.DefaultBlockSizeThreshold + } + if o.Compression <= DefaultCompression || o.Compression >= sstable.NCompression { + o.Compression = SnappyCompression + } + if o.IndexBlockSize <= 0 { + o.IndexBlockSize = o.BlockSize + } + if o.TargetFileSize <= 0 { + o.TargetFileSize = 2 << 20 // 2 MB + } + return o +} + +// Options holds the optional parameters for configuring pebble. These options +// apply to the DB at large; per-query options are defined by the IterOptions +// and WriteOptions types. +type Options struct { + // Sync sstables periodically in order to smooth out writes to disk. This + // option does not provide any persistency guarantee, but is used to avoid + // latency spikes if the OS automatically decides to write out a large chunk + // of dirty filesystem buffers. This option only controls SSTable syncs; WAL + // syncs are controlled by WALBytesPerSync. + // + // The default value is 512KB. + BytesPerSync int + + // Cache is used to cache uncompressed blocks from sstables. + // + // The default cache size is 8 MB. + Cache *cache.Cache + + // Cleaner cleans obsolete files. + // + // The default cleaner uses the DeleteCleaner. + Cleaner Cleaner + + // 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. + // + // The default value uses the same ordering as bytes.Compare. + Comparer *Comparer + + // DebugCheck is invoked, if non-nil, whenever a new version is being + // installed. Typically, this is set to pebble.DebugCheckLevels in tests + // or tools only, to check invariants over all the data in the database. + DebugCheck func(*DB) error + + // Disable the write-ahead log (WAL). Disabling the write-ahead log prohibits + // crash recovery, but can improve performance if crash recovery is not + // needed (e.g. when only temporary state is being stored in the database). + // + // TODO(peter): untested + DisableWAL bool + + // ErrorIfExists causes an error on Open if the database already exists. + // The error can be checked with errors.Is(err, ErrDBAlreadyExists). + // + // The default value is false. + ErrorIfExists bool + + // ErrorIfNotExists causes an error on Open if the database does not already + // exist. The error can be checked with errors.Is(err, ErrDBDoesNotExist). + // + // The default value is false which will cause a database to be created if it + // does not already exist. + ErrorIfNotExists bool + + // ErrorIfNotPristine causes an error on Open if the database already exists + // and any operations have been performed on the database. The error can be + // checked with errors.Is(err, ErrDBNotPristine). + // + // Note that a database that contained keys that were all subsequently deleted + // may or may not trigger the error. Currently, we check if there are any live + // SSTs or log records to replay. + ErrorIfNotPristine bool + + // EventListener provides hooks to listening to significant DB events such as + // flushes, compactions, and table deletion. + EventListener *EventListener + + // Experimental contains experimental options which are off by default. + // These options are temporary and will eventually either be deleted, moved + // out of the experimental group, or made the non-adjustable default. These + // options may change at any time, so do not rely on them. + Experimental struct { + // The threshold of L0 read-amplification at which compaction concurrency + // is enabled (if CompactionDebtConcurrency was not already exceeded). + // Every multiple of this value enables another concurrent + // compaction up to MaxConcurrentCompactions. + L0CompactionConcurrency int + + // CompactionDebtConcurrency controls the threshold of compaction debt + // at which additional compaction concurrency slots are added. For every + // multiple of this value in compaction debt bytes, an additional + // 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 uint64 + + // 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: + // + // AllowedSeeks = FileSize / ReadCompactionRate + // + // From LevelDB: + // ``` + // We arrange to automatically compact this file after + // a certain number of seeks. Let's assume: + // (1) One seek costs 10ms + // (2) Writing or reading 1MB costs 10ms (100MB/s) + // (3) A compaction of 1MB does 25MB of IO: + // 1MB read from this level + // 10-12MB read from next level (boundaries may be misaligned) + // 10-12MB written to next level + // This implies that 25 seeks cost the same as the compaction + // of 1MB of data. I.e., one seek costs approximately the + // same as the compaction of 40KB of data. We are a little + // conservative and allow approximately one seek for every 16KB + // of data before triggering a compaction. + // ``` + ReadCompactionRate int64 + + // ReadSamplingMultiplier is a multiplier for the readSamplingPeriod in + // iterator.maybeSampleRead() to control the frequency of read sampling + // to trigger a read triggered compaction. A value of -1 prevents sampling + // and disables read triggered compactions. The default is 1 << 4. which + // gets multiplied with a constant of 1 << 16 to yield 1 << 20 (1MB). + ReadSamplingMultiplier int64 + + // TableCacheShards is the number of shards per table cache. + // Reducing the value can reduce the number of idle goroutines per DB + // instance which can be useful in scenarios with a lot of DB instances + // and a large number of CPUs, but doing so can lead to higher contention + // in the table cache and reduced performance. + // + // The default value is the number of logical CPUs, which can be + // limited by runtime.GOMAXPROCS. + TableCacheShards int + + // KeyValidationFunc is a function to validate a user key in an SSTable. + // + // Currently, this function is used to validate the smallest and largest + // keys in an SSTable undergoing compaction. In this case, returning an + // error from the validation function will result in a panic at runtime, + // given that there is rarely any way of recovering from malformed keys + // present in compacted files. By default, validation is not performed. + // + // Additional use-cases may be added in the future. + // + // NOTE: callers should take care to not mutate the key being validated. + KeyValidationFunc func(userKey []byte) error + + // ValidateOnIngest schedules validation of sstables after they have + // been ingested. + // + // By default, this value is false. + ValidateOnIngest bool + + // LevelMultiplier configures the size multiplier used to determine the + // desired size of each level of the LSM. Defaults to 10. + LevelMultiplier int + + // MultiLevelCompactionHeuristic determines whether to add an additional + // level to a conventional two level compaction. If nil, a multilevel + // compaction will never get triggered. + MultiLevelCompactionHeuristic MultiLevelHeuristic + + // MaxWriterConcurrency is used to indicate the maximum number of + // compression workers the compression queue is allowed to use. If + // MaxWriterConcurrency > 0, then the Writer will use parallelism, to + // compress and write blocks to disk. Otherwise, the writer will + // compress and write blocks to disk synchronously. + MaxWriterConcurrency int + + // ForceWriterParallelism is used to force parallelism in the sstable + // Writer for the metamorphic tests. Even with the MaxWriterConcurrency + // option set, we only enable parallelism in the sstable Writer if there + // is enough CPU available, and this option bypasses that. + ForceWriterParallelism bool + + // CPUWorkPermissionGranter should be set if Pebble should be given the + // ability to optionally schedule additional CPU. See the documentation + // for CPUWorkPermissionGranter for more details. + CPUWorkPermissionGranter CPUWorkPermissionGranter + + // EnableValueBlocks is used to decide whether to enable writing + // 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 + // (else ignored). If non-nil, a ShortAttribute can be extracted from the + // value and stored with the key, when the value is stored elsewhere. + ShortAttributeExtractor ShortAttributeExtractor + + // RequiredInPlaceValueBound specifies an optional span of user key + // 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 + // a WAL write and memtable rotation. Only effectual if the the format + // major version is at least `FormatFlushableIngest`. + DisableIngestAsFlushable func() bool + + // 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 + } + + // 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 + // map during normal usage of a DB. + Filters map[string]FilterPolicy + + // FlushDelayDeleteRange configures how long the database should wait before + // forcing a flush of a memtable that contains a range deletion. Disk space + // cannot be reclaimed until the range deletion is flushed. No automatic + // flush occurs if zero. + FlushDelayDeleteRange time.Duration + + // FlushDelayRangeKey configures how long the database should wait before + // forcing a flush of a memtable that contains a range key. Range keys in + // the memtable prevent lazy combined iteration, so it's desirable to flush + // range keys promptly. No automatic flush occurs if zero. + FlushDelayRangeKey time.Duration + + // FlushSplitBytes denotes the target number of bytes per sublevel in + // each flush split interval (i.e. range between two flush split keys) + // in L0 sstables. When set to zero, only a single sstable is generated + // by each flush. When set to a non-zero value, flushes are split at + // points to meet L0's TargetFileSize, any grandparent-related overlap + // options, and at boundary keys of L0 flush split intervals (which are + // targeted to contain around FlushSplitBytes bytes in each sublevel + // between pairs of boundary keys). Splitting sstables during flush + // allows increased compaction flexibility and concurrency when those + // tables are compacted to lower levels. + FlushSplitBytes int64 + + // FormatMajorVersion sets the format of on-disk files. It is + // recommended to set the format major version to an explicit + // version, as the default may change over time. + // + // At Open if the existing database is formatted using a later + // format major version that is known to this version of Pebble, + // Pebble will continue to use the later format major version. If + // the existing database's version is unknown, the caller may use + // FormatMostCompatible and will be able to open the database + // regardless of its actual version. + // + // If the existing database is formatted using a format major + // version earlier than the one specified, Open will automatically + // ratchet the database to the specified format major version. + FormatMajorVersion FormatMajorVersion + + // FS provides the interface for persistent file storage. + // + // The default value uses the underlying operating system's file system. + FS vfs.FS + + // Lock, if set, must be a database lock acquired through LockDirectory for + // the same directory passed to Open. If provided, Open will skip locking + // the directory. Closing the database will not release the lock, and it's + // the responsibility of the caller to release the lock after closing the + // database. + // + // Open will enforce that the Lock passed locks the same directory passed to + // Open. Concurrent calls to Open using the same Lock are detected and + // prohibited. + Lock *Lock + + // The count of L0 files necessary to trigger an L0 compaction. + L0CompactionFileThreshold int + + // The amount of L0 read-amplification necessary to trigger an L0 compaction. + L0CompactionThreshold int + + // Hard limit on L0 read-amplification, computed as the number of L0 + // sublevels. Writes are stopped when this threshold is reached. + L0StopWritesThreshold int + + // The maximum number of bytes for LBase. The base level is the level which + // L0 is compacted into. The base level is determined dynamically based on + // the existing data in the LSM. The maximum number of bytes for other levels + // is computed dynamically based on the base level's maximum size. When the + // maximum number of bytes for a level is exceeded, compaction is requested. + LBaseMaxBytes int64 + + // Per-level options. Options for at least one level must be specified. The + // options for the last level are used for all subsequent levels. + Levels []LevelOptions + + // LoggerAndTracer will be used, if non-nil, else Logger will be used and + // tracing will be a noop. + + // Logger used to write log messages. + // + // The default logger uses the Go standard library log package. + Logger Logger + // LoggerAndTracer is used for writing log messages and traces. + LoggerAndTracer LoggerAndTracer + + // MaxManifestFileSize is the maximum size the MANIFEST file is allowed to + // become. When the MANIFEST exceeds this size it is rolled over and a new + // MANIFEST is created. + MaxManifestFileSize int64 + + // MaxOpenFiles is a soft limit on the number of open files that can be + // used by the DB. + // + // The default value is 1000. + MaxOpenFiles int + + // The size of a MemTable in steady state. The actual MemTable size starts at + // min(256KB, MemTableSize) and doubles for each subsequent MemTable up to + // MemTableSize. This reduces the memory pressure caused by MemTables for + // short lived (test) DB instances. Note that more than one MemTable can be + // in existence since flushing a MemTable involves creating a new one and + // writing the contents of the old one in the + // background. MemTableStopWritesThreshold places a hard limit on the size of + // the queued MemTables. + // + // The default value is 4MB. + MemTableSize uint64 + + // 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 + // written with {Batch,DB}.Merge. + // + // The default merger concatenates values. + Merger *Merger + + // MaxConcurrentCompactions specifies the maximum number of concurrent + // compactions. The default is 1. Concurrent compactions are performed + // - when L0 read-amplification passes the L0CompactionConcurrency threshold + // - for automatic background compactions + // - when a manual compaction for a level is split and parallelized + // MaxConcurrentCompactions must be greater than 0. + MaxConcurrentCompactions func() int + + // DisableAutomaticCompactions dictates whether automatic compactions are + // scheduled or not. The default is false (enabled). This option is only used + // externally when running a manual compaction, and internally for tests. + DisableAutomaticCompactions bool + + // NoSyncOnClose decides whether the Pebble instance 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 + + // NumPrevManifest is the number of non-current or older manifests which + // we want to keep around for debugging purposes. By default, we're going + // to keep one older manifest. + NumPrevManifest int + + // ReadOnly indicates that the DB should be opened in read-only mode. Writes + // to the DB will return an error, background compactions are disabled, and + // the flush that normally occurs after replaying the WAL at startup is + // disabled. + ReadOnly bool + + // TableCache is an initialized TableCache which should be set as an + // option if the DB needs to be initialized with a pre-existing table cache. + // If TableCache is nil, then a table cache which is unique to the DB instance + // is created. TableCache can be shared between db instances by setting it here. + // The TableCache set here must use the same underlying cache as Options.Cache + // and pebble will panic otherwise. + TableCache *TableCache + + // 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. + TablePropertyCollectors []func() TablePropertyCollector + + // BlockPropertyCollectors is a list of BlockPropertyCollector creation + // functions. A new BlockPropertyCollector is created for each sstable + // built and lives for the lifetime of writing that table. + BlockPropertyCollectors []func() BlockPropertyCollector + + // WALBytesPerSync sets the number of bytes to write to a WAL before calling + // Sync on it in the background. Just like with BytesPerSync above, this + // helps smooth out disk write latencies, and avoids cases where the OS + // writes a lot of buffered data to disk at once. However, this is less + // necessary with WALs, as many write operations already pass in + // Sync = true. + // + // The default value is 0, i.e. no background syncing. This matches the + // default behaviour in RocksDB. + WALBytesPerSync int + + // WALDir specifies the directory to store write-ahead logs (WALs) in. If + // empty (the default), WALs will be stored in the same directory as sstables + // (i.e. the directory passed to pebble.Open). + WALDir string + + // WALMinSyncInterval is the minimum duration between syncs of the WAL. If + // WAL syncs are requested faster than this interval, they will be + // artificially delayed. Introducing a small artificial delay (500us) between + // WAL syncs can allow more operations to arrive and reduce IO operations + // while having a minimal impact on throughput. This option is supplied as a + // closure in order to allow the value to be changed dynamically. The default + // value is 0. + // + // TODO(peter): rather than a closure, should there be another mechanism for + // 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 { + // strictWALTail configures whether or not a database's WALs created + // prior to the most recent one should be interpreted strictly, + // requiring a clean EOF. RocksDB 6.2.1 and the version of Pebble + // included in CockroachDB 20.1 do not guarantee that closed WALs end + // cleanly. If this option is set within an OPTIONS file, Pebble + // interprets previous WALs strictly, requiring a clean EOF. + // Otherwise, it interprets them permissively in the same manner as + // RocksDB 6.2.1. + strictWALTail bool + + // disableDeleteOnlyCompactions prevents the scheduling of delete-only + // compactions that drop sstables wholy covered by range tombstones or + // range key tombstones. + disableDeleteOnlyCompactions bool + + // disableElisionOnlyCompactions prevents the scheduling of elision-only + // compactions that rewrite sstables in place in order to elide obsolete + // keys. + disableElisionOnlyCompactions bool + + // disableLazyCombinedIteration is a private option used by the + // metamorphic tests to test equivalence between lazy-combined iteration + // and constructing the range-key iterator upfront. It's a private + // option to avoid littering the public interface with options that we + // do not want to allow users to actually configure. + disableLazyCombinedIteration bool + + // 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. + // See the initialization of FS in EnsureDefaults. Note that care has + // been taken to ensure that it is still safe to continue using the FS + // after this closer has been invoked. However, if write operations + // against the FS are made after the DB is closed, the FS may leak a + // goroutine indefinitely. + fsCloser io.Closer + } +} + +// DebugCheckLevels calls CheckLevels on the provided database. +// It may be set in the DebugCheck field of Options to check +// level invariants whenever a new version is installed. +func DebugCheckLevels(db *DB) error { + return db.CheckLevels(nil) +} + +// EnsureDefaults ensures that the default values for all options are set if a +// valid value was not already specified. Returns the new options. +func (o *Options) EnsureDefaults() *Options { + if o == nil { + o = &Options{} + } + if o.BytesPerSync <= 0 { + o.BytesPerSync = 512 << 10 // 512 KB + } + if o.Cleaner == nil { + o.Cleaner = DeleteCleaner{} + } + if o.Comparer == nil { + o.Comparer = DefaultComparer + } + if o.Experimental.DisableIngestAsFlushable == nil { + o.Experimental.DisableIngestAsFlushable = func() bool { return false } + } + if o.Experimental.L0CompactionConcurrency <= 0 { + o.Experimental.L0CompactionConcurrency = 10 + } + if o.Experimental.CompactionDebtConcurrency <= 0 { + o.Experimental.CompactionDebtConcurrency = 1 << 30 // 1 GB + } + if o.Experimental.KeyValidationFunc == nil { + o.Experimental.KeyValidationFunc = func([]byte) error { return nil } + } + if o.L0CompactionThreshold <= 0 { + o.L0CompactionThreshold = 4 + } + if o.L0CompactionFileThreshold <= 0 { + // Some justification for the default of 500: + // Why not smaller?: + // - The default target file size for L0 is 2MB, so 500 files is <= 1GB + // of data. At observed compaction speeds of > 20MB/s, L0 can be + // cleared of all files in < 1min, so this backlog is not huge. + // - 500 files is low overhead for instantiating L0 sublevels from + // scratch. + // - Lower values were observed to cause excessive and inefficient + // compactions out of L0 in a TPCC import benchmark. + // Why not larger?: + // - More than 1min to compact everything out of L0. + // - CockroachDB's admission control system uses a threshold of 1000 + // files to start throttling writes to Pebble. Using 500 here gives + // us headroom between when Pebble should start compacting L0 and + // when the admission control threshold is reached. + // + // We can revisit this default in the future based on better + // experimental understanding. + // + // TODO(jackson): Experiment with slightly lower thresholds [or higher + // admission control thresholds] to see whether a higher L0 score at the + // threshold (currently 2.0) is necessary for some workloads to avoid + // starving L0 in favor of lower-level compactions. + o.L0CompactionFileThreshold = 500 + } + if o.L0StopWritesThreshold <= 0 { + o.L0StopWritesThreshold = 12 + } + if o.LBaseMaxBytes <= 0 { + o.LBaseMaxBytes = 64 << 20 // 64 MB + } + if o.Levels == nil { + o.Levels = make([]LevelOptions, 1) + for i := range o.Levels { + if i > 0 { + l := &o.Levels[i] + if l.TargetFileSize <= 0 { + l.TargetFileSize = o.Levels[i-1].TargetFileSize * 2 + } + } + o.Levels[i].EnsureDefaults() + } + } else { + for i := range o.Levels { + o.Levels[i].EnsureDefaults() + } + } + if o.Logger == nil { + o.Logger = DefaultLogger + } + if o.EventListener == nil { + o.EventListener = &EventListener{} + } + o.EventListener.EnsureDefaults(o.Logger) + if o.MaxManifestFileSize == 0 { + o.MaxManifestFileSize = 128 << 20 // 128 MB + } + if o.MaxOpenFiles == 0 { + o.MaxOpenFiles = 1000 + } + if o.MemTableSize <= 0 { + o.MemTableSize = 4 << 20 // 4 MB + } + if o.MemTableStopWritesThreshold <= 0 { + o.MemTableStopWritesThreshold = 2 + } + if o.Merger == nil { + o.Merger = DefaultMerger + } + o.private.strictWALTail = true + if o.MaxConcurrentCompactions == nil { + o.MaxConcurrentCompactions = func() int { return 1 } + } + if o.NumPrevManifest <= 0 { + o.NumPrevManifest = 1 + } + + if o.FormatMajorVersion == FormatDefault { + o.FormatMajorVersion = FormatMostCompatible + } + + if o.FS == nil { + o.WithFSDefaults() + } + if o.FlushSplitBytes <= 0 { + o.FlushSplitBytes = 2 * o.Levels[0].TargetFileSize + } + if o.Experimental.LevelMultiplier <= 0 { + o.Experimental.LevelMultiplier = defaultLevelMultiplier + } + if o.Experimental.ReadCompactionRate == 0 { + o.Experimental.ReadCompactionRate = 16000 + } + if o.Experimental.ReadSamplingMultiplier == 0 { + o.Experimental.ReadSamplingMultiplier = 1 << 4 + } + if o.Experimental.TableCacheShards <= 0 { + o.Experimental.TableCacheShards = runtime.GOMAXPROCS(0) + } + if o.Experimental.CPUWorkPermissionGranter == nil { + o.Experimental.CPUWorkPermissionGranter = defaultCPUWorkGranter{} + } + if o.Experimental.MultiLevelCompactionHeuristic == nil { + o.Experimental.MultiLevelCompactionHeuristic = WriteAmpHeuristic{} + } + + o.initMaps() + return o +} + +// WithFSDefaults configures the Options to wrap the configured filesystem with +// the default virtual file system middleware, like disk-health checking. +func (o *Options) WithFSDefaults() *Options { + if o.FS == nil { + o.FS = vfs.Default + } + o.FS, o.private.fsCloser = vfs.WithDiskHealthChecks(o.FS, 5*time.Second, + func(info vfs.DiskSlowInfo) { + o.EventListener.DiskSlow(info) + }) + return o +} + +// AddEventListener adds the provided event listener to the Options, in addition +// to any existing event listener. +func (o *Options) AddEventListener(l EventListener) { + if o.EventListener != nil { + l = TeeEventListener(l, *o.EventListener) + } + o.EventListener = &l +} + +func (o *Options) equal() Equal { + if o.Comparer.Equal == nil { + return bytes.Equal + } + return o.Comparer.Equal +} + +// initMaps initializes the Comparers, Filters, and Mergers maps. +func (o *Options) initMaps() { + for i := range o.Levels { + l := &o.Levels[i] + if l.FilterPolicy != nil { + if o.Filters == nil { + o.Filters = make(map[string]FilterPolicy) + } + name := l.FilterPolicy.Name() + if _, ok := o.Filters[name]; !ok { + o.Filters[name] = l.FilterPolicy + } + } + } +} + +// Level returns the LevelOptions for the specified level. +func (o *Options) Level(level int) LevelOptions { + if level < len(o.Levels) { + return o.Levels[level] + } + n := len(o.Levels) - 1 + l := o.Levels[n] + for i := n; i < level; i++ { + l.TargetFileSize *= 2 + } + return l +} + +// Clone creates a shallow-copy of the supplied options. +func (o *Options) Clone() *Options { + n := &Options{} + if o != nil { + *n = *o + } + return n +} + +func filterPolicyName(p FilterPolicy) string { + if p == nil { + return "none" + } + return p.Name() +} + +func (o *Options) String() string { + var buf bytes.Buffer + + cacheSize := int64(cacheDefaultSize) + if o.Cache != nil { + cacheSize = o.Cache.MaxSize() + } + + fmt.Fprintf(&buf, "[Version]\n") + fmt.Fprintf(&buf, " pebble_version=0.1\n") + fmt.Fprintf(&buf, "\n") + fmt.Fprintf(&buf, "[Options]\n") + fmt.Fprintf(&buf, " bytes_per_sync=%d\n", o.BytesPerSync) + fmt.Fprintf(&buf, " cache_size=%d\n", cacheSize) + fmt.Fprintf(&buf, " cleaner=%s\n", o.Cleaner) + fmt.Fprintf(&buf, " compaction_debt_concurrency=%d\n", o.Experimental.CompactionDebtConcurrency) + fmt.Fprintf(&buf, " comparer=%s\n", o.Comparer.Name) + fmt.Fprintf(&buf, " disable_wal=%t\n", o.DisableWAL) + if o.Experimental.DisableIngestAsFlushable != nil && o.Experimental.DisableIngestAsFlushable() { + fmt.Fprintf(&buf, " disable_ingest_as_flushable=%t\n", true) + } + fmt.Fprintf(&buf, " flush_delay_delete_range=%s\n", o.FlushDelayDeleteRange) + fmt.Fprintf(&buf, " flush_delay_range_key=%s\n", o.FlushDelayRangeKey) + fmt.Fprintf(&buf, " flush_split_bytes=%d\n", o.FlushSplitBytes) + fmt.Fprintf(&buf, " format_major_version=%d\n", o.FormatMajorVersion) + fmt.Fprintf(&buf, " l0_compaction_concurrency=%d\n", o.Experimental.L0CompactionConcurrency) + fmt.Fprintf(&buf, " l0_compaction_file_threshold=%d\n", o.L0CompactionFileThreshold) + fmt.Fprintf(&buf, " l0_compaction_threshold=%d\n", o.L0CompactionThreshold) + fmt.Fprintf(&buf, " l0_stop_writes_threshold=%d\n", o.L0StopWritesThreshold) + fmt.Fprintf(&buf, " lbase_max_bytes=%d\n", o.LBaseMaxBytes) + if o.Experimental.LevelMultiplier != defaultLevelMultiplier { + fmt.Fprintf(&buf, " level_multiplier=%d\n", o.Experimental.LevelMultiplier) + } + fmt.Fprintf(&buf, " max_concurrent_compactions=%d\n", o.MaxConcurrentCompactions()) + fmt.Fprintf(&buf, " max_manifest_file_size=%d\n", o.MaxManifestFileSize) + 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.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) + fmt.Fprintf(&buf, " strict_wal_tail=%t\n", o.private.strictWALTail) + fmt.Fprintf(&buf, " table_cache_shards=%d\n", o.Experimental.TableCacheShards) + fmt.Fprintf(&buf, " table_property_collectors=[") + for i := range o.TablePropertyCollectors { + if i > 0 { + fmt.Fprintf(&buf, ",") + } + // NB: This creates a new TablePropertyCollector, but Options.String() is + // called rarely so the overhead of doing so is not consequential. + fmt.Fprintf(&buf, "%s", o.TablePropertyCollectors[i]().Name()) + } + fmt.Fprintf(&buf, "]\n") + fmt.Fprintf(&buf, " validate_on_ingest=%t\n", o.Experimental.ValidateOnIngest) + fmt.Fprintf(&buf, " wal_dir=%s\n", o.WALDir) + 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. + // + // These options are only encoded if true, because we do not want them to + // appear in production serialized Options files, since they're testing-only + // options. They're only serialized when true, which still ensures that the + // metamorphic tests may propagate them to subprocesses. + if o.private.disableDeleteOnlyCompactions { + fmt.Fprintln(&buf, " disable_delete_only_compactions=true") + } + if o.private.disableElisionOnlyCompactions { + fmt.Fprintln(&buf, " disable_elision_only_compactions=true") + } + if o.private.disableLazyCombinedIteration { + fmt.Fprintln(&buf, " disable_lazy_combined_iteration=true") + } + + for i := range o.Levels { + l := &o.Levels[i] + fmt.Fprintf(&buf, "\n") + fmt.Fprintf(&buf, "[Level \"%d\"]\n", i) + fmt.Fprintf(&buf, " block_restart_interval=%d\n", l.BlockRestartInterval) + fmt.Fprintf(&buf, " block_size=%d\n", l.BlockSize) + fmt.Fprintf(&buf, " block_size_threshold=%d\n", l.BlockSizeThreshold) + fmt.Fprintf(&buf, " compression=%s\n", l.Compression) + fmt.Fprintf(&buf, " filter_policy=%s\n", filterPolicyName(l.FilterPolicy)) + fmt.Fprintf(&buf, " filter_type=%s\n", l.FilterType) + fmt.Fprintf(&buf, " index_block_size=%d\n", l.IndexBlockSize) + fmt.Fprintf(&buf, " target_file_size=%d\n", l.TargetFileSize) + } + + return buf.String() +} + +func parseOptions(s string, fn func(section, key, value string) error) error { + var section string + for _, line := range strings.Split(s, "\n") { + line = strings.TrimSpace(line) + if len(line) == 0 { + // Skip blank lines. + continue + } + if line[0] == ';' || line[0] == '#' { + // Skip comments. + continue + } + n := len(line) + if line[0] == '[' && line[n-1] == ']' { + // Parse section. + section = line[1 : n-1] + continue + } + + pos := strings.Index(line, "=") + if pos < 0 { + const maxLen = 50 + if len(line) > maxLen { + line = line[:maxLen-3] + "..." + } + return base.CorruptionErrorf("invalid key=value syntax: %q", errors.Safe(line)) + } + + key := strings.TrimSpace(line[:pos]) + value := strings.TrimSpace(line[pos+1:]) + + // RocksDB uses a similar (INI-style) syntax for the OPTIONS file, but + // different section names and keys. The "CFOptions ..." paths are the + // RocksDB versions which we map to the Pebble paths. + mappedSection := section + if section == `CFOptions "default"` { + mappedSection = "Options" + switch key { + case "comparator": + key = "comparer" + case "merge_operator": + key = "merger" + } + } + + if err := fn(mappedSection, key, value); err != nil { + return err + } + } + return nil +} + +// ParseHooks contains callbacks to create options fields which can have +// user-defined implementations. +type ParseHooks struct { + NewCache func(size int64) *Cache + NewCleaner func(name string) (Cleaner, error) + NewComparer func(name string) (*Comparer, error) + NewFilterPolicy func(name string) (FilterPolicy, error) + NewMerger func(name string) (*Merger, error) + SkipUnknown func(name, value string) bool +} + +// Parse parses the options from the specified string. Note that certain +// options cannot be parsed into populated fields. For example, comparer and +// merger. +func (o *Options) Parse(s string, hooks *ParseHooks) error { + return parseOptions(s, func(section, key, value string) error { + // WARNING: DO NOT remove entries from the switches below because doing so + // causes a key previously written to the OPTIONS file to be considered unknown, + // a backwards incompatible change. Instead, leave in support for parsing the + // key but simply don't parse the value. + + switch { + case section == "Version": + switch key { + case "pebble_version": + default: + if hooks != nil && hooks.SkipUnknown != nil && hooks.SkipUnknown(section+"."+key, value) { + return nil + } + return errors.Errorf("pebble: unknown option: %s.%s", + errors.Safe(section), errors.Safe(key)) + } + return nil + + case section == "Options": + var err error + switch key { + case "bytes_per_sync": + o.BytesPerSync, err = strconv.Atoi(value) + case "cache_size": + var n int64 + n, err = strconv.ParseInt(value, 10, 64) + if err == nil && hooks != nil && hooks.NewCache != nil { + if o.Cache != nil { + o.Cache.Unref() + } + o.Cache = hooks.NewCache(n) + } + // We avoid calling cache.New in parsing because it makes it + // too easy to leak a cache. + case "cleaner": + switch value { + case "archive": + o.Cleaner = ArchiveCleaner{} + case "delete": + o.Cleaner = DeleteCleaner{} + default: + if hooks != nil && hooks.NewCleaner != nil { + o.Cleaner, err = hooks.NewCleaner(value) + } + } + case "comparer": + switch value { + case "leveldb.BytewiseComparator": + o.Comparer = DefaultComparer + default: + if hooks != nil && hooks.NewComparer != nil { + o.Comparer, err = hooks.NewComparer(value) + } + } + case "compaction_debt_concurrency": + 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`. + o.FlushDelayDeleteRange, err = time.ParseDuration(value) + case "disable_delete_only_compactions": + o.private.disableDeleteOnlyCompactions, err = strconv.ParseBool(value) + case "disable_elision_only_compactions": + o.private.disableElisionOnlyCompactions, err = strconv.ParseBool(value) + case "disable_ingest_as_flushable": + var v bool + v, err = strconv.ParseBool(value) + if err == nil { + o.Experimental.DisableIngestAsFlushable = func() bool { return v } + } + case "disable_lazy_combined_iteration": + o.private.disableLazyCombinedIteration, err = strconv.ParseBool(value) + case "disable_wal": + o.DisableWAL, err = strconv.ParseBool(value) + case "flush_delay_delete_range": + o.FlushDelayDeleteRange, err = time.ParseDuration(value) + case "flush_delay_range_key": + o.FlushDelayRangeKey, err = time.ParseDuration(value) + case "flush_split_bytes": + o.FlushSplitBytes, err = strconv.ParseInt(value, 10, 64) + case "format_major_version": + // NB: The version written here may be stale. Open does + // not use the format major version encoded in the + // OPTIONS file other than to validate that the encoded + // version is valid right here. + var v uint64 + v, err = strconv.ParseUint(value, 10, 64) + if vers := FormatMajorVersion(v); vers > internalFormatNewest || vers == FormatDefault { + err = errors.Newf("unknown format major version %d", o.FormatMajorVersion) + } + if err == nil { + o.FormatMajorVersion = FormatMajorVersion(v) + } + case "l0_compaction_concurrency": + o.Experimental.L0CompactionConcurrency, err = strconv.Atoi(value) + case "l0_compaction_file_threshold": + o.L0CompactionFileThreshold, err = strconv.Atoi(value) + case "l0_compaction_threshold": + o.L0CompactionThreshold, err = strconv.Atoi(value) + case "l0_stop_writes_threshold": + o.L0StopWritesThreshold, err = strconv.Atoi(value) + case "l0_sublevel_compactions": + // Do nothing; option existed in older versions of pebble. + case "lbase_max_bytes": + o.LBaseMaxBytes, err = strconv.ParseInt(value, 10, 64) + case "level_multiplier": + o.Experimental.LevelMultiplier, err = strconv.Atoi(value) + case "max_concurrent_compactions": + var concurrentCompactions int + concurrentCompactions, err = strconv.Atoi(value) + if concurrentCompactions <= 0 { + err = errors.New("max_concurrent_compactions cannot be <= 0") + } else { + o.MaxConcurrentCompactions = func() int { return concurrentCompactions } + } + case "max_manifest_file_size": + o.MaxManifestFileSize, err = strconv.ParseInt(value, 10, 64) + case "max_open_files": + o.MaxOpenFiles, err = strconv.Atoi(value) + case "mem_table_size": + 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.TargetByteDeletionRate, err = strconv.Atoi(value) + case "min_flush_rate": + // Do nothing; option existed in older versions of pebble, and + // may be meaningful again eventually. + case "point_tombstone_weight": + // Do nothing; deprecated. + case "strict_wal_tail": + o.private.strictWALTail, err = strconv.ParseBool(value) + case "merger": + switch value { + case "nullptr": + o.Merger = nil + case "pebble.concatenate": + o.Merger = DefaultMerger + default: + if hooks != nil && hooks.NewMerger != nil { + o.Merger, err = hooks.NewMerger(value) + } + } + case "read_compaction_rate": + o.Experimental.ReadCompactionRate, err = strconv.ParseInt(value, 10, 64) + case "read_sampling_multiplier": + o.Experimental.ReadSamplingMultiplier, err = strconv.ParseInt(value, 10, 64) + case "table_cache_shards": + o.Experimental.TableCacheShards, err = strconv.Atoi(value) + case "table_format": + switch value { + case "leveldb": + case "rocksdbv2": + default: + return errors.Errorf("pebble: unknown table format: %q", errors.Safe(value)) + } + case "table_property_collectors": + // TODO(peter): set o.TablePropertyCollectors + case "validate_on_ingest": + o.Experimental.ValidateOnIngest, err = strconv.ParseBool(value) + case "wal_dir": + o.WALDir = value + case "wal_bytes_per_sync": + o.WALBytesPerSync, err = strconv.Atoi(value) + case "max_writer_concurrency": + 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 + } + return errors.Errorf("pebble: unknown option: %s.%s", + errors.Safe(section), errors.Safe(key)) + } + return err + + case strings.HasPrefix(section, "Level "): + var index int + if n, err := fmt.Sscanf(section, `Level "%d"`, &index); err != nil { + return err + } else if n != 1 { + if hooks != nil && hooks.SkipUnknown != nil && hooks.SkipUnknown(section, value) { + return nil + } + return errors.Errorf("pebble: unknown section: %q", errors.Safe(section)) + } + + if len(o.Levels) <= index { + newLevels := make([]LevelOptions, index+1) + copy(newLevels, o.Levels) + o.Levels = newLevels + } + l := &o.Levels[index] + + var err error + switch key { + case "block_restart_interval": + l.BlockRestartInterval, err = strconv.Atoi(value) + case "block_size": + l.BlockSize, err = strconv.Atoi(value) + case "block_size_threshold": + l.BlockSizeThreshold, err = strconv.Atoi(value) + case "compression": + switch value { + case "Default": + l.Compression = DefaultCompression + case "NoCompression": + l.Compression = NoCompression + case "Snappy": + l.Compression = SnappyCompression + case "ZSTD": + l.Compression = ZstdCompression + default: + return errors.Errorf("pebble: unknown compression: %q", errors.Safe(value)) + } + case "filter_policy": + if hooks != nil && hooks.NewFilterPolicy != nil { + l.FilterPolicy, err = hooks.NewFilterPolicy(value) + } + case "filter_type": + switch value { + case "table": + l.FilterType = TableFilter + default: + return errors.Errorf("pebble: unknown filter type: %q", errors.Safe(value)) + } + case "index_block_size": + l.IndexBlockSize, err = strconv.Atoi(value) + case "target_file_size": + l.TargetFileSize, err = strconv.ParseInt(value, 10, 64) + default: + if hooks != nil && hooks.SkipUnknown != nil && hooks.SkipUnknown(section+"."+key, value) { + return nil + } + return errors.Errorf("pebble: unknown option: %s.%s", errors.Safe(section), errors.Safe(key)) + } + return err + } + if hooks != nil && hooks.SkipUnknown != nil && hooks.SkipUnknown(section+"."+key, value) { + return nil + } + return errors.Errorf("pebble: unknown section: %q", errors.Safe(section)) + }) +} + +func (o *Options) checkOptions(s string) (strictWALTail bool, err error) { + // TODO(jackson): Refactor to avoid awkwardness of the strictWALTail return value. + return strictWALTail, parseOptions(s, func(section, key, value string) error { + switch section + "." + key { + case "Options.comparer": + if value != o.Comparer.Name { + return errors.Errorf("pebble: comparer name from file %q != comparer name from options %q", + errors.Safe(value), errors.Safe(o.Comparer.Name)) + } + case "Options.merger": + // RocksDB allows the merge operator to be unspecified, in which case it + // shows up as "nullptr". + if value != "nullptr" && value != o.Merger.Name { + return errors.Errorf("pebble: merger name from file %q != merger name from options %q", + errors.Safe(value), errors.Safe(o.Merger.Name)) + } + case "Options.strict_wal_tail": + strictWALTail, err = strconv.ParseBool(value) + if err != nil { + return errors.Errorf("pebble: error parsing strict_wal_tail value %q: %w", value, err) + } + } + return nil + }) +} + +// Check verifies the options are compatible with the previous options +// serialized by Options.String(). For example, the Comparer and Merger must be +// the same, or data will not be able to be properly read from the DB. +func (o *Options) Check(s string) error { + _, err := o.checkOptions(s) + return err +} + +// Validate verifies that the options are mutually consistent. For example, +// L0StopWritesThreshold must be >= L0CompactionThreshold, otherwise a write +// stall would persist indefinitely. +func (o *Options) Validate() error { + // Note that we can presume Options.EnsureDefaults has been called, so there + // is no need to check for zero values. + + var buf strings.Builder + if o.Experimental.L0CompactionConcurrency < 1 { + fmt.Fprintf(&buf, "L0CompactionConcurrency (%d) must be >= 1\n", + o.Experimental.L0CompactionConcurrency) + } + if o.L0StopWritesThreshold < o.L0CompactionThreshold { + fmt.Fprintf(&buf, "L0StopWritesThreshold (%d) must be >= L0CompactionThreshold (%d)\n", + o.L0StopWritesThreshold, o.L0CompactionThreshold) + } + if uint64(o.MemTableSize) >= maxMemTableSize { + fmt.Fprintf(&buf, "MemTableSize (%s) must be < %s\n", + 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 > internalFormatNewest { + fmt.Fprintf(&buf, "FormatMajorVersion (%d) must be <= %d\n", + 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") + } + if buf.Len() == 0 { + return nil + } + return errors.New(buf.String()) +} + +// MakeReaderOptions constructs sstable.ReaderOptions from the corresponding +// options in the receiver. +func (o *Options) MakeReaderOptions() sstable.ReaderOptions { + var readerOpts sstable.ReaderOptions + if o != nil { + readerOpts.Cache = o.Cache + 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 + } + return readerOpts +} + +// MakeWriterOptions constructs sstable.WriterOptions for the specified level +// from the corresponding options in the receiver. +func (o *Options) MakeWriterOptions(level int, format sstable.TableFormat) sstable.WriterOptions { + var writerOpts sstable.WriterOptions + writerOpts.TableFormat = format + if o != nil { + writerOpts.Cache = o.Cache + writerOpts.Comparer = o.Comparer + if o.Merger != nil { + writerOpts.MergerName = o.Merger.Name + } + writerOpts.TablePropertyCollectors = o.TablePropertyCollectors + writerOpts.BlockPropertyCollectors = o.BlockPropertyCollectors + } + 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 + writerOpts.BlockSize = levelOpts.BlockSize + writerOpts.BlockSizeThreshold = levelOpts.BlockSizeThreshold + writerOpts.Compression = levelOpts.Compression + writerOpts.FilterPolicy = levelOpts.FilterPolicy + writerOpts.FilterType = levelOpts.FilterType + writerOpts.IndexBlockSize = levelOpts.IndexBlockSize + return writerOpts +} diff --git a/pebble/options_test.go b/pebble/options_test.go new file mode 100644 index 0000000..46a5863 --- /dev/null +++ b/pebble/options_test.go @@ -0,0 +1,325 @@ +// Copyright 2018 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 ( + "fmt" + "math/rand" + "runtime" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +// testingRandomized randomizes some default options. Currently, it's +// used for testing under a random format major version in some tests. +func (o *Options) testingRandomized(t testing.TB) *Options { + if o == nil { + o = &Options{} + } + if o.Logger == nil { + o.Logger = testLogger{t: t} + } + if o.FormatMajorVersion == FormatDefault { + // Pick a random format major version from the range + // [MostCompatible, FormatNewest]. + o.FormatMajorVersion = FormatMajorVersion(rand.Intn(int(internalFormatNewest)) + 1) + t.Logf("Running %s with format major version %s", t.Name(), o.FormatMajorVersion.String()) + } + return o +} + +func testingRandomized(t testing.TB, o *Options) *Options { + o.testingRandomized(t) + return o +} + +func TestLevelOptions(t *testing.T) { + var opts *Options + opts = opts.EnsureDefaults() + + testCases := []struct { + level int + targetFileSize int64 + }{ + {0, 2 << 20}, + {1, (2 * 2) << 20}, + {2, (4 * 2) << 20}, + {3, (8 * 2) << 20}, + {4, (16 * 2) << 20}, + {5, (32 * 2) << 20}, + {6, (64 * 2) << 20}, + } + for _, c := range testCases { + l := opts.Level(c.level) + if c.targetFileSize != l.TargetFileSize { + t.Fatalf("%d: expected target-file-size %d, but found %d", + c.level, c.targetFileSize, l.TargetFileSize) + } + } +} + +func TestOptionsString(t *testing.T) { + n := runtime.GOMAXPROCS(8) + defer runtime.GOMAXPROCS(n) + + const expected = `[Version] + pebble_version=0.1 + +[Options] + bytes_per_sync=524288 + cache_size=8388608 + cleaner=delete + compaction_debt_concurrency=1073741824 + comparer=leveldb.BytewiseComparator + disable_wal=false + flush_delay_delete_range=0s + flush_delay_range_key=0s + flush_split_bytes=4194304 + format_major_version=1 + l0_compaction_concurrency=10 + l0_compaction_file_threshold=500 + l0_compaction_threshold=4 + l0_stop_writes_threshold=12 + lbase_max_bytes=67108864 + max_concurrent_compactions=1 + max_manifest_file_size=134217728 + max_open_files=1000 + mem_table_size=4194304 + mem_table_stop_writes_threshold=2 + min_deletion_rate=0 + merger=pebble.concatenate + read_compaction_rate=16000 + read_sampling_multiplier=16 + strict_wal_tail=true + table_cache_shards=8 + table_property_collectors=[] + validate_on_ingest=false + wal_dir= + wal_bytes_per_sync=0 + max_writer_concurrency=0 + force_writer_parallelism=false + secondary_cache_size_bytes=0 + create_on_shared=0 + +[Level "0"] + block_restart_interval=16 + block_size=4096 + block_size_threshold=90 + compression=Snappy + filter_policy=none + filter_type=table + index_block_size=4096 + target_file_size=2097152 +` + + var opts *Options + opts = opts.EnsureDefaults() + if v := opts.String(); expected != v { + t.Fatalf("expected\n%s\nbut found\n%s", expected, v) + } +} + +func TestOptionsCheck(t *testing.T) { + var opts *Options + opts = opts.EnsureDefaults() + s := opts.String() + require.NoError(t, opts.Check(s)) + require.Regexp(t, `invalid key=value syntax`, opts.Check("foo\n")) + + tmp := *opts + tmp.Comparer = &Comparer{Name: "foo"} + require.Regexp(t, `comparer name from file.*!=.*`, tmp.Check(s)) + + tmp = *opts + tmp.Merger = &Merger{Name: "foo"} + require.Regexp(t, `merger name from file.*!=.*`, tmp.Check(s)) + + // RocksDB uses a similar (INI-style) syntax for the OPTIONS file, but + // different section names and keys. + s = ` +[CFOptions "default"] + comparator=rocksdb-comparer + merge_operator=rocksdb-merger +` + tmp = *opts + tmp.Comparer = &Comparer{Name: "foo"} + require.Regexp(t, `comparer name from file.*!=.*`, tmp.Check(s)) + + tmp.Comparer = &Comparer{Name: "rocksdb-comparer"} + tmp.Merger = &Merger{Name: "foo"} + require.Regexp(t, `merger name from file.*!=.*`, tmp.Check(s)) + + tmp.Merger = &Merger{Name: "rocksdb-merger"} + require.NoError(t, tmp.Check(s)) + + // RocksDB allows the merge operator to be unspecified, in which case it + // shows up as "nullptr". + s = ` +[CFOptions "default"] + merge_operator=nullptr +` + tmp = *opts + require.NoError(t, tmp.Check(s)) +} + +type testCleaner struct{} + +func (testCleaner) Clean(fs vfs.FS, fileType base.FileType, path string) error { + return nil +} + +func (testCleaner) String() string { + return "test-cleaner" +} + +func TestOptionsParse(t *testing.T) { + testComparer := *DefaultComparer + testComparer.Name = "test-comparer" + testMerger := *DefaultMerger + testMerger.Name = "test-merger" + var newCacheSize int64 + + hooks := &ParseHooks{ + NewCache: func(size int64) *Cache { + newCacheSize = size + return nil + }, + NewCleaner: func(name string) (Cleaner, error) { + if name == (testCleaner{}).String() { + return testCleaner{}, nil + } + return nil, errors.Errorf("unknown cleaner: %q", name) + }, + NewComparer: func(name string) (*Comparer, error) { + if name == testComparer.Name { + return &testComparer, nil + } + return nil, errors.Errorf("unknown comparer: %q", name) + }, + NewMerger: func(name string) (*Merger, error) { + if name == testMerger.Name { + return &testMerger, nil + } + return nil, errors.Errorf("unknown merger: %q", name) + }, + } + + testCases := []struct { + cleaner Cleaner + comparer *Comparer + merger *Merger + }{ + {testCleaner{}, nil, nil}, + {nil, &testComparer, nil}, + {nil, nil, &testMerger}, + } + for _, c := range testCases { + t.Run("", func(t *testing.T) { + var opts Options + opts.Comparer = c.comparer + opts.Merger = c.merger + opts.WALDir = "wal" + opts.Levels = make([]LevelOptions, 3) + opts.Levels[0].BlockSize = 1024 + opts.Levels[1].BlockSize = 2048 + opts.Levels[2].BlockSize = 4096 + opts.Experimental.CompactionDebtConcurrency = 100 + opts.FlushDelayDeleteRange = 10 * time.Second + opts.FlushDelayRangeKey = 11 * time.Second + opts.Experimental.LevelMultiplier = 5 + opts.TargetByteDeletionRate = 200 + opts.Experimental.ReadCompactionRate = 300 + opts.Experimental.ReadSamplingMultiplier = 400 + opts.Experimental.TableCacheShards = 500 + opts.Experimental.MaxWriterConcurrency = 1 + opts.Experimental.ForceWriterParallelism = true + opts.Experimental.SecondaryCacheSizeBytes = 1024 + opts.EnsureDefaults() + str := opts.String() + + newCacheSize = 0 + var parsedOptions Options + require.NoError(t, parsedOptions.Parse(str, hooks)) + parsedStr := parsedOptions.String() + if str != parsedStr { + t.Fatalf("expected\n%s\nbut found\n%s", str, parsedStr) + } + require.Nil(t, parsedOptions.Cache) + require.NotEqual(t, newCacheSize, 0) + }) + } +} + +func TestOptionsValidate(t *testing.T) { + testCases := []struct { + options string + expected string + }{ + {``, ``}, + {` +[Options] + l0_compaction_concurrency=0 +`, + `L0CompactionConcurrency \(0\) must be >= 1`, + }, + {` +[Options] + l0_compaction_threshold=2 + l0_stop_writes_threshold=1 +`, + `L0StopWritesThreshold .* must be >= L0CompactionThreshold .*`, + }, + {` +[Options] + mem_table_size=4294967296 +`, + `MemTableSize \(4\.0GB\) must be < [2|4]\.0GB`, + }, + {` +[Options] + mem_table_stop_writes_threshold=1 +`, + `MemTableStopWritesThreshold .* must be >= 2`, + }, + } + + for _, c := range testCases { + t.Run("", func(t *testing.T) { + var opts Options + opts.EnsureDefaults() + require.NoError(t, opts.Parse(c.options, nil)) + err := opts.Validate() + if c.expected == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Regexp(t, c.expected, err.Error()) + } + }) + } +} + +// This test isn't being done in TestOptionsValidate +// cause it doesn't support setting pointers. +func TestOptionsValidateCache(t *testing.T) { + var opts Options + opts.EnsureDefaults() + opts.Cache = NewCache(8 << 20) + defer opts.Cache.Unref() + opts.TableCache = NewTableCache(NewCache(8<<20), 10, 1) + defer opts.TableCache.cache.Unref() + defer opts.TableCache.Unref() + + err := opts.Validate() + require.Error(t, err) + if fmt.Sprint(err) != "underlying cache in the TableCache and the Cache dont match" { + t.Errorf("Unexpected error message") + } +} diff --git a/pebble/pacer.go b/pebble/pacer.go new file mode 100644 index 0000000..a959ff4 --- /dev/null +++ b/pebble/pacer.go @@ -0,0 +1,190 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "sync" + "time" +) + +// deletionPacerInfo contains any info from the db necessary to make deletion +// pacing decisions (to limit background IO usage so that it does not contend +// with foreground traffic). +type deletionPacerInfo struct { + freeBytes uint64 + obsoleteBytes uint64 + liveBytes uint64 +} + +// deletionPacer rate limits deletions of obsolete files. This is necessary to +// prevent overloading the disk with too many deletions too quickly after a +// large compaction, or an iterator close. On some SSDs, disk performance can be +// negatively impacted if too many blocks are deleted very quickly, so this +// mechanism helps mitigate that. +type deletionPacer struct { + // 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 + + // 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 + + // history keeps rack of recent deletion history; it used to increase the + // 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. +// +// 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( + now time.Time, targetByteDeletionRate int64, getInfo func() deletionPacerInfo, +) *deletionPacer { + d := &deletionPacer{ + freeSpaceThreshold: 16 << 30, // 16 GB + freeSpaceTimeframe: 10 * time.Second, + + obsoleteBytesMaxRatio: 0.20, + obsoleteBytesTimeframe: 5 * time.Minute, + + targetByteDeletionRate: targetByteDeletionRate, + getInfo: getInfo, + } + d.mu.history.Init(now, deletePacerHistory) + return d +} + +// 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(now time.Time, bytesToDelete uint64) { + p.mu.Lock() + defer p.mu.Unlock() + p.mu.history.Add(now, int64(bytesToDelete)) +} + +// 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 + } + + 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) +} + +// 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 +// are effectively rounded down to the nearest epoch boundary. +type history struct { + epochDuration time.Duration + startTime time.Time + // currEpoch is the epoch of the most recent operation. + currEpoch int64 + // val contains the recent epoch values. + // val[currEpoch % historyEpochs] is the current epoch. + // val[(currEpoch + 1) % historyEpochs] is the oldest epoch. + val [historyEpochs]int64 + // sum is always equal to the sum of values in val. + sum int64 +} + +const historyEpochs = 100 + +// Init the history helper to keep track of data over the given number of +// seconds. +func (h *history) Init(now time.Time, timeframe time.Duration) { + *h = history{ + epochDuration: timeframe / time.Duration(historyEpochs), + startTime: now, + currEpoch: 0, + sum: 0, + } +} + +// Add adds a value for the current time. +func (h *history) Add(now time.Time, val int64) { + h.advance(now) + h.val[h.currEpoch%historyEpochs] += val + h.sum += val +} + +// Sum returns the sum of recent values. The result is approximate in that the +// cut-off time is within 1% of the exact one. +func (h *history) Sum(now time.Time) int64 { + h.advance(now) + return h.sum +} + +func (h *history) epoch(t time.Time) int64 { + return int64(t.Sub(h.startTime) / h.epochDuration) +} + +// advance advances the time to the given time. +func (h *history) advance(now time.Time) { + epoch := h.epoch(now) + for h.currEpoch < epoch { + h.currEpoch++ + // Forget the data for the oldest epoch. + h.sum -= h.val[h.currEpoch%historyEpochs] + h.val[h.currEpoch%historyEpochs] = 0 + } +} diff --git a/pebble/pacer_test.go b/pebble/pacer_test.go new file mode 100644 index 0000000..97009fc --- /dev/null +++ b/pebble/pacer_test.go @@ -0,0 +1,198 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "fmt" + "math" + "math/rand" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestDeletionPacer(t *testing.T) { + const MB = 1 << 20 + const GB = 1 << 30 + testCases := []struct { + freeBytes uint64 + obsoleteBytes uint64 + liveBytes uint64 + // history of deletion reporting; first value in the pair is the time, + // second value is the deleted bytes. The time of pacing is the same as the + // last time in the history. + history [][2]int64 + // expected pacing rate in MB/s. + expected float64 + }{ + { + freeBytes: 160 * GB, + obsoleteBytes: 1 * MB, + liveBytes: 160 * MB, + expected: 100.0, + }, + // As freeBytes is 2GB below the free space threshold, rate should be + // increased by 204.8MB/s. + { + freeBytes: 14 * GB, + obsoleteBytes: 1 * MB, + liveBytes: 100 * MB, + expected: 304.8, + }, + // As freeBytes is 10GB below the free space threshold, rate should be + // increased to by 1GB/s. + { + freeBytes: 6 * GB, + obsoleteBytes: 1 * MB, + liveBytes: 100 * MB, + expected: 1124.0, + }, + // obsoleteBytesRatio is 50%. We need to delete 30GB within 5 minutes. + { + freeBytes: 500 * GB, + obsoleteBytes: 50 * GB, + liveBytes: 100 * GB, + expected: 202.4, + }, + // When obsolete ratio unknown, there should be no throttling. + { + freeBytes: 500 * GB, + obsoleteBytes: 0, + liveBytes: 0, + expected: math.Inf(1), + }, + // History shows 200MB/sec deletions on average over last 5 minutes. + { + freeBytes: 160 * GB, + obsoleteBytes: 1 * MB, + liveBytes: 160 * MB, + history: [][2]int64{{0, 5 * 60 * 200 * MB}}, + expected: 200.0, + }, + // History shows 200MB/sec deletions on average over last 5 minutes and + // freeBytes is 10GB below the threshold. + { + freeBytes: 6 * GB, + obsoleteBytes: 1 * MB, + liveBytes: 160 * MB, + history: [][2]int64{{0, 5 * 60 * 200 * MB}}, + expected: 1224.0, + }, + // History shows 200MB/sec deletions on average over last 5 minutes and + // obsoleteBytesRatio is 50%. + { + freeBytes: 500 * GB, + obsoleteBytes: 50 * GB, + liveBytes: 100 * GB, + history: [][2]int64{{0, 5 * 60 * 200 * MB}}, + expected: 302.4, + }, + // History shows 1000MB/sec deletions on average over last 5 minutes. + { + freeBytes: 160 * GB, + obsoleteBytes: 1 * MB, + liveBytes: 160 * MB, + history: [][2]int64{{0, 60 * 1000 * MB}, {3 * 60, 60 * 4 * 1000 * MB}, {4 * 60, 0}}, + expected: 1000.0, + }, + // First entry in history is too old, it should be discarded. + { + freeBytes: 160 * GB, + obsoleteBytes: 1 * MB, + liveBytes: 160 * MB, + history: [][2]int64{{0, 10 * 60 * 10000 * MB}, {3 * 60, 4 * 60 * 200 * MB}, {7 * 60, 1 * 60 * 200 * MB}}, + expected: 200.0, + }, + } + for tcIdx, tc := range testCases { + t.Run(fmt.Sprintf("%d", tcIdx), func(t *testing.T) { + getInfo := func() deletionPacerInfo { + return deletionPacerInfo{ + freeBytes: tc.freeBytes, + liveBytes: tc.liveBytes, + obsoleteBytes: tc.obsoleteBytes, + } + } + start := time.Now() + last := start + pacer := newDeletionPacer(start, 100*MB, getInfo) + for _, h := range tc.history { + last = start.Add(time.Second * time.Duration(h[0])) + pacer.ReportDeletion(last, uint64(h[1])) + } + result := 1.0 / pacer.PacingDelay(last, 1*MB) + require.InDelta(t, tc.expected, result, 1e-7) + }) + } +} + +// TestDeletionPacerHistory tests the history helper by crosschecking Sum() +// against a naive implementation. +func TestDeletionPacerHistory(t *testing.T) { + type event struct { + time time.Time + // If report is 0, this event is a Sum(). Otherwise it is an Add(). + report int64 + } + numEvents := 1 + rand.Intn(200) + timeframe := time.Duration(1+rand.Intn(60*100)) * time.Second + events := make([]event, numEvents) + startTime := time.Now() + for i := range events { + events[i].time = startTime.Add(time.Duration(rand.Int63n(int64(timeframe)))) + if rand.Intn(3) == 0 { + events[i].report = 0 + } else { + events[i].report = int64(rand.Intn(100000)) + } + } + slices.SortFunc(events, func(a, b event) int { return a.time.Compare(b.time) }) + + var h history + h.Init(startTime, timeframe) + + // partialSums[i] := SUM_j= 0; j-- { + if events[j].time.Before(cutoff) { + return j + } + } + return -1 + } + + // Sum all report values in the last timeframe, and see if recent events + // (allowing 1% error in the cutoff time) match the result. + a := getIdx(e.time.Add(-timeframe * (historyEpochs + 1) / historyEpochs)) + b := getIdx(e.time.Add(-timeframe * (historyEpochs - 1) / historyEpochs)) + found := false + for j := a; j <= b; j++ { + if partialSums[i+1]-partialSums[j+1] == result { + found = true + break + } + } + if !found { + t.Fatalf("incorrect Sum() result %d; %v", result, events[a+1:i+1]) + } + } +} diff --git a/pebble/range_del_test.go b/pebble/range_del_test.go new file mode 100644 index 0000000..75a9ad2 --- /dev/null +++ b/pebble/range_del_test.go @@ -0,0 +1,653 @@ +// Copyright 2018 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 ( + "bytes" + "fmt" + "runtime" + "strings" + "sync" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func TestRangeDel(t *testing.T) { + var d *DB + defer func() { + if d != nil { + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + } + }() + opts := &Options{} + opts.DisableAutomaticCompactions = true + + datadriven.RunTest(t, "testdata/range_del", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + if d != nil { + if err := closeAllSnapshots(d); err != nil { + return err.Error() + } + if err := d.Close(); err != nil { + return err.Error() + } + } + + var err error + if d, err = runDBDefineCmd(td, opts); err != nil { + return err.Error() + } + + d.mu.Lock() + // Disable the "dynamic base level" code for this test. + d.mu.versions.picker.forceBaseLevel1() + s := fmt.Sprintf("mem: %d\n%s", len(d.mu.mem.queue), d.mu.versions.currentVersion().String()) + d.mu.Unlock() + return s + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "compact": + if err := runCompactCmd(td, d); err != nil { + return err.Error() + } + d.mu.Lock() + // Disable the "dynamic base level" code for this test. + d.mu.versions.picker.forceBaseLevel1() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "get": + return runGetCmd(t, td, d) + + case "iter": + snap := Snapshot{ + db: d, + seqNum: InternalKeySeqNumMax, + } + td.MaybeScanArgs(t, "seq", &snap.seqNum) + iter, _ := snap.NewIter(nil) + return runIterCmd(td, iter, true) + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestFlushDelay(t *testing.T) { + opts := &Options{ + FS: vfs.NewMem(), + Comparer: testkeys.Comparer, + FlushDelayDeleteRange: 10 * time.Millisecond, + FlushDelayRangeKey: 10 * time.Millisecond, + FormatMajorVersion: internalFormatNewest, + } + d, err := Open("", opts) + require.NoError(t, err) + + // Ensure that all the various means of writing a rangedel or range key + // trigger their respective flush delays. + cases := []func(){ + func() { + require.NoError(t, d.DeleteRange([]byte("a"), []byte("z"), nil)) + }, + func() { + b := d.NewBatch() + require.NoError(t, b.DeleteRange([]byte("a"), []byte("z"), nil)) + require.NoError(t, b.Commit(nil)) + }, + func() { + b := d.NewBatch() + op := b.DeleteRangeDeferred(1, 1) + op.Key[0] = 'a' + op.Value[0] = 'z' + op.Finish() + require.NoError(t, b.Commit(nil)) + }, + func() { + b := d.NewBatch() + b2 := d.NewBatch() + require.NoError(t, b.DeleteRange([]byte("a"), []byte("z"), nil)) + require.NoError(t, b2.SetRepr(b.Repr())) + require.NoError(t, b2.Commit(nil)) + require.NoError(t, b.Close()) + }, + func() { + b := d.NewBatch() + b2 := d.NewBatch() + require.NoError(t, b.DeleteRange([]byte("a"), []byte("z"), nil)) + require.NoError(t, b2.Apply(b, nil)) + require.NoError(t, b2.Commit(nil)) + require.NoError(t, b.Close()) + }, + func() { + require.NoError(t, d.RangeKeySet([]byte("a"), []byte("z"), nil, nil, nil)) + }, + func() { + require.NoError(t, d.RangeKeyUnset([]byte("a"), []byte("z"), nil, nil)) + }, + func() { + require.NoError(t, d.RangeKeyDelete([]byte("a"), []byte("z"), nil)) + }, + func() { + b := d.NewBatch() + require.NoError(t, b.RangeKeySet([]byte("a"), []byte("z"), nil, nil, nil)) + require.NoError(t, b.Commit(nil)) + }, + func() { + b := d.NewBatch() + require.NoError(t, b.RangeKeyUnset([]byte("a"), []byte("z"), nil, nil)) + require.NoError(t, b.Commit(nil)) + }, + func() { + b := d.NewBatch() + require.NoError(t, b.RangeKeyDelete([]byte("a"), []byte("z"), nil)) + require.NoError(t, b.Commit(nil)) + }, + func() { + b := d.NewBatch() + b2 := d.NewBatch() + require.NoError(t, b.RangeKeySet([]byte("a"), []byte("z"), nil, nil, nil)) + require.NoError(t, b2.SetRepr(b.Repr())) + require.NoError(t, b2.Commit(nil)) + require.NoError(t, b.Close()) + }, + func() { + b := d.NewBatch() + b2 := d.NewBatch() + require.NoError(t, b.RangeKeySet([]byte("a"), []byte("z"), nil, nil, nil)) + require.NoError(t, b2.Apply(b, nil)) + require.NoError(t, b2.Commit(nil)) + require.NoError(t, b.Close()) + }, + } + + for _, f := range cases { + d.mu.Lock() + flushed := d.mu.mem.queue[len(d.mu.mem.queue)-1].flushed + d.mu.Unlock() + f() + <-flushed + } + require.NoError(t, d.Close()) +} + +func TestFlushDelayStress(t *testing.T) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + opts := &Options{ + FS: vfs.NewMem(), + Comparer: testkeys.Comparer, + FlushDelayDeleteRange: time.Duration(rng.Intn(10)+1) * time.Millisecond, + FlushDelayRangeKey: time.Duration(rng.Intn(10)+1) * time.Millisecond, + FormatMajorVersion: internalFormatNewest, + MemTableSize: 8192, + } + + const runs = 100 + for run := 0; run < runs; run++ { + d, err := Open("", opts) + require.NoError(t, err) + + now := time.Now().UnixNano() + writers := runtime.GOMAXPROCS(0) + var wg sync.WaitGroup + wg.Add(writers) + for i := 0; i < writers; i++ { + rng := rand.New(rand.NewSource(uint64(now) + uint64(i))) + go func() { + const ops = 100 + defer wg.Done() + + var k1, k2 [32]byte + for j := 0; j < ops; j++ { + switch rng.Intn(3) { + case 0: + randStr(k1[:], rng) + randStr(k2[:], rng) + require.NoError(t, d.DeleteRange(k1[:], k2[:], nil)) + case 1: + randStr(k1[:], rng) + randStr(k2[:], rng) + require.NoError(t, d.RangeKeySet(k1[:], k2[:], []byte("@2"), nil, nil)) + case 2: + randStr(k1[:], rng) + randStr(k2[:], rng) + require.NoError(t, d.Set(k1[:], k2[:], nil)) + default: + panic("unreachable") + } + } + }() + } + wg.Wait() + time.Sleep(time.Duration(rng.Intn(10)+1) * time.Millisecond) + require.NoError(t, d.Close()) + } +} + +// Verify that range tombstones at higher levels do not unintentionally delete +// newer keys at lower levels. This test sets up one such scenario. The base +// problem is that range tombstones are not truncated to sstable boundaries on +// disk, only in memory. +func TestRangeDelCompactionTruncation(t *testing.T) { + runTest := func(formatVersion FormatMajorVersion) { + // Use a small target file size so that there is a single key per sstable. + d, err := Open("", &Options{ + FS: vfs.NewMem(), + Levels: []LevelOptions{ + {TargetFileSize: 100}, + {TargetFileSize: 100}, + {TargetFileSize: 1}, + }, + DebugCheck: DebugCheckLevels, + FormatMajorVersion: formatVersion, + }) + require.NoError(t, err) + defer d.Close() + + d.mu.Lock() + d.mu.versions.dynamicBaseLevel = false + d.mu.Unlock() + + lsm := func() string { + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + } + expectLSM := func(expected string) { + t.Helper() + expected = strings.TrimSpace(expected) + actual := strings.TrimSpace(lsm()) + if expected != actual { + t.Fatalf("expected\n%s\nbut found\n%s", expected, actual) + } + } + + require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("b"), 100), nil)) + snap1 := d.NewSnapshot() + defer snap1.Close() + // Flush so that each version of "a" ends up in its own L0 table. If we + // allowed both versions in the same L0 table, compaction could trivially + // move the single L0 table to L1. + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("c"), 100), nil)) + + snap2 := d.NewSnapshot() + defer snap2.Close() + require.NoError(t, d.DeleteRange([]byte("a"), []byte("d"), nil)) + + // Compact to produce the L1 tables. + require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) + expectLSM(` +1: + 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] + 000009:[b#12,RANGEDEL-d#inf,RANGEDEL] +`) + + // Compact again to move one of the tables to L2. + require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) + expectLSM(` +1: + 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] +2: + 000009:[b#12,RANGEDEL-d#inf,RANGEDEL] +`) + + // Write "b" and "c" to a new table. + require.NoError(t, d.Set([]byte("b"), []byte("d"), nil)) + require.NoError(t, d.Set([]byte("c"), []byte("e"), nil)) + require.NoError(t, d.Flush()) + expectLSM(` +0.0: + 000011:[b#13,SET-c#14,SET] +1: + 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] +2: + 000009:[b#12,RANGEDEL-d#inf,RANGEDEL] +`) + + // "b" is still visible at this point as it should be. + if _, closer, err := d.Get([]byte("b")); err != nil { + t.Fatalf("expected success, but found %v", err) + } else { + closer.Close() + } + + keys := func() string { + iter, _ := d.NewIter(nil) + defer iter.Close() + var buf bytes.Buffer + var sep string + for iter.First(); iter.Valid(); iter.Next() { + fmt.Fprintf(&buf, "%s%s", sep, iter.Key()) + sep = " " + } + return buf.String() + } + + if expected, actual := `b c`, keys(); expected != actual { + t.Fatalf("expected %q, but found %q", expected, actual) + } + + // Compact the L0 table. This will compact the L0 table into L1 and do to the + // sstable target size settings will create 2 tables in L1. Then L1 table + // containing "c" will be compacted again with the L2 table creating two + // tables in L2. Lastly, the L2 table containing "c" will be compacted + // creating the L3 table. + require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) + if formatVersion < FormatSetWithDelete { + expectLSM(` +1: + 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] +2: + 000012:[b#13,SET-c#inf,RANGEDEL] +3: + 000013:[c#14,SET-d#inf,RANGEDEL] +`) + } else { + expectLSM(` +1: + 000008:[a#12,RANGEDEL-b#inf,RANGEDEL] +2: + 000012:[b#13,SETWITHDEL-c#inf,RANGEDEL] +3: + 000013:[c#14,SET-d#inf,RANGEDEL] +`) + } + + // The L1 table still contains a tombstone from [a,d) which will improperly + // delete the newer version of "b" in L2. + if _, closer, err := d.Get([]byte("b")); err != nil { + t.Errorf("expected success, but found %v", err) + } else { + closer.Close() + } + + if expected, actual := `b c`, keys(); expected != actual { + t.Errorf("expected %q, but found %q", expected, actual) + } + } + + versions := []FormatMajorVersion{ + FormatMostCompatible, + FormatSetWithDelete - 1, + FormatSetWithDelete, + FormatNewest, + } + for _, version := range versions { + t.Run(fmt.Sprintf("version-%s", version), func(t *testing.T) { + runTest(version) + }) + } +} + +// This is an alternate scenario to the one created in +// TestRangeDelCompactionTruncation that would result in the bounds for an +// sstable expanding to overlap its left neighbor if we failed to truncate an +// sstable's boundaries to the compaction input boundaries. +func TestRangeDelCompactionTruncation2(t *testing.T) { + // Use a small target file size so that there is a single key per sstable. + d, err := Open("", &Options{ + FS: vfs.NewMem(), + Levels: []LevelOptions{ + {TargetFileSize: 200}, + {TargetFileSize: 200}, + {TargetFileSize: 1}, + }, + DebugCheck: DebugCheckLevels, + }) + require.NoError(t, err) + defer d.Close() + + lsm := func() string { + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + } + expectLSM := func(expected string) { + t.Helper() + expected = strings.TrimSpace(expected) + actual := strings.TrimSpace(lsm()) + if expected != actual { + t.Fatalf("expected\n%s\nbut found\n%s", expected, actual) + } + } + + require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("b"), 100), nil)) + snap1 := d.NewSnapshot() + defer snap1.Close() + // Flush so that each version of "b" ends up in its own L0 table. If we + // allowed both versions in the same L0 table, compaction could trivially + // move the single L0 table to L1. + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("c"), 100), nil)) + snap2 := d.NewSnapshot() + defer snap2.Close() + require.NoError(t, d.DeleteRange([]byte("a"), []byte("d"), nil)) + + // Compact to produce the L1 tables. + require.NoError(t, d.Compact([]byte("b"), []byte("b\x00"), false)) + expectLSM(` +6: + 000009:[a#12,RANGEDEL-d#inf,RANGEDEL] +`) + + require.NoError(t, d.Set([]byte("c"), bytes.Repeat([]byte("d"), 100), nil)) + require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) + expectLSM(` +6: + 000012:[a#12,RANGEDEL-c#inf,RANGEDEL] + 000013:[c#13,SET-d#inf,RANGEDEL] +`) +} + +// TODO(peter): rewrite this test, TestRangeDelCompactionTruncation, and +// TestRangeDelCompactionTruncation2 as data-driven tests. +func TestRangeDelCompactionTruncation3(t *testing.T) { + // Use a small target file size so that there is a single key per sstable. + d, err := Open("tmp", &Options{ + Cleaner: ArchiveCleaner{}, + FS: vfs.NewMem(), + Levels: []LevelOptions{ + {TargetFileSize: 200}, + {TargetFileSize: 200}, + {TargetFileSize: 1}, + }, + DebugCheck: DebugCheckLevels, + }) + require.NoError(t, err) + defer d.Close() + + d.mu.Lock() + d.mu.versions.dynamicBaseLevel = false + d.mu.Unlock() + + lsm := func() string { + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + } + expectLSM := func(expected string) { + t.Helper() + expected = strings.TrimSpace(expected) + actual := strings.TrimSpace(lsm()) + if expected != actual { + t.Fatalf("expected\n%s\nbut found\n%s", expected, actual) + } + } + + require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("b"), 100), nil)) + snap1 := d.NewSnapshot() + defer snap1.Close() + + // Flush so that each version of "b" ends up in its own L0 table. If we + // allowed both versions in the same L0 table, compaction could trivially + // move the single L0 table to L1. + require.NoError(t, d.Flush()) + require.NoError(t, d.Set([]byte("b"), bytes.Repeat([]byte("c"), 100), nil)) + snap2 := d.NewSnapshot() + defer snap2.Close() + + require.NoError(t, d.DeleteRange([]byte("a"), []byte("d"), nil)) + snap3 := d.NewSnapshot() + defer snap3.Close() + + if _, _, err := d.Get([]byte("b")); err != ErrNotFound { + t.Fatalf("expected not found, but found %v", err) + } + + // Compact a few times to move the tables down to L3. + for i := 0; i < 3; i++ { + require.NoError(t, d.Compact([]byte("b"), []byte("b\x00"), false)) + } + expectLSM(` +3: + 000009:[a#12,RANGEDEL-d#inf,RANGEDEL] +`) + + require.NoError(t, d.Set([]byte("c"), bytes.Repeat([]byte("d"), 100), nil)) + + require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) + expectLSM(` +3: + 000013:[a#12,RANGEDEL-c#inf,RANGEDEL] +4: + 000014:[c#13,SET-d#inf,RANGEDEL] +`) + + require.NoError(t, d.Compact([]byte("c"), []byte("c\x00"), false)) + expectLSM(` +3: + 000013:[a#12,RANGEDEL-c#inf,RANGEDEL] +5: + 000014:[c#13,SET-d#inf,RANGEDEL] +`) + + if _, _, err := d.Get([]byte("b")); err != ErrNotFound { + t.Fatalf("expected not found, but found %v", err) + } + + require.NoError(t, d.Compact([]byte("a"), []byte("a\x00"), false)) + expectLSM(` +4: + 000013:[a#12,RANGEDEL-c#inf,RANGEDEL] +5: + 000014:[c#13,SET-d#inf,RANGEDEL] +`) + + if v, _, err := d.Get([]byte("b")); err != ErrNotFound { + t.Fatalf("expected not found, but found %v [%s]", err, v) + } +} + +func BenchmarkRangeDelIterate(b *testing.B) { + for _, entries := range []int{10, 1000, 100000} { + b.Run(fmt.Sprintf("entries=%d", entries), func(b *testing.B) { + for _, deleted := range []int{entries, entries - 1} { + b.Run(fmt.Sprintf("deleted=%d", deleted), func(b *testing.B) { + for _, snapshotCompact := range []bool{false, true} { + b.Run(fmt.Sprintf("snapshotAndCompact=%t", snapshotCompact), func(b *testing.B) { + benchmarkRangeDelIterate(b, entries, deleted, snapshotCompact) + }) + } + }) + } + }) + } +} + +func benchmarkRangeDelIterate(b *testing.B, entries, deleted int, snapshotCompact bool) { + mem := vfs.NewMem() + cache := NewCache(128 << 20) // 128 MB + defer cache.Unref() + + d, err := Open("", &Options{ + Cache: cache, + FS: mem, + DebugCheck: DebugCheckLevels, + }) + if err != nil { + b.Fatal(err) + } + defer d.Close() + + makeKey := func(i int) []byte { + return []byte(fmt.Sprintf("%09d", i)) + } + + // Create an sstable with N entries and ingest it. This is a fast way + // to get a lot of entries into pebble. + f, err := mem.Create("ext") + if err != nil { + b.Fatal(err) + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + BlockSize: 32 << 10, // 32 KB + }) + for i := 0; i < entries; i++ { + key := base.MakeInternalKey(makeKey(i), 0, InternalKeyKindSet) + if err := w.Add(key, nil); err != nil { + b.Fatal(err) + } + } + if err := w.Close(); err != nil { + b.Fatal(err) + } + if err := d.Ingest([]string{"ext"}); err != nil { + b.Fatal(err) + } + + // Some benchmarks test snapshots that force the range tombstone into the + // same level as the covered data. + // See https://github.com/cockroachdb/pebble/issues/1070. + if snapshotCompact { + s := d.NewSnapshot() + defer func() { require.NoError(b, s.Close()) }() + } + + // Create a range tombstone that deletes most (or all) of those entries. + from := makeKey(0) + to := makeKey(deleted) + if err := d.DeleteRange(from, to, nil); err != nil { + b.Fatal(err) + } + + if snapshotCompact { + require.NoError(b, d.Compact(makeKey(0), makeKey(entries), false)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + iter, _ := d.NewIter(nil) + iter.SeekGE(from) + if deleted < entries { + if !iter.Valid() { + b.Fatal("key not found") + } + } else if iter.Valid() { + b.Fatal("unexpected key found") + } + if err := iter.Close(); err != nil { + b.Fatal(err) + } + } +} diff --git a/pebble/range_keys.go b/pebble/range_keys.go new file mode 100644 index 0000000..3d0561a --- /dev/null +++ b/pebble/range_keys.go @@ -0,0 +1,713 @@ +// Copyright 2021 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/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/sstable" +) + +// constructRangeKeyIter constructs the range-key iterator stack, populating +// i.rangeKey.rangeKeyIter with the resulting iterator. +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, false /* internalKeys */, &i.rangeKey.rangeKeyBuffers.internal) + + // If there's an indexed batch with range keys, include it. + if i.batch != nil { + if i.batch.index == nil { + // This isn't an indexed batch. We shouldn't have gotten this far. + panic(errors.AssertionFailedf("creating an iterator over an unindexed batch")) + } else { + // Only include the batch's range key iterator if it has any keys. + // NB: This can force reconstruction of the rangekey iterator stack + // in SetOptions if subsequently range keys are added. See + // SetOptions. + if i.batch.countRangeKeys > 0 { + i.batch.initRangeKeyIter(&i.opts, &i.batchRangeKeyIter, i.batchSeqNum) + i.rangeKey.iterConfig.AddLevel(&i.batchRangeKeyIter) + } + } + } + + if !i.batchOnlyIter { + // 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); 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. We + // maintain a separate manifest.LevelMetadata for each level containing only + // files that contain range keys, however we don't compute a separate + // L0Sublevels data structure too. + // + // We first use L0's LevelMetadata to peek and see whether L0 contains any + // range keys at all. If it does, we create a range key level iterator per + // level that contains range keys using the information from L0Sublevels. + // Some sublevels may not contain any range keys, and we need to iterate + // through the fileMetadata to determine that. Since L0's file count should + // not significantly exceed ~1000 files (see L0CompactionFileThreshold), + // this should be okay. + if !current.RangeKeyLevels[0].Empty() { + // L0 contains at least 1 file containing range keys. + // Add level iterators for the L0 sublevels, iterating from newest to + // oldest. + for j := len(current.L0SublevelFiles) - 1; j >= 0; j-- { + iter := current.L0SublevelFiles[j].Iter() + if !containsAnyRangeKeys(iter) { + continue + } + + li := i.rangeKey.iterConfig.NewLevelIter() + li.Init( + i.opts.SpanIterOptions(), + i.cmp, + i.newIterRangeKey, + iter.Filter(manifest.KeyTypeRange), + manifest.L0Sublevel(j), + manifest.KeyTypeRange, + ) + i.rangeKey.iterConfig.AddLevel(li) + } + } + + // 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 := i.opts.SpanIterOptions() + li.Init(spanIterOpts, i.cmp, i.newIterRangeKey, current.RangeKeyLevels[level].Iter(), + manifest.Level(level), manifest.KeyTypeRange) + i.rangeKey.iterConfig.AddLevel(li) + } + } +} + +func containsAnyRangeKeys(iter manifest.LevelIterator) bool { + for f := iter.First(); f != nil; f = iter.Next() { + if f.HasRangeKeys { + return true + } + } + return false +} + +// Range key masking +// +// Pebble iterators may be configured such that range keys with suffixes mask +// point keys with lower suffixes. The intended use is implementing a MVCC +// delete range operation using range keys, when suffixes are MVCC timestamps. +// +// To enable masking, the user populates the IterOptions's RangeKeyMasking +// field. The Suffix field configures which range keys act as masks. The +// intended use is to hold a MVCC read timestamp. When implementing a MVCC +// delete range operation, only range keys that are visible at the read +// timestamp should be visible. If a range key has a suffix ≤ +// RangeKeyMasking.Suffix, it acts as a mask. +// +// Range key masking is facilitated by the keyspan.InterleavingIter. The +// interleaving iterator interleaves range keys and point keys during combined +// iteration. During user iteration, the interleaving iterator is configured +// with a keyspan.SpanMask, implemented by the rangeKeyMasking struct below. +// The SpanMask interface defines two methods: SpanChanged and SkipPoint. +// +// SpanChanged is used to keep the current mask up-to-date. Whenever the point +// iterator has stepped into or out of the bounds of a range key, the +// interleaving iterator invokes SpanChanged passing the current covering range +// key. The below rangeKeyMasking implementation scans the range keys looking +// for the range key with the largest suffix that's still ≤ the suffix supplied +// to IterOptions.RangeKeyMasking.Suffix (the "read timestamp"). If it finds a +// range key that meets the condition, the range key should act as a mask. The +// span and the relevant range key's suffix are saved. +// +// The above ensures that `rangeKeyMasking.maskActiveSuffix` always contains the +// current masking suffix such that any point keys with lower suffixes should be +// skipped. +// +// There are two ways in which masked point keys are skipped. +// +// 1. Interleaving iterator SkipPoint +// +// Whenever the interleaving iterator encounters a point key that falls within +// the bounds of a range key, it invokes SkipPoint. The interleaving iterator +// guarantees that the SpanChanged method described above has already been +// invoked with the covering range key. The below rangeKeyMasking implementation +// of SkipPoint splits the key into prefix and suffix, compares the suffix to +// the `maskActiveSuffix` updated by SpanChanged and returns true if +// suffix(point) < maskActiveSuffix. +// +// The SkipPoint logic is sufficient to ensure that the Pebble iterator filters +// out all masked point keys. However, it requires the iterator read each masked +// point key. For broad range keys that mask many points, this may be expensive. +// +// 2. Block property filter +// +// For more efficient handling of braad range keys that mask many points, the +// IterOptions.RangeKeyMasking field has an optional Filter option. This Filter +// field takes a superset of the block-property filter interface, adding a +// method to dynamically configure the filter's filtering criteria. +// +// To make use of the Filter option, the user is required to define and +// configure a block-property collector that collects a property containing at +// least the maximum suffix of a key within a block. +// +// When the SpanChanged method described above is invoked, rangeKeyMasking also +// reconfigures the user-provided filter. It invokes a SetSuffix method, +// providing the `maskActiveSuffix`, requesting that from now on the +// block-property filter return Intersects()=false for any properties indicating +// that a block contains exclusively keys with suffixes greater than the +// provided suffix. +// +// Note that unlike other block-property filters, the filter used for masking +// must not apply across the entire keyspace. It must only filter blocks that +// lie within the bounds of the range key that set the mask suffix. To +// accommodate this, rangeKeyMasking implements a special interface: +// sstable.BoundLimitedBlockPropertyFilter. This interface extends the block +// property filter interface with two new methods: KeyIsWithinLowerBound and +// KeyIsWithinUpperBound. The rangeKeyMasking type wraps the user-provided block +// property filter, implementing these two methods and overriding Intersects to +// always return true if there is no active mask. +// +// The logic to ensure that a mask block-property filter is only applied within +// the bounds of the masking range key is subtle. The interleaving iterator +// guarantees that it never invokes SpanChanged until the point iterator is +// positioned within the range key. During forward iteration, this guarantees +// that any block that a sstable reader might attempt to load contains only keys +// greater than or equal to the range key's lower bound. During backward +// iteration, it provides the analagous guarantee on the range key's upper +// bound. +// +// The above ensures that an sstable reader only needs to verify that a block +// that it skips meets the opposite bound. This is where the +// KeyIsWithinLowerBound and KeyIsWithinUpperBound methods are used. When an +// sstable iterator is configured with a BoundLimitedBlockPropertyFilter, it +// checks for intersection with the block-property filter before every block +// load, like ordinary block-property filters. However, if the bound-limited +// block property filter indicates that it does NOT intersect, the filter's +// relevant KeyIsWithin{Lower,Upper}Bound method is queried, using a block +// index separator as the bound. If the method indicates that the provided index +// separator does not fall within the range key bounds, the no-intersection +// result is ignored, and the block is read. + +type rangeKeyMasking struct { + cmp base.Compare + split base.Split + filter BlockPropertyFilterMask + // maskActiveSuffix holds the suffix of a range key currently acting as a + // mask, hiding point keys with suffixes greater than it. maskActiveSuffix + // is only ever non-nil if IterOptions.RangeKeyMasking.Suffix is non-nil. + // maskActiveSuffix is updated whenever the iterator passes over a new range + // key. The maskActiveSuffix should only be used if maskSpan is non-nil. + // + // See SpanChanged. + maskActiveSuffix []byte + // maskSpan holds the span from which the active mask suffix was extracted. + // The span is used for bounds comparisons, to ensure that a range-key mask + // is not applied beyond the bounds of the range key. + maskSpan *keyspan.Span + parent *Iterator +} + +func (m *rangeKeyMasking) init(parent *Iterator, cmp base.Compare, split base.Split) { + m.cmp = cmp + m.split = split + if parent.opts.RangeKeyMasking.Filter != nil { + m.filter = parent.opts.RangeKeyMasking.Filter() + } + m.parent = parent +} + +// SpanChanged implements the keyspan.SpanMask interface, used during range key +// iteration. +func (m *rangeKeyMasking) SpanChanged(s *keyspan.Span) { + if s == nil && m.maskSpan == nil { + return + } + m.maskSpan = nil + m.maskActiveSuffix = m.maskActiveSuffix[:0] + + // Find the smallest suffix of a range key contained within the Span, + // excluding suffixes less than m.opts.RangeKeyMasking.Suffix. + if s != nil { + m.parent.rangeKey.stale = true + if m.parent.opts.RangeKeyMasking.Suffix != nil { + for j := range s.Keys { + if s.Keys[j].Suffix == nil { + continue + } + if m.cmp(s.Keys[j].Suffix, m.parent.opts.RangeKeyMasking.Suffix) < 0 { + continue + } + if len(m.maskActiveSuffix) == 0 || m.cmp(m.maskActiveSuffix, s.Keys[j].Suffix) > 0 { + m.maskSpan = s + m.maskActiveSuffix = append(m.maskActiveSuffix[:0], s.Keys[j].Suffix...) + } + } + } + } + + if m.maskSpan != nil && m.parent.opts.RangeKeyMasking.Filter != nil { + // Update the block-property filter to filter point keys with suffixes + // greater than m.maskActiveSuffix. + err := m.filter.SetSuffix(m.maskActiveSuffix) + if err != nil { + m.parent.err = err + } + } + // If no span is active, we leave the inner block-property filter configured + // with its existing suffix. That's okay, because Intersects calls are first + // evaluated by iteratorRangeKeyState.Intersects, which considers all blocks + // as intersecting if there's no active mask. +} + +// SkipPoint implements the keyspan.SpanMask interface, used during range key +// iteration. Whenever a point key is covered by a non-empty Span, the +// interleaving iterator invokes SkipPoint. This function is responsible for +// performing range key masking. +// +// If a non-nil IterOptions.RangeKeyMasking.Suffix is set, range key masking is +// enabled. Masking hides point keys, transparently skipping over the keys. +// Whether or not a point key is masked is determined by comparing the point +// key's suffix, the overlapping span's keys' suffixes, and the user-configured +// IterOption's RangeKeyMasking.Suffix. When configured with a masking threshold +// _t_, and there exists a span with suffix _r_ covering a point key with suffix +// _p_, and +// +// _t_ ≤ _r_ < _p_ +// +// then the point key is elided. Consider the following rendering, where using +// integer suffixes with higher integers sort before suffixes with lower +// integers, (for example @7 ≤ @6 < @5): +// +// ^ +// @9 | •―――――――――――――――○ [e,m)@9 +// s 8 | • l@8 +// u 7 |------------------------------------ @7 RangeKeyMasking.Suffix +// f 6 | [h,q)@6 •―――――――――――――――――○ (threshold) +// f 5 | • h@5 +// f 4 | • n@4 +// i 3 | •―――――――――――○ [f,l)@3 +// x 2 | • b@2 +// 1 | +// 0 |___________________________________ +// a b c d e f g h i j k l m n o p q +// +// An iterator scanning the entire keyspace with the masking threshold set to @7 +// will observe point keys b@2 and l@8. The span keys [h,q)@6 and [f,l)@3 serve +// as masks, because cmp(@6,@7) ≥ 0 and cmp(@3,@7) ≥ 0. The span key [e,m)@9 +// does not serve as a mask, because cmp(@9,@7) < 0. +// +// Although point l@8 falls within the user key bounds of [e,m)@9, [e,m)@9 is +// non-masking due to its suffix. The point key l@8 also falls within the user +// key bounds of [h,q)@6, but since cmp(@6,@8) ≥ 0, l@8 is unmasked. +// +// Invariant: The userKey is within the user key bounds of the span most +// recently provided to `SpanChanged`. +func (m *rangeKeyMasking) SkipPoint(userKey []byte) bool { + m.parent.stats.RangeKeyStats.ContainedPoints++ + if m.maskSpan == nil { + // No range key is currently acting as a mask, so don't skip. + return false + } + // Range key masking is enabled and the current span includes a range key + // that is being used as a mask. (NB: SpanChanged already verified that the + // range key's suffix is ≥ RangeKeyMasking.Suffix). + // + // This point key falls within the bounds of the range key (guaranteed by + // the InterleavingIter). Skip the point key if the range key's suffix is + // greater than the point key's suffix. + pointSuffix := userKey[m.split(userKey):] + if len(pointSuffix) > 0 && m.cmp(m.maskActiveSuffix, pointSuffix) < 0 { + m.parent.stats.RangeKeyStats.SkippedPoints++ + return true + } + return false +} + +// The iteratorRangeKeyState type implements the sstable package's +// BoundLimitedBlockPropertyFilter interface in order to use block property +// filters for range key masking. The iteratorRangeKeyState implementation wraps +// the block-property filter provided in Options.RangeKeyMasking.Filter. +// +// Using a block-property filter for range-key masking requires limiting the +// filter's effect to the bounds of the range key currently acting as a mask. +// Consider the range key [a,m)@10, and an iterator positioned just before the +// below block, bounded by index separators `c` and `z`: +// +// c z +// x | c@9 c@5 c@1 d@7 e@4 y@4 | ... +// iter pos +// +// The next block cannot be skipped, despite the range key suffix @10 is greater +// than all the block's keys' suffixes, because it contains a key (y@4) outside +// the bounds of the range key. +// +// This extended BoundLimitedBlockPropertyFilter interface adds two new methods, +// KeyIsWithinLowerBound and KeyIsWithinUpperBound, for testing whether a +// particular block is within bounds. +// +// The iteratorRangeKeyState implements these new methods by first checking if +// the iterator is currently positioned within a range key. If not, the provided +// key is considered out-of-bounds. If the iterator is positioned within a range +// key, it compares the corresponding range key bound. +var _ sstable.BoundLimitedBlockPropertyFilter = (*rangeKeyMasking)(nil) + +// Name implements the limitedBlockPropertyFilter interface defined in the +// sstable package by passing through to the user-defined block property filter. +func (m *rangeKeyMasking) Name() string { + return m.filter.Name() +} + +// Intersects implements the limitedBlockPropertyFilter interface defined in the +// sstable package by passing the intersection decision to the user-provided +// block property filter only if a range key is covering the current iterator +// position. +func (m *rangeKeyMasking) Intersects(prop []byte) (bool, error) { + if m.maskSpan == nil { + // No span is actively masking. + return true, nil + } + return m.filter.Intersects(prop) +} + +// 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(key []byte) bool { + // Invariant: m.maskSpan != nil + // + // The provided `key` is an inclusive lower bound of the block we're + // considering skipping. + 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(key []byte) bool { + // Invariant: m.maskSpan != nil + // + // 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, key) > 0 +} + +// lazyCombinedIter implements the internalIterator interface, wrapping a +// pointIter. It requires the pointIter's the levelIters be configured with +// pointers to its combinedIterState. When the levelIter observes a file +// containing a range key, the lazyCombinedIter constructs the combined +// range+point key iterator stack and switches to it. +type lazyCombinedIter struct { + // parent holds a pointer to the root *pebble.Iterator containing this + // iterator. It's used to mutate the internalIterator in use when switching + // to combined iteration. + parent *Iterator + pointIter internalIterator + combinedIterState combinedIterState +} + +// combinedIterState encapsulates the current state of combined iteration. +// Various low-level iterators (mergingIter, leveliter) hold pointers to the +// *pebble.Iterator's combinedIterState. This allows them to check whether or +// not they must monitor for files containing range keys (!initialized), or not. +// +// When !initialized, low-level iterators watch for files containing range keys. +// When one is discovered, they set triggered=true and key to the smallest +// (forward direction) or largest (reverse direction) range key that's been +// observed. +type combinedIterState struct { + // key holds the smallest (forward direction) or largest (backward + // direction) user key from a range key bound discovered during the iterator + // operation that triggered the switch to combined iteration. + // + // Slices stored here must be stable. This is possible because callers pass + // a Smallest/Largest bound from a fileMetadata, which are immutable. A key + // slice's bytes must not be overwritten. + key []byte + triggered bool + initialized bool +} + +// Assert that *lazyCombinedIter implements internalIterator. +var _ internalIterator = (*lazyCombinedIter)(nil) + +// initCombinedIteration is invoked after a pointIter positioning operation +// resulted in i.combinedIterState.triggered=true. +// +// The `dir` parameter is `+1` or `-1` indicating forward iteration or backward +// iteration respectively. +// +// The `pointKey` and `pointValue` parameters provide the new point key-value +// pair that the iterator was just positioned to. The combined iterator should +// be seeded with this point key-value pair and return the smaller (forward +// iteration) or largest (backward iteration) of the two. +// +// The `seekKey` parameter is non-nil only if the iterator operation that +// triggered the switch to combined iteration was a SeekGE, SeekPrefixGE or +// SeekLT. It provides the seek key supplied and is used to seek the range-key +// iterator using the same key. This is necessary for SeekGE/SeekPrefixGE +// operations that land in the middle of a range key and must truncate to the +// user-provided seek key. +func (i *lazyCombinedIter) initCombinedIteration( + dir int8, pointKey *InternalKey, pointValue base.LazyValue, seekKey []byte, +) (*InternalKey, base.LazyValue) { + // Invariant: i.parent.rangeKey is nil. + // Invariant: !i.combinedIterState.initialized. + if invariants.Enabled { + if i.combinedIterState.initialized { + panic("pebble: combined iterator already initialized") + } + if i.parent.rangeKey != nil { + panic("pebble: iterator already has a range-key iterator stack") + } + } + + // We need to determine the key to seek the range key iterator to. If + // seekKey is not nil, the user-initiated operation that triggered the + // switch to combined iteration was itself a seek, and we can use that key. + // Otherwise, a First/Last or relative positioning operation triggered the + // switch to combined iteration. + // + // The levelIter that observed a file containing range keys populated + // combinedIterState.key with the smallest (forward) or largest (backward) + // range key it observed. If multiple levelIters observed files with range + // keys during the same operation on the mergingIter, combinedIterState.key + // is the smallest [during forward iteration; largest in reverse iteration] + // such key. + if seekKey == nil { + // Use the levelIter-populated key. + seekKey = i.combinedIterState.key + + // We may need to adjust the levelIter-populated seek key to the + // surfaced point key. If the key observed is beyond [in the iteration + // direction] the current point key, there may still exist a range key + // at an earlier key. Consider the following example: + // + // L5: 000003:[bar.DEL.5, foo.RANGEKEYSET.9] + // L6: 000001:[bar.SET.2] 000002:[bax.RANGEKEYSET.8] + // + // A call to First() seeks the levels to files L5.000003 and L6.000001. + // The L5 levelIter observes that L5.000003 contains the range key with + // start key `foo`, and triggers a switch to combined iteration, setting + // `combinedIterState.key` = `foo`. + // + // The L6 levelIter did not observe the true first range key + // (bax.RANGEKEYSET.8), because it appears in a later sstable. When the + // combined iterator is initialized, the range key iterator must be + // seeked to a key that will find `bax`. To accomplish this, we seek the + // key instead to `bar`. It is guaranteed that no range key exists + // earlier than `bar`, otherwise a levelIter would've observed it and + // set `combinedIterState.key` to its start key. + if pointKey != nil { + if dir == +1 && i.parent.cmp(i.combinedIterState.key, pointKey.UserKey) > 0 { + seekKey = pointKey.UserKey + } else if dir == -1 && i.parent.cmp(seekKey, pointKey.UserKey) < 0 { + seekKey = pointKey.UserKey + } + } + } + + // An operation on the point iterator observed a file containing range keys, + // so we must switch to combined interleaving iteration. First, construct + // the range key iterator stack. It must not exist, otherwise we'd already + // be performing combined iteration. + i.parent.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState) + i.parent.rangeKey.init(i.parent.comparer.Compare, i.parent.comparer.Split, &i.parent.opts) + i.parent.constructRangeKeyIter() + + // Initialize the Iterator's interleaving iterator. + i.parent.rangeKey.iiter.Init( + &i.parent.comparer, i.parent.pointIter, i.parent.rangeKey.rangeKeyIter, + 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. + i.parent.iter = &i.parent.rangeKey.iiter + i.combinedIterState.initialized = true + i.combinedIterState.key = nil + + // All future iterator operations will go directly through the combined + // iterator. + // + // Initialize the interleaving iterator. We pass the point key-value pair so + // that the interleaving iterator knows where the point iterator is + // positioned. Additionally, we pass the seek key to which the range-key + // iterator should be seeked in order to initialize its position. + // + // In the forward direction (invert for backwards), the seek key is a key + // guaranteed to find the smallest range key that's greater than the last + // key the iterator returned. The range key may be less than pointKey, in + // which case the range key will be interleaved next instead of the point + // key. + if dir == +1 { + var prefix []byte + if i.parent.hasPrefix { + prefix = i.parent.prefixOrFullSeekKey + } + return i.parent.rangeKey.iiter.InitSeekGE(prefix, seekKey, pointKey, pointValue) + } + return i.parent.rangeKey.iiter.InitSeekLT(seekKey, pointKey, pointValue) +} + +func (i *lazyCombinedIter) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.SeekGE(key, flags) + } + k, v := i.pointIter.SeekGE(key, flags) + if i.combinedIterState.triggered { + return i.initCombinedIteration(+1, k, v, key) + } + return k, v +} + +func (i *lazyCombinedIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.SeekPrefixGE(prefix, key, flags) + } + k, v := i.pointIter.SeekPrefixGE(prefix, key, flags) + if i.combinedIterState.triggered { + return i.initCombinedIteration(+1, k, v, key) + } + return k, v +} + +func (i *lazyCombinedIter) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*InternalKey, base.LazyValue) { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.SeekLT(key, flags) + } + k, v := i.pointIter.SeekLT(key, flags) + if i.combinedIterState.triggered { + return i.initCombinedIteration(-1, k, v, key) + } + return k, v +} + +func (i *lazyCombinedIter) First() (*InternalKey, base.LazyValue) { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.First() + } + k, v := i.pointIter.First() + if i.combinedIterState.triggered { + return i.initCombinedIteration(+1, k, v, nil) + } + return k, v +} + +func (i *lazyCombinedIter) Last() (*InternalKey, base.LazyValue) { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.Last() + } + k, v := i.pointIter.Last() + if i.combinedIterState.triggered { + return i.initCombinedIteration(-1, k, v, nil) + } + return k, v +} + +func (i *lazyCombinedIter) Next() (*InternalKey, base.LazyValue) { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.Next() + } + k, v := i.pointIter.Next() + if i.combinedIterState.triggered { + return i.initCombinedIteration(+1, k, v, nil) + } + return k, v +} + +func (i *lazyCombinedIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.NextPrefix(succKey) + } + k, v := i.pointIter.NextPrefix(succKey) + if i.combinedIterState.triggered { + return i.initCombinedIteration(+1, k, v, nil) + } + return k, v +} + +func (i *lazyCombinedIter) Prev() (*InternalKey, base.LazyValue) { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.Prev() + } + k, v := i.pointIter.Prev() + if i.combinedIterState.triggered { + return i.initCombinedIteration(-1, k, v, nil) + } + return k, v +} + +func (i *lazyCombinedIter) Error() error { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.Error() + } + return i.pointIter.Error() +} + +func (i *lazyCombinedIter) Close() error { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.Close() + } + return i.pointIter.Close() +} + +func (i *lazyCombinedIter) SetBounds(lower, upper []byte) { + if i.combinedIterState.initialized { + i.parent.rangeKey.iiter.SetBounds(lower, upper) + return + } + i.pointIter.SetBounds(lower, upper) +} + +func (i *lazyCombinedIter) SetContext(ctx context.Context) { + if i.combinedIterState.initialized { + i.parent.rangeKey.iiter.SetContext(ctx) + return + } + i.pointIter.SetContext(ctx) +} + +func (i *lazyCombinedIter) String() string { + if i.combinedIterState.initialized { + return i.parent.rangeKey.iiter.String() + } + return i.pointIter.String() +} diff --git a/pebble/rangekey/rangekey.go b/pebble/rangekey/rangekey.go new file mode 100644 index 0000000..93e7fbe --- /dev/null +++ b/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/pebble/read_compaction_queue.go b/pebble/read_compaction_queue.go new file mode 100644 index 0000000..450b7e9 --- /dev/null +++ b/pebble/read_compaction_queue.go @@ -0,0 +1,94 @@ +package pebble + +import "github.com/cockroachdb/pebble/internal/base" + +// The maximum number of elements in the readCompactions queue. +// We want to limit the number of elements so that we only do +// compactions for ranges which are being read recently. +const readCompactionMaxQueueSize = 5 + +// The readCompactionQueue is a queue of read compactions with +// 0 overlapping ranges. +type readCompactionQueue struct { + // Invariant: A contiguous prefix of the queue contains + // all the elements in the queue, in order of insertion. + // When we remove duplicates from the queue, we break + // the invariant that a contiguous prefix of the queue + // has all the elements in it. To fix this, we shift + // the elements of the queue to the left. This is cheap + // because the queue has a max length of 5. + queue [readCompactionMaxQueueSize]*readCompaction + + // The size of the queue which is occupied. + // A size of k, implies that the first k elements + // of the queue are occupied. + // The size will be <= readCompactionMaxQueueSize. + size int +} + +// combine should be used to combine an older queue with a newer +// queue. +func (qu *readCompactionQueue) combine(newQu *readCompactionQueue, cmp base.Compare) { + + for i := 0; i < newQu.size; i++ { + qu.add(newQu.queue[i], cmp) + } +} + +// add adds read compactions to the queue, while maintaining the invariant +// that there are no overlapping ranges in the queue. +func (qu *readCompactionQueue) add(rc *readCompaction, cmp base.Compare) { + sz := qu.size + for i := 0; i < sz; i++ { + left := qu.queue[i] + right := rc + if cmp(left.start, right.start) > 0 { + left, right = right, left + } + if cmp(right.start, left.end) <= 0 { + qu.queue[i] = nil + qu.size-- + } + } + + // Get rid of the holes which may have been formed + // in the queue. + qu.shiftLeft() + + if qu.size == readCompactionMaxQueueSize { + // Make space at the end. + copy(qu.queue[0:], qu.queue[1:]) + qu.queue[qu.size-1] = rc + } else { + qu.size++ + qu.queue[qu.size-1] = rc + } +} + +// Shifts the non-nil elements of the queue to the left so +// that a continguous prefix of the queue is non-nil. +func (qu *readCompactionQueue) shiftLeft() { + nilPos := -1 + for i := 0; i < readCompactionMaxQueueSize; i++ { + if qu.queue[i] == nil && nilPos == -1 { + nilPos = i + } else if qu.queue[i] != nil && nilPos != -1 { + qu.queue[nilPos] = qu.queue[i] + qu.queue[i] = nil + nilPos++ + } + } +} + +// remove will remove the oldest element from the queue. +func (qu *readCompactionQueue) remove() *readCompaction { + if qu.size == 0 { + return nil + } + + c := qu.queue[0] + copy(qu.queue[0:], qu.queue[1:]) + qu.queue[qu.size-1] = nil + qu.size-- + return c +} diff --git a/pebble/read_state.go b/pebble/read_state.go new file mode 100644 index 0000000..d3a78ba --- /dev/null +++ b/pebble/read_state.go @@ -0,0 +1,106 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import "sync/atomic" + +// readState encapsulates the state needed for reading (the current version and +// list of memtables). Loading the readState is done without grabbing +// DB.mu. Instead, a separate DB.readState.RWMutex is used for +// synchronization. This mutex solely covers the current readState object which +// means it is rarely or ever contended. +// +// Note that various fancy lock-free mechanisms can be imagined for loading the +// readState, but benchmarking showed the ones considered to purely be +// pessimizations. The RWMutex version is a single atomic increment for the +// RLock and an atomic decrement for the RUnlock. It is difficult to do better +// than that without something like thread-local storage which isn't available +// in Go. +type readState struct { + db *DB + refcnt atomic.Int32 + current *version + memtables flushableList +} + +// ref adds a reference to the readState. +func (s *readState) ref() { + s.refcnt.Add(1) +} + +// unref 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 NOT held as version.unref() will acquire it. See unrefLocked() if DB.mu +// is held by the caller. +func (s *readState) unref() { + if s.refcnt.Add(-1) != 0 { + return + } + s.current.Unref() + for _, mem := range s.memtables { + mem.readerUnref(true) + } + + // The last reference to the readState was released. Check to see if there + // are new obsolete tables to delete. + s.db.maybeScheduleObsoleteTableDeletion() +} + +// unrefLocked removes a reference to the readState. If this was the last +// reference, the reference the readState holds on the version is +// released. +// +// DB.mu must be held. See unref() if DB.mu is NOT held by the caller. +func (s *readState) unrefLocked() { + if s.refcnt.Add(-1) != 0 { + return + } + s.current.UnrefLocked() + for _, mem := range s.memtables { + mem.readerUnrefLocked(true) + } + + // 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 +// unreferenced when the caller is finished with it. +func (d *DB) loadReadState() *readState { + d.readState.RLock() + state := d.readState.val + state.ref() + d.readState.RUnlock() + return state +} + +// updateReadStateLocked creates a new readState from the current version and +// list of memtables. Requires DB.mu is held. If checker is not nil, it is +// called after installing the new readState. +func (d *DB) updateReadStateLocked(checker func(*DB) error) { + s := &readState{ + db: d, + current: d.mu.versions.currentVersion(), + memtables: d.mu.mem.queue, + } + s.refcnt.Store(1) + s.current.Ref() + for _, mem := range s.memtables { + mem.readerRef() + } + + d.readState.Lock() + old := d.readState.val + d.readState.val = s + d.readState.Unlock() + if checker != nil { + if err := checker(d); err != nil { + d.opts.Logger.Fatalf("checker failed with error: %s", err) + } + } + if old != nil { + old.unrefLocked() + } +} diff --git a/pebble/read_state_test.go b/pebble/read_state_test.go new file mode 100644 index 0000000..4f9d5f5 --- /dev/null +++ b/pebble/read_state_test.go @@ -0,0 +1,46 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package pebble + +import ( + "fmt" + "testing" + "time" + + "github.com/cockroachdb/pebble/vfs" + "golang.org/x/exp/rand" +) + +func BenchmarkReadState(b *testing.B) { + d, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + if err != nil { + b.Fatal(err) + } + + for _, updateFrac := range []float32{0, 0.1, 0.5} { + b.Run(fmt.Sprintf("updates=%.0f", updateFrac*100), func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + for pb.Next() { + if rng.Float32() < updateFrac { + d.mu.Lock() + d.updateReadStateLocked(nil) + d.mu.Unlock() + } else { + s := d.loadReadState() + s.unref() + } + } + }) + }) + } + + if err := d.Close(); err != nil { + b.Fatal(err) + } +} diff --git a/pebble/record/log_writer.go b/pebble/record/log_writer.go new file mode 100644 index 0000000..891879d --- /dev/null +++ b/pebble/record/log_writer.go @@ -0,0 +1,771 @@ +// Copyright 2018 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 record + +import ( + "context" + "encoding/binary" + "io" + "runtime/pprof" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/crc" + "github.com/prometheus/client_golang/prometheus" +) + +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 atomic.Int32 + // buf[:flushed] has already been flushed to w. + flushed int32 + buf [blockSize]byte +} + +type flusher interface { + Flush() error +} + +type syncer interface { + Sync() error +} + +const ( + syncConcurrencyBits = 12 + + // SyncConcurrency is the maximum number of concurrent sync operations that + // can be performed. Note that a sync operation is initiated either by a call + // to SyncRecord or by a call to Close. Exported as this value also limits + // the commit concurrency in commitPipeline. + SyncConcurrency = 1 << syncConcurrencyBits +) + +type syncSlot struct { + wg *sync.WaitGroup + err *error +} + +// syncQueue is a lock-free fixed-size single-producer, single-consumer +// queue. The single-producer can push to the head, and the single-consumer can +// pop multiple values from the tail. Popping calls Done() on each of the +// available *sync.WaitGroup elements. +type syncQueue struct { + // headTail packs together a 32-bit head index and a 32-bit tail index. Both + // are indexes into slots modulo len(slots)-1. + // + // tail = index of oldest data in queue + // head = index of next slot to fill + // + // Slots in the range [tail, head) are owned by consumers. A consumer + // continues to own a slot outside this range until it nils the slot, at + // which point ownership passes to the producer. + // + // The head index is stored in the most-significant bits so that we can + // atomically add to it and the overflow is harmless. + 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. + slots [SyncConcurrency]syncSlot + + // 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 atomic.Bool +} + +const dequeueBits = 32 + +func (q *syncQueue) unpack(ptrs uint64) (head, tail uint32) { + const mask = 1<> dequeueBits) & mask) + tail = uint32(ptrs & mask) + return +} + +func (q *syncQueue) push(wg *sync.WaitGroup, err *error) { + 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 + } + if f.close { + // If the writer is closed, pretend the sync timer fired immediately so + // that we can process any queued sync requests. + f.syncQ.clearBlocked() + if !f.syncQ.empty() { + break + } + return + } + f.ready.Wait() + continue + } + // Found work to do, so no longer idle. + workStartTime := time.Now() + idleDuration := workStartTime.Sub(idleStartTime) + pending = append(pending[:0], f.pending...) + f.pending = f.pending[:0] + f.metrics.PendingBufferLen.AddSample(int64(len(pending))) + + // Grab the list of sync waiters. Note that syncQueue.load() will return + // 0,0 while we're waiting for the min-sync-interval to expire. This + // allows flushing to proceed even if we're not ready to sync. + head, tail, realSyncQLen := f.syncQ.load() + f.metrics.SyncQueueLen.AddSample(int64(realSyncQLen)) + + // Grab the portion of the current block that requires flushing. Note that + // the current block can be added to the pending blocks list after we + // release the flusher lock, but it won't be part of pending. This has to + // 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 := w.block.written.Load() + data := w.block.buf[w.block.flushed:written] + w.block.flushed = written + + // If flusher has an error, we propagate it to waiters. Note in spite of + // error we consume the pending list above to free blocks for writers. + if f.err != nil { + f.syncQ.pop(head, tail, f.err, w.queueSemChan) + // Update the idleStartTime if work could not be done, so that we don't + // include the duration we tried to do work as idle. We don't bother + // with the rest of the accounting, which means we will undercount. + idleStartTime = time.Now() + continue + } + f.Unlock() + synced, syncLatency, bytesWritten, err := w.flushPending(data, pending, head, tail) + f.Lock() + if synced && f.fsyncLatency != nil { + f.fsyncLatency.Observe(float64(syncLatency)) + } + f.err = err + if f.err != nil { + f.syncQ.clearBlocked() + // Update the idleStartTime if work could not be done, so that we don't + // include the duration we tried to do work as idle. We don't bother + // with the rest of the accounting, which means we will undercount. + idleStartTime = time.Now() + continue + } + + if synced && f.minSyncInterval != nil { + // A sync was performed. Make sure we've waited for the min sync + // interval before syncing again. + if min := f.minSyncInterval(); min > 0 { + f.syncQ.setBlocked() + if syncTimer == nil { + syncTimer = w.afterFunc(min, func() { + f.syncQ.clearBlocked() + f.ready.Signal() + }) + } else { + syncTimer.Reset(min) + } + } + } + // Finished work, and started idling. + idleStartTime = time.Now() + workDuration := idleStartTime.Sub(workStartTime) + f.metrics.WriteThroughput.Bytes += bytesWritten + f.metrics.WriteThroughput.WorkDuration += workDuration + f.metrics.WriteThroughput.IdleDuration += idleDuration + } +} + +func (w *LogWriter) flushPending( + data []byte, pending []*block, head, tail uint32, +) (synced bool, syncLatency time.Duration, bytesWritten int64, err error) { + defer func() { + // Translate panics into errors. The errors will cause flushLoop to shut + // down, but allows us to do so in a controlled way and avoid swallowing + // the stack that created the panic if panic'ing itself hits a panic + // (e.g. unlock of unlocked mutex). + if r := recover(); r != nil { + err = errors.Newf("%v", r) + } + }() + + for _, b := range pending { + bytesWritten += blockSize - int64(b.flushed) + if err = w.flushBlock(b); err != nil { + break + } + } + if n := len(data); err == nil && n > 0 { + bytesWritten += int64(n) + _, err = w.w.Write(data) + } + + synced = head != tail + if synced { + if err == nil && w.s != nil { + syncLatency, err = w.syncWithLatency() + } + f := &w.flusher + if popErr := f.syncQ.pop(head, tail, err, w.queueSemChan); popErr != nil { + return synced, syncLatency, bytesWritten, popErr + } + } + + return synced, syncLatency, bytesWritten, err +} + +func (w *LogWriter) syncWithLatency() (time.Duration, error) { + start := time.Now() + err := w.s.Sync() + syncLatency := time.Since(start) + return syncLatency, err +} + +func (w *LogWriter) flushBlock(b *block) error { + if _, err := w.w.Write(b.buf[b.flushed:]); err != nil { + return err + } + b.written.Store(0) + b.flushed = 0 + w.free.Lock() + w.free.blocks = append(w.free.blocks, b) + 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() { + // 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 { + 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] + w.free.Unlock() + + f := &w.flusher + f.Lock() + f.pending = append(f.pending, w.block) + w.block = nextBlock + f.ready.Signal() + w.err = w.flusher.err + f.Unlock() + + w.blockNum++ +} + +// Close flushes and syncs any unwritten data and closes the writer. +// Where required, external synchronisation is provided by commitPipeline.mu. +func (w *LogWriter) Close() error { + f := &w.flusher + + // Emit an EOF trailer signifying the end of this log. This helps readers + // differentiate between a corrupted entry in the middle of a log from + // garbage at the tail from a recycled log file. + w.emitEOFTrailer() + + // Signal the flush loop to close. + f.Lock() + f.close = true + f.ready.Signal() + f.Unlock() + + // Wait for the flush loop to close. The flush loop will not close until all + // pending data has been written or an error occurs. + <-f.closed + + // Sync any flushed data to disk. NB: flushLoop will sync after flushing the + // last buffered data only if it was requested via syncQ, so we need to sync + // here to ensure that all the data is synced. + err := w.flusher.err + var syncLatency time.Duration + if err == nil && w.s != nil { + syncLatency, err = w.syncWithLatency() + } + f.Lock() + if f.fsyncLatency != nil { + f.fsyncLatency.Observe(float64(syncLatency)) + } + free := w.free.blocks + f.Unlock() + + if w.c != nil { + cerr := w.c.Close() + w.c = nil + if cerr != nil { + return cerr + } + } + + for _, b := range free { + b.flushed = 0 + b.written.Store(0) + blockPool.Put(b) + } + + w.err = errClosedWriter + return err +} + +// WriteRecord writes a complete record. Returns the offset just past the end +// of the record. +// External synchronisation provided by commitPipeline.mu. +func (w *LogWriter) WriteRecord(p []byte) (int64, error) { + logSize, err := w.SyncRecord(p, nil, nil) + return logSize, err +} + +// 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, err2 error) { + if w.err != nil { + return -1, w.err + } + + // The `i == 0` condition ensures we handle empty records. Such records can + // possibly be generated for VersionEdits stored in the MANIFEST. While the + // 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++ { + p = w.emitFragment(i, p) + } + + if wg != nil { + // If we've been asked to persist the record, add the WaitGroup to the sync + // queue and signal the flushLoop. Note that flushLoop will write partial + // blocks to the file if syncing has been requested. The contract is that + // any record written to the LogWriter to this point will be flushed to the + // OS and synced to disk. + f := &w.flusher + f.syncQ.push(wg, err) + f.ready.Signal() + } + + 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, 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.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.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 + b.written.Store(i + int32(recyclableHeaderSize)) +} + +func (w *LogWriter) emitFragment(n int, p []byte) (remainingP []byte) { + b := w.block + i := b.written.Load() + first := n == 0 + last := blockSize-i-recyclableHeaderSize >= int32(len(p)) + + if last { + if first { + b.buf[i+6] = recyclableFullChunkType + } else { + b.buf[i+6] = recyclableLastChunkType + } + } else { + if first { + b.buf[i+6] = recyclableFirstChunkType + } else { + b.buf[i+6] = recyclableMiddleChunkType + } + } + + binary.LittleEndian.PutUint32(b.buf[i+7:i+11], w.logNum) + + r := copy(b.buf[i+recyclableHeaderSize:], p) + 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)) + b.written.Store(j) + + 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.Load(); i < blockSize; i++ { + b.buf[i] = 0 + } + w.queueBlock() + } + return p[r:] +} + +// Metrics must be called after Close. The callee will no longer modify the +// returned LogWriterMetrics. +func (w *LogWriter) Metrics() *LogWriterMetrics { + return w.flusher.metrics +} + +// LogWriterMetrics contains misc metrics for the log writer. +type LogWriterMetrics struct { + WriteThroughput base.ThroughputMetric + PendingBufferLen base.GaugeSampleMetric + SyncQueueLen base.GaugeSampleMetric +} + +// Merge merges metrics from x. Requires that x is non-nil. +func (m *LogWriterMetrics) Merge(x *LogWriterMetrics) error { + m.WriteThroughput.Merge(x.WriteThroughput) + m.PendingBufferLen.Merge(x.PendingBufferLen) + m.SyncQueueLen.Merge(x.SyncQueueLen) + return nil +} diff --git a/pebble/record/log_writer_test.go b/pebble/record/log_writer_test.go new file mode 100644 index 0000000..973105a --- /dev/null +++ b/pebble/record/log_writer_test.go @@ -0,0 +1,593 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package record + +import ( + "bytes" + "fmt" + "math" + "sort" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/cockroachdb/pebble/vfs/vfstest" + "github.com/prometheus/client_golang/prometheus" + prometheusgo "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" +) + +type syncErrorFile struct { + vfs.File + err error +} + +func (f syncErrorFile) Sync() error { + return f.err +} + +func TestSyncQueue(t *testing.T) { + var q syncQueue + var closed atomic.Bool + + var flusherWG sync.WaitGroup + flusherWG.Add(1) + go func() { + defer flusherWG.Done() + for { + if closed.Load() { + return + } + head, tail, _ := q.load() + q.pop(head, tail, nil, nil) + } + }() + + var commitMu sync.Mutex + var doneWG sync.WaitGroup + for i := 0; i < SyncConcurrency; i++ { + doneWG.Add(1) + go func(i int) { + defer doneWG.Done() + for j := 0; j < 1000; j++ { + wg := &sync.WaitGroup{} + wg.Add(1) + // syncQueue is a single-producer, single-consumer queue. We need to + // provide mutual exclusion on the producer side. + commitMu.Lock() + q.push(wg, new(error)) + commitMu.Unlock() + wg.Wait() + } + }(i) + } + doneWG.Wait() + + closed.Store(true) + flusherWG.Wait() +} + +func TestFlusherCond(t *testing.T) { + var mu sync.Mutex + var q syncQueue + var c flusherCond + var closed bool + + c.init(&mu, &q) + + var flusherWG sync.WaitGroup + flusherWG.Add(1) + go func() { + defer flusherWG.Done() + + mu.Lock() + defer mu.Unlock() + + for { + for { + if closed { + return + } + if !q.empty() { + break + } + c.Wait() + } + + head, tail, _ := q.load() + q.pop(head, tail, nil, nil) + } + }() + + var commitMu sync.Mutex + var doneWG sync.WaitGroup + // NB: we're testing with low concurrency here, because what we want to + // stress is that signalling of the flusherCond works + // correctly. Specifically, we want to make sure that a signal is "lost", + // causing the test to wedge. + for i := 0; i < 2; i++ { + doneWG.Add(1) + go func(i int) { + defer doneWG.Done() + for j := 0; j < 10000; j++ { + wg := &sync.WaitGroup{} + wg.Add(1) + // syncQueue is a single-producer, single-consumer queue. We need to + // provide mutual exclusion on the producer side. + commitMu.Lock() + q.push(wg, new(error)) + commitMu.Unlock() + c.Signal() + wg.Wait() + } + }(i) + } + doneWG.Wait() + + mu.Lock() + closed = true + c.Signal() + mu.Unlock() + flusherWG.Wait() +} + +func TestSyncError(t *testing.T) { + mem := vfs.NewMem() + f, err := mem.Create("log") + require.NoError(t, err) + + injectedErr := errors.New("injected error") + w := NewLogWriter(syncErrorFile{f, injectedErr}, 0, LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), + }) + + syncRecord := func() { + var syncErr error + var syncWG sync.WaitGroup + syncWG.Add(1) + _, err = w.SyncRecord([]byte("hello"), &syncWG, &syncErr) + require.NoError(t, err) + syncWG.Wait() + if injectedErr != syncErr { + t.Fatalf("unexpected %v but found %v", injectedErr, syncErr) + } + } + // First waiter receives error. + syncRecord() + // All subsequent waiters also receive the error. + syncRecord() + syncRecord() +} + +type syncFile struct { + writePos atomic.Int64 + syncPos atomic.Int64 +} + +func (f *syncFile) Write(buf []byte) (int, error) { + n := len(buf) + f.writePos.Add(int64(n)) + return n, nil +} + +func (f *syncFile) Sync() error { + f.syncPos.Store(f.writePos.Load()) + return nil +} + +func TestSyncRecord(t *testing.T) { + f := &syncFile{} + w := NewLogWriter(f, 0, LogWriterConfig{WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + + var syncErr error + for i := 0; i < 100000; i++ { + var syncWG sync.WaitGroup + syncWG.Add(1) + offset, err := w.SyncRecord([]byte("hello"), &syncWG, &syncErr) + require.NoError(t, err) + syncWG.Wait() + require.NoError(t, syncErr) + if v := f.writePos.Load(); offset != v { + t.Fatalf("expected write pos %d, but found %d", offset, v) + } + if v := f.syncPos.Load(); offset != v { + t.Fatalf("expected sync pos %d, but found %d", offset, v) + } + } +} + +func TestSyncRecordWithSignalChan(t *testing.T) { + f := &syncFile{} + semChan := make(chan struct{}, 5) + for i := 0; i < cap(semChan); i++ { + semChan <- struct{}{} + } + w := NewLogWriter(f, 0, LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), + QueueSemChan: semChan, + }) + require.Equal(t, cap(semChan), len(semChan)) + var syncErr error + for i := 0; i < 5; i++ { + var syncWG sync.WaitGroup + syncWG.Add(1) + _, err := w.SyncRecord([]byte("hello"), &syncWG, &syncErr) + require.NoError(t, err) + syncWG.Wait() + require.NoError(t, syncErr) + // The waitgroup is released before the channel is read, so wait if + // necessary. + require.Eventually(t, func() bool { + return cap(semChan)-(i+1) == len(semChan) + }, 10*time.Second, time.Millisecond) + } +} + +type fakeTimer struct { + f func() +} + +func (t *fakeTimer) Reset(d time.Duration) bool { + return false +} + +func (t *fakeTimer) Stop() bool { + return false +} + +func try(initialSleep, maxTotalSleep time.Duration, f func() error) error { + totalSleep := time.Duration(0) + for d := initialSleep; ; d *= 2 { + time.Sleep(d) + totalSleep += d + if err := f(); err == nil || totalSleep >= maxTotalSleep { + return err + } + } +} + +func TestMinSyncInterval(t *testing.T) { + const minSyncInterval = 100 * time.Millisecond + + f := &syncFile{} + w := NewLogWriter(f, 0, LogWriterConfig{ + WALMinSyncInterval: func() time.Duration { + return minSyncInterval + }, + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), + }) + + var timer fakeTimer + w.afterFunc = func(d time.Duration, f func()) syncTimer { + if d != minSyncInterval { + t.Fatalf("expected minSyncInterval %s, but found %s", minSyncInterval, d) + } + timer.f = f + timer.Reset(d) + return &timer + } + + syncRecord := func(n int) *sync.WaitGroup { + wg := &sync.WaitGroup{} + wg.Add(1) + _, err := w.SyncRecord(bytes.Repeat([]byte{'a'}, n), wg, new(error)) + require.NoError(t, err) + return wg + } + + // Sync one record which will cause the sync timer to kick in. + syncRecord(1).Wait() + + startWritePos := f.writePos.Load() + startSyncPos := f.syncPos.Load() + + // Write a bunch of large records. The sync position should not change + // because we haven't triggered the timer. But note that the writes should + // not block either even though syncing isn't being done. + var wg *sync.WaitGroup + for i := 0; i < 100; i++ { + wg = syncRecord(10000) + if v := f.syncPos.Load(); startSyncPos != v { + t.Fatalf("expected syncPos %d, but found %d", startSyncPos, v) + } + // NB: we can't use syncQueue.load() here as that will return 0,0 while the + // syncQueue is blocked. + head, tail := w.flusher.syncQ.unpack(w.flusher.syncQ.headTail.Load()) + waiters := head - tail + if waiters != uint32(i+1) { + t.Fatalf("expected %d waiters, but found %d", i+1, waiters) + } + } + + err := try(time.Millisecond, 5*time.Second, func() error { + v := f.writePos.Load() + if v > startWritePos { + return nil + } + return errors.Errorf("expected writePos > %d, but found %d", startWritePos, v) + }) + require.NoError(t, err) + + // Fire the timer, and then wait for the last record to sync. + timer.f() + wg.Wait() + + if w, s := f.writePos.Load(), f.syncPos.Load(); w != s { + t.Fatalf("expected syncPos %d, but found %d", s, w) + } +} + +func TestMinSyncIntervalClose(t *testing.T) { + const minSyncInterval = 100 * time.Millisecond + + f := &syncFile{} + w := NewLogWriter(f, 0, LogWriterConfig{ + WALMinSyncInterval: func() time.Duration { + return minSyncInterval + }, + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), + }) + + var timer fakeTimer + w.afterFunc = func(d time.Duration, f func()) syncTimer { + if d != minSyncInterval { + t.Fatalf("expected minSyncInterval %s, but found %s", minSyncInterval, d) + } + timer.f = f + timer.Reset(d) + return &timer + } + + syncRecord := func(n int) *sync.WaitGroup { + wg := &sync.WaitGroup{} + wg.Add(1) + _, err := w.SyncRecord(bytes.Repeat([]byte{'a'}, n), wg, new(error)) + require.NoError(t, err) + return wg + } + + // Sync one record which will cause the sync timer to kick in. + syncRecord(1).Wait() + + // Syncing another record will not complete until the timer is fired OR the + // writer is closed. + wg := syncRecord(1) + require.NoError(t, w.Close()) + wg.Wait() +} + +type syncFileWithWait struct { + f syncFile + writeWG sync.WaitGroup + syncWG sync.WaitGroup +} + +func (f *syncFileWithWait) Write(buf []byte) (int, error) { + f.writeWG.Wait() + return f.f.Write(buf) +} + +func (f *syncFileWithWait) Sync() error { + f.syncWG.Wait() + return f.f.Sync() +} + +func TestMetricsWithoutSync(t *testing.T) { + f := &syncFileWithWait{} + f.writeWG.Add(1) + w := NewLogWriter(f, 0, LogWriterConfig{WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + offset, err := w.SyncRecord([]byte("hello"), nil, nil) + require.NoError(t, err) + const recordSize = 16 + require.EqualValues(t, recordSize, offset) + // We have 512KB of buffer capacity, and 5 bytes + overhead = 16 bytes for + // each record. Write 28 * 1024 records to fill it up to 87.5%. This + // constitutes ~14 blocks (each 32KB). + const numRecords = 28 << 10 + for i := 0; i < numRecords; i++ { + _, err = w.SyncRecord([]byte("hello"), nil, nil) + require.NoError(t, err) + } + // Unblock the flush loop. It will run once or twice to write these blocks, + // plus may run one more time due to the Close, so up to 3 runs. So ~14 + // blocks flushed over up to 3 runs. + f.writeWG.Done() + w.Close() + m := w.Metrics() + // Mean is >= 4 filled blocks. + require.LessOrEqual(t, float64(4), m.PendingBufferLen.Mean()) + // None of these writes asked to be synced. + require.EqualValues(t, 0, int(m.SyncQueueLen.Mean())) + require.Less(t, int64(numRecords*recordSize), m.WriteThroughput.Bytes) +} + +func TestMetricsWithSync(t *testing.T) { + f := &syncFileWithWait{} + f.syncWG.Add(1) + syncLatencyMicros := prometheus.NewHistogram(prometheus.HistogramOpts{ + Buckets: []float64{0, + float64(time.Millisecond), + float64(2 * time.Millisecond), + float64(3 * time.Millisecond), + float64(4 * time.Millisecond), + float64(5 * time.Millisecond), + float64(6 * time.Millisecond), + float64(7 * time.Millisecond), + float64(8 * time.Millisecond), + float64(9 * time.Millisecond), + float64(10 * time.Millisecond)}, + }) + + w := NewLogWriter(f, 0, LogWriterConfig{ + WALFsyncLatency: syncLatencyMicros, + }, + ) + var wg sync.WaitGroup + wg.Add(100) + for i := 0; i < 100; i++ { + var syncErr error + _, err := w.SyncRecord([]byte("hello"), &wg, &syncErr) + require.NoError(t, err) + } + + const syncLatency = 100 * time.Millisecond + go func() { + time.Sleep(syncLatency) + // Unblock the flush loop. It may have run once or twice for these writes, + // plus may run one more time due to the Close, so up to 3 runs. So 100 + // elements in the sync queue, spread over up to 3 runs. + f.syncWG.Done() + }() + + // Close() will only return after flushing is finished. + require.NoError(t, w.Close()) + + m := w.Metrics() + require.LessOrEqual(t, float64(30), m.SyncQueueLen.Mean()) + + writeTo := &prometheusgo.Metric{} + require.NoError(t, syncLatencyMicros.Write(writeTo)) + for i := 0; i < 100; i += 10 { + t.Logf("%d%%: %v", i, valueAtQuantileWindowed(writeTo.Histogram, float64(i))) + } + // Allow for some inaccuracy in sleep and for two syncs, one of which was + // fast. + require.LessOrEqual(t, float64(syncLatency/(2*time.Microsecond)), + valueAtQuantileWindowed(writeTo.Histogram, 90)) + require.LessOrEqual(t, syncLatency/2, m.WriteThroughput.WorkDuration) +} + +func valueAtQuantileWindowed(histogram *prometheusgo.Histogram, q float64) float64 { + buckets := histogram.Bucket + n := float64(*histogram.SampleCount) + if n == 0 { + return 0 + } + + // NB: The 0.5 is added for rounding purposes; it helps in cases where + // SampleCount is small. + rank := uint64(((q / 100) * n) + 0.5) + + // Since we are missing the +Inf bucket, CumulativeCounts may never exceed + // rank. By omitting the highest bucket we have from the search, the failed + // search will land on that last bucket and we don't have to do any special + // checks regarding landing on a non-existent bucket. + b := sort.Search(len(buckets)-1, func(i int) bool { return *buckets[i].CumulativeCount >= rank }) + + var ( + bucketStart float64 // defaults to 0, which we assume is the lower bound of the smallest bucket + bucketEnd = *buckets[b].UpperBound + count = *buckets[b].CumulativeCount + ) + + // Calculate the linearly interpolated value within the bucket. + if b > 0 { + bucketStart = *buckets[b-1].UpperBound + count -= *buckets[b-1].CumulativeCount + rank -= *buckets[b-1].CumulativeCount + } + val := bucketStart + (bucketEnd-bucketStart)*(float64(rank)/float64(count)) + if math.IsNaN(val) || math.IsInf(val, -1) { + return 0 + } + + // Should not extrapolate past the upper bound of the largest bucket. + // + // NB: SampleCount includes the implicit +Inf bucket but the + // buckets[len(buckets)-1].UpperBound refers to the largest bucket defined + // by us -- the client library doesn't give us access to the +Inf bucket + // which Prometheus uses under the hood. With a high enough quantile, the + // val computed further below surpasses the upper bound of the largest + // bucket. Using that interpolated value feels wrong since we'd be + // extrapolating. Also, for specific metrics if we see our q99 values to be + // hitting the top-most bucket boundary, that's an indication for us to + // choose better buckets for more accuracy. It's also worth noting that the + // prometheus client library does the same thing when the resulting value is + // in the +Inf bucket, whereby they return the upper bound of the second + // last bucket -- see [1]. + // + // [1]: https://github.com/prometheus/prometheus/blob/d9162189/promql/quantile.go#L103. + if val > *buckets[len(buckets)-1].UpperBound { + return *buckets[len(buckets)-1].UpperBound + } + + return val +} + +// TestQueueWALBlocks tests queueing many un-flushed WAL blocks when syncing is +// blocked. +func TestQueueWALBlocks(t *testing.T) { + blockWriteCh := make(chan struct{}, 1) + f := errorfs.WrapFile(vfstest.DiscardFile, errorfs.InjectorFunc(func(op errorfs.Op) error { + if op.Kind == errorfs.OpFileWrite { + <-blockWriteCh + } + return nil + })) + w := NewLogWriter(f, 0, LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), + }) + const numBlocks = 1024 + var b [blockSize]byte + var logSize int64 + for i := 0; i < numBlocks; i++ { + var err error + logSize, err = w.SyncRecord(b[:], nil, nil) + if err != nil { + t.Fatal(err) + } + } + close(blockWriteCh) + require.NoError(t, w.Close()) + + m := w.Metrics() + t.Logf("LogSize is %s", humanize.Bytes.Int64(logSize)) + t.Logf("Mean pending buffer len is %.2f", m.PendingBufferLen.Mean()) + require.GreaterOrEqual(t, logSize, int64(numBlocks*blockSize)) +} + +// BenchmarkQueueWALBlocks exercises queueing within the LogWriter. It can be +// useful to measure allocations involved when flushing is slow enough to +// accumulate a large backlog fo queued blocks. +func BenchmarkQueueWALBlocks(b *testing.B) { + const dataVolume = 64 << 20 /* 64 MB */ + for _, writeSize := range []int64{64, 512, 1024, 2048, 32768} { + b.Run(fmt.Sprintf("record-size=%s", humanize.Bytes.Int64(writeSize)), func(b *testing.B) { + record := make([]byte, writeSize) + numRecords := int(dataVolume / writeSize) + + for j := 0; j < b.N; j++ { + b.StopTimer() + blockWriteCh := make(chan struct{}, 1) + f := errorfs.WrapFile(vfstest.DiscardFile, errorfs.InjectorFunc(func(op errorfs.Op) error { + if op.Kind == errorfs.OpFileWrite { + <-blockWriteCh + } + return nil + })) + w := NewLogWriter(f, 0, LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), + }) + + b.StartTimer() + for n := numRecords; n > 0; n-- { + if _, err := w.SyncRecord(record[:], nil, nil); err != nil { + b.Fatal(err) + } + } + b.StopTimer() + + b.SetBytes(dataVolume) + close(blockWriteCh) + require.NoError(b, w.Close()) + } + }) + } +} diff --git a/pebble/record/record.go b/pebble/record/record.go new file mode 100644 index 0000000..8924bfb --- /dev/null +++ b/pebble/record/record.go @@ -0,0 +1,644 @@ +// 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 record reads and writes sequences of records. Each record is a stream +// of bytes that completes before the next record starts. +// +// When reading, call Next to obtain an io.Reader for the next record. Next will +// return io.EOF when there are no more records. It is valid to call Next +// without reading the current record to exhaustion. +// +// When writing, call Next to obtain an io.Writer for the next record. Calling +// Next finishes the current record. Call Close to finish the final record. +// +// Optionally, call Flush to finish the current record and flush the underlying +// writer without starting a new record. To start a new record after flushing, +// call Next. +// +// Neither Readers or Writers are safe to use concurrently. +// +// Example code: +// +// func read(r io.Reader) ([]string, error) { +// var ss []string +// records := record.NewReader(r) +// for { +// rec, err := records.Next() +// if err == io.EOF { +// break +// } +// if err != nil { +// log.Printf("recovering from %v", err) +// r.Recover() +// continue +// } +// s, err := io.ReadAll(rec) +// if err != nil { +// log.Printf("recovering from %v", err) +// r.Recover() +// continue +// } +// ss = append(ss, string(s)) +// } +// return ss, nil +// } +// +// func write(w io.Writer, ss []string) error { +// records := record.NewWriter(w) +// for _, s := range ss { +// rec, err := records.Next() +// if err != nil { +// return err +// } +// if _, err := rec.Write([]byte(s)), err != nil { +// return err +// } +// } +// return records.Close() +// } +// +// The wire format is that the stream is divided into 32KiB blocks, and each +// block contains a number of tightly packed chunks. Chunks cannot cross block +// boundaries. The last block may be shorter than 32 KiB. Any unused bytes in a +// block must be zero. +// +// A record maps to one or more chunks. There are two chunk formats: legacy and +// recyclable. The legacy chunk format: +// +// +----------+-----------+-----------+--- ... ---+ +// | CRC (4B) | Size (2B) | Type (1B) | Payload | +// +----------+-----------+-----------+--- ... ---+ +// +// CRC is computed over the type and payload +// Size is the length of the payload in bytes +// Type is the chunk type +// +// There are four chunk types: whether the chunk is the full record, or the +// first, middle or last chunk of a multi-chunk record. A multi-chunk record +// has one first chunk, zero or more middle chunks, and one last chunk. +// +// The recyclyable chunk format is similar to the legacy format, but extends +// the chunk header with an additional log number field. This allows reuse +// (recycling) of log files which can provide significantly better performance +// when syncing frequently as it avoids needing to update the file +// metadata. Additionally, recycling log files is a prequisite for using direct +// IO with log writing. The recyclyable format is: +// +// +----------+-----------+-----------+----------------+--- ... ---+ +// | CRC (4B) | Size (2B) | Type (1B) | Log number (4B)| Payload | +// +----------+-----------+-----------+----------------+--- ... ---+ +// +// Recyclable chunks are distinguished from legacy chunks by the addition of 4 +// extra "recyclable" chunk types that map directly to the legacy chunk types +// (i.e. full, first, middle, last). The CRC is computed over the type, log +// number, and payload. +// +// The wire format allows for limited recovery in the face of data corruption: +// on a format error (such as a checksum mismatch), the reader moves to the +// next block and looks for the next full or first chunk. +package record + +// The C++ Level-DB code calls this the log, but it has been renamed to record +// to avoid clashing with the standard log package, and because it is generally +// useful outside of logging. The C++ code also uses the term "physical record" +// instead of "chunk", but "chunk" is shorter and less confusing. + +import ( + "encoding/binary" + "io" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/crc" +) + +// These constants are part of the wire format and should not be changed. +const ( + fullChunkType = 1 + firstChunkType = 2 + middleChunkType = 3 + lastChunkType = 4 + + recyclableFullChunkType = 5 + recyclableFirstChunkType = 6 + recyclableMiddleChunkType = 7 + recyclableLastChunkType = 8 +) + +const ( + blockSize = 32 * 1024 + blockSizeMask = blockSize - 1 + legacyHeaderSize = 7 + recyclableHeaderSize = legacyHeaderSize + 4 +) + +var ( + // ErrNotAnIOSeeker is returned if the io.Reader underlying a Reader does not implement io.Seeker. + ErrNotAnIOSeeker = errors.New("pebble/record: reader does not implement io.Seeker") + + // ErrNoLastRecord is returned if LastRecordOffset is called and there is no previous record. + ErrNoLastRecord = errors.New("pebble/record: no last record exists") + + // ErrZeroedChunk is returned if a chunk is encountered that is zeroed. This + // usually occurs due to log file preallocation. + ErrZeroedChunk = base.CorruptionErrorf("pebble/record: zeroed chunk") + + // ErrInvalidChunk is returned if a chunk is encountered with an invalid + // header, length, or checksum. This usually occurs when a log is recycled, + // but can also occur due to corruption. + ErrInvalidChunk = base.CorruptionErrorf("pebble/record: invalid chunk") +) + +// IsInvalidRecord returns true if the error matches one of the error types +// returned for invalid records. These are treated in a way similar to io.EOF +// in recovery code. +func IsInvalidRecord(err error) bool { + return err == ErrZeroedChunk || err == ErrInvalidChunk || err == io.ErrUnexpectedEOF +} + +// Reader reads records from an underlying io.Reader. +type Reader struct { + // r is the underlying reader. + r io.Reader + // logNum is the low 32-bits of the log's file number. May be zero when used + // with log files that do not have a file number (e.g. the MANIFEST). + logNum uint32 + // blockNum is the zero based block number currently held in buf. + blockNum int64 + // seq is the sequence number of the current record. + seq int + // buf[begin:end] is the unread portion of the current chunk's payload. The + // low bound, begin, excludes the chunk header. + begin, end int + // n is the number of bytes of buf that are valid. Once reading has started, + // only the final block can have n < blockSize. + n int + // recovering is true when recovering from corruption. + recovering bool + // last is whether the current chunk is the last chunk of the record. + last bool + // err is any accumulated error. + err error + // buf is the buffer. + buf [blockSize]byte +} + +// NewReader returns a new reader. If the file contains records encoded using +// the recyclable record format, then the log number in those records must +// match the specified logNum. +func NewReader(r io.Reader, logNum base.DiskFileNum) *Reader { + return &Reader{ + r: r, + logNum: uint32(logNum), + blockNum: -1, + } +} + +// nextChunk sets r.buf[r.i:r.j] to hold the next chunk's payload, reading the +// next block into the buffer if necessary. +func (r *Reader) nextChunk(wantFirst bool) error { + for { + if r.end+legacyHeaderSize <= r.n { + checksum := binary.LittleEndian.Uint32(r.buf[r.end+0 : r.end+4]) + length := binary.LittleEndian.Uint16(r.buf[r.end+4 : r.end+6]) + chunkType := r.buf[r.end+6] + + if checksum == 0 && length == 0 && chunkType == 0 { + if r.end+recyclableHeaderSize > r.n { + // Skip the rest of the block if the recyclable header size does not + // fit within it. + r.end = r.n + continue + } + if r.recovering { + // Skip the rest of the block, if it looks like it is all + // zeroes. This is common with WAL preallocation. + // + // Set r.err to be an error so r.recover actually recovers. + r.err = ErrZeroedChunk + r.recover() + continue + } + return ErrZeroedChunk + } + + headerSize := legacyHeaderSize + if chunkType >= recyclableFullChunkType && chunkType <= recyclableLastChunkType { + headerSize = recyclableHeaderSize + if r.end+headerSize > r.n { + return ErrInvalidChunk + } + + logNum := binary.LittleEndian.Uint32(r.buf[r.end+7 : r.end+11]) + if logNum != r.logNum { + if wantFirst { + // If we're looking for the first chunk of a record, we can treat a + // previous instance of the log as EOF. + return io.EOF + } + // Otherwise, treat this chunk as invalid in order to prevent reading + // of a partial record. + return ErrInvalidChunk + } + + chunkType -= (recyclableFullChunkType - 1) + } + + r.begin = r.end + headerSize + r.end = r.begin + int(length) + if r.end > r.n { + // The chunk straddles a 32KB boundary (or the end of file). + if r.recovering { + r.recover() + continue + } + return ErrInvalidChunk + } + if checksum != crc.New(r.buf[r.begin-headerSize+6:r.end]).Value() { + if r.recovering { + r.recover() + continue + } + return ErrInvalidChunk + } + if wantFirst { + if chunkType != fullChunkType && chunkType != firstChunkType { + continue + } + } + r.last = chunkType == fullChunkType || chunkType == lastChunkType + r.recovering = false + return nil + } + if r.n < blockSize && r.blockNum >= 0 { + if !wantFirst || r.end != r.n { + // This can happen if the previous instance of the log ended with a + // partial block at the same blockNum as the new log but extended + // beyond the partial block of the new log. + return ErrInvalidChunk + } + return io.EOF + } + n, err := io.ReadFull(r.r, r.buf[:]) + if err != nil && err != io.ErrUnexpectedEOF { + if err == io.EOF && !wantFirst { + return io.ErrUnexpectedEOF + } + return err + } + r.begin, r.end, r.n = 0, 0, n + r.blockNum++ + } +} + +// Next returns a reader for the next record. It returns io.EOF if there are no +// more records. The reader returned becomes stale after the next Next call, +// and should no longer be used. +func (r *Reader) Next() (io.Reader, error) { + r.seq++ + if r.err != nil { + return nil, r.err + } + r.begin = r.end + r.err = r.nextChunk(true) + if r.err != nil { + return nil, r.err + } + return singleReader{r, r.seq}, nil +} + +// Offset returns the current offset within the file. If called immediately +// before a call to Next(), Offset() will return the record offset. +func (r *Reader) Offset() int64 { + if r.blockNum < 0 { + return 0 + } + return int64(r.blockNum)*blockSize + int64(r.end) +} + +// recover clears any errors read so far, so that calling Next will start +// reading from the next good 32KiB block. If there are no such blocks, Next +// will return io.EOF. recover also marks the current reader, the one most +// recently returned by Next, as stale. If recover is called without any +// prior error, then recover is a no-op. +func (r *Reader) recover() { + if r.err == nil { + return + } + r.recovering = true + r.err = nil + // Discard the rest of the current block. + r.begin, r.end, r.last = r.n, r.n, false + // Invalidate any outstanding singleReader. + r.seq++ +} + +// seekRecord seeks in the underlying io.Reader such that calling r.Next +// returns the record whose first chunk header starts at the provided offset. +// Its behavior is undefined if the argument given is not such an offset, as +// the bytes at that offset may coincidentally appear to be a valid header. +// +// It returns ErrNotAnIOSeeker if the underlying io.Reader does not implement +// io.Seeker. +// +// seekRecord will fail and return an error if the Reader previously +// encountered an error, including io.EOF. Such errors can be cleared by +// calling Recover. Calling seekRecord after Recover will make calling Next +// return the record at the given offset, instead of the record at the next +// good 32KiB block as Recover normally would. Calling seekRecord before +// Recover has no effect on Recover's semantics other than changing the +// starting point for determining the next good 32KiB block. +// +// The offset is always relative to the start of the underlying io.Reader, so +// negative values will result in an error as per io.Seeker. +func (r *Reader) seekRecord(offset int64) error { + r.seq++ + if r.err != nil { + return r.err + } + + s, ok := r.r.(io.Seeker) + if !ok { + return ErrNotAnIOSeeker + } + + // Only seek to an exact block offset. + c := int(offset & blockSizeMask) + if _, r.err = s.Seek(offset&^blockSizeMask, io.SeekStart); r.err != nil { + return r.err + } + + // Clear the state of the internal reader. + r.begin, r.end, r.n = 0, 0, 0 + r.blockNum, r.recovering, r.last = -1, false, false + if r.err = r.nextChunk(false); r.err != nil { + return r.err + } + + // Now skip to the offset requested within the block. A subsequent + // call to Next will return the block at the requested offset. + r.begin, r.end = c, c + + return nil +} + +type singleReader struct { + r *Reader + seq int +} + +func (x singleReader) Read(p []byte) (int, error) { + r := x.r + if r.seq != x.seq { + return 0, errors.New("pebble/record: stale reader") + } + if r.err != nil { + return 0, r.err + } + for r.begin == r.end { + if r.last { + return 0, io.EOF + } + if r.err = r.nextChunk(false); r.err != nil { + return 0, r.err + } + } + n := copy(p, r.buf[r.begin:r.end]) + r.begin += n + return n, nil +} + +// Writer writes records to an underlying io.Writer. +type Writer struct { + // w is the underlying writer. + w io.Writer + // seq is the sequence number of the current record. + seq int + // f is w as a flusher. + f flusher + // buf[i:j] is the bytes that will become the current chunk. + // The low bound, i, includes the chunk header. + i, j int + // buf[:written] has already been written to w. + // written is zero unless Flush has been called. + written int + // baseOffset is the base offset in w at which writing started. If + // w implements io.Seeker, it's relative to the start of w, 0 otherwise. + baseOffset int64 + // blockNumber is the zero based block number currently held in buf. + blockNumber int64 + // lastRecordOffset is the offset in w where the last record was + // written (including the chunk header). It is a relative offset to + // baseOffset, thus the absolute offset of the last record is + // baseOffset + lastRecordOffset. + lastRecordOffset int64 + // first is whether the current chunk is the first chunk of the record. + first bool + // pending is whether a chunk is buffered but not yet written. + pending bool + // err is any accumulated error. + err error + // buf is the buffer. + buf [blockSize]byte +} + +// NewWriter returns a new Writer. +func NewWriter(w io.Writer) *Writer { + f, _ := w.(flusher) + + var o int64 + if s, ok := w.(io.Seeker); ok { + var err error + if o, err = s.Seek(0, io.SeekCurrent); err != nil { + o = 0 + } + } + return &Writer{ + w: w, + f: f, + baseOffset: o, + lastRecordOffset: -1, + } +} + +// fillHeader fills in the header for the pending chunk. +func (w *Writer) fillHeader(last bool) { + if w.i+legacyHeaderSize > w.j || w.j > blockSize { + panic("pebble/record: bad writer state") + } + if last { + if w.first { + w.buf[w.i+6] = fullChunkType + } else { + w.buf[w.i+6] = lastChunkType + } + } else { + if w.first { + w.buf[w.i+6] = firstChunkType + } else { + w.buf[w.i+6] = middleChunkType + } + } + binary.LittleEndian.PutUint32(w.buf[w.i+0:w.i+4], crc.New(w.buf[w.i+6:w.j]).Value()) + binary.LittleEndian.PutUint16(w.buf[w.i+4:w.i+6], uint16(w.j-w.i-legacyHeaderSize)) +} + +// writeBlock writes the buffered block to the underlying writer, and reserves +// space for the next chunk's header. +func (w *Writer) writeBlock() { + _, w.err = w.w.Write(w.buf[w.written:]) + w.i = 0 + w.j = legacyHeaderSize + w.written = 0 + w.blockNumber++ +} + +// writePending finishes the current record and writes the buffer to the +// underlying writer. +func (w *Writer) writePending() { + if w.err != nil { + return + } + if w.pending { + w.fillHeader(true) + w.pending = false + } + _, w.err = w.w.Write(w.buf[w.written:w.j]) + w.written = w.j +} + +// Close finishes the current record and closes the writer. +func (w *Writer) Close() error { + w.seq++ + w.writePending() + if w.err != nil { + return w.err + } + w.err = errors.New("pebble/record: closed Writer") + return nil +} + +// Flush finishes the current record, writes to the underlying writer, and +// flushes it if that writer implements interface{ Flush() error }. +func (w *Writer) Flush() error { + w.seq++ + w.writePending() + if w.err != nil { + return w.err + } + if w.f != nil { + w.err = w.f.Flush() + return w.err + } + return nil +} + +// Next returns a writer for the next record. The writer returned becomes stale +// after the next Close, Flush or Next call, and should no longer be used. +func (w *Writer) Next() (io.Writer, error) { + w.seq++ + if w.err != nil { + return nil, w.err + } + if w.pending { + w.fillHeader(true) + } + w.i = w.j + w.j = w.j + legacyHeaderSize + // Check if there is room in the block for the header. + if w.j > blockSize { + // Fill in the rest of the block with zeroes. + for k := w.i; k < blockSize; k++ { + w.buf[k] = 0 + } + w.writeBlock() + if w.err != nil { + return nil, w.err + } + } + w.lastRecordOffset = w.baseOffset + w.blockNumber*blockSize + int64(w.i) + w.first = true + w.pending = true + return singleWriter{w, w.seq}, nil +} + +// WriteRecord writes a complete record. Returns the offset just past the end +// of the record. +func (w *Writer) WriteRecord(p []byte) (int64, error) { + if w.err != nil { + return -1, w.err + } + t, err := w.Next() + if err != nil { + return -1, err + } + if _, err := t.Write(p); err != nil { + return -1, err + } + w.writePending() + offset := w.blockNumber*blockSize + int64(w.j) + return offset, w.err +} + +// Size returns the current size of the file. +func (w *Writer) Size() int64 { + if w == nil { + return 0 + } + return w.blockNumber*blockSize + int64(w.j) +} + +// LastRecordOffset returns the offset in the underlying io.Writer of the last +// record so far - the one created by the most recent Next call. It is the +// offset of the first chunk header, suitable to pass to Reader.SeekRecord. +// +// If that io.Writer also implements io.Seeker, the return value is an absolute +// offset, in the sense of io.SeekStart, regardless of whether the io.Writer +// was initially at the zero position when passed to NewWriter. Otherwise, the +// return value is a relative offset, being the number of bytes written between +// the NewWriter call and any records written prior to the last record. +// +// If there is no last record, i.e. nothing was written, LastRecordOffset will +// return ErrNoLastRecord. +func (w *Writer) LastRecordOffset() (int64, error) { + if w.err != nil { + return 0, w.err + } + if w.lastRecordOffset < 0 { + return 0, ErrNoLastRecord + } + return w.lastRecordOffset, nil +} + +type singleWriter struct { + w *Writer + seq int +} + +func (x singleWriter) Write(p []byte) (int, error) { + w := x.w + if w.seq != x.seq { + return 0, errors.New("pebble/record: stale writer") + } + if w.err != nil { + return 0, w.err + } + n0 := len(p) + for len(p) > 0 { + // Write a block, if it is full. + if w.j == blockSize { + w.fillHeader(false) + w.writeBlock() + if w.err != nil { + return 0, w.err + } + w.first = false + } + // Copy bytes into the buffer. + n := copy(w.buf[w.j:], p) + w.j += n + p = p[n:] + } + return n0, nil +} diff --git a/pebble/record/record_test.go b/pebble/record/record_test.go new file mode 100644 index 0000000..d052079 --- /dev/null +++ b/pebble/record/record_test.go @@ -0,0 +1,1064 @@ +// 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 record + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math" + "strings" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func short(s string) string { + if len(s) < 64 { + return s + } + return fmt.Sprintf("%s...(skipping %d bytes)...%s", s[:20], len(s)-40, s[len(s)-20:]) +} + +// big returns a string of length n, composed of repetitions of partial. +func big(partial string, n int) string { + return strings.Repeat(partial, n/len(partial)+1)[:n] +} + +type recordWriter interface { + WriteRecord([]byte) (int64, error) + Close() error +} + +func testGeneratorWriter( + t *testing.T, reset func(), gen func() (string, bool), newWriter func(io.Writer) recordWriter, +) { + buf := new(bytes.Buffer) + + reset() + w := newWriter(buf) + for { + s, ok := gen() + if !ok { + break + } + if _, err := w.WriteRecord([]byte(s)); err != nil { + t.Fatalf("Write: %v", err) + } + } + if err := w.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + reset() + r := NewReader(buf, 0 /* logNum */) + for { + s, ok := gen() + if !ok { + break + } + rr, err := r.Next() + if err != nil { + t.Fatalf("reader.Next: %v", err) + } + x, err := io.ReadAll(rr) + if err != nil { + t.Fatalf("ReadAll: %v", err) + } + if string(x) != s { + t.Fatalf("got %q, want %q", short(string(x)), short(s)) + } + } + if _, err := r.Next(); err != io.EOF { + t.Fatalf("got %v, want %v", err, io.EOF) + } +} + +func testGenerator(t *testing.T, reset func(), gen func() (string, bool)) { + t.Run("Writer", func(t *testing.T) { + testGeneratorWriter(t, reset, gen, func(w io.Writer) recordWriter { + return NewWriter(w) + }) + }) + + t.Run("LogWriter", func(t *testing.T) { + testGeneratorWriter(t, reset, gen, func(w io.Writer) recordWriter { + return NewLogWriter(w, 0 /* logNum */, LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + }) + }) +} + +func testLiterals(t *testing.T, s []string) { + var i int + reset := func() { + i = 0 + } + gen := func() (string, bool) { + if i == len(s) { + return "", false + } + i++ + return s[i-1], true + } + testGenerator(t, reset, gen) +} + +func TestMany(t *testing.T) { + const n = 1e5 + var i int + reset := func() { + i = 0 + } + gen := func() (string, bool) { + if i == n { + return "", false + } + i++ + return fmt.Sprintf("%d.", i-1), true + } + testGenerator(t, reset, gen) +} + +func TestRandom(t *testing.T) { + const n = 1e2 + var ( + i int + r *rand.Rand + ) + reset := func() { + i, r = 0, rand.New(rand.NewSource(0)) + } + gen := func() (string, bool) { + if i == n { + return "", false + } + i++ + return strings.Repeat(string(uint8(i)), r.Intn(2*blockSize+16)), true + } + testGenerator(t, reset, gen) +} + +func TestBasic(t *testing.T) { + testLiterals(t, []string{ + strings.Repeat("a", 1000), + strings.Repeat("b", 97270), + strings.Repeat("c", 8000), + }) +} + +func TestBoundary(t *testing.T) { + for i := blockSize - 16; i < blockSize+16; i++ { + s0 := big("abcd", i) + for j := blockSize - 16; j < blockSize+16; j++ { + s1 := big("ABCDE", j) + testLiterals(t, []string{s0, s1}) + testLiterals(t, []string{s0, "", s1}) + testLiterals(t, []string{s0, "x", s1}) + } + } +} + +func TestFlush(t *testing.T) { + buf := new(bytes.Buffer) + w := NewWriter(buf) + // Write a couple of records. Everything should still be held + // in the record.Writer buffer, so that buf.Len should be 0. + w0, _ := w.Next() + w0.Write([]byte("0")) + w1, _ := w.Next() + w1.Write([]byte("11")) + if got, want := buf.Len(), 0; got != want { + t.Fatalf("buffer length #0: got %d want %d", got, want) + } + // Flush the record.Writer buffer, which should yield 17 bytes. + // 17 = 2*7 + 1 + 2, which is two headers and 1 + 2 payload bytes. + require.NoError(t, w.Flush()) + if got, want := buf.Len(), 17; got != want { + t.Fatalf("buffer length #1: got %d want %d", got, want) + } + // Do another write, one that isn't large enough to complete the block. + // The write should not have flowed through to buf. + w2, _ := w.Next() + w2.Write(bytes.Repeat([]byte("2"), 10000)) + if got, want := buf.Len(), 17; got != want { + t.Fatalf("buffer length #2: got %d want %d", got, want) + } + // Flushing should get us up to 10024 bytes written. + // 10024 = 17 + 7 + 10000. + require.NoError(t, w.Flush()) + if got, want := buf.Len(), 10024; got != want { + t.Fatalf("buffer length #3: got %d want %d", got, want) + } + // Do a bigger write, one that completes the current block. + // We should now have 32768 bytes (a complete block), without + // an explicit flush. + w3, _ := w.Next() + w3.Write(bytes.Repeat([]byte("3"), 40000)) + if got, want := buf.Len(), 32768; got != want { + t.Fatalf("buffer length #4: got %d want %d", got, want) + } + // Flushing should get us up to 50038 bytes written. + // 50038 = 10024 + 2*7 + 40000. There are two headers because + // the one record was split into two chunks. + require.NoError(t, w.Flush()) + if got, want := buf.Len(), 50038; got != want { + t.Fatalf("buffer length #5: got %d want %d", got, want) + } + // Check that reading those records give the right lengths. + r := NewReader(buf, 0 /* logNum */) + wants := []int64{1, 2, 10000, 40000} + for i, want := range wants { + rr, _ := r.Next() + n, err := io.Copy(io.Discard, rr) + if err != nil { + t.Fatalf("read #%d: %v", i, err) + } + if n != want { + t.Fatalf("read #%d: got %d bytes want %d", i, n, want) + } + } +} + +func TestNonExhaustiveRead(t *testing.T) { + const n = 100 + buf := new(bytes.Buffer) + p := make([]byte, 10) + rnd := rand.New(rand.NewSource(1)) + + w := NewWriter(buf) + for i := 0; i < n; i++ { + length := len(p) + rnd.Intn(3*blockSize) + s := string(uint8(i)) + "123456789abcdefgh" + _, _ = w.WriteRecord([]byte(big(s, length))) + } + if err := w.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + r := NewReader(buf, 0 /* logNum */) + for i := 0; i < n; i++ { + rr, _ := r.Next() + _, err := io.ReadFull(rr, p) + if err != nil { + t.Fatalf("ReadFull: %v", err) + } + want := string(uint8(i)) + "123456789" + if got := string(p); got != want { + t.Fatalf("read #%d: got %q want %q", i, got, want) + } + } +} + +func TestStaleReader(t *testing.T) { + buf := new(bytes.Buffer) + + w := NewWriter(buf) + _, err := w.WriteRecord([]byte("0")) + require.NoError(t, err) + + _, err = w.WriteRecord([]byte("11")) + require.NoError(t, err) + + require.NoError(t, w.Close()) + + r := NewReader(buf, 0 /* logNum */) + r0, err := r.Next() + require.NoError(t, err) + + r1, err := r.Next() + require.NoError(t, err) + + p := make([]byte, 1) + if _, err := r0.Read(p); err == nil || !strings.Contains(err.Error(), "stale") { + t.Fatalf("stale read #0: unexpected error: %v", err) + } + if _, err := r1.Read(p); err != nil { + t.Fatalf("fresh read #1: got %v want nil error", err) + } + if p[0] != '1' { + t.Fatalf("fresh read #1: byte contents: got '%c' want '1'", p[0]) + } +} + +type testRecords struct { + records [][]byte // The raw value of each record. + offsets []int64 // The offset of each record within buf, derived from writer.LastRecordOffset. + buf []byte // The serialized records form of all records. +} + +// makeTestRecords generates test records of specified lengths. +// The first record will consist of repeating 0x00 bytes, the next record of +// 0x01 bytes, and so forth. The values will loop back to 0x00 after 0xff. +func makeTestRecords(recordLengths ...int) (*testRecords, error) { + ret := &testRecords{} + ret.records = make([][]byte, len(recordLengths)) + ret.offsets = make([]int64, len(recordLengths)) + for i, n := range recordLengths { + ret.records[i] = bytes.Repeat([]byte{byte(i)}, n) + } + + buf := new(bytes.Buffer) + w := NewWriter(buf) + for i, rec := range ret.records { + wRec, err := w.Next() + if err != nil { + return nil, err + } + + // Alternate between one big write and many small writes. + cSize := 8 + if i&1 == 0 { + cSize = len(rec) + } + for ; len(rec) > cSize; rec = rec[cSize:] { + if _, err := wRec.Write(rec[:cSize]); err != nil { + return nil, err + } + } + if _, err := wRec.Write(rec); err != nil { + return nil, err + } + + ret.offsets[i], err = w.LastRecordOffset() + if err != nil { + return nil, err + } + } + + if err := w.Close(); err != nil { + return nil, err + } + + ret.buf = buf.Bytes() + return ret, nil +} + +// corruptBlock corrupts the checksum of the record that starts at the +// specified block offset. The number of the block offset is 0 based. +func corruptBlock(buf []byte, blockNum int) { + // Ensure we always permute at least 1 byte of the checksum. + if buf[blockSize*blockNum] == 0x00 { + buf[blockSize*blockNum] = 0xff + } else { + buf[blockSize*blockNum] = 0x00 + } + + buf[blockSize*blockNum+1] = 0x00 + buf[blockSize*blockNum+2] = 0x00 + buf[blockSize*blockNum+3] = 0x00 +} + +func TestRecoverNoOp(t *testing.T) { + recs, err := makeTestRecords( + blockSize-legacyHeaderSize, + blockSize-legacyHeaderSize, + blockSize-legacyHeaderSize, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + r := NewReader(bytes.NewReader(recs.buf), 0 /* logNum */) + _, err = r.Next() + if err != nil || r.err != nil { + t.Fatalf("reader.Next: %v reader.err: %v", err, r.err) + } + + seq, begin, end, n := r.seq, r.begin, r.end, r.n + + // Should be a no-op since r.err == nil. + r.recover() + + // r.err was nil, nothing should have changed. + if seq != r.seq || begin != r.begin || end != r.end || n != r.n { + t.Fatal("reader.Recover when no error existed, was not a no-op") + } +} + +func TestBasicRecover(t *testing.T) { + recs, err := makeTestRecords( + blockSize-legacyHeaderSize, + blockSize-legacyHeaderSize, + blockSize-legacyHeaderSize, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + // Corrupt the checksum of the second record r1 in our file. + corruptBlock(recs.buf, 1) + + underlyingReader := bytes.NewReader(recs.buf) + r := NewReader(underlyingReader, 0 /* logNum */) + + // The first record r0 should be read just fine. + r0, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + r0Data, err := io.ReadAll(r0) + if err != nil { + t.Fatalf("ReadAll: %v", err) + } + if !bytes.Equal(r0Data, recs.records[0]) { + t.Fatal("Unexpected output in r0's data") + } + + // The next record should have a checksum mismatch. + _, err = r.Next() + if err == nil { + t.Fatal("Expected an error while reading a corrupted record") + } + if err != ErrInvalidChunk { + t.Fatalf("Unexpected error returned: %v", err) + } + + // Recover from that checksum mismatch. + r.recover() + currentOffset, err := underlyingReader.Seek(0, io.SeekCurrent) + if err != nil { + t.Fatalf("current offset: %v", err) + } + if currentOffset != blockSize*2 { + t.Fatalf("current offset: got %d, want %d", currentOffset, blockSize*2) + } + + // The third record r2 should be read just fine. + r2, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + r2Data, err := io.ReadAll(r2) + if err != nil { + t.Fatalf("ReadAll: %v", err) + } + if !bytes.Equal(r2Data, recs.records[2]) { + t.Fatal("Unexpected output in r2's data") + } +} + +func TestRecoverSingleBlock(t *testing.T) { + // The first record will be blockSize * 3 bytes long. Since each block has + // a 7 byte header, the first record will roll over into 4 blocks. + recs, err := makeTestRecords( + blockSize*3, + blockSize-legacyHeaderSize, + blockSize/2, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + // Corrupt the checksum for the portion of the first record that exists in + // the 4th block. + corruptBlock(recs.buf, 3) + + // The first record should fail, but only when we read deeper beyond the + // first block. + r := NewReader(bytes.NewReader(recs.buf), 0 /* logNum */) + r0, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + + // Reading deeper should yield a checksum mismatch. + _, err = io.ReadAll(r0) + if err == nil { + t.Fatal("Expected a checksum mismatch error, got nil") + } + if err != ErrInvalidChunk { + t.Fatalf("Unexpected error returned: %v", err) + } + + // Recover from that checksum mismatch. + r.recover() + + // All of the data in the second record r1 is lost because the first record + // r0 shared a partial block with it. The second record also overlapped + // into the block with the third record r2. Recovery should jump to that + // block, skipping over the end of the second record and start parsing the + // third record. + r2, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + r2Data, _ := io.ReadAll(r2) + if !bytes.Equal(r2Data, recs.records[2]) { + t.Fatal("Unexpected output in r2's data") + } +} + +func TestRecoverMultipleBlocks(t *testing.T) { + recs, err := makeTestRecords( + // The first record will consume 3 entire blocks but a fraction of the 4th. + blockSize*3, + // The second record will completely fill the remainder of the 4th block. + 3*(blockSize-legacyHeaderSize)-2*blockSize-2*legacyHeaderSize, + // Consume the entirety of the 5th block. + blockSize-legacyHeaderSize, + // Consume the entirety of the 6th block. + blockSize-legacyHeaderSize, + // Consume roughly half of the 7th block. + blockSize/2, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + // Corrupt the checksum for the portion of the first record that exists in the 4th block. + corruptBlock(recs.buf, 3) + + // Now corrupt the two blocks in a row that correspond to recs.records[2:4]. + corruptBlock(recs.buf, 4) + corruptBlock(recs.buf, 5) + + // The first record should fail, but only when we read deeper beyond the first block. + r := NewReader(bytes.NewReader(recs.buf), 0 /* logNum */) + r0, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + + // Reading deeper should yield a checksum mismatch. + _, err = io.ReadAll(r0) + if err == nil { + t.Fatal("Exptected a checksum mismatch error, got nil") + } + if err != ErrInvalidChunk { + t.Fatalf("Unexpected error returned: %v", err) + } + + // Recover from that checksum mismatch. + r.recover() + + // All of the data in the second record is lost because the first + // record shared a partial block with it. The following two records + // have corrupted checksums as well, so the call above to r.Recover + // should result in r.Next() being a reader to the 5th record. + r4, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + + r4Data, _ := io.ReadAll(r4) + if !bytes.Equal(r4Data, recs.records[4]) { + t.Fatal("Unexpected output in r4's data") + } +} + +// verifyLastBlockRecover reads each record from recs expecting that the +// last record will be corrupted. It will then try Recover and verify that EOF +// is returned. +func verifyLastBlockRecover(recs *testRecords) error { + r := NewReader(bytes.NewReader(recs.buf), 0 /* logNum */) + // Loop to one element larger than the number of records to verify EOF. + for i := 0; i < len(recs.records)+1; i++ { + _, err := r.Next() + switch i { + case len(recs.records) - 1: + if err == nil { + return errors.New("Expected a checksum mismatch error, got nil") + } + r.recover() + case len(recs.records): + if err != io.EOF { + return errors.Errorf("Expected io.EOF, got %v", err) + } + default: + if err != nil { + return errors.Errorf("Next: %v", err) + } + } + } + return nil +} + +func TestRecoverLastPartialBlock(t *testing.T) { + recs, err := makeTestRecords( + // The first record will consume 3 entire blocks but a fraction of the 4th. + blockSize*3, + // The second record will completely fill the remainder of the 4th block. + 3*(blockSize-legacyHeaderSize)-2*blockSize-2*legacyHeaderSize, + // Consume roughly half of the 5th block. + blockSize/2, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + // Corrupt the 5th block. + corruptBlock(recs.buf, 4) + + // Verify Recover works when the last block is corrupted. + if err := verifyLastBlockRecover(recs); err != nil { + t.Fatalf("verifyLastBlockRecover: %v", err) + } +} + +func TestRecoverLastCompleteBlock(t *testing.T) { + recs, err := makeTestRecords( + // The first record will consume 3 entire blocks but a fraction of the 4th. + blockSize*3, + // The second record will completely fill the remainder of the 4th block. + 3*(blockSize-legacyHeaderSize)-2*blockSize-2*legacyHeaderSize, + // Consume the entire 5th block. + blockSize-legacyHeaderSize, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + // Corrupt the 5th block. + corruptBlock(recs.buf, 4) + + // Verify Recover works when the last block is corrupted. + if err := verifyLastBlockRecover(recs); err != nil { + t.Fatalf("verifyLastBlockRecover: %v", err) + } +} + +func TestReaderOffset(t *testing.T) { + recs, err := makeTestRecords( + blockSize*2, + 400, + 500, + 600, + 700, + 800, + 9000, + 1000, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + // The first record should fail, but only when we read deeper beyond the first block. + r := NewReader(bytes.NewReader(recs.buf), 0 /* logNum */) + for i, offset := range recs.offsets { + if offset != r.Offset() { + t.Fatalf("%d: expected offset %d, but found %d", i, offset, r.Offset()) + } + rec, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + if _, err = io.ReadAll(rec); err != nil { + t.Fatalf("ReadAll: %v", err) + } + } +} + +func TestSeekRecord(t *testing.T) { + recs, err := makeTestRecords( + // The first record will consume 3 entire blocks but a fraction of the 4th. + blockSize*3, + // The second record will completely fill the remainder of the 4th block. + 3*(blockSize-legacyHeaderSize)-2*blockSize-2*legacyHeaderSize, + // Consume the entirety of the 5th block. + blockSize-legacyHeaderSize, + // Consume the entirety of the 6th block. + blockSize-legacyHeaderSize, + // Consume roughly half of the 7th block. + blockSize/2, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + r := NewReader(bytes.NewReader(recs.buf), 0 /* logNum */) + // Seek to a valid block offset, but within a multiblock record. This should cause the next call to + // Next after SeekRecord to return the next valid FIRST/FULL chunk of the subsequent record. + err = r.seekRecord(blockSize) + if err != nil { + t.Fatalf("SeekRecord: %v", err) + } + rec, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + rData, _ := io.ReadAll(rec) + if !bytes.Equal(rData, recs.records[1]) { + t.Fatalf("Unexpected output in record 1's data, got %v want %v", rData, recs.records[1]) + } + + // Seek 3 bytes into the second block, which is still in the middle of the first record, but not + // at a valid chunk boundary. Should result in an error upon calling r.Next. + err = r.seekRecord(blockSize + 3) + if err != nil { + t.Fatalf("SeekRecord: %v", err) + } + if _, err = r.Next(); err == nil { + t.Fatalf("Expected an error seeking to an invalid chunk boundary") + } + r.recover() + + // Seek to the fifth block and verify all records can be read as appropriate. + err = r.seekRecord(blockSize * 4) + if err != nil { + t.Fatalf("SeekRecord: %v", err) + } + + check := func(i int) { + for ; i < len(recs.records); i++ { + rec, err := r.Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + + rData, _ := io.ReadAll(rec) + if !bytes.Equal(rData, recs.records[i]) { + t.Fatalf("Unexpected output in record #%d's data, got %v want %v", i, rData, recs.records[i]) + } + } + } + check(2) + + // Seek back to the fourth block, and read all subsequent records and verify them. + err = r.seekRecord(blockSize * 3) + if err != nil { + t.Fatalf("SeekRecord: %v", err) + } + check(1) + + // Now seek past the end of the file and verify it causes an error. + err = r.seekRecord(1 << 20) + if err == nil { + t.Fatalf("Seek past the end of a file didn't cause an error") + } + if err != io.ErrUnexpectedEOF { + t.Fatalf("Seeking past EOF raised unexpected error: %v", err) + } + r.recover() // Verify recovery works. + + // Validate the current records are returned after seeking to a valid offset. + err = r.seekRecord(blockSize * 4) + if err != nil { + t.Fatalf("SeekRecord: %v", err) + } + check(2) +} + +func TestLastRecordOffset(t *testing.T) { + recs, err := makeTestRecords( + // The first record will consume 3 entire blocks but a fraction of the 4th. + blockSize*3, + // The second record will completely fill the remainder of the 4th block. + 3*(blockSize-legacyHeaderSize)-2*blockSize-2*legacyHeaderSize, + // Consume the entirety of the 5th block. + blockSize-legacyHeaderSize, + // Consume the entirety of the 6th block. + blockSize-legacyHeaderSize, + // Consume roughly half of the 7th block. + blockSize/2, + ) + if err != nil { + t.Fatalf("makeTestRecords: %v", err) + } + + wants := []int64{0, 98332, 131072, 163840, 196608} + for i, got := range recs.offsets { + if want := wants[i]; got != want { + t.Errorf("record #%d: got %d, want %d", i, got, want) + } + } +} + +func TestNoLastRecordOffset(t *testing.T) { + buf := new(bytes.Buffer) + w := NewWriter(buf) + defer w.Close() + + if _, err := w.LastRecordOffset(); err != ErrNoLastRecord { + t.Fatalf("Expected ErrNoLastRecord, got: %v", err) + } + + require.NoError(t, w.Flush()) + + if _, err := w.LastRecordOffset(); err != ErrNoLastRecord { + t.Fatalf("LastRecordOffset: got: %v, want ErrNoLastRecord", err) + } + + _, err := w.WriteRecord([]byte("testrecord")) + require.NoError(t, err) + + if off, err := w.LastRecordOffset(); err != nil { + t.Fatalf("LastRecordOffset: %v", err) + } else if off != 0 { + t.Fatalf("LastRecordOffset: got %d, want 0", off) + } +} + +func TestInvalidLogNum(t *testing.T) { + var buf bytes.Buffer + w := NewLogWriter(&buf, 1, LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + for i := 0; i < 10; i++ { + s := fmt.Sprintf("%04d\n", i) + _, err := w.WriteRecord([]byte(s)) + require.NoError(t, err) + } + require.NoError(t, w.Close()) + + { + r := NewReader(bytes.NewReader(buf.Bytes()), 1) + for i := 0; i < 10; i++ { + rr, err := r.Next() + require.NoError(t, err) + + x, err := io.ReadAll(rr) + require.NoError(t, err) + + s := fmt.Sprintf("%04d\n", i) + if s != string(x) { + t.Fatalf("expected %s, but found %s", s, x) + } + } + if _, err := r.Next(); err != io.EOF { + t.Fatalf("expected EOF, but found %s", err) + } + } + + { + r := NewReader(bytes.NewReader(buf.Bytes()), 2) + if _, err := r.Next(); err != io.EOF { + t.Fatalf("expected %s, but found %s\n", io.EOF, err) + } + } +} + +func TestSize(t *testing.T) { + var buf bytes.Buffer + zeroes := make([]byte, 8<<10) + w := NewWriter(&buf) + for i := 0; i < 100; i++ { + n := rand.Intn(len(zeroes)) + _, err := w.WriteRecord(zeroes[:n]) + require.NoError(t, err) + require.NoError(t, w.Flush()) + if buf.Len() != int(w.Size()) { + t.Fatalf("expected %d, but found %d", buf.Len(), w.Size()) + } + } + require.NoError(t, w.Close()) +} + +type limitedWriter struct { + io.Writer + limit int +} + +func (w *limitedWriter) Write(p []byte) (n int, err error) { + w.limit-- + if w.limit < 0 { + return len(p), nil + } + return w.Writer.Write(p) +} + +func TestRecycleLog(t *testing.T) { + const min = 16 + const max = 4096 + + rnd := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + randBlock := func() []byte { + data := make([]byte, rand.Intn(max-min)+min) + tmp := data + for len(tmp) >= 8 { + binary.LittleEndian.PutUint64(tmp, rand.Uint64()) + tmp = tmp[8:] + } + r := rand.Uint64() + for i := 0; i < len(tmp); i++ { + tmp[i] = byte(r) + r >>= 8 + } + return data + } + + // Recycle a log file 100 times, writing a random number of records filled + // with random data. + backing := make([]byte, 1<<20) + for i := 1; i <= 100; i++ { + blocks := rnd.Intn(100) + limitedBuf := &limitedWriter{ + Writer: bytes.NewBuffer(backing[:0]), + limit: blocks, + } + + w := NewLogWriter(limitedBuf, base.DiskFileNum(i), LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + sizes := make([]int, 10+rnd.Intn(100)) + for j := range sizes { + data := randBlock() + if _, err := w.WriteRecord(data); err != nil { + t.Fatalf("%d/%d: %v", i, j, err) + } + sizes[j] = len(data) + } + if err := w.Close(); err != nil { + t.Fatalf("%d: %v", i, err) + } + + r := NewReader(bytes.NewReader(backing), base.DiskFileNum(i)) + for j := range sizes { + rr, err := r.Next() + if err != nil { + // If we limited output then an EOF, zeroed, or invalid chunk is expected. + if limitedBuf.limit < 0 && (err == io.EOF || err == ErrZeroedChunk || err == ErrInvalidChunk) { + break + } + t.Fatalf("%d/%d: %v", i, j, err) + } + x, err := io.ReadAll(rr) + if err != nil { + // If we limited output then an EOF, zeroed, or invalid chunk is expected. + if limitedBuf.limit < 0 && (err == io.EOF || err == ErrZeroedChunk || err == ErrInvalidChunk) { + break + } + t.Fatalf("%d/%d: %v", i, j, err) + } + if sizes[j] != len(x) { + t.Fatalf("%d/%d: expected record %d, but found %d", i, j, sizes[j], len(x)) + } + } + if _, err := r.Next(); err != io.EOF && err != ErrZeroedChunk && err != ErrInvalidChunk { + t.Fatalf("%d: expected EOF, but found %v", i, err) + } + } +} + +func TestTruncatedLog(t *testing.T) { + backing := make([]byte, 2*blockSize) + w := NewLogWriter(bytes.NewBuffer(backing[:0]), base.DiskFileNum(1), LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + // Write a record that spans 2 blocks. + _, err := w.WriteRecord(bytes.Repeat([]byte("s"), blockSize+100)) + require.NoError(t, err) + require.NoError(t, w.Close()) + // Create a reader only for the first block. + r := NewReader(bytes.NewReader(backing[:blockSize]), base.DiskFileNum(1)) + rr, err := r.Next() + require.NoError(t, err) + _, err = io.ReadAll(rr) + require.EqualValues(t, err, io.ErrUnexpectedEOF) +} + +func TestRecycleLogWithPartialBlock(t *testing.T) { + backing := make([]byte, 27) + w := NewLogWriter(bytes.NewBuffer(backing[:0]), base.DiskFileNum(1), LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + // Will write a chunk with 11 byte header + 5 byte payload. + _, err := w.WriteRecord([]byte("aaaaa")) + require.NoError(t, err) + // Close will write a 11-byte EOF chunk. + require.NoError(t, w.Close()) + + w = NewLogWriter(bytes.NewBuffer(backing[:0]), base.DiskFileNum(2), LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + // Will write a chunk with 11 byte header + 1 byte payload. + _, err = w.WriteRecord([]byte("a")) + require.NoError(t, err) + // Close will write a 11-byte EOF chunk. + require.NoError(t, w.Close()) + + r := NewReader(bytes.NewReader(backing), base.DiskFileNum(2)) + _, err = r.Next() + require.NoError(t, err) + // 4 bytes left, which are not enough for even the legacy header. + if _, err = r.Next(); err != io.EOF { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestRecycleLogNumberOverflow(t *testing.T) { + // We truncate log numbers to 32-bits when writing to the WAL. Test log + // recycling at the wraparound point, ensuring that EOF chunks are + // interpreted correctly. + + backing := make([]byte, 27) + w := NewLogWriter(bytes.NewBuffer(backing[:0]), base.DiskFileNum(math.MaxUint32), LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + // Will write a chunk with 11 byte header + 5 byte payload. + _, err := w.WriteRecord([]byte("aaaaa")) + require.NoError(t, err) + // Close will write a 11-byte EOF chunk. + require.NoError(t, w.Close()) + + w = NewLogWriter(bytes.NewBuffer(backing[:0]), base.DiskFileNum(math.MaxUint32+1), LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + // Will write a chunk with 11 byte header + 1 byte payload. + _, err = w.WriteRecord([]byte("a")) + require.NoError(t, err) + // Close will write a 11-byte EOF chunk. + require.NoError(t, w.Close()) + + r := NewReader(bytes.NewReader(backing), base.DiskFileNum(math.MaxUint32+1)) + _, err = r.Next() + require.NoError(t, err) + // 4 bytes left, which are not enough for even the legacy header. + if _, err = r.Next(); err != io.EOF { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestRecycleLogWithPartialRecord(t *testing.T) { + const recordSize = (blockSize * 3) / 2 + + // Write a record that is larger than the log block size. + backing1 := make([]byte, 2*blockSize) + w := NewLogWriter(bytes.NewBuffer(backing1[:0]), base.DiskFileNum(1), LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + _, err := w.WriteRecord(bytes.Repeat([]byte("a"), recordSize)) + require.NoError(t, err) + require.NoError(t, w.Close()) + + // Write another record to a new incarnation of the WAL that is larger than + // the block size. + backing2 := make([]byte, 2*blockSize) + w = NewLogWriter(bytes.NewBuffer(backing2[:0]), base.DiskFileNum(2), LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + _, err = w.WriteRecord(bytes.Repeat([]byte("b"), recordSize)) + require.NoError(t, err) + require.NoError(t, w.Close()) + + // Copy the second block from the first WAL to the second block of the second + // WAL. This produces a scenario where it appears we crashed after writing + // the first block of the second WAL, but before writing the second block. + copy(backing2[blockSize:], backing1[blockSize:]) + + // Verify that we can't read a partial record from the second WAL. + r := NewReader(bytes.NewReader(backing2), base.DiskFileNum(2)) + rr, err := r.Next() + require.NoError(t, err) + + _, err = io.ReadAll(rr) + require.Equal(t, err, ErrInvalidChunk) +} + +func BenchmarkRecordWrite(b *testing.B) { + for _, size := range []int{8, 16, 32, 64, 256, 1028, 4096, 65_536} { + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + w := NewLogWriter(io.Discard, 0 /* logNum */, LogWriterConfig{ + WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{})}) + defer w.Close() + buf := make([]byte, size) + + b.SetBytes(int64(len(buf))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := w.WriteRecord(buf); err != nil { + b.Fatal(err) + } + } + b.StopTimer() + }) + } +} diff --git a/pebble/record/rotation.go b/pebble/record/rotation.go new file mode 100644 index 0000000..a2dd322 --- /dev/null +++ b/pebble/record/rotation.go @@ -0,0 +1,82 @@ +// 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 record + +// RotationHelper is a type used to inform the decision of rotating a record log +// file. +// +// The assumption is that multiple records can be coalesced into a single record +// (called a snapshot). Starting a new file, where the first record is a +// snapshot of the current state is referred to as "rotating" the log. +// +// Normally we rotate files when a certain file size is reached. But in certain +// cases (e.g. contents become very large), this can result in too frequent +// rotation. This helper contains logic to impose extra conditions on the +// rotation. +// +// The rotation helper uses "size" as a unit-less estimation that is correlated +// with the on-disk size of a record or snapshot. +type RotationHelper struct { + // lastSnapshotSize is the size of the last snapshot. + lastSnapshotSize int64 + // sizeSinceLastSnapshot is the sum of sizes of records applied since the last + // snapshot. + sizeSinceLastSnapshot int64 + lastRecordSize int64 +} + +// AddRecord makes the rotation helper aware of a new record. +func (rh *RotationHelper) AddRecord(recordSize int64) { + rh.sizeSinceLastSnapshot += recordSize + rh.lastRecordSize = recordSize +} + +// ShouldRotate returns whether we should start a new log file (with a snapshot). +// Does not need to be called if other rotation factors (log file size) are not +// satisfied. +func (rh *RotationHelper) ShouldRotate(nextSnapshotSize int64) bool { + // The primary goal is to ensure that when reopening a log file, the number of + // edits that need to be replayed on top of the snapshot is "sane" while + // keeping the rotation frequency as low as possible. + // + // For the purposes of this description, we assume that the log is mainly + // storing a collection of "entries", with edits adding or removing entries. + // Consider the following cases: + // + // - The number of live entries is roughly stable: after writing the snapshot + // (with S entries), we require that there be enough edits such that the + // cumulative number of entries in those edits, E, be greater than S. This + // will ensure that at most 50% of data written out is due to rotation. + // + // - The number of live entries K in the DB is shrinking drastically, say from + // S to S/10: After this shrinking, E = 0.9S, and so if we used the previous + // snapshot entry count, S, as the threshold that needs to be exceeded, we + // will further delay the snapshot writing. Which means on reopen we will + // need to replay 0.9S edits to get to a version with 0.1S entries. It would + // be better to create a new snapshot when E exceeds the number of entries in + // the current version. + // + // - The number of live entries L in the DB is growing; say the last snapshot + // had S entries, and now we have 10S entries, so E = 9S. If we required + // that E is at least the current number of entries, we would further delay + // writing a new snapshot (which is not desirable). + // + // The logic below uses the min of the last snapshot size count and the size + // count in the current version. + return rh.sizeSinceLastSnapshot > rh.lastSnapshotSize || rh.sizeSinceLastSnapshot > nextSnapshotSize +} + +// Rotate makes the rotation helper aware that we are rotating to a new snapshot +// (to which we will apply the latest edit). +func (rh *RotationHelper) Rotate(snapshotSize int64) { + rh.lastSnapshotSize = snapshotSize + rh.sizeSinceLastSnapshot = rh.lastRecordSize +} + +// DebugInfo returns the last snapshot size and size of the edits since the last +// snapshot; used for testing and debugging. +func (rh *RotationHelper) DebugInfo() (lastSnapshotSize int64, sizeSinceLastSnapshot int64) { + return rh.lastSnapshotSize, rh.sizeSinceLastSnapshot +} diff --git a/pebble/record/rotation_test.go b/pebble/record/rotation_test.go new file mode 100644 index 0000000..22660f1 --- /dev/null +++ b/pebble/record/rotation_test.go @@ -0,0 +1,49 @@ +// 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 record + +import ( + "fmt" + "strconv" + "testing" + + "github.com/cockroachdb/datadriven" +) + +func TestRotation(t *testing.T) { + var rh RotationHelper + datadriven.RunTest(t, "testdata/rotation", func(t *testing.T, td *datadriven.TestData) string { + oneIntArg := func() int64 { + if len(td.CmdArgs) != 1 { + td.Fatalf(t, "expected one integer argument") + } + n, err := strconv.Atoi(td.CmdArgs[0].String()) + if err != nil { + td.Fatalf(t, "expected one integer argument") + } + return int64(n) + } + switch td.Cmd { + case "add": + size := oneIntArg() + rh.AddRecord(size) + + case "should-rotate": + nextSnapshotSize := oneIntArg() + return fmt.Sprint(rh.ShouldRotate(nextSnapshotSize)) + + case "rotate": + snapshotSize := oneIntArg() + rh.Rotate(snapshotSize) + + default: + td.Fatalf(t, "unknown command %s", td.Cmd) + } + + // For commands with no output, show the debug info. + a, b := rh.DebugInfo() + return fmt.Sprintf("last-snapshot-size: %d\nsize-since-last-snapshot: %d", a, b) + }) +} diff --git a/pebble/record/testdata/rotation b/pebble/record/testdata/rotation new file mode 100644 index 0000000..ca8d341 --- /dev/null +++ b/pebble/record/testdata/rotation @@ -0,0 +1,65 @@ +rotate 100 +---- +last-snapshot-size: 100 +size-since-last-snapshot: 0 + +add 10 +---- +last-snapshot-size: 100 +size-since-last-snapshot: 10 + +# We should only rotate if the next snapshot is much smaller. +should-rotate 100 +---- +false + +should-rotate 5 +---- +true + +add 50 +---- +last-snapshot-size: 100 +size-since-last-snapshot: 60 + +add 50 +---- +last-snapshot-size: 100 +size-since-last-snapshot: 110 + +add 50 +---- +last-snapshot-size: 100 +size-since-last-snapshot: 160 + +# We exceeded the last snapshot size, we should rotate regardless. +should-rotate 1 +---- +true + +should-rotate 1000 +---- +true + +add 1 +---- +last-snapshot-size: 100 +size-since-last-snapshot: 161 + +rotate 10 +---- +last-snapshot-size: 10 +size-since-last-snapshot: 1 + +add 5 +---- +last-snapshot-size: 10 +size-since-last-snapshot: 6 + +should-rotate 5 +---- +true + +should-rotate 100 +---- +false diff --git a/pebble/replay/replay.go b/pebble/replay/replay.go new file mode 100644 index 0000000..d1d894f --- /dev/null +++ b/pebble/replay/replay.go @@ -0,0 +1,1145 @@ +// 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 replay implements collection and replaying of compaction benchmarking +// workloads. A workload is a collection of flushed and ingested sstables, along +// with the corresponding manifests describing the order and grouping with which +// they were applied. Replaying a workload flushes and ingests the same keys and +// sstables to reproduce the write workload for the purpose of evaluating +// compaction heuristics. +package replay + +import ( + "context" + "encoding/binary" + "fmt" + "io" + "os" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/bytealloc" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/internal/rangedel" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "golang.org/x/perf/benchfmt" + "golang.org/x/sync/errgroup" +) + +// A Pacer paces replay of a workload, determining when to apply the next +// incoming write. +type Pacer interface { + pace(r *Runner, step workloadStep) time.Duration +} + +// computeReadAmp calculates the read amplification from a manifest.Version +func computeReadAmp(v *manifest.Version) int { + refRAmp := v.L0Sublevels.ReadAmplification() + for _, lvl := range v.Levels[1:] { + if !lvl.Empty() { + refRAmp++ + } + } + return refRAmp +} + +// waitForReadAmpLE is a common function used by PaceByReferenceReadAmp and +// PaceByFixedReadAmp to wait on the dbMetricsNotifier condition variable if the +// read amplification observed is greater than the specified target (refRAmp). +func waitForReadAmpLE(r *Runner, rAmp int) { + r.dbMetricsCond.L.Lock() + m := r.dbMetrics + ra := m.ReadAmp() + for ra > rAmp { + r.dbMetricsCond.Wait() + ra = r.dbMetrics.ReadAmp() + } + r.dbMetricsCond.L.Unlock() +} + +// Unpaced implements Pacer by applying each new write as soon as possible. It +// may be useful for examining performance under high read amplification. +type Unpaced struct{} + +func (Unpaced) pace(*Runner, workloadStep) (d time.Duration) { return } + +// PaceByReferenceReadAmp implements Pacer by applying each new write following +// the collected workloads read amplification. +type PaceByReferenceReadAmp struct{} + +func (PaceByReferenceReadAmp) pace(r *Runner, w workloadStep) time.Duration { + startTime := time.Now() + refRAmp := computeReadAmp(w.pv) + waitForReadAmpLE(r, refRAmp) + return time.Since(startTime) +} + +// PaceByFixedReadAmp implements Pacer by applying each new write following a +// fixed read amplification. +type PaceByFixedReadAmp int + +func (pra PaceByFixedReadAmp) pace(r *Runner, _ workloadStep) time.Duration { + startTime := time.Now() + waitForReadAmpLE(r, int(pra)) + return time.Since(startTime) +} + +// Metrics holds the various statistics on a replay run and its performance. +type Metrics struct { + CompactionCounts struct { + Total int64 + Default int64 + DeleteOnly int64 + ElisionOnly int64 + Move int64 + Read int64 + Rewrite int64 + MultiLevel int64 + } + EstimatedDebt SampledMetric + Final *pebble.Metrics + Ingest struct { + BytesIntoL0 uint64 + // BytesWeightedByLevel is calculated as the number of bytes ingested + // into a level multiplied by the level's distance from the bottommost + // level (L6), summed across all levels. It can be used to guage how + // effective heuristics are at ingesting files into lower levels, saving + // write amplification. + BytesWeightedByLevel uint64 + } + // PaceDuration is the time waiting for the pacer to allow the workload to + // continue. + PaceDuration time.Duration + ReadAmp SampledMetric + // QuiesceDuration is the time between completing application of the workload and + // compactions quiescing. + QuiesceDuration time.Duration + TombstoneCount SampledMetric + // TotalSize holds the total size of the database, sampled after each + // workload step. + TotalSize SampledMetric + TotalWriteAmp float64 + WorkloadDuration time.Duration + WriteBytes uint64 + WriteStalls map[string]int + WriteStallsDuration map[string]time.Duration + WriteThroughput SampledMetric +} + +// Plot holds an ascii plot and its name. +type Plot struct { + Name string + Plot string +} + +// Plots returns a slice of ascii plots describing metrics change over time. +func (m *Metrics) Plots(width, height int) []Plot { + const scaleMB = 1.0 / float64(1<<20) + return []Plot{ + {Name: "Write throughput (MB/s)", Plot: m.WriteThroughput.PlotIncreasingPerSec(width, height, scaleMB)}, + {Name: "Estimated compaction debt (MB)", Plot: m.EstimatedDebt.Plot(width, height, scaleMB)}, + {Name: "Total database size (MB)", Plot: m.TotalSize.Plot(width, height, scaleMB)}, + {Name: "ReadAmp", Plot: m.ReadAmp.Plot(width, height, 1.0)}, + } +} + +// WriteBenchmarkString writes the metrics in the form of a series of +// 'Benchmark' lines understandable by benchstat. +func (m *Metrics) WriteBenchmarkString(name string, w io.Writer) error { + type benchmarkSection struct { + label string + values []benchfmt.Value + } + groups := []benchmarkSection{ + {label: "CompactionCounts", values: []benchfmt.Value{ + {Value: float64(m.CompactionCounts.Total), Unit: "compactions"}, + {Value: float64(m.CompactionCounts.Default), Unit: "default"}, + {Value: float64(m.CompactionCounts.DeleteOnly), Unit: "delete"}, + {Value: float64(m.CompactionCounts.ElisionOnly), Unit: "elision"}, + {Value: float64(m.CompactionCounts.Move), Unit: "move"}, + {Value: float64(m.CompactionCounts.Read), Unit: "read"}, + {Value: float64(m.CompactionCounts.Rewrite), Unit: "rewrite"}, + {Value: float64(m.CompactionCounts.MultiLevel), Unit: "multilevel"}, + }}, + // Total database sizes sampled after every workload step and + // compaction. This can be used to evaluate the relative LSM space + // amplification between runs of the same workload. Calculating the true + // space amplification continuously is prohibitvely expensive (it + // requires totally compacting a copy of the LSM). + {label: "DatabaseSize/mean", values: []benchfmt.Value{ + {Value: m.TotalSize.Mean(), Unit: "bytes"}, + }}, + {label: "DatabaseSize/max", values: []benchfmt.Value{ + {Value: float64(m.TotalSize.Max()), Unit: "bytes"}, + }}, + // Time applying the workload and time waiting for compactions to + // quiesce after the workload has completed. + {label: "DurationWorkload", values: []benchfmt.Value{ + {Value: m.WorkloadDuration.Seconds(), Unit: "sec/op"}, + }}, + {label: "DurationQuiescing", values: []benchfmt.Value{ + {Value: m.QuiesceDuration.Seconds(), Unit: "sec/op"}, + }}, + {label: "DurationPaceDelay", values: []benchfmt.Value{ + {Value: m.PaceDuration.Seconds(), Unit: "sec/op"}, + }}, + // Estimated compaction debt, sampled after every workload step and + // compaction. + {label: "EstimatedDebt/mean", values: []benchfmt.Value{ + {Value: m.EstimatedDebt.Mean(), Unit: "bytes"}, + }}, + {label: "EstimatedDebt/max", values: []benchfmt.Value{ + {Value: float64(m.EstimatedDebt.Max()), Unit: "bytes"}, + }}, + {label: "FlushUtilization", values: []benchfmt.Value{ + {Value: m.Final.Flush.WriteThroughput.Utilization(), Unit: "util"}, + }}, + {label: "IngestedIntoL0", values: []benchfmt.Value{ + {Value: float64(m.Ingest.BytesIntoL0), Unit: "bytes"}, + }}, + {label: "IngestWeightedByLevel", values: []benchfmt.Value{ + {Value: float64(m.Ingest.BytesWeightedByLevel), Unit: "bytes"}, + }}, + {label: "ReadAmp/mean", values: []benchfmt.Value{ + {Value: m.ReadAmp.Mean(), Unit: "files"}, + }}, + {label: "ReadAmp/max", values: []benchfmt.Value{ + {Value: float64(m.ReadAmp.Max()), Unit: "files"}, + }}, + {label: "TombstoneCount/mean", values: []benchfmt.Value{ + {Value: m.TombstoneCount.Mean(), Unit: "tombstones"}, + }}, + {label: "TombstoneCount/max", values: []benchfmt.Value{ + {Value: float64(m.TombstoneCount.Max()), Unit: "tombstones"}, + }}, + {label: "Throughput", values: []benchfmt.Value{ + {Value: float64(m.WriteBytes) / (m.WorkloadDuration + m.QuiesceDuration).Seconds(), Unit: "B/s"}, + }}, + {label: "WriteAmp", values: []benchfmt.Value{ + {Value: float64(m.TotalWriteAmp), Unit: "wamp"}, + }}, + } + + for _, reason := range []string{"L0", "memtable"} { + groups = append(groups, benchmarkSection{ + label: fmt.Sprintf("WriteStall/%s", reason), + values: []benchfmt.Value{ + {Value: float64(m.WriteStalls[reason]), Unit: "stalls"}, + {Value: float64(m.WriteStallsDuration[reason].Seconds()), Unit: "stallsec/op"}, + }, + }) + } + + bw := benchfmt.NewWriter(w) + for _, grp := range groups { + err := bw.Write(&benchfmt.Result{ + Name: benchfmt.Name(fmt.Sprintf("BenchmarkReplay/%s/%s", name, grp.label)), + Iters: 1, + Values: grp.values, + }) + if err != nil { + return err + } + } + return nil +} + +// Runner runs a captured workload against a test database, collecting +// metrics on performance. +type Runner struct { + RunDir string + WorkloadFS vfs.FS + WorkloadPath string + Pacer Pacer + Opts *pebble.Options + MaxWriteBytes uint64 + + // Internal state. + + d *pebble.DB + // dbMetrics and dbMetricsCond work in unison to update the metrics and + // notify (broadcast) to any waiting clients that metrics have been updated. + dbMetrics *pebble.Metrics + dbMetricsCond sync.Cond + cancel func() + err atomic.Value + errgroup *errgroup.Group + readerOpts sstable.ReaderOptions + stagingDir string + steps chan workloadStep + stepsApplied chan workloadStep + + metrics struct { + estimatedDebt SampledMetric + quiesceDuration time.Duration + readAmp SampledMetric + tombstoneCount SampledMetric + totalSize SampledMetric + paceDurationNano atomic.Uint64 + workloadDuration time.Duration + writeBytes atomic.Uint64 + writeThroughput SampledMetric + } + writeStallMetrics struct { + sync.Mutex + countByReason map[string]int + durationByReason map[string]time.Duration + } + // compactionMu holds state for tracking the number of compactions + // started and completed and waking waiting goroutines when a new compaction + // completes. See nextCompactionCompletes. + compactionMu struct { + sync.Mutex + ch chan struct{} + started int64 + completed int64 + } + workload struct { + manifests []string + // manifest{Idx,Off} record the starting position of the workload + // relative to the initial database state. + manifestIdx int + manifestOff int64 + // sstables records the set of captured workload sstables by file num. + sstables map[base.FileNum]struct{} + } +} + +// Run begins executing the workload and returns. +// +// The workload application will respect the provided context's cancellation. +func (r *Runner) Run(ctx context.Context) error { + // Find the workload start relative to the RunDir's existing database state. + // A prefix of the workload's manifest edits are expected to have already + // been applied to the checkpointed existing database state. + var err error + r.workload.manifests, r.workload.sstables, err = findWorkloadFiles(r.WorkloadPath, r.WorkloadFS) + if err != nil { + return err + } + r.workload.manifestIdx, r.workload.manifestOff, err = findManifestStart(r.RunDir, r.Opts.FS, r.workload.manifests) + if err != nil { + return err + } + + // Set up a staging dir for files that will be ingested. + r.stagingDir = r.Opts.FS.PathJoin(r.RunDir, "staging") + if err := r.Opts.FS.MkdirAll(r.stagingDir, os.ModePerm); err != nil { + return err + } + + r.dbMetricsCond = sync.Cond{ + L: &sync.Mutex{}, + } + + // Extend the user-provided Options with extensions necessary for replay + // mechanics. + r.compactionMu.ch = make(chan struct{}) + r.Opts.AddEventListener(r.eventListener()) + r.writeStallMetrics.countByReason = make(map[string]int) + r.writeStallMetrics.durationByReason = make(map[string]time.Duration) + r.Opts.EnsureDefaults() + r.readerOpts = r.Opts.MakeReaderOptions() + r.Opts.DisableWAL = true + r.d, err = pebble.Open(r.RunDir, r.Opts) + if err != nil { + return err + } + + r.dbMetrics = r.d.Metrics() + + // Use a buffered channel to allow the prepareWorkloadSteps to read ahead, + // buffering up to cap(r.steps) steps ahead of the current applied state. + // Flushes need to be buffered and ingested sstables need to be copied, so + // pipelining this preparation makes it more likely the step will be ready + // to apply when the pacer decides to apply it. + r.steps = make(chan workloadStep, 5) + r.stepsApplied = make(chan workloadStep, 5) + + ctx, r.cancel = context.WithCancel(ctx) + r.errgroup, ctx = errgroup.WithContext(ctx) + r.errgroup.Go(func() error { return r.prepareWorkloadSteps(ctx) }) + r.errgroup.Go(func() error { return r.applyWorkloadSteps(ctx) }) + r.errgroup.Go(func() error { return r.refreshMetrics(ctx) }) + return nil +} + +// refreshMetrics runs in its own goroutine, collecting metrics from the Pebble +// instance whenever a) a workload step completes, or b) a compaction completes. +// The Pacer implementations that pace based on read-amplification rely on these +// refreshed metrics to decide when to allow the workload to proceed. +func (r *Runner) refreshMetrics(ctx context.Context) error { + startAt := time.Now() + var workloadExhausted bool + var workloadExhaustedAt time.Time + stepsApplied := r.stepsApplied + compactionCount, alreadyCompleted, compactionCh := r.nextCompactionCompletes(0) + for { + if !alreadyCompleted { + select { + case <-ctx.Done(): + return ctx.Err() + case <-compactionCh: + // Fall through to refreshing dbMetrics. + case _, ok := <-stepsApplied: + if !ok { + workloadExhausted = true + workloadExhaustedAt = time.Now() + // Set the [stepsApplied] channel to nil so that we'll never + // hit this case again, and we don't busy loop. + stepsApplied = nil + // Record the replay time. + r.metrics.workloadDuration = workloadExhaustedAt.Sub(startAt) + } + // Fall through to refreshing dbMetrics. + } + } + + m := r.d.Metrics() + r.dbMetricsCond.L.Lock() + r.dbMetrics = m + r.dbMetricsCond.Broadcast() + r.dbMetricsCond.L.Unlock() + + // Collect sample metrics. These metrics are calculated by sampling + // every time we collect metrics. + r.metrics.readAmp.record(int64(m.ReadAmp())) + r.metrics.estimatedDebt.record(int64(m.Compact.EstimatedDebt)) + r.metrics.tombstoneCount.record(int64(m.Keys.TombstoneCount)) + r.metrics.totalSize.record(int64(m.DiskSpaceUsage())) + r.metrics.writeThroughput.record(int64(r.metrics.writeBytes.Load())) + + compactionCount, alreadyCompleted, compactionCh = r.nextCompactionCompletes(compactionCount) + // Consider whether replaying is complete. There are two necessary + // conditions: + // + // 1. The workload must be exhausted. + // 2. Compactions must have quiesced. + // + // The first condition is simple. The replay tool is responsible for + // applying the workload. The goroutine responsible for applying the + // workload closes the `stepsApplied` channel after the last step has + // been applied, and we'll flip `workloadExhausted` to true. + // + // The second condition is tricky. The replay tool doesn't control + // compactions and doesn't have visibility into whether the compaction + // picker is about to schedule a new compaction. We can tell when + // compactions are in progress or may be immeninent (eg, flushes in + // progress). If it appears that compactions have quiesced, pause for a + // fixed duration to see if a new one is scheduled. If not, consider + // compactions quiesced. + if workloadExhausted && !alreadyCompleted && r.compactionsAppearQuiesced(m) { + select { + case <-compactionCh: + // A new compaction just finished; compactions have not + // quiesced. + continue + case <-time.After(time.Second): + // No compactions completed. If it still looks like they've + // quiesced according to the metrics, consider them quiesced. + if r.compactionsAppearQuiesced(r.d.Metrics()) { + r.metrics.quiesceDuration = time.Since(workloadExhaustedAt) + return nil + } + } + } + } +} + +// compactionsAppearQuiesced returns true if the database may have quiesced, and +// there likely won't be additional compactions scheduled. Detecting quiescence +// is a bit fraught: The various signals that Pebble makes available are +// adjusted at different points in the compaction lifecycle, and database +// mutexes are dropped and acquired between them. This makes it difficult to +// reliably identify when compactions quiesce. +// +// For example, our call to DB.Metrics() may acquire the DB.mu mutex when a +// compaction has just successfully completed, but before it's managed to +// schedule the next compaction (DB.mu is dropped while it attempts to acquire +// the manifest lock). +func (r *Runner) compactionsAppearQuiesced(m *pebble.Metrics) bool { + r.compactionMu.Lock() + defer r.compactionMu.Unlock() + if m.Flush.NumInProgress > 0 { + return false + } else if m.Compact.NumInProgress > 0 && r.compactionMu.started != r.compactionMu.completed { + return false + } + return true +} + +// nextCompactionCompletes may be used to be notified when new compactions +// complete. The caller is responsible for holding on to a monotonically +// increasing count representing the number of compactions that have been +// observed, beginning at zero. +// +// The caller passes their current count as an argument. If a new compaction has +// already completed since their provided count, nextCompactionCompletes returns +// the new count and a true boolean return value. If a new compaction has not +// yet completed, it returns a channel that will be closed when the next +// compaction completes. This scheme allows the caller to select{...}, +// performing some action on every compaction completion. +func (r *Runner) nextCompactionCompletes( + lastObserved int64, +) (count int64, alreadyOccurred bool, ch chan struct{}) { + r.compactionMu.Lock() + defer r.compactionMu.Unlock() + + if lastObserved < r.compactionMu.completed { + // There has already been another compaction since the last one observed + // by this caller. Return immediately. + return r.compactionMu.completed, true, nil + } + + // The last observed compaction is still the most recent compaction. + // Return a channel that the caller can wait on to be notified when the + // next compaction occurs. + if r.compactionMu.ch == nil { + r.compactionMu.ch = make(chan struct{}) + } + return lastObserved, false, r.compactionMu.ch +} + +// Wait waits for the workload replay to complete. Wait returns once the entire +// workload has been replayed, and compactions have quiesced. +func (r *Runner) Wait() (Metrics, error) { + err := r.errgroup.Wait() + if storedErr := r.err.Load(); storedErr != nil { + err = storedErr.(error) + } + pm := r.d.Metrics() + total := pm.Total() + var ingestBytesWeighted uint64 + for l := 0; l < len(pm.Levels); l++ { + ingestBytesWeighted += pm.Levels[l].BytesIngested * uint64(len(pm.Levels)-l-1) + } + + m := Metrics{ + Final: pm, + EstimatedDebt: r.metrics.estimatedDebt, + PaceDuration: time.Duration(r.metrics.paceDurationNano.Load()), + ReadAmp: r.metrics.readAmp, + QuiesceDuration: r.metrics.quiesceDuration, + TombstoneCount: r.metrics.tombstoneCount, + TotalSize: r.metrics.totalSize, + TotalWriteAmp: total.WriteAmp(), + WorkloadDuration: r.metrics.workloadDuration, + WriteBytes: r.metrics.writeBytes.Load(), + WriteStalls: make(map[string]int), + WriteStallsDuration: make(map[string]time.Duration), + WriteThroughput: r.metrics.writeThroughput, + } + + r.writeStallMetrics.Lock() + for reason, count := range r.writeStallMetrics.countByReason { + m.WriteStalls[reason] = count + } + for reason, duration := range r.writeStallMetrics.durationByReason { + m.WriteStallsDuration[reason] = duration + } + r.writeStallMetrics.Unlock() + m.CompactionCounts.Total = pm.Compact.Count + m.CompactionCounts.Default = pm.Compact.DefaultCount + m.CompactionCounts.DeleteOnly = pm.Compact.DeleteOnlyCount + m.CompactionCounts.ElisionOnly = pm.Compact.ElisionOnlyCount + m.CompactionCounts.Move = pm.Compact.MoveCount + m.CompactionCounts.Read = pm.Compact.ReadCount + m.CompactionCounts.Rewrite = pm.Compact.RewriteCount + m.CompactionCounts.MultiLevel = pm.Compact.MultiLevelCount + m.Ingest.BytesIntoL0 = pm.Levels[0].BytesIngested + m.Ingest.BytesWeightedByLevel = ingestBytesWeighted + return m, err +} + +// Close closes remaining open resources, including the database. It must be +// called after Wait. +func (r *Runner) Close() error { + return r.d.Close() +} + +// A workloadStep describes a single manifest edit in the workload. It may be a +// flush or ingest that should be applied to the test database, or it may be a +// compaction that is surfaced to allow the replay logic to compare against the +// state of the database at workload collection time. +type workloadStep struct { + kind stepKind + ve manifest.VersionEdit + // a Version describing the state of the LSM *before* the workload was + // collected. + pv *manifest.Version + // a Version describing the state of the LSM when the workload was + // collected. + v *manifest.Version + // non-nil for flushStepKind + flushBatch *pebble.Batch + tablesToIngest []string + cumulativeWriteBytes uint64 +} + +type stepKind uint8 + +const ( + flushStepKind stepKind = iota + ingestStepKind + compactionStepKind +) + +// eventListener returns a Pebble EventListener that is installed on the replay +// database so that the replay runner has access to internal Pebble events. +func (r *Runner) eventListener() pebble.EventListener { + var writeStallBegin time.Time + var writeStallReason string + l := pebble.EventListener{ + BackgroundError: func(err error) { + r.err.Store(err) + r.cancel() + }, + WriteStallBegin: func(info pebble.WriteStallBeginInfo) { + r.writeStallMetrics.Lock() + defer r.writeStallMetrics.Unlock() + writeStallReason = info.Reason + // Take just the first word of the reason. + if j := strings.IndexByte(writeStallReason, ' '); j != -1 { + writeStallReason = writeStallReason[:j] + } + switch writeStallReason { + case "L0", "memtable": + r.writeStallMetrics.countByReason[writeStallReason]++ + default: + panic(fmt.Sprintf("unrecognized write stall reason %q", info.Reason)) + } + writeStallBegin = time.Now() + }, + WriteStallEnd: func() { + r.writeStallMetrics.Lock() + defer r.writeStallMetrics.Unlock() + r.writeStallMetrics.durationByReason[writeStallReason] += time.Since(writeStallBegin) + }, + CompactionBegin: func(_ pebble.CompactionInfo) { + r.compactionMu.Lock() + defer r.compactionMu.Unlock() + r.compactionMu.started++ + }, + CompactionEnd: func(_ pebble.CompactionInfo) { + // Keep track of the number of compactions that complete and notify + // anyone waiting for a compaction to complete. See the function + // nextCompactionCompletes for the corresponding receiver side. + r.compactionMu.Lock() + defer r.compactionMu.Unlock() + r.compactionMu.completed++ + if r.compactionMu.ch != nil { + // Signal that a compaction has completed. + close(r.compactionMu.ch) + r.compactionMu.ch = nil + } + }, + } + l.EnsureDefaults(nil) + return l +} + +// applyWorkloadSteps runs in its own goroutine, reading workload steps off the +// r.steps channel and applying them to the test database. +func (r *Runner) applyWorkloadSteps(ctx context.Context) error { + for { + var ok bool + var step workloadStep + select { + case <-ctx.Done(): + return ctx.Err() + case step, ok = <-r.steps: + if !ok { + // Exhausted the workload. Exit. + close(r.stepsApplied) + return nil + } + } + + paceDur := r.Pacer.pace(r, step) + r.metrics.paceDurationNano.Add(uint64(paceDur)) + + switch step.kind { + case flushStepKind: + if err := step.flushBatch.Commit(&pebble.WriteOptions{Sync: false}); err != nil { + return err + } + _, err := r.d.AsyncFlush() + if err != nil { + return err + } + r.metrics.writeBytes.Store(step.cumulativeWriteBytes) + r.stepsApplied <- step + case ingestStepKind: + if err := r.d.Ingest(step.tablesToIngest); err != nil { + return err + } + r.metrics.writeBytes.Store(step.cumulativeWriteBytes) + r.stepsApplied <- step + case compactionStepKind: + // No-op. + // TODO(jackson): Should we elide this earlier? + default: + panic("unreachable") + } + } +} + +// prepareWorkloadSteps runs in its own goroutine, reading the workload +// manifests in order to reconstruct the workload and prepare each step to be +// applied. It sends each workload step to the r.steps channel. +func (r *Runner) prepareWorkloadSteps(ctx context.Context) error { + defer func() { close(r.steps) }() + + idx := r.workload.manifestIdx + + var cumulativeWriteBytes uint64 + var flushBufs flushBuffers + var v *manifest.Version + var previousVersion *manifest.Version + var bve manifest.BulkVersionEdit + bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata) + applyVE := func(ve *manifest.VersionEdit) error { + return bve.Accumulate(ve) + } + currentVersion := func() (*manifest.Version, error) { + var err error + v, err = bve.Apply(v, + r.Opts.Comparer.Compare, + r.Opts.Comparer.FormatKey, + r.Opts.FlushSplitBytes, + r.Opts.Experimental.ReadCompactionRate, + nil, /* zombies */ + manifest.ProhibitSplitUserKeys) + bve = manifest.BulkVersionEdit{AddedByFileNum: bve.AddedByFileNum} + return v, err + } + + for ; idx < len(r.workload.manifests); idx++ { + if r.MaxWriteBytes != 0 && cumulativeWriteBytes > r.MaxWriteBytes { + break + } + + err := func() error { + manifestName := r.workload.manifests[idx] + f, err := r.WorkloadFS.Open(r.WorkloadFS.PathJoin(r.WorkloadPath, manifestName)) + if err != nil { + return err + } + defer f.Close() + + rr := record.NewReader(f, 0 /* logNum */) + // A manifest's first record always holds the initial version state. + // If this is the first manifest we're examining, we load it in + // order to seed `metas` with the file metadata of the existing + // files. Otherwise, we can skip it because we already know all the + // file metadatas up to this point. + rec, err := rr.Next() + if err != nil { + return err + } + if idx == r.workload.manifestIdx { + var ve manifest.VersionEdit + if err := ve.Decode(rec); err != nil { + return err + } + if err := applyVE(&ve); err != nil { + return err + } + } + + // Read the remaining of the manifests version edits, one-by-one. + for { + rec, err := rr.Next() + if err == io.EOF || record.IsInvalidRecord(err) { + break + } else if err != nil { + return err + } + var ve manifest.VersionEdit + if err = ve.Decode(rec); err == io.EOF || record.IsInvalidRecord(err) { + break + } else if err != nil { + return err + } + if err := applyVE(&ve); err != nil { + return err + } + if idx == r.workload.manifestIdx && rr.Offset() <= r.workload.manifestOff { + // The record rec began at an offset strictly less than + // rr.Offset(), which means it's strictly less than + // r.workload.manifestOff, and we should skip it. + continue + } + if len(ve.NewFiles) == 0 && len(ve.DeletedFiles) == 0 { + // Skip WAL rotations and other events that don't affect the + // files of the LSM. + continue + } + + s := workloadStep{ve: ve} + if len(ve.DeletedFiles) > 0 { + // If a version edit deletes files, we assume it's a compaction. + s.kind = compactionStepKind + } else { + // Default to ingest. If any files have unequal + // smallest,largest sequence numbers, we'll update this to a + // flush. + s.kind = ingestStepKind + } + var newFiles []base.DiskFileNum + for _, nf := range ve.NewFiles { + newFiles = append(newFiles, nf.Meta.FileBacking.DiskFileNum) + if s.kind == ingestStepKind && (nf.Meta.SmallestSeqNum != nf.Meta.LargestSeqNum || nf.Level != 0) { + s.kind = flushStepKind + } + } + // Add the current reference *Version to the step. This provides + // access to, for example, the read-amplification of the + // database at this point when the workload was collected. This + // can be useful for pacing. + if s.v, err = currentVersion(); err != nil { + return err + } + // On the first time through, we set the previous version to the current + // version otherwise we set it to the actual previous version. + if previousVersion == nil { + previousVersion = s.v + } + s.pv = previousVersion + previousVersion = s.v + + // It's possible that the workload collector captured this + // version edit, but wasn't able to collect all of the + // corresponding sstables before being terminated. + if s.kind == flushStepKind || s.kind == ingestStepKind { + for _, fileNum := range newFiles { + if _, ok := r.workload.sstables[fileNum.FileNum()]; !ok { + // TODO(jackson,leon): This isn't exactly an error + // condition. Give this more thought; do we want to + // require graceful exiting of workload collection, + // such that the last version edit must have had its + // corresponding sstables collected? + return errors.Newf("sstable %s not found", fileNum) + } + } + } + + switch s.kind { + case flushStepKind: + // Load all of the flushed sstables' keys into a batch. + s.flushBatch = r.d.NewBatch() + if err := loadFlushedSSTableKeys(s.flushBatch, r.WorkloadFS, r.WorkloadPath, newFiles, r.readerOpts, &flushBufs); err != nil { + return errors.Wrapf(err, "flush in %q at offset %d", manifestName, rr.Offset()) + } + cumulativeWriteBytes += uint64(s.flushBatch.Len()) + case ingestStepKind: + // Copy the ingested sstables into a staging area within the + // run dir. This is necessary for two reasons: + // a) Ingest will remove the source file, and we don't want + // to mutate the workload. + // b) If the workload stored on another volume, Ingest + // would need to fall back to copying the file since + // it's not possible to link across volumes. The true + // workload likely linked the file. Staging the file + // ahead of time ensures that we're able to Link the + // file like the original workload did. + for _, fileNum := range newFiles { + src := base.MakeFilepath(r.WorkloadFS, r.WorkloadPath, base.FileTypeTable, fileNum) + dst := base.MakeFilepath(r.Opts.FS, r.stagingDir, base.FileTypeTable, fileNum) + if err := vfs.CopyAcrossFS(r.WorkloadFS, src, r.Opts.FS, dst); err != nil { + return errors.Wrapf(err, "ingest in %q at offset %d", manifestName, rr.Offset()) + } + finfo, err := r.Opts.FS.Stat(dst) + if err != nil { + return errors.Wrapf(err, "stating %q", dst) + } + cumulativeWriteBytes += uint64(finfo.Size()) + s.tablesToIngest = append(s.tablesToIngest, dst) + } + case compactionStepKind: + // Nothing to do. + } + s.cumulativeWriteBytes = cumulativeWriteBytes + + select { + case <-ctx.Done(): + return ctx.Err() + case r.steps <- s: + } + + if r.MaxWriteBytes != 0 && cumulativeWriteBytes > r.MaxWriteBytes { + break + } + } + return nil + }() + if err != nil { + return err + } + } + return nil +} + +// findWorkloadFiles finds all manifests and tables in the provided path on fs. +func findWorkloadFiles( + path string, fs vfs.FS, +) (manifests []string, sstables map[base.FileNum]struct{}, err error) { + dirents, err := fs.List(path) + if err != nil { + return nil, nil, err + } + sstables = make(map[base.FileNum]struct{}) + for _, dirent := range dirents { + typ, fileNum, ok := base.ParseFilename(fs, dirent) + if !ok { + continue + } + switch typ { + case base.FileTypeManifest: + manifests = append(manifests, dirent) + case base.FileTypeTable: + sstables[fileNum.FileNum()] = struct{}{} + } + } + if len(manifests) == 0 { + return nil, nil, errors.Newf("no manifests found") + } + sort.Strings(manifests) + return manifests, sstables, err +} + +// findManifestStart takes a database directory and FS containing the initial +// database state that a workload will be run against, and a list of a workloads +// manifests. It examines the database's current manifest to determine where +// workload replay should begin, so as to not duplicate already-applied version +// edits. +// +// It returns the index of the starting manifest, and the database's current +// offset within the manifest. +func findManifestStart( + dbDir string, dbFS vfs.FS, manifests []string, +) (index int, offset int64, err error) { + // Identify the database's current manifest. + dbDesc, err := pebble.Peek(dbDir, dbFS) + if err != nil { + return 0, 0, err + } + dbManifest := dbFS.PathBase(dbDesc.ManifestFilename) + // If there is no initial database state, begin workload replay from the + // beginning of the first manifest. + if !dbDesc.Exists { + return 0, 0, nil + } + for index = 0; index < len(manifests); index++ { + if manifests[index] == dbManifest { + break + } + } + if index == len(manifests) { + // The initial database state has a manifest that does not appear within + // the workload's set of manifests. This is possible if we began + // recording the workload at the same time as a manifest rotation, but + // more likely we're applying a workload to a different initial database + // state than the one from which the workload was collected. Either way, + // start from the beginning of the first manifest. + return 0, 0, nil + } + // Find the initial database's offset within the manifest. + info, err := dbFS.Stat(dbFS.PathJoin(dbDir, dbManifest)) + if err != nil { + return 0, 0, err + } + return index, info.Size(), nil +} + +// loadFlushedSSTableKeys copies keys from the sstables specified by `fileNums` +// in the directory specified by `path` into the provided the batch. Keys are +// applied to the batch in the order dictated by their sequence numbers within +// the sstables, ensuring the relative relationship between sequence numbers is +// maintained. +// +// Preserving the relative relationship between sequence numbers is not strictly +// necessary, but it ensures we accurately exercise some microoptimizations (eg, +// detecting user key changes by descending trailer). There may be additional +// dependencies on sequence numbers in the future. +func loadFlushedSSTableKeys( + b *pebble.Batch, + fs vfs.FS, + path string, + fileNums []base.DiskFileNum, + readOpts sstable.ReaderOptions, + bufs *flushBuffers, +) error { + // Load all the keys across all the sstables. + for _, fileNum := range fileNums { + if err := func() error { + filePath := base.MakeFilepath(fs, path, base.FileTypeTable, fileNum) + f, err := fs.Open(filePath) + if err != nil { + return err + } + readable, err := sstable.NewSimpleReadable(f) + if err != nil { + f.Close() + return err + } + r, err := sstable.NewReader(readable, readOpts) + if err != nil { + return err + } + defer r.Close() + + // Load all the point keys. + iter, err := r.NewIter(nil, nil) + if err != nil { + return err + } + defer iter.Close() + for k, lv := iter.First(); k != nil; k, lv = iter.Next() { + var key flushedKey + key.Trailer = k.Trailer + bufs.alloc, key.UserKey = bufs.alloc.Copy(k.UserKey) + if v, callerOwned, err := lv.Value(nil); err != nil { + return err + } else if callerOwned { + key.value = v + } else { + bufs.alloc, key.value = bufs.alloc.Copy(v) + } + bufs.keys = append(bufs.keys, key) + } + + // Load all the range tombstones. + if iter, err := r.NewRawRangeDelIter(); err != nil { + return err + } else if iter != nil { + defer iter.Close() + for s := iter.First(); s != nil; s = iter.Next() { + if err := rangedel.Encode(s, func(k base.InternalKey, v []byte) error { + var key flushedKey + key.Trailer = k.Trailer + bufs.alloc, key.UserKey = bufs.alloc.Copy(k.UserKey) + bufs.alloc, key.value = bufs.alloc.Copy(v) + bufs.keys = append(bufs.keys, key) + return nil + }); err != nil { + return err + } + } + } + + // Load all the range keys. + if iter, err := r.NewRawRangeKeyIter(); err != nil { + return err + } else if iter != nil { + defer iter.Close() + for s := iter.First(); s != nil; s = iter.Next() { + if err := rangekey.Encode(s, func(k base.InternalKey, v []byte) error { + var key flushedKey + key.Trailer = k.Trailer + bufs.alloc, key.UserKey = bufs.alloc.Copy(k.UserKey) + bufs.alloc, key.value = bufs.alloc.Copy(v) + bufs.keys = append(bufs.keys, key) + return nil + }); err != nil { + return err + } + } + } + return nil + }(); err != nil { + return err + } + } + + // Sort the flushed keys by their sequence numbers so that we can apply them + // to the batch in the same order, maintaining the relative relationship + // between keys. + // NB: We use a stable sort so that keys corresponding to span fragments + // (eg, range tombstones and range keys) have a deterministic ordering for + // testing. + sort.Stable(bufs.keys) + + // Add the keys to the batch in the order they were committed when the + // workload was captured. + for i := 0; i < len(bufs.keys); i++ { + var err error + switch bufs.keys[i].Kind() { + case base.InternalKeyKindDelete: + err = b.Delete(bufs.keys[i].UserKey, nil) + case base.InternalKeyKindDeleteSized: + v, _ := binary.Uvarint(bufs.keys[i].value) + // Batch.DeleteSized takes just the length of the value being + // deleted and adds the key's length to derive the overall entry + // size of the value being deleted. This has already been done to + // the key we're reading from the sstable, so we must subtract the + // key length from the encoded value before calling b.DeleteSized, + // which will again add the key length before encoding. + err = b.DeleteSized(bufs.keys[i].UserKey, uint32(v-uint64(len(bufs.keys[i].UserKey))), nil) + case base.InternalKeyKindSet, base.InternalKeyKindSetWithDelete: + err = b.Set(bufs.keys[i].UserKey, bufs.keys[i].value, nil) + case base.InternalKeyKindMerge: + err = b.Merge(bufs.keys[i].UserKey, bufs.keys[i].value, nil) + case base.InternalKeyKindSingleDelete: + err = b.SingleDelete(bufs.keys[i].UserKey, nil) + case base.InternalKeyKindRangeDelete: + err = b.DeleteRange(bufs.keys[i].UserKey, bufs.keys[i].value, nil) + case base.InternalKeyKindRangeKeySet, base.InternalKeyKindRangeKeyUnset, base.InternalKeyKindRangeKeyDelete: + s, err := rangekey.Decode(bufs.keys[i].InternalKey, bufs.keys[i].value, nil) + if err != nil { + return err + } + if len(s.Keys) != 1 { + return errors.Newf("range key span unexpectedly contains %d keys", len(s.Keys)) + } + switch bufs.keys[i].Kind() { + case base.InternalKeyKindRangeKeySet: + err = b.RangeKeySet(s.Start, s.End, s.Keys[0].Suffix, s.Keys[0].Value, nil) + case base.InternalKeyKindRangeKeyUnset: + err = b.RangeKeyUnset(s.Start, s.End, s.Keys[0].Suffix, nil) + case base.InternalKeyKindRangeKeyDelete: + err = b.RangeKeyDelete(s.Start, s.End, nil) + default: + err = errors.Newf("unexpected key kind %q", bufs.keys[i].Kind()) + } + if err != nil { + return err + } + default: + err = errors.Newf("unexpected key kind %q", bufs.keys[i].Kind()) + } + if err != nil { + return err + } + } + + // Done with the flushBuffers. Reset. + bufs.keys = bufs.keys[:0] + return nil +} + +type flushBuffers struct { + keys flushedKeysByTrailer + alloc bytealloc.A +} + +type flushedKeysByTrailer []flushedKey + +func (s flushedKeysByTrailer) Len() int { return len(s) } +func (s flushedKeysByTrailer) Less(i, j int) bool { return s[i].Trailer < s[j].Trailer } +func (s flushedKeysByTrailer) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +type flushedKey struct { + base.InternalKey + value []byte +} diff --git a/pebble/replay/replay_test.go b/pebble/replay/replay_test.go new file mode 100644 index 0000000..93bbdbe --- /dev/null +++ b/pebble/replay/replay_test.go @@ -0,0 +1,585 @@ +package replay + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "io" + "math/rand" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/datatest" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/rangekey" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func runReplayTest(t *testing.T, path string) { + fs := vfs.NewMem() + var ctx context.Context + var r Runner + var ct *datatest.CompactionTracker + datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "cat": + var buf bytes.Buffer + for _, arg := range td.CmdArgs { + f, err := fs.Open(arg.String()) + if err != nil { + fmt.Fprintf(&buf, "%s: %s\n", arg, err) + continue + } + io.Copy(&buf, f) + require.NoError(t, f.Close()) + } + return buf.String() + case "corpus": + for _, arg := range td.CmdArgs { + t.Run(fmt.Sprintf("corpus/%s", arg.String()), func(t *testing.T) { + collectCorpus(t, fs, arg.String()) + }) + } + return "" + case "list-files": + return runListFiles(t, fs, td) + case "replay": + name := td.CmdArgs[0].String() + pacerVariant := td.CmdArgs[1].String() + var pacer Pacer + if pacerVariant == "reference" { + pacer = PaceByReferenceReadAmp{} + } else if pacerVariant == "fixed" { + i, err := strconv.Atoi(td.CmdArgs[2].String()) + require.NoError(t, err) + pacer = PaceByFixedReadAmp(i) + } else { + pacer = Unpaced{} + } + + // Convert the testdata/replay:235 datadriven command position into + // a run directory suffixed with the line number: eg, 'run-235' + lineOffset := strings.LastIndexByte(td.Pos, ':') + require.Positive(t, lineOffset) + runDir := fmt.Sprintf("run-%s", td.Pos[lineOffset+1:]) + if err := fs.MkdirAll(runDir, os.ModePerm); err != nil { + return err.Error() + } + + checkpointDir := fs.PathJoin(name, "checkpoint") + ok, err := vfs.Clone(fs, fs, checkpointDir, runDir) + if err != nil { + return err.Error() + } else if !ok { + return fmt.Sprintf("%q does not exist", checkpointDir) + } + + opts := &pebble.Options{ + FS: fs, + Comparer: testkeys.Comparer, + FormatMajorVersion: pebble.FormatRangeKeys, + L0CompactionFileThreshold: 1, + } + setDefaultExperimentalOpts(opts) + ct = datatest.NewCompactionTracker(opts) + + r = Runner{ + RunDir: runDir, + WorkloadFS: fs, + WorkloadPath: name, + Pacer: pacer, + Opts: opts, + } + ctx = context.Background() + if err := r.Run(ctx); err != nil { + return err.Error() + } + return "" + case "scan-keys": + var buf bytes.Buffer + it, _ := r.d.NewIter(nil) + defer it.Close() + for valid := it.First(); valid; valid = it.Next() { + fmt.Fprintf(&buf, "%s: %s\n", it.Key(), it.Value()) + } + if err := it.Error(); err != nil { + fmt.Fprintln(&buf, err.Error()) + } + return buf.String() + case "tree": + return fs.String() + case "wait-for-compactions": + var target int + if len(td.CmdArgs) == 1 { + i, err := strconv.Atoi(td.CmdArgs[0].String()) + require.NoError(t, err) + target = i + } + ct.WaitForInflightCompactionsToEqual(target) + return "" + case "wait": + m, err := r.Wait() + if err != nil { + return err.Error() + } + return fmt.Sprintf("replayed %s in writes", humanize.Bytes.Uint64(m.WriteBytes)) + case "close": + if err := r.Close(); err != nil { + return err.Error() + } + return "" + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +func setDefaultExperimentalOpts(opts *pebble.Options) { + opts.Experimental.TableCacheShards = 2 +} + +func TestReplay(t *testing.T) { + runReplayTest(t, "testdata/replay") +} + +func TestReplayPaced(t *testing.T) { + runReplayTest(t, "testdata/replay_paced") +} + +func TestLoadFlushedSSTableKeys(t *testing.T) { + var buf bytes.Buffer + var diskFileNums []base.DiskFileNum + opts := &pebble.Options{ + DisableAutomaticCompactions: true, + EventListener: &pebble.EventListener{ + FlushEnd: func(info pebble.FlushInfo) { + for _, tbl := range info.Output { + diskFileNums = append(diskFileNums, tbl.FileNum.DiskFileNum()) + } + }, + }, + FS: vfs.NewMem(), + Comparer: testkeys.Comparer, + FormatMajorVersion: pebble.FormatRangeKeys, + } + d, err := pebble.Open("", opts) + require.NoError(t, err) + defer d.Close() + + var flushBufs flushBuffers + datadriven.RunTest(t, "testdata/flushed_sstable_keys", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "commit": + b := d.NewIndexedBatch() + if err := datatest.DefineBatch(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + + b := d.NewBatch() + err := loadFlushedSSTableKeys(b, opts.FS, "", diskFileNums, opts.MakeReaderOptions(), &flushBufs) + if err != nil { + b.Close() + return err.Error() + } + + br, _ := pebble.ReadBatch(b.Repr()) + kind, ukey, v, ok, err := br.Next() + for ; ok; kind, ukey, v, ok, err = br.Next() { + fmt.Fprintf(&buf, "%s.%s", ukey, kind) + switch kind { + case base.InternalKeyKindRangeDelete, + base.InternalKeyKindRangeKeyDelete: + fmt.Fprintf(&buf, "-%s", v) + case base.InternalKeyKindSet, + base.InternalKeyKindMerge: + fmt.Fprintf(&buf, ": %s", v) + case base.InternalKeyKindRangeKeySet, base.InternalKeyKindRangeKeyUnset: + s, err := rangekey.Decode(base.MakeInternalKey(ukey, 0, kind), v, nil) + if err != nil { + return err.Error() + } + if kind == base.InternalKeyKindRangeKeySet { + fmt.Fprintf(&buf, "-%s: %s → %s", s.End, s.Keys[0].Suffix, s.Keys[0].Value) + } else { + fmt.Fprintf(&buf, "-%s: %s", s.End, s.Keys[0].Suffix) + } + case base.InternalKeyKindDelete, base.InternalKeyKindSingleDelete: + default: + fmt.Fprintf(&buf, ": %x", v) + } + fmt.Fprintln(&buf) + } + if err != nil { + fmt.Fprintf(&buf, "err: %s\n", err) + } + + s := buf.String() + buf.Reset() + require.NoError(t, b.Close()) + + diskFileNums = diskFileNums[:0] + return s + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +func collectCorpus(t *testing.T, fs *vfs.MemFS, name string) { + require.NoError(t, fs.RemoveAll("build")) + require.NoError(t, fs.MkdirAll("build", os.ModePerm)) + + var d *pebble.DB + var wc *WorkloadCollector + defer func() { + if d != nil { + require.NoError(t, d.Close()) + } + }() + datadriven.RunTest(t, filepath.Join("testdata", "corpus", name), func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "commit": + b := d.NewBatch() + if err := datatest.DefineBatch(td, b); err != nil { + return err.Error() + } + if err := b.Commit(nil); err != nil { + return err.Error() + } + return "" + case "flush": + require.NoError(t, d.Flush()) + return "" + case "list-files": + if d != nil { + d.TestOnlyWaitForCleaning() + } + return runListFiles(t, fs, td) + case "open": + wc = NewWorkloadCollector("build") + opts := &pebble.Options{ + Comparer: testkeys.Comparer, + DisableAutomaticCompactions: true, + FormatMajorVersion: pebble.FormatRangeKeys, + FS: fs, + MaxManifestFileSize: 96, + } + setDefaultExperimentalOpts(opts) + wc.Attach(opts) + var err error + d, err = pebble.Open("build", opts) + require.NoError(t, err) + return "" + case "close": + err := d.Close() + require.NoError(t, err) + d = nil + return "" + case "start": + require.NoError(t, fs.MkdirAll(name, os.ModePerm)) + require.NotNil(t, wc) + wc.Start(fs, name) + require.NoError(t, d.Checkpoint(fs.PathJoin(name, "checkpoint"), pebble.WithFlushedWAL())) + return "started" + case "stat": + var buf bytes.Buffer + for _, arg := range td.CmdArgs { + fi, err := fs.Stat(arg.String()) + if err != nil { + fmt.Fprintf(&buf, "%s: %s\n", arg.String(), err) + continue + } + fmt.Fprintf(&buf, "%s:\n", arg.String()) + fmt.Fprintf(&buf, " size: %d\n", fi.Size()) + } + return buf.String() + case "stop": + wc.mu.Lock() + for wc.mu.tablesEnqueued != wc.mu.tablesCopied { + wc.mu.copyCond.Wait() + } + wc.mu.Unlock() + wc.Stop() + return "stopped" + case "tree": + return fs.String() + case "make-file": + dir := td.CmdArgs[0].String() + require.NoError(t, fs.MkdirAll(dir, os.ModePerm)) + fT := td.CmdArgs[1].String() + filePath := fs.PathJoin(dir, td.CmdArgs[2].String()) + + if fT != "file" { + fileNumInt, err := strconv.Atoi(td.CmdArgs[2].String()) + require.NoError(t, err) + fileNum := base.FileNum(fileNumInt) + switch fT { + case "table": + filePath = base.MakeFilepath(fs, dir, base.FileTypeTable, fileNum.DiskFileNum()) + case "log": + filePath = base.MakeFilepath(fs, dir, base.FileTypeLog, fileNum.DiskFileNum()) + case "manifest": + filePath = base.MakeFilepath(fs, dir, base.FileTypeManifest, fileNum.DiskFileNum()) + } + } + f, err := fs.Create(filePath) + require.NoError(t, err) + b, err := hex.DecodeString(strings.ReplaceAll(td.Input, "\n", "")) + require.NoError(t, err) + _, err = f.Write(b) + require.NoError(t, err) + return "created" + case "find-workload-files": + var buf bytes.Buffer + dir := td.CmdArgs[0].String() + m, s, err := findWorkloadFiles(dir, fs) + + fmt.Fprintln(&buf, "manifests") + sort.Strings(m) + for _, elem := range m { + fmt.Fprintf(&buf, " %s\n", elem) + } + var res []string + for key := range s { + res = append(res, key.String()) + } + sort.Strings(res) + + fmt.Fprintln(&buf, "sstables") + for _, elem := range res { + fmt.Fprintf(&buf, " %s\n", elem) + } + fmt.Fprintln(&buf, "error") + if err != nil { + fmt.Fprintf(&buf, " %s\n", err.Error()) + } + return buf.String() + case "find-manifest-start": + var buf bytes.Buffer + dir := td.CmdArgs[0].String() + m, _, err := findWorkloadFiles(dir, fs) + sort.Strings(m) + require.NoError(t, err) + i, o, err := findManifestStart(dir, fs, m) + errString := "nil" + if err != nil { + errString = err.Error() + } + fmt.Fprintf(&buf, "index: %d, offset: %d, error: %s\n", i, o, errString) + return buf.String() + case "delete-all": + err := fs.RemoveAll(td.CmdArgs[0].String()) + if err != nil { + return err.Error() + } + return "" + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +func TestCollectCorpus(t *testing.T) { + fs := vfs.NewMem() + datadriven.Walk(t, "testdata/corpus", func(t *testing.T, path string) { + collectCorpus(t, fs, filepath.Base(path)) + fs = vfs.NewMem() + }) +} + +func runListFiles(t *testing.T, fs vfs.FS, td *datadriven.TestData) string { + var buf bytes.Buffer + for _, arg := range td.CmdArgs { + listFiles(t, fs, &buf, arg.String()) + } + return buf.String() +} + +func TestBenchmarkString(t *testing.T) { + m := Metrics{ + Final: &pebble.Metrics{}, + EstimatedDebt: SampledMetric{samples: []sample{{value: 5 << 25}}}, + PaceDuration: time.Second / 4, + QuiesceDuration: time.Second / 2, + ReadAmp: SampledMetric{samples: []sample{{value: 10}}}, + TombstoneCount: SampledMetric{samples: []sample{{value: 295}}}, + TotalSize: SampledMetric{samples: []sample{{value: 5 << 30}}}, + TotalWriteAmp: 5.6, + WorkloadDuration: time.Second, + WriteBytes: 30 * (1 << 20), + WriteStalls: map[string]int{"memtable": 1, "L0": 2}, + WriteStallsDuration: map[string]time.Duration{"memtable": time.Minute, "L0": time.Hour}, + } + m.Ingest.BytesIntoL0 = 5 << 20 + m.Ingest.BytesWeightedByLevel = 9 << 20 + + var buf bytes.Buffer + require.NoError(t, m.WriteBenchmarkString("tpcc", &buf)) + require.Equal(t, strings.TrimSpace(` +BenchmarkBenchmarkReplay/tpcc/CompactionCounts 1 0 compactions 0 default 0 delete 0 elision 0 move 0 read 0 rewrite 0 multilevel +BenchmarkBenchmarkReplay/tpcc/DatabaseSize/mean 1 5.36870912e+09 bytes +BenchmarkBenchmarkReplay/tpcc/DatabaseSize/max 1 5.36870912e+09 bytes +BenchmarkBenchmarkReplay/tpcc/DurationWorkload 1 1 sec/op +BenchmarkBenchmarkReplay/tpcc/DurationQuiescing 1 0.5 sec/op +BenchmarkBenchmarkReplay/tpcc/DurationPaceDelay 1 0.25 sec/op +BenchmarkBenchmarkReplay/tpcc/EstimatedDebt/mean 1 1.6777216e+08 bytes +BenchmarkBenchmarkReplay/tpcc/EstimatedDebt/max 1 1.6777216e+08 bytes +BenchmarkBenchmarkReplay/tpcc/FlushUtilization 1 0 util +BenchmarkBenchmarkReplay/tpcc/IngestedIntoL0 1 5.24288e+06 bytes +BenchmarkBenchmarkReplay/tpcc/IngestWeightedByLevel 1 9.437184e+06 bytes +BenchmarkBenchmarkReplay/tpcc/ReadAmp/mean 1 10 files +BenchmarkBenchmarkReplay/tpcc/ReadAmp/max 1 10 files +BenchmarkBenchmarkReplay/tpcc/TombstoneCount/mean 1 295 tombstones +BenchmarkBenchmarkReplay/tpcc/TombstoneCount/max 1 295 tombstones +BenchmarkBenchmarkReplay/tpcc/Throughput 1 2.097152e+07 B/s +BenchmarkBenchmarkReplay/tpcc/WriteAmp 1 5.6 wamp +BenchmarkBenchmarkReplay/tpcc/WriteStall/L0 1 2 stalls 3600 stallsec/op +BenchmarkBenchmarkReplay/tpcc/WriteStall/memtable 1 1 stalls 60 stallsec/op`), + strings.TrimSpace(buf.String())) +} + +func listFiles(t *testing.T, fs vfs.FS, w io.Writer, name string) { + ls, err := fs.List(name) + if err != nil { + fmt.Fprintf(w, "%s: %s\n", name, err) + return + } + sort.Strings(ls) + fmt.Fprintf(w, "%s:\n", name) + for _, dirent := range ls { + fmt.Fprintf(w, " %s\n", dirent) + } +} + +// TestCompactionsQuiesce replays a workload that produces a nontrivial number of +// compactions several times. It's intended to exercise Waits termination, which +// is dependent on compactions quiescing. +func TestCompactionsQuiesce(t *testing.T) { + const replayCount = 1 + workloadFS := getHeavyWorkload(t) + fs := vfs.NewMem() + var done [replayCount]atomic.Bool + for i := 0; i < replayCount; i++ { + func(i int) { + runDir := fmt.Sprintf("run%d", i) + require.NoError(t, fs.MkdirAll(runDir, os.ModePerm)) + r := Runner{ + RunDir: runDir, + WorkloadFS: workloadFS, + WorkloadPath: "workload", + Pacer: Unpaced{}, + Opts: &pebble.Options{ + Comparer: testkeys.Comparer, + FS: fs, + FormatMajorVersion: pebble.FormatNewest, + LBaseMaxBytes: 1, + }, + } + r.Opts.Experimental.LevelMultiplier = 2 + require.NoError(t, r.Run(context.Background())) + defer r.Close() + + var m Metrics + var err error + go func() { + m, err = r.Wait() + done[i].Store(true) + }() + + wait := 30 * time.Second + if invariants.Enabled { + wait = time.Minute + if invariants.RaceEnabled { + wait = 5 * time.Minute + } + } + + // The above call to [Wait] should eventually return. [Wait] blocks + // until the workload has replayed AND compactions have quiesced. A + // bug in either could prevent [Wait] from ever returning. + require.Eventually(t, func() bool { return done[i].Load() }, + wait, time.Millisecond, "(*replay.Runner).Wait didn't terminate") + require.NoError(t, err) + // Require at least 5 compactions. + require.Greater(t, m.Final.Compact.Count, int64(5)) + require.Equal(t, int64(0), m.Final.Compact.NumInProgress) + for l := 0; l < len(m.Final.Levels)-1; l++ { + require.Less(t, m.Final.Levels[l].Score, 1.0) + } + }(i) + } +} + +// getHeavyWorkload returns a FS containing a workload in the `workload` +// directory that flushes enough randomly generated keys that replaying it +// should generate a non-trivial number of compactions. +func getHeavyWorkload(t *testing.T) vfs.FS { + heavyWorkload.Once.Do(func() { + t.Run("buildHeavyWorkload", func(t *testing.T) { + heavyWorkload.fs = buildHeavyWorkload(t) + }) + }) + return heavyWorkload.fs +} + +var heavyWorkload struct { + sync.Once + fs vfs.FS +} + +func buildHeavyWorkload(t *testing.T) vfs.FS { + o := &pebble.Options{ + Comparer: testkeys.Comparer, + FS: vfs.NewMem(), + FormatMajorVersion: pebble.FormatNewest, + } + wc := NewWorkloadCollector("") + wc.Attach(o) + d, err := pebble.Open("", o) + require.NoError(t, err) + + destFS := vfs.NewMem() + require.NoError(t, destFS.MkdirAll("workload", os.ModePerm)) + wc.Start(destFS, "workload") + + ks := testkeys.Alpha(5) + var bufKey = make([]byte, ks.MaxLen()) + var bufVal [512]byte + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < 100; i++ { + b := d.NewBatch() + for j := 0; j < 1000; j++ { + rng.Read(bufVal[:]) + n := testkeys.WriteKey(bufKey[:], ks, rng.Int63n(ks.Count())) + require.NoError(t, b.Set(bufKey[:n], bufVal[:], pebble.NoSync)) + } + require.NoError(t, b.Commit(pebble.NoSync)) + require.NoError(t, d.Flush()) + } + wc.WaitAndStop() + + defer d.Close() + return destFS +} diff --git a/pebble/replay/sampled_metric.go b/pebble/replay/sampled_metric.go new file mode 100644 index 0000000..5edcbe1 --- /dev/null +++ b/pebble/replay/sampled_metric.go @@ -0,0 +1,135 @@ +// 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 replay + +import ( + "math" + "time" + + "github.com/guptarohit/asciigraph" +) + +// SampledMetric holds a metric that is sampled at various points of workload +// replay. Samples are collected when a new step in the workload is applied to +// the database, and whenever a compaction completes. +type SampledMetric struct { + samples []sample + first time.Time +} + +type sample struct { + since time.Duration + value int64 +} + +func (m *SampledMetric) record(v int64) { + if m.first.IsZero() { + m.first = time.Now() + } + m.samples = append(m.samples, sample{ + since: time.Since(m.first), + value: v, + }) +} + +// Plot returns an ASCII graph plot of the metric over time, with the provided +// width and height determining the size of the graph and the number of representable discrete x and y +// points. All values are first +// multiplied by the provided scale parameter before graphing. +func (m *SampledMetric) Plot(width, height int, scale float64) string { + values := m.Values(width) + for i := range values { + values[i] *= scale + } + return asciigraph.Plot(values, asciigraph.Height(height)) +} + +// PlotIncreasingPerSec returns an ASCII graph plot of the increasing delta of a +// metric over time, per-second. The provided width and height determine the +// size of the graph and the number of representable discrete x and y points. +// All deltas are multiplied by the provided scale parameter and scaled to +// per-second before graphing. +func (m *SampledMetric) PlotIncreasingPerSec(width, height int, scale float64) string { + bucketDur, values := m.values(width) + deltas := make([]float64, width) + for i := range values { + if i == 0 { + deltas[i] = (values[i] * scale) / bucketDur.Seconds() + } else if values[i] > values[i-1] { + deltas[i] = (values[i] - values[i-1]) * scale / bucketDur.Seconds() + } + } + return asciigraph.Plot(deltas, asciigraph.Height(height)) +} + +// Mean calculates the mean value of the metric. +func (m *SampledMetric) Mean() float64 { + var sum float64 + if len(m.samples) == 0 { + return 0.0 + } + for _, s := range m.samples { + sum += float64(s.value) + } + return sum / float64(len(m.samples)) +} + +// Min calculates the mininum value of the metric. +func (m *SampledMetric) Min() int64 { + min := int64(math.MaxInt64) + for _, s := range m.samples { + if min > s.value { + min = s.value + } + } + return min +} + +// Max calculates the maximum value of the metric. +func (m *SampledMetric) Max() int64 { + var max int64 + for _, s := range m.samples { + if max < s.value { + max = s.value + } + } + return max +} + +// Values returns the values of the metric, distributed across n discrete +// buckets that are equally spaced over time. If multiple values fall within a +// bucket, the latest recorded value is used. If no values fall within a bucket, +// the next recorded value is used. +func (m *SampledMetric) Values(n int) []float64 { + _, values := m.values(n) + return values +} + +func (m *SampledMetric) values(buckets int) (bucketDur time.Duration, values []float64) { + if len(m.samples) == 0 || buckets < 1 { + return bucketDur, nil + } + + values = make([]float64, buckets) + totalDur := m.samples[len(m.samples)-1].since + bucketDur = totalDur / time.Duration(buckets) + + for i, b := 0, 0; i < len(m.samples); i++ { + // Fill any buckets that precede this value with the previous value. + bi := int(m.samples[i].since / bucketDur) + if bi == buckets { + bi = buckets - 1 + } + if b < bi { + b++ + for ; b < bi; b++ { + values[b] = float64(m.samples[i].value) + } + } + values[bi] = float64(m.samples[i].value) + b = bi + } + return bucketDur, values +} diff --git a/pebble/replay/sampled_metric_test.go b/pebble/replay/sampled_metric_test.go new file mode 100644 index 0000000..c4c636f --- /dev/null +++ b/pebble/replay/sampled_metric_test.go @@ -0,0 +1,75 @@ +// 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 replay + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/stretchr/testify/require" +) + +func TestSampledMetric(t *testing.T) { + var m SampledMetric + var buf bytes.Buffer + datadriven.RunTest(t, "testdata/sampled_metric", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "init": + m = SampledMetric{samples: m.samples[:0]} + var cumDur time.Duration + for _, line := range strings.Split(td.Input, "\n") { + fields := strings.Fields(line) + v, err := strconv.ParseInt(fields[0], 10, 64) + require.NoError(t, err) + dur, err := time.ParseDuration(fields[1]) + require.NoError(t, err) + cumDur += dur + m.samples = append(m.samples, sample{ + value: v, + since: cumDur, + }) + } + return "" + case "values": + buf.Reset() + var width int + td.ScanArgs(t, "width", &width) + for i, v := range m.Values(width) { + if i > 0 { + fmt.Fprint(&buf, " ") + } + fmt.Fprintf(&buf, "%.1f", v) + } + return buf.String() + case "plot": + var width, height int + var scaleStr string + td.ScanArgs(t, "width", &width) + td.ScanArgs(t, "height", &height) + td.ScanArgs(t, "scale", &scaleStr) + var scale float64 + _, err := fmt.Sscanf(scaleStr, "%f", &scale) + require.NoError(t, err) + return m.Plot(width, height, scale) + case "plot-increasing-per-sec": + var width, height int + var scaleStr string + td.ScanArgs(t, "width", &width) + td.ScanArgs(t, "height", &height) + td.ScanArgs(t, "scale", &scaleStr) + var scale float64 + _, err := fmt.Sscanf(scaleStr, "%f", &scale) + require.NoError(t, err) + return m.PlotIncreasingPerSec(width, height, scale) + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} diff --git a/pebble/replay/testdata/collect/clean_before_copy b/pebble/replay/testdata/collect/clean_before_copy new file mode 100644 index 0000000..5af61dc --- /dev/null +++ b/pebble/replay/testdata/collect/clean_before_copy @@ -0,0 +1,40 @@ +# This tests a race between Clean and the workload collector. It creates an +# sstable during a flush, and then immediately Cleans the file. The workload +# collector's Cleaner must delay the cleaning of the file until it's been copied +# and then delete it. + +start +---- + +create-manifest filenum=000001 +---- + +flush +000002 +---- +created src/000002.sst +[JOB 0] flushed 1 memtable (100B) to L0 [000002] (10KB), in 0.1s (0.1s total), output rate 100KB/s + +clean +src/000002.sst +---- + +wait +---- +dst: + 000002.sst + MANIFEST-000001 + +# The file should now be both removed from src/ and a copy should be present in +# dst/. + +ls src dst +---- +src: + MANIFEST-000001 +dst: + 000002.sst + MANIFEST-000001 + +stop +---- diff --git a/pebble/replay/testdata/collect/copy_before_clean b/pebble/replay/testdata/collect/copy_before_clean new file mode 100644 index 0000000..00b3325 --- /dev/null +++ b/pebble/replay/testdata/collect/copy_before_clean @@ -0,0 +1,51 @@ +start +---- + +create-manifest filenum=000001 +---- + +flush +000002 +---- +created src/000002.sst +[JOB 0] flushed 1 memtable (100B) to L0 [000002] (10KB), in 0.1s (0.1s total), output rate 100KB/s + +# Wait for 000002.sst to be copied. + +wait +---- +dst: + 000002.sst + MANIFEST-000001 + +# The file 000002.sst should exist in both src and dst. + +ls src dst +---- +src: + 000002.sst + MANIFEST-000001 +dst: + 000002.sst + MANIFEST-000001 + +cmp-files src/000002.sst dst/000002.sst +---- +equal + +# Now that the file has been copied, a clean should immediately delete it. + +clean +src/000002.sst +---- + +ls src dst +---- +src: + MANIFEST-000001 +dst: + 000002.sst + MANIFEST-000001 + +stop +---- diff --git a/pebble/replay/testdata/collect/manifest_copying b/pebble/replay/testdata/collect/manifest_copying new file mode 100644 index 0000000..50c71e6 --- /dev/null +++ b/pebble/replay/testdata/collect/manifest_copying @@ -0,0 +1,81 @@ +# When collection begins, it should start from the most recent manifest. + +create-manifest filenum=000001 +---- + +create-manifest filenum=000002 +---- + +# Since collection hasn't started yet, cleaning the old manifest should +# immediately remove it. + +clean +src/MANIFEST-000001 +---- + +ls src +---- +src: + MANIFEST-000002 + +start +---- + +stat src/MANIFEST-000002 +---- +src/MANIFEST-000002: + size: 100 + +flush +000003 +---- +created src/000003.sst +[JOB 0] flushed 1 memtable (100B) to L0 [000003] (10KB), in 0.1s (0.1s total), output rate 100KB/s + +wait +---- +dst: + 000003.sst + MANIFEST-000002 + +# The new file should have a larger size than it did when we stat'd the src +# manifest because a version edit should've been appended by the flush, and +# copied while copying the flushed sstables. + +stat dst/MANIFEST-000002 +---- +dst/MANIFEST-000002: + size: 125 + +cmp-files src/MANIFEST-000002 dst/MANIFEST-000002 +---- +equal + +# Test a manifest rotation. + +create-manifest filenum=000004 +---- + +flush +000005 +000006 +---- +created src/000005.sst +created src/000006.sst +[JOB 0] flushed 1 memtable (100B) to L0 [000005 000006] (20KB), in 0.1s (0.1s total), output rate 200KB/s + +wait +---- +dst: + 000003.sst + 000005.sst + 000006.sst + MANIFEST-000002 + MANIFEST-000004 + +cmp-files src/MANIFEST-000004 dst/MANIFEST-000004 +---- +equal + +stop +---- diff --git a/pebble/replay/testdata/collect/start_stop b/pebble/replay/testdata/collect/start_stop new file mode 100644 index 0000000..72d8f24 --- /dev/null +++ b/pebble/replay/testdata/collect/start_stop @@ -0,0 +1,98 @@ +# This test exercises starting and stopping the collector twice. + +create-manifest filenum=000001 +---- + +start +---- + +ingest +000002 +000003 +000004 +---- +created src/000002.sst +created src/000003.sst +created src/000004.sst +[JOB 0] ingested L0:000002 (10KB), L0:000003 (10KB), L0:000004 (10KB) + + +wait +---- +dst: + 000002.sst + 000003.sst + 000004.sst + MANIFEST-000001 + +clean +src/000003.sst +---- + +stop +---- + +flush +000005 +000006 +---- +created src/000005.sst +created src/000006.sst +[JOB 0] flushed 1 memtable (100B) to L0 [000005 000006] (20KB), in 0.1s (0.1s total), output rate 200KB/s + +# dst/ should now have the original insgested files (00000{2-4}.sst) and the +# manifest, but not the more-recently flushed files (00000{5-6}.sst). src/ +# should not have 000003.sst, because it was cleaned (and collected). + +ls src dst +---- +src: + 000002.sst + 000004.sst + 000005.sst + 000006.sst + MANIFEST-000001 +dst: + 000002.sst + 000003.sst + 000004.sst + MANIFEST-000001 + +start +---- + +# Cleaning one of the files created by the flush while we were not collecting +# should result in its immediate removal. + +clean +src/000005.sst +---- + +ls src +---- +src: + 000002.sst + 000004.sst + 000006.sst + MANIFEST-000001 + +flush +000007 +000008 +---- +created src/000007.sst +created src/000008.sst +[JOB 0] flushed 1 memtable (100B) to L0 [000007 000008] (20KB), in 0.1s (0.1s total), output rate 200KB/s + +wait +---- +dst: + 000002.sst + 000003.sst + 000004.sst + 000007.sst + 000008.sst + MANIFEST-000001 + +stop +---- diff --git a/pebble/replay/testdata/corpus/findManifestStart b/pebble/replay/testdata/corpus/findManifestStart new file mode 100644 index 0000000..a0652eb --- /dev/null +++ b/pebble/replay/testdata/corpus/findManifestStart @@ -0,0 +1,128 @@ +make-file build manifest 1 +bf13d7161a00010114636f636b726f6163685f636f6d70617261746f7203 +0204009040740904000102020303f94e6fc7660401020b031a04bc716700 +0ca1e1a10111016989726c67632d0001e00200000000001ef089fd0b5382 +526096000188001729f6a6a778912009010124000000000003ca700605d7 +fff39b060167000d94ed94011ef089fd0b5382526d73800188001729f6a6 +ab521a000901522b00000000001ef089fd0b538252fac0000188001729f6 +a6d69b723809019b2b0000000000d2569b570605d7fff39b060167000ef7 +dd84011ef089fd0b53825301c1800188001729f6a6d8e47bc00901a52b00 +000000001ef089fd0b538253a9e0800188001729f6a70c12ea6809011a2c +0000000000a5579a580605d7fff39b060167000fb4cc89011ef089fd0b53 +8253bb46000188001729f6a7117960900901242c00000000001ef089fd0b +5382542d14800188001729f6a73411420809017f2c0000000000a458ff58 +0605d7fff39b0601670010b48e8b011ef089fd0b5382543eeb8001880017 +29f6a73999328009018c2c00000000001ef089fd0b538254f25a00018800 +1729f6a7705567800901092d00000000008c59895a0605d8fff39b060167 +0011dc8e88011ef089fd0b5382550459000188001729f6a775e12c700901 +172d00000000001ef089fd0b538255b420000188001729f6a7ab512e7809 +01832d0000000000975a835b0605d8fff39b0601670012c6bf82011ef089 +fd0b538255c334000188001729f6a7b00bf7680901902d00000000001ef0 +89fd0b53825683a9000188001729f6a7ead329700901002e000000000090 +5b805c0605d8fff39b0601670013ff9d91011ef089fd0b5382568d0d0001 +88001729f6a7edbe201809010a2e00000000001ef089fd0b5382573b6500 +0188001729f6a822d807280901b42e00000000008a5cb45d0605d8fff39b +0601670014c4ff84011ef089fd0b53825747ac000188001729f6a826a109 +900901c12e00000000001ef089fd0b5382580a +---- +created + +find-manifest-start build +---- +index: 0, offset: 0, error: nil + +open +---- + +list-files build +---- +build: + 000002.log + CURRENT + LOCK + MANIFEST-000001 + OPTIONS-000003 + marker.format-version.000007.008 + marker.manifest.000001.MANIFEST-000001 + +commit +set a a +set b b +set c c +---- + +flush +---- + +list-files build +---- +build: + 000002.log + 000004.log + 000005.sst + CURRENT + LOCK + MANIFEST-000001 + OPTIONS-000003 + marker.format-version.000007.008 + marker.manifest.000001.MANIFEST-000001 + + +close +---- + +open +---- + +list-files build +---- +build: + 000005.sst + 000006.log + CURRENT + LOCK + MANIFEST-000001 + MANIFEST-000007 + OPTIONS-000008 + marker.format-version.000007.008 + marker.manifest.000002.MANIFEST-000007 + +delete-all build/MANIFEST-000007 +---- + +find-manifest-start build +---- +index: 0, offset: 0, error: nil + +make-file build manifest 7 +bf13d7161a00010114636f636b726f6163685f636f6d70617261746f7203 +0204009040740904000102020303f94e6fc7660401020b031a04bc716700 +0ca1e1a10111016989726c67632d0001e00200000000001ef089fd0b5382 +526096000188001729f6a6a778912009010124000000000003ca700605d7 +fff39b060167000d94ed94011ef089fd0b5382526d73800188001729f6a6 +ab521a000901522b00000000001ef089fd0b538252fac0000188001729f6 +a6d69b723809019b2b0000000000d2569b570605d7fff39b060167000ef7 +dd84011ef089fd0b53825301c1800188001729f6a6d8e47bc00901a52b00 +000000001ef089fd0b538253a9e0800188001729f6a70c12ea6809011a2c +0000000000a5579a580605d7fff39b060167000fb4cc89011ef089fd0b53 +8253bb46000188001729f6a7117960900901242c00000000001ef089fd0b +5382542d14800188001729f6a73411420809017f2c0000000000a458ff58 +0605d7fff39b0601670010b48e8b011ef089fd0b5382543eeb8001880017 +29f6a73999328009018c2c00000000001ef089fd0b538254f25a00018800 +1729f6a7705567800901092d00000000008c59895a0605d8fff39b060167 +0011dc8e88011ef089fd0b5382550459000188001729f6a775e12c700901 +172d00000000001ef089fd0b538255b420000188001729f6a7ab512e7809 +01832d0000000000975a835b0605d8fff39b0601670012c6bf82011ef089 +fd0b538255c334000188001729f6a7b00bf7680901902d00000000001ef0 +89fd0b53825683a9000188001729f6a7ead329700901002e000000000090 +5b805c0605d8fff39b0601670013ff9d91011ef089fd0b5382568d0d0001 +88001729f6a7edbe201809010a2e00000000001ef089fd0b5382573b6500 +0188001729f6a822d807280901b42e00000000008a5cb45d0605d8fff39b +0601670014c4ff84011ef089fd0b53825747ac000188001729f6a826a109 +900901c12e00000000001ef089fd0b5382580a +---- +created + +find-manifest-start build +---- +index: 1, offset: 739, error: nil diff --git a/pebble/replay/testdata/corpus/findWorkloadFiles b/pebble/replay/testdata/corpus/findWorkloadFiles new file mode 100644 index 0000000..30e5d02 --- /dev/null +++ b/pebble/replay/testdata/corpus/findWorkloadFiles @@ -0,0 +1,77 @@ +make-file capture log 1 +---- +created + +make-file capture log 2 +---- +created + +make-file capture log 3 +---- +created + +make-file capture table 1 +---- +created + +make-file capture table 4 +---- +created + +make-file capture file totally_not_relevant_file_000001.log +---- +created + +list-files capture +---- +capture: + 000001.log + 000001.sst + 000002.log + 000003.log + 000004.sst + totally_not_relevant_file_000001.log + +find-workload-files capture +---- +manifests +sstables +error + no manifests found + +make-file capture manifest 1 +---- +created + +make-file capture manifest 2 +---- +created + +make-file capture manifest 3 +---- +created + + +list-files capture +---- +capture: + 000001.log + 000001.sst + 000002.log + 000003.log + 000004.sst + MANIFEST-000001 + MANIFEST-000002 + MANIFEST-000003 + totally_not_relevant_file_000001.log + +find-workload-files capture +---- +manifests + MANIFEST-000001 + MANIFEST-000002 + MANIFEST-000003 +sstables + 000001 + 000004 +error diff --git a/pebble/replay/testdata/corpus/high_read_amp b/pebble/replay/testdata/corpus/high_read_amp new file mode 100644 index 0000000..d05b6f1 --- /dev/null +++ b/pebble/replay/testdata/corpus/high_read_amp @@ -0,0 +1,121 @@ +open +---- + +list-files build +---- +build: + 000002.log + CURRENT + LOCK + MANIFEST-000001 + OPTIONS-000003 + marker.format-version.000007.008 + marker.manifest.000001.MANIFEST-000001 + +commit +set a a +set b b +set c c +set d d +set de d +set e e +set ed e +set f f +set fe f +set g g +set ge g +set h h +set he h +set i i +set ie i +set j j +set k k +set l l +set m m +set n n +set o o +set p p +set q q +set r r +set s s +set t t +set u u +set v v +set w w +set x x +set y y +set z z +---- + +flush +---- + +commit +set c c +---- + +flush +---- + + +commit +set a a +---- + +flush +---- + +list-files build +---- +build: + 000005.sst + 000006.log + 000007.sst + 000009.log + 000010.sst + CURRENT + LOCK + MANIFEST-000008 + MANIFEST-000011 + OPTIONS-000003 + marker.format-version.000007.008 + marker.manifest.000003.MANIFEST-000011 + +start +---- +started + +list-files high_read_amp/checkpoint +---- +high_read_amp/checkpoint: + 000005.sst + 000007.sst + 000009.log + 000010.sst + MANIFEST-000011 + OPTIONS-000003 + marker.format-version.000001.008 + marker.manifest.000001.MANIFEST-000011 + +commit +set d d +set e e +set f f +set i i +set h h +set g g +---- + +flush +---- + +stop +---- +stopped + +list-files high_read_amp +---- +high_read_amp: + 000013.sst + MANIFEST-000011 + checkpoint diff --git a/pebble/replay/testdata/corpus/simple b/pebble/replay/testdata/corpus/simple new file mode 100644 index 0000000..8e61209 --- /dev/null +++ b/pebble/replay/testdata/corpus/simple @@ -0,0 +1,87 @@ +open +---- + +list-files build +---- +build: + 000002.log + CURRENT + LOCK + MANIFEST-000001 + OPTIONS-000003 + marker.format-version.000007.008 + marker.manifest.000001.MANIFEST-000001 + +commit +set a a +set b b +set c c +---- + +flush +---- + +list-files build +---- +build: + 000002.log + 000004.log + 000005.sst + CURRENT + LOCK + MANIFEST-000001 + OPTIONS-000003 + marker.format-version.000007.008 + marker.manifest.000001.MANIFEST-000001 + +start +---- +started + +list-files simple +---- +simple: + checkpoint + +list-files simple/checkpoint +---- +simple/checkpoint: + 000004.log + 000005.sst + MANIFEST-000001 + OPTIONS-000003 + marker.format-version.000001.008 + marker.manifest.000001.MANIFEST-000001 + +commit +set d d +set e e +set f f +set i i +set h h +set g g +---- + +flush +---- + +stop +---- +stopped + +list-files simple +---- +simple: + 000007.sst + MANIFEST-000001 + MANIFEST-000008 + checkpoint + +stat simple/MANIFEST-000001 simple/MANIFEST-000008 simple/000007.sst +---- +simple/MANIFEST-000001: + size: 98 +simple/MANIFEST-000008: + size: 122 +simple/000007.sst: + size: 686 diff --git a/pebble/replay/testdata/flushed_sstable_keys b/pebble/replay/testdata/flushed_sstable_keys new file mode 100644 index 0000000..df90927 --- /dev/null +++ b/pebble/replay/testdata/flushed_sstable_keys @@ -0,0 +1,65 @@ +commit +set a a +set b b +set c c +---- + +flush +---- +a.SET: a +b.SET: b +c.SET: c + +# Test that the keys in the batch are in the same order they were originally +# committed, not sorted by user key. + +commit +set c c +set b b +set a a +---- + +flush +---- +c.SET: c +b.SET: b +a.SET: a + +# Test that the keys in the batch are in the same order they were originally +# committed, not sorted by user key. + +commit +set c c +del b +del-range d f +singledel a +---- + +flush +---- +c.SET: c +b.DEL +d.RANGEDEL-f +a.SINGLEDEL + +commit +set x foo +range-key-del a z +range-key-unset g h @3 +range-key-set l m @1 foo +set a bar +del y +---- + +flush +---- +x.SET: foo +a.RANGEKEYDEL-g +g.RANGEKEYDEL-h +h.RANGEKEYDEL-l +l.RANGEKEYDEL-m +m.RANGEKEYDEL-z +g.RANGEKEYUNSET-h: @3 +l.RANGEKEYSET-m: @1 → foo +a.SET: bar +y.DEL diff --git a/pebble/replay/testdata/replay b/pebble/replay/testdata/replay new file mode 100644 index 0000000..b89d4ba --- /dev/null +++ b/pebble/replay/testdata/replay @@ -0,0 +1,108 @@ +corpus simple +---- + +tree +---- + / + build/ + 89 000004.log + 658 000005.sst + 49 000006.log + 686 000007.sst + 16 CURRENT + 0 LOCK + 98 MANIFEST-000001 + 122 MANIFEST-000008 + 1189 OPTIONS-000003 + 0 marker.format-version.000007.008 + 0 marker.manifest.000002.MANIFEST-000008 + simple/ + 686 000007.sst + 98 MANIFEST-000001 + 122 MANIFEST-000008 + checkpoint/ + 25 000004.log + 658 000005.sst + 98 MANIFEST-000001 + 1189 OPTIONS-000003 + 0 marker.format-version.000001.008 + 0 marker.manifest.000001.MANIFEST-000001 + +cat build/OPTIONS-000003 +---- +---- +[Version] + pebble_version=0.1 + +[Options] + bytes_per_sync=524288 + cache_size=8388608 + cleaner=replay.WorkloadCollector("delete") + compaction_debt_concurrency=1073741824 + comparer=pebble.internal.testkeys + disable_wal=false + flush_delay_delete_range=0s + flush_delay_range_key=0s + flush_split_bytes=4194304 + format_major_version=8 + l0_compaction_concurrency=10 + l0_compaction_file_threshold=500 + l0_compaction_threshold=4 + l0_stop_writes_threshold=12 + lbase_max_bytes=67108864 + max_concurrent_compactions=1 + max_manifest_file_size=96 + max_open_files=1000 + mem_table_size=4194304 + mem_table_stop_writes_threshold=2 + min_deletion_rate=0 + merger=pebble.concatenate + read_compaction_rate=16000 + read_sampling_multiplier=16 + strict_wal_tail=true + table_cache_shards=2 + table_property_collectors=[] + validate_on_ingest=false + wal_dir= + wal_bytes_per_sync=0 + max_writer_concurrency=0 + force_writer_parallelism=false + secondary_cache_size_bytes=0 + create_on_shared=0 + +[Level "0"] + block_restart_interval=16 + block_size=4096 + block_size_threshold=90 + compression=Snappy + filter_policy=none + filter_type=table + index_block_size=4096 + target_file_size=2097152 +---- +---- + +replay simple unpaced +---- + +wait +---- +replayed 42B in writes + +# NB: The file sizes are non-deterministic after replay (because compactions are +# nondeterministic). We don't `tree` here as a result. + +scan-keys +---- +a: a +b: b +c: c +d: d +e: e +f: f +g: g +h: h +i: i + +close +---- diff --git a/pebble/replay/testdata/replay_paced b/pebble/replay/testdata/replay_paced new file mode 100644 index 0000000..0dfad63 --- /dev/null +++ b/pebble/replay/testdata/replay_paced @@ -0,0 +1,80 @@ +corpus high_read_amp +---- + +tree +---- + / + build/ + 936 000005.sst + 632 000007.sst + 89 000009.log + 632 000010.sst + 200 000012.log + 686 000013.sst + 16 CURRENT + 0 LOCK + 122 MANIFEST-000008 + 205 MANIFEST-000011 + 1189 OPTIONS-000003 + 0 marker.format-version.000007.008 + 0 marker.manifest.000003.MANIFEST-000011 + high_read_amp/ + 686 000013.sst + 205 MANIFEST-000011 + checkpoint/ + 936 000005.sst + 632 000007.sst + 39 000009.log + 632 000010.sst + 157 MANIFEST-000011 + 1189 OPTIONS-000003 + 0 marker.format-version.000001.008 + 0 marker.manifest.000001.MANIFEST-000011 + +replay high_read_amp fixed 1 +---- + +wait-for-compactions +---- + +wait +---- +replayed 42B in writes + +scan-keys +---- +a: a +b: b +c: c +d: d +de: d +e: e +ed: e +f: f +fe: f +g: g +ge: g +h: h +he: h +i: i +ie: i +j: j +k: k +l: l +m: m +n: n +o: o +p: p +q: q +r: r +s: s +t: t +u: u +v: v +w: w +x: x +y: y +z: z + +close +---- diff --git a/pebble/replay/testdata/sampled_metric b/pebble/replay/testdata/sampled_metric new file mode 100644 index 0000000..ef4cb72 --- /dev/null +++ b/pebble/replay/testdata/sampled_metric @@ -0,0 +1,70 @@ +init +0 0ns +0 0ns +5 1ms +10 1ms +10 1ms +10 0ns +100 1s +100 1ms +1000 3s +1000 1s +1000 1s +1000 3s +5000 2s +5000 3s +5000 10s +---- + +values width=1 +---- +5000.0 + +values width=2 +---- +5000.0 5000.0 + +values width=3 +---- +1000.0 5000.0 5000.0 + +values width=4 +---- +1000.0 5000.0 5000.0 5000.0 + +values width=10 +---- +100.0 1000.0 1000.0 1000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 + +values width=20 +---- +100.0 1000.0 1000.0 1000.0 1000.0 1000.0 1000.0 1000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 5000.0 + +plot width=80 height=10 scale=0.1 +---- + 500 ┤ ╭──────────────────────────────────────────────── + 450 ┤ │ + 400 ┤ │ + 350 ┤ │ + 300 ┤ │ + 250 ┤ │ + 201 ┤ │ + 151 ┤ │ + 101 ┤ ╭──────────────────────────╯ + 51 ┤ │ + 1 ┼───╯ + + +plot-increasing-per-sec width=80 height=10 scale=0.1 +---- + 1333 ┤ ╭╮ + 1200 ┤ ││ + 1066 ┤ ││ + 933 ┤ ││ + 800 ┤ ││ + 667 ┤ ││ + 533 ┤ ││ + 400 ┤ ││ + 267 ┤ ╭╮ ││ + 133 ┤ ││ ││ + 0 ┼───╯╰─────────────────────────╯╰─────────────────────────────────────────────── diff --git a/pebble/replay/workload_capture.go b/pebble/replay/workload_capture.go new file mode 100644 index 0000000..743cf70 --- /dev/null +++ b/pebble/replay/workload_capture.go @@ -0,0 +1,438 @@ +// 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 replay + +import ( + "fmt" + "io" + "sync" + "sync/atomic" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/vfs" +) + +type workloadCaptureState uint8 + +const ( + obsolete = workloadCaptureState(1) << iota + readyForProcessing + capturedSuccessfully +) + +func (wcs workloadCaptureState) is(flag workloadCaptureState) bool { return wcs&flag != 0 } + +type manifestDetails struct { + sourceFilepath string + sourceFile vfs.File + + destFile vfs.File +} + +// WorkloadCollector is designed to capture workloads by handling manifest +// files, flushed SSTs and ingested SSTs. The collector hooks into the +// pebble.EventListener and pebble.Cleaner in order keep track of file states. +type WorkloadCollector struct { + mu struct { + sync.Mutex + fileState map[string]workloadCaptureState + // pendingSSTables holds a slice of file paths to sstables that need to + // be copied but haven't yet. The `copyFiles` goroutine grabs these + // files, and the flush and ingest event handlers append them. + pendingSSTables []string + // manifestIndex is an index into `manifests`, pointing to the + // manifest currently being copied. + manifestIndex int + // appending to manifests requires holding mu. Only the `copyFiles` + // goroutine is permitted to read or edit the struct contents once + // appended, so it does not need to hold mu while accessing the structs' + // fields. + manifests []*manifestDetails + + // The following condition variable and counts are used in tests to + // synchronize with the copying goroutine. + copyCond sync.Cond + tablesCopied int + tablesEnqueued int + } + // Stores the current manifest that is being used by the database. + curManifest atomic.Uint64 + // Stores whether the workload collector is enabled. + enabled atomic.Bool + buffer []byte + // config contains information that is only set on the creation of the + // WorkloadCollector. + config struct { + // srcFS and srcDir represent the location from which the workload collector + // collects the files from. + srcFS vfs.FS + srcDir string + // destFS and destDir represent the location to which the workload collector + // sends the files to. + destFS vfs.FS + destDir string + // cleaner stores the cleaner to use when files become obsolete and need to + // be cleaned. + cleaner base.Cleaner + } + copier struct { + sync.Cond + stop bool + done chan struct{} + } +} + +// NewWorkloadCollector is used externally to create a New WorkloadCollector. +func NewWorkloadCollector(srcDir string) *WorkloadCollector { + wc := &WorkloadCollector{} + wc.buffer = make([]byte, 1<<10 /* 1KB */) + wc.config.srcDir = srcDir + wc.mu.copyCond.L = &wc.mu.Mutex + wc.mu.fileState = make(map[string]workloadCaptureState) + wc.copier.Cond.L = &wc.mu.Mutex + return wc +} + +// Attach is used to set up the WorkloadCollector by attaching itself to +// pebble.Options EventListener and Cleaner. +func (w *WorkloadCollector) Attach(opts *pebble.Options) { + opts.AddEventListener(pebble.EventListener{ + FlushEnd: w.onFlushEnd, + ManifestCreated: w.onManifestCreated, + TableIngested: w.onTableIngest, + }) + + opts.EnsureDefaults() + // Replace the original Cleaner with the workload collector's implementation, + // which will invoke the original Cleaner, but only once the collector's copied + // what it needs. + c := cleaner{ + name: fmt.Sprintf("replay.WorkloadCollector(%q)", opts.Cleaner), + clean: w.clean, + } + w.config.cleaner, opts.Cleaner = opts.Cleaner, c + w.config.srcFS = opts.FS +} + +// enqueueCopyLocked enqueues the sstable with the provided filenum be copied in +// the background. Requires w.mu. +func (w *WorkloadCollector) enqueueCopyLocked(fileNum base.DiskFileNum) { + fileName := base.MakeFilename(base.FileTypeTable, fileNum) + w.mu.fileState[fileName] |= readyForProcessing + w.mu.pendingSSTables = append(w.mu.pendingSSTables, w.srcFilepath(fileName)) + w.mu.tablesEnqueued++ +} + +// cleanFile calls the cleaner on the specified path and removes the path from +// the fileState map. +func (w *WorkloadCollector) cleanFile(fileType base.FileType, path string) error { + err := w.config.cleaner.Clean(w.config.srcFS, fileType, path) + if err == nil { + w.mu.Lock() + delete(w.mu.fileState, w.config.srcFS.PathBase(path)) + w.mu.Unlock() + } + return err +} + +// clean deletes files only after they have been processed or are not required +// for the workload collection. +func (w *WorkloadCollector) clean(fs vfs.FS, fileType base.FileType, path string) error { + if !w.IsRunning() { + return w.cleanFile(fileType, path) + } + w.mu.Lock() + fileName := fs.PathBase(path) + if fileState, ok := w.mu.fileState[fileName]; !ok || fileState.is(capturedSuccessfully) { + // Delete the file if it has been captured or the file is not important + // to capture which means it can be deleted. + w.mu.Unlock() + return w.cleanFile(fileType, path) + } + w.mu.fileState[fileName] |= obsolete + w.mu.Unlock() + return nil +} + +// onTableIngest is attached to a pebble.DB as an EventListener.TableIngested +// func. It enqueues all ingested tables to be copied. +func (w *WorkloadCollector) onTableIngest(info pebble.TableIngestInfo) { + if !w.IsRunning() { + return + } + w.mu.Lock() + defer w.mu.Unlock() + for _, table := range info.Tables { + w.enqueueCopyLocked(table.FileNum.DiskFileNum()) + } + w.copier.Broadcast() +} + +// onFlushEnd is attached to a pebble.DB as an EventListener.FlushEnd func. It +// enqueues all flushed tables to be copied. +func (w *WorkloadCollector) onFlushEnd(info pebble.FlushInfo) { + if !w.IsRunning() { + return + } + w.mu.Lock() + defer w.mu.Unlock() + for _, table := range info.Output { + w.enqueueCopyLocked(table.FileNum.DiskFileNum()) + } + w.copier.Broadcast() +} + +// onManifestCreated is attached to a pebble.DB as an +// EventListener.ManifestCreated func. It records the the new manifest so that +// it's copied asynchronously in the background. +func (w *WorkloadCollector) onManifestCreated(info pebble.ManifestCreateInfo) { + w.curManifest.Store(uint64(info.FileNum)) + if !w.enabled.Load() { + return + } + w.mu.Lock() + defer w.mu.Unlock() + + // mark the manifest file as ready for processing to prevent it from being + // cleaned before we process it. + fileName := base.MakeFilename(base.FileTypeManifest, info.FileNum) + w.mu.fileState[fileName] |= readyForProcessing + w.mu.manifests = append(w.mu.manifests, &manifestDetails{ + sourceFilepath: info.Path, + }) +} + +// copyFiles is run in a separate goroutine, copying sstables and manifests. +func (w *WorkloadCollector) copyFiles() { + w.mu.Lock() + defer w.mu.Unlock() + // NB: This loop must hold w.mu at the beginning of each iteration. It may + // drop w.mu at times, but it must reacquire it before the next iteration. + for !w.copier.stop { + // The following performs the workload capture. It waits on a condition + // variable (fileListener) to let it know when new files are available to be + // collected. + if len(w.mu.pendingSSTables) == 0 { + w.copier.Wait() + } + // Grab the manifests to copy. + index := w.mu.manifestIndex + pendingManifests := w.mu.manifests[index:] + var pending []string + pending, w.mu.pendingSSTables = w.mu.pendingSSTables, nil + func() { + // Note the unusual lock order; Temporarily unlock the + // mutex, but re-acquire it before returning. + w.mu.Unlock() + defer w.mu.Lock() + + // Copy any updates to the manifests files. + w.copyManifests(index, pendingManifests) + // Copy the SSTables provided in pending. copySSTables takes + // ownership of the pending slice. + w.copySSTables(pending) + }() + + // This helps in tests; Tests can wait on the copyCond condition + // variable until the necessary bits have been copied. + w.mu.tablesCopied += len(pending) + w.mu.copyCond.Broadcast() + } + + for idx := range w.mu.manifests { + if f := w.mu.manifests[idx].sourceFile; f != nil { + if err := f.Close(); err != nil { + panic(err) + } + w.mu.manifests[idx].sourceFile = nil + } + if f := w.mu.manifests[idx].destFile; f != nil { + if err := f.Close(); err != nil { + panic(err) + } + w.mu.manifests[idx].destFile = nil + } + } + close(w.copier.done) +} + +// copyManifests copies any un-copied portions of the source manifests. +func (w *WorkloadCollector) copyManifests(startAtIndex int, manifests []*manifestDetails) { + destFS := w.config.destFS + + for index, manifest := range manifests { + if manifest.destFile == nil && manifest.sourceFile == nil { + // This is the first time we've read from this manifest, and we + // don't yet have open file descriptors for the src or dst files. It + // is safe to write to manifest.{destFile,sourceFile} without + // holding d.mu, because the copyFiles goroutine is the only + // goroutine that accesses the fields of the `manifestDetails` + // struct. + var err error + manifest.destFile, err = destFS.Create(w.destFilepath(destFS.PathBase(manifest.sourceFilepath))) + if err != nil { + panic(err) + } + manifest.sourceFile, err = w.config.srcFS.Open(manifest.sourceFilepath) + if err != nil { + panic(err) + } + } + + numBytesRead, err := io.CopyBuffer(manifest.destFile, manifest.sourceFile, w.buffer) + if err != nil { + panic(err) + } + + // Read 0 bytes from the current manifest and this is not the + // latest/newest manifest which means we have read its entirety. No new + // data will be written to it, because only the latest manifest may + // receive edits. Close the current source and destination files and + // move the manifest to start at the next index in w.mu.manifests. + if numBytesRead == 0 && index != len(manifests)-1 { + // Rotating the manifests so we can close the files. + if err := manifests[index].sourceFile.Close(); err != nil { + panic(err) + } + manifests[index].sourceFile = nil + if err := manifests[index].destFile.Close(); err != nil { + panic(err) + } + manifests[index].destFile = nil + w.mu.Lock() + w.mu.manifestIndex = startAtIndex + index + 1 + w.mu.Unlock() + } + } +} + +// copySSTables copies the provided sstables to the stored workload. If a file +// has already been marked as obsolete, then file will be cleaned by the +// w.config.cleaner after it is copied. The provided slice will be mutated and +// should not be used following the call to this function. +func (w *WorkloadCollector) copySSTables(pending []string) { + for _, filePath := range pending { + err := vfs.CopyAcrossFS(w.config.srcFS, + filePath, + w.config.destFS, + w.destFilepath(w.config.srcFS.PathBase(filePath))) + if err != nil { + panic(err) + } + } + + // Identify the subset of `pending` files that should now be cleaned. The + // WorkloadCollector intercepts Cleaner.Clean calls to defer cleaning until + // copying has completed. If Cleaner.Clean has already been invoked for any + // of the files that copied, we can now actually Clean them. + pendingClean := pending[:0] + w.mu.Lock() + for _, filePath := range pending { + fileName := w.config.srcFS.PathBase(filePath) + if w.mu.fileState[fileName].is(obsolete) { + pendingClean = append(pendingClean, filePath) + } else { + w.mu.fileState[fileName] |= capturedSuccessfully + } + } + w.mu.Unlock() + + for _, path := range pendingClean { + _ = w.cleanFile(base.FileTypeTable, path) + } +} + +// Start begins collecting a workload. All flushed and ingested sstables, plus +// corresponding manifests are copied to the provided destination path on the +// provided FS. +func (w *WorkloadCollector) Start(destFS vfs.FS, destPath string) { + w.mu.Lock() + defer w.mu.Unlock() + + // If the collector not is running then that means w.enabled == 0 so swap it + // to 1 and continue else return since it is already running. + if !w.enabled.CompareAndSwap(false, true) { + return + } + w.config.destFS = destFS + w.config.destDir = destPath + + // Initialize the tracked manifests to the database's current manifest, if + // the database has already started. Every database Open creates a new + // manifest. There are two cases: + // 1. The database has already been opened. Then `w.atomic.curManifest` + // contains the file number of the current manifest. We must initialize + // the w.mu.manifests slice to contain this first manifest. + // 2. The database has not yet been opened. Then `w.atomic.curManifest` is + // still zero. Once the associated database is opened, it'll invoke + // onManifestCreated which will handle enqueuing the manifest on + // `w.mu.manifests`. + fileNum := base.FileNum(w.curManifest.Load()) + if fileNum != 0 { + fileName := base.MakeFilename(base.FileTypeManifest, fileNum.DiskFileNum()) + w.mu.manifests = append(w.mu.manifests[:0], &manifestDetails{sourceFilepath: w.srcFilepath(fileName)}) + w.mu.fileState[fileName] |= readyForProcessing + } + + // Begin copying files asynchronously in the background. + w.copier.done = make(chan struct{}) + w.copier.stop = false + go w.copyFiles() +} + +// WaitAndStop waits for all enqueued sstables to be copied over, and then +// calls Stop. Gracefully ensures that all sstables referenced in the collected +// manifest's latest version edit will exist in the copy directory. +func (w *WorkloadCollector) WaitAndStop() { + w.mu.Lock() + for w.mu.tablesEnqueued != w.mu.tablesCopied { + w.mu.copyCond.Wait() + } + w.mu.Unlock() + w.Stop() +} + +// Stop stops collection of the workload. +func (w *WorkloadCollector) Stop() { + w.mu.Lock() + // If the collector is running then that means w.enabled == true so swap it to + // false and continue else return since it is not running. + if !w.enabled.CompareAndSwap(true, false) { + w.mu.Unlock() + return + } + w.copier.stop = true + w.copier.Broadcast() + w.mu.Unlock() + <-w.copier.done +} + +// IsRunning returns whether the WorkloadCollector is currently running. +func (w *WorkloadCollector) IsRunning() bool { + return w.enabled.Load() +} + +// srcFilepath returns the file path to the named file in the source directory +// on the source filesystem. +func (w *WorkloadCollector) srcFilepath(name string) string { + return w.config.srcFS.PathJoin(w.config.srcDir, name) +} + +// destFilepath returns the file path to the named file in the destination +// directory on the destination filesystem. +func (w *WorkloadCollector) destFilepath(name string) string { + return w.config.destFS.PathJoin(w.config.destDir, name) +} + +type cleaner struct { + name string + clean func(vfs.FS, base.FileType, string) error +} + +func (c cleaner) String() string { return c.name } +func (c cleaner) Clean(fs vfs.FS, fileType base.FileType, path string) error { + return c.clean(fs, fileType, path) +} diff --git a/pebble/replay/workload_capture_test.go b/pebble/replay/workload_capture_test.go new file mode 100644 index 0000000..4fafe71 --- /dev/null +++ b/pebble/replay/workload_capture_test.go @@ -0,0 +1,200 @@ +package replay + +import ( + "bytes" + "crypto/rand" + "fmt" + "io" + "strconv" + "strings" + "testing" + "time" + "unicode" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestWorkloadCollector(t *testing.T) { + const srcDir = `src` + const destDir = `dst` + datadriven.Walk(t, "testdata/collect", func(t *testing.T, path string) { + fs := vfs.NewMem() + require.NoError(t, fs.MkdirAll(srcDir, 0755)) + require.NoError(t, fs.MkdirAll(destDir, 0755)) + c := NewWorkloadCollector(srcDir) + o := &pebble.Options{FS: fs} + c.Attach(o) + var currentManifest vfs.File + var buf bytes.Buffer + defer func() { + if currentManifest != nil { + currentManifest.Close() + } + }() + datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string { + buf.Reset() + switch td.Cmd { + case "cmp-files": + if len(td.CmdArgs) != 2 { + return fmt.Sprintf("expected exactly 2 args, received %d", len(td.CmdArgs)) + } + b1 := readFile(t, fs, td.CmdArgs[0].String()) + b2 := readFile(t, fs, td.CmdArgs[1].String()) + if !bytes.Equal(b1, b2) { + return fmt.Sprintf("files are unequal: %s (%s) and %s (%s)", + td.CmdArgs[0].String(), humanize.Bytes.Uint64(uint64(len(b1))), + td.CmdArgs[1].String(), humanize.Bytes.Uint64(uint64(len(b2)))) + } + return "equal" + case "clean": + for _, path := range strings.Fields(td.Input) { + typ, _, ok := base.ParseFilename(fs, path) + require.True(t, ok) + require.NoError(t, o.Cleaner.Clean(fs, typ, path)) + } + return "" + case "create-manifest": + if currentManifest != nil { + require.NoError(t, currentManifest.Close()) + } + + var fileNum uint64 + var err error + td.ScanArgs(t, "filenum", &fileNum) + path := base.MakeFilepath(fs, srcDir, base.FileTypeManifest, base.DiskFileNum(fileNum)) + currentManifest, err = fs.Create(path) + require.NoError(t, err) + _, err = currentManifest.Write(randData(100)) + require.NoError(t, err) + + c.onManifestCreated(pebble.ManifestCreateInfo{ + Path: path, + FileNum: base.DiskFileNum(fileNum), + }) + return "" + case "flush": + flushInfo := pebble.FlushInfo{ + Done: true, + Input: 1, + Duration: 100 * time.Millisecond, + TotalDuration: 100 * time.Millisecond, + } + for _, line := range strings.Split(td.Input, "\n") { + if line == "" { + continue + } + + parts := strings.FieldsFunc(line, func(r rune) bool { return unicode.IsSpace(r) || r == ':' }) + tableInfo := pebble.TableInfo{Size: 10 << 10} + fileNum, err := strconv.ParseUint(parts[0], 10, 64) + require.NoError(t, err) + tableInfo.FileNum = base.FileNum(fileNum) + + p := writeFile(t, fs, srcDir, base.FileTypeTable, tableInfo.FileNum.DiskFileNum(), randData(int(tableInfo.Size))) + fmt.Fprintf(&buf, "created %s\n", p) + flushInfo.Output = append(flushInfo.Output, tableInfo) + + // Simulate a version edit applied to the current manifest. + _, err = currentManifest.Write(randData(25)) + require.NoError(t, err) + } + flushInfo.InputBytes = 100 // Determinism + fmt.Fprint(&buf, flushInfo.String()) + c.onFlushEnd(flushInfo) + return buf.String() + case "ingest": + ingestInfo := pebble.TableIngestInfo{} + for _, line := range strings.Split(td.Input, "\n") { + if line == "" { + continue + } + + parts := strings.FieldsFunc(line, func(r rune) bool { return unicode.IsSpace(r) || r == ':' }) + tableInfo := pebble.TableInfo{Size: 10 << 10} + fileNum, err := strconv.ParseUint(parts[0], 10, 64) + require.NoError(t, err) + tableInfo.FileNum = base.FileNum(fileNum) + + p := writeFile(t, fs, srcDir, base.FileTypeTable, tableInfo.FileNum.DiskFileNum(), randData(int(tableInfo.Size))) + fmt.Fprintf(&buf, "created %s\n", p) + ingestInfo.Tables = append(ingestInfo.Tables, struct { + pebble.TableInfo + Level int + }{Level: 0, TableInfo: tableInfo}) + + // Simulate a version edit applied to the current manifest. + _, err = currentManifest.Write(randData(25)) + require.NoError(t, err) + } + fmt.Fprint(&buf, ingestInfo.String()) + c.onTableIngest(ingestInfo) + return buf.String() + + case "ls": + return runListFiles(t, fs, td) + case "start": + c.Start(fs, destDir) + return "" + case "stat": + var buf bytes.Buffer + for _, arg := range td.CmdArgs { + fi, err := fs.Stat(arg.String()) + if err != nil { + fmt.Fprintf(&buf, "%s: %s\n", arg.String(), err) + continue + } + fmt.Fprintf(&buf, "%s:\n", arg.String()) + fmt.Fprintf(&buf, " size: %d\n", fi.Size()) + } + return buf.String() + case "stop": + c.Stop() + return "" + case "wait": + // Wait until all pending sstables have been copied, then list + // the files in the destination directory. + c.mu.Lock() + for c.mu.tablesEnqueued != c.mu.tablesCopied { + c.mu.copyCond.Wait() + } + c.mu.Unlock() + listFiles(t, fs, &buf, destDir) + return buf.String() + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) + }) +} + +func randData(byteCount int) []byte { + b := make([]byte, byteCount) + rand.Read(b) + return b +} + +func writeFile( + t *testing.T, fs vfs.FS, dir string, typ base.FileType, fileNum base.DiskFileNum, data []byte, +) string { + path := base.MakeFilepath(fs, dir, typ, fileNum) + f, err := fs.Create(path) + require.NoError(t, err) + _, err = f.Write(data) + require.NoError(t, err) + require.NoError(t, f.Close()) + return path +} + +func readFile(t *testing.T, fs vfs.FS, path string) []byte { + r, err := fs.Open(path) + require.NoError(t, err) + b, err := io.ReadAll(r) + require.NoError(t, err) + require.NoError(t, r.Close()) + return b +} diff --git a/pebble/scan_internal.go b/pebble/scan_internal.go new file mode 100644 index 0000000..62bb58e --- /dev/null +++ b/pebble/scan_internal.go @@ -0,0 +1,1016 @@ +// 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" + "github.com/cockroachdb/pebble/sstable" +) + +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) +} + +func (p *pointCollapsingIterator) SetContext(ctx context.Context) { + p.iter.SetContext(ctx) +} + +// 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 { + ctx context.Context + 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( + categoryAndQoS sstable.CategoryAndQoS, 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 + i.opts.IterOptions.CategoryAndQoS = categoryAndQoS + addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) { + li := &levels[levelsIndex] + rli := &rangeDelLevels[levelsIndex] + + li.init( + i.ctx, 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(i.ctx, 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() error { + // 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 { + return err + } + 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) + } + return nil +} + +// 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/pebble/scan_internal_test.go b/pebble/scan_internal_test.go new file mode 100644 index 0000000..2c3acbc --- /dev/null +++ b/pebble/scan_internal_test.go @@ -0,0 +1,566 @@ +// 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" + "math" + "strconv" + "strings" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/itertest" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestScanStatistics(t *testing.T) { + var d *DB + type scanInternalReader interface { + ScanStatistics( + ctx context.Context, + lower, upper []byte, + opts ScanStatisticsOptions, + ) (LSMKeyStatistics, error) + } + batches := map[string]*Batch{} + snaps := map[string]*Snapshot{} + ctx := context.TODO() + + getOpts := func() *Options { + opts := &Options{ + FS: vfs.NewMem(), + Logger: testLogger{t: t}, + Comparer: testkeys.Comparer, + FormatMajorVersion: FormatRangeKeys, + BlockPropertyCollectors: []func() BlockPropertyCollector{ + sstable.NewTestKeysBlockPropertyCollector, + }, + } + opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": remote.NewInMem(), + }) + opts.Experimental.CreateOnShared = remote.CreateOnSharedAll + opts.Experimental.CreateOnSharedLocator = "" + opts.DisableAutomaticCompactions = true + opts.EnsureDefaults() + opts.WithFSDefaults() + return opts + } + cleanup := func() (err error) { + for key, batch := range batches { + err = firstError(err, batch.Close()) + delete(batches, key) + } + for key, snap := range snaps { + err = firstError(err, snap.Close()) + delete(snaps, key) + } + if d != nil { + err = firstError(err, d.Close()) + d = nil + } + return err + } + defer cleanup() + + datadriven.RunTest(t, "testdata/scan_statistics", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "reset": + if err := cleanup(); err != nil { + t.Fatal(err) + return err.Error() + } + var err error + d, err = Open("", getOpts()) + require.NoError(t, err) + require.NoError(t, d.SetCreatorID(1)) + return "" + case "snapshot": + s := d.NewSnapshot() + var name string + td.ScanArgs(t, "name", &name) + snaps[name] = s + return "" + case "batch": + var name string + td.MaybeScanArgs(t, "name", &name) + commit := td.HasArg("commit") + b := d.NewIndexedBatch() + require.NoError(t, runBatchDefineCmd(td, b)) + var err error + if commit { + func() { + defer func() { + if r := recover(); r != nil { + err = errors.New(r.(string)) + } + }() + err = b.Commit(nil) + }() + } else if name != "" { + batches[name] = b + } + if err != nil { + return err.Error() + } + count := b.Count() + if commit { + return fmt.Sprintf("committed %d keys\n", count) + } + return fmt.Sprintf("wrote %d keys to batch %q\n", count, name) + case "compact": + if err := runCompactCmd(td, d); err != nil { + return err.Error() + } + return runLSMCmd(td, d) + case "flush": + err := d.Flush() + if err != nil { + return err.Error() + } + return "" + case "commit": + name := pluckStringCmdArg(td, "batch") + b := batches[name] + defer b.Close() + count := b.Count() + require.NoError(t, d.Apply(b, nil)) + delete(batches, name) + return fmt.Sprintf("committed %d keys\n", count) + case "scan-statistics": + var lower, upper []byte + var reader scanInternalReader = d + var b strings.Builder + var showSnapshotPinned = false + var keyKindsToDisplay []InternalKeyKind + var showLevels []string + + for _, arg := range td.CmdArgs { + switch arg.Key { + case "lower": + lower = []byte(arg.Vals[0]) + case "upper": + upper = []byte(arg.Vals[0]) + case "show-snapshot-pinned": + showSnapshotPinned = true + case "keys": + for _, key := range arg.Vals { + keyKindsToDisplay = append(keyKindsToDisplay, base.ParseKind(key)) + } + case "levels": + showLevels = append(showLevels, arg.Vals...) + default: + } + } + stats, err := reader.ScanStatistics(ctx, lower, upper, ScanStatisticsOptions{}) + if err != nil { + return err.Error() + } + + for _, level := range showLevels { + lvl, err := strconv.Atoi(level) + if err != nil || lvl >= numLevels { + return fmt.Sprintf("invalid level %s", level) + } + + fmt.Fprintf(&b, "Level %d:\n", lvl) + if showSnapshotPinned { + fmt.Fprintf(&b, " compaction pinned count: %d\n", stats.Levels[lvl].SnapshotPinnedKeys) + } + for _, kind := range keyKindsToDisplay { + fmt.Fprintf(&b, " %s key count: %d\n", kind.String(), stats.Levels[lvl].KindsCount[kind]) + if stats.Levels[lvl].LatestKindsCount[kind] > 0 { + fmt.Fprintf(&b, " %s latest count: %d\n", kind.String(), stats.Levels[lvl].LatestKindsCount[kind]) + } + } + } + + fmt.Fprintf(&b, "Aggregate:\n") + if showSnapshotPinned { + fmt.Fprintf(&b, " snapshot pinned count: %d\n", stats.Accumulated.SnapshotPinnedKeys) + } + for _, kind := range keyKindsToDisplay { + fmt.Fprintf(&b, " %s key count: %d\n", kind.String(), stats.Accumulated.KindsCount[kind]) + if stats.Accumulated.LatestKindsCount[kind] > 0 { + fmt.Fprintf(&b, " %s latest count: %d\n", kind.String(), stats.Accumulated.LatestKindsCount[kind]) + } + } + return b.String() + default: + return fmt.Sprintf("unknown command %q", td.Cmd) + } + }) +} + +func TestScanInternal(t *testing.T) { + var d *DB + type scanInternalReader interface { + ScanInternal( + ctx context.Context, + categoryAndQoS sstable.CategoryAndQoS, + 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 []keyspan.Key) error, + visitSharedFile func(sst *SharedSSTMeta) error, + ) error + } + batches := map[string]*Batch{} + snaps := map[string]*Snapshot{} + efos := map[string]*EventuallyFileOnlySnapshot{} + parseOpts := func(td *datadriven.TestData) (*Options, error) { + opts := &Options{ + FS: vfs.NewMem(), + Logger: testLogger{t: t}, + Comparer: testkeys.Comparer, + FormatMajorVersion: FormatVirtualSSTables, + BlockPropertyCollectors: []func() BlockPropertyCollector{ + sstable.NewTestKeysBlockPropertyCollector, + }, + } + opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ + "": remote.NewInMem(), + }) + opts.Experimental.CreateOnShared = remote.CreateOnSharedAll + opts.Experimental.CreateOnSharedLocator = "" + opts.DisableAutomaticCompactions = true + opts.EnsureDefaults() + opts.WithFSDefaults() + + for _, cmdArg := range td.CmdArgs { + switch cmdArg.Key { + case "format-major-version": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return nil, err + } + // Override the DB version. + opts.FormatMajorVersion = FormatMajorVersion(v) + case "block-size": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return nil, err + } + for i := range opts.Levels { + opts.Levels[i].BlockSize = v + } + case "index-block-size": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return nil, err + } + for i := range opts.Levels { + opts.Levels[i].IndexBlockSize = v + } + case "target-file-size": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return nil, err + } + for i := range opts.Levels { + opts.Levels[i].TargetFileSize = int64(v) + } + case "bloom-bits-per-key": + v, err := strconv.Atoi(cmdArg.Vals[0]) + if err != nil { + return nil, err + } + fp := bloom.FilterPolicy(v) + opts.Filters = map[string]FilterPolicy{fp.Name(): fp} + for i := range opts.Levels { + opts.Levels[i].FilterPolicy = fp + } + case "merger": + switch cmdArg.Vals[0] { + case "appender": + opts.Merger = base.DefaultMerger + default: + return nil, errors.Newf("unrecognized Merger %q\n", cmdArg.Vals[0]) + } + } + } + return opts, nil + } + cleanup := func() (err error) { + for key, batch := range batches { + err = firstError(err, batch.Close()) + delete(batches, key) + } + for key, snap := range snaps { + err = firstError(err, snap.Close()) + delete(snaps, key) + } + for key, es := range efos { + err = firstError(err, es.Close()) + delete(efos, key) + } + if d != nil { + err = firstError(err, d.Close()) + d = nil + } + return err + } + defer cleanup() + + datadriven.RunTest(t, "testdata/scan_internal", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + if err := cleanup(); err != nil { + return err.Error() + } + opts, err := parseOpts(td) + if err != nil { + return err.Error() + } + d, err = runDBDefineCmd(td, opts) + if err != nil { + return err.Error() + } + return runLSMCmd(td, d) + + case "reset": + if err := cleanup(); err != nil { + t.Fatal(err) + return err.Error() + } + opts, err := parseOpts(td) + if err != nil { + t.Fatal(err) + return err.Error() + } + + d, err = Open("", opts) + require.NoError(t, err) + require.NoError(t, d.SetCreatorID(1)) + return "" + case "snapshot": + s := d.NewSnapshot() + var name string + td.ScanArgs(t, "name", &name) + snaps[name] = s + return "" + case "wait-for-file-only-snapshot": + if len(td.CmdArgs) != 1 { + panic("insufficient args for file-only-snapshot command") + } + name := td.CmdArgs[0].Key + es := efos[name] + if err := es.WaitForFileOnlySnapshot(context.TODO(), 1*time.Millisecond); err != nil { + return err.Error() + } + return "ok" + case "file-only-snapshot": + if len(td.CmdArgs) != 1 { + panic("insufficient args for file-only-snapshot command") + } + name := td.CmdArgs[0].Key + var keyRanges []KeyRange + for _, line := range strings.Split(td.Input, "\n") { + fields := strings.Fields(line) + if len(fields) != 2 { + return "expected two fields for file-only snapshot KeyRanges" + } + kr := KeyRange{Start: []byte(fields[0]), End: []byte(fields[1])} + keyRanges = append(keyRanges, kr) + } + + s := d.NewEventuallyFileOnlySnapshot(keyRanges) + efos[name] = s + return "ok" + case "batch": + var name string + td.MaybeScanArgs(t, "name", &name) + commit := td.HasArg("commit") + ingest := td.HasArg("ingest") + b := d.NewIndexedBatch() + require.NoError(t, runBatchDefineCmd(td, b)) + var err error + if commit { + func() { + defer func() { + if r := recover(); r != nil { + err = errors.New(r.(string)) + } + }() + err = b.Commit(nil) + }() + } else if ingest { + points, rangeDels, rangeKeys := batchSort(b) + file, err := d.opts.FS.Create("temp0.sst") + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(file), d.opts.MakeWriterOptions(0, sstable.TableFormatPebblev4)) + for span := rangeDels.First(); span != nil; span = rangeDels.Next() { + require.NoError(t, w.DeleteRange(span.Start, span.End)) + } + rangeDels.Close() + for span := rangeKeys.First(); span != nil; span = rangeKeys.Next() { + keys := []keyspan.Key{} + for i := range span.Keys { + keys = append(keys, span.Keys[i]) + keys[i].Trailer = base.MakeTrailer(0, keys[i].Kind()) + } + keyspan.SortKeysByTrailer(&keys) + newSpan := &keyspan.Span{Start: span.Start, End: span.End, Keys: keys} + rangekey.Encode(newSpan, w.AddRangeKey) + } + rangeKeys.Close() + for key, val := points.First(); key != nil; key, val = points.Next() { + var value []byte + value, _, err = val.Value(value) + require.NoError(t, err) + require.NoError(t, w.Add(*key, value)) + } + points.Close() + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{"temp0.sst"})) + } else if name != "" { + batches[name] = b + } + if err != nil { + return err.Error() + } + count := b.Count() + if commit { + return fmt.Sprintf("committed %d keys\n", count) + } + return fmt.Sprintf("wrote %d keys to batch %q\n", count, name) + case "compact": + if err := runCompactCmd(td, d); err != nil { + return err.Error() + } + return runLSMCmd(td, d) + case "flush": + err := d.Flush() + if err != nil { + return err.Error() + } + return "" + case "lsm": + return runLSMCmd(td, d) + case "commit": + name := pluckStringCmdArg(td, "batch") + b := batches[name] + defer b.Close() + count := b.Count() + require.NoError(t, d.Apply(b, nil)) + delete(batches, name) + return fmt.Sprintf("committed %d keys\n", count) + case "scan-internal": + var lower, upper []byte + var reader scanInternalReader = d + var b strings.Builder + var fileVisitor func(sst *SharedSSTMeta) error + for _, arg := range td.CmdArgs { + switch arg.Key { + case "lower": + lower = []byte(arg.Vals[0]) + case "upper": + upper = []byte(arg.Vals[0]) + case "snapshot": + name := arg.Vals[0] + snap, ok := snaps[name] + if !ok { + return fmt.Sprintf("no snapshot found for name %s", name) + } + reader = snap + case "file-only-snapshot": + name := arg.Vals[0] + efos, ok := efos[name] + if !ok { + return fmt.Sprintf("no snapshot found for name %s", name) + } + reader = efos + case "skip-shared": + fileVisitor = func(sst *SharedSSTMeta) error { + fmt.Fprintf(&b, "shared file: %s [%s-%s] [point=%s-%s] [range=%s-%s]\n", sst.fileNum, sst.Smallest.String(), sst.Largest.String(), sst.SmallestPointKey.String(), sst.LargestPointKey.String(), sst.SmallestRangeKey.String(), sst.LargestRangeKey.String()) + return nil + } + } + } + err := reader.ScanInternal(context.TODO(), sstable.CategoryAndQoS{}, lower, upper, + func(key *InternalKey, value LazyValue, _ IteratorLevel) error { + v := value.InPlaceValue() + fmt.Fprintf(&b, "%s (%s)\n", key, v) + return nil + }, + func(start, end []byte, seqNum uint64) error { + fmt.Fprintf(&b, "%s-%s#%d,RANGEDEL\n", start, end, seqNum) + return nil + }, + func(start, end []byte, keys []keyspan.Key) error { + s := keyspan.Span{Start: start, End: end, Keys: keys} + fmt.Fprintf(&b, "%s\n", s.String()) + return nil + }, + fileVisitor, + ) + if err != nil { + return err.Error() + } + return b.String() + default: + return fmt.Sprintf("unknown command %q", td.Cmd) + } + }) +} + +func TestPointCollapsingIter(t *testing.T) { + var def string + datadriven.RunTest(t, "testdata/point_collapsing_iter", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "define": + def = d.Input + return "" + + case "iter": + f := &fakeIter{} + var spans []keyspan.Span + for _, line := range strings.Split(def, "\n") { + for _, key := range strings.Fields(line) { + j := strings.Index(key, ":") + k := base.ParseInternalKey(key[:j]) + v := []byte(key[j+1:]) + if k.Kind() == InternalKeyKindRangeDelete { + spans = append(spans, keyspan.Span{ + Start: k.UserKey, + End: v, + Keys: []keyspan.Key{{Trailer: k.Trailer}}, + KeysOrder: 0, + }) + continue + } + f.keys = append(f.keys, k) + f.vals = append(f.vals, v) + } + } + + ksIter := keyspan.NewIter(base.DefaultComparer.Compare, spans) + pcIter := &pointCollapsingIterator{ + comparer: base.DefaultComparer, + merge: base.DefaultMerger.Merge, + seqNum: math.MaxUint64, + } + pcIter.iter.Init(base.DefaultComparer, f, ksIter, keyspan.InterleavingIterOpts{}) + defer pcIter.Close() + return itertest.RunInternalIterCmd(t, d, pcIter, itertest.Verbose) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) +} diff --git a/pebble/scripts/changed-go-pkgs.sh b/pebble/scripts/changed-go-pkgs.sh new file mode 100755 index 0000000..529ff0d --- /dev/null +++ b/pebble/scripts/changed-go-pkgs.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +BASE_SHA="$1" +HEAD_SHA="$2" + +if [ -z "$HEAD_SHA" ];then + echo "Usage: $0 " + exit 1 +fi + +git diff --name-only "${BASE_SHA}..${HEAD_SHA}" -- "*.go" \ + | xargs -rn1 dirname \ + | sort -u \ + | xargs echo diff --git a/pebble/scripts/code-coverage-publish.sh b/pebble/scripts/code-coverage-publish.sh new file mode 100755 index 0000000..d5496d0 --- /dev/null +++ b/pebble/scripts/code-coverage-publish.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# This script runs genhtml (part of lcov) on lcov artifacts generated by the +# code-coverage.sh script and uploads the result to a GCS bucket. + +BUCKET="${BUCKET:-crl-codecover-public}" + +set -euxo pipefail + +publish() { + PROFILE="$1" + TITLE="$2" + + if [ ! -f "$PROFILE" ]; then + echo "$PROFILE does not exist" + exit 1 + fi + + DIR="$(date -r "$PROFILE" -u '+%Y-%m-%d %H:%MZ') $(git rev-parse --short=8 HEAD) - $TITLE" + + mkdir -p "artifacts/$DIR" + # The filename shows up on the generated page, let's make it useful. + cp "$PROFILE" "artifacts/$DIR.lcov" + genhtml "artifacts/$DIR.lcov" -o "artifacts/$DIR" + + gsutil -m cp -Z -r "artifacts/$DIR" "gs://$BUCKET/pebble/$DIR" +} + +publish "artifacts/profile-tests.lcov" "tests only" +publish "artifacts/profile-meta.lcov" "meta test only" +publish "artifacts/profile-tests-and-meta.lcov" "tests + meta" + +# Regenerate index.html. +echo 'Pebble coverage

Pebble coverage runs:

' >> artifacts/index.html + +gsutil cp artifacts/index.html "gs://$BUCKET/pebble/index.html" +gsutil setmeta -h "Cache-Control: public, max-age=300, no-transform" "gs://$BUCKET/pebble/index.html" diff --git a/pebble/scripts/code-coverage.sh b/pebble/scripts/code-coverage.sh new file mode 100755 index 0000000..e234f62 --- /dev/null +++ b/pebble/scripts/code-coverage.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# This script runs unit tests and the metamorphic tests with coverage +# instrumentation and generates three lcov files: +# - ./artifacts/profile-tests.lcov +# - ./artifacts/profile-meta.lcov +# - ./artifacts/profile-tests-and-meta.lcov + +set -euxo pipefail + +mkdir -p artifacts + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +test_failed=0 +# The coverpkg argument ensures that coverage is not restricted to the tested +# package; so this will get us overall coverage for all tests. +go test -tags invariants ./... -coverprofile=artifacts/profile-tests.gocov -coverpkg=./... || test_failed=1 + +# The metamorphic test executes itself for each run; we don't get coverage for +# the inner run. To fix this, we use metarunner as the "inner" binary and we +# instrument it with coverage (see https://go.dev/testing/coverage/#building). +go build -tags invariants -o "${tmpdir}/metarunner" -cover ./internal/metamorphic/metarunner +mkdir -p "${tmpdir}/metacover" + +GOCOVERDIR="${tmpdir}/metacover" go test ./internal/metamorphic \ + -count 50 --inner-binary="${tmpdir}/metarunner" || test_failed=1 + +go tool covdata textfmt -i "${tmpdir}/metacover" -o artifacts/profile-meta.gocov + +# TODO(radu): make the crossversion metamorphic test work. + +go run github.com/cockroachdb/code-cov-utils/convert@v1.1.0 -out artifacts/profile-tests.lcov \ + -trim-prefix github.com/cockroachdb/pebble/ \ + artifacts/profile-tests.gocov + +go run github.com/cockroachdb/code-cov-utils/convert@v1.1.0 -out artifacts/profile-meta.lcov \ + -trim-prefix github.com/cockroachdb/pebble/ \ + artifacts/profile-meta.gocov + +go run github.com/cockroachdb/code-cov-utils/convert@v1.1.0 -out artifacts/profile-tests-and-meta.lcov \ + -trim-prefix github.com/cockroachdb/pebble/ \ + artifacts/profile-tests.gocov artifacts/profile-meta.gocov + +if [ $test_failed -eq 1 ]; then + # TODO(radu): somehow plumb the error and publish it. + echo "WARNING: some tests have failed; coverage might be incomplete." +fi diff --git a/pebble/scripts/pr-codecov-run-tests.sh b/pebble/scripts/pr-codecov-run-tests.sh new file mode 100755 index 0000000..eb8c72a --- /dev/null +++ b/pebble/scripts/pr-codecov-run-tests.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# This script runs unit tests with coverage enabled for a specific list of +# package paths and outputs the coverage to a json file. +# +# Package paths that are not valid in this tree are tolerated. + +set -xeuo pipefail + +output_json_file="$1" +packages="$2" + +# Find the targets. We need to convert from, e.g. +# . objstorage objstorage/objstorageprovider +# to +# . ./objstorage ./objstorage/objstorageprovider + +paths="" +sep="" + +for p in ${packages}; do + # Check that the path exists and contains Go files. + if ls "${p}"/*.go >/dev/null 2>&1; then + if [[ $p != "." ]]; then + p="./$p" + fi + paths="${paths}${sep}${p}" + sep=" " + fi +done + +if [ -z "${paths}" ]; then + echo "Skipping" + touch "${output_json_file}" + exit 0 +fi + +tmpfile=$(mktemp --suffix -coverprofile) +trap 'rm -f "${tmpfile}"' EXIT + +make testcoverage COVER_PROFILE="${tmpfile}" PKG="$paths" +go run github.com/cockroachdb/code-cov-utils/gocover2json@v1.0.0 \ + --trim-prefix github.com/cockroachdb/pebble/ \ + "${tmpfile}" "${output_json_file}" diff --git a/pebble/scripts/run-crossversion-meta.sh b/pebble/scripts/run-crossversion-meta.sh new file mode 100755 index 0000000..788820c --- /dev/null +++ b/pebble/scripts/run-crossversion-meta.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -ex + +BRANCH=$(git symbolic-ref --short HEAD) + +TEMPDIR=(`mktemp -d -t crossversion-$(date +%Y-%m-%d-%H-%M-%S)-XXXXXXXXXX`) + +VERSIONS="" +for branch in "$@" +do + git checkout "$branch" + sha=`git rev-parse --short HEAD` + + # If the branch name has a "-", pull off the suffix. With the + # crl-release-{XX.X} release branch naming scheme, this will extract the + # {XX.X}. + version=`cut -d- -f3 <<< "$branch"` + + echo "Building $version ($sha)" + go test -c -o "$TEMPDIR/meta.$version.test" ./internal/metamorphic + VERSIONS="$VERSIONS -version $version,$sha,$TEMPDIR/meta.$version.test" +done + +# Return to whence we came. +git checkout $BRANCH + +if [[ -z "${STRESS}" ]]; then + go test ./internal/metamorphic/crossversion \ + -test.v \ + -test.timeout "${TIMEOUT:-30m}" \ + -test.run 'TestMetaCrossVersion$' \ + -seed ${SEED:-0} \ + -factor ${FACTOR:-10} \ + $(echo $VERSIONS) +else + stress -p 1 go test ./internal/metamorphic/crossversion \ + -test.v \ + -test.timeout "${TIMEOUT:-30m}" \ + -test.run 'TestMetaCrossVersion$' \ + -seed ${SEED:-0} \ + -factor ${FACTOR:-10} \ + $(echo $VERSIONS) +fi + +rm -rf $TEMPDIR diff --git a/pebble/shims/cmp/cmp.go b/pebble/shims/cmp/cmp.go new file mode 100644 index 0000000..93d4d28 --- /dev/null +++ b/pebble/shims/cmp/cmp.go @@ -0,0 +1,74 @@ +// This file has been ported over from go 1.21.0 so that we can avoid +// having to upgrade for basic comparison functions. Copyright notice +// is preserved: +// Copyright 2023 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 cmp provides types and functions related to comparing +// ordered values. +package cmp + +// 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. +// +// Note that floating-point types may contain NaN ("not-a-number") values. +// An operator such as == or < will always report false when +// comparing a NaN value with any other value, NaN or not. +// See the [Compare] function for a consistent way to compare NaN values. +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} + +// Less reports whether x is less than y. +// For floating-point types, a NaN is considered less than any non-NaN, +// and -0.0 is not less than (is equal to) 0.0. +func Less[T Ordered](x, y T) bool { + return (isNaN(x) && !isNaN(y)) || x < y +} + +// Compare returns +// +// -1 if x is less than y, +// 0 if x equals y, +// +1 if x is greater than y. +// +// For floating-point types, a NaN is considered less than any non-NaN, +// a NaN is considered equal to a NaN, and -0.0 is equal to 0.0. +func Compare[T Ordered](x, y T) int { + xNaN := isNaN(x) + yNaN := isNaN(y) + if xNaN && yNaN { + return 0 + } + if xNaN || x < y { + return -1 + } + if yNaN || x > y { + return +1 + } + return 0 +} + +// isNaN reports whether x is a NaN without requiring the math package. +// This will always return false if T is not floating-point. +func isNaN[T Ordered](x T) bool { + return x != x +} + +// Or returns the first of its arguments that is not equal to the zero value. +// If no argument is non-zero, it returns the zero value. +func Or[T comparable](vals ...T) T { + var zero T + for _, val := range vals { + if val != zero { + return val + } + } + return zero +} diff --git a/pebble/shims/slices/slices.go b/pebble/shims/slices/slices.go new file mode 100644 index 0000000..22a5305 --- /dev/null +++ b/pebble/shims/slices/slices.go @@ -0,0 +1,519 @@ +// This file has been ported over from go 1.21.0 so that we can avoid +// having to upgrade for basic comparison functions. Copyright notice +// is preserved: +// 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 slices defines various functions useful with slices of any type. +package slices + +import ( + "unsafe" + + "github.com/cockroachdb/pebble/shims/cmp" +) + +// Equal reports whether two slices are equal: the same length and all +// elements equal. If the lengths are different, Equal returns false. +// Otherwise, the elements are compared in increasing index order, and the +// comparison stops at the first unequal pair. +// Floating point NaNs are not considered equal. +func Equal[S ~[]E, E comparable](s1, s2 S) bool { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + if s1[i] != s2[i] { + return false + } + } + return true +} + +// EqualFunc reports whether two slices are equal using an equality +// function on each pair of elements. If the lengths are different, +// EqualFunc returns false. Otherwise, the elements are compared in +// increasing index order, and the comparison stops at the first index +// for which eq returns false. +func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool { + if len(s1) != len(s2) { + return false + } + for i, v1 := range s1 { + v2 := s2[i] + if !eq(v1, v2) { + return false + } + } + return true +} + +// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair +// of elements. The elements are compared sequentially, starting at index 0, +// until one element is not equal to the other. +// The result of comparing the first non-matching elements is returned. +// If both slices are equal until one of them ends, the shorter slice is +// considered less than the longer one. +// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2. +func Compare[S ~[]E, E cmp.Ordered](s1, s2 S) int { + for i, v1 := range s1 { + if i >= len(s2) { + return +1 + } + v2 := s2[i] + if c := cmp.Compare(v1, v2); c != 0 { + return c + } + } + if len(s1) < len(s2) { + return -1 + } + return 0 +} + +// CompareFunc is like [Compare] but uses a custom comparison function on each +// pair of elements. +// The result is the first non-zero result of cmp; if cmp always +// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2), +// and +1 if len(s1) > len(s2). +func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int { + for i, v1 := range s1 { + if i >= len(s2) { + return +1 + } + v2 := s2[i] + if c := cmp(v1, v2); c != 0 { + return c + } + } + if len(s1) < len(s2) { + return -1 + } + return 0 +} + +// Index returns the index of the first occurrence of v in s, +// or -1 if not present. +func Index[S ~[]E, E comparable](s S, v E) int { + for i := range s { + if v == s[i] { + return i + } + } + return -1 +} + +// IndexFunc returns the first index i satisfying f(s[i]), +// or -1 if none do. +func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { + for i := range s { + if f(s[i]) { + return i + } + } + return -1 +} + +// Contains reports whether v is present in s. +func Contains[S ~[]E, E comparable](s S, v E) bool { + return Index(s, v) >= 0 +} + +// ContainsFunc reports whether at least one +// element e of s satisfies f(e). +func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool { + return IndexFunc(s, f) >= 0 +} + +// Insert inserts the values v... into s at index i, +// returning the modified slice. +// The elements at s[i:] are shifted up to make room. +// In the returned slice r, r[i] == v[0], +// and r[i+len(v)] == value originally at r[i]. +// Insert panics if i is out of range. +// This function is O(len(s) + len(v)). +func Insert[S ~[]E, E any](s S, i int, v ...E) S { + _ = s[i:] // bounds check + + m := len(v) + if m == 0 { + return s + } + n := len(s) + if i == n { + return append(s, v...) + } + if n+m > cap(s) { + // Use append rather than make so that we bump the size of + // the slice up to the next storage class. + // This is what Grow does but we don't call Grow because + // that might copy the values twice. + s2 := append(s[:i], make(S, n+m-i)...) + copy(s2[i:], v) + copy(s2[i+m:], s[i:]) + return s2 + } + s = s[:n+m] + + // before: + // s: aaaaaaaabbbbccccccccdddd + // ^ ^ ^ ^ + // i i+m n n+m + // after: + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // + // a are the values that don't move in s. + // v are the values copied in from v. + // b and c are the values from s that are shifted up in index. + // d are the values that get overwritten, never to be seen again. + + if !overlaps(v, s[i+m:]) { + // Easy case - v does not overlap either the c or d regions. + // (It might be in some of a or b, or elsewhere entirely.) + // The data we copy up doesn't write to v at all, so just do it. + + copy(s[i+m:], s[i:]) + + // Now we have + // s: aaaaaaaabbbbbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // Note the b values are duplicated. + + copy(s[i:], v) + + // Now we have + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // That's the result we want. + return s + } + + // The hard case - v overlaps c or d. We can't just shift up + // the data because we'd move or clobber the values we're trying + // to insert. + // So instead, write v on top of d, then rotate. + copy(s[n:], v) + + // Now we have + // s: aaaaaaaabbbbccccccccvvvv + // ^ ^ ^ ^ + // i i+m n n+m + + rotateRight(s[i:], m) + + // Now we have + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // That's the result we want. + return s +} + +// Delete removes the elements s[i:j] from s, returning the modified slice. +// Delete panics if j > len(s) or s[i:j] is not a valid slice of s. +// Delete is O(len(s)-i), so if many items must be deleted, it is better to +// make a single call deleting them all together than to delete one at a time. +// Delete zeroes the elements s[len(s)-(j-i):len(s)]. +func Delete[S ~[]E, E any](s S, i, j int) S { + _ = s[i:j:len(s)] // bounds check + + if i == j { + return s + } + + // oldlen := len(s) + s = append(s[:i], s[j:]...) + // go1.21 feature: clear(s[len(s):oldlen]) // zero/nil out the obsolete elements, for GC + return s +} + +// DeleteFunc removes any elements from s for which del returns true, +// returning the modified slice. +// DeleteFunc zeroes the elements between the new length and the original length. +func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { + i := IndexFunc(s, del) + if i == -1 { + return s + } + // Don't start copying elements until we find one to delete. + for j := i + 1; j < len(s); j++ { + if v := s[j]; !del(v) { + s[i] = v + i++ + } + } + // go1.21 feature: clear(s[i:]) // zero/nil out the obsolete elements, for GC + return s[:i] +} + +// Replace replaces the elements s[i:j] by the given v, and returns the +// modified slice. +// Replace panics if j > len(s) or s[i:j] is not a valid slice of s. +// When len(v) < (j-i), Replace zeroes the elements between the new length and the original length. +func Replace[S ~[]E, E any](s S, i, j int, v ...E) S { + _ = s[i:j] // bounds check + + if i == j { + return Insert(s, i, v...) + } + if j == len(s) { + return append(s[:i], v...) + } + + tot := len(s[:i]) + len(v) + len(s[j:]) + if tot > cap(s) { + // Too big to fit, allocate and copy over. + s2 := append(s[:i], make(S, tot-i)...) // See Insert + copy(s2[i:], v) + copy(s2[i+len(v):], s[j:]) + return s2 + } + + r := s[:tot] + + if i+len(v) <= j { + // Easy, as v fits in the deleted portion. + copy(r[i:], v) + copy(r[i+len(v):], s[j:]) + // go1.21 feature: clear(s[tot:]) // zero/nil out the obsolete elements, for GC + return r + } + + // We are expanding (v is bigger than j-i). + // The situation is something like this: + // (example has i=4,j=8,len(s)=16,len(v)=6) + // s: aaaaxxxxbbbbbbbbyy + // ^ ^ ^ ^ + // i j len(s) tot + // a: prefix of s + // x: deleted range + // b: more of s + // y: area to expand into + + if !overlaps(r[i+len(v):], v) { + // Easy, as v is not clobbered by the first copy. + copy(r[i+len(v):], s[j:]) + copy(r[i:], v) + return r + } + + // This is a situation where we don't have a single place to which + // we can copy v. Parts of it need to go to two different places. + // We want to copy the prefix of v into y and the suffix into x, then + // rotate |y| spots to the right. + // + // v[2:] v[:2] + // | | + // s: aaaavvvvbbbbbbbbvv + // ^ ^ ^ ^ + // i j len(s) tot + // + // If either of those two destinations don't alias v, then we're good. + y := len(v) - (j - i) // length of y portion + + if !overlaps(r[i:j], v) { + copy(r[i:j], v[y:]) + copy(r[len(s):], v[:y]) + rotateRight(r[i:], y) + return r + } + if !overlaps(r[len(s):], v) { + copy(r[len(s):], v[:y]) + copy(r[i:j], v[y:]) + rotateRight(r[i:], y) + return r + } + + // Now we know that v overlaps both x and y. + // That means that the entirety of b is *inside* v. + // So we don't need to preserve b at all; instead we + // can copy v first, then copy the b part of v out of + // v to the right destination. + k := startIdx(v, s[j:]) + copy(r[i:], v) + copy(r[i+len(v):], r[i+k:]) + return r +} + +// Clone returns a copy of the slice. +// The elements are copied using assignment, so this is a shallow clone. +func Clone[S ~[]E, E any](s S) S { + // The s[:0:0] preserves nil in case it matters. + return append(s[:0:0], s...) +} + +// Compact replaces consecutive runs of equal elements with a single copy. +// This is like the uniq command found on Unix. +// Compact modifies the contents of the slice s and returns the modified slice, +// which may have a smaller length. +// Compact zeroes the elements between the new length and the original length. +func Compact[S ~[]E, E comparable](s S) S { + if len(s) < 2 { + return s + } + i := 1 + for k := 1; k < len(s); k++ { + if s[k] != s[k-1] { + if i != k { + s[i] = s[k] + } + i++ + } + } + // go1.21 feature: clear(s[i:]) // zero/nil out the obsolete elements, for GC + return s[:i] +} + +// CompactFunc is like [Compact] but uses an equality function to compare elements. +// For runs of elements that compare equal, CompactFunc keeps the first one. +// CompactFunc zeroes the elements between the new length and the original length. +func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S { + if len(s) < 2 { + return s + } + i := 1 + for k := 1; k < len(s); k++ { + if !eq(s[k], s[k-1]) { + if i != k { + s[i] = s[k] + } + i++ + } + } + // go1.21 feature: clear(s[i:]) // zero/nil out the obsolete elements, for GC + return s[:i] +} + +// Grow increases the slice's capacity, if necessary, to guarantee space for +// another n elements. After Grow(n), at least n elements can be appended +// to the slice without another allocation. If n is negative or too large to +// allocate the memory, Grow panics. +func Grow[S ~[]E, E any](s S, n int) S { + if n < 0 { + panic("cannot be negative") + } + if n -= cap(s) - len(s); n > 0 { + s = append(s[:cap(s)], make([]E, n)...)[:len(s)] + } + return s +} + +// Clip removes unused capacity from the slice, returning s[:len(s):len(s)]. +func Clip[S ~[]E, E any](s S) S { + return s[:len(s):len(s)] +} + +// Rotation algorithm explanation: +// +// rotate left by 2 +// start with +// 0123456789 +// split up like this +// 01 234567 89 +// swap first 2 and last 2 +// 89 234567 01 +// join first parts +// 89234567 01 +// recursively rotate first left part by 2 +// 23456789 01 +// join at the end +// 2345678901 +// +// rotate left by 8 +// start with +// 0123456789 +// split up like this +// 01 234567 89 +// swap first 2 and last 2 +// 89 234567 01 +// join last parts +// 89 23456701 +// recursively rotate second part left by 6 +// 89 01234567 +// join at the end +// 8901234567 + +// TODO: There are other rotate algorithms. +// This algorithm has the desirable property that it moves each element exactly twice. +// The triple-reverse algorithm is simpler and more cache friendly, but takes more writes. +// The follow-cycles algorithm can be 1-write but it is not very cache friendly. + +// rotateLeft rotates b left by n spaces. +// s_final[i] = s_orig[i+r], wrapping around. +func rotateLeft[E any](s []E, r int) { + for r != 0 && r != len(s) { + if r*2 <= len(s) { + swap(s[:r], s[len(s)-r:]) + s = s[:len(s)-r] + } else { + swap(s[:len(s)-r], s[r:]) + s, r = s[len(s)-r:], r*2-len(s) + } + } +} +func rotateRight[E any](s []E, r int) { + rotateLeft(s, len(s)-r) +} + +// swap swaps the contents of x and y. x and y must be equal length and disjoint. +func swap[E any](x, y []E) { + for i := 0; i < len(x); i++ { + x[i], y[i] = y[i], x[i] + } +} + +// overlaps reports whether the memory ranges a[0:len(a)] and b[0:len(b)] overlap. +func overlaps[E any](a, b []E) bool { + if len(a) == 0 || len(b) == 0 { + return false + } + elemSize := unsafe.Sizeof(a[0]) + if elemSize == 0 { + return false + } + // TODO: use a runtime/unsafe facility once one becomes available. See issue 12445. + // Also see crypto/internal/alias/alias.go:AnyOverlap + return uintptr(unsafe.Pointer(&a[0])) <= uintptr(unsafe.Pointer(&b[len(b)-1]))+(elemSize-1) && + uintptr(unsafe.Pointer(&b[0])) <= uintptr(unsafe.Pointer(&a[len(a)-1]))+(elemSize-1) +} + +// startIdx returns the index in haystack where the needle starts. +// prerequisite: the needle must be aliased entirely inside the haystack. +func startIdx[E any](haystack, needle []E) int { + p := &needle[0] + for i := range haystack { + if p == &haystack[i] { + return i + } + } + // TODO: what if the overlap is by a non-integral number of Es? + panic("needle not found") +} + +// Reverse reverses the elements of the slice in place. +func Reverse[S ~[]E, E any](s S) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} + +// Concat returns a new slice concatenating the passed in slices. +func Concat[S ~[]E, E any](slices ...S) S { + size := 0 + for _, s := range slices { + size += len(s) + if size < 0 { + panic("len out of range") + } + } + newslice := Grow[S](nil, size) + for _, s := range slices { + newslice = append(newslice, s...) + } + return newslice +} diff --git a/pebble/shims/slices/sort.go b/pebble/shims/slices/sort.go new file mode 100644 index 0000000..cf445a5 --- /dev/null +++ b/pebble/shims/slices/sort.go @@ -0,0 +1,202 @@ +// This file has been ported over from go 1.21.0 so that we can avoid +// having to upgrade for basic comparison functions. Copyright notice +// is preserved: +// Copyright 2023 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. + +//go:generate go run $GOROOT/src/sort/gen_sort_variants.go -generic + +package slices + +import ( + "math/bits" + + "github.com/cockroachdb/pebble/shims/cmp" +) + +// Sort sorts a slice of any ordered type in ascending order. +// When sorting floating-point numbers, NaNs are ordered before other values. +func Sort[S ~[]E, E cmp.Ordered](x S) { + n := len(x) + pdqsortOrdered(x, 0, n, bits.Len(uint(n))) +} + +// SortFunc sorts the slice x in ascending order as determined by the cmp +// function. This sort is not guaranteed to be stable. +// cmp(a, b) should return a negative number when a < b, a positive number when +// a > b and zero when a == b. +// +// SortFunc requires that cmp is a strict weak ordering. +// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings. +func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) { + n := len(x) + pdqsortCmpFunc(x, 0, n, bits.Len(uint(n)), cmp) +} + +// SortStableFunc sorts the slice x while keeping the original order of equal +// elements, using cmp to compare elements in the same way as [SortFunc]. +func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) { + stableCmpFunc(x, len(x), cmp) +} + +// IsSorted reports whether x is sorted in ascending order. +func IsSorted[S ~[]E, E cmp.Ordered](x S) bool { + for i := len(x) - 1; i > 0; i-- { + if cmp.Less(x[i], x[i-1]) { + return false + } + } + return true +} + +// IsSortedFunc reports whether x is sorted in ascending order, with cmp as the +// comparison function as defined by [SortFunc]. +func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool { + for i := len(x) - 1; i > 0; i-- { + if cmp(x[i], x[i-1]) < 0 { + return false + } + } + return true +} + +// Min returns the minimal value in x. It panics if x is empty. +// For floating-point numbers, Min propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Min[S ~[]E, E cmp.Ordered](x S) E { + if len(x) < 1 { + panic("slices.Min: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if x[i] < m { + m = x[i] + } + } + return m +} + +// MinFunc returns the minimal value in x, using cmp to compare elements. +// It panics if x is empty. If there is more than one minimal element +// according to the cmp function, MinFunc returns the first one. +func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E { + if len(x) < 1 { + panic("slices.MinFunc: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if cmp(x[i], m) < 0 { + m = x[i] + } + } + return m +} + +// Max returns the maximal value in x. It panics if x is empty. +// For floating-point E, Max propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Max[S ~[]E, E cmp.Ordered](x S) E { + if len(x) < 1 { + panic("slices.Max: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if x[i] > m { + m = x[i] + } + } + return m +} + +// MaxFunc returns the maximal value in x, using cmp to compare elements. +// It panics if x is empty. If there is more than one maximal element +// according to the cmp function, MaxFunc returns the first one. +func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E { + if len(x) < 1 { + panic("slices.MaxFunc: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if cmp(x[i], m) > 0 { + m = x[i] + } + } + return m +} + +// BinarySearch searches for target in a sorted slice and returns the position +// where target is found, or the position where target would appear in the +// sort order; it also returns a bool saying whether the target is really found +// in the slice. The slice must be sorted in increasing order. +func BinarySearch[S ~[]E, E cmp.Ordered](x S, target E) (int, bool) { + // Inlining is faster than calling BinarySearchFunc with a lambda. + n := len(x) + // Define x[-1] < target and x[n] >= target. + // Invariant: x[i-1] < target, x[j] >= target. + i, j := 0, n + for i < j { + h := int(uint(i+j) >> 1) // avoid overflow when computing h + // i ≤ h < j + if cmp.Less(x[h], target) { + i = h + 1 // preserves x[i-1] < target + } else { + j = h // preserves x[j] >= target + } + } + // i == j, x[i-1] < target, and x[j] (= x[i]) >= target => answer is i. + return i, i < n && (x[i] == target || (isNaN(x[i]) && isNaN(target))) +} + +// BinarySearchFunc works like [BinarySearch], but uses a custom comparison +// function. The slice must be sorted in increasing order, where "increasing" +// is defined by cmp. cmp should return 0 if the slice element matches +// the target, a negative number if the slice element precedes the target, +// or a positive number if the slice element follows the target. +// cmp must implement the same ordering as the slice, such that if +// cmp(a, t) < 0 and cmp(b, t) >= 0, then a must precede b in the slice. +func BinarySearchFunc[S ~[]E, E, T any](x S, target T, cmp func(E, T) int) (int, bool) { + n := len(x) + // Define cmp(x[-1], target) < 0 and cmp(x[n], target) >= 0 . + // Invariant: cmp(x[i - 1], target) < 0, cmp(x[j], target) >= 0. + i, j := 0, n + for i < j { + h := int(uint(i+j) >> 1) // avoid overflow when computing h + // i ≤ h < j + if cmp(x[h], target) < 0 { + i = h + 1 // preserves cmp(x[i - 1], target) < 0 + } else { + j = h // preserves cmp(x[j], target) >= 0 + } + } + // i == j, cmp(x[i-1], target) < 0, and cmp(x[j], target) (= cmp(x[i], target)) >= 0 => answer is i. + return i, i < n && cmp(x[i], target) == 0 +} + +type sortedHint int // hint for pdqsort when choosing the pivot + +const ( + unknownHint sortedHint = iota + increasingHint + decreasingHint +) + +// xorshift paper: https://www.jstatsoft.org/article/view/v008i14/xorshift.pdf +type xorshift uint64 + +func (r *xorshift) Next() uint64 { + *r ^= *r << 13 + *r ^= *r >> 17 + *r ^= *r << 5 + return uint64(*r) +} + +func nextPowerOfTwo(length int) uint { + return 1 << bits.Len(uint(length)) +} + +// isNaN reports whether x is a NaN without requiring the math package. +// This will always return false if T is not floating-point. +func isNaN[T cmp.Ordered](x T) bool { + return x != x +} diff --git a/pebble/shims/slices/zsortanyfunc.go b/pebble/shims/slices/zsortanyfunc.go new file mode 100644 index 0000000..386910c --- /dev/null +++ b/pebble/shims/slices/zsortanyfunc.go @@ -0,0 +1,482 @@ +// This file has been ported over from go 1.21.0 so that we can avoid +// having to upgrade for basic comparison functions. Copyright notice +// is preserved: +// Code generated by gen_sort_variants.go; DO NOT EDIT. + +// Copyright 2022 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 slices + +// insertionSortCmpFunc sorts data[a:b] using insertion sort. +func insertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + for i := a + 1; i < b; i++ { + for j := i; j > a && (cmp(data[j], data[j-1]) < 0); j-- { + data[j], data[j-1] = data[j-1], data[j] + } + } +} + +// siftDownCmpFunc implements the heap property on data[lo:hi]. +// first is an offset into the array where the root of the heap lies. +func siftDownCmpFunc[E any](data []E, lo, hi, first int, cmp func(a, b E) int) { + root := lo + for { + child := 2*root + 1 + if child >= hi { + break + } + if child+1 < hi && (cmp(data[first+child], data[first+child+1]) < 0) { + child++ + } + if !(cmp(data[first+root], data[first+child]) < 0) { + return + } + data[first+root], data[first+child] = data[first+child], data[first+root] + root = child + } +} + +func heapSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + first := a + lo := 0 + hi := b - a + + // Build heap with greatest element at top. + for i := (hi - 1) / 2; i >= 0; i-- { + siftDownCmpFunc(data, i, hi, first, cmp) + } + + // Pop elements, largest first, into end of data. + for i := hi - 1; i >= 0; i-- { + data[first], data[first+i] = data[first+i], data[first] + siftDownCmpFunc(data, lo, i, first, cmp) + } +} + +// pdqsortCmpFunc sorts data[a:b]. +// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort. +// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf +// C++ implementation: https://github.com/orlp/pdqsort +// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/ +// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort. +func pdqsortCmpFunc[E any](data []E, a, b, limit int, cmp func(a, b E) int) { + const maxInsertion = 12 + + var ( + wasBalanced = true // whether the last partitioning was reasonably balanced + wasPartitioned = true // whether the slice was already partitioned + ) + + for { + length := b - a + + if length <= maxInsertion { + insertionSortCmpFunc(data, a, b, cmp) + return + } + + // Fall back to heapsort if too many bad choices were made. + if limit == 0 { + heapSortCmpFunc(data, a, b, cmp) + return + } + + // If the last partitioning was imbalanced, we need to breaking patterns. + if !wasBalanced { + breakPatternsCmpFunc(data, a, b, cmp) + limit-- + } + + pivot, hint := choosePivotCmpFunc(data, a, b, cmp) + if hint == decreasingHint { + reverseRangeCmpFunc(data, a, b, cmp) + // The chosen pivot was pivot-a elements after the start of the array. + // After reversing it is pivot-a elements before the end of the array. + // The idea came from Rust's implementation. + pivot = (b - 1) - (pivot - a) + hint = increasingHint + } + + // The slice is likely already sorted. + if wasBalanced && wasPartitioned && hint == increasingHint { + if partialInsertionSortCmpFunc(data, a, b, cmp) { + return + } + } + + // Probably the slice contains many duplicate elements, partition the slice into + // elements equal to and elements greater than the pivot. + if a > 0 && !(cmp(data[a-1], data[pivot]) < 0) { + mid := partitionEqualCmpFunc(data, a, b, pivot, cmp) + a = mid + continue + } + + mid, alreadyPartitioned := partitionCmpFunc(data, a, b, pivot, cmp) + wasPartitioned = alreadyPartitioned + + leftLen, rightLen := mid-a, b-mid + balanceThreshold := length / 8 + if leftLen < rightLen { + wasBalanced = leftLen >= balanceThreshold + pdqsortCmpFunc(data, a, mid, limit, cmp) + a = mid + 1 + } else { + wasBalanced = rightLen >= balanceThreshold + pdqsortCmpFunc(data, mid+1, b, limit, cmp) + b = mid + } + } +} + +// partitionCmpFunc does one quicksort partition. +// Let p = data[pivot] +// Moves elements in data[a:b] around, so that data[i]

=p for inewpivot. +// On return, data[newpivot] = p +func partitionCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int, alreadyPartitioned bool) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for i <= j && (cmp(data[i], data[a]) < 0) { + i++ + } + for i <= j && !(cmp(data[j], data[a]) < 0) { + j-- + } + if i > j { + data[j], data[a] = data[a], data[j] + return j, true + } + data[i], data[j] = data[j], data[i] + i++ + j-- + + for { + for i <= j && (cmp(data[i], data[a]) < 0) { + i++ + } + for i <= j && !(cmp(data[j], data[a]) < 0) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + data[j], data[a] = data[a], data[j] + return j, false +} + +// partitionEqualCmpFunc partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot]. +// It assumed that data[a:b] does not contain elements smaller than the data[pivot]. +func partitionEqualCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for { + for i <= j && !(cmp(data[a], data[i]) < 0) { + i++ + } + for i <= j && (cmp(data[a], data[j]) < 0) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + return i +} + +// partialInsertionSortCmpFunc partially sorts a slice, returns true if the slice is sorted at the end. +func partialInsertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) bool { + const ( + maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted + shortestShifting = 50 // don't shift any elements on short arrays + ) + i := a + 1 + for j := 0; j < maxSteps; j++ { + for i < b && !(cmp(data[i], data[i-1]) < 0) { + i++ + } + + if i == b { + return true + } + + if b-a < shortestShifting { + return false + } + + data[i], data[i-1] = data[i-1], data[i] + + // Shift the smaller one to the left. + if i-a >= 2 { + for j := i - 1; j >= 1; j-- { + if !(cmp(data[j], data[j-1]) < 0) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + // Shift the greater one to the right. + if b-i >= 2 { + for j := i + 1; j < b; j++ { + if !(cmp(data[j], data[j-1]) < 0) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + } + return false +} + +// breakPatternsCmpFunc scatters some elements around in an attempt to break some patterns +// that might cause imbalanced partitions in quicksort. +func breakPatternsCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + length := b - a + if length >= 8 { + random := xorshift(length) + modulus := nextPowerOfTwo(length) + + for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ { + other := int(uint(random.Next()) & (modulus - 1)) + if other >= length { + other -= length + } + data[idx], data[a+other] = data[a+other], data[idx] + } + } +} + +// choosePivotCmpFunc chooses a pivot in data[a:b]. +// +// [0,8): chooses a static pivot. +// [8,shortestNinther): uses the simple median-of-three method. +// [shortestNinther,∞): uses the Tukey ninther method. +func choosePivotCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) (pivot int, hint sortedHint) { + const ( + shortestNinther = 50 + maxSwaps = 4 * 3 + ) + + l := b - a + + var ( + swaps int + i = a + l/4*1 + j = a + l/4*2 + k = a + l/4*3 + ) + + if l >= 8 { + if l >= shortestNinther { + // Tukey ninther method, the idea came from Rust's implementation. + i = medianAdjacentCmpFunc(data, i, &swaps, cmp) + j = medianAdjacentCmpFunc(data, j, &swaps, cmp) + k = medianAdjacentCmpFunc(data, k, &swaps, cmp) + } + // Find the median among i, j, k and stores it into j. + j = medianCmpFunc(data, i, j, k, &swaps, cmp) + } + + switch swaps { + case 0: + return j, increasingHint + case maxSwaps: + return j, decreasingHint + default: + return j, unknownHint + } +} + +// order2CmpFunc returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a. +func order2CmpFunc[E any](data []E, a, b int, swaps *int, cmp func(a, b E) int) (int, int) { + if cmp(data[b], data[a]) < 0 { + *swaps++ + return b, a + } + return a, b +} + +// medianCmpFunc returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c. +func medianCmpFunc[E any](data []E, a, b, c int, swaps *int, cmp func(a, b E) int) int { + a, b = order2CmpFunc(data, a, b, swaps, cmp) + b, c = order2CmpFunc(data, b, c, swaps, cmp) + a, b = order2CmpFunc(data, a, b, swaps, cmp) + return b +} + +// medianAdjacentCmpFunc finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a. +func medianAdjacentCmpFunc[E any](data []E, a int, swaps *int, cmp func(a, b E) int) int { + return medianCmpFunc(data, a-1, a, a+1, swaps, cmp) +} + +func reverseRangeCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + i := a + j := b - 1 + for i < j { + data[i], data[j] = data[j], data[i] + i++ + j-- + } +} + +func swapRangeCmpFunc[E any](data []E, a, b, n int, cmp func(a, b E) int) { + for i := 0; i < n; i++ { + data[a+i], data[b+i] = data[b+i], data[a+i] + } +} + +func stableCmpFunc[E any](data []E, n int, cmp func(a, b E) int) { + blockSize := 20 // must be > 0 + a, b := 0, blockSize + for b <= n { + insertionSortCmpFunc(data, a, b, cmp) + a = b + b += blockSize + } + insertionSortCmpFunc(data, a, n, cmp) + + for blockSize < n { + a, b = 0, 2*blockSize + for b <= n { + symMergeCmpFunc(data, a, a+blockSize, b, cmp) + a = b + b += 2 * blockSize + } + if m := a + blockSize; m < n { + symMergeCmpFunc(data, a, m, n, cmp) + } + blockSize *= 2 + } +} + +// symMergeCmpFunc merges the two sorted subsequences data[a:m] and data[m:b] using +// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum +// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz +// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in +// Computer Science, pages 714-723. Springer, 2004. +// +// Let M = m-a and N = b-n. Wolog M < N. +// The recursion depth is bound by ceil(log(N+M)). +// The algorithm needs O(M*log(N/M + 1)) calls to data.Less. +// The algorithm needs O((M+N)*log(M)) calls to data.Swap. +// +// The paper gives O((M+N)*log(M)) as the number of assignments assuming a +// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation +// in the paper carries through for Swap operations, especially as the block +// swapping rotate uses only O(M+N) Swaps. +// +// symMerge assumes non-degenerate arguments: a < m && m < b. +// Having the caller check this condition eliminates many leaf recursion calls, +// which improves performance. +func symMergeCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) { + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[a] into data[m:b] + // if data[a:m] only contains one element. + if m-a == 1 { + // Use binary search to find the lowest index i + // such that data[i] >= data[a] for m <= i < b. + // Exit the search loop with i == b in case no such index exists. + i := m + j := b + for i < j { + h := int(uint(i+j) >> 1) + if cmp(data[h], data[a]) < 0 { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[a] reaches the position before i. + for k := a; k < i-1; k++ { + data[k], data[k+1] = data[k+1], data[k] + } + return + } + + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[m] into data[a:m] + // if data[m:b] only contains one element. + if b-m == 1 { + // Use binary search to find the lowest index i + // such that data[i] > data[m] for a <= i < m. + // Exit the search loop with i == m in case no such index exists. + i := a + j := m + for i < j { + h := int(uint(i+j) >> 1) + if !(cmp(data[m], data[h]) < 0) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[m] reaches the position i. + for k := m; k > i; k-- { + data[k], data[k-1] = data[k-1], data[k] + } + return + } + + mid := int(uint(a+b) >> 1) + n := mid + m + var start, r int + if m > mid { + start = n - b + r = mid + } else { + start = a + r = m + } + p := n - 1 + + for start < r { + c := int(uint(start+r) >> 1) + if !(cmp(data[p-c], data[c]) < 0) { + start = c + 1 + } else { + r = c + } + } + + end := n - start + if start < m && m < end { + rotateCmpFunc(data, start, m, end, cmp) + } + if a < start && start < mid { + symMergeCmpFunc(data, a, start, mid, cmp) + } + if mid < end && end < b { + symMergeCmpFunc(data, mid, end, b, cmp) + } +} + +// rotateCmpFunc rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data: +// Data of the form 'x u v y' is changed to 'x v u y'. +// rotate performs at most b-a many calls to data.Swap, +// and it assumes non-degenerate arguments: a < m && m < b. +func rotateCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) { + i := m - a + j := b - m + + for i != j { + if i > j { + swapRangeCmpFunc(data, m-i, m, j, cmp) + i -= j + } else { + swapRangeCmpFunc(data, m-i, m+j-i, i, cmp) + j -= i + } + } + // i == j + swapRangeCmpFunc(data, m-i, m, i, cmp) +} diff --git a/pebble/shims/slices/zsortordered.go b/pebble/shims/slices/zsortordered.go new file mode 100644 index 0000000..d2d2e7d --- /dev/null +++ b/pebble/shims/slices/zsortordered.go @@ -0,0 +1,484 @@ +// This file has been ported over from go 1.21.0 so that we can avoid +// having to upgrade for basic comparison functions. Copyright notice +// is preserved: +// Code generated by gen_sort_variants.go; DO NOT EDIT. + +// Copyright 2022 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 slices + +import "github.com/cockroachdb/pebble/shims/cmp" + +// insertionSortOrdered sorts data[a:b] using insertion sort. +func insertionSortOrdered[E cmp.Ordered](data []E, a, b int) { + for i := a + 1; i < b; i++ { + for j := i; j > a && cmp.Less(data[j], data[j-1]); j-- { + data[j], data[j-1] = data[j-1], data[j] + } + } +} + +// siftDownOrdered implements the heap property on data[lo:hi]. +// first is an offset into the array where the root of the heap lies. +func siftDownOrdered[E cmp.Ordered](data []E, lo, hi, first int) { + root := lo + for { + child := 2*root + 1 + if child >= hi { + break + } + if child+1 < hi && cmp.Less(data[first+child], data[first+child+1]) { + child++ + } + if !cmp.Less(data[first+root], data[first+child]) { + return + } + data[first+root], data[first+child] = data[first+child], data[first+root] + root = child + } +} + +func heapSortOrdered[E cmp.Ordered](data []E, a, b int) { + first := a + lo := 0 + hi := b - a + + // Build heap with greatest element at top. + for i := (hi - 1) / 2; i >= 0; i-- { + siftDownOrdered(data, i, hi, first) + } + + // Pop elements, largest first, into end of data. + for i := hi - 1; i >= 0; i-- { + data[first], data[first+i] = data[first+i], data[first] + siftDownOrdered(data, lo, i, first) + } +} + +// pdqsortOrdered sorts data[a:b]. +// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort. +// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf +// C++ implementation: https://github.com/orlp/pdqsort +// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/ +// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort. +func pdqsortOrdered[E cmp.Ordered](data []E, a, b, limit int) { + const maxInsertion = 12 + + var ( + wasBalanced = true // whether the last partitioning was reasonably balanced + wasPartitioned = true // whether the slice was already partitioned + ) + + for { + length := b - a + + if length <= maxInsertion { + insertionSortOrdered(data, a, b) + return + } + + // Fall back to heapsort if too many bad choices were made. + if limit == 0 { + heapSortOrdered(data, a, b) + return + } + + // If the last partitioning was imbalanced, we need to breaking patterns. + if !wasBalanced { + breakPatternsOrdered(data, a, b) + limit-- + } + + pivot, hint := choosePivotOrdered(data, a, b) + if hint == decreasingHint { + reverseRangeOrdered(data, a, b) + // The chosen pivot was pivot-a elements after the start of the array. + // After reversing it is pivot-a elements before the end of the array. + // The idea came from Rust's implementation. + pivot = (b - 1) - (pivot - a) + hint = increasingHint + } + + // The slice is likely already sorted. + if wasBalanced && wasPartitioned && hint == increasingHint { + if partialInsertionSortOrdered(data, a, b) { + return + } + } + + // Probably the slice contains many duplicate elements, partition the slice into + // elements equal to and elements greater than the pivot. + if a > 0 && !cmp.Less(data[a-1], data[pivot]) { + mid := partitionEqualOrdered(data, a, b, pivot) + a = mid + continue + } + + mid, alreadyPartitioned := partitionOrdered(data, a, b, pivot) + wasPartitioned = alreadyPartitioned + + leftLen, rightLen := mid-a, b-mid + balanceThreshold := length / 8 + if leftLen < rightLen { + wasBalanced = leftLen >= balanceThreshold + pdqsortOrdered(data, a, mid, limit) + a = mid + 1 + } else { + wasBalanced = rightLen >= balanceThreshold + pdqsortOrdered(data, mid+1, b, limit) + b = mid + } + } +} + +// partitionOrdered does one quicksort partition. +// Let p = data[pivot] +// Moves elements in data[a:b] around, so that data[i]

=p for inewpivot. +// On return, data[newpivot] = p +func partitionOrdered[E cmp.Ordered](data []E, a, b, pivot int) (newpivot int, alreadyPartitioned bool) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for i <= j && cmp.Less(data[i], data[a]) { + i++ + } + for i <= j && !cmp.Less(data[j], data[a]) { + j-- + } + if i > j { + data[j], data[a] = data[a], data[j] + return j, true + } + data[i], data[j] = data[j], data[i] + i++ + j-- + + for { + for i <= j && cmp.Less(data[i], data[a]) { + i++ + } + for i <= j && !cmp.Less(data[j], data[a]) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + data[j], data[a] = data[a], data[j] + return j, false +} + +// partitionEqualOrdered partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot]. +// It assumed that data[a:b] does not contain elements smaller than the data[pivot]. +func partitionEqualOrdered[E cmp.Ordered](data []E, a, b, pivot int) (newpivot int) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for { + for i <= j && !cmp.Less(data[a], data[i]) { + i++ + } + for i <= j && cmp.Less(data[a], data[j]) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + return i +} + +// partialInsertionSortOrdered partially sorts a slice, returns true if the slice is sorted at the end. +func partialInsertionSortOrdered[E cmp.Ordered](data []E, a, b int) bool { + const ( + maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted + shortestShifting = 50 // don't shift any elements on short arrays + ) + i := a + 1 + for j := 0; j < maxSteps; j++ { + for i < b && !cmp.Less(data[i], data[i-1]) { + i++ + } + + if i == b { + return true + } + + if b-a < shortestShifting { + return false + } + + data[i], data[i-1] = data[i-1], data[i] + + // Shift the smaller one to the left. + if i-a >= 2 { + for j := i - 1; j >= 1; j-- { + if !cmp.Less(data[j], data[j-1]) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + // Shift the greater one to the right. + if b-i >= 2 { + for j := i + 1; j < b; j++ { + if !cmp.Less(data[j], data[j-1]) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + } + return false +} + +// breakPatternsOrdered scatters some elements around in an attempt to break some patterns +// that might cause imbalanced partitions in quicksort. +func breakPatternsOrdered[E cmp.Ordered](data []E, a, b int) { + length := b - a + if length >= 8 { + random := xorshift(length) + modulus := nextPowerOfTwo(length) + + for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ { + other := int(uint(random.Next()) & (modulus - 1)) + if other >= length { + other -= length + } + data[idx], data[a+other] = data[a+other], data[idx] + } + } +} + +// choosePivotOrdered chooses a pivot in data[a:b]. +// +// [0,8): chooses a static pivot. +// [8,shortestNinther): uses the simple median-of-three method. +// [shortestNinther,∞): uses the Tukey ninther method. +func choosePivotOrdered[E cmp.Ordered](data []E, a, b int) (pivot int, hint sortedHint) { + const ( + shortestNinther = 50 + maxSwaps = 4 * 3 + ) + + l := b - a + + var ( + swaps int + i = a + l/4*1 + j = a + l/4*2 + k = a + l/4*3 + ) + + if l >= 8 { + if l >= shortestNinther { + // Tukey ninther method, the idea came from Rust's implementation. + i = medianAdjacentOrdered(data, i, &swaps) + j = medianAdjacentOrdered(data, j, &swaps) + k = medianAdjacentOrdered(data, k, &swaps) + } + // Find the median among i, j, k and stores it into j. + j = medianOrdered(data, i, j, k, &swaps) + } + + switch swaps { + case 0: + return j, increasingHint + case maxSwaps: + return j, decreasingHint + default: + return j, unknownHint + } +} + +// order2Ordered returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a. +func order2Ordered[E cmp.Ordered](data []E, a, b int, swaps *int) (int, int) { + if cmp.Less(data[b], data[a]) { + *swaps++ + return b, a + } + return a, b +} + +// medianOrdered returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c. +func medianOrdered[E cmp.Ordered](data []E, a, b, c int, swaps *int) int { + a, b = order2Ordered(data, a, b, swaps) + b, c = order2Ordered(data, b, c, swaps) + a, b = order2Ordered(data, a, b, swaps) + return b +} + +// medianAdjacentOrdered finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a. +func medianAdjacentOrdered[E cmp.Ordered](data []E, a int, swaps *int) int { + return medianOrdered(data, a-1, a, a+1, swaps) +} + +func reverseRangeOrdered[E cmp.Ordered](data []E, a, b int) { + i := a + j := b - 1 + for i < j { + data[i], data[j] = data[j], data[i] + i++ + j-- + } +} + +func swapRangeOrdered[E cmp.Ordered](data []E, a, b, n int) { + for i := 0; i < n; i++ { + data[a+i], data[b+i] = data[b+i], data[a+i] + } +} + +func stableOrdered[E cmp.Ordered](data []E, n int) { + blockSize := 20 // must be > 0 + a, b := 0, blockSize + for b <= n { + insertionSortOrdered(data, a, b) + a = b + b += blockSize + } + insertionSortOrdered(data, a, n) + + for blockSize < n { + a, b = 0, 2*blockSize + for b <= n { + symMergeOrdered(data, a, a+blockSize, b) + a = b + b += 2 * blockSize + } + if m := a + blockSize; m < n { + symMergeOrdered(data, a, m, n) + } + blockSize *= 2 + } +} + +// symMergeOrdered merges the two sorted subsequences data[a:m] and data[m:b] using +// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum +// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz +// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in +// Computer Science, pages 714-723. Springer, 2004. +// +// Let M = m-a and N = b-n. Wolog M < N. +// The recursion depth is bound by ceil(log(N+M)). +// The algorithm needs O(M*log(N/M + 1)) calls to data.Less. +// The algorithm needs O((M+N)*log(M)) calls to data.Swap. +// +// The paper gives O((M+N)*log(M)) as the number of assignments assuming a +// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation +// in the paper carries through for Swap operations, especially as the block +// swapping rotate uses only O(M+N) Swaps. +// +// symMerge assumes non-degenerate arguments: a < m && m < b. +// Having the caller check this condition eliminates many leaf recursion calls, +// which improves performance. +func symMergeOrdered[E cmp.Ordered](data []E, a, m, b int) { + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[a] into data[m:b] + // if data[a:m] only contains one element. + if m-a == 1 { + // Use binary search to find the lowest index i + // such that data[i] >= data[a] for m <= i < b. + // Exit the search loop with i == b in case no such index exists. + i := m + j := b + for i < j { + h := int(uint(i+j) >> 1) + if cmp.Less(data[h], data[a]) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[a] reaches the position before i. + for k := a; k < i-1; k++ { + data[k], data[k+1] = data[k+1], data[k] + } + return + } + + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[m] into data[a:m] + // if data[m:b] only contains one element. + if b-m == 1 { + // Use binary search to find the lowest index i + // such that data[i] > data[m] for a <= i < m. + // Exit the search loop with i == m in case no such index exists. + i := a + j := m + for i < j { + h := int(uint(i+j) >> 1) + if !cmp.Less(data[m], data[h]) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[m] reaches the position i. + for k := m; k > i; k-- { + data[k], data[k-1] = data[k-1], data[k] + } + return + } + + mid := int(uint(a+b) >> 1) + n := mid + m + var start, r int + if m > mid { + start = n - b + r = mid + } else { + start = a + r = m + } + p := n - 1 + + for start < r { + c := int(uint(start+r) >> 1) + if !cmp.Less(data[p-c], data[c]) { + start = c + 1 + } else { + r = c + } + } + + end := n - start + if start < m && m < end { + rotateOrdered(data, start, m, end) + } + if a < start && start < mid { + symMergeOrdered(data, a, start, mid) + } + if mid < end && end < b { + symMergeOrdered(data, mid, end, b) + } +} + +// rotateOrdered rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data: +// Data of the form 'x u v y' is changed to 'x v u y'. +// rotate performs at most b-a many calls to data.Swap, +// and it assumes non-degenerate arguments: a < m && m < b. +func rotateOrdered[E cmp.Ordered](data []E, a, m, b int) { + i := m - a + j := b - m + + for i != j { + if i > j { + swapRangeOrdered(data, m-i, m, j) + i -= j + } else { + swapRangeOrdered(data, m-i, m+j-i, i) + j -= i + } + } + // i == j + swapRangeOrdered(data, m-i, m, i) +} diff --git a/pebble/snapshot.go b/pebble/snapshot.go new file mode 100644 index 0000000..5477b54 --- /dev/null +++ b/pebble/snapshot.go @@ -0,0 +1,560 @@ +// Copyright 2012 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" + "io" + "math" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/rangekey" + "github.com/cockroachdb/pebble/sstable" +) + +// 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 + + // The next/prev link for the snapshotList doubly-linked list of snapshots. + prev, next *Snapshot +} + +var _ Reader = (*Snapshot)(nil) + +// Get gets the value for the given key. It returns ErrNotFound if the Snapshot +// does not contain the key. +// +// The caller should not modify the contents of the returned slice, but it is +// safe to modify the contents of the argument after Get returns. The returned +// slice will remain valid until the returned Closer is closed. On success, the +// caller MUST call closer.Close() or a memory leak will occur. +func (s *Snapshot) Get(key []byte) ([]byte, io.Closer, error) { + if s.db == nil { + panic(ErrClosed) + } + return s.db.getInternal(key, nil /* batch */, s) +} + +// 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, 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, error) { + if s.db == nil { + panic(ErrClosed) + } + return s.db.newIter(ctx, nil /* batch */, newIterOpts{ + snapshot: snapshotIterOpts{seqNum: s.seqNum}, + }, o), 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 (s *Snapshot) ScanInternal( + ctx context.Context, + categoryAndQoS sstable.CategoryAndQoS, + 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 s.db == nil { + panic(ErrClosed) + } + scanInternalOpts := &scanInternalOptions{ + CategoryAndQoS: categoryAndQoS, + visitPointKey: visitPointKey, + visitRangeDel: visitRangeDel, + visitRangeKey: visitRangeKey, + visitSharedFile: visitSharedFile, + skipSharedLevels: visitSharedFile != nil, + IterOptions: IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + LowerBound: lower, + UpperBound: upper, + }, + } + + iter, err := s.db.newInternalIter(ctx, snapshotIterOpts{seqNum: s.seqNum}, scanInternalOpts) + if err != nil { + return err + } + defer iter.close() + + return scanInternalImpl(ctx, lower, upper, iter, scanInternalOpts) +} + +// 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 + // disk space by dropping obsolete records that were pinned by s. + if e := s.db.mu.snapshots.earliest(); e > s.seqNum { + s.db.maybeScheduleCompactionPicker(pickElisionOnly) + } + 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 +} + +func (l *snapshotList) init() { + l.root.next = &l.root + l.root.prev = &l.root +} + +func (l *snapshotList) empty() bool { + return l.root.next == &l.root +} + +func (l *snapshotList) count() int { + if l.empty() { + return 0 + } + var count int + for i := l.root.next; i != &l.root; i = i.next { + count++ + } + return count +} + +func (l *snapshotList) earliest() uint64 { + v := uint64(math.MaxUint64) + if !l.empty() { + v = l.root.next.seqNum + } + return v +} + +func (l *snapshotList) toSlice() []uint64 { + if l.empty() { + return nil + } + var results []uint64 + for i := l.root.next; i != &l.root; i = i.next { + results = append(results, i.seqNum) + } + return results +} + +func (l *snapshotList) pushBack(s *Snapshot) { + if s.list != nil || s.prev != nil || s.next != nil { + panic("pebble: snapshot list is inconsistent") + } + s.prev = l.root.prev + s.prev.next = s + s.next = &l.root + s.next.prev = s + s.list = l +} + +func (l *snapshotList) remove(s *Snapshot) { + if s == &l.root { + panic("pebble: cannot remove snapshot list root node") + } + if s.list != l { + panic("pebble: snapshot list is inconsistent") + } + s.prev.next = s.next + s.next.prev = s.prev + s.next = nil // avoid memory leaks + 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 */, newIterOpts{snapshot: sOpts}, o), nil + } + + if es.excised.Load() { + return nil, ErrSnapshotExcised + } + sOpts := snapshotIterOpts{seqNum: es.seqNum} + iter := es.db.newIter(ctx, nil /* batch */, newIterOpts{snapshot: 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, + categoryAndQoS sstable.CategoryAndQoS, + 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{ + CategoryAndQoS: categoryAndQoS, + IterOptions: IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + LowerBound: lower, + UpperBound: upper, + }, + visitPointKey: visitPointKey, + visitRangeDel: visitRangeDel, + visitRangeKey: visitRangeKey, + visitSharedFile: visitSharedFile, + skipSharedLevels: visitSharedFile != nil, + } + iter, err := es.db.newInternalIter(ctx, sOpts, opts) + if err != nil { + return err + } + 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/pebble/snapshot_test.go b/pebble/snapshot_test.go new file mode 100644 index 0000000..f0514c8 --- /dev/null +++ b/pebble/snapshot_test.go @@ -0,0 +1,384 @@ +// Copyright 2012 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 ( + "bytes" + "fmt" + "math/rand" + "reflect" + "runtime" + "strings" + "sync" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestSnapshotListToSlice(t *testing.T) { + testCases := []struct { + vals []uint64 + }{ + {nil}, + {[]uint64{1}}, + {[]uint64{1, 2, 3}}, + {[]uint64{3, 2, 1}}, + } + for _, c := range testCases { + t.Run("", func(t *testing.T) { + var l snapshotList + l.init() + for _, v := range c.vals { + l.pushBack(&Snapshot{seqNum: v}) + } + slice := l.toSlice() + if !reflect.DeepEqual(c.vals, slice) { + t.Fatalf("expected %d, but got %d", c.vals, slice) + } + }) + } +} + +func testSnapshotImpl(t *testing.T, newSnapshot func(d *DB) Reader) { + var d *DB + var snapshots map[string]Reader + + close := func() { + for _, s := range snapshots { + require.NoError(t, s.Close()) + } + snapshots = nil + if d != nil { + require.NoError(t, d.Close()) + d = nil + } + } + defer close() + + randVersion := func() FormatMajorVersion { + minVersion := formatUnusedPrePebblev1MarkedCompacted + return FormatMajorVersion(int(minVersion) + rand.Intn( + int(internalFormatNewest)-int(minVersion)+1)) + } + datadriven.RunTest(t, "testdata/snapshot", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + close() + + var err error + options := &Options{ + FS: vfs.NewMem(), + FormatMajorVersion: randVersion(), + } + if td.HasArg("block-size") { + var blockSize int + td.ScanArgs(t, "block-size", &blockSize) + options.Levels = make([]LevelOptions, 1) + options.Levels[0].BlockSize = blockSize + options.Levels[0].IndexBlockSize = blockSize + } + d, err = Open("", options) + if err != nil { + return err.Error() + } + snapshots = make(map[string]Reader) + + for _, line := range strings.Split(td.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + var err error + switch parts[0] { + case "set": + if len(parts) != 3 { + return fmt.Sprintf("%s expects 2 arguments", parts[0]) + } + err = d.Set([]byte(parts[1]), []byte(parts[2]), nil) + case "del": + if len(parts) != 2 { + return fmt.Sprintf("%s expects 1 argument", parts[0]) + } + err = d.Delete([]byte(parts[1]), nil) + case "merge": + if len(parts) != 3 { + return fmt.Sprintf("%s expects 2 arguments", parts[0]) + } + err = d.Merge([]byte(parts[1]), []byte(parts[2]), nil) + case "snapshot": + if len(parts) != 2 { + return fmt.Sprintf("%s expects 1 argument", parts[0]) + } + snapshots[parts[1]] = newSnapshot(d) + case "compact": + if len(parts) != 2 { + return fmt.Sprintf("%s expects 1 argument", parts[0]) + } + keys := strings.Split(parts[1], "-") + if len(keys) != 2 { + return fmt.Sprintf("malformed key range: %s", parts[1]) + } + err = d.Compact([]byte(keys[0]), []byte(keys[1]), false) + default: + return fmt.Sprintf("unknown op: %s", parts[0]) + } + if err != nil { + return err.Error() + } + } + return "" + + case "db-state": + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "iter": + var iter *Iterator + if len(td.CmdArgs) == 1 { + if td.CmdArgs[0].Key != "snapshot" { + return fmt.Sprintf("unknown argument: %s", td.CmdArgs[0]) + } + if len(td.CmdArgs[0].Vals) != 1 { + return fmt.Sprintf("%s expects 1 value: %s", td.CmdArgs[0].Key, td.CmdArgs[0]) + } + name := td.CmdArgs[0].Vals[0] + snapshot := snapshots[name] + if snapshot == nil { + return fmt.Sprintf("unable to find snapshot \"%s\"", name) + } + iter, _ = snapshot.NewIter(nil) + } else { + iter, _ = d.NewIter(nil) + } + defer iter.Close() + + var b bytes.Buffer + for _, line := range strings.Split(td.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + switch parts[0] { + case "first": + iter.First() + case "last": + iter.Last() + case "seek-ge": + if len(parts) != 2 { + return "seek-ge \n" + } + iter.SeekGE([]byte(strings.TrimSpace(parts[1]))) + case "seek-lt": + if len(parts) != 2 { + return "seek-lt \n" + } + iter.SeekLT([]byte(strings.TrimSpace(parts[1]))) + case "next": + iter.Next() + case "prev": + iter.Prev() + default: + return fmt.Sprintf("unknown op: %s", parts[0]) + } + if iter.Valid() { + fmt.Fprintf(&b, "%s:%s\n", iter.Key(), iter.Value()) + } else if err := iter.Error(); err != nil { + fmt.Fprintf(&b, "err=%v\n", err) + } else { + fmt.Fprintf(&b, ".\n") + } + } + return b.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestSnapshot(t *testing.T) { + testSnapshotImpl(t, func(d *DB) Reader { + return d.NewSnapshot() + }) +} + +func TestEventuallyFileOnlySnapshot(t *testing.T) { + testSnapshotImpl(t, func(d *DB) Reader { + // NB: all keys in testdata/snapshot fall within the ASCII keyrange a-z. + return d.NewEventuallyFileOnlySnapshot([]KeyRange{{Start: []byte("a"), End: []byte("z")}}) + }) +} + +func TestSnapshotClosed(t *testing.T) { + d, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + + catch := func(f func()) (err error) { + defer func() { + if r := recover(); r != nil { + err = r.(error) + } + }() + f() + return nil + } + + snap := d.NewSnapshot() + require.NoError(t, snap.Close()) + require.True(t, errors.Is(catch(func() { _ = snap.Close() }), ErrClosed)) + require.True(t, errors.Is(catch(func() { _, _, _ = snap.Get(nil) }), ErrClosed)) + require.True(t, errors.Is(catch(func() { snap.NewIter(nil) }), ErrClosed)) + + require.NoError(t, d.Close()) +} + +func TestSnapshotRangeDeletionStress(t *testing.T) { + const runs = 200 + const middleKey = runs * runs + + d, err := Open("", &Options{ + FS: vfs.NewMem(), + }) + require.NoError(t, err) + + mkkey := func(k int) []byte { + return []byte(fmt.Sprintf("%08d", k)) + } + v := []byte("hello world") + + snapshots := make([]*Snapshot, 0, runs) + for r := 0; r < runs; r++ { + // We use a keyspace that is 2*runs*runs wide. In other words there are + // 2*runs sections of the keyspace, each with runs elements. On every + // run, we write to the r-th element of each section of the keyspace. + for i := 0; i < 2*runs; i++ { + err := d.Set(mkkey(runs*i+r), v, nil) + require.NoError(t, err) + } + + // Now we delete some of the keyspace through a DeleteRange. We delete from + // the middle of the keyspace outwards. The keyspace is made of 2*runs + // sections, and we delete an additional two of these sections per run. + err := d.DeleteRange(mkkey(middleKey-runs*r), mkkey(middleKey+runs*r), nil) + require.NoError(t, err) + + snapshots = append(snapshots, d.NewSnapshot()) + } + + // Check that all the snapshots contain the expected number of keys. + // Iterating over so many keys is slow, so do it in parallel. + var wg sync.WaitGroup + sem := make(chan struct{}, runtime.GOMAXPROCS(0)) + for r := range snapshots { + wg.Add(1) + sem <- struct{}{} + go func(r int) { + defer func() { + <-sem + wg.Done() + }() + + // Count the keys at this snapshot. + iter, _ := snapshots[r].NewIter(nil) + var keysFound int + for iter.First(); iter.Valid(); iter.Next() { + keysFound++ + } + err := firstError(iter.Error(), iter.Close()) + if err != nil { + t.Error(err) + return + } + + // At the time that this snapshot was taken, (r+1)*2*runs unique keys + // were Set (one in each of the 2*runs sections per run). But this + // run also deleted the 2*r middlemost sections. When this snapshot + // was taken, a Set to each of those sections had been made (r+1) + // times, so 2*r*(r+1) previously-set keys are now deleted. + + keysExpected := (r+1)*2*runs - 2*r*(r+1) + if keysFound != keysExpected { + t.Errorf("%d: found %d keys, want %d", r, keysFound, keysExpected) + } + if err := snapshots[r].Close(); err != nil { + t.Error(err) + } + }(r) + } + wg.Wait() + require.NoError(t, d.Close()) +} + +// TestNewSnapshotRace tests atomicity of NewSnapshot. +// +// It tests for a regression of a previous race condition in which NewSnapshot +// would retrieve the visible sequence number for a new snapshot before +// locking the database mutex to add the snapshot. A write and flush that +// that occurred between the reading of the sequence number and appending the +// snapshot could drop keys required by the snapshot. +func TestNewSnapshotRace(t *testing.T) { + const runs = 10 + d, err := Open("", &Options{FS: vfs.NewMem()}) + require.NoError(t, err) + + v := []byte(`foo`) + ch := make(chan string) + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + for k := range ch { + if err := d.Set([]byte(k), v, nil); err != nil { + t.Error(err) + return + } + if err := d.Flush(); err != nil { + t.Error(err) + return + } + } + }() + for i := 0; i < runs; i++ { + // This main test goroutine sets `k` before creating a new snapshot. + // The key `k` should always be present within the snapshot. + k := fmt.Sprintf("key%06d", i) + require.NoError(t, d.Set([]byte(k), v, nil)) + + // Lock d.mu in another goroutine so that our call to NewSnapshot + // will need to contend for d.mu. + wg.Add(1) + locked := make(chan struct{}) + go func() { + defer wg.Done() + d.mu.Lock() + close(locked) + time.Sleep(20 * time.Millisecond) + d.mu.Unlock() + }() + <-locked + + // Tell the other goroutine to overwrite `k` with a later sequence + // number. It's indeterminate which key we'll read, but we should + // always read one of them. + ch <- k + s := d.NewSnapshot() + _, c, err := s.Get([]byte(k)) + require.NoError(t, err) + require.NoError(t, c.Close()) + require.NoError(t, s.Close()) + } + close(ch) + wg.Wait() + require.NoError(t, d.Close()) +} diff --git a/pebble/sstable/block.go b/pebble/sstable/block.go new file mode 100644 index 0000000..9634d2d --- /dev/null +++ b/pebble/sstable/block.go @@ -0,0 +1,1863 @@ +// Copyright 2018 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" + "encoding/binary" + "unsafe" + + "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/manual" + "github.com/cockroachdb/pebble/internal/rangedel" + "github.com/cockroachdb/pebble/internal/rangekey" +) + +func uvarintLen(v uint32) int { + i := 0 + for v >= 0x80 { + v >>= 7 + i++ + } + return i + 1 +} + +type blockWriter struct { + restartInterval int + nEntries int + nextRestart int + buf []byte + // For datablocks in TableFormatPebblev3, we steal the most significant bit + // in restarts for encoding setHasSameKeyPrefixSinceLastRestart. This leaves + // us with 31 bits, which is more than enough (no one needs > 2GB blocks). + // Typically, restarts occur every 16 keys, and by storing this bit with the + // restart, we can optimize for the case where a user wants to skip to the + // next prefix which happens to be in the same data block, but is > 16 keys + // away. We have seen production situations with 100+ versions per MVCC key + // (which share the same prefix). Additionally, for such writers, the prefix + // compression of the key, that shares the key with the preceding key, is + // limited to the prefix part of the preceding key -- this ensures that when + // doing NPrefix (see blockIter) we don't need to assemble the full key + // for each step since by limiting the length of the shared key we are + // 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 + // 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 + prevKey []byte + tmp [4]byte + // We don't know the state of the sets that were at the end of the previous + // block, so this is initially 0. It may be true for the second and later + // restarts in a block. Not having inter-block information is fine since we + // will optimize by stepping through restarts only within the same block. + // Note that the first restart is the first key in the block. + setHasSameKeyPrefixSinceLastRestart bool +} + +func (w *blockWriter) clear() { + *w = blockWriter{ + buf: w.buf[:0], + restarts: w.restarts[:0], + curKey: w.curKey[:0], + curValue: w.curValue[:0], + prevKey: w.prevKey[:0], + } +} + +// MaximumBlockSize is an extremely generous maximum block size of 256MiB. We +// explicitly place this limit to reserve a few bits in the restart for +// internal use. +const MaximumBlockSize = 1 << 28 +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, + value []byte, + maxSharedKeyLen int, + addValuePrefix bool, + valuePrefix valuePrefix, + setHasSameKeyPrefix bool, +) { + shared := 0 + if !setHasSameKeyPrefix { + w.setHasSameKeyPrefixSinceLastRestart = false + } + if w.nEntries == w.nextRestart { + w.nextRestart = w.nEntries + w.restartInterval + restart := uint32(len(w.buf)) + if w.setHasSameKeyPrefixSinceLastRestart { + restart = restart | setHasSameKeyPrefixRestartMask + } + w.setHasSameKeyPrefixSinceLastRestart = true + w.restarts = append(w.restarts, restart) + } else { + // TODO(peter): Manually inlined version of base.SharedPrefixLen(). This + // is 3% faster on BenchmarkWriter on go1.16. Remove if future versions + // show this to not be a performance win. For now, functions that use of + // unsafe cannot be inlined. + n := maxSharedKeyLen + if n > len(w.prevKey) { + n = len(w.prevKey) + } + asUint64 := func(b []byte, i int) uint64 { + return binary.LittleEndian.Uint64(b[i:]) + } + for shared < n-7 && asUint64(w.curKey, shared) == asUint64(w.prevKey, shared) { + shared += 8 + } + for shared < n && w.curKey[shared] == w.prevKey[shared] { + shared++ + } + } + + lenValuePlusOptionalPrefix := len(value) + if addValuePrefix { + lenValuePlusOptionalPrefix++ + } + needed := 3*binary.MaxVarintLen32 + len(w.curKey[shared:]) + lenValuePlusOptionalPrefix + n := len(w.buf) + if cap(w.buf) < n+needed { + newCap := 2 * cap(w.buf) + if newCap == 0 { + newCap = 1024 + } + for newCap < n+needed { + newCap *= 2 + } + newBuf := make([]byte, n, newCap) + copy(newBuf, w.buf) + w.buf = newBuf + } + w.buf = w.buf[:n+needed] + + // TODO(peter): Manually inlined versions of binary.PutUvarint(). This is 15% + // faster on BenchmarkWriter on go1.13. Remove if go1.14 or future versions + // show this to not be a performance win. + { + x := uint32(shared) + for x >= 0x80 { + w.buf[n] = byte(x) | 0x80 + x >>= 7 + n++ + } + w.buf[n] = byte(x) + n++ + } + + { + x := uint32(keySize - shared) + for x >= 0x80 { + w.buf[n] = byte(x) | 0x80 + x >>= 7 + n++ + } + w.buf[n] = byte(x) + n++ + } + + { + x := uint32(lenValuePlusOptionalPrefix) + for x >= 0x80 { + w.buf[n] = byte(x) | 0x80 + x >>= 7 + n++ + } + w.buf[n] = byte(x) + n++ + } + + n += copy(w.buf[n:], w.curKey[shared:]) + if addValuePrefix { + w.buf[n : n+1][0] = byte(valuePrefix) + n++ + } + n += copy(w.buf[n:], value) + w.buf = w.buf[:n] + + w.curValue = w.buf[n-len(value):] + + w.nEntries++ +} + +func (w *blockWriter) add(key InternalKey, value []byte) { + w.addWithOptionalValuePrefix( + 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, + valuePrefix valuePrefix, + setHasSameKeyPrefix bool, +) { + w.curKey, w.prevKey = w.prevKey, w.curKey + + size := key.Size() + if cap(w.curKey) < size { + 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( + size, value, maxSharedKeyLen, addValuePrefix, valuePrefix, setHasSameKeyPrefix) +} + +func (w *blockWriter) finish() []byte { + // Write the restart points to the buffer. + if w.nEntries == 0 { + // Every block must have at least one restart point. + if cap(w.restarts) > 0 { + w.restarts = w.restarts[:1] + w.restarts[0] = 0 + } else { + w.restarts = append(w.restarts, 0) + } + } + tmp4 := w.tmp[:4] + for _, x := range w.restarts { + binary.LittleEndian.PutUint32(tmp4, x) + w.buf = append(w.buf, tmp4...) + } + binary.LittleEndian.PutUint32(tmp4, uint32(len(w.restarts))) + w.buf = append(w.buf, tmp4...) + result := w.buf + + // Reset the block state. + w.nEntries = 0 + w.nextRestart = 0 + w.buf = w.buf[:0] + w.restarts = w.restarts[:0] + return result +} + +// emptyBlockSize holds the size of an empty block. Every block ends +// in a uint32 trailer encoding the number of restart points within the +// block. +const emptyBlockSize = 4 + +func (w *blockWriter) estimatedSize() int { + return len(w.buf) + 4*len(w.restarts) + emptyBlockSize +} + +type blockEntry struct { + offset int32 + keyStart int32 + keyEnd int32 + valStart int32 + valSize int32 +} + +// blockIter is an iterator over a single block of data. +// +// A blockIter provides an additional guarantee around key stability when a +// block has a restart interval of 1 (i.e. when there is no prefix +// compression). Key stability refers to whether the InternalKey.UserKey bytes +// returned by a positioning call will remain stable after a subsequent +// positioning call. The normal case is that a positioning call will invalidate +// any previously returned InternalKey.UserKey. If a block has a restart +// interval of 1 (no prefix compression), blockIter guarantees that +// InternalKey.UserKey will point to the key as stored in the block itself +// which will remain valid until the blockIter is closed. The key stability +// guarantee is used by the range tombstone and range key code, which knows that +// the respective blocks are always encoded with a restart interval of 1. This +// per-block key stability guarantee is sufficient for range tombstones and +// range deletes as they are always encoded in a single block. +// +// A blockIter also provides a value stability guarantee for range deletions and +// range keys since there is only a single range deletion and range key block +// per sstable and the blockIter will not release the bytes for the block until +// it is closed. +// +// Note on why blockIter knows about lazyValueHandling: +// +// blockIter's positioning functions (that return a LazyValue), are too +// complex to inline even prior to lazyValueHandling. blockIter.Next and +// blockIter.First were by far the cheapest and had costs 195 and 180 +// respectively, which exceeds the budget of 80. We initially tried to keep +// the lazyValueHandling logic out of blockIter by wrapping it with a +// lazyValueDataBlockIter. singleLevelIter and twoLevelIter would use this +// wrapped iter. The functions in lazyValueDataBlockIter were simple, in that +// they called the corresponding blockIter func and then decided whether the +// value was in fact in-place (so return immediately) or needed further +// handling. But these also turned out too costly for mid-stack inlining since +// simple calls like the following have a high cost that is barely under the +// budget of 80 +// +// k, v := i.data.SeekGE(key, flags) // cost 74 +// k, v := i.data.Next() // cost 72 +// +// We have 2 options for minimizing performance regressions: +// - Include the lazyValueHandling logic in the already non-inlineable +// blockIter functions: Since most of the time is spent in data block iters, +// it is acceptable to take the small hit of unnecessary branching (which +// hopefully branch prediction will predict correctly) for other kinds of +// blocks. +// - Duplicate the logic of singleLevelIterator and twoLevelIterator for the +// v3 sstable and only use the aforementioned lazyValueDataBlockIter for a +// v3 sstable. We would want to manage these copies via code generation. +// +// We have picked the first option here. +type blockIter struct { + cmp Compare + // offset is the byte index that marks where the current key/value is + // encoded in the block. + offset int32 + // nextOffset is the byte index where the next key/value is encoded in the + // block. + nextOffset int32 + // A "restart point" in a block is a point where the full key is encoded, + // instead of just having a suffix of the key encoded. See readEntry() for + // how prefix compression of keys works. Keys in between two restart points + // only have a suffix encoded in the block. When restart interval is 1, no + // prefix compression of keys happens. This is the case with range tombstone + // blocks. + // + // All restart offsets are listed in increasing order in + // i.ptr[i.restarts:len(block)-4], while numRestarts is encoded in the last + // 4 bytes of the block as a uint32 (i.ptr[len(block)-4:]). i.restarts can + // therefore be seen as the point where data in the block ends, and a list + // of offsets of all restart points begins. + restarts int32 + // Number of restart points in this block. Encoded at the end of the block + // as a uint32. + numRestarts int32 + globalSeqNum uint64 + ptr unsafe.Pointer + data []byte + // key contains the raw key the iterator is currently pointed at. This may + // point directly to data stored in the block (for a key which has no prefix + // compression), to fullKey (for a prefix compressed key), or to a slice of + // data stored in cachedBuf (during reverse iteration). + key []byte + // fullKey is a buffer used for key prefix decompression. + fullKey []byte + // val contains the value the iterator is currently pointed at. If non-nil, + // this points to a slice of the block data. + val []byte + // lazyValue is val turned into a LazyValue, whenever a positioning method + // returns a non-nil key-value pair. + lazyValue base.LazyValue + // ikey contains the decoded InternalKey the iterator is currently pointed + // at. Note that the memory backing ikey.UserKey is either data stored + // directly in the block, fullKey, or cachedBuf. The key stability guarantee + // for blocks built with a restart interval of 1 is achieved by having + // ikey.UserKey always point to data stored directly in the block. + ikey InternalKey + // cached and cachedBuf are used during reverse iteration. They are needed + // because we can't perform prefix decoding in reverse, only in the forward + // direction. In order to iterate in reverse, we decode and cache the entries + // between two restart points. + // + // Note that cached[len(cached)-1] contains the previous entry to the one the + // blockIter is currently pointed at. As usual, nextOffset will contain the + // offset of the next entry. During reverse iteration, nextOffset will be + // updated to point to offset, and we'll set the blockIter to point at the + // entry cached[len(cached)-1]. See Prev() for more details. + // + // 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 + handle bufferHandle + // for block iteration for already loaded blocks. + firstUserKey []byte + lazyValueHandling struct { + vbr *valueBlockReader + hasValuePrefix bool + } + hideObsoletePoints bool +} + +// blockIter implements the base.InternalIterator interface. +var _ base.InternalIterator = (*blockIter)(nil) + +func newBlockIter(cmp Compare, block block) (*blockIter, error) { + i := &blockIter{} + 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, 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)") + } + i.cmp = cmp + i.restarts = int32(len(block)) - 4*(1+numRestarts) + i.numRestarts = numRestarts + i.globalSeqNum = globalSeqNum + i.ptr = unsafe.Pointer(&block[0]) + 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 { + return err + } + } else { + // Block is empty. + i.firstUserKey = nil + } + return nil +} + +// 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() { + i.clearCache() + i.offset = 0 + i.nextOffset = 0 + i.restarts = 0 + i.numRestarts = 0 + i.data = nil +} + +// isDataInvalidated returns true when the blockIter has been invalidated +// using an invalidate call. NB: this is different from blockIter.Valid +// which is part of the InternalIterator implementation. +func (i *blockIter) isDataInvalidated() bool { + return i.data == nil +} + +func (i *blockIter) resetForReuse() blockIter { + return blockIter{ + fullKey: i.fullKey[:0], + cached: i.cached[:0], + cachedBuf: i.cachedBuf[:0], + data: nil, + } +} + +func (i *blockIter) readEntry() { + ptr := unsafe.Pointer(uintptr(i.ptr) + uintptr(i.offset)) + + // This is an ugly performance hack. Reading entries from blocks is one of + // the inner-most routines and decoding the 3 varints per-entry takes + // significant time. Neither go1.11 or go1.12 will inline decodeVarint for + // us, so we do it manually. This provides a 10-15% performance improvement + // on blockIter benchmarks on both go1.11 and go1.12. + // + // TODO(peter): remove this hack if go:inline is ever supported. + + var shared uint32 + if a := *((*uint8)(ptr)); a < 128 { + shared = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + shared = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + shared = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + shared = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + shared = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + var unshared uint32 + if a := *((*uint8)(ptr)); a < 128 { + unshared = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + unshared = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + unshared = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + unshared = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + unshared = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + var value uint32 + if a := *((*uint8)(ptr)); a < 128 { + value = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + value = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + value = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + value = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + value = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + unsharedKey := getBytes(ptr, int(unshared)) + // TODO(sumeer): move this into the else block below. + i.fullKey = append(i.fullKey[:shared], unsharedKey...) + if shared == 0 { + // Provide stability for the key across positioning calls if the key + // doesn't share a prefix with the previous key. This removes requiring the + // key to be copied if the caller knows the block has a restart interval of + // 1. An important example of this is range-del blocks. + i.key = unsharedKey + } else { + i.key = i.fullKey + } + ptr = unsafe.Pointer(uintptr(ptr) + uintptr(unshared)) + i.val = getBytes(ptr, int(value)) + i.nextOffset = int32(uintptr(ptr)-uintptr(i.ptr)) + int32(value) +} + +func (i *blockIter) readFirstKey() error { + ptr := i.ptr + + // This is an ugly performance hack. Reading entries from blocks is one of + // the inner-most routines and decoding the 3 varints per-entry takes + // significant time. Neither go1.11 or go1.12 will inline decodeVarint for + // us, so we do it manually. This provides a 10-15% performance improvement + // on blockIter benchmarks on both go1.11 and go1.12. + // + // TODO(peter): remove this hack if go:inline is ever supported. + + if shared := *((*uint8)(ptr)); shared == 0 { + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else { + // The shared length is != 0, which is invalid. + panic("first key in block must have zero shared length") + } + + var unshared uint32 + if a := *((*uint8)(ptr)); a < 128 { + unshared = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + unshared = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + unshared = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + unshared = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + unshared = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + // Skip the value length. + if a := *((*uint8)(ptr)); a < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a := *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); a < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if a := *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); a < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if a := *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); a < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + firstKey := getBytes(ptr, int(unshared)) + // Manually inlining base.DecodeInternalKey provides a 5-10% speedup on + // BlockIter benchmarks. + if n := len(firstKey) - 8; n >= 0 { + i.firstUserKey = firstKey[:n:n] + } else { + i.firstUserKey = nil + return base.CorruptionErrorf("pebble/table: invalid firstKey in block") + } + return nil +} + +// 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 { + 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) + } + } else { + i.ikey.Trailer = uint64(InternalKeyKindInvalid) + i.ikey.UserKey = nil + } + return hiddenPoint +} + +func (i *blockIter) clearCache() { + i.cached = i.cached[:0] + i.cachedBuf = i.cachedBuf[:0] +} + +func (i *blockIter) cacheEntry() { + var valStart int32 + valSize := int32(len(i.val)) + if valSize > 0 { + valStart = int32(uintptr(unsafe.Pointer(&i.val[0])) - uintptr(i.ptr)) + } + + i.cached = append(i.cached, blockEntry{ + offset: i.offset, + keyStart: int32(len(i.cachedBuf)), + keyEnd: int32(len(i.cachedBuf) + len(i.key)), + valStart: valStart, + valSize: valSize, + }) + 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) { + if invariants.Enabled && i.isDataInvalidated() { + panic(errors.AssertionFailedf("invalidated blockIter used")) + } + + 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. + i.offset = 0 + var index int32 + + { + // NB: manually inlined sort.Seach is ~5% faster. + // + // Define f(-1) == false and f(n) == true. + // Invariant: f(index-1) == false, f(upper) == true. + upper := i.numRestarts + for index < upper { + h := int32(uint(index+upper) >> 1) // avoid overflow when computing h + // index ≤ h < upper + offset := decodeRestart(i.data[i.restarts+4*h:]) + // For a restart point, there are 0 bytes shared with the previous key. + // The varint encoding of 0 occupies 1 byte. + ptr := unsafe.Pointer(uintptr(i.ptr) + uintptr(offset+1)) + + // Decode the key at that restart point, and compare it to the key + // sought. See the comment in readEntry for why we manually inline the + // varint decoding. + var v1 uint32 + if a := *((*uint8)(ptr)); a < 128 { + v1 = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + v1 = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + v1 = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + v1 = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + v1 = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + if *((*uint8)(ptr)) < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))) < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))) < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))) < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + // Manually inlining part of base.DecodeInternalKey provides a 5-10% + // speedup on BlockIter benchmarks. + s := getBytes(ptr, int(v1)) + var k []byte + if n := len(s) - 8; n >= 0 { + k = s[:n:n] + } + // Else k is invalid, and left as nil + + 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 + } + } + // index == upper, f(index-1) == false, and f(upper) (= f(index)) == true + // => answer is index. + } + + // 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() + hiddenPoint := i.decodeInternalKey(i.key) + + // Iterate from that restart point to somewhere >= the key sought. + if !i.valid() { + return nil, base.LazyValue{} + } + if !hiddenPoint && i.cmp(i.ikey.UserKey, key) >= 0 { + // Initialize i.lazyValue + if !i.lazyValueHandling.hasValuePrefix || + base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { + i.lazyValue = base.MakeInPlaceValue(i.val) + } else if i.lazyValueHandling.vbr == nil || !isValueHandle(valuePrefix(i.val[0])) { + i.lazyValue = base.MakeInPlaceValue(i.val[1:]) + } else { + i.lazyValue = i.lazyValueHandling.vbr.getLazyValueForPrefixAndValueHandle(i.val) + } + return &i.ikey, i.lazyValue + } + for i.Next(); i.valid(); i.Next() { + if i.cmp(i.ikey.UserKey, key) >= 0 { + // i.Next() has already initialized i.lazyValue. + return &i.ikey, i.lazyValue + } + } + return nil, base.LazyValue{} +} + +// SeekPrefixGE implements internalIterator.SeekPrefixGE, as documented in the +// pebble package. +func (i *blockIter) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + // This should never be called as prefix iteration is handled by sstable.Iterator. + panic("pebble: SeekPrefixGE unimplemented") +} + +// SeekLT implements internalIterator.SeekLT, as documented in the pebble +// package. +func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, base.LazyValue) { + if invariants.Enabled && i.isDataInvalidated() { + panic(errors.AssertionFailedf("invalidated blockIter used")) + } + + 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. + i.offset = 0 + var index int32 + + { + // NB: manually inlined sort.Search is ~5% faster. + // + // Define f(-1) == false and f(n) == true. + // Invariant: f(index-1) == false, f(upper) == true. + upper := i.numRestarts + for index < upper { + h := int32(uint(index+upper) >> 1) // avoid overflow when computing h + // index ≤ h < upper + offset := decodeRestart(i.data[i.restarts+4*h:]) + // For a restart point, there are 0 bytes shared with the previous key. + // The varint encoding of 0 occupies 1 byte. + ptr := unsafe.Pointer(uintptr(i.ptr) + uintptr(offset+1)) + + // Decode the key at that restart point, and compare it to the key + // sought. See the comment in readEntry for why we manually inline the + // varint decoding. + var v1 uint32 + if a := *((*uint8)(ptr)); a < 128 { + v1 = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + v1 = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + v1 = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + v1 = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + v1 = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + if *((*uint8)(ptr)) < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))) < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))) < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))) < 128 { + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + + // Manually inlining part of base.DecodeInternalKey provides a 5-10% + // speedup on BlockIter benchmarks. + s := getBytes(ptr, int(v1)) + var k []byte + if n := len(s) - 8; n >= 0 { + k = s[:n:n] + } + // Else k is invalid, and left as nil + + 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 + } + } + // index == upper, f(index-1) == false, and f(upper) (= f(index)) == true + // => answer is index. + } + + // 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):]) + if index < i.numRestarts { + targetOffset = decodeRestart(i.data[i.restarts+4*(index):]) + } + } else if index == 0 { + // If index == 0 then all keys in this block are larger than the key + // sought. + i.offset = -1 + i.nextOffset = 0 + return nil, base.LazyValue{} + } + + // Iterate from that restart point to somewhere >= the key sought, then back + // up to the previous entry. The expectation is that we'll be performing + // reverse iteration, so we cache the entries as we advance forward. + i.nextOffset = i.offset + + for { + i.offset = i.nextOffset + i.readEntry() + // 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) + + // 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. + return i.Prev() + } + + if i.nextOffset >= targetOffset { + // 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 + } + + i.cacheEntry() + } + + if !i.valid() { + return nil, base.LazyValue{} + } + if !i.lazyValueHandling.hasValuePrefix || + base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { + i.lazyValue = base.MakeInPlaceValue(i.val) + } else if i.lazyValueHandling.vbr == nil || !isValueHandle(valuePrefix(i.val[0])) { + i.lazyValue = base.MakeInPlaceValue(i.val[1:]) + } else { + i.lazyValue = i.lazyValueHandling.vbr.getLazyValueForPrefixAndValueHandle(i.val) + } + return &i.ikey, i.lazyValue +} + +// First implements internalIterator.First, as documented in the pebble +// package. +func (i *blockIter) First() (*InternalKey, base.LazyValue) { + if invariants.Enabled && i.isDataInvalidated() { + panic(errors.AssertionFailedf("invalidated blockIter used")) + } + + i.offset = 0 + if !i.valid() { + return nil, base.LazyValue{} + } + i.clearCache() + i.readEntry() + 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) + } else if i.lazyValueHandling.vbr == nil || !isValueHandle(valuePrefix(i.val[0])) { + i.lazyValue = base.MakeInPlaceValue(i.val[1:]) + } else { + i.lazyValue = i.lazyValueHandling.vbr.getLazyValueForPrefixAndValueHandle(i.val) + } + return &i.ikey, i.lazyValue +} + +func decodeRestart(b []byte) int32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return int32(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | + uint32(b[3]&restartMaskLittleEndianHighByteWithoutSetHasSamePrefix)<<24) +} + +// Last implements internalIterator.Last, as documented in the pebble package. +func (i *blockIter) Last() (*InternalKey, base.LazyValue) { + if invariants.Enabled && i.isDataInvalidated() { + panic(errors.AssertionFailedf("invalidated blockIter used")) + } + + // Seek forward from the last restart point. + i.offset = decodeRestart(i.data[i.restarts+4*(i.numRestarts-1):]) + if !i.valid() { + return nil, base.LazyValue{} + } + + i.readEntry() + i.clearCache() + + for i.nextOffset < i.restarts { + i.cacheEntry() + i.offset = i.nextOffset + i.readEntry() + } + + 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) + } else if i.lazyValueHandling.vbr == nil || !isValueHandle(valuePrefix(i.val[0])) { + i.lazyValue = base.MakeInPlaceValue(i.val[1:]) + } else { + i.lazyValue = i.lazyValueHandling.vbr.getLazyValueForPrefixAndValueHandle(i.val) + } + return &i.ikey, i.lazyValue +} + +// Next implements internalIterator.Next, as documented in the pebble +// package. +func (i *blockIter) Next() (*InternalKey, base.LazyValue) { + if len(i.cachedBuf) > 0 { + // We're switching from reverse iteration to forward iteration. We need to + // populate i.fullKey with the current key we're positioned at so that + // readEntry() can use i.fullKey for key prefix decompression. Note that we + // don't know whether i.key is backed by i.cachedBuf or i.fullKey (if + // SeekLT was the previous call, i.key may be backed by i.fullKey), but + // copying into i.fullKey works for both cases. + // + // TODO(peter): Rather than clearing the cache, we could instead use the + // cache until it is exhausted. This would likely be faster than falling + // through to the normal forward iteration code below. + i.fullKey = append(i.fullKey[:0], i.key...) + i.clearCache() + } + +start: + i.offset = i.nextOffset + if !i.valid() { + return nil, base.LazyValue{} + } + i.readEntry() + // Manually inlined version of i.decodeInternalKey(i.key). + if n := len(i.key) - 8; n >= 0 { + 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 + } + if !i.lazyValueHandling.hasValuePrefix || + base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { + i.lazyValue = base.MakeInPlaceValue(i.val) + } else if i.lazyValueHandling.vbr == nil || !isValueHandle(valuePrefix(i.val[0])) { + i.lazyValue = base.MakeInPlaceValue(i.val[1:]) + } else { + i.lazyValue = i.lazyValueHandling.vbr.getLazyValueForPrefixAndValueHandle(i.val) + } + return &i.ikey, i.lazyValue +} + +// NextPrefix implements (base.InternalIterator).NextPrefix. +func (i *blockIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { + if i.lazyValueHandling.hasValuePrefix { + return i.nextPrefixV3(succKey) + } + 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 +} + +func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue) { + // Doing nexts that involve a key comparison can be expensive (and the cost + // depends on the key length), so we use the same threshold of 3 that we use + // for TableFormatPebblev2 in blockIter.nextPrefix above. The next fast path + // that looks at setHasSamePrefix takes ~5ns per key, which is ~150x faster + // than doing a SeekGE within the block, so we do this 16 times + // (~5ns*16=80ns), and then switch to looking at restarts. Doing the binary + // search for the restart consumes > 100ns. If the number of versions is > + // 17, we will increment nextFastCount to 17, then do a binary search, and + // on average need to find a key between two restarts, so another 8 steps + // corresponding to nextFastCount, for a mean total of 17 + 8 = 25 such + // steps. + // + // TODO(sumeer): use the configured restartInterval for the sstable when it + // was written (which we don't currently store) instead of the default value + // of 16. + const nextCmpThresholdBeforeSeek = 3 + const nextFastThresholdBeforeRestarts = 16 + nextCmpCount := 0 + nextFastCount := 0 + usedRestarts := false + // INVARIANT: blockIter is valid. + if invariants.Enabled && !i.valid() { + panic(errors.AssertionFailedf("nextPrefixV3 called on invalid blockIter")) + } + prevKeyIsSet := i.ikey.Kind() == InternalKeyKindSet + for { + i.offset = i.nextOffset + if !i.valid() { + return nil, base.LazyValue{} + } + // Need to decode the length integers, so we can compute nextOffset. + ptr := unsafe.Pointer(uintptr(i.ptr) + uintptr(i.offset)) + // This is an ugly performance hack. Reading entries from blocks is one of + // the inner-most routines and decoding the 3 varints per-entry takes + // significant time. Neither go1.11 or go1.12 will inline decodeVarint for + // us, so we do it manually. This provides a 10-15% performance improvement + // on blockIter benchmarks on both go1.11 and go1.12. + // + // TODO(peter): remove this hack if go:inline is ever supported. + + // Decode the shared key length integer. + var shared uint32 + if a := *((*uint8)(ptr)); a < 128 { + shared = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + shared = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + shared = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + shared = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + shared = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + // Decode the unshared key length integer. + var unshared uint32 + if a := *((*uint8)(ptr)); a < 128 { + unshared = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + unshared = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + unshared = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + unshared = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + unshared = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + // Decode the value length integer. + var value uint32 + if a := *((*uint8)(ptr)); a < 128 { + value = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + value = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + value = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + value = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + value = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + // The starting position of the value. + valuePtr := unsafe.Pointer(uintptr(ptr) + uintptr(unshared)) + i.nextOffset = int32(uintptr(valuePtr)-uintptr(i.ptr)) + int32(value) + if invariants.Enabled && unshared < 8 { + // This should not happen since only the key prefix is shared, so even + // if the prefix length is the same as the user key length, the unshared + // will include the trailer. + panic(errors.AssertionFailedf("unshared %d is too small", unshared)) + } + // 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 { + panic(errors.AssertionFailedf("value is of length 0, but we expect a valuePrefix")) + } + valPrefix := *((*valuePrefix)(valuePtr)) + if setHasSamePrefix(valPrefix) { + // Fast-path. No need to assemble i.fullKey, or update i.key. We know + // that subsequent keys will not have a shared length that is greater + // than the prefix of the current key, which is also the prefix of + // i.key. Since we are continuing to iterate, we don't need to + // initialize i.ikey and i.lazyValue (these are initialized before + // returning). + nextFastCount++ + if nextFastCount > nextFastThresholdBeforeRestarts { + if usedRestarts { + // Exhausted iteration budget. This will never happen unless + // someone is using a restart interval > 16. It is just to guard + // against long restart intervals causing too much iteration. + break + } + // Haven't used restarts yet, so find the first restart at or beyond + // the current offset. + targetOffset := i.offset + var index int32 + { + // NB: manually inlined sort.Sort is ~5% faster. + // + // f defined for a restart point is true iff the offset >= + // targetOffset. + // Define f(-1) == false and f(i.numRestarts) == true. + // Invariant: f(index-1) == false, f(upper) == true. + upper := i.numRestarts + for index < upper { + h := int32(uint(index+upper) >> 1) // avoid overflow when computing h + // index ≤ h < upper + offset := decodeRestart(i.data[i.restarts+4*h:]) + if offset < targetOffset { + index = h + 1 // preserves f(index-1) == false + } else { + upper = h // preserves f(upper) == true + } + } + // index == upper, f(index-1) == false, and f(upper) (= f(index)) == true + // => answer is index. + } + usedRestarts = true + nextFastCount = 0 + if index == i.numRestarts { + // Already past the last real restart, so iterate a bit more until + // we are done with the block. + continue + } + // Have some real restarts after index. NB: index is the first + // restart at or beyond the current offset. + startingIndex := index + for index != i.numRestarts && + // The restart at index is 4 bytes written in little endian format + // starting at i.restart+4*index. The 0th byte is the least + // significant and the 3rd byte is the most significant. Since the + // most significant bit of the 3rd byte is what we use for + // encoding the set-has-same-prefix information, the indexing + // below has +3. + i.data[i.restarts+4*index+3]&restartMaskLittleEndianHighByteOnlySetHasSamePrefix != 0 { + // We still have the same prefix, so move to the next restart. + index++ + } + // index is the first restart that did not have the same prefix. + if index != startingIndex { + // Managed to skip past at least one restart. Resume iteration + // from index-1. Since nextFastCount has been reset to 0, we + // should be able to iterate to the next prefix. + i.offset = decodeRestart(i.data[i.restarts+4*(index-1):]) + i.readEntry() + } + // Else, unable to skip past any restart. Resume iteration. Since + // nextFastCount has been reset to 0, we should be able to iterate + // to the next prefix. + continue + } + continue + } else if prevKeyIsSet { + prefixChanged = true + } + } else { + prevKeyIsSet = false + } + // Slow-path cases: + // - (Likely) The prefix has changed. + // - (Unlikely) The prefix has not changed. + // We assemble the key etc. under the assumption that it is the likely + // case. + unsharedKey := getBytes(ptr, int(unshared)) + // TODO(sumeer): move this into the else block below. This is a bit tricky + // since the current logic assumes we have always copied the latest key + // into fullKey, which is why when we get to the next key we can (a) + // access i.fullKey[:shared], (b) append only the unsharedKey to + // i.fullKey. For (a), we can access i.key[:shared] since that memory is + // valid (even if unshared). For (b), we will need to remember whether + // i.key refers to i.fullKey or not, and can append the unsharedKey only + // in the former case and for the latter case need to copy the shared part + // too. This same comment applies to the other place where we can do this + // optimization, in readEntry(). + i.fullKey = append(i.fullKey[:shared], unsharedKey...) + i.val = getBytes(valuePtr, int(value)) + if shared == 0 { + // Provide stability for the key across positioning calls if the key + // doesn't share a prefix with the previous key. This removes requiring the + // key to be copied if the caller knows the block has a restart interval of + // 1. An important example of this is range-del blocks. + i.key = unsharedKey + } else { + i.key = i.fullKey + } + // Manually inlined version of i.decodeInternalKey(i.key). + hiddenPoint := false + if n := len(i.key) - 8; n >= 0 { + 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) + } + } else { + i.ikey.Trailer = uint64(InternalKeyKindInvalid) + i.ikey.UserKey = nil + } + nextCmpCount++ + if invariants.Enabled && prefixChanged && i.cmp(i.ikey.UserKey, succKey) < 0 { + panic(errors.AssertionFailedf("prefix should have changed but %x < %x", + i.ikey.UserKey, succKey)) + } + 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")) + } + if base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { + i.lazyValue = base.MakeInPlaceValue(i.val) + } else if i.lazyValueHandling.vbr == nil || !isValueHandle(valuePrefix(i.val[0])) { + i.lazyValue = base.MakeInPlaceValue(i.val[1:]) + } else { + i.lazyValue = i.lazyValueHandling.vbr.getLazyValueForPrefixAndValueHandle(i.val) + } + return &i.ikey, i.lazyValue + } + // Else prefix has not changed. + + if nextCmpCount >= nextCmpThresholdBeforeSeek { + break + } + } + return i.SeekGE(succKey, base.SeekGEFlagsNone) +} + +// Prev implements internalIterator.Prev, as documented in the pebble +// package. +func (i *blockIter) Prev() (*InternalKey, base.LazyValue) { +start: + for n := len(i.cached) - 1; n >= 0; n-- { + i.nextOffset = i.offset + e := &i.cached[n] + i.offset = e.offset + i.val = getBytes(unsafe.Pointer(uintptr(i.ptr)+uintptr(e.valStart)), int(e.valSize)) + // Manually inlined version of i.decodeInternalKey(i.key). + i.key = i.cachedBuf[e.keyStart:e.keyEnd] + if n := len(i.key) - 8; n >= 0 { + 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) + } + } else { + i.ikey.Trailer = uint64(InternalKeyKindInvalid) + i.ikey.UserKey = nil + } + i.cached = i.cached[:n] + if !i.lazyValueHandling.hasValuePrefix || + base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { + i.lazyValue = base.MakeInPlaceValue(i.val) + } else if i.lazyValueHandling.vbr == nil || !isValueHandle(valuePrefix(i.val[0])) { + i.lazyValue = base.MakeInPlaceValue(i.val[1:]) + } else { + i.lazyValue = i.lazyValueHandling.vbr.getLazyValueForPrefixAndValueHandle(i.val) + } + return &i.ikey, i.lazyValue + } + + i.clearCache() + if i.offset <= 0 { + i.offset = -1 + i.nextOffset = 0 + return nil, base.LazyValue{} + } + + targetOffset := i.offset + var index int32 + + { + // NB: manually inlined sort.Sort is ~5% faster. + // + // Define f(-1) == false and f(n) == true. + // Invariant: f(index-1) == false, f(upper) == true. + upper := i.numRestarts + for index < upper { + h := int32(uint(index+upper) >> 1) // avoid overflow when computing h + // 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 + } + } + // index == upper, f(index-1) == false, and f(upper) (= f(index)) == true + // => 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() + } + + 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) + } else if i.lazyValueHandling.vbr == nil || !isValueHandle(valuePrefix(i.val[0])) { + i.lazyValue = base.MakeInPlaceValue(i.val[1:]) + } else { + i.lazyValue = i.lazyValueHandling.vbr.getLazyValueForPrefixAndValueHandle(i.val) + } + return &i.ikey, i.lazyValue +} + +// Key implements internalIterator.Key, as documented in the pebble package. +func (i *blockIter) Key() *InternalKey { + return &i.ikey +} + +func (i *blockIter) value() base.LazyValue { + return i.lazyValue +} + +// Error implements internalIterator.Error, as documented in the pebble +// package. +func (i *blockIter) Error() error { + return nil // infallible +} + +// Close implements internalIterator.Close, as documented in the pebble +// package. +func (i *blockIter) Close() error { + i.handle.Release() + i.handle = bufferHandle{} + i.val = nil + i.lazyValue = base.LazyValue{} + i.lazyValueHandling.vbr = nil + return nil +} + +func (i *blockIter) SetBounds(lower, upper []byte) { + // This should never be called as bounds are handled by sstable.Iterator. + panic("pebble: SetBounds unimplemented") +} + +func (i *blockIter) SetContext(_ context.Context) {} + +func (i *blockIter) valid() bool { + return i.offset >= 0 && i.offset < i.restarts +} + +// fragmentBlockIter wraps a blockIter, implementing the +// keyspan.FragmentIterator interface. It's used for reading range deletion and +// range key blocks. +// +// Range deletions and range keys are fragmented before they're persisted to the +// block. Overlapping fragments have identical bounds. The fragmentBlockIter +// gathers all the fragments with identical bounds within a block and returns a +// single keyspan.Span describing all the keys defined over the span. +// +// # Memory lifetime +// +// A Span returned by fragmentBlockIter is only guaranteed to be stable until +// the next fragmentBlockIter iteration positioning method. A Span's Keys slice +// may be reused, so the user must not assume it's stable. +// +// Blocks holding range deletions and range keys are configured to use a restart +// interval of 1. This provides key stability. The caller may treat the various +// byte slices (start, end, suffix, value) as stable for the lifetime of the +// iterator. +type fragmentBlockIter struct { + blockIter blockIter + keyBuf [2]keyspan.Key + span keyspan.Span + 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 { + return fragmentBlockIter{blockIter: i.blockIter.resetForReuse()} +} + +func (i *fragmentBlockIter) decodeSpanKeys(k *InternalKey, internalValue []byte) { + // TODO(jackson): The use of i.span.Keys to accumulate keys across multiple + // calls to Decode is too confusing and subtle. Refactor to make it + // explicit. + + // decode the contents of the fragment's value. This always includes at + // least the end key: RANGEDELs store the end key directly as the value, + // whereas the various range key kinds store are more complicated. The + // details of the range key internal value format are documented within the + // internal/rangekey package. + switch k.Kind() { + case base.InternalKeyKindRangeDelete: + i.span = rangedel.Decode(*k, internalValue, i.span.Keys) + i.err = nil + case base.InternalKeyKindRangeKeySet, base.InternalKeyKindRangeKeyUnset, base.InternalKeyKindRangeKeyDelete: + i.span, i.err = rangekey.Decode(*k, internalValue, i.span.Keys) + default: + i.span = keyspan.Span{} + i.err = base.CorruptionErrorf("pebble: corrupt keyspan fragment of kind %d", k.Kind()) + } +} + +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 +// key block, they may be persisted as multiple internal keys in order to encode +// multiple sequence numbers or key kinds. +// +// gatherForward iterates forward, re-combining the fragmented internal keys to +// reconstruct a keyspan.Span that holds all the keys defined over the span. +func (i *fragmentBlockIter) gatherForward(k *InternalKey, lazyValue base.LazyValue) *keyspan.Span { + i.span = keyspan.Span{} + if k == nil || !i.blockIter.valid() { + return nil + } + i.err = nil + // Use the i.keyBuf array to back the Keys slice to prevent an allocation + // when a span contains few keys. + i.span.Keys = i.keyBuf[:0] + + // Decode the span's end key and individual keys from the value. + internalValue := lazyValue.InPlaceValue() + i.decodeSpanKeys(k, internalValue) + if i.err != nil { + return nil + } + prevEnd := i.span.End + + // There might exist additional internal keys with identical bounds encoded + // within the block. Iterate forward, accumulating all the keys with + // identical bounds to s. + k, lazyValue = i.blockIter.Next() + internalValue = lazyValue.InPlaceValue() + for k != nil && i.blockIter.cmp(k.UserKey, i.span.Start) == 0 { + i.decodeSpanKeys(k, internalValue) + if i.err != nil { + return nil + } + + // Since k indicates an equal start key, the encoded end key must + // exactly equal the original end key from the first internal key. + // Overlapping fragments are required to have exactly equal start and + // end bounds. + if i.blockIter.cmp(prevEnd, i.span.End) != 0 { + i.err = base.CorruptionErrorf("pebble: corrupt keyspan fragmentation") + i.span = keyspan.Span{} + return nil + } + 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 +} + +// gatherBackward 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 +// key block, they may be persisted as multiple internal keys in order to encode +// multiple sequence numbers or key kinds. +// +// gatherBackward iterates backwards, re-combining the fragmented internal keys +// to reconstruct a keyspan.Span that holds all the keys defined over the span. +func (i *fragmentBlockIter) gatherBackward(k *InternalKey, lazyValue base.LazyValue) *keyspan.Span { + i.span = keyspan.Span{} + if k == nil || !i.blockIter.valid() { + return nil + } + i.err = nil + // Use the i.keyBuf array to back the Keys slice to prevent an allocation + // when a span contains few keys. + i.span.Keys = i.keyBuf[:0] + + // Decode the span's end key and individual keys from the value. + internalValue := lazyValue.InPlaceValue() + i.decodeSpanKeys(k, internalValue) + if i.err != nil { + return nil + } + prevEnd := i.span.End + + // There might exist additional internal keys with identical bounds encoded + // within the block. Iterate backward, accumulating all the keys with + // identical bounds to s. + k, lazyValue = i.blockIter.Prev() + internalValue = lazyValue.InPlaceValue() + for k != nil && i.blockIter.cmp(k.UserKey, i.span.Start) == 0 { + i.decodeSpanKeys(k, internalValue) + if i.err != nil { + return nil + } + + // Since k indicates an equal start key, the encoded end key must + // exactly equal the original end key from the first internal key. + // Overlapping fragments are required to have exactly equal start and + // end bounds. + if i.blockIter.cmp(prevEnd, i.span.End) != 0 { + i.err = base.CorruptionErrorf("pebble: corrupt keyspan fragmentation") + i.span = keyspan.Span{} + return nil + } + k, lazyValue = i.blockIter.Prev() + internalValue = lazyValue.InPlaceValue() + } + // i.blockIter is positioned over the last internal key for the previous + // span. + + // 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 +} + +// Error implements (keyspan.FragmentIterator).Error. +func (i *fragmentBlockIter) Error() error { + return i.err +} + +// Close implements (keyspan.FragmentIterator).Close. +func (i *fragmentBlockIter) Close() error { + var err error + if i.closeHook != nil { + err = i.closeHook(i) + } + err = firstError(err, i.blockIter.Close()) + return err +} + +// First implements (keyspan.FragmentIterator).First +func (i *fragmentBlockIter) First() *keyspan.Span { + i.dir = +1 + return i.gatherForward(i.blockIter.First()) +} + +// Last implements (keyspan.FragmentIterator).Last. +func (i *fragmentBlockIter) Last() *keyspan.Span { + i.dir = -1 + return i.gatherBackward(i.blockIter.Last()) +} + +// Next implements (keyspan.FragmentIterator).Next. +func (i *fragmentBlockIter) Next() *keyspan.Span { + switch { + case i.dir == -1 && !i.span.Valid(): + // Switching directions. + // + // i.blockIter is exhausted, before the first key. Move onto the first. + i.blockIter.First() + i.dir = +1 + case i.dir == -1 && i.span.Valid(): + // Switching directions. + // + // i.blockIter is currently positioned over the last internal key for + // the previous span. Next it once to move to the first internal key + // that makes up the current span, and gatherForwaad to land on the + // first internal key making up the next span. + // + // In the diagram below, if the last span returned to the user during + // reverse iteration was [b,c), i.blockIter is currently positioned at + // [a,b). The block iter must be positioned over [d,e) to gather the + // next span's fragments. + // + // ... [a,b) [b,c) [b,c) [b,c) [d,e) ... + // ^ ^ + // i.blockIter want + if x := i.gatherForward(i.blockIter.Next()); invariants.Enabled && !x.Valid() { + panic("pebble: invariant violation: next entry unexpectedly invalid") + } + i.dir = +1 + } + // We know that this blockIter has in-place values. + return i.gatherForward(&i.blockIter.ikey, base.MakeInPlaceValue(i.blockIter.val)) +} + +// Prev implements (keyspan.FragmentIterator).Prev. +func (i *fragmentBlockIter) Prev() *keyspan.Span { + switch { + case i.dir == +1 && !i.span.Valid(): + // Switching directions. + // + // i.blockIter is exhausted, after the last key. Move onto the last. + i.blockIter.Last() + i.dir = -1 + case i.dir == +1 && i.span.Valid(): + // Switching directions. + // + // i.blockIter is currently positioned over the first internal key for + // the next span. Prev it once to move to the last internal key that + // makes up the current span, and gatherBackward to land on the last + // internal key making up the previous span. + // + // In the diagram below, if the last span returned to the user during + // forward iteration was [b,c), i.blockIter is currently positioned at + // [d,e). The block iter must be positioned over [a,b) to gather the + // previous span's fragments. + // + // ... [a,b) [b,c) [b,c) [b,c) [d,e) ... + // ^ ^ + // want i.blockIter + if x := i.gatherBackward(i.blockIter.Prev()); invariants.Enabled && !x.Valid() { + panic("pebble: invariant violation: previous entry unexpectedly invalid") + } + i.dir = -1 + } + // We know that this blockIter has in-place values. + return i.gatherBackward(&i.blockIter.ikey, base.MakeInPlaceValue(i.blockIter.val)) +} + +// SeekGE implements (keyspan.FragmentIterator).SeekGE. +func (i *fragmentBlockIter) SeekGE(k []byte) *keyspan.Span { + if s := i.SeekLT(k); s != nil && i.blockIter.cmp(k, s.End) < 0 { + return s + } + // TODO(jackson): If the above i.SeekLT(k) discovers a span but the span + // doesn't meet the k < s.End comparison, then there's no need for the + // SeekLT to gatherBackward. + return i.Next() +} + +// SeekLT implements (keyspan.FragmentIterator).SeekLT. +func (i *fragmentBlockIter) SeekLT(k []byte) *keyspan.Span { + i.dir = -1 + return i.gatherBackward(i.blockIter.SeekLT(k, base.SeekLTFlagsNone)) +} + +// String implements fmt.Stringer. +func (i *fragmentBlockIter) String() string { + return "fragment-block-iter" +} + +// SetCloseHook implements sstable.FragmentIterator. +func (i *fragmentBlockIter) SetCloseHook(fn func(i keyspan.FragmentIterator) error) { + i.closeHook = fn +} diff --git a/pebble/sstable/block_property.go b/pebble/sstable/block_property.go new file mode 100644 index 0000000..d85a2b2 --- /dev/null +++ b/pebble/sstable/block_property.go @@ -0,0 +1,820 @@ +// Copyright 2021 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 ( + "encoding/binary" + "fmt" + "math" + "sync" + "unsafe" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/rangekey" +) + +// Block properties are an optional user-facing feature that can be used to +// filter data blocks (and whole sstables) from an Iterator before they are +// loaded. They do not apply to range delete blocks. These are expected to +// very concisely represent a set of some attribute value contained within the +// key or value, such that the set includes all the attribute values in the +// block. This has some similarities with OLAP pruning approaches that +// maintain min-max attribute values for some column (which concisely +// represent a set), that is then used to prune at query time. In Pebble's +// case, data blocks are small, typically 25-50KB, so these properties should +// reduce their precision in order to be concise -- a good rule of thumb is to +// not consume more than 50-100 bytes across all properties maintained for a +// block, i.e., a 500x reduction compared to loading the data block. +// +// A block property must be assigned a unique name, which is encoded and +// stored in the sstable. This name must be unique among all user-properties +// encoded in an sstable. +// +// A property is represented as a []byte. A nil value or empty byte slice are +// considered semantically identical. The caller is free to choose the +// semantics of an empty byte slice e.g. they could use it to represent the +// empty set or the universal set, whichever they think is more common and +// therefore better to encode more concisely. The serialization of the +// property for the various Finish*() calls in a BlockPropertyCollector +// implementation should be identical, since the corresponding +// BlockPropertyFilter implementation is not told the context in which it is +// deserializing the property. +// +// Block properties are more general than table properties and should be +// preferred over using table properties. A BlockPropertyCollector can achieve +// identical behavior to table properties by returning the nil slice from +// FinishDataBlock and FinishIndexBlock, and interpret them as the universal +// set in BlockPropertyFilter, and return a non-universal set in FinishTable. +// +// Block property filtering is nondeterministic because the separation of keys +// into blocks is nondeterministic. Clients use block-property filters to +// implement efficient application of a filter F that applies to key-value pairs +// (abbreviated as kv-filter). Consider correctness defined as surfacing exactly +// the same key-value pairs that would be surfaced if one applied the filter F +// above normal iteration. With this correctness definition, block property +// filtering may introduce two kinds of errors: +// +// a) Block property filtering that uses a kv-filter may produce additional +// key-value pairs that don't satisfy the filter because of the separation +// of keys into blocks. Clients may remove these extra key-value pairs by +// re-applying the kv filter while reading results back from Pebble. +// +// b) Block property filtering may surface deleted key-value pairs if the +// kv filter is not a strict function of the key's user key. A block +// containing k.DEL may be filtered, while a block containing the deleted +// key k.SET may not be filtered, if the kv filter applies to one but not +// the other. +// +// This error may be avoided trivially by using a kv filter that is a pure +// function of the user key. A filter that examines values or key kinds +// requires care to ensure F(k.SET, ) = F(k.DEL) = F(k.SINGLEDEL). +// +// The combination of range deletions and filtering by table-level properties +// add another opportunity for deleted point keys to be surfaced. The pebble +// Iterator stack takes care to correctly apply filtered tables' range deletions +// to lower tables, preventing this form of nondeterministic error. +// +// In addition to the non-determinism discussed in (b), which limits the use +// of properties over values, we now have support for values that are not +// stored together with the key, and may not even be retrieved during +// compactions. If Pebble is configured with such value separation, block +// properties must only apply to the key, and will be provided a nil value. + +// BlockPropertyCollector is used when writing a sstable. +// +// - All calls to Add are included in the next FinishDataBlock, after which +// the next data block is expected to start. +// +// - The index entry generated for the data block, which contains the return +// value from FinishDataBlock, is not immediately included in the current +// index block. It is included when AddPrevDataBlockToIndexBlock is called. +// An alternative would be to return an opaque handle from FinishDataBlock +// and pass it to a new AddToIndexBlock method, which requires more +// plumbing, and passing of an interface{} results in a undesirable heap +// allocation. AddPrevDataBlockToIndexBlock must be called before keys are +// added to the new data block. +type BlockPropertyCollector interface { + // Name returns the name of the block property collector. + Name() string + // Add is called with each new entry added to a data block in the sstable. + // The callee can assume that these are in sorted order. + Add(key InternalKey, value []byte) error + // FinishDataBlock is called when all the entries have been added to a + // data block. Subsequent Add calls will be for the next data block. It + // returns the property value for the finished block. + FinishDataBlock(buf []byte) ([]byte, error) + // AddPrevDataBlockToIndexBlock adds the entry corresponding to the + // previous FinishDataBlock to the current index block. + AddPrevDataBlockToIndexBlock() + // FinishIndexBlock is called when an index block, containing all the + // key-value pairs since the last FinishIndexBlock, will no longer see new + // entries. It returns the property value for the index block. + FinishIndexBlock(buf []byte) ([]byte, error) + // FinishTable is called when the sstable is finished, and returns the + // property value for the sstable. + FinishTable(buf []byte) ([]byte, error) +} + +// SuffixReplaceableBlockCollector is an extension to the BlockPropertyCollector +// 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. +// +// A collector which supports being updated in such cases must be able to derive +// its updated value from its old value and the change being made to the suffix, +// without needing to be passed each updated K/V. +// +// For example, a collector that only inspects values would can simply copy its +// previously computed property as-is, since key-suffix replacement does not +// change values, while a collector that depends only on key suffixes, like one +// which collected mvcc-timestamp bounds from timestamp-suffixed keys, can just +// set its new bounds from the new suffix, as it is common to all keys, without +// needing to recompute it from every key. +// +// An implementation of DataBlockIntervalCollector can also implement this +// interface, in which case the BlockPropertyCollector returned by passing it to +// NewBlockIntervalCollector will also implement this interface automatically. +type SuffixReplaceableBlockCollector interface { + // UpdateKeySuffixes is called when a block is updated to change the suffix of + // all keys in the block, and is passed the old value for that prop, if any, + // for that block as well as the old and new suffix. + UpdateKeySuffixes(oldProp []byte, oldSuffix, newSuffix []byte) error +} + +// BlockPropertyFilter is used in an Iterator to filter sstables and blocks +// within the sstable. It should not maintain any per-sstable state, and must +// be thread-safe. +type BlockPropertyFilter = base.BlockPropertyFilter + +// BoundLimitedBlockPropertyFilter implements the block-property filter but +// imposes an additional constraint on its usage, requiring that only blocks +// containing exclusively keys between its lower and upper bounds may be +// filtered. The bounds may be change during iteration, so the filter doesn't +// expose the bounds, instead implementing KeyIsWithin[Lower,Upper]Bound methods +// for performing bound comparisons. +// +// To be used, a BoundLimitedBlockPropertyFilter must be supplied directly +// through NewBlockPropertiesFilterer's dedicated parameter. If supplied through +// the ordinary slice of block property filters, this filter's bounds will be +// ignored. +// +// The current [lower,upper) bounds of the filter are unknown, because they may +// be changing. During forward iteration the lower bound is externally +// guaranteed, meaning Intersects only returns false if the sstable iterator is +// already known to be positioned at a key ≥ lower. The sstable iterator is then +// only responsible for ensuring filtered blocks also meet the upper bound, and +// should only allow a block to be filtered if all its keys are < upper. The +// sstable iterator may invoke KeyIsWithinUpperBound(key) to perform this check, +// where key is an inclusive upper bound on the block's keys. +// +// During backward iteration the upper bound is externally guaranteed, and +// Intersects only returns false if the sstable iterator is already known to be +// positioned at a key < upper. The sstable iterator is responsible for ensuring +// filtered blocks also meet the lower bound, enforcing that a block is only +// filtered if all its keys are ≥ lower. This check is made through passing the +// block's inclusive lower bound to KeyIsWithinLowerBound. +// +// Implementations may become active or inactive through implementing Intersects +// to return true whenever the filter is disabled. +// +// Usage of BoundLimitedBlockPropertyFilter is subtle, and Pebble consumers +// should not implement this interface directly. This interface is an internal +// detail in the implementation of block-property range-key masking. +type BoundLimitedBlockPropertyFilter interface { + BlockPropertyFilter + + // KeyIsWithinLowerBound tests whether the provided internal key falls + // within the current lower 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 upper + // bound. + 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 []byte) bool +} + +// BlockIntervalCollector is a helper implementation of BlockPropertyCollector +// for users who want to represent a set of the form [lower,upper) where both +// lower and upper are uint64, and lower <= upper. +// +// The set is encoded as: +// - Two varint integers, (lower,upper-lower), when upper-lower > 0 +// - Nil, when upper-lower=0 +// +// Users must not expect this to preserve differences between empty sets -- +// they will all get turned into the semantically equivalent [0,0). +// +// A BlockIntervalCollector that collects over point and range keys needs to +// have both the point and range DataBlockIntervalCollector specified, since +// point and range keys are fed to the BlockIntervalCollector in an interleaved +// fashion, independently of one another. This also implies that the +// DataBlockIntervalCollectors for point and range keys should be references to +// independent instances, rather than references to the same collector, as point +// and range keys are tracked independently. +type BlockIntervalCollector struct { + name string + points DataBlockIntervalCollector + ranges DataBlockIntervalCollector + + blockInterval interval + indexInterval interval + tableInterval interval +} + +var _ BlockPropertyCollector = &BlockIntervalCollector{} + +// DataBlockIntervalCollector is the interface used by BlockIntervalCollector +// that contains the actual logic pertaining to the property. It only +// maintains state for the current data block, and resets that state in +// FinishDataBlock. This interface can be used to reduce parsing costs. +type DataBlockIntervalCollector interface { + // Add is called with each new entry added to a data block in the sstable. + // The callee can assume that these are in sorted order. + Add(key InternalKey, value []byte) error + // FinishDataBlock is called when all the entries have been added to a + // data block. Subsequent Add calls will be for the next data block. It + // returns the [lower, upper) for the finished block. + FinishDataBlock() (lower uint64, upper uint64, err error) +} + +// NewBlockIntervalCollector constructs a BlockIntervalCollector with the given +// name. The BlockIntervalCollector makes use of the given point and range key +// DataBlockIntervalCollectors when encountering point and range keys, +// respectively. +// +// The caller may pass a nil DataBlockIntervalCollector for one of the point or +// range key collectors, in which case keys of those types will be ignored. This +// allows for flexible construction of BlockIntervalCollectors that operate on +// just point keys, just range keys, or both point and range keys. +// +// If both point and range keys are to be tracked, two independent collectors +// should be provided, rather than the same collector passed in twice (see the +// comment on BlockIntervalCollector for more detail) +func NewBlockIntervalCollector( + name string, pointCollector, rangeCollector DataBlockIntervalCollector, +) BlockPropertyCollector { + if pointCollector == nil && rangeCollector == nil { + panic("sstable: at least one interval collector must be provided") + } + bic := BlockIntervalCollector{ + name: name, + points: pointCollector, + ranges: rangeCollector, + } + if _, ok := pointCollector.(SuffixReplaceableBlockCollector); ok { + return &suffixReplacementBlockCollectorWrapper{bic} + } + return &bic +} + +// Name implements the BlockPropertyCollector interface. +func (b *BlockIntervalCollector) Name() string { + return b.name +} + +// Add implements the BlockPropertyCollector interface. +func (b *BlockIntervalCollector) Add(key InternalKey, value []byte) error { + if rangekey.IsRangeKey(key.Kind()) { + if b.ranges != nil { + return b.ranges.Add(key, value) + } + } else if b.points != nil { + return b.points.Add(key, value) + } + return nil +} + +// FinishDataBlock implements the BlockPropertyCollector interface. +func (b *BlockIntervalCollector) FinishDataBlock(buf []byte) ([]byte, error) { + if b.points == nil { + return buf, nil + } + var err error + b.blockInterval.lower, b.blockInterval.upper, err = b.points.FinishDataBlock() + if err != nil { + return buf, err + } + buf = b.blockInterval.encode(buf) + b.tableInterval.union(b.blockInterval) + return buf, nil +} + +// AddPrevDataBlockToIndexBlock implements the BlockPropertyCollector +// interface. +func (b *BlockIntervalCollector) AddPrevDataBlockToIndexBlock() { + b.indexInterval.union(b.blockInterval) + b.blockInterval = interval{} +} + +// FinishIndexBlock implements the BlockPropertyCollector interface. +func (b *BlockIntervalCollector) FinishIndexBlock(buf []byte) ([]byte, error) { + buf = b.indexInterval.encode(buf) + b.indexInterval = interval{} + return buf, nil +} + +// FinishTable implements the BlockPropertyCollector interface. +func (b *BlockIntervalCollector) FinishTable(buf []byte) ([]byte, error) { + // If the collector is tracking range keys, the range key interval is union-ed + // with the point key interval for the table. + if b.ranges != nil { + var rangeInterval interval + var err error + rangeInterval.lower, rangeInterval.upper, err = b.ranges.FinishDataBlock() + if err != nil { + return buf, err + } + b.tableInterval.union(rangeInterval) + } + return b.tableInterval.encode(buf), nil +} + +type interval struct { + lower uint64 + upper uint64 +} + +func (i interval) encode(buf []byte) []byte { + if i.lower < i.upper { + var encoded [binary.MaxVarintLen64 * 2]byte + n := binary.PutUvarint(encoded[:], i.lower) + n += binary.PutUvarint(encoded[n:], i.upper-i.lower) + buf = append(buf, encoded[:n]...) + } + return buf +} + +func (i *interval) decode(buf []byte) error { + if len(buf) == 0 { + *i = interval{} + return nil + } + var n int + i.lower, n = binary.Uvarint(buf) + if n <= 0 || n >= len(buf) { + return base.CorruptionErrorf("cannot decode interval from buf %x", buf) + } + pos := n + i.upper, n = binary.Uvarint(buf[pos:]) + pos += n + if pos != len(buf) || n <= 0 { + return base.CorruptionErrorf("cannot decode interval from buf %x", buf) + } + // Delta decode. + i.upper += i.lower + if i.upper < i.lower { + return base.CorruptionErrorf("unexpected overflow, upper %d < lower %d", i.upper, i.lower) + } + return nil +} + +func (i *interval) union(x interval) { + if x.lower >= x.upper { + // x is the empty set. + return + } + if i.lower >= i.upper { + // i is the empty set. + *i = x + return + } + // Both sets are non-empty. + if x.lower < i.lower { + i.lower = x.lower + } + if x.upper > i.upper { + i.upper = x.upper + } +} + +func (i interval) intersects(x interval) bool { + if i.lower >= i.upper || x.lower >= x.upper { + // At least one of the sets is empty. + return false + } + // Neither set is empty. + return i.upper > x.lower && i.lower < x.upper +} + +type suffixReplacementBlockCollectorWrapper struct { + BlockIntervalCollector +} + +// UpdateKeySuffixes implements the SuffixReplaceableBlockCollector interface. +func (w *suffixReplacementBlockCollectorWrapper) UpdateKeySuffixes( + oldProp []byte, from, to []byte, +) error { + return w.BlockIntervalCollector.points.(SuffixReplaceableBlockCollector).UpdateKeySuffixes(oldProp, from, to) +} + +// BlockIntervalFilter is an implementation of BlockPropertyFilter when the +// corresponding collector is a BlockIntervalCollector. That is, the set is of +// the form [lower, upper). +type BlockIntervalFilter struct { + name string + filterInterval interval +} + +var _ BlockPropertyFilter = (*BlockIntervalFilter)(nil) + +// NewBlockIntervalFilter constructs a BlockPropertyFilter that filters blocks +// based on an interval property collected by BlockIntervalCollector and the +// given [lower, upper) bounds. The given name specifies the +// BlockIntervalCollector's properties to read. +func NewBlockIntervalFilter(name string, lower uint64, upper uint64) *BlockIntervalFilter { + b := new(BlockIntervalFilter) + b.Init(name, lower, upper) + return b +} + +// Init initializes (or re-initializes, clearing previous state) an existing +// BLockPropertyFilter to filter blocks based on an interval property collected +// by BlockIntervalCollector and the given [lower, upper) bounds. The given name +// specifies the BlockIntervalCollector's properties to read. +func (b *BlockIntervalFilter) Init(name string, lower, upper uint64) { + *b = BlockIntervalFilter{ + name: name, + filterInterval: interval{lower: lower, upper: upper}, + } +} + +// Name implements the BlockPropertyFilter interface. +func (b *BlockIntervalFilter) Name() string { + return b.name +} + +// Intersects implements the BlockPropertyFilter interface. +func (b *BlockIntervalFilter) Intersects(prop []byte) (bool, error) { + var i interval + if err := i.decode(prop); err != nil { + return false, err + } + return i.intersects(b.filterInterval), nil +} + +// SetInterval adjusts the [lower, upper) bounds used by the filter. It is not +// generally safe to alter the filter while it's in use, except as part of the +// implementation of BlockPropertyFilterMask.SetSuffix used for range-key +// masking. +func (b *BlockIntervalFilter) SetInterval(lower, upper uint64) { + b.filterInterval = interval{lower: lower, upper: upper} +} + +// When encoding block properties for each block, we cannot afford to encode +// the name. Instead, the name is mapped to a shortID, in the scope of that +// sstable, and the shortID is encoded. Since we use a uint8, there is a limit +// of 256 block property collectors per sstable. +type shortID uint8 + +type blockPropertiesEncoder struct { + propsBuf []byte + scratch []byte +} + +func (e *blockPropertiesEncoder) getScratchForProp() []byte { + return e.scratch[:0] +} + +func (e *blockPropertiesEncoder) resetProps() { + e.propsBuf = e.propsBuf[:0] +} + +func (e *blockPropertiesEncoder) addProp(id shortID, scratch []byte) { + const lenID = 1 + lenProp := uvarintLen(uint32(len(scratch))) + n := lenID + lenProp + len(scratch) + if cap(e.propsBuf)-len(e.propsBuf) < n { + size := len(e.propsBuf) + 2*n + if size < 2*cap(e.propsBuf) { + size = 2 * cap(e.propsBuf) + } + buf := make([]byte, len(e.propsBuf), size) + copy(buf, e.propsBuf) + e.propsBuf = buf + } + pos := len(e.propsBuf) + b := e.propsBuf[pos : pos+lenID] + b[0] = byte(id) + pos += lenID + b = e.propsBuf[pos : pos+lenProp] + n = binary.PutUvarint(b, uint64(len(scratch))) + pos += n + b = e.propsBuf[pos : pos+len(scratch)] + pos += len(scratch) + copy(b, scratch) + e.propsBuf = e.propsBuf[0:pos] + e.scratch = scratch +} + +func (e *blockPropertiesEncoder) unsafeProps() []byte { + return e.propsBuf +} + +func (e *blockPropertiesEncoder) props() []byte { + buf := make([]byte, len(e.propsBuf)) + copy(buf, e.propsBuf) + return buf +} + +type blockPropertiesDecoder struct { + props []byte +} + +func (d *blockPropertiesDecoder) done() bool { + return len(d.props) == 0 +} + +// REQUIRES: !done() +func (d *blockPropertiesDecoder) next() (id shortID, prop []byte, err error) { + const lenID = 1 + id = shortID(d.props[0]) + propLen, m := binary.Uvarint(d.props[lenID:]) + n := lenID + m + if m <= 0 || propLen == 0 || (n+int(propLen)) > len(d.props) { + return 0, nil, base.CorruptionErrorf("corrupt block property length") + } + prop = d.props[n : n+int(propLen)] + d.props = d.props[n+int(propLen):] + return id, prop, nil +} + +// BlockPropertiesFilterer provides filtering support when reading an sstable +// in the context of an iterator that has a slice of BlockPropertyFilters. +// After the call to NewBlockPropertiesFilterer, the caller must call +// IntersectsUserPropsAndFinishInit to check if the sstable intersects with +// the filters. If it does intersect, this function also finishes initializing +// the BlockPropertiesFilterer using the shortIDs for the relevant filters. +// Subsequent checks for relevance of a block should use the intersects +// method. +type BlockPropertiesFilterer struct { + filters []BlockPropertyFilter + // Maps shortID => index in filters. This can be sparse, and shortIDs for + // which there is no filter are represented with an index of -1. The + // length of this can be shorter than the shortIDs allocated in the + // sstable. e.g. if the sstable used shortIDs 0, 1, 2, 3, and the iterator + // has two filters, corresponding to shortIDs 2, 0, this would be: + // len(shortIDToFiltersIndex)==3, 0=>1, 1=>-1, 2=>0. + shortIDToFiltersIndex []int + + // boundLimitedFilter, if non-nil, holds a single block-property filter with + // additional constraints on its filtering. A boundLimitedFilter may only + // filter blocks that are wholly contained within its bounds. During forward + // iteration the lower bound (and during backward iteration the upper bound) + // must be externally guaranteed, with Intersects only returning false if + // that bound is met. The opposite bound is verified during iteration by the + // sstable iterator. + // + // boundLimitedFilter is permitted to be defined on a property (`Name()`) + // for which another filter exists in filters. In this case both filters + // will be consulted, and either filter may exclude block(s). Only a single + // bound-limited block-property filter may be set. + // + // The boundLimitedShortID field contains the shortID of the filter's + // property within the sstable. It's set to -1 if the property was not + // collected when the table was built. + boundLimitedFilter BoundLimitedBlockPropertyFilter + boundLimitedShortID int +} + +var blockPropertiesFiltererPool = sync.Pool{ + New: func() interface{} { + return &BlockPropertiesFilterer{} + }, +} + +// newBlockPropertiesFilterer returns a partially initialized filterer. To complete +// initialization, call IntersectsUserPropsAndFinishInit. +func newBlockPropertiesFilterer( + filters []BlockPropertyFilter, limited BoundLimitedBlockPropertyFilter, +) *BlockPropertiesFilterer { + filterer := blockPropertiesFiltererPool.Get().(*BlockPropertiesFilterer) + *filterer = BlockPropertiesFilterer{ + filters: filters, + shortIDToFiltersIndex: filterer.shortIDToFiltersIndex[:0], + boundLimitedFilter: limited, + boundLimitedShortID: -1, + } + return filterer +} + +func releaseBlockPropertiesFilterer(filterer *BlockPropertiesFilterer) { + *filterer = BlockPropertiesFilterer{ + shortIDToFiltersIndex: filterer.shortIDToFiltersIndex[:0], + } + blockPropertiesFiltererPool.Put(filterer) +} + +// IntersectsTable evaluates the provided block-property filter against the +// provided set of table-level properties. If there is no intersection between +// the filters and the table or an error is encountered, IntersectsTable returns +// a nil filterer (and possibly an error). If there is an intersection, +// IntersectsTable returns a non-nil filterer that may be used by an iterator +// reading the table. +func IntersectsTable( + filters []BlockPropertyFilter, + limited BoundLimitedBlockPropertyFilter, + userProperties map[string]string, +) (*BlockPropertiesFilterer, error) { + f := newBlockPropertiesFilterer(filters, limited) + ok, err := f.intersectsUserPropsAndFinishInit(userProperties) + if !ok || err != nil { + releaseBlockPropertiesFilterer(f) + return nil, err + } + return f, nil +} + +// intersectsUserPropsAndFinishInit is called with the user properties map for +// the sstable and returns whether the sstable intersects the filters. It +// additionally initializes the shortIDToFiltersIndex for the filters that are +// relevant to this sstable. +func (f *BlockPropertiesFilterer) intersectsUserPropsAndFinishInit( + userProperties map[string]string, +) (bool, error) { + for i := range f.filters { + props, ok := userProperties[f.filters[i].Name()] + if !ok { + // Collector was not used when writing this file, so it is + // considered intersecting. + continue + } + if len(props) < 1 { + return false, base.CorruptionErrorf( + "block properties for %s is corrupted", f.filters[i].Name()) + } + 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. + n := len(f.shortIDToFiltersIndex) + if n <= int(shortID) { + if cap(f.shortIDToFiltersIndex) <= int(shortID) { + index := make([]int, shortID+1, 2*(shortID+1)) + copy(index, f.shortIDToFiltersIndex) + f.shortIDToFiltersIndex = index + } else { + f.shortIDToFiltersIndex = f.shortIDToFiltersIndex[:shortID+1] + } + for j := n; j < int(shortID); j++ { + f.shortIDToFiltersIndex[j] = -1 + } + } + f.shortIDToFiltersIndex[shortID] = i + } + if f.boundLimitedFilter == nil { + return true, nil + } + + // There's a bound-limited filter. Find its shortID. It's possible that + // there's an existing filter in f.filters on the same property. That's + // okay. Both filters will be consulted whenever a relevant prop is decoded. + props, ok := userProperties[f.boundLimitedFilter.Name()] + if !ok { + // The collector was not used when writing this file, so it's + // intersecting. We leave f.boundLimitedShortID=-1, so the filter will + // be unused within this file. + return true, nil + } + if len(props) < 1 { + return false, base.CorruptionErrorf( + "block properties for %s is corrupted", f.boundLimitedFilter.Name()) + } + 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. + // + // NB: If a block-property filter needs to be toggled inactive/active, it + // should be implemented within the Intersects implementation. + // + // TODO(jackson): We could filter at the table-level by threading the table + // smallest and largest bounds here. + + // The bound-limited filter isn't included in shortIDToFiltersIndex. + // + // When determining intersection, we decode props only up to the shortID + // len(shortIDToFiltersIndex). If f.limitedShortID is greater than any of + // the existing filters' shortIDs, we need to grow shortIDToFiltersIndex. + // Growing the index with -1s ensures we're able to consult the index + // without length checks. + if n := len(f.shortIDToFiltersIndex); n <= f.boundLimitedShortID { + if cap(f.shortIDToFiltersIndex) <= f.boundLimitedShortID { + index := make([]int, f.boundLimitedShortID+1) + copy(index, f.shortIDToFiltersIndex) + f.shortIDToFiltersIndex = index + } else { + f.shortIDToFiltersIndex = f.shortIDToFiltersIndex[:f.boundLimitedShortID+1] + } + for j := n; j <= f.boundLimitedShortID; j++ { + f.shortIDToFiltersIndex[j] = -1 + } + } + return true, nil +} + +type intersectsResult int8 + +const ( + blockIntersects intersectsResult = iota + blockExcluded + // blockMaybeExcluded is returned by BlockPropertiesFilterer.intersects when + // no filters unconditionally exclude the block, but the bound-limited block + // property filter will exclude it if the block's bounds fall within the + // filter's current bounds. See the reader's + // {single,two}LevelIterator.resolveMaybeExcluded methods. + blockMaybeExcluded +) + +func (f *BlockPropertiesFilterer) intersects(props []byte) (ret intersectsResult, err error) { + i := 0 + decoder := blockPropertiesDecoder{props: props} + ret = blockIntersects + for i < len(f.shortIDToFiltersIndex) { + var id int + var prop []byte + if !decoder.done() { + var shortID shortID + var err error + shortID, prop, err = decoder.next() + if err != nil { + return ret, err + } + id = int(shortID) + } else { + id = math.MaxUint8 + 1 + } + for i < len(f.shortIDToFiltersIndex) && id > i { + // The property for this id is not encoded for this block, but there + // may still be a filter for this id. + if intersects, err := f.intersectsFilter(i, nil); err != nil { + return ret, err + } else if intersects == blockExcluded { + return blockExcluded, nil + } else if intersects == blockMaybeExcluded { + ret = blockMaybeExcluded + } + i++ + } + if i >= len(f.shortIDToFiltersIndex) { + return ret, nil + } + // INVARIANT: id <= i. And since i is always incremented by 1, id==i. + if id != i { + panic(fmt.Sprintf("%d != %d", id, i)) + } + if intersects, err := f.intersectsFilter(i, prop); err != nil { + return ret, err + } else if intersects == blockExcluded { + return blockExcluded, nil + } else if intersects == blockMaybeExcluded { + ret = blockMaybeExcluded + } + i++ + } + // ret == blockIntersects || ret == blockMaybeExcluded + return ret, nil +} + +func (f *BlockPropertiesFilterer) intersectsFilter(i int, prop []byte) (intersectsResult, error) { + if f.shortIDToFiltersIndex[i] >= 0 { + intersects, err := f.filters[f.shortIDToFiltersIndex[i]].Intersects(prop) + if err != nil { + return blockIntersects, err + } + if !intersects { + return blockExcluded, nil + } + } + if i == f.boundLimitedShortID { + // The bound-limited filter uses this id. + // + // The bound-limited filter only applies within a keyspan interval. We + // expect the Intersects call to be cheaper than bounds checks. If + // Intersects determines that there is no intersection, we return + // `blockMaybeExcluded` if no other bpf unconditionally excludes the + // block. + intersects, err := f.boundLimitedFilter.Intersects(prop) + if err != nil { + return blockIntersects, err + } else if !intersects { + return blockMaybeExcluded, nil + } + } + return blockIntersects, nil +} diff --git a/pebble/sstable/block_property_test.go b/pebble/sstable/block_property_test.go new file mode 100644 index 0000000..3b9bfc3 --- /dev/null +++ b/pebble/sstable/block_property_test.go @@ -0,0 +1,1487 @@ +// Copyright 2021 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" + "fmt" + "io" + "math" + "math/rand" + "sort" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/stretchr/testify/require" +) + +func TestIntervalEncodeDecode(t *testing.T) { + testCases := []struct { + name string + lower uint64 + upper uint64 + len int + }{ + { + name: "empty zero", + lower: 0, + upper: 0, + len: 0, + }, + { + name: "empty non-zero", + lower: 5, + upper: 5, + len: 0, + }, + { + name: "empty lower > upper", + lower: math.MaxUint64, + upper: math.MaxUint64 - 1, + len: 0, + }, + { + name: "small", + lower: 50, + upper: 61, + len: 2, + }, + { + name: "big", + lower: 0, + upper: math.MaxUint64, + len: 11, + }, + } + for _, tc := range testCases { + buf := make([]byte, 100) + t.Run(tc.name, func(t *testing.T) { + i1 := interval{lower: tc.lower, upper: tc.upper} + b1 := i1.encode(nil) + b2 := i1.encode(buf[:0]) + require.True(t, bytes.Equal(b1, b2), "%x != %x", b1, b2) + expectedInterval := i1 + if expectedInterval.lower >= expectedInterval.upper { + expectedInterval = interval{} + } + // Arbitrary initial value. + arbitraryInterval := interval{lower: 1000, upper: 1000} + i2 := arbitraryInterval + i2.decode(b1) + require.Equal(t, expectedInterval, i2) + i2 = arbitraryInterval + i2.decode(b2) + require.Equal(t, expectedInterval, i2) + require.Equal(t, tc.len, len(b1)) + }) + } +} + +func TestIntervalUnionIntersects(t *testing.T) { + testCases := []struct { + name string + i1 interval + i2 interval + union interval + intersects bool + }{ + { + name: "empty and empty", + i1: interval{}, + i2: interval{}, + union: interval{}, + intersects: false, + }, + { + name: "empty and empty non-zero", + i1: interval{}, + i2: interval{100, 99}, + union: interval{}, + intersects: false, + }, + { + name: "empty and non-empty", + i1: interval{}, + i2: interval{80, 100}, + union: interval{80, 100}, + intersects: false, + }, + { + name: "disjoint sets", + i1: interval{50, 60}, + i2: interval{math.MaxUint64 - 5, math.MaxUint64}, + union: interval{50, math.MaxUint64}, + intersects: false, + }, + { + name: "adjacent sets", + i1: interval{50, 60}, + i2: interval{60, 100}, + union: interval{50, 100}, + intersects: false, + }, + { + name: "overlapping sets", + i1: interval{50, 60}, + i2: interval{59, 120}, + union: interval{50, 120}, + intersects: true, + }, + } + isEmpty := func(i interval) bool { + return i.lower >= i.upper + } + // adjustUnionExpectation exists because union does not try to + // canonicalize empty sets by turning them into [0, 0), since it is + // unnecessary -- the higher level context of the BlockIntervalCollector + // will do so when calling interval.encode. + adjustUnionExpectation := func(expected interval, i1 interval, i2 interval) interval { + if isEmpty(i2) { + return i1 + } + if isEmpty(i1) { + return i2 + } + return expected + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.intersects, tc.i1.intersects(tc.i2)) + require.Equal(t, tc.intersects, tc.i2.intersects(tc.i1)) + require.Equal(t, !isEmpty(tc.i1), tc.i1.intersects(tc.i1)) + require.Equal(t, !isEmpty(tc.i2), tc.i2.intersects(tc.i2)) + union := tc.i1 + union.union(tc.i2) + require.Equal(t, adjustUnionExpectation(tc.union, tc.i1, tc.i2), union) + union = tc.i2 + union.union(tc.i1) + require.Equal(t, adjustUnionExpectation(tc.union, tc.i2, tc.i1), union) + }) + } +} + +type testDataBlockIntervalCollector struct { + i interval +} + +func (c *testDataBlockIntervalCollector) Add(key InternalKey, value []byte) error { + return nil +} + +func (c *testDataBlockIntervalCollector) FinishDataBlock() (lower uint64, upper uint64, err error) { + return c.i.lower, c.i.upper, nil +} + +func TestBlockIntervalCollector(t *testing.T) { + var points, ranges testDataBlockIntervalCollector + bic := NewBlockIntervalCollector("foo", &points, &ranges) + require.Equal(t, "foo", bic.Name()) + // Set up the point key collector with an initial (empty) interval. + points.i = interval{1, 1} + // First data block has empty point key interval. + encoded, err := bic.FinishDataBlock(nil) + require.NoError(t, err) + require.True(t, bytes.Equal(nil, encoded)) + bic.AddPrevDataBlockToIndexBlock() + // Second data block contains a point and range key interval. The latter + // should not contribute to the block interval. + points.i = interval{20, 25} + ranges.i = interval{5, 150} + encoded, err = bic.FinishDataBlock(nil) + require.NoError(t, err) + var decoded interval + require.NoError(t, decoded.decode(encoded)) + require.Equal(t, interval{20, 25}, decoded) + var encodedIndexBlock []byte + // Finish index block before including second data block. + encodedIndexBlock, err = bic.FinishIndexBlock(nil) + require.NoError(t, err) + require.True(t, bytes.Equal(nil, encodedIndexBlock)) + bic.AddPrevDataBlockToIndexBlock() + // Third data block. + points.i = interval{10, 15} + encoded, err = bic.FinishDataBlock(nil) + require.NoError(t, err) + require.NoError(t, decoded.decode(encoded)) + require.Equal(t, interval{10, 15}, decoded) + bic.AddPrevDataBlockToIndexBlock() + // Fourth data block. + points.i = interval{100, 105} + encoded, err = bic.FinishDataBlock(nil) + require.NoError(t, err) + require.NoError(t, decoded.decode(encoded)) + require.Equal(t, interval{100, 105}, decoded) + // Finish index block before including fourth data block. + encodedIndexBlock, err = bic.FinishIndexBlock(nil) + require.NoError(t, err) + require.NoError(t, decoded.decode(encodedIndexBlock)) + require.Equal(t, interval{10, 25}, decoded) + bic.AddPrevDataBlockToIndexBlock() + // Finish index block that contains only fourth data block. + encodedIndexBlock, err = bic.FinishIndexBlock(nil) + require.NoError(t, err) + require.NoError(t, decoded.decode(encodedIndexBlock)) + require.Equal(t, interval{100, 105}, decoded) + var encodedTable []byte + // Finish table. The table interval is the union of the current point key + // table interval [10, 105) and the range key interval [5, 150). + encodedTable, err = bic.FinishTable(nil) + require.NoError(t, err) + require.NoError(t, decoded.decode(encodedTable)) + require.Equal(t, interval{5, 150}, decoded) +} + +func TestBlockIntervalFilter(t *testing.T) { + testCases := []struct { + name string + filter interval + prop interval + intersects bool + }{ + { + name: "non-empty and empty", + filter: interval{10, 15}, + prop: interval{}, + intersects: false, + }, + { + name: "does not intersect", + filter: interval{10, 15}, + prop: interval{15, 20}, + intersects: false, + }, + { + name: "intersects", + filter: interval{10, 15}, + prop: interval{14, 20}, + intersects: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var points testDataBlockIntervalCollector + name := "foo" + bic := NewBlockIntervalCollector(name, &points, nil) + bif := NewBlockIntervalFilter(name, tc.filter.lower, tc.filter.upper) + points.i = tc.prop + prop, _ := bic.FinishDataBlock(nil) + intersects, err := bif.Intersects(prop) + require.NoError(t, err) + require.Equal(t, tc.intersects, intersects) + }) + } +} + +func TestBlockPropertiesEncoderDecoder(t *testing.T) { + var encoder blockPropertiesEncoder + scratch := encoder.getScratchForProp() + scratch = append(scratch, []byte("foo")...) + encoder.addProp(1, scratch) + scratch = encoder.getScratchForProp() + require.LessOrEqual(t, 3, cap(scratch)) + scratch = append(scratch, []byte("cockroach")...) + encoder.addProp(10, scratch) + props1 := encoder.props() + unsafeProps := encoder.unsafeProps() + require.True(t, bytes.Equal(props1, unsafeProps), "%x != %x", props1, unsafeProps) + decodeProps1 := func() { + decoder := blockPropertiesDecoder{props: props1} + require.False(t, decoder.done()) + id, prop, err := decoder.next() + require.NoError(t, err) + require.Equal(t, shortID(1), id) + require.Equal(t, string(prop), "foo") + require.False(t, decoder.done()) + id, prop, err = decoder.next() + require.NoError(t, err) + require.Equal(t, shortID(10), id) + require.Equal(t, string(prop), "cockroach") + require.True(t, decoder.done()) + } + decodeProps1() + + encoder.resetProps() + scratch = encoder.getScratchForProp() + require.LessOrEqual(t, 9, cap(scratch)) + scratch = append(scratch, []byte("bar")...) + encoder.addProp(10, scratch) + props2 := encoder.props() + unsafeProps = encoder.unsafeProps() + require.True(t, bytes.Equal(props2, unsafeProps), "%x != %x", props2, unsafeProps) + // Safe props should still decode. + decodeProps1() + // Decode props2 + decoder := blockPropertiesDecoder{props: props2} + require.False(t, decoder.done()) + id, prop, err := decoder.next() + require.NoError(t, err) + require.Equal(t, shortID(10), id) + require.Equal(t, string(prop), "bar") + require.True(t, decoder.done()) +} + +// filterWithTrueForEmptyProp is a wrapper for BlockPropertyFilter that +// delegates to it except when the property is empty, in which case it returns +// true. +type filterWithTrueForEmptyProp struct { + BlockPropertyFilter +} + +func (b filterWithTrueForEmptyProp) Intersects(prop []byte) (bool, error) { + if len(prop) == 0 { + return true, nil + } + return b.BlockPropertyFilter.Intersects(prop) +} + +func TestBlockPropertiesFilterer_IntersectsUserPropsAndFinishInit(t *testing.T) { + // props with id=0, interval [10, 20); id=10, interval [110, 120). + var dbic testDataBlockIntervalCollector + bic0 := NewBlockIntervalCollector("p0", &dbic, nil) + bic0Id := byte(0) + bic10 := NewBlockIntervalCollector("p10", &dbic, nil) + bic10Id := byte(10) + dbic.i = interval{10, 20} + prop0 := append([]byte(nil), bic0Id) + _, err := bic0.FinishDataBlock(nil) + require.NoError(t, err) + prop0, err = bic0.FinishTable(prop0) + require.NoError(t, err) + dbic.i = interval{110, 120} + prop10 := append([]byte(nil), bic10Id) + _, err = bic10.FinishDataBlock(nil) + require.NoError(t, err) + prop10, err = bic10.FinishTable(prop10) + require.NoError(t, err) + prop0Str := string(prop0) + prop10Str := string(prop10) + type filter struct { + name string + i interval + } + testCases := []struct { + name string + userProps map[string]string + filters []filter + + // Expected results + intersects bool + shortIDToFiltersIndex []int + }{ + { + name: "no filter, no props", + userProps: map[string]string{}, + filters: nil, + intersects: true, + }, + { + name: "no props", + userProps: map[string]string{}, + filters: []filter{ + {name: "p0", i: interval{20, 30}}, + {name: "p10", i: interval{20, 30}}, + }, + intersects: true, + }, + { + name: "prop0, does not intersect", + userProps: map[string]string{"p0": prop0Str}, + filters: []filter{ + {name: "p0", i: interval{20, 30}}, + {name: "p10", i: interval{20, 30}}, + }, + intersects: false, + }, + { + name: "prop0, intersects", + userProps: map[string]string{"p0": prop0Str}, + filters: []filter{ + {name: "p0", i: interval{11, 21}}, + {name: "p10", i: interval{20, 30}}, + }, + intersects: true, + shortIDToFiltersIndex: []int{0}, + }, + { + name: "prop10, does not intersect", + userProps: map[string]string{"p10": prop10Str}, + filters: []filter{ + {name: "p0", i: interval{11, 21}}, + {name: "p10", i: interval{20, 30}}, + }, + intersects: false, + }, + { + name: "prop10, intersects", + userProps: map[string]string{"p10": prop10Str}, + filters: []filter{ + {name: "p0", i: interval{11, 21}}, + {name: "p10", i: interval{115, 125}}, + }, + intersects: true, + shortIDToFiltersIndex: []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1}, + }, + { + name: "prop10, intersects", + userProps: map[string]string{"p10": prop10Str}, + filters: []filter{ + {name: "p10", i: interval{115, 125}}, + {name: "p0", i: interval{11, 21}}, + }, + intersects: true, + shortIDToFiltersIndex: []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0}, + }, + { + name: "prop0 and prop10, does not intersect", + userProps: map[string]string{"p0": prop0Str, "p10": prop10Str}, + filters: []filter{ + {name: "p10", i: interval{115, 125}}, + {name: "p0", i: interval{20, 30}}, + }, + intersects: false, + shortIDToFiltersIndex: []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0}, + }, + { + name: "prop0 and prop10, does not intersect", + userProps: map[string]string{"p0": prop0Str, "p10": prop10Str}, + filters: []filter{ + {name: "p0", i: interval{10, 20}}, + {name: "p10", i: interval{125, 135}}, + }, + intersects: false, + shortIDToFiltersIndex: []int{0}, + }, + { + name: "prop0 and prop10, intersects", + userProps: map[string]string{"p0": prop0Str, "p10": prop10Str}, + filters: []filter{ + {name: "p10", i: interval{115, 125}}, + {name: "p0", i: interval{10, 20}}, + }, + intersects: true, + shortIDToFiltersIndex: []int{1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var filters []BlockPropertyFilter + for _, f := range tc.filters { + filter := NewBlockIntervalFilter(f.name, f.i.lower, f.i.upper) + filters = append(filters, filter) + } + filterer := newBlockPropertiesFilterer(filters, nil) + intersects, err := filterer.intersectsUserPropsAndFinishInit(tc.userProps) + require.NoError(t, err) + require.Equal(t, tc.intersects, intersects) + require.Equal(t, tc.shortIDToFiltersIndex, filterer.shortIDToFiltersIndex) + }) + } +} + +func TestBlockPropertiesFilterer_Intersects(t *testing.T) { + // Setup two different properties values to filter against. + var emptyProps []byte + // props with id=0, interval [10, 20); id=10, interval [110, 120). + var encoder blockPropertiesEncoder + var dbic testDataBlockIntervalCollector + bic0 := NewBlockIntervalCollector("", &dbic, nil) + bic0Id := shortID(0) + bic10 := NewBlockIntervalCollector("", &dbic, nil) + bic10Id := shortID(10) + dbic.i = interval{10, 20} + prop, err := bic0.FinishDataBlock(encoder.getScratchForProp()) + require.NoError(t, err) + encoder.addProp(bic0Id, prop) + dbic.i = interval{110, 120} + prop, err = bic10.FinishDataBlock(encoder.getScratchForProp()) + require.NoError(t, err) + encoder.addProp(bic10Id, prop) + props0And10 := encoder.props() + type filter struct { + shortID shortID + i interval + intersectsForEmptyProp bool + } + testCases := []struct { + name string + props []byte + // filters must be in ascending order of shortID. + filters []filter + intersects bool + }{ + { + name: "no filter, empty props", + props: emptyProps, + intersects: true, + }, + { + name: "no filter", + props: props0And10, + intersects: true, + }, + { + name: "filter 0, empty props, does not intersect", + props: emptyProps, + filters: []filter{ + { + shortID: 0, + i: interval{5, 15}, + }, + }, + intersects: false, + }, + { + name: "filter 10, empty props, does not intersect", + props: emptyProps, + filters: []filter{ + { + shortID: 0, + i: interval{105, 111}, + }, + }, + intersects: false, + }, + { + name: "filter 0, intersects", + props: props0And10, + filters: []filter{ + { + shortID: 0, + i: interval{5, 15}, + }, + }, + intersects: true, + }, + { + name: "filter 0, does not intersect", + props: props0And10, + filters: []filter{ + { + shortID: 0, + i: interval{20, 25}, + }, + }, + intersects: false, + }, + { + name: "filter 10, intersects", + props: props0And10, + filters: []filter{ + { + shortID: 10, + i: interval{105, 111}, + }, + }, + intersects: true, + }, + { + name: "filter 10, does not intersect", + props: props0And10, + filters: []filter{ + { + shortID: 10, + i: interval{105, 110}, + }, + }, + intersects: false, + }, + { + name: "filter 5, does not intersect since no property", + props: props0And10, + filters: []filter{ + { + shortID: 5, + i: interval{105, 110}, + }, + }, + intersects: false, + }, + { + name: "filter 0 and 5, intersects and not intersects means overall not intersects", + props: props0And10, + filters: []filter{ + { + shortID: 0, + i: interval{5, 15}, + }, + { + shortID: 5, + i: interval{105, 110}, + }, + }, + intersects: false, + }, + { + name: "filter 0, 5, 7, 11, all intersect", + props: props0And10, + filters: []filter{ + { + shortID: 0, + i: interval{5, 15}, + }, + { + shortID: 5, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + { + shortID: 7, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + { + shortID: 11, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + }, + intersects: true, + }, + { + name: "filter 0, 5, 7, 10, 11, all intersect", + props: props0And10, + filters: []filter{ + { + shortID: 0, + i: interval{5, 15}, + }, + { + shortID: 5, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + { + shortID: 7, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + { + shortID: 10, + i: interval{105, 111}, + }, + { + shortID: 11, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + }, + intersects: true, + }, + { + name: "filter 0, 5, 7, 10, 11, all intersect except for 10", + props: props0And10, + filters: []filter{ + { + shortID: 0, + i: interval{5, 15}, + }, + { + shortID: 5, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + { + shortID: 7, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + { + shortID: 10, + i: interval{105, 110}, + }, + { + shortID: 11, + i: interval{105, 110}, + intersectsForEmptyProp: true, + }, + }, + intersects: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var filters []BlockPropertyFilter + var shortIDToFiltersIndex []int + if len(tc.filters) > 0 { + shortIDToFiltersIndex = make([]int, tc.filters[len(tc.filters)-1].shortID+1) + for i := range shortIDToFiltersIndex { + shortIDToFiltersIndex[i] = -1 + } + } + for _, f := range tc.filters { + filter := NewBlockIntervalFilter("", f.i.lower, f.i.upper) + bpf := BlockPropertyFilter(filter) + if f.intersectsForEmptyProp { + bpf = filterWithTrueForEmptyProp{filter} + } + shortIDToFiltersIndex[f.shortID] = len(filters) + filters = append(filters, bpf) + } + doFiltering := func() { + bpFilterer := BlockPropertiesFilterer{ + filters: filters, + shortIDToFiltersIndex: shortIDToFiltersIndex, + boundLimitedShortID: -1, + } + intersects, err := bpFilterer.intersects(tc.props) + require.NoError(t, err) + require.Equal(t, tc.intersects, intersects == blockIntersects) + } + doFiltering() + if len(filters) > 1 { + // Permute the filters so that the use of + // shortIDToFiltersIndex is better tested. + permutation := rand.Perm(len(filters)) + filterPerm := make([]BlockPropertyFilter, len(filters)) + for i := range permutation { + filterPerm[i] = filters[permutation[i]] + shortIDToFiltersIndex[tc.filters[permutation[i]].shortID] = i + } + filters = filterPerm + doFiltering() + } + }) + } +} + +// valueCharBlockIntervalCollector implements DataBlockIntervalCollector by +// maintaining the (inclusive) lower and (exclusive) upper bound of a fixed +// character position in the value, when represented as an integer. +type valueCharBlockIntervalCollector struct { + charIdx int + initialized bool + lower, upper uint64 +} + +var _ DataBlockIntervalCollector = &valueCharBlockIntervalCollector{} + +// Add implements DataBlockIntervalCollector by maintaining the lower and upper +// bound of a fixed character position in the value. +func (c *valueCharBlockIntervalCollector) Add(_ InternalKey, value []byte) error { + charIdx := c.charIdx + if charIdx == -1 { + charIdx = len(value) - 1 + } + val, err := strconv.Atoi(string(value[charIdx])) + if err != nil { + return err + } + uval := uint64(val) + if !c.initialized { + c.lower, c.upper = uval, uval+1 + c.initialized = true + return nil + } + if uval < c.lower { + c.lower = uval + } + if uval >= c.upper { + c.upper = uval + 1 + } + + return nil +} + +// Finish implements DataBlockIntervalCollector, returning the lower and upper +// bound for the block. The range is reset to zero in anticipation of the next +// block. +func (c *valueCharBlockIntervalCollector) FinishDataBlock() (lower, upper uint64, err error) { + l, u := c.lower, c.upper + c.lower, c.upper = 0, 0 + c.initialized = false + return l, u, nil +} + +// testKeysSuffixIntervalCollector maintains an interval over the timestamps in +// MVCC-like suffixes for keys (e.g. foo@123). +type suffixIntervalCollector struct { + initialized bool + lower, upper uint64 +} + +// Add implements DataBlockIntervalCollector by adding the timestamp(s) in the +// suffix(es) of this record to the current interval. +// +// Note that range sets and unsets may have multiple suffixes. Range key deletes +// do not have a suffix. All other point keys have a single suffix. +func (c *suffixIntervalCollector) Add(key InternalKey, value []byte) error { + var bs [][]byte + // Range keys have their suffixes encoded into the value. + if rangekey.IsRangeKey(key.Kind()) { + if key.Kind() == base.InternalKeyKindRangeKeyDelete { + return nil + } + s, err := rangekey.Decode(key, value, nil) + if err != nil { + return err + } + for _, k := range s.Keys { + if len(k.Suffix) > 0 { + bs = append(bs, k.Suffix) + } + } + } else { + // All other keys have a single suffix encoded into the value. + bs = append(bs, key.UserKey) + } + + for _, b := range bs { + i := testkeys.Comparer.Split(b) + ts, err := strconv.Atoi(string(b[i+1:])) + if err != nil { + return err + } + uts := uint64(ts) + if !c.initialized { + c.lower, c.upper = uts, uts+1 + c.initialized = true + continue + } + if uts < c.lower { + c.lower = uts + } + if uts >= c.upper { + c.upper = uts + 1 + } + } + return nil +} + +// FinishDataBlock implements DataBlockIntervalCollector. +func (c *suffixIntervalCollector) FinishDataBlock() (lower, upper uint64, err error) { + l, u := c.lower, c.upper + c.lower, c.upper = 0, 0 + c.initialized = false + return l, u, nil +} + +func TestBlockProperties(t *testing.T) { + var r *Reader + defer func() { + if r != nil { + require.NoError(t, r.Close()) + } + }() + + var stats base.InternalIteratorStats + datadriven.RunTest(t, "testdata/block_properties", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "build": + if r != nil { + _ = r.Close() + r = nil + } + var output string + r, output = runBlockPropertiesBuildCmd(td) + return output + + case "collectors": + return runCollectorsCmd(r, td) + + case "table-props": + return runTablePropsCmd(r, td) + + case "block-props": + return runBlockPropsCmd(r, td) + + case "filter": + var points, ranges []BlockPropertyFilter + for _, cmd := range td.CmdArgs { + filter, err := parseIntervalFilter(cmd) + if err != nil { + return err.Error() + } + switch cmd.Key { + case "point-filter": + points = append(points, filter) + case "range-filter": + ranges = append(ranges, filter) + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + } + + // Point keys filter matches. + var buf bytes.Buffer + var f *BlockPropertiesFilterer + buf.WriteString("points: ") + if len(points) > 0 { + f = newBlockPropertiesFilterer(points, nil) + ok, err := f.intersectsUserPropsAndFinishInit(r.Properties.UserProperties) + if err != nil { + return err.Error() + } + buf.WriteString(strconv.FormatBool(ok)) + if !ok { + f = nil + } + + // Enumerate point key data blocks encoded into the index. + if f != nil { + indexH, err := r.readIndex(context.Background(), nil, nil) + if err != nil { + return err.Error() + } + defer indexH.Release() + + buf.WriteString(", blocks=[") + + var blocks []int + var i int + iter, _ := newBlockIter(r.Compare, indexH.Get()) + for key, value := iter.First(); key != nil; key, value = iter.Next() { + bh, err := decodeBlockHandleWithProperties(value.InPlaceValue()) + if err != nil { + return err.Error() + } + intersects, err := f.intersects(bh.Props) + if err != nil { + return err.Error() + } + if intersects == blockIntersects { + blocks = append(blocks, i) + } + i++ + } + for i, b := range blocks { + buf.WriteString(strconv.Itoa(b)) + if i < len(blocks)-1 { + buf.WriteString(",") + } + } + buf.WriteString("]") + } + } else { + // Without filters, the table matches by default. + buf.WriteString("true (no filters provided)") + } + buf.WriteString("\n") + + // Range key filter matches. + buf.WriteString("ranges: ") + if len(ranges) > 0 { + f := newBlockPropertiesFilterer(ranges, nil) + ok, err := f.intersectsUserPropsAndFinishInit(r.Properties.UserProperties) + if err != nil { + return err.Error() + } + buf.WriteString(strconv.FormatBool(ok)) + } else { + // Without filters, the table matches by default. + buf.WriteString("true (no filters provided)") + } + buf.WriteString("\n") + + return buf.String() + + case "iter": + var lower, upper []byte + var filters []BlockPropertyFilter + for _, arg := range td.CmdArgs { + switch arg.Key { + case "lower": + lower = []byte(arg.Vals[0]) + case "upper": + upper = []byte(arg.Vals[0]) + case "point-key-filter": + f, err := parseIntervalFilter(arg) + if err != nil { + return err.Error() + } + filters = append(filters, f) + } + } + filterer := newBlockPropertiesFilterer(filters, nil) + ok, err := filterer.intersectsUserPropsAndFinishInit(r.Properties.UserProperties) + if err != nil { + return err.Error() + } else if !ok { + return "filter excludes entire table" + } + iter, err := r.NewIterWithBlockPropertyFilters( + lower, upper, filterer, false /* use (bloom) filter */, &stats, + CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}) + if err != nil { + return err.Error() + } + return runIterCmd(td, iter, false, runIterCmdEveryOpAfter(func(w io.Writer) { + // After every op, point the value of MaybeFilteredKeys. + fmt.Fprintf(w, " MaybeFilteredKeys()=%t", iter.MaybeFilteredKeys()) + })) + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestBlockProperties_BoundLimited(t *testing.T) { + var r *Reader + defer func() { + if r != nil { + require.NoError(t, r.Close()) + } + }() + + var stats base.InternalIteratorStats + datadriven.RunTest(t, "testdata/block_properties_boundlimited", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "build": + if r != nil { + _ = r.Close() + r = nil + } + var output string + r, output = runBlockPropertiesBuildCmd(td) + return output + case "collectors": + return runCollectorsCmd(r, td) + case "table-props": + return runTablePropsCmd(r, td) + case "block-props": + return runBlockPropsCmd(r, td) + case "iter": + var buf bytes.Buffer + var lower, upper []byte + filter := boundLimitedWrapper{ + w: &buf, + cmp: testkeys.Comparer.Compare, + } + for _, arg := range td.CmdArgs { + switch arg.Key { + case "lower": + lower = []byte(arg.Vals[0]) + case "upper": + upper = []byte(arg.Vals[0]) + case "filter": + f, err := parseIntervalFilter(arg) + if err != nil { + return err.Error() + } + filter.inner = f + case "filter-upper": + ik := base.MakeInternalKey([]byte(arg.Vals[0]), 0, base.InternalKeyKindSet) + filter.upper = &ik + case "filter-lower": + ik := base.MakeInternalKey([]byte(arg.Vals[0]), 0, base.InternalKeyKindSet) + filter.lower = &ik + } + } + if filter.inner == nil { + return "missing block property filter" + } + + filterer := newBlockPropertiesFilterer(nil, &filter) + ok, err := filterer.intersectsUserPropsAndFinishInit(r.Properties.UserProperties) + if err != nil { + return err.Error() + } else if !ok { + return "filter excludes entire table" + } + iter, err := r.NewIterWithBlockPropertyFilters( + lower, upper, filterer, false /* use (bloom) filter */, &stats, + CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}) + if err != nil { + return err.Error() + } + return runIterCmd(td, iter, false, runIterCmdEveryOp(func(w io.Writer) { + // Copy the bound-limited-wrapper's accumulated output to the + // iterator's writer. This interleaves its output with the + // iterator output. + io.Copy(w, &buf) + buf.Reset() + }), runIterCmdEveryOpAfter(func(w io.Writer) { + // After every op, point the value of MaybeFilteredKeys. + fmt.Fprintf(w, " MaybeFilteredKeys()=%t", iter.MaybeFilteredKeys()) + })) + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} + +type boundLimitedWrapper struct { + w io.Writer + cmp base.Compare + inner BlockPropertyFilter + lower *InternalKey + upper *InternalKey +} + +func (bl *boundLimitedWrapper) Name() string { return bl.inner.Name() } + +func (bl *boundLimitedWrapper) Intersects(prop []byte) (bool, error) { + propString := fmt.Sprintf("%x", prop) + var i interval + if err := i.decode(prop); err == nil { + // If it decodes as an interval, pretty print it as an interval. + propString = fmt.Sprintf("[%d, %d)", i.lower, i.upper) + } + + v, err := bl.inner.Intersects(prop) + if bl.w != nil { + fmt.Fprintf(bl.w, " filter.Intersects(%s) = (%t, %v)\n", propString, v, err) + } + return v, err +} + +func (bl *boundLimitedWrapper) KeyIsWithinLowerBound(key []byte) (ret bool) { + if bl.lower == nil { + ret = true + } else { + ret = bl.cmp(key, bl.lower.UserKey) >= 0 + } + if bl.w != nil { + fmt.Fprintf(bl.w, " filter.KeyIsWithinLowerBound(%s) = %t\n", key, ret) + } + return ret +} + +func (bl *boundLimitedWrapper) KeyIsWithinUpperBound(key []byte) (ret bool) { + if bl.upper == nil { + ret = true + } else { + ret = bl.cmp(key, bl.upper.UserKey) <= 0 + } + if bl.w != nil { + fmt.Fprintf(bl.w, " filter.KeyIsWithinUpperBound(%s) = %t\n", key, ret) + } + return ret +} + +func parseIntervalFilter(cmd datadriven.CmdArg) (BlockPropertyFilter, error) { + name := cmd.Vals[0] + minS, maxS := cmd.Vals[1], cmd.Vals[2] + min, err := strconv.ParseUint(minS, 10, 64) + if err != nil { + return nil, err + } + max, err := strconv.ParseUint(maxS, 10, 64) + if err != nil { + return nil, err + } + return NewBlockIntervalFilter(name, min, max), nil +} + +func runCollectorsCmd(r *Reader, td *datadriven.TestData) string { + var lines []string + for k, v := range r.Properties.UserProperties { + lines = append(lines, fmt.Sprintf("%d: %s", v[0], k)) + } + linesSorted := sort.StringSlice(lines) + linesSorted.Sort() + return strings.Join(lines, "\n") +} + +func runTablePropsCmd(r *Reader, td *datadriven.TestData) string { + var lines []string + for _, val := range r.Properties.UserProperties { + id := shortID(val[0]) + var i interval + if err := i.decode([]byte(val[1:])); err != nil { + return err.Error() + } + lines = append(lines, fmt.Sprintf("%d: [%d, %d)", id, i.lower, i.upper)) + } + linesSorted := sort.StringSlice(lines) + linesSorted.Sort() + return strings.Join(lines, "\n") +} + +func runBlockPropertiesBuildCmd(td *datadriven.TestData) (r *Reader, out string) { + opts := WriterOptions{ + TableFormat: TableFormatPebblev2, + IndexBlockSize: math.MaxInt32, // Default to a single level index for simplicity. + } + for _, cmd := range td.CmdArgs { + switch cmd.Key { + case "block-size": + if len(cmd.Vals) != 1 { + return r, fmt.Sprintf("%s: arg %s expects 1 value", td.Cmd, cmd.Key) + } + var err error + opts.BlockSize, err = strconv.Atoi(cmd.Vals[0]) + if err != nil { + return r, err.Error() + } + case "collectors": + for _, c := range cmd.Vals { + var points, ranges DataBlockIntervalCollector + switch c { + case "value-first": + points = &valueCharBlockIntervalCollector{charIdx: 0} + case "value-last": + points = &valueCharBlockIntervalCollector{charIdx: -1} + case "suffix": + points, ranges = &suffixIntervalCollector{}, &suffixIntervalCollector{} + case "suffix-point-keys-only": + points = &suffixIntervalCollector{} + case "suffix-range-keys-only": + ranges = &suffixIntervalCollector{} + case "nil-points-and-ranges": + points, ranges = nil, nil + default: + return r, fmt.Sprintf("unknown collector: %s", c) + } + name := c + opts.BlockPropertyCollectors = append( + opts.BlockPropertyCollectors, + func() BlockPropertyCollector { + return NewBlockIntervalCollector(name, points, ranges) + }) + } + case "index-block-size": + var err error + opts.IndexBlockSize, err = strconv.Atoi(cmd.Vals[0]) + if err != nil { + return r, err.Error() + } + } + } + var meta *WriterMetadata + var err error + func() { + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("%v", r) + } + }() + meta, r, err = runBuildCmd(td, &opts, 0) + }() + if err != nil { + return r, err.Error() + } + return r, fmt.Sprintf("point: [%s,%s]\nrangedel: [%s,%s]\nrangekey: [%s,%s]\nseqnums: [%d,%d]\n", + meta.SmallestPoint, meta.LargestPoint, + meta.SmallestRangeDel, meta.LargestRangeDel, + meta.SmallestRangeKey, meta.LargestRangeKey, + meta.SmallestSeqNum, meta.LargestSeqNum) +} + +func runBlockPropsCmd(r *Reader, td *datadriven.TestData) string { + bh, err := r.readIndex(context.Background(), nil, nil) + if err != nil { + return err.Error() + } + twoLevelIndex := r.Properties.IndexPartitions > 0 + i, err := newBlockIter(r.Compare, bh.Get()) + if err != nil { + return err.Error() + } + defer bh.Release() + var sb strings.Builder + decodeProps := func(props []byte, indent string) error { + d := blockPropertiesDecoder{props: props} + var lines []string + for !d.done() { + id, prop, err := d.next() + if err != nil { + return err + } + var i interval + if err := i.decode(prop); err != nil { + return err + } + lines = append(lines, fmt.Sprintf("%s%d: [%d, %d)\n", indent, id, i.lower, i.upper)) + } + linesSorted := sort.StringSlice(lines) + linesSorted.Sort() + for _, line := range lines { + sb.WriteString(line) + } + return nil + } + + for key, val := i.First(); key != nil; key, val = i.Next() { + sb.WriteString(fmt.Sprintf("%s:\n", key)) + bhp, err := decodeBlockHandleWithProperties(val.InPlaceValue()) + if err != nil { + return err.Error() + } + if err := decodeProps(bhp.Props, " "); err != nil { + return err.Error() + } + + // If the table has a two-level index, also decode the index + // block that bhp points to, along with its block properties. + if twoLevelIndex { + subiter := &blockIter{} + subIndex, err := r.readBlock( + context.Background(), bhp.BlockHandle, nil, nil, nil, nil, nil) + if err != nil { + return err.Error() + } + if err := subiter.init( + r.Compare, subIndex.Get(), 0 /* globalSeqNum */, false); err != nil { + return err.Error() + } + for key, value := subiter.First(); key != nil; key, value = subiter.Next() { + sb.WriteString(fmt.Sprintf(" %s:\n", key)) + dataBH, err := decodeBlockHandleWithProperties(value.InPlaceValue()) + if err != nil { + return err.Error() + } + if err := decodeProps(dataBH.Props, " "); err != nil { + return err.Error() + } + } + subIndex.Release() + } + } + return sb.String() +} + +type keyCountCollector struct { + name string + block, index, table int +} + +var _ BlockPropertyCollector = &keyCountCollector{} +var _ SuffixReplaceableBlockCollector = &keyCountCollector{} + +func keyCountCollectorFn(name string) func() BlockPropertyCollector { + return func() BlockPropertyCollector { return &keyCountCollector{name: name} } +} + +func (p *keyCountCollector) Name() string { return p.name } + +func (p *keyCountCollector) Add(k InternalKey, _ []byte) error { + if rangekey.IsRangeKey(k.Kind()) { + p.table++ + } else { + p.block++ + } + return nil +} + +func (p *keyCountCollector) FinishDataBlock(buf []byte) ([]byte, error) { + buf = append(buf, []byte(strconv.Itoa(int(p.block)))...) + p.table += p.block + return buf, nil +} + +func (p *keyCountCollector) AddPrevDataBlockToIndexBlock() { + p.index += p.block + p.block = 0 +} + +func (p *keyCountCollector) FinishIndexBlock(buf []byte) ([]byte, error) { + buf = append(buf, []byte(strconv.Itoa(int(p.index)))...) + p.index = 0 + return buf, nil +} + +func (p *keyCountCollector) FinishTable(buf []byte) ([]byte, error) { + buf = append(buf, []byte(strconv.Itoa(int(p.table)))...) + p.table = 0 + return buf, nil +} + +func (p *keyCountCollector) UpdateKeySuffixes(old []byte, _, _ []byte) error { + n, err := strconv.Atoi(string(old)) + if err != nil { + return err + } + p.block = n + return nil +} + +// intSuffixCollector is testing prop collector that collects the min and +// max value of numeric suffix of keys (interpreting suffixLen bytes as ascii +// for conversion with atoi). +type intSuffixCollector struct { + suffixLen int + min, max uint64 // inclusive +} + +func makeIntSuffixCollector(len int) intSuffixCollector { + return intSuffixCollector{len, math.MaxUint64, 0} +} + +func (p *intSuffixCollector) setFromSuffix(to []byte) error { + if len(to) >= p.suffixLen { + parsed, err := strconv.Atoi(string(to[len(to)-p.suffixLen:])) + if err != nil { + return err + } + p.min = uint64(parsed) + p.max = uint64(parsed) + } + return nil +} + +type intSuffixTablePropCollector struct { + name string + intSuffixCollector +} + +var _ TablePropertyCollector = &intSuffixTablePropCollector{} +var _ SuffixReplaceableTableCollector = &intSuffixTablePropCollector{} + +func intSuffixTablePropCollectorFn(name string, len int) func() TablePropertyCollector { + return func() TablePropertyCollector { return &intSuffixTablePropCollector{name, makeIntSuffixCollector(len)} } +} + +func (p *intSuffixCollector) Add(key InternalKey, _ []byte) error { + if len(key.UserKey) > p.suffixLen { + parsed, err := strconv.Atoi(string(key.UserKey[len(key.UserKey)-p.suffixLen:])) + if err != nil { + return err + } + v := uint64(parsed) + if v > p.max { + p.max = v + } + if v < p.min { + p.min = v + } + } + return nil +} + +func (p *intSuffixTablePropCollector) Finish(userProps map[string]string) error { + userProps[p.name+".min"] = fmt.Sprint(p.min) + userProps[p.name+".max"] = fmt.Sprint(p.max) + return nil +} + +func (p *intSuffixTablePropCollector) Name() string { return p.name } + +func (p *intSuffixTablePropCollector) UpdateKeySuffixes( + oldProps map[string]string, from, to []byte, +) error { + return p.setFromSuffix(to) +} + +// testIntSuffixIntervalCollector is a wrapper for testIntSuffixCollector that +// uses it to implement a block interval collector. +type intSuffixIntervalCollector struct { + intSuffixCollector +} + +func intSuffixIntervalCollectorFn(name string, length int) func() BlockPropertyCollector { + return func() BlockPropertyCollector { + return NewBlockIntervalCollector(name, &intSuffixIntervalCollector{makeIntSuffixCollector(length)}, nil) + } +} + +var _ DataBlockIntervalCollector = &intSuffixIntervalCollector{} +var _ SuffixReplaceableBlockCollector = &intSuffixIntervalCollector{} + +func (p *intSuffixIntervalCollector) FinishDataBlock() (lower uint64, upper uint64, err error) { + return p.min, p.max + 1, nil +} + +func (p *intSuffixIntervalCollector) UpdateKeySuffixes(oldProp []byte, from, to []byte) error { + return p.setFromSuffix(to) +} diff --git a/pebble/sstable/block_property_test_utils.go b/pebble/sstable/block_property_test_utils.go new file mode 100644 index 0000000..0ade68f --- /dev/null +++ b/pebble/sstable/block_property_test_utils.go @@ -0,0 +1,117 @@ +// 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 sstable + +import ( + "math" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" +) + +// Code in this file contains utils for testing. It implements interval block +// property collectors and filters on the suffixes of keys in the format used +// by the testkeys package (eg, 'key@5'). + +const testKeysBlockPropertyName = `pebble.internal.testkeys.suffixes` + +// NewTestKeysBlockPropertyCollector constructs a sstable property collector +// over testkey suffixes. +func NewTestKeysBlockPropertyCollector() BlockPropertyCollector { + return NewBlockIntervalCollector( + testKeysBlockPropertyName, + &testKeysSuffixIntervalCollector{}, + nil) +} + +// NewTestKeysBlockPropertyFilter constructs a new block-property filter that excludes +// blocks containing exclusively suffixed keys where all the suffixes fall +// outside of the range [filterMin, filterMax). +// +// The filter only filters based on data derived from the key. The iteration +// results of this block property filter are deterministic for unsuffixed keys +// and keys with suffixes within the range [filterMin, filterMax). For keys with +// suffixes outside the range, iteration is nondeterministic. +func NewTestKeysBlockPropertyFilter(filterMin, filterMax uint64) *BlockIntervalFilter { + return NewBlockIntervalFilter(testKeysBlockPropertyName, filterMin, filterMax) +} + +// NewTestKeysMaskingFilter constructs a TestKeysMaskingFilter that implements +// pebble.BlockPropertyFilterMask for efficient range-key masking using the +// testkeys block property filter. The masking filter wraps a block interval +// filter, and modifies the configured interval when Pebble requests it. +func NewTestKeysMaskingFilter() TestKeysMaskingFilter { + return TestKeysMaskingFilter{BlockIntervalFilter: NewTestKeysBlockPropertyFilter(0, math.MaxUint64)} +} + +// TestKeysMaskingFilter implements BlockPropertyFilterMask and may be used to mask +// point keys with the testkeys-style suffixes (eg, @4) that are masked by range +// keys with testkeys-style suffixes. +type TestKeysMaskingFilter struct { + *BlockIntervalFilter +} + +// SetSuffix implements pebble.BlockPropertyFilterMask. +func (f TestKeysMaskingFilter) SetSuffix(suffix []byte) error { + ts, err := testkeys.ParseSuffix(suffix) + if err != nil { + return err + } + f.BlockIntervalFilter.SetInterval(uint64(ts), math.MaxUint64) + return nil +} + +// Intersects implements the BlockPropertyFilter interface. +func (f TestKeysMaskingFilter) Intersects(prop []byte) (bool, error) { + return f.BlockIntervalFilter.Intersects(prop) +} + +var _ DataBlockIntervalCollector = (*testKeysSuffixIntervalCollector)(nil) + +// testKeysSuffixIntervalCollector maintains an interval over the timestamps in +// MVCC-like suffixes for keys (e.g. foo@123). +type testKeysSuffixIntervalCollector struct { + initialized bool + lower, upper uint64 +} + +// Add implements DataBlockIntervalCollector by adding the timestamp(s) in the +// suffix(es) of this record to the current interval. +// +// Note that range sets and unsets may have multiple suffixes. Range key deletes +// do not have a suffix. All other point keys have a single suffix. +func (c *testKeysSuffixIntervalCollector) Add(key base.InternalKey, value []byte) error { + i := testkeys.Comparer.Split(key.UserKey) + if i == len(key.UserKey) { + c.initialized = true + c.lower, c.upper = 0, math.MaxUint64 + return nil + } + ts, err := testkeys.ParseSuffix(key.UserKey[i:]) + if err != nil { + return err + } + uts := uint64(ts) + if !c.initialized { + c.lower, c.upper = uts, uts+1 + c.initialized = true + return nil + } + if uts < c.lower { + c.lower = uts + } + if uts >= c.upper { + c.upper = uts + 1 + } + return nil +} + +// FinishDataBlock implements DataBlockIntervalCollector. +func (c *testKeysSuffixIntervalCollector) FinishDataBlock() (lower, upper uint64, err error) { + l, u := c.lower, c.upper + c.lower, c.upper = 0, 0 + c.initialized = false + return l, u, nil +} diff --git a/pebble/sstable/block_test.go b/pebble/sstable/block_test.go new file mode 100644 index 0000000..14e6f7f --- /dev/null +++ b/pebble/sstable/block_test.go @@ -0,0 +1,513 @@ +// Copyright 2018 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" + "fmt" + "strconv" + "strings" + "testing" + "time" + "unsafe" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/itertest" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func ikey(s string) InternalKey { + return InternalKey{UserKey: []byte(s)} +} + +func TestBlockWriter(t *testing.T) { + w := &rawBlockWriter{ + blockWriter: blockWriter{restartInterval: 16}, + } + w.add(ikey("apple"), nil) + w.add(ikey("apricot"), nil) + w.add(ikey("banana"), nil) + block := w.finish() + + expected := []byte( + "\x00\x05\x00apple" + + "\x02\x05\x00ricot" + + "\x00\x06\x00banana" + + "\x00\x00\x00\x00\x01\x00\x00\x00") + if !bytes.Equal(expected, block) { + t.Fatalf("expected\n%q\nfound\n%q", expected, block) + } +} + +func TestBlockWriterWithPrefix(t *testing.T) { + w := &rawBlockWriter{ + blockWriter: blockWriter{restartInterval: 2}, + } + curKey := func() string { + return string(base.DecodeInternalKey(w.curKey).UserKey) + } + addAdapter := func( + key InternalKey, + value []byte, + addValuePrefix bool, + valuePrefix valuePrefix, + setHasSameKeyPrefix bool) { + w.addWithOptionalValuePrefix( + key, false, value, len(key.UserKey), addValuePrefix, valuePrefix, setHasSameKeyPrefix) + } + addAdapter( + ikey("apple"), []byte("red"), false, 0, true) + require.Equal(t, "apple", curKey()) + require.Equal(t, "red", string(w.curValue)) + addAdapter( + ikey("apricot"), []byte("orange"), true, '\xff', false) + require.Equal(t, "apricot", curKey()) + require.Equal(t, "orange", string(w.curValue)) + // Even though this call has setHasSameKeyPrefix=true, the previous call, + // which was after the last restart set it to false. So the restart encoded + // with banana has this cumulative bit set to false. + addAdapter( + ikey("banana"), []byte("yellow"), true, '\x00', true) + require.Equal(t, "banana", curKey()) + require.Equal(t, "yellow", string(w.curValue)) + addAdapter( + ikey("cherry"), []byte("red"), false, 0, true) + require.Equal(t, "cherry", curKey()) + require.Equal(t, "red", string(w.curValue)) + // All intervening calls has setHasSameKeyPrefix=true, so the cumulative bit + // will be set to true in this restart. + addAdapter( + ikey("mango"), []byte("juicy"), false, 0, true) + require.Equal(t, "mango", curKey()) + require.Equal(t, "juicy", string(w.curValue)) + + block := w.finish() + + expected := []byte( + "\x00\x0d\x03apple\x00\x00\x00\x00\x00\x00\x00\x00red" + + "\x02\x0d\x07ricot\x00\x00\x00\x00\x00\x00\x00\x00\xfforange" + + "\x00\x0e\x07banana\x00\x00\x00\x00\x00\x00\x00\x00\x00yellow" + + "\x00\x0e\x03cherry\x00\x00\x00\x00\x00\x00\x00\x00red" + + "\x00\x0d\x05mango\x00\x00\x00\x00\x00\x00\x00\x00juicy" + + // Restarts are: + // 00000000 (restart at apple), 2a000000 (restart at banana), 56000080 (restart at mango) + // 03000000 (number of restart, i.e., 3). The restart at mango has 1 in the most significant + // bit of the uint32, so the last byte in the little endian encoding is \x80. + "\x00\x00\x00\x00\x2a\x00\x00\x00\x56\x00\x00\x80\x03\x00\x00\x00") + if !bytes.Equal(expected, block) { + t.Fatalf("expected\n%x\nfound\n%x", expected, block) + } +} + +func testBlockCleared(t *testing.T, w, b *blockWriter) { + require.Equal(t, w.restartInterval, b.restartInterval) + require.Equal(t, w.nEntries, b.nEntries) + require.Equal(t, w.nextRestart, b.nextRestart) + require.Equal(t, len(w.buf), len(b.buf)) + require.Equal(t, len(w.restarts), len(b.restarts)) + require.Equal(t, len(w.curKey), len(b.curKey)) + require.Equal(t, len(w.prevKey), len(b.prevKey)) + require.Equal(t, len(w.curValue), len(b.curValue)) + require.Equal(t, w.tmp, b.tmp) + + // Make sure that we didn't lose the allocated byte slices. + require.True(t, cap(w.buf) > 0 && cap(b.buf) == 0) + require.True(t, cap(w.restarts) > 0 && cap(b.restarts) == 0) + require.True(t, cap(w.curKey) > 0 && cap(b.curKey) == 0) + require.True(t, cap(w.prevKey) > 0 && cap(b.prevKey) == 0) + require.True(t, cap(w.curValue) > 0 && cap(b.curValue) == 0) +} + +func TestBlockClear(t *testing.T) { + w := blockWriter{restartInterval: 16} + w.add(ikey("apple"), nil) + w.add(ikey("apricot"), nil) + w.add(ikey("banana"), nil) + + w.clear() + + // Once a block is cleared, we expect its fields to be cleared, but we expect + // it to keep its allocated byte slices. + b := blockWriter{} + testBlockCleared(t, &w, &b) +} + +func TestInvalidInternalKeyDecoding(t *testing.T) { + // Invalid keys since they don't have an 8 byte trailer. + testCases := []string{ + "", + "\x01\x02\x03\x04\x05\x06\x07", + "foo", + } + for _, tc := range testCases { + i := blockIter{} + i.decodeInternalKey([]byte(tc)) + require.Nil(t, i.ikey.UserKey) + require.Equal(t, uint64(InternalKeyKindInvalid), i.ikey.Trailer) + } +} + +func TestBlockIter(t *testing.T) { + // k is a block that maps three keys "apple", "apricot", "banana" to empty strings. + k := block([]byte( + "\x00\x05\x00apple" + + "\x02\x05\x00ricot" + + "\x00\x06\x00banana" + + "\x00\x00\x00\x00\x01\x00\x00\x00")) + var testcases = []struct { + index int + key string + }{ + {0, ""}, + {0, "a"}, + {0, "aaaaaaaaaaaaaaa"}, + {0, "app"}, + {0, "apple"}, + {1, "appliance"}, + {1, "apricos"}, + {1, "apricot"}, + {2, "azzzzzzzzzzzzzz"}, + {2, "b"}, + {2, "banan"}, + {2, "banana"}, + {3, "banana\x00"}, + {3, "c"}, + } + for _, tc := range testcases { + i, err := newRawBlockIter(bytes.Compare, k) + require.NoError(t, err) + i.SeekGE([]byte(tc.key)) + for j, keyWant := range []string{"apple", "apricot", "banana"}[tc.index:] { + if !i.Valid() { + t.Fatalf("key=%q, index=%d, j=%d: Valid got false, keyWant true", tc.key, tc.index, j) + } + if keyGot := string(i.Key().UserKey); keyGot != keyWant { + t.Fatalf("key=%q, index=%d, j=%d: got %q, keyWant %q", tc.key, tc.index, j, keyGot, keyWant) + } + i.Next() + } + if i.Valid() { + t.Fatalf("key=%q, index=%d: Valid got true, keyWant false", tc.key, tc.index) + } + if err := i.Close(); err != nil { + t.Fatalf("key=%q, index=%d: got err=%v", tc.key, tc.index, err) + } + } + + { + i, err := newRawBlockIter(bytes.Compare, k) + require.NoError(t, err) + i.Last() + for j, keyWant := range []string{"banana", "apricot", "apple"} { + if !i.Valid() { + t.Fatalf("j=%d: Valid got false, want true", j) + } + if keyGot := string(i.Key().UserKey); keyGot != keyWant { + t.Fatalf("j=%d: got %q, want %q", j, keyGot, keyWant) + } + i.Prev() + } + if i.Valid() { + t.Fatalf("Valid got true, want false") + } + if err := i.Close(); err != nil { + t.Fatalf("got err=%v", err) + } + } +} + +func TestBlockIter2(t *testing.T) { + makeIkey := func(s string) InternalKey { + j := strings.Index(s, ":") + seqNum, err := strconv.Atoi(s[j+1:]) + if err != nil { + panic(err) + } + return base.MakeInternalKey([]byte(s[:j]), uint64(seqNum), InternalKeyKindSet) + } + + var block []byte + + for _, r := range []int{1, 2, 3, 4} { + t.Run(fmt.Sprintf("restart=%d", r), func(t *testing.T) { + datadriven.RunTest(t, "testdata/block", func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "build": + w := &blockWriter{restartInterval: r} + for _, e := range strings.Split(strings.TrimSpace(d.Input), ",") { + w.add(makeIkey(e), nil) + } + block = w.finish() + return "" + + case "iter": + iter, err := newBlockIter(bytes.Compare, block) + if err != nil { + return err.Error() + } + + iter.globalSeqNum, err = scanGlobalSeqNum(d) + if err != nil { + return err.Error() + } + return itertest.RunInternalIterCmd(t, d, iter, itertest.Condensed) + + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) + }) + } +} + +func TestBlockIterKeyStability(t *testing.T) { + w := &blockWriter{restartInterval: 1} + expected := [][]byte{ + []byte("apple"), + []byte("apricot"), + []byte("banana"), + } + for i := range expected { + w.add(InternalKey{UserKey: expected[i]}, nil) + } + block := w.finish() + + i, err := newBlockIter(bytes.Compare, block) + require.NoError(t, err) + + // Check that the supplied slice resides within the bounds of the block. + check := func(v []byte) { + t.Helper() + begin := unsafe.Pointer(&v[0]) + end := unsafe.Pointer(uintptr(begin) + uintptr(len(v))) + blockBegin := unsafe.Pointer(&block[0]) + blockEnd := unsafe.Pointer(uintptr(blockBegin) + uintptr(len(block))) + if uintptr(begin) < uintptr(blockBegin) || uintptr(end) > uintptr(blockEnd) { + t.Fatalf("key %p-%p resides outside of block %p-%p", begin, end, blockBegin, blockEnd) + } + } + + // Check that various means of iterating over the data match our expected + // values. Note that this is only guaranteed because of the usage of a + // restart-interval of 1 so that prefix compression was not performed. + for j := range expected { + keys := [][]byte{} + for key, _ := i.SeekGE(expected[j], base.SeekGEFlagsNone); key != nil; key, _ = i.Next() { + check(key.UserKey) + keys = append(keys, key.UserKey) + } + require.EqualValues(t, expected[j:], keys) + } + + for j := range expected { + keys := [][]byte{} + for key, _ := i.SeekLT(expected[j], base.SeekLTFlagsNone); key != nil; key, _ = i.Prev() { + check(key.UserKey) + keys = append(keys, key.UserKey) + } + for i, j := 0, len(keys)-1; i < j; i, j = i+1, j-1 { + keys[i], keys[j] = keys[j], keys[i] + } + require.EqualValues(t, expected[:j], keys) + } +} + +// Regression test for a bug in blockIter.Next where it was failing to handle +// the case where it is switching from reverse to forward iteration. When that +// switch occurs we need to populate blockIter.fullKey so that prefix +// decompression works properly. +func TestBlockIterReverseDirections(t *testing.T) { + w := &blockWriter{restartInterval: 4} + keys := [][]byte{ + []byte("apple0"), + []byte("apple1"), + []byte("apple2"), + []byte("banana"), + []byte("carrot"), + } + for i := range keys { + w.add(InternalKey{UserKey: keys[i]}, nil) + } + block := w.finish() + + for targetPos := 0; targetPos < w.restartInterval; targetPos++ { + t.Run("", func(t *testing.T) { + i, err := newBlockIter(bytes.Compare, block) + require.NoError(t, err) + + pos := 3 + if key, _ := i.SeekLT([]byte("carrot"), base.SeekLTFlagsNone); !bytes.Equal(keys[pos], key.UserKey) { + t.Fatalf("expected %s, but found %s", keys[pos], key.UserKey) + } + for pos > targetPos { + pos-- + if key, _ := i.Prev(); !bytes.Equal(keys[pos], key.UserKey) { + t.Fatalf("expected %s, but found %s", keys[pos], key.UserKey) + } + } + pos++ + if key, _ := i.Next(); !bytes.Equal(keys[pos], key.UserKey) { + t.Fatalf("expected %s, but found %s", keys[pos], key.UserKey) + } + }) + } +} + +func BenchmarkBlockIterSeekGE(b *testing.B) { + const blockSize = 32 << 10 + + for _, restartInterval := range []int{16} { + b.Run(fmt.Sprintf("restart=%d", restartInterval), + func(b *testing.B) { + w := &blockWriter{ + restartInterval: restartInterval, + } + + var ikey InternalKey + var keys [][]byte + for i := 0; w.estimatedSize() < blockSize; i++ { + key := []byte(fmt.Sprintf("%05d", i)) + keys = append(keys, key) + ikey.UserKey = key + w.add(ikey, nil) + } + + it, err := newBlockIter(bytes.Compare, w.finish()) + if err != nil { + b.Fatal(err) + } + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + k := keys[rng.Intn(len(keys))] + it.SeekGE(k, base.SeekGEFlagsNone) + if testing.Verbose() { + if !it.valid() { + b.Fatal("expected to find key") + } + if !bytes.Equal(k, it.Key().UserKey) { + b.Fatalf("expected %s, but found %s", k, it.Key().UserKey) + } + } + } + }) + } +} + +func BenchmarkBlockIterSeekLT(b *testing.B) { + const blockSize = 32 << 10 + + for _, restartInterval := range []int{16} { + b.Run(fmt.Sprintf("restart=%d", restartInterval), + func(b *testing.B) { + w := &blockWriter{ + restartInterval: restartInterval, + } + + var ikey InternalKey + var keys [][]byte + for i := 0; w.estimatedSize() < blockSize; i++ { + key := []byte(fmt.Sprintf("%05d", i)) + keys = append(keys, key) + ikey.UserKey = key + w.add(ikey, nil) + } + + it, err := newBlockIter(bytes.Compare, w.finish()) + if err != nil { + b.Fatal(err) + } + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + j := rng.Intn(len(keys)) + it.SeekLT(keys[j], base.SeekLTFlagsNone) + if testing.Verbose() { + if j == 0 { + if it.valid() { + b.Fatal("unexpected key") + } + } else { + if !it.valid() { + b.Fatal("expected to find key") + } + k := keys[j-1] + if !bytes.Equal(k, it.Key().UserKey) { + b.Fatalf("expected %s, but found %s", k, it.Key().UserKey) + } + } + } + } + }) + } +} + +func BenchmarkBlockIterNext(b *testing.B) { + const blockSize = 32 << 10 + + for _, restartInterval := range []int{16} { + b.Run(fmt.Sprintf("restart=%d", restartInterval), + func(b *testing.B) { + w := &blockWriter{ + restartInterval: restartInterval, + } + + var ikey InternalKey + for i := 0; w.estimatedSize() < blockSize; i++ { + ikey.UserKey = []byte(fmt.Sprintf("%05d", i)) + w.add(ikey, nil) + } + + it, err := newBlockIter(bytes.Compare, w.finish()) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if !it.valid() { + it.First() + } + it.Next() + } + }) + } +} + +func BenchmarkBlockIterPrev(b *testing.B) { + const blockSize = 32 << 10 + + for _, restartInterval := range []int{16} { + b.Run(fmt.Sprintf("restart=%d", restartInterval), + func(b *testing.B) { + w := &blockWriter{ + restartInterval: restartInterval, + } + + var ikey InternalKey + for i := 0; w.estimatedSize() < blockSize; i++ { + ikey.UserKey = []byte(fmt.Sprintf("%05d", i)) + w.add(ikey, nil) + } + + it, err := newBlockIter(bytes.Compare, w.finish()) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if !it.valid() { + it.Last() + } + it.Prev() + } + }) + } +} diff --git a/pebble/sstable/buffer_pool.go b/pebble/sstable/buffer_pool.go new file mode 100644 index 0000000..2e98d44 --- /dev/null +++ b/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/pebble/sstable/buffer_pool_test.go b/pebble/sstable/buffer_pool_test.go new file mode 100644 index 0000000..66ae094 --- /dev/null +++ b/pebble/sstable/buffer_pool_test.go @@ -0,0 +1,78 @@ +// 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 ( + "bytes" + "fmt" + "io" + "testing" + + "github.com/cockroachdb/datadriven" +) + +func writeBufferPool(w io.Writer, bp *BufferPool) { + for i := 0; i < cap(bp.pool); i++ { + if i > 0 { + fmt.Fprint(w, " ") + } + if i >= len(bp.pool) { + fmt.Fprint(w, "[ ]") + continue + } + sz := len(bp.pool[i].v.Buf()) + if bp.pool[i].b == nil { + fmt.Fprintf(w, "[%4d]", sz) + } else { + fmt.Fprintf(w, "<%4d>", sz) + } + } +} + +func TestBufferPool(t *testing.T) { + var bp BufferPool + var buf bytes.Buffer + handles := map[string]Buf{} + drainPool := func() { + for h, b := range handles { + b.Release() + delete(handles, h) + } + bp.Release() + } + defer drainPool() + datadriven.RunTest(t, "testdata/buffer_pool", func(t *testing.T, td *datadriven.TestData) string { + buf.Reset() + switch td.Cmd { + case "init": + if cap(bp.pool) > 0 { + drainPool() + } + var initialSize int + td.ScanArgs(t, "size", &initialSize) + bp.Init(initialSize) + writeBufferPool(&buf, &bp) + return buf.String() + case "alloc": + var n int + var handle string + td.ScanArgs(t, "n", &n) + td.ScanArgs(t, "handle", &handle) + handles[handle] = bp.Alloc(n) + writeBufferPool(&buf, &bp) + return buf.String() + case "release": + var handle string + td.ScanArgs(t, "handle", &handle) + b := handles[handle] + b.Release() + delete(handles, handle) + writeBufferPool(&buf, &bp) + return buf.String() + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} diff --git a/pebble/sstable/category_stats.go b/pebble/sstable/category_stats.go new file mode 100644 index 0000000..4a5d5e3 --- /dev/null +++ b/pebble/sstable/category_stats.go @@ -0,0 +1,172 @@ +// Copyright 2018 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 ( + "sync" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/redact" + "github.com/cockroachdb/pebble/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" +) + +// Category is a user-understandable string, where stats are aggregated for +// each category. The cardinality of this should be low, say < 20. The prefix +// "pebble-" is reserved for internal Pebble categories. +// +// Examples of categories that can be useful in the CockroachDB context are: +// sql-user, sql-stats, raft, rangefeed, mvcc-gc, range-snapshot. +type Category string + +// QoSLevel describes whether the read is latency-sensitive or not. Each +// category must map to a single QoSLevel. While category strings are opaque +// to Pebble, the QoSLevel may be internally utilized in Pebble to better +// optimize future reads. +type QoSLevel int + +const ( + // LatencySensitiveQoSLevel is the default when QoSLevel is not specified, + // and represents reads that are latency-sensitive. + LatencySensitiveQoSLevel QoSLevel = iota + // NonLatencySensitiveQoSLevel represents reads that are not + // latency-sensitive. + NonLatencySensitiveQoSLevel +) + +// SafeFormat implements the redact.SafeFormatter interface. +func (q QoSLevel) SafeFormat(p redact.SafePrinter, verb rune) { + switch q { + case LatencySensitiveQoSLevel: + p.Printf("latency") + case NonLatencySensitiveQoSLevel: + p.Printf("non-latency") + default: + p.Printf("") + } +} + +// StringToQoSForTesting returns the QoSLevel for the string, or panics if the +// string is not known. +func StringToQoSForTesting(s string) QoSLevel { + switch s { + case "latency": + return LatencySensitiveQoSLevel + case "non-latency": + return NonLatencySensitiveQoSLevel + } + panic(errors.AssertionFailedf("unknown QoS %s", s)) +} + +// CategoryAndQoS specifies both the Category and the QoSLevel. +type CategoryAndQoS struct { + Category + QoSLevel +} + +// CategoryStats provides stats about a category of reads. +type CategoryStats struct { + // BlockBytes is the bytes in the loaded blocks. If the block was + // compressed, this is the compressed bytes. Currently, only the index + // blocks, data blocks containing points, and filter blocks are included. + // Additionally, value blocks read after the corresponding iterator is + // closed are not included. + BlockBytes uint64 + // BlockBytesInCache is the subset of BlockBytes that were in the block + // cache. + BlockBytesInCache uint64 +} + +func (s *CategoryStats) aggregate(a CategoryStats) { + s.BlockBytes += a.BlockBytes + s.BlockBytesInCache += a.BlockBytesInCache +} + +// CategoryStatsAggregate is the aggregate for the given category. +type CategoryStatsAggregate struct { + Category + QoSLevel + CategoryStats +} + +type categoryStatsWithMu struct { + mu sync.Mutex + // Protected by mu. + stats CategoryStatsAggregate +} + +// CategoryStatsCollector collects and aggregates the stats per category. +type CategoryStatsCollector struct { + // mu protects additions to statsMap. + mu sync.Mutex + // Category => categoryStatsWithMu. + statsMap sync.Map +} + +func (c *CategoryStatsCollector) reportStats( + category Category, qosLevel QoSLevel, stats CategoryStats, +) { + v, ok := c.statsMap.Load(category) + if !ok { + c.mu.Lock() + v, _ = c.statsMap.LoadOrStore(category, &categoryStatsWithMu{ + stats: CategoryStatsAggregate{Category: category, QoSLevel: qosLevel}, + }) + c.mu.Unlock() + } + aggStats := v.(*categoryStatsWithMu) + aggStats.mu.Lock() + aggStats.stats.CategoryStats.aggregate(stats) + aggStats.mu.Unlock() +} + +// GetStats returns the aggregated stats. +func (c *CategoryStatsCollector) GetStats() []CategoryStatsAggregate { + var stats []CategoryStatsAggregate + c.statsMap.Range(func(_, v any) bool { + aggStats := v.(*categoryStatsWithMu) + aggStats.mu.Lock() + s := aggStats.stats + aggStats.mu.Unlock() + if len(s.Category) == 0 { + s.Category = "_unknown" + } + stats = append(stats, s) + return true + }) + slices.SortFunc(stats, func(a, b CategoryStatsAggregate) int { + return cmp.Compare(a.Category, b.Category) + }) + return stats +} + +// iterStatsAccumulator is a helper for a sstable iterator to accumulate +// stats, which are reported to the CategoryStatsCollector when the +// accumulator is closed. +type iterStatsAccumulator struct { + Category + QoSLevel + stats CategoryStats + collector *CategoryStatsCollector +} + +func (accum *iterStatsAccumulator) init( + categoryAndQoS CategoryAndQoS, collector *CategoryStatsCollector, +) { + accum.Category = categoryAndQoS.Category + accum.QoSLevel = categoryAndQoS.QoSLevel + accum.collector = collector +} + +func (accum *iterStatsAccumulator) reportStats(blockBytes, blockBytesInCache uint64) { + accum.stats.BlockBytes += blockBytes + accum.stats.BlockBytesInCache += blockBytesInCache +} + +func (accum *iterStatsAccumulator) close() { + if accum.collector != nil { + accum.collector.reportStats(accum.Category, accum.QoSLevel, accum.stats) + } +} diff --git a/pebble/sstable/comparer.go b/pebble/sstable/comparer.go new file mode 100644 index 0000000..66a20b5 --- /dev/null +++ b/pebble/sstable/comparer.go @@ -0,0 +1,34 @@ +// 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 "github.com/cockroachdb/pebble/internal/base" + +// Compare exports the base.Compare type. +type Compare = base.Compare + +// Equal exports the base.Equal type. +type Equal = base.Equal + +// AbbreviatedKey exports the base.AbbreviatedKey type. +type AbbreviatedKey = base.AbbreviatedKey + +// Separator exports the base.Separator type. +type Separator = base.Separator + +// Successor exports the base.Successor type. +type Successor = base.Successor + +// Split exports the base.Split type. +type Split = base.Split + +// Comparer exports the base.Comparer type. +type Comparer = base.Comparer + +// DefaultComparer exports the base.DefaultComparer variable. +var DefaultComparer = base.DefaultComparer + +// Merger exports the base.Merger type. +type Merger = base.Merger diff --git a/pebble/sstable/compression.go b/pebble/sstable/compression.go new file mode 100644 index 0000000..0db70c8 --- /dev/null +++ b/pebble/sstable/compression.go @@ -0,0 +1,99 @@ +// Copyright 2021 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 ( + "encoding/binary" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/golang/snappy" +) + +func decompressedLen(blockType blockType, b []byte) (int, int, error) { + switch blockType { + case noCompressionBlockType: + return 0, 0, nil + case snappyCompressionBlockType: + l, err := snappy.DecodedLen(b) + return l, 0, err + case zstdCompressionBlockType: + // This will also be used by zlib, bzip2 and lz4 to retrieve the decodedLen + // if we implement these algorithms in the future. + decodedLenU64, varIntLen := binary.Uvarint(b) + if varIntLen <= 0 { + return 0, 0, base.CorruptionErrorf("pebble/table: compression block has invalid length") + } + return int(decodedLenU64), varIntLen, nil + default: + return 0, 0, base.CorruptionErrorf("pebble/table: unknown block compression: %d", errors.Safe(blockType)) + } +} + +func decompressInto(blockType blockType, compressed []byte, buf []byte) ([]byte, error) { + var result []byte + var err error + switch blockType { + case snappyCompressionBlockType: + result, err = snappy.Decode(buf, compressed) + case zstdCompressionBlockType: + result, err = decodeZstd(buf, compressed) + } + if err != nil { + return nil, base.MarkCorruptionError(err) + } + if len(result) != 0 && (len(result) != len(buf) || &result[0] != &buf[0]) { + return nil, base.CorruptionErrorf("pebble/table: decompressed into unexpected buffer: %p != %p", + errors.Safe(result), errors.Safe(buf)) + } + return result, nil +} + +// 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(blockType blockType, b []byte) (*cache.Value, error) { + if blockType == noCompressionBlockType { + return nil, nil + } + // first obtain the decoded length. + decodedLen, prefixLen, err := decompressedLen(blockType, b) + if err != nil { + return nil, err + } + b = b[prefixLen:] + // Allocate sufficient space from the cache. + decoded := cache.Alloc(decodedLen) + decodedBuf := decoded.Buf() + if _, err := decompressInto(blockType, b, decodedBuf); err != nil { + cache.Free(decoded) + return nil, err + } + return decoded, nil +} + +// compressBlock compresses an SST block, using compressBuf as the desired destination. +func compressBlock( + compression Compression, b []byte, compressedBuf []byte, +) (blockType blockType, compressed []byte) { + switch compression { + case SnappyCompression: + return snappyCompressionBlockType, snappy.Encode(compressedBuf, b) + case NoCompression: + return noCompressionBlockType, b + } + + if len(compressedBuf) < binary.MaxVarintLen64 { + compressedBuf = append(compressedBuf, make([]byte, binary.MaxVarintLen64-len(compressedBuf))...) + } + varIntLen := binary.PutUvarint(compressedBuf, uint64(len(b))) + switch compression { + case ZstdCompression: + return zstdCompressionBlockType, encodeZstd(compressedBuf, varIntLen, b) + default: + return noCompressionBlockType, b + } +} diff --git a/pebble/sstable/compression_cgo.go b/pebble/sstable/compression_cgo.go new file mode 100644 index 0000000..ad7d844 --- /dev/null +++ b/pebble/sstable/compression_cgo.go @@ -0,0 +1,34 @@ +// Copyright 2021 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 cgo +// +build cgo + +package sstable + +import ( + "bytes" + + "github.com/DataDog/zstd" +) + +// decodeZstd decompresses b with the Zstandard algorithm. +// It reuses the preallocated capacity of decodedBuf if it is sufficient. +// On success, it returns the decoded byte slice. +func decodeZstd(decodedBuf, b []byte) ([]byte, error) { + return zstd.Decompress(decodedBuf, b) +} + +// encodeZstd compresses b with the Zstandard algorithm at default compression +// level (level 3). It reuses the preallocated capacity of compressedBuf if it +// is sufficient. The subslice `compressedBuf[:varIntLen]` should already encode +// the length of `b` before calling encodeZstd. It returns the encoded byte +// slice, including the `compressedBuf[:varIntLen]` prefix. +func encodeZstd(compressedBuf []byte, varIntLen int, b []byte) []byte { + buf := bytes.NewBuffer(compressedBuf[:varIntLen]) + writer := zstd.NewWriterLevel(buf, 3) + writer.Write(b) + writer.Close() + return buf.Bytes() +} diff --git a/pebble/sstable/compression_cgo_test.go b/pebble/sstable/compression_cgo_test.go new file mode 100644 index 0000000..1de7395 --- /dev/null +++ b/pebble/sstable/compression_cgo_test.go @@ -0,0 +1,19 @@ +// Copyright 2021 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 cgo +// +build cgo + +package sstable + +// useStandardZstdLib indicates whether the zstd implementation is a port of the +// official one in the facebook/zstd repository. +// +// This constant is only used in tests. Some tests rely on reproducibility of +// SST files, but a custom implementation of zstd will produce different +// compression result. So those tests have to be disabled in such cases. +// +// We cannot always use the official facebook/zstd implementation since it +// relies on CGo. +const useStandardZstdLib = true diff --git a/pebble/sstable/compression_nocgo.go b/pebble/sstable/compression_nocgo.go new file mode 100644 index 0000000..42c34fb --- /dev/null +++ b/pebble/sstable/compression_nocgo.go @@ -0,0 +1,30 @@ +// Copyright 2021 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 !cgo +// +build !cgo + +package sstable + +import "github.com/klauspost/compress/zstd" + +// decodeZstd decompresses b with the Zstandard algorithm. +// It reuses the preallocated capacity of decodedBuf if it is sufficient. +// On success, it returns the decoded byte slice. +func decodeZstd(decodedBuf, b []byte) ([]byte, error) { + decoder, _ := zstd.NewReader(nil) + defer decoder.Close() + return decoder.DecodeAll(b, decodedBuf[:0]) +} + +// encodeZstd compresses b with the Zstandard algorithm at default compression +// level (level 3). It reuses the preallocated capacity of compressedBuf if it +// is sufficient. The subslice `compressedBuf[:varIntLen]` should already encode +// the length of `b` before calling encodeZstd. It returns the encoded byte +// slice, including the `compressedBuf[:varIntLen]` prefix. +func encodeZstd(compressedBuf []byte, varIntLen int, b []byte) []byte { + encoder, _ := zstd.NewWriter(nil) + defer encoder.Close() + return encoder.EncodeAll(b, compressedBuf[:varIntLen]) +} diff --git a/pebble/sstable/compression_nocgo_test.go b/pebble/sstable/compression_nocgo_test.go new file mode 100644 index 0000000..1c755a2 --- /dev/null +++ b/pebble/sstable/compression_nocgo_test.go @@ -0,0 +1,19 @@ +// Copyright 2021 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 !cgo +// +build !cgo + +package sstable + +// useStandardZstdLib indicates whether the zstd implementation is a port of the +// official one in the facebook/zstd repository. +// +// This constant is only used in tests. Some tests rely on reproducibility of +// SST files, but a custom implementation of zstd will produce different +// compression result. So those tests have to be disabled in such cases. +// +// We cannot always use the official facebook/zstd implementation since it +// relies on CGo. +const useStandardZstdLib = false diff --git a/pebble/sstable/compression_test.go b/pebble/sstable/compression_test.go new file mode 100644 index 0000000..4ee542f --- /dev/null +++ b/pebble/sstable/compression_test.go @@ -0,0 +1,60 @@ +// 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 ( + "encoding/binary" + "math/rand" + "testing" + "time" + + "github.com/cockroachdb/pebble/internal/cache" + "github.com/stretchr/testify/require" +) + +func TestCompressionRoundtrip(t *testing.T) { + seed := time.Now().UnixNano() + t.Logf("seed %d", seed) + rng := rand.New(rand.NewSource(seed)) + + for compression := DefaultCompression + 1; compression < NCompression; compression++ { + t.Run(compression.String(), func(t *testing.T) { + payload := make([]byte, rng.Intn(10<<10 /* 10 KiB */)) + rng.Read(payload) + // Create a randomly-sized buffer to house the compressed output. If it's + // not sufficient, compressBlock should allocate one that is. + compressedBuf := make([]byte, rng.Intn(1<<10 /* 1 KiB */)) + + btyp, compressed := compressBlock(compression, payload, compressedBuf) + v, err := decompressBlock(btyp, compressed) + require.NoError(t, err) + got := payload + if v != nil { + got = v.Buf() + require.Equal(t, payload, got) + cache.Free(v) + } + }) + } +} + +// TestDecompressionError tests that a decompressing a value that does not +// decompress returns an error. +func TestDecompressionError(t *testing.T) { + rng := rand.New(rand.NewSource(1 /* fixed seed */)) + + // Create a buffer to represent a faux zstd compressed block. It's prefixed + // with a uvarint of the appropriate length, followed by garabge. + fauxCompressed := make([]byte, rng.Intn(10<<10 /* 10 KiB */)) + compressedPayloadLen := len(fauxCompressed) - binary.MaxVarintLen64 + n := binary.PutUvarint(fauxCompressed, uint64(compressedPayloadLen)) + fauxCompressed = fauxCompressed[:n+compressedPayloadLen] + rng.Read(fauxCompressed[n:]) + + v, err := decompressBlock(zstdCompressionBlockType, fauxCompressed) + t.Log(err) + require.Error(t, err) + require.Nil(t, v) +} diff --git a/pebble/sstable/data_test.go b/pebble/sstable/data_test.go new file mode 100644 index 0000000..2b1926a --- /dev/null +++ b/pebble/sstable/data_test.go @@ -0,0 +1,500 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package sstable + +import ( + "bytes" + "context" + "fmt" + "io" + "strconv" + "strings" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/vfs" +) + +func optsFromArgs(td *datadriven.TestData, writerOpts *WriterOptions) error { + for _, arg := range td.CmdArgs { + switch arg.Key { + case "leveldb": + if len(arg.Vals) != 0 { + return errors.Errorf("%s: arg %s expects 0 values", td.Cmd, arg.Key) + } + writerOpts.TableFormat = TableFormatLevelDB + case "block-size": + if len(arg.Vals) != 1 { + return errors.Errorf("%s: arg %s expects 1 value", td.Cmd, arg.Key) + } + var err error + writerOpts.BlockSize, err = strconv.Atoi(arg.Vals[0]) + if err != nil { + return err + } + case "index-block-size": + if len(arg.Vals) != 1 { + return errors.Errorf("%s: arg %s expects 1 value", td.Cmd, arg.Key) + } + var err error + writerOpts.IndexBlockSize, err = strconv.Atoi(arg.Vals[0]) + if err != nil { + return err + } + case "filter": + writerOpts.FilterPolicy = bloom.FilterPolicy(10) + case "comparer-split-4b-suffix": + writerOpts.Comparer = test4bSuffixComparer + case "writing-to-lowest-level": + writerOpts.WritingToLowestLevel = true + case "is-strict-obsolete": + writerOpts.IsStrictObsolete = true + } + } + return nil +} + +func runBuildCmd( + td *datadriven.TestData, writerOpts *WriterOptions, cacheSize int, +) (*WriterMetadata, *Reader, error) { + + f0 := &memFile{} + if err := optsFromArgs(td, writerOpts); err != nil { + return nil, nil, err + } + + w := NewWriter(f0, *writerOpts) + var rangeDels []keyspan.Span + rangeDelFrag := keyspan.Fragmenter{ + Cmp: DefaultComparer.Compare, + Format: DefaultComparer.FormatKey, + Emit: func(s keyspan.Span) { + rangeDels = append(rangeDels, s) + }, + } + var rangeKeys []keyspan.Span + rangeKeyFrag := keyspan.Fragmenter{ + Cmp: DefaultComparer.Compare, + Format: DefaultComparer.FormatKey, + Emit: func(s keyspan.Span) { + rangeKeys = append(rangeKeys, s) + }, + } + for _, data := range strings.Split(td.Input, "\n") { + if strings.HasPrefix(data, "rangekey:") { + var err error + func() { + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("%v", r) + } + }() + rangeKeyFrag.Add(keyspan.ParseSpan(strings.TrimPrefix(data, "rangekey:"))) + }() + if err != nil { + return nil, nil, err + } + continue + } + + forceObsolete := false + if strings.HasPrefix(data, "force-obsolete:") { + data = strings.TrimSpace(strings.TrimPrefix(data, "force-obsolete:")) + forceObsolete = true + } + j := strings.Index(data, ":") + key := base.ParseInternalKey(data[:j]) + value := []byte(data[j+1:]) + switch key.Kind() { + case InternalKeyKindRangeDelete: + if forceObsolete { + return nil, nil, errors.Errorf("force-obsolete is not allowed for RANGEDEL") + } + var err error + func() { + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("%v", r) + } + }() + rangeDelFrag.Add(keyspan.Span{ + Start: key.UserKey, + End: value, + Keys: []keyspan.Key{{Trailer: key.Trailer}}, + }) + }() + if err != nil { + return nil, nil, err + } + default: + if err := w.AddWithForceObsolete(key, value, forceObsolete); err != nil { + return nil, nil, err + } + } + } + rangeDelFrag.Finish() + for _, v := range rangeDels { + for _, k := range v.Keys { + ik := base.InternalKey{UserKey: v.Start, Trailer: k.Trailer} + if err := w.Add(ik, v.End); err != nil { + return nil, nil, err + } + } + } + rangeKeyFrag.Finish() + for _, s := range rangeKeys { + if err := w.addRangeKeySpan(s); err != nil { + return nil, nil, err + } + } + if err := w.Close(); err != nil { + return nil, nil, err + } + meta, err := w.Metadata() + if err != nil { + return nil, nil, err + } + + readerOpts := ReaderOptions{Comparer: writerOpts.Comparer} + if writerOpts.FilterPolicy != nil { + readerOpts.Filters = map[string]FilterPolicy{ + writerOpts.FilterPolicy.Name(): writerOpts.FilterPolicy, + } + } + if cacheSize > 0 { + readerOpts.Cache = cache.New(int64(cacheSize)) + defer readerOpts.Cache.Unref() + } + r, err := NewMemReader(f0.Data(), readerOpts) + if err != nil { + return nil, nil, err + } + return meta, r, nil +} + +func runBuildRawCmd( + td *datadriven.TestData, opts *WriterOptions, +) (*WriterMetadata, *Reader, error) { + mem := vfs.NewMem() + provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(mem, "" /* dirName */)) + if err != nil { + return nil, nil, err + } + defer provider.Close() + + f0, _, err := provider.Create(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.CreateOptions{}) + if err != nil { + return nil, nil, err + } + + w := NewWriter(f0, *opts) + for i := range td.CmdArgs { + arg := &td.CmdArgs[i] + if arg.Key == "range-del-v1" { + w.rangeDelV1Format = true + break + } + } + + for _, data := range strings.Split(td.Input, "\n") { + if strings.HasPrefix(data, "rangekey:") { + data = strings.TrimPrefix(data, "rangekey:") + if err := w.addRangeKeySpan(keyspan.ParseSpan(data)); err != nil { + return nil, nil, err + } + continue + } + + j := strings.Index(data, ":") + key := base.ParseInternalKey(data[:j]) + value := []byte(data[j+1:]) + switch key.Kind() { + case base.InternalKeyKindRangeKeyDelete, + base.InternalKeyKindRangeKeyUnset, + base.InternalKeyKindRangeKeySet: + if err := w.AddRangeKey(key, value); err != nil { + return nil, nil, err + } + default: + if err := w.Add(key, value); err != nil { + return nil, nil, err + } + } + } + if err := w.Close(); err != nil { + return nil, nil, err + } + meta, err := w.Metadata() + if err != nil { + return nil, nil, err + } + + f1, err := provider.OpenForReading(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.OpenOptions{}) + if err != nil { + return nil, nil, err + } + r, err := NewReader(f1, ReaderOptions{}) + if err != nil { + return nil, nil, err + } + return meta, r, nil +} + +func scanGlobalSeqNum(td *datadriven.TestData) (uint64, error) { + for _, arg := range td.CmdArgs { + switch arg.Key { + case "globalSeqNum": + if len(arg.Vals) != 1 { + return 0, errors.Errorf("%s: arg %s expects 1 value", td.Cmd, arg.Key) + } + v, err := strconv.Atoi(arg.Vals[0]) + if err != nil { + return 0, err + } + return uint64(v), nil + } + } + return 0, nil +} + +type runIterCmdOption func(*runIterCmdOptions) + +type runIterCmdOptions struct { + everyOp func(io.Writer) + everyOpAfter func(io.Writer) + stats *base.InternalIteratorStats +} + +func runIterCmdEveryOp(everyOp func(io.Writer)) runIterCmdOption { + return func(opts *runIterCmdOptions) { opts.everyOp = everyOp } +} + +func runIterCmdEveryOpAfter(everyOp func(io.Writer)) runIterCmdOption { + return func(opts *runIterCmdOptions) { opts.everyOpAfter = everyOp } +} + +func runIterCmdStats(stats *base.InternalIteratorStats) runIterCmdOption { + return func(opts *runIterCmdOptions) { opts.stats = stats } +} + +func runIterCmd( + td *datadriven.TestData, origIter Iterator, printValue bool, opt ...runIterCmdOption, +) string { + var opts runIterCmdOptions + for _, o := range opt { + o(&opts) + } + + iter := newIterAdapter(origIter) + defer iter.Close() + + var b bytes.Buffer + var prefix []byte + for _, line := range strings.Split(td.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + switch parts[0] { + case "seek-ge": + if len(parts) < 2 || len(parts) > 3 { + return "seek-ge [ []\n" + } + prefix = []byte(strings.TrimSpace(parts[1])) + var flags base.SeekGEFlags + if len(parts) == 3 { + if trySeekUsingNext, err := strconv.ParseBool(parts[2]); err != nil { + return err.Error() + } else if trySeekUsingNext { + flags = flags.EnableTrySeekUsingNext() + } + } + iter.SeekPrefixGE(prefix, prefix /* key */, flags) + case "seek-lt": + if len(parts) != 2 { + return "seek-lt \n" + } + prefix = nil + iter.SeekLT([]byte(strings.TrimSpace(parts[1])), base.SeekLTFlagsNone) + case "first": + prefix = nil + iter.First() + case "last": + prefix = nil + iter.Last() + case "next": + iter.Next() + case "next-ignore-result": + iter.NextIgnoreResult() + case "prev": + iter.Prev() + case "next-prefix": + if len(parts) != 1 { + return "next-prefix should have no parameter\n" + } + if iter.Key() == nil { + return "next-prefix cannot be called on exhauster iterator\n" + } + k := iter.Key().UserKey + prefixLen := testkeys.Comparer.Split(k) + k = k[:prefixLen] + kSucc := testkeys.Comparer.ImmediateSuccessor(nil, k) + iter.NextPrefix(kSucc) + case "set-bounds": + if len(parts) <= 1 || len(parts) > 3 { + return "set-bounds lower= upper=\n" + } + var lower []byte + var upper []byte + for _, part := range parts[1:] { + arg := strings.Split(strings.TrimSpace(part), "=") + switch arg[0] { + case "lower": + lower = []byte(arg[1]) + if len(lower) == 0 { + lower = nil + } + case "upper": + upper = []byte(arg[1]) + if len(upper) == 0 { + upper = nil + } + default: + return fmt.Sprintf("set-bounds: unknown arg: %s", arg) + } + } + iter.SetBounds(lower, upper) + case "stats": + // The timing is non-deterministic, so set to 0. + opts.stats.BlockReadDuration = 0 + fmt.Fprintf(&b, "%+v\n", *opts.stats) + continue + case "reset-stats": + *opts.stats = base.InternalIteratorStats{} + continue + case "internal-iter-state": + fmt.Fprintf(&b, "| %T:\n", origIter) + si, _ := origIter.(*singleLevelIterator) + if twoLevelIter, ok := origIter.(*twoLevelIterator); ok { + si = &twoLevelIter.singleLevelIterator + if twoLevelIter.topLevelIndex.valid() { + fmt.Fprintf(&b, "| topLevelIndex.Key() = %q\n", twoLevelIter.topLevelIndex.Key()) + v := twoLevelIter.topLevelIndex.value() + bhp, err := decodeBlockHandleWithProperties(v.InPlaceValue()) + if err != nil { + fmt.Fprintf(&b, "| topLevelIndex.InPlaceValue() failed to decode as BHP: %s\n", err) + } else { + fmt.Fprintf(&b, "| topLevelIndex.InPlaceValue() = (Offset: %d, Length: %d, Props: %x)\n", + bhp.Offset, bhp.Length, bhp.Props) + } + } else { + fmt.Fprintf(&b, "| topLevelIndex iter invalid\n") + } + fmt.Fprintf(&b, "| topLevelIndex.isDataInvalidated()=%t\n", twoLevelIter.topLevelIndex.isDataInvalidated()) + } + if si.index.valid() { + fmt.Fprintf(&b, "| index.Key() = %q\n", si.index.Key()) + v := si.index.value() + bhp, err := decodeBlockHandleWithProperties(v.InPlaceValue()) + if err != nil { + fmt.Fprintf(&b, "| index.InPlaceValue() failed to decode as BHP: %s\n", err) + } else { + fmt.Fprintf(&b, "| index.InPlaceValue() = (Offset: %d, Length: %d, Props: %x)\n", + bhp.Offset, bhp.Length, bhp.Props) + } + } else { + fmt.Fprintf(&b, "| index iter invalid\n") + } + fmt.Fprintf(&b, "| index.isDataInvalidated()=%t\n", si.index.isDataInvalidated()) + fmt.Fprintf(&b, "| data.isDataInvalidated()=%t\n", si.data.isDataInvalidated()) + fmt.Fprintf(&b, "| hideObsoletePoints = %t\n", si.hideObsoletePoints) + fmt.Fprintf(&b, "| dataBH = (Offset: %d, Length: %d)\n", si.dataBH.Offset, si.dataBH.Length) + fmt.Fprintf(&b, "| (boundsCmp,positionedUsingLatestBounds) = (%d,%t)\n", si.boundsCmp, si.positionedUsingLatestBounds) + fmt.Fprintf(&b, "| exhaustedBounds = %d\n", si.exhaustedBounds) + + continue + } + if opts.everyOp != nil { + opts.everyOp(&b) + } + if iter.Valid() && checkValidPrefix(prefix, iter.Key().UserKey) { + fmt.Fprintf(&b, "<%s:%d>", iter.Key().UserKey, iter.Key().SeqNum()) + if printValue { + fmt.Fprintf(&b, ":%s", string(iter.Value())) + } + } else if err := iter.Error(); err != nil { + fmt.Fprintf(&b, "", err) + } else { + fmt.Fprintf(&b, ".") + } + if opts.everyOpAfter != nil { + opts.everyOpAfter(&b) + } + b.WriteString("\n") + } + return b.String() +} + +func runRewriteCmd( + td *datadriven.TestData, r *Reader, writerOpts WriterOptions, +) (*WriterMetadata, *Reader, error) { + var from, to []byte + for _, arg := range td.CmdArgs { + switch arg.Key { + case "from": + from = []byte(arg.Vals[0]) + case "to": + to = []byte(arg.Vals[0]) + } + } + if from == nil || to == nil { + return nil, r, errors.New("missing from/to") + } + + opts := writerOpts + if err := optsFromArgs(td, &opts); err != nil { + return nil, r, err + } + + f := &memFile{} + meta, _, err := rewriteKeySuffixesInBlocks(r, f, opts, from, to, 2) + if err != nil { + return nil, r, errors.Wrap(err, "rewrite failed") + } + readerOpts := ReaderOptions{Comparer: opts.Comparer} + if opts.FilterPolicy != nil { + readerOpts.Filters = map[string]FilterPolicy{ + opts.FilterPolicy.Name(): opts.FilterPolicy, + } + } + r.Close() + + r, err = NewMemReader(f.Data(), readerOpts) + if err != nil { + return nil, nil, err + } + return meta, r, nil +} diff --git a/pebble/sstable/filter.go b/pebble/sstable/filter.go new file mode 100644 index 0000000..7b2e1ab --- /dev/null +++ b/pebble/sstable/filter.go @@ -0,0 +1,122 @@ +// 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 "sync/atomic" + +// FilterMetrics holds metrics for the filter policy. +type FilterMetrics struct { + // The number of hits for the filter policy. This is the + // number of times the filter policy was successfully used to avoid access + // of a data block. + Hits int64 + // The number of misses for the filter policy. This is the number of times + // the filter policy was checked but was unable to filter an access of a data + // block. + Misses int64 +} + +// 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 +} + +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 +} + +// BlockHandleWithProperties is used for data blocks and first/lower level +// index blocks, since they can be annotated using BlockPropertyCollectors. +type BlockHandleWithProperties struct { + BlockHandle + Props []byte +} + +type filterWriter interface { + addKey(key []byte) + finish() ([]byte, error) + metaName() string + policyName() string +} + +type tableFilterReader struct { + policy FilterPolicy + metrics *FilterMetricsTracker +} + +func newTableFilterReader(policy FilterPolicy) *tableFilterReader { + return &tableFilterReader{ + policy: policy, + metrics: nil, + } +} + +func (f *tableFilterReader) mayContain(data, key []byte) bool { + mayContain := f.policy.MayContain(TableFilter, data, key) + if f.metrics != nil { + if mayContain { + f.metrics.misses.Add(1) + } else { + f.metrics.hits.Add(1) + } + } + return mayContain +} + +type tableFilterWriter struct { + policy FilterPolicy + writer FilterWriter + // count is the count of the number of keys added to the filter. + count int +} + +func newTableFilterWriter(policy FilterPolicy) *tableFilterWriter { + return &tableFilterWriter{ + policy: policy, + writer: policy.NewWriter(TableFilter), + } +} + +func (f *tableFilterWriter) addKey(key []byte) { + f.count++ + f.writer.AddKey(key) +} + +func (f *tableFilterWriter) finish() ([]byte, error) { + if f.count == 0 { + return nil, nil + } + return f.writer.Finish(nil), nil +} + +func (f *tableFilterWriter) metaName() string { + return "fullfilter." + f.policy.Name() +} + +func (f *tableFilterWriter) policyName() string { + return f.policy.Name() +} diff --git a/pebble/sstable/format.go b/pebble/sstable/format.go new file mode 100644 index 0000000..82310a5 --- /dev/null +++ b/pebble/sstable/format.go @@ -0,0 +1,257 @@ +// 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 sstable + +import ( + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" +) + +// TableFormat specifies the format version for sstables. The legacy LevelDB +// format is format version 1. +type TableFormat uint32 + +// The available table formats, representing the tuple (magic number, version +// number). Note that these values are not (and should not) be serialized to +// disk. The ordering should follow the order the versions were introduced to +// Pebble (i.e. the history is linear). +const ( + TableFormatUnspecified TableFormat = iota + TableFormatLevelDB + TableFormatRocksDBv2 + TableFormatPebblev1 // Block properties. + TableFormatPebblev2 // Range keys. + TableFormatPebblev3 // Value blocks. + TableFormatPebblev4 // DELSIZED tombstones. + NumTableFormats + + 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) { + switch string(magic) { + case levelDBMagic: + return TableFormatLevelDB, nil + case rocksDBMagic: + if version != rocksDBFormatVersion2 { + return TableFormatUnspecified, base.CorruptionErrorf( + "pebble/table: unsupported rocksdb format version %d", errors.Safe(version), + ) + } + return TableFormatRocksDBv2, nil + case pebbleDBMagic: + switch version { + case 1: + return TableFormatPebblev1, nil + case 2: + 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), + ) + } + default: + return TableFormatUnspecified, base.CorruptionErrorf( + "pebble/table: invalid table (bad magic number: 0x%x)", magic, + ) + } +} + +// AsTuple returns the TableFormat's (Magic String, Version) tuple. +func (f TableFormat) AsTuple() (string, uint32) { + switch f { + case TableFormatLevelDB: + return levelDBMagic, 0 + case TableFormatRocksDBv2: + return rocksDBMagic, 2 + case TableFormatPebblev1: + return pebbleDBMagic, 1 + case TableFormatPebblev2: + return pebbleDBMagic, 2 + case TableFormatPebblev3: + return pebbleDBMagic, 3 + case TableFormatPebblev4: + return pebbleDBMagic, 4 + default: + panic("sstable: unknown table format version tuple") + } +} + +// String returns the TableFormat (Magic String,Version) tuple. +func (f TableFormat) String() string { + switch f { + case TableFormatLevelDB: + return "(LevelDB)" + case TableFormatRocksDBv2: + return "(RocksDB,v2)" + case TableFormatPebblev1: + return "(Pebble,v1)" + case TableFormatPebblev2: + return "(Pebble,v2)" + case TableFormatPebblev3: + return "(Pebble,v3)" + case TableFormatPebblev4: + return "(Pebble,v4)" + default: + panic("sstable: unknown table format version tuple") + } +} diff --git a/pebble/sstable/format_test.go b/pebble/sstable/format_test.go new file mode 100644 index 0000000..f5589c1 --- /dev/null +++ b/pebble/sstable/format_test.go @@ -0,0 +1,96 @@ +// 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 sstable + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTableFormat_RoundTrip(t *testing.T) { + tcs := []struct { + name string + magic string + version uint32 + want TableFormat + wantErr string + }{ + // Valid cases. + { + name: "LevelDB", + magic: levelDBMagic, + version: 0, + want: TableFormatLevelDB, + }, + { + name: "RocksDBv2", + magic: rocksDBMagic, + version: 2, + want: TableFormatRocksDBv2, + }, + { + name: "PebbleDBv1", + magic: pebbleDBMagic, + version: 1, + want: TableFormatPebblev1, + }, + { + name: "PebbleDBv2", + magic: pebbleDBMagic, + version: 2, + want: TableFormatPebblev2, + }, + { + name: "PebbleDBv3", + magic: pebbleDBMagic, + version: 3, + want: TableFormatPebblev3, + }, + { + name: "PebbleDBv4", + magic: pebbleDBMagic, + version: 4, + want: TableFormatPebblev4, + }, + // Invalid cases. + { + name: "Invalid RocksDB version", + magic: rocksDBMagic, + version: 1, + wantErr: "pebble/table: unsupported rocksdb format version 1", + }, + { + name: "Invalid PebbleDB version", + magic: pebbleDBMagic, + version: 5, + wantErr: "pebble/table: unsupported pebble format version 5", + }, + { + name: "Unknown magic string", + magic: "foo", + wantErr: "pebble/table: invalid table (bad magic number: 0x666f6f)", + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + // Tuple -> TableFormat. + f, err := ParseTableFormat([]byte(tc.magic), tc.version) + if tc.wantErr != "" { + require.Error(t, err) + require.Equal(t, tc.wantErr, err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tc.want, f) + + // TableFormat -> Tuple. + s, v := f.AsTuple() + require.Equal(t, tc.magic, s) + require.Equal(t, tc.version, v) + }) + } +} diff --git a/pebble/sstable/internal.go b/pebble/sstable/internal.go new file mode 100644 index 0000000..0fe7c99 --- /dev/null +++ b/pebble/sstable/internal.go @@ -0,0 +1,36 @@ +// Copyright 2018 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/pebble/internal/base" + +// InternalKeyKind exports the base.InternalKeyKind type. +type InternalKeyKind = base.InternalKeyKind + +// SeekGEFlags exports base.SeekGEFlags. +type SeekGEFlags = base.SeekGEFlags + +// SeekLTFlags exports base.SeekLTFlags. +type SeekLTFlags = base.SeekLTFlags + +// These constants are part of the file format, and should not be changed. +const ( + InternalKeyKindDelete = base.InternalKeyKindDelete + InternalKeyKindSet = base.InternalKeyKindSet + InternalKeyKindMerge = base.InternalKeyKindMerge + InternalKeyKindLogData = base.InternalKeyKindLogData + InternalKeyKindSingleDelete = base.InternalKeyKindSingleDelete + InternalKeyKindRangeDelete = base.InternalKeyKindRangeDelete + InternalKeyKindSetWithDelete = base.InternalKeyKindSetWithDelete + InternalKeyKindDeleteSized = base.InternalKeyKindDeleteSized + InternalKeyKindMax = base.InternalKeyKindMax + InternalKeyKindInvalid = base.InternalKeyKindInvalid + InternalKeySeqNumBatch = base.InternalKeySeqNumBatch + InternalKeySeqNumMax = base.InternalKeySeqNumMax + InternalKeyRangeDeleteSentinel = base.InternalKeyRangeDeleteSentinel +) + +// InternalKey exports the base.InternalKey type. +type InternalKey = base.InternalKey diff --git a/pebble/sstable/layout.go b/pebble/sstable/layout.go new file mode 100644 index 0000000..5736ead --- /dev/null +++ b/pebble/sstable/layout.go @@ -0,0 +1,307 @@ +// 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" + "github.com/cockroachdb/pebble/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" +) + +// 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"}) + } + } + + slices.SortFunc(blocks, func(a, b block) int { + return cmp.Compare(a.Offset, b.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 /* iterStats */, 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/pebble/sstable/options.go b/pebble/sstable/options.go new file mode 100644 index 0000000..c5e1f79 --- /dev/null +++ b/pebble/sstable/options.go @@ -0,0 +1,305 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package sstable + +import ( + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/cache" +) + +// Compression is the per-block compression algorithm to use. +type Compression int + +// The available compression types. +const ( + DefaultCompression Compression = iota + NoCompression + SnappyCompression + ZstdCompression + 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: + return "Default" + case NoCompression: + return "NoCompression" + case SnappyCompression: + return "Snappy" + case ZstdCompression: + return "ZSTD" + default: + return "Unknown" + } +} + +// FilterType exports the base.FilterType type. +type FilterType = base.FilterType + +// Exported TableFilter constants. +const ( + TableFilter = base.TableFilter +) + +// FilterWriter exports the base.FilterWriter type. +type FilterWriter = base.FilterWriter + +// FilterPolicy exports the base.FilterPolicy type. +type FilterPolicy = base.FilterPolicy + +// TablePropertyCollector provides a hook for collecting user-defined +// properties based on the keys and values stored in an sstable. A new +// TablePropertyCollector is created for an sstable when the sstable is being +// written. +type TablePropertyCollector interface { + // Add is called with each new entry added to the sstable. While the sstable + // is itself sorted by key, do not assume that the entries are added in any + // order. In particular, the ordering of point entries and range tombstones + // is unspecified. + Add(key InternalKey, value []byte) error + + // Finish is called when all entries have been added to the sstable. The + // collected properties (if any) should be added to the specified map. Note + // that in case of an error during sstable construction, Finish may not be + // called. + Finish(userProps map[string]string) error + + // The name of the property collector. + Name() string +} + +// SuffixReplaceableTableCollector is an extension to the TablePropertyCollector +// interface that allows a table 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. +// +// A collector which supports being updated in such cases must be able to derive +// its updated value from its old value and the change being made to the suffix, +// without needing to be passed each updated K/V. +// +// For example, a collector that only inspects values can simply copy its +// previously computed property as-is, since key-suffix replacement does not +// change values, while a collector that depends only on key suffixes, like one +// which collected mvcc-timestamp bounds from timestamp-suffixed keys, can just +// set its new bounds from the new suffix, as it is common to all keys, without +// needing to recompute it from every key. +type SuffixReplaceableTableCollector interface { + // UpdateKeySuffixes is called when a table is updated to change the suffix of + // all keys in the table, and is passed the old value for that prop, if any, + // for that table as well as the old and new suffix. + UpdateKeySuffixes(oldProps map[string]string, oldSuffix, newSuffix []byte) error +} + +// ReaderOptions holds the parameters needed for reading an sstable. +type ReaderOptions struct { + // Cache is used to cache uncompressed blocks from sstables. + // + // 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. + // + // 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 + // map during normal usage of a DB. + Filters map[string]FilterPolicy + + // Merger defines the associative merge operation to use for merging values + // written with {Batch,DB}.Merge. The MergerName is checked for consistency + // with the value stored in the sstable when it was written. + MergerName string + + // Logger is an optional logger and tracer. + LoggerAndTracer base.LoggerAndTracer +} + +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 +} + +// WriterOptions holds the parameters used to control building an sstable. +type WriterOptions struct { + // BlockRestartInterval is the number of keys between restart points + // for delta encoding of keys. + // + // The default value is 16. + BlockRestartInterval int + + // BlockSize is the target uncompressed size in bytes of each table block. + // + // The default value is 4096. + BlockSize int + + // BlockSizeThreshold finishes a block if the block size is larger than the + // specified percentage of the target block size and adding the next entry + // would cause the block to be larger than the target block size. + // + // The default value is 90 + BlockSizeThreshold int + + // Cache is used to cache uncompressed blocks from sstables. + // + // The default is a nil cache. + Cache *cache.Cache + + // 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. + // + // The default value uses the same ordering as bytes.Compare. + Comparer *Comparer + + // Compression defines the per-block compression to use. + // + // The default value (DefaultCompression) uses snappy compression. + Compression Compression + + // FilterPolicy defines a filter algorithm (such as a Bloom filter) that can + // reduce disk reads for Get calls. + // + // One such implementation is bloom.FilterPolicy(10) from the pebble/bloom + // package. + // + // The default value means to use no filter. + FilterPolicy FilterPolicy + + // FilterType defines whether an existing filter policy is applied at a + // block-level or table-level. Block-level filters use less memory to create, + // but are slower to access as a check for the key in the index must first be + // performed to locate the filter block. A table-level filter will require + // memory proportional to the number of keys in an sstable to create, but + // avoids the index lookup when determining if a key is present. Table-level + // filters should be preferred except under constrained memory situations. + FilterType FilterType + + // IndexBlockSize is the target uncompressed size in bytes of each index + // block. When the index block size is larger than this target, two-level + // indexes are automatically enabled. Setting this option to a large value + // (such as math.MaxInt32) disables the automatic creation of two-level + // indexes. + // + // The default value is the value of BlockSize. + IndexBlockSize int + + // Merger defines the associative merge operation to use for merging values + // written with {Batch,DB}.Merge. The MergerName is checked for consistency + // with the value stored in the sstable when it was written. + MergerName string + + // TableFormat specifies the format version for writing sstables. The default + // is TableFormatRocksDBv2 which creates RocksDB compatible sstables. Use + // TableFormatLevelDB to create LevelDB compatible sstable which can be used + // 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. + TablePropertyCollectors []func() TablePropertyCollector + + // BlockPropertyCollectors is a list of BlockPropertyCollector creation + // functions. A new BlockPropertyCollector is created for each sstable + // built and lives for the lifetime of writing that table. + BlockPropertyCollectors []func() BlockPropertyCollector + + // Checksum specifies which checksum to use. + Checksum ChecksumType + + // Parallelism is used to indicate that the sstable Writer is allowed to + // compress data blocks and write datablocks to disk in parallel with the + // Writer client goroutine. + Parallelism bool + + // ShortAttributeExtractor mirrors + // Options.Experimental.ShortAttributeExtractor. + ShortAttributeExtractor base.ShortAttributeExtractor + + // RequiredInPlaceValueBound mirrors + // Options.Experimental.RequiredInPlaceValueBound. + RequiredInPlaceValueBound UserKeyPrefixBound +} + +func (o WriterOptions) ensureDefaults() WriterOptions { + if o.BlockRestartInterval <= 0 { + o.BlockRestartInterval = base.DefaultBlockRestartInterval + } + if o.BlockSize <= 0 { + o.BlockSize = base.DefaultBlockSize + } + if o.BlockSizeThreshold <= 0 { + o.BlockSizeThreshold = base.DefaultBlockSizeThreshold + } + if o.Comparer == nil { + o.Comparer = base.DefaultComparer + } + if o.Compression <= DefaultCompression || o.Compression >= NCompression { + o.Compression = SnappyCompression + } + if o.IndexBlockSize <= 0 { + o.IndexBlockSize = o.BlockSize + } + if o.MergerName == "" { + o.MergerName = base.DefaultMerger.Name + } + if o.Checksum == ChecksumTypeNone { + o.Checksum = ChecksumTypeCRC32c + } + // By default, if the table format is not specified, fall back to using the + // most compatible format. + if o.TableFormat == TableFormatUnspecified { + o.TableFormat = TableFormatRocksDBv2 + } + return o +} diff --git a/pebble/sstable/properties.go b/pebble/sstable/properties.go new file mode 100644 index 0000000..3bbf34a --- /dev/null +++ b/pebble/sstable/properties.go @@ -0,0 +1,450 @@ +// Copyright 2018 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" + "encoding/binary" + "fmt" + "math" + "reflect" + "sort" + "unsafe" + + "github.com/cockroachdb/pebble/internal/intern" +) + +const propertiesBlockRestartInterval = math.MaxInt32 +const propGlobalSeqnumName = "rocksdb.external_sst_file.global_seqno" + +var propTagMap = make(map[string]reflect.StructField) +var propBoolTrue = []byte{'1'} +var propBoolFalse = []byte{'0'} + +var propOffsetTagMap = make(map[uintptr]string) + +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: + case reflect.Uint32: + case reflect.Uint64: + case reflect.String: + 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 + } + } +} + +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 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"` + // The name of the prefix extractor used in this table. Empty if no prefix + // extractor is used. + PrefixExtractorName string `prop:"rocksdb.prefix.extractor.name"` + // If filtering is enabled, was the filter created on the key prefix. + PrefixFiltering bool `prop:"rocksdb.block.based.table.prefix.filtering"` + // A comma separated list of names of the property collectors used in this + // table. + PropertyCollectorNames string `prop:"rocksdb.property.collectors"` + // 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"` + // 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 + // open snapshots. This value is comparable to RawKeySize. + SnapshotPinnedKeySize uint64 `prop:"pebble.raw.snapshot-pinned-keys.size"` + // The cumulative bytes of values in this table that were pinned by + // open snapshots. This value is comparable to RawValueSize. + SnapshotPinnedValueSize uint64 `prop:"pebble.raw.snapshot-pinned-values.size"` + // Size of the top-level index if kTwoLevelIndexSearch is used. + TopLevelIndexSize uint64 `prop:"rocksdb.top-level.index.size"` + // User collected properties. + UserProperties map[string]string + // If filtering is enabled, was the filter created on the whole key. + WholeKeyFiltering bool `prop:"rocksdb.block.based.table.whole.key.filtering"` + + // Loaded set indicating which fields have been loaded from disk. Indexed by + // the field's byte offset within the struct + // (reflect.StructField.Offset). Only set if the properties have been loaded + // from a file. Only exported for testing purposes. + Loaded map[uintptr]struct{} +} + +// NumPointDeletions returns the number of point deletions in this table. +func (p *Properties) NumPointDeletions() uint64 { + return p.NumDeletions - p.NumRangeDeletions +} + +// NumRangeKeys returns a count of the number of range keys in this table. +func (p *Properties) NumRangeKeys() uint64 { + return p.NumRangeKeyDels + p.NumRangeKeySets + p.NumRangeKeyUnsets +} + +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 + } + + f := v.Field(i) + // 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 := loaded[ft.Offset]; !ok { + continue + } + } + + fmt.Fprintf(buf, "%s: ", tag) + switch ft.Type.Kind() { + case reflect.Bool: + fmt.Fprintf(buf, "%t\n", f.Bool()) + case reflect.Uint32: + fmt.Fprintf(buf, "%d\n", f.Uint()) + case reflect.Uint64: + fmt.Fprintf(buf, "%d\n", f.Uint()) + case reflect.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) + } + sort.Strings(keys) + for _, key := range keys { + fmt.Fprintf(&buf, "%s: %s\n", key, p.UserProperties[key]) + } + return buf.String() +} + +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() { + if f, ok := propTagMap[string(i.Key().UserKey)]; ok { + p.Loaded[f.Offset] = struct{}{} + field := v.FieldByIndex(f.Index) + switch f.Type.Kind() { + case reflect.Bool: + field.SetBool(bytes.Equal(i.Value(), propBoolTrue)) + case reflect.Uint32: + field.SetUint(uint64(binary.LittleEndian.Uint32(i.Value()))) + case reflect.Uint64: + var n uint64 + if string(i.Key().UserKey) == propGlobalSeqnumName { + n = binary.LittleEndian.Uint64(i.Value()) + } else { + n, _ = binary.Uvarint(i.Value()) + } + field.SetUint(n) + case reflect.String: + field.SetString(intern.Bytes(i.Value())) + default: + panic("not reached") + } + continue + } + if p.UserProperties == nil { + p.UserProperties = make(map[string]string) + } + + if _, denied := deniedUserProperties[string(i.Key().UserKey)]; !denied { + p.UserProperties[intern.Bytes(i.Key().UserKey)] = string(i.Value()) + } + } + return nil +} + +func (p *Properties) saveBool(m map[string][]byte, offset uintptr, value bool) { + tag := propOffsetTagMap[offset] + if value { + m[tag] = propBoolTrue + } else { + m[tag] = propBoolFalse + } +} + +func (p *Properties) saveUint32(m map[string][]byte, offset uintptr, value uint32) { + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], value) + m[propOffsetTagMap[offset]] = buf[:] +} + +func (p *Properties) saveUint64(m map[string][]byte, offset uintptr, value uint64) { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], value) + m[propOffsetTagMap[offset]] = buf[:] +} + +func (p *Properties) saveUvarint(m map[string][]byte, offset uintptr, value uint64) { + var buf [10]byte + n := binary.PutUvarint(buf[:], value) + m[propOffsetTagMap[offset]] = buf[:n] +} + +func (p *Properties) saveString(m map[string][]byte, offset uintptr, value string) { + m[propOffsetTagMap[offset]] = []byte(value) +} + +func (p *Properties) save(tblFormat TableFormat, w *rawBlockWriter) { + m := make(map[string][]byte) + for k, v := range p.UserProperties { + m[k] = []byte(v) + } + + if p.ComparerName != "" { + p.saveString(m, unsafe.Offsetof(p.ComparerName), p.ComparerName) + } + if p.CompressionName != "" { + p.saveString(m, unsafe.Offsetof(p.CompressionName), p.CompressionName) + } + if p.CompressionOptions != "" { + p.saveString(m, unsafe.Offsetof(p.CompressionOptions), p.CompressionOptions) + } + 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.FilterPolicyName != "" { + p.saveString(m, unsafe.Offsetof(p.FilterPolicyName), p.FilterPolicyName) + } + p.saveUvarint(m, unsafe.Offsetof(p.FilterSize), p.FilterSize) + 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) + 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) + p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeyUnsets), p.NumRangeKeyUnsets) + p.saveUvarint(m, unsafe.Offsetof(p.RawRangeKeyKeySize), p.RawRangeKeyKeySize) + p.saveUvarint(m, unsafe.Offsetof(p.RawRangeKeyValueSize), p.RawRangeKeyValueSize) + } + if p.NumValueBlocks > 0 { + p.saveUvarint(m, unsafe.Offsetof(p.NumValueBlocks), p.NumValueBlocks) + } + if p.NumValuesInValueBlocks > 0 { + p.saveUvarint(m, unsafe.Offsetof(p.NumValuesInValueBlocks), p.NumValuesInValueBlocks) + } + if p.PrefixExtractorName != "" { + p.saveString(m, unsafe.Offsetof(p.PrefixExtractorName), p.PrefixExtractorName) + } + p.saveBool(m, unsafe.Offsetof(p.PrefixFiltering), p.PrefixFiltering) + if p.PropertyCollectorNames != "" { + p.saveString(m, unsafe.Offsetof(p.PropertyCollectorNames), p.PropertyCollectorNames) + } + if p.SnapshotPinnedKeys > 0 { + p.saveUvarint(m, unsafe.Offsetof(p.SnapshotPinnedKeys), p.SnapshotPinnedKeys) + p.saveUvarint(m, unsafe.Offsetof(p.SnapshotPinnedKeySize), p.SnapshotPinnedKeySize) + p.saveUvarint(m, unsafe.Offsetof(p.SnapshotPinnedValueSize), p.SnapshotPinnedValueSize) + } + p.saveUvarint(m, unsafe.Offsetof(p.RawKeySize), p.RawKeySize) + p.saveUvarint(m, unsafe.Offsetof(p.RawValueSize), p.RawValueSize) + if p.ValueBlocksSize > 0 { + p.saveUvarint(m, unsafe.Offsetof(p.ValueBlocksSize), p.ValueBlocksSize) + } + 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) + } + sort.Strings(keys) + for _, key := range keys { + w.add(InternalKey{UserKey: []byte(key)}, m[key]) + } +} diff --git a/pebble/sstable/properties_test.go b/pebble/sstable/properties_test.go new file mode 100644 index 0000000..09c719d --- /dev/null +++ b/pebble/sstable/properties_test.go @@ -0,0 +1,146 @@ +// Copyright 2018 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 ( + "math/rand" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "testing/quick" + "time" + + "github.com/kr/pretty" + "github.com/stretchr/testify/require" +) + +func TestPropertiesLoad(t *testing.T) { + expected := Properties{ + CommonProperties: CommonProperties{ + NumEntries: 1727, + NumDeletions: 17, + NumRangeDeletions: 17, + RawKeySize: 23938, + RawValueSize: 1912, + }, + ComparerName: "leveldb.BytewiseComparator", + CompressionName: "Snappy", + CompressionOptions: "window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; ", + DataSize: 13913, + ExternalFormatVersion: 2, + IndexSize: 325, + MergerName: "nullptr", + NumDataBlocks: 14, + PrefixExtractorName: "nullptr", + PropertyCollectorNames: "[KeyCountPropertyCollector]", + UserProperties: map[string]string{ + "test.key-count": "1727", + }, + WholeKeyFiltering: false, + } + + { + // Check that we can read properties from a table. + f, err := os.Open(filepath.FromSlash("testdata/h.sst")) + require.NoError(t, err) + + r, err := newReader(f, ReaderOptions{}) + + require.NoError(t, err) + defer r.Close() + + r.Properties.Loaded = nil + + if diff := pretty.Diff(expected, r.Properties); diff != nil { + t.Fatalf("%s", strings.Join(diff, "\n")) + } + } +} + +var testProps = Properties{ + CommonProperties: CommonProperties{ + NumDeletions: 15, + NumEntries: 16, + NumRangeDeletions: 18, + NumRangeKeyDels: 19, + NumRangeKeySets: 20, + RawKeySize: 25, + RawValueSize: 26, + }, + ComparerName: "comparator name", + CompressionName: "compression name", + CompressionOptions: "compression option", + DataSize: 3, + ExternalFormatVersion: 4, + FilterPolicyName: "filter policy name", + FilterSize: 5, + GlobalSeqNum: 8, + IndexPartitions: 10, + IndexSize: 11, + IndexType: 12, + IsStrictObsolete: true, + MergerName: "merge operator name", + NumDataBlocks: 14, + NumMergeOperands: 17, + NumRangeKeyUnsets: 21, + NumValueBlocks: 22, + NumValuesInValueBlocks: 23, + PrefixExtractorName: "prefix extractor name", + PrefixFiltering: true, + PropertyCollectorNames: "prefix collector names", + TopLevelIndexSize: 27, + WholeKeyFiltering: true, + UserProperties: map[string]string{ + "user-prop-a": "1", + "user-prop-b": "2", + }, +} + +func TestPropertiesSave(t *testing.T) { + expected := &Properties{} + *expected = testProps + + check1 := func(expected *Properties) { + // Check that we can save properties and read them back. + var w rawBlockWriter + w.restartInterval = propertiesBlockRestartInterval + expected.save(TableFormatPebblev2, &w) + var props Properties + + require.NoError(t, props.load(w.finish(), 0, make(map[string]struct{}))) + props.Loaded = nil + if diff := pretty.Diff(*expected, props); diff != nil { + t.Fatalf("%s", strings.Join(diff, "\n")) + } + } + + check1(expected) + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < 1000; i++ { + v, _ := quick.Value(reflect.TypeOf(Properties{}), rng) + props := v.Interface().(Properties) + if props.IndexPartitions == 0 { + props.TopLevelIndexSize = 0 + } + check1(&props) + } +} + +func BenchmarkPropertiesLoad(b *testing.B) { + var w rawBlockWriter + w.restartInterval = propertiesBlockRestartInterval + testProps.save(TableFormatPebblev2, &w) + block := w.finish() + + b.ResetTimer() + p := &Properties{} + for i := 0; i < b.N; i++ { + *p = Properties{} + require.NoError(b, p.load(block, 0, nil)) + } +} diff --git a/pebble/sstable/random_test.go b/pebble/sstable/random_test.go new file mode 100644 index 0000000..83e83ca --- /dev/null +++ b/pebble/sstable/random_test.go @@ -0,0 +1,395 @@ +// 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 ( + "fmt" + "math/rand" + "runtime/debug" + "slices" + "strings" + "testing" + "time" + + "github.com/cockroachdb/metamorphic" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/bytealloc" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/stretchr/testify/require" +) + +// IterIterator_RandomErrors builds random sstables and runs random iterator +// operations against them while randomly injecting errors. It ensures that if +// an error is injected during an operation, operation surfaces the error to the +// caller. +func TestIterator_RandomErrors(t *testing.T) { + root := time.Now().UnixNano() + // Run the test a few times with various seeds for more consistent code + // coverage. + for i := int64(0); i < 50; i++ { + seed := root + i + t.Run(fmt.Sprintf("seed=%d", seed), func(t *testing.T) { + runErrorInjectionTest(t, seed) + }) + } +} + +func runErrorInjectionTest(t *testing.T, seed int64) { + t.Logf("seed %d", seed) + fs := vfs.NewMem() + f, err := fs.Create("random.sst") + require.NoError(t, err) + rng := rand.New(rand.NewSource(seed)) + cfg := randomTableConfig{ + wopts: nil, /* leave to randomize */ + keys: testkeys.Alpha(3 + rng.Intn(2)), + keyCount: 10_000, + maxValLen: rng.Intn(64) + 1, + maxSuffix: rng.Int63n(95) + 5, + maxSeqNum: rng.Int63n(1000) + 10, + rng: rng, + } + cfg.randomize() + _, err = buildRandomSSTable(f, cfg) + require.NoError(t, err) + + f, err = fs.Open("random.sst") + require.NoError(t, err) + // Randomly inject errors into 25% of file operations. We use an + // errorfs.Toggle to avoid injecting errors until the file has been opened. + toggle := &errorfs.Toggle{Injector: errorfs.ErrInjected.If(errorfs.Randomly(0.25, seed))} + counter := &errorfs.Counter{Injector: toggle} + var stack []byte + f = errorfs.WrapFile(f, errorfs.InjectorFunc(func(op errorfs.Op) error { + err := counter.MaybeError(op) + if err != nil { + // Save the stack trace of the most recently injected error. + stack = debug.Stack() + } + return err + })) + readable, err := NewSimpleReadable(f) + require.NoError(t, err) + r, err := NewReader(readable, cfg.readerOpts()) + require.NoError(t, err) + defer r.Close() + + var filterer *BlockPropertiesFilterer + if rng.Float64() < 0.75 { + low, high := uint64(cfg.randSuffix()), uint64(cfg.randSuffix()) + if low > high { + low, high = high, low + } + filterer = newBlockPropertiesFilterer([]BlockPropertyFilter{ + NewTestKeysBlockPropertyFilter(low, high), + }, nil) + } + + // TOOD(jackson): NewIterWithBlockPropertyFilters returns an iterator over + // point keys only. Should we add variants of this test that run random + // operations on the range deletion and range key iterators? + var stats base.InternalIteratorStats + it, err := r.NewIterWithBlockPropertyFilters( + nil /* lower TODO */, nil, /* upper TODO */ + filterer, + rng.Intn(2) == 1, /* use filter block */ + &stats, + CategoryAndQoS{}, + nil, /* CategoryStatsCollector */ + TrivialReaderProvider{r}, + ) + require.NoError(t, err) + defer it.Close() + + // Begin injecting errors. + toggle.On() + + ops := opRunner{randomTableConfig: cfg, it: it} + nextOp := metamorphic.Weighted[func() bool]{ + {Item: ops.runSeekGE, Weight: 2}, + {Item: ops.runSeekPrefixGE, Weight: 2}, + {Item: ops.runSeekLT, Weight: 2}, + {Item: ops.runFirst, Weight: 1}, + {Item: ops.runLast, Weight: 1}, + {Item: ops.runNext, Weight: 5}, + {Item: ops.runNextPrefix, Weight: 5}, + {Item: ops.runPrev, Weight: 5}, + }.RandomDeck(rng) + + for i := 0; i < 1000; i++ { + beforeCount := counter.Load() + + // nextOp returns a function that *may* run the operation. If the + // current test state makes the operation an invalid operation, the the + // function returns `false` indicating it was not run. If the operation + // is a valid operation and was performed, `opFunc` returns true. + // + // This loop will run exactly 1 operation, skipping randomly chosen + // operations that cannot be run on an iterator in its current state. + for opFunc := nextOp(); !opFunc(); { + opFunc = nextOp() + } + + t.Logf("%s = %s [err = %v]", ops.latestOpDesc, ops.k, it.Error()) + afterCount := counter.Load() + // TODO(jackson): Consider running all commands against a parallel + // iterator constructed over a sstable containing the same data in a + // standard construction (eg, typical block sizes) and no error + // injection. Then we can assert the results are identical. + + if afterCount > beforeCount { + if ops.k != nil || it.Error() == nil { + t.Errorf("error swallowed during %s with stack %s", + ops.latestOpDesc, string(stack)) + } + } + } +} + +type opRunner struct { + randomTableConfig + it Iterator + + latestOpDesc string + latestSeekKey []byte + dir int8 + k *base.InternalKey + v base.LazyValue +} + +func (r *opRunner) runSeekGE() bool { + k := r.randKey() + flags := base.SeekGEFlagsNone + if strings.HasPrefix(r.latestOpDesc, "SeekGE") && + r.wopts.Comparer.Compare(k, r.latestSeekKey) > 0 && r.rng.Intn(2) == 1 { + flags = flags.EnableTrySeekUsingNext() + } + r.latestOpDesc = fmt.Sprintf("SeekGE(%q, TrySeekUsingNext()=%t)", + k, flags.TrySeekUsingNext()) + r.latestSeekKey = k + r.k, r.v = r.it.SeekGE(k, base.SeekGEFlagsNone) + r.dir = +1 + return true +} + +func (r *opRunner) runSeekPrefixGE() bool { + k := r.randKey() + i := r.wopts.Comparer.Split(k) + flags := base.SeekGEFlagsNone + if strings.HasPrefix(r.latestOpDesc, "SeekPrefixGE") && + r.wopts.Comparer.Compare(k, r.latestSeekKey) > 0 && r.rng.Intn(2) == 1 { + flags = flags.EnableTrySeekUsingNext() + } + r.latestOpDesc = fmt.Sprintf("SeekPrefixGE(%q, %q, TrySeekUsingNext()=%t)", + k[:i], k, flags.TrySeekUsingNext()) + r.latestSeekKey = k + r.k, r.v = r.it.SeekPrefixGE(k[:i], k, flags) + r.dir = +1 + return true +} + +func (r *opRunner) runSeekLT() bool { + k := r.randKey() + r.latestOpDesc = fmt.Sprintf("SeekLT(%q)", k) + r.k, r.v = r.it.SeekLT(k, base.SeekLTFlagsNone) + r.dir = -1 + return true +} + +func (r *opRunner) runFirst() bool { + r.latestOpDesc = "First()" + r.k, r.v = r.it.First() + r.dir = +1 + return true +} + +func (r *opRunner) runLast() bool { + r.latestOpDesc = "Last()" + r.k, r.v = r.it.Last() + r.dir = -1 + return true +} + +func (r *opRunner) runNext() bool { + if r.dir == +1 && r.k == nil { + return false + } + r.latestOpDesc = "Next()" + r.k, r.v = r.it.Next() + r.dir = +1 + return true +} + +func (r *opRunner) runNextPrefix() bool { + // NextPrefix cannot be called to change directions or when an iterator is + // exhausted. + if r.dir == -1 || r.k == nil { + return false + } + p := r.k.UserKey[:r.wopts.Comparer.Split(r.k.UserKey)] + succKey := r.wopts.Comparer.ImmediateSuccessor(nil, p) + r.latestOpDesc = fmt.Sprintf("NextPrefix(%q)", succKey) + r.k, r.v = r.it.NextPrefix(succKey) + r.dir = +1 + return true +} + +func (r *opRunner) runPrev() bool { + if r.dir == -1 && r.k == nil { + return false + } + r.latestOpDesc = "Prev()" + r.k, r.v = r.it.Prev() + r.dir = -1 + return true +} + +type randomTableConfig struct { + wopts *WriterOptions + keys testkeys.Keyspace + keyCount int + maxValLen int + maxSuffix int64 + maxSeqNum int64 + rng *rand.Rand +} + +func (cfg *randomTableConfig) readerOpts() ReaderOptions { + rOpts := ReaderOptions{ + Comparer: testkeys.Comparer, + Filters: map[string]FilterPolicy{}, + } + if cfg.wopts.FilterPolicy != nil { + rOpts.Filters[cfg.wopts.FilterPolicy.Name()] = cfg.wopts.FilterPolicy + } + return rOpts +} + +func (cfg *randomTableConfig) randomize() { + if cfg.wopts == nil { + cfg.wopts = &WriterOptions{ + // Test all table formats in [TableFormatLevelDB, TableFormatMax]. + TableFormat: TableFormat(cfg.rng.Intn(int(TableFormatMax)) + 1), + BlockRestartInterval: (1 << cfg.rng.Intn(6)), // {1, 2, 4, ..., 32} + BlockSizeThreshold: min(int(100*cfg.rng.Float64()), 1), // 1-100% + BlockSize: (1 << cfg.rng.Intn(18)), // {1, 2, 4, ..., 128 KiB} + IndexBlockSize: (1 << cfg.rng.Intn(20)), // {1, 2, 4, ..., 512 KiB} + BlockPropertyCollectors: nil, + WritingToLowestLevel: cfg.rng.Intn(2) == 1, + Parallelism: cfg.rng.Intn(2) == 1, + } + if v := cfg.rng.Intn(11); v > 0 { + cfg.wopts.FilterPolicy = bloom.FilterPolicy(v) + } + if cfg.wopts.TableFormat >= TableFormatPebblev1 && cfg.rng.Float64() < 0.75 { + cfg.wopts.BlockPropertyCollectors = append(cfg.wopts.BlockPropertyCollectors, NewTestKeysBlockPropertyCollector) + } + } + cfg.wopts.ensureDefaults() + cfg.wopts.Comparer = testkeys.Comparer +} + +func (cfg *randomTableConfig) randKey() []byte { + return testkeys.KeyAt(cfg.keys, cfg.randKeyIdx(), cfg.randSuffix()) +} +func (cfg *randomTableConfig) randSuffix() int64 { return cfg.rng.Int63n(cfg.maxSuffix + 1) } +func (cfg *randomTableConfig) randKeyIdx() int64 { return cfg.rng.Int63n(cfg.keys.Count()) } + +func buildRandomSSTable(f vfs.File, cfg randomTableConfig) (*WriterMetadata, error) { + // Construct a weighted distribution of key kinds. + kinds := metamorphic.Weighted[base.InternalKeyKind]{ + {Item: base.InternalKeyKindSet, Weight: 25}, + {Item: base.InternalKeyKindSetWithDelete, Weight: 25}, + {Item: base.InternalKeyKindDelete, Weight: 5}, + {Item: base.InternalKeyKindSingleDelete, Weight: 2}, + {Item: base.InternalKeyKindMerge, Weight: 1}, + } + // TODO(jackson): Support writing range deletions and range keys. + // TestIterator_RandomErrors only reads through the point iterator, so those + // keys won't be visible regardless, but their existence should be benign. + + // DELSIZED require Pebblev4 or later. + if cfg.wopts.TableFormat >= TableFormatPebblev4 { + kinds = append(kinds, metamorphic.ItemWeight[base.InternalKeyKind]{ + Item: base.InternalKeyKindDeleteSized, Weight: 5, + }) + } + nextRandomKind := kinds.RandomDeck(cfg.rng) + + type keyID struct { + idx int64 + suffix int64 + seqNum int64 + } + keyMap := make(map[keyID]bool) + // Constrain the space we generate keys to the middle 90% of the keyspace. + // This helps exercise code paths that are only run when a seek key is + // beyond or before all index block entries. + sstKeys := cfg.keys.Slice(cfg.keys.Count()/20, cfg.keys.Count()-cfg.keys.Count()/20) + randomKey := func() keyID { + k := keyID{ + idx: cfg.rng.Int63n(sstKeys.Count()), + suffix: cfg.rng.Int63n(cfg.maxSuffix + 1), + seqNum: cfg.rng.Int63n(cfg.maxSeqNum + 1), + } + // If we've already generated this exact key, try again. + for keyMap[k] { + k = keyID{ + idx: cfg.rng.Int63n(sstKeys.Count()), + suffix: cfg.rng.Int63n(cfg.maxSuffix + 1), + seqNum: cfg.rng.Int63n(cfg.maxSeqNum + 1), + } + } + keyMap[k] = true + return k + } + + var alloc bytealloc.A + keys := make([]base.InternalKey, cfg.keyCount) + for i := range keys { + keyID := randomKey() + kind := nextRandomKind() + + var keyBuf []byte + alloc, keyBuf = alloc.Alloc(testkeys.SuffixLen(keyID.suffix) + cfg.keys.MaxLen()) + n := testkeys.WriteKeyAt(keyBuf, sstKeys, keyID.idx, keyID.suffix) + keys[i] = base.MakeInternalKey(keyBuf[:n], uint64(keyID.seqNum), kind) + } + // The Writer requires the keys to be written in sorted order. Sort them. + slices.SortFunc(keys, func(a, b base.InternalKey) int { + return base.InternalCompare(testkeys.Comparer.Compare, a, b) + }) + + // Release keyMap and alloc; we don't need them and this function can be + // memory intensive. + keyMap = nil + alloc = nil + + valueBuf := make([]byte, cfg.maxValLen) + w := NewWriter(objstorageprovider.NewFileWritable(f), *cfg.wopts) + for i := 0; i < len(keys); i++ { + var value []byte + switch keys[i].Kind() { + case base.InternalKeyKindSet, base.InternalKeyKindMerge: + value = valueBuf[:cfg.rng.Intn(cfg.maxValLen+1)] + cfg.rng.Read(value) + } + if err := w.Add(keys[i], value); err != nil { + return nil, err + } + } + if err := w.Close(); err != nil { + return nil, err + } + metadata, err := w.Metadata() + if err != nil { + return nil, err + } + return metadata, nil +} diff --git a/pebble/sstable/raw_block.go b/pebble/sstable/raw_block.go new file mode 100644 index 0000000..d33b51a --- /dev/null +++ b/pebble/sstable/raw_block.go @@ -0,0 +1,262 @@ +// Copyright 2018 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 ( + "encoding/binary" + "sort" + "unsafe" + + "github.com/cockroachdb/pebble/internal/base" +) + +type rawBlockWriter struct { + blockWriter +} + +func (w *rawBlockWriter) add(key InternalKey, value []byte) { + w.curKey, w.prevKey = w.prevKey, w.curKey + + size := len(key.UserKey) + if cap(w.curKey) < size { + w.curKey = make([]byte, 0, size*2) + } + w.curKey = w.curKey[:size] + copy(w.curKey, key.UserKey) + + w.storeWithOptionalValuePrefix( + size, value, len(key.UserKey), false, 0, false) +} + +// rawBlockIter is an iterator over a single block of data. Unlike blockIter, +// keys are stored in "raw" format (i.e. not as internal keys). Note that there +// is significant similarity between this code and the code in blockIter. Yet +// reducing duplication is difficult due to the blockIter being performance +// critical. rawBlockIter must only be used for blocks where the value is +// stored together with the key. +type rawBlockIter struct { + cmp Compare + offset int32 + nextOffset int32 + restarts int32 + numRestarts int32 + ptr unsafe.Pointer + data []byte + key, val []byte + ikey InternalKey + cached []blockEntry + cachedBuf []byte +} + +func newRawBlockIter(cmp Compare, block block) (*rawBlockIter, error) { + i := &rawBlockIter{} + return i, i.init(cmp, block) +} + +func (i *rawBlockIter) init(cmp Compare, block block) 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)") + } + i.cmp = cmp + i.restarts = int32(len(block)) - 4*(1+numRestarts) + i.numRestarts = numRestarts + i.ptr = unsafe.Pointer(&block[0]) + i.data = block + if i.key == nil { + i.key = make([]byte, 0, 256) + } else { + i.key = i.key[:0] + } + i.val = nil + i.clearCache() + return nil +} + +func (i *rawBlockIter) readEntry() { + ptr := unsafe.Pointer(uintptr(i.ptr) + uintptr(i.offset)) + shared, ptr := decodeVarint(ptr) + unshared, ptr := decodeVarint(ptr) + value, ptr := decodeVarint(ptr) + i.key = append(i.key[:shared], getBytes(ptr, int(unshared))...) + i.key = i.key[:len(i.key):len(i.key)] + ptr = unsafe.Pointer(uintptr(ptr) + uintptr(unshared)) + i.val = getBytes(ptr, int(value)) + i.nextOffset = int32(uintptr(ptr)-uintptr(i.ptr)) + int32(value) +} + +func (i *rawBlockIter) loadEntry() { + i.readEntry() + i.ikey.UserKey = i.key +} + +func (i *rawBlockIter) clearCache() { + i.cached = i.cached[:0] + i.cachedBuf = i.cachedBuf[:0] +} + +func (i *rawBlockIter) cacheEntry() { + var valStart int32 + valSize := int32(len(i.val)) + if valSize > 0 { + valStart = int32(uintptr(unsafe.Pointer(&i.val[0])) - uintptr(i.ptr)) + } + + i.cached = append(i.cached, blockEntry{ + offset: i.offset, + keyStart: int32(len(i.cachedBuf)), + keyEnd: int32(len(i.cachedBuf) + len(i.key)), + valStart: valStart, + valSize: valSize, + }) + i.cachedBuf = append(i.cachedBuf, i.key...) +} + +// SeekGE implements internalIterator.SeekGE, as documented in the pebble +// package. +func (i *rawBlockIter) SeekGE(key []byte) bool { + // 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. + i.offset = 0 + index := sort.Search(int(i.numRestarts), func(j int) bool { + offset := int32(binary.LittleEndian.Uint32(i.data[int(i.restarts)+4*j:])) + // For a restart point, there are 0 bytes shared with the previous key. + // The varint encoding of 0 occupies 1 byte. + ptr := unsafe.Pointer(uintptr(i.ptr) + uintptr(offset+1)) + // Decode the key at that restart point, and compare it to the key sought. + v1, ptr := decodeVarint(ptr) + _, ptr = decodeVarint(ptr) + s := getBytes(ptr, int(v1)) + return i.cmp(key, s) < 0 + }) + + // 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. + if index > 0 { + i.offset = int32(binary.LittleEndian.Uint32(i.data[int(i.restarts)+4*(index-1):])) + } + i.loadEntry() + + // Iterate from that restart point to somewhere >= the key sought. + for valid := i.Valid(); valid; valid = i.Next() { + if i.cmp(key, i.key) <= 0 { + break + } + } + return i.Valid() +} + +// First implements internalIterator.First, as documented in the pebble +// package. +func (i *rawBlockIter) First() bool { + i.offset = 0 + i.loadEntry() + return i.Valid() +} + +// Last implements internalIterator.Last, as documented in the pebble package. +func (i *rawBlockIter) Last() bool { + // Seek forward from the last restart point. + i.offset = int32(binary.LittleEndian.Uint32(i.data[i.restarts+4*(i.numRestarts-1):])) + + i.readEntry() + i.clearCache() + i.cacheEntry() + + for i.nextOffset < i.restarts { + i.offset = i.nextOffset + i.readEntry() + i.cacheEntry() + } + + i.ikey.UserKey = i.key + return i.Valid() +} + +// Next implements internalIterator.Next, as documented in the pebble +// package. +func (i *rawBlockIter) Next() bool { + i.offset = i.nextOffset + if !i.Valid() { + return false + } + i.loadEntry() + return true +} + +// Prev implements internalIterator.Prev, as documented in the pebble +// package. +func (i *rawBlockIter) Prev() bool { + if n := len(i.cached) - 1; n > 0 && i.cached[n].offset == i.offset { + i.nextOffset = i.offset + e := &i.cached[n-1] + i.offset = e.offset + i.val = getBytes(unsafe.Pointer(uintptr(i.ptr)+uintptr(e.valStart)), int(e.valSize)) + i.ikey.UserKey = i.cachedBuf[e.keyStart:e.keyEnd] + i.cached = i.cached[:n] + return true + } + + if i.offset == 0 { + i.offset = -1 + i.nextOffset = 0 + return false + } + + targetOffset := i.offset + index := sort.Search(int(i.numRestarts), func(j int) bool { + offset := int32(binary.LittleEndian.Uint32(i.data[int(i.restarts)+4*j:])) + return offset >= targetOffset + }) + i.offset = 0 + if index > 0 { + i.offset = int32(binary.LittleEndian.Uint32(i.data[int(i.restarts)+4*(index-1):])) + } + + i.readEntry() + i.clearCache() + i.cacheEntry() + + for i.nextOffset < targetOffset { + i.offset = i.nextOffset + i.readEntry() + i.cacheEntry() + } + + i.ikey.UserKey = i.key + return true +} + +// Key implements internalIterator.Key, as documented in the pebble package. +func (i *rawBlockIter) Key() InternalKey { + return i.ikey +} + +// Value implements internalIterator.Value, as documented in the pebble +// package. +func (i *rawBlockIter) Value() []byte { + return i.val +} + +// Valid implements internalIterator.Valid, as documented in the pebble +// package. +func (i *rawBlockIter) Valid() bool { + return i.offset >= 0 && i.offset < i.restarts +} + +// Error implements internalIterator.Error, as documented in the pebble +// package. +func (i *rawBlockIter) Error() error { + return nil +} + +// Close implements internalIterator.Close, as documented in the pebble +// package. +func (i *rawBlockIter) Close() error { + i.val = nil + return nil +} diff --git a/pebble/sstable/reader.go b/pebble/sstable/reader.go new file mode 100644 index 0000000..c6884cc --- /dev/null +++ b/pebble/sstable/reader.go @@ -0,0 +1,1268 @@ +// 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" + "io" + "os" + "time" + + "github.com/cespare/xxhash/v2" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/bytealloc" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/crc" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/private" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" + "github.com/cockroachdb/pebble/shims/cmp" + "github.com/cockroachdb/pebble/shims/slices" +) + +var errCorruptIndexEntry = base.CorruptionErrorf("pebble/table: corrupt index entry") +var errReaderClosed = errors.New("pebble/table: reader is closed") + +// decodeBlockHandle returns the block handle encoded at the start of src, as +// well as the number of bytes it occupies. It returns zero if given invalid +// input. A block handle for a data block or a first/lower level index block +// should not be decoded using decodeBlockHandle since the caller may validate +// that the number of bytes decoded is equal to the length of src, which will +// be false if the properties are not decoded. In those cases the caller +// should use decodeBlockHandleWithProperties. +func decodeBlockHandle(src []byte) (BlockHandle, int) { + offset, n := binary.Uvarint(src) + length, m := binary.Uvarint(src[n:]) + if n == 0 || m == 0 { + return BlockHandle{}, 0 + } + return BlockHandle{offset, length}, n + m +} + +// decodeBlockHandleWithProperties returns the block handle and properties +// encoded in src. src needs to be exactly the length that was encoded. This +// method must be used for data block and first/lower level index blocks. The +// properties in the block handle point to the bytes in src. +func decodeBlockHandleWithProperties(src []byte) (BlockHandleWithProperties, error) { + bh, n := decodeBlockHandle(src) + if n == 0 { + return BlockHandleWithProperties{}, errors.Errorf("invalid BlockHandle") + } + return BlockHandleWithProperties{ + BlockHandle: bh, + Props: src[n:], + }, nil +} + +func encodeBlockHandle(dst []byte, b BlockHandle) int { + n := binary.PutUvarint(dst, b.Offset) + m := binary.PutUvarint(dst[n:], b.Length) + return n + m +} + +func encodeBlockHandleWithProperties(dst []byte, b BlockHandleWithProperties) []byte { + n := encodeBlockHandle(dst, b.BlockHandle) + dst = append(dst[:n], b.Props...) + return dst +} + +// block is a []byte that holds a sequence of key/value pairs plus an index +// over those pairs. +type block []byte + +type loadBlockResult int8 + +const ( + loadBlockOK loadBlockResult = iota + // Could be due to error or because no block left to load. + loadBlockFailed + loadBlockIrrelevant +) + +type blockTransform func([]byte) ([]byte, error) + +// ReaderOption provide an interface to do work on Reader while it is being +// opened. +type ReaderOption interface { + // readerApply is called on the reader during opening in order to set internal + // parameters. + readerApply(*Reader) +} + +// Comparers is a map from comparer name to comparer. It is used for debugging +// tools which may be used on multiple databases configured with different +// comparers. Comparers implements the OpenOption interface and can be passed +// as a parameter to NewReader. +type Comparers map[string]*Comparer + +func (c Comparers) readerApply(r *Reader) { + if r.Compare != nil || r.Properties.ComparerName == "" { + return + } + if comparer, ok := c[r.Properties.ComparerName]; ok { + r.Compare = comparer.Compare + r.FormatKey = comparer.FormatKey + r.Split = comparer.Split + } +} + +// Mergers is a map from merger name to merger. It is used for debugging tools +// which may be used on multiple databases configured with different +// mergers. Mergers implements the OpenOption interface and can be passed as +// a parameter to NewReader. +type Mergers map[string]*Merger + +func (m Mergers) readerApply(r *Reader) { + if r.mergerOK || r.Properties.MergerName == "" { + return + } + _, r.mergerOK = m[r.Properties.MergerName] +} + +// cacheOpts is a Reader open option for specifying the cache ID and sstable file +// number. If not specified, a unique cache ID will be used. +type cacheOpts struct { + cacheID uint64 + fileNum base.DiskFileNum +} + +// Marker function to indicate the option should be applied before reading the +// sstable properties and, in the write path, before writing the default +// sstable properties. +func (c *cacheOpts) preApply() {} + +func (c *cacheOpts) readerApply(r *Reader) { + if r.cacheID == 0 { + r.cacheID = c.cacheID + } + if r.fileNum.FileNum() == 0 { + r.fileNum = c.fileNum + } +} + +func (c *cacheOpts) writerApply(w *Writer) { + if w.cacheID == 0 { + w.cacheID = c.cacheID + } + if w.fileNum.FileNum() == 0 { + w.fileNum = c.fileNum + } +} + +// rawTombstonesOpt is a Reader open option for specifying that range +// tombstones returned by Reader.NewRangeDelIter() should not be +// fragmented. Used by debug tools to get a raw view of the tombstones +// contained in an sstable. +type rawTombstonesOpt struct{} + +func (rawTombstonesOpt) preApply() {} + +func (rawTombstonesOpt) readerApply(r *Reader) { + r.rawTombstones = true +} + +func init() { + 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, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, + ) (Iterator, error) + NewCompactionIter( + bytesIterated *uint64, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + 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.DiskFileNum + err error + indexBH BlockHandle + filterBH BlockHandle + rangeDelBH BlockHandle + rangeKeyBH BlockHandle + rangeDelTransform blockTransform + valueBIH valueBlocksIndexHandle + propertiesBH BlockHandle + metaIndexBH BlockHandle + footerBH BlockHandle + opts ReaderOptions + Compare Compare + FormatKey base.FormatKey + Split Split + tableFilter *tableFilterReader + // Keep types that are not multiples of 8 bytes at the end and with + // decreasing size. + Properties Properties + tableFormat TableFormat + 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. +func (r *Reader) Close() error { + r.opts.Cache.Unref() + + if r.readable != nil { + r.err = firstError(r.err, r.readable.Close()) + r.readable = nil + } + + if r.err != nil { + return r.err + } + // Make any future calls to Get, NewIter or Close return an error. + r.err = errReaderClosed + return nil +} + +// NewIterWithBlockPropertyFilters returns an iterator for the contents of the +// table. If an error occurs, NewIterWithBlockPropertyFilters cleans up after +// itself and returns a nil iterator. +func (r *Reader) NewIterWithBlockPropertyFilters( + lower, upper []byte, + filterer *BlockPropertiesFilterer, + useFilterBlock bool, + stats *base.InternalIteratorStats, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, +) (Iterator, error) { + return r.newIterWithBlockPropertyFiltersAndContext( + context.Background(), lower, upper, filterer, false, useFilterBlock, stats, + categoryAndQoS, statsCollector, rp, nil) +} + +// NewIterWithBlockPropertyFiltersAndContextEtc is similar to +// NewIterWithBlockPropertyFilters and additionally accepts a context for +// tracing. +// +// 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, + hideObsoletePoints, useFilterBlock bool, + stats *base.InternalIteratorStats, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, +) (Iterator, error) { + return r.newIterWithBlockPropertyFiltersAndContext( + ctx, lower, upper, filterer, hideObsoletePoints, useFilterBlock, stats, categoryAndQoS, + statsCollector, 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, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + 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, v, lower, upper, filterer, useFilterBlock, hideObsoletePoints, stats, + categoryAndQoS, statsCollector, rp, nil /* bufferPool */) + if err != nil { + return nil, err + } + return i, nil + } + + i := singleLevelIterPool.Get().(*singleLevelIterator) + err := i.init(ctx, r, v, lower, upper, filterer, useFilterBlock, hideObsoletePoints, stats, + categoryAndQoS, statsCollector, rp, nil /* bufferPool */) + if err != nil { + return nil, err + } + return i, nil +} + +// NewIter returns an iterator for the contents of the table. If an error +// occurs, NewIter cleans up after itself and returns a nil iterator. NewIter +// must only be used when the Reader is guaranteed to outlive any LazyValues +// returned from the iter. +func (r *Reader) NewIter(lower, upper []byte) (Iterator, error) { + return r.NewIterWithBlockPropertyFilters( + lower, upper, nil, true /* useFilterBlock */, nil, /* stats */ + CategoryAndQoS{}, nil /*statsCollector */, TrivialReaderProvider{Reader: r}) +} + +// 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, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, + bufferPool *BufferPool, +) (Iterator, error) { + return r.newCompactionIter(bytesIterated, categoryAndQoS, statsCollector, rp, nil, bufferPool) +} + +func (r *Reader) newCompactionIter( + bytesIterated *uint64, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, + v *virtualState, + bufferPool *BufferPool, +) (Iterator, error) { + if r.Properties.IndexType == twoLevelIndex { + i := twoLevelIterPool.Get().(*twoLevelIterator) + err := i.init( + context.Background(), + r, v, nil /* lower */, nil /* upper */, nil, + false /* useFilter */, v != nil && v.isForeign, /* hideObsoletePoints */ + nil /* stats */, categoryAndQoS, statsCollector, rp, bufferPool, + ) + if err != nil { + return nil, err + } + i.setupForCompaction() + return &twoLevelCompactionIterator{ + twoLevelIterator: i, + bytesIterated: bytesIterated, + }, nil + } + i := singleLevelIterPool.Get().(*singleLevelIterator) + err := i.init( + context.Background(), r, v, nil /* lower */, nil, /* upper */ + nil, false /* useFilter */, v != nil && v.isForeign, /* hideObsoletePoints */ + nil /* stats */, categoryAndQoS, statsCollector, rp, bufferPool, + ) + if err != nil { + return nil, err + } + i.setupForCompaction() + return &compactionIterator{ + singleLevelIterator: i, + bytesIterated: bytesIterated, + }, nil +} + +// NewRawRangeDelIter returns an internal iterator for the contents of the +// range-del block for the table. Returns nil if the table does not contain +// any range deletions. +// +// TODO(sumeer): plumb context.Context since this path is relevant in the user-facing +// iterator. Add WithContext methods since the existing ones are public. +func (r *Reader) NewRawRangeDelIter() (keyspan.FragmentIterator, error) { + if r.rangeDelBH.Length == 0 { + return nil, nil + } + h, err := r.readRangeDel(nil /* stats */, nil /* iterStats */) + if err != nil { + return nil, err + } + i := &fragmentBlockIter{elideSameSeqnum: true} + if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum, false); err != nil { + return nil, err + } + return i, nil +} + +// NewRawRangeKeyIter returns an internal iterator for the contents of the +// range-key block for the table. Returns nil if the table does not contain any +// range keys. +// +// TODO(sumeer): plumb context.Context since this path is relevant in the user-facing +// iterator. Add WithContext methods since the existing ones are public. +func (r *Reader) NewRawRangeKeyIter() (keyspan.FragmentIterator, error) { + if r.rangeKeyBH.Length == 0 { + return nil, nil + } + h, err := r.readRangeKey(nil /* stats */, nil /* iterStats */) + if err != nil { + return nil, err + } + i := rangeKeyFragmentBlockIterPool.Get().(*rangeKeyFragmentBlockIter) + if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum, false); err != nil { + return nil, err + } + return i, nil +} + +type rangeKeyFragmentBlockIter struct { + fragmentBlockIter +} + +func (i *rangeKeyFragmentBlockIter) Close() error { + err := i.fragmentBlockIter.Close() + i.fragmentBlockIter = i.fragmentBlockIter.resetForReuse() + rangeKeyFragmentBlockIterPool.Put(i) + return err +} + +func (r *Reader) readIndex( + ctx context.Context, stats *base.InternalIteratorStats, iterStats *iterStatsAccumulator, +) (bufferHandle, error) { + ctx = objiotracing.WithBlockType(ctx, objiotracing.MetadataBlock) + return r.readBlock(ctx, r.indexBH, nil, nil, stats, iterStats, nil /* buffer pool */) +} + +func (r *Reader) readFilter( + ctx context.Context, stats *base.InternalIteratorStats, iterStats *iterStatsAccumulator, +) (bufferHandle, error) { + ctx = objiotracing.WithBlockType(ctx, objiotracing.FilterBlock) + return r.readBlock(ctx, r.filterBH, nil /* transform */, nil /* readHandle */, stats, iterStats, nil /* buffer pool */) +} + +func (r *Reader) readRangeDel( + stats *base.InternalIteratorStats, iterStats *iterStatsAccumulator, +) (bufferHandle, error) { + ctx := objiotracing.WithBlockType(context.Background(), objiotracing.MetadataBlock) + return r.readBlock(ctx, r.rangeDelBH, r.rangeDelTransform, nil /* readHandle */, stats, iterStats, nil /* buffer pool */) +} + +func (r *Reader) readRangeKey( + stats *base.InternalIteratorStats, iterStats *iterStatsAccumulator, +) (bufferHandle, error) { + ctx := objiotracing.WithBlockType(context.Background(), objiotracing.MetadataBlock) + return r.readBlock(ctx, r.rangeKeyBH, nil /* transform */, nil /* readHandle */, stats, iterStats, nil /* buffer pool */) +} + +func checkChecksum( + checksumType ChecksumType, b []byte, bh BlockHandle, fileNum base.FileNum, +) error { + expectedChecksum := binary.LittleEndian.Uint32(b[bh.Length+1:]) + var computedChecksum uint32 + switch checksumType { + case ChecksumTypeCRC32c: + computedChecksum = crc.New(b[:bh.Length+1]).Value() + case ChecksumTypeXXHash64: + computedChecksum = uint32(xxhash.Sum64(b[:bh.Length+1])) + default: + return errors.Errorf("unsupported checksum type: %d", checksumType) + } + + if expectedChecksum != computedChecksum { + return base.CorruptionErrorf( + "pebble/table: invalid table %s (checksum mismatch at %d/%d)", + errors.Safe(fileNum), errors.Safe(bh.Offset), errors.Safe(bh.Length)) + } + return nil +} + +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, + iterStats *iterStatsAccumulator, + 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(ctx, int64(bh.Offset), int64(bh.Length+blockTrailerLen)) + } + if stats != nil { + stats.BlockBytes += bh.Length + stats.BlockBytesInCache += bh.Length + } + if iterStats != nil { + iterStats.reportStats(bh.Length, bh.Length) + } + // 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)), + } + } + + readStartTime := time.Now() + var err error + if readHandle != nil { + err = readHandle.ReadAt(ctx, compressed.get(), int64(bh.Offset)) + } else { + err = r.readable.ReadAt(ctx, compressed.get(), int64(bh.Offset)) + } + readDuration := time.Since(readStartTime) + // TODO(sumeer): should the threshold be configurable. + const slowReadTracingThreshold = 5 * time.Millisecond + // The invariants.Enabled path is for deterministic testing. + if invariants.Enabled { + readDuration = slowReadTracingThreshold + } + // Call IsTracingEnabled to avoid the allocations of boxing integers into an + // interface{}, unless necessary. + if readDuration >= slowReadTracingThreshold && r.opts.LoggerAndTracer.IsTracingEnabled(ctx) { + r.opts.LoggerAndTracer.Eventf(ctx, "reading %d bytes took %s", + int(bh.Length+blockTrailerLen), readDuration.String()) + } + if stats != nil { + stats.BlockReadDuration += readDuration + } + if err != nil { + compressed.release() + return bufferHandle{}, err + } + if err := checkChecksum(r.checksumType, compressed.get(), bh, r.fileNum.FileNum()); err != nil { + compressed.release() + return bufferHandle{}, err + } + + typ := blockType(compressed.get()[bh.Length]) + compressed.truncate(int(bh.Length)) + + 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 + } + + 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 very rare, so the extra copy of the + // transformed data is not problematic. + tmpTransformed, err := transform(decompressed.get()) + if err != nil { + decompressed.release() + return bufferHandle{}, err + } + + 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 + } + if iterStats != nil { + iterStats.reportStats(bh.Length, 0) + } + 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) { + // Convert v1 (RocksDB format) range-del blocks to v2 blocks on the fly. The + // v1 format range-del blocks have unfragmented and unsorted range + // 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, false); err != nil { + return nil, err + } + var tombstones []keyspan.Span + for key, value := iter.First(); key != nil; key, value = iter.Next() { + t := keyspan.Span{ + Start: key.UserKey, + End: value.InPlaceValue(), + Keys: []keyspan.Key{{Trailer: key.Trailer}}, + } + tombstones = append(tombstones, t) + } + keyspan.Sort(r.Compare, tombstones) + + // Fragment the tombstones, outputting them directly to a block writer. + rangeDelBlock := blockWriter{ + restartInterval: 1, + } + frag := keyspan.Fragmenter{ + Cmp: r.Compare, + Format: r.FormatKey, + Emit: func(s keyspan.Span) { + for _, k := range s.Keys { + startIK := InternalKey{UserKey: s.Start, Trailer: k.Trailer} + rangeDelBlock.add(startIK, s.End) + } + }, + } + for i := range tombstones { + frag.Add(tombstones[i]) + } + frag.Finish() + + // Return the contents of the constructed v2 format range-del block. + return rangeDelBlock.finish(), nil +} + +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 */ + nil /* iterStats */, &r.metaBufferPool) + if err != nil { + return err + } + data := b.Get() + defer b.Release() + + if uint64(len(data)) != metaindexBH.Length { + return base.CorruptionErrorf("pebble/table: unexpected metaindex block size: %d vs %d", + errors.Safe(len(data)), errors.Safe(metaindexBH.Length)) + } + + i, err := newRawBlockIter(bytes.Compare, data) + if err != nil { + return err + } + + meta := map[string]BlockHandle{} + for valid := i.First(); valid; valid = i.Next() { + value := i.Value() + if bytes.Equal(i.Key().UserKey, []byte(metaValueIndexName)) { + vbih, n, err := decodeValueBlocksIndexHandle(i.Value()) + if err != nil { + return err + } + if n == 0 || n != len(value) { + return base.CorruptionErrorf("pebble/table: invalid table (bad value blocks index handle)") + } + r.valueBIH = vbih + } else { + bh, n := decodeBlockHandle(value) + if n == 0 || n != len(value) { + return base.CorruptionErrorf("pebble/table: invalid table (bad block handle)") + } + meta[string(i.Key().UserKey)] = bh + } + } + if err := i.Close(); err != nil { + return err + } + + if bh, ok := meta[metaPropertiesName]; ok { + b, err = r.readBlock( + context.Background(), bh, nil /* transform */, nil /* readHandle */, nil, /* stats */ + nil /* iterStats */, nil /* buffer pool */) + if err != nil { + return err + } + r.propertiesBH = bh + err := r.Properties.load(b.Get(), bh.Offset, r.opts.DeniedUserProperties) + b.Release() + if err != nil { + return err + } + } + + if bh, ok := meta[metaRangeDelV2Name]; ok { + r.rangeDelBH = bh + } else if bh, ok := meta[metaRangeDelName]; ok { + r.rangeDelBH = bh + if !r.rawTombstones { + r.rangeDelTransform = r.transformRangeDelV1 + } + } + + if bh, ok := meta[metaRangeKeyName]; ok { + r.rangeKeyBH = bh + } + + for name, fp := range r.opts.Filters { + types := []struct { + ftype FilterType + prefix string + }{ + {TableFilter, "fullfilter."}, + } + var done bool + for _, t := range types { + if bh, ok := meta[t.prefix+name]; ok { + r.filterBH = bh + + switch t.ftype { + case TableFilter: + r.tableFilter = newTableFilterReader(fp) + default: + return base.CorruptionErrorf("unknown filter type: %v", errors.Safe(t.ftype)) + } + + done = true + break + } + } + if done { + break + } + } + return nil +} + +// Layout returns the layout (block organization) for an sstable. +func (r *Reader) Layout() (*Layout, error) { + if r.err != nil { + return nil, r.err + } + + l := &Layout{ + Data: make([]BlockHandleWithProperties, 0, r.Properties.NumDataBlocks), + Filter: r.filterBH, + RangeDel: r.rangeDelBH, + RangeKey: r.rangeKeyBH, + ValueIndex: r.valueBIH.h, + Properties: r.propertiesBH, + MetaIndex: r.metaIndexBH, + Footer: r.footerBH, + Format: r.tableFormat, + } + + indexH, err := r.readIndex(context.Background(), nil, nil) + if err != nil { + return nil, err + } + defer indexH.Release() + + var alloc bytealloc.A + + if r.Properties.IndexPartitions == 0 { + l.Index = append(l.Index, r.indexBH) + iter, _ := newBlockIter(r.Compare, indexH.Get()) + for key, value := iter.First(); key != nil; key, value = iter.Next() { + dataBH, err := decodeBlockHandleWithProperties(value.InPlaceValue()) + if err != nil { + return nil, errCorruptIndexEntry + } + if len(dataBH.Props) > 0 { + alloc, dataBH.Props = alloc.Copy(dataBH.Props) + } + l.Data = append(l.Data, dataBH) + } + } else { + l.TopIndex = r.indexBH + topIter, _ := newBlockIter(r.Compare, indexH.Get()) + iter := &blockIter{} + for key, value := topIter.First(); key != nil; key, value = topIter.Next() { + indexBH, err := decodeBlockHandleWithProperties(value.InPlaceValue()) + if err != nil { + return nil, errCorruptIndexEntry + } + l.Index = append(l.Index, indexBH.BlockHandle) + + subIndex, err := r.readBlock(context.Background(), indexBH.BlockHandle, + nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* iterStats */, nil /* buffer pool */) + if err != nil { + return nil, err + } + 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() { + dataBH, err := decodeBlockHandleWithProperties(value.InPlaceValue()) + if len(dataBH.Props) > 0 { + alloc, dataBH.Props = alloc.Copy(dataBH.Props) + } + if err != nil { + return nil, errCorruptIndexEntry + } + l.Data = append(l.Data, dataBH) + } + subIndex.Release() + *iter = iter.resetForReuse() + } + } + if r.valueBIH.h.Length != 0 { + vbiH, err := r.readBlock(context.Background(), r.valueBIH.h, nil, nil, nil, nil, nil /* buffer pool */) + if err != nil { + return nil, err + } + defer vbiH.Release() + vbiBlock := vbiH.Get() + indexEntryLen := int(r.valueBIH.blockNumByteLength + r.valueBIH.blockOffsetByteLength + + r.valueBIH.blockLengthByteLength) + i := 0 + for len(vbiBlock) != 0 { + if len(vbiBlock) < indexEntryLen { + return nil, errors.Errorf( + "remaining value index block %d does not contain a full entry of length %d", + len(vbiBlock), indexEntryLen) + } + n := int(r.valueBIH.blockNumByteLength) + bn := int(littleEndianGet(vbiBlock, n)) + if bn != i { + return nil, errors.Errorf("unexpected block num %d, expected %d", + bn, i) + } + i++ + vbiBlock = vbiBlock[n:] + n = int(r.valueBIH.blockOffsetByteLength) + blockOffset := littleEndianGet(vbiBlock, n) + vbiBlock = vbiBlock[n:] + n = int(r.valueBIH.blockLengthByteLength) + blockLen := littleEndianGet(vbiBlock, n) + vbiBlock = vbiBlock[n:] + l.ValueBlock = append(l.ValueBlock, BlockHandle{Offset: blockOffset, Length: blockLen}) + } + } + + return l, nil +} + +// ValidateBlockChecksums validates the checksums for each block in the SSTable. +func (r *Reader) ValidateBlockChecksums() error { + // Pre-compute the BlockHandles for the underlying file. + l, err := r.Layout() + if err != nil { + return err + } + + // Construct the set of blocks to check. Note that the footer is not checked + // as it is not a block with a checksum. + blocks := make([]BlockHandle, len(l.Data)) + for i := range l.Data { + blocks[i] = l.Data[i].BlockHandle + } + blocks = append(blocks, l.Index...) + blocks = append(blocks, l.TopIndex, l.Filter, l.RangeDel, l.RangeKey, l.Properties, l.MetaIndex) + + // Sorting by offset ensures we are performing a sequential scan of the + // file. + slices.SortFunc(blocks, func(a, b BlockHandle) int { + return cmp.Compare(a.Offset, b.Offset) + }) + + // Check all blocks sequentially. Make use of read-ahead, given we are + // scanning the entire file from start to end. + rh := r.readable.NewReadHandle(context.TODO()) + defer rh.Close() + + for _, bh := range blocks { + // Certain blocks may not be present, in which case we skip them. + if bh.Length == 0 { + continue + } + + // Read the block, which validates the checksum. + h, err := r.readBlock(context.Background(), bh, nil, rh, nil, nil /* iterStats */, nil /* buffer pool */) + if err != nil { + return err + } + h.Release() + } + + 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 +// included in the estimation. +// +// This function does not account for any metablock space usage. Assumes there +// is at least partial overlap, i.e., `[start, end]` falls neither completely +// before nor completely after the file's range. +// +// Only blocks containing point keys are considered. Range deletion and range +// key blocks are not considered. +// +// TODO(ajkr): account for metablock space usage. Perhaps look at the fraction of +// data blocks overlapped and add that same fraction of the metadata blocks to the +// estimate. +func (r *Reader) EstimateDiskUsage(start, end []byte) (uint64, error) { + if r.err != nil { + return 0, r.err + } + + indexH, err := r.readIndex(context.Background(), nil, nil) + if err != nil { + return 0, err + } + defer indexH.Release() + + // Iterators over the bottom-level index blocks containing start and end. + // These may be different in case of partitioned index but will both point + // to the same blockIter over the single index in the unpartitioned case. + var startIdxIter, endIdxIter *blockIter + if r.Properties.IndexPartitions == 0 { + iter, err := newBlockIter(r.Compare, indexH.Get()) + if err != nil { + return 0, err + } + startIdxIter = iter + endIdxIter = iter + } else { + topIter, err := newBlockIter(r.Compare, indexH.Get()) + if err != nil { + return 0, err + } + + key, val := topIter.SeekGE(start, base.SeekGEFlagsNone) + if key == nil { + // The range falls completely after this file, or an error occurred. + return 0, topIter.Error() + } + startIdxBH, err := decodeBlockHandleWithProperties(val.InPlaceValue()) + if err != nil { + return 0, errCorruptIndexEntry + } + startIdxBlock, err := r.readBlock(context.Background(), startIdxBH.BlockHandle, + nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* iterStats */, nil /* buffer pool */) + if err != nil { + return 0, err + } + defer startIdxBlock.Release() + startIdxIter, err = newBlockIter(r.Compare, startIdxBlock.Get()) + if err != nil { + return 0, err + } + + key, val = topIter.SeekGE(end, base.SeekGEFlagsNone) + if key == nil { + if err := topIter.Error(); err != nil { + return 0, err + } + } else { + endIdxBH, err := decodeBlockHandleWithProperties(val.InPlaceValue()) + if err != nil { + return 0, errCorruptIndexEntry + } + endIdxBlock, err := r.readBlock(context.Background(), + endIdxBH.BlockHandle, nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* iterStats */, nil /* buffer pool */) + if err != nil { + return 0, err + } + defer endIdxBlock.Release() + endIdxIter, err = newBlockIter(r.Compare, endIdxBlock.Get()) + if err != nil { + return 0, err + } + } + } + // startIdxIter should not be nil at this point, while endIdxIter can be if the + // range spans past the end of the file. + + key, val := startIdxIter.SeekGE(start, base.SeekGEFlagsNone) + if key == nil { + // The range falls completely after this file, or an error occurred. + return 0, startIdxIter.Error() + } + startBH, err := decodeBlockHandleWithProperties(val.InPlaceValue()) + if err != nil { + return 0, errCorruptIndexEntry + } + + includeInterpolatedValueBlocksSize := func(dataBlockSize uint64) uint64 { + // INVARIANT: r.Properties.DataSize > 0 since startIdxIter is not nil. + // Linearly interpolate what is stored in value blocks. + // + // TODO(sumeer): if we need more accuracy, without loading any data blocks + // (which contain the value handles, and which may also be insufficient if + // the values are in separate files), we will need to accumulate the + // logical size of the key-value pairs and store the cumulative value for + // each data block in the index block entry. This increases the size of + // the BlockHandle, so wait until this becomes necessary. + return dataBlockSize + + uint64((float64(dataBlockSize)/float64(r.Properties.DataSize))* + float64(r.Properties.ValueBlocksSize)) + } + if endIdxIter == nil { + // The range spans beyond this file. Include data blocks through the last. + return includeInterpolatedValueBlocksSize(r.Properties.DataSize - startBH.Offset), nil + } + key, val = endIdxIter.SeekGE(end, base.SeekGEFlagsNone) + if key == nil { + if err := endIdxIter.Error(); err != nil { + return 0, err + } + // The range spans beyond this file. Include data blocks through the last. + return includeInterpolatedValueBlocksSize(r.Properties.DataSize - startBH.Offset), nil + } + endBH, err := decodeBlockHandleWithProperties(val.InPlaceValue()) + if err != nil { + return 0, errCorruptIndexEntry + } + return includeInterpolatedValueBlocksSize( + endBH.Offset + endBH.Length + blockTrailerLen - startBH.Offset), nil +} + +// TableFormat returns the format version for the table. +func (r *Reader) TableFormat() (TableFormat, error) { + if r.err != nil { + return TableFormatUnspecified, r.err + } + return r.tableFormat, nil +} + +// NewReader returns a new table reader for the file. Closing the reader will +// close the file. +func NewReader(f objstorage.Readable, o ReaderOptions, extraOpts ...ReaderOption) (*Reader, error) { + o = o.ensureDefaults() + r := &Reader{ + readable: f, + opts: o, + } + if r.opts.Cache == nil { + r.opts.Cache = cache.New(0) + } else { + r.opts.Cache.Ref() + } + + if f == nil { + r.err = errors.New("pebble/table: nil file") + return nil, r.Close() + } + + // Note that the extra options are applied twice. First here for pre-apply + // options, and then below for post-apply options. Pre and post refer to + // before and after reading the metaindex and properties. + type preApply interface{ preApply() } + for _, opt := range extraOpts { + if _, ok := opt.(preApply); ok { + opt.readerApply(r) + } + } + if r.cacheID == 0 { + r.cacheID = r.opts.Cache.NewID() + } + + footer, err := readFooter(f) + if err != nil { + r.err = err + return nil, r.Close() + } + r.checksumType = footer.checksum + r.tableFormat = footer.format + // Read the metaindex. + if err := r.readMetaindex(footer.metaindexBH); err != nil { + r.err = err + return nil, r.Close() + } + r.indexBH = footer.indexBH + r.metaIndexBH = footer.metaindexBH + r.footerBH = footer.footerBH + + if r.Properties.ComparerName == "" || o.Comparer.Name == r.Properties.ComparerName { + r.Compare = o.Comparer.Compare + r.FormatKey = o.Comparer.FormatKey + r.Split = o.Comparer.Split + } + + if o.MergerName == r.Properties.MergerName { + r.mergerOK = true + } + + // Apply the extra options again now that the comparer and merger names are + // known. + for _, opt := range extraOpts { + if _, ok := opt.(preApply); !ok { + opt.readerApply(r) + } + } + + if r.Compare == nil { + r.err = errors.Errorf("pebble/table: %d: unknown comparer %s", + errors.Safe(r.fileNum), errors.Safe(r.Properties.ComparerName)) + } + if !r.mergerOK { + if name := r.Properties.MergerName; name != "" && name != "nullptr" { + r.err = errors.Errorf("pebble/table: %d: unknown merger %s", + errors.Safe(r.fileNum), errors.Safe(r.Properties.MergerName)) + } + } + if r.err != nil { + return nil, r.Close() + } + + return r, nil +} + +// ReadableFile describes the smallest subset of vfs.File that is required for +// reading SSTs. +type ReadableFile interface { + io.ReaderAt + io.Closer + Stat() (os.FileInfo, error) +} + +// NewSimpleReadable wraps a ReadableFile in a objstorage.Readable +// implementation (which does not support read-ahead) +func NewSimpleReadable(r ReadableFile) (objstorage.Readable, error) { + info, err := r.Stat() + if err != nil { + return nil, err + } + 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 { + 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(_ context.Context) objstorage.ReadHandle { + return &s.rh +} diff --git a/pebble/sstable/reader_iter.go b/pebble/sstable/reader_iter.go new file mode 100644 index 0000000..2b5a267 --- /dev/null +++ b/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/pebble/sstable/reader_iter_single_lvl.go b/pebble/sstable/reader_iter_single_lvl.go new file mode 100644 index 0000000..73780b8 --- /dev/null +++ b/pebble/sstable/reader_iter_single_lvl.go @@ -0,0 +1,1413 @@ +// 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 and iterStats are slightly different. stats is a shared struct + // supplied from the outside, and represents stats for the whole iterator + // tree and can be reset from the outside (e.g. when the pebble.Iterator is + // being reused). It is currently only provided when the iterator tree is + // rooted at pebble.Iterator. iterStats is this sstable iterator's private + // stats that are reported to a CategoryStatsCollector when this iterator is + // closed. More paths are instrumented with this as the + // CategoryStatsCollector needed for this is provided by the + // tableCacheContainer (which is more universally used). + stats *base.InternalIteratorStats + iterStats iterStatsAccumulator + 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, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, + bufferPool *BufferPool, +) error { + if r.err != nil { + return r.err + } + i.iterStats.init(categoryAndQoS, statsCollector) + indexH, err := r.readIndex(ctx, stats, &i.iterStats) + 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{ + 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 +} + +func (i *singleLevelIterator) SetContext(ctx context.Context) { + i.ctx = ctx +} + +// 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.iterStats, 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( + h BlockHandle, stats *base.InternalIteratorStats, +) (bufferHandle, error) { + ctx := objiotracing.WithBlockType(i.ctx, objiotracing.ValueBlock) + return i.reader.readBlock(ctx, h, nil, i.vbRH, stats, &i.iterStats, 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, &i.iterStats) + 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 { + // TODO(jackson): Can this case be turned into a panic? Once an error is + // encountered, the iterator must be re-seeked. + 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 { + // TODO(jackson): Can this case be turned into a panic? Once an error is + // encountered, the iterator must be re-seeked. + 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 { + i.iterStats.close() + 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/pebble/sstable/reader_iter_two_lvl.go b/pebble/sstable/reader_iter_two_lvl.go new file mode 100644 index 0000000..36fa8ac --- /dev/null +++ b/pebble/sstable/reader_iter_two_lvl.go @@ -0,0 +1,1092 @@ +// 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.iterStats, 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, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, + bufferPool *BufferPool, +) error { + if r.err != nil { + return r.err + } + i.iterStats.init(categoryAndQoS, statsCollector) + topLevelIndexH, err := r.readIndex(ctx, stats, &i.iterStats) + 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{ + 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, &i.iterStats) + 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 { + // TODO(jackson): Can this case be turned into a panic? Once an error is + // encountered, the iterator must be re-seeked. + 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 { + // TODO(jackson): Can this case be turned into a panic? Once an error is + // encountered, the iterator must be re-seeked. + 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 { + i.iterStats.close() + 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/pebble/sstable/reader_test.go b/pebble/sstable/reader_test.go new file mode 100644 index 0000000..fa5237d --- /dev/null +++ b/pebble/sstable/reader_test.go @@ -0,0 +1,2117 @@ +// Copyright 2018 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" + "math" + "os" + "path" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +// get is a testing helper that simulates a read and helps verify bloom filters +// until they are available through iterators. +func (r *Reader) get(key []byte) (value []byte, err error) { + if r.err != nil { + return nil, r.err + } + + if r.tableFilter != nil { + dataH, err := r.readFilter(context.Background(), nil /* stats */, nil) + if err != nil { + return nil, err + } + var lookupKey []byte + if r.Split != nil { + lookupKey = key[:r.Split(key)] + } else { + lookupKey = key + } + mayContain := r.tableFilter.mayContain(dataH.Get(), lookupKey) + dataH.Release() + if !mayContain { + return nil, base.ErrNotFound + } + } + + i, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return nil, err + } + var v base.LazyValue + ikey, v := i.SeekGE(key, base.SeekGEFlagsNone) + value, _, err = v.Value(nil) + if err != nil { + return nil, err + } + + if ikey == nil || r.Compare(key, ikey.UserKey) != 0 { + err := i.Close() + if err == nil { + err = base.ErrNotFound + } + return nil, err + } + + // The value will be "freed" when the iterator is closed, so make a copy + // which will outlast the lifetime of the iterator. + newValue := make([]byte, len(value)) + copy(newValue, value) + if err := i.Close(); err != nil { + return nil, err + } + return newValue, nil +} + +// iterAdapter adapts the new Iterator API which returns the key and value from +// positioning methods (Seek*, First, Last, Next, Prev) to the old API which +// returned a boolean corresponding to Valid. Only used by test code. +type iterAdapter struct { + Iterator + key *InternalKey + val []byte +} + +func newIterAdapter(iter Iterator) *iterAdapter { + return &iterAdapter{ + Iterator: iter, + } +} + +func (i *iterAdapter) update(key *InternalKey, val base.LazyValue) bool { + i.key = key + if v, _, err := val.Value(nil); err != nil { + i.key = nil + i.val = nil + } else { + i.val = v + } + return i.key != nil +} + +func (i *iterAdapter) String() string { + return "iter-adapter" +} + +func (i *iterAdapter) SeekGE(key []byte, flags base.SeekGEFlags) bool { + return i.update(i.Iterator.SeekGE(key, flags)) +} + +func (i *iterAdapter) SeekPrefixGE(prefix, key []byte, flags base.SeekGEFlags) bool { + return i.update(i.Iterator.SeekPrefixGE(prefix, key, flags)) +} + +func (i *iterAdapter) SeekLT(key []byte, flags base.SeekLTFlags) bool { + return i.update(i.Iterator.SeekLT(key, flags)) +} + +func (i *iterAdapter) First() bool { + return i.update(i.Iterator.First()) +} + +func (i *iterAdapter) Last() bool { + return i.update(i.Iterator.Last()) +} + +func (i *iterAdapter) Next() bool { + return i.update(i.Iterator.Next()) +} + +func (i *iterAdapter) NextPrefix(succKey []byte) bool { + return i.update(i.Iterator.NextPrefix(succKey)) +} + +func (i *iterAdapter) NextIgnoreResult() { + i.Iterator.Next() + i.update(nil, base.LazyValue{}) +} + +func (i *iterAdapter) Prev() bool { + return i.update(i.Iterator.Prev()) +} + +func (i *iterAdapter) Key() *InternalKey { + return i.key +} + +func (i *iterAdapter) Value() []byte { + return i.val +} + +func (i *iterAdapter) Valid() bool { + return i.key != nil +} + +func (i *iterAdapter) SetBounds(lower, upper []byte) { + i.Iterator.SetBounds(lower, upper) + i.key = nil +} + +func (i *iterAdapter) SetContext(ctx context.Context) { + i.Iterator.SetContext(ctx) +} + +func TestVirtualReader(t *testing.T) { + // A faux filenum used to create fake filemetadata for testing. + var fileNum int = 1 + nextFileNum := func() base.FileNum { + fileNum++ + return base.FileNum(fileNum - 1) + } + + // Set during the latest build command. + var r *Reader + var meta manifest.PhysicalFileMeta + var bp BufferPool + + // Set during the latest virtualize command. + var vMeta1 manifest.VirtualFileMeta + var v VirtualReader + + defer func() { + if r != nil { + require.NoError(t, r.Close()) + bp.Release() + } + }() + + createPhysicalMeta := func(w *WriterMetadata, r *Reader) (manifest.PhysicalFileMeta, error) { + meta := &manifest.FileMetadata{} + meta.FileNum = nextFileNum() + meta.CreationTime = time.Now().Unix() + meta.Size = w.Size + meta.SmallestSeqNum = w.SmallestSeqNum + meta.LargestSeqNum = w.LargestSeqNum + + if w.HasPointKeys { + meta.ExtendPointKeyBounds(r.Compare, w.SmallestPoint, w.LargestPoint) + } + if w.HasRangeDelKeys { + meta.ExtendPointKeyBounds(r.Compare, w.SmallestRangeDel, w.LargestRangeDel) + } + if w.HasRangeKeys { + meta.ExtendRangeKeyBounds(r.Compare, w.SmallestRangeKey, w.LargestRangeKey) + } + meta.InitPhysicalBacking() + + if err := meta.Validate(r.Compare, r.opts.Comparer.FormatKey); err != nil { + return manifest.PhysicalFileMeta{}, err + } + + return meta.PhysicalMeta(), nil + } + + formatWMeta := func(m *WriterMetadata) string { + var b bytes.Buffer + if m.HasPointKeys { + fmt.Fprintf(&b, "point: [%s-%s]\n", m.SmallestPoint, m.LargestPoint) + } + if m.HasRangeDelKeys { + fmt.Fprintf(&b, "rangedel: [%s-%s]\n", m.SmallestRangeDel, m.LargestRangeDel) + } + if m.HasRangeKeys { + fmt.Fprintf(&b, "rangekey: [%s-%s]\n", m.SmallestRangeKey, m.LargestRangeKey) + } + fmt.Fprintf(&b, "seqnums: [%d-%d]\n", m.SmallestSeqNum, m.LargestSeqNum) + return b.String() + } + + formatVirtualReader := func(v *VirtualReader) string { + var b bytes.Buffer + fmt.Fprintf(&b, "bounds: [%s-%s]\n", v.vState.lower, v.vState.upper) + fmt.Fprintf(&b, "filenum: %s\n", v.vState.fileNum.String()) + fmt.Fprintf( + &b, "props: %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d, %s: %d\n", + "NumEntries", + v.Properties.NumEntries, + "RawKeySize", + v.Properties.RawKeySize, + "RawValueSize", + v.Properties.RawValueSize, + "RawPointTombstoneKeySize", + v.Properties.RawPointTombstoneKeySize, + "RawPointTombstoneValueSize", + v.Properties.RawPointTombstoneValueSize, + "NumSizedDeletions", + v.Properties.NumSizedDeletions, + "NumDeletions", + v.Properties.NumDeletions, + "NumRangeDeletions", + v.Properties.NumRangeDeletions, + "NumRangeKeyDels", + v.Properties.NumRangeKeyDels, + "NumRangeKeySets", + v.Properties.NumRangeKeySets, + "ValueBlocksSize", + v.Properties.ValueBlocksSize, + ) + return b.String() + } + + datadriven.RunTest(t, "testdata/virtual_reader", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "build": + if r != nil { + bp.Release() + _ = r.Close() + r = nil + meta.FileMetadata = nil + vMeta1.FileMetadata = nil + v = VirtualReader{} + } + var wMeta *WriterMetadata + var err error + writerOpts := &WriterOptions{ + TableFormat: TableFormatMax, + } + // Use a single level index by default. + writerOpts.IndexBlockSize = 100000 + if len(td.CmdArgs) == 1 { + if td.CmdArgs[0].String() == "twoLevel" { + // Force a two level index. + writerOpts.IndexBlockSize = 1 + writerOpts.BlockSize = 1 + } + } + wMeta, r, err = runBuildCmd(td, writerOpts, 0) + if err != nil { + return err.Error() + } + bp.Init(5) + + // Create a fake filemetada using the writer meta. + meta, err = createPhysicalMeta(wMeta, r) + if err != nil { + return err.Error() + } + r.fileNum = meta.FileBacking.DiskFileNum + return formatWMeta(wMeta) + + case "virtualize": + // virtualize will split the previously built physical sstable into + // a single sstable with virtual bounds. The command assumes that + // the bounds for the virtual sstable are valid. For the purposes of + // this command the bounds must be valid keys. In general, and for + // this command, range key/range del spans must also not span across + // virtual sstable bounds. + if meta.FileMetadata == nil { + return "build must be called at least once before virtualize" + } + if vMeta1.FileMetadata != nil { + vMeta1.FileMetadata = nil + v = VirtualReader{} + } + vMeta := &manifest.FileMetadata{ + FileBacking: meta.FileBacking, + SmallestSeqNum: meta.SmallestSeqNum, + LargestSeqNum: meta.LargestSeqNum, + Virtual: true, + } + // Parse the virtualization bounds. + bounds := strings.Split(td.CmdArgs[0].String(), "-") + vMeta.Smallest = base.ParseInternalKey(bounds[0]) + vMeta.Largest = base.ParseInternalKey(bounds[1]) + vMeta.FileNum = nextFileNum() + var err error + vMeta.Size, err = r.EstimateDiskUsage(vMeta.Smallest.UserKey, vMeta.Largest.UserKey) + if err != nil { + return err.Error() + } + vMeta.ValidateVirtual(meta.FileMetadata) + + vMeta1 = vMeta.VirtualMeta() + v = MakeVirtualReader(r, vMeta1, false /* isForeign */) + return formatVirtualReader(&v) + + case "citer": + // Creates a compaction iterator from the virtual reader, and then + // just scans the keyspace. Which is all a compaction iterator is + // used for. This tests the First and Next calls. + if vMeta1.FileMetadata == nil { + return "virtualize must be called before creating compaction iters" + } + + var rp ReaderProvider + var bytesIterated uint64 + iter, err := v.NewCompactionIter(&bytesIterated, CategoryAndQoS{}, nil, rp, &bp) + if err != nil { + return err.Error() + } + + var buf bytes.Buffer + for key, val := iter.First(); key != nil; key, val = iter.Next() { + fmt.Fprintf(&buf, "%s:%s\n", key.String(), val.InPlaceValue()) + } + err = iter.Close() + if err != nil { + return err.Error() + } + return buf.String() + + case "constrain": + if vMeta1.FileMetadata == nil { + return "virtualize must be called before constrain" + } + splits := strings.Split(td.CmdArgs[0].String(), ",") + of, ol := []byte(splits[0]), []byte(splits[1]) + inclusive, f, l := v.vState.constrainBounds(of, ol, splits[2] == "true") + var buf bytes.Buffer + buf.Write(f) + buf.WriteByte(',') + buf.Write(l) + buf.WriteByte(',') + if inclusive { + buf.WriteString("true") + } else { + buf.WriteString("false") + } + buf.WriteByte('\n') + return buf.String() + + case "scan-range-del": + if vMeta1.FileMetadata == nil { + return "virtualize must be called before scan-range-del" + } + iter, err := v.NewRawRangeDelIter() + if err != nil { + return err.Error() + } + if iter == nil { + return "" + } + defer iter.Close() + + var buf bytes.Buffer + for s := iter.First(); s != nil; s = iter.Next() { + fmt.Fprintf(&buf, "%s\n", s) + } + return buf.String() + + case "scan-range-key": + if vMeta1.FileMetadata == nil { + return "virtualize must be called before scan-range-key" + } + iter, err := v.NewRawRangeKeyIter() + if err != nil { + return err.Error() + } + if iter == nil { + return "" + } + defer iter.Close() + + var buf bytes.Buffer + for s := iter.First(); s != nil; s = iter.Next() { + fmt.Fprintf(&buf, "%s\n", s) + } + return buf.String() + + case "iter": + if vMeta1.FileMetadata == nil { + return "virtualize must be called before iter" + } + var lower, upper []byte + if len(td.CmdArgs) > 0 { + splits := strings.Split(td.CmdArgs[0].String(), "-") + lower, upper = []byte(splits[0]), []byte(splits[1]) + } + + var stats base.InternalIteratorStats + iter, err := v.NewIterWithBlockPropertyFiltersAndContextEtc( + context.Background(), lower, upper, nil, false, false, + &stats, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}) + if err != nil { + return err.Error() + } + return runIterCmd(td, iter, true, runIterCmdStats(&stats)) + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestReader(t *testing.T) { + writerOpts := map[string]WriterOptions{ + // No bloom filters. + "default": {}, + "bloom10bit": { + // The standard policy. + FilterPolicy: bloom.FilterPolicy(10), + FilterType: base.TableFilter, + }, + "bloom1bit": { + // A policy with many false positives. + FilterPolicy: bloom.FilterPolicy(1), + FilterType: base.TableFilter, + }, + "bloom100bit": { + // A policy unlikely to have false positives. + FilterPolicy: bloom.FilterPolicy(100), + FilterType: base.TableFilter, + }, + } + + blockSizes := map[string]int{ + "1bytes": 1, + "5bytes": 5, + "10bytes": 10, + "25bytes": 25, + "Maxbytes": math.MaxInt32, + } + + opts := map[string]*Comparer{ + "default": testkeys.Comparer, + "prefixFilter": fixtureComparer, + } + + testDirs := map[string]string{ + "default": "testdata/reader", + "prefixFilter": "testdata/prefixreader", + } + + for format := TableFormatPebblev2; format <= TableFormatMax; format++ { + for dName, blockSize := range blockSizes { + for iName, indexBlockSize := range blockSizes { + for lName, tableOpt := range writerOpts { + for oName, cmp := range opts { + tableOpt.BlockSize = blockSize + tableOpt.Comparer = cmp + tableOpt.IndexBlockSize = indexBlockSize + tableOpt.TableFormat = format + + t.Run( + fmt.Sprintf("format=%d,opts=%s,writerOpts=%s,blockSize=%s,indexSize=%s", + format, oName, lName, dName, iName), + func(t *testing.T) { + runTestReader( + t, tableOpt, testDirs[oName], nil /* Reader */, true) + }) + } + } + } + } + } +} + +func TestReaderHideObsolete(t *testing.T) { + blockSizes := map[string]int{ + "1bytes": 1, + "5bytes": 5, + "10bytes": 10, + "25bytes": 25, + "Maxbytes": math.MaxInt32, + } + for dName, blockSize := range blockSizes { + opts := WriterOptions{ + TableFormat: TableFormatPebblev4, + BlockSize: blockSize, + IndexBlockSize: blockSize, + Comparer: testkeys.Comparer, + } + t.Run(fmt.Sprintf("blockSize=%s", dName), func(t *testing.T) { + runTestReader( + t, opts, "testdata/reader_hide_obsolete", + nil /* Reader */, true) + }) + } +} + +func TestHamletReader(t *testing.T) { + prebuiltSSTs := []string{ + "testdata/h.ldb", + "testdata/h.sst", + "testdata/h.no-compression.sst", + "testdata/h.no-compression.two_level_index.sst", + "testdata/h.block-bloom.no-compression.sst", + "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", + "testdata/h.table-bloom.no-compression.sst", + } + + for _, prebuiltSST := range prebuiltSSTs { + f, err := os.Open(filepath.FromSlash(prebuiltSST)) + require.NoError(t, err) + + r, err := newReader(f, ReaderOptions{}) + require.NoError(t, err) + + t.Run( + fmt.Sprintf("sst=%s", prebuiltSST), + func(t *testing.T) { + runTestReader(t, WriterOptions{}, "testdata/hamletreader", r, false) + }, + ) + } +} + +func forEveryTableFormat[I any]( + t *testing.T, formatTable [NumTableFormats]I, runTest func(*testing.T, TableFormat, I), +) { + t.Helper() + for tf := TableFormatUnspecified + 1; tf <= TableFormatMax; tf++ { + t.Run(tf.String(), func(t *testing.T) { + runTest(t, tf, formatTable[tf]) + }) + } +} + +func TestReaderStats(t *testing.T) { + forEveryTableFormat[string](t, + [NumTableFormats]string{ + TableFormatUnspecified: "", + TableFormatLevelDB: "testdata/readerstats_LevelDB", + TableFormatRocksDBv2: "testdata/readerstats_LevelDB", + TableFormatPebblev1: "testdata/readerstats_LevelDB", + TableFormatPebblev2: "testdata/readerstats_LevelDB", + TableFormatPebblev3: "testdata/readerstats_Pebblev3", + TableFormatPebblev4: "testdata/readerstats_Pebblev3", + }, func(t *testing.T, format TableFormat, dir string) { + if dir == "" { + t.Skip() + } + writerOpt := WriterOptions{ + BlockSize: 32 << 10, + IndexBlockSize: 32 << 10, + Comparer: testkeys.Comparer, + TableFormat: format, + } + runTestReader(t, writerOpt, dir, nil /* Reader */, false /* printValue */) + }) +} + +func TestReaderWithBlockPropertyFilter(t *testing.T) { + // Some of these tests examine internal iterator state, so they require + // determinism. When the invariants tag is set, disableBoundsOpt may disable + // the bounds optimization depending on the iterator pointer address. This + // can add nondeterminism to the internal iterator statae. Disable this + // nondeterminism for the duration of this test. + ensureBoundsOptDeterminism = true + defer func() { ensureBoundsOptDeterminism = false }() + + forEveryTableFormat[string](t, + [NumTableFormats]string{ + TableFormatUnspecified: "", // Block properties unsupported + TableFormatLevelDB: "", // Block properties unsupported + TableFormatRocksDBv2: "", // Block properties unsupported + TableFormatPebblev1: "", // Block properties unsupported + TableFormatPebblev2: "testdata/reader_bpf/Pebblev2", + TableFormatPebblev3: "testdata/reader_bpf/Pebblev3", + TableFormatPebblev4: "testdata/reader_bpf/Pebblev3", + }, func(t *testing.T, format TableFormat, dir string) { + if dir == "" { + t.Skip("Block-properties unsupported") + } + writerOpt := WriterOptions{ + Comparer: testkeys.Comparer, + TableFormat: format, + BlockPropertyCollectors: []func() BlockPropertyCollector{NewTestKeysBlockPropertyCollector}, + } + runTestReader(t, writerOpt, dir, nil /* Reader */, false) + }) +} + +func TestInjectedErrors(t *testing.T) { + prebuiltSSTs := []string{ + "testdata/h.ldb", + "testdata/h.sst", + "testdata/h.no-compression.sst", + "testdata/h.no-compression.two_level_index.sst", + "testdata/h.block-bloom.no-compression.sst", + "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", + "testdata/h.table-bloom.no-compression.sst", + } + + for _, prebuiltSST := range prebuiltSSTs { + run := func(i int) (reterr error) { + f, err := vfs.Default.Open(filepath.FromSlash(prebuiltSST)) + require.NoError(t, err) + + r, err := newReader(errorfs.WrapFile(f, errorfs.ErrInjected.If(errorfs.OnIndex(int32(i)))), ReaderOptions{}) + if err != nil { + return firstError(err, f.Close()) + } + defer func() { reterr = firstError(reterr, r.Close()) }() + + _, err = r.EstimateDiskUsage([]byte("borrower"), []byte("lender")) + if err != nil { + return err + } + + iter, err := r.NewIter(nil, nil) + if err != nil { + return err + } + defer func() { reterr = firstError(reterr, iter.Close()) }() + for k, v := iter.First(); k != nil; k, v = iter.Next() { + val, _, err := v.Value(nil) + if err != nil { + return err + } + if val == nil { + break + } + } + if err = iter.Error(); err != nil { + return err + } + return nil + } + for i := 0; ; i++ { + err := run(i) + if errors.Is(err, errorfs.ErrInjected) { + t.Logf("%q, index %d: %s", prebuiltSST, i, err) + continue + } + if err != nil { + t.Errorf("%q, index %d: non-injected error: %+v", prebuiltSST, i, err) + break + } + t.Logf("%q: no error at index %d", prebuiltSST, i) + break + } + } +} + +func TestInvalidReader(t *testing.T) { + invalid, err := NewSimpleReadable(vfs.NewMemFile([]byte("invalid sst bytes"))) + if err != nil { + t.Fatal(err) + } + testCases := []struct { + readable objstorage.Readable + expected string + }{ + {nil, "nil file"}, + {invalid, "invalid table"}, + } + for _, tc := range testCases { + r, err := NewReader(tc.readable, ReaderOptions{}) + if !strings.Contains(err.Error(), tc.expected) { + t.Fatalf("expected %q, but found %q", tc.expected, err.Error()) + } + if r != nil { + t.Fatalf("found non-nil reader returned with non-nil error %q", err.Error()) + } + } +} + +func indexLayoutString(t *testing.T, r *Reader) string { + indexH, err := r.readIndex(context.Background(), nil, nil) + require.NoError(t, err) + defer indexH.Release() + var buf strings.Builder + twoLevelIndex := r.Properties.IndexType == twoLevelIndex + buf.WriteString("index entries:\n") + iter, err := newBlockIter(r.Compare, indexH.Get()) + defer func() { + require.NoError(t, iter.Close()) + }() + require.NoError(t, err) + for key, value := iter.First(); key != nil; key, value = iter.Next() { + bh, err := decodeBlockHandleWithProperties(value.InPlaceValue()) + require.NoError(t, err) + fmt.Fprintf(&buf, " %s: size %d\n", string(key.UserKey), bh.Length) + if twoLevelIndex { + b, err := r.readBlock( + context.Background(), bh.BlockHandle, nil, nil, nil, nil, nil) + require.NoError(t, err) + defer b.Release() + iter2, err := newBlockIter(r.Compare, b.Get()) + defer func() { + require.NoError(t, iter2.Close()) + }() + require.NoError(t, err) + for key, value := iter2.First(); key != nil; key, value = iter2.Next() { + bh, err := decodeBlockHandleWithProperties(value.InPlaceValue()) + require.NoError(t, err) + fmt.Fprintf(&buf, " %s: size %d\n", string(key.UserKey), bh.Length) + } + } + } + return buf.String() +} + +func runTestReader(t *testing.T, o WriterOptions, dir string, r *Reader, printValue bool) { + datadriven.Walk(t, dir, func(t *testing.T, path string) { + defer func() { + if r != nil { + r.Close() + r = nil + } + }() + + datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { + switch d.Cmd { + case "build": + if r != nil { + r.Close() + r = nil + } + var cacheSize int + var printLayout bool + d.MaybeScanArgs(t, "cache-size", &cacheSize) + d.MaybeScanArgs(t, "print-layout", &printLayout) + d.MaybeScanArgs(t, "block-size", &o.BlockSize) + d.MaybeScanArgs(t, "index-block-size", &o.IndexBlockSize) + + var err error + _, r, err = runBuildCmd(d, &o, cacheSize) + if err != nil { + return err.Error() + } + if printLayout { + return indexLayoutString(t, r) + } + return "" + + case "iter": + seqNum, err := scanGlobalSeqNum(d) + if err != nil { + return err.Error() + } + var stats base.InternalIteratorStats + r.Properties.GlobalSeqNum = seqNum + var bpfs []BlockPropertyFilter + if d.HasArg("block-property-filter") { + var filterMin, filterMax uint64 + d.ScanArgs(t, "block-property-filter", &filterMin, &filterMax) + bpf := NewTestKeysBlockPropertyFilter(filterMin, filterMax) + bpfs = append(bpfs, bpf) + } + hideObsoletePoints := false + if d.HasArg("hide-obsolete-points") { + d.ScanArgs(t, "hide-obsolete-points", &hideObsoletePoints) + if hideObsoletePoints { + hideObsoletePoints, bpfs = r.TryAddBlockPropertyFilterForHideObsoletePoints( + InternalKeySeqNumMax, InternalKeySeqNumMax-1, bpfs) + require.True(t, hideObsoletePoints) + } + } + var filterer *BlockPropertiesFilterer + if len(bpfs) > 0 { + filterer = newBlockPropertiesFilterer(bpfs, nil) + intersects, err := + filterer.intersectsUserPropsAndFinishInit(r.Properties.UserProperties) + if err != nil { + return err.Error() + } + if !intersects { + return "table does not intersect BlockPropertyFilter" + } + } + iter, err := r.NewIterWithBlockPropertyFiltersAndContextEtc( + context.Background(), + nil, /* lower */ + nil, /* upper */ + filterer, + hideObsoletePoints, + true, /* use filter block */ + &stats, + CategoryAndQoS{}, + nil, + TrivialReaderProvider{Reader: r}, + ) + if err != nil { + return err.Error() + } + return runIterCmd(d, iter, printValue, runIterCmdStats(&stats)) + + case "get": + var b bytes.Buffer + for _, k := range strings.Split(d.Input, "\n") { + v, err := r.get([]byte(k)) + if err != nil { + fmt.Fprintf(&b, "\n", err) + } else { + fmt.Fprintln(&b, string(v)) + } + } + return b.String() + default: + return fmt.Sprintf("unknown command: %s", d.Cmd) + } + }) + }) +} + +func TestReaderCheckComparerMerger(t *testing.T) { + const testTable = "test" + + testComparer := &base.Comparer{ + Name: "test.comparer", + Compare: base.DefaultComparer.Compare, + Equal: base.DefaultComparer.Equal, + Separator: base.DefaultComparer.Separator, + Successor: base.DefaultComparer.Successor, + } + testMerger := &base.Merger{ + Name: "test.merger", + Merge: base.DefaultMerger.Merge, + } + writerOpts := WriterOptions{ + Comparer: testComparer, + MergerName: "test.merger", + } + + mem := vfs.NewMem() + f0, err := mem.Create(testTable) + require.NoError(t, err) + + w := NewWriter(objstorageprovider.NewFileWritable(f0), writerOpts) + require.NoError(t, w.Set([]byte("test"), nil)) + require.NoError(t, w.Close()) + + testCases := []struct { + comparers []*base.Comparer + mergers []*base.Merger + expected string + }{ + { + []*base.Comparer{testComparer}, + []*base.Merger{testMerger}, + "", + }, + { + []*base.Comparer{testComparer, base.DefaultComparer}, + []*base.Merger{testMerger, base.DefaultMerger}, + "", + }, + { + []*base.Comparer{}, + []*base.Merger{testMerger}, + "unknown comparer test.comparer", + }, + { + []*base.Comparer{base.DefaultComparer}, + []*base.Merger{testMerger}, + "unknown comparer test.comparer", + }, + { + []*base.Comparer{testComparer}, + []*base.Merger{}, + "unknown merger test.merger", + }, + { + []*base.Comparer{testComparer}, + []*base.Merger{base.DefaultMerger}, + "unknown merger test.merger", + }, + } + + for _, c := range testCases { + t.Run("", func(t *testing.T) { + f1, err := mem.Open(testTable) + require.NoError(t, err) + + comparers := make(Comparers) + for _, comparer := range c.comparers { + comparers[comparer.Name] = comparer + } + mergers := make(Mergers) + for _, merger := range c.mergers { + mergers[merger.Name] = merger + } + + r, err := newReader(f1, ReaderOptions{}, comparers, mergers) + if err != nil { + if r != nil { + t.Fatalf("found non-nil reader returned with non-nil error %q", err.Error()) + } + if !strings.HasSuffix(err.Error(), c.expected) { + t.Fatalf("expected %q, but found %q", c.expected, err.Error()) + } + } else if c.expected != "" { + t.Fatalf("expected %q, but found success", c.expected) + } + if r != nil { + _ = r.Close() + } + }) + } +} +func checkValidPrefix(prefix, key []byte) bool { + return prefix == nil || bytes.HasPrefix(key, prefix) +} + +func testBytesIteratedWithCompression( + t *testing.T, + compression Compression, + allowedSizeDeviationPercent uint64, + blockSizes []int, + maxNumEntries []uint64, +) { + for i, blockSize := range blockSizes { + for _, indexBlockSize := range blockSizes { + for _, numEntries := range []uint64{0, 1, maxNumEntries[i]} { + r := buildTestTable(t, numEntries, blockSize, indexBlockSize, compression) + var bytesIterated, prevIterated uint64 + var pool BufferPool + pool.Init(5) + citer, err := r.NewCompactionIter( + &bytesIterated, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}, &pool) + require.NoError(t, err) + + for key, _ := citer.First(); key != nil; key, _ = citer.Next() { + if bytesIterated < prevIterated { + t.Fatalf("bytesIterated moved backward: %d < %d", bytesIterated, prevIterated) + } + prevIterated = bytesIterated + } + + expected := r.Properties.DataSize + allowedSizeDeviation := expected * allowedSizeDeviationPercent / 100 + // There is some inaccuracy due to compression estimation. + if bytesIterated < expected-allowedSizeDeviation || bytesIterated > expected+allowedSizeDeviation { + t.Fatalf("bytesIterated: got %d, want %d", bytesIterated, expected) + } + + require.NoError(t, citer.Close()) + require.NoError(t, r.Close()) + pool.Release() + } + } + } +} + +func TestBytesIterated(t *testing.T) { + blockSizes := []int{10, 100, 1000, 4096, math.MaxInt32} + t.Run("Compressed", func(t *testing.T) { + testBytesIteratedWithCompression(t, SnappyCompression, 1, blockSizes, []uint64{1e5, 1e5, 1e5, 1e5, 1e5}) + }) + t.Run("Uncompressed", func(t *testing.T) { + testBytesIteratedWithCompression(t, NoCompression, 0, blockSizes, []uint64{1e5, 1e5, 1e5, 1e5, 1e5}) + }) + t.Run("Zstd", func(t *testing.T) { + // compression with zstd is extremely slow with small block size (esp the nocgo version). + // use less numEntries to make the test run at reasonable speed (under 10 seconds). + maxNumEntries := []uint64{1e2, 1e2, 1e3, 4e3, 1e5} + if useStandardZstdLib { + maxNumEntries = []uint64{1e3, 1e3, 1e4, 4e4, 1e5} + } + testBytesIteratedWithCompression(t, ZstdCompression, 1, blockSizes, maxNumEntries) + }) +} + +func TestCompactionIteratorSetupForCompaction(t *testing.T) { + tmpDir := path.Join(t.TempDir()) + provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(vfs.Default, tmpDir)) + require.NoError(t, err) + defer provider.Close() + blockSizes := []int{10, 100, 1000, 4096, math.MaxInt32} + for _, blockSize := range blockSizes { + for _, indexBlockSize := range blockSizes { + for _, numEntries := range []uint64{0, 1, 1e5} { + r := buildTestTableWithProvider(t, provider, numEntries, blockSize, indexBlockSize, DefaultCompression) + var bytesIterated uint64 + var pool BufferPool + pool.Init(5) + citer, err := r.NewCompactionIter( + &bytesIterated, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}, &pool) + require.NoError(t, err) + switch i := citer.(type) { + case *compactionIterator: + require.True(t, objstorageprovider.TestingCheckMaxReadahead(i.dataRH)) + // Each key has one version, so no value block, regardless of + // sstable version. + require.Nil(t, i.vbRH) + case *twoLevelCompactionIterator: + require.True(t, objstorageprovider.TestingCheckMaxReadahead(i.dataRH)) + // Each key has one version, so no value block, regardless of + // sstable version. + require.Nil(t, i.vbRH) + default: + require.Failf(t, fmt.Sprintf("unknown compaction iterator type: %T", citer), "") + } + require.NoError(t, citer.Close()) + require.NoError(t, r.Close()) + pool.Release() + } + } + } +} + +func TestReadaheadSetupForV3TablesWithMultipleVersions(t *testing.T) { + tmpDir := path.Join(t.TempDir()) + provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(vfs.Default, tmpDir)) + require.NoError(t, err) + defer provider.Close() + f0, _, err := provider.Create(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.CreateOptions{}) + require.NoError(t, err) + + w := NewWriter(f0, WriterOptions{ + TableFormat: TableFormatPebblev3, + Comparer: testkeys.Comparer, + }) + keys := testkeys.Alpha(1) + keyBuf := make([]byte, 1+testkeys.MaxSuffixLen) + // Write a few keys with multiple timestamps (MVCC versions). + for i := int64(0); i < 2; i++ { + for j := int64(2); j >= 1; j-- { + n := testkeys.WriteKeyAt(keyBuf[:], keys, i, j) + key := keyBuf[:n] + require.NoError(t, w.Set(key, key)) + } + } + require.NoError(t, w.Close()) + f1, err := provider.OpenForReading(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.OpenOptions{}) + require.NoError(t, err) + r, err := NewReader(f1, ReaderOptions{Comparer: testkeys.Comparer}) + require.NoError(t, err) + defer r.Close() + { + var pool BufferPool + pool.Init(5) + citer, err := r.NewCompactionIter( + nil, CategoryAndQoS{}, nil, TrivialReaderProvider{Reader: r}, &pool) + require.NoError(t, err) + defer citer.Close() + i := citer.(*compactionIterator) + require.True(t, objstorageprovider.TestingCheckMaxReadahead(i.dataRH)) + require.True(t, objstorageprovider.TestingCheckMaxReadahead(i.vbRH)) + } + { + iter, err := r.NewIter(nil, nil) + require.NoError(t, err) + defer iter.Close() + i := iter.(*singleLevelIterator) + require.False(t, objstorageprovider.TestingCheckMaxReadahead(i.dataRH)) + require.False(t, objstorageprovider.TestingCheckMaxReadahead(i.vbRH)) + } +} + +func TestReaderChecksumErrors(t *testing.T) { + for _, checksumType := range []ChecksumType{ChecksumTypeCRC32c, ChecksumTypeXXHash64} { + t.Run(fmt.Sprintf("checksum-type=%d", checksumType), func(t *testing.T) { + for _, twoLevelIndex := range []bool{false, true} { + t.Run(fmt.Sprintf("two-level-index=%t", twoLevelIndex), func(t *testing.T) { + mem := vfs.NewMem() + + { + // Create an sstable with 3 data blocks. + f, err := mem.Create("test") + require.NoError(t, err) + + const blockSize = 32 + indexBlockSize := 4096 + if twoLevelIndex { + indexBlockSize = 1 + } + + w := NewWriter(objstorageprovider.NewFileWritable(f), WriterOptions{ + BlockSize: blockSize, + IndexBlockSize: indexBlockSize, + Checksum: checksumType, + }) + require.NoError(t, w.Set(bytes.Repeat([]byte("a"), blockSize), nil)) + require.NoError(t, w.Set(bytes.Repeat([]byte("b"), blockSize), nil)) + require.NoError(t, w.Set(bytes.Repeat([]byte("c"), blockSize), nil)) + require.NoError(t, w.Close()) + } + + // Load the layout so that we no the location of the data blocks. + var layout *Layout + { + f, err := mem.Open("test") + require.NoError(t, err) + + r, err := newReader(f, ReaderOptions{}) + require.NoError(t, err) + layout, err = r.Layout() + require.NoError(t, err) + require.EqualValues(t, len(layout.Data), 3) + require.NoError(t, r.Close()) + } + + for _, bh := range layout.Data { + // Read the sstable and corrupt the first byte in the target data + // block. + orig, err := mem.Open("test") + require.NoError(t, err) + data, err := io.ReadAll(orig) + require.NoError(t, err) + require.NoError(t, orig.Close()) + + // Corrupt the first byte in the block. + data[bh.Offset] ^= 0xff + + corrupted, err := mem.Create("corrupted") + require.NoError(t, err) + _, err = corrupted.Write(data) + require.NoError(t, err) + require.NoError(t, corrupted.Close()) + + // Verify that we encounter a checksum mismatch error while iterating + // over the sstable. + corrupted, err = mem.Open("corrupted") + require.NoError(t, err) + + r, err := newReader(corrupted, ReaderOptions{}) + require.NoError(t, err) + + iter, err := r.NewIter(nil, nil) + require.NoError(t, err) + for k, _ := iter.First(); k != nil; k, _ = iter.Next() { + } + require.Regexp(t, `checksum mismatch`, iter.Error()) + require.Regexp(t, `checksum mismatch`, iter.Close()) + + iter, err = r.NewIter(nil, nil) + require.NoError(t, err) + for k, _ := iter.Last(); k != nil; k, _ = iter.Prev() { + } + require.Regexp(t, `checksum mismatch`, iter.Error()) + require.Regexp(t, `checksum mismatch`, iter.Close()) + + require.NoError(t, r.Close()) + } + }) + } + }) + } +} + +func TestValidateBlockChecksums(t *testing.T) { + seed := uint64(time.Now().UnixNano()) + rng := rand.New(rand.NewSource(seed)) + t.Logf("using seed = %d", seed) + + allFiles := []string{ + "testdata/h.no-compression.sst", + "testdata/h.no-compression.two_level_index.sst", + "testdata/h.sst", + "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", + "testdata/h.table-bloom.no-compression.sst", + "testdata/h.table-bloom.sst", + "testdata/h.zstd-compression.sst", + } + + type corruptionLocation int + const ( + corruptionLocationData corruptionLocation = iota + corruptionLocationIndex + corruptionLocationTopIndex + corruptionLocationFilter + corruptionLocationRangeDel + corruptionLocationProperties + corruptionLocationMetaIndex + ) + + testCases := []struct { + name string + files []string + corruptionLocations []corruptionLocation + }{ + { + name: "no corruption", + corruptionLocations: []corruptionLocation{}, + }, + { + name: "data block corruption", + corruptionLocations: []corruptionLocation{ + corruptionLocationData, + }, + }, + { + name: "index block corruption", + corruptionLocations: []corruptionLocation{ + corruptionLocationIndex, + }, + }, + { + name: "top index block corruption", + files: []string{ + "testdata/h.no-compression.two_level_index.sst", + }, + corruptionLocations: []corruptionLocation{ + corruptionLocationTopIndex, + }, + }, + { + name: "filter block corruption", + files: []string{ + "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", + "testdata/h.table-bloom.no-compression.sst", + "testdata/h.table-bloom.sst", + }, + corruptionLocations: []corruptionLocation{ + corruptionLocationFilter, + }, + }, + { + name: "range deletion block corruption", + corruptionLocations: []corruptionLocation{ + corruptionLocationRangeDel, + }, + }, + { + name: "properties block corruption", + corruptionLocations: []corruptionLocation{ + corruptionLocationProperties, + }, + }, + { + name: "metaindex block corruption", + corruptionLocations: []corruptionLocation{ + corruptionLocationMetaIndex, + }, + }, + { + name: "multiple blocks corrupted", + corruptionLocations: []corruptionLocation{ + corruptionLocationData, + corruptionLocationIndex, + corruptionLocationRangeDel, + corruptionLocationProperties, + corruptionLocationMetaIndex, + }, + }, + } + + testFn := func(t *testing.T, file string, corruptionLocations []corruptionLocation) { + // Create a copy of the SSTable that we can freely corrupt. + f, err := os.Open(filepath.FromSlash(file)) + require.NoError(t, err) + + pathCopy := path.Join(t.TempDir(), path.Base(file)) + fCopy, err := os.OpenFile(pathCopy, os.O_CREATE|os.O_RDWR, 0600) + require.NoError(t, err) + defer fCopy.Close() + + _, err = io.Copy(fCopy, f) + require.NoError(t, err) + err = fCopy.Sync() + require.NoError(t, err) + require.NoError(t, f.Close()) + + filter := bloom.FilterPolicy(10) + r, err := newReader(fCopy, ReaderOptions{ + Filters: map[string]FilterPolicy{ + filter.Name(): filter, + }, + }) + require.NoError(t, err) + defer func() { require.NoError(t, r.Close()) }() + + // Prior to corruption, validation is successful. + require.NoError(t, r.ValidateBlockChecksums()) + + // If we are not testing for corruption, we can stop here. + if len(corruptionLocations) == 0 { + return + } + + // Perform bit flips in various corruption locations. + layout, err := r.Layout() + require.NoError(t, err) + for _, location := range corruptionLocations { + var bh BlockHandle + switch location { + case corruptionLocationData: + bh = layout.Data[rng.Intn(len(layout.Data))].BlockHandle + case corruptionLocationIndex: + bh = layout.Index[rng.Intn(len(layout.Index))] + case corruptionLocationTopIndex: + bh = layout.TopIndex + case corruptionLocationFilter: + bh = layout.Filter + case corruptionLocationRangeDel: + bh = layout.RangeDel + case corruptionLocationProperties: + bh = layout.Properties + case corruptionLocationMetaIndex: + bh = layout.MetaIndex + default: + t.Fatalf("unknown location") + } + + // Corrupt a random byte within the selected block. + pos := int64(bh.Offset) + rng.Int63n(int64(bh.Length)) + t.Logf("altering file=%s @ offset = %d", file, pos) + + b := make([]byte, 1) + n, err := fCopy.ReadAt(b, pos) + require.NoError(t, err) + require.Equal(t, 1, n) + t.Logf("data (before) = %08b", b) + + b[0] ^= 0xff + t.Logf("data (after) = %08b", b) + + _, err = fCopy.WriteAt(b, pos) + require.NoError(t, err) + } + + // Write back to the file. + err = fCopy.Sync() + require.NoError(t, err) + + // Confirm that checksum validation fails. + err = r.ValidateBlockChecksums() + require.Error(t, err) + require.Regexp(t, `checksum mismatch`, err.Error()) + } + + for _, tc := range testCases { + // By default, test across all files, unless overridden. + files := tc.files + if files == nil { + files = allFiles + } + for _, file := range files { + t.Run(tc.name+" "+path.Base(file), func(t *testing.T) { + testFn(t, file, tc.corruptionLocations) + }) + } + } +} + +func TestReader_TableFormat(t *testing.T) { + test := func(t *testing.T, want TableFormat) { + fs := vfs.NewMem() + f, err := fs.Create("test") + require.NoError(t, err) + + opts := WriterOptions{TableFormat: want} + w := NewWriter(objstorageprovider.NewFileWritable(f), opts) + err = w.Close() + require.NoError(t, err) + + f, err = fs.Open("test") + require.NoError(t, err) + r, err := newReader(f, ReaderOptions{}) + require.NoError(t, err) + defer r.Close() + + got, err := r.TableFormat() + require.NoError(t, err) + require.Equal(t, want, got) + } + + for tf := TableFormatLevelDB; tf <= TableFormatMax; tf++ { + t.Run(tf.String(), func(t *testing.T) { + test(t, tf) + }) + } +} + +func buildTestTable( + t *testing.T, numEntries uint64, blockSize, indexBlockSize int, compression Compression, +) *Reader { + provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(vfs.NewMem(), "" /* dirName */)) + require.NoError(t, err) + defer provider.Close() + return buildTestTableWithProvider(t, provider, numEntries, blockSize, indexBlockSize, compression) +} + +func buildTestTableWithProvider( + t *testing.T, + provider objstorage.Provider, + numEntries uint64, + blockSize, indexBlockSize int, + compression Compression, +) *Reader { + f0, _, err := provider.Create(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.CreateOptions{}) + require.NoError(t, err) + + w := NewWriter(f0, WriterOptions{ + BlockSize: blockSize, + IndexBlockSize: indexBlockSize, + Compression: compression, + FilterPolicy: nil, + }) + + var ikey InternalKey + for i := uint64(0); i < numEntries; i++ { + key := make([]byte, 8+i%3) + value := make([]byte, i%100) + binary.BigEndian.PutUint64(key, i) + ikey.UserKey = key + w.Add(ikey, value) + } + + require.NoError(t, w.Close()) + + // Re-open that filename for reading. + f1, err := provider.OpenForReading(context.Background(), base.FileTypeTable, base.FileNum(0).DiskFileNum(), objstorage.OpenOptions{}) + require.NoError(t, err) + + c := cache.New(128 << 20) + defer c.Unref() + r, err := NewReader(f1, ReaderOptions{ + Cache: c, + }) + require.NoError(t, err) + return r +} + +func buildBenchmarkTable( + b *testing.B, options WriterOptions, confirmTwoLevelIndex bool, offset int, +) (*Reader, [][]byte) { + mem := vfs.NewMem() + f0, err := mem.Create("bench") + if err != nil { + b.Fatal(err) + } + + w := NewWriter(objstorageprovider.NewFileWritable(f0), options) + + var keys [][]byte + var ikey InternalKey + for i := uint64(0); i < 1e6; i++ { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, i+uint64(offset)) + keys = append(keys, key) + ikey.UserKey = key + w.Add(ikey, nil) + } + + if err := w.Close(); err != nil { + b.Fatal(err) + } + + // Re-open that filename for reading. + f1, err := mem.Open("bench") + if err != nil { + b.Fatal(err) + } + c := cache.New(128 << 20) + defer c.Unref() + r, err := newReader(f1, ReaderOptions{ + Cache: c, + }) + if err != nil { + b.Fatal(err) + } + if confirmTwoLevelIndex && r.Properties.IndexPartitions == 0 { + b.Fatalf("should have constructed two level index") + } + return r, keys +} + +var basicBenchmarks = []struct { + name string + options WriterOptions +}{ + { + name: "restart=16,compression=Snappy", + options: WriterOptions{ + BlockSize: 32 << 10, + BlockRestartInterval: 16, + FilterPolicy: nil, + Compression: SnappyCompression, + TableFormat: TableFormatPebblev2, + }, + }, + { + name: "restart=16,compression=ZSTD", + options: WriterOptions{ + BlockSize: 32 << 10, + BlockRestartInterval: 16, + FilterPolicy: nil, + Compression: ZstdCompression, + TableFormat: TableFormatPebblev2, + }, + }, +} + +func BenchmarkTableIterSeekGE(b *testing.B) { + for _, bm := range basicBenchmarks { + b.Run(bm.name, + func(b *testing.B) { + r, keys := buildBenchmarkTable(b, bm.options, false, 0) + it, err := r.NewIter(nil /* lower */, nil /* upper */) + require.NoError(b, err) + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + it.SeekGE(keys[rng.Intn(len(keys))], base.SeekGEFlagsNone) + } + + b.StopTimer() + it.Close() + r.Close() + }) + } +} + +func BenchmarkTableIterSeekLT(b *testing.B) { + for _, bm := range basicBenchmarks { + b.Run(bm.name, + func(b *testing.B) { + r, keys := buildBenchmarkTable(b, bm.options, false, 0) + it, err := r.NewIter(nil /* lower */, nil /* upper */) + require.NoError(b, err) + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + it.SeekLT(keys[rng.Intn(len(keys))], base.SeekLTFlagsNone) + } + + b.StopTimer() + it.Close() + r.Close() + }) + } +} + +func BenchmarkTableIterNext(b *testing.B) { + for _, bm := range basicBenchmarks { + b.Run(bm.name, + func(b *testing.B) { + r, _ := buildBenchmarkTable(b, bm.options, false, 0) + it, err := r.NewIter(nil /* lower */, nil /* upper */) + require.NoError(b, err) + + b.ResetTimer() + var sum int64 + var key *InternalKey + for i := 0; i < b.N; i++ { + if key == nil { + key, _ = it.First() + } + sum += int64(binary.BigEndian.Uint64(key.UserKey)) + key, _ = it.Next() + } + if testing.Verbose() { + fmt.Fprint(io.Discard, sum) + } + + b.StopTimer() + it.Close() + r.Close() + }) + } +} + +func BenchmarkTableIterPrev(b *testing.B) { + for _, bm := range basicBenchmarks { + b.Run(bm.name, + func(b *testing.B) { + r, _ := buildBenchmarkTable(b, bm.options, false, 0) + it, err := r.NewIter(nil /* lower */, nil /* upper */) + require.NoError(b, err) + + b.ResetTimer() + var sum int64 + var key *InternalKey + for i := 0; i < b.N; i++ { + if key == nil { + key, _ = it.Last() + } + sum += int64(binary.BigEndian.Uint64(key.UserKey)) + key, _ = it.Prev() + } + if testing.Verbose() { + fmt.Fprint(io.Discard, sum) + } + + b.StopTimer() + it.Close() + r.Close() + }) + } +} + +func BenchmarkLayout(b *testing.B) { + r, _ := buildBenchmarkTable(b, WriterOptions{}, false, 0) + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Layout() + } + b.StopTimer() + r.Close() +} + +func BenchmarkSeqSeekGEExhausted(b *testing.B) { + // Snappy with no bloom filter. + options := basicBenchmarks[0].options + + for _, twoLevelIndex := range []bool{false, true} { + switch twoLevelIndex { + case false: + options.IndexBlockSize = 0 + case true: + options.IndexBlockSize = 512 + } + const offsetCount = 5000 + reader, keys := buildBenchmarkTable(b, options, twoLevelIndex, offsetCount) + var preKeys [][]byte + for i := 0; i < offsetCount; i++ { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, uint64(i)) + preKeys = append(preKeys, key) + } + var postKeys [][]byte + for i := 0; i < offsetCount; i++ { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, uint64(i+offsetCount+len(keys))) + postKeys = append(postKeys, key) + } + for _, exhaustedBounds := range []bool{false, true} { + for _, prefixSeek := range []bool{false, true} { + exhausted := "file" + if exhaustedBounds { + exhausted = "bounds" + } + seekKind := "ge" + if prefixSeek { + seekKind = "prefix-ge" + } + b.Run(fmt.Sprintf( + "two-level=%t/exhausted=%s/seek=%s", twoLevelIndex, exhausted, seekKind), + func(b *testing.B) { + var upper []byte + var seekKeys [][]byte + if exhaustedBounds { + seekKeys = preKeys + upper = keys[0] + } else { + seekKeys = postKeys + } + it, err := reader.NewIter(nil /* lower */, upper) + require.NoError(b, err) + b.ResetTimer() + pos := 0 + var seekGEFlags SeekGEFlags + for i := 0; i < b.N; i++ { + seekKey := seekKeys[0] + var k *InternalKey + if prefixSeek { + k, _ = it.SeekPrefixGE(seekKey, seekKey, seekGEFlags) + } else { + k, _ = it.SeekGE(seekKey, seekGEFlags) + } + if k != nil { + b.Fatal("found a key") + } + if it.Error() != nil { + b.Fatalf("%s", it.Error().Error()) + } + pos++ + if pos == len(seekKeys) { + pos = 0 + seekGEFlags = seekGEFlags.DisableTrySeekUsingNext() + } else { + seekGEFlags = seekGEFlags.EnableTrySeekUsingNext() + } + } + b.StopTimer() + it.Close() + }) + } + } + reader.Close() + } +} + +func BenchmarkIteratorScanManyVersions(b *testing.B) { + options := WriterOptions{ + BlockSize: 32 << 10, + BlockRestartInterval: 16, + FilterPolicy: nil, + Compression: SnappyCompression, + Comparer: testkeys.Comparer, + } + // 10,000 key prefixes, each with 100 versions. + const keyCount = 10000 + const sharedPrefixLen = 32 + const unsharedPrefixLen = 8 + const versionCount = 100 + + // Take the very large keyspace consisting of alphabetic characters of + // lengths up to unsharedPrefixLen and reduce it down to keyCount keys by + // picking every 1 key every keyCount keys. + keys := testkeys.Alpha(unsharedPrefixLen) + keys = keys.EveryN(keys.Count() / keyCount) + if keys.Count() < keyCount { + b.Fatalf("expected %d keys, found %d", keyCount, keys.Count()) + } + keyBuf := make([]byte, sharedPrefixLen+unsharedPrefixLen+testkeys.MaxSuffixLen) + for i := 0; i < sharedPrefixLen; i++ { + keyBuf[i] = 'A' + byte(i) + } + // v2 sstable is 115,178,070 bytes. v3 sstable is 107,181,105 bytes with + // 99,049,269 bytes in value blocks. + setupBench := func(b *testing.B, tableFormat TableFormat, cacheSize int64) *Reader { + mem := vfs.NewMem() + f0, err := mem.Create("bench") + require.NoError(b, err) + options.TableFormat = tableFormat + w := NewWriter(objstorageprovider.NewFileWritable(f0), options) + val := make([]byte, 100) + rng := rand.New(rand.NewSource(100)) + for i := int64(0); i < keys.Count(); i++ { + for v := 0; v < versionCount; v++ { + n := testkeys.WriteKeyAt(keyBuf[sharedPrefixLen:], keys, i, int64(versionCount-v+1)) + key := keyBuf[:n+sharedPrefixLen] + rng.Read(val) + require.NoError(b, w.Set(key, val)) + } + } + require.NoError(b, w.Close()) + c := cache.New(cacheSize) + defer c.Unref() + // Re-open the filename for reading. + f0, err = mem.Open("bench") + require.NoError(b, err) + r, err := newReader(f0, ReaderOptions{ + Cache: c, + Comparer: testkeys.Comparer, + }) + require.NoError(b, err) + return r + } + for _, format := range []TableFormat{TableFormatPebblev2, TableFormatPebblev3} { + b.Run(fmt.Sprintf("format=%s", format.String()), func(b *testing.B) { + // 150MiB results in a high cache hit rate for both formats. 20MiB + // results in a high cache hit rate for the data blocks in + // TableFormatPebblev3. + for _, cacheSize := range []int64{20 << 20, 150 << 20} { + b.Run(fmt.Sprintf("cache-size=%s", humanize.Bytes.Int64(cacheSize)), + func(b *testing.B) { + r := setupBench(b, format, cacheSize) + defer func() { + require.NoError(b, r.Close()) + }() + for _, readValue := range []bool{false, true} { + b.Run(fmt.Sprintf("read-value=%t", readValue), func(b *testing.B) { + iter, err := r.NewIter(nil, nil) + require.NoError(b, err) + var k *InternalKey + var v base.LazyValue + var valBuf [100]byte + b.ResetTimer() + for i := 0; i < b.N; i++ { + if k == nil { + k, _ = iter.First() + if k == nil { + b.Fatalf("k is nil") + } + } + k, v = iter.Next() + if k != nil && readValue { + _, callerOwned, err := v.Value(valBuf[:]) + if err != nil { + b.Fatal(err) + } else if callerOwned { + b.Fatalf("unexpected callerOwned: %t", callerOwned) + } + } + } + }) + } + }) + } + }) + } +} + +func BenchmarkIteratorScanNextPrefix(b *testing.B) { + options := WriterOptions{ + BlockSize: 32 << 10, + BlockRestartInterval: 16, + FilterPolicy: nil, + Compression: SnappyCompression, + TableFormat: TableFormatPebblev3, + Comparer: testkeys.Comparer, + } + const keyCount = 10000 + const sharedPrefixLen = 32 + const unsharedPrefixLen = 8 + val := make([]byte, 100) + rand.New(rand.NewSource(100)).Read(val) + + // Take the very large keyspace consisting of alphabetic characters of + // lengths up to unsharedPrefixLen and reduce it down to keyCount keys by + // picking every 1 key every keyCount keys. + keys := testkeys.Alpha(unsharedPrefixLen) + keys = keys.EveryN(keys.Count() / keyCount) + if keys.Count() < keyCount { + b.Fatalf("expected %d keys, found %d", keyCount, keys.Count()) + } + keyBuf := make([]byte, sharedPrefixLen+unsharedPrefixLen+testkeys.MaxSuffixLen) + for i := 0; i < sharedPrefixLen; i++ { + keyBuf[i] = 'A' + byte(i) + } + setupBench := func(b *testing.B, versCount int) (r *Reader, succKeys [][]byte) { + mem := vfs.NewMem() + f0, err := mem.Create("bench") + require.NoError(b, err) + w := NewWriter(objstorageprovider.NewFileWritable(f0), options) + for i := int64(0); i < keys.Count(); i++ { + for v := 0; v < versCount; v++ { + n := testkeys.WriteKeyAt(keyBuf[sharedPrefixLen:], keys, i, int64(versCount-v+1)) + key := keyBuf[:n+sharedPrefixLen] + require.NoError(b, w.Set(key, val)) + if v == 0 { + prefixLen := testkeys.Comparer.Split(key) + prefixKey := key[:prefixLen] + succKey := testkeys.Comparer.ImmediateSuccessor(nil, prefixKey) + succKeys = append(succKeys, succKey) + } + } + } + require.NoError(b, w.Close()) + // NB: This 200MiB cache is sufficient for even the largest file: 10,000 + // keys * 100 versions = 1M keys, where each key-value pair is ~140 bytes + // = 140MB. So we are not measuring the caching benefit of + // TableFormatPebblev3 storing older values in value blocks. + c := cache.New(200 << 20) + defer c.Unref() + // Re-open the filename for reading. + f0, err = mem.Open("bench") + require.NoError(b, err) + r, err = newReader(f0, ReaderOptions{ + Cache: c, + Comparer: testkeys.Comparer, + }) + require.NoError(b, err) + return r, succKeys + } + // Analysis of some sample results with TableFormatPebblev2: + // versions=1/method=seek-ge-10 22107622 53.57 ns/op + // versions=1/method=next-prefix-10 36292837 33.07 ns/op + // versions=2/method=seek-ge-10 14429138 82.92 ns/op + // versions=2/method=next-prefix-10 19676055 60.78 ns/op + // versions=10/method=seek-ge-10 1453726 825.2 ns/op + // versions=10/method=next-prefix-10 2450498 489.6 ns/op + // versions=100/method=seek-ge-10 965143 1257 ns/op + // versions=100/method=next-prefix-10 1000000 1054 ns/op + // + // With 1 version, both SeekGE and NextPrefix will be able to complete after + // doing a single call to blockIter.Next. However, SeekGE has to do two key + // comparisons unlike the one key comparison in NextPrefix. This is because + // SeekGE also compares *before* calling Next since it is possible that the + // preceding SeekGE is already at the right place. + // + // With 2 versions, both will do two calls to blockIter.Next. The difference + // in the cost is the same as in the 1 version case. + // + // With 10 versions, it is still likely that the desired key is in the same + // data block. NextPrefix will seek only the blockIter. And in the rare case + // that the key is in the next data block, it will step the index block (not + // seek). In comparison, SeekGE will seek the index block too. + // + // With 100 versions we more often cross from one data block to the next, so + // the difference in cost declines. + // + // Some sample results with TableFormatPebblev3: + + // versions=1/method=seek-ge-10 18702609 53.90 ns/op + // versions=1/method=next-prefix-10 77440167 15.41 ns/op + // versions=2/method=seek-ge-10 13554286 87.91 ns/op + // versions=2/method=next-prefix-10 62148526 19.25 ns/op + // versions=10/method=seek-ge-10 1316676 910.5 ns/op + // versions=10/method=next-prefix-10 18829448 62.61 ns/op + // versions=100/method=seek-ge-10 1166139 1025 ns/op + // versions=100/method=next-prefix-10 4443386 265.3 ns/op + // + // NextPrefix is much cheaper than in TableFormatPebblev2 with larger number + // of versions. It is also cheaper with 1 and 2 versions since + // setHasSamePrefix=false eliminates a key comparison. + for _, versionCount := range []int{1, 2, 10, 100} { + b.Run(fmt.Sprintf("versions=%d", versionCount), func(b *testing.B) { + r, succKeys := setupBench(b, versionCount) + defer func() { + require.NoError(b, r.Close()) + }() + for _, method := range []string{"seek-ge", "next-prefix"} { + b.Run(fmt.Sprintf("method=%s", method), func(b *testing.B) { + for _, readValue := range []bool{false, true} { + b.Run(fmt.Sprintf("read-value=%t", readValue), func(b *testing.B) { + iter, err := r.NewIter(nil, nil) + require.NoError(b, err) + var nextFunc func(index int) (*InternalKey, base.LazyValue) + switch method { + case "seek-ge": + nextFunc = func(index int) (*InternalKey, base.LazyValue) { + var flags base.SeekGEFlags + return iter.SeekGE(succKeys[index], flags.EnableTrySeekUsingNext()) + } + case "next-prefix": + nextFunc = func(index int) (*InternalKey, base.LazyValue) { + return iter.NextPrefix(succKeys[index]) + } + default: + b.Fatalf("unknown method %s", method) + } + n := keys.Count() + j := n + var k *InternalKey + var v base.LazyValue + var valBuf [100]byte + b.ResetTimer() + for i := 0; i < b.N; i++ { + if k == nil { + if j != n { + b.Fatalf("unexpected %d != %d", j, n) + } + k, _ = iter.First() + j = 0 + } else { + k, v = nextFunc(int(j - 1)) + if k != nil && readValue { + _, callerOwned, err := v.Value(valBuf[:]) + if err != nil { + b.Fatal(err) + } else if callerOwned { + b.Fatalf("unexpected callerOwned: %t", callerOwned) + } + } + + } + if k != nil { + j++ + } + } + }) + } + }) + } + }) + } +} + +func BenchmarkIteratorScanObsolete(b *testing.B) { + options := WriterOptions{ + BlockSize: 32 << 10, + BlockRestartInterval: 16, + FilterPolicy: nil, + Compression: SnappyCompression, + Comparer: testkeys.Comparer, + } + const keyCount = 1 << 20 + const keyLen = 10 + + // Take the very large keyspace consisting of alphabetic characters of + // lengths up to unsharedPrefixLen and reduce it down to keyCount keys by + // picking every 1 key every keyCount keys. + keys := testkeys.Alpha(keyLen) + keys = keys.EveryN(keys.Count() / keyCount) + if keys.Count() < keyCount { + b.Fatalf("expected %d keys, found %d", keyCount, keys.Count()) + } + expectedKeyCount := keys.Count() + keyBuf := make([]byte, keyLen) + setupBench := func(b *testing.B, tableFormat TableFormat, cacheSize int64) *Reader { + mem := vfs.NewMem() + f0, err := mem.Create("bench") + require.NoError(b, err) + options.TableFormat = tableFormat + w := NewWriter(objstorageprovider.NewFileWritable(f0), options) + val := make([]byte, 100) + rng := rand.New(rand.NewSource(100)) + for i := int64(0); i < keys.Count(); i++ { + n := testkeys.WriteKey(keyBuf, keys, i) + key := keyBuf[:n] + rng.Read(val) + forceObsolete := true + if i == 0 { + forceObsolete = false + } + require.NoError(b, w.AddWithForceObsolete( + base.MakeInternalKey(key, 0, InternalKeyKindSet), val, forceObsolete)) + } + require.NoError(b, w.Close()) + c := cache.New(cacheSize) + defer c.Unref() + // Re-open the filename for reading. + f0, err = mem.Open("bench") + require.NoError(b, err) + r, err := newReader(f0, ReaderOptions{ + Cache: c, + Comparer: testkeys.Comparer, + }) + require.NoError(b, err) + return r + } + for _, format := range []TableFormat{TableFormatPebblev3, TableFormatPebblev4} { + b.Run(fmt.Sprintf("format=%s", format.String()), func(b *testing.B) { + // 150MiB results in a high cache hit rate for both formats. + for _, cacheSize := range []int64{1, 150 << 20} { + b.Run(fmt.Sprintf("cache-size=%s", humanize.Bytes.Int64(cacheSize)), + func(b *testing.B) { + r := setupBench(b, format, cacheSize) + defer func() { + require.NoError(b, r.Close()) + }() + for _, hideObsoletePoints := range []bool{false, true} { + b.Run(fmt.Sprintf("hide-obsolete=%t", hideObsoletePoints), func(b *testing.B) { + var filterer *BlockPropertiesFilterer + if format == TableFormatPebblev4 && hideObsoletePoints { + filterer = newBlockPropertiesFilterer( + []BlockPropertyFilter{obsoleteKeyBlockPropertyFilter{}}, nil) + intersects, err := + filterer.intersectsUserPropsAndFinishInit(r.Properties.UserProperties) + if err != nil { + b.Fatalf("%s", err.Error()) + } + if !intersects { + b.Fatalf("sstable does not intersect") + } + } + iter, err := r.NewIterWithBlockPropertyFiltersAndContextEtc( + context.Background(), nil, nil, filterer, hideObsoletePoints, + true, nil, CategoryAndQoS{}, nil, + TrivialReaderProvider{Reader: r}) + require.NoError(b, err) + b.ResetTimer() + for i := 0; i < b.N; i++ { + count := int64(0) + k, _ := iter.First() + for k != nil { + count++ + k, _ = iter.Next() + } + if format == TableFormatPebblev4 && hideObsoletePoints { + if count != 1 { + b.Fatalf("found %d points", count) + } + } else { + if count != expectedKeyCount { + b.Fatalf("found %d points", count) + } + } + } + }) + } + }) + } + }) + } +} + +func newReader(r ReadableFile, o ReaderOptions, extraOpts ...ReaderOption) (*Reader, error) { + readable, err := NewSimpleReadable(r) + if err != nil { + return nil, err + } + return NewReader(readable, o, extraOpts...) +} diff --git a/pebble/sstable/reader_virtual.go b/pebble/sstable/reader_virtual.go new file mode 100644 index 0000000..4f05e39 --- /dev/null +++ b/pebble/sstable/reader_virtual.go @@ -0,0 +1,213 @@ +// 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, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, + bufferPool *BufferPool, +) (Iterator, error) { + return v.reader.newCompactionIter( + bytesIterated, categoryAndQoS, statsCollector, 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, + categoryAndQoS CategoryAndQoS, + statsCollector *CategoryStatsCollector, + rp ReaderProvider, +) (Iterator, error) { + return v.reader.newIterWithBlockPropertyFiltersAndContext( + ctx, lower, upper, filterer, hideObsoletePoints, useFilterBlock, stats, + categoryAndQoS, statsCollector, 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/pebble/sstable/suffix_rewriter.go b/pebble/sstable/suffix_rewriter.go new file mode 100644 index 0000000..9672ded --- /dev/null +++ b/pebble/sstable/suffix_rewriter.go @@ -0,0 +1,589 @@ +package sstable + +import ( + "bytes" + "context" + "math" + "sync" + + "github.com/cespare/xxhash/v2" + "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/rangekey" + "github.com/cockroachdb/pebble/objstorage" +) + +// RewriteKeySuffixes is deprecated. +// +// TODO(sumeer): remove after switching CockroachDB to RewriteKeySuffixesAndReturnFormat. +func RewriteKeySuffixes( + sst []byte, + rOpts ReaderOptions, + out objstorage.Writable, + o WriterOptions, + from, to []byte, + concurrency int, +) (*WriterMetadata, error) { + meta, _, err := RewriteKeySuffixesAndReturnFormat(sst, rOpts, out, o, from, to, concurrency) + return meta, err +} + +// RewriteKeySuffixesAndReturnFormat copies the content of the passed SSTable +// bytes to a new sstable, written to `out`, in which the suffix `from` has is +// replaced with `to` in every key. The input sstable must consist of only +// Sets or RangeKeySets and every key must have `from` as its suffix as +// determined by the Split function of the Comparer in the passed +// WriterOptions. Range deletes must not exist in this sstable, as they will +// be ignored. +// +// Data blocks are rewritten in parallel by `concurrency` workers and then +// assembled into a final SST. Filters are copied from the original SST without +// 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. +// +// The WriterOptions.TableFormat is ignored, and the output sstable has the +// 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, + out objstorage.Writable, + o WriterOptions, + from, to []byte, + concurrency int, +) (*WriterMetadata, TableFormat, error) { + r, err := NewMemReader(sst, rOpts) + if err != nil { + return nil, TableFormatUnspecified, err + } + defer r.Close() + return rewriteKeySuffixesInBlocks(r, out, o, from, to, concurrency) +} + +func rewriteKeySuffixesInBlocks( + r *Reader, out objstorage.Writable, o WriterOptions, from, to []byte, concurrency int, +) (*WriterMetadata, TableFormat, error) { + if o.Comparer == nil || o.Comparer.Split == nil { + return nil, TableFormatUnspecified, + errors.New("a valid splitter is required to rewrite suffixes") + } + if concurrency < 1 { + return nil, TableFormatUnspecified, errors.New("concurrency must be >= 1") + } + // Even though NumValueBlocks = 0 => NumValuesInValueBlocks = 0, check both + // as a defensive measure. + if r.Properties.NumValueBlocks > 0 || r.Properties.NumValuesInValueBlocks > 0 { + return nil, TableFormatUnspecified, + errors.New("sstable with a single suffix should not have value blocks") + } + + tableFormat := r.tableFormat + o.TableFormat = tableFormat + w := NewWriter(out, o) + defer func() { + if w != nil { + w.Close() + } + }() + + for _, c := range w.propCollectors { + if _, ok := c.(SuffixReplaceableTableCollector); !ok { + return nil, TableFormatUnspecified, + errors.Errorf("property collector %s does not support suffix replacement", c.Name()) + } + } + for _, c := range w.blockPropCollectors { + if _, ok := c.(SuffixReplaceableBlockCollector); !ok { + return nil, TableFormatUnspecified, + errors.Errorf("block property collector %s does not support suffix replacement", c.Name()) + } + } + + l, err := r.Layout() + if err != nil { + return nil, TableFormatUnspecified, errors.Wrap(err, "reading layout") + } + + if err := rewriteDataBlocksToWriter(r, w, l.Data, from, to, w.split, concurrency); err != nil { + return nil, TableFormatUnspecified, errors.Wrap(err, "rewriting data blocks") + } + + // Copy over the range key block and replace suffixes in it if it exists. + if err := rewriteRangeKeyBlockToWriter(r, w, from, to); err != nil { + return nil, TableFormatUnspecified, errors.Wrap(err, "rewriting range key blocks") + } + + // Copy over the filter block if it exists (rewriteDataBlocksToWriter will + // already have ensured this is valid if it exists). + if w.filter != nil && l.Filter.Length > 0 { + filterBlock, _, err := readBlockBuf(r, l.Filter, nil) + if err != nil { + return nil, TableFormatUnspecified, errors.Wrap(err, "reading filter") + } + w.filter = copyFilterWriter{ + origPolicyName: w.filter.policyName(), origMetaName: w.filter.metaName(), data: filterBlock, + } + } + + if err := w.Close(); err != nil { + w = nil + return nil, TableFormatUnspecified, err + } + writerMeta, err := w.Metadata() + w = nil + return writerMeta, tableFormat, err +} + +var errBadKind = errors.New("key does not have expected kind (set)") + +type blockWithSpan struct { + start, end InternalKey + data []byte +} + +func rewriteBlocks( + r *Reader, + restartInterval int, + checksumType ChecksumType, + compression Compression, + input []BlockHandleWithProperties, + output []blockWithSpan, + totalWorkers, worker int, + from, to []byte, + split Split, +) error { + bw := blockWriter{ + restartInterval: restartInterval, + } + buf := blockBuf{checksummer: checksummer{checksumType: checksumType}} + if checksumType == ChecksumTypeXXHash { + buf.checksummer.xxHasher = xxhash.New() + } + + var blockAlloc bytealloc.A + var keyAlloc bytealloc.A + var scratch InternalKey + + var inputBlock, inputBlockBuf []byte + + iter := &blockIter{} + + // We'll assume all blocks are _roughly_ equal so round-robin static partition + // of each worker doing every ith block is probably enough. + for i := worker; i < len(input); i += totalWorkers { + bh := input[i] + + var err error + inputBlock, inputBlockBuf, err = readBlockBuf(r, bh.BlockHandle, inputBlockBuf) + if err != nil { + return err + } + if err := iter.init(r.Compare, inputBlock, r.Properties.GlobalSeqNum, false); err != nil { + return err + } + + if cap(bw.restarts) < int(iter.restarts) { + bw.restarts = make([]uint32, 0, iter.restarts) + } + if cap(bw.buf) == 0 { + bw.buf = make([]byte, 0, len(inputBlock)) + } + if cap(bw.restarts) < int(iter.numRestarts) { + bw.restarts = make([]uint32, 0, iter.numRestarts) + } + + for key, val := iter.First(); key != nil; key, val = iter.Next() { + if key.Kind() != InternalKeyKindSet { + return errBadKind + } + si := split(key.UserKey) + oldSuffix := key.UserKey[si:] + if !bytes.Equal(oldSuffix, from) { + err := errors.Errorf("key has suffix %q, expected %q", oldSuffix, from) + return err + } + newLen := si + len(to) + if cap(scratch.UserKey) < newLen { + scratch.UserKey = make([]byte, 0, len(key.UserKey)*2+len(to)-len(from)) + } + + scratch.Trailer = key.Trailer + scratch.UserKey = scratch.UserKey[:newLen] + copy(scratch.UserKey, key.UserKey[:si]) + copy(scratch.UserKey[si:], to) + + // 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 && + key.Kind() == InternalKeyKindSet { + if len(v) < 1 { + return errors.Errorf("value has no prefix") + } + prefix := valuePrefix(v[0]) + if isValueHandle(prefix) { + return errors.Errorf("value prefix is incorrect") + } + if setHasSamePrefix(prefix) { + return errors.Errorf("multiple keys with same key prefix") + } + } + bw.add(scratch, v) + if output[i].start.UserKey == nil { + keyAlloc, output[i].start = cloneKeyWithBuf(scratch, keyAlloc) + } + } + *iter = iter.resetForReuse() + + keyAlloc, output[i].end = cloneKeyWithBuf(scratch, keyAlloc) + + finished := compressAndChecksum(bw.finish(), compression, &buf) + + // copy our finished block into the output buffer. + blockAlloc, output[i].data = blockAlloc.Alloc(len(finished) + blockTrailerLen) + copy(output[i].data, finished) + copy(output[i].data[len(finished):], buf.tmp[:blockTrailerLen]) + } + return nil +} + +func rewriteDataBlocksToWriter( + r *Reader, + w *Writer, + data []BlockHandleWithProperties, + from, to []byte, + split Split, + concurrency int, +) error { + if r.Properties.NumEntries == 0 { + // No point keys. + return nil + } + blocks := make([]blockWithSpan, len(data)) + + if w.filter != nil { + if r.Properties.FilterPolicyName != w.filter.policyName() { + return errors.New("mismatched filters") + } + if was, is := r.Properties.ComparerName, w.props.ComparerName; was != is { + return errors.Errorf("mismatched Comparer %s vs %s, replacement requires same splitter to copy filters", was, is) + } + } + + g := &sync.WaitGroup{} + g.Add(concurrency) + errCh := make(chan error, concurrency) + for i := 0; i < concurrency; i++ { + worker := i + go func() { + defer g.Done() + err := rewriteBlocks( + r, + w.dataBlockBuf.dataBlock.restartInterval, + w.blockBuf.checksummer.checksumType, + w.compression, + data, + blocks, + concurrency, + worker, + from, to, + split, + ) + if err != nil { + errCh <- err + } + }() + } + g.Wait() + close(errCh) + if err, ok := <-errCh; ok { + return err + } + + for _, p := range w.propCollectors { + if err := p.(SuffixReplaceableTableCollector).UpdateKeySuffixes(r.Properties.UserProperties, from, to); err != nil { + return err + } + } + + var decoder blockPropertiesDecoder + var oldShortIDs []shortID + var oldProps [][]byte + if len(w.blockPropCollectors) > 0 { + oldProps = make([][]byte, len(w.blockPropCollectors)) + oldShortIDs = make([]shortID, math.MaxUint8) + for i, p := range w.blockPropCollectors { + if prop, ok := r.Properties.UserProperties[p.Name()]; ok { + was, is := shortID(byte(prop[0])), shortID(i) + oldShortIDs[was] = is + } + } + } + + for i := range blocks { + // Write the rewritten block to the file. + if err := w.writable.Write(blocks[i].data); err != nil { + return err + } + + n := len(blocks[i].data) + bh := BlockHandle{Offset: w.meta.Size, Length: uint64(n) - blockTrailerLen} + // Update the overall size. + w.meta.Size += uint64(n) + + // Load any previous values for our prop collectors into oldProps. + for i := range oldProps { + oldProps[i] = nil + } + decoder.props = data[i].Props + for !decoder.done() { + id, val, err := decoder.next() + if err != nil { + return err + } + oldProps[oldShortIDs[id]] = val + } + + for i, p := range w.blockPropCollectors { + if err := p.(SuffixReplaceableBlockCollector).UpdateKeySuffixes(oldProps[i], from, to); err != nil { + return err + } + } + + bhp, err := w.maybeAddBlockPropertiesToBlockHandle(bh) + if err != nil { + return err + } + var nextKey InternalKey + if i+1 < len(blocks) { + nextKey = blocks[i+1].start + } + if err = w.addIndexEntrySync(blocks[i].end, nextKey, bhp, w.dataBlockBuf.tmp[:]); err != nil { + return err + } + } + + w.meta.updateSeqNum(blocks[0].start.SeqNum()) + w.props.NumEntries = r.Properties.NumEntries + w.props.RawKeySize = r.Properties.RawKeySize + w.props.RawValueSize = r.Properties.RawValueSize + w.meta.SetSmallestPointKey(blocks[0].start) + w.meta.SetLargestPointKey(blocks[len(blocks)-1].end) + return nil +} + +func rewriteRangeKeyBlockToWriter(r *Reader, w *Writer, from, to []byte) error { + iter, err := r.NewRawRangeKeyIter() + if err != nil { + return err + } + if iter == nil { + // No range keys. + return nil + } + defer iter.Close() + + for s := iter.First(); s != nil; s = iter.Next() { + if !s.Valid() { + break + } + for i := range s.Keys { + if s.Keys[i].Kind() != base.InternalKeyKindRangeKeySet { + return errBadKind + } + if !bytes.Equal(s.Keys[i].Suffix, from) { + return errors.Errorf("key has suffix %q, expected %q", s.Keys[i].Suffix, from) + } + s.Keys[i].Suffix = to + } + + err := rangekey.Encode(s, func(k base.InternalKey, v []byte) error { + // Calling AddRangeKey instead of addRangeKeySpan bypasses the fragmenter. + // This is okay because the raw fragments off of `iter` are already + // fragmented, and suffix replacement should not affect fragmentation. + return w.AddRangeKey(k, v) + }) + if err != nil { + return err + } + } + + return nil +} + +type copyFilterWriter struct { + origMetaName string + origPolicyName string + data []byte +} + +func (copyFilterWriter) addKey(key []byte) { panic("unimplemented") } +func (c copyFilterWriter) finish() ([]byte, error) { return c.data, nil } +func (c copyFilterWriter) metaName() string { return c.origMetaName } +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. +// +// 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) { + if o.Comparer == nil || o.Comparer.Split == nil { + return nil, errors.New("a valid splitter is required to rewrite suffixes") + } + + o.IsStrictObsolete = false + w := NewWriter(out, o) + defer func() { + if w != nil { + w.Close() + } + }() + i, err := r.NewIter(nil, nil) + if err != nil { + return nil, err + } + defer i.Close() + + k, v := i.First() + var scratch InternalKey + for k != nil { + if k.Kind() != InternalKeyKindSet { + return nil, errors.New("invalid key type") + } + oldSuffix := k.UserKey[r.Split(k.UserKey):] + if !bytes.Equal(oldSuffix, from) { + return nil, errors.Errorf("key has suffix %q, expected %q", oldSuffix, from) + } + scratch.UserKey = append(scratch.UserKey[:0], k.UserKey[:len(k.UserKey)-len(from)]...) + scratch.UserKey = append(scratch.UserKey, to...) + scratch.Trailer = k.Trailer + + val, _, err := v.Value(nil) + if err != nil { + return nil, err + } + if w.addPoint(scratch, val, false); err != nil { + return nil, err + } + k, v = i.Next() + } + if err := rewriteRangeKeyBlockToWriter(r, w, from, to); err != nil { + return nil, err + } + if err := w.Close(); err != nil { + w = nil + return nil, err + } + writerMeta, err := w.Metadata() + w = nil + return writerMeta, err +} + +// NewMemReader opens a reader over the SST stored in the passed []byte. +func NewMemReader(sst []byte, o ReaderOptions) (*Reader, error) { + return NewReader(newMemReader(sst), o) +} + +func readBlockBuf(r *Reader, bh BlockHandle, buf []byte) ([]byte, []byte, error) { + raw := r.readable.(*memReader).b[bh.Offset : bh.Offset+bh.Length+blockTrailerLen] + if err := checkChecksum(r.checksumType, raw, bh, 0); err != nil { + return nil, buf, err + } + typ := blockType(raw[bh.Length]) + raw = raw[:bh.Length] + if typ == noCompressionBlockType { + return raw, buf, nil + } + decompressedLen, prefix, err := decompressedLen(typ, raw) + if err != nil { + return nil, buf, err + } + if cap(buf) < decompressedLen { + buf = make([]byte, decompressedLen) + } + res, err := decompressInto(typ, raw[prefix:], buf[:decompressedLen]) + return res, buf, err +} + +// 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 { + b []byte + r *bytes.Reader + rh objstorage.NoopReadHandle +} + +var _ objstorage.Readable = (*memReader)(nil) + +func newMemReader(b []byte) *memReader { + r := &memReader{ + b: b, + r: bytes.NewReader(b), + } + r.rh = objstorage.MakeNoopReadHandle(r) + return r +} + +// 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 is part of objstorage.Readable. +func (*memReader) Close() error { + return nil +} + +// Stat is part of objstorage.Readable. +func (m *memReader) Size() int64 { + return int64(len(m.b)) +} + +// NewReadHandle is part of objstorage.Readable. +func (m *memReader) NewReadHandle(_ context.Context) objstorage.ReadHandle { + return &m.rh +} diff --git a/pebble/sstable/suffix_rewriter_test.go b/pebble/sstable/suffix_rewriter_test.go new file mode 100644 index 0000000..897e0dd --- /dev/null +++ b/pebble/sstable/suffix_rewriter_test.go @@ -0,0 +1,278 @@ +package sstable + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/rand" + "strconv" + "testing" + + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/stretchr/testify/require" +) + +func TestRewriteSuffixProps(t *testing.T) { + from, to := []byte("_212"), []byte("_646") + for format := TableFormatPebblev2; format <= TableFormatMax; format++ { + t.Run(format.String(), func(t *testing.T) { + wOpts := WriterOptions{ + FilterPolicy: bloom.FilterPolicy(10), + Comparer: test4bSuffixComparer, + TablePropertyCollectors: []func() TablePropertyCollector{ + intSuffixTablePropCollectorFn("ts3", 3), intSuffixTablePropCollectorFn("ts2", 2), + }, + BlockPropertyCollectors: []func() BlockPropertyCollector{ + keyCountCollectorFn("count"), + intSuffixIntervalCollectorFn("bp3", 3), + intSuffixIntervalCollectorFn("bp2", 2), + intSuffixIntervalCollectorFn("bp1", 1), + }, + TableFormat: format, + } + if format >= TableFormatPebblev4 { + wOpts.IsStrictObsolete = true + } + + const keyCount = 1e5 + const rangeKeyCount = 100 + // Setup our test SST. + sst := make4bSuffixTestSST(t, wOpts, []byte(from), keyCount, rangeKeyCount) + + expectedProps := make(map[string]string) + expectedProps["ts2.min"] = "46" + expectedProps["ts2.max"] = "46" + expectedProps["ts3.min"] = "646" + expectedProps["ts3.max"] = "646" + + // Also expect to see the aggregated block properties with their updated value + // at the correct (new) shortIDs. Seeing the rolled up value here is almost an + // end-to-end test since we only fed them each block during rewrite. + expectedProps["count"] = string(append([]byte{1}, strconv.Itoa(keyCount+rangeKeyCount)...)) + expectedProps["bp2"] = string(interval{46, 47}.encode([]byte{2})) + expectedProps["bp3"] = string(interval{646, 647}.encode([]byte{0})) + + // Swap the order of two of the props so they have new shortIDs, and remove + // one. rwOpts inherits the IsStrictObsolete value from wOpts. + rwOpts := wOpts + if rand.Intn(2) != 0 { + rwOpts.TableFormat = TableFormatPebblev2 + rwOpts.IsStrictObsolete = false + t.Log("table format set to TableFormatPebblev2") + } + fmt.Printf("from format %s, to format %s\n", format.String(), rwOpts.TableFormat.String()) + rwOpts.BlockPropertyCollectors = rwOpts.BlockPropertyCollectors[:3] + rwOpts.BlockPropertyCollectors[0], rwOpts.BlockPropertyCollectors[1] = rwOpts.BlockPropertyCollectors[1], rwOpts.BlockPropertyCollectors[0] + + // Rewrite the SST using updated options and check the returned props. + readerOpts := ReaderOptions{ + Comparer: test4bSuffixComparer, + Filters: map[string]base.FilterPolicy{wOpts.FilterPolicy.Name(): wOpts.FilterPolicy}, + } + r, err := NewMemReader(sst, readerOpts) + require.NoError(t, err) + defer r.Close() + + var sstBytes [2][]byte + adjustPropsForEffectiveFormat := func(effectiveFormat TableFormat) { + if effectiveFormat == TableFormatPebblev4 { + expectedProps["obsolete-key"] = string([]byte{3}) + } else { + delete(expectedProps, "obsolete-key") + } + } + for i, byBlocks := range []bool{false, true} { + t.Run(fmt.Sprintf("byBlocks=%v", byBlocks), func(t *testing.T) { + rewrittenSST := &memFile{} + if byBlocks { + _, rewriteFormat, err := rewriteKeySuffixesInBlocks( + r, rewrittenSST, rwOpts, from, to, 8) + // rewriteFormat is equal to the original format, since + // rwOpts.TableFormat is ignored. + require.Equal(t, wOpts.TableFormat, rewriteFormat) + require.NoError(t, err) + adjustPropsForEffectiveFormat(rewriteFormat) + } else { + _, err := RewriteKeySuffixesViaWriter(r, rewrittenSST, rwOpts, from, to) + require.NoError(t, err) + adjustPropsForEffectiveFormat(rwOpts.TableFormat) + } + + sstBytes[i] = rewrittenSST.Data() + // Check that a reader on the rewritten STT has the expected props. + rRewritten, err := NewMemReader(rewrittenSST.Data(), readerOpts) + require.NoError(t, err) + defer rRewritten.Close() + require.Equal(t, expectedProps, rRewritten.Properties.UserProperties) + require.False(t, rRewritten.Properties.IsStrictObsolete) + + // Compare the block level props from the data blocks in the layout, + // only if we did not do a rewrite from one format to another. If the + // format changes, the block boundaries change slightly. + if !byBlocks && wOpts.TableFormat != rwOpts.TableFormat { + return + } + layout, err := r.Layout() + require.NoError(t, err) + newLayout, err := rRewritten.Layout() + require.NoError(t, err) + + ival := interval{} + for i := range layout.Data { + oldProps := make([][]byte, len(wOpts.BlockPropertyCollectors)) + oldDecoder := blockPropertiesDecoder{layout.Data[i].Props} + for !oldDecoder.done() { + id, val, err := oldDecoder.next() + require.NoError(t, err) + oldProps[id] = val + } + newProps := make([][]byte, len(rwOpts.BlockPropertyCollectors)) + newDecoder := blockPropertiesDecoder{newLayout.Data[i].Props} + for !newDecoder.done() { + id, val, err := newDecoder.next() + require.NoError(t, err) + if int(id) < len(newProps) { + newProps[id] = val + } + } + require.Equal(t, oldProps[0], newProps[1]) + ival.decode(newProps[0]) + require.Equal(t, interval{646, 647}, ival) + ival.decode(newProps[2]) + require.Equal(t, interval{46, 47}, ival) + } + }) + } + if wOpts.TableFormat == rwOpts.TableFormat { + // Both methods of rewriting should produce the same result. + require.Equal(t, sstBytes[0], sstBytes[1]) + } + }) + } +} + +// memFile is a file-like struct that buffers all data written to it in memory. +// Implements the objstorage.Writable interface. +type memFile struct { + buf bytes.Buffer +} + +var _ objstorage.Writable = (*memFile)(nil) + +// Finish is part of the objstorage.Writable interface. +func (*memFile) Finish() error { + return nil +} + +// Abort is part of the objstorage.Writable interface. +func (*memFile) Abort() {} + +// Write is part of the objstorage.Writable interface. +func (f *memFile) Write(p []byte) error { + _, err := f.buf.Write(p) + return err +} + +// Data returns the in-memory buffer behind this MemFile. +func (f *memFile) Data() []byte { + return f.buf.Bytes() +} + +func make4bSuffixTestSST( + t testing.TB, writerOpts WriterOptions, suffix []byte, keys int, rangeKeys int, +) []byte { + key := make([]byte, 28) + endKey := make([]byte, 24) + copy(key[24:], suffix) + + f := &memFile{} + w := NewWriter(f, writerOpts) + for i := 0; i < keys; i++ { + binary.BigEndian.PutUint64(key[:8], 123) // 16-byte shared prefix + binary.BigEndian.PutUint64(key[8:16], 456) + binary.BigEndian.PutUint64(key[16:], uint64(i)) + err := w.AddWithForceObsolete( + base.MakeInternalKey(key, 0, InternalKeyKindSet), key, false) + if err != nil { + t.Fatal(err) + } + } + for i := 0; i < rangeKeys; i++ { + binary.BigEndian.PutUint64(key[:8], 123) // 16-byte shared prefix + binary.BigEndian.PutUint64(key[8:16], 456) + binary.BigEndian.PutUint64(key[16:], uint64(i)) + binary.BigEndian.PutUint64(endKey[:8], 123) // 16-byte shared prefix + binary.BigEndian.PutUint64(endKey[8:16], 456) + binary.BigEndian.PutUint64(endKey[16:], uint64(i+1)) + if err := w.RangeKeySet(key[:24], endKey[:24], suffix, key); err != nil { + t.Fatal(err) + } + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + return f.buf.Bytes() +} + +func BenchmarkRewriteSST(b *testing.B) { + from, to := []byte("_123"), []byte("_456") + writerOpts := WriterOptions{ + FilterPolicy: bloom.FilterPolicy(10), + Comparer: test4bSuffixComparer, + TableFormat: TableFormatPebblev2, + } + + sizes := []int{100, 10000, 1e6} + compressions := []Compression{NoCompression, SnappyCompression} + + files := make([][]*Reader, len(compressions)) + + for comp := range compressions { + files[comp] = make([]*Reader, len(sizes)) + + for size := range sizes { + writerOpts.Compression = compressions[comp] + sst := make4bSuffixTestSST(b, writerOpts, from, sizes[size], 0 /* rangeKeys */) + r, err := NewMemReader(sst, ReaderOptions{ + Comparer: test4bSuffixComparer, + Filters: map[string]base.FilterPolicy{writerOpts.FilterPolicy.Name(): writerOpts.FilterPolicy}, + }) + if err != nil { + b.Fatal(err) + } + files[comp][size] = r + } + } + + b.ResetTimer() + for comp := range compressions { + b.Run(compressions[comp].String(), func(b *testing.B) { + for sz := range sizes { + r := files[comp][sz] + b.Run(fmt.Sprintf("keys=%d", sizes[sz]), func(b *testing.B) { + b.Run("ReaderWriterLoop", func(b *testing.B) { + b.SetBytes(r.readable.Size()) + for i := 0; i < b.N; i++ { + if _, err := RewriteKeySuffixesViaWriter(r, &discardFile{}, writerOpts, from, to); err != nil { + b.Fatal(err) + } + } + }) + for _, concurrency := range []int{1, 2, 4, 8, 16} { + b.Run(fmt.Sprintf("RewriteKeySuffixes,concurrency=%d", concurrency), func(b *testing.B) { + b.SetBytes(r.readable.Size()) + for i := 0; i < b.N; i++ { + if _, _, err := rewriteKeySuffixesInBlocks(r, &discardFile{}, writerOpts, []byte("_123"), []byte("_456"), concurrency); err != nil { + b.Fatal(err) + } + } + }) + } + }) + } + }) + } +} diff --git a/pebble/sstable/table.go b/pebble/sstable/table.go new file mode 100644 index 0000000..040efaf --- /dev/null +++ b/pebble/sstable/table.go @@ -0,0 +1,455 @@ +// 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 implements readers and writers of pebble tables. +// +// Tables are either opened for reading or created for writing but not both. +// +// A reader can create iterators, which allow seeking and next/prev +// iteration. There may be multiple key/value pairs that have the same key and +// different sequence numbers. +// +// A reader can be used concurrently. Multiple goroutines can call NewIter +// concurrently, and each iterator can run concurrently with other iterators. +// However, any particular iterator should not be used concurrently, and iterators +// should not be used once a reader is closed. +// +// A writer writes key/value pairs in increasing key order, and cannot be used +// concurrently. A table cannot be read until the writer has finished. +// +// Readers and writers can be created with various options. Passing a nil +// Options pointer is valid and means to use the default values. +// +// One such option is to define the 'less than' ordering for keys. The default +// Comparer uses the natural ordering consistent with bytes.Compare. The same +// ordering should be used for reading and writing a table. +// +// To return the value for a key: +// +// r := table.NewReader(file, options) +// defer r.Close() +// i := r.NewIter(nil, nil) +// defer i.Close() +// ikey, value := r.SeekGE(key) +// if options.Comparer.Compare(ikey.UserKey, key) != 0 { +// // not found +// } else { +// // value is the first record containing key +// } +// +// To count the number of entries in a table: +// +// i, n := r.NewIter(nil, nil), 0 +// for key, value := i.First(); key != nil; key, value = i.Next() { +// n++ +// } +// if err := i.Close(); err != nil { +// return 0, err +// } +// return n, nil +// +// To write a table with three entries: +// +// w := table.NewWriter(file, options) +// if err := w.Set([]byte("apple"), []byte("red")); err != nil { +// w.Close() +// return err +// } +// if err := w.Set([]byte("banana"), []byte("yellow")); err != nil { +// w.Close() +// return err +// } +// if err := w.Set([]byte("cherry"), []byte("red")); err != nil { +// w.Close() +// return err +// } +// return w.Close() +package sstable // import "github.com/cockroachdb/pebble/sstable" + +import ( + "context" + "encoding/binary" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" +) + +/* +The table file format looks like: + + +[data block 0] +[data block 1] +... +[data block N-1] +[meta filter block] (optional) +[index block] (for single level index) +[meta rangedel block] (optional) +[meta range key block] (optional) +[value block 0] (optional) +[value block M-1] (optional) +[meta value index block] (optional) +[meta properties block] +[metaindex block] +[footer] + + +A Reader eagerly loads the footer, metaindex block and meta properties block, +because the data contained in those blocks is needed on every read, and even +before reading. For example, the meta properties block is used to verify the +comparer and merger are compatible, and the metaindex block contains the +location of the meta properties (and other meta blocks). In situations where +file system locality matters, or one wants to minimize number of read +requests when eagerly loading these blocks, having these three as a suffix +of the file is convenient. + +The interleaving of the index block(s) between the meta blocks is done to +match RocksDB/LevelDB behavior. + +Each block consists of some data and a 5 byte trailer: a 1 byte block type and a +4 byte checksum. The checksum is computed over the compressed data and the first +byte of the trailer (i.e. the block type), and is serialized as little-endian. +The block type gives the per-block compression used; each block is compressed +independently. The checksum algorithm is described in the pebble/crc package. + +Most blocks, other than the meta filter block, value blocks and meta value +index block, contain key/value pairs. The remainder of this comment refers to +the decompressed block, containing key/value pairs, which has its 5 byte +trailer stripped. The decompressed block data consists of a sequence of such +key/value entries followed by a block suffix. Each key is encoded as a shared +prefix length and a remainder string. For example, if two adjacent keys are +"tweedledee" and "tweedledum", then the second key would be encoded as {8, +"um"}. The shared prefix length is varint encoded. The remainder string and the +value are encoded as a varint-encoded length followed by the literal contents. +To continue the example, suppose that the key "tweedledum" mapped to the value +"socks". The encoded key/value entry would be: "\x08\x02\x05umsocks". + +Every block has a restart interval I. Every I'th key/value entry in that block +is called a restart point, and shares no key prefix with the previous entry. +Continuing the example above, if the key after "tweedledum" was "two", but was +part of a restart point, then that key would be encoded as {0, "two"} instead +of {2, "o"}. If a block has P restart points, then the block suffix consists +of (P+1)*4 bytes: (P+1) little-endian uint32 values. The first P of these +uint32 values are the block offsets of each restart point. The final uint32 +value is P itself. Thus, when seeking for a particular key, one can use binary +search to find the largest restart point whose key is <= the key sought. + +An index block is a block with N key/value entries. The i'th value is the +encoded block handle of the i'th data block. The i'th key is a separator for +i < N-1, and a successor for i == N-1. The separator between blocks i and i+1 +is a key that is >= every key in block i and is < every key i block i+1. The +successor for the final block is a key that is >= every key in block N-1. The +index block restart interval is 1: every entry is a restart point. + +A block handle is an offset, a length, and optional block properties (for data +blocks and first/lower level index blocks); the length does not include the 5 +byte trailer. All numbers are varint-encoded, with no padding between the two +values. The maximum size of an encoded block handle without properties is 20 +bytes. It is not advised to have properties that accumulate to be longer than +100 bytes. + +Instead of a single index block, the sstable can have a two-level index (this +is used to prevent a single huge index block). A two-level index consists of a +sequence of lower-level index blocks with block handles for data blocks +followed by a single top-level index block with block handles for the +lower-level index blocks. + +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. + +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 ( + blockTrailerLen = 5 + blockHandleMaxLenWithoutProperties = 10 + 10 + // blockHandleLikelyMaxLen can be used for pre-allocating buffers to + // reduce memory copies. It is not guaranteed that a block handle will not + // exceed this length. + blockHandleLikelyMaxLen = blockHandleMaxLenWithoutProperties + 100 + + levelDBFooterLen = 48 + levelDBMagic = "\x57\xfb\x80\x8b\x24\x75\x47\xdb" + levelDBMagicOffset = levelDBFooterLen - len(levelDBMagic) + + rocksDBFooterLen = 1 + 2*blockHandleMaxLenWithoutProperties + 4 + 8 + rocksDBMagic = "\xf7\xcf\xf4\x85\xb7\x41\xe2\x88" + rocksDBMagicOffset = rocksDBFooterLen - len(rocksDBMagic) + rocksDBVersionOffset = rocksDBMagicOffset - 4 + rocksDBExternalFormatVersion = 2 + + pebbleDBMagic = "\xf0\x9f\xaa\xb3\xf0\x9f\xaa\xb3" // 🪳🪳 + + minFooterLen = levelDBFooterLen + maxFooterLen = rocksDBFooterLen + + levelDBFormatVersion = 0 + rocksDBFormatVersion2 = 2 + + metaRangeKeyName = "pebble.range_key" + metaValueIndexName = "pebble.value_index" + metaPropertiesName = "rocksdb.properties" + metaRangeDelName = "rocksdb.range_del" + metaRangeDelV2Name = "rocksdb.range_del2" + + // Index Types. + // A space efficient index block that is optimized for binary-search-based + // index. + binarySearchIndex = 0 + // hashSearchIndex = 1 + // A two-level index implementation. Both levels are binary search indexes. + twoLevelIndex = 2 + // binarySearchWithFirstKeyIndex = 3 + + // RocksDB always includes this in the properties block. Since Pebble + // doesn't use zstd compression, the string will always be the same. + // This should be removed if we ever decide to diverge from the RocksDB + // properties block. + rocksDBCompressionOptions = "window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; " +) + +// ChecksumType specifies the checksum used for blocks. +type ChecksumType byte + +// The available checksum types. +const ( + ChecksumTypeNone ChecksumType = 0 + ChecksumTypeCRC32c ChecksumType = 1 + ChecksumTypeXXHash ChecksumType = 2 + ChecksumTypeXXHash64 ChecksumType = 3 +) + +// String implements fmt.Stringer. +func (t ChecksumType) String() string { + switch t { + case ChecksumTypeCRC32c: + return "crc32c" + case ChecksumTypeNone: + return "none" + case ChecksumTypeXXHash: + return "xxhash" + case ChecksumTypeXXHash64: + return "xxhash64" + default: + panic(errors.Newf("sstable: unknown checksum type: %d", t)) + } +} + +type blockType byte + +const ( + // The block type gives the per-block compression format. + // These constants are part of the file format and should not be changed. + // They are different from the Compression constants because the latter + // are designed so that the zero value of the Compression type means to + // use the default compression (which is snappy). + // Not all compression types listed here are supported. + noCompressionBlockType blockType = 0 + snappyCompressionBlockType blockType = 1 + zlibCompressionBlockType blockType = 2 + bzip2CompressionBlockType blockType = 3 + lz4CompressionBlockType blockType = 4 + lz4hcCompressionBlockType blockType = 5 + xpressCompressionBlockType blockType = 6 + zstdCompressionBlockType blockType = 7 +) + +// String implements fmt.Stringer. +func (t blockType) String() string { + switch t { + case 0: + return "none" + case 1: + return "snappy" + case 2: + return "zlib" + case 3: + return "bzip2" + case 4: + return "lz4" + case 5: + return "lz4hc" + case 6: + return "xpress" + case 7: + return "zstd" + default: + panic(errors.Newf("sstable: unknown block type: %d", t)) + } +} + +// legacy (LevelDB) footer format: +// +// metaindex handle (varint64 offset, varint64 size) +// index handle (varint64 offset, varint64 size) +// to make the total size 2 * BlockHandle::kMaxEncodedLength +// table_magic_number (8 bytes) +// +// new (RocksDB) footer format: +// +// checksum type (char, 1 byte) +// metaindex handle (varint64 offset, varint64 size) +// index handle (varint64 offset, varint64 size) +// to make the total size 2 * BlockHandle::kMaxEncodedLength + 1 +// footer version (4 bytes) +// table_magic_number (8 bytes) +type footer struct { + format TableFormat + checksum ChecksumType + metaindexBH BlockHandle + indexBH BlockHandle + footerBH BlockHandle +} + +func readFooter(f objstorage.Readable) (footer, error) { + var footer footer + size := f.Size() + if size < minFooterLen { + return footer, base.CorruptionErrorf("pebble/table: invalid table (file size is too small)") + } + + buf := make([]byte, maxFooterLen) + off := size - maxFooterLen + if off < 0 { + off = 0 + buf = buf[:size] + } + if err := f.ReadAt(context.TODO(), buf, off); err != nil { + return footer, errors.Wrap(err, "pebble/table: invalid table (could not read footer)") + } + + switch magic := buf[len(buf)-len(rocksDBMagic):]; string(magic) { + case levelDBMagic: + if len(buf) < levelDBFooterLen { + return footer, base.CorruptionErrorf( + "pebble/table: invalid table (footer too short): %d", errors.Safe(len(buf))) + } + footer.footerBH.Offset = uint64(off+int64(len(buf))) - levelDBFooterLen + buf = buf[len(buf)-levelDBFooterLen:] + footer.footerBH.Length = uint64(len(buf)) + footer.format = TableFormatLevelDB + footer.checksum = ChecksumTypeCRC32c + + case rocksDBMagic, pebbleDBMagic: + // NOTE: The Pebble magic string implies the same footer format as that used + // by the RocksDBv2 table format. + if len(buf) < rocksDBFooterLen { + return footer, base.CorruptionErrorf("pebble/table: invalid table (footer too short): %d", errors.Safe(len(buf))) + } + footer.footerBH.Offset = uint64(off+int64(len(buf))) - rocksDBFooterLen + buf = buf[len(buf)-rocksDBFooterLen:] + footer.footerBH.Length = uint64(len(buf)) + version := binary.LittleEndian.Uint32(buf[rocksDBVersionOffset:rocksDBMagicOffset]) + + format, err := ParseTableFormat(magic, version) + if err != nil { + return footer, err + } + footer.format = format + + switch ChecksumType(buf[0]) { + case ChecksumTypeCRC32c: + footer.checksum = ChecksumTypeCRC32c + case ChecksumTypeXXHash64: + footer.checksum = ChecksumTypeXXHash64 + default: + return footer, base.CorruptionErrorf("pebble/table: unsupported checksum type %d", errors.Safe(footer.checksum)) + } + buf = buf[1:] + + default: + return footer, base.CorruptionErrorf("pebble/table: invalid table (bad magic number: 0x%x)", magic) + } + + { + end := uint64(size) + var n int + footer.metaindexBH, n = decodeBlockHandle(buf) + if n == 0 || footer.metaindexBH.Offset+footer.metaindexBH.Length > end { + return footer, base.CorruptionErrorf("pebble/table: invalid table (bad metaindex block handle)") + } + buf = buf[n:] + + footer.indexBH, n = decodeBlockHandle(buf) + if n == 0 || footer.indexBH.Offset+footer.indexBH.Length > end { + return footer, base.CorruptionErrorf("pebble/table: invalid table (bad index block handle)") + } + } + + return footer, nil +} + +func (f footer) encode(buf []byte) []byte { + switch magic, version := f.format.AsTuple(); magic { + case levelDBMagic: + buf = buf[:levelDBFooterLen] + for i := range buf { + buf[i] = 0 + } + n := encodeBlockHandle(buf[0:], f.metaindexBH) + encodeBlockHandle(buf[n:], f.indexBH) + copy(buf[len(buf)-len(levelDBMagic):], levelDBMagic) + + case rocksDBMagic, pebbleDBMagic: + buf = buf[:rocksDBFooterLen] + for i := range buf { + buf[i] = 0 + } + switch f.checksum { + case ChecksumTypeNone: + buf[0] = byte(ChecksumTypeNone) + case ChecksumTypeCRC32c: + buf[0] = byte(ChecksumTypeCRC32c) + case ChecksumTypeXXHash: + buf[0] = byte(ChecksumTypeXXHash) + case ChecksumTypeXXHash64: + buf[0] = byte(ChecksumTypeXXHash64) + default: + panic("unknown checksum type") + } + n := 1 + n += encodeBlockHandle(buf[n:], f.metaindexBH) + encodeBlockHandle(buf[n:], f.indexBH) + binary.LittleEndian.PutUint32(buf[rocksDBVersionOffset:], version) + copy(buf[len(buf)-len(rocksDBMagic):], magic) + + default: + panic("sstable: unspecified table format version") + } + + return buf +} + +func supportsTwoLevelIndex(format TableFormat) bool { + switch format { + case TableFormatLevelDB: + return false + case TableFormatRocksDBv2, TableFormatPebblev1, TableFormatPebblev2, TableFormatPebblev3, TableFormatPebblev4: + return true + default: + panic("sstable: unspecified table format version") + } +} diff --git a/pebble/sstable/table_test.go b/pebble/sstable/table_test.go new file mode 100644 index 0000000..19fffef --- /dev/null +++ b/pebble/sstable/table_test.go @@ -0,0 +1,867 @@ +// 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 ( + "bufio" + "bytes" + "context" + "encoding/binary" + "fmt" + "io" + "math" + "os" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/vfs" + "github.com/kr/pretty" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +// nonsenseWords are words that aren't in testdata/h.txt. +var nonsenseWords = []string{ + // Edge cases. + "", + "\x00", + "\xff", + "`", + "a\x00", + "aaaaaa", + "pol\x00nius", + "youth\x00", + "youti", + "zzzzzz", + // Capitalized versions of actual words in testdata/h.txt. + "A", + "Hamlet", + "thEE", + "YOUTH", + // The following were generated by http://soybomb.com/tricks/words/ + "pectures", + "exectly", + "tricatrippian", + "recens", + "whiratroce", + "troped", + "balmous", + "droppewry", + "toilizing", + "crocias", + "eathrass", + "cheakden", + "speablett", + "skirinies", + "prefing", + "bonufacision", +} + +var ( + wordCount = map[string]string{} + minWord = "" + maxWord = "" +) + +func init() { + f, err := os.Open(filepath.FromSlash("testdata/h.txt")) + if err != nil { + panic(err) + } + defer f.Close() + r := bufio.NewReader(f) + + for first := true; ; { + s, err := r.ReadBytes('\n') + if err == io.EOF { + break + } + if err != nil { + panic(err) + } + k := strings.TrimSpace(string(s[8:])) + v := strings.TrimSpace(string(s[:8])) + wordCount[k] = v + + if first { + first = false + minWord = k + maxWord = k + continue + } + if minWord > k { + minWord = k + } + if maxWord < k { + maxWord = k + } + } + + if len(wordCount) != 1710 { + panic(fmt.Sprintf("h.txt entry count: got %d, want %d", len(wordCount), 1710)) + } + + for _, s := range nonsenseWords { + if _, ok := wordCount[s]; ok { + panic(fmt.Sprintf("nonsense word %q was in h.txt", s)) + } + } +} + +func check(f vfs.File, comparer *Comparer, fp FilterPolicy) error { + opts := ReaderOptions{ + Comparer: comparer, + } + if fp != nil { + opts.Filters = map[string]FilterPolicy{ + fp.Name(): fp, + } + } + + r, err := newReader(f, opts) + if err != nil { + return err + } + + // Check that each key/value pair in wordCount is also in the table. + words := make([]string, 0, len(wordCount)) + for k, v := range wordCount { + words = append(words, k) + // Check using Get. + if v1, err := r.get([]byte(k)); string(v1) != string(v) || err != nil { + return errors.Errorf("Get %q: got (%q, %v), want (%q, %v)", k, v1, err, v, error(nil)) + } else if len(v1) != cap(v1) { + return errors.Errorf("Get %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) + } + + // Check using SeekGE. + iter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return err + } + i := newIterAdapter(iter) + if !i.SeekGE([]byte(k), base.SeekGEFlagsNone) || string(i.Key().UserKey) != k { + return errors.Errorf("Find %q: key was not in the table", k) + } + if k1 := i.Key().UserKey; len(k1) != cap(k1) { + return errors.Errorf("Find %q: len(k1)=%d, cap(k1)=%d", k, len(k1), cap(k1)) + } + if string(i.Value()) != v { + return errors.Errorf("Find %q: got value %q, want %q", k, i.Value(), v) + } + if v1 := i.Value(); len(v1) != cap(v1) { + return errors.Errorf("Find %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) + } + + // Check using SeekLT. + if !i.SeekLT([]byte(k), base.SeekLTFlagsNone) { + i.First() + } else { + i.Next() + } + if string(i.Key().UserKey) != k { + return errors.Errorf("Find %q: key was not in the table", k) + } + if k1 := i.Key().UserKey; len(k1) != cap(k1) { + return errors.Errorf("Find %q: len(k1)=%d, cap(k1)=%d", k, len(k1), cap(k1)) + } + if string(i.Value()) != v { + return errors.Errorf("Find %q: got value %q, want %q", k, i.Value(), v) + } + if v1 := i.Value(); len(v1) != cap(v1) { + return errors.Errorf("Find %q: len(v1)=%d, cap(v1)=%d", k, len(v1), cap(v1)) + } + + if err := i.Close(); err != nil { + return err + } + } + + // Check that nonsense words are not in the table. + for _, s := range nonsenseWords { + // Check using Get. + if _, err := r.get([]byte(s)); err != base.ErrNotFound { + return errors.Errorf("Get %q: got %v, want ErrNotFound", s, err) + } + + // Check using Find. + iter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return err + } + i := newIterAdapter(iter) + if i.SeekGE([]byte(s), base.SeekGEFlagsNone) && s == string(i.Key().UserKey) { + return errors.Errorf("Find %q: unexpectedly found key in the table", s) + } + if err := i.Close(); err != nil { + return err + } + } + + // Check that the number of keys >= a given start key matches the expected number. + var countTests = []struct { + count int + start string + }{ + // cat h.txt | cut -c 9- | wc -l gives 1710. + {1710, ""}, + // cat h.txt | cut -c 9- | grep -v "^[a-b]" | wc -l gives 1522. + {1522, "c"}, + // cat h.txt | cut -c 9- | grep -v "^[a-j]" | wc -l gives 940. + {940, "k"}, + // cat h.txt | cut -c 9- | grep -v "^[a-x]" | wc -l gives 12. + {12, "y"}, + // cat h.txt | cut -c 9- | grep -v "^[a-z]" | wc -l gives 0. + {0, "~"}, + } + for _, ct := range countTests { + iter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return err + } + n, i := 0, newIterAdapter(iter) + for valid := i.SeekGE([]byte(ct.start), base.SeekGEFlagsNone); valid; valid = i.Next() { + n++ + } + if n != ct.count { + return errors.Errorf("count %q: got %d, want %d", ct.start, n, ct.count) + } + n = 0 + for valid := i.Last(); valid; valid = i.Prev() { + if bytes.Compare(i.Key().UserKey, []byte(ct.start)) < 0 { + break + } + n++ + } + if n != ct.count { + return errors.Errorf("count %q: got %d, want %d", ct.start, n, ct.count) + } + if err := i.Close(); err != nil { + return err + } + } + + // Check lower/upper bounds behavior. Randomly choose a lower and upper bound + // and then guarantee that iteration finds the expected number if entries. + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + sort.Strings(words) + for i := 0; i < 10; i++ { + lowerIdx := -1 + upperIdx := len(words) + if rng.Intn(5) != 0 { + lowerIdx = rng.Intn(len(words)) + } + if rng.Intn(5) != 0 { + upperIdx = rng.Intn(len(words)) + } + if lowerIdx > upperIdx { + lowerIdx, upperIdx = upperIdx, lowerIdx + } + + var lower, upper []byte + if lowerIdx >= 0 { + lower = []byte(words[lowerIdx]) + } else { + lowerIdx = 0 + } + if upperIdx < len(words) { + upper = []byte(words[upperIdx]) + } + + iter, err := r.NewIter(lower, upper) + if err != nil { + return err + } + i := newIterAdapter(iter) + + if lower == nil { + n := 0 + for valid := i.First(); valid; valid = i.Next() { + n++ + } + if expected := upperIdx; expected != n { + return errors.Errorf("expected %d, but found %d", expected, n) + } + } + + if upper == nil { + n := 0 + for valid := i.Last(); valid; valid = i.Prev() { + n++ + } + if expected := len(words) - lowerIdx; expected != n { + return errors.Errorf("expected %d, but found %d", expected, n) + } + } + + if lower != nil { + n := 0 + for valid := i.SeekGE(lower, base.SeekGEFlagsNone); valid; valid = i.Next() { + n++ + } + if expected := upperIdx - lowerIdx; expected != n { + return errors.Errorf("expected %d, but found %d", expected, n) + } + } + + if upper != nil { + n := 0 + for valid := i.SeekLT(upper, base.SeekLTFlagsNone); valid; valid = i.Prev() { + n++ + } + if expected := upperIdx - lowerIdx; expected != n { + return errors.Errorf("expected %d, but found %d", expected, n) + } + } + + if err := i.Close(); err != nil { + return err + } + } + + return r.Close() +} + +var ( + memFileSystem = vfs.NewMem() + tmpFileCount int +) + +func build( + compression Compression, + fp FilterPolicy, + ftype FilterType, + comparer *Comparer, + propCollector func() TablePropertyCollector, + blockSize int, + indexBlockSize int, +) (vfs.File, error) { + // Create a sorted list of wordCount's keys. + keys := make([]string, len(wordCount)) + i := 0 + for k := range wordCount { + keys[i] = k + i++ + } + sort.Strings(keys) + + // Write the key/value pairs to a new table, in increasing key order. + filename := fmt.Sprintf("/tmp%d", tmpFileCount) + f0, err := memFileSystem.Create(filename) + if err != nil { + return nil, err + } + tmpFileCount++ + + writerOpts := WriterOptions{ + BlockSize: blockSize, + Comparer: comparer, + Compression: compression, + FilterPolicy: fp, + FilterType: ftype, + IndexBlockSize: indexBlockSize, + MergerName: "nullptr", + } + if propCollector != nil { + writerOpts.TablePropertyCollectors = append(writerOpts.TablePropertyCollectors, propCollector) + } + + w := NewWriter(objstorageprovider.NewFileWritable(f0), writerOpts) + // Use rangeDelV1Format for testing byte equality with RocksDB. + w.rangeDelV1Format = true + var rangeDelLength int + var rangeDelCounter int + var rangeDelStart InternalKey + for i, k := range keys { + v := wordCount[k] + ikey := base.MakeInternalKey([]byte(k), 0, InternalKeyKindSet) + if err := w.Add(ikey, []byte(v)); err != nil { + return nil, err + } + // This mirrors the logic in `make-table.cc`. It adds range deletions of + // increasing length for every 100 keys added. + if i%100 == 0 { + rangeDelStart = ikey.Clone() + rangeDelCounter = 0 + rangeDelLength++ + } + rangeDelCounter++ + + if rangeDelCounter == rangeDelLength { + if err := w.DeleteRange(rangeDelStart.UserKey, ikey.UserKey); err != nil { + return nil, err + } + } + } + if err := w.Close(); err != nil { + return nil, err + } + + // Re-open that filename for reading. + f1, err := memFileSystem.Open(filename) + if err != nil { + return nil, err + } + return f1, nil +} + +func testReader(t *testing.T, filename string, comparer *Comparer, fp FilterPolicy) { + // Check that we can read a pre-made table. + f, err := vfs.Default.Open(filepath.FromSlash("testdata/" + filename)) + if err != nil { + t.Error(err) + return + } + err = check(f, comparer, fp) + if err != nil { + t.Error(err) + return + } +} + +func TestReaderLevelDB(t *testing.T) { testReader(t, "h.ldb", nil, nil) } +func TestReaderDefaultCompression(t *testing.T) { testReader(t, "h.sst", nil, nil) } +func TestReaderNoCompression(t *testing.T) { testReader(t, "h.no-compression.sst", nil, nil) } +func TestReaderBlockBloomIgnored(t *testing.T) { + testReader(t, "h.block-bloom.no-compression.sst", nil, nil) +} +func TestReaderTableBloomIgnored(t *testing.T) { + testReader(t, "h.table-bloom.no-compression.sst", nil, nil) +} + +func TestReaderBloomUsed(t *testing.T) { + // wantActualNegatives is the minimum number of nonsense words (i.e. false + // positives or true negatives) to run through our filter. Some nonsense + // words might be rejected even before the filtering step, if they are out + // of the [minWord, maxWord] range of keys in the table. + wantActualNegatives := 0 + for _, s := range nonsenseWords { + if minWord < s && s < maxWord { + wantActualNegatives++ + } + } + + files := []struct { + path string + comparer *Comparer + }{ + {"h.table-bloom.no-compression.sst", nil}, + {"h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", fixtureComparer}, + } + for _, tc := range files { + t.Run(tc.path, func(t *testing.T) { + for _, degenerate := range []bool{false, true} { + t.Run(fmt.Sprintf("degenerate=%t", degenerate), func(t *testing.T) { + c := &countingFilterPolicy{ + FilterPolicy: bloom.FilterPolicy(10), + degenerate: degenerate, + } + testReader(t, tc.path, tc.comparer, c) + + if c.truePositives != len(wordCount) { + t.Errorf("degenerate=%t: true positives: got %d, want %d", degenerate, c.truePositives, len(wordCount)) + } + if c.falseNegatives != 0 { + t.Errorf("degenerate=%t: false negatives: got %d, want %d", degenerate, c.falseNegatives, 0) + } + + if got := c.falsePositives + c.trueNegatives; got < wantActualNegatives { + t.Errorf("degenerate=%t: actual negatives (false positives + true negatives): "+ + "got %d (%d + %d), want >= %d", + degenerate, got, c.falsePositives, c.trueNegatives, wantActualNegatives) + } + + if !degenerate { + // The true negative count should be much greater than the false + // positive count. + if c.trueNegatives < 10*c.falsePositives { + t.Errorf("degenerate=%t: true negative to false positive ratio (%d:%d) is too small", + degenerate, c.trueNegatives, c.falsePositives) + } + } + }) + } + }) + } +} + +func TestBloomFilterFalsePositiveRate(t *testing.T) { + f, err := os.Open(filepath.FromSlash("testdata/h.table-bloom.no-compression.sst")) + require.NoError(t, err) + + c := &countingFilterPolicy{ + FilterPolicy: bloom.FilterPolicy(1), + } + r, err := newReader(f, ReaderOptions{ + Filters: map[string]FilterPolicy{ + c.Name(): c, + }, + }) + require.NoError(t, err) + + const n = 10000 + // key is a buffer that will be re-used for n Get calls, each with a + // different key. The "m" in the 2-byte prefix means that the key falls in + // the [minWord, maxWord] range and so will not be rejected prior to + // applying the Bloom filter. The "!" in the 2-byte prefix means that the + // key is not actually in the table. The filter will only see actual + // negatives: false positives or true negatives. + key := []byte("m!....") + for i := 0; i < n; i++ { + binary.LittleEndian.PutUint32(key[2:6], uint32(i)) + r.get(key) + } + + if c.truePositives != 0 { + t.Errorf("true positives: got %d, want 0", c.truePositives) + } + if c.falseNegatives != 0 { + t.Errorf("false negatives: got %d, want 0", c.falseNegatives) + } + if got := c.falsePositives + c.trueNegatives; got != n { + t.Errorf("actual negatives (false positives + true negatives): got %d (%d + %d), want %d", + got, c.falsePositives, c.trueNegatives, n) + } + + // According the the comments in the C++ LevelDB code, the false positive + // rate should be approximately 1% for for bloom.FilterPolicy(10). The 10 + // was the parameter used to write the .sst file. When reading the file, + // the 1 in the bloom.FilterPolicy(1) above doesn't matter, only the + // bloom.FilterPolicy matters. + if got := float64(100*c.falsePositives) / n; got < 0.2 || 5 < got { + t.Errorf("false positive rate: got %v%%, want approximately 1%%", got) + } + + require.NoError(t, r.Close()) +} + +type countingFilterPolicy struct { + FilterPolicy + degenerate bool + + truePositives int + falsePositives int + falseNegatives int + trueNegatives int +} + +func (c *countingFilterPolicy) MayContain(ftype FilterType, filter, key []byte) bool { + got := true + if c.degenerate { + // When degenerate is true, we override the embedded FilterPolicy's + // MayContain method to always return true. Doing so is a valid, if + // inefficient, implementation of the FilterPolicy interface. + } else { + got = c.FilterPolicy.MayContain(ftype, filter, key) + } + _, want := wordCount[string(key)] + + switch { + case got && want: + c.truePositives++ + case got && !want: + c.falsePositives++ + case !got && want: + c.falseNegatives++ + case !got && !want: + c.trueNegatives++ + } + return got +} + +func TestWriterRoundTrip(t *testing.T) { + blockSizes := []int{100, 1000, 2048, 4096, math.MaxInt32} + for _, blockSize := range blockSizes { + for _, indexBlockSize := range blockSizes { + for name, fp := range map[string]FilterPolicy{ + "none": nil, + "bloom10bit": bloom.FilterPolicy(10), + } { + t.Run(fmt.Sprintf("bloom=%s", name), func(t *testing.T) { + f, err := build(DefaultCompression, fp, TableFilter, + nil, nil, blockSize, indexBlockSize) + require.NoError(t, err) + + // Check that we can read a freshly made table. + require.NoError(t, check(f, nil, nil)) + }) + } + } + } +} + +func TestFinalBlockIsWritten(t *testing.T) { + keys := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"} + valueLengths := []int{0, 1, 22, 28, 33, 40, 50, 61, 87, 100, 143, 200} + xxx := bytes.Repeat([]byte("x"), valueLengths[len(valueLengths)-1]) + for _, blockSize := range []int{5, 10, 25, 50, 100} { + for _, indexBlockSize := range []int{5, 10, 25, 50, 100, math.MaxInt32} { + for nk := 0; nk <= len(keys); nk++ { + loop: + for _, vLen := range valueLengths { + got, memFS := 0, vfs.NewMem() + + wf, err := memFS.Create("foo") + if err != nil { + t.Errorf("nk=%d, vLen=%d: memFS create: %v", nk, vLen, err) + continue + } + w := NewWriter(objstorageprovider.NewFileWritable(wf), WriterOptions{ + BlockSize: blockSize, + IndexBlockSize: indexBlockSize, + }) + for _, k := range keys[:nk] { + if err := w.Add(InternalKey{UserKey: []byte(k)}, xxx[:vLen]); err != nil { + t.Errorf("nk=%d, vLen=%d: set: %v", nk, vLen, err) + continue loop + } + } + if err := w.Close(); err != nil { + t.Errorf("nk=%d, vLen=%d: writer close: %v", nk, vLen, err) + continue + } + + rf, err := memFS.Open("foo") + if err != nil { + t.Errorf("nk=%d, vLen=%d: memFS open: %v", nk, vLen, err) + continue + } + r, err := newReader(rf, ReaderOptions{}) + if err != nil { + t.Errorf("nk=%d, vLen=%d: reader open: %v", nk, vLen, err) + } + iter, err := r.NewIter(nil /* lower */, nil /* upper */) + require.NoError(t, err) + i := newIterAdapter(iter) + for valid := i.First(); valid; valid = i.Next() { + got++ + } + if err := i.Close(); err != nil { + t.Errorf("nk=%d, vLen=%d: Iterator close: %v", nk, vLen, err) + continue + } + if err := r.Close(); err != nil { + t.Errorf("nk=%d, vLen=%d: reader close: %v", nk, vLen, err) + continue + } + + if got != nk { + t.Errorf("nk=%2d, vLen=%3d: got %2d keys, want %2d", nk, vLen, got, nk) + continue + } + } + } + } + } +} + +func TestReaderGlobalSeqNum(t *testing.T) { + f, err := os.Open(filepath.FromSlash("testdata/h.sst")) + require.NoError(t, err) + + r, err := newReader(f, ReaderOptions{}) + require.NoError(t, err) + + const globalSeqNum = 42 + r.Properties.GlobalSeqNum = globalSeqNum + + iter, err := r.NewIter(nil /* lower */, nil /* upper */) + require.NoError(t, err) + i := newIterAdapter(iter) + for valid := i.First(); valid; valid = i.Next() { + if globalSeqNum != i.Key().SeqNum() { + t.Fatalf("expected %d, but found %d", globalSeqNum, i.Key().SeqNum()) + } + } + require.NoError(t, i.Close()) + require.NoError(t, r.Close()) +} + +func TestMetaIndexEntriesSorted(t *testing.T) { + f, err := build(DefaultCompression, nil, /* filter policy */ + TableFilter, nil, nil, 4096, 4096) + require.NoError(t, err) + + r, err := newReader(f, ReaderOptions{}) + require.NoError(t, err) + + b, err := r.readBlock( + context.Background(), r.metaIndexBH, nil, nil, nil, nil, nil) + require.NoError(t, err) + defer b.Release() + + i, err := newRawBlockIter(bytes.Compare, b.Get()) + require.NoError(t, err) + + var keys []string + for valid := i.First(); valid; valid = i.Next() { + keys = append(keys, string(i.Key().UserKey)) + } + if !sort.StringsAreSorted(keys) { + t.Fatalf("metaindex block out of order: %v", keys) + } + + require.NoError(t, i.Close()) + require.NoError(t, r.Close()) +} + +func TestFooterRoundTrip(t *testing.T) { + buf := make([]byte, 100+maxFooterLen) + for format := TableFormatLevelDB; format < TableFormatMax; format++ { + t.Run(fmt.Sprintf("format=%s", format), func(t *testing.T) { + checksums := []ChecksumType{ChecksumTypeCRC32c} + if format != TableFormatLevelDB { + checksums = []ChecksumType{ChecksumTypeCRC32c, ChecksumTypeXXHash64} + } + for _, checksum := range checksums { + t.Run(fmt.Sprintf("checksum=%d", checksum), func(t *testing.T) { + footer := footer{ + format: format, + checksum: checksum, + metaindexBH: BlockHandle{Offset: 1, Length: 2}, + indexBH: BlockHandle{Offset: 3, Length: 4}, + } + for _, offset := range []int64{0, 1, 100} { + t.Run(fmt.Sprintf("offset=%d", offset), func(t *testing.T) { + mem := vfs.NewMem() + f, err := mem.Create("test") + require.NoError(t, err) + + _, err = f.Write(buf[:offset]) + require.NoError(t, err) + + encoded := footer.encode(buf[100:]) + _, err = f.Write(encoded) + require.NoError(t, err) + require.NoError(t, f.Close()) + + footer.footerBH.Offset = uint64(offset) + footer.footerBH.Length = uint64(len(encoded)) + + f, err = mem.Open("test") + require.NoError(t, err) + + readable, err := NewSimpleReadable(f) + require.NoError(t, err) + + result, err := readFooter(readable) + require.NoError(t, err) + require.NoError(t, readable.Close()) + + if diff := pretty.Diff(footer, result); diff != nil { + t.Fatalf("expected %+v, but found %+v\n%s", + footer, result, strings.Join(diff, "\n")) + } + }) + } + }) + } + }) + } +} + +func TestReadFooter(t *testing.T) { + encode := func(format TableFormat, checksum ChecksumType) string { + f := footer{ + format: format, + checksum: checksum, + } + return string(f.encode(make([]byte, maxFooterLen))) + } + + testCases := []struct { + encoded string + expected string + }{ + {strings.Repeat("a", minFooterLen-1), "file size is too small"}, + {strings.Repeat("a", levelDBFooterLen), "bad magic number"}, + {strings.Repeat("a", rocksDBFooterLen), "bad magic number"}, + {encode(TableFormatLevelDB, 0)[1:], "file size is too small"}, + {encode(TableFormatRocksDBv2, 0)[1:], "footer too short"}, + {encode(TableFormatRocksDBv2, ChecksumTypeNone), "unsupported checksum type"}, + {encode(TableFormatRocksDBv2, ChecksumTypeXXHash), "unsupported checksum type"}, + } + for _, c := range testCases { + t.Run("", func(t *testing.T) { + mem := vfs.NewMem() + f, err := mem.Create("test") + require.NoError(t, err) + + _, err = f.Write([]byte(c.encoded)) + require.NoError(t, err) + require.NoError(t, f.Close()) + + f, err = mem.Open("test") + require.NoError(t, err) + + readable, err := NewSimpleReadable(f) + require.NoError(t, err) + + if _, err := readFooter(readable); err == nil { + t.Fatalf("expected %q, but found success", c.expected) + } else if !strings.Contains(err.Error(), c.expected) { + t.Fatalf("expected %q, but found %v", c.expected, err) + } + }) + } +} + +type errorPropCollector struct{} + +func (errorPropCollector) Add(key InternalKey, _ []byte) error { + return errors.Errorf("add %s failed", key) +} + +func (errorPropCollector) Finish(_ map[string]string) error { + return errors.Errorf("finish failed") +} + +func (errorPropCollector) Name() string { + return "errorPropCollector" +} + +func TestTablePropertyCollectorErrors(t *testing.T) { + + var testcases map[string]func(w *Writer) error = map[string]func(w *Writer) error{ + "add a#0,1 failed": func(w *Writer) error { + return w.Set([]byte("a"), []byte("b")) + }, + "add c#0,0 failed": func(w *Writer) error { + return w.Delete([]byte("c")) + }, + "add d#0,15 failed": func(w *Writer) error { + return w.DeleteRange([]byte("d"), []byte("e")) + }, + "add f#0,2 failed": func(w *Writer) error { + return w.Merge([]byte("f"), []byte("g")) + }, + "finish failed": func(w *Writer) error { + return w.Close() + }, + } + + for e, fun := range testcases { + mem := vfs.NewMem() + f, err := mem.Create("foo") + require.NoError(t, err) + + var opts WriterOptions + opts.TablePropertyCollectors = append(opts.TablePropertyCollectors, + func() TablePropertyCollector { + return errorPropCollector{} + }) + + w := NewWriter(objstorageprovider.NewFileWritable(f), opts) + + require.Regexp(t, e, fun(w)) + } +} diff --git a/pebble/sstable/testdata/.gitignore b/pebble/sstable/testdata/.gitignore new file mode 100644 index 0000000..20ae9cb --- /dev/null +++ b/pebble/sstable/testdata/.gitignore @@ -0,0 +1 @@ +/make-table diff --git a/pebble/sstable/testdata/Makefile b/pebble/sstable/testdata/Makefile new file mode 100644 index 0000000..cdb2890 --- /dev/null +++ b/pebble/sstable/testdata/Makefile @@ -0,0 +1,17 @@ +all: rebuild + +.PHONY: rebuild +rebuild: make-table h.txt + ./make-table + +h.txt: hamlet-act-1.txt + cat hamlet-act-1.txt | \ + tr '[:upper:]' '[:lower:]' | \ + grep -o -E '\w+' | \ + sort | \ + uniq -c | \ + awk '{printf "%7s %s\n", $$1, $$2}' \ + > h.txt + +make-table: make-table.cc + g++ -std=c++14 -o make-table make-table.cc -lrocksdb diff --git a/pebble/sstable/testdata/block b/pebble/sstable/testdata/block new file mode 100644 index 0000000..121640b --- /dev/null +++ b/pebble/sstable/testdata/block @@ -0,0 +1,135 @@ +build +a:1,b:2,c:3,d:4 +---- + +iter +first +next +next +next +next +---- +. + +iter +seek-ge a +next +next +next +next +---- +. + +iter +seek-ge b +next +next +next +---- +. + +iter +seek-ge c +next +next +---- +. + +iter +seek-ge d +next +---- +. + +iter +seek-ge e +---- +. + +iter +seek-ge b +seek-ge c +seek-ge d +seek-ge e +---- +. + +iter +last +prev +prev +prev +prev +---- +. + +iter +seek-lt e +prev +prev +prev +prev +---- +. + +iter +seek-lt d +prev +prev +prev +---- +. + +iter +seek-lt c +prev +prev +---- +. + +iter +seek-lt b +prev +---- +. + +iter +seek-lt a +prev +next +---- +.. + +iter +seek-ge d +next +next +prev +---- +.. + +iter +seek-lt d +seek-lt c +seek-lt b +seek-lt a +---- +. + +iter globalSeqNum=1 +first +next +next +next +next +---- +. + +iter globalSeqNum=10 +first +next +next +next +next +---- +. diff --git a/pebble/sstable/testdata/block_properties b/pebble/sstable/testdata/block_properties new file mode 100644 index 0000000..8cf2db1 --- /dev/null +++ b/pebble/sstable/testdata/block_properties @@ -0,0 +1,671 @@ +# The following collectors are available: +# - value-first - uses the first character of the value to construct an interval +# - value-last - uses the last character of the value to construct an interval +# - suffix - constructs an interval from the '@timestamp' suffix of each key +# - suffix-point-keys-only - same as "suffix", but only applies to point keys +# - suffix-range-keys-only - same as "suffix", but only applies to range keys +# - nil-points-and-ranges - a trivial collector with neither a point nor range collector + +# Single collector. + +build collectors=(value-first) +a.SET.1:10 +b.SET.2:20 +c.SET.3:30 +---- +point: [a#1,1,c#3,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,3] + +# collectors returns the collectors used when writing the table, keyed by the +# shortID of the collector. +collectors +---- +0: value-first + +# table-props returns the table-level properties, keyed by the shortID. +table-props +---- +0: [1, 4) + +# block-props returns the block-level properties. For each block, the separator +# is printed, along with the properties for the block, keyed by the shortID. +block-props +---- +d#72057594037927935,17: + 0: [1, 4) + +# Multiple collectors. + +build collectors=(value-first,value-last) +a.SET.1:17 +b.SET.2:29 +c.SET.3:38 +---- +point: [a#1,1,c#3,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,3] + +collectors +---- +0: value-first +1: value-last + +table-props +---- +0: [1, 4) +1: [7, 10) + +block-props +---- +d#72057594037927935,17: + 0: [1, 4) + 1: [7, 10) + +# Reduce the block size to a value such that each block has at most two KV +# pairs. + +build block-size=25 collectors=(value-first,value-last) +a.SET.1:15 +b.SET.2:86 +c.SET.3:72 +d.SET.4:21 +e.SET.5:47 +f.SET.6:54 +g.SET.7:63 +h.SET.8:38 +---- +point: [a#1,1,h#8,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,8] + +collectors +---- +0: value-first +1: value-last + +table-props +---- +0: [1, 9) +1: [1, 9) + +block-props +---- +b#2,1: + 0: [1, 9) + 1: [5, 7) +d#4,1: + 0: [2, 8) + 1: [1, 3) +f#6,1: + 0: [4, 6) + 1: [4, 8) +i#72057594037927935,17: + 0: [3, 7) + 1: [3, 9) + +# Range keys contribute to the table-level property but do not affect point key +# data blocks. + +build collectors=(suffix) +a@5.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +rangekey: d@10-e@15:{(#4,RANGEKEYSET,@20,foo)} +rangekey: e@15-f@20:{(#5,RANGEKEYUNSET,@25)} +rangekey: f@20-z@25:{(#6,RANGEKEYDEL)} +---- +point: [a@5#1,1,c@15#3,1] +rangedel: [#0,0,#0,0] +rangekey: [d@10#4,21,z@25#72057594037927935,19] +seqnums: [1,6] + +collectors +---- +0: suffix + +block-props +---- +d#72057594037927935,17: + 0: [5, 16) + +table-props +---- +0: [5, 26) + +# Same as the above, but only collect point key properties. + +build collectors=(suffix-point-keys-only) +a@5.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +rangekey: d@10-e@15:{(#4,RANGEKEYSET,@20,foo)} +rangekey: e@15-f@20:{(#5,RANGEKEYUNSET,@25)} +rangekey: f@20-z@25:{(#6,RANGEKEYDEL)} +---- +point: [a@5#1,1,c@15#3,1] +rangedel: [#0,0,#0,0] +rangekey: [d@10#4,21,z@25#72057594037927935,19] +seqnums: [1,6] + +collectors +---- +0: suffix-point-keys-only + +block-props +---- +d#72057594037927935,17: + 0: [5, 16) + +table-props +---- +0: [5, 16) + +# Same as the above, but only collect range key properties. + +build collectors=(suffix-range-keys-only) +a@5.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +rangekey: d@10-e@15:{(#4,RANGEKEYSET,@20,foo)} +rangekey: e@15-f@20:{(#5,RANGEKEYUNSET,@25)} +rangekey: f@20-z@25:{(#6,RANGEKEYDEL)} +---- +point: [a@5#1,1,c@15#3,1] +rangedel: [#0,0,#0,0] +rangekey: [d@10#4,21,z@25#72057594037927935,19] +seqnums: [1,6] + +collectors +---- +0: suffix-range-keys-only + +block-props +---- +d#72057594037927935,17: + +table-props +---- +0: [20, 26) + +# Create a table with multiple data blocks and a range key block. Two block +# property collectors are used, one for range keys and one for point keys, each +# acting independently. + +build block-size=1 collectors=(suffix-point-keys-only,suffix-range-keys-only) +a@5.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +rangekey: d@10-e@15:{(#4,RANGEKEYSET,@20,foo)} +rangekey: e@15-f@20:{(#5,RANGEKEYUNSET,@25)} +rangekey: f@20-z@25:{(#6,RANGEKEYDEL)} +---- +point: [a@5#1,1,c@15#3,1] +rangedel: [#0,0,#0,0] +rangekey: [d@10#4,21,z@25#72057594037927935,19] +seqnums: [1,6] + +collectors +---- +0: suffix-point-keys-only +1: suffix-range-keys-only + +block-props +---- +b#72057594037927935,17: + 0: [5, 6) +c#72057594037927935,17: + 0: [10, 11) +d#72057594037927935,17: + 0: [15, 16) + +table-props +---- +0: [5, 16) +1: [20, 26) + +# Partially matching point key filter. + +filter point-filter=(suffix-point-keys-only,10,20) +---- +points: true, blocks=[1,2] +ranges: true (no filters provided) + +# Non-matching point key filter. + +filter point-filter=(suffix-point-keys-only,100,200) +---- +points: false +ranges: true (no filters provided) + +# Partially matching range key filter. + +filter range-filter=(suffix-range-keys-only,10,25) +---- +points: true (no filters provided) +ranges: true + +# Non-matching range key filter. + +filter range-filter=(suffix-range-keys-only,100,200) +---- +points: true (no filters provided) +ranges: false + +# Matching point and range key filter. + +filter point-filter=(suffix-point-keys-only,10,20) range-filter=(suffix-range-keys-only,10,25) +---- +points: true, blocks=[1,2] +ranges: true + +# Matching point key filter and non-matching range key filter. + +filter point-filter=(suffix-point-keys-only,10,20) range-filter=(suffix-range-keys-only,100,200) +---- +points: true, blocks=[1,2] +ranges: false + +# Non-matching point key filter and matching range key filter. + +filter point-filter=(suffix-point-keys-only,100,200) range-filter=(suffix-range-keys-only,10,25) +---- +points: false +ranges: true + +# Non-matching point and range key filter. + +filter point-filter=(suffix-point-keys-only,100,200) range-filter=(suffix-range-keys-only,100,100) +---- +points: false +ranges: false + +# Providing a nil collector for both points and ranges is a user-error. + +build collectors=(nil-points-and-ranges) +---- +sstable: at least one interval collector must be provided + +# Test a small index-block-size and block-size, so every data block has one KV +# and every index block points to one data block. + +build collectors=(suffix-point-keys-only) index-block-size=1 block-size=1 +a@1.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +d@25.SET.4:bax +e@3.SET.5:box +f@5.SET.3:mop +---- +point: [a@1#1,1,f@5#3,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,5] + +collectors +---- +0: suffix-point-keys-only + +table-props +---- +0: [1, 26) + +# Because of the tiny index block size, every index block should have the same +# properties as the single data block contained in the table. Indentation shows +# the hierarchy. + +block-props +---- +b#72057594037927935,17: + 0: [1, 2) + b#72057594037927935,17: + 0: [1, 2) +c#72057594037927935,17: + 0: [10, 11) + c#72057594037927935,17: + 0: [10, 11) +d#72057594037927935,17: + 0: [15, 16) + d#72057594037927935,17: + 0: [15, 16) +e#72057594037927935,17: + 0: [25, 26) + e#72057594037927935,17: + 0: [25, 26) +f#72057594037927935,17: + 0: [3, 4) + f#72057594037927935,17: + 0: [3, 4) +g#72057594037927935,17: + 0: [5, 6) + g#72057594037927935,17: + 0: [5, 6) + +# Test the same sstable, but with a larger index block size that fits multiple +# (3) KV pairs. Each entry in the top-level index should hold the unioned ranges +# of all the data blocks' properties. + +build collectors=(suffix-point-keys-only) index-block-size=64 block-size=1 +a@1.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +d@25.SET.4:bax +e@3.SET.5:box +f@5.SET.3:mop +---- +point: [a@1#1,1,f@5#3,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,5] + +collectors +---- +0: suffix-point-keys-only + +block-props +---- +d#72057594037927935,17: + 0: [1, 16) + b#72057594037927935,17: + 0: [1, 2) + c#72057594037927935,17: + 0: [10, 11) + d#72057594037927935,17: + 0: [15, 16) +g#72057594037927935,17: + 0: [3, 26) + e#72057594037927935,17: + 0: [25, 26) + f#72057594037927935,17: + 0: [3, 4) + g#72057594037927935,17: + 0: [5, 6) + +# Regression test for a bug in boundary checking when skipping over irrelevant +# index blocks in a two-level indexed sstable. + +iter lower=a point-key-filter=(suffix-point-keys-only,1,2) +seek-lt h +last +---- + MaybeFilteredKeys()=true + MaybeFilteredKeys()=true + +# Same as above, but each index block holds 2 keys. This exercises a variant of +# the above bug. Specifically, the bounds check performed /within/ skipBackward, +# instead of within SeekLT and Last. + +build collectors=(suffix-point-keys-only) index-block-size=48 block-size=1 +a@1.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +d@25.SET.4:bax +e@3.SET.5:box +f@5.SET.3:mop +---- +point: [a@1#1,1,f@5#3,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,5] + +block-props +---- +c#72057594037927935,17: + 0: [1, 11) + b#72057594037927935,17: + 0: [1, 2) + c#72057594037927935,17: + 0: [10, 11) +e#72057594037927935,17: + 0: [15, 26) + d#72057594037927935,17: + 0: [15, 16) + e#72057594037927935,17: + 0: [25, 26) +g#72057594037927935,17: + 0: [3, 6) + f#72057594037927935,17: + 0: [3, 4) + g#72057594037927935,17: + 0: [5, 6) + +# Regression test for a bug in boundary checking when skipping over irrelevant +# index blocks in a two-level indexed sstable. + +iter lower=a upper=z point-key-filter=(suffix-point-keys-only,1,2) +seek-lt h +---- + MaybeFilteredKeys()=true + +# Test MaybeFilteredKeys(). + +# Use timestamp range [1,9), which matches a@1, e@3 and f@5 and filters +# a continuous section of three keys b@10, c@15 and d@25. + +iter point-key-filter=(suffix-point-keys-only,1,9) +first +next +next +next +seek-ge b +seek-ge e@3 +---- + MaybeFilteredKeys()=false + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=false + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false + + +# NB: `seek-ge e` and `seek-ge dog` return MaybeFilteredKeys()=true, despite no +# filtered keys existing within the range [e,e@3) or [dog,e@3). This is a +# consequence of the index separator `e`. After seeking the index block, the +# iterator only knows that the first block MAY contain keys ≤ e. However, it can +# be skipped regardless, because block properties filters exclude it. In this +# case, the iterator still returns MaybeFilteredKeys()=true, since keys MAY have +# been excluded by the filter. + +iter point-key-filter=(suffix-point-keys-only,1,9) +seek-ge e +seek-ge dog +---- + MaybeFilteredKeys()=true + MaybeFilteredKeys()=true + +iter point-key-filter=(suffix-point-keys-only,1,100) +first +next +next +next +next +next +next +seek-lt d +seek-ge d +---- + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + +# [10,16) intersects {b@10, c@15}. + +iter point-key-filter=(suffix-point-keys-only,10,16) +last +prev +prev +seek-lt a +seek-lt c +seek-lt ca +seek-lt f +seek-lt e +seek-lt d +---- + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + MaybeFilteredKeys()=true + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false + +# Test monotonically increasing bounds optimization, with the first seek +# filtering keys. The subsequent seek must not reuse the current iterator +# position and improperly returning MaybeFilteredKeys=false when keys were +# filtered. + +iter point-key-filter=(suffix-point-keys-only,10,16) +set-bounds lower=b upper=ee +seek-ge d +set-bounds lower=ee upper=g +seek-ge ee +---- +. MaybeFilteredKeys()=false +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true + +iter point-key-filter=(suffix-point-keys-only,10,16) +set-bounds lower=a upper=b +seek-ge a +set-bounds lower=b upper=e +seek-ge b +seek-ge bb +---- +. MaybeFilteredKeys()=false +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false + +# Test monotonically decreasing bounds optimization, with the first seek +# filtering keys. The subsequent seek must not reuse the current iterator +# position and improperly returning MaybeFilteredKeys=false when keys were +# filtered. + +iter point-key-filter=(suffix-point-keys-only,10,16) +set-bounds lower=e upper=f +seek-lt f +set-bounds lower=c upper=e +seek-lt e +set-bounds lower=a upper=c +seek-lt c +seek-lt b +---- +. MaybeFilteredKeys()=false +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true + MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=true + +# The below case tests try-seek-using-next. +# +# The `seek-ge aa` does not reposition the iterator. This case should preserve +# the existing MaybeFilteredKeys()=true value. +# +# The `seek-ge c@16` and seek-ge c@19` must also return MaybeFilteredKeys()=true. + +iter point-key-filter=(suffix-point-keys-only,10,16) +seek-ge a +seek-ge aa true +seek-ge bb true +seek-ge c@16 true +seek-ge c@19 true +---- + MaybeFilteredKeys()=true + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true + +# Test another case of monotonically increasing bounds optimization, with a +# different index block structure. The first seek down below should filter keys, +# and leave the top-level index positioned at the last index block. The +# subsequent seek must return MaybeFilteredKeys=true when keys were filtered. + +build collectors=(suffix-point-keys-only) index-block-size=1 block-size=64 +a@1.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +d@25.SET.4:bax +e@3.SET.5:box +f@5.SET.3:mop +---- +point: [a@1#1,1,f@5#3,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,5] + +block-props +---- +d#72057594037927935,17: + 0: [1, 16) + d#72057594037927935,17: + 0: [1, 16) +g#72057594037927935,17: + 0: [3, 26) + g#72057594037927935,17: + 0: [3, 26) + +iter point-key-filter=(suffix-point-keys-only,1,2) +set-bounds lower=b upper=e +seek-ge d +set-bounds lower=e upper=g +seek-ge ee +---- +. MaybeFilteredKeys()=false +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true + +# Test another case of monotonically increasing bounds optimization, with a new +# index block structure: This one has only one level. The first seek down below +# should filter keys, and leave the index positioned at the last index block. +# The subsequent seek must return MaybeFilteredKeys=true when keys were +# filtered. + +build collectors=(suffix-point-keys-only) block-size=32 +a@1.SET.1:foo +b@10.SET.2:bar +c@15.SET.3:baz +d@25.SET.4:bax +e@3.SET.5:box +f@5.SET.3:mop +---- +point: [a@1#1,1,f@5#3,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,5] + +block-props +---- +c#72057594037927935,17: + 0: [1, 11) +e#72057594037927935,17: + 0: [15, 26) +g#72057594037927935,17: + 0: [3, 6) + +iter point-key-filter=(suffix-point-keys-only,1,2) +set-bounds lower=b upper=ee +seek-ge d +set-bounds lower=ee upper=g +seek-ge ee +---- +. MaybeFilteredKeys()=false +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true +. MaybeFilteredKeys()=true diff --git a/pebble/sstable/testdata/block_properties_boundlimited b/pebble/sstable/testdata/block_properties_boundlimited new file mode 100644 index 0000000..d67907e --- /dev/null +++ b/pebble/sstable/testdata/block_properties_boundlimited @@ -0,0 +1,167 @@ +build block-size=28 collectors=(suffix) +a@5.SET.1:15 +b@2.SET.2:86 +c@9.SET.3:72 +d@3.SET.4:21 +e@2.SET.5:47 +f@0.SET.6:54 +g@8.SET.7:63 +h@3.SET.8:38 +---- +point: [a@5#1,1,h@3#8,1] +rangedel: [#0,0,#0,0] +rangekey: [#0,0,#0,0] +seqnums: [1,8] + +collectors +---- +0: suffix + +table-props +---- +0: [0, 10) + +block-props +---- +c#72057594037927935,17: + 0: [2, 6) +e#72057594037927935,17: + 0: [3, 10) +g#72057594037927935,17: + 0: [0, 3) +i#72057594037927935,17: + 0: [3, 9) + +# Test an interator with a bound-limited filter that has a filtering criteria +# too narrow to exclude any blocks. + +iter filter=(suffix,1,20) +first +next +next +next +next +next +next +next +next +---- + filter.Intersects([2, 6)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + filter.Intersects([3, 10)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + filter.Intersects([0, 3)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + filter.Intersects([3, 9)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=false + +# Test an interator with a bound-limited filter that excludes one block, the +# third block. + +iter filter=(suffix,3,20) +first +next +next +next +next +next +next +---- + filter.Intersects([2, 6)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + filter.Intersects([3, 10)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + filter.Intersects([0, 3)) = (false, ) + filter.KeyIsWithinUpperBound(g) = true + filter.Intersects([3, 9)) = (true, ) + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=false + +# Test the same case but with an upper bound set that prevents skipping the +# block. + +iter filter=(suffix,3,20) filter-upper=f@9 +first +next +next +next +next +next +next +next +next +---- + filter.Intersects([2, 6)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + filter.Intersects([3, 10)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + filter.Intersects([0, 3)) = (false, ) + filter.KeyIsWithinUpperBound(g) = false + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false + filter.Intersects([3, 9)) = (true, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=false + +# Test a case that filters the first two blocks. The third block is not filtered +# due to block-property intersection. The fourth block is not filtered due to +# the upper bound. + +iter filter=(suffix,0,1) filter-upper=h@6 +first +next +next +next +next +---- + filter.Intersects([2, 6)) = (false, ) + filter.KeyIsWithinUpperBound(c) = true + filter.Intersects([3, 10)) = (false, ) + filter.KeyIsWithinUpperBound(e) = true + filter.Intersects([0, 3)) = (true, ) + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false + filter.Intersects([3, 9)) = (false, ) + filter.KeyIsWithinUpperBound(i) = false + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=false + +# Test a similar case in reverse. In reverse if the very first block is reached, +# we do not know whether or not it's actually within the bounds because we don't +# have another index separator to bound the block. As such, there's no call to +# KeyIsWithinLowerBound for the first block of the sstable [ie, the last one +# visited by the iterator]. + +iter filter=(suffix,9,10) filter-lower=a@0 +last +prev +prev +prev +prev +---- + filter.Intersects([3, 9)) = (false, ) + filter.KeyIsWithinLowerBound(g) = true + filter.Intersects([0, 3)) = (false, ) + filter.KeyIsWithinLowerBound(e) = true + filter.Intersects([3, 10)) = (true, ) + MaybeFilteredKeys()=true + MaybeFilteredKeys()=false + filter.Intersects([2, 6)) = (false, ) + MaybeFilteredKeys()=false + MaybeFilteredKeys()=false +. MaybeFilteredKeys()=false + +# Add tests with other non-limited filters set, including one with the same +# Name. diff --git a/pebble/sstable/testdata/buffer_pool b/pebble/sstable/testdata/buffer_pool new file mode 100644 index 0000000..7016162 --- /dev/null +++ b/pebble/sstable/testdata/buffer_pool @@ -0,0 +1,84 @@ +# Each command prints the current state of the buffer pool. +# +# [ ] - Indicates a cell within BufferPool.pool's underlying array that's +# unused and does not hold a buffer. +# [ n] - Indicates a cell within BufferPool.pool that is not currently in use, +# but does hold a buffer of size n. +# < n> - Indicates a cell within BufferPool.pool that holds a buffer of size +# n, and that buffer is presently in-use and ineligible for reuse. + +init size=5 +---- +[ ] [ ] [ ] [ ] [ ] + +alloc n=512 handle=foo +---- +< 512> [ ] [ ] [ ] [ ] + +release handle=foo +---- +[ 512] [ ] [ ] [ ] [ ] + +# Allocating again should use the existing buffer. + +alloc n=512 handle=bar +---- +< 512> [ ] [ ] [ ] [ ] + +# Allocating again should allocate a new buffer for the next slot. + +alloc n=512 handle=bax +---- +< 512> < 512> [ ] [ ] [ ] + +release handle=bar +---- +[ 512] < 512> [ ] [ ] [ ] + +release handle=bax +---- +[ 512] [ 512] [ ] [ ] [ ] + +# Fill up the entire preallocated pool slice. + +alloc n=128 handle=bar +---- +< 512> [ 512] [ ] [ ] [ ] + +alloc n=1 handle=bax +---- +< 512> < 512> [ ] [ ] [ ] + +alloc n=1 handle=bux +---- +< 512> < 512> < 1> [ ] [ ] + +alloc n=1024 handle=foo +---- +< 512> < 512> < 1> <1024> [ ] + +alloc n=1024 handle=fax +---- +< 512> < 512> < 1> <1024> <1024> + +# Allocating one more should grow the underlying slice, and allocate a +# new appropriately sized buffer. + +alloc n=2048 handle=zed +---- +< 512> < 512> < 1> <1024> <1024> <2048> [ ] [ ] [ ] [ ] + +release handle=bux +---- +< 512> < 512> [ 1] <1024> <1024> <2048> [ ] [ ] [ ] [ ] + +alloc n=2 handle=bux +---- +< 512> < 512> [ 1] <1024> <1024> <2048> < 2> [ ] [ ] [ ] + +init size=0 +---- + +alloc n=1 handle=foo +---- +< 1> diff --git a/pebble/sstable/testdata/h.block-bloom.no-compression.sst b/pebble/sstable/testdata/h.block-bloom.no-compression.sst new file mode 100644 index 0000000000000000000000000000000000000000..8dd2188e72cb4ad48dcd2581da54f6679498f740 GIT binary patch literal 30659 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�TaYS{H0j&UYfIS(_hj2EzdOYiNo{dL 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 literal 0 HcmV?d00001 diff --git a/pebble/sstable/testdata/h.no-compression.two_level_index.sst b/pebble/sstable/testdata/h.no-compression.two_level_index.sst new file mode 100644 index 0000000000000000000000000000000000000000..b0e85a04e1935edcef37c25f766a9334a16dad51 GIT binary patch literal 28539 zcma)_Ym8*ub=T`u_uQGD=bd|V^TCn85)tGB?o3tnV}=_mB1T5SkPrp&0YpZpyH0o4 zovJ$RdUVfR5rQHjB|L;gBpyOwDOM5$1cwhKhJ3Ke133y1$9X7Df+56;LVyskt=P`P z{QjqJt@YoX7%@^wb6)$gp8vJ>+Pix@{krO1{d;m)-K&n;dNJPI8E)OK)+@idXf{`a z{@rTP*72^cm+Smi*Xw1I&uRSlxV0Cy?^Nq~v_9y+Qk_kji)Pl$SIg|>e)ZmUJ@NN; zs`zMs>rORk=kfMSJ5_#KJ*dXx$;zK^G3I8Ouk`L!^?1^(Hx2CdU#@18MO}Qo$no{{50by3e(^=aPH;EwYW0-T)Glf|a9;nw}CIrfG}{W}%SZeAGNxLeIz zn_hLde+n()`RK;&YMI2ScB-?@vtf0+%C7de3c}o}rnM2_PQRW`Hy@4;7sllI%~7fJx1KlV~%^*#ghGBIR=3&huF6c?RD7kn!@~ zkZ)5Q_fg`UCsw*y4tGfJN^6p#T<9rZtjd8VKSFZlX5lOx+jN}HzZim z!hSdHRnvsh59G?fz;CwT8ruWj{u|9QLD`}mDs?;|hudLO#k>+R^J-+JwNxE zsE0XqvqwW_w=j?~^w~)`-b`VT9yPTICo_uajc$}fh6_2y-)BCqVNfmgZ-7#?&)f*f z9+-EQ3g%>F`4($avkE&jjbUlbi;0;I^%Wc%GJ{aBK_PQPtPVnB@Nuti-e~`^Mr8)A z00jf=ZJ5H*0Goeom}_qfmo%Ff{sxws--BHt90I+NBbbj7IIidO6!D?^z!8(kM}m|S zM_JdVxKo`+y8y67bF!XJW8edc<-{Q_VC97=k|>s@QDT|?VT1sf4@81jCOL2gADxC2 zQ|Z9HdRR5n=17P;)O3JgA9HuPn$-Cr5Mo*{e2j?CoXEsXmL^u35An`~egK9$t3e1M z7z0M{(EdJ3a%^_8d_ICM69yKv#9pmx%ud`u&Qg70=+XcRq-l)5T$*kSQ7dm*+&^nA z&Jeh44GC0VLLhLwo-L2TK}Nls8j^vaA$l^kcn)3zlM-w{hK_(%mxkNbdCo|~JV_^s zNYCmOWKR+Nr}^Bx-h4CktM3 zI6M%8=Jx%T;i18yTjoR8rU8Ni%*a-mL?QolTBq$D+~gWRm28K3`FP0VQYu_m1$)jB zviukZfOqo-uI#v-olm(NFUhxmhTpGn=Vt%_{|*4~AppRS004d*0Px`r0Qhs?_@_s` zSKeyixhvbY!rNuNYD>(&s2$0pRL70$-OJUuopx#!V-c~;UJ7PG^mt6%kjjkSzIWX$ z+O%tYy|memw(m<|SEU>fa2IBimpaAFhUJFLCT%^$Ld}8y-Vox|rg(L-bnZSvrf}X& zO&esg+j;HL`+!@Eb(txUd5sARD=ocS6vAT&UN({6dbz@9&kGd=c^Xe{-jQ$xIT1xf z!7mrqcNt^4m%}9#(4dp5^v<@|-`pRcFW8>xt;3<*|FULogPnJ(V6TQ8!~+u;vlXgVDfhf zhLg&WDUGM1z&FUB+1qqHEj~v$CLY`LNC~3G7KaY|uaMB1FIOeB=-=zlXSIdeVnb_Y z7F8z4lS88?414-It`}~0r23Q7g5!xu4lsV_WjgT|3SqF#;ra-gWk<-?b2vV+NSmZk zhG2ue0^$vNcPf$}8%8HmeSzUxi`xc7SKCCEf5iOT-N93EvZ`L2pKz&jpYxR z>G2eJlAZDjZaXd2KqkB?4ePz?4Keo#j2z~UZh0ycB;H! zP!=mIQDAmm-MT25=`E7?3g=RAopQJ3<_+^m)EMV1Q$b zak}*iQ}`h_rYz8e@OANB^tOkGpUf=kMYB$oru95V2D{;D)PWsfMCf8 zse4XLV?rx7qeKo3;ag)%gOAaT%;yMRock)CKn@+6X9B@C&k0}L69K0e^+kPJfL2#l zWsyS94^gF)I+-khIyNJx5hb!p^6&Jz*F)A7GAPZQ$Z;7|z<1(rM zB3aUu!Lv%b>0ep&3yaSc@7o&K6XnL;%ei8^iYRw>V)GIb&GkvmBb?Sqx#*b49tduc z9F=i6wXD#HYiJ+zl5tD0sWdp&Itjtkq_vhk^HGVnoUqEJ+Iw;>uE#so^9{AMTz4|9 zJ??RKNidpa7_y;!`hVy5Q+)q19RDxi^nU@T|52R&2XXrUcoX#hm+yc7k6j5-Wy!D=nBrh1QX1d|gFWAP_* za+bmJgJQ}^{5tWXl)LcH5V>5JG1_U38Q3>96U|UEU#w{Mr0`Os8+ZGY<#8a{!6P`+ zBumjEv&me&na^Z)3f)w4Gj&6OvCE1}18KZ|qTr$IVS-n!;_#vME5Vx-eU(T~rhZPK z6hbuO#$xWWi|;!jd0|#+4}m|eox6}0$Q-6gY8e+-!fm2Z@C>4P%9GBIXnCWxk|L5G zafz0$3_HpoS~Ubb1g_X*26$axqoFmHJ)^DCrKIIhfzns>mUKfG~k}H7pHADt3^B;MrXgH6Bd;FX@`c7N^!qRUd77?MJS?7 zv2jJ7;JnDQ6-yq)r;G?>7^7D?kEcUXh7=b0>(U)Com_MtVZ=@slQ-Y=fL4UeRG1R~ zw(ckY9(0u#PcphlYrSvnae49l>0yE=;+G4bvw%_iF{H@0f%J}$1b5eb9fBiT1cs8z0=Q!wZoIUD z7gAfmC3%4m7Zn;=E&?alPgg1fn2Iw}cN5ET_ilryHl2@D_E5(-4-itfL&9%fVj`#i zzxjQV?>~Xl|2R(nBRKu<$LW6;PXBLioc>?Jj%~;?vqJgGQ{cBD&x8Y zqxX$jpE)!kFrQpouVNCjW<2o(3`8e!UuIf~qw5q&*zAe#(emaMCh@GP86*0{+5DG)y)3z0MHh+dR@Qp2z=3KVf0 z?IN^)(0_=XBj;a;3&xLpswt#WK9=qnvaY*8WGGTD*jbRHFvaM%841uuRw!u*l+3Nl zM3>m;h?m9I^;8(w^jP#L0v!|~XmOxK%W@qqQS3;S9;&J@-jy#k^Y;XxGPwwKgBowt zWziO%A*q7`wZ{gRglueyVF*|&QO*34ncrl;rEjaa+;t|nH_wT z8L*BLF`q7(7A(V`X0`QDD-VfPmCA|Fq|z6@N<^R=;nwYhMp3WDo#9oLFBW2h?ZEM%DhnM8aVbDV)WW1ftC{4MA+vYx@$=(d=C=5qBG%mEOEeCo#nsf{3$lAH8+IuSJ=x z{@atRM_8+jS?Et!tn5{x+Ln}>sZ=(l6dq*LRCHa$)m?>{5sT?uG)X%o7y}ZsRBUXxP zk?T34VvJEWKn73ntjne2ciIV+=ob0^7k>Zep~Xk>`ya&bzYo9v*YW#*8NYwF3H`tO zec$x2@U=6l{K1AX%^@iL_g4K|t`T%J8ISD-BRBJUs50WxzE)mh?H)?&YB5Vyx=?aa zq-Idlbhl+LK=d;2g+^P6sD!04h3>P;57e}_#Eo{+n5xNmB^hCCPEk5=9nsCR^D>>m zz*_TNOfi_uO1|yM8a(l5*MvuQHit9RI;V{u@wP1X^{tMGOL`Xp9_{UlLCE%+;pi&a zg9+uTq=4^5s;jC1tSC|2@)m|J${`Q-z~`xS;BzpXVe&ov*StOY0-sl=og&1@Wk_Dd*{$ZJGwLeRFXsq+t!m;13|`;A-HpX zW-ypxuY4&-Vw=plB5)XG#W0DHk&YN5Ua0TMt>LgRq;ef|JvTGqIks*Wi{;gsMp;6G zNsMp~VPne_tIefyq&mp$CV`cFiWyK+#%8iQ_>PTBocQ>W>$?E6FC?m+ocxoXjcD`; z$*WW0gnd{hh+c%@Rqq$(AMZMyoL)24?A&VrY>seMPsY|<3(lw|%4Vp+F04Hk3Mb=c zgv`$IalI_EE4SHO93w#qB9${Z6ocdP00MXWUSB#YM1ydJB=rNtStN>*&;Ggd!_h`IVG z&bY0Tyq(vn0L4nL!ks{eO3Q{=${4VPFNuVU$wsstHD9HXkh=q_50yxpkabSeb%5Po zx}^~ezmutPxb_P7847=-TEqh98x9wRwoN(ZbEC}XSGFP z%8;?SBP`_rC1sz5=U>Pe&aXjZfDgg$vPkXXZ;V&yU!Cx>M9Y|g^};vI!056LONOG9 zeh8S{X3rA4e%obxewH^SEp^6EBcyxMB=MBOPEq1zHrIy&$!{ACuCdDn4LESl?#W=0 zVs}$1$UAS!1mw!uq1^j=>E+9aP0T?+sYr0*4T`!!M(vq2>IB$Viv=+qrG5 zz|F%+5QnpyskSDWW+$J}I>KdsUV7Co&XEnzH6R|!a~6$ux@A!(x|+J-ilR_HZI0yK zwq|O*dy&rYutTLOsmuocjvb7lD&+E-U8Ax&dXZXA9plR@gVV>-}=MGz~(OF5FiZpECEBEm-bCcwf*g>=`4U z)~sX24F)sDtA|!9x}3J`^~yI?)tM?Z&_)^RM30@stPGqlP(13$b6KuiLC|QjHvpEk z1Si+Fg$=G}jh8}euOVR~@`X97q9yUPh>wh0Wfd4Un%Yp75Mst+mhHIib|X6thqtaT zrLhcxeodynhJ^_L2W8xROOp>?!Z5kdlK}ZS9w=}Qgw~22E@L@2ZprIwSO4b_x6k19 zKabb{JYN5|iTD4{CePpe`>+2_;M1bfide`cPuMSEIo4P>4ciOO4C_(nnam7)%FDAd>tc7#$**v7HB?zZ8wSj4bqo|5EbDT&xdqQc zQsh}fmV_baAA@`UWVdeSXEFV=(X7587aUn7X7-fnG`|D}@ZgNFm2-LlPb#YR_L`DnU-*BKj zMJg;;=1*WkWORxJv0*pWMRVx|I})j+lziekQHFAl4pT8B22&YtVcfxm1tO-c!qH>F zy-u@;tRSngmMsjZ&pQMyrC$}RrFboKr+N4ggHuCa4L^B%r18TK^=#d7?@nx>=I?hgQ1K{k`na*r{2?q|lwyRIGJ+PP_S7^9pHJKp9g}9eKGu5H6IkXlC0`#cnH% zrzIOyQL?~@h-VV4AK)ybSD~$@NGZvZhm>^KjdS=?8svHrj^sb6DQDFAh0Hx_lm7bX zs9ns9b$FU6kC-T$z`U-He8E6F!c`FG%5%GKYVb(NsMx=OP02YFs0P$KJwIEQSt@KM z`%O2h(WO@{*C6MxHiJdh%#Af3`uy4<5wNmd)?pKcf6AJ)()>`{qur~f%8CX{4 zVFl=z4~{y#$~MKoA#8aX(!=VqKBa!j)E8{lxBYtqZK*H|gjkt)J09wU|I)yTM4^cB zP!>BUR?s`CMu3w9R;^5cX@Nxthpz11`67n%a1SYp4BdJtwuZ|oy6(8u;n6Z}5KbaC z?+sAzm7j|y=aglu-B#~lsuoxaoFbR6y?h^o9sd!R|4(uG{}7k|K3x9y;PStHguSaVwY$gJR9uO zNv40XR>{K0!czes3HY`$P8ku5&C6z)E93BJYli@iAyNm&6A9z;pA{ zkEveJhQN)y?_tNSG@h3&=8(sCZ0I4W_G?%Q2r-H_w7aNLr(oeb3f{$yJ7Sm!Y?^>WqehKGfCk zY{i=Q;E4+Vb83Ay+k^8{{^h18mLZb%hpH%Uo?9N8)Bmbyn&F${#5pWfN>VVEOj%1| zh27JNwpT){#f;}sjF`uOC1`SNMC~ygd0g_*_Pzuuzd-M)l`n!{ol8vj`40Va-cMfR zU%b*(hM8>;MlibY5ojZ^%|}C8&(19nAiSQ`Vd_-CK(^3NIw?^-<5^ocUerhJgW-wY z=Qd%uV^=bxnF%X|TFZrVa?p;>%E(PHC?lAw(!rRALN~V3XAV?0APo2^mjA^0bLos3ryDwge$;&kzsa zoM``rpFqLN%n7N*VnIVVFwC`q|Gcw_<|3wS2@8iL;0OHbXi1j~xl((J7g02$-S$Xh+%6D5O^ zi83A+*B1DZw$mb$jON+ASiq^%J!``>fsn!f;+)hjJYA(CjvNaqSK_9Wj*H?<#N0za zJYSN5Oo|`Rxeu?Vs|n&A0;X~*uYQ0#={WI|7i|KN5lmQ5PwR-hbnZuS^0=H<;EN(5 zCsX44j_4OUIc)i?9Zm z%gfpdcvRH(Ji|me@aU;rNJYYf_g6b8Vk;Dr^*Nzx6!NMfTJbEsqbrX+GCmOS7r?HbC(pun6m$#jE4k;a-9uD zR0$JKPWg^(nzJmJa1vS`HxWB2Mq4$KzK~K*j*8NrRQ7EpvPbCx1-*lRLml4k+`h@H zA&Jt4V2BFIIDaJ6y9}MM4ffgfY+^10$Q~_yCstvPuo%9k%cZZdL%JA^h-3N&S~y;? zlR3!DBtxPP6se`jWzoN%!Z7Ia8Tsd6Mi}JF$*RoAbX7L&^9lH&O|N-!jI`ObT?!lr zuZiIyvx+D^Gfq{XFPI3NxIHe&kAZE+#}taEeS)tUQkBU*J`F7t8$;VW92ZO+iK$+( z23Zp@mT?$*u{JT17Ny`zAUWR9SR$wYDSp2OPd<***CsyY`2P_B|6jxDFE#=H&wa-a z{R0id(y!N7-k@)55QodPU6O%%k*kj)c#vq1DjG0VS6>{m;c}$I?j{W>d23I+HI{i^ zfk-~0&jFFTr{-AX&8pD7CyF{O);laHyD#c=1N!q{@q_)|-7WrL!O6ofT>a~P?m_Rt z_SF%{>&?yIeel~4dUv-u+whIetuOq+-+a)!vwbp+FMsPRKdqNI5~10=^y#nujR)%N z6@FjA?{$8Eh96w$^>%;s%ihzwz5Og2_-|kLc{Rz#iVd}X^(md)+g8D4y!FZUGwOzt zjM=7}Pki(L>uINsdQ1yz~7mv0IP!aQp)H?I+`!2*sDk3ne*d$a3u>6mqhr$=RvV(I$WEj9MIKAoJ;?GLet9o}ytjuN`S8{sxW!q6-RX%> zm&?^th|}zzPN{pGK4mMyycMbS?`=1?S*X*zL`_a5xFTcMZ0|P5B3`w)izu>Vx3@!` z+hSI)cHjD_ck4CqhZrJSPnNp`(F^^*cc1D36_R(Aja&D(`x@xq-@5;$SMjFa<Qn&|7`ne)whTeDz`% zK~en&b?ATbJA=rFOXa4Xy=(L6FWtJkwWu$<)-}0r`N8Tg$Lc7W?#TDq`?tP? zY}ffaIzf0#ZuR23u@Er68`nF!!$b#%Ls{1=m-2O=`ert0je0)7UgF7SAVYh zog2M}@uZvZQzrbgKV5xSXXf5U8{%W{ue|#s|E&6!PgGz1H-5T$w{HBu{^^U~{E1J0 M>ks_#|N3qJ7m4tc_5c6? literal 0 HcmV?d00001 diff --git a/pebble/sstable/testdata/h.sst b/pebble/sstable/testdata/h.sst new file mode 100644 index 0000000000000000000000000000000000000000..cc5c23b4a488fb30f3377d0754ba58525a0088e4 GIT binary patch literal 15432 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 literal 0 HcmV?d00001 diff --git a/pebble/sstable/testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst b/pebble/sstable/testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst new file mode 100644 index 0000000000000000000000000000000000000000..0978e58904d32f195cf5d8797236be28f905806d GIT binary patch literal 30716 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 literal 0 HcmV?d00001 diff --git a/pebble/sstable/testdata/h.table-bloom.sst b/pebble/sstable/testdata/h.table-bloom.sst new file mode 100644 index 0000000000000000000000000000000000000000..916496f29aecdb6c10226e6a002dea7ed33b0958 GIT binary patch literal 17722 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 literal 0 HcmV?d00001 diff --git a/pebble/sstable/testdata/h.txt b/pebble/sstable/testdata/h.txt new file mode 100644 index 0000000..ed8f750 --- /dev/null +++ b/pebble/sstable/testdata/h.txt @@ -0,0 +1,1710 @@ + 97 a + 2 aboard + 2 about + 1 above + 1 abroad + 1 absurd + 1 abused + 1 accord + 1 account + 1 achievements + 1 acquaint + 5 act + 1 action + 1 actions + 1 addition + 1 address + 4 adieu + 1 admiration + 1 adoption + 1 adulterate + 1 advantage + 1 advice + 2 affair + 3 affection + 1 after + 1 afternoon + 13 again + 5 against + 2 ah + 5 air + 1 airs + 1 alas + 36 all + 1 alleys + 1 allow + 4 almost + 4 alone + 2 along + 1 already + 1 always + 9 am + 1 amazed + 1 ambiguous + 1 ambitious + 13 an + 227 and + 1 angel + 1 angels + 1 anger + 1 angry + 1 another + 4 answer + 1 antic + 6 any + 1 apparel + 2 apparition + 4 appear + 1 appears + 1 appetite + 1 approve + 1 apt + 21 are + 2 arm + 2 armed + 1 armour + 2 arms + 1 arrant + 6 art + 1 artery + 1 article + 1 articles + 56 as + 1 aside + 1 asking + 1 assail + 1 assistant + 2 assume + 18 at + 1 attendants + 1 attent + 1 attribute + 1 audience + 2 aught + 1 auspicious + 1 avoid + 1 avouch + 1 awake + 7 away + 2 awhile + 7 ay + 1 baby + 1 back + 1 baked + 1 bark + 1 barr + 1 base + 1 baser + 1 bawds + 42 be + 5 bear + 1 beard + 1 bearers + 1 bears + 2 beast + 1 beating + 1 beauty + 1 beaver + 2 beckons + 4 bed + 4 been + 1 beetles + 1 befitted + 6 before + 1 beg + 1 beguile + 1 behold + 1 behoves + 4 being + 1 belief + 6 believe + 1 bell + 2 bend + 5 beneath + 1 benefit + 30 bernardo + 2 beseech + 1 besmirch + 5 best + 1 beteem + 1 bethought + 2 better + 2 between + 2 beware + 1 beyond + 2 bid + 2 bird + 3 birth + 1 bites + 1 bitter + 1 black + 1 blast + 1 blastments + 1 blasts + 1 blazes + 1 blazon + 3 blessing + 7 blood + 1 blossoms + 1 blows + 1 bodes + 5 body + 1 bonds + 1 bones + 1 book + 1 books + 2 born + 1 borrower + 1 borrowing + 1 bosom + 3 both + 2 bound + 1 bounteous + 1 bow + 2 boy + 2 brain + 1 bray + 1 brazen + 1 breach + 3 break + 1 breaking + 1 breath + 1 breathing + 1 brief + 1 bring + 1 brokers + 6 brother + 1 brow + 1 bruit + 1 bulk + 1 buried + 2 burns + 1 burnt + 2 burst + 4 business + 58 but + 1 buttons + 1 buy + 31 by + 4 call + 1 calumnious + 2 came + 5 can + 1 canker + 2 cannon + 3 cannot + 1 canon + 1 canonized + 2 canst + 1 cap + 1 carefully + 1 carriage + 1 carrying + 1 carve + 3 cast + 2 castle + 1 catch + 1 cautel + 1 caution + 1 celebrated + 1 celestial + 1 cellarage + 2 censure + 1 cerements + 1 certain + 1 chances + 1 change + 1 character + 3 charge + 1 chariest + 1 charitable + 1 charm + 1 chaste + 1 cheer + 2 chief + 1 chiefest + 2 choice + 1 choose + 1 circumscribed + 2 circumstance + 1 clad + 8 claudius + 1 clearly + 1 clepe + 1 cliff + 1 climatures + 1 cloak + 2 clouds + 5 cock + 2 cold + 1 coldly + 1 colleagued + 1 colour + 1 combat + 1 combated + 1 combined + 17 come + 7 comes + 1 comest + 1 comfort + 1 coming + 1 command + 1 commandment + 2 commend + 1 commendable + 4 common + 1 compact + 1 competent + 1 complete + 1 complexion + 1 compulsatory + 1 comrade + 1 conceal + 1 condolement + 1 confess + 1 confine + 1 confined + 1 conqueror + 3 consent + 1 constantly + 1 contagious + 1 contracted + 1 contrive + 1 conveniently + 1 convoy + 1 copied + 4 cornelius + 1 coronation + 1 corruption + 2 corse + 1 costly + 1 couch + 2 could + 2 countenance + 1 country + 1 countrymen + 1 couple + 2 course + 1 courses + 1 court + 1 courteous + 1 courtier + 2 cousin + 1 covenant + 1 crack + 1 credent + 1 crescent + 2 crew + 1 cried + 1 cries + 1 crimes + 1 cross + 1 crowing + 2 crown + 1 crows + 1 crust + 1 curd + 2 cursed + 3 custom + 1 customary + 1 cut + 54 d + 1 daily + 1 dalliance + 1 damn + 2 damned + 3 dane + 1 danger + 1 dared + 1 dares + 2 daughter + 1 dawning + 8 day + 1 days + 7 dead + 4 dear + 2 dearest + 1 dearly + 6 death + 1 decline + 1 deed + 1 deeds + 1 deep + 1 defeated + 1 defect + 1 defend + 1 dejected + 1 delated + 1 delight + 2 deliver + 1 demonstrated + 13 denmark + 1 denote + 1 depart + 1 depends + 1 deprive + 1 design + 6 desire + 1 desperate + 1 desperation + 3 dew + 1 dews + 1 dexterity + 14 did + 1 didst + 1 die + 1 died + 1 diet + 1 dignity + 1 direct + 1 dirge + 1 disappointed + 1 disasters + 1 disclosed + 1 discourse + 1 discretion + 1 disjoint + 2 dispatch + 3 disposition + 1 distilled + 1 distilment + 1 distracted + 1 divide + 36 do + 3 does + 1 dole + 3 done + 1 doom + 1 doomsday + 7 doth + 2 double + 4 doubt + 1 doubtful + 7 down + 1 drains + 1 dram + 1 draughts + 1 draw + 1 draws + 1 dread + 1 dreaded + 2 dreadful + 1 dream + 1 dreamt + 1 drink + 1 drinks + 1 dropping + 1 droppings + 1 drum + 1 drunkards + 1 dull + 1 duller + 1 dulls + 2 dumb + 1 dust + 1 duties + 7 duty + 1 dwelling + 1 dye + 1 e + 5 each + 2 eager + 1 eale + 5 ear + 3 ears + 9 earth + 1 earthly + 2 ease + 1 east + 1 eastward + 1 eclipse + 1 edge + 2 effect + 1 eleven + 4 else + 2 elsinore + 1 embark + 1 empire + 1 emulate + 2 en + 1 encounter + 1 encumber + 1 end + 1 enemy + 1 enmity + 1 enough + 12 enter + 1 enterprise + 1 entertainment + 1 entrance + 1 entreated + 1 entreatments + 1 equal + 5 er + 4 ere + 1 ergrowth + 1 ermaster + 1 erring + 1 eruption + 1 erwhelm + 1 esteem + 1 et + 1 eternal + 1 eternity + 8 even + 1 events + 6 ever + 1 everlasting + 3 every + 1 exactly + 1 excellent + 8 exeunt + 6 exit + 2 express + 1 extinct + 1 extorted + 1 extravagant + 6 eye + 7 eyes + 2 face + 1 faded + 1 fail + 3 fair + 1 fairy + 4 faith + 1 falling + 1 false + 1 familiar + 1 fancy + 2 fantasy + 2 far + 2 fare + 8 farewell + 3 fashion + 2 fast + 1 fat + 2 fate + 1 fates + 28 father + 1 fathers + 1 fathoms + 4 fault + 2 favour + 9 fear + 1 fearful + 1 fed + 1 fee + 2 fell + 2 fellow + 3 few + 4 fie + 1 fierce + 3 figure + 1 filial + 2 find + 1 fingers + 4 fire + 1 fires + 1 first + 2 fit + 1 fits + 1 fitting + 2 fix + 1 flames + 1 flat + 2 flesh + 1 flood + 1 flourish + 1 flushing + 1 foe + 9 follow + 1 follows + 1 fond + 1 food + 1 fool + 1 fools + 1 foot + 45 for + 1 forbid + 1 forced + 1 foreign + 1 foreknowing + 1 foresaid + 1 forfeit + 1 forged + 1 forget + 4 form + 2 forms + 3 forth + 1 fortified + 6 fortinbras + 1 forts + 1 fortune + 1 forward + 1 fought + 6 foul + 1 frailty + 1 frame + 3 france + 10 francisco + 1 free + 1 freely + 1 freeze + 1 fretful + 3 friend + 1 friending + 5 friends + 21 from + 1 frown + 1 frowningly + 1 fruitful + 2 full + 3 funeral + 1 furnish + 4 further + 1 gaged + 2 gainst + 1 gait + 1 galled + 1 galls + 1 gape + 1 garbage + 1 garden + 1 gates + 1 gaudy + 1 general + 1 generous + 1 gentle + 5 gentlemen + 4 gertrude + 1 get + 26 ghost + 1 gibber + 3 gifts + 1 gins + 1 girl + 13 give + 4 given + 3 giving + 2 glad + 1 glimpses + 1 globe + 1 glow + 15 go + 1 goblin + 8 god + 3 goes + 1 going + 4 gone + 20 good + 1 goodly + 6 grace + 1 graces + 2 gracious + 1 grapple + 1 grave + 1 graves + 1 great + 1 greatness + 2 green + 1 greeting + 3 grief + 1 grizzled + 2 gross + 3 ground + 2 grow + 1 grown + 2 grows + 1 guard + 2 guilty + 1 ha + 2 habit + 13 had + 1 hail + 1 hair + 1 hallow + 100 hamlet + 5 hand + 4 hands + 2 hang + 1 hap + 1 happily + 1 harbingers + 2 hard + 1 hardy + 1 harrow + 1 harrows + 3 has + 4 hast + 7 haste + 1 hatch + 15 hath + 31 have + 1 havior + 34 he + 6 head + 1 headed + 1 headshake + 3 health + 9 hear + 4 heard + 1 hearing + 2 hears + 1 hearsed + 10 heart + 3 heartily + 1 hearts + 1 heat + 21 heaven + 1 heavens + 1 heavy + 1 hebenon + 1 height + 1 held + 3 hell + 2 help + 8 her + 1 heraldry + 1 hercules + 11 here + 1 hereafter + 3 herein + 1 hic + 1 hideous + 1 hies + 2 high + 1 higher + 1 hill + 2 hillo + 21 him + 3 himself + 57 his + 1 hither + 1 hitherto + 5 ho + 9 hold + 1 holding + 2 holds + 1 holla + 1 holy + 2 honest + 5 honour + 1 honourable + 1 hoops + 85 horatio + 4 horrible + 1 horridly + 1 host + 1 hot + 6 hour + 2 house + 7 how + 1 howsoever + 1 humbly + 1 hundred + 1 husbandry + 1 hyperion + 124 i + 1 ice + 22 if + 1 ignorance + 1 ii + 1 iii + 1 illume + 1 illusion + 1 image + 1 imagination + 1 immediate + 1 imminent + 1 immortal + 3 impart + 1 impartment + 1 impatient + 1 imperfections + 1 imperial + 1 impious + 1 implements + 1 implorators + 1 importing + 1 importuned + 1 importunity + 1 impotent + 1 impress + 118 in + 1 incest + 2 incestuous + 1 incorrect + 1 increase + 8 indeed + 1 infants + 1 infinite + 1 influence + 1 inform + 1 inheritance + 1 inky + 2 instant + 1 instrumental + 1 intent + 1 intents + 5 into + 1 inurn + 1 investments + 1 invites + 1 invulnerable + 1 inward + 62 is + 1 issue + 126 it + 1 its + 9 itself + 1 iv + 1 jaws + 1 jelly + 1 jocund + 2 joint + 1 jointress + 1 joy + 1 judgment + 1 juice + 1 julius + 1 jump + 3 keep + 1 keeps + 1 kept + 1 kettle + 1 key + 1 kin + 1 kind + 23 king + 1 kingdom + 1 knave + 1 knew + 1 knotted + 17 know + 2 known + 1 knows + 1 labourer + 1 laboursome + 1 lack + 1 lacks + 16 laertes + 2 land + 3 lands + 1 larger + 3 last + 1 lasting + 3 late + 2 law + 1 lawless + 1 lay + 1 lazar + 1 lead + 2 least + 8 leave + 1 leavens + 1 left + 1 leisure + 1 lend + 1 lender + 1 lends + 1 length + 1 leperous + 2 less + 1 lesson + 23 let + 1 lethe + 1 lets + 1 levies + 1 lewdness + 1 libertine + 1 lids + 1 liegemen + 1 lies + 7 life + 1 lifted + 1 light + 1 lightest + 23 like + 1 link + 1 lion + 1 lips + 1 liquid + 6 list + 1 lists + 3 little + 3 live + 1 livery + 1 lives + 18 ll + 1 lo + 1 loan + 1 loathsome + 1 lock + 1 locks + 1 lodge + 1 lofty + 4 long + 3 longer + 10 look + 2 looks + 1 loose + 60 lord + 1 lords + 1 lordship + 2 lose + 1 loses + 1 loss + 5 lost + 1 loud + 8 love + 5 loves + 2 loving + 2 lust + 1 luxury + 2 m + 4 madam + 8 made + 1 madness + 1 maid + 1 maiden + 2 main + 1 majestical + 1 majesty + 8 make + 2 makes + 2 making + 1 malicious + 11 man + 1 manner + 1 manners + 1 mantle + 2 many + 1 marble + 46 marcellus + 2 march + 2 mark + 3 marriage + 2 married + 1 marrow + 3 marry + 1 mart + 1 martial + 1 marvel + 1 matin + 1 matter + 19 may + 47 me + 2 mean + 2 means + 1 meats + 1 meditation + 3 meet + 1 meeting + 1 melt + 5 memory + 3 men + 2 mercy + 1 mere + 1 merely + 1 message + 1 met + 2 methinks + 1 methought + 1 mettle + 1 middle + 7 might + 1 mightiest + 1 milk + 6 mind + 6 mine + 1 ministers + 1 minute + 1 minutes + 1 mirth + 1 mock + 1 mockery + 1 moderate + 1 moiety + 1 moist + 2 mole + 1 moment + 3 month + 1 months + 1 moods + 2 moon + 19 more + 3 morn + 3 morning + 28 most + 1 mote + 5 mother + 1 motion + 2 motive + 1 mourn + 1 mourning + 1 mouse + 1 mouth + 1 moved + 9 much + 3 murder + 14 must + 126 my + 4 myself + 2 name + 1 nations + 2 native + 2 natural + 13 nature + 7 nay + 1 ne + 3 near + 1 necessaries + 1 need + 1 needful + 1 needs + 1 neither + 1 nemean + 1 nephew + 1 neptune + 1 nerve + 6 never + 1 new + 2 news + 22 night + 1 nighted + 1 nightly + 3 nights + 1 niobe + 1 nipping + 28 no + 1 nobility + 5 noble + 2 none + 14 nor + 5 norway + 80 not + 2 note + 2 nothing + 19 now + 30 o + 1 oath + 3 obey + 1 object + 1 obligation + 1 obsequious + 1 observance + 1 observant + 1 observation + 1 obstinate + 1 occasion + 1 odd + 176 of + 6 off + 2 offence + 1 offend + 1 offended + 2 offer + 7 oft + 4 old + 1 omen + 25 on + 8 once + 6 one + 1 oped + 1 open + 15 ophelia + 1 opinion + 1 opposed + 1 opposition + 1 oppress + 28 or + 2 orchard + 1 ordnance + 1 origin + 4 other + 45 our + 2 ourself + 1 ourselves + 8 out + 6 own + 1 ownself + 4 pale + 1 pales + 1 palm + 1 palmy + 1 pardon + 1 parle + 1 parley + 6 part + 6 particular + 1 partisan + 1 passeth + 1 passing + 1 past + 1 pastors + 1 path + 1 patrick + 1 pay + 1 pe + 2 peace + 1 peevish + 2 perchance + 1 perform + 1 perfume + 1 perhaps + 1 perilous + 1 permanent + 1 pernicious + 1 persever + 1 person + 1 personal + 1 persons + 1 perturbed + 1 pester + 1 petition + 1 petty + 1 philosophy + 3 phrase + 1 piece + 1 pin + 1 pioner + 1 pious + 1 pith + 1 pity + 3 place + 1 plain + 1 planets + 5 platform + 1 plausive + 2 play + 1 please + 1 pledge + 2 point + 1 polacks + 1 pole + 13 polonius + 1 ponderous + 1 pooh + 9 poor + 1 porches + 1 porpentine + 1 portentous + 1 possess + 1 posset + 3 post + 1 pour + 3 power + 7 pray + 1 prayers + 1 preceding + 1 precepts + 1 precurse + 1 preparations + 1 presence + 1 present + 1 pressures + 1 prey + 2 prick + 1 pride + 1 primrose + 1 primy + 1 prince + 1 prison + 1 private + 1 privy + 1 probation + 1 process + 1 proclaims + 2 prodigal + 1 prologue + 1 promise + 1 pronouncing + 1 prophetic + 1 proportions + 1 propose + 1 puff + 1 pure + 1 purged + 1 purpose + 1 purse + 1 pursuest + 2 put + 1 puts + 1 quarrel + 7 queen + 2 question + 1 questionable + 1 quicksilver + 1 quiet + 1 quietly + 1 quills + 1 radiant + 2 rank + 1 rankly + 1 rate + 1 ratified + 2 re + 1 reaches + 1 rear + 5 reason + 1 rebels + 1 reckless + 1 reckoning + 1 recks + 1 records + 1 recover + 1 red + 1 rede + 1 reels + 1 relief + 1 relieved + 1 remain + 6 remember + 1 remembrance + 1 remove + 1 removed + 1 render + 1 reply + 1 report + 1 request + 1 requite + 1 reserve + 1 resolutes + 1 resolve + 2 rest + 1 retrograde + 2 return + 1 reveal + 1 revel + 3 revenge + 1 revisit + 1 rhenish + 1 rich + 1 rid + 3 right + 1 rise + 1 rivals + 1 river + 1 roar + 1 romage + 1 roman + 1 rome + 2 room + 1 roots + 1 rotten + 1 roughly + 2 rouse + 2 royal + 1 ruled + 1 running + 1 russet + 39 s + 1 sable + 2 safety + 3 said + 1 sail + 1 saint + 1 salt + 5 same + 1 sanctified + 1 sate + 1 satyr + 1 saviour + 6 saw + 1 saws + 11 say + 1 saying + 3 says + 1 scale + 1 scandal + 1 scanter + 1 scapes + 1 scarcely + 5 scene + 1 scent + 1 scholar + 1 scholars + 1 school + 2 scope + 3 sea + 2 seal + 4 season + 1 seat + 1 second + 1 secrecy + 1 secret + 1 secrets + 2 secure + 1 seduce + 7 see + 1 seed + 1 seeing + 1 seek + 2 seem + 1 seeming + 3 seems + 8 seen + 1 seized + 1 select + 1 self + 1 sense + 1 sensible + 1 sent + 1 sepulchre + 1 serious + 2 serpent + 1 servant + 1 servants + 1 service + 4 set + 2 shake + 22 shall + 1 shalt + 1 shame + 1 shameful + 2 shape + 1 shapes + 1 shark + 6 she + 1 sheeted + 1 sheets + 1 shift + 1 shipwrights + 1 shoes + 2 shot + 6 should + 1 shoulder + 1 shouldst + 6 show + 2 shows + 1 shrewdly + 1 shrill + 1 shrunk + 2 sick + 1 side + 3 sight + 1 silence + 1 silver + 1 simple + 1 sin + 1 since + 1 sinews + 1 singeth + 3 sir + 1 sirs + 3 sister + 4 sit + 2 sits + 1 skirts + 1 slander + 1 slaughter + 1 slay + 1 sledded + 1 sleep + 3 sleeping + 2 slow + 2 smile + 1 smiles + 2 smiling + 1 smooth + 1 smote + 48 so + 1 soe + 2 soft + 2 soil + 1 soldier + 1 soldiers + 2 solemn + 1 solid + 13 some + 3 something + 1 sometime + 1 sometimes + 1 somewhat + 3 son + 1 songs + 1 sore + 3 sorrow + 1 sorry + 1 sort + 8 soul + 1 souls + 2 sound + 1 sounding + 1 source + 1 sovereignty + 27 speak + 1 speaking + 1 speech + 1 speed + 1 spend + 1 spheres + 8 spirit + 1 spirits + 1 spite + 1 spoke + 2 spring + 1 springes + 1 squeak + 4 st + 1 stale + 1 stalk + 1 stalks + 1 stamp + 5 stand + 1 stands + 3 star + 2 stars + 1 start + 1 started + 8 state + 1 stately + 1 station + 7 stay + 2 steel + 1 steep + 1 sterling + 1 stiffly + 8 still + 2 sting + 2 stir + 1 stirring + 1 stole + 1 stomach + 2 stood + 1 stop + 1 story + 6 strange + 1 stranger + 1 streets + 1 strict + 2 strike + 1 strokes + 1 strong + 2 struck + 1 stubbornness + 1 student + 1 stung + 3 subject + 1 substance + 10 such + 1 sudden + 1 suit + 3 suits + 1 sulphurous + 1 summit + 1 summons + 2 sun + 1 sunday + 1 suppliance + 1 supposal + 1 suppress + 1 sure + 1 surprised + 1 surrender + 1 survivor + 1 suspiration + 1 sustain + 1 swaggering + 10 swear + 1 sweaty + 1 sweep + 2 sweet + 2 swift + 1 swinish + 5 sword + 2 sworn + 18 t + 1 ta + 1 table + 2 tables + 1 taint + 10 take + 1 taken + 3 takes + 1 tale + 1 talk + 1 task + 1 tax + 2 teach + 2 tears + 9 tell + 1 temple + 1 tempt + 1 tenable + 1 tenantless + 1 tend + 2 tender + 3 tenders + 2 term + 2 terms + 1 tether + 1 tetter + 15 than + 2 thanks + 83 that + 1 thaw + 237 the + 23 thee + 10 their + 10 them + 1 theme + 15 then + 18 there + 4 therefore + 1 thereto + 13 these + 1 thews + 14 they + 1 thin + 3 thine + 6 thing + 3 things + 16 think + 1 thinking + 1 third + 67 this + 1 thorns + 1 thorny + 7 those + 28 thou + 10 though + 2 thought + 4 thoughts + 1 thrice + 2 thrift + 1 throat + 2 throne + 3 through + 1 throw + 1 thunder + 9 thus + 36 thy + 1 thyself + 4 till + 10 time + 1 times + 22 tis + 192 to + 1 toe + 7 together + 1 toils + 2 told + 4 tongue + 9 too + 1 top + 1 tormenting + 3 touching + 4 toward + 1 toy + 1 toys + 1 traduced + 1 tragedy + 1 trains + 1 traitorous + 1 trappings + 1 treads + 2 treasure + 1 tremble + 1 tried + 1 trifling + 1 triumph + 1 trivial + 1 trouble + 1 troubles + 2 truant + 5 true + 1 truepenny + 1 truly + 2 trumpet + 1 trumpets + 1 truncheon + 1 truster + 2 truth + 2 tush + 3 twelve + 1 twere + 2 twice + 2 twill + 1 twixt + 5 two + 1 ubique + 1 unanel + 5 uncle + 1 undergo + 1 understand + 2 understanding + 1 uneffectual + 1 unfledged + 3 unfold + 1 unforced + 1 unfortified + 1 ungracious + 1 unhand + 1 unholy + 1 unhousel + 1 unimproved + 1 unmanly + 1 unmask + 1 unmaster + 1 unmix + 2 unnatural + 1 unprevailing + 1 unprofitable + 1 unproportioned + 1 unrighteous + 1 unschool + 1 unsifted + 4 unto + 1 unvalued + 1 unweeded + 10 up + 1 uphoarded + 18 upon + 19 us + 1 use + 1 uses + 1 usurp + 1 v + 1 vailed + 1 vain + 2 valiant + 1 vanish + 1 vanquisher + 1 vast + 9 very + 1 vial + 1 vicious + 1 vigour + 1 vile + 5 villain + 2 violence + 1 violet + 3 virtue + 1 virtues + 1 virtuous + 1 visage + 1 vision + 2 visit + 5 voice + 4 voltimand + 1 volume + 1 vow + 3 vows + 2 vulgar + 1 wake + 6 walk + 1 walks + 1 wants + 1 war + 2 warlike + 1 warning + 1 warrant + 1 wars + 1 wary + 17 was + 1 wassail + 12 watch + 1 watchman + 3 waves + 2 waxes + 2 way + 1 ways + 34 we + 1 weak + 1 wears + 1 weary + 1 wedding + 1 weed + 1 week + 2 weigh + 1 weighing + 3 welcome + 14 well + 1 went + 3 were + 1 west + 1 westward + 1 wharf + 42 what + 1 whatsoever + 8 when + 1 whence + 9 where + 1 wherefore + 4 wherein + 2 whereof + 1 whether + 16 which + 2 while + 1 whiles + 1 whilst + 1 whirling + 1 whisper + 8 who + 3 whole + 2 wholesome + 8 whose + 13 why + 3 wicked + 1 wide + 1 wife + 1 wild + 25 will + 1 willing + 1 willingly + 1 wilt + 2 wind + 2 winds + 1 windy + 1 wings + 1 wipe + 1 wisdom + 1 wisdoms + 1 wisest + 1 wishes + 2 wit + 1 witch + 1 witchcraft + 65 with + 2 withal + 11 within + 3 without + 1 witness + 4 wittenberg + 3 woe + 2 woman + 1 womb + 1 won + 1 wonder + 1 wonderful + 1 wondrous + 1 wont + 1 woodcocks + 3 word + 2 words + 1 wore + 2 work + 3 world + 1 worm + 1 worth + 1 worthy + 14 would + 3 wouldst + 1 wretch + 2 writ + 1 writing + 1 wrong + 1 wrung + 1 yea + 4 yes + 1 yesternight + 7 yet + 1 yielding + 1 yon + 1 yond + 110 you + 6 young + 49 your + 7 yourself + 5 youth diff --git a/pebble/sstable/testdata/h.zstd-compression.sst b/pebble/sstable/testdata/h.zstd-compression.sst new file mode 100644 index 0000000000000000000000000000000000000000..37de2506ad933c09dd082c6108ec6fc7a36c360b GIT binary patch literal 11826 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 literal 0 HcmV?d00001 diff --git a/pebble/sstable/testdata/hamlet-act-1.txt b/pebble/sstable/testdata/hamlet-act-1.txt new file mode 100644 index 0000000..2491678 --- /dev/null +++ b/pebble/sstable/testdata/hamlet-act-1.txt @@ -0,0 +1,1234 @@ +The Tragedy of Hamlet, Prince of Denmark + +ACT I + +SCENE I. Elsinore. A platform before the castle. + +FRANCISCO at his post. Enter to him BERNARDO +BERNARDO +Who's there? +FRANCISCO +Nay, answer me: stand, and unfold yourself. +BERNARDO +Long live the king! +FRANCISCO +Bernardo? +BERNARDO +He. +FRANCISCO +You come most carefully upon your hour. +BERNARDO +'Tis now struck twelve; get thee to bed, Francisco. +FRANCISCO +For this relief much thanks: 'tis bitter cold, +And I am sick at heart. +BERNARDO +Have you had quiet guard? +FRANCISCO +Not a mouse stirring. +BERNARDO +Well, good night. +If you do meet Horatio and Marcellus, +The rivals of my watch, bid them make haste. +FRANCISCO +I think I hear them. Stand, ho! Who's there? +Enter HORATIO and MARCELLUS + +HORATIO +Friends to this ground. +MARCELLUS +And liegemen to the Dane. +FRANCISCO +Give you good night. +MARCELLUS +O, farewell, honest soldier: +Who hath relieved you? +FRANCISCO +Bernardo has my place. +Give you good night. +Exit + +MARCELLUS +Holla! Bernardo! +BERNARDO +Say, +What, is Horatio there? +HORATIO +A piece of him. +BERNARDO +Welcome, Horatio: welcome, good Marcellus. +MARCELLUS +What, has this thing appear'd again to-night? +BERNARDO +I have seen nothing. +MARCELLUS +Horatio says 'tis but our fantasy, +And will not let belief take hold of him +Touching this dreaded sight, twice seen of us: +Therefore I have entreated him along +With us to watch the minutes of this night; +That if again this apparition come, +He may approve our eyes and speak to it. +HORATIO +Tush, tush, 'twill not appear. +BERNARDO +Sit down awhile; +And let us once again assail your ears, +That are so fortified against our story +What we have two nights seen. +HORATIO +Well, sit we down, +And let us hear Bernardo speak of this. +BERNARDO +Last night of all, +When yond same star that's westward from the pole +Had made his course to illume that part of heaven +Where now it burns, Marcellus and myself, +The bell then beating one,-- +Enter Ghost + +MARCELLUS +Peace, break thee off; look, where it comes again! +BERNARDO +In the same figure, like the king that's dead. +MARCELLUS +Thou art a scholar; speak to it, Horatio. +BERNARDO +Looks it not like the king? mark it, Horatio. +HORATIO +Most like: it harrows me with fear and wonder. +BERNARDO +It would be spoke to. +MARCELLUS +Question it, Horatio. +HORATIO +What art thou that usurp'st this time of night, +Together with that fair and warlike form +In which the majesty of buried Denmark +Did sometimes march? by heaven I charge thee, speak! +MARCELLUS +It is offended. +BERNARDO +See, it stalks away! +HORATIO +Stay! speak, speak! I charge thee, speak! +Exit Ghost + +MARCELLUS +'Tis gone, and will not answer. +BERNARDO +How now, Horatio! you tremble and look pale: +Is not this something more than fantasy? +What think you on't? +HORATIO +Before my God, I might not this believe +Without the sensible and true avouch +Of mine own eyes. +MARCELLUS +Is it not like the king? +HORATIO +As thou art to thyself: +Such was the very armour he had on +When he the ambitious Norway combated; +So frown'd he once, when, in an angry parle, +He smote the sledded Polacks on the ice. +'Tis strange. +MARCELLUS +Thus twice before, and jump at this dead hour, +With martial stalk hath he gone by our watch. +HORATIO +In what particular thought to work I know not; +But in the gross and scope of my opinion, +This bodes some strange eruption to our state. +MARCELLUS +Good now, sit down, and tell me, he that knows, +Why this same strict and most observant watch +So nightly toils the subject of the land, +And why such daily cast of brazen cannon, +And foreign mart for implements of war; +Why such impress of shipwrights, whose sore task +Does not divide the Sunday from the week; +What might be toward, that this sweaty haste +Doth make the night joint-labourer with the day: +Who is't that can inform me? +HORATIO +That can I; +At least, the whisper goes so. Our last king, +Whose image even but now appear'd to us, +Was, as you know, by Fortinbras of Norway, +Thereto prick'd on by a most emulate pride, +Dared to the combat; in which our valiant Hamlet-- +For so this side of our known world esteem'd him-- +Did slay this Fortinbras; who by a seal'd compact, +Well ratified by law and heraldry, +Did forfeit, with his life, all those his lands +Which he stood seized of, to the conqueror: +Against the which, a moiety competent +Was gaged by our king; which had return'd +To the inheritance of Fortinbras, +Had he been vanquisher; as, by the same covenant, +And carriage of the article design'd, +His fell to Hamlet. Now, sir, young Fortinbras, +Of unimproved mettle hot and full, +Hath in the skirts of Norway here and there +Shark'd up a list of lawless resolutes, +For food and diet, to some enterprise +That hath a stomach in't; which is no other-- +As it doth well appear unto our state-- +But to recover of us, by strong hand +And terms compulsatory, those foresaid lands +So by his father lost: and this, I take it, +Is the main motive of our preparations, +The source of this our watch and the chief head +Of this post-haste and romage in the land. +BERNARDO +I think it be no other but e'en so: +Well may it sort that this portentous figure +Comes armed through our watch; so like the king +That was and is the question of these wars. +HORATIO +A mote it is to trouble the mind's eye. +In the most high and palmy state of Rome, +A little ere the mightiest Julius fell, +The graves stood tenantless and the sheeted dead +Did squeak and gibber in the Roman streets: +As stars with trains of fire and dews of blood, +Disasters in the sun; and the moist star +Upon whose influence Neptune's empire stands +Was sick almost to doomsday with eclipse: +And even the like precurse of fierce events, +As harbingers preceding still the fates +And prologue to the omen coming on, +Have heaven and earth together demonstrated +Unto our climatures and countrymen.-- +But soft, behold! lo, where it comes again! +Re-enter Ghost + +I'll cross it, though it blast me. Stay, illusion! +If thou hast any sound, or use of voice, +Speak to me: +If there be any good thing to be done, +That may to thee do ease and grace to me, +Speak to me: +Cock crows + +If thou art privy to thy country's fate, +Which, happily, foreknowing may avoid, O, speak! +Or if thou hast uphoarded in thy life +Extorted treasure in the womb of earth, +For which, they say, you spirits oft walk in death, +Speak of it: stay, and speak! Stop it, Marcellus. +MARCELLUS +Shall I strike at it with my partisan? +HORATIO +Do, if it will not stand. +BERNARDO +'Tis here! +HORATIO +'Tis here! +MARCELLUS +'Tis gone! +Exit Ghost + +We do it wrong, being so majestical, +To offer it the show of violence; +For it is, as the air, invulnerable, +And our vain blows malicious mockery. +BERNARDO +It was about to speak, when the cock crew. +HORATIO +And then it started like a guilty thing +Upon a fearful summons. I have heard, +The cock, that is the trumpet to the morn, +Doth with his lofty and shrill-sounding throat +Awake the god of day; and, at his warning, +Whether in sea or fire, in earth or air, +The extravagant and erring spirit hies +To his confine: and of the truth herein +This present object made probation. +MARCELLUS +It faded on the crowing of the cock. +Some say that ever 'gainst that season comes +Wherein our Saviour's birth is celebrated, +The bird of dawning singeth all night long: +And then, they say, no spirit dares stir abroad; +The nights are wholesome; then no planets strike, +No fairy takes, nor witch hath power to charm, +So hallow'd and so gracious is the time. +HORATIO +So have I heard and do in part believe it. +But, look, the morn, in russet mantle clad, +Walks o'er the dew of yon high eastward hill: +Break we our watch up; and by my advice, +Let us impart what we have seen to-night +Unto young Hamlet; for, upon my life, +This spirit, dumb to us, will speak to him. +Do you consent we shall acquaint him with it, +As needful in our loves, fitting our duty? +MARCELLUS +Let's do't, I pray; and I this morning know +Where we shall find him most conveniently. +Exeunt + +SCENE II. A room of state in the castle. + +Enter KING CLAUDIUS, QUEEN GERTRUDE, HAMLET, POLONIUS, LAERTES, VOLTIMAND, CORNELIUS, Lords, and Attendants +KING CLAUDIUS +Though yet of Hamlet our dear brother's death +The memory be green, and that it us befitted +To bear our hearts in grief and our whole kingdom +To be contracted in one brow of woe, +Yet so far hath discretion fought with nature +That we with wisest sorrow think on him, +Together with remembrance of ourselves. +Therefore our sometime sister, now our queen, +The imperial jointress to this warlike state, +Have we, as 'twere with a defeated joy,-- +With an auspicious and a dropping eye, +With mirth in funeral and with dirge in marriage, +In equal scale weighing delight and dole,-- +Taken to wife: nor have we herein barr'd +Your better wisdoms, which have freely gone +With this affair along. For all, our thanks. +Now follows, that you know, young Fortinbras, +Holding a weak supposal of our worth, +Or thinking by our late dear brother's death +Our state to be disjoint and out of frame, +Colleagued with the dream of his advantage, +He hath not fail'd to pester us with message, +Importing the surrender of those lands +Lost by his father, with all bonds of law, +To our most valiant brother. So much for him. +Now for ourself and for this time of meeting: +Thus much the business is: we have here writ +To Norway, uncle of young Fortinbras,-- +Who, impotent and bed-rid, scarcely hears +Of this his nephew's purpose,--to suppress +His further gait herein; in that the levies, +The lists and full proportions, are all made +Out of his subject: and we here dispatch +You, good Cornelius, and you, Voltimand, +For bearers of this greeting to old Norway; +Giving to you no further personal power +To business with the king, more than the scope +Of these delated articles allow. +Farewell, and let your haste commend your duty. +CORNELIUS VOLTIMAND +In that and all things will we show our duty. +KING CLAUDIUS +We doubt it nothing: heartily farewell. +Exeunt VOLTIMAND and CORNELIUS + +And now, Laertes, what's the news with you? +You told us of some suit; what is't, Laertes? +You cannot speak of reason to the Dane, +And loose your voice: what wouldst thou beg, Laertes, +That shall not be my offer, not thy asking? +The head is not more native to the heart, +The hand more instrumental to the mouth, +Than is the throne of Denmark to thy father. +What wouldst thou have, Laertes? +LAERTES +My dread lord, +Your leave and favour to return to France; +From whence though willingly I came to Denmark, +To show my duty in your coronation, +Yet now, I must confess, that duty done, +My thoughts and wishes bend again toward France +And bow them to your gracious leave and pardon. +KING CLAUDIUS +Have you your father's leave? What says Polonius? +LORD POLONIUS +He hath, my lord, wrung from me my slow leave +By laboursome petition, and at last +Upon his will I seal'd my hard consent: +I do beseech you, give him leave to go. +KING CLAUDIUS +Take thy fair hour, Laertes; time be thine, +And thy best graces spend it at thy will! +But now, my cousin Hamlet, and my son,-- +HAMLET +[Aside] A little more than kin, and less than kind. +KING CLAUDIUS +How is it that the clouds still hang on you? +HAMLET +Not so, my lord; I am too much i' the sun. +QUEEN GERTRUDE +Good Hamlet, cast thy nighted colour off, +And let thine eye look like a friend on Denmark. +Do not for ever with thy vailed lids +Seek for thy noble father in the dust: +Thou know'st 'tis common; all that lives must die, +Passing through nature to eternity. +HAMLET +Ay, madam, it is common. +QUEEN GERTRUDE +If it be, +Why seems it so particular with thee? +HAMLET +Seems, madam! nay it is; I know not 'seems.' +'Tis not alone my inky cloak, good mother, +Nor customary suits of solemn black, +Nor windy suspiration of forced breath, +No, nor the fruitful river in the eye, +Nor the dejected 'havior of the visage, +Together with all forms, moods, shapes of grief, +That can denote me truly: these indeed seem, +For they are actions that a man might play: +But I have that within which passeth show; +These but the trappings and the suits of woe. +KING CLAUDIUS +'Tis sweet and commendable in your nature, Hamlet, +To give these mourning duties to your father: +But, you must know, your father lost a father; +That father lost, lost his, and the survivor bound +In filial obligation for some term +To do obsequious sorrow: but to persever +In obstinate condolement is a course +Of impious stubbornness; 'tis unmanly grief; +It shows a will most incorrect to heaven, +A heart unfortified, a mind impatient, +An understanding simple and unschool'd: +For what we know must be and is as common +As any the most vulgar thing to sense, +Why should we in our peevish opposition +Take it to heart? Fie! 'tis a fault to heaven, +A fault against the dead, a fault to nature, +To reason most absurd: whose common theme +Is death of fathers, and who still hath cried, +From the first corse till he that died to-day, +'This must be so.' We pray you, throw to earth +This unprevailing woe, and think of us +As of a father: for let the world take note, +You are the most immediate to our throne; +And with no less nobility of love +Than that which dearest father bears his son, +Do I impart toward you. For your intent +In going back to school in Wittenberg, +It is most retrograde to our desire: +And we beseech you, bend you to remain +Here, in the cheer and comfort of our eye, +Our chiefest courtier, cousin, and our son. +QUEEN GERTRUDE +Let not thy mother lose her prayers, Hamlet: +I pray thee, stay with us; go not to Wittenberg. +HAMLET +I shall in all my best obey you, madam. +KING CLAUDIUS +Why, 'tis a loving and a fair reply: +Be as ourself in Denmark. Madam, come; +This gentle and unforced accord of Hamlet +Sits smiling to my heart: in grace whereof, +No jocund health that Denmark drinks to-day, +But the great cannon to the clouds shall tell, +And the king's rouse the heavens all bruit again, +Re-speaking earthly thunder. Come away. +Exeunt all but HAMLET + +HAMLET +O, that this too too solid flesh would melt +Thaw and resolve itself into a dew! +Or that the Everlasting had not fix'd +His canon 'gainst self-slaughter! O God! God! +How weary, stale, flat and unprofitable, +Seem to me all the uses of this world! +Fie on't! ah fie! 'tis an unweeded garden, +That grows to seed; things rank and gross in nature +Possess it merely. That it should come to this! +But two months dead: nay, not so much, not two: +So excellent a king; that was, to this, +Hyperion to a satyr; so loving to my mother +That he might not beteem the winds of heaven +Visit her face too roughly. Heaven and earth! +Must I remember? why, she would hang on him, +As if increase of appetite had grown +By what it fed on: and yet, within a month-- +Let me not think on't--Frailty, thy name is woman!-- +A little month, or ere those shoes were old +With which she follow'd my poor father's body, +Like Niobe, all tears:--why she, even she-- +O, God! a beast, that wants discourse of reason, +Would have mourn'd longer--married with my uncle, +My father's brother, but no more like my father +Than I to Hercules: within a month: +Ere yet the salt of most unrighteous tears +Had left the flushing in her galled eyes, +She married. O, most wicked speed, to post +With such dexterity to incestuous sheets! +It is not nor it cannot come to good: +But break, my heart; for I must hold my tongue. +Enter HORATIO, MARCELLUS, and BERNARDO + +HORATIO +Hail to your lordship! +HAMLET +I am glad to see you well: +Horatio,--or I do forget myself. +HORATIO +The same, my lord, and your poor servant ever. +HAMLET +Sir, my good friend; I'll change that name with you: +And what make you from Wittenberg, Horatio? Marcellus? +MARCELLUS +My good lord-- +HAMLET +I am very glad to see you. Good even, sir. +But what, in faith, make you from Wittenberg? +HORATIO +A truant disposition, good my lord. +HAMLET +I would not hear your enemy say so, +Nor shall you do mine ear that violence, +To make it truster of your own report +Against yourself: I know you are no truant. +But what is your affair in Elsinore? +We'll teach you to drink deep ere you depart. +HORATIO +My lord, I came to see your father's funeral. +HAMLET +I pray thee, do not mock me, fellow-student; +I think it was to see my mother's wedding. +HORATIO +Indeed, my lord, it follow'd hard upon. +HAMLET +Thrift, thrift, Horatio! the funeral baked meats +Did coldly furnish forth the marriage tables. +Would I had met my dearest foe in heaven +Or ever I had seen that day, Horatio! +My father!--methinks I see my father. +HORATIO +Where, my lord? +HAMLET +In my mind's eye, Horatio. +HORATIO +I saw him once; he was a goodly king. +HAMLET +He was a man, take him for all in all, +I shall not look upon his like again. +HORATIO +My lord, I think I saw him yesternight. +HAMLET +Saw? who? +HORATIO +My lord, the king your father. +HAMLET +The king my father! +HORATIO +Season your admiration for awhile +With an attent ear, till I may deliver, +Upon the witness of these gentlemen, +This marvel to you. +HAMLET +For God's love, let me hear. +HORATIO +Two nights together had these gentlemen, +Marcellus and Bernardo, on their watch, +In the dead vast and middle of the night, +Been thus encounter'd. A figure like your father, +Armed at point exactly, cap-a-pe, +Appears before them, and with solemn march +Goes slow and stately by them: thrice he walk'd +By their oppress'd and fear-surprised eyes, +Within his truncheon's length; whilst they, distilled +Almost to jelly with the act of fear, +Stand dumb and speak not to him. This to me +In dreadful secrecy impart they did; +And I with them the third night kept the watch; +Where, as they had deliver'd, both in time, +Form of the thing, each word made true and good, +The apparition comes: I knew your father; +These hands are not more like. +HAMLET +But where was this? +MARCELLUS +My lord, upon the platform where we watch'd. +HAMLET +Did you not speak to it? +HORATIO +My lord, I did; +But answer made it none: yet once methought +It lifted up its head and did address +Itself to motion, like as it would speak; +But even then the morning cock crew loud, +And at the sound it shrunk in haste away, +And vanish'd from our sight. +HAMLET +'Tis very strange. +HORATIO +As I do live, my honour'd lord, 'tis true; +And we did think it writ down in our duty +To let you know of it. +HAMLET +Indeed, indeed, sirs, but this troubles me. +Hold you the watch to-night? +MARCELLUS BERNARDO +We do, my lord. +HAMLET +Arm'd, say you? +MARCELLUS BERNARDO +Arm'd, my lord. +HAMLET +From top to toe? +MARCELLUS BERNARDO +My lord, from head to foot. +HAMLET +Then saw you not his face? +HORATIO +O, yes, my lord; he wore his beaver up. +HAMLET +What, look'd he frowningly? +HORATIO +A countenance more in sorrow than in anger. +HAMLET +Pale or red? +HORATIO +Nay, very pale. +HAMLET +And fix'd his eyes upon you? +HORATIO +Most constantly. +HAMLET +I would I had been there. +HORATIO +It would have much amazed you. +HAMLET +Very like, very like. Stay'd it long? +HORATIO +While one with moderate haste might tell a hundred. +MARCELLUS BERNARDO +Longer, longer. +HORATIO +Not when I saw't. +HAMLET +His beard was grizzled--no? +HORATIO +It was, as I have seen it in his life, +A sable silver'd. +HAMLET +I will watch to-night; +Perchance 'twill walk again. +HORATIO +I warrant it will. +HAMLET +If it assume my noble father's person, +I'll speak to it, though hell itself should gape +And bid me hold my peace. I pray you all, +If you have hitherto conceal'd this sight, +Let it be tenable in your silence still; +And whatsoever else shall hap to-night, +Give it an understanding, but no tongue: +I will requite your loves. So, fare you well: +Upon the platform, 'twixt eleven and twelve, +I'll visit you. +All +Our duty to your honour. +HAMLET +Your loves, as mine to you: farewell. +Exeunt all but HAMLET + +My father's spirit in arms! all is not well; +I doubt some foul play: would the night were come! +Till then sit still, my soul: foul deeds will rise, +Though all the earth o'erwhelm them, to men's eyes. +Exit + +SCENE III. A room in Polonius' house. + +Enter LAERTES and OPHELIA +LAERTES +My necessaries are embark'd: farewell: +And, sister, as the winds give benefit +And convoy is assistant, do not sleep, +But let me hear from you. +OPHELIA +Do you doubt that? +LAERTES +For Hamlet and the trifling of his favour, +Hold it a fashion and a toy in blood, +A violet in the youth of primy nature, +Forward, not permanent, sweet, not lasting, +The perfume and suppliance of a minute; No more. +OPHELIA +No more but so? +LAERTES +Think it no more; +For nature, crescent, does not grow alone +In thews and bulk, but, as this temple waxes, +The inward service of the mind and soul +Grows wide withal. Perhaps he loves you now, +And now no soil nor cautel doth besmirch +The virtue of his will: but you must fear, +His greatness weigh'd, his will is not his own; +For he himself is subject to his birth: +He may not, as unvalued persons do, +Carve for himself; for on his choice depends +The safety and health of this whole state; +And therefore must his choice be circumscribed +Unto the voice and yielding of that body +Whereof he is the head. Then if he says he loves you, +It fits your wisdom so far to believe it +As he in his particular act and place +May give his saying deed; which is no further +Than the main voice of Denmark goes withal. +Then weigh what loss your honour may sustain, +If with too credent ear you list his songs, +Or lose your heart, or your chaste treasure open +To his unmaster'd importunity. +Fear it, Ophelia, fear it, my dear sister, +And keep you in the rear of your affection, +Out of the shot and danger of desire. +The chariest maid is prodigal enough, +If she unmask her beauty to the moon: +Virtue itself 'scapes not calumnious strokes: +The canker galls the infants of the spring, +Too oft before their buttons be disclosed, +And in the morn and liquid dew of youth +Contagious blastments are most imminent. +Be wary then; best safety lies in fear: +Youth to itself rebels, though none else near. +OPHELIA +I shall the effect of this good lesson keep, +As watchman to my heart. But, good my brother, +Do not, as some ungracious pastors do, +Show me the steep and thorny way to heaven; +Whiles, like a puff'd and reckless libertine, +Himself the primrose path of dalliance treads, +And recks not his own rede. +LAERTES +O, fear me not. +I stay too long: but here my father comes. +Enter POLONIUS + +A double blessing is a double grace, +Occasion smiles upon a second leave. +LORD POLONIUS +Yet here, Laertes! aboard, aboard, for shame! +The wind sits in the shoulder of your sail, +And you are stay'd for. There; my blessing with thee! +And these few precepts in thy memory +See thou character. Give thy thoughts no tongue, +Nor any unproportioned thought his act. +Be thou familiar, but by no means vulgar. +Those friends thou hast, and their adoption tried, +Grapple them to thy soul with hoops of steel; +But do not dull thy palm with entertainment +Of each new-hatch'd, unfledged comrade. Beware +Of entrance to a quarrel, but being in, +Bear't that the opposed may beware of thee. +Give every man thy ear, but few thy voice; +Take each man's censure, but reserve thy judgment. +Costly thy habit as thy purse can buy, +But not express'd in fancy; rich, not gaudy; +For the apparel oft proclaims the man, +And they in France of the best rank and station +Are of a most select and generous chief in that. +Neither a borrower nor a lender be; +For loan oft loses both itself and friend, +And borrowing dulls the edge of husbandry. +This above all: to thine ownself be true, +And it must follow, as the night the day, +Thou canst not then be false to any man. +Farewell: my blessing season this in thee! +LAERTES +Most humbly do I take my leave, my lord. +LORD POLONIUS +The time invites you; go; your servants tend. +LAERTES +Farewell, Ophelia; and remember well +What I have said to you. +OPHELIA +'Tis in my memory lock'd, +And you yourself shall keep the key of it. +LAERTES +Farewell. +Exit + +LORD POLONIUS +What is't, Ophelia, be hath said to you? +OPHELIA +So please you, something touching the Lord Hamlet. +LORD POLONIUS +Marry, well bethought: +'Tis told me, he hath very oft of late +Given private time to you; and you yourself +Have of your audience been most free and bounteous: +If it be so, as so 'tis put on me, +And that in way of caution, I must tell you, +You do not understand yourself so clearly +As it behoves my daughter and your honour. +What is between you? give me up the truth. +OPHELIA +He hath, my lord, of late made many tenders +Of his affection to me. +LORD POLONIUS +Affection! pooh! you speak like a green girl, +Unsifted in such perilous circumstance. +Do you believe his tenders, as you call them? +OPHELIA +I do not know, my lord, what I should think. +LORD POLONIUS +Marry, I'll teach you: think yourself a baby; +That you have ta'en these tenders for true pay, +Which are not sterling. Tender yourself more dearly; +Or--not to crack the wind of the poor phrase, +Running it thus--you'll tender me a fool. +OPHELIA +My lord, he hath importuned me with love +In honourable fashion. +LORD POLONIUS +Ay, fashion you may call it; go to, go to. +OPHELIA +And hath given countenance to his speech, my lord, +With almost all the holy vows of heaven. +LORD POLONIUS +Ay, springes to catch woodcocks. I do know, +When the blood burns, how prodigal the soul +Lends the tongue vows: these blazes, daughter, +Giving more light than heat, extinct in both, +Even in their promise, as it is a-making, +You must not take for fire. From this time +Be somewhat scanter of your maiden presence; +Set your entreatments at a higher rate +Than a command to parley. For Lord Hamlet, +Believe so much in him, that he is young +And with a larger tether may he walk +Than may be given you: in few, Ophelia, +Do not believe his vows; for they are brokers, +Not of that dye which their investments show, +But mere implorators of unholy suits, +Breathing like sanctified and pious bawds, +The better to beguile. This is for all: +I would not, in plain terms, from this time forth, +Have you so slander any moment leisure, +As to give words or talk with the Lord Hamlet. +Look to't, I charge you: come your ways. +OPHELIA +I shall obey, my lord. +Exeunt + +SCENE IV. The platform. + +Enter HAMLET, HORATIO, and MARCELLUS +HAMLET +The air bites shrewdly; it is very cold. +HORATIO +It is a nipping and an eager air. +HAMLET +What hour now? +HORATIO +I think it lacks of twelve. +HAMLET +No, it is struck. +HORATIO +Indeed? I heard it not: then it draws near the season +Wherein the spirit held his wont to walk. +A flourish of trumpets, and ordnance shot off, within + +What does this mean, my lord? +HAMLET +The king doth wake to-night and takes his rouse, +Keeps wassail, and the swaggering up-spring reels; +And, as he drains his draughts of Rhenish down, +The kettle-drum and trumpet thus bray out +The triumph of his pledge. +HORATIO +Is it a custom? +HAMLET +Ay, marry, is't: +But to my mind, though I am native here +And to the manner born, it is a custom +More honour'd in the breach than the observance. +This heavy-headed revel east and west +Makes us traduced and tax'd of other nations: +They clepe us drunkards, and with swinish phrase +Soil our addition; and indeed it takes +From our achievements, though perform'd at height, +The pith and marrow of our attribute. +So, oft it chances in particular men, +That for some vicious mole of nature in them, +As, in their birth--wherein they are not guilty, +Since nature cannot choose his origin-- +By the o'ergrowth of some complexion, +Oft breaking down the pales and forts of reason, +Or by some habit that too much o'er-leavens +The form of plausive manners, that these men, +Carrying, I say, the stamp of one defect, +Being nature's livery, or fortune's star,-- +Their virtues else--be they as pure as grace, +As infinite as man may undergo-- +Shall in the general censure take corruption +From that particular fault: the dram of eale +Doth all the noble substance of a doubt +To his own scandal. +HORATIO +Look, my lord, it comes! +Enter Ghost + +HAMLET +Angels and ministers of grace defend us! +Be thou a spirit of health or goblin damn'd, +Bring with thee airs from heaven or blasts from hell, +Be thy intents wicked or charitable, +Thou comest in such a questionable shape +That I will speak to thee: I'll call thee Hamlet, +King, father, royal Dane: O, answer me! +Let me not burst in ignorance; but tell +Why thy canonized bones, hearsed in death, +Have burst their cerements; why the sepulchre, +Wherein we saw thee quietly inurn'd, +Hath oped his ponderous and marble jaws, +To cast thee up again. What may this mean, +That thou, dead corse, again in complete steel +Revisit'st thus the glimpses of the moon, +Making night hideous; and we fools of nature +So horridly to shake our disposition +With thoughts beyond the reaches of our souls? +Say, why is this? wherefore? what should we do? +Ghost beckons HAMLET + +HORATIO +It beckons you to go away with it, +As if it some impartment did desire +To you alone. +MARCELLUS +Look, with what courteous action +It waves you to a more removed ground: +But do not go with it. +HORATIO +No, by no means. +HAMLET +It will not speak; then I will follow it. +HORATIO +Do not, my lord. +HAMLET +Why, what should be the fear? +I do not set my life in a pin's fee; +And for my soul, what can it do to that, +Being a thing immortal as itself? +It waves me forth again: I'll follow it. +HORATIO +What if it tempt you toward the flood, my lord, +Or to the dreadful summit of the cliff +That beetles o'er his base into the sea, +And there assume some other horrible form, +Which might deprive your sovereignty of reason +And draw you into madness? think of it: +The very place puts toys of desperation, +Without more motive, into every brain +That looks so many fathoms to the sea +And hears it roar beneath. +HAMLET +It waves me still. +Go on; I'll follow thee. +MARCELLUS +You shall not go, my lord. +HAMLET +Hold off your hands. +HORATIO +Be ruled; you shall not go. +HAMLET +My fate cries out, +And makes each petty artery in this body +As hardy as the Nemean lion's nerve. +Still am I call'd. Unhand me, gentlemen. +By heaven, I'll make a ghost of him that lets me! +I say, away! Go on; I'll follow thee. +Exeunt Ghost and HAMLET + +HORATIO +He waxes desperate with imagination. +MARCELLUS +Let's follow; 'tis not fit thus to obey him. +HORATIO +Have after. To what issue will this come? +MARCELLUS +Something is rotten in the state of Denmark. +HORATIO +Heaven will direct it. +MARCELLUS +Nay, let's follow him. +Exeunt + +SCENE V. Another part of the platform. + +Enter GHOST and HAMLET +HAMLET +Where wilt thou lead me? speak; I'll go no further. +Ghost +Mark me. +HAMLET +I will. +Ghost +My hour is almost come, +When I to sulphurous and tormenting flames +Must render up myself. +HAMLET +Alas, poor ghost! +Ghost +Pity me not, but lend thy serious hearing +To what I shall unfold. +HAMLET +Speak; I am bound to hear. +Ghost +So art thou to revenge, when thou shalt hear. +HAMLET +What? +Ghost +I am thy father's spirit, +Doom'd for a certain term to walk the night, +And for the day confined to fast in fires, +Till the foul crimes done in my days of nature +Are burnt and purged away. But that I am forbid +To tell the secrets of my prison-house, +I could a tale unfold whose lightest word +Would harrow up thy soul, freeze thy young blood, +Make thy two eyes, like stars, start from their spheres, +Thy knotted and combined locks to part +And each particular hair to stand on end, +Like quills upon the fretful porpentine: +But this eternal blazon must not be +To ears of flesh and blood. List, list, O, list! +If thou didst ever thy dear father love-- +HAMLET +O God! +Ghost +Revenge his foul and most unnatural murder. +HAMLET +Murder! +Ghost +Murder most foul, as in the best it is; +But this most foul, strange and unnatural. +HAMLET +Haste me to know't, that I, with wings as swift +As meditation or the thoughts of love, +May sweep to my revenge. +Ghost +I find thee apt; +And duller shouldst thou be than the fat weed +That roots itself in ease on Lethe wharf, +Wouldst thou not stir in this. Now, Hamlet, hear: +'Tis given out that, sleeping in my orchard, +A serpent stung me; so the whole ear of Denmark +Is by a forged process of my death +Rankly abused: but know, thou noble youth, +The serpent that did sting thy father's life +Now wears his crown. +HAMLET +O my prophetic soul! My uncle! +Ghost +Ay, that incestuous, that adulterate beast, +With witchcraft of his wit, with traitorous gifts,-- +O wicked wit and gifts, that have the power +So to seduce!--won to his shameful lust +The will of my most seeming-virtuous queen: +O Hamlet, what a falling-off was there! +From me, whose love was of that dignity +That it went hand in hand even with the vow +I made to her in marriage, and to decline +Upon a wretch whose natural gifts were poor +To those of mine! +But virtue, as it never will be moved, +Though lewdness court it in a shape of heaven, +So lust, though to a radiant angel link'd, +Will sate itself in a celestial bed, +And prey on garbage. +But, soft! methinks I scent the morning air; +Brief let me be. Sleeping within my orchard, +My custom always of the afternoon, +Upon my secure hour thy uncle stole, +With juice of cursed hebenon in a vial, +And in the porches of my ears did pour +The leperous distilment; whose effect +Holds such an enmity with blood of man +That swift as quicksilver it courses through +The natural gates and alleys of the body, +And with a sudden vigour doth posset +And curd, like eager droppings into milk, +The thin and wholesome blood: so did it mine; +And a most instant tetter bark'd about, +Most lazar-like, with vile and loathsome crust, +All my smooth body. +Thus was I, sleeping, by a brother's hand +Of life, of crown, of queen, at once dispatch'd: +Cut off even in the blossoms of my sin, +Unhousel'd, disappointed, unanel'd, +No reckoning made, but sent to my account +With all my imperfections on my head: +O, horrible! O, horrible! most horrible! +If thou hast nature in thee, bear it not; +Let not the royal bed of Denmark be +A couch for luxury and damned incest. +But, howsoever thou pursuest this act, +Taint not thy mind, nor let thy soul contrive +Against thy mother aught: leave her to heaven +And to those thorns that in her bosom lodge, +To prick and sting her. Fare thee well at once! +The glow-worm shows the matin to be near, +And 'gins to pale his uneffectual fire: +Adieu, adieu! Hamlet, remember me. +Exit + +HAMLET +O all you host of heaven! O earth! what else? +And shall I couple hell? O, fie! Hold, hold, my heart; +And you, my sinews, grow not instant old, +But bear me stiffly up. Remember thee! +Ay, thou poor ghost, while memory holds a seat +In this distracted globe. Remember thee! +Yea, from the table of my memory +I'll wipe away all trivial fond records, +All saws of books, all forms, all pressures past, +That youth and observation copied there; +And thy commandment all alone shall live +Within the book and volume of my brain, +Unmix'd with baser matter: yes, by heaven! +O most pernicious woman! +O villain, villain, smiling, damned villain! +My tables,--meet it is I set it down, +That one may smile, and smile, and be a villain; +At least I'm sure it may be so in Denmark: +Writing + +So, uncle, there you are. Now to my word; +It is 'Adieu, adieu! remember me.' +I have sworn 't. +MARCELLUS HORATIO +[Within] My lord, my lord,-- +MARCELLUS +[Within] Lord Hamlet,-- +HORATIO +[Within] Heaven secure him! +HAMLET +So be it! +HORATIO +[Within] Hillo, ho, ho, my lord! +HAMLET +Hillo, ho, ho, boy! come, bird, come. +Enter HORATIO and MARCELLUS + +MARCELLUS +How is't, my noble lord? +HORATIO +What news, my lord? +HAMLET +O, wonderful! +HORATIO +Good my lord, tell it. +HAMLET +No; you'll reveal it. +HORATIO +Not I, my lord, by heaven. +MARCELLUS +Nor I, my lord. +HAMLET +How say you, then; would heart of man once think it? +But you'll be secret? +HORATIO MARCELLUS +Ay, by heaven, my lord. +HAMLET +There's ne'er a villain dwelling in all Denmark +But he's an arrant knave. +HORATIO +There needs no ghost, my lord, come from the grave +To tell us this. +HAMLET +Why, right; you are i' the right; +And so, without more circumstance at all, +I hold it fit that we shake hands and part: +You, as your business and desire shall point you; +For every man has business and desire, +Such as it is; and for mine own poor part, +Look you, I'll go pray. +HORATIO +These are but wild and whirling words, my lord. +HAMLET +I'm sorry they offend you, heartily; +Yes, 'faith heartily. +HORATIO +There's no offence, my lord. +HAMLET +Yes, by Saint Patrick, but there is, Horatio, +And much offence too. Touching this vision here, +It is an honest ghost, that let me tell you: +For your desire to know what is between us, +O'ermaster 't as you may. And now, good friends, +As you are friends, scholars and soldiers, +Give me one poor request. +HORATIO +What is't, my lord? we will. +HAMLET +Never make known what you have seen to-night. +HORATIO MARCELLUS +My lord, we will not. +HAMLET +Nay, but swear't. +HORATIO +In faith, +My lord, not I. +MARCELLUS +Nor I, my lord, in faith. +HAMLET +Upon my sword. +MARCELLUS +We have sworn, my lord, already. +HAMLET +Indeed, upon my sword, indeed. +Ghost +[Beneath] Swear. +HAMLET +Ah, ha, boy! say'st thou so? art thou there, +truepenny? +Come on--you hear this fellow in the cellarage-- +Consent to swear. +HORATIO +Propose the oath, my lord. +HAMLET +Never to speak of this that you have seen, +Swear by my sword. +Ghost +[Beneath] Swear. +HAMLET +Hic et ubique? then we'll shift our ground. +Come hither, gentlemen, +And lay your hands again upon my sword: +Never to speak of this that you have heard, +Swear by my sword. +Ghost +[Beneath] Swear. +HAMLET +Well said, old mole! canst work i' the earth so fast? +A worthy pioner! Once more remove, good friends. +HORATIO +O day and night, but this is wondrous strange! +HAMLET +And therefore as a stranger give it welcome. +There are more things in heaven and earth, Horatio, +Than are dreamt of in your philosophy. But come; +Here, as before, never, so help you mercy, +How strange or odd soe'er I bear myself, +As I perchance hereafter shall think meet +To put an antic disposition on, +That you, at such times seeing me, never shall, +With arms encumber'd thus, or this headshake, +Or by pronouncing of some doubtful phrase, +As 'Well, well, we know,' or 'We could, an if we would,' +Or 'If we list to speak,' or 'There be, an if they might,' +Or such ambiguous giving out, to note +That you know aught of me: this not to do, +So grace and mercy at your most need help you, Swear. +Ghost +[Beneath] Swear. +HAMLET +Rest, rest, perturbed spirit! +They swear + +So, gentlemen, +With all my love I do commend me to you: +And what so poor a man as Hamlet is +May do, to express his love and friending to you, +God willing, shall not lack. Let us go in together; +And still your fingers on your lips, I pray. +The time is out of joint: O cursed spite, +That ever I was born to set it right! +Nay, come, let's go together. +Exeunt diff --git a/pebble/sstable/testdata/hamletreader/hamlet_iter b/pebble/sstable/testdata/hamletreader/hamlet_iter new file mode 100644 index 0000000..c680f29 --- /dev/null +++ b/pebble/sstable/testdata/hamletreader/hamlet_iter @@ -0,0 +1,389 @@ +iter +first +next +next +next +next +---- + + + + + + +iter +seek-ge a +next +next +next +next +---- + + + + + + +iter +seek-ge b +next +next +next +---- + + + + + +iter +seek-ge c +next +next +---- + + + + +iter +seek-ge d +next +---- + + + +iter +seek-ge e +---- + + +iter +seek-ge b +seek-ge c +seek-ge d +seek-ge e +---- + + + + + +iter +last +prev +prev +prev +prev +---- + + + + + + +iter +seek-lt e +prev +prev +prev +prev +---- + + + + + + +iter +seek-lt d +prev +prev +prev +---- + + + + + +iter +seek-lt c +prev +prev +---- + + + + +iter +seek-lt b +prev +---- + + + +iter +seek-lt a +---- +. + +iter +seek-lt d +seek-lt c +seek-lt b +seek-lt a +---- + + + +. + +iter globalSeqNum=1 +first +next +next +next +next +---- + + + + + + +iter globalSeqNum=10 +first +next +next +next +next +---- + + + + + + +iter globalSeqNum=0 +seek-lt x +---- + + +get +b +a +f +d +c +---- + +97 + +54 + + +iter +seek-ge aboard +prev +prev +---- + + +. + +iter +seek-ge calumnious +next +next +next +prev +prev +prev +prev +prev +---- + + + + + + + + + + +iter +seek-lt yourself +next +next +next +---- + + + +. + +iter +seek-lt yourself +prev +prev +prev +next +next +seek-ge calumnious +prev +prev +prev +first +next +next +prev +prev +prev +---- + + + + + + + + + + + + + + + +. + +iter +seek-ge m +next +next +next +---- + + + + + +iter +seek-lt m +next +next +next +---- + + + + + +iter +seek-ge a +seek-ge b +seek-ge c +seek-ge d +seek-ge e +seek-ge f +seek-ge g +seek-ge h +seek-ge i +seek-ge j +seek-ge k +seek-ge l +seek-ge m +seek-ge n +seek-ge o +seek-ge p +seek-ge q +seek-ge r +seek-ge s +seek-ge t +seek-ge u +seek-ge v +seek-ge w +seek-ge x +seek-ge y +seek-ge z +---- + + + + + + + + + + + + + + + + + + + + + + + + + +. + +iter +seek-lt a +seek-lt b +seek-lt c +seek-lt d +seek-lt e +seek-lt f +seek-lt g +seek-lt h +seek-lt i +seek-lt j +seek-lt k +seek-lt l +seek-lt m +seek-lt n +seek-lt o +seek-lt p +seek-lt q +seek-lt r +seek-lt s +seek-lt t +seek-lt u +seek-lt v +seek-lt w +seek-lt x +seek-lt y +seek-lt z +---- +. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pebble/sstable/testdata/make-table.cc b/pebble/sstable/testdata/make-table.cc new file mode 100644 index 0000000..44d12b3 --- /dev/null +++ b/pebble/sstable/testdata/make-table.cc @@ -0,0 +1,245 @@ +// Copyright 2011 The LevelDB-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. + +// This program adds N lines from infile to a leveldb table at outfile. +// The h.txt infile was generated via: +// cat hamlet-act-1.txt | tr '[:upper:]' '[:lower:]' | grep -o -E '\w+' | sort | uniq -c > infile +// +// To build and run: +// g++ make-table.cc -lleveldb && ./a.out + +#include +#include +#include + +#include "rocksdb/env.h" +#include "rocksdb/filter_policy.h" +#include "rocksdb/slice_transform.h" +#include "rocksdb/sst_file_writer.h" +#include "rocksdb/table.h" + +const char* infile = "h.txt"; + +// A dummy prefix extractor that cuts off the last two bytes for keys of +// length three or over. This is not a valid prefix extractor and barely +// enough to do a little bit of unit testing. +// +// TODO(tbg): write some test infra using CockroachDB MVCC data. +class PrefixExtractor : public rocksdb::SliceTransform { + public: + PrefixExtractor() {} + + virtual const char* Name() const { return "leveldb.BytewiseComparator"; } + + virtual rocksdb::Slice Transform(const rocksdb::Slice& src) const { + auto sl = rocksdb::Slice(src.data(), src.size()); + return sl; + } + + virtual bool InDomain(const rocksdb::Slice& src) const { return true; } +}; + +class KeyCountPropertyCollector : public rocksdb::TablePropertiesCollector { + public: + KeyCountPropertyCollector() + : count_(0) { + } + + rocksdb::Status AddUserKey(const rocksdb::Slice&, const rocksdb::Slice&, + rocksdb::EntryType type, rocksdb::SequenceNumber, + uint64_t) override { + count_++; + return rocksdb::Status::OK(); + } + + rocksdb::Status Finish(rocksdb::UserCollectedProperties* properties) override { + char buf[16]; + sprintf(buf, "%d", count_); + *properties = rocksdb::UserCollectedProperties{ + {"test.key-count", buf}, + }; + return rocksdb::Status::OK(); + } + + const char* Name() const override { return "KeyCountPropertyCollector"; } + + rocksdb::UserCollectedProperties GetReadableProperties() const override { + return rocksdb::UserCollectedProperties{}; + } + + private: + int count_; +}; + +class KeyCountPropertyCollectorFactory : public rocksdb::TablePropertiesCollectorFactory { + virtual rocksdb::TablePropertiesCollector* CreateTablePropertiesCollector( + rocksdb::TablePropertiesCollectorFactory::Context context) override { + return new KeyCountPropertyCollector(); + } + const char* Name() const override { return "KeyCountPropertyCollector"; } +}; + +int write() { + for (int i = 0; i < 12; ++i) { + rocksdb::Options options; + rocksdb::BlockBasedTableOptions table_options; + const char* outfile; + + table_options.block_size = 2048; + table_options.index_shortening = rocksdb::BlockBasedTableOptions::IndexShorteningMode::kShortenSeparatorsAndSuccessor; + + switch (i) { + case 0: + outfile = "h.ldb"; + table_options.format_version = 0; + table_options.whole_key_filtering = false; + break; + + case 1: + outfile = "h.sst"; + options.table_properties_collector_factories.emplace_back( + new KeyCountPropertyCollectorFactory); + table_options.whole_key_filtering = false; + break; + + case 2: + outfile = "h.no-compression.sst"; + options.table_properties_collector_factories.emplace_back( + new KeyCountPropertyCollectorFactory); + options.compression = rocksdb::kNoCompression; + table_options.whole_key_filtering = false; + break; + + case 3: + outfile = "h.block-bloom.no-compression.sst"; + options.compression = rocksdb::kNoCompression; + table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, true)); + table_options.whole_key_filtering = true; + break; + + case 4: + outfile = "h.table-bloom.sst"; + table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false)); + table_options.whole_key_filtering = true; + break; + + case 5: + outfile = "h.table-bloom.no-compression.sst"; + options.compression = rocksdb::kNoCompression; + table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false)); + table_options.whole_key_filtering = true; + break; + + case 6: + // TODO(peter): unused at this time + // + // outfile = "h.block-bloom.no-compression.prefix_extractor.sst"; + // options.compression = rocksdb::kNoCompression; + // options.prefix_extractor.reset(new PrefixExtractor); + // table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, true)); + // table_options.whole_key_filtering = true; + // break; + continue; + + case 7: + // TODO(peter): unused at this time + // + // outfile = "h.table-bloom.no-compression.prefix_extractor.sst"; + // options.compression = rocksdb::kNoCompression; + // options.prefix_extractor.reset(new PrefixExtractor); + // table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false)); + // table_options.whole_key_filtering = true; + // break; + continue; + + case 8: + // TODO(peter): unused at this time + // + // outfile = "h.block-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst"; + // options.compression = rocksdb::kNoCompression; + // options.prefix_extractor.reset(new PrefixExtractor); + // table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, true)); + // table_options.whole_key_filtering = false; + // break; + continue; + + case 9: + outfile = "h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst"; + options.compression = rocksdb::kNoCompression; + options.prefix_extractor.reset(new PrefixExtractor); + table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false)); + table_options.whole_key_filtering = false; + break; + + case 10: + outfile = "h.no-compression.two_level_index.sst"; + options.table_properties_collector_factories.emplace_back( + new KeyCountPropertyCollectorFactory); + options.compression = rocksdb::kNoCompression; + table_options.index_type = rocksdb::BlockBasedTableOptions::IndexType::kTwoLevelIndexSearch; + // Use small metadata_block_size to stress two_level_index. + table_options.metadata_block_size = 128; + table_options.whole_key_filtering = false; + break; + + case 11: + outfile = "h.zstd-compression.sst"; + options.table_properties_collector_factories.emplace_back( + new KeyCountPropertyCollectorFactory); + options.compression = rocksdb::kZSTD; + table_options.whole_key_filtering = false; + break; + + default: + continue; + } + + options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(table_options)); + + std::unique_ptr tb(new rocksdb::SstFileWriter({}, options)); + rocksdb::Status status = tb->Open(outfile); + if (!status.ok()) { + std::cerr << "SstFileWriter::Open: " << status.ToString() << std::endl; + return 1; + } + + int rangeDelLength = 0; + int rangeDelCounter = 0; + std::ifstream in(infile); + std::string s; + std::string rangeDelStart; + for (int i = 0; getline(in, s); i++) { + std::string key(s, 8); + std::string val(s, 0, 7); + val = val.substr(1 + val.rfind(' ')); + tb->Put(key.c_str(), val.c_str()); + // Add range deletions of increasing length. + if (i % 100 == 0) { + rangeDelStart = key; + rangeDelCounter = 0; + rangeDelLength++; + } + rangeDelCounter++; + + if (rangeDelCounter == rangeDelLength) { + tb->DeleteRange(rangeDelStart, key.c_str()); + } + } + + rocksdb::ExternalSstFileInfo info; + status = tb->Finish(&info); + if (!status.ok()) { + std::cerr << "TableBuilder::Finish: " << status.ToString() << std::endl; + return 1; + } + + std::cout << outfile << ": wrote " << info.num_entries << " entries, " << info.file_size << "b" << std::endl; + } + return 0; +} + +int main(int argc, char** argv) { + return write(); + +} diff --git a/pebble/sstable/testdata/prefixreader/bloom b/pebble/sstable/testdata/prefixreader/bloom new file mode 100644 index 0000000..58d478b --- /dev/null +++ b/pebble/sstable/testdata/prefixreader/bloom @@ -0,0 +1,22 @@ +build +a.SET.1:A +aa.SET.2:AA +aaa.SET.3:AAA +aaaa.SET.4:AAAA +b.SET.5:B +---- + +get +b +ab +a +aaa +aa +aaaa +---- +B + +A +AAA +AA +AAAA diff --git a/pebble/sstable/testdata/prefixreader/iter b/pebble/sstable/testdata/prefixreader/iter new file mode 100644 index 0000000..d1b8ac8 --- /dev/null +++ b/pebble/sstable/testdata/prefixreader/iter @@ -0,0 +1,270 @@ +build +a.SET.1:A +aa.SET.2:AA +c.SET.3:C +d.SET.4:D +---- + +iter +seek-prefix-ge a +next +next +---- +:A +:AA +. + +iter +seek-prefix-ge aa +next +---- +:AA +. + +iter +seek-prefix-ge aa +prev +---- +:AA +. + +iter +seek-prefix-ge c +prev +---- +:C +. + +iter +seek-prefix-ge b +---- +. + +iter +seek-prefix-ge c +next +---- +:C +. + +iter +seek-prefix-ge d +next +---- +:D +. + +iter +seek-prefix-ge e +---- +. + +iter +seek-prefix-ge c +seek-prefix-ge d +seek-prefix-ge e +---- +:C +:D +. + +iter +seek-prefix-ge c +next +seek-prefix-ge a +next +next +---- +:C +. +:A +:AA +. + +iter +seek-prefix-ge aa +next +seek-prefix-ge a +next +next +seek-prefix-ge c +next +---- +:AA +. +:A +:AA +. +:C +. + +iter +seek-prefix-ge c +next +seek-prefix-ge aa +next +seek-prefix-ge a +next +next +---- +:C +. +:AA +. +:A +:AA +. + +iter +seek-prefix-ge a +next +next +---- +:A +:AA +. + +iter +seek-prefix-ge a +next +prev +prev +---- +:A +:AA +:A +. + +iter +seek-prefix-ge a +prev +---- +:A +. + +iter +seek-prefix-ge a +seek-ge a +next +next +next +next +---- +:A +:A +:AA +:C +:D +. + +iter +seek-prefix-ge a +seek-ge aa +next +next +next +---- +:A +:AA +:C +:D +. + +iter +seek-prefix-ge aa +seek-ge c +next +next +---- +:AA +:C +:D +. + +iter +seek-prefix-ge aa +seek-lt c +next +next +next +---- +:AA +:AA +:C +:D +. + +iter +seek-prefix-ge aa +seek-lt c +prev +prev +---- +:AA +:AA +:A +. + +iter +seek-lt c +seek-prefix-ge aa +prev +---- +:AA +:AA +. + +iter +seek-lt c +seek-prefix-ge a +next +next + +---- +:AA +:A +:AA +. + +iter +seek-ge aa +seek-prefix-ge a +next +next + +---- +:AA +:A +:AA +. + +iter +seek-prefix-ge 1 +---- +. + +get +a +aa +f +d +c +---- +A +AA + +D +C + +iter +seek-prefix-ge a false +seek-prefix-ge a true +seek-prefix-ge aa true +seek-prefix-ge d true +seek-prefix-ge c false +---- +:A +:A +:AA +:D +:C diff --git a/pebble/sstable/testdata/reader/bloom b/pebble/sstable/testdata/reader/bloom new file mode 100644 index 0000000..58d478b --- /dev/null +++ b/pebble/sstable/testdata/reader/bloom @@ -0,0 +1,22 @@ +build +a.SET.1:A +aa.SET.2:AA +aaa.SET.3:AAA +aaaa.SET.4:AAAA +b.SET.5:B +---- + +get +b +ab +a +aaa +aa +aaaa +---- +B + +A +AAA +AA +AAAA diff --git a/pebble/sstable/testdata/reader/iter b/pebble/sstable/testdata/reader/iter new file mode 100644 index 0000000..6256795 --- /dev/null +++ b/pebble/sstable/testdata/reader/iter @@ -0,0 +1,720 @@ +build +a.SET.1:A +b.SET.2:B +c.SET.3:C +d.SET.4:D +---- + +iter +first +next +next +next +next +---- +:A +:B +:C +:D +. + +iter +seek-ge a +next +next +next +next +---- +:A +:B +:C +:D +. + +iter +seek-ge b +next +next +next +---- +:B +:C +:D +. + +iter +seek-ge c +next +next +---- +:C +:D +. + +iter +seek-ge d +next +---- +:D +. + +iter +seek-ge e +---- +. + +iter +seek-ge d +seek-ge z +---- +:D +. + +iter +seek-ge b +seek-ge c +seek-ge d +seek-ge e +---- +:B +:C +:D +. + +iter +last +prev +prev +prev +prev +---- +:D +:C +:B +:A +. + +iter +seek-lt e +prev +prev +prev +prev +---- +:D +:C +:B +:A +. + +iter +seek-lt d +prev +prev +prev +---- +:C +:B +:A +. + +iter +seek-lt c +prev +prev +---- +:B +:A +. + +iter +seek-lt b +prev +---- +:A +. + +iter +seek-lt a +---- +. + +iter +seek-lt d +seek-lt c +seek-lt b +seek-lt a +---- +:C +:B +:A +. + +iter globalSeqNum=1 +first +next +next +next +next +---- +:A +:B +:C +:D +. + +iter globalSeqNum=10 +first +next +next +next +next +---- +:A +:B +:C +:D +. + +iter globalSeqNum=0 +seek-lt x +---- +:D + +get +b +a +f +d +c +---- +B +A + +D +C + +# Verify that clearing the bounds on an iterator also clears +# previously set block{Lower,Upper}. + +iter +seek-ge c +seek-lt b +set-bounds lower=b upper=c +seek-ge c +seek-lt b +set-bounds lower= upper= +seek-ge c +seek-lt b +---- +:C +:A +. +. +. +. +:C +:A + +# Verify that seeking past the end of the sstable leaves the iterator +# in a state where prev returns the last key in the table. + +iter +seek-lt d +seek-ge f +prev +---- +:C +. +:D + +# Verify that seeking before the beginning of the sstable leaves the +# iterator in a state where next returns the first key in the table. + +iter +seek-ge b +seek-lt a +next +---- +:B +. +:A + + +# Verify the optimization to use next when doing SeekGE. + +iter +seek-ge a false +seek-ge a true +seek-ge b true +seek-ge c true +seek-ge d true +seek-ge e true +---- +:A +:A +:B +:C +:D +. + +# Verify the optimization to use next when doing SeekPrefixGE. + +iter +seek-prefix-ge a false +seek-prefix-ge a true +seek-prefix-ge b true +seek-prefix-ge c true +seek-prefix-ge d true +seek-prefix-ge e true +---- +:A +:A +:B +:C +:D +. + +# Verify that iteration from before the beginning or after the end of +# the sstable does not "wrap around". A bug previously allowed this to +# happen by letting the data block iterator and index iterator get out +# of sync. + +build +a.SET.1:a +---- + +iter +first +prev +next +next +next +---- +:a +. +:a +. +. + +iter +last +next +prev +prev +prev +---- +:a +. +:a +. +. + +# Build a sufficiently large SST to enable two-level indexes. + +build +a.SET.1:A +aae.SET.1:E +aaf.SET.1:F +aag.SET.1:G +aah.SET.1:H +aai.SET.1:I +aaj.SET.1:J +aak.SET.1:K +aal.SET.1:L +aam.SET.1:M +aan.SET.1:N +aao.SET.1:O +aap.SET.1:P +aaq.SET.1:Q +aar.SET.1:R +aas.SET.1:S +aat.SET.1:T +aau.SET.1:U +aav.SET.1:V +aaw.SET.1:W +aax.SET.1:X +aay.SET.1:Y +aaz.SET.1:Z +b.SET.2:B +bbe.SET.2:E +bbf.SET.2:F +bbg.SET.2:G +bbh.SET.2:H +bbi.SET.2:I +bbj.SET.2:J +bbk.SET.2:K +bbl.SET.2:L +bbm.SET.2:M +bbn.SET.2:N +bbo.SET.2:O +bbp.SET.2:P +bbq.SET.2:Q +bbr.SET.2:R +bbs.SET.2:S +bbt.SET.2:T +bbu.SET.2:U +bbv.SET.2:V +bbw.SET.2:W +bbx.SET.2:X +bby.SET.2:Y +bbz.SET.2:Z +c.SET.3:C +cc.RANGEDEL.3:ccc +cce.SET.3:E +ccf.SET.3:F +ccg.SET.3:G +cch.SET.3:H +cci.SET.3:I +ccj.SET.3:J +cck.SET.3:K +ccl.SET.3:L +ccm.SET.3:M +ccn.SET.3:N +cco.SET.3:O +ccp.SET.3:P +ccq.SET.3:Q +ccr.SET.3:R +ccs.SET.3:S +cct.SET.3:T +ccu.SET.3:U +ccv.SET.3:V +ccw.SET.3:W +ccx.SET.3:X +ccy.SET.3:Y +ccz.SET.3:Z +d.SET.4:D +dd.RANGEDEL.4:ddd +dde.SET.4:E +ddf.SET.4:F +ddg.SET.4:G +ddh.SET.4:H +ddi.SET.4:I +ddj.SET.4:J +ddk.SET.4:K +ddl.SET.4:L +ddm.SET.4:M +ddn.SET.4:N +ddo.SET.4:O +ddp.SET.4:P +ddq.SET.4:Q +ddr.SET.4:R +dds.SET.4:S +ddt.SET.4:T +ddu.SET.4:U +ddv.SET.4:V +ddw.SET.4:W +ddx.SET.4:X +ddy.SET.4:Y +ddz.SET.4:Z +---- + +iter +first +prev +next +next +next +next +next +---- +:A +. +:A +:E +:F +:G +:H + +iter +last +next +prev +prev +prev +---- +:Z +. +:Z +:Y +:X + +iter +first +prev +next +next +seek-ge x +prev +prev +---- +:A +. +:A +:E +. +:Z +:Y + +iter +first +prev +next +next +seek-prefix-ge x +prev +prev +---- +:A +. +:A +:E +. +. +. + +iter +last +next +prev +prev +seek-lt a +next +next +---- +:Z +. +:Z +:Y +. +:A +:E + +# Test that SeekPrefixGE does not position the iterator far outside the iterator bounds. +# Doing so would break the subsequent SeekGE that is utilizing the next instead of seek +# optimization. +iter +set-bounds lower=a upper=aae +seek-ge a +seek-prefix-ge aad +set-bounds lower=aae upper=b +seek-ge aae +next +---- +. +:A +. +. +:E +:F + +# Test that using Next does not mislead a twoLevelIterator into believing that the +# iterator has been positioned based on the latest iterator bounds. The Next call +# immediately after SetBounds has a non-deterministic result, hence we use +# next-ignore-result. +iter +set-bounds lower=bbq upper=d +seek-ge bbq +set-bounds lower=b upper=bbf +next-ignore-result +set-bounds lower=bbf upper=c +seek-ge bbf +next +---- +. +:Q +. +. +. +:F +:G + +build +a@10.SET.10:a10 +a@5.SET.5:a5 +b@20.SET.20:b20 +b@17.SET.17:b17 +c@30.SET.30:c30 +d@40.SET.40:d40 +---- + +iter +first +next +next +next +next +next +next +---- +:a10 +:a5 +:b20 +:b17 +:c30 +:d40 +. + +iter +seek-ge a@5 +prev +seek-lt b +next +next +seek-lt c +prev +seek-ge b@18 +prev +next +---- +:a5 +:a10 +:a5 +:b20 +:b17 +:b17 +:b20 +:b17 +:b20 +:b17 + +iter +seek-ge a@10 +next-prefix +next-prefix +next-prefix +next-prefix +---- +:a10 +:b20 +:c30 +:d40 +. + +build +a@10.SET.10:a10 +a@5.SET.5:a5 +a@3.DEL.3: +aa@30.SET.10:aa30 +abcd@50.SET.10:abcd50 +abcd@49.SET.9:abcd49 +abcd@48.SET.8:abcd48 +abcd@47.SET.7:abcd47 +b@20.SET.20:b20 +b@17.SET.17:b17 +b@15.SET.15:b15 +c.SET.20:c +c@90.SET.18:c90 +d@70.SET.16:d70 +---- + +iter +seek-ge a@10 +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +---- +:a10 +:aa30 +:abcd50 +:b20 +:c +:d70 +. + + +build +a@49.SET.49:a49 +a@48.SET.48:a48 +a@47.SET.47:a47 +a@46.SET.46:a46 +a@45.SET.45:a45 +a@44.SET.44:a44 +a@43.SET.43:a43 +a@42.SET.42:a42 +a@41.SET.41:a41 +a@40.SET.40:a40 +a@39.SET.39:a39 +a@38.SET.38:a38 +a@37.SET.37:a37 +a@36.SET.36:a36 +a@35.SET.35:a35 +a@34.SET.34:a34 +a@33.SET.33:a33 +a@32.SET.32:a32 +a@31.SET.31:a31 +a@30.SET.30:a30 +a@29.SET.29:a29 +a@28.SET.28:a28 +a@27.SET.27:a27 +a@26.SET.26:a26 +a@25.SET.25:a25 +a@24.SET.24:a24 +a@23.SET.23:a23 +a@22.SET.22:a22 +a@21.SET.21:a21 +a@20.SET.20:a20 +a@19.SET.19:a19 +a@18.SET.18:a18 +a@17.SET.17:a17 +a@16.SET.16:a16 +a@15.SET.15:a15 +a@14.SET.14:a14 +a@13.SET.13:a13 +a@12.SET.12:a12 +a@11.SET.11:a11 +a@10.SET.10:a10 +b@20.SET.20:b20 +---- + +iter +seek-ge a@49 +next-prefix +next-prefix +seek-ge a@47 +next-prefix +next-prefix +seek-ge a@36 +next-prefix +next-prefix +seek-ge a@33 +next-prefix +next-prefix +seek-ge a@30 +next-prefix +next-prefix +seek-ge a@26 +next-prefix +next-prefix +seek-ge a@20 +next-prefix +next-prefix +seek-ge aa@10 +next-prefix +---- +:a49 +:b20 +. +:a47 +:b20 +. +:a36 +:b20 +. +:a33 +:b20 +. +:a30 +:b20 +. +:a26 +:b20 +. +:a20 +:b20 +. +:b20 +. diff --git a/pebble/sstable/testdata/reader_bpf/Pebblev2/iter b/pebble/sstable/testdata/reader_bpf/Pebblev2/iter new file mode 100644 index 0000000..407eb0d --- /dev/null +++ b/pebble/sstable/testdata/reader_bpf/Pebblev2/iter @@ -0,0 +1,56 @@ +# Test case for bug https://github.com/cockroachdb/pebble/issues/2036 Build +# sstable with two-level index, with two data blocks in each lower-level index +# block. +build block-size=1 index-block-size=40 print-layout=true +c@10.SET.10:cAT10 +d@7.SET.9:dAT7 +e@15.SET.8:eAT15 +f@7.SET.5:fAT7 +---- +index entries: + d@7: size 53 + c@10: size 28 + d@7: size 26 + g: size 51 + e@15: size 28 + g: size 26 + +iter +first +next +next +next +---- + + + + + + +# The block property filter matches data block 2 and 4. +iter block-property-filter=(7,8) +first +next +---- + + + +# Use the same block property filter, but use seeks to find these entries. +# With the bug the second seek-ge below would step to the second lower-level +# index block and only see the entry in the data block 4. +iter block-property-filter=(7,8) +set-bounds lower=a upper=c +seek-ge a +seek-ge b true +set-bounds lower=c upper=g +seek-ge c +next +next +---- +. +. +. +. + + +. diff --git a/pebble/sstable/testdata/reader_bpf/Pebblev3/iter b/pebble/sstable/testdata/reader_bpf/Pebblev3/iter new file mode 100644 index 0000000..8a37664 --- /dev/null +++ b/pebble/sstable/testdata/reader_bpf/Pebblev3/iter @@ -0,0 +1,146 @@ +# Test case for bug https://github.com/cockroachdb/pebble/issues/2036 Build +# sstable with two-level index, with two data blocks in each lower-level index +# block. +build block-size=1 index-block-size=40 print-layout=true +c@10.SET.10:cAT10 +d@7.SET.9:dAT7 +e@15.SET.8:eAT15 +f@7.SET.5:fAT7 +---- +index entries: + d@7: size 53 + c@10: size 29 + d@7: size 27 + g: size 51 + e@15: size 29 + g: size 27 + +iter +first +next +next +next +---- + + + + + + +# The block property filter matches data block 2 and 4. +iter block-property-filter=(7,8) +first +next +---- + + + +# Use the same block property filter, but use seeks to find these entries. +# With the bug the second seek-ge below would step to the second lower-level +# index block and only see the entry in the data block 4. +iter block-property-filter=(7,8) +set-bounds lower=a upper=c +seek-ge a +seek-ge b true +set-bounds lower=c upper=g +seek-ge c +next +next +---- +. +. +. +. + + +. + +# Regression test for #2816 +# +# This unit test tests a scenario where the two-level index iterator's position +# could diverge from the currently loaded index block. When taking advantage of +# the monotonic bounds optimization at the two-level index level, the iterator +# would mistakenly seek within the wrong index block. +# +# This allowed the final `seek-ge wc` and `next` to both return wz@8. + +build block-size=1 index-block-size=1 print-layout=true +eu@2.SET.2:eu +wb@2.SET.2:wb +wz@8.SET.8:wzAT8 +ye@1.SET.1:yeAT1 +---- +index entries: + f: size 26 + f: size 26 + wc: size 27 + wc: size 26 + x: size 26 + x: size 29 + z: size 26 + z: size 29 + +iter block-property-filter=(8,9) +set-bounds lower=v upper=v +seek-ge wz@8 +internal-iter-state +seek-ge wb@2 +internal-iter-state +set-bounds lower=v upper=z +internal-iter-state +seek-ge wc +internal-iter-state +next +---- +. +. +| *sstable.twoLevelIterator: +| topLevelIndex.Key() = "x#72057594037927935,17" +| topLevelIndex.InPlaceValue() = (Offset: 193, Length: 26, Props: 00020801) +| topLevelIndex.isDataInvalidated()=false +| index.Key() = "x#72057594037927935,17" +| index.InPlaceValue() = (Offset: 62, Length: 29, Props: 00020801) +| index.isDataInvalidated()=false +| data.isDataInvalidated()=false +| hideObsoletePoints = false +| dataBH = (Offset: 62, Length: 29) +| (boundsCmp,positionedUsingLatestBounds) = (0,true) +| exhaustedBounds = 1 +. +| *sstable.twoLevelIterator: +| topLevelIndex.Key() = "wc#72057594037927935,17" +| topLevelIndex.InPlaceValue() = (Offset: 161, Length: 27, Props: 00020201) +| topLevelIndex.isDataInvalidated()=false +| index iter invalid +| index.isDataInvalidated()=true +| data.isDataInvalidated()=true +| hideObsoletePoints = false +| dataBH = (Offset: 62, Length: 29) +| (boundsCmp,positionedUsingLatestBounds) = (0,true) +| exhaustedBounds = 1 +. +| *sstable.twoLevelIterator: +| topLevelIndex.Key() = "wc#72057594037927935,17" +| topLevelIndex.InPlaceValue() = (Offset: 161, Length: 27, Props: 00020201) +| topLevelIndex.isDataInvalidated()=false +| index iter invalid +| index.isDataInvalidated()=true +| data.isDataInvalidated()=true +| hideObsoletePoints = false +| dataBH = (Offset: 62, Length: 29) +| (boundsCmp,positionedUsingLatestBounds) = (1,false) +| exhaustedBounds = 1 + +| *sstable.twoLevelIterator: +| topLevelIndex.Key() = "x#72057594037927935,17" +| topLevelIndex.InPlaceValue() = (Offset: 193, Length: 26, Props: 00020801) +| topLevelIndex.isDataInvalidated()=false +| index.Key() = "x#72057594037927935,17" +| index.InPlaceValue() = (Offset: 62, Length: 29, Props: 00020801) +| index.isDataInvalidated()=false +| data.isDataInvalidated()=false +| hideObsoletePoints = false +| dataBH = (Offset: 62, Length: 29) +| (boundsCmp,positionedUsingLatestBounds) = (0,false) +| exhaustedBounds = 0 +. diff --git a/pebble/sstable/testdata/reader_hide_obsolete/iter b/pebble/sstable/testdata/reader_hide_obsolete/iter new file mode 100644 index 0000000..5f6eab7 --- /dev/null +++ b/pebble/sstable/testdata/reader_hide_obsolete/iter @@ -0,0 +1,318 @@ +build +a.SET.1:A +b.SINGLEDEL.4: +b.SET.2:B +c.DEL.5: +c.SET.3:C +d.SET.4:D4 +d.SET.2:D2 +---- + +iter +first +next +next +next +next +next +next +next +prev +prev +prev +prev +prev +prev +prev +prev +---- +:A +: +:B +: +:C +:D4 +:D2 +. +:D2 +:D4 +:C +: +:B +: +:A +. + +iter hide-obsolete-points=true +first +next +next +next +next +prev +prev +prev +prev +prev +---- +:A +: +: +:D4 +. +:D4 +: +: +:A +. + +iter hide-obsolete-points=true +seek-ge c +prev +prev +next +next +next +seek-lt c +next +prev +prev +---- +: +: +:A +: +: +:D4 +: +: +: +:A + +build +a.SET.3:A +a.MERGE.2:A2 +b.MERGE.20:B20 +b.MERGE.18:B18 +b.SET.16:B16 +b.SET.14:B14 +c.MERGE.30:C30 +c.MERGE.28:C28 +c.DEL.26: +---- + +iter +first +next +next +next +next +next +next +next +---- +:A +:A2 +:B20 +:B18 +:B16 +:B14 +:C30 +:C28 + +iter hide-obsolete-points=true +first +next +next +next +next +next +next +last +prev +prev +prev +prev +prev +prev +---- +:A +:B20 +:B18 +:B16 +:C30 +:C28 +: +: +:C28 +:C30 +:B16 +:B18 +:B20 +:A + +build +b.MERGE.20:B20 +b.SETWITHDEL.16:B16 +b.SETWITHDEL.14:B14 +c.MERGE.30:C30 +c.DELSIZED.28: +c.DEL.26: +---- + +iter +first +next +next +next +next +next +next +---- +:B20 +:B16 +:B14 +:C30 +: +: +. + +iter hide-obsolete-points=true +first +next +next +next +next +---- +:B20 +:B16 +:C30 +: +. + +build +b.SETWITHDEL.20:B20 +b.MERGE.16:B16 +b.MERGE.14:B14 +b.SET.12:B12 +---- + +iter +first +next +next +next +next +---- +:B20 +:B16 +:B14 +:B12 +. + +iter hide-obsolete-points=true +first +next +---- +:B20 +. + +build writing-to-lowest-level +a.SET.10:A10 +b.DEL.20: +b.MERGE.16:B16 +b.MERGE.14:B14 +b.SET.12:B12 +---- + +iter +first +next +next +next +next +next +---- +:A10 +: +:B16 +:B14 +:B12 +. + +iter hide-obsolete-points=true +first +next +---- +:A10 +. + +build writing-to-lowest-level +a.SET.10:A10 +b.DEL.16:B16 +b.SETWITHDEL.14:B14 +c.DELSIZED.30: +c.SET.28:C28 +d.SINGLEDEL.40: +d.MERGE.30:D30 +---- + +iter +first +next +next +next +next +next +next +next +---- +:A10 +:B16 +:B14 +: +:C28 +: +:D30 +. + +iter hide-obsolete-points=true +first +next +---- +:A10 +. + +build writing-to-lowest-level +force-obsolete: a.SET.1:A +force-obsolete: b.SINGLEDEL.4: +force-obsolete: b.SET.2:B +c.DEL.5: +force-obsolete: d.SET.10:D10 +---- + +iter +first +next +next +next +next +next +---- +:A +: +:B +: +:D10 +. + +iter hide-obsolete-points=true +first +---- +table does not intersect BlockPropertyFilter + +build is-strict-obsolete +d.SINGLEDEL.40: +d.MERGE.30:D30 +---- +MERGE not supported in a strict-obsolete sstable diff --git a/pebble/sstable/testdata/readerstats_LevelDB/iter b/pebble/sstable/testdata/readerstats_LevelDB/iter new file mode 100644 index 0000000..1ba247d --- /dev/null +++ b/pebble/sstable/testdata/readerstats_LevelDB/iter @@ -0,0 +1,59 @@ +# Two keys in each data block. +build block-size=30 index-block-size=30 cache-size=10000 +a.SET.1:A +b.SET.2:B +c.SET.3:C +d.SET.4:D +---- + +# The first iteration has cache misses for both blocks. The second iteration +# hits the cache. Then reset stats. +iter +first +stats +next +stats +next +stats +next +stats +next +stats +first +stats +next +stats +next +stats +next +stats +next +stats +reset-stats +stats +first +stats +---- + +{BlockBytes:74 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:74 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:108 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:108 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +. +{BlockBytes:108 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:142 BlockBytesInCache:34 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:142 BlockBytesInCache:34 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:176 BlockBytesInCache:68 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:176 BlockBytesInCache:68 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +. +{BlockBytes:176 BlockBytesInCache:68 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:34 BlockBytesInCache:34 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} diff --git a/pebble/sstable/testdata/readerstats_Pebblev3/iter b/pebble/sstable/testdata/readerstats_Pebblev3/iter new file mode 100644 index 0000000..b20a4b2 --- /dev/null +++ b/pebble/sstable/testdata/readerstats_Pebblev3/iter @@ -0,0 +1,79 @@ +build print-layout=true +c@10.SET.10:cAT10 +c@9.SET.9:cAT9 +c@8.SET.8:cAT8 +d@7.SET.9:dAT7 +e@39.SET.49:eAT39 +e@38.SET.48:eAT38 +e@37.SET.47:eAT37 +e@36.SET.46:eAT36 +e@35.SET.45:eAT35 +e@34.SET.44:eAT34 +e@33.SET.43:eAT33 +e@32.SET.42:eAT32 +e@31.SET.41:eAT31 +e@30.SET.40:eAT30 +e@29.SET.39:eAT29 +e@28.SET.38:eAT28 +e@27.SET.37:eAT27 +e@26.SET.36:eAT26 +---- +index entries: + f: size 228 + +# Iterating across older versions and fetching the older version values. +iter +first +stats +next +stats +next +stats +next +stats +---- + +{BlockBytes:251 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +{BlockBytes:328 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:1 ValueBytes:4 ValueBytesFetched:4}} + +{BlockBytes:328 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:2 ValueBytes:8 ValueBytesFetched:8}} + +{BlockBytes:328 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:2 ValueBytes:8 ValueBytesFetched:8}} + +# seek-ge e@37 starts at the restart point at the beginning of the block and +# iterates over 3 irrelevant separated versions before getting to e@37 +# (another separated version). Which is why the SeparatedPointValue count is +# 4. Only the last separated version has its value fetched. +iter +seek-ge e@37 +stats +next +next +next +next +stats +---- + +{BlockBytes:328 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:4 ValueBytes:18 ValueBytesFetched:5}} + + + + +{BlockBytes:328 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:8 ValueBytes:38 ValueBytesFetched:25}} + +# seek-ge e@26 lands at the restart point e@26. +iter +seek-ge e@26 +stats +prev +stats +prev +stats +---- + +{BlockBytes:328 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:1 ValueBytes:5 ValueBytesFetched:5}} + +{BlockBytes:328 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:2 ValueBytes:10 ValueBytesFetched:10}} + +{BlockBytes:328 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:3 ValueBytes:15 ValueBytesFetched:15}} diff --git a/pebble/sstable/testdata/rewriter b/pebble/sstable/testdata/rewriter new file mode 100644 index 0000000..3f2ea58 --- /dev/null +++ b/pebble/sstable/testdata/rewriter @@ -0,0 +1,237 @@ +build block-size=1 index-block-size=1 filter +a_xyz.SET.1:a +b_xyz.SET.1:b +c_xyz.SET.1:c +---- +point: [a_xyz#1,1-c_xyz#1,1] +seqnums: [1-1] + +rewrite from=xyz to=123 block-size=1 index-block-size=1 filter +---- +rewrite failed: a valid splitter is required to rewrite suffixes + +rewrite from=xyz to=123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +rewrite failed: rewriting data blocks: mismatched Comparer leveldb.BytewiseComparator vs comparer-split-4b-suffix, replacement requires same splitter to copy filters + +build block-size=1 index-block-size=1 filter comparer-split-4b-suffix +aa_xyz.SET.1:a +ba_xyz.SET.1:b +ca_xyz.SET.1:c +---- +point: [aa_xyz#1,1-ca_xyz#1,1] +seqnums: [1-1] + +rewrite from=yz to=23 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +rewrite failed: rewriting data blocks: key has suffix "_xyz", expected "yz" + +rewrite from=a_xyz to=a_123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +rewrite failed: rewriting data blocks: key has suffix "_xyz", expected "a_xyz" + +build block-size=1 index-block-size=1 filter comparer-split-4b-suffix +a_xyz.SET.1:a +b_xyz.SET.1:b +c_xyz.SET.1:c +---- +point: [a_xyz#1,1-c_xyz#1,1] +seqnums: [1-1] + +layout +---- + 0 data (25) + 30 data (25) + 60 data (25) + 90 filter (69) + 164 index (22) + 191 index (22) + 218 index (22) + 245 top-index (48) + 298 properties (630) + 933 meta-index (79) + 1017 footer (53) + 1070 EOF + +scan +---- +a_xyz#1,1:a +b_xyz#1,1:b +c_xyz#1,1:c + +get +b_xyz +f_xyz +c_xyz +---- +b +get f_xyz: pebble: not found +c + +rewrite from=_xyz to=_123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +point: [a_123#1,1-c_123#1,1] +seqnums: [1-1] + +layout +---- + 0 data (25) + 30 data (25) + 60 data (25) + 90 filter (69) + 164 index (22) + 191 index (22) + 218 index (22) + 245 top-index (48) + 298 properties (630) + 933 meta-index (79) + 1017 footer (53) + 1070 EOF + +scan +---- +a_123#1,1:a +b_123#1,1:b +c_123#1,1:c + +get +b_123 +f_123 +c_123 +---- +b +get f_123: pebble: not found +c + +rewrite from=_123 to=_456 block-size=1 index-block-size=1 filter comparer-split-4b-suffix concurrency=2 +---- +point: [a_456#1,1-c_456#1,1] +seqnums: [1-1] + +layout +---- + 0 data (25) + 30 data (25) + 60 data (25) + 90 filter (69) + 164 index (22) + 191 index (22) + 218 index (22) + 245 top-index (48) + 298 properties (630) + 933 meta-index (79) + 1017 footer (53) + 1070 EOF + +scan +---- +a_456#1,1:a +b_456#1,1:b +c_456#1,1:c + +get +b_456 +f_456 +c_456 +---- +b +get f_456: pebble: not found +c + +rewrite from=_456 to=_xyz block-size=1 index-block-size=1 filter comparer-split-4b-suffix concurrency=3 +---- +point: [a_xyz#1,1-c_xyz#1,1] +seqnums: [1-1] + +layout +---- + 0 data (25) + 30 data (25) + 60 data (25) + 90 filter (69) + 164 index (22) + 191 index (22) + 218 index (22) + 245 top-index (48) + 298 properties (630) + 933 meta-index (79) + 1017 footer (53) + 1070 EOF + +scan +---- +a_xyz#1,1:a +b_xyz#1,1:b +c_xyz#1,1:c + +get +b_xyz +f_xyz +c_xyz +---- +b +get f_xyz: pebble: not found +c + + +rewrite from=_xyz to=_123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix concurrency=4 +---- +point: [a_123#1,1-c_123#1,1] +seqnums: [1-1] + +layout +---- + 0 data (25) + 30 data (25) + 60 data (25) + 90 filter (69) + 164 index (22) + 191 index (22) + 218 index (22) + 245 top-index (48) + 298 properties (630) + 933 meta-index (79) + 1017 footer (53) + 1070 EOF + +scan +---- +a_123#1,1:a +b_123#1,1:b +c_123#1,1:c + +get +b_123 +f_123 +c_123 +---- +b +get f_123: pebble: not found +c + +# Rewrite a table that contain only range keys. + +build block-size=1 index-block-size=1 filter comparer-split-4b-suffix +rangekey: a-b:{(#1,RANGEKEYSET,_xyz)} +rangekey: b-c:{(#1,RANGEKEYSET,_xyz)} +rangekey: c-d:{(#1,RANGEKEYSET,_xyz)} +---- +rangekey: [a#1,21-d#72057594037927935,21] +seqnums: [1-1] + +scan-range-key +---- +a-b:{(#1,RANGEKEYSET,_xyz)} +b-c:{(#1,RANGEKEYSET,_xyz)} +c-d:{(#1,RANGEKEYSET,_xyz)} + +rewrite from=_xyz to=_123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +rangekey: [a#1,21-d#72057594037927935,21] +seqnums: [1-1] + +scan-range-key +---- +a-b:{(#1,RANGEKEYSET,_123)} +b-c:{(#1,RANGEKEYSET,_123)} +c-d:{(#1,RANGEKEYSET,_123)} diff --git a/pebble/sstable/testdata/rewriter_v3 b/pebble/sstable/testdata/rewriter_v3 new file mode 100644 index 0000000..4f37789 --- /dev/null +++ b/pebble/sstable/testdata/rewriter_v3 @@ -0,0 +1,237 @@ +build block-size=1 index-block-size=1 filter +a_xyz.SET.1:a +b_xyz.SET.1:b +c_xyz.SET.1:c +---- +point: [a_xyz#1,1-c_xyz#1,1] +seqnums: [1-1] + +rewrite from=xyz to=123 block-size=1 index-block-size=1 filter +---- +rewrite failed: a valid splitter is required to rewrite suffixes + +rewrite from=xyz to=123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +rewrite failed: rewriting data blocks: mismatched Comparer leveldb.BytewiseComparator vs comparer-split-4b-suffix, replacement requires same splitter to copy filters + +build block-size=1 index-block-size=1 filter comparer-split-4b-suffix +aa_xyz.SET.1:a +ba_xyz.SET.1:b +ca_xyz.SET.1:c +---- +point: [aa_xyz#1,1-ca_xyz#1,1] +seqnums: [1-1] + +rewrite from=yz to=23 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +rewrite failed: rewriting data blocks: key has suffix "_xyz", expected "yz" + +rewrite from=a_xyz to=a_123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +rewrite failed: rewriting data blocks: key has suffix "_xyz", expected "a_xyz" + +build block-size=1 index-block-size=1 filter comparer-split-4b-suffix +a_xyz.SET.1:a +b_xyz.SET.1:b +c_xyz.SET.1:c +---- +point: [a_xyz#1,1-c_xyz#1,1] +seqnums: [1-1] + +layout +---- + 0 data (26) + 31 data (26) + 62 data (26) + 93 filter (69) + 167 index (22) + 194 index (22) + 221 index (22) + 248 top-index (48) + 301 properties (630) + 936 meta-index (79) + 1020 footer (53) + 1073 EOF + +scan +---- +a_xyz#1,1:a +b_xyz#1,1:b +c_xyz#1,1:c + +get +b_xyz +f_xyz +c_xyz +---- +b +get f_xyz: pebble: not found +c + +rewrite from=_xyz to=_123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +point: [a_123#1,1-c_123#1,1] +seqnums: [1-1] + +layout +---- + 0 data (26) + 31 data (26) + 62 data (26) + 93 filter (69) + 167 index (22) + 194 index (22) + 221 index (22) + 248 top-index (48) + 301 properties (630) + 936 meta-index (79) + 1020 footer (53) + 1073 EOF + +scan +---- +a_123#1,1:a +b_123#1,1:b +c_123#1,1:c + +get +b_123 +f_123 +c_123 +---- +b +get f_123: pebble: not found +c + +rewrite from=_123 to=_456 block-size=1 index-block-size=1 filter comparer-split-4b-suffix concurrency=2 +---- +point: [a_456#1,1-c_456#1,1] +seqnums: [1-1] + +layout +---- + 0 data (26) + 31 data (26) + 62 data (26) + 93 filter (69) + 167 index (22) + 194 index (22) + 221 index (22) + 248 top-index (48) + 301 properties (630) + 936 meta-index (79) + 1020 footer (53) + 1073 EOF + +scan +---- +a_456#1,1:a +b_456#1,1:b +c_456#1,1:c + +get +b_456 +f_456 +c_456 +---- +b +get f_456: pebble: not found +c + +rewrite from=_456 to=_xyz block-size=1 index-block-size=1 filter comparer-split-4b-suffix concurrency=3 +---- +point: [a_xyz#1,1-c_xyz#1,1] +seqnums: [1-1] + +layout +---- + 0 data (26) + 31 data (26) + 62 data (26) + 93 filter (69) + 167 index (22) + 194 index (22) + 221 index (22) + 248 top-index (48) + 301 properties (630) + 936 meta-index (79) + 1020 footer (53) + 1073 EOF + +scan +---- +a_xyz#1,1:a +b_xyz#1,1:b +c_xyz#1,1:c + +get +b_xyz +f_xyz +c_xyz +---- +b +get f_xyz: pebble: not found +c + + +rewrite from=_xyz to=_123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix concurrency=4 +---- +point: [a_123#1,1-c_123#1,1] +seqnums: [1-1] + +layout +---- + 0 data (26) + 31 data (26) + 62 data (26) + 93 filter (69) + 167 index (22) + 194 index (22) + 221 index (22) + 248 top-index (48) + 301 properties (630) + 936 meta-index (79) + 1020 footer (53) + 1073 EOF + +scan +---- +a_123#1,1:a +b_123#1,1:b +c_123#1,1:c + +get +b_123 +f_123 +c_123 +---- +b +get f_123: pebble: not found +c + +# Rewrite a table that contain only range keys. + +build block-size=1 index-block-size=1 filter comparer-split-4b-suffix +rangekey: a-b:{(#1,RANGEKEYSET,_xyz)} +rangekey: b-c:{(#1,RANGEKEYSET,_xyz)} +rangekey: c-d:{(#1,RANGEKEYSET,_xyz)} +---- +rangekey: [a#1,21-d#72057594037927935,21] +seqnums: [1-1] + +scan-range-key +---- +a-b:{(#1,RANGEKEYSET,_xyz)} +b-c:{(#1,RANGEKEYSET,_xyz)} +c-d:{(#1,RANGEKEYSET,_xyz)} + +rewrite from=_xyz to=_123 block-size=1 index-block-size=1 filter comparer-split-4b-suffix +---- +rangekey: [a#1,21-d#72057594037927935,21] +seqnums: [1-1] + +scan-range-key +---- +a-b:{(#1,RANGEKEYSET,_123)} +b-c:{(#1,RANGEKEYSET,_123)} +c-d:{(#1,RANGEKEYSET,_123)} diff --git a/pebble/sstable/testdata/size_estimate b/pebble/sstable/testdata/size_estimate new file mode 100644 index 0000000..114ac69 --- /dev/null +++ b/pebble/sstable/testdata/size_estimate @@ -0,0 +1,144 @@ +# Sequence of ops which tests all of the code paths in the size_estimate type. + +init 1 +---- +success + +# Empty size should be 1 +size +---- +1 + +# There's a single inflight entry, so the size should be 4. +add_inflight 4 +---- +4 + +num_inflight_entries +---- +1 + +num_entries +---- +1 + +# Compression ratio defaults to 1, so the size of the inflight entry fully +# counts towards size. +size +---- +4 + +# After compression, entry only had a size of 3. The total size is therefore +# 3, since this is the first entry. The max estimated size is 4 since we +# ensure that it is monotonically non decreasing. +entry_written 3 4 +---- +4 + +num_entries +---- +1 + +# There should be 0 inflight entries once the previous entry has been written. +num_inflight_entries +---- +0 + +# Compression ratio is 0.75 at this point. The total size is 3, and the inflight +# size is 5, so that returned size is uint64(3 + 0.75*5) = uint64(6.75). +add_inflight 5 +---- +6 + +num_entries +---- +2 + +# We don't clear the empty size, so even after clearing a size of 1 is returned. +clear +---- +1 + +# Test writing multiple inflight entries. +add_inflight 4 +---- +4 + +add_inflight 5 +---- +9 + +num_entries +---- +2 + +num_inflight_entries +---- +2 + +# First inflight entry written. The entry didn't get compressed. The total size +# now is less than 9, but the max estimated size should still be 9. +entry_written 4 4 +---- +9 + +num_entries +---- +2 + +num_inflight_entries +---- +1 + +# At this point, inflightSize is 13, the totalSize is 4. The compression ratio +# is 1. So, the returned size should be 17. +add_inflight 8 +---- +17 + +# One entry has been written. +num_written_entries +---- +1 + +# The inflight entry had a size of 5, but the entry added had a size of 3 +# because of compression/size estimation. The compression ratio is (4+3)/(4+5) +# = 0.77 at this point. The inflightSize is 8. The true size is 7+8*0.77 = +# 13.22, but the maxEstimatedSize is returned. +entry_written 7 5 +---- +17 + +# The inflight size is 0, and the total size is 11. +entry_written 11 8 +---- +17 + +num_written_entries +---- +3 + +# The compression ratio is (4+3+4)/(4+5+8)=0.647, and the inflight size is 20, +# 20*0.64 = 12.94, so the total size is uint64(12.94 + 11) +add_inflight 20 +---- +23 + +num_inflight_entries +---- +1 + +# We can write an entry, which increases the written size from 11 to 19, but +# it might not have an inflightSize, because it was never inflight. In such a +# case, the numInflightEntries, shouldn't be decreased. +entry_written 19 0 +---- +31 + +num_inflight_entries +---- +1 + +num_written_entries +---- +4 diff --git a/pebble/sstable/testdata/virtual_reader b/pebble/sstable/testdata/virtual_reader new file mode 100644 index 0000000..1fa1669 --- /dev/null +++ b/pebble/sstable/testdata/virtual_reader @@ -0,0 +1,692 @@ +# Test 1: Start with a simple sanity checking test which uses singleLevel +# iterators as the backing iterator for the sstable. This will also test the +# compaction iterator since it's the simplest. +build +a.SET.1:a +b.SET.1:b +c.SET.1:c +d.SET.1:d +---- +point: [a#1,1-d#1,1] +seqnums: [1-1] + +# Note that the RawKeySize,RawValueSize aren't accurate here because we use +# Reader.EstimateDiskUsage with virtual sstables bounds to determine virtual +# sstable size which is then used to extrapolate virtual sstable properties, +# and for tiny sstables, virtual sstable sizes aren't accurate. In this +# testcase, the virtual sstable size is 50, whereas the backing sstable size is +# 850. +virtualize b.SET.1-c.SET.1 +---- +bounds: [b#1,1-c#1,1] +filenum: 000002 +props: NumEntries: 1, RawKeySize: 3, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 0 + +citer +---- +b#1,1:b +c#1,1:c + +# Test 2: Similar to test 1 but force two level iterators. +build twoLevel +a.SET.1:a +b.SET.1:b +c.SET.1:c +d.SET.1:d +---- +point: [a#1,1-d#1,1] +seqnums: [1-1] + +virtualize b.SET.1-c.SET.1 +---- +bounds: [b#1,1-c#1,1] +filenum: 000004 +props: NumEntries: 1, RawKeySize: 2, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 0 + +citer +---- +b#1,1:b +c#1,1:c + +# Test the constrain bounds function. It performs some subtle shrinking and +# expanding of bounds. The current virtual sstable bounds are [b,c]. +# 1. start key < virtual sstable start key, end key is exclusive. +constrain a,bb,false +---- +b,bb,false + +# 2. start key < virtual sstable start key, end key is inclusive. +constrain a,bb,true +---- +b,bb,true + +# 3. start key is within virtual sstable bounds, end key is at virtual sstable +# end bound, but is exclusive. +constrain bb,c,false +---- +bb,c,false + +# 3. start key is within virtual sstable bounds, end key is at virtual sstable +# end bound, but is inclusive. +constrain bb,c,true +---- +bb,c,true + +# 4. start key is within virtual sstable bounds, end key is above virtual +# sstable end bound and is exclusive. +constrain bb,e,false +---- +bb,c,true + +# 5. start key is within virtual sstable bounds, end key is above virtual +# sstable end bound and is inclusive. +constrain bb,e,true +---- +bb,c,true + +# 6. Both start, end keys fit within virtual sstable bounds. +constrain bb,bbb,false +---- +bb,bbb,false + +# 6. Both start, end keys are out of bounds, but overlap. +constrain a,d,false +---- +b,c,true + +# 7. start, end keys have no overlap with virtual sstable bounds. Note that +# lower becomes greater than upper here. We support this in the iterators +# and don't return any keys for this case. +constrain a,aa,false +---- +b,aa,false + +scan-range-del +---- + +scan-range-key +---- + +# Test 3: Tests raw range key/range del iterators, and makes sure that they +# respect virtual bounds. +build twoLevel +a.SET.1:a +d.SET.2:d +f.SET.3:f +d.RANGEDEL.4:e +rangekey: a-d:{(#11,RANGEKEYSET,@t10,foo)} +g.RANGEDEL.5:l +rangekey: y-z:{(#12,RANGEKEYSET,@t11,foo)} +---- +point: [a#1,1-f#3,1] +rangedel: [d#4,15-l#72057594037927935,15] +rangekey: [a#11,21-z#72057594037927935,21] +seqnums: [1-12] + +# Note that we shouldn't have range del spans which cross virtual sstable +# boundaries. NumRangeKeySets must be > 1. +virtualize a.SET.1-f.SET.1 +---- +bounds: [a#1,1-f#1,1] +filenum: 000006 +props: NumEntries: 1, RawKeySize: 4, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 1, NumRangeDeletions: 1, NumRangeKeyDels: 0, NumRangeKeySets: 1, ValueBlocksSize: 0 + +scan-range-del +---- +d-e:{(#4,RANGEDEL)} + +scan-range-key +---- +a-d:{(#11,RANGEKEYSET,@t10,foo)} + +# Test 4: Test iterators with various bounds, and various operations. This calls +# VirtualReader.NewIterWithBlockPropertyFilters and performs various operations +# on those. +build +a.SET.1:a +b.SET.2:b +c.SET.3:c +d.SET.4:d +dd.SET.5:dd +ddd.SET.6:ddd +g.SET.8:g +h.SET.9:h +---- +point: [a#1,1-h#9,1] +seqnums: [1-9] + +virtualize dd.SET.5-ddd.SET.6 +---- +bounds: [dd#5,1-ddd#6,1] +filenum: 000008 +props: NumEntries: 1, RawKeySize: 10, RawValueSize: 2, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 0 + +# Check lower bound enforcement during SeekPrefixGE. +iter +seek-prefix-ge d +next +next +---- +:dd +:ddd +. + +# Build a simpler sstable for the rest of the tests. +build +a.SET.1:a +b.SET.2:b +c.SET.3:c +d.SET.4:d +e.SET.5:e +f.SET.6:f +g.SET.8:g +h.SET.9:h +---- +point: [a#1,1-h#9,1] +seqnums: [1-9] + +# Set bounds c-f for the virtual sstable. +virtualize c.SET.3-f.SET.6 +---- +bounds: [c#3,1-f#6,1] +filenum: 000010 +props: NumEntries: 1, RawKeySize: 9, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 0 + +# Just test a basic iterator once virtual sstable bounds have been set. +iter +first +next +next +next +next +---- +:c +:d +:e +:f +. + +# Create an iterator with bounds. External bounds should still be restricted +# along with virtual sstable bounds. +iter a-d +first +next +---- +:c +. + +iter d-g +first +next +next +next +---- +:d +:e +:f +. + +# e is turned into an exclusive bounds, and thus it is hidden. +iter +set-bounds lower=d upper=e +first +next +---- +. +:d +. + +# Virtual sstable lower bound must be enforced internally from within the +# iterator. +iter +seek-ge b +next +next +next +next +---- +:c +:d +:e +:f +. + +# Upper bound enforcement by SeekGE. +iter +seek-ge g +---- +. + +# Test prev. +iter +seek-ge d +prev +next +prev +prev +---- +:d +:c +:d +:c +. + +# Test SeekLT +build +a.SET.1:a +b.SET.2:b +c.SET.3:c +d.SET.4:d +e.SET.5:e +f.SET.6:f +f.SET.1:ff +g.SET.8:g +h.SET.9:h +---- +point: [a#1,1-h#9,1] +seqnums: [1-9] + +virtualize c.SET.3-f.SET.1:ff +---- +bounds: [c#3,1-f#0,1] +filenum: 000012 +props: NumEntries: 2, RawKeySize: 11, RawValueSize: 2, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 3 + +iter +set-bounds lower=d upper=e +seek-lt e +---- +. +:d + +iter +seek-ge f +next +next +---- +:f +:ff +. + +iter +seek-lt f +next +next +prev +prev +prev +prev +prev +---- +:e +:f +:ff +:f +:e +:d +:c +. + +# We should get f here, not g as SeekLT will apply the virtual sstable end +# bound. +iter +seek-lt h +---- +:ff + +iter +last +---- +:ff + +virtualize f.SET.6-h.SET.9 +---- +bounds: [f#6,1-h#9,1] +filenum: 000013 +props: NumEntries: 2, RawKeySize: 11, RawValueSize: 2, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 3 + +iter +seek-lt z +---- +:h + +iter +last +---- +:h + +iter +set-bounds lower=c upper=g +first +last +---- +. +:f +:ff + +# Test 5: Same as test 4, but force two level iterators. +build twoLevel +a.SET.1:a +b.SET.2:b +c.SET.3:c +d.SET.4:d +dd.SET.5:dd +ddd.SET.6:ddd +g.SET.8:g +h.SET.9:h +---- +point: [a#1,1-h#9,1] +seqnums: [1-9] + +virtualize dd.SET.5-ddd.SET.6 +---- +bounds: [dd#5,1-ddd#6,1] +filenum: 000015 +props: NumEntries: 1, RawKeySize: 4, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 0 + +# Check lower bound enforcement during SeekPrefixGE. +iter +seek-prefix-ge d +next +next +---- +:dd +:ddd +. + +# Build a simpler sstable for the rest of the tests. +build twoLevel +a.SET.1:a +b.SET.2:b +c.SET.3:c +d.SET.4:d +e.SET.5:e +f.SET.6:f +g.SET.8:g +h.SET.9:h +---- +point: [a#1,1-h#9,1] +seqnums: [1-9] + +# Set bounds c-f for the virtual sstable. +virtualize c.SET.3-f.SET.6 +---- +bounds: [c#3,1-f#6,1] +filenum: 000017 +props: NumEntries: 1, RawKeySize: 7, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 0 + +# Just test a basic iterator once virtual sstable bounds have been set. +iter +first +next +next +next +next +---- +:c +:d +:e +:f +. + +# Create an iterator with bounds. External bounds should still be restricted +# along with virtual sstable bounds. +iter a-d +first +next +---- +:c +. + +iter d-g +first +next +next +next +---- +:d +:e +:f +. + +# e is turned into an exclusive bounds, and thus it is hidden. +iter +set-bounds lower=d upper=e +first +next +---- +. +:d +. + +# Virtual sstable lower bound must be enforced internally from within the +# iterator. +iter +seek-ge b +next +next +next +next +---- +:c +:d +:e +:f +. + +# Upper bound enforcement by SeekGE. +iter +seek-ge g +---- +. + +# Test prev. +iter +seek-ge d +prev +next +prev +prev +---- +:d +:c +:d +:c +. + +# Test SeekLT +build twoLevel +a.SET.1:a +b.SET.2:b +c.SET.3:c +d.SET.4:d +e.SET.5:e +f.SET.6:f +f.SET.1:ff +g.SET.8:g +h.SET.9:h +---- +point: [a#1,1-h#9,1] +seqnums: [1-9] + +virtualize c.SET.3-f.SET.1:ff +---- +bounds: [c#3,1-f#0,1] +filenum: 000019 +props: NumEntries: 1, RawKeySize: 7, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 2 + +iter +set-bounds lower=d upper=e +seek-lt e +---- +. +:d + +iter +seek-ge f +next +next +---- +:f +:ff +. + +iter +seek-lt f +next +next +prev +prev +prev +prev +prev +---- +:e +:f +:ff +:f +:e +:d +:c +. + +# We should get f here, not g as SeekLT will apply the virtual sstable end +# bound. +iter +seek-lt h +---- +:ff + +iter +last +---- +:ff + +virtualize f.SET.6-h.SET.9 +---- +bounds: [f#6,1-h#9,1] +filenum: 000020 +props: NumEntries: 1, RawKeySize: 7, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 0, NumRangeDeletions: 0, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 2 + +iter +seek-lt z +---- +:h + +iter +last +---- +:h + +iter +set-bounds lower=c upper=g +first +last +---- +. +:f +:ff + +# Test 6: Exclusive sentinel handling. Note that this test only ensures that +# exclusive sentinel handling is correct for some code path, but not all of +# them, in the iterators. Consider a randomized test. +build +a.SET.1:a +d.SET.2:d +e.SET.3:e +d.RANGEDEL.4:e +f.SET.5:f +---- +point: [a#1,1-f#5,1] +rangedel: [d#4,15-e#72057594037927935,15] +seqnums: [1-5] + +virtualize a.SET.1-e.RANGEDEL.72057594037927935 +---- +bounds: [a#1,1-e#72057594037927935,15] +filenum: 000022 +props: NumEntries: 1, RawKeySize: 4, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 1, NumRangeDeletions: 1, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 0 + +iter +first +next +next +seek-lt f +---- +:a +:d +. +:d + +# Don't expose e from the compaction iter. +citer +---- +a#1,1:a +d#2,1:d + +scan-range-del +---- +d-e:{(#4,RANGEDEL)} + + +build twoLevel +a.SET.1:a +d.SET.2:d +e.SET.3:e +d.RANGEDEL.4:e +f.SET.5:f +---- +point: [a#1,1-f#5,1] +rangedel: [d#4,15-e#72057594037927935,15] +seqnums: [1-5] + +virtualize a.SET.1-e.RANGEDEL.72057594037927935 +---- +bounds: [a#1,1-e#72057594037927935,15] +filenum: 000024 +props: NumEntries: 1, RawKeySize: 4, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 1, NumRangeDeletions: 1, NumRangeKeyDels: 0, NumRangeKeySets: 0, ValueBlocksSize: 0 + +iter +first +next +next +seek-lt f +---- +:a +:d +. +:d + +# Don't expose e from the compaction iter. +citer +---- +a#1,1:a +d#2,1:d + +scan-range-del +---- +d-e:{(#4,RANGEDEL)} + +# Test NumRangeKeySets. +build twoLevel +a.SET.1:a +b.SET.5:b +d.SET.2:d +f.SET.3:f +d.RANGEDEL.4:e +rangekey: a-d:{(#11,RANGEKEYSET,@t10,foo)} +g.RANGEDEL.5:l +rangekey: y-z:{(#12,RANGEKEYSET,@t11,foo)} +---- +point: [a#1,1-f#3,1] +rangedel: [d#4,15-l#72057594037927935,15] +rangekey: [a#11,21-z#72057594037927935,21] +seqnums: [1-12] + +# Virtual sstable doesn't contain range key set, but NumRangeKeySets in the +# properties must be > 0. +virtualize a.SET.1-b.SET.5 +---- +bounds: [a#1,1-b#5,1] +filenum: 000026 +props: NumEntries: 1, RawKeySize: 3, RawValueSize: 1, RawPointTombstoneKeySize: 0, RawPointTombstoneValueSize: 0, NumSizedDeletions: 0, NumDeletions: 1, NumRangeDeletions: 1, NumRangeKeyDels: 0, NumRangeKeySets: 1, ValueBlocksSize: 0 diff --git a/pebble/sstable/testdata/writer b/pebble/sstable/testdata/writer new file mode 100644 index 0000000..61d3a24 --- /dev/null +++ b/pebble/sstable/testdata/writer @@ -0,0 +1,370 @@ +build +a.SET.1:a +---- +point: [a#1,1-a#1,1] +seqnums: [1-1] + +scan +---- +a#1,1:a + +scan-range-del +---- + +scan-range-key +---- + +build props=(deletions,deleted) +a.SET.1:a +b.DEL.2: +c.MERGE.3:c +d.RANGEDEL.4:e +f.SET.5:f +g.DEL.6: +h.MERGE.7:h +i.RANGEDEL.8:j +rangekey: j-k:{(#9,RANGEKEYDEL)} +rangekey: k-l:{(#10,RANGEKEYUNSET,@t5)} +rangekey: l-m:{(#11,RANGEKEYSET,@t10,foo)} +---- +point: [a#1,1-h#7,2] +rangedel: [d#4,15-j#72057594037927935,15] +rangekey: [j#9,19-m#72057594037927935,21] +seqnums: [1-11] +props "deletions": + rocksdb.num.range-deletions: 2 +props "deleted": + rocksdb.deleted.keys: 4 + + +build props=(deletions,deleted) +a.SET.1:a +b.DEL.2: +c.MERGE.3:c +d.SINGLEDEL.4: +e.SINGLEDEL.5: +f.SET.6:f +g.DEL.7: +h.SINGLEDEL.8: +rangekey: j-k:{(#9,RANGEKEYDEL)} +rangekey: k-l:{(#10,RANGEKEYUNSET,@t5)} +rangekey: l-m:{(#11,RANGEKEYSET,@t10,foo)} +---- +point: [a#1,1-h#8,7] +rangekey: [j#9,19-m#72057594037927935,21] +seqnums: [1-11] +props "deletions": + rocksdb.num.range-deletions: 0 +props "deleted": + rocksdb.deleted.keys: 5 + + +build +a.SET.1:a +b.DEL.2: +c.MERGE.3:c +d.RANGEDEL.4:e +f.SET.5:f +g.DEL.6: +h.MERGE.7:h +i.RANGEDEL.8:j +---- +point: [a#1,1-h#7,2] +rangedel: [d#4,15-j#72057594037927935,15] +seqnums: [1-8] + +scan +---- +a#1,1:a +b#2,0: +c#3,2:c +f#5,1:f +g#6,0: +h#7,2:h + +scan-range-del +---- +d-e:{(#4,RANGEDEL)} +i-j:{(#8,RANGEDEL)} + +# 3: a-----------m +# 2: f------------s +# 1: j---------------z + +build +a.RANGEDEL.3:m +f.RANGEDEL.2:s +j.RANGEDEL.1:z +---- +rangedel: [a#3,15-z#72057594037927935,15] +seqnums: [1-3] + +scan +---- + +scan-range-del +---- +a-f:{(#3,RANGEDEL)} +f-j:{(#3,RANGEDEL) (#2,RANGEDEL)} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +m-s:{(#2,RANGEDEL) (#1,RANGEDEL)} +s-z:{(#1,RANGEDEL)} + +scan-range-key +---- + +# The range tombstone upper bound is exclusive, so a point operation +# on that same key will be the actual boundary. + +build +a.RANGEDEL.3:b +b.SET.4:c +---- +point: [b#4,1-b#4,1] +rangedel: [a#3,15-b#72057594037927935,15] +seqnums: [3-4] + +build +a.RANGEDEL.3:b +b.SET.2:c +---- +point: [b#2,1-b#2,1] +rangedel: [a#3,15-b#72057594037927935,15] +seqnums: [2-3] + +build +a.RANGEDEL.3:c +b.SET.2:c +---- +point: [b#2,1-b#2,1] +rangedel: [a#3,15-c#72057594037927935,15] +seqnums: [2-3] + +# Keys must be added in order. + +build +a.SET.1:b +a.SET.2:c +---- +pebble: keys must be added in strictly increasing order: a#1,SET, a#2,SET + +build +b.SET.1:a +a.SET.2:b +---- +pebble: keys must be added in strictly increasing order: b#1,SET, a#2,SET + +build +b.RANGEDEL.1:c +a.RANGEDEL.2:b +---- +pebble: keys must be added in order: b > a + +build-raw +.RANGEDEL.1:b +---- +rangedel: [#1,15-b#72057594037927935,15] +seqnums: [1-1] + +build-raw +a.RANGEDEL.1:c +a.RANGEDEL.2:c +---- +pebble: keys must be added in strictly increasing order: a#1,RANGEDEL, a#2,RANGEDEL + +build-raw +a.RANGEDEL.1:c +b.RANGEDEL.2:d +---- +pebble: overlapping tombstones must be fragmented: a-c:{(#1,RANGEDEL)} vs b-d:{(#2,RANGEDEL)} + +build-raw +a.RANGEDEL.2:c +a.RANGEDEL.1:d +---- +pebble: overlapping tombstones must be fragmented: a-c:{(#2,RANGEDEL)} vs a-d:{(#1,RANGEDEL)} + +build-raw +a.RANGEDEL.1:c +c.RANGEDEL.2:d +---- +rangedel: [a#1,15-d#72057594037927935,15] +seqnums: [1-2] + +build-raw +rangekey: a-b:{(#1,RANGEKEYSET,@t10,foo)} +rangekey: a-b:{(#2,RANGEKEYSET,@t10,foo)} +---- +rangekey: [a#2,21-b#72057594037927935,21] +seqnums: [1-2] + +build-raw +rangekey: b-c:{(#2,RANGEKEYSET,@t10,foo)} +rangekey: a-b:{(#1,RANGEKEYSET,@t10,foo)} +---- +pebble: spans must be added in order: b > a + +build-raw +a.RANGEKEYDEL.1:c +b.RANGEKEYDEL.2:d +---- +pebble: overlapping range keys must be fragmented: a#1,RANGEKEYDEL, b#2,RANGEKEYDEL + +build-raw +a.RANGEKEYDEL.2:c +a.RANGEKEYDEL.1:d +---- +pebble: overlapping range keys must be fragmented: a#2,RANGEKEYDEL, a#1,RANGEKEYDEL + +build-raw +rangekey: a-c:{(#1,RANGEKEYSET,@t10,foo)} +rangekey: c-d:{(#2,RANGEKEYSET,@t10,foo)} +---- +rangekey: [a#1,21-d#72057594037927935,21] +seqnums: [1-2] + +# Range keys may have perfectly aligned spans (including sequence numbers), +# though the key kinds must be ordered (descending). + +build-raw +a.RANGEKEYDEL.1:b +a.RANGEKEYDEL.1:b +---- +pebble: range keys starts must be added in increasing order: a#1,RANGEKEYDEL, a#1,RANGEKEYDEL + +build-raw +rangekey: a-b:{(#1,RANGEKEYSET,@t10,foo) (#1,RANGEKEYUNSET,@t10) (#1,RANGEKEYDEL)} +---- +rangekey: [a#1,21-b#72057594037927935,19] +seqnums: [1-1] + +# The range-del-v1 format supports unfragmented and unsorted range +# tombstones. + +build-raw range-del-v1 +a.RANGEDEL.1:c +a.RANGEDEL.2:c +---- +rangedel: [a#2,15-c#72057594037927935,15] +seqnums: [1-2] + +scan-range-del +---- +a-c:{(#2,RANGEDEL) (#1,RANGEDEL)} + +build-raw range-del-v1 +a.RANGEDEL.1:c +b.RANGEDEL.2:d +---- +rangedel: [a#1,15-d#72057594037927935,15] +seqnums: [1-2] + +scan-range-del +---- +a-b:{(#1,RANGEDEL)} +b-c:{(#2,RANGEDEL) (#1,RANGEDEL)} +c-d:{(#2,RANGEDEL)} + +build-raw range-del-v1 +a.RANGEDEL.2:c +a.RANGEDEL.1:d +---- +rangedel: [a#2,15-d#72057594037927935,15] +seqnums: [1-2] + +scan-range-del +---- +a-c:{(#2,RANGEDEL) (#1,RANGEDEL)} +c-d:{(#1,RANGEDEL)} + +# This matches an early test case, except we're passing overlapping +# range tombstones to the sstable writer and requiring them to be +# fragmented at read time. + +build-raw range-del-v1 +j.RANGEDEL.1:z +f.RANGEDEL.2:s +a.RANGEDEL.3:m +---- +rangedel: [a#3,15-z#72057594037927935,15] +seqnums: [1-3] + +scan-range-del +---- +a-f:{(#3,RANGEDEL)} +f-j:{(#3,RANGEDEL) (#2,RANGEDEL)} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +m-s:{(#2,RANGEDEL) (#1,RANGEDEL)} +s-z:{(#1,RANGEDEL)} + +# Setting a very small index-block-size results in a two-level index. + +build block-size=1 index-block-size=1 +a.SET.1:a +b.SET.1:b +c.SET.1:c +---- +point: [a#1,1-c#1,1] +seqnums: [1-1] + +layout +---- + 0 data (21) + 26 data (21) + 52 data (21) + 78 index (22) + 105 index (22) + 132 index (22) + 159 top-index (50) + 214 properties (580) + 799 meta-index (33) + 837 footer (53) + 890 EOF + +scan +---- +a#1,1:a +b#1,1:b +c#1,1:c + +# Enabling leveldb format disables the creation of a two-level index +# (the input data here mirrors the test case above). + +build leveldb block-size=1 index-block-size=1 +a.SET.1:a +b.SET.1:b +c.SET.1:c +---- +point: [a#1,1-c#1,1] +seqnums: [1-1] + +layout +---- + 0 data (21) + 26 data (21) + 52 data (21) + 78 index (47) + 130 properties (678) + 813 meta-index (33) + 851 leveldb-footer (48) + 899 EOF + +# Range keys, if present, are shown in the layout. + +build +rangekey: a-b:{(#3,RANGEKEYSET,@t3,foo)} +rangekey: b-c:{(#2,RANGEKEYSET,@t2,bar)} +rangekey: c-d:{(#1,RANGEKEYSET,@t1,baz)} +---- +rangekey: [a#3,21-d#72057594037927935,21] +seqnums: [1-3] + +layout +---- + 0 data (8) + 13 index (21) + 39 range-key (82) + 126 properties (628) + 759 meta-index (57) + 821 footer (53) + 874 EOF diff --git a/pebble/sstable/testdata/writer_range_keys b/pebble/sstable/testdata/writer_range_keys new file mode 100644 index 0000000..7b379b6 --- /dev/null +++ b/pebble/sstable/testdata/writer_range_keys @@ -0,0 +1,135 @@ +# NOTE: The operations SET, UNSET, and DEL in this test file are aliases for +# RANGEKEYSET, RANGEKEYUNSET and RANGEKEYDEL, respectively. + +# Keys must be added in order of start key. + +build +SET b-d @3=foo +SET a-c @5=bar +---- +pebble: spans must be added in order: b > a + +# All disjoint RANGEKEYSETs. +# +# ^ +# | •―――○ [e,f) SET @3=baz +# | •―――○ [c,d) SET @2=bar +# |•―――――――○ [a,c) SET @1=foo +# |___________________________________ +# a b c d e f g h i + +build +SET a-c @1=foo +SET c-d @2=bar +SET e-f @3=baz +---- +a-c:{(#0,RANGEKEYSET,@1,foo)} +c-d:{(#0,RANGEKEYSET,@2,bar)} +e-f:{(#0,RANGEKEYSET,@3,baz)} + +# Merge aligned RANGEKEYSETs. +# +# ^ +# |•―――――――○ [a,c) SET @3=baz +# |•―――――――○ [a,c) SET @1=bar +# |•―――――――○ [a,c) SET @2=foo +# |___________________________________ +# a b c d e f g h i +# +# Note that suffixes are sorted in descending order of the timestamp value in +# the suffix, rather than in lexical order. + +build +SET a-c @2=foo +SET a-c @1=bar +SET a-c @3=baz +---- +a-c:{(#0,RANGEKEYSET,@3,baz) (#0,RANGEKEYSET,@2,foo) (#0,RANGEKEYSET,@1,bar)} + +# Aligned spans, mixed range key kinds. +# +# ^ +# | •―――――――――――○ [f,i) DEL +# | •―――――――――――○ [f,i) SET @9=v9 +# | •―――――――――――○ [f,i) SET @8=v8 +# | •―――――――――――○ [f,i) SET @7=v7 +# | •―――――――――――○ [f,i) UNSET @6 +# | •―――――――――――○ [f,i) SET @5=v5 +# | •―――――――――――○ [f,i) SET @4=v4 +# | •―――○ [d,e) SET @9=v9 +# | •―――○ [d,e) DEL +# | •―――○ [c,d) SET @5=v5 +# | •―――○ [c,d) SET @2=v2 +# |•―――――――○ [a,c) UNSET @5 +# |•―――――――○ [a,c) SET @1=v1 +# |___________________________________ +# a b c d e f g h i + +build +SET a-c @1=v1 +UNSET a-c @5 +SET c-d @2=v2 +SET c-d @5=v5 +DEL d-e +SET d-e @9=v9 +SET f-i @4=v4 +SET f-i @5=v5 +UNSET f-i @6 +SET f-i @7=v7 +SET f-i @8=v8 +SET f-i @9=v9 +DEL f-i +---- +a-c:{(#0,RANGEKEYSET,@1,v1) (#0,RANGEKEYUNSET,@5)} +c-d:{(#0,RANGEKEYSET,@5,v5) (#0,RANGEKEYSET,@2,v2)} +d-e:{(#0,RANGEKEYSET,@9,v9) (#0,RANGEKEYDEL)} +f-i:{(#0,RANGEKEYSET,@9,v9) (#0,RANGEKEYSET,@8,v8) (#0,RANGEKEYSET,@7,v7) (#0,RANGEKEYSET,@5,v5) (#0,RANGEKEYSET,@4,v4) (#0,RANGEKEYUNSET,@6) (#0,RANGEKEYDEL)} + +# Merge overlapping RANGEKEYSETs. +# +# ^ +# | •―――○ [c,d) SET @3=baz +# | •―――――――――――――――○ [b,f) SET @2=bar +# |•―――――――○ [a,c) SET @1=foo +# |___________________________________ +# a b c d e f + +build +SET a-c @1=foo +SET b-f @2=bar +SET c-d @3=baz +---- +a-b:{(#0,RANGEKEYSET,@1,foo)} +b-c:{(#0,RANGEKEYSET,@2,bar) (#0,RANGEKEYSET,@1,foo)} +c-d:{(#0,RANGEKEYSET,@3,baz) (#0,RANGEKEYSET,@2,bar)} +d-f:{(#0,RANGEKEYSET,@2,bar)} + +# Overlapping spans, mixed range keys kinds. +# +# ^ +# | •―――――○ [l,o) DEL +# | •―――――――――――――○ [e,l) SET @4=baz +# | •―○ [p,q) DEL +# | •―――――――――○ [c,h) UNSET @3 +# | •―○ [b,c) SET @2=bar +# |•―――――――――――――――○ [a,i) SET @1=foo +# |___________________________________ +# a b c d e f g h i j k l m n o p q + +build +SET a-i @1=foo +SET b-c @2=bar +UNSET c-h @3 +DEL c-d +SET e-q @4=baz +DEL l-o +---- +a-b:{(#0,RANGEKEYSET,@1,foo)} +b-c:{(#0,RANGEKEYSET,@2,bar) (#0,RANGEKEYSET,@1,foo)} +c-d:{(#0,RANGEKEYSET,@1,foo) (#0,RANGEKEYUNSET,@3) (#0,RANGEKEYDEL)} +d-e:{(#0,RANGEKEYSET,@1,foo) (#0,RANGEKEYUNSET,@3)} +e-h:{(#0,RANGEKEYSET,@4,baz) (#0,RANGEKEYSET,@1,foo) (#0,RANGEKEYUNSET,@3)} +h-i:{(#0,RANGEKEYSET,@4,baz) (#0,RANGEKEYSET,@1,foo)} +i-l:{(#0,RANGEKEYSET,@4,baz)} +l-o:{(#0,RANGEKEYSET,@4,baz) (#0,RANGEKEYDEL)} +o-q:{(#0,RANGEKEYSET,@4,baz)} diff --git a/pebble/sstable/testdata/writer_v3 b/pebble/sstable/testdata/writer_v3 new file mode 100644 index 0000000..a003c5f --- /dev/null +++ b/pebble/sstable/testdata/writer_v3 @@ -0,0 +1,343 @@ +build +a.SET.1:a +---- +point: [a#1,1-a#1,1] +seqnums: [1-1] + +scan +---- +a#1,1:a + +scan-range-del +---- + +scan-range-key +---- + +build +a.SET.1:a +b.DEL.2: +c.MERGE.3:c +d.RANGEDEL.4:e +f.SET.5:f +g.DEL.6: +h.MERGE.7:h +i.RANGEDEL.8:j +rangekey: j-k:{(#9,RANGEKEYDEL)} +rangekey: k-l:{(#10,RANGEKEYUNSET,@t5)} +rangekey: l-m:{(#11,RANGEKEYSET,@t10,foo)} +---- +point: [a#1,1-h#7,2] +rangedel: [d#4,15-j#72057594037927935,15] +rangekey: [j#9,19-m#72057594037927935,21] +seqnums: [1-11] + +build +a.SET.1:a +b.DEL.2: +c.MERGE.3:c +d.RANGEDEL.4:e +f.SET.5:f +g.DEL.6: +h.MERGE.7:h +i.RANGEDEL.8:j +---- +point: [a#1,1-h#7,2] +rangedel: [d#4,15-j#72057594037927935,15] +seqnums: [1-8] + +scan +---- +a#1,1:a +b#2,0: +c#3,2:c +f#5,1:f +g#6,0: +h#7,2:h + +scan-range-del +---- +d-e:{(#4,RANGEDEL)} +i-j:{(#8,RANGEDEL)} + +# 3: a-----------m +# 2: f------------s +# 1: j---------------z + +build +a.RANGEDEL.3:m +f.RANGEDEL.2:s +j.RANGEDEL.1:z +---- +rangedel: [a#3,15-z#72057594037927935,15] +seqnums: [1-3] + +scan +---- + +scan-range-del +---- +a-f:{(#3,RANGEDEL)} +f-j:{(#3,RANGEDEL) (#2,RANGEDEL)} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +m-s:{(#2,RANGEDEL) (#1,RANGEDEL)} +s-z:{(#1,RANGEDEL)} + +scan-range-key +---- + +# The range tombstone upper bound is exclusive, so a point operation +# on that same key will be the actual boundary. + +build +a.RANGEDEL.3:b +b.SET.4:c +---- +point: [b#4,1-b#4,1] +rangedel: [a#3,15-b#72057594037927935,15] +seqnums: [3-4] + +build +a.RANGEDEL.3:b +b.SET.2:c +---- +point: [b#2,1-b#2,1] +rangedel: [a#3,15-b#72057594037927935,15] +seqnums: [2-3] + +build +a.RANGEDEL.3:c +b.SET.2:c +---- +point: [b#2,1-b#2,1] +rangedel: [a#3,15-c#72057594037927935,15] +seqnums: [2-3] + +# Keys must be added in order. + +build +a.SET.1:b +a.SET.2:c +---- +pebble: keys must be added in strictly increasing order: a#1,SET, a#2,SET + +build +b.SET.1:a +a.SET.2:b +---- +pebble: keys must be added in strictly increasing order: b#1,SET, a#2,SET + +build +b.RANGEDEL.1:c +a.RANGEDEL.2:b +---- +pebble: keys must be added in order: b > a + +build-raw +.RANGEDEL.1:b +---- +rangedel: [#1,15-b#72057594037927935,15] +seqnums: [1-1] + +build-raw +a.RANGEDEL.1:c +a.RANGEDEL.2:c +---- +pebble: keys must be added in strictly increasing order: a#1,RANGEDEL, a#2,RANGEDEL + +build-raw +a.RANGEDEL.1:c +b.RANGEDEL.2:d +---- +pebble: overlapping tombstones must be fragmented: a-c:{(#1,RANGEDEL)} vs b-d:{(#2,RANGEDEL)} + +build-raw +a.RANGEDEL.2:c +a.RANGEDEL.1:d +---- +pebble: overlapping tombstones must be fragmented: a-c:{(#2,RANGEDEL)} vs a-d:{(#1,RANGEDEL)} + +build-raw +a.RANGEDEL.1:c +c.RANGEDEL.2:d +---- +rangedel: [a#1,15-d#72057594037927935,15] +seqnums: [1-2] + +build-raw +rangekey: a-b:{(#1,RANGEKEYSET,@t10,foo)} +rangekey: a-b:{(#2,RANGEKEYSET,@t10,foo)} +---- +rangekey: [a#2,21-b#72057594037927935,21] +seqnums: [1-2] + +build-raw +rangekey: b-c:{(#2,RANGEKEYSET,@t10,foo)} +rangekey: a-b:{(#1,RANGEKEYSET,@t10,foo)} +---- +pebble: spans must be added in order: b > a + +build-raw +a.RANGEKEYDEL.1:c +b.RANGEKEYDEL.2:d +---- +pebble: overlapping range keys must be fragmented: a#1,RANGEKEYDEL, b#2,RANGEKEYDEL + +build-raw +a.RANGEKEYDEL.2:c +a.RANGEKEYDEL.1:d +---- +pebble: overlapping range keys must be fragmented: a#2,RANGEKEYDEL, a#1,RANGEKEYDEL + +build-raw +rangekey: a-c:{(#1,RANGEKEYSET,@t10,foo)} +rangekey: c-d:{(#2,RANGEKEYSET,@t10,foo)} +---- +rangekey: [a#1,21-d#72057594037927935,21] +seqnums: [1-2] + +# Range keys may have perfectly aligned spans (including sequence numbers), +# though the key kinds must be ordered (descending). + +build-raw +a.RANGEKEYDEL.1:b +a.RANGEKEYDEL.1:b +---- +pebble: range keys starts must be added in increasing order: a#1,RANGEKEYDEL, a#1,RANGEKEYDEL + +build-raw +rangekey: a-b:{(#1,RANGEKEYSET,@t10,foo) (#1,RANGEKEYUNSET,@t10) (#1,RANGEKEYDEL)} +---- +rangekey: [a#1,21-b#72057594037927935,19] +seqnums: [1-1] + +# The range-del-v1 format supports unfragmented and unsorted range +# tombstones. + +build-raw range-del-v1 +a.RANGEDEL.1:c +a.RANGEDEL.2:c +---- +rangedel: [a#2,15-c#72057594037927935,15] +seqnums: [1-2] + +scan-range-del +---- +a-c:{(#2,RANGEDEL) (#1,RANGEDEL)} + +build-raw range-del-v1 +a.RANGEDEL.1:c +b.RANGEDEL.2:d +---- +rangedel: [a#1,15-d#72057594037927935,15] +seqnums: [1-2] + +scan-range-del +---- +a-b:{(#1,RANGEDEL)} +b-c:{(#2,RANGEDEL) (#1,RANGEDEL)} +c-d:{(#2,RANGEDEL)} + +build-raw range-del-v1 +a.RANGEDEL.2:c +a.RANGEDEL.1:d +---- +rangedel: [a#2,15-d#72057594037927935,15] +seqnums: [1-2] + +scan-range-del +---- +a-c:{(#2,RANGEDEL) (#1,RANGEDEL)} +c-d:{(#1,RANGEDEL)} + +# This matches an early test case, except we're passing overlapping +# range tombstones to the sstable writer and requiring them to be +# fragmented at read time. + +build-raw range-del-v1 +j.RANGEDEL.1:z +f.RANGEDEL.2:s +a.RANGEDEL.3:m +---- +rangedel: [a#3,15-z#72057594037927935,15] +seqnums: [1-3] + +scan-range-del +---- +a-f:{(#3,RANGEDEL)} +f-j:{(#3,RANGEDEL) (#2,RANGEDEL)} +j-m:{(#3,RANGEDEL) (#2,RANGEDEL) (#1,RANGEDEL)} +m-s:{(#2,RANGEDEL) (#1,RANGEDEL)} +s-z:{(#1,RANGEDEL)} + +# Setting a very small index-block-size results in a two-level index. + +build block-size=1 index-block-size=1 +a.SET.1:a +b.SET.1:b +c.SET.1:c +---- +point: [a#1,1-c#1,1] +seqnums: [1-1] + +layout +---- + 0 data (22) + 27 data (22) + 54 data (22) + 81 index (22) + 108 index (22) + 135 index (22) + 162 top-index (51) + 218 properties (580) + 803 meta-index (33) + 841 footer (53) + 894 EOF + +scan +---- +a#1,1:a +b#1,1:b +c#1,1:c + +# Enabling leveldb format disables the creation of a two-level index +# (the input data here mirrors the test case above). + +build leveldb block-size=1 index-block-size=1 +a.SET.1:a +b.SET.1:b +c.SET.1:c +---- +point: [a#1,1-c#1,1] +seqnums: [1-1] + +layout +---- + 0 data (21) + 26 data (21) + 52 data (21) + 78 index (47) + 130 properties (678) + 813 meta-index (33) + 851 leveldb-footer (48) + 899 EOF + +# Range keys, if present, are shown in the layout. + +build +rangekey: a-b:{(#3,RANGEKEYSET,@t3,foo)} +rangekey: b-c:{(#2,RANGEKEYSET,@t2,bar)} +rangekey: c-d:{(#1,RANGEKEYSET,@t1,baz)} +---- +rangekey: [a#3,21-d#72057594037927935,21] +seqnums: [1-3] + +layout +---- + 0 data (8) + 13 index (21) + 39 range-key (82) + 126 properties (628) + 759 meta-index (57) + 821 footer (53) + 874 EOF diff --git a/pebble/sstable/testdata/writer_value_blocks b/pebble/sstable/testdata/writer_value_blocks new file mode 100644 index 0000000..e98a4b5 --- /dev/null +++ b/pebble/sstable/testdata/writer_value_blocks @@ -0,0 +1,330 @@ +# Size of value index is 3 bytes plus 5 + 5 = 10 bytes of trailer of the value +# block and value index block. So size 18 - 13 = 5 size of the value in the +# value block. +build +a@2.SET.1:a2 +b@5.SET.7:b5 +b@4.DEL.3: +b@3.SET.2:bat3 +b@2.SET.1:vbat2 +---- +value-blocks: num-values 1, num-blocks: 1, size: 18 + +scan-raw +---- +a@2#1,1:in-place a2, same-pre false +b@5#7,1:in-place b5, same-pre false +b@4#3,0: +b@3#2,1:in-place bat3, same-pre false +b@2#1,1:value-handle len 5 block 0 offset 0, att 5, same-pre true + +scan +---- +a@2#1,1:a2 +b@5#7,1:b5 +b@4#3,0: +b@3#2,1:bat3 +b@2#1,1:vbat2 + +scan-cloned-lazy-values +---- +0(in-place: len 2): a2 +1(in-place: len 2): b5 +2(in-place: len 0): +3(in-place: len 4): bat3 +4(lazy: len 5, attr: 5): vbat2 + +# Size of value index is 3 bytes plus 5 + 5 = 10 bytes of trailer of the value +# block and value index block. So size 33 - 13 = 20 is the total size of the +# values in the value block. +build +blue@10.SET.20:blue10 +blue@8.SET.18:blue8 +blue@8.SET.16:blue8s +blue@6.DEL.14: +blue@4.SET.12:blue4 +blue@3.SET.10:blue3 +red@9.SET.18:red9 +red@7.SET.8:red7 +---- +value-blocks: num-values 4, num-blocks: 1, size: 33 + +scan-raw +---- +blue@10#20,1:in-place blue10, same-pre false +blue@8#18,1:value-handle len 5 block 0 offset 0, att 5, same-pre true +blue@8#16,1:value-handle len 6 block 0 offset 5, att 6, same-pre true +blue@6#14,0: +blue@4#12,1:in-place blue4, same-pre false +blue@3#10,1:value-handle len 5 block 0 offset 11, att 5, same-pre true +red@9#18,1:in-place red9, same-pre false +red@7#8,1:value-handle len 4 block 0 offset 16, att 4, same-pre true + +scan +---- +blue@10#20,1:blue10 +blue@8#18,1:blue8 +blue@8#16,1:blue8s +blue@6#14,0: +blue@4#12,1:blue4 +blue@3#10,1:blue3 +red@9#18,1:red9 +red@7#8,1:red7 + +scan-cloned-lazy-values +---- +0(in-place: len 6): blue10 +1(lazy: len 5, attr: 5): blue8 +2(lazy: len 6, attr: 6): blue8s +3(in-place: len 0): +4(in-place: len 5): blue4 +5(lazy: len 5, attr: 5): blue3 +6(in-place: len 4): red9 +7(lazy: len 4, attr: 4): red7 + +# Multiple value blocks. Trailers of 5+5+5 for the two value blocks and the +# value index block, totals to 15. The values are 5+6+15=26. The value index +# block has to encode two tuples, each of 4 bytes (blockNumByteLength=1, +# blockOffsetByteLength=2, blockLengthByteLength=1), so 2*4=8. The total is +# 15+26+8=49 bytes, which corresponds to "size: 49" below. +build block-size=8 +blue@10.SET.20:blue10 +blue@8.SET.18:blue8 +blue@8.SET.16:blue8s +blue@6.SET.16:blue6isverylong +---- +value-blocks: num-values 3, num-blocks: 2, size: 49 + +scan-raw +---- +blue@10#20,1:in-place blue10, same-pre false +blue@8#18,1:value-handle len 5 block 0 offset 0, att 5, same-pre true +blue@8#16,1:value-handle len 6 block 0 offset 5, att 6, same-pre true +blue@6#16,1:value-handle len 15 block 1 offset 0, att 7, same-pre true + +scan +---- +blue@10#20,1:blue10 +blue@8#18,1:blue8 +blue@8#16,1:blue8s +blue@6#16,1:blue6isverylong + +scan-cloned-lazy-values +---- +0(in-place: len 6): blue10 +1(lazy: len 5, attr: 5): blue8 +2(lazy: len 6, attr: 6): blue8s +3(lazy: len 15, attr: 7): blue6isverylong + +layout +---- + 0 data (33) + 0 record (25 = 3 [0] + 15 + 7) [restart] + blue@10#20,1:blue10 + 25 [restart 0] + 33 [trailer compression=none checksum=0x5fb0d551] + 38 data (29) + 38 record (21 = 3 [0] + 14 + 4) [restart] + blue@8#18,1:value handle {valueLen:5 blockNum:0 offsetInBlock:0} + 59 [restart 38] + 67 [trailer compression=none checksum=0x628e4a10] + 72 data (29) + 72 record (21 = 3 [0] + 14 + 4) [restart] + blue@8#16,1:value handle {valueLen:6 blockNum:0 offsetInBlock:5} + 93 [restart 72] + 101 [trailer compression=none checksum=0x4e65b9b6] + 106 data (29) + 106 record (21 = 3 [0] + 14 + 4) [restart] + blue@6#16,1:value handle {valueLen:15 blockNum:1 offsetInBlock:0} + 127 [restart 106] + 135 [trailer compression=none checksum=0x9f60e629] + 140 index (28) + 140 block:0/33 [restart] + 160 [restart 140] + 168 [trailer compression=none checksum=0x32b37f08] + 173 index (27) + 173 block:38/29 [restart] + 192 [restart 173] + 200 [trailer compression=none checksum=0x21d27815] + 205 index (30) + 205 block:72/29 [restart] + 227 [restart 205] + 235 [trailer compression=none checksum=0xba0b26fe] + 240 index (22) + 240 block:106/29 [restart] + 254 [restart 240] + 262 [trailer compression=none checksum=0x802be702] + 267 top-index (85) + 267 block:140/28 [restart] + 288 block:173/27 [restart] + 308 block:205/30 [restart] + 331 block:240/22 [restart] + 346 [restart 267] + 350 [restart 288] + 354 [restart 308] + 358 [restart 331] + 352 [trailer compression=snappy checksum=0x8bd0d63a] + 357 value-block (11) + 373 value-block (15) + 393 value-index (8) + 406 properties (676) + 406 obsolete-key (16) [restart] + 422 pebble.num.value-blocks (27) + 449 pebble.num.values.in.value-blocks (21) + 470 pebble.value-blocks.size (21) + 491 rocksdb.block.based.table.index.type (43) + 534 rocksdb.block.based.table.prefix.filtering (20) + 554 rocksdb.block.based.table.whole.key.filtering (23) + 577 rocksdb.comparator (37) + 614 rocksdb.compression (16) + 630 rocksdb.compression_options (106) + 736 rocksdb.data.size (14) + 750 rocksdb.deleted.keys (15) + 765 rocksdb.external_sst_file.global_seqno (41) + 806 rocksdb.external_sst_file.version (14) + 820 rocksdb.filter.size (15) + 835 rocksdb.index.partitions (20) + 855 rocksdb.index.size (9) + 864 rocksdb.merge.operands (18) + 882 rocksdb.merge.operator (24) + 906 rocksdb.num.data.blocks (19) + 925 rocksdb.num.entries (11) + 936 rocksdb.num.range-deletions (19) + 955 rocksdb.prefix.extractor.name (31) + 986 rocksdb.property.collectors (34) + 1020 rocksdb.raw.key.size (16) + 1036 rocksdb.raw.value.size (14) + 1050 rocksdb.top-level.index.size (24) + 1074 [restart 406] + 1082 [trailer compression=none checksum=0xbf6fe705] + 1087 meta-index (64) + 1087 pebble.value_index block:393/8 value-blocks-index-lengths: 1(num), 2(offset), 1(length) [restart] + 1114 rocksdb.properties block:406/676 [restart] + 1139 [restart 1087] + 1143 [restart 1114] + 1151 [trailer compression=none checksum=0x5a8a2a98] + 1156 footer (53) + 1156 checksum type: crc32c + 1157 meta: offset=1087, length=64 + 1160 index: offset=267, length=85 + 1163 [padding] + 1197 version: 4 + 1201 magic number: 0xf09faab3f09faab3 + 1209 EOF + +# Require that [c,e) must be in-place. +build in-place-bound=(c,e) +blue@10.SET.20:blue10 +blue@8.SET.18:blue8 +c@10.SET.16:c10 +c@8.SET.14:c8 +e@20.SET.25:eat20 +e@18.SET.23:eat18 +---- +value-blocks: num-values 2, num-blocks: 1, size: 23 + +scan-raw +---- +blue@10#20,1:in-place blue10, same-pre false +blue@8#18,1:value-handle len 5 block 0 offset 0, att 5, same-pre true +c@10#16,1:in-place c10, same-pre false +c@8#14,1:in-place c8, same-pre false +e@20#25,1:in-place eat20, same-pre false +e@18#23,1:value-handle len 5 block 0 offset 5, att 5, same-pre true + +scan +---- +blue@10#20,1:blue10 +blue@8#18,1:blue8 +c@10#16,1:c10 +c@8#14,1:c8 +e@20#25,1:eat20 +e@18#23,1:eat18 + +scan-cloned-lazy-values +---- +0(in-place: len 6): blue10 +1(lazy: len 5, attr: 5): blue8 +2(in-place: len 3): c10 +3(in-place: len 2): c8 +4(in-place: len 5): eat20 +5(lazy: len 5, attr: 5): eat18 + +# Try write empty values to value blocks. +build +b@5.SET.7:b5 +b@3.SET.2: +c@6.DEL.7: +c@5.DEL.6: +---- +value-blocks: num-values 0, num-blocks: 0, size: 0 + +scan-raw +---- +b@5#7,1:in-place b5, same-pre false +b@3#2,1:in-place , same-pre true +c@6#7,0: +c@5#6,0: + +scan +---- +b@5#7,1:b5 +b@3#2,1: +c@6#7,0: +c@5#6,0: + +layout +---- + 0 data (66) + 0 record (17 = 3 [0] + 11 + 3) [restart] + b@5#7,1:b5 + 17 record (14 = 3 [1] + 10 + 1) + b@3#2,1: + 31 record (14 = 3 [0] + 11 + 0) + c@6#7,0: + 45 record (13 = 3 [1] + 10 + 0) + c@5#6,0: + 58 [restart 0] + 66 [trailer compression=none checksum=0x4e91250f] + 71 index (22) + 71 block:0/66 [restart] + 85 [restart 71] + 93 [trailer compression=none checksum=0xf80f5bcf] + 98 properties (606) + 98 obsolete-key (16) [restart] + 114 pebble.raw.point-tombstone.key.size (39) + 153 rocksdb.block.based.table.index.type (43) + 196 rocksdb.block.based.table.prefix.filtering (20) + 216 rocksdb.block.based.table.whole.key.filtering (23) + 239 rocksdb.comparator (37) + 276 rocksdb.compression (16) + 292 rocksdb.compression_options (106) + 398 rocksdb.data.size (13) + 411 rocksdb.deleted.keys (15) + 426 rocksdb.external_sst_file.global_seqno (41) + 467 rocksdb.external_sst_file.version (14) + 481 rocksdb.filter.size (15) + 496 rocksdb.index.size (14) + 510 rocksdb.merge.operands (18) + 528 rocksdb.merge.operator (24) + 552 rocksdb.num.data.blocks (19) + 571 rocksdb.num.entries (11) + 582 rocksdb.num.range-deletions (19) + 601 rocksdb.prefix.extractor.name (31) + 632 rocksdb.property.collectors (34) + 666 rocksdb.raw.key.size (16) + 682 rocksdb.raw.value.size (14) + 696 [restart 98] + 704 [trailer compression=none checksum=0xb3084f65] + 709 meta-index (32) + 709 rocksdb.properties block:98/606 [restart] + 733 [restart 709] + 741 [trailer compression=none checksum=0x907a9f2c] + 746 footer (53) + 746 checksum type: crc32c + 747 meta: offset=709, length=32 + 750 index: offset=71, length=22 + 752 [padding] + 787 version: 4 + 791 magic number: 0xf09faab3f09faab3 + 799 EOF diff --git a/pebble/sstable/unsafe.go b/pebble/sstable/unsafe.go new file mode 100644 index 0000000..11ec068 --- /dev/null +++ b/pebble/sstable/unsafe.go @@ -0,0 +1,35 @@ +// Copyright 2018 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 ( + "unsafe" + + "github.com/cockroachdb/pebble/internal/manual" +) + +func getBytes(ptr unsafe.Pointer, length int) []byte { + return (*[manual.MaxArrayLen]byte)(ptr)[:length:length] +} + +func decodeVarint(ptr unsafe.Pointer) (uint32, unsafe.Pointer) { + if a := *((*uint8)(ptr)); a < 128 { + return uint32(a), + unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + return uint32(b)<<7 | uint32(a), + unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + return uint32(c)<<14 | uint32(b)<<7 | uint32(a), + unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + return uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a), + unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + return uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a), + unsafe.Pointer(uintptr(ptr) + 5) + } +} diff --git a/pebble/sstable/unsafe_test.go b/pebble/sstable/unsafe_test.go new file mode 100644 index 0000000..2a071b1 --- /dev/null +++ b/pebble/sstable/unsafe_test.go @@ -0,0 +1,75 @@ +// Copyright 2018 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 ( + "encoding/binary" + "fmt" + "io" + "testing" + "time" + "unsafe" + + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +func TestGetBytes(t *testing.T) { + const size = (1 << 31) - 1 + // No need to actually allocate a huge slice, which can cause OOM on small + // machines (like the GitHub CI runners). + block := make([]byte, 100) + data := getBytes(unsafe.Pointer(&block[0]), size) + require.EqualValues(t, size, len(data)) +} + +func TestDecodeVarint(t *testing.T) { + vals := []uint32{ + 0, + 1, + 1 << 7, + 1 << 8, + 1 << 14, + 1 << 15, + 1 << 20, + 1 << 21, + 1 << 28, + 1 << 29, + 1 << 31, + } + buf := make([]byte, 5) + for _, v := range vals { + binary.PutUvarint(buf, uint64(v)) + u, _ := decodeVarint(unsafe.Pointer(&buf[0])) + if v != u { + fmt.Printf("%d %d\n", v, u) + } + } +} + +func BenchmarkDecodeVarint(b *testing.B) { + rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) + vals := make([]unsafe.Pointer, 10000) + for i := range vals { + buf := make([]byte, 5) + binary.PutUvarint(buf, uint64(rng.Uint32())) + vals[i] = unsafe.Pointer(&buf[0]) + } + + b.ResetTimer() + var ptr unsafe.Pointer + for i, n := 0, 0; i < b.N; i += n { + n = len(vals) + if n > b.N-i { + n = b.N - i + } + for j := 0; j < n; j++ { + _, ptr = decodeVarint(vals[j]) + } + } + if testing.Verbose() { + fmt.Fprint(io.Discard, ptr) + } +} diff --git a/pebble/sstable/value_block.go b/pebble/sstable/value_block.go new file mode 100644 index 0000000..a93b4ba --- /dev/null +++ b/pebble/sstable/value_block.go @@ -0,0 +1,957 @@ +// 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 sstable + +import ( + "context" + "encoding/binary" + "io" + "sync" + "unsafe" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "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). 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 +// level, and what is the latest value from the perspective of the whole LSM +// is an older MVCC version. This only affects performance and not +// correctness. This local knowledge is also why we continue to store these +// older versions in the same sstable -- we need to be able to conveniently +// 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.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 +// byte prefix (valuePrefix). This single byte prefix is split into multiple +// parts, where nb represents information that is encoded in n bits. +// +// +---------------+--------------------+-----------+--------------------+ +// | value-kind 2b | SET-same-prefix 1b | unused 2b | short-attribute 3b | +// +---------------+--------------------+-----------+--------------------+ +// +// The 2 bit value-kind specifies whether this is an in-place value or a value +// handle pointing to a value block. We use 2 bits here for future +// representation of values that are in separate files. The 1 bit +// SET-same-prefix is true if this key is a SET and is immediately preceded by +// a SET that shares the same prefix. The 3 bit short-attribute is described +// in base.ShortAttribute -- it stores user-defined attributes about the +// value. It is unused for in-place values. +// +// Value Handle and Value Blocks: +// valueHandles refer to values in value blocks. Value blocks are simpler than +// normal data blocks (that contain key-value pairs, and allow for binary +// search), which makes them cheap for value retrieval purposes. A valueHandle +// is a tuple (valueLen, blockNum, offsetInBlock), where blockNum is the 0 +// indexed value block number and offsetInBlock is the byte offset in that +// block containing the value. The valueHandle.valueLen is included since +// there are multiple use cases in CockroachDB that need the value length but +// not the value, for which we can avoid reading the value in the value block +// (see +// https://github.com/cockroachdb/pebble/issues/1170#issuecomment-958203245). +// +// A value block has a checksum like other blocks, and is optionally +// compressed. An uncompressed value block is a sequence of values with no +// separator or length (we rely on the valueHandle to demarcate). The +// valueHandle.offsetInBlock points to the value, of length +// valueHandle.valueLen. While writing a sstable, all the (possibly +// compressed) value blocks need to be held in-memory until they can be +// written. Value blocks are placed after the "meta rangedel" and "meta range +// key" blocks since value blocks are considered less likely to be read. +// +// Meta Value Index Block: +// Since the (key, valueHandle) pair are written before there is any knowledge +// of the byte offset of the value block in the file, or its compressed +// length, we need another lookup to map the valueHandle.blockNum to the +// information needed to read it from the file. This information is provided +// by the "value index block". The "value index block" is referred to by the +// metaindex block. The design intentionally avoids making the "value index +// block" a general purpose key-value block, since each caller wants to lookup +// the information for a particular blockNum (there is no need for SeekGE +// etc.). Instead, this index block stores a sequence of (blockNum, +// blockOffset, blockLength) tuples, where the blockNums are consecutive +// integers, and the tuples are encoded with a fixed width encoding. This +// allows a reader to find the tuple for block K by looking at the offset +// K*fixed-width. The fixed width for each field is decided by looking at the +// maximum value of each of these fields. As a concrete example of a large +// sstable with many value blocks, we constructed a 100MB sstable with many +// versions and had 2475 value blocks (~32KB each). This sstable had this +// tuple encoded using 2+4+2=8 bytes, which means the uncompressed value index +// block was 2475*8=~19KB, which is modest. Therefore, we don't support more +// than one value index block. Consider the example of 2 byte blockNum, 4 byte +// blockOffset and 2 byte blockLen. The value index block will look like: +// +// +---------------+------------------+---------------+ +// | blockNum (2B) | blockOffset (4B) | blockLen (2B) | +// +---------------+------------------+---------------+ +// | 0 | 7,123,456 | 30,000 | +// +---------------+------------------+---------------+ +// | 1 | 7,153,456 | 20,000 | +// +---------------+------------------+---------------+ +// | 2 | 7,173,456 | 25,567 | +// +---------------+------------------+---------------+ +// | .... | ... | ... | +// +// +// The metaindex block contains the valueBlocksIndexHandle which in addition +// to the BlockHandle also specifies the widths of these tuple fields. In the +// above example, the +// valueBlockIndexHandle.{blockNumByteLength,blockOffsetByteLength,blockLengthByteLength} +// will be (2,4,2). + +// valueHandle is stored with a key when the value is in a value block. This +// handle is the pointer to that value. +type valueHandle struct { + valueLen uint32 + blockNum uint32 + offsetInBlock uint32 +} + +// valuePrefix is the single byte prefix for either the in-place value or the +// encoded valueHandle. It encoded multiple kinds of information. +type valuePrefix byte + +const ( + // 2 most-significant bits of valuePrefix encodes the value-kind. + valueKindMask valuePrefix = '\xC0' + valueKindIsValueHandle valuePrefix = '\x80' + valueKindIsInPlaceValue valuePrefix = '\x00' + + // 1 bit indicates SET has same key prefix as immediately preceding key that + // is also a SET. If the immediately preceding key in the same block is a + // SET, AND this bit is 0, the prefix must have changed. + // + // Note that the current policy of only storing older MVCC versions in value + // blocks means that valueKindIsValueHandle => SET has same prefix. But no + // code should rely on this behavior. Also, SET has same prefix does *not* + // imply valueKindIsValueHandle. + setHasSameKeyPrefixMask valuePrefix = '\x20' + + // 3 least-significant bits for the user-defined base.ShortAttribute. + // Undefined for valueKindIsInPlaceValue. + userDefinedShortAttributeMask valuePrefix = '\x07' +) + +// valueHandle fields are varint encoded, so maximum 5 bytes each, plus 1 byte +// for the valuePrefix. This could alternatively be group varint encoded, but +// experiments were inconclusive +// (https://github.com/cockroachdb/pebble/pull/1443#issuecomment-1270298802). +const valueHandleMaxLen = 5*3 + 1 + +// Assert blockHandleLikelyMaxLen >= valueHandleMaxLen. +const _ = uint(blockHandleLikelyMaxLen - valueHandleMaxLen) + +func encodeValueHandle(dst []byte, v valueHandle) int { + n := 0 + n += binary.PutUvarint(dst[n:], uint64(v.valueLen)) + n += binary.PutUvarint(dst[n:], uint64(v.blockNum)) + n += binary.PutUvarint(dst[n:], uint64(v.offsetInBlock)) + return n +} + +func makePrefixForValueHandle(setHasSameKeyPrefix bool, attribute base.ShortAttribute) valuePrefix { + prefix := valueKindIsValueHandle | valuePrefix(attribute) + if setHasSameKeyPrefix { + prefix = prefix | setHasSameKeyPrefixMask + } + return prefix +} + +func makePrefixForInPlaceValue(setHasSameKeyPrefix bool) valuePrefix { + prefix := valueKindIsInPlaceValue + if setHasSameKeyPrefix { + prefix = prefix | setHasSameKeyPrefixMask + } + return prefix +} + +func isValueHandle(b valuePrefix) bool { + return b&valueKindMask == valueKindIsValueHandle +} + +// REQUIRES: isValueHandle(b) +func getShortAttribute(b valuePrefix) base.ShortAttribute { + return base.ShortAttribute(b & userDefinedShortAttributeMask) +} + +func setHasSamePrefix(b valuePrefix) bool { + return b&setHasSameKeyPrefixMask == setHasSameKeyPrefixMask +} + +func decodeLenFromValueHandle(src []byte) (uint32, []byte) { + ptr := unsafe.Pointer(&src[0]) + var v uint32 + if a := *((*uint8)(ptr)); a < 128 { + v = uint32(a) + src = src[1:] + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + v = uint32(b)<<7 | uint32(a) + src = src[2:] + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + v = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + src = src[3:] + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + v = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + src = src[4:] + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + v = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + src = src[5:] + } + return v, src +} + +func decodeRemainingValueHandle(src []byte) valueHandle { + var vh valueHandle + ptr := unsafe.Pointer(&src[0]) + // Manually inlined uvarint decoding. Saves ~25% in benchmarks. Unrolling + // a loop for i:=0; i<2; i++, saves ~6%. + var v uint32 + if a := *((*uint8)(ptr)); a < 128 { + v = uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 1) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + v = uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + v = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 3) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + v = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 4) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + v = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + ptr = unsafe.Pointer(uintptr(ptr) + 5) + } + vh.blockNum = v + + if a := *((*uint8)(ptr)); a < 128 { + v = uint32(a) + } else if a, b := a&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 1))); b < 128 { + v = uint32(b)<<7 | uint32(a) + } else if b, c := b&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 2))); c < 128 { + v = uint32(c)<<14 | uint32(b)<<7 | uint32(a) + } else if c, d := c&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 3))); d < 128 { + v = uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + } else { + d, e := d&0x7f, *((*uint8)(unsafe.Pointer(uintptr(ptr) + 4))) + v = uint32(e)<<28 | uint32(d)<<21 | uint32(c)<<14 | uint32(b)<<7 | uint32(a) + } + vh.offsetInBlock = v + + return vh +} + +func decodeValueHandle(src []byte) valueHandle { + valLen, src := decodeLenFromValueHandle(src) + vh := decodeRemainingValueHandle(src) + vh.valueLen = valLen + return vh +} + +// valueBlocksIndexHandle is placed in the metaindex if there are any value +// blocks. If there are no value blocks, there is no value blocks index, and +// no entry in the metaindex. Note that the lack of entry in the metaindex +// should not be used to ascertain whether the values are prefixed, since the +// former is an emergent property of the data that was written and not known +// until all the key-value pairs in the sstable are written. +type valueBlocksIndexHandle struct { + h BlockHandle + blockNumByteLength uint8 + blockOffsetByteLength uint8 + blockLengthByteLength uint8 +} + +const valueBlocksIndexHandleMaxLen = blockHandleMaxLenWithoutProperties + 3 + +// Assert blockHandleLikelyMaxLen >= valueBlocksIndexHandleMaxLen. +const _ = uint(blockHandleLikelyMaxLen - valueBlocksIndexHandleMaxLen) + +func encodeValueBlocksIndexHandle(dst []byte, v valueBlocksIndexHandle) int { + n := encodeBlockHandle(dst, v.h) + dst[n] = v.blockNumByteLength + n++ + dst[n] = v.blockOffsetByteLength + n++ + dst[n] = v.blockLengthByteLength + n++ + return n +} + +func decodeValueBlocksIndexHandle(src []byte) (valueBlocksIndexHandle, int, error) { + var vbih valueBlocksIndexHandle + var n int + vbih.h, n = decodeBlockHandle(src) + if n <= 0 { + return vbih, 0, errors.Errorf("bad BlockHandle %x", src) + } + if len(src) != n+3 { + return vbih, 0, errors.Errorf("bad BlockHandle %x", src) + } + vbih.blockNumByteLength = src[n] + vbih.blockOffsetByteLength = src[n+1] + vbih.blockLengthByteLength = src[n+2] + return vbih, n + 3, nil +} + +type valueBlocksAndIndexStats struct { + numValueBlocks uint64 + numValuesInValueBlocks uint64 + // Includes both value blocks and value index block. + valueBlocksAndIndexSize uint64 +} + +// valueBlockWriter writes a sequence of value blocks, and the value blocks +// index, for a sstable. +type valueBlockWriter struct { + // The configured uncompressed block size and size threshold + blockSize, blockSizeThreshold int + // Configured compression. + compression Compression + // checksummer with configured checksum type. + checksummer checksummer + // Block finished callback. + blockFinishedFunc func(compressedSize int) + + // buf is the current block being written to (uncompressed). + buf *blockBuffer + // compressedBuf is used for compressing the block. + compressedBuf *blockBuffer + // Sequence of blocks that are finished. + blocks []blockAndHandle + // Cumulative value block bytes written so far. + totalBlockBytes uint64 + numValues uint64 +} + +type blockAndHandle struct { + block *blockBuffer + handle BlockHandle + compressed bool +} + +type blockBuffer struct { + b []byte +} + +// Pool of block buffers that should be roughly the blockSize. +var uncompressedValueBlockBufPool = sync.Pool{ + New: func() interface{} { + return &blockBuffer{} + }, +} + +// Pool of block buffers for compressed value blocks. These may widely vary in +// size based on compression ratios. +var compressedValueBlockBufPool = sync.Pool{ + New: func() interface{} { + return &blockBuffer{} + }, +} + +func releaseToValueBlockBufPool(pool *sync.Pool, b *blockBuffer) { + // Don't pool buffers larger than 128KB, in case we had some rare large + // values. + if len(b.b) > 128*1024 { + return + } + if invariants.Enabled { + // Set the bytes to a random value. Cap the number of bytes being + // randomized to prevent test timeouts. + length := cap(b.b) + if length > 1000 { + length = 1000 + } + b.b = b.b[:length:length] + rand.Read(b.b) + } + pool.Put(b) +} + +var valueBlockWriterPool = sync.Pool{ + New: func() interface{} { + return &valueBlockWriter{} + }, +} + +func newValueBlockWriter( + blockSize int, + blockSizeThreshold int, + compression Compression, + checksumType ChecksumType, + // compressedSize should exclude the block trailer. + blockFinishedFunc func(compressedSize int), +) *valueBlockWriter { + w := valueBlockWriterPool.Get().(*valueBlockWriter) + *w = valueBlockWriter{ + blockSize: blockSize, + blockSizeThreshold: blockSizeThreshold, + compression: compression, + checksummer: checksummer{ + checksumType: checksumType, + }, + blockFinishedFunc: blockFinishedFunc, + buf: uncompressedValueBlockBufPool.Get().(*blockBuffer), + compressedBuf: compressedValueBlockBufPool.Get().(*blockBuffer), + blocks: w.blocks[:0], + } + w.buf.b = w.buf.b[:0] + w.compressedBuf.b = w.compressedBuf.b[:0] + return w +} + +func releaseValueBlockWriter(w *valueBlockWriter) { + for i := range w.blocks { + if w.blocks[i].compressed { + releaseToValueBlockBufPool(&compressedValueBlockBufPool, w.blocks[i].block) + } else { + releaseToValueBlockBufPool(&uncompressedValueBlockBufPool, w.blocks[i].block) + } + w.blocks[i].block = nil + } + if w.buf != nil { + releaseToValueBlockBufPool(&uncompressedValueBlockBufPool, w.buf) + } + if w.compressedBuf != nil { + releaseToValueBlockBufPool(&compressedValueBlockBufPool, w.compressedBuf) + } + *w = valueBlockWriter{ + blocks: w.blocks[:0], + } + valueBlockWriterPool.Put(w) +} + +func (w *valueBlockWriter) addValue(v []byte) (valueHandle, error) { + if invariants.Enabled && len(v) == 0 { + return valueHandle{}, errors.Errorf("cannot write empty value to value block") + } + w.numValues++ + blockLen := len(w.buf.b) + valueLen := len(v) + if blockLen >= w.blockSize || + (blockLen > w.blockSizeThreshold && blockLen+valueLen > w.blockSize) { + // Block is not currently empty and adding this value will become too big, + // so finish this block. + w.compressAndFlush() + blockLen = len(w.buf.b) + if invariants.Enabled && blockLen != 0 { + panic("blockLen of new block should be 0") + } + } + vh := valueHandle{ + valueLen: uint32(valueLen), + blockNum: uint32(len(w.blocks)), + offsetInBlock: uint32(blockLen), + } + blockLen = int(vh.offsetInBlock + vh.valueLen) + if cap(w.buf.b) < blockLen { + size := 2 * cap(w.buf.b) + if size < 1024 { + size = 1024 + } + for size < blockLen { + size *= 2 + } + buf := make([]byte, blockLen, size) + _ = copy(buf, w.buf.b) + w.buf.b = buf + } else { + w.buf.b = w.buf.b[:blockLen] + } + buf := w.buf.b[vh.offsetInBlock:] + n := copy(buf, v) + if n != len(buf) { + panic("incorrect length computation") + } + return vh, nil +} + +func (w *valueBlockWriter) compressAndFlush() { + // Compress the buffer, discarding the result if the improvement isn't at + // least 12.5%. + blockType := noCompressionBlockType + b := w.buf + if w.compression != NoCompression { + blockType, w.compressedBuf.b = + compressBlock(w.compression, w.buf.b, w.compressedBuf.b[:cap(w.compressedBuf.b)]) + if len(w.compressedBuf.b) < len(w.buf.b)-len(w.buf.b)/8 { + b = w.compressedBuf + } else { + blockType = noCompressionBlockType + } + } + n := len(b.b) + if n+blockTrailerLen > cap(b.b) { + block := make([]byte, n+blockTrailerLen) + copy(block, b.b) + b.b = block + } else { + b.b = b.b[:n+blockTrailerLen] + } + b.b[n] = byte(blockType) + w.computeChecksum(b.b) + bh := BlockHandle{Offset: w.totalBlockBytes, Length: uint64(n)} + w.totalBlockBytes += uint64(len(b.b)) + // blockFinishedFunc length excludes the block trailer. + w.blockFinishedFunc(n) + compressed := blockType != noCompressionBlockType + w.blocks = append(w.blocks, blockAndHandle{ + block: b, + handle: bh, + compressed: compressed, + }) + // Handed off a buffer to w.blocks, so need get a new one. + if compressed { + w.compressedBuf = compressedValueBlockBufPool.Get().(*blockBuffer) + } else { + w.buf = uncompressedValueBlockBufPool.Get().(*blockBuffer) + } + w.buf.b = w.buf.b[:0] +} + +func (w *valueBlockWriter) computeChecksum(block []byte) { + n := len(block) - blockTrailerLen + checksum := w.checksummer.checksum(block[:n], block[n:n+1]) + binary.LittleEndian.PutUint32(block[n+1:], checksum) +} + +func (w *valueBlockWriter) finish( + writer io.Writer, fileOffset uint64, +) (valueBlocksIndexHandle, valueBlocksAndIndexStats, error) { + if len(w.buf.b) > 0 { + w.compressAndFlush() + } + n := len(w.blocks) + if n == 0 { + return valueBlocksIndexHandle{}, valueBlocksAndIndexStats{}, nil + } + largestOffset := uint64(0) + largestLength := uint64(0) + for i := range w.blocks { + _, err := writer.Write(w.blocks[i].block.b) + if err != nil { + return valueBlocksIndexHandle{}, valueBlocksAndIndexStats{}, err + } + w.blocks[i].handle.Offset += fileOffset + largestOffset = w.blocks[i].handle.Offset + if largestLength < w.blocks[i].handle.Length { + largestLength = w.blocks[i].handle.Length + } + } + vbihOffset := fileOffset + w.totalBlockBytes + + vbih := valueBlocksIndexHandle{ + h: BlockHandle{ + Offset: vbihOffset, + }, + blockNumByteLength: uint8(lenLittleEndian(uint64(n - 1))), + blockOffsetByteLength: uint8(lenLittleEndian(largestOffset)), + blockLengthByteLength: uint8(lenLittleEndian(largestLength)), + } + var err error + if vbih, err = w.writeValueBlocksIndex(writer, vbih); err != nil { + return valueBlocksIndexHandle{}, valueBlocksAndIndexStats{}, err + } + stats := valueBlocksAndIndexStats{ + numValueBlocks: uint64(n), + numValuesInValueBlocks: w.numValues, + valueBlocksAndIndexSize: w.totalBlockBytes + vbih.h.Length + blockTrailerLen, + } + return vbih, stats, err +} + +func (w *valueBlockWriter) writeValueBlocksIndex( + writer io.Writer, h valueBlocksIndexHandle, +) (valueBlocksIndexHandle, error) { + blockLen := + int(h.blockNumByteLength+h.blockOffsetByteLength+h.blockLengthByteLength) * len(w.blocks) + h.h.Length = uint64(blockLen) + blockLen += blockTrailerLen + var buf []byte + if cap(w.buf.b) < blockLen { + buf = make([]byte, blockLen) + w.buf.b = buf + } else { + buf = w.buf.b[:blockLen] + } + b := buf + for i := range w.blocks { + littleEndianPut(uint64(i), b, int(h.blockNumByteLength)) + b = b[int(h.blockNumByteLength):] + littleEndianPut(w.blocks[i].handle.Offset, b, int(h.blockOffsetByteLength)) + b = b[int(h.blockOffsetByteLength):] + littleEndianPut(w.blocks[i].handle.Length, b, int(h.blockLengthByteLength)) + b = b[int(h.blockLengthByteLength):] + } + if len(b) != blockTrailerLen { + panic("incorrect length calculation") + } + b[0] = byte(noCompressionBlockType) + w.computeChecksum(buf) + if _, err := writer.Write(buf); err != nil { + return valueBlocksIndexHandle{}, err + } + return h, nil +} + +// littleEndianPut writes v to b using little endian encoding, under the +// assumption that v can be represented using n bytes. +func littleEndianPut(v uint64, b []byte, n int) { + _ = b[n-1] // bounds check + for i := 0; i < n; i++ { + b[i] = byte(v) + v = v >> 8 + } +} + +// lenLittleEndian returns the minimum number of bytes needed to encode v +// using little endian encoding. +func lenLittleEndian(v uint64) int { + n := 0 + for i := 0; i < 8; i++ { + n++ + v = v >> 8 + if v == 0 { + break + } + } + return n +} + +func littleEndianGet(b []byte, n int) uint64 { + _ = b[n-1] // bounds check + v := uint64(b[0]) + for i := 1; i < n; i++ { + v |= uint64(b[i]) << (8 * i) + } + return v +} + +// UserKeyPrefixBound represents a [Lower,Upper) bound of user key prefixes. +// If both are nil, there is no bound specified. Else, Compare(Lower,Upper) +// must be < 0. +type UserKeyPrefixBound struct { + // Lower is a lower bound user key prefix. + Lower []byte + // Upper is an upper bound user key prefix. + Upper []byte +} + +// IsEmpty returns true iff the bound is empty. +func (ukb *UserKeyPrefixBound) IsEmpty() bool { + return len(ukb.Lower) == 0 && len(ukb.Upper) == 0 +} + +type blockProviderWhenOpen interface { + readBlockForVBR( + h BlockHandle, stats *base.InternalIteratorStats, + ) (bufferHandle, error) +} + +type blockProviderWhenClosed struct { + rp ReaderProvider + r *Reader +} + +func (bpwc *blockProviderWhenClosed) open() error { + var err error + bpwc.r, err = bpwc.rp.GetReader() + return err +} + +func (bpwc *blockProviderWhenClosed) close() { + bpwc.rp.Close() + bpwc.r = nil +} + +func (bpwc blockProviderWhenClosed) readBlockForVBR( + h BlockHandle, stats *base.InternalIteratorStats, +) (bufferHandle, error) { + // This is rare, since most block reads happen when the corresponding + // sstable iterator is open. So we are willing to sacrifice a proper context + // for tracing. + // + // TODO(sumeer): consider fixing this. See + // https://github.com/cockroachdb/pebble/pull/3065#issue-1991175365 for an + // alternative. + ctx := objiotracing.WithBlockType(context.Background(), 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 /* iterStats */, nil /* buffer pool */) +} + +// ReaderProvider supports the implementation of blockProviderWhenClosed. +// GetReader and Close can be called multiple times in pairs. +type ReaderProvider interface { + GetReader() (r *Reader, err error) + Close() +} + +// TrivialReaderProvider implements ReaderProvider for a Reader that will +// outlive the top-level iterator in the iterator tree. +type TrivialReaderProvider struct { + *Reader +} + +var _ ReaderProvider = TrivialReaderProvider{} + +// GetReader implements ReaderProvider. +func (trp TrivialReaderProvider) GetReader() (*Reader, error) { + return trp.Reader, nil +} + +// Close implements ReaderProvider. +func (trp TrivialReaderProvider) Close() {} + +// valueBlockReader is used to retrieve values in value +// blocks. It is used when the sstable was written with +// Properties.ValueBlocksAreEnabled. +type valueBlockReader struct { + bpOpen blockProviderWhenOpen + rp ReaderProvider + vbih valueBlocksIndexHandle + stats *base.InternalIteratorStats + + // 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 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 + // 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 bufferHandle + lazyFetcher base.LazyFetcher + closed bool + bufToMangle []byte +} + +func (r *valueBlockReader) getLazyValueForPrefixAndValueHandle(handle []byte) base.LazyValue { + fetcher := &r.lazyFetcher + valLen, h := decodeLenFromValueHandle(handle[1:]) + *fetcher = base.LazyFetcher{ + Fetcher: r, + Attribute: base.AttributeAndLen{ + ValueLen: int32(valLen), + ShortAttribute: getShortAttribute(valuePrefix(handle[0])), + }, + } + if r.stats != nil { + r.stats.SeparatedPointValue.Count++ + r.stats.SeparatedPointValue.ValueBytes += uint64(valLen) + } + return base.LazyValue{ + ValueOrHandle: h, + Fetcher: fetcher, + } +} + +func (r *valueBlockReader) close() { + r.bpOpen = nil + r.vbiBlock = nil + r.vbiCache.Release() + // Set the handle to empty since Release does not nil the Handle.value. If + // 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 = bufferHandle{} + r.valueBlock = nil + r.valueBlockPtr = nil + r.valueCache.Release() + // See comment above. + r.valueCache = bufferHandle{} + r.closed = true + // rp, vbih, stats remain valid, so that LazyFetcher.ValueFetcher can be + // implemented. +} + +// Fetch implements base.ValueFetcher. +func (r *valueBlockReader) Fetch( + handle []byte, valLen int32, buf []byte, +) (val []byte, callerOwned bool, err error) { + if !r.closed { + val, err := r.getValueInternal(handle, valLen) + if invariants.Enabled { + val = r.doValueMangling(val) + } + return val, false, err + } + + bp := blockProviderWhenClosed{rp: r.rp} + err = bp.open() + if err != nil { + return nil, false, err + } + defer bp.close() + defer r.close() + r.bpOpen = bp + var v []byte + v, err = r.getValueInternal(handle, valLen) + if err != nil { + return nil, false, err + } + buf = append(buf[:0], v...) + return buf, true, nil +} + +// doValueMangling attempts to uncover violations of the contract listed in +// the declaration comment of LazyValue. It is expensive, hence only called +// when invariants.Enabled. +func (r *valueBlockReader) doValueMangling(v []byte) []byte { + // Randomly set the bytes in the previous retrieved value to 0, since + // property P1 only requires the valueBlockReader to maintain the memory of + // one fetched value. + if rand.Intn(2) == 0 { + for i := range r.bufToMangle { + r.bufToMangle[i] = 0 + } + } + // Store the current value in a new buffer for future mangling. + r.bufToMangle = append([]byte(nil), v...) + return r.bufToMangle +} + +func (r *valueBlockReader) getValueInternal(handle []byte, valLen int32) (val []byte, err error) { + vh := decodeRemainingValueHandle(handle) + vh.valueLen = uint32(valLen) + if r.vbiBlock == nil { + ch, err := r.bpOpen.readBlockForVBR(r.vbih.h, r.stats) + if err != nil { + return nil, err + } + r.vbiCache = ch + r.vbiBlock = ch.Get() + } + if r.valueBlock == nil || r.valueBlockNum != vh.blockNum { + vbh, err := r.getBlockHandle(vh.blockNum) + if err != nil { + return nil, err + } + vbCacheHandle, err := r.bpOpen.readBlockForVBR(vbh, r.stats) + if err != nil { + return nil, err + } + r.valueBlockNum = vh.blockNum + r.valueCache.Release() + r.valueCache = vbCacheHandle + r.valueBlock = vbCacheHandle.Get() + r.valueBlockPtr = unsafe.Pointer(&r.valueBlock[0]) + } + if r.stats != nil { + r.stats.SeparatedPointValue.ValueBytesFetched += uint64(valLen) + } + return r.valueBlock[vh.offsetInBlock : vh.offsetInBlock+vh.valueLen], nil +} + +func (r *valueBlockReader) getBlockHandle(blockNum uint32) (BlockHandle, error) { + indexEntryLen := + int(r.vbih.blockNumByteLength + r.vbih.blockOffsetByteLength + r.vbih.blockLengthByteLength) + offsetInIndex := indexEntryLen * int(blockNum) + if len(r.vbiBlock) < offsetInIndex+indexEntryLen { + return BlockHandle{}, errors.Errorf( + "cannot read at offset %d and length %d from block of length %d", + offsetInIndex, indexEntryLen, len(r.vbiBlock)) + } + b := r.vbiBlock[offsetInIndex : offsetInIndex+indexEntryLen] + n := int(r.vbih.blockNumByteLength) + bn := littleEndianGet(b, n) + if uint32(bn) != blockNum { + return BlockHandle{}, + errors.Errorf("expected block num %d but found %d", blockNum, bn) + } + b = b[n:] + n = int(r.vbih.blockOffsetByteLength) + blockOffset := littleEndianGet(b, n) + b = b[n:] + n = int(r.vbih.blockLengthByteLength) + blockLen := littleEndianGet(b, n) + return BlockHandle{Offset: blockOffset, Length: blockLen}, nil +} diff --git a/pebble/sstable/value_block_test.go b/pebble/sstable/value_block_test.go new file mode 100644 index 0000000..f292c8a --- /dev/null +++ b/pebble/sstable/value_block_test.go @@ -0,0 +1,111 @@ +// 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 sstable + +import ( + "fmt" + "math" + "math/rand" + "testing" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" +) + +func TestValueHandleEncodeDecode(t *testing.T) { + testCases := []valueHandle{ + {valueLen: 23, blockNum: 100003, offsetInBlock: 2300}, + {valueLen: math.MaxUint32 - 1, blockNum: math.MaxUint32 / 2, offsetInBlock: math.MaxUint32 - 2}, + } + var buf [valueHandleMaxLen]byte + for _, tc := range testCases { + t.Run(fmt.Sprintf("%+v", tc), func(t *testing.T) { + n := encodeValueHandle(buf[:], tc) + vh := decodeValueHandle(buf[:n]) + require.Equal(t, tc, vh) + }) + } +} + +func TestValuePrefix(t *testing.T) { + testCases := []struct { + isHandle bool + setHasSamePrefix bool + attr base.ShortAttribute + }{ + { + isHandle: false, + setHasSamePrefix: false, + }, + { + isHandle: false, + setHasSamePrefix: true, + }, + { + isHandle: true, + setHasSamePrefix: false, + attr: 5, + }, + { + isHandle: true, + setHasSamePrefix: true, + attr: 2, + }, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%+v", tc), func(t *testing.T) { + var prefix valuePrefix + if tc.isHandle { + prefix = makePrefixForValueHandle(tc.setHasSamePrefix, tc.attr) + } else { + prefix = makePrefixForInPlaceValue(tc.setHasSamePrefix) + } + require.Equal(t, tc.isHandle, isValueHandle(prefix)) + require.Equal(t, tc.setHasSamePrefix, setHasSamePrefix(prefix)) + if tc.isHandle { + require.Equal(t, tc.attr, getShortAttribute(prefix)) + } + }) + } +} + +func TestValueBlocksIndexHandleEncodeDecode(t *testing.T) { + testCases := []valueBlocksIndexHandle{ + { + h: BlockHandle{ + Offset: math.MaxUint64 / 2, + Length: math.MaxUint64 / 4, + }, + blockNumByteLength: 53, + blockOffsetByteLength: math.MaxUint8, + blockLengthByteLength: math.MaxUint8 / 2, + }, + } + var buf [valueBlocksIndexHandleMaxLen]byte + for _, tc := range testCases { + t.Run(fmt.Sprintf("%+v", tc), func(t *testing.T) { + n := encodeValueBlocksIndexHandle(buf[:], tc) + vbih, n2, err := decodeValueBlocksIndexHandle(buf[:n]) + require.NoError(t, err) + require.Equal(t, n, n2) + require.Equal(t, tc, vbih) + }) + } +} + +func TestLittleEndianGetPut(t *testing.T) { + testCases := []uint64{ + 0, (1 << 10) - 1, (1 << 25) + 1, math.MaxUint32, math.MaxUint64, uint64(rand.Int63())} + var buf [8]byte + for _, tc := range testCases { + t.Run(fmt.Sprintf("%d", tc), func(t *testing.T) { + length := lenLittleEndian(tc) + b := buf[:length:length] + littleEndianPut(tc, b, length) + v := littleEndianGet(b, length) + require.Equal(t, tc, v) + }) + } +} diff --git a/pebble/sstable/write_queue.go b/pebble/sstable/write_queue.go new file mode 100644 index 0000000..898f327 --- /dev/null +++ b/pebble/sstable/write_queue.go @@ -0,0 +1,140 @@ +package sstable + +import ( + "sync" + + "github.com/cockroachdb/pebble/internal/base" +) + +type writeTask struct { + // Since writeTasks are pooled, the compressionDone channel will be re-used. + // It is necessary that any writes to the channel have already been read, + // before adding the writeTask back to the pool. + compressionDone chan bool + buf *dataBlockBuf + // If this is not nil, then this index block will be flushed. + flushableIndexBlock *indexBlockBuf + // currIndexBlock is the index block on which indexBlock.add must be called. + currIndexBlock *indexBlockBuf + indexEntrySep InternalKey + // inflightIndexEntrySize is used to decrement Writer.indexBlock.sizeEstimate.inflightSize. + indexInflightSize int + // If the index block is finished, then we set the finishedIndexProps here. + finishedIndexProps []byte +} + +// It is not the responsibility of the writeTask to clear the +// task.flushableIndexBlock, and task.buf. +func (task *writeTask) clear() { + *task = writeTask{ + indexEntrySep: base.InvalidInternalKey, + compressionDone: task.compressionDone, + } +} + +// Note that only the Writer client goroutine will be adding tasks to the writeQueue. +// Both the Writer client and the compression goroutines will be able to write to +// writeTask.compressionDone to indicate that the compression job associated with +// a writeTask has finished. +type writeQueue struct { + tasks chan *writeTask + wg sync.WaitGroup + writer *Writer + + // err represents an error which is encountered when the write queue attempts + // to write a block to disk. The error is stored here to skip unnecessary block + // writes once the first error is encountered. + err error + closed bool +} + +func newWriteQueue(size int, writer *Writer) *writeQueue { + w := &writeQueue{} + w.tasks = make(chan *writeTask, size) + w.writer = writer + + w.wg.Add(1) + go w.runWorker() + return w +} + +func (w *writeQueue) performWrite(task *writeTask) error { + var bh BlockHandle + var bhp BlockHandleWithProperties + + var err error + if bh, err = w.writer.writeCompressedBlock(task.buf.compressed, task.buf.tmp[:]); err != nil { + return err + } + + bhp = BlockHandleWithProperties{BlockHandle: bh, Props: task.buf.dataBlockProps} + if err = w.writer.addIndexEntry( + task.indexEntrySep, bhp, task.buf.tmp[:], task.flushableIndexBlock, task.currIndexBlock, + task.indexInflightSize, task.finishedIndexProps); err != nil { + return err + } + + return nil +} + +// It is necessary to ensure that none of the buffers in the writeTask, +// dataBlockBuf, indexBlockBuf, are pointed to by another struct. +func (w *writeQueue) releaseBuffers(task *writeTask) { + task.buf.clear() + dataBlockBufPool.Put(task.buf) + + // This index block is no longer used by the Writer, so we can add it back + // to the pool. + if task.flushableIndexBlock != nil { + task.flushableIndexBlock.clear() + indexBlockBufPool.Put(task.flushableIndexBlock) + } + + task.clear() + writeTaskPool.Put(task) +} + +func (w *writeQueue) runWorker() { + for task := range w.tasks { + <-task.compressionDone + + if w.err == nil { + w.err = w.performWrite(task) + } + + w.releaseBuffers(task) + } + w.wg.Done() +} + +func (w *writeQueue) add(task *writeTask) { + w.tasks <- task +} + +// addSync will perform the writeTask synchronously with the caller goroutine. Calls to addSync +// are no longer valid once writeQueue.add has been called at least once. +func (w *writeQueue) addSync(task *writeTask) error { + // This should instantly return without blocking. + <-task.compressionDone + + if w.err == nil { + w.err = w.performWrite(task) + } + + w.releaseBuffers(task) + + return w.err +} + +// finish should only be called once no more tasks will be added to the writeQueue. +// finish will return any error which was encountered while tasks were processed. +func (w *writeQueue) finish() error { + if w.closed { + return w.err + } + + close(w.tasks) + w.wg.Wait() + w.closed = true + return w.err +} diff --git a/pebble/sstable/writer.go b/pebble/sstable/writer.go new file mode 100644 index 0000000..883ee7c --- /dev/null +++ b/pebble/sstable/writer.go @@ -0,0 +1,2450 @@ +// 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" + "encoding/binary" + "fmt" + "math" + "runtime" + "sort" + "sync" + + "github.com/cespare/xxhash/v2" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/bytealloc" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/crc" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/private" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/objstorage" +) + +// encodedBHPEstimatedSize estimates the size of the encoded BlockHandleWithProperties. +// It would also be nice to account for the length of the data block properties here, +// but isn't necessary since this is an estimate. +const encodedBHPEstimatedSize = binary.MaxVarintLen64 * 2 + +var errWriterClosed = errors.New("pebble: writer is closed") + +// WriterMetadata holds info about a finished sstable. +type WriterMetadata struct { + Size uint64 + SmallestPoint InternalKey + // LargestPoint, LargestRangeKey, LargestRangeDel should not be accessed + // before Writer.Close is called, because they may only be set on + // Writer.Close. + LargestPoint InternalKey + SmallestRangeDel InternalKey + LargestRangeDel InternalKey + SmallestRangeKey InternalKey + LargestRangeKey InternalKey + HasPointKeys bool + HasRangeDelKeys bool + HasRangeKeys bool + SmallestSeqNum uint64 + LargestSeqNum uint64 + Properties Properties +} + +// SetSmallestPointKey sets the smallest point key to the given key. +// NB: this method set the "absolute" smallest point key. Any existing key is +// overridden. +func (m *WriterMetadata) SetSmallestPointKey(k InternalKey) { + m.SmallestPoint = k + m.HasPointKeys = true +} + +// SetSmallestRangeDelKey sets the smallest rangedel key to the given key. +// NB: this method set the "absolute" smallest rangedel key. Any existing key is +// overridden. +func (m *WriterMetadata) SetSmallestRangeDelKey(k InternalKey) { + m.SmallestRangeDel = k + m.HasRangeDelKeys = true +} + +// SetSmallestRangeKey sets the smallest range key to the given key. +// NB: this method set the "absolute" smallest range key. Any existing key is +// overridden. +func (m *WriterMetadata) SetSmallestRangeKey(k InternalKey) { + m.SmallestRangeKey = k + m.HasRangeKeys = true +} + +// SetLargestPointKey sets the largest point key to the given key. +// NB: this method set the "absolute" largest point key. Any existing key is +// overridden. +func (m *WriterMetadata) SetLargestPointKey(k InternalKey) { + m.LargestPoint = k + m.HasPointKeys = true +} + +// SetLargestRangeDelKey sets the largest rangedel key to the given key. +// NB: this method set the "absolute" largest rangedel key. Any existing key is +// overridden. +func (m *WriterMetadata) SetLargestRangeDelKey(k InternalKey) { + m.LargestRangeDel = k + m.HasRangeDelKeys = true +} + +// SetLargestRangeKey sets the largest range key to the given key. +// NB: this method set the "absolute" largest range key. Any existing key is +// overridden. +func (m *WriterMetadata) SetLargestRangeKey(k InternalKey) { + m.LargestRangeKey = k + m.HasRangeKeys = true +} + +func (m *WriterMetadata) updateSeqNum(seqNum uint64) { + if m.SmallestSeqNum > seqNum { + m.SmallestSeqNum = seqNum + } + if m.LargestSeqNum < seqNum { + m.LargestSeqNum = seqNum + } +} + +// Writer is a table writer. +type Writer struct { + writable objstorage.Writable + meta WriterMetadata + err error + // cacheID and fileNum are used to remove blocks written to the sstable from + // the cache, providing a defense in depth against bugs which cause cache + // collisions. + cacheID uint64 + fileNum base.DiskFileNum + // The following fields are copied from Options. + blockSize int + blockSizeThreshold int + indexBlockSize int + indexBlockSizeThreshold int + compare Compare + split Split + formatKey base.FormatKey + compression Compression + separator Separator + successor Successor + tableFormat TableFormat + isStrictObsolete bool + writingToLowestLevel bool + cache *cache.Cache + restartInterval int + checksumType ChecksumType + // disableKeyOrderChecks disables the checks that keys are added to an + // sstable in order. It is intended for internal use only in the construction + // of invalid sstables for testing. See tool/make_test_sstables.go. + disableKeyOrderChecks bool + // With two level indexes, the index/filter of a SST file is partitioned into + // smaller blocks with an additional top-level index on them. When reading an + // index/filter, only the top-level index is loaded into memory. The two level + // index/filter then uses the top-level index to load on demand into the block + // cache the partitions that are required to perform the index/filter query. + // + // Two level indexes are enabled automatically when there is more than one + // index block. + // + // This is useful when there are very large index blocks, which generally occurs + // with the usage of large keys. With large index blocks, the index blocks fight + // the data blocks for block cache space and the index blocks are likely to be + // re-read many times from the disk. The top level index, which has a much + // smaller memory footprint, can be used to prevent the entire index block from + // being loaded into the block cache. + twoLevelIndex bool + // Internal flag to allow creation of range-del-v1 format blocks. Only used + // for testing. Note that v2 format blocks are backwards compatible with v1 + // format blocks. + rangeDelV1Format bool + indexBlock *indexBlockBuf + rangeDelBlock blockWriter + rangeKeyBlock blockWriter + topLevelIndexBlock blockWriter + 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 + // nil, or the full keys otherwise. + filter filterWriter + indexPartitions []indexBlockAndBlockProperties + + // indexBlockAlloc is used to bulk-allocate byte slices used to store index + // blocks in indexPartitions. These live until the index finishes. + indexBlockAlloc []byte + // indexSepAlloc is used to bulk-allocate index block separator slices stored + // in indexPartitions. These live until the index finishes. + indexSepAlloc bytealloc.A + + // To allow potentially overlapping (i.e. un-fragmented) range keys spans to + // be added to the Writer, a keyspan.Fragmenter is used to retain the keys + // and values, emitting fragmented, coalesced spans as appropriate. Range + // keys must be added in order of their start user-key. + fragmenter keyspan.Fragmenter + rangeKeyEncoder rangekey.Encoder + rangeKeysBySuffix keyspan.KeysBySuffix + rangeKeySpan keyspan.Span + rkBuf []byte + // dataBlockBuf consists of the state which is currently owned by and used by + // the Writer client goroutine. This state can be handed off to other goroutines. + dataBlockBuf *dataBlockBuf + // blockBuf consists of the state which is owned by and used by the Writer client + // goroutine. + blockBuf blockBuf + + coordination coordinationState + + // Information (other than the byte slice) about the last point key, to + // avoid extracting it again. + lastPointKeyInfo pointKeyInfo + + // For value blocks. + shortAttributeExtractor base.ShortAttributeExtractor + requiredInPlaceValueBound UserKeyPrefixBound + valueBlockWriter *valueBlockWriter +} + +type pointKeyInfo struct { + trailer uint64 + // Only computed when w.valueBlockWriter is not nil. + userKeyLen int + // 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 { + parallelismEnabled bool + + // writeQueue is used to write data blocks to disk. The writeQueue is primarily + // used to maintain the order in which data blocks must be written to disk. For + // this reason, every single data block write must be done through the writeQueue. + writeQueue *writeQueue + + sizeEstimate dataBlockEstimates +} + +func (c *coordinationState) init(parallelismEnabled bool, writer *Writer) { + c.parallelismEnabled = parallelismEnabled + // useMutex is false regardless of parallelismEnabled, because we do not do + // parallel compression yet. + c.sizeEstimate.useMutex = false + + // writeQueueSize determines the size of the write queue, or the number + // of items which can be added to the queue without blocking. By default, we + // use a writeQueue size of 0, since we won't be doing any block writes in + // parallel. + writeQueueSize := 0 + if parallelismEnabled { + writeQueueSize = runtime.GOMAXPROCS(0) + } + c.writeQueue = newWriteQueue(writeQueueSize, writer) +} + +// sizeEstimate is a general purpose helper for estimating two kinds of sizes: +// A. The compressed sstable size, which is useful for deciding when to start +// +// a new sstable during flushes or compactions. In practice, we use this in +// estimating the data size (excluding the index). +// +// B. The size of index blocks to decide when to start a new index block. +// +// There are some terminology peculiarities which are due to the origin of +// sizeEstimate for use case A with parallel compression enabled (for which +// the code has not been merged). Specifically this relates to the terms +// "written" and "compressed". +// - The notion of "written" for case A is sufficiently defined by saying that +// the data block is compressed. Waiting for the actual data block write to +// happen can result in unnecessary estimation, when we already know how big +// it will be in compressed form. Additionally, with the forthcoming value +// blocks containing older MVCC values, these compressed block will be held +// in-memory until late in the sstable writing, and we do want to accurately +// account for them without waiting for the actual write. +// For case B, "written" means that the index entry has been fully +// generated, and has been added to the uncompressed block buffer for that +// index block. It does not include actually writing a potentially +// compressed index block. +// - The notion of "compressed" is to differentiate between a "inflight" size +// and the actual size, and is handled via computing a compression ratio +// observed so far (defaults to 1). +// For case A, this is actual data block compression, so the "inflight" size +// is uncompressed blocks (that are no longer being written to) and the +// "compressed" size is after they have been compressed. +// For case B the inflight size is for a key-value pair in the index for +// which the value size (the encoded size of the BlockHandleWithProperties) +// is not accurately known, while the compressed size is the size of that +// entry when it has been added to the (in-progress) index ssblock. +// +// Usage: To update state, one can optionally provide an inflight write value +// using addInflight (used for case B). When something is "written" the state +// can be updated using either writtenWithDelta or writtenWithTotal, which +// provide the actual delta size or the total size (latter must be +// monotonically non-decreasing). If there were no calls to addInflight, there +// isn't any real estimation happening here. So case A does not do any real +// estimation. However, when we introduce parallel compression, there will be +// estimation in that the client goroutine will call addInFlight and the +// compression goroutines will call writtenWithDelta. +type sizeEstimate struct { + // emptySize is the size when there is no inflight data, and numEntries is 0. + // emptySize is constant once set. + emptySize uint64 + + // inflightSize is the estimated size of some inflight data which hasn't + // been written yet. + inflightSize uint64 + + // totalSize is the total size of the data which has already been written. + totalSize uint64 + + // numWrittenEntries is the total number of entries which have already been + // written. + numWrittenEntries uint64 + // numInflightEntries is the total number of entries which are inflight, and + // haven't been written. + numInflightEntries uint64 + + // maxEstimatedSize stores the maximum result returned from sizeEstimate.size. + // It ensures that values returned from subsequent calls to Writer.EstimatedSize + // never decrease. + maxEstimatedSize uint64 + + // We assume that the entries added to the sizeEstimate can be compressed. + // For this reason, we keep track of a compressedSize and an uncompressedSize + // to compute a compression ratio for the inflight entries. If the entries + // aren't being compressed, then compressedSize and uncompressedSize must be + // equal. + compressedSize uint64 + uncompressedSize uint64 +} + +func (s *sizeEstimate) init(emptySize uint64) { + s.emptySize = emptySize +} + +func (s *sizeEstimate) size() uint64 { + ratio := float64(1) + if s.uncompressedSize > 0 { + ratio = float64(s.compressedSize) / float64(s.uncompressedSize) + } + estimatedInflightSize := uint64(float64(s.inflightSize) * ratio) + total := s.totalSize + estimatedInflightSize + if total > s.maxEstimatedSize { + s.maxEstimatedSize = total + } else { + total = s.maxEstimatedSize + } + + if total == 0 { + return s.emptySize + } + + return total +} + +func (s *sizeEstimate) numTotalEntries() uint64 { + return s.numWrittenEntries + s.numInflightEntries +} + +func (s *sizeEstimate) addInflight(size int) { + s.numInflightEntries++ + s.inflightSize += uint64(size) +} + +func (s *sizeEstimate) writtenWithTotal(newTotalSize uint64, inflightSize int) { + finalEntrySize := int(newTotalSize - s.totalSize) + s.writtenWithDelta(finalEntrySize, inflightSize) +} + +func (s *sizeEstimate) writtenWithDelta(finalEntrySize int, inflightSize int) { + if inflightSize > 0 { + // This entry was previously inflight, so we should decrement inflight + // entries and update the "compression" stats for future estimation. + s.numInflightEntries-- + s.inflightSize -= uint64(inflightSize) + s.uncompressedSize += uint64(inflightSize) + s.compressedSize += uint64(finalEntrySize) + } + s.numWrittenEntries++ + s.totalSize += uint64(finalEntrySize) +} + +func (s *sizeEstimate) clear() { + *s = sizeEstimate{emptySize: s.emptySize} +} + +type indexBlockBuf struct { + // block will only be accessed from the writeQueue. + block blockWriter + + size struct { + useMutex bool + mu sync.Mutex + estimate sizeEstimate + } + + // restartInterval matches indexBlockBuf.block.restartInterval. We store it twice, because the `block` + // must only be accessed from the writeQueue goroutine. + restartInterval int +} + +func (i *indexBlockBuf) clear() { + i.block.clear() + if i.size.useMutex { + i.size.mu.Lock() + defer i.size.mu.Unlock() + } + i.size.estimate.clear() + i.restartInterval = 0 +} + +var indexBlockBufPool = sync.Pool{ + New: func() interface{} { + return &indexBlockBuf{} + }, +} + +const indexBlockRestartInterval = 1 + +func newIndexBlockBuf(useMutex bool) *indexBlockBuf { + i := indexBlockBufPool.Get().(*indexBlockBuf) + i.size.useMutex = useMutex + i.restartInterval = indexBlockRestartInterval + i.block.restartInterval = indexBlockRestartInterval + i.size.estimate.init(emptyBlockSize) + return i +} + +func (i *indexBlockBuf) shouldFlush( + sep InternalKey, valueLen, targetBlockSize, sizeThreshold int, +) bool { + if i.size.useMutex { + i.size.mu.Lock() + defer i.size.mu.Unlock() + } + + nEntries := i.size.estimate.numTotalEntries() + return shouldFlush( + sep, valueLen, i.restartInterval, int(i.size.estimate.size()), + int(nEntries), targetBlockSize, sizeThreshold) +} + +func (i *indexBlockBuf) add(key InternalKey, value []byte, inflightSize int) { + i.block.add(key, value) + size := i.block.estimatedSize() + if i.size.useMutex { + i.size.mu.Lock() + defer i.size.mu.Unlock() + } + i.size.estimate.writtenWithTotal(uint64(size), inflightSize) +} + +func (i *indexBlockBuf) finish() []byte { + b := i.block.finish() + return b +} + +func (i *indexBlockBuf) addInflight(inflightSize int) { + if i.size.useMutex { + i.size.mu.Lock() + defer i.size.mu.Unlock() + } + i.size.estimate.addInflight(inflightSize) +} + +func (i *indexBlockBuf) estimatedSize() uint64 { + if i.size.useMutex { + i.size.mu.Lock() + defer i.size.mu.Unlock() + } + + // Make sure that the size estimation works as expected when parallelism + // is disabled. + if invariants.Enabled && !i.size.useMutex { + if i.size.estimate.inflightSize != 0 { + panic("unexpected inflight entry in index block size estimation") + } + + // NB: The i.block should only be accessed from the writeQueue goroutine, + // when parallelism is enabled. We break that invariant here, but that's + // okay since parallelism is disabled. + if i.size.estimate.size() != uint64(i.block.estimatedSize()) { + panic("index block size estimation sans parallelism is incorrect") + } + } + return i.size.estimate.size() +} + +// sizeEstimate is used for sstable size estimation. sizeEstimate can be +// accessed by the Writer client and compressionQueue goroutines. Fields +// should only be read/updated through the functions defined on the +// *sizeEstimate type. +type dataBlockEstimates struct { + // If we don't do block compression in parallel, then we don't need to take + // the performance hit of synchronizing using this mutex. + useMutex bool + mu sync.Mutex + + estimate sizeEstimate +} + +// inflightSize is the uncompressed block size estimate which has been +// previously provided to addInflightDataBlock(). If addInflightDataBlock() +// has not been called, this must be set to 0. compressedSize is the +// compressed size of the block. +func (d *dataBlockEstimates) dataBlockCompressed(compressedSize int, inflightSize int) { + if d.useMutex { + d.mu.Lock() + defer d.mu.Unlock() + } + d.estimate.writtenWithDelta(compressedSize+blockTrailerLen, inflightSize) +} + +// size is an estimated size of datablock data which has been written to disk. +func (d *dataBlockEstimates) size() uint64 { + if d.useMutex { + d.mu.Lock() + defer d.mu.Unlock() + } + // If there is no parallel compression, there should not be any inflight bytes. + if invariants.Enabled && !d.useMutex { + if d.estimate.inflightSize != 0 { + panic("unexpected inflight entry in data block size estimation") + } + } + return d.estimate.size() +} + +// Avoid linter unused error. +var _ = (&dataBlockEstimates{}).addInflightDataBlock + +// NB: unused since no parallel compression. +func (d *dataBlockEstimates) addInflightDataBlock(size int) { + if d.useMutex { + d.mu.Lock() + defer d.mu.Unlock() + } + + d.estimate.addInflight(size) +} + +var writeTaskPool = sync.Pool{ + New: func() interface{} { + t := &writeTask{} + t.compressionDone = make(chan bool, 1) + return t + }, +} + +type checksummer struct { + checksumType ChecksumType + xxHasher *xxhash.Digest +} + +func (c *checksummer) checksum(block []byte, blockType []byte) (checksum uint32) { + // Calculate the checksum. + switch c.checksumType { + case ChecksumTypeCRC32c: + checksum = crc.New(block).Update(blockType).Value() + case ChecksumTypeXXHash64: + if c.xxHasher == nil { + c.xxHasher = xxhash.New() + } else { + c.xxHasher.Reset() + } + c.xxHasher.Write(block) + c.xxHasher.Write(blockType) + checksum = uint32(c.xxHasher.Sum64()) + default: + panic(errors.Newf("unsupported checksum type: %d", c.checksumType)) + } + return checksum +} + +type blockBuf struct { + // tmp is a scratch buffer, large enough to hold either footerLen bytes, + // blockTrailerLen bytes, (5 * binary.MaxVarintLen64) bytes, and most + // likely large enough for a block handle with properties. + tmp [blockHandleLikelyMaxLen]byte + // compressedBuf is the destination buffer for compression. It is re-used over the + // lifetime of the blockBuf, avoiding the allocation of a temporary buffer for each block. + compressedBuf []byte + checksummer checksummer +} + +func (b *blockBuf) clear() { + // We can't assign b.compressedBuf[:0] to compressedBuf because snappy relies + // on the length of the buffer, and not the capacity to determine if it needs + // to make an allocation. + *b = blockBuf{ + compressedBuf: b.compressedBuf, checksummer: b.checksummer, + } +} + +// A dataBlockBuf holds all the state required to compress and write a data block to disk. +// A dataBlockBuf begins its lifecycle owned by the Writer client goroutine. The Writer +// client goroutine adds keys to the sstable, writing directly into a dataBlockBuf's blockWriter +// until the block is full. Once a dataBlockBuf's block is full, the dataBlockBuf may be passed +// to other goroutines for compression and file I/O. +type dataBlockBuf struct { + blockBuf + dataBlock blockWriter + + // uncompressed is a reference to a byte slice which is owned by the dataBlockBuf. It is the + // next byte slice to be compressed. The uncompressed byte slice will be backed by the + // dataBlock.buf. + uncompressed []byte + // compressed is a reference to a byte slice which is owned by the dataBlockBuf. It is the + // compressed byte slice which must be written to disk. The compressed byte slice may be + // backed by the dataBlock.buf, or the dataBlockBuf.compressedBuf, depending on whether + // we use the result of the compression. + compressed []byte + + // We're making calls to BlockPropertyCollectors from the Writer client goroutine. We need to + // pass the encoded block properties over to the write queue. To prevent copies, and allocations, + // we give each dataBlockBuf, a blockPropertiesEncoder. + blockPropsEncoder blockPropertiesEncoder + // dataBlockProps is set when Writer.finishDataBlockProps is called. The dataBlockProps slice is + // a shallow copy of the internal buffer of the dataBlockBuf.blockPropsEncoder. + dataBlockProps []byte + + // sepScratch is reusable scratch space for computing separator keys. + sepScratch []byte +} + +func (d *dataBlockBuf) clear() { + d.blockBuf.clear() + d.dataBlock.clear() + + d.uncompressed = nil + d.compressed = nil + d.dataBlockProps = nil + d.sepScratch = d.sepScratch[:0] +} + +var dataBlockBufPool = sync.Pool{ + New: func() interface{} { + return &dataBlockBuf{} + }, +} + +func newDataBlockBuf(restartInterval int, checksumType ChecksumType) *dataBlockBuf { + d := dataBlockBufPool.Get().(*dataBlockBuf) + d.dataBlock.restartInterval = restartInterval + d.checksummer.checksumType = checksumType + return d +} + +func (d *dataBlockBuf) finish() { + d.uncompressed = d.dataBlock.finish() +} + +func (d *dataBlockBuf) compressAndChecksum(c Compression) { + d.compressed = compressAndChecksum(d.uncompressed, c, &d.blockBuf) +} + +func (d *dataBlockBuf) shouldFlush( + key InternalKey, valueLen, targetBlockSize, sizeThreshold int, +) bool { + return shouldFlush( + key, valueLen, d.dataBlock.restartInterval, d.dataBlock.estimatedSize(), + d.dataBlock.nEntries, targetBlockSize, sizeThreshold) +} + +type indexBlockAndBlockProperties struct { + nEntries int + // sep is the last key added to this block, for computing a separator later. + sep InternalKey + properties []byte + // block is the encoded block produced by blockWriter.finish. + block []byte +} + +// Set sets the value for the given key. The sequence number is set to 0. +// Intended for use to externally construct an sstable before ingestion into a +// DB. For a given Writer, the keys passed to Set must be in strictly increasing +// order. +// +// TODO(peter): untested +func (w *Writer) Set(key, value []byte) error { + if w.err != nil { + return w.err + } + 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 +// 0. Intended for use to externally construct an sstable before ingestion into +// a DB. +// +// TODO(peter): untested +func (w *Writer) Delete(key []byte) error { + if w.err != nil { + return w.err + } + 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) +// (inclusive on start, exclusive on end). The sequence number is set to +// 0. Intended for use to externally construct an sstable before ingestion into +// a DB. +// +// TODO(peter): untested +func (w *Writer) DeleteRange(start, end []byte) error { + if w.err != nil { + return w.err + } + return w.addTombstone(base.MakeInternalKey(start, 0, InternalKeyKindRangeDelete), end) +} + +// Merge adds an action to the DB that merges the value at key with the new +// value. The details of the merge are dependent upon the configured merge +// operator. The sequence number is set to 0. Intended for use to externally +// construct an sstable before ingestion into a DB. +// +// TODO(peter): untested +func (w *Writer) Merge(key, value []byte) error { + if w.err != nil { + return w.err + } + 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, +// the keys passed to Add must be in increasing order. The exception to this +// rule is range deletion tombstones. Range deletion tombstones need to be +// added ordered by their start key, but they can be added out of order from +// 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 + } + + switch key.Kind() { + case InternalKeyKindRangeDelete: + return w.addTombstone(key, value) + case base.InternalKeyKindRangeKeyDelete, + base.InternalKeyKindRangeKeySet, + base.InternalKeyKindRangeKeyUnset: + w.err = errors.Errorf( + "pebble: range keys must be added via one of the RangeKey* functions") + return w.err + } + return w.addPoint(key, value, forceObsolete) +} + +func (w *Writer) makeAddPointDecisionV2(key InternalKey) error { + prevTrailer := w.lastPointKeyInfo.trailer + w.lastPointKeyInfo.trailer = key.Trailer + if w.dataBlockBuf.dataBlock.nEntries == 0 { + return nil + } + if !w.disableKeyOrderChecks { + prevPointUserKey := w.dataBlockBuf.dataBlock.getCurUserKey() + cmpUser := w.compare(prevPointUserKey, key.UserKey) + if cmpUser > 0 || (cmpUser == 0 && prevTrailer <= key.Trailer) { + return errors.Errorf( + "pebble: keys must be added in strictly increasing order: %s, %s", + InternalKey{UserKey: prevPointUserKey, Trailer: prevTrailer}.Pretty(w.formatKey), + key.Pretty(w.formatKey)) + } + } + 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, isObsolete bool, err error) { + prevPointKeyInfo := w.lastPointKeyInfo + w.lastPointKeyInfo.userKeyLen = len(key.UserKey) + w.lastPointKeyInfo.prefixLen = w.lastPointKeyInfo.userKeyLen + if w.split != nil { + w.lastPointKeyInfo.prefixLen = w.split(key.UserKey) + } + w.lastPointKeyInfo.trailer = key.Trailer + w.lastPointKeyInfo.isObsolete = false + if !w.meta.HasPointKeys { + return false, false, false, nil + } + keyKind := base.TrailerKind(key.Trailer) + prevPointUserKey := w.getLastPointUserKey() + prevPointKey := InternalKey{UserKey: prevPointUserKey, Trailer: prevPointKeyInfo.trailer} + prevKeyKind := base.TrailerKind(prevPointKeyInfo.trailer) + considerWriteToValueBlock := prevKeyKind == InternalKeyKindSet && + keyKind == InternalKeyKindSet + if considerWriteToValueBlock && !w.requiredInPlaceValueBound.IsEmpty() { + keyPrefix := key.UserKey[:w.lastPointKeyInfo.prefixLen] + cmpUpper := w.compare( + w.requiredInPlaceValueBound.Upper, keyPrefix) + if cmpUpper <= 0 { + // Common case for CockroachDB. Make it empty since all future keys in + // this sstable will also have cmpUpper <= 0. + w.requiredInPlaceValueBound = UserKeyPrefixBound{} + } else if w.compare(keyPrefix, w.requiredInPlaceValueBound.Lower) >= 0 { + considerWriteToValueBlock = false + } + } + // cmpPrefix is initialized iff considerWriteToValueBlock. + var cmpPrefix int + var cmpUser int + if considerWriteToValueBlock { + // Compare the prefixes. + cmpPrefix = w.compare(prevPointUserKey[:prevPointKeyInfo.prefixLen], + key.UserKey[:w.lastPointKeyInfo.prefixLen]) + cmpUser = cmpPrefix + if cmpPrefix == 0 { + // Need to compare suffixes to compute cmpUser. + cmpUser = w.compare(prevPointUserKey[prevPointKeyInfo.prefixLen:], + key.UserKey[w.lastPointKeyInfo.prefixLen:]) + } + } 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, 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, 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. + setHasSamePrefix = cmpPrefix == 0 + considerWriteToValueBlock = setHasSamePrefix + // Use of 0 here is somewhat arbitrary. Given the minimum 3 byte encoding of + // valueHandle, this should be > 3. But tiny values are common in test and + // unlikely in production, so we use 0 here for better test coverage. + const tinyValueThreshold = 0 + if considerWriteToValueBlock && valueLen <= tinyValueThreshold { + considerWriteToValueBlock = false + } + return setHasSamePrefix, considerWriteToValueBlock, isObsolete, nil +} + +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, isObsolete, err = + w.makeAddPointDecisionV3(key, len(value)) + addPrefixToValueStoredWithKey = base.TrailerKind(key.Trailer) == InternalKeyKindSet + } else { + err = w.makeAddPointDecisionV2(key) + } + if err != nil { + return err + } + isObsolete = w.tableFormat >= TableFormatPebblev4 && (isObsolete || forceObsolete) + w.lastPointKeyInfo.isObsolete = isObsolete + var valueStoredWithKey []byte + var prefix valuePrefix + var valueStoredWithKeyLen int + if writeToValueBlock { + vh, err := w.valueBlockWriter.addValue(value) + if err != nil { + return err + } + n := encodeValueHandle(w.blockBuf.tmp[:], vh) + valueStoredWithKey = w.blockBuf.tmp[:n] + valueStoredWithKeyLen = len(valueStoredWithKey) + 1 + var attribute base.ShortAttribute + if w.shortAttributeExtractor != nil { + // TODO(sumeer): for compactions, it is possible that the input sstable + // already has this value in the value section and so we have already + // extracted the ShortAttribute. Avoid extracting it again. This will + // require changing the Writer.Add interface. + if attribute, err = w.shortAttributeExtractor( + key.UserKey, w.lastPointKeyInfo.prefixLen, value); err != nil { + return err + } + } + prefix = makePrefixForValueHandle(setHasSameKeyPrefix, attribute) + } else { + valueStoredWithKey = value + valueStoredWithKeyLen = len(value) + if addPrefixToValueStoredWithKey { + valueStoredWithKeyLen++ + } + prefix = makePrefixForInPlaceValue(setHasSameKeyPrefix) + } + + if err := w.maybeFlush(key, valueStoredWithKeyLen); err != nil { + return err + } + + for i := range w.propCollectors { + if err := w.propCollectors[i].Add(key, value); err != nil { + w.err = err + return err + } + } + for i := range w.blockPropCollectors { + v := value + if addPrefixToValueStoredWithKey { + // Values for SET are not required to be in-place, and in the future may + // not even be read by the compaction, so pass nil values. Block + // property collectors in such Pebble DB's must not look at the value. + v = nil + } + if err := w.blockPropCollectors[i].Add(key, v); err != nil { + w.err = err + return err + } + } + if w.tableFormat >= TableFormatPebblev4 { + w.obsoleteCollector.AddPoint(isObsolete) + } + + w.maybeAddToFilter(key.UserKey) + w.dataBlockBuf.dataBlock.addWithOptionalValuePrefix( + key, isObsolete, valueStoredWithKey, maxSharedKeyLen, addPrefixToValueStoredWithKey, prefix, + setHasSameKeyPrefix) + + w.meta.updateSeqNum(key.SeqNum()) + + if !w.meta.HasPointKeys { + 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 + // we don't want SmallestPoint.UserKey to be nil. + // + // todo(bananabrick): Determine if it's okay to have a nil SmallestPoint + // .UserKey now that we don't rely on a nil UserKey to determine if the + // key has been set or not. + w.meta.SetSmallestPointKey(k.Clone()) + } + + w.props.NumEntries++ + 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++ + } + w.props.RawKeySize += uint64(key.Size()) + w.props.RawValueSize += uint64(len(value)) + return nil +} + +func (w *Writer) prettyTombstone(k InternalKey, value []byte) fmt.Formatter { + return keyspan.Span{ + Start: k.UserKey, + End: value, + Keys: []keyspan.Key{{Trailer: k.Trailer}}, + }.Pretty(w.formatKey) +} + +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 := 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", + prevKey.Pretty(w.formatKey), key.Pretty(w.formatKey)) + return w.err + case c == 0: + prevValue := w.rangeDelBlock.curValue + if w.compare(prevValue, value) != 0 { + w.err = errors.Errorf("pebble: overlapping tombstones must be fragmented: %s vs %s", + w.prettyTombstone(prevKey, prevValue), + w.prettyTombstone(key, value)) + return w.err + } + if prevKey.SeqNum() <= key.SeqNum() { + w.err = errors.Errorf("pebble: keys must be added in strictly increasing order: %s, %s", + prevKey.Pretty(w.formatKey), key.Pretty(w.formatKey)) + return w.err + } + default: + prevValue := w.rangeDelBlock.curValue + if w.compare(prevValue, key.UserKey) > 0 { + w.err = errors.Errorf("pebble: overlapping tombstones must be fragmented: %s vs %s", + w.prettyTombstone(prevKey, prevValue), + w.prettyTombstone(key, value)) + return w.err + } + } + } + + if key.Trailer == InternalKeyRangeDeleteSentinel { + w.err = errors.Errorf("pebble: cannot add range delete sentinel: %s", key.Pretty(w.formatKey)) + return w.err + } + + for i := range w.propCollectors { + if err := w.propCollectors[i].Add(key, value); err != nil { + w.err = err + return err + } + } + + w.meta.updateSeqNum(key.SeqNum()) + + switch { + case w.rangeDelV1Format: + // Range tombstones are not fragmented in the v1 (i.e. RocksDB) range + // deletion block format, so we need to track the largest range tombstone + // end key as every range tombstone is added. + // + // Note that writing the v1 format is only supported for tests. + if w.props.NumRangeDeletions == 0 { + w.meta.SetSmallestRangeDelKey(key.Clone()) + w.meta.SetLargestRangeDelKey(base.MakeRangeDeleteSentinelKey(value).Clone()) + } else { + if base.InternalCompare(w.compare, w.meta.SmallestRangeDel, key) > 0 { + w.meta.SetSmallestRangeDelKey(key.Clone()) + } + end := base.MakeRangeDeleteSentinelKey(value) + if base.InternalCompare(w.compare, w.meta.LargestRangeDel, end) < 0 { + w.meta.SetLargestRangeDelKey(end.Clone()) + } + } + + default: + // Range tombstones are fragmented in the v2 range deletion block format, + // so the start key of the first range tombstone added will be the smallest + // range tombstone key. The largest range tombstone key will be determined + // in Writer.Close() as the end key of the last range tombstone added. + if w.props.NumRangeDeletions == 0 { + w.meta.SetSmallestRangeDelKey(key.Clone()) + } + } + + w.props.NumEntries++ + w.props.NumDeletions++ + w.props.NumRangeDeletions++ + w.props.RawKeySize += uint64(key.Size()) + w.props.RawValueSize += uint64(len(value)) + w.rangeDelBlock.add(key, value) + return nil +} + +// RangeKeySet sets a range between start (inclusive) and end (exclusive) with +// the given suffix to the given value. The resulting range key is given the +// sequence number zero, with the expectation that the resulting sstable will be +// ingested. +// +// Keys must be added to the table in increasing order of start key. Spans are +// not required to be fragmented. The same suffix may not be set or unset twice +// over the same keyspan, because it would result in inconsistent state. Both +// the Set and Unset would share the zero sequence number, and a key cannot be +// both simultaneously set and unset. +func (w *Writer) RangeKeySet(start, end, suffix, value []byte) error { + return w.addRangeKeySpan(keyspan.Span{ + Start: w.tempRangeKeyCopy(start), + End: w.tempRangeKeyCopy(end), + Keys: []keyspan.Key{ + { + Trailer: base.MakeTrailer(0, base.InternalKeyKindRangeKeySet), + Suffix: w.tempRangeKeyCopy(suffix), + Value: w.tempRangeKeyCopy(value), + }, + }, + }) +} + +// RangeKeyUnset un-sets a range between start (inclusive) and end (exclusive) +// with the given suffix. The resulting range key is given the +// sequence number zero, with the expectation that the resulting sstable will be +// ingested. +// +// Keys must be added to the table in increasing order of start key. Spans are +// not required to be fragmented. The same suffix may not be set or unset twice +// over the same keyspan, because it would result in inconsistent state. Both +// the Set and Unset would share the zero sequence number, and a key cannot be +// both simultaneously set and unset. +func (w *Writer) RangeKeyUnset(start, end, suffix []byte) error { + return w.addRangeKeySpan(keyspan.Span{ + Start: w.tempRangeKeyCopy(start), + End: w.tempRangeKeyCopy(end), + Keys: []keyspan.Key{ + { + Trailer: base.MakeTrailer(0, base.InternalKeyKindRangeKeyUnset), + Suffix: w.tempRangeKeyCopy(suffix), + }, + }, + }) +} + +// RangeKeyDelete deletes a range between start (inclusive) and end (exclusive). +// +// Keys must be added to the table in increasing order of start key. Spans are +// not required to be fragmented. +func (w *Writer) RangeKeyDelete(start, end []byte) error { + return w.addRangeKeySpan(keyspan.Span{ + Start: w.tempRangeKeyCopy(start), + End: w.tempRangeKeyCopy(end), + Keys: []keyspan.Key{ + {Trailer: base.MakeTrailer(0, base.InternalKeyKindRangeKeyDelete)}, + }, + }) +} + +// AddRangeKey adds a range key set, unset, or delete key/value pair to the +// table being written. +// +// Range keys must be supplied in strictly ascending order of start key (i.e. +// user key ascending, sequence number descending, and key type descending). +// Ranges added must also be supplied in fragmented span order - i.e. other than +// spans that are perfectly aligned (same start and end keys), spans may not +// overlap. Range keys may be added out of order relative to point keys and +// range deletions. +func (w *Writer) AddRangeKey(key InternalKey, value []byte) error { + if w.err != nil { + return w.err + } + return w.addRangeKey(key, value) +} + +func (w *Writer) addRangeKeySpan(span keyspan.Span) error { + if w.compare(span.Start, span.End) >= 0 { + return errors.Errorf( + "pebble: start key must be strictly less than end key", + ) + } + if w.fragmenter.Start() != nil && w.compare(w.fragmenter.Start(), span.Start) > 0 { + return errors.Errorf("pebble: spans must be added in order: %s > %s", + w.formatKey(w.fragmenter.Start()), w.formatKey(span.Start)) + } + // Add this span to the fragmenter. + w.fragmenter.Add(span) + return w.err +} + +func (w *Writer) encodeRangeKeySpan(span keyspan.Span) { + // This method is the emit function of the Fragmenter. + // + // NB: The span should only contain range keys and be internally consistent + // (eg, no duplicate suffixes, no additional keys after a RANGEKEYDEL). + // + // We use w.rangeKeysBySuffix and w.rangeKeySpan to avoid allocations. + + // Sort the keys by suffix. Iteration doesn't *currently* depend on it, but + // we may want to in the future. + w.rangeKeysBySuffix.Cmp = w.compare + w.rangeKeysBySuffix.Keys = span.Keys + sort.Sort(&w.rangeKeysBySuffix) + + w.rangeKeySpan = span + w.rangeKeySpan.Keys = w.rangeKeysBySuffix.Keys + w.err = firstError(w.err, w.rangeKeyEncoder.Encode(&w.rangeKeySpan)) +} + +func (w *Writer) addRangeKey(key InternalKey, value []byte) error { + if !w.disableKeyOrderChecks && w.rangeKeyBlock.nEntries > 0 { + 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 + // key and value when it was first added to the range key block. + panic(errors.Errorf("pebble: invalid end key for span: %s", + prevStartKey.Pretty(w.formatKey))) + } + + curStartKey := key + curEndKey, _, ok := rangekey.DecodeEndKey(curStartKey.Kind(), value) + if !ok { + w.err = errors.Errorf("pebble: invalid end key for span: %s", + curStartKey.Pretty(w.formatKey)) + return w.err + } + + // Start keys must be strictly increasing. + if base.InternalCompare(w.compare, prevStartKey, curStartKey) >= 0 { + w.err = errors.Errorf( + "pebble: range keys starts must be added in increasing order: %s, %s", + prevStartKey.Pretty(w.formatKey), key.Pretty(w.formatKey)) + return w.err + } + + // Start keys are increasing. If the start user keys are equal, the + // end keys must be equal (i.e. aligned spans). + if w.compare(prevStartKey.UserKey, curStartKey.UserKey) == 0 { + if w.compare(prevEndKey, curEndKey) != 0 { + w.err = errors.Errorf("pebble: overlapping range keys must be fragmented: %s, %s", + prevStartKey.Pretty(w.formatKey), + curStartKey.Pretty(w.formatKey)) + return w.err + } + } else if w.compare(prevEndKey, curStartKey.UserKey) > 0 { + // If the start user keys are NOT equal, the spans must be disjoint (i.e. + // no overlap). + // NOTE: the inequality excludes zero, as we allow the end key of the + // lower span be the same as the start key of the upper span, because + // the range end key is considered an exclusive bound. + w.err = errors.Errorf("pebble: overlapping range keys must be fragmented: %s, %s", + prevStartKey.Pretty(w.formatKey), + curStartKey.Pretty(w.formatKey)) + return w.err + } + } + + // TODO(travers): Add an invariant-gated check to ensure that suffix-values + // are sorted within coalesced spans. + + // Range-keys and point-keys are intended to live in "parallel" keyspaces. + // However, we track a single seqnum in the table metadata that spans both of + // these keyspaces. + // TODO(travers): Consider tracking range key seqnums separately. + w.meta.updateSeqNum(key.SeqNum()) + + // Range tombstones are fragmented, so the start key of the first range key + // added will be the smallest. The largest range key is determined in + // Writer.Close() as the end key of the last range key added to the block. + if w.props.NumRangeKeys() == 0 { + w.meta.SetSmallestRangeKey(key.Clone()) + } + + // Update block properties. + w.props.RawRangeKeyKeySize += uint64(key.Size()) + w.props.RawRangeKeyValueSize += uint64(len(value)) + switch key.Kind() { + case base.InternalKeyKindRangeKeyDelete: + w.props.NumRangeKeyDels++ + case base.InternalKeyKindRangeKeySet: + w.props.NumRangeKeySets++ + case base.InternalKeyKindRangeKeyUnset: + w.props.NumRangeKeyUnsets++ + default: + panic(errors.Errorf("pebble: invalid range key type: %s", key.Kind())) + } + + for i := range w.blockPropCollectors { + if err := w.blockPropCollectors[i].Add(key, value); err != nil { + return err + } + } + + // Add the key to the block. + w.rangeKeyBlock.add(key, value) + return nil +} + +// tempRangeKeyBuf returns a slice of length n from the Writer's rkBuf byte +// slice. Any byte written to the returned slice is retained for the lifetime of +// the Writer. +func (w *Writer) tempRangeKeyBuf(n int) []byte { + if cap(w.rkBuf)-len(w.rkBuf) < n { + size := len(w.rkBuf) + 2*n + if size < 2*cap(w.rkBuf) { + size = 2 * cap(w.rkBuf) + } + buf := make([]byte, len(w.rkBuf), size) + copy(buf, w.rkBuf) + w.rkBuf = buf + } + b := w.rkBuf[len(w.rkBuf) : len(w.rkBuf)+n] + w.rkBuf = w.rkBuf[:len(w.rkBuf)+n] + return b +} + +// tempRangeKeyCopy returns a copy of the provided slice, stored in the Writer's +// range key buffer. +func (w *Writer) tempRangeKeyCopy(k []byte) []byte { + if len(k) == 0 { + return nil + } + buf := w.tempRangeKeyBuf(len(k)) + copy(buf, k) + return buf +} + +func (w *Writer) maybeAddToFilter(key []byte) { + if w.filter != nil { + if w.split != nil { + prefix := key[:w.split(key)] + w.filter.addKey(prefix) + } else { + w.filter.addKey(key) + } + } +} + +func (w *Writer) flush(key InternalKey) error { + // We're finishing a data block. + err := w.finishDataBlockProps(w.dataBlockBuf) + if err != nil { + return err + } + w.dataBlockBuf.finish() + w.dataBlockBuf.compressAndChecksum(w.compression) + // Since dataBlockEstimates.addInflightDataBlock was never called, the + // inflightSize is set to 0. + w.coordination.sizeEstimate.dataBlockCompressed(len(w.dataBlockBuf.compressed), 0) + + // Determine if the index block should be flushed. Since we're accessing the + // dataBlockBuf.dataBlock.curKey here, we have to make sure that once we start + // to pool the dataBlockBufs, the curKey isn't used by the Writer once the + // 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 := 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. + // When we determine that an index block should be flushed, we need to call + // BlockPropertyCollector.FinishIndexBlock. But block property collector + // calls must happen sequentially from the Writer client. Therefore, we need + // to determine that we are going to flush the index block from the Writer + // client. + shouldFlushIndexBlock := supportsTwoLevelIndex(w.tableFormat) && w.indexBlock.shouldFlush( + sep, encodedBHPEstimatedSize, w.indexBlockSize, w.indexBlockSizeThreshold, + ) + + var indexProps []byte + var flushableIndexBlock *indexBlockBuf + if shouldFlushIndexBlock { + flushableIndexBlock = w.indexBlock + w.indexBlock = newIndexBlockBuf(w.coordination.parallelismEnabled) + // Call BlockPropertyCollector.FinishIndexBlock, since we've decided to + // flush the index block. + indexProps, err = w.finishIndexBlockProps() + if err != nil { + return err + } + } + + // We've called BlockPropertyCollector.FinishDataBlock, and, if necessary, + // BlockPropertyCollector.FinishIndexBlock. Since we've decided to finish + // the data block, we can call + // BlockPropertyCollector.AddPrevDataBlockToIndexBlock. + w.addPrevDataBlockToIndexBlockProps() + + // Schedule a write. + writeTask := writeTaskPool.Get().(*writeTask) + // We're setting compressionDone to indicate that compression of this block + // has already been completed. + writeTask.compressionDone <- true + writeTask.buf = w.dataBlockBuf + writeTask.indexEntrySep = sep + writeTask.currIndexBlock = w.indexBlock + writeTask.indexInflightSize = sep.Size() + encodedBHPEstimatedSize + writeTask.finishedIndexProps = indexProps + writeTask.flushableIndexBlock = flushableIndexBlock + + // The writeTask corresponds to an unwritten index entry. + w.indexBlock.addInflight(writeTask.indexInflightSize) + + w.dataBlockBuf = nil + if w.coordination.parallelismEnabled { + w.coordination.writeQueue.add(writeTask) + } else { + err = w.coordination.writeQueue.addSync(writeTask) + } + w.dataBlockBuf = newDataBlockBuf(w.restartInterval, w.checksumType) + + return err +} + +func (w *Writer) maybeFlush(key InternalKey, valueLen int) error { + if !w.dataBlockBuf.shouldFlush(key, valueLen, w.blockSize, w.blockSizeThreshold) { + return nil + } + + err := w.flush(key) + + if err != nil { + w.err = err + return err + } + + return nil +} + +// dataBlockBuf.dataBlockProps set by this method must be encoded before any future use of the +// dataBlockBuf.blockPropsEncoder, since the properties slice will get reused by the +// blockPropsEncoder. +func (w *Writer) finishDataBlockProps(buf *dataBlockBuf) error { + if len(w.blockPropCollectors) == 0 { + return nil + } + var err error + buf.blockPropsEncoder.resetProps() + for i := range w.blockPropCollectors { + scratch := buf.blockPropsEncoder.getScratchForProp() + if scratch, err = w.blockPropCollectors[i].FinishDataBlock(scratch); err != nil { + return err + } + if len(scratch) > 0 { + buf.blockPropsEncoder.addProp(shortID(i), scratch) + } + } + + buf.dataBlockProps = buf.blockPropsEncoder.unsafeProps() + return nil +} + +// The BlockHandleWithProperties returned by this method must be encoded before any future use of +// the Writer.blockPropsEncoder, since the properties slice will get reused by the blockPropsEncoder. +// maybeAddBlockPropertiesToBlockHandle should only be called if block is being written synchronously +// with the Writer client. +func (w *Writer) maybeAddBlockPropertiesToBlockHandle( + bh BlockHandle, +) (BlockHandleWithProperties, error) { + err := w.finishDataBlockProps(w.dataBlockBuf) + if err != nil { + return BlockHandleWithProperties{}, err + } + return BlockHandleWithProperties{BlockHandle: bh, Props: w.dataBlockBuf.dataBlockProps}, nil +} + +func (w *Writer) indexEntrySep(prevKey, key InternalKey, dataBlockBuf *dataBlockBuf) InternalKey { + // Make a rough guess that we want key-sized scratch to compute the separator. + if cap(dataBlockBuf.sepScratch) < key.Size() { + dataBlockBuf.sepScratch = make([]byte, 0, key.Size()*2) + } + + var sep InternalKey + if key.UserKey == nil && key.Trailer == 0 { + sep = prevKey.Successor(w.compare, w.successor, dataBlockBuf.sepScratch[:0]) + } else { + sep = prevKey.Separator(w.compare, w.separator, dataBlockBuf.sepScratch[:0], key) + } + return sep +} + +// addIndexEntry adds an index entry for the specified key and block handle. +// addIndexEntry can be called from both the Writer client goroutine, and the +// writeQueue goroutine. If the flushIndexBuf != nil, then the indexProps, as +// they're used when the index block is finished. +// +// Invariant: +// 1. addIndexEntry must not store references to the sep InternalKey, the tmp +// byte slice, bhp.Props. That is, these must be either deep copied or +// encoded. +// 2. addIndexEntry must not hold references to the flushIndexBuf, and the writeTo +// indexBlockBufs. +func (w *Writer) addIndexEntry( + sep InternalKey, + bhp BlockHandleWithProperties, + tmp []byte, + flushIndexBuf *indexBlockBuf, + writeTo *indexBlockBuf, + inflightSize int, + indexProps []byte, +) error { + if bhp.Length == 0 { + // A valid blockHandle must be non-zero. + // In particular, it must have a non-zero length. + return nil + } + + encoded := encodeBlockHandleWithProperties(tmp, bhp) + + if flushIndexBuf != nil { + if cap(w.indexPartitions) == 0 { + w.indexPartitions = make([]indexBlockAndBlockProperties, 0, 32) + } + // Enable two level indexes if there is more than one index block. + w.twoLevelIndex = true + if err := w.finishIndexBlock(flushIndexBuf, indexProps); err != nil { + return err + } + } + + writeTo.add(sep, encoded, inflightSize) + return nil +} + +func (w *Writer) addPrevDataBlockToIndexBlockProps() { + for i := range w.blockPropCollectors { + w.blockPropCollectors[i].AddPrevDataBlockToIndexBlock() + } +} + +// addIndexEntrySync adds an index entry for the specified key and block handle. +// Writer.addIndexEntry is only called synchronously once Writer.Close is called. +// addIndexEntrySync should only be called if we're sure that index entries +// aren't being written asynchronously. +// +// Invariant: +// 1. addIndexEntrySync must not store references to the prevKey, key InternalKey's, +// the tmp byte slice. That is, these must be either deep copied or encoded. +func (w *Writer) addIndexEntrySync( + prevKey, key InternalKey, bhp BlockHandleWithProperties, tmp []byte, +) error { + sep := w.indexEntrySep(prevKey, key, w.dataBlockBuf) + shouldFlush := supportsTwoLevelIndex( + w.tableFormat) && w.indexBlock.shouldFlush( + sep, encodedBHPEstimatedSize, w.indexBlockSize, w.indexBlockSizeThreshold, + ) + var flushableIndexBlock *indexBlockBuf + var props []byte + var err error + if shouldFlush { + flushableIndexBlock = w.indexBlock + w.indexBlock = newIndexBlockBuf(w.coordination.parallelismEnabled) + + // Call BlockPropertyCollector.FinishIndexBlock, since we've decided to + // flush the index block. + props, err = w.finishIndexBlockProps() + if err != nil { + return err + } + } + + err = w.addIndexEntry(sep, bhp, tmp, flushableIndexBlock, w.indexBlock, 0, props) + if flushableIndexBlock != nil { + flushableIndexBlock.clear() + indexBlockBufPool.Put(flushableIndexBlock) + } + w.addPrevDataBlockToIndexBlockProps() + return err +} + +func shouldFlush( + key InternalKey, + valueLen int, + restartInterval, estimatedBlockSize, numEntries, targetBlockSize, sizeThreshold int, +) bool { + if numEntries == 0 { + return false + } + + if estimatedBlockSize >= targetBlockSize { + return true + } + + // The block is currently smaller than the target size. + if estimatedBlockSize <= sizeThreshold { + // The block is smaller than the threshold size at which we'll consider + // flushing it. + return false + } + + newSize := estimatedBlockSize + key.Size() + valueLen + if numEntries%restartInterval == 0 { + newSize += 4 + } + newSize += 4 // varint for shared prefix length + newSize += uvarintLen(uint32(key.Size())) // varint for unshared key bytes + newSize += uvarintLen(uint32(valueLen)) // varint for value size + // Flush if the block plus the new entry is larger than the target size. + return newSize > targetBlockSize +} + +func cloneKeyWithBuf(k InternalKey, a bytealloc.A) (bytealloc.A, InternalKey) { + if len(k.UserKey) == 0 { + return a, k + } + a, keyCopy := a.Copy(k.UserKey) + return a, InternalKey{UserKey: keyCopy, Trailer: k.Trailer} +} + +// Invariants: The byte slice returned by finishIndexBlockProps is heap-allocated +// +// and has its own lifetime, independent of the Writer and the blockPropsEncoder, +// +// and it is safe to: +// 1. Reuse w.blockPropsEncoder without first encoding the byte slice returned. +// 2. Store the byte slice in the Writer since it is a copy and not supported by +// an underlying buffer. +func (w *Writer) finishIndexBlockProps() ([]byte, error) { + w.blockPropsEncoder.resetProps() + for i := range w.blockPropCollectors { + scratch := w.blockPropsEncoder.getScratchForProp() + var err error + if scratch, err = w.blockPropCollectors[i].FinishIndexBlock(scratch); err != nil { + return nil, err + } + if len(scratch) > 0 { + w.blockPropsEncoder.addProp(shortID(i), scratch) + } + } + return w.blockPropsEncoder.props(), nil +} + +// finishIndexBlock finishes the current index block and adds it to the top +// level index block. This is only used when two level indexes are enabled. +// +// Invariants: +// 1. The props slice passed into finishedIndexBlock must not be a +// owned by any other struct, since it will be stored in the Writer.indexPartitions +// slice. +// 2. None of the buffers owned by indexBuf will be shallow copied and stored elsewhere. +// That is, it must be safe to reuse indexBuf after finishIndexBlock has been called. +func (w *Writer) finishIndexBlock(indexBuf *indexBlockBuf, props []byte) error { + part := indexBlockAndBlockProperties{ + nEntries: indexBuf.block.nEntries, properties: props, + } + w.indexSepAlloc, part.sep = cloneKeyWithBuf( + indexBuf.block.getCurKey(), w.indexSepAlloc, + ) + bk := indexBuf.finish() + if len(w.indexBlockAlloc) < len(bk) { + // Allocate enough bytes for approximately 16 index blocks. + w.indexBlockAlloc = make([]byte, len(bk)*16) + } + n := copy(w.indexBlockAlloc, bk) + part.block = w.indexBlockAlloc[:n:n] + w.indexBlockAlloc = w.indexBlockAlloc[n:] + w.indexPartitions = append(w.indexPartitions, part) + return nil +} + +func (w *Writer) writeTwoLevelIndex() (BlockHandle, error) { + props, err := w.finishIndexBlockProps() + if err != nil { + return BlockHandle{}, err + } + // Add the final unfinished index. + if err = w.finishIndexBlock(w.indexBlock, props); err != nil { + return BlockHandle{}, err + } + + for i := range w.indexPartitions { + b := &w.indexPartitions[i] + w.props.NumDataBlocks += uint64(b.nEntries) + + data := b.block + w.props.IndexSize += uint64(len(data)) + bh, err := w.writeBlock(data, w.compression, &w.blockBuf) + if err != nil { + return BlockHandle{}, err + } + bhp := BlockHandleWithProperties{ + BlockHandle: bh, + Props: b.properties, + } + encoded := encodeBlockHandleWithProperties(w.blockBuf.tmp[:], bhp) + w.topLevelIndexBlock.add(b.sep, encoded) + } + + // NB: RocksDB includes the block trailer length in the index size + // property, though it doesn't include the trailer in the top level + // index size property. + w.props.IndexPartitions = uint64(len(w.indexPartitions)) + w.props.TopLevelIndexSize = uint64(w.topLevelIndexBlock.estimatedSize()) + w.props.IndexSize += w.props.TopLevelIndexSize + blockTrailerLen + + return w.writeBlock(w.topLevelIndexBlock.finish(), w.compression, &w.blockBuf) +} + +func compressAndChecksum(b []byte, compression Compression, blockBuf *blockBuf) []byte { + // Compress the buffer, discarding the result if the improvement isn't at + // least 12.5%. + blockType, compressed := compressBlock(compression, b, blockBuf.compressedBuf) + if blockType != noCompressionBlockType && cap(compressed) > cap(blockBuf.compressedBuf) { + blockBuf.compressedBuf = compressed[:cap(compressed)] + } + if len(compressed) < len(b)-len(b)/8 { + b = compressed + } else { + blockType = noCompressionBlockType + } + + blockBuf.tmp[0] = byte(blockType) + + // Calculate the checksum. + checksum := blockBuf.checksummer.checksum(b, blockBuf.tmp[:1]) + binary.LittleEndian.PutUint32(blockBuf.tmp[1:5], checksum) + return b +} + +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.FileNum() != 0 { + // Remove the block being written from the cache. This provides defense in + // depth against bugs which cause cache collisions. + // + // TODO(peter): Alternatively, we could add the uncompressed value to the + // cache. + w.cache.Delete(w.cacheID, w.fileNum, bh.Offset) + } + + // Write the bytes to the file. + if err := w.writable.Write(block); err != nil { + return BlockHandle{}, err + } + w.meta.Size += uint64(len(block)) + if err := w.writable.Write(blockTrailerBuf[:blockTrailerLen]); err != nil { + return BlockHandle{}, err + } + w.meta.Size += blockTrailerLen + + return bh, nil +} + +// Write implements io.Writer. This is analogous to writeCompressedBlock for +// blocks that already incorporate the trailer, and don't need the callee to +// return a BlockHandle. +func (w *Writer) Write(blockWithTrailer []byte) (n int, err error) { + offset := w.meta.Size + 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. + // + // TODO(peter): Alternatively, we could add the uncompressed value to the + // cache. + w.cache.Delete(w.cacheID, w.fileNum, offset) + } + w.meta.Size += uint64(len(blockWithTrailer)) + if err := w.writable.Write(blockWithTrailer); err != nil { + return 0, err + } + return len(blockWithTrailer), nil +} + +func (w *Writer) writeBlock( + b []byte, compression Compression, blockBuf *blockBuf, +) (BlockHandle, error) { + b = compressAndChecksum(b, compression, blockBuf) + return w.writeCompressedBlock(b, blockBuf.tmp[:]) +} + +// assertFormatCompatibility ensures that the features present on the table are +// compatible with the table format version. +func (w *Writer) assertFormatCompatibility() error { + // PebbleDBv1: block properties. + if len(w.blockPropCollectors) > 0 && w.tableFormat < TableFormatPebblev1 { + return errors.Newf( + "table format version %s is less than the minimum required version %s for block properties", + w.tableFormat, TableFormatPebblev1, + ) + } + + // PebbleDBv2: range keys. + if w.props.NumRangeKeys() > 0 && w.tableFormat < TableFormatPebblev2 { + return errors.Newf( + "table format version %s is less than the minimum required version %s for range keys", + w.tableFormat, TableFormatPebblev2, + ) + } + + // 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 +} + +// Close finishes writing the table and closes the underlying file that the +// table was written to. +func (w *Writer) Close() (err error) { + defer func() { + if w.valueBlockWriter != nil { + releaseValueBlockWriter(w.valueBlockWriter) + // Defensive code in case Close gets called again. We don't want to put + // the same object to a sync.Pool. + w.valueBlockWriter = nil + } + if w.writable != nil { + w.writable.Abort() + w.writable = nil + } + // Record any error in the writer (so we can exit early if Close is called + // again). + if err != nil { + w.err = err + } + }() + + // finish must be called before we check for an error, because finish will + // block until every single task added to the writeQueue has been processed, + // and an error could be encountered while any of those tasks are processed. + if err := w.coordination.writeQueue.finish(); err != nil { + return err + } + + if w.err != nil { + return w.err + } + + // The w.meta.LargestPointKey is only used once the Writer is closed, so it is safe to set it + // when the Writer is closed. + // + // The following invariants ensure that setting the largest key at this point of a Writer close + // is correct: + // 1. Keys must only be added to the Writer in an increasing order. + // 2. The current w.dataBlockBuf is guaranteed to have the latest key added to the Writer. This + // must be true, because a w.dataBlockBuf is only switched out when a dataBlock is flushed, + // 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(w.dataBlockBuf.dataBlock.getCurKey().Clone()) + } + + // Finish the last data block, or force an empty data block if there + // aren't any data blocks at all. + if w.dataBlockBuf.dataBlock.nEntries > 0 || w.indexBlock.block.nEntries == 0 { + bh, err := w.writeBlock(w.dataBlockBuf.dataBlock.finish(), w.compression, &w.dataBlockBuf.blockBuf) + if err != nil { + return err + } + bhp, err := w.maybeAddBlockPropertiesToBlockHandle(bh) + if err != nil { + return err + } + prevKey := w.dataBlockBuf.dataBlock.getCurKey() + if err := w.addIndexEntrySync(prevKey, InternalKey{}, bhp, w.dataBlockBuf.tmp[:]); err != nil { + return err + } + } + w.props.DataSize = w.meta.Size + + // Write the filter block. + var metaindex rawBlockWriter + metaindex.restartInterval = 1 + if w.filter != nil { + b, err := w.filter.finish() + if err != nil { + return err + } + bh, err := w.writeBlock(b, NoCompression, &w.blockBuf) + if err != nil { + return err + } + n := encodeBlockHandle(w.blockBuf.tmp[:], bh) + metaindex.add(InternalKey{UserKey: []byte(w.filter.metaName())}, w.blockBuf.tmp[:n]) + w.props.FilterPolicyName = w.filter.policyName() + w.props.FilterSize = bh.Length + } + + var indexBH BlockHandle + if w.twoLevelIndex { + w.props.IndexType = twoLevelIndex + // Write the two level index block. + indexBH, err = w.writeTwoLevelIndex() + if err != nil { + return err + } + } else { + w.props.IndexType = binarySearchIndex + // NB: RocksDB includes the block trailer length in the index size + // property, though it doesn't include the trailer in the filter size + // property. + w.props.IndexSize = uint64(w.indexBlock.estimatedSize()) + blockTrailerLen + w.props.NumDataBlocks = uint64(w.indexBlock.block.nEntries) + + // Write the single level index block. + indexBH, err = w.writeBlock(w.indexBlock.finish(), w.compression, &w.blockBuf) + if err != nil { + return err + } + } + + // Write the range-del block. The block handle must added to the meta index block + // after the properties block has been written. This is because the entries in the + // metaindex block must be sorted by key. + var rangeDelBH BlockHandle + if w.props.NumRangeDeletions > 0 { + if !w.rangeDelV1Format { + // Because the range tombstones are fragmented in the v2 format, the end + // key of the last added range tombstone will be the largest range + // tombstone key. Note that we need to make this into a range deletion + // sentinel because sstable boundaries are inclusive while the end key of + // a range deletion tombstone is exclusive. A Clone() is necessary as + // rangeDelBlock.curValue is the same slice that will get passed + // into w.writer, and some implementations of vfs.File mutate the + // slice passed into Write(). Also, w.meta will often outlive the + // blockWriter, and so cloning curValue allows the rangeDelBlock's + // internal buffer to get gc'd. + k := base.MakeRangeDeleteSentinelKey(w.rangeDelBlock.curValue).Clone() + w.meta.SetLargestRangeDelKey(k) + } + rangeDelBH, err = w.writeBlock(w.rangeDelBlock.finish(), NoCompression, &w.blockBuf) + if err != nil { + return err + } + } + + // Write the range-key block, flushing any remaining spans from the + // fragmenter first. + w.fragmenter.Finish() + + var rangeKeyBH BlockHandle + if w.props.NumRangeKeys() > 0 { + key := w.rangeKeyBlock.getCurKey() + kind := key.Kind() + endKey, _, ok := rangekey.DecodeEndKey(kind, w.rangeKeyBlock.curValue) + if !ok { + return errors.Newf("invalid end key: %s", w.rangeKeyBlock.curValue) + } + k := base.MakeExclusiveSentinelKey(kind, endKey).Clone() + w.meta.SetLargestRangeKey(k) + // TODO(travers): The lack of compression on the range key block matches the + // lack of compression on the range-del block. Revisit whether we want to + // enable compression on this block. + rangeKeyBH, err = w.writeBlock(w.rangeKeyBlock.finish(), NoCompression, &w.blockBuf) + if err != nil { + return err + } + } + + if w.valueBlockWriter != nil { + vbiHandle, vbStats, err := w.valueBlockWriter.finish(w, w.meta.Size) + if err != nil { + return err + } + w.props.NumValueBlocks = vbStats.numValueBlocks + w.props.NumValuesInValueBlocks = vbStats.numValuesInValueBlocks + w.props.ValueBlocksSize = vbStats.valueBlocksAndIndexSize + if vbStats.numValueBlocks > 0 { + n := encodeValueBlocksIndexHandle(w.blockBuf.tmp[:], vbiHandle) + metaindex.add(InternalKey{UserKey: []byte(metaValueIndexName)}, w.blockBuf.tmp[:n]) + } + } + + // Add the range key block handle to the metaindex block. Note that we add the + // block handle to the metaindex block before the other meta blocks as the + // metaindex block entries must be sorted, and the range key block name sorts + // before the other block names. + if w.props.NumRangeKeys() > 0 { + n := encodeBlockHandle(w.blockBuf.tmp[:], rangeKeyBH) + metaindex.add(InternalKey{UserKey: []byte(metaRangeKeyName)}, w.blockBuf.tmp[:n]) + } + + { + userProps := make(map[string]string) + for i := range w.propCollectors { + if err := w.propCollectors[i].Finish(userProps); err != nil { + return err + } + } + for i := range w.blockPropCollectors { + scratch := w.blockPropsEncoder.getScratchForProp() + // Place the shortID in the first byte. + scratch = append(scratch, byte(i)) + buf, err := w.blockPropCollectors[i].FinishTable(scratch) + if err != nil { + return err + } + var prop string + if len(buf) > 0 { + prop = string(buf) + } + // NB: The property is populated in the map even if it is the + // empty string, since the presence in the map is what indicates + // that the block property collector was used when writing. + userProps[w.blockPropCollectors[i].Name()] = prop + } + if len(userProps) > 0 { + w.props.UserProperties = userProps + } + + // Write the properties block. + var raw rawBlockWriter + // The restart interval is set to infinity because the properties block + // is always read sequentially and cached in a heap located object. This + // reduces table size without a significant impact on performance. + raw.restartInterval = propertiesBlockRestartInterval + w.props.CompressionOptions = rocksDBCompressionOptions + w.props.save(w.tableFormat, &raw) + bh, err := w.writeBlock(raw.finish(), NoCompression, &w.blockBuf) + if err != nil { + return err + } + n := encodeBlockHandle(w.blockBuf.tmp[:], bh) + metaindex.add(InternalKey{UserKey: []byte(metaPropertiesName)}, w.blockBuf.tmp[:n]) + } + + // Add the range deletion block handle to the metaindex block. + if w.props.NumRangeDeletions > 0 { + n := encodeBlockHandle(w.blockBuf.tmp[:], rangeDelBH) + // The v2 range-del block encoding is backwards compatible with the v1 + // encoding. We add meta-index entries for both the old name and the new + // name so that old code can continue to find the range-del block and new + // code knows that the range tombstones in the block are fragmented and + // sorted. + metaindex.add(InternalKey{UserKey: []byte(metaRangeDelName)}, w.blockBuf.tmp[:n]) + if !w.rangeDelV1Format { + metaindex.add(InternalKey{UserKey: []byte(metaRangeDelV2Name)}, w.blockBuf.tmp[:n]) + } + } + + // Write the metaindex block. It might be an empty block, if the filter + // policy is nil. NoCompression is specified because a) RocksDB never + // compresses the meta-index block and b) RocksDB has some code paths which + // expect the meta-index block to not be compressed. + metaindexBH, err := w.writeBlock(metaindex.blockWriter.finish(), NoCompression, &w.blockBuf) + if err != nil { + return err + } + + // Write the table footer. + footer := footer{ + format: w.tableFormat, + checksum: w.blockBuf.checksummer.checksumType, + metaindexBH: metaindexBH, + indexBH: indexBH, + } + encoded := footer.encode(w.blockBuf.tmp[:]) + if err := w.writable.Write(footer.encode(w.blockBuf.tmp[:])); err != nil { + return err + } + w.meta.Size += uint64(len(encoded)) + w.meta.Properties = w.props + + // Check that the features present in the table are compatible with the format + // configured for the table. + if err = w.assertFormatCompatibility(); err != nil { + return err + } + + if err := w.writable.Finish(); err != nil { + w.writable = nil + return err + } + w.writable = nil + + w.dataBlockBuf.clear() + dataBlockBufPool.Put(w.dataBlockBuf) + w.dataBlockBuf = nil + w.indexBlock.clear() + indexBlockBufPool.Put(w.indexBlock) + w.indexBlock = nil + + // Make any future calls to Set or Close return an error. + w.err = errWriterClosed + return nil +} + +// EstimatedSize returns the estimated size of the sstable being written if a +// call to Finish() was made without adding additional keys. +func (w *Writer) EstimatedSize() uint64 { + return w.coordination.sizeEstimate.size() + + uint64(w.dataBlockBuf.dataBlock.estimatedSize()) + + w.indexBlock.estimatedSize() +} + +// Metadata returns the metadata for the finished sstable. Only valid to call +// after the sstable has been finished. +func (w *Writer) Metadata() (*WriterMetadata, error) { + if w.writable != nil { + return nil, errors.New("pebble: writer is not closed") + } + return &w.meta, nil +} + +// WriterOption provide an interface to do work on Writer while it is being +// opened. +type WriterOption interface { + // writerApply is called on the writer during opening in order to set + // internal parameters. + writerApply(*Writer) +} + +// PreviousPointKeyOpt is a WriterOption that provides access to the last +// point key written to the writer while building a sstable. +type PreviousPointKeyOpt struct { + w *Writer +} + +// UnsafeKey returns the last point key written to the writer to which this +// option was passed during creation. The returned key points directly into +// a buffer belonging to the Writer. The value's lifetime ends the next time a +// point key is added to the Writer. +// Invariant: UnsafeKey isn't and shouldn't be called after the Writer is closed. +func (o PreviousPointKeyOpt) UnsafeKey() base.InternalKey { + if o.w == nil { + return base.InvalidInternalKey + } + + 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 o.w.dataBlockBuf.dataBlock.getCurKey() + } + return base.InternalKey{} +} + +func (o *PreviousPointKeyOpt) writerApply(w *Writer) { + o.w = w +} + +// NewWriter returns a new table writer for the file. Closing the writer will +// close the file. +func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...WriterOption) *Writer { + o = o.ensureDefaults() + w := &Writer{ + writable: writable, + meta: WriterMetadata{ + SmallestSeqNum: math.MaxUint64, + }, + blockSize: o.BlockSize, + blockSizeThreshold: (o.BlockSize*o.BlockSizeThreshold + 99) / 100, + indexBlockSize: o.IndexBlockSize, + indexBlockSizeThreshold: (o.IndexBlockSize*o.BlockSizeThreshold + 99) / 100, + compare: o.Comparer.Compare, + split: o.Comparer.Split, + formatKey: o.Comparer.FormatKey, + compression: o.Compression, + 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, + indexBlock: newIndexBlockBuf(o.Parallelism), + rangeDelBlock: blockWriter{ + restartInterval: 1, + }, + rangeKeyBlock: blockWriter{ + restartInterval: 1, + }, + topLevelIndexBlock: blockWriter{ + restartInterval: 1, + }, + fragmenter: keyspan.Fragmenter{ + Cmp: o.Comparer.Compare, + Format: o.Comparer.FormatKey, + }, + } + if w.tableFormat >= TableFormatPebblev3 { + w.shortAttributeExtractor = o.ShortAttributeExtractor + w.requiredInPlaceValueBound = o.RequiredInPlaceValueBound + w.valueBlockWriter = newValueBlockWriter( + w.blockSize, w.blockSizeThreshold, w.compression, w.checksumType, func(compressedSize int) { + w.coordination.sizeEstimate.dataBlockCompressed(compressedSize, 0) + }) + } + + w.dataBlockBuf = newDataBlockBuf(w.restartInterval, w.checksumType) + + w.blockBuf = blockBuf{ + checksummer: checksummer{checksumType: o.Checksum}, + } + + w.coordination.init(o.Parallelism, w) + + if writable == nil { + w.err = errors.New("pebble: nil writable") + return w + } + + // Note that WriterOptions are applied in two places; the ones with a + // preApply() method are applied here. The rest are applied down below after + // default properties are set. + type preApply interface{ preApply() } + for _, opt := range extraOpts { + if _, ok := opt.(preApply); ok { + opt.writerApply(w) + } + } + + w.props.PrefixExtractorName = "nullptr" + if o.FilterPolicy != nil { + switch o.FilterType { + case TableFilter: + w.filter = newTableFilterWriter(o.FilterPolicy) + if w.split != nil { + w.props.PrefixExtractorName = o.Comparer.Name + w.props.PrefixFiltering = true + } else { + w.props.WholeKeyFiltering = true + } + default: + panic(fmt.Sprintf("unknown filter type: %v", o.FilterType)) + } + } + + 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 || + w.tableFormat >= TableFormatPebblev4 { + var buf bytes.Buffer + buf.WriteString("[") + if len(o.TablePropertyCollectors) > 0 { + w.propCollectors = make([]TablePropertyCollector, len(o.TablePropertyCollectors)) + for i := range o.TablePropertyCollectors { + w.propCollectors[i] = o.TablePropertyCollectors[i]() + if i > 0 { + buf.WriteString(",") + } + 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 { + // The shortID assigned to a collector is the same as its index in + // this slice. + for i := range o.BlockPropertyCollectors { + w.blockPropCollectors[i] = o.BlockPropertyCollectors[i]() + if i > 0 || len(o.TablePropertyCollectors) > 0 { + buf.WriteString(",") + } + 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() + } + + // Apply the remaining WriterOptions that do not have a preApply() method. + for _, opt := range extraOpts { + if _, ok := opt.(preApply); ok { + continue + } + opt.writerApply(w) + } + + // Initialize the range key fragmenter and encoder. + w.fragmenter.Emit = w.encodeRangeKeySpan + w.rangeKeyEncoder.Emit = w.addRangeKey + return w +} + +// internalGetProperties is a private, internal-use-only function that takes a +// Writer and returns a pointer to its Properties, allowing direct mutation. +// It's used by internal Pebble flushes and compactions to set internal +// properties. It gets installed in private. +func internalGetProperties(w *Writer) *Properties { + return &w.props +} + +func init() { + private.SSTableWriterDisableKeyOrderChecks = func(i interface{}) { + w := i.(*Writer) + w.disableKeyOrderChecks = true + } + 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/pebble/sstable/writer_fixture_test.go b/pebble/sstable/writer_fixture_test.go new file mode 100644 index 0000000..12932ed --- /dev/null +++ b/pebble/sstable/writer_fixture_test.go @@ -0,0 +1,191 @@ +// Copyright 2019 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 sstable + +import ( + "bytes" + "fmt" + "math" + "os" + "path/filepath" + "testing" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/stretchr/testify/require" +) + +const ( + noPrefixFilter = false + prefixFilter = true + + noFullKeyBloom = false + fullKeyBloom = true + + defaultIndexBlockSize = math.MaxInt32 + smallIndexBlockSize = 128 +) + +type keyCountPropertyCollector struct { + count int +} + +func (c *keyCountPropertyCollector) Add(key InternalKey, value []byte) error { + c.count++ + return nil +} + +func (c *keyCountPropertyCollector) Finish(userProps map[string]string) error { + userProps["test.key-count"] = fmt.Sprint(c.count) + return nil +} + +func (c *keyCountPropertyCollector) Name() string { + return "KeyCountPropertyCollector" +} + +var fixtureComparer = func() *Comparer { + c := *base.DefaultComparer + // NB: this is named as such only to match the built-in RocksDB comparer. + c.Name = "leveldb.BytewiseComparator" + c.Split = func(a []byte) int { + // TODO(tbg): this matches logic in testdata/make-table.cc. It's + // difficult to provide a more meaningful prefix extractor on the given + // dataset since it's not MVCC, and so it's impossible to come up with a + // sensible one. We need to add a better dataset and use that instead to + // get confidence that prefix extractors are working as intended. + return len(a) + } + return &c +}() + +type fixtureOpts struct { + compression Compression + fullKeyFilter bool + prefixFilter bool + indexBlockSize int +} + +func (o fixtureOpts) String() string { + return fmt.Sprintf( + "compression=%s,fullKeyFilter=%t,prefixFilter=%t", + o.compression, o.fullKeyFilter, o.prefixFilter, + ) +} + +var fixtures = map[fixtureOpts]struct { + filename string + comparer *Comparer + propCollector func() TablePropertyCollector +}{ + {SnappyCompression, noFullKeyBloom, noPrefixFilter, defaultIndexBlockSize}: { + "testdata/h.sst", nil, + func() TablePropertyCollector { + return &keyCountPropertyCollector{} + }, + }, + {SnappyCompression, fullKeyBloom, noPrefixFilter, defaultIndexBlockSize}: { + "testdata/h.table-bloom.sst", nil, nil, + }, + {NoCompression, noFullKeyBloom, noPrefixFilter, defaultIndexBlockSize}: { + "testdata/h.no-compression.sst", nil, + func() TablePropertyCollector { + return &keyCountPropertyCollector{} + }, + }, + {NoCompression, fullKeyBloom, noPrefixFilter, defaultIndexBlockSize}: { + "testdata/h.table-bloom.no-compression.sst", nil, nil, + }, + {NoCompression, noFullKeyBloom, prefixFilter, defaultIndexBlockSize}: { + "testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst", + fixtureComparer, nil, + }, + {NoCompression, noFullKeyBloom, noPrefixFilter, smallIndexBlockSize}: { + "testdata/h.no-compression.two_level_index.sst", nil, + func() TablePropertyCollector { + return &keyCountPropertyCollector{} + }, + }, + {ZstdCompression, noFullKeyBloom, noPrefixFilter, defaultIndexBlockSize}: { + "testdata/h.zstd-compression.sst", nil, + func() TablePropertyCollector { + return &keyCountPropertyCollector{} + }, + }, +} + +func runTestFixtureOutput(opts fixtureOpts) error { + fixture, ok := fixtures[opts] + if !ok { + return errors.Errorf("fixture missing: %+v", opts) + } + + compression := opts.compression + + var fp base.FilterPolicy + if opts.fullKeyFilter || opts.prefixFilter { + fp = bloom.FilterPolicy(10) + } + ftype := base.TableFilter + + // Check that a freshly made table is byte-for-byte equal to a pre-made + // table. + want, err := os.ReadFile(filepath.FromSlash(fixture.filename)) + if err != nil { + return err + } + + f, err := build(compression, fp, ftype, fixture.comparer, fixture.propCollector, 2048, opts.indexBlockSize) + if err != nil { + return err + } + stat, err := f.Stat() + if err != nil { + return err + } + got := make([]byte, stat.Size()) + _, err = f.ReadAt(got, 0) + if err != nil { + return err + } + + if !bytes.Equal(got, want) { + i := 0 + for ; i < len(got) && i < len(want) && got[i] == want[i]; i++ { + } + os.WriteFile("fail.txt", got, 0644) + return errors.Errorf("built table %s does not match pre-made table. From byte %d onwards,\ngot:\n% x\nwant:\n% x", + fixture.filename, i, got[i:], want[i:]) + } + return nil +} + +func TestFixtureOutput(t *testing.T) { + for opt := range fixtures { + // Note: we disabled the zstd fixture test when CGO_ENABLED=0, because the + // implementation between DataDog/zstd and klauspost/compress are + // different, which leads to different compression output + // . + // Since the fixture test requires bit-to-bit reproducibility, we cannot + // run the zstd test when the implementation is not based on facebook/zstd. + if !useStandardZstdLib && opt.compression == ZstdCompression { + continue + } + t.Run(opt.String(), func(t *testing.T) { + require.NoError(t, runTestFixtureOutput(opt)) + }) + } +} diff --git a/pebble/sstable/writer_rangekey_test.go b/pebble/sstable/writer_rangekey_test.go new file mode 100644 index 0000000..bbe6946 --- /dev/null +++ b/pebble/sstable/writer_rangekey_test.go @@ -0,0 +1,124 @@ +package sstable + +import ( + "bytes" + "crypto/rand" + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestWriter_RangeKeys(t *testing.T) { + var r *Reader + defer func() { + if r != nil { + require.NoError(t, r.Close()) + } + }() + + buildFn := func(td *datadriven.TestData) (*Reader, error) { + mem := vfs.NewMem() + f, err := mem.Create("test") + if err != nil { + return nil, err + } + + // Use a "suffix-aware" Comparer, that will sort suffix-values in + // descending order of timestamp, rather than in lexical order. + cmp := testkeys.Comparer + w := NewWriter(objstorageprovider.NewFileWritable(f), WriterOptions{ + Comparer: cmp, + TableFormat: TableFormatPebblev2, + }) + for _, data := range strings.Split(td.Input, "\n") { + // Format. One of: + // - SET $START-$END $SUFFIX=$VALUE + // - UNSET $START-$END $SUFFIX + // - DEL $START-$END + parts := strings.Split(data, " ") + kind, startEnd := parts[0], parts[1] + + startEndSplit := bytes.Split([]byte(startEnd), []byte("-")) + + var start, end, suffix, value []byte + start, end = startEndSplit[0], startEndSplit[1] + + switch kind { + case "SET": + sv := bytes.Split([]byte(parts[2]), []byte("=")) + suffix, value = sv[0], sv[1] + err = w.RangeKeySet(start, end, suffix, value) + case "UNSET": + suffix = []byte(parts[2]) + err = w.RangeKeyUnset(start, end, suffix) + case "DEL": + err = w.RangeKeyDelete(start, end) + default: + return nil, errors.Newf("unexpected key kind: %s", kind) + } + if err != nil { + return nil, err + } + + // Scramble the bytes in each of the input arrays. This helps with + // flushing out subtle bugs due to byte slice re-use. + for _, slice := range [][]byte{start, end, suffix, value} { + _, _ = rand.Read(slice) + } + } + + if err = w.Close(); err != nil { + return nil, err + } + + f, err = mem.Open("test") + if err != nil { + return nil, err + } + + r, err = newReader(f, ReaderOptions{Comparer: cmp}) + if err != nil { + return nil, err + } + + return r, nil + } + + datadriven.RunTest(t, "testdata/writer_range_keys", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "build": + if r != nil { + _ = r.Close() + r = nil + } + + var err error + r, err = buildFn(td) + if err != nil { + return err.Error() + } + + iter, err := r.NewRawRangeKeyIter() + if err != nil { + return err.Error() + } + defer iter.Close() + + var buf bytes.Buffer + for s := iter.First(); s != nil; s = iter.Next() { + _, _ = fmt.Fprintf(&buf, "%s\n", s) + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} diff --git a/pebble/sstable/writer_test.go b/pebble/sstable/writer_test.go new file mode 100644 index 0000000..20f9e90 --- /dev/null +++ b/pebble/sstable/writer_test.go @@ -0,0 +1,1075 @@ +// Copyright 2018 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" + "encoding/binary" + "fmt" + "math/rand" + "strconv" + "strings" + "sync" + "testing" + "unsafe" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func testWriterParallelism(t *testing.T, parallelism bool) { + for _, format := range []TableFormat{TableFormatPebblev2, TableFormatPebblev3} { + tdFile := "testdata/writer" + if format == TableFormatPebblev3 { + tdFile = "testdata/writer_v3" + } + t.Run(format.String(), func(t *testing.T) { runDataDriven(t, tdFile, format, parallelism) }) + } +} +func TestWriter(t *testing.T) { + testWriterParallelism(t, false) +} + +func testRewriterParallelism(t *testing.T, parallelism bool) { + for _, format := range []TableFormat{TableFormatPebblev2, TableFormatPebblev3} { + tdFile := "testdata/rewriter" + if format == TableFormatPebblev3 { + tdFile = "testdata/rewriter_v3" + } + t.Run(format.String(), func(t *testing.T) { runDataDriven(t, tdFile, format, parallelism) }) + } +} + +func TestRewriter(t *testing.T) { + testRewriterParallelism(t, false) +} + +func TestWriterParallel(t *testing.T) { + testWriterParallelism(t, true) +} + +func TestRewriterParallel(t *testing.T) { + testRewriterParallelism(t, true) +} + +func runDataDriven(t *testing.T, file string, tableFormat TableFormat, parallelism bool) { + var r *Reader + defer func() { + if r != nil { + require.NoError(t, r.Close()) + } + }() + + format := func(td *datadriven.TestData, m *WriterMetadata) string { + var requestedProps []string + for _, cmdArg := range td.CmdArgs { + switch cmdArg.Key { + case "props": + requestedProps = cmdArg.Vals + } + } + + var b bytes.Buffer + if m.HasPointKeys { + fmt.Fprintf(&b, "point: [%s-%s]\n", m.SmallestPoint, m.LargestPoint) + } + if m.HasRangeDelKeys { + fmt.Fprintf(&b, "rangedel: [%s-%s]\n", m.SmallestRangeDel, m.LargestRangeDel) + } + if m.HasRangeKeys { + fmt.Fprintf(&b, "rangekey: [%s-%s]\n", m.SmallestRangeKey, m.LargestRangeKey) + } + fmt.Fprintf(&b, "seqnums: [%d-%d]\n", m.SmallestSeqNum, m.LargestSeqNum) + + if len(requestedProps) > 0 { + props := strings.Split(r.Properties.String(), "\n") + for _, requestedProp := range requestedProps { + fmt.Fprintf(&b, "props %q:\n", requestedProp) + for _, prop := range props { + if strings.Contains(prop, requestedProp) { + fmt.Fprintf(&b, " %s\n", prop) + } + } + } + } + + return b.String() + } + + datadriven.RunTest(t, file, func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "build": + if r != nil { + _ = r.Close() + r = nil + } + var meta *WriterMetadata + var err error + meta, r, err = runBuildCmd(td, &WriterOptions{ + TableFormat: tableFormat, + Parallelism: parallelism, + }, 0) + if err != nil { + return err.Error() + } + return format(td, meta) + + case "build-raw": + if r != nil { + _ = r.Close() + r = nil + } + var meta *WriterMetadata + var err error + meta, r, err = runBuildRawCmd(td, &WriterOptions{ + TableFormat: tableFormat, + }) + if err != nil { + return err.Error() + } + return format(td, meta) + + case "scan": + origIter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return err.Error() + } + iter := newIterAdapter(origIter) + defer iter.Close() + + var buf bytes.Buffer + for valid := iter.First(); valid; valid = iter.Next() { + fmt.Fprintf(&buf, "%s:%s\n", iter.Key(), iter.Value()) + } + return buf.String() + + case "get": + var buf bytes.Buffer + for _, k := range strings.Split(td.Input, "\n") { + value, err := r.get([]byte(k)) + if err != nil { + fmt.Fprintf(&buf, "get %s: %s\n", k, err.Error()) + } else { + fmt.Fprintf(&buf, "%s\n", value) + } + } + return buf.String() + + case "scan-range-del": + iter, err := r.NewRawRangeDelIter() + if err != nil { + return err.Error() + } + if iter == nil { + return "" + } + defer iter.Close() + + var buf bytes.Buffer + for s := iter.First(); s != nil; s = iter.Next() { + fmt.Fprintf(&buf, "%s\n", s) + } + return buf.String() + + case "scan-range-key": + iter, err := r.NewRawRangeKeyIter() + if err != nil { + return err.Error() + } + if iter == nil { + return "" + } + defer iter.Close() + + var buf bytes.Buffer + for s := iter.First(); s != nil; s = iter.Next() { + fmt.Fprintf(&buf, "%s\n", s) + } + return buf.String() + + case "layout": + l, err := r.Layout() + if err != nil { + return err.Error() + } + verbose := false + if len(td.CmdArgs) > 0 { + if td.CmdArgs[0].Key == "verbose" { + verbose = true + } else { + return "unknown arg" + } + } + var buf bytes.Buffer + l.Describe(&buf, verbose, r, nil) + return buf.String() + + case "rewrite": + var meta *WriterMetadata + var err error + meta, r, err = runRewriteCmd(td, r, WriterOptions{ + TableFormat: tableFormat, + }) + if err != nil { + return err.Error() + } + if err != nil { + return err.Error() + } + return format(td, meta) + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestWriterWithValueBlocks(t *testing.T) { + var r *Reader + defer func() { + if r != nil { + require.NoError(t, r.Close()) + } + }() + formatVersion := TableFormatMax + formatMeta := func(m *WriterMetadata) string { + return fmt.Sprintf("value-blocks: num-values %d, num-blocks: %d, size: %d", + m.Properties.NumValuesInValueBlocks, m.Properties.NumValueBlocks, + m.Properties.ValueBlocksSize) + } + + parallelism := false + if rand.Intn(2) == 0 { + parallelism = true + } + t.Logf("writer parallelism %t", parallelism) + attributeExtractor := func( + key []byte, keyPrefixLen int, value []byte) (base.ShortAttribute, error) { + require.NotNil(t, key) + require.Less(t, 0, keyPrefixLen) + attribute := base.ShortAttribute(len(value) & '\x07') + return attribute, nil + } + + datadriven.RunTest(t, "testdata/writer_value_blocks", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "build": + if r != nil { + _ = r.Close() + r = nil + } + var meta *WriterMetadata + var err error + var blockSize int + if td.HasArg("block-size") { + td.ScanArgs(t, "block-size", &blockSize) + } + var inPlaceValueBound UserKeyPrefixBound + if td.HasArg("in-place-bound") { + var l, u string + td.ScanArgs(t, "in-place-bound", &l, &u) + inPlaceValueBound.Lower = []byte(l) + inPlaceValueBound.Upper = []byte(u) + } + meta, r, err = runBuildCmd(td, &WriterOptions{ + BlockSize: blockSize, + Comparer: testkeys.Comparer, + TableFormat: formatVersion, + Parallelism: parallelism, + RequiredInPlaceValueBound: inPlaceValueBound, + ShortAttributeExtractor: attributeExtractor, + }, 0) + if err != nil { + return err.Error() + } + return formatMeta(meta) + + case "layout": + l, err := r.Layout() + if err != nil { + return err.Error() + } + var buf bytes.Buffer + l.Describe(&buf, true, r, func(key *base.InternalKey, value []byte) { + fmt.Fprintf(&buf, " %s:%s\n", key.String(), string(value)) + }) + return buf.String() + + case "scan-raw": + // Raw scan does not fetch from value blocks. + origIter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return err.Error() + } + forceIgnoreValueBlocks := func(i *singleLevelIterator) { + i.vbReader = nil + i.data.lazyValueHandling.vbr = nil + i.data.lazyValueHandling.hasValuePrefix = false + } + switch i := origIter.(type) { + case *twoLevelIterator: + forceIgnoreValueBlocks(&i.singleLevelIterator) + case *singleLevelIterator: + forceIgnoreValueBlocks(i) + } + iter := newIterAdapter(origIter) + defer iter.Close() + + var buf bytes.Buffer + for valid := iter.First(); valid; valid = iter.Next() { + v := iter.Value() + if iter.Key().Kind() == InternalKeyKindSet { + prefix := valuePrefix(v[0]) + setWithSamePrefix := setHasSamePrefix(prefix) + if isValueHandle(prefix) { + attribute := getShortAttribute(prefix) + vh := decodeValueHandle(v[1:]) + fmt.Fprintf(&buf, "%s:value-handle len %d block %d offset %d, att %d, same-pre %t\n", + iter.Key(), vh.valueLen, vh.blockNum, vh.offsetInBlock, attribute, setWithSamePrefix) + } else { + fmt.Fprintf(&buf, "%s:in-place %s, same-pre %t\n", iter.Key(), v[1:], setWithSamePrefix) + } + } else { + fmt.Fprintf(&buf, "%s:%s\n", iter.Key(), v) + } + } + return buf.String() + + case "scan": + origIter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return err.Error() + } + iter := newIterAdapter(origIter) + defer iter.Close() + var buf bytes.Buffer + for valid := iter.First(); valid; valid = iter.Next() { + fmt.Fprintf(&buf, "%s:%s\n", iter.Key(), iter.Value()) + } + return buf.String() + + case "scan-cloned-lazy-values": + iter, err := r.NewIter(nil /* lower */, nil /* upper */) + if err != nil { + return err.Error() + } + var fetchers [100]base.LazyFetcher + var values []base.LazyValue + n := 0 + var b []byte + for k, lv := iter.First(); k != nil; k, lv = iter.Next() { + var lvClone base.LazyValue + lvClone, b = lv.Clone(b, &fetchers[n]) + if lv.Fetcher != nil { + _, callerOwned, err := lv.Value(nil) + require.False(t, callerOwned) + require.NoError(t, err) + } + n++ + values = append(values, lvClone) + } + require.NoError(t, iter.Error()) + iter.Close() + var buf bytes.Buffer + for i := range values { + fmt.Fprintf(&buf, "%d", i) + v, callerOwned, err := values[i].Value(nil) + require.NoError(t, err) + if values[i].Fetcher != nil { + require.True(t, callerOwned) + fmt.Fprintf(&buf, "(lazy: len %d, attr: %d): %s\n", + values[i].Len(), values[i].Fetcher.Attribute.ShortAttribute, string(v)) + v2, callerOwned, err := values[i].Value(nil) + require.NoError(t, err) + require.True(t, callerOwned) + require.Equal(t, &v[0], &v2[0]) + + } else { + require.False(t, callerOwned) + fmt.Fprintf(&buf, "(in-place: len %d): %s\n", values[i].Len(), string(v)) + } + } + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func testBlockBufClear(t *testing.T, b1, b2 *blockBuf) { + require.Equal(t, b1.tmp, b2.tmp) +} + +func TestBlockBufClear(t *testing.T) { + b1 := &blockBuf{} + b1.tmp[0] = 1 + b1.compressedBuf = make([]byte, 1) + b1.clear() + testBlockBufClear(t, b1, &blockBuf{}) +} + +func TestClearDataBlockBuf(t *testing.T) { + d := newDataBlockBuf(1, ChecksumTypeCRC32c) + d.blockBuf.compressedBuf = make([]byte, 1) + d.dataBlock.add(ikey("apple"), nil) + d.dataBlock.add(ikey("banana"), nil) + + d.clear() + testBlockCleared(t, &d.dataBlock, &blockWriter{}) + testBlockBufClear(t, &d.blockBuf, &blockBuf{}) + + dataBlockBufPool.Put(d) +} + +func TestClearIndexBlockBuf(t *testing.T) { + i := newIndexBlockBuf(false) + i.block.add(ikey("apple"), nil) + i.block.add(ikey("banana"), nil) + i.clear() + + testBlockCleared(t, &i.block, &blockWriter{}) + require.Equal( + t, i.size.estimate, sizeEstimate{emptySize: emptyBlockSize}, + ) + indexBlockBufPool.Put(i) +} + +func TestClearWriteTask(t *testing.T) { + w := writeTaskPool.Get().(*writeTask) + ch := make(chan bool, 1) + w.compressionDone = ch + w.buf = &dataBlockBuf{} + w.flushableIndexBlock = &indexBlockBuf{} + w.currIndexBlock = &indexBlockBuf{} + w.indexEntrySep = ikey("apple") + w.indexInflightSize = 1 + w.finishedIndexProps = []byte{'a', 'v'} + + w.clear() + + var nilDataBlockBuf *dataBlockBuf + var nilIndexBlockBuf *indexBlockBuf + // Channels should be the same(no new channel should be allocated) + require.Equal(t, w.compressionDone, ch) + require.Equal(t, w.buf, nilDataBlockBuf) + require.Equal(t, w.flushableIndexBlock, nilIndexBlockBuf) + require.Equal(t, w.currIndexBlock, nilIndexBlockBuf) + require.Equal(t, w.indexEntrySep, base.InvalidInternalKey) + require.Equal(t, w.indexInflightSize, 0) + require.Equal(t, w.finishedIndexProps, []byte(nil)) + + writeTaskPool.Put(w) +} + +func TestDoubleClose(t *testing.T) { + // There is code in Cockroach land which relies on Writer.Close being + // idempotent. We should test this in Pebble, so that we don't cause + // Cockroach test failures. + f := &discardFile{} + w := NewWriter(f, WriterOptions{ + BlockSize: 1, + TableFormat: TableFormatPebblev1, + }) + w.Set(ikey("a").UserKey, nil) + w.Set(ikey("b").UserKey, nil) + err := w.Close() + require.NoError(t, err) + err = w.Close() + require.Equal(t, err, errWriterClosed) +} + +func TestParallelWriterErrorProp(t *testing.T) { + fs := vfs.NewMem() + f, err := fs.Create("test") + require.NoError(t, err) + opts := WriterOptions{ + TableFormat: TableFormatPebblev1, BlockSize: 1, Parallelism: true, + } + + w := NewWriter(objstorageprovider.NewFileWritable(f), opts) + // Directly testing this, because it's difficult to get the Writer to + // encounter an error, precisely when the writeQueue is doing block writes. + w.coordination.writeQueue.err = errors.New("write queue write error") + w.Set(ikey("a").UserKey, nil) + w.Set(ikey("b").UserKey, nil) + err = w.Close() + require.Equal(t, err.Error(), "write queue write error") +} + +func TestSizeEstimate(t *testing.T) { + var sizeEstimate sizeEstimate + datadriven.RunTest(t, "testdata/size_estimate", + func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "init": + if len(td.CmdArgs) != 1 { + return "init " + } + emptySize, err := strconv.Atoi(td.CmdArgs[0].String()) + if err != nil { + return "invalid empty size" + } + sizeEstimate.init(uint64(emptySize)) + return "success" + case "clear": + sizeEstimate.clear() + return fmt.Sprintf("%d", sizeEstimate.size()) + case "size": + return fmt.Sprintf("%d", sizeEstimate.size()) + case "add_inflight": + if len(td.CmdArgs) != 1 { + return "add_inflight " + } + inflightSize, err := strconv.Atoi(td.CmdArgs[0].String()) + if err != nil { + return "invalid inflight size" + } + sizeEstimate.addInflight(inflightSize) + return fmt.Sprintf("%d", sizeEstimate.size()) + case "entry_written": + if len(td.CmdArgs) != 2 { + return "entry_written " + } + newTotalSize, err := strconv.Atoi(td.CmdArgs[0].String()) + if err != nil { + return "invalid inflight size" + } + inflightSize, err := strconv.Atoi(td.CmdArgs[1].String()) + if err != nil { + return "invalid inflight size" + } + sizeEstimate.writtenWithTotal(uint64(newTotalSize), inflightSize) + return fmt.Sprintf("%d", sizeEstimate.size()) + case "num_written_entries": + return fmt.Sprintf("%d", sizeEstimate.numWrittenEntries) + case "num_inflight_entries": + return fmt.Sprintf("%d", sizeEstimate.numInflightEntries) + case "num_entries": + return fmt.Sprintf("%d", sizeEstimate.numWrittenEntries+sizeEstimate.numInflightEntries) + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestWriterClearCache(t *testing.T) { + // Verify that Writer clears the cache of blocks that it writes. + mem := vfs.NewMem() + opts := ReaderOptions{ + Cache: cache.New(64 << 20), + Comparer: testkeys.Comparer, + } + defer opts.Cache.Unref() + + writerOpts := WriterOptions{ + Cache: opts.Cache, + Comparer: testkeys.Comparer, + TableFormat: TableFormatPebblev3, + } + cacheOpts := &cacheOpts{cacheID: 1, fileNum: base.FileNum(1).DiskFileNum()} + invalidData := func() *cache.Value { + invalid := []byte("invalid data") + v := cache.Alloc(len(invalid)) + copy(v.Buf(), invalid) + return v + } + + build := func(name string) { + f, err := mem.Create(name) + require.NoError(t, err) + + w := NewWriter(objstorageprovider.NewFileWritable(f), writerOpts, cacheOpts) + require.NoError(t, w.Set([]byte("hello"), []byte("world"))) + require.NoError(t, w.Set([]byte("hello@42"), []byte("world@42"))) + require.NoError(t, w.Set([]byte("hello@5"), []byte("world@5"))) + require.NoError(t, w.Close()) + } + + // Build the sstable a first time so that we can determine the locations of + // all of the blocks. + build("test") + + f, err := mem.Open("test") + require.NoError(t, err) + + r, err := newReader(f, opts) + require.NoError(t, err) + + layout, err := r.Layout() + require.NoError(t, err) + + foreachBH := func(layout *Layout, f func(bh BlockHandle)) { + for _, bh := range layout.Data { + f(bh.BlockHandle) + } + for _, bh := range layout.Index { + f(bh) + } + f(layout.TopIndex) + f(layout.Filter) + f(layout.RangeDel) + for _, bh := range layout.ValueBlock { + f(bh) + } + if layout.ValueIndex.Length != 0 { + f(layout.ValueIndex) + } + f(layout.Properties) + f(layout.MetaIndex) + } + + // Poison the cache for each of the blocks. + poison := func(bh BlockHandle) { + opts.Cache.Set(cacheOpts.cacheID, cacheOpts.fileNum, bh.Offset, invalidData()).Release() + } + foreachBH(layout, poison) + + // Build the table a second time. This should clear the cache for the blocks + // that are written. + build("test") + + // Verify that the written blocks have been cleared from the cache. + check := func(bh BlockHandle) { + h := opts.Cache.Get(cacheOpts.cacheID, cacheOpts.fileNum, bh.Offset) + if h.Get() != nil { + t.Fatalf("%d: expected cache to be cleared, but found %q", bh.Offset, h.Get()) + } + } + foreachBH(layout, check) + + require.NoError(t, r.Close()) +} + +type discardFile struct { + wrote int64 +} + +var _ objstorage.Writable = (*discardFile)(nil) + +func (f *discardFile) Finish() error { + return nil +} + +func (f *discardFile) Abort() {} + +func (f *discardFile) Write(p []byte) error { + f.wrote += int64(len(p)) + return nil +} + +type blockPropErrSite uint + +const ( + errSiteAdd blockPropErrSite = iota + errSiteFinishBlock + errSiteFinishIndex + errSiteFinishTable + errSiteNone +) + +type testBlockPropCollector struct { + errSite blockPropErrSite + err error +} + +func (c *testBlockPropCollector) Name() string { return "testBlockPropCollector" } + +func (c *testBlockPropCollector) Add(_ InternalKey, _ []byte) error { + if c.errSite == errSiteAdd { + return c.err + } + return nil +} + +func (c *testBlockPropCollector) FinishDataBlock(_ []byte) ([]byte, error) { + if c.errSite == errSiteFinishBlock { + return nil, c.err + } + return nil, nil +} + +func (c *testBlockPropCollector) AddPrevDataBlockToIndexBlock() {} + +func (c *testBlockPropCollector) FinishIndexBlock(_ []byte) ([]byte, error) { + if c.errSite == errSiteFinishIndex { + return nil, c.err + } + return nil, nil +} + +func (c *testBlockPropCollector) FinishTable(_ []byte) ([]byte, error) { + if c.errSite == errSiteFinishTable { + return nil, c.err + } + return nil, nil +} + +func TestWriterBlockPropertiesErrors(t *testing.T) { + blockPropErr := errors.Newf("block property collector failed") + testCases := []blockPropErrSite{ + errSiteAdd, + errSiteFinishBlock, + errSiteFinishIndex, + errSiteFinishTable, + errSiteNone, + } + + var ( + k1 = base.MakeInternalKey([]byte("a"), 0, base.InternalKeyKindSet) + v1 = []byte("apples") + k2 = base.MakeInternalKey([]byte("b"), 0, base.InternalKeyKindSet) + v2 = []byte("bananas") + k3 = base.MakeInternalKey([]byte("c"), 0, base.InternalKeyKindSet) + v3 = []byte("carrots") + ) + + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + fs := vfs.NewMem() + f, err := fs.Create("test") + require.NoError(t, err) + + w := NewWriter(objstorageprovider.NewFileWritable(f), WriterOptions{ + BlockSize: 1, + BlockPropertyCollectors: []func() BlockPropertyCollector{ + func() BlockPropertyCollector { + return &testBlockPropCollector{ + errSite: tc, + err: blockPropErr, + } + }, + }, + TableFormat: TableFormatPebblev1, + }) + + err = w.Add(k1, v1) + switch tc { + case errSiteAdd: + require.Error(t, err) + require.Equal(t, blockPropErr, err) + return + case errSiteFinishBlock: + require.NoError(t, err) + // Addition of a second key completes the first block. + err = w.Add(k2, v2) + require.Error(t, err) + require.Equal(t, blockPropErr, err) + return + case errSiteFinishIndex: + require.NoError(t, err) + // Addition of a second key completes the first block. + err = w.Add(k2, v2) + require.NoError(t, err) + // The index entry for the first block is added after the completion of + // the second block, which is triggered by adding a third key. + err = w.Add(k3, v3) + require.Error(t, err) + require.Equal(t, blockPropErr, err) + return + } + + err = w.Close() + if tc == errSiteFinishTable { + require.Error(t, err) + require.Equal(t, blockPropErr, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestWriter_TableFormatCompatibility(t *testing.T) { + testCases := []struct { + name string + minFormat TableFormat + configureFn func(opts *WriterOptions) + writeFn func(w *Writer) error + }{ + { + name: "block properties", + minFormat: TableFormatPebblev1, + configureFn: func(opts *WriterOptions) { + opts.BlockPropertyCollectors = []func() BlockPropertyCollector{ + func() BlockPropertyCollector { + return NewBlockIntervalCollector( + "collector", &valueCharBlockIntervalCollector{charIdx: 0}, nil, + ) + }, + } + }, + }, + { + name: "range keys", + minFormat: TableFormatPebblev2, + writeFn: func(w *Writer) error { + return w.RangeKeyDelete([]byte("a"), []byte("b")) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for tf := TableFormatLevelDB; tf <= TableFormatMax; tf++ { + t.Run(tf.String(), func(t *testing.T) { + fs := vfs.NewMem() + f, err := fs.Create("sst") + require.NoError(t, err) + + opts := WriterOptions{TableFormat: tf} + if tc.configureFn != nil { + tc.configureFn(&opts) + } + + w := NewWriter(objstorageprovider.NewFileWritable(f), opts) + if tc.writeFn != nil { + err = tc.writeFn(w) + require.NoError(t, err) + } + + err = w.Close() + if tf < tc.minFormat { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } + }) + } +} + +// Tests for races, such as https://github.com/cockroachdb/cockroach/issues/77194, +// in the Writer. +func TestWriterRace(t *testing.T) { + ks := testkeys.Alpha(5) + ks = ks.EveryN(ks.Count() / 1_000) + keys := make([][]byte, ks.Count()) + for ki := 0; ki < len(keys); ki++ { + keys[ki] = testkeys.Key(ks, int64(ki)) + } + readerOpts := ReaderOptions{ + Comparer: testkeys.Comparer, + Filters: map[string]base.FilterPolicy{}, + } + + var wg sync.WaitGroup + for i := 0; i < 16; i++ { + wg.Add(1) + go func() { + val := make([]byte, rand.Intn(1000)) + opts := WriterOptions{ + Comparer: testkeys.Comparer, + BlockSize: rand.Intn(1 << 10), + Compression: NoCompression, + } + defer wg.Done() + f := &memFile{} + w := NewWriter(f, opts) + for ki := 0; ki < len(keys); ki++ { + require.NoError( + t, + w.Add(base.MakeInternalKey(keys[ki], uint64(ki), InternalKeyKindSet), val), + ) + require.Equal( + t, w.dataBlockBuf.dataBlock.getCurKey().UserKey, keys[ki], + ) + } + require.NoError(t, w.Close()) + require.Equal(t, w.meta.LargestPoint.UserKey, keys[len(keys)-1]) + r, err := NewMemReader(f.Data(), readerOpts) + require.NoError(t, err) + defer r.Close() + it, err := r.NewIter(nil, nil) + require.NoError(t, err) + defer it.Close() + ki := 0 + for k, v := it.First(); k != nil; k, v = it.Next() { + require.Equal(t, k.UserKey, keys[ki]) + vBytes, _, err := v.Value(nil) + require.NoError(t, err) + require.Equal(t, vBytes, val) + ki++ + } + }() + } + wg.Wait() +} + +func TestObsoleteBlockPropertyCollectorFilter(t *testing.T) { + var c obsoleteKeyBlockPropertyCollector + var f obsoleteKeyBlockPropertyFilter + require.Equal(t, c.Name(), f.Name()) + // Data block with 1 obsolete and 1 non-obsolete point. + c.AddPoint(false) + c.AddPoint(true) + finishAndCheck := func(finishFunc func([]byte) ([]byte, error), expectedIntersects bool) { + var buf [1]byte + prop, err := finishFunc(buf[:0:1]) + require.NoError(t, err) + expectedLength := 1 + if expectedIntersects { + // The common case is encoded in 0 bytes + expectedLength = 0 + } + require.Equal(t, expectedLength, len(prop)) + // Confirm that the collector used the slice. + require.Equal(t, unsafe.Pointer(&buf[0]), unsafe.Pointer(&prop[:1][0])) + intersects, err := f.Intersects(prop) + require.NoError(t, err) + require.Equal(t, expectedIntersects, intersects) + } + finishAndCheck(c.FinishDataBlock, true) + c.AddPrevDataBlockToIndexBlock() + // Data block with only obsolete points. + c.AddPoint(true) + c.AddPoint(true) + finishAndCheck(c.FinishDataBlock, false) + c.AddPrevDataBlockToIndexBlock() + // Index block has one obsolete block and one non-obsolete block. + finishAndCheck(c.FinishIndexBlock, true) + + // Data block with obsolete point. + c.AddPoint(true) + finishAndCheck(c.FinishDataBlock, false) + c.AddPrevDataBlockToIndexBlock() + // Data block with obsolete point. + c.AddPoint(true) + finishAndCheck(c.FinishDataBlock, false) + c.AddPrevDataBlockToIndexBlock() + // Index block has only obsolete blocks. + finishAndCheck(c.FinishIndexBlock, false) + // Table is not obsolete. + finishAndCheck(c.FinishTable, true) + + // Reset the collector state. + c = obsoleteKeyBlockPropertyCollector{} + // Table with only obsolete blocks. + + // Data block with obsolete point. + c.AddPoint(true) + finishAndCheck(c.FinishDataBlock, false) + c.AddPrevDataBlockToIndexBlock() + // Data block with obsolete point. + c.AddPoint(true) + finishAndCheck(c.FinishDataBlock, false) + c.AddPrevDataBlockToIndexBlock() + // Index block has only obsolete blocks. + finishAndCheck(c.FinishIndexBlock, false) + // Table is obsolete. + finishAndCheck(c.FinishTable, false) +} + +func BenchmarkWriter(b *testing.B) { + keys := make([][]byte, 1e6) + const keyLen = 24 + keySlab := make([]byte, keyLen*len(keys)) + for i := range keys { + key := keySlab[i*keyLen : i*keyLen+keyLen] + binary.BigEndian.PutUint64(key[:8], 123) // 16-byte shared prefix + binary.BigEndian.PutUint64(key[8:16], 456) + binary.BigEndian.PutUint64(key[16:], uint64(i)) + keys[i] = key + } + for _, format := range []TableFormat{TableFormatPebblev2, TableFormatPebblev3} { + b.Run(fmt.Sprintf("format=%s", format.String()), func(b *testing.B) { + runWriterBench(b, keys, nil, format) + }) + } +} + +func BenchmarkWriterWithVersions(b *testing.B) { + keys := make([][]byte, 1e6) + const keyLen = 26 + keySlab := make([]byte, keyLen*len(keys)) + for i := range keys { + key := keySlab[i*keyLen : i*keyLen+keyLen] + binary.BigEndian.PutUint64(key[:8], 123) // 16-byte shared prefix + binary.BigEndian.PutUint64(key[8:16], 456) + // @ is ascii value 64. Placing any byte with value 64 in these 8 bytes + // will confuse testkeys.Comparer, when we pass it a key after splitting + // of the suffix, since Comparer thinks this prefix is also a key with a + // suffix. Hence, we print as a base 10 string. + require.Equal(b, 8, copy(key[16:], fmt.Sprintf("%8d", i/2))) + key[24] = '@' + // Ascii representation of single digit integer 2-(i%2). + key[25] = byte(48 + 2 - (i % 2)) + keys[i] = key + } + // TableFormatPebblev3 can sometimes be ~50% slower than + // TableFormatPebblev2, since testkeys.Compare is expensive (mainly due to + // split) and with v3 we have to call it twice for 50% of the Set calls, + // since they have the same prefix as the preceding key. + for _, format := range []TableFormat{TableFormatPebblev2, TableFormatPebblev3} { + b.Run(fmt.Sprintf("format=%s", format.String()), func(b *testing.B) { + runWriterBench(b, keys, testkeys.Comparer, format) + }) + } +} + +func runWriterBench(b *testing.B, keys [][]byte, comparer *base.Comparer, format TableFormat) { + for _, bs := range []int{base.DefaultBlockSize, 32 << 10} { + b.Run(fmt.Sprintf("block=%s", humanize.Bytes.Int64(int64(bs))), func(b *testing.B) { + for _, filter := range []bool{true, false} { + b.Run(fmt.Sprintf("filter=%t", filter), func(b *testing.B) { + for _, comp := range []Compression{NoCompression, SnappyCompression, ZstdCompression} { + b.Run(fmt.Sprintf("compression=%s", comp), func(b *testing.B) { + opts := WriterOptions{ + BlockRestartInterval: 16, + BlockSize: bs, + Comparer: comparer, + Compression: comp, + TableFormat: format, + } + if filter { + opts.FilterPolicy = bloom.FilterPolicy(10) + } + f := &discardFile{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + f.wrote = 0 + w := NewWriter(f, opts) + + for j := range keys { + if err := w.Set(keys[j], keys[j]); err != nil { + b.Fatal(err) + } + } + if err := w.Close(); err != nil { + b.Fatal(err) + } + b.SetBytes(int64(f.wrote)) + } + }) + } + }) + } + }) + } +} + +var test4bSuffixComparer = &base.Comparer{ + Compare: base.DefaultComparer.Compare, + Equal: base.DefaultComparer.Equal, + Separator: base.DefaultComparer.Separator, + Successor: base.DefaultComparer.Successor, + Split: func(key []byte) int { + if len(key) > 4 { + return len(key) - 4 + } + return len(key) + }, + Name: "comparer-split-4b-suffix", +} diff --git a/pebble/table_cache.go b/pebble/table_cache.go new file mode 100644 index 0000000..c4e8bf1 --- /dev/null +++ b/pebble/table_cache.go @@ -0,0 +1,1208 @@ +// Copyright 2020 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 ( + "bytes" + "context" + "fmt" + "io" + "runtime/debug" + "runtime/pprof" + "sync" + "sync/atomic" + "unsafe" + + "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/objstorageprovider/objiotracing" + "github.com/cockroachdb/pebble/sstable" +) + +var emptyIter = &errorIter{err: nil} +var emptyKeyspanIter = &errorKeyspanIter{err: nil} + +// filteredAll is a singleton internalIterator implementation used when an +// sstable does contain point keys, but all the keys are filtered by the active +// PointKeyFilters set in the iterator's IterOptions. +// +// filteredAll implements filteredIter, ensuring the level iterator recognizes +// when it may need to return file boundaries to keep the rangeDelIter open +// during mergingIter operation. +var filteredAll = &filteredAllKeysIter{errorIter: errorIter{err: nil}} + +var _ filteredIter = filteredAll + +type filteredAllKeysIter struct { + errorIter +} + +func (s *filteredAllKeysIter) MaybeFilteredKeys() bool { + return true +} + +var tableCacheLabels = pprof.Labels("pebble", "table-cache") + +// tableCacheOpts contains the db specific fields +// of a table cache. This is stored in the tableCacheContainer +// along with the table cache. +// NB: It is important to make sure that the fields in this +// struct are read-only. Since the fields here are shared +// by every single tableCacheShard, if non read-only fields +// are updated, we could have unnecessary evictions of those +// fields, and the surrounding fields from the CPU caches. +type tableCacheOpts struct { + // 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 + opts sstable.ReaderOptions + filterMetrics *sstable.FilterMetricsTracker + sstStatsCollector *sstable.CategoryStatsCollector +} + +// tableCacheContainer contains the table cache and +// fields which are unique to the DB. +type tableCacheContainer struct { + tableCache *TableCache + + // dbOpts contains fields relevant to the table cache + // which are unique to each DB. + dbOpts tableCacheOpts +} + +// 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, + sstStatsCollector *sstable.CategoryStatsCollector, +) *tableCacheContainer { + // We will release a ref to table cache acquired here when tableCacheContainer.close is called. + if tc != nil { + if tc.cache != opts.Cache { + panic("pebble: underlying cache for the table cache and db are different") + } + tc.Ref() + } else { + // NewTableCache should create a ref to tc which the container should + // drop whenever it is closed. + tc = NewTableCache(opts.Cache, opts.Experimental.TableCacheShards, size) + } + + t := &tableCacheContainer{} + t.tableCache = tc + t.dbOpts.loggerAndTracer = opts.LoggerAndTracer + t.dbOpts.cacheID = cacheID + t.dbOpts.objProvider = objProvider + t.dbOpts.opts = opts.MakeReaderOptions() + t.dbOpts.filterMetrics = &sstable.FilterMetricsTracker{} + t.dbOpts.iterCount = new(atomic.Int32) + t.dbOpts.sstStatsCollector = sstStatsCollector + return t +} + +// Before calling close, make sure that there will be no further need +// to access any of the files associated with the store. +func (c *tableCacheContainer) close() error { + // We want to do some cleanup work here. Check for leaked iterators + // 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 := c.dbOpts.iterCount.Load(); v > 0 { + err = errors.Errorf("leaked iterators: %d", errors.Safe(v)) + } + + // Release nodes here. + for _, shard := range c.tableCache.shards { + if shard != nil { + shard.removeDB(&c.dbOpts) + } + } + return firstError(err, c.tableCache.Unref()) +} + +func (c *tableCacheContainer) newIters( + ctx context.Context, + file *manifest.FileMetadata, + opts *IterOptions, + internalOpts internalIterOpts, +) (internalIterator, keyspan.FragmentIterator, error) { + return c.tableCache.getShard(file.FileBacking.DiskFileNum).newIters(ctx, file, opts, internalOpts, &c.dbOpts) +} + +func (c *tableCacheContainer) newRangeKeyIter( + file *manifest.FileMetadata, opts keyspan.SpanIterOptions, +) (keyspan.FragmentIterator, error) { + 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.FileBacking.DiskFileNum).getTableProperties(file, &c.dbOpts) +} + +func (c *tableCacheContainer) evict(fileNum base.DiskFileNum) { + c.tableCache.getShard(fileNum).evict(fileNum, &c.dbOpts, false) +} + +func (c *tableCacheContainer) metrics() (CacheMetrics, FilterMetrics) { + var m CacheMetrics + for i := range c.tableCache.shards { + s := c.tableCache.shards[i] + s.mu.RLock() + m.Count += int64(len(s.mu.nodes)) + s.mu.RUnlock() + m.Hits += s.hits.Load() + m.Misses += s.misses.Load() + } + m.Size = m.Count * int64(unsafe.Sizeof(sstable.Reader{})) + f := c.dbOpts.filterMetrics.Load() + return m, f +} + +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(c.dbOpts.iterCount.Load()) +} + +// TableCache is a shareable cache for open sstables. +type TableCache struct { + refs atomic.Int64 + + cache *Cache + shards []*tableCacheShard +} + +// Ref adds a reference to the table cache. Once tableCache.init returns, +// the table cache only remains valid if there is at least one reference +// to it. +func (c *TableCache) Ref() { + 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 { + panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) + } +} + +// Unref removes a reference to the table cache. +func (c *TableCache) Unref() error { + v := c.refs.Add(-1) + switch { + case v < 0: + panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) + case v == 0: + var err error + for i := range c.shards { + // The cache shard is not allocated yet, nothing to close + if c.shards[i] == nil { + continue + } + err = firstError(err, c.shards[i].Close()) + } + + // Unref the cache which we create a reference to when the tableCache + // is first instantiated. + c.cache.Unref() + return err + } + return nil +} + +// NewTableCache will create a reference to the table cache. It is the callers responsibility +// to call tableCache.Unref if they will no longer hold a reference to the table cache. +func NewTableCache(cache *Cache, numShards int, size int) *TableCache { + if size == 0 { + panic("pebble: cannot create a table cache of size 0") + } else if numShards == 0 { + panic("pebble: cannot create a table cache with 0 shards") + } + + c := &TableCache{} + c.cache = cache + c.cache.Ref() + + c.shards = make([]*tableCacheShard, numShards) + for i := range c.shards { + c.shards[i] = &tableCacheShard{} + c.shards[i].init(size / len(c.shards)) + } + + // Hold a ref to the cache here. + c.refs.Store(1) + + return c +} + +func (c *TableCache) getShard(fileNum base.DiskFileNum) *tableCacheShard { + return c.shards[uint64(fileNum.FileNum())%uint64(len(c.shards))] +} + +type tableCacheKey struct { + cacheID uint64 + fileNum base.DiskFileNum +} + +type tableCacheShard struct { + hits atomic.Int64 + misses atomic.Int64 + iterCount atomic.Int32 + + size int + + mu struct { + sync.RWMutex + nodes map[tableCacheKey]*tableCacheNode + // The iters map is only created and populated in race builds. + iters map[io.Closer][]byte + + handHot *tableCacheNode + handCold *tableCacheNode + handTest *tableCacheNode + + coldTarget int + sizeHot int + sizeCold int + sizeTest int + } + releasing sync.WaitGroup + releasingCh chan *tableCacheValue + releaseLoopExit sync.WaitGroup +} + +func (c *tableCacheShard) init(size int) { + c.size = size + + c.mu.nodes = make(map[tableCacheKey]*tableCacheNode) + c.mu.coldTarget = size + c.releasingCh = make(chan *tableCacheValue, 100) + c.releaseLoopExit.Add(1) + go c.releaseLoop() + + if invariants.RaceEnabled { + c.mu.iters = make(map[io.Closer][]byte) + } +} + +func (c *tableCacheShard) releaseLoop() { + pprof.Do(context.Background(), tableCacheLabels, func(context.Context) { + defer c.releaseLoopExit.Done() + for v := range c.releasingCh { + v.release(c) + } + }) +} + +// checkAndIntersectFilters checks the specific table and block property filters +// for intersection with any available table and block-level properties. Returns +// true for ok if this table should be read by this iterator. +func (c *tableCacheShard) checkAndIntersectFilters( + v *tableCacheValue, + tableFilter func(userProps map[string]string) bool, + blockPropertyFilters []BlockPropertyFilter, + boundLimitedFilter sstable.BoundLimitedBlockPropertyFilter, +) (ok bool, filterer *sstable.BlockPropertiesFilterer, err error) { + if tableFilter != nil && + !tableFilter(v.reader.Properties.UserProperties) { + return false, nil, nil + } + + if boundLimitedFilter != nil || len(blockPropertyFilters) > 0 { + filterer, err = sstable.IntersectsTable( + blockPropertyFilters, + boundLimitedFilter, + v.reader.Properties.UserProperties, + ) + // NB: IntersectsTable will return a nil filterer if the table-level + // properties indicate there's no intersection with the provided filters. + if filterer == nil || err != nil { + return false, nil, err + } + } + return true, filterer, nil +} + +func (c *tableCacheShard) newIters( + ctx context.Context, + file *manifest.FileMetadata, + opts *IterOptions, + internalOpts internalIterOpts, + dbOpts *tableCacheOpts, +) (internalIterator, keyspan.FragmentIterator, error) { + // TODO(sumeer): constructing the Reader should also use a plumbed context, + // since parts of the sstable are read during the construction. The Reader + // should not remember that context since the Reader can be long-lived. + + // Calling findNode gives us the responsibility of decrementing v's + // refCount. If opening the underlying table resulted in error, then we + // decrement this straight away. Otherwise, we pass that responsibility to + // the sstable iterator, which decrements when it is closed. + v := c.findNode(file, dbOpts) + if v.err != nil { + defer c.unrefValue(v) + 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, + 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 := cr.NewRawRangeDelIter() + if err != nil { + c.unrefValue(v) + return nil, nil, err + } + + if !ok { + c.unrefValue(v) + // Return an empty iterator. This iterator has no mutable state, so + // using a singleton is fine. + // NB: We still return the potentially non-empty rangeDelIter. This + // ensures the iterator observes the file's range deletions even if the + // block property filters exclude all the file's point keys. The range + // deletions may still delete keys lower in the LSM in files that DO + // match the active filters. + // + // The point iterator returned must implement the filteredIter + // interface, so that the level iterator surfaces file boundaries when + // range deletions are present. + return filteredAll, rangeDelIter, err + } + + var iter sstable.Iterator + 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 { + 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 + } + var categoryAndQoS sstable.CategoryAndQoS + if opts != nil { + categoryAndQoS = opts.CategoryAndQoS + } + if internalOpts.bytesIterated != nil { + iter, err = cr.NewCompactionIter( + internalOpts.bytesIterated, categoryAndQoS, dbOpts.sstStatsCollector, rp, + internalOpts.bufferPool) + } else { + iter, err = cr.NewIterWithBlockPropertyFiltersAndContextEtc( + ctx, opts.GetLowerBound(), opts.GetUpperBound(), filterer, hideObsoletePoints, useFilter, + internalOpts.stats, categoryAndQoS, dbOpts.sstStatsCollector, rp) + } + if err != nil { + if rangeDelIter != nil { + _ = rangeDelIter.Close() + } + c.unrefValue(v) + return nil, nil, err + } + // NB: v.closeHook takes responsibility for calling unrefValue(v) here. Take + // care to avoid introducing an allocation here by adding a closure. + iter.SetCloseHook(v.closeHook) + + c.iterCount.Add(1) + dbOpts.iterCount.Add(1) + if invariants.RaceEnabled { + c.mu.Lock() + c.mu.iters[iter] = debug.Stack() + c.mu.Unlock() + } + return iter, rangeDelIter, nil +} + +func (c *tableCacheShard) newRangeKeyIter( + 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 + // decrement this straight away. Otherwise, we pass that responsibility to + // the sstable iterator, which decrements when it is closed. + v := c.findNode(file, dbOpts) + if v.err != nil { + defer c.unrefValue(v) + return nil, v.err + } + + ok := true + var err error + // Don't filter a table's range keys if the file contains RANGEKEYDELs. + // The RANGEKEYDELs may delete range keys in other levels. Skipping the + // 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 v.reader.Properties.NumRangeKeyDels == 0 { + ok, _, err = c.checkAndIntersectFilters(v, nil, opts.RangeKeyFilters, nil) + } + if err != nil { + c.unrefValue(v) + return nil, err + } + if !ok { + c.unrefValue(v) + // Return the empty iterator. This iterator has no mutable state, so + // using a singleton is fine. + return emptyKeyspanIter, err + } + + var iter keyspan.FragmentIterator + 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) + + if err != nil { + return nil, err + } + + if iter == nil { + // NewRawRangeKeyIter can return nil even if there's no error. However, + // the keyspan.LevelIter expects a non-nil iterator if err is nil. + return emptyKeyspanIter, nil + } + + return iter, nil +} + +type tableCacheShardReaderProvider struct { + c *tableCacheShard + file *manifest.FileMetadata + dbOpts *tableCacheOpts + v *tableCacheValue +} + +var _ sstable.ReaderProvider = &tableCacheShardReaderProvider{} + +// GetReader implements sstable.ReaderProvider. Note that it is not the +// responsibility of tableCacheShardReaderProvider to ensure that the file +// continues to exist. The ReaderProvider is used in iterators where the +// top-level iterator is pinning the read state and preventing the files from +// 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. + v := rp.c.findNode(rp.file, rp.dbOpts) + if v.err != nil { + defer rp.c.unrefValue(v) + return nil, v.err + } + rp.v = v + return v.reader, nil +} + +// Close implements sstable.ReaderProvider. +func (rp *tableCacheShardReaderProvider) Close() { + rp.c.unrefValue(rp.v) + rp.v = nil +} + +// getTableProperties return sst table properties for target file +func (c *tableCacheShard) getTableProperties( + file *fileMetadata, dbOpts *tableCacheOpts, +) (*sstable.Properties, error) { + // Calling findNode gives us the responsibility of decrementing v's refCount here + v := c.findNode(file, dbOpts) + defer c.unrefValue(v) + + if v.err != nil { + return nil, v.err + } + return &v.reader.Properties, nil +} + +// releaseNode releases a node from the tableCacheShard. +// +// c.mu must be held when calling this. +func (c *tableCacheShard) releaseNode(n *tableCacheNode) { + c.unlinkNode(n) + c.clearNode(n) +} + +// unlinkNode removes a node from the tableCacheShard, leaving the shard +// reference in place. +// +// c.mu must be held when calling this. +func (c *tableCacheShard) unlinkNode(n *tableCacheNode) { + key := tableCacheKey{n.cacheID, n.fileNum} + delete(c.mu.nodes, key) + + switch n.ptype { + case tableCacheNodeHot: + c.mu.sizeHot-- + case tableCacheNodeCold: + c.mu.sizeCold-- + case tableCacheNodeTest: + c.mu.sizeTest-- + } + + if n == c.mu.handHot { + c.mu.handHot = c.mu.handHot.prev() + } + if n == c.mu.handCold { + c.mu.handCold = c.mu.handCold.prev() + } + if n == c.mu.handTest { + c.mu.handTest = c.mu.handTest.prev() + } + + if n.unlink() == n { + // This was the last entry in the cache. + c.mu.handHot = nil + c.mu.handCold = nil + c.mu.handTest = nil + } + + n.links.prev = nil + n.links.next = nil +} + +func (c *tableCacheShard) clearNode(n *tableCacheNode) { + if v := n.value; v != nil { + n.value = nil + c.unrefValue(v) + } +} + +// unrefValue decrements the reference count for the specified value, releasing +// it if the reference count fell to 0. Note that the value has a reference if +// 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 v.refCount.Add(-1) == 0 { + c.releasing.Add(1) + c.releasingCh <- v + } +} + +// findNode returns the node for the table with the given file number, creating +// 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.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 + v.refCount.Add(1) + c.mu.RUnlock() + n.referenced.Store(true) + c.hits.Add(1) + <-v.loaded + return v + } + c.mu.RUnlock() + + c.mu.Lock() + + n := c.mu.nodes[key] + switch { + case n == nil: + // Slow-path miss of a non-existent node. + n = &tableCacheNode{ + fileNum: meta.FileBacking.DiskFileNum, + ptype: tableCacheNodeCold, + } + c.addNode(n, dbOpts) + c.mu.sizeCold++ + + case n.value != nil: + // Slow-path hit of a hot or cold node. + // + // The caller is responsible for decrementing the refCount. + v := n.value + v.refCount.Add(1) + n.referenced.Store(true) + c.hits.Add(1) + c.mu.Unlock() + <-v.loaded + return v + + default: + // Slow-path miss of a test node. + c.unlinkNode(n) + c.mu.coldTarget++ + if c.mu.coldTarget > c.size { + c.mu.coldTarget = c.size + } + + n.referenced.Store(false) + n.ptype = tableCacheNodeHot + c.addNode(n, dbOpts) + c.mu.sizeHot++ + } + + c.misses.Add(1) + + v := &tableCacheValue{ + 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 { + if invariants.RaceEnabled { + c.mu.Lock() + delete(c.mu.iters, i) + c.mu.Unlock() + } + c.unrefValue(v) + c.iterCount.Add(-1) + dbOpts.iterCount.Add(-1) + return nil + } + n.value = v + + c.mu.Unlock() + + // 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( + loadInfo{ + backingFileNum: meta.FileBacking.DiskFileNum, + smallestSeqNum: meta.SmallestSeqNum, + largestSeqNum: meta.LargestSeqNum, + }, c, dbOpts) + }) + return v +} + +func (c *tableCacheShard) addNode(n *tableCacheNode, dbOpts *tableCacheOpts) { + c.evictNodes() + n.cacheID = dbOpts.cacheID + key := tableCacheKey{n.cacheID, n.fileNum} + c.mu.nodes[key] = n + + n.links.next = n + n.links.prev = n + if c.mu.handHot == nil { + // First element. + c.mu.handHot = n + c.mu.handCold = n + c.mu.handTest = n + } else { + c.mu.handHot.link(n) + } + + if c.mu.handCold == c.mu.handHot { + c.mu.handCold = c.mu.handCold.prev() + } +} + +func (c *tableCacheShard) evictNodes() { + for c.size <= c.mu.sizeHot+c.mu.sizeCold && c.mu.handCold != nil { + c.runHandCold() + } +} + +func (c *tableCacheShard) runHandCold() { + n := c.mu.handCold + if n.ptype == tableCacheNodeCold { + if n.referenced.Load() { + n.referenced.Store(false) + n.ptype = tableCacheNodeHot + c.mu.sizeCold-- + c.mu.sizeHot++ + } else { + c.clearNode(n) + n.ptype = tableCacheNodeTest + c.mu.sizeCold-- + c.mu.sizeTest++ + for c.size < c.mu.sizeTest && c.mu.handTest != nil { + c.runHandTest() + } + } + } + + c.mu.handCold = c.mu.handCold.next() + + for c.size-c.mu.coldTarget <= c.mu.sizeHot && c.mu.handHot != nil { + c.runHandHot() + } +} + +func (c *tableCacheShard) runHandHot() { + if c.mu.handHot == c.mu.handTest && c.mu.handTest != nil { + c.runHandTest() + if c.mu.handHot == nil { + return + } + } + + n := c.mu.handHot + if n.ptype == tableCacheNodeHot { + if n.referenced.Load() { + n.referenced.Store(false) + } else { + n.ptype = tableCacheNodeCold + c.mu.sizeHot-- + c.mu.sizeCold++ + } + } + + c.mu.handHot = c.mu.handHot.next() +} + +func (c *tableCacheShard) runHandTest() { + if c.mu.sizeCold > 0 && c.mu.handTest == c.mu.handCold && c.mu.handCold != nil { + c.runHandCold() + if c.mu.handTest == nil { + return + } + } + + n := c.mu.handTest + if n.ptype == tableCacheNodeTest { + c.mu.coldTarget-- + if c.mu.coldTarget < 0 { + c.mu.coldTarget = 0 + } + c.unlinkNode(n) + c.clearNode(n) + } + + c.mu.handTest = c.mu.handTest.next() +} + +func (c *tableCacheShard) evict(fileNum base.DiskFileNum, dbOpts *tableCacheOpts, allowLeak bool) { + c.mu.Lock() + key := tableCacheKey{dbOpts.cacheID, fileNum} + n := c.mu.nodes[key] + var v *tableCacheValue + if n != nil { + // NB: This is equivalent to tableCacheShard.releaseNode(), but we perform + // the tableCacheNode.release() call synchronously below to ensure the + // sstable file descriptor is closed before returning. Note that + // tableCacheShard.releasing needs to be incremented while holding + // tableCacheShard.mu in order to avoid a race with Close() + c.unlinkNode(n) + v = n.value + if v != nil { + if !allowLeak { + if t := v.refCount.Add(-1); t != 0 { + dbOpts.loggerAndTracer.Fatalf("sstable %s: refcount is not zero: %d\n%s", fileNum, t, debug.Stack()) + } + } + c.releasing.Add(1) + } + } + + c.mu.Unlock() + + if v != nil { + v.release(c) + } + + dbOpts.opts.Cache.EvictFile(dbOpts.cacheID, fileNum) +} + +// removeDB evicts any nodes which have a reference to the DB +// 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.DiskFileNum + + c.mu.RLock() + // Collect the fileNums which need to be cleaned. + var firstNode *tableCacheNode + node := c.mu.handHot + for node != firstNode { + if firstNode == nil { + firstNode = node + } + + if node.cacheID == dbOpts.cacheID { + fileNums = append(fileNums, node.fileNum) + } + node = node.next() + } + c.mu.RUnlock() + + // Evict all the nodes associated with the DB. + // This should synchronously close all the files + // associated with the DB. + for _, fileNum := range fileNums { + c.evict(fileNum, dbOpts, true) + } +} + +func (c *tableCacheShard) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + + // 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 := c.iterCount.Load(); v > 0 { + if !invariants.RaceEnabled { + err = errors.Errorf("leaked iterators: %d", errors.Safe(v)) + } else { + var buf bytes.Buffer + for _, stack := range c.mu.iters { + fmt.Fprintf(&buf, "%s\n", stack) + } + err = errors.Errorf("leaked iterators: %d\n%s", errors.Safe(v), buf.String()) + } + } + + for c.mu.handHot != nil { + n := c.mu.handHot + if n.value != nil { + if n.value.refCount.Add(-1) == 0 { + c.releasing.Add(1) + c.releasingCh <- n.value + } + } + c.unlinkNode(n) + } + c.mu.nodes = nil + c.mu.handHot = nil + c.mu.handCold = nil + c.mu.handTest = nil + + // Only shutdown the releasing goroutine if there were no leaked + // iterators. If there were leaked iterators, we leave the goroutine running + // and the releasingCh open so that a subsequent iterator close can + // complete. This behavior is used by iterator leak tests. Leaking the + // goroutine for these tests is less bad not closing the iterator which + // triggers other warnings about block cache handles not being released. + if err != nil { + c.releasing.Wait() + return err + } + + close(c.releasingCh) + c.releasing.Wait() + c.releaseLoopExit.Wait() + return err +} + +type tableCacheValue struct { + closeHook func(i sstable.Iterator) error + reader *sstable.Reader + err error + loaded chan struct{} + // Reference count for the value. The reader is closed when the reference + // count drops to zero. + refCount atomic.Int32 +} + +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 + 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 err != nil { + v.err = errors.Wrapf( + err, "pebble: backing file %s error", errors.Safe(loadInfo.backingFileNum.FileNum())) + } + 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, loadInfo.backingFileNum} + n := c.mu.nodes[key] + if n != nil && n.value == v { + c.releaseNode(n) + } + } + close(v.loaded) +} + +func (v *tableCacheValue) release(c *tableCacheShard) { + <-v.loaded + // Nothing to be done about an error at this point. Close the reader if it is + // open. + if v.reader != nil { + _ = v.reader.Close() + } + c.releasing.Done() +} + +type tableCacheNodeType int8 + +const ( + tableCacheNodeTest tableCacheNodeType = iota + tableCacheNodeCold + tableCacheNodeHot +) + +func (p tableCacheNodeType) String() string { + switch p { + case tableCacheNodeTest: + return "test" + case tableCacheNodeCold: + return "cold" + case tableCacheNodeHot: + return "hot" + } + return "unknown" +} + +type tableCacheNode struct { + fileNum base.DiskFileNum + value *tableCacheValue + + links struct { + next *tableCacheNode + prev *tableCacheNode + } + 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 atomic.Bool + + // Storing the cache id associated with the DB instance here + // avoids the need to thread the dbOpts struct through many functions. + cacheID uint64 +} + +func (n *tableCacheNode) next() *tableCacheNode { + if n == nil { + return nil + } + return n.links.next +} + +func (n *tableCacheNode) prev() *tableCacheNode { + if n == nil { + return nil + } + return n.links.prev +} + +func (n *tableCacheNode) link(s *tableCacheNode) { + s.links.prev = n.links.prev + s.links.prev.links.next = s + s.links.next = n + s.links.next.links.prev = s +} + +func (n *tableCacheNode) unlink() *tableCacheNode { + next := n.links.next + n.links.prev.links.next = n.links.next + n.links.next.links.prev = n.links.prev + n.links.prev = n + n.links.next = n + return next +} diff --git a/pebble/table_cache_test.go b/pebble/table_cache_test.go new file mode 100644 index 0000000..103d62a --- /dev/null +++ b/pebble/table_cache_test.go @@ -0,0 +1,1263 @@ +// Copyright 2013 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 ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "path" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" +) + +type tableCacheTestFile struct { + vfs.File + fs *tableCacheTestFS + name string +} + +func (f *tableCacheTestFile) Close() error { + f.fs.mu.Lock() + if f.fs.closeCounts != nil { + f.fs.closeCounts[f.name]++ + } + f.fs.mu.Unlock() + return f.File.Close() +} + +type tableCacheTestFS struct { + vfs.FS + + mu sync.Mutex + openCounts map[string]int + closeCounts map[string]int + openErrorEnabled bool +} + +func (fs *tableCacheTestFS) Open(name string, opts ...vfs.OpenOption) (vfs.File, error) { + fs.mu.Lock() + if fs.openErrorEnabled { + fs.mu.Unlock() + return nil, errors.New("injected error") + } + if fs.openCounts != nil { + fs.openCounts[name]++ + } + fs.mu.Unlock() + f, err := fs.FS.Open(name, opts...) + if len(opts) < 1 || opts[0] != vfs.RandomReadsOption { + return nil, errors.Errorf("sstable file %s not opened with random reads option", name) + } + if err != nil { + return nil, err + } + return &tableCacheTestFile{f, fs, name}, nil +} + +func (fs *tableCacheTestFS) validate( + t *testing.T, c *tableCacheContainer, f func(i, gotO, gotC int) error, +) { + if err := fs.validateOpenTables(f); err != nil { + t.Error(err) + return + } + c.close() + if err := fs.validateNoneStillOpen(); err != nil { + t.Error(err) + return + } +} + +func (fs *tableCacheTestFS) setOpenError(enabled bool) { + fs.mu.Lock() + defer fs.mu.Unlock() + fs.openErrorEnabled = enabled +} + +// validateOpenTables validates that no tables in the cache are open twice, and +// the number still open is no greater than tableCacheTestCacheSize. +func (fs *tableCacheTestFS) validateOpenTables(f func(i, gotO, gotC int) error) error { + // try backs off to let any clean-up goroutines do their work. + return try(100*time.Microsecond, 20*time.Second, func() error { + fs.mu.Lock() + defer fs.mu.Unlock() + + numStillOpen := 0 + for i := 0; i < tableCacheTestNumTables; i++ { + filename := base.MakeFilepath(fs, "", fileTypeTable, base.FileNum(uint64(i)).DiskFileNum()) + gotO, gotC := fs.openCounts[filename], fs.closeCounts[filename] + if gotO > gotC { + numStillOpen++ + } + if gotC != gotO && gotC != gotO-1 { + return errors.Errorf("i=%d: table closed too many or too few times: opened %d times, closed %d times", + i, gotO, gotC) + } + if f != nil { + if err := f(i, gotO, gotC); err != nil { + return err + } + } + } + if numStillOpen > tableCacheTestCacheSize { + return errors.Errorf("numStillOpen is %d, want <= %d", numStillOpen, tableCacheTestCacheSize) + } + return nil + }) +} + +// validateNoneStillOpen validates that no tables in the cache are open. +func (fs *tableCacheTestFS) validateNoneStillOpen() error { + // try backs off to let any clean-up goroutines do their work. + return try(100*time.Microsecond, 20*time.Second, func() error { + fs.mu.Lock() + defer fs.mu.Unlock() + + for i := 0; i < tableCacheTestNumTables; i++ { + filename := base.MakeFilepath(fs, "", fileTypeTable, base.FileNum(uint64(i)).DiskFileNum()) + gotO, gotC := fs.openCounts[filename], fs.closeCounts[filename] + if gotO != gotC { + return errors.Errorf("i=%d: opened %d times, closed %d times", i, gotO, gotC) + } + } + return nil + }) +} + +const ( + tableCacheTestNumTables = 300 + tableCacheTestCacheSize = 100 +) + +// newTableCacheTest returns a shareable table cache to be used for tests. +// It is the caller's responsibility to unref the table cache. +func newTableCacheTest(size int64, tableCacheSize int, numShards int) *TableCache { + cache := NewCache(size) + defer cache.Unref() + return NewTableCache(cache, numShards, tableCacheSize) +} + +func newTableCacheContainerTest( + tc *TableCache, dirname string, +) (*tableCacheContainer, *tableCacheTestFS, error) { + xxx := bytes.Repeat([]byte("x"), tableCacheTestNumTables) + fs := &tableCacheTestFS{ + FS: vfs.NewMem(), + } + objProvider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(fs, dirname)) + if err != nil { + return nil, nil, err + } + defer objProvider.Close() + + for i := 0; i < tableCacheTestNumTables; i++ { + w, _, err := objProvider.Create(context.Background(), fileTypeTable, base.FileNum(uint64(i)).DiskFileNum(), objstorage.CreateOptions{}) + if err != nil { + return nil, nil, errors.Wrap(err, "fs.Create") + } + tw := sstable.NewWriter(w, sstable.WriterOptions{TableFormat: sstable.TableFormatPebblev2}) + ik := base.ParseInternalKey(fmt.Sprintf("k.SET.%d", i)) + if err := tw.Add(ik, xxx[:i]); err != nil { + return nil, nil, errors.Wrap(err, "tw.Set") + } + if err := tw.RangeKeySet([]byte("k"), []byte("l"), nil, xxx[:i]); err != nil { + return nil, nil, errors.Wrap(err, "tw.Set") + } + if err := tw.Close(); err != nil { + return nil, nil, errors.Wrap(err, "tw.Close") + } + } + + fs.mu.Lock() + fs.openCounts = map[string]int{} + fs.closeCounts = map[string]int{} + fs.mu.Unlock() + + opts := &Options{} + opts.EnsureDefaults() + if tc == nil { + opts.Cache = NewCache(8 << 20) // 8 MB + defer opts.Cache.Unref() + } else { + opts.Cache = tc.cache + } + + c := newTableCacheContainer(tc, opts.Cache.NewID(), objProvider, opts, tableCacheTestCacheSize, + &sstable.CategoryStatsCollector{}) + return c, fs, nil +} + +// Test basic reference counting for the table cache. +func TestTableCacheRefs(t *testing.T) { + tc := newTableCacheTest(8<<20, 10, 2) + + v := tc.refs.Load() + if v != 1 { + require.Equal(t, 1, v) + } + + tc.Ref() + v = tc.refs.Load() + if v != 2 { + require.Equal(t, 2, v) + } + + tc.Unref() + v = tc.refs.Load() + if v != 1 { + require.Equal(t, 1, v) + } + + tc.Unref() + v = tc.refs.Load() + if v != 0 { + require.Equal(t, 0, v) + } + + defer func() { + if r := recover(); r != nil { + if fmt.Sprint(r) != "pebble: inconsistent reference count: -1" { + t.Fatalf("unexpected panic message") + } + } else if r == nil { + t.Fatalf("expected panic") + } + }() + tc.Unref() +} + +// Basic test to determine if reads through the table cache are wired correctly. +func TestVirtualReadsWiring(t *testing.T) { + var d *DB + var err error + d, err = Open("", + &Options{ + FS: vfs.NewMem(), + FormatMajorVersion: internalFormatNewest, + Comparer: testkeys.Comparer, + // Compactions which conflict with virtual sstable creation can be + // picked by Pebble. We disable that. + DisableAutomaticCompactions: true, + }) + require.NoError(t, err) + defer d.Close() + + b := newBatch(d) + // Some combination of sets, range deletes, and range key sets/unsets, so + // all of the table cache iterator functions are utilized. + require.NoError(t, b.Set([]byte{'a'}, []byte{'a'}, nil)) + require.NoError(t, b.Set([]byte{'d'}, []byte{'d'}, nil)) + require.NoError(t, b.DeleteRange([]byte{'c'}, []byte{'e'}, nil)) + require.NoError(t, b.Set([]byte{'f'}, []byte{'f'}, nil)) + require.NoError(t, b.RangeKeySet([]byte{'f'}, []byte{'k'}, nil, []byte{'c'}, nil)) + require.NoError(t, b.RangeKeyUnset([]byte{'j'}, []byte{'k'}, nil, nil)) + require.NoError(t, b.Set([]byte{'z'}, []byte{'z'}, nil)) + require.NoError(t, d.Apply(b, nil)) + require.NoError(t, d.Flush()) + require.NoError(t, d.Compact([]byte{'a'}, []byte{'b'}, false)) + require.Equal(t, 1, int(d.Metrics().Levels[6].NumFiles)) + + d.mu.Lock() + + // Virtualize the single sstable in the lsm. + + currVersion := d.mu.versions.currentVersion() + l6 := currVersion.Levels[6] + l6FileIter := l6.Iter() + parentFile := l6FileIter.First() + f1 := FileNum(d.mu.versions.nextFileNum) + f2 := f1 + 1 + d.mu.versions.nextFileNum += 2 + + v1 := &manifest.FileMetadata{ + FileBacking: parentFile.FileBacking, + FileNum: f1, + CreationTime: time.Now().Unix(), + Size: parentFile.Size / 2, + SmallestSeqNum: parentFile.SmallestSeqNum, + LargestSeqNum: parentFile.LargestSeqNum, + Smallest: base.MakeInternalKey([]byte{'a'}, parentFile.Smallest.SeqNum(), InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte{'a'}, parentFile.Smallest.SeqNum(), InternalKeyKindSet), + HasPointKeys: true, + Virtual: true, + } + v1.Stats.NumEntries = 1 + + v2 := &manifest.FileMetadata{ + FileBacking: parentFile.FileBacking, + FileNum: f2, + CreationTime: time.Now().Unix(), + Size: parentFile.Size / 2, + SmallestSeqNum: parentFile.SmallestSeqNum, + LargestSeqNum: parentFile.LargestSeqNum, + Smallest: base.MakeInternalKey([]byte{'d'}, parentFile.Smallest.SeqNum()+1, InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte{'z'}, parentFile.Largest.SeqNum(), InternalKeyKindSet), + HasPointKeys: true, + Virtual: true, + } + v2.Stats.NumEntries = 6 + + v1.LargestPointKey = v1.Largest + v1.SmallestPointKey = v1.Smallest + + v2.LargestPointKey = v2.Largest + v2.SmallestPointKey = v2.Smallest + + v1.ValidateVirtual(parentFile) + d.checkVirtualBounds(v1) + v2.ValidateVirtual(parentFile) + d.checkVirtualBounds(v2) + + // Write the version edit. + fileMetrics := func(ve *versionEdit) map[int]*LevelMetrics { + metrics := newFileMetrics(ve.NewFiles) + for de, f := range ve.DeletedFiles { + lm := metrics[de.Level] + if lm == nil { + lm = &LevelMetrics{} + metrics[de.Level] = lm + } + metrics[de.Level].NumFiles-- + metrics[de.Level].Size -= int64(f.Size) + } + return metrics + } + + applyVE := func(ve *versionEdit) error { + d.mu.versions.logLock() + jobID := d.mu.nextJobID + d.mu.nextJobID++ + + err := d.mu.versions.logAndApply(jobID, ve, fileMetrics(ve), false, func() []compactionInfo { + return d.getInProgressCompactionInfoLocked(nil) + }) + d.updateReadStateLocked(nil) + return err + } + + ve := manifest.VersionEdit{} + d1 := manifest.DeletedFileEntry{Level: 6, FileNum: parentFile.FileNum} + n1 := manifest.NewFileEntry{Level: 6, Meta: v1} + n2 := manifest.NewFileEntry{Level: 6, Meta: v2} + + ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata) + ve.DeletedFiles[d1] = parentFile + ve.NewFiles = append(ve.NewFiles, n1) + ve.NewFiles = append(ve.NewFiles, n2) + ve.CreatedBackingTables = append(ve.CreatedBackingTables, parentFile.FileBacking) + + require.NoError(t, applyVE(&ve)) + + currVersion = d.mu.versions.currentVersion() + l6 = currVersion.Levels[6] + l6FileIter = l6.Iter() + for f := l6FileIter.First(); f != nil; f = l6FileIter.Next() { + require.Equal(t, true, f.Virtual) + } + d.mu.Unlock() + + // Confirm that there were only 2 virtual sstables in L6. + require.Equal(t, 2, int(d.Metrics().Levels[6].NumFiles)) + + // These reads will go through the table cache. + iter, _ := d.NewIter(nil) + expected := []byte{'a', 'f', 'z'} + for i, x := 0, iter.First(); x; i, x = i+1, iter.Next() { + require.Equal(t, []byte{expected[i]}, iter.Value()) + } + iter.Close() +} + +// The table cache shouldn't be usable after all the dbs close. +func TestSharedTableCacheUseAfterAllFree(t *testing.T) { + tc := newTableCacheTest(8<<20, 10, 1) + db1, err := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + require.NoError(t, err) + + // Release our reference, now that the db has a reference. + tc.Unref() + + db2, err := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + require.NoError(t, err) + + require.NoError(t, db1.Close()) + require.NoError(t, db2.Close()) + + v := tc.refs.Load() + if v != 0 { + t.Fatalf("expected reference count %d, got %d", 0, v) + } + + defer func() { + // The cache ref gets incremented before the panic, so we should + // decrement it to prevent the finalizer from detecting a leak. + tc.cache.Unref() + + if r := recover(); r != nil { + if fmt.Sprint(r) != "pebble: inconsistent reference count: 1" { + t.Fatalf("unexpected panic message") + } + } else if r == nil { + t.Fatalf("expected panic") + } + }() + + db3, _ := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + _ = db3 +} + +// Test whether a shared table cache is usable by a db, after +// one of the db's releases its reference. +func TestSharedTableCacheUseAfterOneFree(t *testing.T) { + tc := newTableCacheTest(8<<20, 10, 1) + db1, err := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + require.NoError(t, err) + + // Release our reference, now that the db has a reference. + tc.Unref() + + db2, err := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + require.NoError(t, err) + defer func() { + require.NoError(t, db2.Close()) + }() + + // Make db1 release a reference to the cache. It should + // still be usable by db2. + require.NoError(t, db1.Close()) + v := tc.refs.Load() + if v != 1 { + t.Fatalf("expected reference count %d, got %d", 1, v) + } + + // Check if db2 is still usable. + start := []byte("a") + end := []byte("d") + require.NoError(t, db2.Set(start, nil, nil)) + require.NoError(t, db2.Flush()) + require.NoError(t, db2.DeleteRange(start, end, nil)) + require.NoError(t, db2.Compact(start, end, false)) +} + +// A basic test which makes sure that a shared table cache is usable +// by more than one database at once. +func TestSharedTableCacheUsable(t *testing.T) { + tc := newTableCacheTest(8<<20, 10, 1) + db1, err := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + require.NoError(t, err) + + // Release our reference, now that the db has a reference. + tc.Unref() + + defer func() { + require.NoError(t, db1.Close()) + }() + + db2, err := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + require.NoError(t, err) + defer func() { + require.NoError(t, db2.Close()) + }() + + start := []byte("a") + end := []byte("z") + require.NoError(t, db1.Set(start, nil, nil)) + require.NoError(t, db1.Flush()) + require.NoError(t, db1.DeleteRange(start, end, nil)) + require.NoError(t, db1.Compact(start, end, false)) + + start = []byte("x") + end = []byte("y") + require.NoError(t, db2.Set(start, nil, nil)) + require.NoError(t, db2.Flush()) + require.NoError(t, db2.Set(start, []byte{'a'}, nil)) + require.NoError(t, db2.Flush()) + require.NoError(t, db2.DeleteRange(start, end, nil)) + require.NoError(t, db2.Compact(start, end, false)) +} + +func TestSharedTableConcurrent(t *testing.T) { + tc := newTableCacheTest(8<<20, 10, 1) + db1, err := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + require.NoError(t, err) + + // Release our reference, now that the db has a reference. + tc.Unref() + + defer func() { + require.NoError(t, db1.Close()) + }() + + db2, err := Open("test", + &Options{ + FS: vfs.NewMem(), + Cache: tc.cache, + TableCache: tc, + }) + require.NoError(t, err) + defer func() { + require.NoError(t, db2.Close()) + }() + + var wg sync.WaitGroup + wg.Add(2) + + // Now that both dbs have a reference to the table cache, + // we'll run go routines which will use the DBs concurrently. + concFunc := func(db *DB) { + for i := 0; i < 1000; i++ { + start := []byte("a") + end := []byte("z") + require.NoError(t, db.Set(start, nil, nil)) + require.NoError(t, db.Flush()) + require.NoError(t, db.DeleteRange(start, end, nil)) + require.NoError(t, db.Compact(start, end, false)) + } + wg.Done() + } + + go concFunc(db1) + go concFunc(db2) + + wg.Wait() +} + +func testTableCacheRandomAccess(t *testing.T, concurrent bool) { + const N = 2000 + c, fs, err := newTableCacheContainerTest(nil, "") + require.NoError(t, err) + + rngMu := sync.Mutex{} + rng := rand.New(rand.NewSource(1)) + + errc := make(chan error, N) + for i := 0; i < N; i++ { + go func(i int) { + rngMu.Lock() + fileNum, sleepTime := rng.Intn(tableCacheTestNumTables), rng.Intn(1000) + rngMu.Unlock() + m := &fileMetadata{FileNum: FileNum(fileNum)} + m.InitPhysicalBacking() + m.Ref() + defer m.Unref() + iter, _, err := c.newIters(context.Background(), m, nil, internalIterOpts{}) + if err != nil { + errc <- errors.Errorf("i=%d, fileNum=%d: find: %v", i, fileNum, err) + return + } + key, value := iter.SeekGE([]byte("k"), base.SeekGEFlagsNone) + if concurrent { + time.Sleep(time.Duration(sleepTime) * time.Microsecond) + } + if key == nil { + errc <- errors.Errorf("i=%d, fileNum=%d: valid.0: got false, want true", i, fileNum) + return + } + v, _, err := value.Value(nil) + if err != nil { + errc <- errors.Errorf("i=%d, fileNum=%d: err extracting value: %v", err) + } + if got := len(v); got != fileNum { + errc <- errors.Errorf("i=%d, fileNum=%d: value: got %d bytes, want %d", i, fileNum, got, fileNum) + return + } + if key, _ := iter.Next(); key != nil { + errc <- errors.Errorf("i=%d, fileNum=%d: next.1: got true, want false", i, fileNum) + return + } + if err := iter.Close(); err != nil { + errc <- errors.Wrapf(err, "close error i=%d, fileNum=%dv", i, fileNum) + return + } + errc <- nil + }(i) + if !concurrent { + require.NoError(t, <-errc) + } + } + if concurrent { + for i := 0; i < N; i++ { + require.NoError(t, <-errc) + } + } + fs.validate(t, c, nil) +} + +func TestTableCacheRandomAccessSequential(t *testing.T) { testTableCacheRandomAccess(t, false) } +func TestTableCacheRandomAccessConcurrent(t *testing.T) { testTableCacheRandomAccess(t, true) } + +func testTableCacheFrequentlyUsedInternal(t *testing.T, rangeIter bool) { + const ( + N = 1000 + pinned0 = 7 + pinned1 = 11 + ) + c, fs, err := newTableCacheContainerTest(nil, "") + require.NoError(t, err) + + for i := 0; i < N; i++ { + for _, j := range [...]int{pinned0, i % tableCacheTestNumTables, pinned1} { + var iter io.Closer + var err error + m := &fileMetadata{FileNum: FileNum(j)} + m.InitPhysicalBacking() + m.Ref() + if rangeIter { + iter, err = c.newRangeKeyIter(m, keyspan.SpanIterOptions{}) + } else { + iter, _, err = c.newIters(context.Background(), m, nil, internalIterOpts{}) + } + if err != nil { + t.Fatalf("i=%d, j=%d: find: %v", i, j, err) + } + if err := iter.Close(); err != nil { + t.Fatalf("i=%d, j=%d: close: %v", i, j, err) + } + } + } + + fs.validate(t, c, func(i, gotO, gotC int) error { + if i == pinned0 || i == pinned1 { + if gotO != 1 || gotC != 0 { + return errors.Errorf("i=%d: pinned table: got %d, %d, want %d, %d", i, gotO, gotC, 1, 0) + } + } + return nil + }) +} + +func TestTableCacheFrequentlyUsed(t *testing.T) { + for i, iterType := range []string{"point", "range"} { + t.Run(fmt.Sprintf("iter=%s", iterType), func(t *testing.T) { + testTableCacheFrequentlyUsedInternal(t, i == 1) + }) + } +} + +func TestSharedTableCacheFrequentlyUsed(t *testing.T) { + const ( + N = 1000 + pinned0 = 7 + pinned1 = 11 + ) + tc := newTableCacheTest(8<<20, 2*tableCacheTestCacheSize, 16) + c1, fs1, err := newTableCacheContainerTest(tc, "") + require.NoError(t, err) + c2, fs2, err := newTableCacheContainerTest(tc, "") + require.NoError(t, err) + tc.Unref() + + for i := 0; i < N; i++ { + for _, j := range [...]int{pinned0, i % tableCacheTestNumTables, pinned1} { + m := &fileMetadata{FileNum: FileNum(j)} + m.InitPhysicalBacking() + m.Ref() + iter1, _, err := c1.newIters(context.Background(), m, nil, internalIterOpts{}) + if err != nil { + t.Fatalf("i=%d, j=%d: find: %v", i, j, err) + } + iter2, _, err := c2.newIters(context.Background(), m, nil, internalIterOpts{}) + if err != nil { + t.Fatalf("i=%d, j=%d: find: %v", i, j, err) + } + + if err := iter1.Close(); err != nil { + t.Fatalf("i=%d, j=%d: close: %v", i, j, err) + } + if err := iter2.Close(); err != nil { + t.Fatalf("i=%d, j=%d: close: %v", i, j, err) + } + } + } + + fs1.validate(t, c1, func(i, gotO, gotC int) error { + if i == pinned0 || i == pinned1 { + if gotO != 1 || gotC != 0 { + return errors.Errorf("i=%d: pinned table: got %d, %d, want %d, %d", i, gotO, gotC, 1, 0) + } + } + return nil + }) + + fs2.validate(t, c2, func(i, gotO, gotC int) error { + if i == pinned0 || i == pinned1 { + if gotO != 1 || gotC != 0 { + return errors.Errorf("i=%d: pinned table: got %d, %d, want %d, %d", i, gotO, gotC, 1, 0) + } + } + return nil + }) +} + +func testTableCacheEvictionsInternal(t *testing.T, rangeIter bool) { + const ( + N = 1000 + lo, hi = 10, 20 + ) + c, fs, err := newTableCacheContainerTest(nil, "") + require.NoError(t, err) + + rng := rand.New(rand.NewSource(2)) + for i := 0; i < N; i++ { + j := rng.Intn(tableCacheTestNumTables) + var iter io.Closer + var err error + m := &fileMetadata{FileNum: FileNum(j)} + m.InitPhysicalBacking() + m.Ref() + if rangeIter { + iter, err = c.newRangeKeyIter(m, keyspan.SpanIterOptions{}) + } else { + iter, _, err = c.newIters(context.Background(), m, nil, internalIterOpts{}) + } + if err != nil { + t.Fatalf("i=%d, j=%d: find: %v", i, j, err) + } + if err := iter.Close(); err != nil { + t.Fatalf("i=%d, j=%d: close: %v", i, j, err) + } + + c.evict(base.FileNum(lo + rng.Uint64n(hi-lo)).DiskFileNum()) + } + + sumEvicted, nEvicted := 0, 0 + sumSafe, nSafe := 0, 0 + fs.validate(t, c, func(i, gotO, gotC int) error { + if lo <= i && i < hi { + sumEvicted += gotO + nEvicted++ + } else { + sumSafe += gotO + nSafe++ + } + return nil + }) + fEvicted := float64(sumEvicted) / float64(nEvicted) + fSafe := float64(sumSafe) / float64(nSafe) + // The magic 1.25 number isn't derived from formal modeling. It's just a guess. For + // (lo, hi, tableCacheTestCacheSize, tableCacheTestNumTables) = (10, 20, 100, 300), + // the ratio seems to converge on roughly 1.5 for large N, compared to 1.0 if we do + // not evict any cache entries. + if ratio := fEvicted / fSafe; ratio < 1.25 { + t.Errorf("evicted tables were opened %.3f times on average, safe tables %.3f, ratio %.3f < 1.250", + fEvicted, fSafe, ratio) + } +} + +func TestTableCacheEvictions(t *testing.T) { + for i, iterType := range []string{"point", "range"} { + t.Run(fmt.Sprintf("iter=%s", iterType), func(t *testing.T) { + testTableCacheEvictionsInternal(t, i == 1) + }) + } +} + +func TestSharedTableCacheEvictions(t *testing.T) { + const ( + N = 1000 + lo, hi = 10, 20 + ) + tc := newTableCacheTest(8<<20, 2*tableCacheTestCacheSize, 16) + c1, fs1, err := newTableCacheContainerTest(tc, "") + require.NoError(t, err) + c2, fs2, err := newTableCacheContainerTest(tc, "") + require.NoError(t, err) + tc.Unref() + + rng := rand.New(rand.NewSource(2)) + for i := 0; i < N; i++ { + j := rng.Intn(tableCacheTestNumTables) + m := &fileMetadata{FileNum: FileNum(j)} + m.InitPhysicalBacking() + m.Ref() + iter1, _, err := c1.newIters(context.Background(), m, nil, internalIterOpts{}) + if err != nil { + t.Fatalf("i=%d, j=%d: find: %v", i, j, err) + } + + iter2, _, err := c2.newIters(context.Background(), m, nil, internalIterOpts{}) + if err != nil { + t.Fatalf("i=%d, j=%d: find: %v", i, j, err) + } + + if err := iter1.Close(); err != nil { + t.Fatalf("i=%d, j=%d: close: %v", i, j, err) + } + + if err := iter2.Close(); err != nil { + t.Fatalf("i=%d, j=%d: close: %v", i, j, err) + } + + c1.evict(base.FileNum(lo + rng.Uint64n(hi-lo)).DiskFileNum()) + c2.evict(base.FileNum(lo + rng.Uint64n(hi-lo)).DiskFileNum()) + } + + check := func(fs *tableCacheTestFS, c *tableCacheContainer) (float64, float64, float64) { + sumEvicted, nEvicted := 0, 0 + sumSafe, nSafe := 0, 0 + fs.validate(t, c, func(i, gotO, gotC int) error { + if lo <= i && i < hi { + sumEvicted += gotO + nEvicted++ + } else { + sumSafe += gotO + nSafe++ + } + return nil + }) + fEvicted := float64(sumEvicted) / float64(nEvicted) + fSafe := float64(sumSafe) / float64(nSafe) + + return fEvicted, fSafe, fEvicted / fSafe + } + + // The magic 1.25 number isn't derived from formal modeling. It's just a guess. For + // (lo, hi, tableCacheTestCacheSize, tableCacheTestNumTables) = (10, 20, 100, 300), + // the ratio seems to converge on roughly 1.5 for large N, compared to 1.0 if we do + // not evict any cache entries. + if fEvicted, fSafe, ratio := check(fs1, c1); ratio < 1.25 { + t.Errorf( + "evicted tables were opened %.3f times on average, safe tables %.3f, ratio %.3f < 1.250", + fEvicted, fSafe, ratio, + ) + } + + if fEvicted, fSafe, ratio := check(fs2, c2); ratio < 1.25 { + t.Errorf( + "evicted tables were opened %.3f times on average, safe tables %.3f, ratio %.3f < 1.250", + fEvicted, fSafe, ratio, + ) + } +} + +func TestTableCacheIterLeak(t *testing.T) { + c, _, err := newTableCacheContainerTest(nil, "") + require.NoError(t, err) + + m := &fileMetadata{FileNum: 0} + m.InitPhysicalBacking() + m.Ref() + defer m.Unref() + iter, _, err := c.newIters(context.Background(), m, nil, internalIterOpts{}) + require.NoError(t, err) + + if err := c.close(); err == nil { + t.Fatalf("expected failure, but found success") + } else if !strings.HasPrefix(err.Error(), "leaked iterators:") { + t.Fatalf("expected leaked iterators, but found %+v", err) + } else { + t.Log(err.Error()) + } + require.NoError(t, iter.Close()) +} + +func TestSharedTableCacheIterLeak(t *testing.T) { + tc := newTableCacheTest(8<<20, 2*tableCacheTestCacheSize, 16) + c1, _, err := newTableCacheContainerTest(tc, "") + require.NoError(t, err) + c2, _, err := newTableCacheContainerTest(tc, "") + require.NoError(t, err) + c3, _, err := newTableCacheContainerTest(tc, "") + require.NoError(t, err) + tc.Unref() + + m := &fileMetadata{FileNum: 0} + m.InitPhysicalBacking() + m.Ref() + defer m.Unref() + iter, _, err := c1.newIters(context.Background(), m, nil, internalIterOpts{}) + require.NoError(t, err) + + if err := c1.close(); err == nil { + t.Fatalf("expected failure, but found success") + } else if !strings.HasPrefix(err.Error(), "leaked iterators:") { + t.Fatalf("expected leaked iterators, but found %+v", err) + } else { + t.Log(err.Error()) + } + + // Closing c2 shouldn't error out since c2 isn't leaking any iterators. + require.NoError(t, c2.close()) + + // Closing c3 should error out since c3 holds the last reference to + // the TableCache, and when the TableCache closes, it will detect + // that there was a leaked iterator. + if err := c3.close(); err == nil { + t.Fatalf("expected failure, but found success") + } else if !strings.HasPrefix(err.Error(), "leaked iterators:") { + t.Fatalf("expected leaked iterators, but found %+v", err) + } else { + t.Log(err.Error()) + } + + require.NoError(t, iter.Close()) +} + +func TestTableCacheRetryAfterFailure(t *testing.T) { + // Test a retry can succeed after a failure, i.e., errors are not cached. + c, fs, err := newTableCacheContainerTest(nil, "") + require.NoError(t, err) + + fs.setOpenError(true /* enabled */) + m := &fileMetadata{FileNum: 0} + m.InitPhysicalBacking() + m.Ref() + defer m.Unref() + if _, _, err = c.newIters(context.Background(), m, nil, internalIterOpts{}); err == nil { + t.Fatalf("expected failure, but found success") + } + require.Equal(t, "pebble: backing file 000000 error: injected error", err.Error()) + fs.setOpenError(false /* enabled */) + var iter internalIterator + iter, _, err = c.newIters(context.Background(), m, nil, internalIterOpts{}) + require.NoError(t, err) + require.NoError(t, iter.Close()) + fs.validate(t, c, nil) +} + +// memFile is a file-like struct that buffers all data written to it in memory. +// Implements the objstorage.Writable interface. +type memFile struct { + buf bytes.Buffer +} + +var _ objstorage.Writable = (*memFile)(nil) + +// Finish is part of the objstorage.Writable interface. +func (*memFile) Finish() error { + return nil +} + +// Abort is part of the objstorage.Writable interface. +func (*memFile) Abort() {} + +// Write is part of the objstorage.Writable interface. +func (f *memFile) Write(p []byte) error { + _, err := f.buf.Write(p) + return err +} + +func TestTableCacheErrorBadMagicNumber(t *testing.T) { + var file memFile + tw := sstable.NewWriter(&file, sstable.WriterOptions{TableFormat: sstable.TableFormatPebblev2}) + tw.Set([]byte("a"), nil) + require.NoError(t, tw.Close()) + buf := file.buf.Bytes() + // Bad magic number. + buf[len(buf)-1] = 0 + fs := &tableCacheTestFS{ + FS: vfs.NewMem(), + } + const testFileNum = 3 + objProvider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(fs, "")) + require.NoError(t, err) + w, _, err := objProvider.Create(context.Background(), fileTypeTable, + base.FileNum(testFileNum).DiskFileNum(), objstorage.CreateOptions{}) + w.Write(buf) + require.NoError(t, w.Finish()) + opts := &Options{} + opts.EnsureDefaults() + opts.Cache = NewCache(8 << 20) // 8 MB + defer opts.Cache.Unref() + c := newTableCacheContainer(nil, opts.Cache.NewID(), objProvider, opts, tableCacheTestCacheSize, + &sstable.CategoryStatsCollector{}) + require.NoError(t, err) + defer c.close() + + m := &fileMetadata{FileNum: testFileNum} + m.InitPhysicalBacking() + m.Ref() + defer m.Unref() + if _, _, err = c.newIters(context.Background(), m, nil, internalIterOpts{}); err == nil { + t.Fatalf("expected failure, but found success") + } + require.Equal(t, + "pebble: backing file 000003 error: pebble/table: invalid table (bad magic number: 0xf09faab3f09faa00)", + err.Error()) +} + +func TestTableCacheEvictClose(t *testing.T) { + errs := make(chan error, 10) + db, err := Open("test", + &Options{ + FS: vfs.NewMem(), + EventListener: &EventListener{ + TableDeleted: func(info TableDeleteInfo) { + errs <- info.Err + }, + }, + }) + require.NoError(t, err) + + start := []byte("a") + end := []byte("z") + require.NoError(t, db.Set(start, nil, nil)) + require.NoError(t, db.Flush()) + require.NoError(t, db.DeleteRange(start, end, nil)) + require.NoError(t, db.Compact(start, end, false)) + require.NoError(t, db.Close()) + close(errs) + + for err := range errs { + require.NoError(t, err) + } +} + +func TestTableCacheClockPro(t *testing.T) { + // Test data was generated from the python code. See also + // internal/cache/clockpro_test.go:TestCache. + f, err := os.Open("internal/cache/testdata/cache") + require.NoError(t, err) + + mem := vfs.NewMem() + objProvider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(mem, "")) + require.NoError(t, err) + defer objProvider.Close() + + makeTable := func(dfn base.DiskFileNum) { + require.NoError(t, err) + f, _, err := objProvider.Create(context.Background(), fileTypeTable, dfn, objstorage.CreateOptions{}) + require.NoError(t, err) + w := sstable.NewWriter(f, sstable.WriterOptions{}) + require.NoError(t, w.Set([]byte("a"), nil)) + require.NoError(t, w.Close()) + } + + opts := &Options{ + Cache: NewCache(8 << 20), // 8 MB + } + opts.EnsureDefaults() + defer opts.Cache.Unref() + + cache := &tableCacheShard{} + // NB: The table cache size of 200 is required for the expected test values. + cache.init(200) + dbOpts := &tableCacheOpts{} + dbOpts.loggerAndTracer = &base.LoggerWithNoopTracer{Logger: opts.Logger} + dbOpts.cacheID = 0 + dbOpts.objProvider = objProvider + dbOpts.opts = opts.MakeReaderOptions() + + scanner := bufio.NewScanner(f) + tables := make(map[int]bool) + line := 1 + + for scanner.Scan() { + fields := bytes.Fields(scanner.Bytes()) + + key, err := strconv.Atoi(string(fields[0])) + require.NoError(t, err) + + // Ensure that underlying sstables exist on disk, creating each table the + // first time it is seen. + if !tables[key] { + makeTable(base.FileNum(uint64(key)).DiskFileNum()) + tables[key] = true + } + + oldHits := cache.hits.Load() + m := &fileMetadata{FileNum: FileNum(key)} + m.InitPhysicalBacking() + m.Ref() + v := cache.findNode(m, dbOpts) + cache.unrefValue(v) + + hit := cache.hits.Load() != oldHits + wantHit := fields[1][0] == 'h' + if hit != wantHit { + t.Errorf("%d: cache hit mismatch: got %v, want %v\n", line, hit, wantHit) + } + line++ + m.Unref() + } +} + +func BenchmarkNewItersAlloc(b *testing.B) { + opts := &Options{ + FS: vfs.NewMem(), + FormatMajorVersion: internalFormatNewest, + } + d, err := Open("", opts) + require.NoError(b, err) + defer func() { require.NoError(b, d.Close()) }() + + require.NoError(b, d.Set([]byte{'a'}, []byte{'a'}, nil)) + require.NoError(b, d.Flush()) + require.NoError(b, d.Compact([]byte{'a'}, []byte{'z'}, false)) + + d.mu.Lock() + currVersion := d.mu.versions.currentVersion() + it := currVersion.Levels[6].Iter() + m := it.First() + require.NotNil(b, m) + d.mu.Unlock() + + // Open once so that the Reader is cached. + iter, _, err := d.newIters(context.Background(), m, nil, internalIterOpts{}) + require.NoError(b, iter.Close()) + require.NoError(b, err) + + for i := 0; i < b.N; i++ { + b.StartTimer() + iter, _, err := d.newIters(context.Background(), m, nil, internalIterOpts{}) + b.StopTimer() + require.NoError(b, err) + require.NoError(b, iter.Close()) + } +} + +// TestTableCacheNoSuchFileError verifies that when the table cache hits a "no +// such file" error, it generates a useful fatal message. +func TestTableCacheNoSuchFileError(t *testing.T) { + const dirname = "test" + mem := vfs.NewMem() + logger := &catchFatalLogger{} + + d, err := Open(dirname, &Options{ + FS: mem, + Logger: logger, + }) + require.NoError(t, err) + defer func() { _ = d.Close() }() + require.NoError(t, d.Set([]byte("a"), []byte("val_a"), nil)) + require.NoError(t, d.Set([]byte("b"), []byte("val_b"), nil)) + require.NoError(t, d.Flush()) + ls, err := mem.List(dirname) + require.NoError(t, err) + + // Find the sst file. + var sst string + for _, file := range ls { + if strings.HasSuffix(file, ".sst") { + if sst != "" { + t.Fatalf("multiple SSTs found: %s, %s", sst, file) + } + sst = file + } + } + if sst == "" { + t.Fatalf("no SST found after flush") + } + require.NoError(t, mem.Remove(path.Join(dirname, sst))) + + _, _, _ = d.Get([]byte("a")) + require.NotZero(t, len(logger.fatalMsgs), "no fatal message emitted") + require.Equal(t, 1, len(logger.fatalMsgs), "expected one fatal message; got: %v", logger.fatalMsgs) + require.Contains(t, logger.fatalMsgs[0], "directory contains 6 files, 0 unknown, 0 tables, 2 logs, 1 manifests") +} + +func BenchmarkTableCacheHotPath(b *testing.B) { + mem := vfs.NewMem() + objProvider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(mem, "")) + require.NoError(b, err) + defer objProvider.Close() + + makeTable := func(dfn base.DiskFileNum) { + require.NoError(b, err) + f, _, err := objProvider.Create(context.Background(), fileTypeTable, dfn, objstorage.CreateOptions{}) + require.NoError(b, err) + w := sstable.NewWriter(f, sstable.WriterOptions{}) + require.NoError(b, w.Set([]byte("a"), nil)) + require.NoError(b, w.Close()) + } + + opts := &Options{ + Cache: NewCache(8 << 20), // 8 MB + } + opts.EnsureDefaults() + defer opts.Cache.Unref() + + cache := &tableCacheShard{} + cache.init(2) + dbOpts := &tableCacheOpts{} + dbOpts.loggerAndTracer = &base.LoggerWithNoopTracer{Logger: opts.Logger} + dbOpts.cacheID = 0 + dbOpts.objProvider = objProvider + dbOpts.opts = opts.MakeReaderOptions() + + makeTable(1) + + m := &fileMetadata{FileNum: 1} + m.InitPhysicalBacking() + m.Ref() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + v := cache.findNode(m, dbOpts) + cache.unrefValue(v) + } +} + +type catchFatalLogger struct { + fatalMsgs []string +} + +var _ Logger = (*catchFatalLogger)(nil) + +func (tl *catchFatalLogger) Infof(format string, args ...interface{}) {} +func (tl *catchFatalLogger) Errorf(format string, args ...interface{}) {} + +func (tl *catchFatalLogger) Fatalf(format string, args ...interface{}) { + tl.fatalMsgs = append(tl.fatalMsgs, fmt.Sprintf(format, args...)) +} diff --git a/pebble/table_stats.go b/pebble/table_stats.go new file mode 100644 index 0000000..76c940c --- /dev/null +++ b/pebble/table_stats.go @@ -0,0 +1,1086 @@ +// Copyright 2020 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 ( + "fmt" + "math" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/sstable" +) + +// In-memory statistics about tables help inform compaction picking, but may +// be expensive to calculate or load from disk. Every time a database is +// opened, these statistics must be reloaded or recalculated. To minimize +// impact on user activity and compactions, we load these statistics +// asynchronously in the background and store loaded statistics in each +// table's *FileMetadata. +// +// This file implements the asynchronous loading of statistics by maintaining +// a list of files that require statistics, alongside their LSM levels. +// Whenever new files are added to the LSM, the files are appended to +// d.mu.tableStats.pending. If a stats collection job is not currently +// running, one is started in a separate goroutine. +// +// The stats collection job grabs and clears the pending list, computes table +// statistics relative to the current readState and updates the tables' file +// metadata. New pending files may accumulate during a stats collection job, +// so a completing job triggers a new job if necessary. Only one job runs at a +// time. +// +// When an existing database is opened, all files lack in-memory statistics. +// These files' stats are loaded incrementally whenever the pending list is +// empty by scanning a current readState for files missing statistics. Once a +// job completes a scan without finding any remaining files without +// statistics, it flips a `loadedInitial` flag. From then on, the stats +// collection job only needs to load statistics for new files appended to the +// pending list. + +func (d *DB) maybeCollectTableStatsLocked() { + if d.shouldCollectTableStatsLocked() { + go d.collectTableStats() + } +} + +// updateTableStatsLocked is called when new files are introduced, after the +// read state has been updated. It may trigger a new stat collection. +// DB.mu must be locked when calling. +func (d *DB) updateTableStatsLocked(newFiles []manifest.NewFileEntry) { + var needStats bool + for _, nf := range newFiles { + if !nf.Meta.StatsValid() { + needStats = true + break + } + } + if !needStats { + return + } + + d.mu.tableStats.pending = append(d.mu.tableStats.pending, newFiles...) + d.maybeCollectTableStatsLocked() +} + +func (d *DB) shouldCollectTableStatsLocked() bool { + return !d.mu.tableStats.loading && + d.closed.Load() == nil && + !d.opts.private.disableTableStats && + (len(d.mu.tableStats.pending) > 0 || !d.mu.tableStats.loadedInitial) +} + +// collectTableStats runs a table stats collection job, returning true if the +// invocation did the collection work, false otherwise (e.g. if another job was +// already running). +func (d *DB) collectTableStats() bool { + const maxTableStatsPerScan = 50 + + d.mu.Lock() + if !d.shouldCollectTableStatsLocked() { + d.mu.Unlock() + return false + } + + pending := d.mu.tableStats.pending + d.mu.tableStats.pending = nil + d.mu.tableStats.loading = true + jobID := d.mu.nextJobID + d.mu.nextJobID++ + loadedInitial := d.mu.tableStats.loadedInitial + // Drop DB.mu before performing IO. + d.mu.Unlock() + + // Every run of collectTableStats either collects stats from the pending + // list (if non-empty) or from scanning the version (loadedInitial is + // false). This job only runs if at least one of those conditions holds. + + // Grab a read state to scan for tables. + rs := d.loadReadState() + var collected []collectedStats + var hints []deleteCompactionHint + if len(pending) > 0 { + collected, hints = d.loadNewFileStats(rs, pending) + } else { + var moreRemain bool + var buf [maxTableStatsPerScan]collectedStats + collected, hints, moreRemain = d.scanReadStateTableStats(rs, buf[:0]) + loadedInitial = !moreRemain + } + rs.unref() + + // Update the FileMetadata with the loaded stats while holding d.mu. + d.mu.Lock() + defer d.mu.Unlock() + d.mu.tableStats.loading = false + if loadedInitial && !d.mu.tableStats.loadedInitial { + d.mu.tableStats.loadedInitial = loadedInitial + d.opts.EventListener.TableStatsLoaded(TableStatsInfo{ + JobID: jobID, + }) + } + + maybeCompact := false + for _, c := range collected { + c.fileMetadata.Stats = c.TableStats + maybeCompact = maybeCompact || fileCompensation(c.fileMetadata) > 0 + c.fileMetadata.StatsMarkValid() + } + d.mu.tableStats.cond.Broadcast() + d.maybeCollectTableStatsLocked() + if len(hints) > 0 && !d.opts.private.disableDeleteOnlyCompactions { + // Verify that all of the hint tombstones' files still exist in the + // current version. Otherwise, the tombstone itself may have been + // compacted into L6 and more recent keys may have had their sequence + // numbers zeroed. + // + // Note that it's possible that the tombstone file is being compacted + // presently. In that case, the file will be present in v. When the + // compaction finishes compacting the tombstone file, it will detect + // and clear the hint. + // + // See DB.maybeUpdateDeleteCompactionHints. + v := d.mu.versions.currentVersion() + keepHints := hints[:0] + for _, h := range hints { + if v.Contains(h.tombstoneLevel, d.cmp, h.tombstoneFile) { + keepHints = append(keepHints, h) + } + } + d.mu.compact.deletionHints = append(d.mu.compact.deletionHints, keepHints...) + } + if maybeCompact { + d.maybeScheduleCompaction() + } + return true +} + +type collectedStats struct { + *fileMetadata + manifest.TableStats +} + +func (d *DB) loadNewFileStats( + rs *readState, pending []manifest.NewFileEntry, +) ([]collectedStats, []deleteCompactionHint) { + var hints []deleteCompactionHint + collected := make([]collectedStats, 0, len(pending)) + for _, nf := range pending { + // A file's stats might have been populated by an earlier call to + // loadNewFileStats if the file was moved. + // NB: We're not holding d.mu which protects f.Stats, but only + // 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.StatsValid() { + continue + } + + // The file isn't guaranteed to still be live in the readState's + // version. It may have been deleted or moved. Skip it if it's not in + // the expected level. + if !rs.current.Contains(nf.Level, d.cmp, nf.Meta) { + continue + } + + stats, newHints, err := d.loadTableStats( + rs.current, nf.Level, + nf.Meta, + ) + if err != nil { + d.opts.EventListener.BackgroundError(err) + continue + } + // NB: We don't update the FileMetadata yet, because we aren't + // holding DB.mu. We'll copy it to the FileMetadata after we're + // finished with IO. + collected = append(collected, collectedStats{ + fileMetadata: nf.Meta, + TableStats: stats, + }) + hints = append(hints, newHints...) + } + return collected, hints +} + +// scanReadStateTableStats is run by an active stat collection job when there +// are no pending new files, but there might be files that existed at Open for +// which we haven't loaded table stats. +func (d *DB) scanReadStateTableStats( + rs *readState, fill []collectedStats, +) ([]collectedStats, []deleteCompactionHint, bool) { + moreRemain := false + var hints []deleteCompactionHint + sizesChecked := make(map[base.DiskFileNum]struct{}) + for l, levelMetadata := range rs.current.Levels { + iter := levelMetadata.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + // NB: We're not holding d.mu which protects f.Stats, but only the + // active stats collection job updates f.Stats for active files, + // 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.StatsValid() { + continue + } + + // Limit how much work we do per read state. The older the read + // state is, the higher the likelihood files are no longer being + // used in the current version. If we've exhausted our allowance, + // return true for the last return value to signal there's more + // work to do. + if len(fill) == cap(fill) { + moreRemain = true + return fill, hints, moreRemain + } + + // If the file is remote and not SharedForeign, we should check if its size + // matches. This is because checkConsistency skips over remote files. + // + // SharedForeign and External files are skipped as their sizes are allowed + // to have a mismatch; the size stored in the FileBacking is just the part + // of the file that is referenced by this Pebble instance, not the size of + // the whole object. + objMeta, err := d.objProvider.Lookup(fileTypeTable, f.FileBacking.DiskFileNum) + if err != nil { + // Set `moreRemain` so we'll try again. + moreRemain = true + d.opts.EventListener.BackgroundError(err) + continue + } + + shouldCheckSize := objMeta.IsRemote() && + !d.objProvider.IsSharedForeign(objMeta) && + !objMeta.IsExternal() + if _, ok := sizesChecked[f.FileBacking.DiskFileNum]; !ok && shouldCheckSize { + size, err := d.objProvider.Size(objMeta) + fileSize := f.FileBacking.Size + if err != nil { + moreRemain = true + d.opts.EventListener.BackgroundError(err) + continue + } + if size != int64(fileSize) { + err := errors.Errorf( + "during consistency check in loadTableStats: L%d: %s: object size mismatch (%s): %d (provider) != %d (MANIFEST)", + errors.Safe(l), f.FileNum, d.objProvider.Path(objMeta), + errors.Safe(size), errors.Safe(fileSize)) + d.opts.EventListener.BackgroundError(err) + d.opts.Logger.Fatalf("%s", err) + } + + sizesChecked[f.FileBacking.DiskFileNum] = struct{}{} + } + + stats, newHints, err := d.loadTableStats( + rs.current, l, f, + ) + if err != nil { + // Set `moreRemain` so we'll try again. + moreRemain = true + d.opts.EventListener.BackgroundError(err) + continue + } + fill = append(fill, collectedStats{ + fileMetadata: f, + TableStats: stats, + }) + hints = append(hints, newHints...) + } + } + return fill, hints, moreRemain +} + +func (d *DB) loadTableStats( + v *version, level int, meta *fileMetadata, +) (manifest.TableStats, []deleteCompactionHint, error) { + var stats manifest.TableStats + var compactionHints []deleteCompactionHint + 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 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 = props.NumRangeKeySets + stats.ValueBlocksSize = props.ValueBlocksSize + return + }) + if err != nil { + return stats, nil, err + } + return stats, compactionHints, nil +} + +// loadTablePointKeyStats calculates the point key statistics for the given +// table. The provided manifest.TableStats are updated. +func (d *DB) loadTablePointKeyStats( + 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 + // of the size of the keys beneath the point tombstones. + // 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. + avgValLogicalSize, compressionRatio, err := d.estimateSizesBeneath(v, level, meta, props) + if err != nil { + return err + } + stats.PointDeletionsBytesEstimate = + 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.CommonReader, v *version, level int, meta *fileMetadata, stats *manifest.TableStats, +) ([]deleteCompactionHint, error) { + iter, err := newCombinedDeletionKeyspanIter(d.opts.Comparer, r, meta) + if err != nil { + return nil, err + } + defer iter.Close() + var compactionHints []deleteCompactionHint + // We iterate over the defragmented range tombstones and range key deletions, + // which ensures we don't double count ranges deleted at different sequence + // numbers. Also, merging abutting tombstones reduces the number of calls to + // estimateReclaimedSizeBeneath which is costly, and improves the accuracy of + // our overall estimate. + 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 + // RANGEDELs. + var maxRangeDeleteSeqNum uint64 + for _, k := range s.Keys { + 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 + // 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 level == numLevels-1 && meta.SmallestSeqNum < maxRangeDeleteSeqNum { + size, err := r.EstimateDiskUsage(start, end) + if err != nil { + return nil, err + } + stats.RangeDeletionsBytesEstimate += size + + // As the file is in the bottommost level, there is no need to collect a + // deletion hint. + continue + } + + // While the size estimates for point keys should only be updated if this + // span contains a range del, the sequence numbers are required for the + // hint. Unconditionally descend, but conditionally update the estimates. + hintType := compactionHintFromKeys(s.Keys) + estimate, hintSeqNum, err := d.estimateReclaimedSizeBeneath(v, level, start, end, hintType) + if err != nil { + return nil, err + } + stats.RangeDeletionsBytesEstimate += estimate + + // If any files were completely contained with the range, + // hintSeqNum is the smallest sequence number contained in any + // such file. + if hintSeqNum == math.MaxUint64 { + continue + } + hint := deleteCompactionHint{ + hintType: hintType, + start: make([]byte, len(start)), + end: make([]byte, len(end)), + tombstoneFile: meta, + tombstoneLevel: level, + tombstoneLargestSeqNum: s.LargestSeqNum(), + tombstoneSmallestSeqNum: s.SmallestSeqNum(), + fileSmallestSeqNum: hintSeqNum, + } + copy(hint.start, start) + copy(hint.end, end) + compactionHints = append(compactionHints, hint) + } + return compactionHints, err +} + +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() { + 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 + } + } + } + if entryCount == 0 { + return 0, 0, nil + } + // 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 value size + // + // ↓ + // + // FileSize RawValueSize + // ----------------------- × ------------ + // RawKeySize+RawValueSize NumEntries + // + // 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( + v *version, level int, start, end []byte, hintType deleteCompactionHintType, +) (estimate uint64, hintSeqNum uint64, err error) { + // Find all files in lower levels that overlap with the deleted range + // [start, end). + // + // An overlapping file might be completely contained by the range + // tombstone, in which case we can count the entire file size in + // our estimate without doing any additional I/O. + // + // Otherwise, estimating the range for the file requires + // additional I/O to read the file's index blocks. + hintSeqNum = math.MaxUint64 + for l := level + 1; l < numLevels; l++ { + overlaps := v.Overlaps(l, d.cmp, start, end, true /* exclusiveEnd */) + iter := overlaps.Iter() + for file := iter.First(); file != nil; file = iter.Next() { + startCmp := d.cmp(start, file.Smallest.UserKey) + endCmp := d.cmp(file.Largest.UserKey, end) + if startCmp <= 0 && (endCmp < 0 || endCmp == 0 && file.Largest.IsExclusiveSentinel()) { + // The range fully contains the file, so skip looking it up in table + // cache/looking at its indexes and add the full file size. Whether the + // disk estimate and hint seqnums are updated depends on a) the type of + // hint that requested the estimate and b) the keys contained in this + // current file. + var updateEstimates, updateHints bool + switch hintType { + case deleteCompactionHintTypePointKeyOnly: + // The range deletion byte estimates should only be updated if this + // table contains point keys. This ends up being an overestimate in + // the case that table also has range keys, but such keys are expected + // to contribute a negligible amount of the table's overall size, + // relative to point keys. + if file.HasPointKeys { + updateEstimates = true + } + // As the initiating span contained only range dels, hints can only be + // updated if this table does _not_ contain range keys. + if !file.HasRangeKeys { + updateHints = true + } + case deleteCompactionHintTypeRangeKeyOnly: + // The initiating span contained only range key dels. The estimates + // apply only to point keys, and are therefore not updated. + updateEstimates = false + // As the initiating span contained only range key dels, hints can + // only be updated if this table does _not_ contain point keys. + if !file.HasPointKeys { + updateHints = true + } + case deleteCompactionHintTypePointAndRangeKey: + // Always update the estimates and hints, as this hint type can drop a + // file, irrespective of the mixture of keys. Similar to above, the + // range del bytes estimates is an overestimate. + updateEstimates, updateHints = true, true + default: + panic(fmt.Sprintf("pebble: unknown hint type %s", hintType)) + } + if updateEstimates { + estimate += file.Size + } + if updateHints && hintSeqNum > file.SmallestSeqNum { + hintSeqNum = file.SmallestSeqNum + } + } else if d.cmp(file.Smallest.UserKey, end) <= 0 && d.cmp(start, file.Largest.UserKey) <= 0 { + // Partial overlap. + if hintType == deleteCompactionHintTypeRangeKeyOnly { + // If the hint that generated this overlap contains only range keys, + // there is no need to calculate disk usage, as the reclaimable space + // is expected to be minimal relative to point keys. + continue + } + var size uint64 + 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 + } + estimate += size + } + } + } + return estimate, hintSeqNum, nil +} + +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: + // + // 1. Estimating the potential for reclaimed space due to a range deletion + // tombstone requires scanning the LSM - a potentially expensive operation + // that should be deferred. + // 2. Range deletions and / or range key deletions present an opportunity to + // compute "deletion hints", which also requires a scan of the LSM to + // compute tables that would be eligible for deletion. + // + // These two tasks are deferred to the table stats collector goroutine. + if props.NumRangeDeletions != 0 || props.NumRangeKeyDels != 0 { + return false + } + + // 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 + } + + var pointEstimate uint64 + if props.NumEntries > 0 { + // Use the file's own average key and value sizes as an estimate. This + // 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. + commonProps := &props.CommonProperties + avgValSize, compressionRatio := estimatePhysicalSizes(meta.Size, commonProps) + pointEstimate = pointDeletionsBytesEstimate(meta.Size, commonProps, avgValSize, compressionRatio) + } + + meta.Stats.NumEntries = props.NumEntries + meta.Stats.NumDeletions = props.NumDeletions + meta.Stats.NumRangeKeySets = props.NumRangeKeySets + meta.Stats.PointDeletionsBytesEstimate = pointEstimate + meta.Stats.RangeDeletionsBytesEstimate = 0 + meta.Stats.ValueBlocksSize = props.ValueBlocksSize + meta.StatsMarkValid() + return true +} + +func pointDeletionsBytesEstimate( + fileSize uint64, props *sstable.CommonProperties, avgValLogicalSize, compressionRatio float64, +) (estimate uint64) { + if props.NumEntries == 0 { + return 0 + } + 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. + // + // 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. + // + // 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 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 value size + // + // ↓ + // + // FileSize RawValSize + // ----------------------- × ---------- + // RawKeySize+RawValueSize NumEntries + // + uncompressedSum := props.RawKeySize + props.RawValueSize + compressionRatio = float64(fileSize) / float64(uncompressedSum) + avgValLogicalSize = (float64(props.RawValueSize) / float64(props.NumEntries)) + return avgValLogicalSize, compressionRatio +} + +// newCombinedDeletionKeyspanIter returns a keyspan.FragmentIterator that +// returns "ranged deletion" spans for a single table, providing a combined view +// of both range deletion and range key deletion spans. The +// tableRangedDeletionIter is intended for use in the specific case of computing +// the statistics and deleteCompactionHints for a single table. +// +// As an example, consider the following set of spans from the range deletion +// and range key blocks of a table: +// +// |---------| |---------| |-------| RANGEKEYDELs +// |-----------|-------------| |-----| RANGEDELs +// __________________________________________________________ +// a b c d e f g h i j k l m n o p q r s t u v w x y z +// +// The tableRangedDeletionIter produces the following set of output spans, where +// '1' indicates a span containing only range deletions, '2' is a span +// containing only range key deletions, and '3' is a span containing a mixture +// of both range deletions and range key deletions. +// +// 1 3 1 3 2 1 3 2 +// |-----|---------|-----|---|-----| |---|-|-----| +// __________________________________________________________ +// a b c d e f g h i j k l m n o p q r s t u v w x y z +// +// Algorithm. +// +// The iterator first defragments the range deletion and range key blocks +// separately. During this defragmentation, the range key block is also filtered +// so that keys other than range key deletes are ignored. The range delete and +// range key delete keyspaces are then merged. +// +// Note that the only fragmentation introduced by merging is from where a range +// del span overlaps with a range key del span. Within the bounds of any overlap +// there is guaranteed to be no further fragmentation, as the constituent spans +// have already been defragmented. To the left and right of any overlap, the +// same reasoning applies. For example, +// +// |--------| |-------| RANGEKEYDEL +// |---------------------------| RANGEDEL +// |----1---|----3---|----1----|---2---| Merged, fragmented spans. +// __________________________________________________________ +// a b c d e f g h i j k l m n o p q r s t u v w x y z +// +// Any fragmented abutting spans produced by the merging iter will be of +// differing types (i.e. a transition from a span with homogenous key kinds to a +// heterogeneous span, or a transition from a span with exclusively range dels +// to a span with exclusively range key dels). Therefore, further +// defragmentation is not required. +// +// Each span returned by the tableRangeDeletionIter will have at most four keys, +// 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, 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. + var equal = keyspan.DefragmentMethodFunc(func(_ base.Equal, a, b *keyspan.Span) bool { return true }) + // Reduce keys by maintaining a slice of at most length two, corresponding to + // the largest and smallest keys in the defragmented span. This maintains the + // contract that the emitted slice is sorted by (SeqNum, Kind) descending. + reducer := func(current, incoming []keyspan.Key) []keyspan.Key { + if len(current) == 0 && len(incoming) == 0 { + // While this should never occur in practice, a defensive return is used + // here to preserve correctness. + return current + } + var largest, smallest keyspan.Key + var set bool + for _, keys := range [2][]keyspan.Key{current, incoming} { + if len(keys) == 0 { + continue + } + first, last := keys[0], keys[len(keys)-1] + if !set { + largest, smallest = first, last + set = true + continue + } + if first.Trailer > largest.Trailer { + largest = first + } + if last.Trailer < smallest.Trailer { + smallest = last + } + } + if largest.Equal(comparer.Equal, smallest) { + current = append(current[:0], largest) + } else { + current = append(current[:0], largest, smallest) + } + return current + } + + // The separate iters for the range dels and range keys are wrapped in a + // merging iter to join the keyspaces into a single keyspace. The separate + // iters are only added if the particular key kind is present. + mIter := &keyspan.MergingIter{} + var transform = keyspan.TransformerFunc(func(cmp base.Compare, in keyspan.Span, out *keyspan.Span) error { + if in.KeysOrder != keyspan.ByTrailerDesc { + panic("pebble: combined deletion iter encountered keys in non-trailer descending order") + } + out.Start, out.End = in.Start, in.End + out.Keys = append(out.Keys[:0], in.Keys...) + out.KeysOrder = keyspan.ByTrailerDesc + // NB: The order of by-trailer descending may have been violated, + // because we've layered rangekey and rangedel iterators from the same + // sstable into the same keyspan.MergingIter. The MergingIter will + // return the keys in the order that the child iterators were provided. + // Sort the keys to ensure they're sorted by trailer descending. + keyspan.SortKeysByTrailer(&out.Keys) + return nil + }) + mIter.Init(comparer.Compare, transform, new(keyspan.MergingBuffers)) + + iter, err := cr.NewRawRangeDelIter() + if err != nil { + return nil, err + } + if iter != nil { + dIter := &keyspan.DefragmentingIter{} + dIter.Init(comparer, iter, equal, reducer, new(keyspan.DefragmentingBuffers)) + iter = dIter + // 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, false, /* panicOnUpperTruncate */ + ) + mIter.AddLevel(iter) + } + + iter, err = cr.NewRawRangeKeyIter() + if err != nil { + return nil, err + } + if iter != nil { + // Wrap the range key iterator in a filter that elides keys other than range + // key deletions. + iter = keyspan.Filter(iter, func(in *keyspan.Span, out *keyspan.Span) (keep bool) { + out.Start, out.End = in.Start, in.End + out.Keys = out.Keys[:0] + for _, k := range in.Keys { + if k.Kind() != base.InternalKeyKindRangeKeyDelete { + continue + } + 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 + mIter.AddLevel(iter) + } + + return mIter, nil +} + +// rangeKeySetsAnnotator implements manifest.Annotator, annotating B-Tree nodes +// with the sum of the files' counts of range key fragments. Its annotation type +// is a *uint64. The count of range key sets may change once a table's stats are +// loaded asynchronously, so its values are marked as cacheable only if a file's +// stats have been loaded. +type rangeKeySetsAnnotator struct{} + +var _ manifest.Annotator = rangeKeySetsAnnotator{} + +func (a rangeKeySetsAnnotator) Zero(dst interface{}) interface{} { + if dst == nil { + return new(uint64) + } + v := dst.(*uint64) + *v = 0 + return v +} + +func (a rangeKeySetsAnnotator) Accumulate( + f *fileMetadata, dst interface{}, +) (v interface{}, cacheOK bool) { + vptr := dst.(*uint64) + *vptr = *vptr + f.Stats.NumRangeKeySets + return vptr, f.StatsValid() +} + +func (a rangeKeySetsAnnotator) Merge(src interface{}, dst interface{}) interface{} { + srcV := src.(*uint64) + dstV := dst.(*uint64) + *dstV = *dstV + *srcV + return dstV +} + +// countRangeKeySetFragments counts the number of RANGEKEYSET keys across all +// files of the LSM. It only counts keys in files for which table stats have +// been loaded. It uses a b-tree annotator to cache intermediate values between +// calculations when possible. +func countRangeKeySetFragments(v *version) (count uint64) { + for l := 0; l < numLevels; l++ { + if v.RangeKeyLevels[l].Empty() { + continue + } + count += *v.RangeKeyLevels[l].Annotation(rangeKeySetsAnnotator{}).(*uint64) + } + return count +} + +// tombstonesAnnotator implements manifest.Annotator, annotating B-Tree nodes +// with the sum of the files' counts of tombstones (DEL, SINGLEDEL and RANGEDELk +// eys). Its annotation type is a *uint64. The count of tombstones may change +// once a table's stats are loaded asynchronously, so its values are marked as +// cacheable only if a file's stats have been loaded. +type tombstonesAnnotator struct{} + +var _ manifest.Annotator = tombstonesAnnotator{} + +func (a tombstonesAnnotator) Zero(dst interface{}) interface{} { + if dst == nil { + return new(uint64) + } + v := dst.(*uint64) + *v = 0 + return v +} + +func (a tombstonesAnnotator) Accumulate( + f *fileMetadata, dst interface{}, +) (v interface{}, cacheOK bool) { + vptr := dst.(*uint64) + *vptr = *vptr + f.Stats.NumDeletions + return vptr, f.StatsValid() +} + +func (a tombstonesAnnotator) Merge(src interface{}, dst interface{}) interface{} { + srcV := src.(*uint64) + dstV := dst.(*uint64) + *dstV = *dstV + *srcV + return dstV +} + +// countTombstones counts the number of tombstone (DEL, SINGLEDEL and RANGEDEL) +// internal keys across all files of the LSM. It only counts keys in files for +// which table stats have been loaded. It uses a b-tree annotator to cache +// intermediate values between calculations when possible. +func countTombstones(v *version) (count uint64) { + for l := 0; l < numLevels; l++ { + if v.Levels[l].Empty() { + continue + } + count += *v.Levels[l].Annotation(tombstonesAnnotator{}).(*uint64) + } + return count +} + +// valueBlocksSizeAnnotator implements manifest.Annotator, annotating B-Tree +// nodes with the sum of the files' Properties.ValueBlocksSize. Its annotation +// type is a *uint64. The value block size may change once a table's stats are +// loaded asynchronously, so its values are marked as cacheable only if a +// file's stats have been loaded. +type valueBlocksSizeAnnotator struct{} + +var _ manifest.Annotator = valueBlocksSizeAnnotator{} + +func (a valueBlocksSizeAnnotator) Zero(dst interface{}) interface{} { + if dst == nil { + return new(uint64) + } + v := dst.(*uint64) + *v = 0 + return v +} + +func (a valueBlocksSizeAnnotator) Accumulate( + f *fileMetadata, dst interface{}, +) (v interface{}, cacheOK bool) { + vptr := dst.(*uint64) + *vptr = *vptr + f.Stats.ValueBlocksSize + return vptr, f.StatsValid() +} + +func (a valueBlocksSizeAnnotator) Merge(src interface{}, dst interface{}) interface{} { + srcV := src.(*uint64) + dstV := dst.(*uint64) + *dstV = *dstV + *srcV + return dstV +} + +// valueBlocksSizeForLevel returns the Properties.ValueBlocksSize across all +// files for a level of the LSM. It only includes the size for files for which +// table stats have been loaded. It uses a b-tree annotator to cache +// intermediate values between calculations when possible. It must not be +// called concurrently. +// +// REQUIRES: 0 <= level <= numLevels. +func valueBlocksSizeForLevel(v *version, level int) (count uint64) { + if v.Levels[level].Empty() { + return 0 + } + return *v.Levels[level].Annotation(valueBlocksSizeAnnotator{}).(*uint64) +} diff --git a/pebble/table_stats_test.go b/pebble/table_stats_test.go new file mode 100644 index 0000000..3abece9 --- /dev/null +++ b/pebble/table_stats_test.go @@ -0,0 +1,277 @@ +// Copyright 2020 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 ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func TestTableStats(t *testing.T) { + // loadedInfo is protected by d.mu. + var loadedInfo *TableStatsInfo + opts := &Options{ + FS: vfs.NewMem(), + EventListener: &EventListener{ + TableStatsLoaded: func(info TableStatsInfo) { + loadedInfo = &info + }, + }, + } + opts.DisableAutomaticCompactions = true + opts.Comparer = testkeys.Comparer + opts.FormatMajorVersion = FormatRangeKeys + + d, err := Open("", opts) + require.NoError(t, err) + defer func() { + if d != nil { + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + } + }() + + datadriven.RunTest(t, "testdata/table_stats", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "disable": + d.mu.Lock() + d.opts.private.disableTableStats = true + d.mu.Unlock() + return "" + + case "enable": + d.mu.Lock() + d.opts.private.disableTableStats = false + d.maybeCollectTableStatsLocked() + d.mu.Unlock() + return "" + + case "define": + require.NoError(t, closeAllSnapshots(d)) + require.NoError(t, d.Close()) + loadedInfo = nil + + d, err = runDBDefineCmd(td, opts) + if err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "reopen": + require.NoError(t, d.Close()) + loadedInfo = nil + + // Open using existing file system. + d, err = Open("", opts) + require.NoError(t, err) + return "" + + case "batch": + b := d.NewBatch() + if err := runBatchDefineCmd(td, b); err != nil { + return err.Error() + } + b.Commit(nil) + return "" + + case "flush": + if err := d.Flush(); err != nil { + return err.Error() + } + + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "ingest": + if err = runBuildCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + if err = runIngestCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "metric": + m := d.Metrics() + // TODO(jackson): Make a generalized command that uses reflection to + // pull out arbitrary Metrics fields. + var buf bytes.Buffer + for _, arg := range td.CmdArgs { + switch arg.String() { + case "keys.missized-tombstones-count": + fmt.Fprintf(&buf, "%s: %d", arg.String(), m.Keys.MissizedTombstonesCount) + default: + return fmt.Sprintf("unrecognized metric %s", arg) + } + } + return buf.String() + + case "lsm": + d.mu.Lock() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "build": + if err := runBuildCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + return "" + + case "ingest-and-excise": + if err := runIngestAndExciseCmd(td, d, d.opts.FS); err != nil { + return err.Error() + } + // Wait for a possible flush. + d.mu.Lock() + for d.mu.compact.flushing { + d.mu.compact.cond.Wait() + } + d.mu.Unlock() + return "" + + case "wait-pending-table-stats": + return runTableStatsCmd(td, d) + + case "wait-loaded-initial": + d.mu.Lock() + for d.mu.tableStats.loading || !d.mu.tableStats.loadedInitial { + d.mu.tableStats.cond.Wait() + } + s := loadedInfo.String() + d.mu.Unlock() + return s + + case "compact": + if err := runCompactCmd(td, d); err != nil { + return err.Error() + } + d.mu.Lock() + // Disable the "dynamic base level" code for this test. + d.mu.versions.picker.forceBaseLevel1() + s := d.mu.versions.currentVersion().String() + d.mu.Unlock() + return s + + case "metadata-stats": + // Prints some metadata about some sstable which is currently in the + // latest version. + return runMetadataCommand(t, td, d) + + case "properties": + return runSSTablePropertiesCmd(t, td, d) + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestTableRangeDeletionIter(t *testing.T) { + var m *fileMetadata + cmp := base.DefaultComparer.Compare + fs := vfs.NewMem() + datadriven.RunTest(t, "testdata/table_stats_deletion_iter", func(t *testing.T, td *datadriven.TestData) string { + switch cmd := td.Cmd; cmd { + case "build": + f, err := fs.Create("tmp.sst") + if err != nil { + return err.Error() + } + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{ + TableFormat: sstable.TableFormatMax, + }) + m = &fileMetadata{} + for _, line := range strings.Split(td.Input, "\n") { + s := keyspan.ParseSpan(line) + // Range dels can be written sequentially. Range keys must be collected. + rKeySpan := &keyspan.Span{Start: s.Start, End: s.End} + for _, k := range s.Keys { + if rangekey.IsRangeKey(k.Kind()) { + rKeySpan.Keys = append(rKeySpan.Keys, k) + } else { + k := base.InternalKey{UserKey: s.Start, Trailer: k.Trailer} + if err = w.Add(k, s.End); err != nil { + return err.Error() + } + } + } + err = rangekey.Encode(rKeySpan, func(k base.InternalKey, v []byte) error { + return w.AddRangeKey(k, v) + }) + if err != nil { + return err.Error() + } + } + if err = w.Close(); err != nil { + return err.Error() + } + meta, err := w.Metadata() + if err != nil { + return err.Error() + } + if meta.HasPointKeys { + m.ExtendPointKeyBounds(cmp, meta.SmallestPoint, meta.LargestPoint) + } + if meta.HasRangeDelKeys { + m.ExtendPointKeyBounds(cmp, meta.SmallestRangeDel, meta.LargestRangeDel) + } + if meta.HasRangeKeys { + m.ExtendRangeKeyBounds(cmp, meta.SmallestRangeKey, meta.LargestRangeKey) + } + return m.DebugString(base.DefaultFormatter, false /* verbose */) + case "spans": + f, err := fs.Open("tmp.sst") + if err != nil { + return err.Error() + } + var r *sstable.Reader + readable, err := sstable.NewSimpleReadable(f) + if err != nil { + return err.Error() + } + r, err = sstable.NewReader(readable, sstable.ReaderOptions{}) + if err != nil { + return err.Error() + } + defer r.Close() + iter, err := newCombinedDeletionKeyspanIter(base.DefaultComparer, r, m) + if err != nil { + return err.Error() + } + defer iter.Close() + var buf bytes.Buffer + for s := iter.First(); s != nil; s = iter.Next() { + buf.WriteString(s.String() + "\n") + } + if buf.Len() == 0 { + return "(none)" + } + return buf.String() + default: + return fmt.Sprintf("unknown command: %s", cmd) + } + }) +} diff --git a/pebble/testdata/.gitignore b/pebble/testdata/.gitignore new file mode 100644 index 0000000..00b4a6c --- /dev/null +++ b/pebble/testdata/.gitignore @@ -0,0 +1 @@ +/make-db diff --git a/pebble/testdata/Makefile b/pebble/testdata/Makefile new file mode 100644 index 0000000..ee84143 --- /dev/null +++ b/pebble/testdata/Makefile @@ -0,0 +1,8 @@ +all: rebuild + +.PHONY: rebuild +rebuild: make-db.go + for stage in 1 2 3 4; do \ + rm -fr db-stage-$$stage && \ + go run make-db.go $$stage; \ + done diff --git a/pebble/testdata/batch_get b/pebble/testdata/batch_get new file mode 100644 index 0000000..d34f97a --- /dev/null +++ b/pebble/testdata/batch_get @@ -0,0 +1,114 @@ +define +set a 1 +---- + +get a +---- +1 + +get b +---- +pebble: not found + +define +set a 1 +set a 2 +---- + +get a +---- +2 + +define +set a 1 +set a 2 +del a +---- + +get a +---- +pebble: not found + +define +set a 1 +set a 2 +del a +set a 3 +---- + +get a +---- +3 + +define +merge a 1 +merge a 2 +merge a 3 +---- + +get a +---- +123 + +commit +---- + +define +merge a 4 +---- + +get a +---- +1234 + +commit +---- + +define +del a +set a 5 +---- + +get a +---- +5 + +define +del-range a b +---- + +get a +---- +pebble: not found + +commit +---- + +define +---- + +get a +---- +pebble: not found + +define +merge b 1 +merge b 2 +del-range b c +merge b 3 +merge b 4 +---- + +get b +---- +34 + +commit +---- + +define +---- + +get b +---- +34 diff --git a/pebble/testdata/batch_range_ops b/pebble/testdata/batch_range_ops new file mode 100644 index 0000000..1e66512 --- /dev/null +++ b/pebble/testdata/batch_range_ops @@ -0,0 +1,140 @@ +define +set a 1 +set b 2 +merge c 3 +del-range a c +del d +range-key-set b c @4 value +---- + +scan +---- +a#12,1:1 +b#17,1:2 +c#22,2:3 +d#32,0: + +scan range-del +---- +a-c:{(#27,RANGEDEL)} + +scan range-key +---- +b-c:{(#35,RANGEKEYSET,@4,value)} + +clear +---- + +define +del-range a b +del-range b c +del-range a c +del-range b d +---- + +scan range-del +---- +a-b:{(#22,RANGEDEL) (#12,RANGEDEL)} +b-c:{(#27,RANGEDEL) (#22,RANGEDEL) (#17,RANGEDEL)} +c-d:{(#27,RANGEDEL)} + +clear +---- + +define +range-key-del a b +range-key-del b c +range-key-del a c +range-key-del b d +---- + +scan range-key +---- +a-b:{(#22,RANGEKEYDEL) (#12,RANGEKEYDEL)} +b-c:{(#27,RANGEKEYDEL) (#22,RANGEKEYDEL) (#17,RANGEKEYDEL)} +c-d:{(#27,RANGEKEYDEL)} + +clear +---- + +define +del-range a b +---- + +scan range-del +---- +a-b:{(#12,RANGEDEL)} + +define +del-range b c +---- + +scan range-del +---- +a-b:{(#12,RANGEDEL)} +b-c:{(#17,RANGEDEL)} + +define +del-range a c +---- + +scan range-del +---- +a-b:{(#22,RANGEDEL) (#12,RANGEDEL)} +b-c:{(#22,RANGEDEL) (#17,RANGEDEL)} + +define +del-range b d +---- + +scan range-del +---- +a-b:{(#22,RANGEDEL) (#12,RANGEDEL)} +b-c:{(#27,RANGEDEL) (#22,RANGEDEL) (#17,RANGEDEL)} +c-d:{(#27,RANGEDEL)} + +# Verify that adding a range tombstone via Batch.Apply invalidates the +# cached fragmented tombstones. + +clear +---- + +define +del-range a b +---- + +scan range-del +---- +a-b:{(#12,RANGEDEL)} + +apply +del-range c d +---- + +scan range-del +---- +a-b:{(#12,RANGEDEL)} +c-d:{(#17,RANGEDEL)} + +# Verify that adding a range key via Batch.Apply invalidates the +# cached fragmented range keys. + +clear +---- + +define +range-key-set a c @2 v +---- + +scan range-key +---- +a-c:{(#12,RANGEKEYSET,@2,v)} + +apply +range-key-unset a b @2 +---- + +scan range-key +---- +a-b:{(#23,RANGEKEYUNSET,@2) (#12,RANGEKEYSET,@2,v)} +b-c:{(#12,RANGEKEYSET,@2,v)} diff --git a/pebble/testdata/batch_reader b/pebble/testdata/batch_reader new file mode 100644 index 0000000..01ea8a2 --- /dev/null +++ b/pebble/testdata/batch_reader @@ -0,0 +1,98 @@ +scan +---- +Count: 0 +eof + +scan +ffffffffffffffffffffffffffffffffffffffffffffffff +---- +Count: 4294967295 +err: invalid key kind 0xff: pebble: invalid batch + +scan +0000000000000000 01000000 # Seqnum = 0, Count = 1 +00 01 61 # DEL "a" +---- +Count: 1 +DEL: "a": "" +eof + +scan +0000000000000000 01000000 # Seqnum = 0, Count = 1 +01 01 62 01 62 # SET "b" = "b" +---- +Count: 1 +SET: "b": "b" +eof + +scan +0000000000000000 01000000 # Seqnum = 0, Count = 1 +01 01 62 01 62 # SET "b" = "b" +---- +Count: 1 +SET: "b": "b" +eof + +scan +0000000000000000 02000000 # Seqnum = 0, Count = 2 +00 01 61 # DEL "a" +01 01 62 01 62 # SET "b" = "b" +---- +Count: 2 +DEL: "a": "" +SET: "b": "b" +eof + +scan +0000000000000000 03000000 # Seqnum = 0, Count = 3 +00 01 61 # DEL "a" +01 01 62 01 62 # SET "b" = "b" +0F 01 62 01 63 # RANGEDEL "b" = "c" +---- +Count: 3 +DEL: "a": "" +SET: "b": "b" +RANGEDEL: "b": "c" +eof + +scan +0000000000000000 03000000 # Seqnum = 0, Count = 3 +00 01 61 # DEL "a" +01 01 62 01 62 # SET "b" = "b" +0F 01 62 01 # RANGEDEL "b"... missing end key string data +---- +Count: 3 +DEL: "a": "" +SET: "b": "b" +err: decoding RANGEDEL value: pebble: invalid batch + +scan +0000000000000000 03000000 # Seqnum = 0, Count = 3 +00 01 61 # DEL "a" +01 01 62 01 62 # SET "b" = "b" +0F 01 62 01 # RANGEDEL "b"... missing end key string data +---- +Count: 3 +DEL: "a": "" +SET: "b": "b" +err: decoding RANGEDEL value: pebble: invalid batch + + +scan +0000000000000000 03000000 # Seqnum = 0, Count = 3 +00 01 61 # DEL "a" +01 01 62 01 62 # SET "b" = "b" +1F 01 62 01 # "1F" kind is garbage +---- +Count: 3 +DEL: "a": "" +SET: "b": "b" +err: invalid key kind 0x1f: pebble: invalid batch + +scan +0000000000000000 01000000 # Seqnum = 0, Count = 1 +01 01 # SET missing user key string data +---- +Count: 1 +err: decoding user key: pebble: invalid batch + diff --git a/pebble/testdata/checkpoint b/pebble/testdata/checkpoint new file mode 100644 index 0000000..6ace513 --- /dev/null +++ b/pebble/testdata/checkpoint @@ -0,0 +1,811 @@ +open db +---- +mkdir-all: db 0755 +open-dir: db +lock: db/LOCK +open-dir: db +open-dir: db +open: db/CURRENT +create: db/MANIFEST-000001 +sync: db/MANIFEST-000001 +remove: db/temporary.000001.dbtmp +create: db/temporary.000001.dbtmp +sync: db/temporary.000001.dbtmp +close: db/temporary.000001.dbtmp +rename: db/temporary.000001.dbtmp -> db/CURRENT +sync: db +open-dir: db +sync: db/MANIFEST-000001 +create: db/000002.log +sync: db +create: db/marker.manifest.000001.MANIFEST-000001 +close: db/marker.manifest.000001.MANIFEST-000001 +sync: db +create: db/marker.format-version.000001.002 +close: db/marker.format-version.000001.002 +sync: db +remove: db/temporary.000000.dbtmp +create: db/temporary.000000.dbtmp +sync: db/temporary.000000.dbtmp +close: db/temporary.000000.dbtmp +rename: db/temporary.000000.dbtmp -> db/CURRENT +create: db/marker.format-version.000002.003 +close: db/marker.format-version.000002.003 +remove: db/marker.format-version.000001.002 +sync: db +create: db/marker.format-version.000003.004 +close: db/marker.format-version.000003.004 +remove: db/marker.format-version.000002.003 +sync: db +create: db/marker.format-version.000004.005 +close: db/marker.format-version.000004.005 +remove: db/marker.format-version.000003.004 +sync: db +create: db/marker.format-version.000005.006 +close: db/marker.format-version.000005.006 +remove: db/marker.format-version.000004.005 +sync: db +create: db/marker.format-version.000006.007 +close: db/marker.format-version.000006.007 +remove: db/marker.format-version.000005.006 +sync: db +create: db/marker.format-version.000007.008 +close: db/marker.format-version.000007.008 +remove: db/marker.format-version.000006.007 +sync: db +create: db/marker.format-version.000008.009 +close: db/marker.format-version.000008.009 +remove: db/marker.format-version.000007.008 +sync: db +create: db/marker.format-version.000009.010 +close: db/marker.format-version.000009.010 +remove: db/marker.format-version.000008.009 +sync: db +create: db/marker.format-version.000010.011 +close: db/marker.format-version.000010.011 +remove: db/marker.format-version.000009.010 +sync: db +create: db/marker.format-version.000011.012 +close: db/marker.format-version.000011.012 +remove: db/marker.format-version.000010.011 +sync: db +create: db/marker.format-version.000012.013 +close: db/marker.format-version.000012.013 +remove: db/marker.format-version.000011.012 +sync: db +create: db/marker.format-version.000013.014 +close: db/marker.format-version.000013.014 +remove: db/marker.format-version.000012.013 +sync: db +create: db/marker.format-version.000014.015 +close: db/marker.format-version.000014.015 +remove: db/marker.format-version.000013.014 +sync: db +create: db/marker.format-version.000015.016 +close: db/marker.format-version.000015.016 +remove: db/marker.format-version.000014.015 +sync: db +create: db/temporary.000003.dbtmp +sync: db/temporary.000003.dbtmp +close: db/temporary.000003.dbtmp +rename: db/temporary.000003.dbtmp -> db/OPTIONS-000003 +sync: db + +batch db +set a 1 +set b 2 +set c 3 +---- +sync-data: db/000002.log + +flush db +---- +sync-data: db/000002.log +close: db/000002.log +create: db/000004.log +sync: db +create: db/000005.sst +sync-data: db/000005.sst +close: db/000005.sst +sync: db +sync: db/MANIFEST-000001 + +batch db +set b 5 +set d 7 +set e 8 +---- +sync-data: db/000004.log + +flush db +---- +sync-data: db/000004.log +close: db/000004.log +reuseForWrite: db/000002.log -> db/000006.log +sync: db +create: db/000007.sst +sync-data: db/000007.sst +close: db/000007.sst +sync: db +sync: db/MANIFEST-000001 + +batch db +set f 9 +set g 10 +---- +sync-data: db/000006.log + +checkpoint db checkpoints/checkpoint1 +---- +mkdir-all: checkpoints/checkpoint1 0755 +open-dir: checkpoints +sync: checkpoints +close: checkpoints +open-dir: +sync: +close: +open-dir: checkpoints/checkpoint1 +link: db/OPTIONS-000003 -> checkpoints/checkpoint1/OPTIONS-000003 +open-dir: checkpoints/checkpoint1 +create: checkpoints/checkpoint1/marker.format-version.000001.016 +sync-data: checkpoints/checkpoint1/marker.format-version.000001.016 +close: checkpoints/checkpoint1/marker.format-version.000001.016 +sync: checkpoints/checkpoint1 +close: checkpoints/checkpoint1 +link: db/000005.sst -> checkpoints/checkpoint1/000005.sst +link: db/000007.sst -> checkpoints/checkpoint1/000007.sst +open: db/MANIFEST-000001 +create: checkpoints/checkpoint1/MANIFEST-000001 +sync-data: checkpoints/checkpoint1/MANIFEST-000001 +close: checkpoints/checkpoint1/MANIFEST-000001 +close: db/MANIFEST-000001 +open-dir: checkpoints/checkpoint1 +create: checkpoints/checkpoint1/marker.manifest.000001.MANIFEST-000001 +sync-data: checkpoints/checkpoint1/marker.manifest.000001.MANIFEST-000001 +close: checkpoints/checkpoint1/marker.manifest.000001.MANIFEST-000001 +sync: checkpoints/checkpoint1 +close: checkpoints/checkpoint1 +open: db/000006.log +create: checkpoints/checkpoint1/000006.log +sync-data: checkpoints/checkpoint1/000006.log +close: checkpoints/checkpoint1/000006.log +close: db/000006.log +sync: checkpoints/checkpoint1 +close: checkpoints/checkpoint1 + +checkpoint db checkpoints/checkpoint1 +---- +checkpoint checkpoints/checkpoint1: file already exists + +# Create a checkpoint that omits SSTs that don't overlap with the [d - f) range. +checkpoint db checkpoints/checkpoint2 restrict=(d-f) +---- +mkdir-all: checkpoints/checkpoint2 0755 +open-dir: checkpoints +sync: checkpoints +close: checkpoints +open-dir: checkpoints/checkpoint2 +link: db/OPTIONS-000003 -> checkpoints/checkpoint2/OPTIONS-000003 +open-dir: checkpoints/checkpoint2 +create: checkpoints/checkpoint2/marker.format-version.000001.016 +sync-data: checkpoints/checkpoint2/marker.format-version.000001.016 +close: checkpoints/checkpoint2/marker.format-version.000001.016 +sync: checkpoints/checkpoint2 +close: checkpoints/checkpoint2 +link: db/000007.sst -> checkpoints/checkpoint2/000007.sst +open: db/MANIFEST-000001 +create: checkpoints/checkpoint2/MANIFEST-000001 +sync-data: checkpoints/checkpoint2/MANIFEST-000001 +close: checkpoints/checkpoint2/MANIFEST-000001 +close: db/MANIFEST-000001 +open-dir: checkpoints/checkpoint2 +create: checkpoints/checkpoint2/marker.manifest.000001.MANIFEST-000001 +sync-data: checkpoints/checkpoint2/marker.manifest.000001.MANIFEST-000001 +close: checkpoints/checkpoint2/marker.manifest.000001.MANIFEST-000001 +sync: checkpoints/checkpoint2 +close: checkpoints/checkpoint2 +open: db/000006.log +create: checkpoints/checkpoint2/000006.log +sync-data: checkpoints/checkpoint2/000006.log +close: checkpoints/checkpoint2/000006.log +close: db/000006.log +sync: checkpoints/checkpoint2 +close: checkpoints/checkpoint2 + +# Create a checkpoint that omits SSTs that don't overlap with [a - e) and [d - f). +checkpoint db checkpoints/checkpoint3 restrict=(a-e, d-f) +---- +mkdir-all: checkpoints/checkpoint3 0755 +open-dir: checkpoints +sync: checkpoints +close: checkpoints +open-dir: checkpoints/checkpoint3 +link: db/OPTIONS-000003 -> checkpoints/checkpoint3/OPTIONS-000003 +open-dir: checkpoints/checkpoint3 +create: checkpoints/checkpoint3/marker.format-version.000001.016 +sync-data: checkpoints/checkpoint3/marker.format-version.000001.016 +close: checkpoints/checkpoint3/marker.format-version.000001.016 +sync: checkpoints/checkpoint3 +close: checkpoints/checkpoint3 +link: db/000005.sst -> checkpoints/checkpoint3/000005.sst +link: db/000007.sst -> checkpoints/checkpoint3/000007.sst +open: db/MANIFEST-000001 +create: checkpoints/checkpoint3/MANIFEST-000001 +sync-data: checkpoints/checkpoint3/MANIFEST-000001 +close: checkpoints/checkpoint3/MANIFEST-000001 +close: db/MANIFEST-000001 +open-dir: checkpoints/checkpoint3 +create: checkpoints/checkpoint3/marker.manifest.000001.MANIFEST-000001 +sync-data: checkpoints/checkpoint3/marker.manifest.000001.MANIFEST-000001 +close: checkpoints/checkpoint3/marker.manifest.000001.MANIFEST-000001 +sync: checkpoints/checkpoint3 +close: checkpoints/checkpoint3 +open: db/000006.log +create: checkpoints/checkpoint3/000006.log +sync-data: checkpoints/checkpoint3/000006.log +close: checkpoints/checkpoint3/000006.log +close: db/000006.log +sync: checkpoints/checkpoint3 +close: checkpoints/checkpoint3 + +compact db +---- +sync-data: db/000006.log +close: db/000006.log +reuseForWrite: db/000004.log -> db/000008.log +sync: db +create: db/000009.sst +sync-data: db/000009.sst +close: db/000009.sst +sync: db +sync: db/MANIFEST-000001 +open: db/000005.sst +read-at(630, 53): db/000005.sst +read-at(593, 37): db/000005.sst +read-at(74, 519): db/000005.sst +read-at(47, 27): db/000005.sst +open: db/000005.sst +close: db/000005.sst +open: db/000009.sst +read-at(625, 53): db/000009.sst +read-at(588, 37): db/000009.sst +read-at(69, 519): db/000009.sst +read-at(42, 27): db/000009.sst +open: db/000009.sst +close: db/000009.sst +open: db/000007.sst +read-at(630, 53): db/000007.sst +read-at(593, 37): db/000007.sst +read-at(74, 519): db/000007.sst +read-at(47, 27): db/000007.sst +open: db/000007.sst +close: db/000007.sst +open: db/000005.sst +read-at(0, 47): db/000005.sst +open: db/000007.sst +read-at(0, 47): db/000007.sst +create: db/000010.sst +close: db/000005.sst +open: db/000009.sst +read-at(0, 42): db/000009.sst +close: db/000007.sst +close: db/000009.sst +sync-data: db/000010.sst +close: db/000010.sst +sync: db +sync: db/MANIFEST-000001 +close: db/000005.sst +close: db/000007.sst +close: db/000009.sst +remove: db/000005.sst +remove: db/000007.sst +remove: db/000009.sst + +batch db +set h 11 +---- +sync-data: db/000008.log + +list db +---- +000006.log +000008.log +000010.sst +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +marker.format-version.000015.016 +marker.manifest.000001.MANIFEST-000001 + +list checkpoints/checkpoint1 +---- +000005.sst +000006.log +000007.sst +MANIFEST-000001 +OPTIONS-000003 +marker.format-version.000001.016 +marker.manifest.000001.MANIFEST-000001 + +open checkpoints/checkpoint1 readonly +---- +open-dir: checkpoints/checkpoint1 +lock: checkpoints/checkpoint1/LOCK +open-dir: checkpoints/checkpoint1 +open-dir: checkpoints/checkpoint1 +open: checkpoints/checkpoint1/MANIFEST-000001 +close: checkpoints/checkpoint1/MANIFEST-000001 +open-dir: checkpoints/checkpoint1 +open: checkpoints/checkpoint1/OPTIONS-000003 +close: checkpoints/checkpoint1/OPTIONS-000003 +open: checkpoints/checkpoint1/000006.log +close: checkpoints/checkpoint1/000006.log + +scan checkpoints/checkpoint1 +---- +open: checkpoints/checkpoint1/000007.sst +read-at(630, 53): checkpoints/checkpoint1/000007.sst +read-at(593, 37): checkpoints/checkpoint1/000007.sst +read-at(74, 519): checkpoints/checkpoint1/000007.sst +read-at(47, 27): checkpoints/checkpoint1/000007.sst +read-at(0, 47): checkpoints/checkpoint1/000007.sst +open: checkpoints/checkpoint1/000005.sst +read-at(630, 53): checkpoints/checkpoint1/000005.sst +read-at(593, 37): checkpoints/checkpoint1/000005.sst +read-at(74, 519): checkpoints/checkpoint1/000005.sst +read-at(47, 27): checkpoints/checkpoint1/000005.sst +read-at(0, 47): checkpoints/checkpoint1/000005.sst +a 1 +b 5 +c 3 +d 7 +e 8 +f 9 +g 10 +. + +scan db +---- +open: db/000010.sst +read-at(657, 53): db/000010.sst +read-at(620, 37): db/000010.sst +read-at(101, 519): db/000010.sst +read-at(74, 27): db/000010.sst +read-at(0, 74): db/000010.sst +a 1 +b 5 +c 3 +d 7 +e 8 +f 9 +g 10 +h 11 +. + +# This checkpoint should only contain the second SST. +list checkpoints/checkpoint2 +---- +000006.log +000007.sst +MANIFEST-000001 +OPTIONS-000003 +marker.format-version.000001.016 +marker.manifest.000001.MANIFEST-000001 + +open checkpoints/checkpoint2 readonly +---- +open-dir: checkpoints/checkpoint2 +lock: checkpoints/checkpoint2/LOCK +open-dir: checkpoints/checkpoint2 +open-dir: checkpoints/checkpoint2 +open: checkpoints/checkpoint2/MANIFEST-000001 +close: checkpoints/checkpoint2/MANIFEST-000001 +open-dir: checkpoints/checkpoint2 +open: checkpoints/checkpoint2/OPTIONS-000003 +close: checkpoints/checkpoint2/OPTIONS-000003 +open: checkpoints/checkpoint2/000006.log +close: checkpoints/checkpoint2/000006.log + +scan checkpoints/checkpoint2 +---- +open: checkpoints/checkpoint2/000007.sst +read-at(630, 53): checkpoints/checkpoint2/000007.sst +read-at(593, 37): checkpoints/checkpoint2/000007.sst +read-at(74, 519): checkpoints/checkpoint2/000007.sst +read-at(47, 27): checkpoints/checkpoint2/000007.sst +read-at(0, 47): checkpoints/checkpoint2/000007.sst +b 5 +d 7 +e 8 +f 9 +g 10 +. + +# This checkpoint should contain both SSTs. +list checkpoints/checkpoint3 +---- +000005.sst +000006.log +000007.sst +MANIFEST-000001 +OPTIONS-000003 +marker.format-version.000001.016 +marker.manifest.000001.MANIFEST-000001 + +open checkpoints/checkpoint3 readonly +---- +open-dir: checkpoints/checkpoint3 +lock: checkpoints/checkpoint3/LOCK +open-dir: checkpoints/checkpoint3 +open-dir: checkpoints/checkpoint3 +open: checkpoints/checkpoint3/MANIFEST-000001 +close: checkpoints/checkpoint3/MANIFEST-000001 +open-dir: checkpoints/checkpoint3 +open: checkpoints/checkpoint3/OPTIONS-000003 +close: checkpoints/checkpoint3/OPTIONS-000003 +open: checkpoints/checkpoint3/000006.log +close: checkpoints/checkpoint3/000006.log + +scan checkpoints/checkpoint3 +---- +open: checkpoints/checkpoint3/000007.sst +read-at(630, 53): checkpoints/checkpoint3/000007.sst +read-at(593, 37): checkpoints/checkpoint3/000007.sst +read-at(74, 519): checkpoints/checkpoint3/000007.sst +read-at(47, 27): checkpoints/checkpoint3/000007.sst +read-at(0, 47): checkpoints/checkpoint3/000007.sst +open: checkpoints/checkpoint3/000005.sst +read-at(630, 53): checkpoints/checkpoint3/000005.sst +read-at(593, 37): checkpoints/checkpoint3/000005.sst +read-at(74, 519): checkpoints/checkpoint3/000005.sst +read-at(47, 27): checkpoints/checkpoint3/000005.sst +read-at(0, 47): checkpoints/checkpoint3/000005.sst +a 1 +b 5 +c 3 +d 7 +e 8 +f 9 +g 10 +. + +# Test virtual sstable checkpointing. Virtual sstable checkpointing will remove +# the backing files which won't be required by the checkpoint. Need to make sure +# that the virtual sstables which are present in the checkpoint manifest are +# still readable, and that the backing files not required are deleted. + +lsm db +---- +6: + 000010:[a#0,SET-g#0,SET] + +build db ext1 format=pebblev2 +set i i +set j j +set k k +---- + +ingest-and-excise db ext1 excise=c-d +---- + +# 12, 13 are virtual sstables. +lsm db +---- +6: + 000012:[a#0,SET-b#0,SET] + 000013:[d#0,SET-g#0,SET] + 000011:[i#19,SET-k#19,SET] + +build db ext2 format=pebblev2 +set z z +---- + +ingest-and-excise db ext2 excise=j-k +---- + +# 12, 13, 15, 16 are virtual. +lsm db +---- +6: + 000012:[a#0,SET-b#0,SET] + 000013:[d#0,SET-g#0,SET] + 000015:[i#19,SET-i#19,SET] + 000016:[k#19,SET-k#19,SET] + 000014:[z#20,SET-z#20,SET] + +# scan db so that it is known what to expect from the checkpoints. +scan db +---- +a 1 +b 5 +d 7 +e 8 +f 9 +g 10 +h 11 +i i +k k +open: db/000014.sst +read-at(636, 53): db/000014.sst +read-at(599, 37): db/000014.sst +z z +. + +# Create a basic checkpoint to see if virtual sstables can be read. +checkpoint db checkpoints/checkpoint4 +---- +mkdir-all: checkpoints/checkpoint4 0755 +open-dir: checkpoints +sync: checkpoints +close: checkpoints +open-dir: checkpoints/checkpoint4 +link: db/OPTIONS-000003 -> checkpoints/checkpoint4/OPTIONS-000003 +open-dir: checkpoints/checkpoint4 +create: checkpoints/checkpoint4/marker.format-version.000001.016 +sync-data: checkpoints/checkpoint4/marker.format-version.000001.016 +close: checkpoints/checkpoint4/marker.format-version.000001.016 +sync: checkpoints/checkpoint4 +close: checkpoints/checkpoint4 +link: db/000010.sst -> checkpoints/checkpoint4/000010.sst +link: db/000011.sst -> checkpoints/checkpoint4/000011.sst +link: db/000014.sst -> checkpoints/checkpoint4/000014.sst +open: db/MANIFEST-000001 +create: checkpoints/checkpoint4/MANIFEST-000001 +sync-data: checkpoints/checkpoint4/MANIFEST-000001 +close: checkpoints/checkpoint4/MANIFEST-000001 +close: db/MANIFEST-000001 +open-dir: checkpoints/checkpoint4 +create: checkpoints/checkpoint4/marker.manifest.000001.MANIFEST-000001 +sync-data: checkpoints/checkpoint4/marker.manifest.000001.MANIFEST-000001 +close: checkpoints/checkpoint4/marker.manifest.000001.MANIFEST-000001 +sync: checkpoints/checkpoint4 +close: checkpoints/checkpoint4 +open: db/000008.log +create: checkpoints/checkpoint4/000008.log +sync-data: checkpoints/checkpoint4/000008.log +close: checkpoints/checkpoint4/000008.log +close: db/000008.log +sync: checkpoints/checkpoint4 +close: checkpoints/checkpoint4 + +open checkpoints/checkpoint4 readonly +---- +open-dir: checkpoints/checkpoint4 +lock: checkpoints/checkpoint4/LOCK +open-dir: checkpoints/checkpoint4 +open-dir: checkpoints/checkpoint4 +open: checkpoints/checkpoint4/MANIFEST-000001 +close: checkpoints/checkpoint4/MANIFEST-000001 +open-dir: checkpoints/checkpoint4 +open: checkpoints/checkpoint4/OPTIONS-000003 +close: checkpoints/checkpoint4/OPTIONS-000003 +open: checkpoints/checkpoint4/000008.log +close: checkpoints/checkpoint4/000008.log + +scan checkpoints/checkpoint4 +---- +open: checkpoints/checkpoint4/000010.sst +read-at(657, 53): checkpoints/checkpoint4/000010.sst +read-at(620, 37): checkpoints/checkpoint4/000010.sst +read-at(101, 519): checkpoints/checkpoint4/000010.sst +read-at(74, 27): checkpoints/checkpoint4/000010.sst +read-at(0, 74): checkpoints/checkpoint4/000010.sst +a 1 +b 5 +d 7 +e 8 +f 9 +g 10 +open: checkpoints/checkpoint4/000011.sst +read-at(653, 53): checkpoints/checkpoint4/000011.sst +read-at(616, 37): checkpoints/checkpoint4/000011.sst +read-at(70, 546): checkpoints/checkpoint4/000011.sst +read-at(43, 27): checkpoints/checkpoint4/000011.sst +read-at(0, 43): checkpoints/checkpoint4/000011.sst +h 11 +i i +k k +open: checkpoints/checkpoint4/000014.sst +read-at(636, 53): checkpoints/checkpoint4/000014.sst +read-at(599, 37): checkpoints/checkpoint4/000014.sst +read-at(53, 546): checkpoints/checkpoint4/000014.sst +read-at(26, 27): checkpoints/checkpoint4/000014.sst +read-at(0, 26): checkpoints/checkpoint4/000014.sst +z z +. + +close checkpoints/checkpoint4 +---- + + +# Backing sst 10 is in the list as it is backing sstables 12, 13. +list db +---- +000006.log +000008.log +000010.sst +000011.sst +000014.sst +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +marker.format-version.000015.016 +marker.manifest.000001.MANIFEST-000001 + + +# Exclude virtual sstable 12. The backing sst should still be present on disk +# in the checkpoint. See the "link: db/000010.sst" line. +checkpoint db checkpoints/checkpoint5 restrict=(d-zz) +---- +mkdir-all: checkpoints/checkpoint5 0755 +open-dir: checkpoints +sync: checkpoints +close: checkpoints +open-dir: checkpoints/checkpoint5 +link: db/OPTIONS-000003 -> checkpoints/checkpoint5/OPTIONS-000003 +open-dir: checkpoints/checkpoint5 +create: checkpoints/checkpoint5/marker.format-version.000001.016 +sync-data: checkpoints/checkpoint5/marker.format-version.000001.016 +close: checkpoints/checkpoint5/marker.format-version.000001.016 +sync: checkpoints/checkpoint5 +close: checkpoints/checkpoint5 +link: db/000010.sst -> checkpoints/checkpoint5/000010.sst +link: db/000011.sst -> checkpoints/checkpoint5/000011.sst +link: db/000014.sst -> checkpoints/checkpoint5/000014.sst +open: db/MANIFEST-000001 +create: checkpoints/checkpoint5/MANIFEST-000001 +sync-data: checkpoints/checkpoint5/MANIFEST-000001 +close: checkpoints/checkpoint5/MANIFEST-000001 +close: db/MANIFEST-000001 +open-dir: checkpoints/checkpoint5 +create: checkpoints/checkpoint5/marker.manifest.000001.MANIFEST-000001 +sync-data: checkpoints/checkpoint5/marker.manifest.000001.MANIFEST-000001 +close: checkpoints/checkpoint5/marker.manifest.000001.MANIFEST-000001 +sync: checkpoints/checkpoint5 +close: checkpoints/checkpoint5 +open: db/000008.log +create: checkpoints/checkpoint5/000008.log +sync-data: checkpoints/checkpoint5/000008.log +close: checkpoints/checkpoint5/000008.log +close: db/000008.log +sync: checkpoints/checkpoint5 +close: checkpoints/checkpoint5 + +open checkpoints/checkpoint5 +---- +mkdir-all: checkpoints/checkpoint5 0755 +open-dir: checkpoints/checkpoint5 +lock: checkpoints/checkpoint5/LOCK +open-dir: checkpoints/checkpoint5 +open-dir: checkpoints/checkpoint5 +open: checkpoints/checkpoint5/MANIFEST-000001 +close: checkpoints/checkpoint5/MANIFEST-000001 +open-dir: checkpoints/checkpoint5 +open: checkpoints/checkpoint5/OPTIONS-000003 +close: checkpoints/checkpoint5/OPTIONS-000003 +open: checkpoints/checkpoint5/000008.log +create: checkpoints/checkpoint5/000017.sst +sync-data: checkpoints/checkpoint5/000017.sst +close: checkpoints/checkpoint5/000017.sst +sync: checkpoints/checkpoint5 +close: checkpoints/checkpoint5/000008.log +create: checkpoints/checkpoint5/MANIFEST-000019 +sync: checkpoints/checkpoint5/MANIFEST-000019 +create: checkpoints/checkpoint5/marker.manifest.000002.MANIFEST-000019 +close: checkpoints/checkpoint5/marker.manifest.000002.MANIFEST-000019 +remove: checkpoints/checkpoint5/marker.manifest.000001.MANIFEST-000001 +sync: checkpoints/checkpoint5 +create: checkpoints/checkpoint5/000018.log +sync: checkpoints/checkpoint5 +create: checkpoints/checkpoint5/temporary.000020.dbtmp +sync: checkpoints/checkpoint5/temporary.000020.dbtmp +close: checkpoints/checkpoint5/temporary.000020.dbtmp +rename: checkpoints/checkpoint5/temporary.000020.dbtmp -> checkpoints/checkpoint5/OPTIONS-000020 +sync: checkpoints/checkpoint5 +remove: checkpoints/checkpoint5/000008.log +remove: checkpoints/checkpoint5/OPTIONS-000003 + +print-backing checkpoints/checkpoint5 +---- +000010 +000011 + +# sstable 12 is gone. +lsm checkpoints/checkpoint5 +---- +0.0: + 000017:[h#18,SET-h#18,SET] +6: + 000013:[d#0,SET-g#0,SET] + 000015:[i#19,SET-i#19,SET] + 000016:[k#19,SET-k#19,SET] + 000014:[z#20,SET-z#20,SET] + +close checkpoints/checkpoint5 +---- + +# Exclude both sstables 12 and 13. The backing sstable 10 should not be linked. +# There should be a remove backing table entry for backing sstable 10. +checkpoint db checkpoints/checkpoint6 restrict=(i-zz) +---- +mkdir-all: checkpoints/checkpoint6 0755 +open-dir: checkpoints +sync: checkpoints +close: checkpoints +open-dir: checkpoints/checkpoint6 +link: db/OPTIONS-000003 -> checkpoints/checkpoint6/OPTIONS-000003 +open-dir: checkpoints/checkpoint6 +create: checkpoints/checkpoint6/marker.format-version.000001.016 +sync-data: checkpoints/checkpoint6/marker.format-version.000001.016 +close: checkpoints/checkpoint6/marker.format-version.000001.016 +sync: checkpoints/checkpoint6 +close: checkpoints/checkpoint6 +link: db/000011.sst -> checkpoints/checkpoint6/000011.sst +link: db/000014.sst -> checkpoints/checkpoint6/000014.sst +open: db/MANIFEST-000001 +create: checkpoints/checkpoint6/MANIFEST-000001 +sync-data: checkpoints/checkpoint6/MANIFEST-000001 +close: checkpoints/checkpoint6/MANIFEST-000001 +close: db/MANIFEST-000001 +open-dir: checkpoints/checkpoint6 +create: checkpoints/checkpoint6/marker.manifest.000001.MANIFEST-000001 +sync-data: checkpoints/checkpoint6/marker.manifest.000001.MANIFEST-000001 +close: checkpoints/checkpoint6/marker.manifest.000001.MANIFEST-000001 +sync: checkpoints/checkpoint6 +close: checkpoints/checkpoint6 +open: db/000008.log +create: checkpoints/checkpoint6/000008.log +sync-data: checkpoints/checkpoint6/000008.log +close: checkpoints/checkpoint6/000008.log +close: db/000008.log +sync: checkpoints/checkpoint6 +close: checkpoints/checkpoint6 + +open checkpoints/checkpoint6 +---- +mkdir-all: checkpoints/checkpoint6 0755 +open-dir: checkpoints/checkpoint6 +lock: checkpoints/checkpoint6/LOCK +open-dir: checkpoints/checkpoint6 +open-dir: checkpoints/checkpoint6 +open: checkpoints/checkpoint6/MANIFEST-000001 +close: checkpoints/checkpoint6/MANIFEST-000001 +open-dir: checkpoints/checkpoint6 +open: checkpoints/checkpoint6/OPTIONS-000003 +close: checkpoints/checkpoint6/OPTIONS-000003 +open: checkpoints/checkpoint6/000008.log +create: checkpoints/checkpoint6/000017.sst +sync-data: checkpoints/checkpoint6/000017.sst +close: checkpoints/checkpoint6/000017.sst +sync: checkpoints/checkpoint6 +close: checkpoints/checkpoint6/000008.log +create: checkpoints/checkpoint6/MANIFEST-000019 +sync: checkpoints/checkpoint6/MANIFEST-000019 +create: checkpoints/checkpoint6/marker.manifest.000002.MANIFEST-000019 +close: checkpoints/checkpoint6/marker.manifest.000002.MANIFEST-000019 +remove: checkpoints/checkpoint6/marker.manifest.000001.MANIFEST-000001 +sync: checkpoints/checkpoint6 +create: checkpoints/checkpoint6/000018.log +sync: checkpoints/checkpoint6 +create: checkpoints/checkpoint6/temporary.000020.dbtmp +sync: checkpoints/checkpoint6/temporary.000020.dbtmp +close: checkpoints/checkpoint6/temporary.000020.dbtmp +rename: checkpoints/checkpoint6/temporary.000020.dbtmp -> checkpoints/checkpoint6/OPTIONS-000020 +sync: checkpoints/checkpoint6 +remove: checkpoints/checkpoint6/000008.log +remove: checkpoints/checkpoint6/OPTIONS-000003 + +print-backing checkpoints/checkpoint6 +---- +000011 + +lsm checkpoints/checkpoint6 +---- +0.0: + 000017:[h#18,SET-h#18,SET] +6: + 000015:[i#19,SET-i#19,SET] + 000016:[k#19,SET-k#19,SET] + 000014:[z#20,SET-z#20,SET] diff --git a/pebble/testdata/cleaner b/pebble/testdata/cleaner new file mode 100644 index 0000000..cd96e7d --- /dev/null +++ b/pebble/testdata/cleaner @@ -0,0 +1,243 @@ +# Test archive cleaner. +open db archive +---- +mkdir-all: db 0755 +mkdir-all: db_wal 0755 +open-dir: db +open-dir: db_wal +lock: db/LOCK +open-dir: db +open-dir: db +open: db/CURRENT +create: db/MANIFEST-000001 +sync: db/MANIFEST-000001 +remove: db/temporary.000001.dbtmp +create: db/temporary.000001.dbtmp +sync: db/temporary.000001.dbtmp +close: db/temporary.000001.dbtmp +rename: db/temporary.000001.dbtmp -> db/CURRENT +sync: db +open-dir: db +sync: db/MANIFEST-000001 +create: db_wal/000002.log +sync: db_wal +create: db/temporary.000003.dbtmp +sync: db/temporary.000003.dbtmp +close: db/temporary.000003.dbtmp +rename: db/temporary.000003.dbtmp -> db/OPTIONS-000003 +sync: db + +batch db +set a 1 +set b 2 +set c 3 +---- +sync-data: db_wal/000002.log + +flush db +---- +sync-data: db_wal/000002.log +close: db_wal/000002.log +create: db_wal/000004.log +sync: db_wal +create: db/000005.sst +sync-data: db/000005.sst +close: db/000005.sst +sync: db +sync: db/MANIFEST-000001 +mkdir-all: db_wal/archive 0755 +rename: db_wal/000002.log -> db_wal/archive/000002.log + +batch db +set d 4 +---- +sync-data: db_wal/000004.log + +compact db +---- +sync-data: db_wal/000004.log +close: db_wal/000004.log +create: db_wal/000006.log +sync: db_wal +create: db/000007.sst +sync-data: db/000007.sst +close: db/000007.sst +sync: db +sync: db/MANIFEST-000001 +mkdir-all: db_wal/archive 0755 +rename: db_wal/000004.log -> db_wal/archive/000004.log +open: db/000005.sst +read-at(744, 53): db/000005.sst +read-at(707, 37): db/000005.sst +read-at(79, 628): db/000005.sst +read-at(52, 27): db/000005.sst +open: db/000005.sst +close: db/000005.sst +open: db/000007.sst +read-at(718, 53): db/000007.sst +read-at(681, 37): db/000007.sst +read-at(53, 628): db/000007.sst +read-at(26, 27): db/000007.sst +open: db/000007.sst +close: db/000007.sst +open: db/000005.sst +read-at(0, 52): db/000005.sst +create: db/000008.sst +close: db/000005.sst +open: db/000007.sst +read-at(0, 26): db/000007.sst +close: db/000007.sst +sync-data: db/000008.sst +close: db/000008.sst +sync: db +sync: db/MANIFEST-000001 +close: db/000005.sst +close: db/000007.sst +mkdir-all: db/archive 0755 +rename: db/000005.sst -> db/archive/000005.sst +mkdir-all: db/archive 0755 +rename: db/000007.sst -> db/archive/000007.sst + +list db +---- +000008.sst +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +archive + +list db_wal +---- +000006.log +archive + +list db/archive +---- +000005.sst +000007.sst + +list db_wal/archive +---- +000002.log +000004.log + +# Test cleanup of extra sstables on open. +open db1 +---- +mkdir-all: db1 0755 +mkdir-all: db1_wal 0755 +open-dir: db1 +open-dir: db1_wal +lock: db1/LOCK +open-dir: db1 +open-dir: db1 +open: db1/CURRENT +create: db1/MANIFEST-000001 +sync: db1/MANIFEST-000001 +remove: db1/temporary.000001.dbtmp +create: db1/temporary.000001.dbtmp +sync: db1/temporary.000001.dbtmp +close: db1/temporary.000001.dbtmp +rename: db1/temporary.000001.dbtmp -> db1/CURRENT +sync: db1 +open-dir: db1 +sync: db1/MANIFEST-000001 +create: db1_wal/000002.log +sync: db1_wal +create: db1/temporary.000003.dbtmp +sync: db1/temporary.000003.dbtmp +close: db1/temporary.000003.dbtmp +rename: db1/temporary.000003.dbtmp -> db1/OPTIONS-000003 +sync: db1 + +batch db1 +set a 1 +set b 2 +set c 3 +---- +sync-data: db1_wal/000002.log + +flush db1 +---- +sync-data: db1_wal/000002.log +close: db1_wal/000002.log +create: db1_wal/000004.log +sync: db1_wal +create: db1/000005.sst +sync-data: db1/000005.sst +close: db1/000005.sst +sync: db1 +sync: db1/MANIFEST-000001 + +close db1 +---- +close: db1 +sync-data: db1_wal/000004.log +close: db1_wal/000004.log +close: db1/MANIFEST-000001 +close: db1 +close: db1 +close: db1_wal +close: db1 + +create-bogus-file db1/000123.sst +---- +create: db1/000123.sst +sync: db1/000123.sst +close: db1/000123.sst + +create-bogus-file db1/000456.sst +---- +create: db1/000456.sst +sync: db1/000456.sst +close: db1/000456.sst + +open db1 +---- +mkdir-all: db1 0755 +mkdir-all: db1_wal 0755 +open-dir: db1 +open-dir: db1_wal +lock: db1/LOCK +open-dir: db1 +open-dir: db1 +open: db1/CURRENT +read-at(0, 16): db1/CURRENT +close: db1/CURRENT +open: db1/MANIFEST-000001 +close: db1/MANIFEST-000001 +open-dir: db1 +open: db1/OPTIONS-000003 +close: db1/OPTIONS-000003 +open: db1_wal/000004.log +close: db1_wal/000004.log +create: db1/MANIFEST-000458 +sync: db1/MANIFEST-000458 +remove: db1/temporary.000458.dbtmp +create: db1/temporary.000458.dbtmp +sync: db1/temporary.000458.dbtmp +close: db1/temporary.000458.dbtmp +rename: db1/temporary.000458.dbtmp -> db1/CURRENT +sync: db1 +create: db1_wal/000457.log +sync: db1_wal +create: db1/temporary.000459.dbtmp +sync: db1/temporary.000459.dbtmp +close: db1/temporary.000459.dbtmp +rename: db1/temporary.000459.dbtmp -> db1/OPTIONS-000459 +sync: db1 +remove: db1_wal/000002.log +remove: db1_wal/000004.log +remove: db1/000123.sst +remove: db1/000456.sst +remove: db1/OPTIONS-000003 + +list db1 +---- +000005.sst +CURRENT +LOCK +MANIFEST-000001 +MANIFEST-000458 +OPTIONS-000459 diff --git a/pebble/testdata/compaction_allow_zero_seqnum b/pebble/testdata/compaction_allow_zero_seqnum new file mode 100644 index 0000000..efe62ab --- /dev/null +++ b/pebble/testdata/compaction_allow_zero_seqnum @@ -0,0 +1,72 @@ +define +L2 + c.SET.2:2 +---- +2: + 000004:[c#2,SET-c#2,SET] + +allow-zero-seqnum +L0:b-b +L0:c-c +L0:d-d +---- +true +false +true + +allow-zero-seqnum +L0:c-c L0:d-d +L0:c-c L1:d-d +L0:b-b L0:b-c +L0:b-b L1:b-c +---- +false +false +false +false + +# We only look for overlaps at L as it isn't valid for a +# compaction rooted at L to not include overlapping tables at +# L. + +allow-zero-seqnum +L1:c-c +---- +true + +# Regression test for a bug where the allow-zero-seqnum check was not +# actually working for flushes due to a failure to clone the +# lower-bound key used for checking for overlap. This caused the +# overlap check to use [b,b] in the test below, rather than [a,b]. + +define +mem + a.SET.2:2 + b.SET.3:3 +L1 + a.SET.0:0 +---- +1: + 000004:[a#0,SET-a#0,SET] + +allow-zero-seqnum +flush +---- +false + +# We never allow zeroing of seqnums during flushing as doing so runs +# afoul of the WAL replay logic which flushes after each WAL is +# replayed, but doesn't construct a version edit in between each +# flush. Both disallowing of seqnum zeroing during flushing, and the +# WAL replay behavior match RocksDB's behavior. + +define +mem + a.SET.2:2 + b.SET.3:3 +---- + +allow-zero-seqnum +flush +---- +false diff --git a/pebble/testdata/compaction_atomic_unit_bounds b/pebble/testdata/compaction_atomic_unit_bounds new file mode 100644 index 0000000..ce140eb --- /dev/null +++ b/pebble/testdata/compaction_atomic_unit_bounds @@ -0,0 +1,61 @@ +define +a.SET.1-b.SET.2 +---- + +atomic-unit-bounds 0 +---- +a-b + +define +a.SET.1-b.SET.2 +c.SET.3-d.SET.4 +e.SET.5-f.SET.6 +---- + +atomic-unit-bounds 0 +---- +a-b + +atomic-unit-bounds 1 +---- +c-d + +atomic-unit-bounds 2 +---- +e-f + +define +a.SET.1-b.RANGEDEL.3 +b.SET.2-c.RANGEDEL.5 +c.SET.4-d.SET.6 +---- + +atomic-unit-bounds 0 +---- +a-d + +atomic-unit-bounds 1 +---- +a-d + +atomic-unit-bounds 2 +---- +a-d + +define +a.SET.1-b.RANGEDEL.72057594037927935 +b.SET.2-c.RANGEDEL.5 +c.SET.4-d.SET.6 +---- + +atomic-unit-bounds 0 +---- +a-b + +atomic-unit-bounds 1 +---- +b-d + +atomic-unit-bounds 2 +---- +b-d diff --git a/pebble/testdata/compaction_check_ordering b/pebble/testdata/compaction_check_ordering new file mode 100644 index 0000000..07e3197 --- /dev/null +++ b/pebble/testdata/compaction_check_ordering @@ -0,0 +1,161 @@ +check-ordering +L0 + a.SET.1-b.SET.2 +---- +OK + +check-ordering +L0 + a.SET.1-b.SET.2 + c.SET.3-d.SET.4 +---- +OK + +check-ordering +L0 + c.SET.3-d.SET.4 + a.SET.1-b.SET.2 +---- +L0 files 000001 and 000002 are not properly ordered: <#3-#4> vs <#1-#2> + +# Seqnum overlaps are allowed in L0 as long as no key ranges overlap. +check-ordering +L0 + c.SET.3-d.SET.4 + a.SET.1-b.SET.5 +---- +OK + +check-ordering +L0 + a.SET.3-d.SET.3 + a.SET.1-b.SET.2 +---- +L0 files 000001 and 000002 are not properly ordered: <#3-#3> vs <#1-#2> + +check-ordering +L0 + a.SET.2-d.SET.4 + a.SET.3-b.SET.3 +---- +L0 files 000001 and 000002 are not properly ordered: <#2-#4> vs <#3-#3> + +check-ordering +L0 + a.SET.3-d.SET.3 + a.SET.3-b.SET.3 +---- +OK + +check-ordering +L1 + a.SET.1-b.SET.2 +---- +OK + +check-ordering +L1 + b.SET.1-a.SET.2 +---- +L1 : file 000001 has inconsistent bounds: b#1,SET vs a#2,SET + +check-ordering +L1 + a.SET.1-b.SET.2 + c.SET.3-d.SET.4 +---- +OK + +check-ordering +L1 + a.SET.1-b.SET.2 + d.SET.3-c.SET.4 +---- +L1 : file 000002 has inconsistent bounds: d#3,SET vs c#4,SET + +check-ordering +L1 + a.SET.1-b.SET.2 + b.SET.1-d.SET.4 +---- +OK + +check-ordering +L1 + a.SET.1-b.SET.2 + b.SET.2-d.SET.4 +---- +L1 files 000001 and 000002 have overlapping ranges: [a#1,SET-b#2,SET] vs [b#2,SET-d#4,SET] + +check-ordering +L1 + a.SET.1-c.SET.2 + b.SET.3-d.SET.4 +---- +L1 files 000001 and 000002 have overlapping ranges: [a#1,SET-c#2,SET] vs [b#3,SET-d#4,SET] + +check-ordering +L1 + a.SET.1-c.SET.2 +L2 + b.SET.3-d.SET.4 +---- +OK + +check-ordering +L1 + a.SET.1-c.SET.2 +L2 + b.SET.3-d.SET.4 + c.SET.5-e.SET.6 +---- +L2 files 000002 and 000003 have overlapping ranges: [b#3,SET-d#4,SET] vs [c#5,SET-e#6,SET] + +# Single sublevel, ordering is fine. +check-ordering +L0.0 + a.SET.1-b.SET.2 + b.SET.1-d.SET.5 +---- +L0.0 files 000001 and 000002 have overlapping ranges: [a#1,SET-b#2,SET] vs [b#1,SET-d#5,SET] + +# Single sublevel, ordering is incorrect. +check-ordering +L0.0 + a.SET.1-b.SET.2 + b.SET.2-d.SET.4 +---- +L0.0 files 000001 and 000002 have overlapping ranges: [a#1,SET-b#2,SET] vs [b#2,SET-d#4,SET] + +# Two sublevels, but ordering is fine. +check-ordering +L0.0 + a.SET.1-b.SET.2 + c.SET.3-d.SET.4 +L0.1 + a.SET.5-b.SET.6 + c.SET.6-d.SET.8 +---- +OK + +# Two sublevels, but first ordering is broken +check-ordering +L0.0 + a.SET.1-b.SET.2 + b.SET.3-d.SET.4 +L0.1 + a.SET.5-b.SET.6 + c.SET.6-d.SET.8 +---- +L0.0 files 000001 and 000002 have overlapping ranges: [a#1,SET-b#2,SET] vs [b#3,SET-d#4,SET] + +# Two sublevels, but second ordering is broken +check-ordering +L0.0 + a.SET.1-b.SET.2 + b.SET.1-d.SET.4 +L0.1 + a.SET.5-b.SET.6 + b.SET.7-d.SET.8 +---- +L0.0 files 000001 and 000002 have overlapping ranges: [a#1,SET-b#2,SET] vs [b#1,SET-d#4,SET] diff --git a/pebble/testdata/compaction_delete_only_hints b/pebble/testdata/compaction_delete_only_hints new file mode 100644 index 0000000..e11d120 --- /dev/null +++ b/pebble/testdata/compaction_delete_only_hints @@ -0,0 +1,417 @@ +# The first few cases are adapted from this ASCII example. The y-axis is +# sequence numbers and the x-axis is the user key space. LSM levels are +# omitted from the visualization. +# +# 250 +# +--------00004 (fragmented)------+ +# V | +# |-b...230:h-| | +# _______________________________________V_____________ snapshot #210 +# 200 |--h.RANGEDEL.200:r--| +# +# _____________________________________________________ snapshot #180 +# +# 150 +--------+ +# +---------+ | 000006 | +# | 000005 | | | +# +_________+ | | +# 100_____________________|________|___________________ snapshot #100 +# +--------+ +# _____________________________________________________ snapshot #70 +# +---------------+ +# 50 | 000007 | +# | | +# +---------------+ +# ______________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z + +define snapshots=(70, 100, 180, 210) +L0 +b.RANGEDEL.230:h h.RANGEDEL.200:r +L2 +d.SET.110:d i.SET.140:i +L3 +k.SET.90:k o.SET.150:o +L4 +m.SET.30:m u.SET.60:u +---- +0.0: + 000004:[b#230,RANGEDEL-r#inf,RANGEDEL] +2: + 000005:[d#110,SET-i#140,SET] +3: + 000006:[k#90,SET-o#150,SET] +4: + 000007:[m#30,SET-u#60,SET] + +# Test a hint that is blocked by open snapshots. No compaction should occur +# and the hint should not be removed. + +get-hints +---- +L0.000004 b-r seqnums(tombstone=200-230, file-smallest=90, type=point-key-only) + +maybe-compact +---- +Deletion hints: + L0.000004 b-r seqnums(tombstone=200-230, file-smallest=90, type=point-key-only) +Compactions: + (none) + +# Adopt the same LSM but without snapshots 100, 180 and 210. + +define snapshots=(70) +L0 +b.RANGEDEL.230:h h.RANGEDEL.200:r +L2 +d.SET.110:d i.SET.140:i +L3 +k.SET.90:k o.SET.150:o +L4 +m.SET.30:m u.SET.60:u +---- +0.0: + 000004:[b#230,RANGEDEL-r#inf,RANGEDEL] +2: + 000005:[d#110,SET-i#140,SET] +3: + 000006:[k#90,SET-o#150,SET] +4: + 000007:[m#30,SET-u#60,SET] + +get-hints +---- +L0.000004 b-r seqnums(tombstone=200-230, file-smallest=90, type=point-key-only) + +maybe-compact +---- +Deletion hints: + (none) +Compactions: + [JOB 100] compacted(delete-only) L2 [000005] (677B) Score=0.00 + L3 [000006] (677B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s + +# Verify that compaction correctly handles the presence of multiple +# overlapping hints which might delete a file multiple times. All of the +# resolvable hints should be removed. + +define snapshots=(70) +L0 +a.RANGEDEL.300:k +L1 +b.RANGEDEL.230:h h.RANGEDEL.200:r +L2 +d.SET.110:d i.SET.140:i +L3 +k.SET.90:k o.SET.150:o +L4 +m.SET.30:m u.SET.60:u +---- +0.0: + 000004:[a#300,RANGEDEL-k#inf,RANGEDEL] +1: + 000005:[b#230,RANGEDEL-r#inf,RANGEDEL] +2: + 000006:[d#110,SET-i#140,SET] +3: + 000007:[k#90,SET-o#150,SET] +4: + 000008:[m#30,SET-u#60,SET] + +get-hints +---- +L0.000004 a-k seqnums(tombstone=300-300, file-smallest=110, type=point-key-only) +L1.000005 b-r seqnums(tombstone=200-230, file-smallest=90, type=point-key-only) + +maybe-compact +---- +Deletion hints: + (none) +Compactions: + [JOB 100] compacted(delete-only) L2 [000006] (677B) Score=0.00 + L3 [000007] (677B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s + +# Test a range tombstone that is already compacted into L6. + +define snapshots=(70) +L0 +m.SET.300:m b.RANGEDEL.230:h h.RANGEDEL.200:r +L2 +d.SET.110:d i.SET.140:i +L3 +k.SET.90:k o.SET.150:o +L4 +m.SET.30:m u.SET.60:u +---- +0.0: + 000004:[b#230,RANGEDEL-r#inf,RANGEDEL] +2: + 000005:[d#110,SET-i#140,SET] +3: + 000006:[k#90,SET-o#150,SET] +4: + 000007:[m#30,SET-u#60,SET] + +get-hints +---- +L0.000004 b-r seqnums(tombstone=200-230, file-smallest=90, type=point-key-only) + +compact a-z +---- +5: + 000008:[b#230,RANGEDEL-u#0,SET] + +maybe-compact +---- +Deletion hints: + (none) +Compactions: + (none) + +# The same test case, without snapshots, with a table (000008) that exists +# within the range del user key bounds, but above it in the LSM. + +define +L1 +b.RANGEDEL.230:h h.RANGEDEL.200:r +L2 +d.SET.110:d i.SET.140:i +L3 +k.SET.90:k o.SET.150:o +L4 +m.SET.30:m u.SET.60:u +L0 +e.SET.240:e m.SET.260:m +---- +0.0: + 000008:[e#240,SET-m#260,SET] +1: + 000004:[b#230,RANGEDEL-r#inf,RANGEDEL] +2: + 000005:[d#110,SET-i#140,SET] +3: + 000006:[k#90,SET-o#150,SET] +4: + 000007:[m#30,SET-u#60,SET] + +get-hints +---- +L1.000004 b-r seqnums(tombstone=200-230, file-smallest=90, type=point-key-only) + +# Tables 000005 and 000006 can be deleted as their largest sequence numbers fall +# below the smallest sequence number of the range del. Table 000007 falls +# outside the user key bounds, and table 000008 exists at a sequence number +# above the range del, so neither are deleted. + +maybe-compact +---- +Deletion hints: + (none) +Compactions: + [JOB 100] compacted(delete-only) L2 [000005] (677B) Score=0.00 + L3 [000006] (677B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s + +# A deletion hint present on an sstable in a higher level should NOT result in a +# deletion-only compaction incorrectly removing an sstable in L6 following an +# elision-only compaction that zeroes the sequence numbers in an L6 table. +# +# This is a regression test for pebble#1285. + +# Create an sstable at L6. We expect that the SET survives the following +# sequence of compactions. +define snapshots=(10, 25) +L6 +a.SET.20:b a.RANGEDEL.15:z +---- +6: + 000004:[a#20,SETWITHDEL-z#inf,RANGEDEL] + +# Note that this test depends on stats being present on the sstables, so we +# collect hints here. We expect none, as the table is in L6. +get-hints +---- +(none) + +# Place a compaction hint on a non-existent table in a higher level in the LSM. +# +# The selection of the sequence numbers for the hints is nuanced, and warrants +# some explanation. The largest tombstone sequence number (27) and file smallest +# sequence number (0) were chosen such that they fall into different snapshot +# stripes, which ensures the hint is not resolved and dropped. The deletion +# range 5-27 is also chosen such that it covers the sequence number range from +# the table, i.e. 15-20, which *appears* to make the keys eligible for deletion. +force-set-hints +L0.000001 a-z 0 5-27 point_key_only +---- +L0.000001 a-z seqnums(tombstone=5-27, file-smallest=0, type=point-key-only) + +# Hints on the table are unchanged, as the new sstable is at L6, and hints are +# not generated on tables at this level. +get-hints +---- +L0.000001 a-z seqnums(tombstone=5-27, file-smallest=0, type=point-key-only) + +# Closing snapshot 10 triggers an elision-only compaction in L6 rather than a +# deletion-only compaction, as the earliest snapshot that remains open is 25, +# preventing the delete compaction hint from being resolved as it does not exist +# in the same snapshot stripe as the table in L6. +close-snapshot +10 +---- +[JOB 100] compacted(elision-only) L6 [000004] (741B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (662B), in 1.0s (2.0s total), output rate 662B/s + +# The deletion hint was removed by the elision-only compaction. +get-hints +---- +(none) + +# The LSM contains the key, as expected. +iter +first +next +---- +a: (b, .) +. + +# Closing the next snapshot should NOT trigger another compaction, as the +# deletion hint was removed in the elision-only compaction. +close-snapshot +25 +---- +(none) + +# The key remains in the LSM. +iter +first +next +---- +a: (b, .) +. + +# Construct a scenario with tables containing a mixture of range dels and range +# key dels that sit within different types of hints. +# +# +------- 000013 (internally fragmented spans) ----| +# | V +# | |-------------------------| m.RANGEKEYDEL:z +# | |-------| i.RANGEKEYDEL:m +# V |-----------------------| f.RANGEDEL:r +# |---------| a.RANGEDEL:f +# +-+ +---+ +---+ +# | | 000006 | | 000009 | | 000012 <- Point keys only. +# +-+ +---+ +---+ +# +---+ +---+ +---+ +# | | 000005 | | 000008 | | 000011 <- Range keys only. +# +---+ +---+ +---+ +# +---+ +---+ +---+ +# | | 000004 | | 000007 | | 000010 <- Point and range keys. +# +---+ +---+ +---+ +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z +# +# Note that table 000013 contains both range dels and range key dels that have +# been internally fragmented. After defragmentation there are three hints +# created: +# - [a, i) - a point-key-only hint +# - [i, r) - a point-and-range-key hint +# - [r, z) - a range-key-only hint +# +# Based on the defragmented hints, the following tables can be deleted: +# - 000006: covered by range del hint [a, i), table contains only point keys. +# - 000007: covered by mixed hint [i, r), table contains point and range keys. +# - 000008: covered by mixed hint [i, r), table contains only range keys. +# - 000009: covered by mixed hint [i, r), table contains only point keys. +# - 000011: covered by range key hint [r, z), table contains only range keys. +# + +# NOTE: the LSM shown in the example above is created bottom-up via ingestions. + +reset +---- + +ingest ext +set a a +range-key-set a c @1 foo +set c c +---- +OK + +ingest ext +range-key-set d f @2 bar +---- +OK + +ingest ext +set g g +set h h +---- +OK + +ingest ext +set i i +range-key-set i k @1 v1 +set k k +---- +OK + +ingest ext +range-key-set l n @2 bar +---- +OK + +ingest ext +set o o +set q q +---- +OK + +ingest ext +set r r +range-key-set r t @1 v1 +set t t +---- +OK + +ingest ext +range-key-set u w @2 bar +---- +OK + +ingest ext +set x x +set z z +---- +OK + +ingest ext +del-range a f +del-range f r +range-key-del i m +range-key-del m z +---- +OK + +describe-lsm +---- +0.0: + 000013:[a#19,RANGEDEL-z#inf,RANGEKEYDEL] +6: + 000004:[a#10,RANGEKEYSET-c#10,SET] + 000005:[d#11,RANGEKEYSET-f#inf,RANGEKEYSET] + 000006:[g#12,SET-h#12,SET] + 000007:[i#13,RANGEKEYSET-k#13,SET] + 000008:[l#14,RANGEKEYSET-n#inf,RANGEKEYSET] + 000009:[o#15,SET-q#15,SET] + 000010:[r#16,RANGEKEYSET-t#16,SET] + 000011:[u#17,RANGEKEYSET-w#inf,RANGEKEYSET] + 000012:[x#18,SET-z#18,SET] + +get-hints +---- +L0.000013 a-i seqnums(tombstone=19-19, file-smallest=12, type=point-key-only) +L0.000013 i-r seqnums(tombstone=19-19, file-smallest=13, type=point-and-range-key) +L0.000013 r-z seqnums(tombstone=19-19, file-smallest=17, type=range-key-only) + +maybe-compact +---- +Deletion hints: + (none) +Compactions: + [JOB 100] compacted(delete-only) L6 [000006 000007 000008 000009 000011] (3.9KB) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s diff --git a/pebble/testdata/compaction_elide_tombstone b/pebble/testdata/compaction_elide_tombstone new file mode 100644 index 0000000..30ceed7 --- /dev/null +++ b/pebble/testdata/compaction_elide_tombstone @@ -0,0 +1,208 @@ +define +---- + +elide start-level=5 +a +b +c +d +e +f +g +h +i +j +k +---- +elideTombstone("a") = true +elideTombstone("b") = true +elideTombstone("c") = true +elideTombstone("d") = true +elideTombstone("e") = true +elideTombstone("f") = true +elideTombstone("g") = true +elideTombstone("h") = true +elideTombstone("i") = true +elideTombstone("j") = true +elideTombstone("k") = true + +elide start-level=1 +a +b +c +d +e +f +g +h +i +j +k +---- +elideTombstone("a") = true +elideTombstone("b") = true +elideTombstone("c") = true +elideTombstone("d") = true +elideTombstone("e") = true +elideTombstone("f") = true +elideTombstone("g") = true +elideTombstone("h") = true +elideTombstone("i") = true +elideTombstone("j") = true +elideTombstone("k") = true + +define +L1 + c.SET.801:c + g.SET.800:g +L1 + x.SET.701:x + y.SET.700:y +L2 + d.SET.601:d + h.SET.600:h +L2 + r.SET.501:r + t.SET.500:t +L3 + f.SET.401:f + g.SET.400:g +L3 + w.SET.301:w + x.SET.300:x +L4 + f.SET.201:f + m.SET.200:m +L4 + t.SET.101:t + t.SET.100:t +---- +1: + 000004:[c#801,SET-g#800,SET] + 000005:[x#701,SET-y#700,SET] +2: + 000006:[d#601,SET-h#600,SET] + 000007:[r#501,SET-t#500,SET] +3: + 000008:[f#401,SET-g#400,SET] + 000009:[w#301,SET-x#300,SET] +4: + 000010:[f#201,SET-m#200,SET] + 000011:[t#101,SET-t#101,SET] + +elide start-level=1 +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +y +z +---- +elideTombstone("b") = true +elideTombstone("c") = true +elideTombstone("d") = true +elideTombstone("e") = true +elideTombstone("f") = false +elideTombstone("g") = false +elideTombstone("h") = false +elideTombstone("i") = false +elideTombstone("j") = false +elideTombstone("k") = false +elideTombstone("l") = false +elideTombstone("m") = false +elideTombstone("n") = true +elideTombstone("o") = true +elideTombstone("p") = true +elideTombstone("q") = true +elideTombstone("r") = true +elideTombstone("s") = true +elideTombstone("t") = false +elideTombstone("u") = true +elideTombstone("v") = true +elideTombstone("w") = false +elideTombstone("x") = false +elideTombstone("y") = true +elideTombstone("z") = true + +define +L1 + a.SET.3:v +L2 + a.RANGEDEL.2:g +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +L3 + e.SET.0:v + f.SET.1:v +L3 + g.SET.1:v + g.SET.0:v +---- +1: + 000004:[a#3,SET-a#3,SET] +2: + 000005:[a#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + 000009:[g#1,SET-g#1,SET] + +elide start-level=0 +b +c +d +e +f +g +---- +elideTombstone("b") = false +elideTombstone("c") = false +elideTombstone("d") = false +elideTombstone("e") = false +elideTombstone("f") = false +elideTombstone("g") = false + +define +L6 + g.SET.0:g + h.RANGEDEL.1:z +---- +6: + 000004:[g#0,SET-z#inf,RANGEDEL] + +elide start-level=1 +a +b +g +goo +z +---- +elideTombstone("a") = true +elideTombstone("b") = true +elideTombstone("g") = false +elideTombstone("goo") = false +elideTombstone("z") = false diff --git a/pebble/testdata/compaction_error_on_user_key_overlap b/pebble/testdata/compaction_error_on_user_key_overlap new file mode 100644 index 0000000..e9dd9d1 --- /dev/null +++ b/pebble/testdata/compaction_error_on_user_key_overlap @@ -0,0 +1,20 @@ + +error-on-user-key-overlap +a.SET.2-b.SET.3 +c.SET.4-d.SET.5 +---- +OK + +# If the end key is the rangedel sentinel key, no error should be returned. + +error-on-user-key-overlap +a.SET.2-c.RANGEDEL.72057594037927935 +c.SET.4-d.SET.5 +---- +OK + +error-on-user-key-overlap +a.SET.2-c.SET.5 +c.SET.4-d.SET.5 +---- +pebble: compaction split user key across two sstables: c#5,SET in 000001 and 000002 diff --git a/pebble/testdata/compaction_expand_inputs b/pebble/testdata/compaction_expand_inputs new file mode 100644 index 0000000..86035f1 --- /dev/null +++ b/pebble/testdata/compaction_expand_inputs @@ -0,0 +1,78 @@ +define +a.SET.1-b.SET.2 +---- + +expand-inputs 0 +---- +0: a#1,1-b#2,1 + +define +a.SET.1-b.SET.2 +c.SET.3-d.SET.4 +e.SET.5-f.SET.6 +---- + +expand-inputs 0 +---- +0: a#1,1-b#2,1 + +expand-inputs 1 +---- +1: c#3,1-d#4,1 + +expand-inputs 2 +---- +2: e#5,1-f#6,1 + +define +a.SET.1-b.SET.2 +b.SET.1-d.SET.4 +e.SET.5-f.SET.6 +---- + +expand-inputs 0 +---- +0: a#1,1-b#2,1 +1: b#1,1-d#4,1 + +expand-inputs 1 +---- +0: a#1,1-b#2,1 +1: b#1,1-d#4,1 + +expand-inputs 2 +---- +2: e#5,1-f#6,1 + +define +a.SET.1-b.SET.2 +b.SET.1-d.SET.4 +d.SET.2-f.SET.6 +---- + +expand-inputs 0 +---- +0: a#1,1-b#2,1 +1: b#1,1-d#4,1 +2: d#2,1-f#6,1 + +expand-inputs 1 +---- +0: a#1,1-b#2,1 +1: b#1,1-d#4,1 +2: d#2,1-f#6,1 + +define +a.SET.1-b.RANGEDEL.72057594037927935 +b.SET.1-d.SET.4 +d.SET.2-f.SET.6 +---- + +expand-inputs 0 +---- +0: a#1,1-b#72057594037927935,15 + +expand-inputs 1 +---- +1: b#1,1-d#4,1 +2: d#2,1-f#6,1 diff --git a/pebble/testdata/compaction_find_grandparent_limit b/pebble/testdata/compaction_find_grandparent_limit new file mode 100644 index 0000000..e1c88ba --- /dev/null +++ b/pebble/testdata/compaction_find_grandparent_limit @@ -0,0 +1,100 @@ +# no grandparents +define +---- + +compact max-overlap=1 +a b c d e f g +---- +a-g + +# 3 equal size grandparents +define +a-b 2 +c-d 2 +e-f 2 +---- + +compact max-overlap=1 +a b c d e f +---- +a-c +d-e +f-f + +compact max-overlap=2 +a b c d e f +---- +a-c +d-e +f-f + +compact max-overlap=4 +a b c d e f +---- +a-e +f-f + +compact max-overlap=1 +b c d e f g h i j +---- +b-c +d-e +f-j + +compact max-overlap=1 +a g h i j +---- +a-a +g-j + +compact max-overlap=1 +a e ee eee eeee f +---- +a-a +e-f + +compact max-overlap=1 +c d e f +---- +c-e +f-f + +# Unequal size grandparents +define +a-b 1 +c-d 2 +e-f 3 +---- + +compact max-overlap=1 +a b c d e f +---- +a-c +d-e +f-f + +compact max-overlap=3 +a b c d e f +---- +a-e +f-f + +# Unequal size grandparents +define +a-b 3 +c-d 2 +e-f 1 +---- + +compact max-overlap=1 +a b c d e f +---- +a-c +d-e +f-f + +compact max-overlap=3 +a b c d e f +---- +a-c +d-f diff --git a/pebble/testdata/compaction_find_l0_limit b/pebble/testdata/compaction_find_l0_limit new file mode 100644 index 0000000..4ea4e6c --- /dev/null +++ b/pebble/testdata/compaction_find_l0_limit @@ -0,0 +1,88 @@ +define flush_split_bytes=4 +L0 + a.SET.1-k.SET.10 size=2 + l.SET.11-o.SET.13 size=2 + p.SET.14-s.SET.16 size=2 + t.SET.17-w.SET.19 size=2 +---- +0.0: + 000001:[a#1,SET-k#10,SET] + 000002:[l#11,SET-o#13,SET] + 000003:[p#14,SET-s#16,SET] + 000004:[t#17,SET-w#19,SET] +flush split keys: + s + +flush +a c f l o s u x +---- +a-s +u-x + +define flush_split_bytes=2 +L0 + a.SET.1-k.SET.10 size=2 + l.SET.11-o.SET.13 size=2 + p.SET.14-s.SET.16 size=2 + t.SET.17-w.SET.19 size=2 +---- +0.0: + 000005:[a#1,SET-k#10,SET] + 000006:[l#11,SET-o#13,SET] + 000007:[p#14,SET-s#16,SET] + 000008:[t#17,SET-w#19,SET] +flush split keys: + o + w + +flush +a c f l o s u x +---- +a-o +s-u +x-x + +define flush_split_bytes=1 +L0 + a.SET.1-k.SET.10 size=2 + l.SET.11-o.SET.13 size=2 + p.SET.14-s.SET.16 size=2 + t.SET.17-w.SET.19 size=2 +---- +0.0: + 000009:[a#1,SET-k#10,SET] + 000010:[l#11,SET-o#13,SET] + 000011:[p#14,SET-s#16,SET] + 000012:[t#17,SET-w#19,SET] +flush split keys: + k + o + s + w + +flush +a c f l o s u x +---- +a-f +l-o +s-u +x-x + +define flush_split_bytes=0 +L0 + a.SET.1-k.SET.10 size=2 + l.SET.11-o.SET.13 size=2 + p.SET.14-s.SET.16 size=2 + t.SET.17-w.SET.19 size=2 +---- +0.0: + 000013:[a#1,SET-k#10,SET] + 000014:[l#11,SET-o#13,SET] + 000015:[p#14,SET-s#16,SET] + 000016:[t#17,SET-w#19,SET] +flush split keys: + +flush +a c f l o s u x +---- +a-x diff --git a/pebble/testdata/compaction_inuse_key_ranges b/pebble/testdata/compaction_inuse_key_ranges new file mode 100644 index 0000000..704de7e --- /dev/null +++ b/pebble/testdata/compaction_inuse_key_ranges @@ -0,0 +1,409 @@ +define +L1 + a.SET.1-b.SET.1 + d.SET.1-e.SET.1 + e.SET.1-f.SET.1 +---- +1: + 000001:[a#1,SET-b#1,SET] + 000002:[d#1,SET-e#1,SET] + 000003:[e#1,SET-f#1,SET] + +inuse-key-ranges +0 a b +0 c d +0 g h +1 a b +---- +a-b +d-e +. +. + +define +L1 + a.SET.1-b.SET.1 +L2 + b.SET.1-c.SET.2 +---- +1: + 000001:[a#1,SET-b#1,SET] +2: + 000002:[b#1,SET-c#2,SET] + +inuse-key-ranges +0 a c +---- +a-c + +define +L1 + a.SET.1-b.SET.1 +L2 + c.SET.1-d.SET.2 +---- +1: + 000001:[a#1,SET-b#1,SET] +2: + 000002:[c#1,SET-d#2,SET] + +inuse-key-ranges +0 a c +---- +a-b c-d + +define +L1 + b.SET.1-c.SET.1 +L2 + a.SET.1-b.SET.2 +---- +1: + 000001:[b#1,SET-c#1,SET] +2: + 000002:[a#1,SET-b#2,SET] + +inuse-key-ranges +0 a c +---- +a-c + +define +L1 + c.SET.1-d.SET.1 +L2 + a.SET.1-b.SET.2 +---- +1: + 000001:[c#1,SET-d#1,SET] +2: + 000002:[a#1,SET-b#2,SET] + +inuse-key-ranges +0 a c +---- +a-b c-d + +define +L1 + a.SET.1-b.SET.1 + c.SET.1-d.SET.1 + f.SET.1-g.SET.1 + i.SET.1-j.SET.1 +---- +1: + 000001:[a#1,SET-b#1,SET] + 000002:[c#1,SET-d#1,SET] + 000003:[f#1,SET-g#1,SET] + 000004:[i#1,SET-j#1,SET] + +inuse-key-ranges +0 a z +0 a c +0 g z +---- +a-b c-d f-g i-j +a-b c-d +f-g i-j + +define +L1 + a.SET.1-b.SET.1 + c.SET.1-d.SET.1 + f.SET.1-g.SET.1 + i.SET.1-j.SET.1 +L6 + a.SET.0-i.SET.0 + k.SET.0-z.SET.0 +---- +1: + 000001:[a#1,SET-b#1,SET] + 000002:[c#1,SET-d#1,SET] + 000003:[f#1,SET-g#1,SET] + 000004:[i#1,SET-j#1,SET] +6: + 000005:[a#0,SET-i#0,SET] + 000006:[k#0,SET-z#0,SET] + +inuse-key-ranges +0 a z +---- +a-j k-z + +define +L0 + a.SET.1-b.SET.1 + c.SET.1-d.SET.1 + f.SET.1-g.SET.1 + i.SET.1-j.SET.1 +L6 + a.SET.0-i.SET.0 + k.SET.0-z.SET.0 +---- +0.0: + 000001:[a#1,SET-b#1,SET] + 000002:[c#1,SET-d#1,SET] + 000003:[f#1,SET-g#1,SET] + 000004:[i#1,SET-j#1,SET] +6: + 000005:[a#0,SET-i#0,SET] + 000006:[k#0,SET-z#0,SET] + +inuse-key-ranges +0 a z +---- +a-j k-z + +define +L0 + a.SET.1-b.SET.1 + aa.SET.1-ab.SET.1 + b.SET.2-d.SET.1 + bb.SET.1-dd.SET.1 + c.SET.1-d.SET.1 + e.SET.1-m.SET.1 + g.SET.1-p.SET.1 +---- +0.3: + 000005:[c#1,SET-d#1,SET] +0.2: + 000004:[bb#1,SET-dd#1,SET] +0.1: + 000002:[aa#1,SET-ab#1,SET] + 000003:[b#2,SET-d#1,SET] + 000007:[g#1,SET-p#1,SET] +0.0: + 000001:[a#1,SET-b#1,SET] + 000006:[e#1,SET-m#1,SET] + +inuse-key-ranges +0 a z +0 e p +0 e f +0 b c +0 q r +0 1 2 +0 ddd dddd +---- +a-dd e-p +e-p +e-m +b-dd +. +. +. + +define +L1 + a.SET.6-b.SET.6 + d.SET.6-g.SET.6 +L2 + c.SET.5-d.SET.5 + i.SET.5-j.SET.5 +L3 + b.SET.1-c.SET.1 +L4 + f.SET.1-k.SET.1 +L6 + m.SET.1-z.SET.1 +---- +1: + 000001:[a#6,SET-b#6,SET] + 000002:[d#6,SET-g#6,SET] +2: + 000003:[c#5,SET-d#5,SET] + 000004:[i#5,SET-j#5,SET] +3: + 000005:[b#1,SET-c#1,SET] +4: + 000006:[f#1,SET-k#1,SET] +6: + 000007:[m#1,SET-z#1,SET] + +inuse-key-ranges +5 a z +5 a b +5 m z +5 m zz +5 mm zz +5 l x +5 l zz +---- +m-z +. +m-z +m-z +m-z +m-z +m-z + +inuse-key-ranges +3 a z +3 f k +3 k m +3 l ll +3 b n +---- +f-k m-z +f-k +f-k m-z +. +f-k m-z + +inuse-key-ranges +2 a z +---- +b-c f-k m-z + +inuse-key-ranges +1 a z +---- +b-d f-k m-z + +inuse-key-ranges +0 a z +0 a k +0 a b +0 bb bc +0 f k +---- +a-k m-z +a-k +a-c +b-c +d-k + +define +L1 + m.SET.6-p.SET.6 +L2 + j.SET.5-n.SET.5 + o.SET.5-t.SET.5 +L3 + e.SET.2-k.SET.2 + s.SET.2-x.SET.2 +L4 + a.SET.1-f.SET.1 + w.SET.1-z.SET.1 +---- +1: + 000001:[m#6,SET-p#6,SET] +2: + 000002:[j#5,SET-n#5,SET] + 000003:[o#5,SET-t#5,SET] +3: + 000004:[e#2,SET-k#2,SET] + 000005:[s#2,SET-x#2,SET] +4: + 000006:[a#1,SET-f#1,SET] + 000007:[w#1,SET-z#1,SET] + +inuse-key-ranges +3 a z +2 a z +1 a z +0 a z +0 a n +0 a mm +0 a nn +0 p z +0 pp z +0 oo z +---- +a-f w-z +a-k s-z +a-n o-z +a-z +a-p +a-p +a-p +m-z +o-z +m-z + +define +L1 + a.SET.6-c.SET.6 +L2 + b.SET.5-b.SET.5 + bb.SET.5-bb.SET.5 + cc.SET.5-cc.SET.5 +---- +1: + 000001:[a#6,SET-c#6,SET] +2: + 000002:[b#5,SET-b#5,SET] + 000003:[bb#5,SET-bb#5,SET] + 000004:[cc#5,SET-cc#5,SET] + +inuse-key-ranges +0 a c +0 a cc +---- +a-c +a-c cc-cc + +define +L1 + a.SET.6-c.SET.6 +L2 + b.SET.5-b.SET.5 + bb.SET.5-bb.SET.5 + bc.SET.5-c.SET.5 + c.SET.5-c.SET.5 + c.SET.5-d.SET.5 +---- +1: + 000001:[a#6,SET-c#6,SET] +2: + 000002:[b#5,SET-b#5,SET] + 000003:[bb#5,SET-bb#5,SET] + 000004:[bc#5,SET-c#5,SET] + 000005:[c#5,SET-c#5,SET] + 000006:[c#5,SET-d#5,SET] + +inuse-key-ranges +0 a c +0 a cc +0 a d +0 c c +---- +a-d +a-d +a-d +a-d + + +define +L0 + d.SET.7-i.SET.7 +L1 + a.SET.6-a.SET.6 + d.SET.6-d.SET.6 + h.SET.6-i.SET.6 +L2 + b.SET.5-b.SET.5 + c.SET.5-c.SET.5 + e.SET.5-e.SET.5 +L3 + bb.SET.4-bb.SET.4 +---- +0.0: + 000001:[d#7,SET-i#7,SET] +1: + 000002:[a#6,SET-a#6,SET] + 000003:[d#6,SET-d#6,SET] + 000004:[h#6,SET-i#6,SET] +2: + 000005:[b#5,SET-b#5,SET] + 000006:[c#5,SET-c#5,SET] + 000007:[e#5,SET-e#5,SET] +3: + 000008:[bb#4,SET-bb#4,SET] + +inuse-key-ranges +0 a z +1 a z +---- +a-a b-b bb-bb c-c d-i +b-b bb-bb c-c e-e diff --git a/pebble/testdata/compaction_iter b/pebble/testdata/compaction_iter new file mode 100644 index 0000000..3e83116 --- /dev/null +++ b/pebble/testdata/compaction_iter @@ -0,0 +1,1218 @@ +define +a.SET.1:b +---- + +iter print-snapshot-pinned +first +next +---- +a#1,1:b (not pinned) +. + +define +a.SET.2:c +a.SET.1:b +---- + +iter print-snapshot-pinned +first +next +---- +a#2,1:c (not pinned) +. + +iter print-snapshot-pinned snapshots=0 +first +next +---- +a#2,1:c (not pinned) +. + +iter snapshots=1 +first +next +---- +a#2,1:c +. + +iter print-snapshot-pinned snapshots=2 +first +next +next +---- +a#2,1:c (not pinned) +a#1,1:b (pinned) +. + +define +a.DEL.2: +a.SET.1:b +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +iter print-snapshot-pinned elide-tombstones=true snapshots=2 +first +next +next +---- +a#2,0: (pinned) +a#1,1:b (pinned) +. + +iter print-snapshot-pinned elide-tombstones=true snapshots=1 +first +next +---- +a#2,0: (pinned) +. + +define +a.DEL.2: +a.SET.1:b +b.SET.3:c +---- + +iter print-snapshot-pinned +first +next +next +---- +a#2,0: (not pinned) +b#3,1:c (not pinned) +. + +iter snapshots=1 +first +next +next +---- +a#2,0: +b#3,1:c +. + +iter snapshots=2 +first +next +next +next +---- +a#2,0: +a#1,1:b +b#3,1:c +. + +define +a.SET.1:a +b.SET.2:b +c.SET.3:c +---- + +iter +first +next +next +next +---- +a#1,1:a +b#2,1:b +c#3,1:c +. + +define +a.MERGE.3:d +a.MERGE.2:c +a.SET.1:b +b.MERGE.2:b +b.MERGE.1:a +---- + +iter +first +next +next +---- +a#3,1:bcd[base] +b#2,2:ab +. + +iter snapshots=3 print-snapshot-pinned +first +next +next +next +---- +a#3,2:d (not pinned) +a#2,1:bc[base] (pinned) +b#2,2:ab (not pinned) +. + +define +a.SET.9:b +a.DEL.8: +a.SET.7:d +a.DEL.6: +a.SET.5:f +---- + +iter +first +next +---- +a#9,1:b +. + +iter snapshots=6 +first +next +next +---- +a#9,1:b +a#5,1:f +. + +iter snapshots=7 +first +next +next +---- +a#9,1:b +a#6,0: +. + +iter snapshots=8 +first +next +next +---- +a#9,1:b +a#7,1:d +. + +iter snapshots=9 +first +next +next +---- +a#9,1:b +a#8,0: +. + +iter snapshots=10 +first +next +---- +a#9,1:b +. + +iter snapshots=(5,6,7,8,9) print-snapshot-pinned +first +next +next +next +next +next +---- +a#9,1:b (not pinned) +a#8,0: (pinned) +a#7,1:d (pinned) +a#6,0: (pinned) +a#5,1:f (pinned) +. + +define +a.INVALID.2:b +a.SET.1:c +---- + +iter +first +---- +err=invalid internal key kind: INVALID + +define +a.SET.2:b +a.INVALID.1:c +---- + +iter +first +next +---- +a#2,1:b +err=invalid internal key kind: INVALID + +define +a.MERGE.2:b +a.INVALID.1:c +---- + +iter +first +next +---- +a#2,2:b +err=invalid internal key kind: INVALID + +define +a.INVALID.2:c +a.RANGEDEL.1:d +---- + +iter +first +tombstones +---- +err=invalid internal key kind: INVALID +. + +define +a.MERGE.2:b +a.MERGE.1:c +a.MERGE.0:d +---- + +iter snapshots=(1,2) print-snapshot-pinned +first +next +next +next +---- +a#2,2:b (not pinned) +a#1,2:c (pinned) +a#0,2:d (pinned) +. + +define +a.SET.2:b +a.RANGEDEL.1:c +b.RANGEDEL.4:d +b.SET.2:e +c.SET.3:f +---- + +# NB: Range deletions are always marked as 'not pinned' currently. Extending +# snapshot-pinning statistics to range deletions and range keys is TODO. + +iter print-snapshot-pinned +first +next +next +next +tombstones +---- +a#2,1:b (not pinned) +a#1,15:c (not pinned) +b#4,15:d (not pinned) +. +a-b#1 +b-c#4 +c-d#4 +. + +iter snapshots=2 print-snapshot-pinned +first +next +next +next +tombstones +---- +a#2,1:b (not pinned) +a#1,15:c (not pinned) +b#4,15:d (not pinned) +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +iter snapshots=3 print-snapshot-pinned +first +next +next +next +next +tombstones +---- +a#2,1:b (not pinned) +a#1,15:c (not pinned) +b#4,15:d (not pinned) +b#2,1:e (pinned) +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +iter snapshots=4 print-snapshot-pinned +first +next +next +next +next +next +tombstones +---- +a#2,1:b (not pinned) +a#1,15:c (not pinned) +b#4,15:d (not pinned) +b#2,1:e (pinned) +c#3,1:f (pinned) +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +define +a.RANGEDEL.3:e +b.SET.4:b +c.SET.3:c +d.SET.2:d +e.SET.1:e +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:e +b#4,1:b +c#3,1:c +e#1,1:e +. +a-e#3 +. + +define +a.RANGEDEL.3:e +b.MERGE.4:b +c.MERGE.3:c +d.MERGE.2:d +e.MERGE.1:e +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:e +b#4,2:b +c#3,2:c +e#1,2:e +. +a-e#3 +. + +define +a.RANGEDEL.3:c +b.MERGE.5:e +b.MERGE.4:d +b.MERGE.2:c +b.MERGE.1:b +d.MERGE.5:c +d.MERGE.4:b +d.RANGEDEL.3:f +d.MERGE.2:e +d.MERGE.1:d +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:c +b#5,1:de[base] +d#5,2:bc +d#3,15:f +. +a-c#3 +d-f#3 +. + +define +a.RANGEDEL.3:d +b.RANGEDEL.2:e +c.RANGEDEL.1:f +---- + +iter +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +c-d#3 +d-e#2 +e-f#1 +. + +iter snapshots=2 +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +c-d#3 +c-d#1 +d-e#2 +d-e#1 +e-f#1 +. + +iter snapshots=3 +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +b-c#2 +c-d#3 +c-d#2 +d-e#2 +e-f#1 +. + +iter snapshots=(2,3) +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +b-c#2 +c-d#3 +c-d#2 +c-d#1 +d-e#2 +d-e#1 +e-f#1 +. + +define +a.RANGEDEL.10:k +f.SET.9:f +f.SET.8:f +---- + +iter snapshots=(9,10) +first +next +tombstones f +next +tombstones +---- +a#10,15:k +f#9,1:f +a-f#10 +. +f#8,1:f +f-k#10 +. + +define +f.RANGEDEL.10:k +f.SET.9:f +f.SET.8:f +---- + +iter snapshots=(9,10) +first +next +tombstones f +next +tombstones +---- +f#10,15:k +f#9,1:f +. +f#8,1:f +f-k#10 +. + +define +a.SET.1:a +b.RANGEDEL.2:d +c.RANGEDEL.3:e +d.SET.4:d +---- + +iter +first +next +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#3,15:e +d#4,1:d +b-c#2 +. +c-d#3 +d-e#3 +. + +iter snapshots=3 +first +next +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#3,15:e +d#4,1:d +b-c#2 +. +c-d#3 +c-d#2 +d-e#3 +. + +define +a.SET.1:a +b.RANGEDEL.2:d +c.SET.4:d +---- + +iter +first +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#4,1:d +b-c#2 +. +c-d#2 +. + +define +a.RANGEDEL.2:d +a.SET.2:a +b.SET.2:b +c.SET.2:c +---- + +iter +first +next +next +next +next +---- +a#2,15:d +a#2,1:a +b#2,1:b +c#2,1:c +. + +define +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#1,7: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#2,7: +. + +define +a.SINGLEDEL.3: +a.SINGLEDEL.2: +a.SET.1:a +---- + +iter +first +---- +. + +define +a.SET.3:a +b.SINGLEDEL.2: +b.DEL.1: +---- + +iter +first +next +next +---- +a#3,1:a +b#2,0: +. + +define +a.SINGLEDEL.2: +a.DEL.1: +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.MERGE.1: +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.SET.1:b +---- + +iter +first +---- +. + +define +a.SET.2:b +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#2,1:b +. + +define +a.MERGE.6:b +a.SINGLEDEL.5: +a.SET.4:a +---- + +iter +first +next +---- +a#6,18:b[base] +. + +# Non-deterministic use of SINGLEDEL where there are two older SETs that have +# not been deleted or single deleted. It is permitted to shadow both. +define +a.MERGE.6:b +a.SINGLEDEL.5: +a.SET.4:a +a.SET.3:a +---- + +iter +first +next +---- +a#6,18:b[base] +. + +define +a.SINGLEDEL.2: +a.SET.1:b +b.SET.3:c +---- + +iter +first +next +---- +b#3,1:c +. + +define +a.SINGLEDEL.3: +a.SET.2:b +a.SET.1:a +---- + +iter +first +next +---- +a#1,1:a +. + +define +a.SINGLEDEL.3: +a.MERGE.2:b +a.MERGE.1:a +---- + +iter +first +next +---- +a#3,0: +. + +define +a.SINGLEDEL.4: +a.SET.3:val +a.SINGLEDEL.2: +a.SET.1:val +---- + +iter +first +---- +. + +iter snapshots=2 +first +next +next +---- +a#2,7: +a#1,1:val +. + +define +a.SINGLEDEL.4: +a.SET.3:val +a.DEL.2: +a.SET.1:val +---- + +iter +first +next +---- +a#2,0: +. + +iter snapshots=2 +first +next +next +---- +a#2,0: +a#1,1:val +. + +iter snapshots=3 +first +next +---- +a#2,0: +. + +iter snapshots=(2,3) +first +next +next +---- +a#2,0: +a#1,1:val +. + +define +a.SINGLEDEL.4: +a.SET.3:c +a.MERGE.2:b +a.SET.1:a +---- + +iter +first +next +---- +a#2,1:ab[base] +. + +iter snapshots=2 +first +next +next +---- +a#2,2:b +a#1,1:a +. + +iter snapshots=3 +first +next +---- +a#2,1:ab[base] +. + +iter snapshots=(2,3,4) +first +next +next +next +next +---- +a#4,7: +a#3,1:c +a#2,2:b +a#1,1:a +. + +define +a.SINGLEDEL.3: +a.RANGEDEL.2:c +a.SET.1:val +---- + +iter +first +next +next +tombstones +---- +a#3,7: +a#2,15:c +. +a-c#2 +. + +define +a.RANGEDEL.3:d +a.DEL.2: +a.SET.1:a +d.DEL.2: +---- + +iter +first +next +next +tombstones +---- +a#3,15:d +d#2,0: +. +a-d#3 +. + +iter snapshots=3 +first +next +next +next +---- +a#3,15:d +a#2,0: +d#2,0: +. + +iter snapshots=2 +first +next +next +next +---- +a#3,15:d +a#1,1:a +d#2,0: +. + +iter snapshots=1 +first +next +next +---- +a#3,15:d +d#2,0: +. + +define +a.MERGE.2:a +b.RANGEDEL.1:c +---- + +iter +first +tombstones a +next +next +tombstones +---- +a#2,2:a +. +b#1,15:c +. +b-c#1 +. + +define +a.MERGE.2:v2 +a.RANGEDEL.1:b +a.MERGE.1:v1 +---- + +iter allow-zero-seqnum=true +first +next +next +next +tombstones +---- +a#2,2:v2 +a#1,15:b +a#0,2:v1 +. +a-b#1 +. + +# Verify that we transform merge+del -> set. + +define +a.MERGE.5:5 +a.DEL.3: +a.MERGE.1:1 +---- + +iter +first +next +---- +a#5,18:5[base] +. + +iter allow-zero-seqnum=true +first +next +---- +a#0,18:5[base] +. + +iter elide-tombstones=true +first +next +---- +a#5,18:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#5,18:5[base] +a#1,2:1 +. + +iter snapshots=2 elide-tombstones=true +first +next +next +---- +a#5,18:5[base] +a#1,2:1 +. + +# Verify that we transform merge+rangedel -> set. This isn't strictly +# necessary, but provides consistency with the behavior for merge+del. + +define +a.RANGEDEL.3:c +b.MERGE.5:5 +b.SET.2:2 +b.MERGE.1:1 +---- + +iter +first +next +next +---- +a#3,15:c +b#5,1:5[base] +. + +iter allow-zero-seqnum=true +first +next +next +---- +a#3,15:c +b#0,1:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#3,15:c +b#5,1:5[base] +b#1,2:1 + +define +a.RANGEDEL.3:c +b.MERGE.5:5 +b.MERGE.2:2 +b.MERGE.1:1 +---- + +iter +first +next +next +---- +a#3,15:c +b#5,1:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#3,15:c +b#5,1:5[base] +b#1,2:1 + +# NB: Zero values are skipped by deletable merger. +define merger=deletable +a.MERGE.4:-2 +a.MERGE.3:-1 +a.MERGE.2:2 +a.MERGE.1:1 +b.MERGE.4:-3 +b.MERGE.3:3 +b.MERGE.2:2 +b.MERGE.1:-2 +---- + +iter +first +next +next +---- +. +. +. + +# Test that range keys are interleaved, and exposed to the fragmenter. + +define +a.SINGLEDEL.4: +a.SET.3:val +a.DEL.2: +a.SET.1:val +c.SET.3:val +---- + +define-range-keys +a-b:{(#3,RANGEKEYSET,@2,foo)} +d-e:{(#3,RANGEKEYSET,@2,foo)} +---- + +iter +first +next +next +next +next +range-keys +---- +a#72057594037927935,21: +a#2,0: +c#3,1:val +d#72057594037927935,21: +. +a-b:{(#3,RANGEKEYSET,@2,foo)} +d-e:{(#3,RANGEKEYSET,@2,foo)} +. diff --git a/pebble/testdata/compaction_iter_delete_sized b/pebble/testdata/compaction_iter_delete_sized new file mode 100644 index 0000000..275927e --- /dev/null +++ b/pebble/testdata/compaction_iter_delete_sized @@ -0,0 +1,1897 @@ +define +a.SET.1:b +---- + +iter +first +next +---- +a#1,1:b +. + +define +a.SET.2:c +a.SET.1:b +---- + +iter +first +next +---- +a#2,1:c +. + +iter snapshots=0 +first +next +---- +a#2,1:c +. + +iter snapshots=1 +first +next +---- +a#2,1:c +. + +iter snapshots=2 +first +next +next +---- +a#2,1:c +a#1,1:b +. + +define +a.DEL.2: +a.SET.1:b +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +iter elide-tombstones=true snapshots=2 +first +next +next +---- +a#2,0: +a#1,1:b +. + +iter elide-tombstones=true snapshots=1 +first +next +---- +a#2,0: +. + +define +a.DEL.2: +a.SET.1:b +b.SET.3:c +---- + +iter +first +next +next +---- +a#2,0: +b#3,1:c +. + +iter snapshots=1 +first +next +next +---- +a#2,0: +b#3,1:c +. + +iter snapshots=2 +first +next +next +next +---- +a#2,0: +a#1,1:b +b#3,1:c +. + +define +a.SET.1:a +b.SET.2:b +c.SET.3:c +---- + +iter +first +next +next +next +---- +a#1,1:a +b#2,1:b +c#3,1:c +. + +define +a.MERGE.3:d +a.MERGE.2:c +a.SET.1:b +b.MERGE.2:b +b.MERGE.1:a +---- + +iter +first +next +next +---- +a#3,1:bcd[base] +b#2,2:ab +. + +iter snapshots=3 print-snapshot-pinned print-force-obsolete +first +next +next +next +---- +a#3,2:d (not pinned) (not force obsolete) +a#2,1:bc[base] (pinned) (not force obsolete) +b#2,2:ab (not pinned) (not force obsolete) +. + +define +a.SET.9:b +a.DEL.8: +a.SET.7:d +a.DEL.6: +a.SET.5:f +---- + +iter +first +next +---- +a#9,18:b +. + +iter snapshots=6 +first +next +next +---- +a#9,18:b +a#5,1:f +. + +iter snapshots=7 +first +next +next +---- +a#9,18:b +a#6,0: +. + +iter snapshots=8 +first +next +next +---- +a#9,18:b +a#7,18:d +. + +iter snapshots=9 +first +next +next +---- +a#9,1:b +a#8,0: +. + +iter snapshots=10 +first +next +---- +a#9,18:b +. + +iter snapshots=(5,6,7,8,9) +first +next +next +next +next +next +---- +a#9,1:b +a#8,0: +a#7,1:d +a#6,0: +a#5,1:f +. + +define +a.INVALID.2:b +a.SET.1:c +---- + +iter +first +---- +err=invalid internal key kind: INVALID + +define +a.SET.2:b +a.INVALID.1:c +---- + +iter +first +next +---- +a#2,18:b +err=invalid internal key kind: INVALID + +define +a.MERGE.2:b +a.INVALID.1:c +---- + +iter +first +next +---- +a#2,2:b +err=invalid internal key kind: INVALID + +define +a.INVALID.2:c +a.RANGEDEL.1:d +---- + +iter +first +tombstones +---- +err=invalid internal key kind: INVALID +. + +define +a.MERGE.2:b +a.MERGE.1:c +a.MERGE.0:d +---- + +iter snapshots=(1,2) print-snapshot-pinned print-force-obsolete +first +next +next +next +---- +a#2,2:b (not pinned) (not force obsolete) +a#1,2:c (pinned) (not force obsolete) +a#0,2:d (pinned) (not force obsolete) +. + +define +a.SET.2:b +a.RANGEDEL.1:c +b.RANGEDEL.4:d +b.SET.2:e +c.SET.3:f +---- + +iter +first +next +next +next +tombstones +---- +a#2,18:b +a#1,15:c +b#4,15:d +. +a-b#1 +b-c#4 +c-d#4 +. + +iter snapshots=2 +first +next +next +next +tombstones +---- +a#2,1:b +a#1,15:c +b#4,15:d +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +iter snapshots=3 print-snapshot-pinned print-force-obsolete +first +next +next +next +next +tombstones +---- +a#2,18:b (not pinned) (not force obsolete) +a#1,15:c (not pinned) (not force obsolete) +b#4,15:d (not pinned) (not force obsolete) +b#2,1:e (pinned) (force obsolete) +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +iter snapshots=4 print-snapshot-pinned print-force-obsolete +first +next +next +next +next +next +tombstones +---- +a#2,18:b (not pinned) (not force obsolete) +a#1,15:c (not pinned) (not force obsolete) +b#4,15:d (not pinned) (not force obsolete) +b#2,1:e (pinned) (force obsolete) +c#3,1:f (pinned) (force obsolete) +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +define +a.RANGEDEL.3:e +b.SET.4:b +c.SET.3:c +d.SET.2:d +e.SET.1:e +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:e +b#4,1:b +c#3,1:c +e#1,1:e +. +a-e#3 +. + +define +a.RANGEDEL.3:e +b.MERGE.4:b +c.MERGE.3:c +d.MERGE.2:d +e.MERGE.1:e +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:e +b#4,2:b +c#3,2:c +e#1,2:e +. +a-e#3 +. + +define +a.RANGEDEL.3:c +b.MERGE.5:e +b.MERGE.4:d +b.MERGE.2:c +b.MERGE.1:b +d.MERGE.5:c +d.MERGE.4:b +d.RANGEDEL.3:f +d.MERGE.2:e +d.MERGE.1:d +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:c +b#5,1:de[base] +d#5,2:bc +d#3,15:f +. +a-c#3 +d-f#3 +. + +define +a.RANGEDEL.3:d +b.RANGEDEL.2:e +c.RANGEDEL.1:f +---- + +iter +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +c-d#3 +d-e#2 +e-f#1 +. + +iter snapshots=2 +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +c-d#3 +c-d#1 +d-e#2 +d-e#1 +e-f#1 +. + +iter snapshots=3 +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +b-c#2 +c-d#3 +c-d#2 +d-e#2 +e-f#1 +. + +iter snapshots=(2,3) +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +b-c#2 +c-d#3 +c-d#2 +c-d#1 +d-e#2 +d-e#1 +e-f#1 +. + +define +a.RANGEDEL.10:k +f.SET.9:f +f.SET.8:f +---- + +iter snapshots=(9,10) print-snapshot-pinned print-force-obsolete +first +next +tombstones f +next +tombstones +---- +a#10,15:k (not pinned) (not force obsolete) +f#9,1:f (pinned) (force obsolete) +a-f#10 +. +f#8,1:f (pinned) (force obsolete) +f-k#10 +. + +define +f.RANGEDEL.10:k +f.SET.9:f +f.SET.8:f +---- + +iter snapshots=(9,10) +first +next +tombstones f +next +tombstones +---- +f#10,15:k +f#9,1:f +. +f#8,1:f +f-k#10 +. + +define +a.SET.1:a +b.RANGEDEL.2:d +c.RANGEDEL.3:e +d.SET.4:d +---- + +iter +first +next +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#3,15:e +d#4,1:d +b-c#2 +. +c-d#3 +d-e#3 +. + +iter snapshots=3 +first +next +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#3,15:e +d#4,1:d +b-c#2 +. +c-d#3 +c-d#2 +d-e#3 +. + +define +a.SET.1:a +b.RANGEDEL.2:d +c.SET.4:d +---- + +iter +first +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#4,1:d +b-c#2 +. +c-d#2 +. + +define +a.RANGEDEL.2:d +a.SET.2:a +b.SET.2:b +c.SET.2:c +---- + +iter +first +next +next +next +next +---- +a#2,15:d +a#2,1:a +b#2,1:b +c#2,1:c +. + +define +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#1,7: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#2,7: +. + +define +a.SINGLEDEL.3: +a.SINGLEDEL.2: +a.SET.1:a +---- + +iter +first +---- +. + +define +a.SET.3:a +b.SINGLEDEL.2: +b.DEL.1: +---- + +iter +first +next +next +---- +a#3,1:a +b#2,0: +. + +define +a.SINGLEDEL.2: +a.DEL.1: +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.MERGE.1: +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.SET.1:b +---- + +iter +first +---- +. + +# SET that meets a SINGLEDEL is transformed into a SETWITHDEL. + +define +a.SET.2:b +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#2,18:b +. + +define +a.MERGE.6:b +a.SINGLEDEL.5: +a.SET.4:a +---- + +iter +first +next +---- +a#6,18:b[base] +. + +# Non-deterministic use of SINGLEDEL where there are two older SETs that have +# not been deleted or single deleted. It is permitted to shadow both. +define +a.MERGE.6:b +a.SINGLEDEL.5: +a.SET.4:a +a.SET.3:a +---- + +iter +first +next +---- +a#6,18:b[base] +. + +define +a.SINGLEDEL.2: +a.SET.1:b +b.SET.3:c +---- + +iter +first +next +---- +b#3,1:c +. + +define +a.SINGLEDEL.3: +a.SET.2:b +a.SET.1:a +---- + +iter +first +next +---- +a#1,1:a +. + +define +a.SINGLEDEL.3: +a.MERGE.2:b +a.MERGE.1:a +---- + +iter +first +next +---- +a#3,0: +. + +define +a.SINGLEDEL.4: +a.SET.3:val +a.SINGLEDEL.2: +a.SET.1:val +---- + +iter +first +---- +. + +iter snapshots=2 +first +next +next +---- +a#2,7: +a#1,1:val +. + +define +a.SINGLEDEL.4: +a.SET.3:val +a.DEL.2: +a.SET.1:val +---- + +iter +first +next +---- +a#2,0: +. + +iter snapshots=2 +first +next +next +---- +a#2,0: +a#1,1:val +. + +iter snapshots=3 +first +next +---- +a#2,0: +. + +iter snapshots=(2,3) +first +next +next +---- +a#2,0: +a#1,1:val +. + +define +a.SINGLEDEL.4: +a.SET.3:c +a.MERGE.2:b +a.SET.1:a +---- + +iter +first +next +---- +a#2,1:ab[base] +. + +iter snapshots=2 +first +next +next +---- +a#2,2:b +a#1,1:a +. + +iter snapshots=3 +first +next +---- +a#2,1:ab[base] +. + +iter snapshots=(2,3,4) +first +next +next +next +next +---- +a#4,7: +a#3,1:c +a#2,2:b +a#1,1:a +. + +define +a.SINGLEDEL.3: +a.RANGEDEL.2:c +a.SET.1:val +---- + +iter +first +next +next +tombstones +---- +a#3,7: +a#2,15:c +. +a-c#2 +. + +define +a.RANGEDEL.3:d +a.DEL.2: +a.SET.1:a +d.DEL.2: +---- + +iter +first +next +next +tombstones +---- +a#3,15:d +d#2,0: +. +a-d#3 +. + +iter snapshots=3 +first +next +next +next +---- +a#3,15:d +a#2,0: +d#2,0: +. + +iter snapshots=2 +first +next +next +next +---- +a#3,15:d +a#1,1:a +d#2,0: +. + +iter snapshots=1 +first +next +next +---- +a#3,15:d +d#2,0: +. + +define +a.MERGE.2:a +b.RANGEDEL.1:c +---- + +iter +first +tombstones a +next +next +tombstones +---- +a#2,2:a +. +b#1,15:c +. +b-c#1 +. + +define +a.MERGE.2:v2 +a.RANGEDEL.1:b +a.MERGE.1:v1 +---- + +iter allow-zero-seqnum=true +first +next +next +next +tombstones +---- +a#2,2:v2 +a#1,15:b +a#0,2:v1 +. +a-b#1 +. + +# Verify that we transform merge+del -> set. + +define +a.MERGE.5:5 +a.DEL.3: +a.MERGE.1:1 +---- + +iter +first +next +---- +a#5,18:5[base] +. + +iter allow-zero-seqnum=true +first +next +---- +a#0,18:5[base] +. + +iter elide-tombstones=true +first +next +---- +a#5,18:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#5,18:5[base] +a#1,2:1 +. + +iter snapshots=2 elide-tombstones=true +first +next +next +---- +a#5,18:5[base] +a#1,2:1 +. + +# Verify that we transform merge+rangedel -> set. This isn't strictly +# necessary, but provides consistency with the behavior for merge+del. + +define +a.RANGEDEL.3:c +b.MERGE.5:5 +b.SET.2:2 +b.MERGE.1:1 +---- + +iter +first +next +next +---- +a#3,15:c +b#5,1:5[base] +. + +iter allow-zero-seqnum=true +first +next +next +---- +a#3,15:c +b#0,1:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#3,15:c +b#5,1:5[base] +b#1,2:1 + +define +a.RANGEDEL.3:c +b.MERGE.5:5 +b.MERGE.2:2 +b.MERGE.1:1 +---- + +iter +first +next +next +---- +a#3,15:c +b#5,1:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#3,15:c +b#5,1:5[base] +b#1,2:1 + +# SET that meets a DEL is transformed into a SETWITHDEL. + +define +a.SET.2:b +a.DEL.1: +---- + +iter +first +next +---- +a#2,18:b +. + +iter snapshots=2 +first +next +next +---- +a#2,1:b +a#1,0: +. + +define +a.SET.3:c +a.DEL.2: +a.SET.1:b +---- + +iter +first +next +---- +a#3,18:c +. + +iter snapshots=2 +first +next +next +---- +a#3,18:c +a#1,1:b +. + +define +a.SET.3:c +a.SET.2:b +a.DEL.1: +---- + +iter +first +next +---- +a#3,18:c +. + +iter snapshots=3 +first +next +next +---- +a#3,1:c +a#2,18:b +. + +iter snapshots=2 +first +next +next +---- +a#3,1:c +a#1,0: +. + +define +a.DEL.3: +a.SET.2:b +a.DEL.1: +---- + +iter +first +next +---- +a#3,0: +. + +iter snapshots=3 +first +next +next +---- +a#3,0: +a#2,18:b +. + +iter snapshots=2 +first +next +next +---- +a#3,0: +a#1,0: +. + +# SETWITHDEL-eligible entries at or under a RANGEDEL at the same user key should +# be skipped. +define +a.SET.3:c +a.RANGEDEL.2:z +a.SET.2:b +a.DEL.1: +---- + +iter allow-zero-seqnum=true +first +next +next +---- +a#0,18:c +a#2,15:z +. + +iter allow-zero-seqnum=true snapshots=3 +first +next +next +next +---- +a#3,1:c +a#2,15:z +a#0,18:b +. + +iter allow-zero-seqnum=true snapshots=2 +first +next +next +next +---- +a#3,18:c +a#2,15:z +a#1,0: +. + +define +a.SET.4:c +a.RANGEDEL.3:z +a.SET.2:b +a.DEL.1: +---- + +iter +first +next +next +---- +a#4,18:c +a#3,15:z +. + +# Invalid keys are emitted under SETWITHDEL. + +define +a.SET.2:b +a.INVALID.1: +---- + +iter +first +next +---- +a#2,18:b +err=invalid internal key kind: INVALID + +define +a.SET.3:c +a.INVALID.2: +a.SET.1:b +---- + +iter +first +next +---- +a#3,18:c +err=invalid internal key kind: INVALID + +# SINGLEDEL that meets a SETWITHDEL is transformed into a DEL. + +define +a.SINGLEDEL.3: +a.SETWITHDEL.2:d +b.SET.1:c +---- + +iter +first +next +next +---- +a#3,0: +b#1,1:c +. + +iter snapshots=2 +first +next +next +---- +a#3,0: +b#1,1:c +. + +iter snapshots=3 +first +next +next +next +---- +a#3,7: +a#2,18:d +b#1,1:c +. + +define +a.SETWITHDEL.3:3 +a.SET.2:d +b.SET.1:c +---- + +iter print-missized-dels +first +next +next +---- +a#3,18:3 +b#1,1:c +. +missized-dels=0 + +iter snapshots=3 +first +next +next +next +---- +a#3,18:3 +a#2,1:d +b#1,1:c +. + +# Test a DELSIZED whose encoded value matches the size of a deleted key. The +# DELSIZED's value should be removed, reflecting that the tombstone already +# dropped the key that it was expected to drop. + +define +a.SET.9:foo +b.DELSIZED.8:varint(11) +b.SET.5:helloworld +c.SET.2:bar +---- + +iter print-missized-dels +first +next +next +---- +a#9,1:foo +b#8,23: +c#2,1:bar +missized-dels=0 + +# Test two DELSIZEDs meeting. The lower-sequenced number value should carry +# forward, at the higher sequence number. The first DELSIZED should be consider +# missized: It never found the key it was supposed to delete. + +define +a.SET.9:foo +b.DELSIZED.9:varint(20) +b.DELSIZED.8:varint(10) +c.SET.2:bar +---- + +iter print-missized-dels +first +next +next +---- +a#9,1:foo +b#9,23:varint(10) +c#2,1:bar +missized-dels=1 + +# Test a DELSIZED whose encoded value is larger than the size of the deleted +# key. The DELSIZED should be replaced by an ordinary DEL with the same sequence +# number. + +define +a.SET.2:foo +b.DELSIZED.8:varint(25) +b.SET.3:hello +c.SET.9:bar +---- + +iter print-missized-dels +first +next +next +---- +a#2,1:foo +b#8,0: +c#9,1:bar +missized-dels=1 + +# Test two DELSIZED at the same user key, but with correctly sized deleted keys. + +define +a.DELSIZED.9:varint(4) +a.SET.8:foo +a.DELSIZED.8:varint(6) +a.SET.5:hello +---- + +iter print-missized-dels +first +next +---- +a#9,23: +. +missized-dels=0 + +# Test the above scenario, except the second DELSIZED is missized. It should +# still count as missized. + +define +a.DELSIZED.9:varint(4) +a.SET.8:foo +a.DELSIZED.8:varint(1) +a.SET.5:hello +---- + +iter print-missized-dels +first +next +---- +a#9,0: +. +missized-dels=1 + +# Test the above scenario, except the second tombstone is a DEL. It should +# NOT count as missized. + +define +a.DELSIZED.9:varint(4) +a.SET.8:foo +a.DEL.8: +a.SET.5:hello +---- + +iter print-missized-dels +first +next +---- +a#9,0: +. +missized-dels=0 + +# Test various DELSIZEDs beneath live keys. SETS should be converted to +# SETWITHDELs when they meet a DELSIZED. + +define +a.SET.7:foo +a.DELSIZED.5:varint(5) +b.SET.4:bar +b.DELSIZED.2:varint(4) +b.SET.1:bax +c.SET.9:coconut +c.DEL.8:del +c.DELSIZED.5:varint(2) +d.SET.8:dragonfruit +---- + +iter print-missized-dels +first +next +next +next +next +---- +a#7,18:foo +b#4,18:bar +c#9,18:coconut +d#8,1:dragonfruit +. +missized-dels=0 + +# Test a DELSIZED meeting a MERGE. This counts as a missized DEL—The user can't +# know the value of the most recent MERGE since it's dependent on LSM state. + +define +a.DELSIZED.9:varint(4) +a.MERGE.8:fo +a.MERGE.7:o +---- + +iter print-missized-dels +first +next +---- +a#9,0: +. +missized-dels=1 + +# Test a DELSIZED that shadows a SINGLEDEL'd key. + +define +a.DELSIZED.4:varint(4) +b.SINGLEDEL.3: +b.SET.1:val +---- + +iter +first +next +tombstones +---- +a#4,23:varint(4) +. +. + +# Repeat the above but with elision of tombstones. + +iter elide-tombstones=t +first +tombstones +---- +. +. + +# Test DELSIZED shadowing SINGLEDEL. + +define +a.DELSIZED.4:varint(4) +a.SET.2:foo +b.SINGLEDEL.3: +b.SET.1:val +---- + +iter +first +next +tombstones +---- +a#4,23: +. +. + +# Repeat the above but with elision of tombstones. + +iter elide-tombstones=t +first +tombstones +---- +. +. + +# Test a very subtle sequence where a elision of tombstones is active, and a +# unskippable RANGEDEL sits between a DELSIZED and the key it was intended to +# delete. The unskippable RANGEDEL breaks the skipping of keys within the +# snapshot stripe, but it's ultimately okay because we preserve skip=true across +# the RANGEDEL return. + +define +a.DELSIZED.5:varint(4) +a.RANGEDEL.4:d +a.SET.3:foo +---- + +iter elide-tombstones=t +first +next +tombstones +---- +a#4,15:d +. +. + +# Try the same test as above, but with allowing sequence number zeroing as well. + +iter elide-tombstones=t allow-zero-seqnum=t +first +next +tombstones +---- +a#4,15:d +. +. + +# Perform a variant of the above test but with a DEL key. + +define +a.DEL.5: +a.RANGEDEL.4:d +a.SET.3:foo +---- + +iter elide-tombstones=t +first +next +tombstones +---- +a#4,15:d +. +. + +# Perform a variant of the above test but with a SINGLEDEL key. + +define +a.SINGLEDEL.5: +a.RANGEDEL.4:d +a.SET.3:foo +---- + +iter elide-tombstones=t +first +next +tombstones +---- +a#4,15:d +. +. + +# Perform a few variants of the above but with a range del with a seqnum equal to +# keys. NB: When seqnums are equal, the order of keys with various kinds is: +# +# DeleteSized < RangeKey{Delete,Unset,Set} < SetWithDelete < RangeDelete < SingleDelete < Set < Delete +# +# NB: Range keys are interleaved always at the maximal sequence number, so the +# compaction iterator should always observe them first. + +define +a.SINGLEDEL.6: +a.SETWITHDEL.5:foo +a.RANGEDEL.5:z +---- + +define-range-keys +a-z:{(#5,RANGEKEYDEL)} +---- + +# In the following case, the SINGLEDEL meets a SETWITHDEL, promoting the +# SINGLEDEL into a DEL. + +iter +first +next +next +next +tombstones +---- +a#72057594037927935,19: +a#6,0: +a#5,15:z +. +a-z#5 +. + +# In this case, SINGLEDEL is elided (despite its transformation into a DEL) due +# to elide-tombstones=t. + +iter elide-tombstones=t +first +next +next +tombstones +---- +a#72057594037927935,19: +a#5,15:z +. +. + +define +a.SINGLEDEL.6: +a.RANGEDEL.5:d +a.SET.5:foo +---- + +# NB: In this case, the RANGEDEL acts as an unintentional snapshot stripe +# change. This is a code artifact, and we will be able to remove this behavior +# when range deletes are interleaved at the maximal sequence number by an +# interleaving iterator (like range keys are). + +iter +first +next +next +next +tombstones +---- +a#6,7: +a#5,15:d +a#5,1:foo +. +a-d#5 +. + +iter elide-tombstones=t allow-zero-seqnum=t +first +next +tombstones +---- +a#5,15:d +. +. + +define +a.SINGLEDEL.6: +a.SETWITHDEL.5:foo +a.RANGEDEL.5:d +---- + +# When the SINGLEDEL and SETWITHDEL meet, the SINGLEDEL is promoted into a DEL. + +iter +first +next +tombstones +---- +a#6,0: +a#5,15:d +a-d#5 +. + +iter elide-tombstones=t +first +next +tombstones +---- +a#5,15:d +. +. + +define +a.DELSIZED.6:varint(3) +a.RANGEDEL.5:d +a.SET.5:foo +---- + +iter +first +next +tombstones +---- +a#6,23:varint(3) +a#5,15:d +a-d#5 +. + +iter elide-tombstones=t +first +next +tombstones +---- +a#5,15:d +. +. + +# Test a DELSIZED with a value that fails to decode. + +define +a.DELSIZED.5:notavarint +a.SET.4:foo +---- + +iter +first +---- +err=DELSIZED holds invalid value: 6e6f7461766172696e74 + +# Test a value-less DELSIZED. + +define +a.DELSIZED.5: +a.SET.4:foo +a.SET.3:bar +---- + +iter print-missized-dels +first +next +---- +a#5,0: +. +missized-dels=0 + +# Regression test for #3087. +# +# When a DELSIZED and a SINGLEDEL meet in a compaction, a DEL key should be +# emitted. + +define +a.DELSIZED.5: +a.SINGLEDEL.3: +a.SET.2:foo +a.SET.1:bar +---- + +iter +first +next +---- +a#5,0: +. + +# When a MERGE and a DEL[SIZED] meet in a compaction, a SETWITHDEL key (NOT a +# SET) should be emitted. Otherwise, a sequence such as SINGLEDDEL, MERGE, DEL, +# SET could result in the SET re-appearing. + +define +a.MERGE.5:foo +a.DEL.3: +---- + +iter +first +next +---- +a#5,18:foo[base] +. diff --git a/pebble/testdata/compaction_iter_set_with_del b/pebble/testdata/compaction_iter_set_with_del new file mode 100644 index 0000000..a0924af --- /dev/null +++ b/pebble/testdata/compaction_iter_set_with_del @@ -0,0 +1,1417 @@ +define +a.SET.1:b +---- + +iter +first +next +---- +a#1,1:b +. + +define +a.SET.2:c +a.SET.1:b +---- + +iter +first +next +---- +a#2,1:c +. + +iter snapshots=0 +first +next +---- +a#2,1:c +. + +iter snapshots=1 +first +next +---- +a#2,1:c +. + +iter snapshots=2 +first +next +next +---- +a#2,1:c +a#1,1:b +. + +define +a.DEL.2: +a.SET.1:b +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +iter elide-tombstones=true snapshots=2 +first +next +next +---- +a#2,0: +a#1,1:b +. + +iter elide-tombstones=true snapshots=1 +first +next +---- +a#2,0: +. + +define +a.DEL.2: +a.SET.1:b +b.SET.3:c +---- + +iter +first +next +next +---- +a#2,0: +b#3,1:c +. + +iter snapshots=1 +first +next +next +---- +a#2,0: +b#3,1:c +. + +iter snapshots=2 +first +next +next +next +---- +a#2,0: +a#1,1:b +b#3,1:c +. + +define +a.SET.1:a +b.SET.2:b +c.SET.3:c +---- + +iter +first +next +next +next +---- +a#1,1:a +b#2,1:b +c#3,1:c +. + +define +a.MERGE.3:d +a.MERGE.2:c +a.SET.1:b +b.MERGE.2:b +b.MERGE.1:a +---- + +iter +first +next +next +---- +a#3,1:bcd[base] +b#2,2:ab +. + +iter snapshots=3 +first +next +next +next +---- +a#3,2:d +a#2,1:bc[base] +b#2,2:ab +. + +define +a.SET.9:b +a.DEL.8: +a.SET.7:d +a.DEL.6: +a.SET.5:f +---- + +iter +first +next +---- +a#9,18:b +. + +iter snapshots=6 +first +next +next +---- +a#9,18:b +a#5,1:f +. + +iter snapshots=7 +first +next +next +---- +a#9,18:b +a#6,0: +. + +iter snapshots=8 +first +next +next +---- +a#9,18:b +a#7,18:d +. + +iter snapshots=9 +first +next +next +---- +a#9,1:b +a#8,0: +. + +iter snapshots=10 +first +next +---- +a#9,18:b +. + +iter snapshots=(5,6,7,8,9) +first +next +next +next +next +next +---- +a#9,1:b +a#8,0: +a#7,1:d +a#6,0: +a#5,1:f +. + +define +a.INVALID.2:b +a.SET.1:c +---- + +iter +first +---- +err=invalid internal key kind: INVALID + +define +a.SET.2:b +a.INVALID.1:c +---- + +iter +first +next +---- +a#2,18:b +err=invalid internal key kind: INVALID + +define +a.MERGE.2:b +a.INVALID.1:c +---- + +iter +first +next +---- +a#2,2:b +err=invalid internal key kind: INVALID + +define +a.INVALID.2:c +a.RANGEDEL.1:d +---- + +iter +first +tombstones +---- +err=invalid internal key kind: INVALID +. + +define +a.MERGE.2:b +a.MERGE.1:c +a.MERGE.0:d +---- + +iter snapshots=(1,2) +first +next +next +next +---- +a#2,2:b +a#1,2:c +a#0,2:d +. + +define +a.SET.2:b +a.RANGEDEL.1:c +b.RANGEDEL.4:d +b.SET.2:e +c.SET.3:f +---- + +iter +first +next +next +next +tombstones +---- +a#2,18:b +a#1,15:c +b#4,15:d +. +a-b#1 +b-c#4 +c-d#4 +. + +iter snapshots=2 +first +next +next +next +tombstones +---- +a#2,1:b +a#1,15:c +b#4,15:d +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +iter snapshots=3 +first +next +next +next +next +tombstones +---- +a#2,18:b +a#1,15:c +b#4,15:d +b#2,1:e +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +iter snapshots=4 +first +next +next +next +next +next +tombstones +---- +a#2,18:b +a#1,15:c +b#4,15:d +b#2,1:e +c#3,1:f +. +a-b#1 +b-c#4 +b-c#1 +c-d#4 +. + +define +a.RANGEDEL.3:e +b.SET.4:b +c.SET.3:c +d.SET.2:d +e.SET.1:e +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:e +b#4,1:b +c#3,1:c +e#1,1:e +. +a-e#3 +. + +define +a.RANGEDEL.3:e +b.MERGE.4:b +c.MERGE.3:c +d.MERGE.2:d +e.MERGE.1:e +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:e +b#4,2:b +c#3,2:c +e#1,2:e +. +a-e#3 +. + +define +a.RANGEDEL.3:c +b.MERGE.5:e +b.MERGE.4:d +b.MERGE.2:c +b.MERGE.1:b +d.MERGE.5:c +d.MERGE.4:b +d.RANGEDEL.3:f +d.MERGE.2:e +d.MERGE.1:d +---- + +iter +first +next +next +next +next +tombstones +---- +a#3,15:c +b#5,1:de[base] +d#5,2:bc +d#3,15:f +. +a-c#3 +d-f#3 +. + +define +a.RANGEDEL.3:d +b.RANGEDEL.2:e +c.RANGEDEL.1:f +---- + +iter +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +c-d#3 +d-e#2 +e-f#1 +. + +iter snapshots=2 +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +c-d#3 +c-d#1 +d-e#2 +d-e#1 +e-f#1 +. + +iter snapshots=3 +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +b-c#2 +c-d#3 +c-d#2 +d-e#2 +e-f#1 +. + +iter snapshots=(2,3) +first +next +next +next +tombstones +---- +a#3,15:d +b#2,15:e +c#1,15:f +. +a-b#3 +b-c#3 +b-c#2 +c-d#3 +c-d#2 +c-d#1 +d-e#2 +d-e#1 +e-f#1 +. + +define +a.RANGEDEL.10:k +f.SET.9:f +f.SET.8:f +---- + +iter snapshots=(9,10) +first +next +tombstones f +next +tombstones +---- +a#10,15:k +f#9,1:f +a-f#10 +. +f#8,1:f +f-k#10 +. + +define +f.RANGEDEL.10:k +f.SET.9:f +f.SET.8:f +---- + +iter snapshots=(9,10) +first +next +tombstones f +next +tombstones +---- +f#10,15:k +f#9,1:f +. +f#8,1:f +f-k#10 +. + +define +a.SET.1:a +b.RANGEDEL.2:d +c.RANGEDEL.3:e +d.SET.4:d +---- + +iter +first +next +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#3,15:e +d#4,1:d +b-c#2 +. +c-d#3 +d-e#3 +. + +iter snapshots=3 +first +next +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#3,15:e +d#4,1:d +b-c#2 +. +c-d#3 +c-d#2 +d-e#3 +. + +define +a.SET.1:a +b.RANGEDEL.2:d +c.SET.4:d +---- + +iter +first +next +next +tombstones c +tombstones +---- +a#1,1:a +b#2,15:d +c#4,1:d +b-c#2 +. +c-d#2 +. + +define +a.RANGEDEL.2:d +a.SET.2:a +b.SET.2:b +c.SET.2:c +---- + +iter +first +next +next +next +next +---- +a#2,15:d +a#2,1:a +b#2,1:b +c#2,1:c +. + +define +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#1,7: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#2,7: +. + +define +a.SINGLEDEL.3: +a.SINGLEDEL.2: +a.SET.1:a +---- + +iter +first +---- +. + +define +a.SET.3:a +b.SINGLEDEL.2: +b.DEL.1: +---- + +iter +first +next +next +---- +a#3,1:a +b#2,0: +. + +define +a.SINGLEDEL.2: +a.DEL.1: +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.MERGE.1: +---- + +iter +first +next +---- +a#2,0: +. + +iter elide-tombstones=true +first +---- +. + +define +a.SINGLEDEL.2: +a.SET.1:b +---- + +iter +first +---- +. + +# SET that meets a SINGLEDEL is transformed into a SETWITHDEL. + +define +a.SET.2:b +a.SINGLEDEL.1: +---- + +iter +first +next +---- +a#2,18:b +. + +define +a.MERGE.6:b +a.SINGLEDEL.5: +a.SET.4:a +---- + +iter +first +next +---- +a#6,18:b[base] +. + +# Non-deterministic use of SINGLEDEL where there are two older SETs that have +# not been deleted or single deleted. It is permitted to shadow both. +define +a.MERGE.6:b +a.SINGLEDEL.5: +a.SET.4:a +a.SET.3:a +---- + +iter +first +next +---- +a#6,18:b[base] +. + +define +a.SINGLEDEL.2: +a.SET.1:b +b.SET.3:c +---- + +iter +first +next +---- +b#3,1:c +. + +define +a.SINGLEDEL.3: +a.SET.2:b +a.SET.1:a +---- + +iter +first +next +---- +a#1,1:a +. + +define +a.SINGLEDEL.3: +a.MERGE.2:b +a.MERGE.1:a +---- + +iter +first +next +---- +a#3,0: +. + +define +a.SINGLEDEL.4: +a.SET.3:val +a.SINGLEDEL.2: +a.SET.1:val +---- + +iter +first +---- +. + +iter snapshots=2 +first +next +next +---- +a#2,7: +a#1,1:val +. + +define +a.SINGLEDEL.4: +a.SET.3:val +a.DEL.2: +a.SET.1:val +---- + +iter +first +next +---- +a#2,0: +. + +iter snapshots=2 +first +next +next +---- +a#2,0: +a#1,1:val +. + +iter snapshots=3 +first +next +---- +a#2,0: +. + +iter snapshots=(2,3) +first +next +next +---- +a#2,0: +a#1,1:val +. + +define +a.SINGLEDEL.4: +a.SET.3:c +a.MERGE.2:b +a.SET.1:a +---- + +iter +first +next +---- +a#2,1:ab[base] +. + +iter snapshots=2 +first +next +next +---- +a#2,2:b +a#1,1:a +. + +iter snapshots=3 +first +next +---- +a#2,1:ab[base] +. + +iter snapshots=(2,3,4) +first +next +next +next +next +---- +a#4,7: +a#3,1:c +a#2,2:b +a#1,1:a +. + +define +a.SINGLEDEL.3: +a.RANGEDEL.2:c +a.SET.1:val +---- + +iter +first +next +next +tombstones +---- +a#3,7: +a#2,15:c +. +a-c#2 +. + +define +a.RANGEDEL.3:d +a.DEL.2: +a.SET.1:a +d.DEL.2: +---- + +iter +first +next +next +tombstones +---- +a#3,15:d +d#2,0: +. +a-d#3 +. + +iter snapshots=3 +first +next +next +next +---- +a#3,15:d +a#2,0: +d#2,0: +. + +iter snapshots=2 +first +next +next +next +---- +a#3,15:d +a#1,1:a +d#2,0: +. + +iter snapshots=1 +first +next +next +---- +a#3,15:d +d#2,0: +. + +define +a.MERGE.2:a +b.RANGEDEL.1:c +---- + +iter +first +tombstones a +next +next +tombstones +---- +a#2,2:a +. +b#1,15:c +. +b-c#1 +. + +define +a.MERGE.2:v2 +a.RANGEDEL.1:b +a.MERGE.1:v1 +---- + +iter allow-zero-seqnum=true +first +next +next +next +tombstones +---- +a#2,2:v2 +a#1,15:b +a#0,2:v1 +. +a-b#1 +. + +# Verify that we transform merge+del -> set. + +define +a.MERGE.5:5 +a.DEL.3: +a.MERGE.1:1 +---- + +iter +first +next +---- +a#5,18:5[base] +. + +iter allow-zero-seqnum=true +first +next +---- +a#0,18:5[base] +. + +iter elide-tombstones=true +first +next +---- +a#5,18:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#5,18:5[base] +a#1,2:1 +. + +iter snapshots=2 elide-tombstones=true +first +next +next +---- +a#5,18:5[base] +a#1,2:1 +. + +# Verify that we transform merge+rangedel -> set. This isn't strictly +# necessary, but provides consistency with the behavior for merge+del. + +define +a.RANGEDEL.3:c +b.MERGE.5:5 +b.SET.2:2 +b.MERGE.1:1 +---- + +iter +first +next +next +---- +a#3,15:c +b#5,1:5[base] +. + +iter allow-zero-seqnum=true +first +next +next +---- +a#3,15:c +b#0,1:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#3,15:c +b#5,1:5[base] +b#1,2:1 + +define +a.RANGEDEL.3:c +b.MERGE.5:5 +b.MERGE.2:2 +b.MERGE.1:1 +---- + +iter +first +next +next +---- +a#3,15:c +b#5,1:5[base] +. + +iter snapshots=2 +first +next +next +---- +a#3,15:c +b#5,1:5[base] +b#1,2:1 + +# SET that meets a DEL is transformed into a SETWITHDEL. + +define +a.SET.2:b +a.DEL.1: +---- + +iter +first +next +---- +a#2,18:b +. + +iter snapshots=2 +first +next +next +---- +a#2,1:b +a#1,0: +. + +define +a.SET.3:c +a.DEL.2: +a.SET.1:b +---- + +iter +first +next +---- +a#3,18:c +. + +iter snapshots=2 +first +next +next +---- +a#3,18:c +a#1,1:b +. + +define +a.SET.3:c +a.SET.2:b +a.DEL.1: +---- + +iter +first +next +---- +a#3,18:c +. + +iter snapshots=3 +first +next +next +---- +a#3,1:c +a#2,18:b +. + +iter snapshots=2 +first +next +next +---- +a#3,1:c +a#1,0: +. + +define +a.DEL.3: +a.SET.2:b +a.DEL.1: +---- + +iter +first +next +---- +a#3,0: +. + +iter snapshots=3 +first +next +next +---- +a#3,0: +a#2,18:b +. + +iter snapshots=2 +first +next +next +---- +a#3,0: +a#1,0: +. + +# SETWITHDEL-eligible entries at or under a RANGEDEL at the same user key should +# be skipped. +define +a.SET.3:c +a.RANGEDEL.2:z +a.SET.2:b +a.DEL.1: +---- + +iter allow-zero-seqnum=true +first +next +next +---- +a#0,18:c +a#2,15:z +. + +iter allow-zero-seqnum=true snapshots=3 +first +next +next +next +---- +a#3,1:c +a#2,15:z +a#0,18:b +. + +iter allow-zero-seqnum=true snapshots=2 +first +next +next +next +---- +a#3,18:c +a#2,15:z +a#1,0: +. + +define +a.SET.4:c +a.RANGEDEL.3:z +a.SET.2:b +a.DEL.1: +---- + +iter +first +next +next +---- +a#4,18:c +a#3,15:z +. + +# Invalid keys are emitted under SETWITHDEL. + +define +a.SET.2:b +a.INVALID.1: +---- + +iter +first +next +---- +a#2,18:b +err=invalid internal key kind: INVALID + +define +a.SET.3:c +a.INVALID.2: +a.SET.1:b +---- + +iter +first +next +---- +a#3,18:c +err=invalid internal key kind: INVALID + +# SINGLEDEL that meets a SETWITHDEL is transformed into a DEL. + +define +a.SINGLEDEL.3: +a.SETWITHDEL.2:d +b.SET.1:c +---- + +iter +first +next +next +---- +a#3,0: +b#1,1:c +. + +iter snapshots=2 +first +next +next +---- +a#3,0: +b#1,1:c +. + +iter snapshots=3 +first +next +next +next +---- +a#3,7: +a#2,18:d +b#1,1:c +. + +define +a.SETWITHDEL.3:3 +a.SET.2:d +b.SET.1:c +---- + +iter +first +next +next +---- +a#3,18:3 +b#1,1:c +. + +iter snapshots=3 +first +next +next +next +---- +a#3,18:3 +a#2,1:d +b#1,1:c +. diff --git a/pebble/testdata/compaction_output_file_size b/pebble/testdata/compaction_output_file_size new file mode 100644 index 0000000..ee212c4 --- /dev/null +++ b/pebble/testdata/compaction_output_file_size @@ -0,0 +1,55 @@ +define +L3 + 010001:a.SET.1111-f.SET.1112 size=10 + 010002:g.SET.1111-l.SET.1112 size=10 +L4 + 001001:a.SET.111-f.SET.112 size=100 + 001002:g.SET.111-l.SET.112 size=100 +L5 + 000101:a.SET.11-f.SET.12 size=1000 + 000102:g.SET.11-l.SET.12 size=1000 +L6 + 000010:a.SET.1-f.SET.2 size=128000000 + 000011:g.SET.1-l.SET.2 size=128000000 range-deletions-bytes-estimate=28000000 +---- +3: + 010001:[a#1111,SET-f#1112,SET] + 010002:[g#1111,SET-l#1112,SET] +4: + 001001:[a#111,SET-f#112,SET] + 001002:[g#111,SET-l#112,SET] +5: + 000101:[a#11,SET-f#12,SET] + 000102:[g#11,SET-l#12,SET] +6: + 000010:[a#1,SET-f#2,SET] + 000011:[g#1,SET-l#2,SET] + +# Max output file size should be 32MiB because Lbase is L3. +pick-auto +---- +L6 -> L6 +L6: 000011 +maxOutputFileSize: 33554432 + +define +L5 + 000101:a.SET.11-f.SET.12 size=1000 + 000102:g.SET.11-l.SET.12 size=1000 +L6 + 000010:a.SET.1-f.SET.2 size=128000000 + 000011:g.SET.1-l.SET.2 size=128000000 range-deletions-bytes-estimate=28000000 +---- +5: + 000101:[a#11,SET-f#12,SET] + 000102:[g#11,SET-l#12,SET] +6: + 000010:[a#1,SET-f#2,SET] + 000011:[g#1,SET-l#2,SET] + +# Max output file size should be 8MiB because Lbase is L5. +pick-auto +---- +L6 -> L6 +L6: 000011 +maxOutputFileSize: 8388608 diff --git a/pebble/testdata/compaction_output_level b/pebble/testdata/compaction_output_level new file mode 100644 index 0000000..7d67475 --- /dev/null +++ b/pebble/testdata/compaction_output_level @@ -0,0 +1,199 @@ +compact start=0 base=1 +---- +output=1 +max-output-file-size=4194304 + +compact start=1 base=1 +---- +output=2 +max-output-file-size=8388608 + +compact start=2 base=1 +---- +output=3 +max-output-file-size=16777216 + +compact start=3 base=1 +---- +output=4 +max-output-file-size=33554432 + +compact start=4 base=1 +---- +output=5 +max-output-file-size=67108864 + +compact start=5 base=1 +---- +output=6 +max-output-file-size=134217728 + +compact start=6 base=1 +---- +output=6 +max-output-file-size=134217728 + + +compact start=0 base=2 +---- +output=2 +max-output-file-size=4194304 + +compact start=1 base=2 +---- +invalid compaction: start level 1 should not be empty (base level 2) + +compact start=2 base=2 +---- +output=3 +max-output-file-size=8388608 + +compact start=3 base=2 +---- +output=4 +max-output-file-size=16777216 + +compact start=4 base=2 +---- +output=5 +max-output-file-size=33554432 + +compact start=5 base=2 +---- +output=6 +max-output-file-size=67108864 + +compact start=6 base=2 +---- +output=6 +max-output-file-size=67108864 + + +compact start=0 base=3 +---- +output=3 +max-output-file-size=4194304 + +compact start=1 base=3 +---- +invalid compaction: start level 1 should not be empty (base level 3) + +compact start=2 base=3 +---- +invalid compaction: start level 2 should not be empty (base level 3) + +compact start=3 base=3 +---- +output=4 +max-output-file-size=8388608 + +compact start=4 base=3 +---- +output=5 +max-output-file-size=16777216 + +compact start=5 base=3 +---- +output=6 +max-output-file-size=33554432 + +compact start=6 base=3 +---- +output=6 +max-output-file-size=33554432 + + +compact start=0 base=4 +---- +output=4 +max-output-file-size=4194304 + +compact start=1 base=4 +---- +invalid compaction: start level 1 should not be empty (base level 4) + +compact start=2 base=4 +---- +invalid compaction: start level 2 should not be empty (base level 4) + +compact start=3 base=4 +---- +invalid compaction: start level 3 should not be empty (base level 4) + +compact start=4 base=4 +---- +output=5 +max-output-file-size=8388608 + +compact start=5 base=4 +---- +output=6 +max-output-file-size=16777216 + +compact start=6 base=4 +---- +output=6 +max-output-file-size=16777216 + + +compact start=0 base=5 +---- +output=5 +max-output-file-size=4194304 + +compact start=1 base=5 +---- +invalid compaction: start level 1 should not be empty (base level 5) + +compact start=2 base=5 +---- +invalid compaction: start level 2 should not be empty (base level 5) + +compact start=3 base=5 +---- +invalid compaction: start level 3 should not be empty (base level 5) + +compact start=4 base=5 +---- +invalid compaction: start level 4 should not be empty (base level 5) + +compact start=5 base=5 +---- +output=6 +max-output-file-size=8388608 + +compact start=6 base=5 +---- +output=6 +max-output-file-size=8388608 + + +compact start=0 base=6 +---- +output=6 +max-output-file-size=4194304 + +compact start=1 base=6 +---- +invalid compaction: start level 1 should not be empty (base level 6) + +compact start=2 base=6 +---- +invalid compaction: start level 2 should not be empty (base level 6) + +compact start=3 base=6 +---- +invalid compaction: start level 3 should not be empty (base level 6) + +compact start=4 base=6 +---- +invalid compaction: start level 4 should not be empty (base level 6) + +compact start=5 base=6 +---- +invalid compaction: start level 5 should not be empty (base level 6) + +compact start=6 base=6 +---- +output=6 +max-output-file-size=4194304 diff --git a/pebble/testdata/compaction_output_splitters b/pebble/testdata/compaction_output_splitters new file mode 100644 index 0000000..237b1ce --- /dev/null +++ b/pebble/testdata/compaction_output_splitters @@ -0,0 +1,112 @@ + +# arraySplitter tests + +reset +---- +ok + +init child0 mock +---- +ok + +init child1 mock +---- +ok + +init main array +---- +ok + +set-should-split child0 no-split +---- +ok + +set-should-split child1 no-split +---- +ok + +should-split-before foo.SET.2 +---- +no-split + +set-should-split child1 split-now +---- +ok + +should-split-before foo.SET.2 +---- +split-now + +set-should-split child1 no-split +---- +ok + +should-split-before foo.SET.2 +---- +no-split + +set-should-split child0 split-now +---- +ok + +should-split-before foo.SET.2 +---- +split-now + +# userKeyChangeSplitter tests + +reset +---- +ok + +init child0 mock +---- +ok + +init main userkey +---- +ok + +should-split-before foo.SET.6 +---- +no-split + +should-split-before foo.SET.5 +---- +no-split + +set-should-split child0 split-now +---- +ok + +should-split-before foo.SET.4 +---- +no-split + +should-split-before foo.SET.3 +---- +no-split + +should-split-before food.SET.6 +---- +split-now + +set-should-split child0 no-split +---- +ok + +should-split-before food.SET.5 +---- +no-split + +set-should-split child0 split-now +---- +ok + +should-split-before food.SET.4 +---- +no-split + +should-split-before food2.SET.4 +---- +split-now diff --git a/pebble/testdata/compaction_picker_L0 b/pebble/testdata/compaction_picker_L0 new file mode 100644 index 0000000..de6f62c --- /dev/null +++ b/pebble/testdata/compaction_picker_L0 @@ -0,0 +1,432 @@ +# 1 L0 file. +define +L0 + 000100:i.SET.101-j.SET.102 +---- +0.0: + 000100:[i#101,SET-j#102,SET] + +pick-auto l0_compaction_threshold=1 +---- +L0 -> L6 +L0: 000100 + +pick-auto l0_compaction_file_threshold=1 +---- +L0 -> L6 +L0: 000100 + +pick-auto l0_compaction_threshold=4 l0_compaction_file_threshold=2 +---- +nil + +# 1 L0 file, 1 Lbase file. + +define +L0 + 000100:i.SET.101-j.SET.102 +L6 + 000200:f.SET.51-l.SET.52 +---- +0.0: + 000100:[i#101,SET-j#102,SET] +6: + 000200:[f#51,SET-l#52,SET] + +pick-auto l0_compaction_threshold=1 +---- +L0 -> L6 +L0: 000100 +L6: 000200 + +pick-auto l0_compaction_threshold=2 +---- +L0 -> L6 +L0: 000100 +L6: 000200 + +pick-auto l0_compaction_threshold=3 +---- +nil + +# 2 L0 files, no overlaps. + +define +L0 + 000100:i.SET.101-j.SET.102 + 000110:k.SET.111-l.SET.112 +L6 + 000200:f.SET.51-l.SET.52 +---- +0.0: + 000100:[i#101,SET-j#102,SET] + 000110:[k#111,SET-l#112,SET] +6: + 000200:[f#51,SET-l#52,SET] + +pick-auto l0_compaction_threshold=1 +---- +L0 -> L6 +L0: 000100,000110 +L6: 000200 + +pick-auto l0_compaction_threshold=2 +---- +L0 -> L6 +L0: 000100,000110 +L6: 000200 + +pick-auto l0_compaction_threshold=3 l0_compaction_file_threshold=512 +---- +nil + +pick-auto l0_compaction_threshold=3 l0_compaction_file_threshold=3 +---- +nil + +pick-auto l0_compaction_threshold=3 l0_compaction_file_threshold=2 +---- +L0 -> L6 +L0: 000100,000110 +L6: 000200 + +# 2 L0 files, with ikey overlap. + +define +L0 + 000100:i.SET.101-p.SET.102 + 000110:j.SET.111-q.SET.112 +L6 + 000200:f.SET.51-s.SET.52 +---- +0.1: + 000110:[j#111,SET-q#112,SET] +0.0: + 000100:[i#101,SET-p#102,SET] +6: + 000200:[f#51,SET-s#52,SET] + +pick-auto l0_compaction_threshold=2 +---- +L0 -> L6 +L0: 000100,000110 +L6: 000200 + +define +L0 + 000100:i.SET.101-p.SET.102 + 000110:j.SET.111-q.SET.112 +L6 + 000200:f.SET.51-s.SET.52 +---- +0.1: + 000110:[j#111,SET-q#112,SET] +0.0: + 000100:[i#101,SET-p#102,SET] +6: + 000200:[f#51,SET-s#52,SET] + +pick-auto l0_compaction_threshold=2 +---- +L0 -> L6 +L0: 000100,000110 +L6: 000200 + +# 2 L0 files, with ukey overlap. + +define +L0 + 000100:i.SET.101-i.SET.102 + 000110:i.SET.111-i.SET.112 +L6 + 000200:f.SET.51-l.SET.52 +---- +0.1: + 000110:[i#111,SET-i#112,SET] +0.0: + 000100:[i#101,SET-i#102,SET] +6: + 000200:[f#51,SET-l#52,SET] + +pick-auto l0_compaction_threshold=2 +---- +L0 -> L6 +L0: 000100,000110 +L6: 000200 + +# 3 L0 files (1 overlap). + +define +L0 + 000100:i.SET.101-p.SET.102 + 000110:j.SET.111-q.SET.112 + 000120:r.SET.113-s.SET.114 +L6 + 000200:f.SET.51-s.SET.52 +---- +0.1: + 000110:[j#111,SET-q#112,SET] +0.0: + 000100:[i#101,SET-p#102,SET] + 000120:[r#113,SET-s#114,SET] +6: + 000200:[f#51,SET-s#52,SET] + +pick-auto l0_compaction_threshold=2 +---- +L0 -> L6 +L0: 000100,000110,000120 +L6: 000200 + +pick-auto l0_compaction_threshold=3 +---- +L0 -> L6 +L0: 000100,000110,000120 +L6: 000200 + +pick-auto l0_compaction_threshold=4 +---- +L0 -> L6 +L0: 000100,000110,000120 +L6: 000200 + +pick-auto l0_compaction_threshold=6 l0_compaction_file_threshold=512 +---- +nil + +# 3 L0 files (1 overlap, 1 intra-L0 compacting). Should avoid the compacting +# file. + +define +L0 + 000100:i.SET.101-p.SET.102 + 000110:j.SET.111-q.SET.112 + 000120:r.SET.113-s.SET.114 +L6 + 000200:f.SET.51-s.SET.52 +compactions + L0 000120 -> L0 +---- +0.1: + 000110:[j#111,SET-q#112,SET] +0.0: + 000100:[i#101,SET-p#102,SET] + 000120:[r#113,SET-s#114,SET] +6: + 000200:[f#51,SET-s#52,SET] +compactions + L0 000120 -> L0 + +pick-auto l0_compaction_threshold=2 +---- +L0 -> L6 +L0: 000100,000110 +L6: 000200 + +# 3 L0 files (1 overlap), Lbase compacting. +# Should choose an intra-L0 compaction. Note that intra-L0 compactions +# don't follow l0_compaction_threshold, but rather a minIntraL0Count constant +# in compaction_picker.go + +define +L0 + 000100:i.SET.101-p.SET.102 + 000110:j.SET.111-q.SET.112 + 000120:r.SET.113-s.SET.114 + 000130:i.SET.110-p.SET.110 + 000140:i.SET.120-p.SET.120 +L6 + 000200:f.SET.51-s.SET.52 +compactions + L6 000200 -> L6 +---- +0.3: + 000140:[i#120,SET-p#120,SET] +0.2: + 000130:[i#110,SET-p#110,SET] +0.1: + 000110:[j#111,SET-q#112,SET] +0.0: + 000100:[i#101,SET-p#102,SET] + 000120:[r#113,SET-s#114,SET] +6: + 000200:[f#51,SET-s#52,SET] +compactions + L6 000200 -> L6 + +pick-auto +---- +L0 -> L0 +L0: 000100,000110,000130,000140 + +max-output-file-size +---- +2097152 + +max-overlap-bytes +---- +20971520 + +# 1 L0 file. Should not choose any compaction, as an intra-L0 compaction +# with one input is unhelpful. + +define +L0 + 000100:i.SET.101-p.SET.102 +L6 + 000200:f.SET.51-s.SET.52 +compactions + L6 000200 -> L6 +---- +0.0: + 000100:[i#101,SET-p#102,SET] +6: + 000200:[f#51,SET-s#52,SET] +compactions + L6 000200 -> L6 + +pick-auto l0_compaction_threshold=1 +---- +nil + +# Test an in-progress L0->Lbase compaction with another L0 file that does not +# overlap any of the compacting files in L0 or Lbase, but does overlap the +# compaction's range. No new compaction should be picked because the +# in-progress compaction's output tables could overlap the non-compacting +# file. + +define +L0 + 000010:a.SET.11-b.SET.12 + 000013:k.SET.23-n.SET.24 + 000011:x.SET.13-z.SET.25 +L1 + 000101:a.SET.1-f.SET.2 + 000102:w.SET.3-z.SET.4 +compactions + L0 000010 000011 -> L1 000101 000102 +---- +0.0: + 000010:[a#11,SET-b#12,SET] + 000013:[k#23,SET-n#24,SET] + 000011:[x#13,SET-z#25,SET] +1: + 000101:[a#1,SET-f#2,SET] + 000102:[w#3,SET-z#4,SET] +compactions + L0 000010 000011 -> L1 000101 000102 + +pick-auto l0_compaction_threshold=2 +---- +nil + +define +L0 + 001621:b.MERGE.1261-b.MERGE.1261 + 001603:d.DEL.1248-d.DEL.1248 + 001609:e.DEL.1253-e.DEL.1253 +L6 + 001615:a.RANGEDEL.1254-c.RANGEDEL.72057594037927935 + 001619:c.SET.0-c.SET.0 +---- +0.0: + 001621:[b#1261,MERGE-b#1261,MERGE] + 001603:[d#1248,DEL-d#1248,DEL] + 001609:[e#1253,DEL-e#1253,DEL] +6: + 001615:[a#1254,RANGEDEL-c#inf,RANGEDEL] + 001619:[c#0,SET-c#0,SET] + +pick-auto +---- +L0 -> L6 +L0: 001621 +L6: 001615 + +define +L0 + 001445:b.RANGEDEL.528-e.RANGEDEL.72057594037927935 + 001448:g.RANGEDEL.529-h.RANGEDEL.72057594037927935 +L6 + 001428:a.MERGE.486-c.RANGEDEL.72057594037927935 + 001424:c.MERGE.479-d.RANGEDEL.72057594037927935 + 001442:f.MERGE.0-i.SET.0 +---- +0.0: + 001445:[b#528,RANGEDEL-e#inf,RANGEDEL] + 001448:[g#529,RANGEDEL-h#inf,RANGEDEL] +6: + 001428:[a#486,MERGE-c#inf,RANGEDEL] + 001424:[c#479,MERGE-d#inf,RANGEDEL] + 001442:[f#0,MERGE-i#0,SET] + +pick-auto +---- +L0 -> L6 +L0: 001445 +L6: 001424,001428 + +define +L0 + 000002:b.SET.12-b.SET.12 + 000003:c.SET.13-c.SET.13 +L6 + 000603:c.SET.03-c.SET.03 +---- +0.0: + 000002:[b#12,SET-b#12,SET] + 000003:[c#13,SET-c#13,SET] +6: + 000603:[c#3,SET-c#3,SET] + +pick-auto +---- +L0 -> L6 +L0: 000002 + +define +L0 + 000053:e.SET.24-e.SET.24 + 000055:x.SET.25-x.SET.25 + 000051:e.DEL.23-e.DEL.23 + 000049:t.SET.22-t.SET.22 + 000046:x.MERGE.21-x.MERGE.21 +L6 + 000045:f.SET.0-x.SET.0 +---- +0.1: + 000051:[e#23,DEL-e#23,DEL] + 000046:[x#21,MERGE-x#21,MERGE] +0.0: + 000053:[e#24,SET-e#24,SET] + 000049:[t#22,SET-t#22,SET] + 000055:[x#25,SET-x#25,SET] +6: + 000045:[f#0,SET-x#0,SET] + +pick-auto +---- +L0 -> L6 +L0: 000051,000053 + +# At low priority, find and compact marked-for-compaction files. + +define +L0 + 000049:t.SET.22-t.SET.22 +L6 + 000045:f.SET.0-x.SET.0 +---- +0.0: + 000049:[t#22,SET-t#22,SET] +6: + 000045:[f#0,SET-x#0,SET] + +mark-for-compaction file=000049 +---- +marked L0.000049 + +pick-auto l0_compaction_threshold=1000 +---- +L0 -> L0 +L0: 000049 diff --git a/pebble/testdata/compaction_picker_concurrency b/pebble/testdata/compaction_picker_concurrency new file mode 100644 index 0000000..d446d93 --- /dev/null +++ b/pebble/testdata/compaction_picker_concurrency @@ -0,0 +1,172 @@ +# Test a file L1.000203 that would be a candidate for a move compaction into +# L2, except that it's bordered by two files participating in the same +# compaction. This is possible if 000203 created by a L0->L1 compaction that +# completed after the compaction of 000201 and 000202 began. +# +# The in-progress compaction of 000201 and 000202 will write an output table +# to L2 that would conflict with 000203 if 000203 was moved into L2. +# +# NB: The L0 files are used to increase the permitted compaction concurrency. +define +L0 + 000301:a.SET.31-a.SET.31 + 000302:a.SET.32-a.SET.32 + 000303:a.SET.33-a.SET.33 + 000304:a.SET.34-a.SET.34 + 000305:a.SET.35-a.SET.35 +L1 + 000201:a.SET.21-b.SET.22 + 000203:k.SET.25-n.SET.26 size=512000000 + 000202:x.SET.23-z.SET.24 +L2 + 000101:a.SET.11-f.SET.12 +L3 + 000010:a.SET.1-z.SET.2 +compactions + L1 000201 000202 -> L2 000101 +---- +0.4: + 000305:[a#35,SET-a#35,SET] +0.3: + 000304:[a#34,SET-a#34,SET] +0.2: + 000303:[a#33,SET-a#33,SET] +0.1: + 000302:[a#32,SET-a#32,SET] +0.0: + 000301:[a#31,SET-a#31,SET] +1: + 000201:[a#21,SET-b#22,SET] + 000203:[k#25,SET-n#26,SET] + 000202:[x#23,SET-z#24,SET] +2: + 000101:[a#11,SET-f#12,SET] +3: + 000010:[a#1,SET-z#2,SET] +compactions + L1 000201 000202 -> L2 000101 + +pick-auto l0_compaction_threshold=10 +---- +nil + +# Test that lowering L0CompactionConcurrency opens up more compaction slots. + +define +L0 + 000301:a.SET.31-a.SET.31 + 000302:a.SET.32-a.SET.32 + 000303:a.SET.33-a.SET.33 + 000304:a.SET.34-a.SET.34 + 000305:a.SET.35-a.SET.35 +L1 + 000201:a.SET.21-b.SET.22 + 000203:k.SET.25-n.SET.26 + 000202:x.SET.23-z.SET.24 +L2 + 000101:a.SET.11-f.SET.12 +L3 + 000010:a.SET.1-z.SET.2 +compactions + L1 000202 -> L2 000101 +---- +0.4: + 000305:[a#35,SET-a#35,SET] +0.3: + 000304:[a#34,SET-a#34,SET] +0.2: + 000303:[a#33,SET-a#33,SET] +0.1: + 000302:[a#32,SET-a#32,SET] +0.0: + 000301:[a#31,SET-a#31,SET] +1: + 000201:[a#21,SET-b#22,SET] + 000203:[k#25,SET-n#26,SET] + 000202:[x#23,SET-z#24,SET] +2: + 000101:[a#11,SET-f#12,SET] +3: + 000010:[a#1,SET-z#2,SET] +compactions + L1 000202 -> L2 000101 + +pick-auto l0_compaction_concurrency=10 +---- +nil + +pick-auto l0_compaction_concurrency=5 +---- +L0 -> L1 +L0: 000301,000302,000303,000304,000305 +L1: 000201 +grandparents: 000101 + +pick-auto l0_compaction_concurrency=1 +---- +L0 -> L1 +L0: 000301,000302,000303,000304,000305 +L1: 000201 +grandparents: 000101 + +# Test that lowering CompactionDebtConcurrency opens up more concurrent +# compaction slots. + +# Test that lowering L0CompactionConcurrency opens up more compaction slots. + +define +L0 + 000301:a.SET.31-a.SET.31 size=64000 + 000302:a.SET.32-a.SET.32 size=64000 + 000303:a.SET.33-a.SET.33 size=64000 + 000304:a.SET.34-a.SET.34 size=64000 + 000305:a.SET.35-a.SET.35 size=64000 +L1 + 000201:a.SET.21-b.SET.22 size=640000 + 000203:k.SET.25-n.SET.26 size=640000 + 000202:x.SET.23-z.SET.24 size=640000 +L2 + 000101:a.SET.11-f.SET.12 size=6400000 +L3 + 000010:a.SET.1-z.SET.2 +compactions + L1 000202 -> L2 000101 +---- +0.4: + 000305:[a#35,SET-a#35,SET] +0.3: + 000304:[a#34,SET-a#34,SET] +0.2: + 000303:[a#33,SET-a#33,SET] +0.1: + 000302:[a#32,SET-a#32,SET] +0.0: + 000301:[a#31,SET-a#31,SET] +1: + 000201:[a#21,SET-b#22,SET] + 000203:[k#25,SET-n#26,SET] + 000202:[x#23,SET-z#24,SET] +2: + 000101:[a#11,SET-f#12,SET] +3: + 000010:[a#1,SET-z#2,SET] +compactions + L1 000202 -> L2 000101 + +pick-auto l0_compaction_concurrency=10 compaction_debt_concurrency=5120000 +---- +nil + +pick-auto l0_compaction_concurrency=10 compaction_debt_concurrency=512000 +---- +L0 -> L1 +L0: 000301,000302,000303,000304,000305 +L1: 000201 +grandparents: 000101 + +pick-auto l0_compaction_concurrency=5 compaction_debt_concurrency=5120000 +---- +L0 -> L1 +L0: 000301,000302,000303,000304,000305 +L1: 000201 +grandparents: 000101 diff --git a/pebble/testdata/compaction_picker_estimated_debt b/pebble/testdata/compaction_picker_estimated_debt new file mode 100644 index 0000000..70bb7a9 --- /dev/null +++ b/pebble/testdata/compaction_picker_estimated_debt @@ -0,0 +1,108 @@ +init 1 +---- +0 + +init 1 +6: 1 +---- +0 + +init 1 +6: 2 +---- +0 + +init 1 +3: 1 +4: 1 +5: 1 +6: 1 +---- +0 + +init 1 +1: 1 +2: 1 +3: 1 +4: 1 +5: 1 +6: 1 +---- +0 + +init 1 +1: 1 +2: 10 +3: 100 +4: 1000 +5: 10000 +6: 100000 +---- +0 + +init 1 +5: 10 +6: 10 +---- +18 + +init 1 +0: 10 +5: 10 +6: 10 +---- +39 + +init 1 +0: 10 +6: 100 +---- +0 + +init 1 +0: 10 +4: 1 +5: 10 +6: 100 +---- +90 + +init 1 +0: 10 +6: 1000 +---- +0 + +init 1 +5: 101 +6: 1000 +---- +21 + +init 1000 +6: 10000 +---- +0 + +init 1000 +5: 1 +6: 10000 +---- +0 + +init 1000 +5: 2000 +6: 10000 +---- +0 + +# Regression test case which was previously computing an overly large +# estimated debt due to faulty handling of L0. + +init 64 +0: 236 +4: 113 +5: 480 +6: 2457 +---- +2414 diff --git a/pebble/testdata/compaction_picker_level_max_bytes b/pebble/testdata/compaction_picker_level_max_bytes new file mode 100644 index 0000000..b0cc6a0 --- /dev/null +++ b/pebble/testdata/compaction_picker_level_max_bytes @@ -0,0 +1,148 @@ +init 1 +---- +6: 9223372036854775807 + +init 1 +6: 1 +---- +6: 1 + +init 1 +6: 2 +---- +5: 1 +6: 2 + +init 1 +6: 2 +---- +5: 1 +6: 2 + +init 1 +3: 1 +4: 1 +5: 1 +6: 1 +---- +3: 1 +4: 2 +5: 3 +6: 4 + +init 1 +1: 1 +2: 1 +3: 1 +4: 1 +5: 1 +6: 1 +---- +1: 1 +2: 1 +3: 2 +4: 3 +5: 4 +6: 6 + +init 1 +1: 1 +2: 10 +3: 100 +4: 1000 +5: 10000 +6: 100000 +---- +1: 1 +2: 10 +3: 100 +4: 1000 +5: 10000 +6: 100000 + +init 1 +6: 10 +---- +5: 1 +6: 9 + +init 1 +6: 100 +---- +4: 1 +5: 9 +6: 90 + +init 1 +6: 1000 +---- +3: 1 +4: 10 +5: 93 +6: 900 + +init 1 +6: 10000 +---- +2: 1 +3: 10 +4: 95 +5: 924 +6: 9000 + +init 1 +6: 100000 +---- +1: 1 +2: 10 +3: 96 +4: 939 +5: 9192 +6: 90000 + +# Smoothing multiplier is +# `(size(Lbottom)/size(Lbase))^(1/(Lbottom-Lbase)) = (1000000/1)^(1/(6-1)) = 1000000^(1/5)` +# +# size(L1) = size(Lbase) = 1 +# size(L2) = size(L1) * 1000000^(1/5) ~= 16 +# size(L3) = size(L2) * 1000000^(1/5) ~= 251 +# size(L4) = size(L3) * 1000000^(1/5) ~= 3981 +# size(L5) = size(L4) * 1000000^(1/5) ~= 63096 +# size(L6) = size(L5) * 1000000^(1/5) ~= 1000000 + +init 1 +6: 1000000 +---- +1: 1 +2: 16 +3: 241 +4: 3737 +5: 57995 +6: 900000 + +# Smoothing multiplier is +# `(size(Lbottom)/size(Lbase))^(Lbottom-Lbase) = (64000000/64)^(1/(6-1)) = 1000000^(1/5)` +# +# size(L1) = size(Lbase) = 64 +# size(L2) = size(L1) * 1000000^(1/5) ~= 1014 +# size(L3) = size(L2) * 1000000^(1/5) ~= 16076 +# size(L4) = size(L3) * 1000000^(1/5) ~= 254789 +# size(L5) = size(L4) * 1000000^(1/5) ~= 4038127 +# size(L6) = size(L5) * 1000000^(1/5) ~= 64000000 + +init 64 +6: 64000000 +---- +1: 64 +2: 993 +3: 15413 +4: 239180 +5: 3711710 +6: 57600000 + +init 1 +0: 4 +6: 10 +---- +5: 1 +6: 13 diff --git a/pebble/testdata/compaction_picker_pick_file b/pebble/testdata/compaction_picker_pick_file new file mode 100644 index 0000000..6ecc838 --- /dev/null +++ b/pebble/testdata/compaction_picker_pick_file @@ -0,0 +1,106 @@ +# Simple test with a single file per level. + +define +L1 + b.SET.11:foo + c.SET.11:foo +L2 + c.SET.0:foo + d.SET.0:foo +---- +1: + 000004:[b#11,SET-c#11,SET] +2: + 000005:[c#0,SET-d#0,SET] + +file-sizes +---- +L1: + 000004:[b#11,1-c#11,1]: 669 bytes (669B) +L2: + 000005:[c#0,1-d#0,1]: 668 bytes (668B) + +pick-file L1 +---- +000004:[b#11,1-c#11,1] + +pick-file L2 +---- +000005:[c#0,1-d#0,1] + +# Test a scenario where we should pick a file with a tiny file size over one +# with a larger file size, because the tiny sized one overlaps zero data in the +# output level. + +define +L5 + b.SET.11: + c.SET.11: +L5 + e.SET.11: +L6 + a.SET.0:foo + d.SET.0:foo +---- +5: + 000004:[b#11,SET-c#11,SET] + 000005:[e#11,SET-e#11,SET] +6: + 000006:[a#0,SET-d#0,SET] + +pick-file L5 +---- +000005:[e#11,1-e#11,1] + +# Test the same scenario as above, but the larger file that overlaps the next +# level only overlaps on its start boundary key ("c"). + +define +L5 + c.SET.11: + d.SET.11: +L5 + e.SET.11: +L6 + a.SET.0:foo + c.SET.0:foo +---- +5: + 000004:[c#11,SET-d#11,SET] + 000005:[e#11,SET-e#11,SET] +6: + 000006:[a#0,SET-c#0,SET] + +pick-file L5 +---- +000005:[e#11,1-e#11,1] + + +# Test a scenario where the file containing e.SET.11 overlaps an L6 file +# containing e.SET.0. These files should be considered overlapping, despite the +# fact that they don't overlap within the internal key keyspace. The overlap +# should then cause the larger file (with a lower overlapping ratio) to be +# picked. + +define +L5 + c.SET.11: + d.SET.11: +L5 + e.SET.11: +L6 + a.SET.0:foo + c.SET.0:foo +L6 + e.SET.0:foo +---- +5: + 000004:[c#11,SET-d#11,SET] + 000005:[e#11,SET-e#11,SET] +6: + 000006:[a#0,SET-c#0,SET] + 000007:[e#0,SET-e#0,SET] + +pick-file L5 +---- +000004:[c#11,1-d#11,1] diff --git a/pebble/testdata/compaction_picker_read_triggered b/pebble/testdata/compaction_picker_read_triggered new file mode 100644 index 0000000..9f92228 --- /dev/null +++ b/pebble/testdata/compaction_picker_read_triggered @@ -0,0 +1,145 @@ +# Verify that pickAuto picks read triggered compactions that are scheduled and LSM is in good shape. This ensures +# that read triggered compactions are lower priority than score based ones. This also verifies that only the files +# within the range set in a readCompaction are chosen for compaction. +define +L5 + 000101:a.SET.11-f.SET.12 size=10 + 000102:g.SET.11-l.SET.12 size=10 +L6 + 000010:a.SET.1-f.SET.2 size=100 + 000011:g.SET.1-l.SET.2 size=100 +---- +5: + 000101:[a#11,SET-f#12,SET] + 000102:[g#11,SET-l#12,SET] +6: + 000010:[a#1,SET-f#2,SET] + 000011:[g#1,SET-l#2,SET] + +pick-auto +---- +nil + +add-read-compaction +5: a-f 000101 +---- + +show-read-compactions +---- +(level: 5, start: a, end: f) + +pick-auto +---- +L5 -> L6 +L5: 000101 +L6: 000010 + +show-read-compactions +---- +(none) + + +# Verify that pickAuto does not pick read triggered compactions when the LSM is in bad shape and instead schedules a +# score-based one. +define +L5 + 000101:a.SET.11-f.SET.12 size=1000000000 +L6 + 000010:a.SET.1-f.SET.2 size=1000000000 +---- +5: + 000101:[a#11,SET-f#12,SET] +6: + 000010:[a#1,SET-f#2,SET] + +add-read-compaction +5: a-f 000101 +---- + +show-read-compactions +---- +(level: 5, start: a, end: f) + +pick-auto +---- +L5 -> L6 +L5: 000101 +L6: 000010 + +show-read-compactions +---- +(level: 5, start: a, end: f) + +# Verify that read compactions out of a level +# are disabled if the size ratio of level sizes +# is higher than what we want. +define +L5 + 000101:a.SET.11-f.SET.12 size=10 + 000102:g.SET.11-l.SET.12 size=10 +L6 + 000010:a.SET.1-f.SET.2 size=100000000 + 000012:g.SET.1-l.SET.2 size=100 +---- +5: + 000101:[a#11,SET-f#12,SET] + 000102:[g#11,SET-l#12,SET] +6: + 000010:[a#1,SET-f#2,SET] + 000012:[g#1,SET-l#2,SET] + +pick-auto +---- +nil + +add-read-compaction +5: a-f 000101 +---- + +show-read-compactions +---- +(level: 5, start: a, end: f) + +pick-auto +---- +nil + +show-read-compactions +---- +(none) + +# Verify that wide read compactions are disabled. +define +L5 + 000101:a.SET.11-f.SET.12 size=5000000 + 000102:g.SET.11-l.SET.12 size=10 +L6 + 000010:a.SET.1-f.SET.2 size=100000000 + 000012:g.SET.1-l.SET.2 size=100 +---- +5: + 000101:[a#11,SET-f#12,SET] + 000102:[g#11,SET-l#12,SET] +6: + 000010:[a#1,SET-f#2,SET] + 000012:[g#1,SET-l#2,SET] + +pick-auto +---- +nil + +add-read-compaction +5: a-f 000101 +---- + +show-read-compactions +---- +(level: 5, start: a, end: f) + +pick-auto +---- +nil + +show-read-compactions +---- +(none) diff --git a/pebble/testdata/compaction_picker_scores b/pebble/testdata/compaction_picker_scores new file mode 100644 index 0000000..314ee6a --- /dev/null +++ b/pebble/testdata/compaction_picker_scores @@ -0,0 +1,264 @@ +# Ensure that a range deletion in a higher level results in a compensated level +# size and a higher level score as a result. + +define lbase-max-bytes=65536 enable-table-stats=false +L5 + a.RANGEDEL.2:f +L6 + a.SET.1: + b.SET.1: + c.SET.1: + d.SET.1: + e.SET.1: +---- +5: + 000004:[a#2,RANGEDEL-f#inf,RANGEDEL] +6: + 000005:[a#1,SET-e#1,SET] + +scores +---- +L Size Score +L0 0B 0.0 +L1 0B 0.0 +L2 0B 0.0 +L3 0B 0.0 +L4 0B 0.0 +L5 729B 0.0 +L6 321KB - + +enable-table-stats +---- + +wait-pending-table-stats +000004 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 328519 + +scores +---- +L Size Score +L0 0B 0.0 +L1 0B 0.0 +L2 0B 0.0 +L3 0B 0.0 +L4 0B 0.0 +L5 729B 4.5 +L6 321KB - + +# Ensure that point deletions in a higher level result in a compensated level +# size and higher level scores as a result. + +define lbase-max-bytes=65536 enable-table-stats=false +L5 + a.DEL.2: + b.DEL.2: + c.DEL.2: + d.DEL.2: + e.DEL.2: +L6 + a.SET.1: + b.SET.1: + c.SET.1: + d.SET.1: + e.SET.1: +---- +5: + 000004:[a#2,DEL-e#2,DEL] +6: + 000005:[a#1,SET-e#1,SET] + +scores +---- +L Size Score +L0 0B 0.0 +L1 0B 0.0 +L2 0B 0.0 +L3 0B 0.0 +L4 0B 0.0 +L5 715B 0.0 +L6 321KB - + +enable-table-stats +---- + +wait-pending-table-stats +000004 +---- +num-entries: 5 +num-deletions: 5 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 164581 +range-deletions-bytes-estimate: 0 + +scores +---- +L Size Score +L0 0B 0.0 +L1 0B 0.0 +L2 0B 0.0 +L3 0B 0.0 +L4 0B 0.0 +L5 715B 2.3 +L6 321KB - + +# Run a similar test as above, but this time the table containing the DELs is +# ingested after the database is initialized. When the ingested sstable's stats +# are loaded and automatic compactions are re-enabled, it should trigger an +# automatic compaction of the ingested sstable on account of the high +# point-deletions-bytes-estimate value. +# +# This a regression test for an issue where the table stats collector wouldn't +# attempt to schedule a compaction if a file only had compensation due to point +# deletions and not range deletions. + +define lbase-max-bytes=65536 enable-table-stats=true auto-compactions=off +L6 + a.SET.1: + b.SET.1: + c.SET.1: + d.SET.1: + e.SET.1: +---- +6: + 000004:[a#1,SET-e#1,SET] + +ingest ext1 +del a: +del b: +del c: +del d: +del e: +---- +5: + 000005:[a:#10,DEL-e:#10,DEL] +6: + 000004:[a#1,SET-e#1,SET] + +wait-pending-table-stats +000005 +---- +num-entries: 5 +num-deletions: 5 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 164616 +range-deletions-bytes-estimate: 0 + +maybe-compact +---- +1 compactions in progress: +5: 000005:a:#10,0-e:#10,0 +6: 000004:a#1,1-e#1,1 + +scores +---- +L Size Score +L0 0B 0.0 +L1 0B 0.0 +L2 0B 0.0 +L3 0B 0.0 +L4 0B 0.0 +L5 0B 0.0 +L6 321KB - + +lsm +---- +6: + 000006:[a#0,SET-e#0,SET] + +# Test the adjustment of level sizes to accommodate in-progress compactions. A +# compaction may be "inProgress" if it's already been applied, but is still +# deleting obsolete files. These compactions' effects have already been applied +# to the LSM, so size adjustment should ignore them and not doubly adjust sizes. + +define lbase-max-bytes=65536 enable-table-stats=false auto-compactions=on pause-cleaning +L5 + aa.SET.2: + bb.SET.2: + cc.SET.2: + dd.SET.2: +L5 + e.SET.2: +L6 + a.SET.1: + b.SET.1: + c.SET.1: + d.SET.1: +L6 + e.SET.1: +---- +5: + 000004:[aa#2,SET-dd#2,SET] + 000005:[e#2,SET-e#2,SET] +6: + 000006:[a#1,SET-d#1,SET] + 000007:[e#1,SET-e#1,SET] + +scores +---- +L Size Score +L0 0B 0.0 +L1 0B 0.0 +L2 0B 0.0 +L3 0B 0.0 +L4 0B 0.0 +L5 641KB 6.3 +L6 385KB - + +lsm verbose +---- +5: + 000004:[aa#2,SET-dd#2,SET] seqnums:[2-2] points:[aa#2,SET-dd#2,SET] + 000005:[e#2,SET-e#2,SET] seqnums:[2-2] points:[e#2,SET-e#2,SET] +6: + 000006:[a#1,SET-d#1,SET] seqnums:[1-1] points:[a#1,SET-d#1,SET] + 000007:[e#1,SET-e#1,SET] seqnums:[1-1] points:[e#1,SET-e#1,SET] + +# Attempting to schedule a compaction should begin a L5->L6 compaction. + +maybe-compact +---- +1 compactions in progress: +5: 000004:aa#2,1-dd#2,1 +6: 000006:a#1,1-d#1,1 + +# The scores and sizes should be stable between when the version edit has been +# applied but the compaction has not completed, and when the compaction is +# finally complete. + +scores wait-for-compaction=version-edit +---- +L Size Score +L0 0B 0.0 +L1 0B 0.0 +L2 0B 0.0 +L3 0B 0.0 +L4 0B 0.0 +L5 129KB 0.5 +L6 898KB - + +lsm +---- +5: + 000005:[e#2,SET-e#2,SET] +6: + 000008:[a#0,SET-dd#0,SET] + 000007:[e#1,SET-e#1,SET] + +resume-cleaning +---- + +scores wait-for-compaction=completion +---- +L Size Score +L0 0B 0.0 +L1 0B 0.0 +L2 0B 0.0 +L3 0B 0.0 +L4 0B 0.0 +L5 129KB 0.5 +L6 898KB - diff --git a/pebble/testdata/compaction_picker_target_level b/pebble/testdata/compaction_picker_target_level new file mode 100644 index 0000000..922e887 --- /dev/null +++ b/pebble/testdata/compaction_picker_target_level @@ -0,0 +1,1312 @@ +init 1 +---- + +init_cp +---- +base: 6 + +queue +---- + +init 1 +6: 1 +---- +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + +init_cp +---- +base: 6 + +queue +---- + +init 1 +6: 1000000 +---- +L6: + 600001:[0001#1,1-1000000#1,1]: 1000000 bytes (977KB) + +init_cp +---- +base: 1 + +queue +---- + +init 1 +5: 1 +6: 10 +---- +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +queue +---- +L5->L6: 1.0 + 500001:[0001#1,1-0001#1,1] marked as compacting + 600001:[0001#1,1-0001#1,1] marked as compacting + +init 1 +5: 2 +6: 10 +---- +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +queue +---- +L5->L6: 2.2 + 500001:[0001#1,1-0001#1,1] marked as compacting + 600001:[0001#1,1-0001#1,1] marked as compacting + +# Smoothing multiplier is +# `(size(Lbottom)/size(Lbase))^(Lbottom-Lbase) = (30/1)^(1/(6-4)) = 30^(1/2)` +# +# size(L4) = size(Lbase) = 1 +# size(L5) = size(L4) * 30^(1/2) ~= 5 +# size(L6) = size(L5) * 30^(1/2) = 30 + +init 1 +5: 2 +6: 30 +---- +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) + 600011:[0011#11,1-0011#11,1]: 1 bytes (1B) + 600012:[0012#12,1-0012#12,1]: 1 bytes (1B) + 600013:[0013#13,1-0013#13,1]: 1 bytes (1B) + 600014:[0014#14,1-0014#14,1]: 1 bytes (1B) + 600015:[0015#15,1-0015#15,1]: 1 bytes (1B) + 600016:[0016#16,1-0016#16,1]: 1 bytes (1B) + 600017:[0017#17,1-0017#17,1]: 1 bytes (1B) + 600018:[0018#18,1-0018#18,1]: 1 bytes (1B) + 600019:[0019#19,1-0019#19,1]: 1 bytes (1B) + 600020:[0020#20,1-0020#20,1]: 1 bytes (1B) + 600021:[0021#21,1-0021#21,1]: 1 bytes (1B) + 600022:[0022#22,1-0022#22,1]: 1 bytes (1B) + 600023:[0023#23,1-0023#23,1]: 1 bytes (1B) + 600024:[0024#24,1-0024#24,1]: 1 bytes (1B) + 600025:[0025#25,1-0025#25,1]: 1 bytes (1B) + 600026:[0026#26,1-0026#26,1]: 1 bytes (1B) + 600027:[0027#27,1-0027#27,1]: 1 bytes (1B) + 600028:[0028#28,1-0028#28,1]: 1 bytes (1B) + 600029:[0029#29,1-0029#29,1]: 1 bytes (1B) + 600030:[0030#30,1-0030#30,1]: 1 bytes (1B) + +init_cp +---- +base: 4 + +queue +---- + +init 1 +4: 2 +5: 2 +6: 100 +---- +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 400002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0100#1,1]: 100 bytes (100B) + +init_cp +---- +base: 4 + +queue +---- +L4->L5: 10.0 + 400001:[0001#1,1-0001#1,1] marked as compacting + 500001:[0001#1,1-0001#1,1] marked as compacting + +init 1 +4: 1 +5: 2 +6: 100 +---- +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0100#1,1]: 100 bytes (100B) + +init_cp +---- +base: 4 + +queue +---- +L4->L5: 5.0 + 400001:[0001#1,1-0001#1,1] marked as compacting + 500001:[0001#1,1-0001#1,1] marked as compacting + +init 1 +4: 1 +5: 11 +6: 100 +---- +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 500007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 500008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 500009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 500010:[0010#10,1-0010#10,1]: 1 bytes (1B) + 500011:[0011#11,1-0011#11,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0100#1,1]: 100 bytes (100B) + +init_cp +---- +base: 4 + +queue +---- +L5->L6: 1.1 + 500001:[0001#1,1-0001#1,1] marked as compacting + 500002:[0002#2,1-0002#2,1] marked as compacting + 500003:[0003#3,1-0003#3,1] marked as compacting + 500004:[0004#4,1-0004#4,1] marked as compacting + 500005:[0005#5,1-0005#5,1] marked as compacting + 500006:[0006#6,1-0006#6,1] marked as compacting + 500007:[0007#7,1-0007#7,1] marked as compacting + 500008:[0008#8,1-0008#8,1] marked as compacting + 500009:[0009#9,1-0009#9,1] marked as compacting + 500010:[0010#10,1-0010#10,1] marked as compacting + 500011:[0011#11,1-0011#11,1] marked as compacting + 600001:[0001#1,1-0100#1,1] marked as compacting + +init 1 +4: 1 +5: 11 50 +6: 100 +---- +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 500007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 500008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 500009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 500010:[0010#10,1-0010#10,1]: 1 bytes (1B) + 500011:[0011#11,1-0011#11,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0100#1,1]: 100 bytes (100B) + +init_cp +---- +base: 4 + +queue +---- +L5->L6: 6.2 + 500001:[0001#1,1-0001#1,1] marked as compacting + 500002:[0002#2,1-0002#2,1] marked as compacting + 500003:[0003#3,1-0003#3,1] marked as compacting + 500004:[0004#4,1-0004#4,1] marked as compacting + 500005:[0005#5,1-0005#5,1] marked as compacting + 500006:[0006#6,1-0006#6,1] marked as compacting + 500007:[0007#7,1-0007#7,1] marked as compacting + 500008:[0008#8,1-0008#8,1] marked as compacting + 500009:[0009#9,1-0009#9,1] marked as compacting + 500010:[0010#10,1-0010#10,1] marked as compacting + 500011:[0011#11,1-0011#11,1] marked as compacting + 600001:[0001#1,1-0100#1,1] marked as compacting + +init 1 +4: 2 +5: 11 +6: 100 +---- +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 400002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 500007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 500008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 500009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 500010:[0010#10,1-0010#10,1]: 1 bytes (1B) + 500011:[0011#11,1-0011#11,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0100#1,1]: 100 bytes (100B) + +init_cp +---- +base: 4 + +queue +---- +L4->L5: 1.8 + 400001:[0001#1,1-0001#1,1] marked as compacting + 500001:[0001#1,1-0001#1,1] marked as compacting + +init 1 +0: 4 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + +init_cp +---- +base: 6 + +queue +---- +L0->L6: 200.0 + 000001:[0001#1,1-0001#1,1] marked as compacting + 000002:[0001#2,1-0001#2,1] marked as compacting + 000003:[0001#3,1-0001#3,1] marked as compacting + 000004:[0001#4,1-0001#4,1] marked as compacting + +init 1 +0: 5 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + +init_cp +---- +base: 6 + +queue +---- +L0->L6: 250.0 + 000001:[0001#1,1-0001#1,1] marked as compacting + 000002:[0001#2,1-0001#2,1] marked as compacting + 000003:[0001#3,1-0001#3,1] marked as compacting + 000004:[0001#4,1-0001#4,1] marked as compacting + 000005:[0001#5,1-0001#5,1] marked as compacting + +init 1 +0: 5 +5: 2 +6: 10 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +queue +---- +L5->L6: 3.2 + 500001:[0001#1,1-0001#1,1] marked as compacting + 600001:[0001#1,1-0001#1,1] marked as compacting + +pick +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: L5->L6: 3.2 + +pick ongoing=(5,6) +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: no compaction + +init 1 +0: 10 +4: 10 +5: 6 +6: 10 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 400002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 400003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 400004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 400005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 400006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 400007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 400008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 400009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 400010:[0010#10,1-0010#10,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) + +init_cp +---- +base: 4 + +queue +---- +L4->L5: 10.0 + 400007:[0007#7,1-0007#7,1] marked as compacting +L4->L5: 7.7 + 400008:[0008#8,1-0008#8,1] marked as compacting + +pick ongoing=(5,6) +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 400002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 400003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 400004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 400005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 400006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 400007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 400008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 400009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 400010:[0010#10,1-0010#10,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: L4->L5: 12.0 + +pick ongoing=(0,4) +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 400002:[0002#2,1-0002#2,1]: 1 bytes (1B) (IsCompacting) + 400003:[0003#3,1-0003#3,1]: 1 bytes (1B) (IsCompacting) + 400004:[0004#4,1-0004#4,1]: 1 bytes (1B) (IsCompacting) + 400005:[0005#5,1-0005#5,1]: 1 bytes (1B) (IsCompacting) + 400006:[0006#6,1-0006#6,1]: 1 bytes (1B) (IsCompacting) + 400007:[0007#7,1-0007#7,1]: 1 bytes (1B) (IsCompacting) + 400008:[0008#8,1-0008#8,1]: 1 bytes (1B) (IsCompacting) + 400009:[0009#9,1-0009#9,1]: 1 bytes (1B) (IsCompacting) + 400010:[0010#10,1-0010#10,1]: 1 bytes (1B) (IsCompacting) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: L5->L6: 3.3 + +pick ongoing=(0,0,0,4) +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) (IsCompacting) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) (IsCompacting) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 400002:[0002#2,1-0002#2,1]: 1 bytes (1B) (IsCompacting) + 400003:[0003#3,1-0003#3,1]: 1 bytes (1B) (IsCompacting) + 400004:[0004#4,1-0004#4,1]: 1 bytes (1B) (IsCompacting) + 400005:[0005#5,1-0005#5,1]: 1 bytes (1B) (IsCompacting) + 400006:[0006#6,1-0006#6,1]: 1 bytes (1B) (IsCompacting) + 400007:[0007#7,1-0007#7,1]: 1 bytes (1B) (IsCompacting) + 400008:[0008#8,1-0008#8,1]: 1 bytes (1B) (IsCompacting) + 400009:[0009#9,1-0009#9,1]: 1 bytes (1B) (IsCompacting) + 400010:[0010#10,1-0010#10,1]: 1 bytes (1B) (IsCompacting) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: no compaction + +init_cp +---- +base: 4 + +pick ongoing=(0,5) +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L4: + 400001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 400002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 400003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 400004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 400005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 400006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 400007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 400008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 400009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 400010:[0010#10,1-0010#10,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) (IsCompacting) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) (IsCompacting) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) (IsCompacting) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) (IsCompacting) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) (IsCompacting) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: L4->L5: 8.6 + +init 1 +0: 20 +6: 10 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) + 000011:[0001#11,1-0001#11,1]: 1 bytes (1B) + 000012:[0001#12,1-0001#12,1]: 1 bytes (1B) + 000013:[0001#13,1-0001#13,1]: 1 bytes (1B) + 000014:[0001#14,1-0001#14,1]: 1 bytes (1B) + 000015:[0001#15,1-0001#15,1]: 1 bytes (1B) + 000016:[0001#16,1-0001#16,1]: 1 bytes (1B) + 000017:[0001#17,1-0001#17,1]: 1 bytes (1B) + 000018:[0001#18,1-0001#18,1]: 1 bytes (1B) + 000019:[0001#19,1-0001#19,1]: 1 bytes (1B) + 000020:[0001#20,1-0001#20,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) + +init_cp ongoing=(0,5) +---- +base: 4 + +queue +---- +L0->L4: 1000.0 + 000001:[0001#1,1-0001#1,1] marked as compacting + 000002:[0001#2,1-0001#2,1] marked as compacting + 000003:[0001#3,1-0001#3,1] marked as compacting + 000004:[0001#4,1-0001#4,1] marked as compacting + 000005:[0001#5,1-0001#5,1] marked as compacting + 000006:[0001#6,1-0001#6,1] marked as compacting + 000007:[0001#7,1-0001#7,1] marked as compacting + 000008:[0001#8,1-0001#8,1] marked as compacting + 000009:[0001#9,1-0001#9,1] marked as compacting + 000010:[0001#10,1-0001#10,1] marked as compacting + 000011:[0001#11,1-0001#11,1] marked as compacting + 000012:[0001#12,1-0001#12,1] marked as compacting + 000013:[0001#13,1-0001#13,1] marked as compacting + 000014:[0001#14,1-0001#14,1] marked as compacting + 000015:[0001#15,1-0001#15,1] marked as compacting + 000016:[0001#16,1-0001#16,1] marked as compacting + 000017:[0001#17,1-0001#17,1] marked as compacting + 000018:[0001#18,1-0001#18,1] marked as compacting + 000019:[0001#19,1-0001#19,1] marked as compacting + 000020:[0001#20,1-0001#20,1] marked as compacting + +pick +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) + 000011:[0001#11,1-0001#11,1]: 1 bytes (1B) + 000012:[0001#12,1-0001#12,1]: 1 bytes (1B) + 000013:[0001#13,1-0001#13,1]: 1 bytes (1B) + 000014:[0001#14,1-0001#14,1]: 1 bytes (1B) + 000015:[0001#15,1-0001#15,1]: 1 bytes (1B) + 000016:[0001#16,1-0001#16,1]: 1 bytes (1B) + 000017:[0001#17,1-0001#17,1]: 1 bytes (1B) + 000018:[0001#18,1-0001#18,1]: 1 bytes (1B) + 000019:[0001#19,1-0001#19,1]: 1 bytes (1B) + 000020:[0001#20,1-0001#20,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: L0->L4: 1000.0 + +pick ongoing=(0,4) +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) + 000011:[0001#11,1-0001#11,1]: 1 bytes (1B) + 000012:[0001#12,1-0001#12,1]: 1 bytes (1B) + 000013:[0001#13,1-0001#13,1]: 1 bytes (1B) + 000014:[0001#14,1-0001#14,1]: 1 bytes (1B) + 000015:[0001#15,1-0001#15,1]: 1 bytes (1B) + 000016:[0001#16,1-0001#16,1]: 1 bytes (1B) + 000017:[0001#17,1-0001#17,1]: 1 bytes (1B) + 000018:[0001#18,1-0001#18,1]: 1 bytes (1B) + 000019:[0001#19,1-0001#19,1]: 1 bytes (1B) + 000020:[0001#20,1-0001#20,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: no compaction + +# We'll only pick a concurrent compaction if it is "high" priority +# (i.e. has a score >= highPriorityThreshold). + +init 1 +0: 20 +5: 1 +6: 10 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) + 000011:[0001#11,1-0001#11,1]: 1 bytes (1B) + 000012:[0001#12,1-0001#12,1]: 1 bytes (1B) + 000013:[0001#13,1-0001#13,1]: 1 bytes (1B) + 000014:[0001#14,1-0001#14,1]: 1 bytes (1B) + 000015:[0001#15,1-0001#15,1]: 1 bytes (1B) + 000016:[0001#16,1-0001#16,1]: 1 bytes (1B) + 000017:[0001#17,1-0001#17,1]: 1 bytes (1B) + 000018:[0001#18,1-0001#18,1]: 1 bytes (1B) + 000019:[0001#19,1-0001#19,1]: 1 bytes (1B) + 000020:[0001#20,1-0001#20,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) + +init_cp ongoing=(0,4) +---- +base: 4 + +queue +---- +L0->L4: 1000.0 + 000001:[0001#1,1-0001#1,1] marked as compacting + 000002:[0001#2,1-0001#2,1] marked as compacting + 000003:[0001#3,1-0001#3,1] marked as compacting + 000004:[0001#4,1-0001#4,1] marked as compacting + 000005:[0001#5,1-0001#5,1] marked as compacting + 000006:[0001#6,1-0001#6,1] marked as compacting + 000007:[0001#7,1-0001#7,1] marked as compacting + 000008:[0001#8,1-0001#8,1] marked as compacting + 000009:[0001#9,1-0001#9,1] marked as compacting + 000010:[0001#10,1-0001#10,1] marked as compacting + 000011:[0001#11,1-0001#11,1] marked as compacting + 000012:[0001#12,1-0001#12,1] marked as compacting + 000013:[0001#13,1-0001#13,1] marked as compacting + 000014:[0001#14,1-0001#14,1] marked as compacting + 000015:[0001#15,1-0001#15,1] marked as compacting + 000016:[0001#16,1-0001#16,1] marked as compacting + 000017:[0001#17,1-0001#17,1] marked as compacting + 000018:[0001#18,1-0001#18,1] marked as compacting + 000019:[0001#19,1-0001#19,1] marked as compacting + 000020:[0001#20,1-0001#20,1] marked as compacting + +pick ongoing=(0,4,4,5) +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) + 000011:[0001#11,1-0001#11,1]: 1 bytes (1B) + 000012:[0001#12,1-0001#12,1]: 1 bytes (1B) + 000013:[0001#13,1-0001#13,1]: 1 bytes (1B) + 000014:[0001#14,1-0001#14,1]: 1 bytes (1B) + 000015:[0001#15,1-0001#15,1]: 1 bytes (1B) + 000016:[0001#16,1-0001#16,1]: 1 bytes (1B) + 000017:[0001#17,1-0001#17,1]: 1 bytes (1B) + 000018:[0001#18,1-0001#18,1]: 1 bytes (1B) + 000019:[0001#19,1-0001#19,1]: 1 bytes (1B) + 000020:[0001#20,1-0001#20,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: no compaction + +pick ongoing=(4,5) +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) + 000011:[0001#11,1-0001#11,1]: 1 bytes (1B) + 000012:[0001#12,1-0001#12,1]: 1 bytes (1B) + 000013:[0001#13,1-0001#13,1]: 1 bytes (1B) + 000014:[0001#14,1-0001#14,1]: 1 bytes (1B) + 000015:[0001#15,1-0001#15,1]: 1 bytes (1B) + 000016:[0001#16,1-0001#16,1]: 1 bytes (1B) + 000017:[0001#17,1-0001#17,1]: 1 bytes (1B) + 000018:[0001#18,1-0001#18,1]: 1 bytes (1B) + 000019:[0001#19,1-0001#19,1]: 1 bytes (1B) + 000020:[0001#20,1-0001#20,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: L0->L4: 1000.0 + +# Verify we can start concurrent Ln->Ln+1 compactions given sufficient +# priority. + +init 1 +5: 4 +6: 10 +---- +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +queue +---- +L5->L6: 5.2 + 500001:[0001#1,1-0001#1,1] marked as compacting + 600001:[0001#1,1-0001#1,1] marked as compacting + +pick +---- +Initial state before pick: +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: L5->L6: 5.2 + +pick ongoing=(5,6) +---- +Initial state before pick: +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) (IsCompacting) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 600006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 600007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 600008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 600009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 600010:[0010#10,1-0010#10,1]: 1 bytes (1B) +Picked: no compaction + +# Verify that L0 score doesn't change with respect to L5's compensation. + +init 5 +0: 10 +5: 5 1 +6: 5 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +queue +---- +L0->L5: 5.0 + 000001:[0001#1,1-0001#1,1] marked as compacting + 000002:[0001#2,1-0001#2,1] marked as compacting + 000003:[0001#3,1-0001#3,1] marked as compacting + 000004:[0001#4,1-0001#4,1] marked as compacting + 000005:[0001#5,1-0001#5,1] marked as compacting + 000006:[0001#6,1-0001#6,1] marked as compacting + 000007:[0001#7,1-0001#7,1] marked as compacting + 000008:[0001#8,1-0001#8,1] marked as compacting + 000009:[0001#9,1-0001#9,1] marked as compacting + 000010:[0001#10,1-0001#10,1] marked as compacting + 500001:[0001#1,1-0001#1,1] marked as compacting +L5->L6: 11.5 + 500005:[0005#5,1-0005#5,1] marked as compacting + 600005:[0005#5,1-0005#5,1] marked as compacting + +init 5 +0: 10 +5: 5 0 +6: 5 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +queue +---- +L0->L5: 5.0 + 000001:[0001#1,1-0001#1,1] marked as compacting + 000002:[0001#2,1-0001#2,1] marked as compacting + 000003:[0001#3,1-0001#3,1] marked as compacting + 000004:[0001#4,1-0001#4,1] marked as compacting + 000005:[0001#5,1-0001#5,1] marked as compacting + 000006:[0001#6,1-0001#6,1] marked as compacting + 000007:[0001#7,1-0001#7,1] marked as compacting + 000008:[0001#8,1-0001#8,1] marked as compacting + 000009:[0001#9,1-0001#9,1] marked as compacting + 000010:[0001#10,1-0001#10,1] marked as compacting + 500001:[0001#1,1-0001#1,1] marked as compacting +L5->L6: 10.8 + 500002:[0002#2,1-0002#2,1] marked as compacting + 600002:[0002#2,1-0002#2,1] marked as compacting + +# Verify that successive manual compactions interleaved with an automatic +# compaction does not trigger an error. + +init 5 +0: 10 +5: 10 +6: 5 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 500007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 500008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 500009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 500010:[0010#10,1-0010#10,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +queue +---- +L5->L6: 9.2 + 500006:[0006#6,1-0006#6,1] marked as compacting +L5->L6: 6.9 + 500007:[0007#7,1-0007#7,1] marked as compacting + +pick_manual level=0 start=0 end=12 +---- +L0->L5, retryLater = false + +pick +---- +Initial state before pick: +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) + 000008:[0001#8,1-0001#8,1]: 1 bytes (1B) + 000009:[0001#9,1-0001#9,1]: 1 bytes (1B) + 000010:[0001#10,1-0001#10,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 500005:[0005#5,1-0005#5,1]: 1 bytes (1B) + 500006:[0006#6,1-0006#6,1]: 1 bytes (1B) + 500007:[0007#7,1-0007#7,1]: 1 bytes (1B) + 500008:[0008#8,1-0008#8,1]: 1 bytes (1B) + 500009:[0009#9,1-0009#9,1]: 1 bytes (1B) + 500010:[0010#10,1-0010#10,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) +Picked: L5->L6: 9.2 + +# Assume the above two compactions (one manual L0 -> L5 and one automatic +# L5 -> L6) have run, and Lbase = L6 now, but the manual compaction code is +# still going to try running a manual compaction from L5 -> L6 since L5 was the +# output of the last manual compaction. No compaction should be picked. + +init 5 +0: 7 +6: 5 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +pick_manual level=5 start=0 end=12 +---- +nil, retryLater = false + + +# Initialize with LbaseMaxBytes of 5, and give L5 a compensated size of 10000. +# Prior to prioritizing levels by the score instead of rawSmoothed score, L5 +# would be picked for compaction over L0, because of its absurdly high compensated +# score. +init 5 +0: 7 +5: 4 10000 +6: 5 +---- +L0: + 000001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 000002:[0001#2,1-0001#2,1]: 1 bytes (1B) + 000003:[0001#3,1-0001#3,1]: 1 bytes (1B) + 000004:[0001#4,1-0001#4,1]: 1 bytes (1B) + 000005:[0001#5,1-0001#5,1]: 1 bytes (1B) + 000006:[0001#6,1-0001#6,1]: 1 bytes (1B) + 000007:[0001#7,1-0001#7,1]: 1 bytes (1B) +L5: + 500001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 500002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 500003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 500004:[0004#4,1-0004#4,1]: 1 bytes (1B) +L6: + 600001:[0001#1,1-0001#1,1]: 1 bytes (1B) + 600002:[0002#2,1-0002#2,1]: 1 bytes (1B) + 600003:[0003#3,1-0003#3,1]: 1 bytes (1B) + 600004:[0004#4,1-0004#4,1]: 1 bytes (1B) + 600005:[0005#5,1-0005#5,1]: 1 bytes (1B) + +init_cp +---- +base: 5 + +queue +---- +L0->L5: 4.4 + 000001:[0001#1,1-0001#1,1] marked as compacting + 000002:[0001#2,1-0001#2,1] marked as compacting + 000003:[0001#3,1-0001#3,1] marked as compacting + 000004:[0001#4,1-0001#4,1] marked as compacting + 000005:[0001#5,1-0001#5,1] marked as compacting + 000006:[0001#6,1-0001#6,1] marked as compacting + 000007:[0001#7,1-0001#7,1] marked as compacting + 500001:[0001#1,1-0001#1,1] marked as compacting diff --git a/pebble/testdata/compaction_read_triggered b/pebble/testdata/compaction_read_triggered new file mode 100644 index 0000000..5c3c796 --- /dev/null +++ b/pebble/testdata/compaction_read_triggered @@ -0,0 +1,166 @@ +# A simple case of read compaction, 2 files in different levels with overlapping ranges +define +L5 +a.SET.55:a b.SET.5:b +L6 +a.SET.54:a b.SET.4:b +---- +5: + 000004:[a#55,SET-b#5,SET] +6: + 000005:[a#54,SET-b#4,SET] + +add-read-compaction +5: a-b 000004 +---- + +show-read-compactions +---- +(level: 5, start: a, end: b) + +maybe-compact +---- +[JOB 100] compacted(read) L5 [000004] (784B) Score=0.00 + L6 [000005] (784B) Score=0.00 -> L6 [000006] (778B), in 1.0s (2.0s total), output rate 778B/s + +show-read-compactions +---- +(none) + +version +---- +6: + 000006:[a#0,SET-b#0,SET] + +# Check to make sure another compaction will not take place + +maybe-compact +---- +(none) + +# Case where there is an in-progress flush. No compaction should occur while flushing is true. +define +L5 +a.SET.55:a b.SET.5:b +L6 +a.SET.54:a b.SET.4:b +---- +5: + 000004:[a#55,SET-b#5,SET] +6: + 000005:[a#54,SET-b#4,SET] + +add-read-compaction flushing=true +5: a-b 000004 +---- + +show-read-compactions +---- +(level: 5, start: a, end: b) + +maybe-compact +---- +(none) + +show-read-compactions +---- +(level: 5, start: a, end: b) + +version +---- +5: + 000004:[a#55,SET-b#5,SET] +6: + 000005:[a#54,SET-b#4,SET] + +add-read-compaction flushing=false +---- + +show-read-compactions +---- +(level: 5, start: a, end: b) + +maybe-compact +---- +[JOB 100] compacted(read) L5 [000004] (784B) Score=0.00 + L6 [000005] (784B) Score=0.00 -> L6 [000006] (778B), in 1.0s (2.0s total), output rate 778B/s + +show-read-compactions +---- +(none) + +version +---- +6: + 000006:[a#0,SET-b#0,SET] + +# Test case where there is mismatch in the level of chosen read compaction and current version. +# In this case, we skip the compaction. +define +L5 +a.SET.55:a b.SET.5:b +L6 +a.SET.55:a b.SET.5:b +---- +5: + 000004:[a#55,SET-b#5,SET] +6: + 000005:[a#55,SET-b#5,SET] + +add-read-compaction +4: a-b 000004 +---- + +show-read-compactions +---- +(level: 4, start: a, end: b) + +maybe-compact +---- +(none) + +show-read-compactions +---- +(none) + +version +---- +5: + 000004:[a#55,SET-b#5,SET] +6: + 000005:[a#55,SET-b#5,SET] + +# The read compaction range overlaps with the appropriate level, but +# the file number is different. +# So, we skip the compaction. +define +L5 +a.SET.55:a b.SET.5:b +L6 +a.SET.55:a b.SET.5:b +---- +5: + 000004:[a#55,SET-b#5,SET] +6: + 000005:[a#55,SET-b#5,SET] + +add-read-compaction +5: a-b 000003 +---- + +show-read-compactions +---- +(level: 5, start: a, end: b) + +maybe-compact +---- +(none) + +show-read-compactions +---- +(none) + +version +---- +5: + 000004:[a#55,SET-b#5,SET] +6: + 000005:[a#55,SET-b#5,SET] diff --git a/pebble/testdata/compaction_setup_inputs b/pebble/testdata/compaction_setup_inputs new file mode 100644 index 0000000..6755a8f --- /dev/null +++ b/pebble/testdata/compaction_setup_inputs @@ -0,0 +1,153 @@ +setup-inputs a a +L0 + a.SET.1-b.SET.2 +---- +L0 + 000001:[a#1,1-b#2,1] + +setup-inputs c c +L0 + a.SET.1-b.SET.2 +---- + +# Verify we expand the start level inputs to a clean cut. +setup-inputs a a +L1 + a.SET.1-b.SET.2 + b.SET.1-c.SET.2 +---- +L1 + 000001:[a#1,1-b#2,1] + 000002:[b#1,1-c#2,1] + +# The range deletion sentinel acts as a clean cut boundary. +setup-inputs a a +L1 + a.SET.1-b.RANGEDEL.72057594037927935 + b.SET.1-c.SET.2 +---- +L1 + 000001:[a#1,1-b#72057594037927935,15] + +# Verify we expand the output level inputs to a clean cut. +setup-inputs a a +L1 + a.SET.5-b.SET.6 +L2 + a.SET.3-c.SET.4 + c.SET.3-d.SET.2 +---- +L1 + 000001:[a#5,1-b#6,1] +L2 + 000002:[a#3,1-c#4,1] + 000003:[c#3,1-d#2,1] + +# Verify we expand the output level inputs to a clean cut. +setup-inputs a a +L1 + a.SET.5-b.SET.6 +L2 + a.SET.3-c.RANGEDEL.72057594037927935 + c.SET.3-d.SET.2 +---- +L1 + 000001:[a#5,1-b#6,1] +L2 + 000002:[a#3,1-c#72057594037927935,15] + +# Verify we grow the start level inputs to include all sstables which +# lie within the output level bounds. +setup-inputs a a +L1 + a.SET.5-b.SET.6 + c.SET.4-e.SET.3 +L2 + a.SET.3-d.SET.4 +---- +L1 + 000001:[a#5,1-b#6,1] + 000002:[c#4,1-e#3,1] +L2 + 000003:[a#3,1-d#4,1] + +# Verify we limit the start level input expansion according to available +# disk capacity. +setup-inputs avail-bytes=10 a a +L1 + a.SET.5-b.SET.6 size=2 + c.SET.4-e.SET.3 size=1 +L2 + a.SET.3-d.SET.4 size=3 +---- +L1 + 000001:[a#5,1-b#6,1] +L2 + 000003:[a#3,1-d#4,1] + +# Verify the available disk capacity limit doesn't affect the +# output level clean-cut expansion. +setup-inputs avail-bytes=10 a a +L1 + a.SET.5-b.SET.6 size=5 + c.SET.4-e.SET.3 size=10 +L2 + a.SET.3-d.SET.4 size=5 + d.SET.2-e.SET.2 size=5 +---- +L1 + 000001:[a#5,1-b#6,1] +L2 + 000003:[a#3,1-d#4,1] + 000004:[d#2,1-e#2,1] + +# We won't grow the start level inputs if doing so would grow the +# output level inputs. +setup-inputs a a +L1 + a.SET.5-b.SET.6 + c.SET.4-e.SET.3 +L2 + a.SET.3-d.SET.4 + e.SET.2-f.SET.1 +---- +L1 + 000001:[a#5,1-b#6,1] +L2 + 000003:[a#3,1-d#4,1] + +# Verify setup inputs can identify compacting files in range +setup-inputs a a +L1 + a.SET.5-b.SET.6 +L2 + a.SET.3-c.SET.4 + c.SET.3-d.SET.2 compacting + d.SET.3-e.SET.6 +---- +L1 + 000001:[a#5,1-b#6,1] +L2 + 000002:[a#3,1-c#4,1] + 000003:[c#3,1-d#2,1] + 000004:[d#3,1-e#6,1] +is-compacting + +# Verify when there is one file in range and it is compacting +setup-inputs a a +L2 + a.SET.3-c.SET.4 compacting + d.SET.3-e.SET.2 +---- +L2 + 000001:[a#3,1-c#4,1] +is-compacting + +# Verify when there is one file in level and is compacting +setup-inputs a a +L2 + a.SET.3-c.SET.4 compacting +---- +L2 + 000001:[a#3,1-c#4,1] +is-compacting diff --git a/pebble/testdata/compaction_setup_inputs_multilevel_dummy b/pebble/testdata/compaction_setup_inputs_multilevel_dummy new file mode 100644 index 0000000..ac92102 --- /dev/null +++ b/pebble/testdata/compaction_setup_inputs_multilevel_dummy @@ -0,0 +1,18 @@ +# init a multi-level compaction with dummy hueristic +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=1 +L2 + a.SET.3-c.SET.4 size=1 +L3 + c.SET.3-d.SET.2 size=1 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] +L3 + 000003:[c#3,1-d#2,1] +init-multi-level(1,2,3) +Original WriteAmp 2.00; ML WriteAmp 1.50 +Original OverlappingRatio 1.00; ML OverlappingRatio 0.50 diff --git a/pebble/testdata/compaction_setup_inputs_multilevel_write_amp b/pebble/testdata/compaction_setup_inputs_multilevel_write_amp new file mode 100644 index 0000000..beb31e9 --- /dev/null +++ b/pebble/testdata/compaction_setup_inputs_multilevel_write_amp @@ -0,0 +1,339 @@ +# Init a multi-level compaction, because multi level write amp is lower +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=1 +L2 + a.SET.3-c.SET.4 size=1 +L3 + c.SET.3-d.SET.2 size=1 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] +L3 + 000003:[c#3,1-d#2,1] +init-multi-level(1,2,3) +Original WriteAmp 2.00; ML WriteAmp 1.50 +Original OverlappingRatio 1.00; ML OverlappingRatio 0.50 + +# Verify that the input level size should not affect the decision to conduct a multi +# level compaction. +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=10 +L2 + a.SET.3-c.SET.4 size=1 +L3 + c.SET.3-d.SET.2 size=1 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] +L3 + 000003:[c#3,1-d#2,1] +init-multi-level(1,2,3) +Original WriteAmp 1.10; ML WriteAmp 1.09 +Original OverlappingRatio 0.10; ML OverlappingRatio 0.09 + +# Don't init a multi-level compaction because write amp from multi level compaction is larger +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=1 +L2 + a.SET.3-c.SET.4 size=1 +L3 + c.SET.3-d.SET.2 size=3 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] + +# Init a multi-level compaction, but note that the second files in L2 and L3 do not get +# chosen, as they don't overlap with the original compaction. +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=6 +L2 + a.SET.3-c.SET.4 size=5 + e.SET.1-h.SET.4 size=4 +L3 + c.SET.3-d.SET.2 size=6 + e.SET.2-h.SET.4 size=4 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] +L3 + 000004:[c#3,1-d#2,1] +init-multi-level(1,2,3) +Original WriteAmp 1.83; ML WriteAmp 1.55 +Original OverlappingRatio 0.83; ML OverlappingRatio 0.55 + +# Init a multi-level compaction without an overlapping file in the lowest level. +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=6 +L2 max-size=5 + a.SET.3-c.SET.4 size=5 +L3 + e.SET.3-f.SET.2 size=100 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] +init-multi-level(1,2,3) +Original WriteAmp 1.83; ML WriteAmp 1.00 +Original OverlappingRatio 0.83; ML OverlappingRatio 0.00 + +# Init a multi-level compaction with no file in the lowest level. +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=6 +L2 + a.SET.3-c.SET.4 size=5 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] +init-multi-level(1,2,3) +Original WriteAmp 1.83; ML WriteAmp 1.00 +Original OverlappingRatio 0.83; ML OverlappingRatio 0.00 + + +# Don't init a multi-level compaction, as the single level compaction results in a move ( +# write amp is 1) while the multi level compaction results in a Write Amp greater than 1 +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=6 +L2 + e.SET.3-f.SET.2 size=100 +L3 + a.SET.3-c.SET.4 size=5 +---- +L1 + 000001:[a#1,1-b#2,1] + +# Init a multi-level compaction, without an overlapping file in the (tie goes to the ML compaction!) +# intermediate and output levels +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=6 +L2 + e.SET.3-f.SET.2 size=1 +L3 + e.SET.4-f.SET.5 size=5 +---- +L1 + 000001:[a#1,1-b#2,1] +init-multi-level(1,2,3) +Original WriteAmp 1.00; ML WriteAmp 1.00 +Original OverlappingRatio 0.00; ML OverlappingRatio 0.00 + + +# Init a multi-level compaction which expands the intermediate level with a file that only +# overlaps with the lowest level. (I.e. it gets included during second setupInputs call) +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=3 +L2 + a.SET.2-b.SET.3 size=5 + c.SET.2-d.SET.3 size=3 +L3 + a.SET.3-c.SET.4 size=3 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#2,1-b#3,1] + 000003:[c#2,1-d#3,1] +L3 + 000004:[a#3,1-c#4,1] +init-multi-level(1,2,3) +Original WriteAmp 2.67; ML WriteAmp 1.27 +Original OverlappingRatio 1.67; ML OverlappingRatio 0.27 + +# Init a multi-level compaction which DOES NOT expand the input level with a file that +# only overlaps with the lowest level, even if it doesn't expand the output level keyspan. +# TODO(msbutler): include this file in the compaction +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=1 + c.SET.2-d.SET.3 size=10 +L2 + a.SET.2-b.SET.3 size=1 +L3 + a.SET.3-c.SET.4 size=1 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000003:[a#2,1-b#3,1] +L3 + 000004:[a#3,1-c#4,1] +init-multi-level(1,2,3) +Original WriteAmp 2.00; ML WriteAmp 1.50 +Original OverlappingRatio 1.00; ML OverlappingRatio 0.50 + +# Verify an expansion of the output level in the initial setupInputs will init a multi-level +# compaction. i.e. without the initial expansion, the multil level compaction would not have +# occurred. +setup-inputs a a +L1 + a.SET.5-b.SET.6 size=1 +L2 + a.SET.3-c.SET.4 size=1 + c.SET.3-d.SET.2 size=1 +L3 + c.SET.4-d.SET.4 size=3 +---- +L1 + 000001:[a#5,1-b#6,1] +L2 + 000002:[a#3,1-c#4,1] + 000003:[c#3,1-d#2,1] +L3 + 000004:[c#4,1-d#4,1] +init-multi-level(1,2,3) +Original WriteAmp 3.00; ML WriteAmp 2.00 +Original OverlappingRatio 2.00; ML OverlappingRatio 1.00 + +setup-inputs a a +L1 + a.SET.5-b.SET.6 size=1 +L2 + a.SET.3-c.SET.4 size=1 +L3 + c.SET.4-d.SET.4 size=3 +---- +L1 + 000001:[a#5,1-b#6,1] +L2 + 000002:[a#3,1-c#4,1] + + +# Verify the second setupInputs call does not add an intermediate file if doing so would expand the +# output level (i.e. the pc.grow logic). +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=1 +L2 + a.SET.2-b.SET.3 size=1 + d.SET.2-f.SET.2 size=1 +L3 + b.SET.1-d.SET.1 size=1 + e.SET.4-f.SET.5 size=1 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#2,1-b#3,1] +L3 + 000004:[b#1,1-d#1,1] +init-multi-level(1,2,3) +Original WriteAmp 2.00; ML WriteAmp 1.50 +Original OverlappingRatio 1.00; ML OverlappingRatio 0.50 + +# Verify the max number of input levels equals 2. +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=6 +L2 + a.SET.3-c.SET.4 size=5 +L3 + c.SET.3-d.SET.2 size=2 +L4 + c.SET.4-d.SET.3 size=1 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] +L3 + 000003:[c#3,1-d#2,1] +init-multi-level(1,2,3) +Original WriteAmp 1.83; ML WriteAmp 1.18 +Original OverlappingRatio 0.83; ML OverlappingRatio 0.18 + +# Don't init multi-level compaction if the max size limit exceeded by initial setupInputs. +setup-inputs avail-bytes=10 a a +L1 + a.SET.1-b.SET.2 size=6 +L2 + a.SET.5-b.SET.6 size=5 +L3 + a.SET.3-d.SET.4 size=3 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#5,1-b#6,1] + +# During second setupInputs call, allow output level expansion even if max size +# limit is exceeded, but not conduct intermediate level expansion. +# +# TODO(msbutler): If second setup inputs exceeds maxSize limits, should the first compaction get +# returned? +setup-inputs avail-bytes=20 a a +L1 + a.SET.1-b.SET.2 size=4 +L2 + a.SET.5-b.SET.6 size=5 + c.SET.4-e.SET.3 size=8 +L3 + a.SET.3-d.SET.4 size=2 + d.SET.2-e.SET.2 size=2 +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#5,1-b#6,1] +L3 + 000004:[a#3,1-d#4,1] + 000005:[d#2,1-e#2,1] +init-multi-level(1,2,3) +Original WriteAmp 2.25; ML WriteAmp 1.44 +Original OverlappingRatio 1.25; ML OverlappingRatio 0.44 + +# Don't init a multi-level compaction if the start level is L5. +setup-inputs a a +L5 + a.SET.1-b.SET.2 size=6 +L6 + a.SET.3-c.SET.4 size=5 +---- +L5 + 000001:[a#1,1-b#2,1] +L6 + 000002:[a#3,1-c#4,1] + +# Don't init a multi-level compaction if the start level is L0. +setup-inputs a a +L0 + a.SET.1-b.SET.2 size=6 +L1 + a.SET.3-c.SET.4 size=5 +---- +L0 + 000001:[a#1,1-b#2,1] +L1 + 000002:[a#3,1-c#4,1] + +# Verify a multi level compaction will not init on a compacting file. +setup-inputs a a +L1 + a.SET.1-b.SET.2 size=1 +L2 + a.SET.3-c.SET.4 size=1 +L3 + c.SET.3-d.SET.2 size=1 compacting +---- +L1 + 000001:[a#1,1-b#2,1] +L2 + 000002:[a#3,1-c#4,1] diff --git a/pebble/testdata/compaction_tombstones b/pebble/testdata/compaction_tombstones new file mode 100644 index 0000000..795c019 --- /dev/null +++ b/pebble/testdata/compaction_tombstones @@ -0,0 +1,419 @@ +# Test an L6 file that contains range tombstones, but whose keys are not in +# the last snapshot stripe. The tombstones wouldn't be elided, so no +# compaction is pursued. +define snapshots=(70, 100, 180, 210) +L6 +b.RANGEDEL.230:h h.RANGEDEL.200:r +---- +6: + 000004:[b#230,RANGEDEL-r#inf,RANGEDEL] + +wait-pending-table-stats +000004 +---- +num-entries: 2 +num-deletions: 2 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +maybe-compact +---- +(none) + +# Test the same scenario, but the file is in the last stripe. Since the file +# only contains deletes, no new sstable is written. +define snapshots=(270, 300, 380, 410) +L6 +b.RANGEDEL.230:h h.RANGEDEL.200:r +---- +6: + 000004:[b#230,RANGEDEL-r#inf,RANGEDEL] + +wait-pending-table-stats +000004 +---- +num-entries: 2 +num-deletions: 2 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +maybe-compact +---- +[JOB 100] compacted(elision-only) L6 [000004] (748B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s + +# Test a table that straddles a snapshot. It should not be compacted. +define snapshots=(50) auto-compactions=off +L6 +a.SET.55:a b.RANGEDEL.5:h +---- +6: + 000004:[a#55,SET-h#inf,RANGEDEL] + +wait-pending-table-stats +000004 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +maybe-compact +---- +(none) + +# Test a table with a point deletion and a non-deletion entry. The table +# should be compacted, and a new table with the point tombstone should be +# written. +define auto-compactions=off +L6 +a.SET.55:a b.DEL.5: +---- +6: + 000004:[a#55,SET-b#5,DEL] + +wait-pending-table-stats +000004 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 93 +range-deletions-bytes-estimate: 0 + +maybe-compact +---- +[JOB 100] compacted(elision-only) L6 [000004] (714B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (663B), in 1.0s (2.0s total), output rate 663B/s + +version +---- +6: + 000005:[a#0,SET-a#0,SET] + +# Checking for a compaction again should not trigger a compaction, because +# 000005 does not contain deletions. + +maybe-compact +---- +(none) + +maybe-compact +---- +(none) + +# Test a table that contains both deletions and non-deletions, but whose +# deletions remove the non-deletions. The compaction should not create a new +# table, but shouldn't happen until the snapshots are removed. +define snapshots=(59, 103) auto-compactions=off +L6 +a.DEL.60: a.SET.55:a b.SET.100:b c.SET.101:c d.SET.102:d b.RANGEDEL.103:z +---- +6: + 000004:[a#60,DEL-z#inf,RANGEDEL] + +wait-pending-table-stats +000004 +---- +num-entries: 6 +num-deletions: 2 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 42 +range-deletions-bytes-estimate: 66 + +maybe-compact +---- +(none) + +close-snapshot +59 +---- +(none) + +close-snapshot +103 +---- +[JOB 100] compacted(elision-only) L6 [000004] (892B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s + +# Test a table that contains both deletions and non-deletions, but whose +# non-deletions well outnumber its deletions. The table should not be +# compacted because it falls beneath the threshold. +define snapshots=(15) auto-compactions=off +L6 +a.DEL.20: a.SET.1:a b.SET.2:b c.SET.3:c d.SET.4:d e.SET.5:e f.SET.6:f g.SET.7:g h.SET.8:h i.SET.9:i j.SET.10:j +---- +6: + 000004:[a#20,DEL-j#10,SET] + +wait-pending-table-stats +000004 +---- +num-entries: 11 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 23 +range-deletions-bytes-estimate: 0 + +close-snapshot +15 +---- +(none) + +# Test a table that contains both deletions and non-deletions, but whose +# deletions remove the non-deletions. Set L5's max bytes low so that an +# automatic compaction will be pursued when we call maybe-compact. +# Automatic compactions need to be disabled to prevent a race where an +# automatic compaction compacts before we've closen the snapshot. +define snapshots=(103) level-max-bytes=(L5 : 1000) auto-compactions=off +L5 +b.SET.200: bb.SET.203: cc.SET.204: +L5 +d.SET.302: dd.SET.303: de.SET.303: +L5 +m.SET.320: n.SET.330: o.SET.340: +L6 +a.SET.55: b.SET.100: c.SET.101: d.SET.102: a.RANGEDEL.103:e +L6 +f.SET.30: z.SET.31: +---- +5: + 000004:[b#200,SET-cc#204,SET] + 000005:[d#302,SET-de#303,SET] + 000006:[m#320,SET-o#340,SET] +6: + 000007:[a#103,RANGEDEL-e#inf,RANGEDEL] + 000008:[f#30,SET-z#31,SET] + +close-snapshot +103 +---- +(none) + +wait-pending-table-stats +000007 +---- +num-entries: 5 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 16492 + +# Because we set max bytes low, maybe-compact will trigger an automatic +# compaction in preference over an elision-only compaction. +# By plain file size, 000006 should be picked because it overlaps +# significantly less data in L6. However, 000007 has significant obsolete +# data. The compaction picker should recognize that it's more efficient to +# compact (000004 + 000005) into 000007. + +maybe-compact +---- +[JOB 100] compacted(default) L5 [000004 000005] (26KB) Score=87.72 + L6 [000007] (17KB) Score=0.73 -> L6 [000009] (25KB), in 1.0s (2.0s total), output rate 25KB/s + +define level-max-bytes=(L5 : 1000) auto-compactions=off +L5 +a.DEL.101: b.DEL.102: c.DEL.103: +L5 +m.SET.107: +L6 +a.SET.001: b.SET.002: c.SET.003: +L6 +f.SET.007: x.SET.008: z.SET.009: +---- +5: + 000004:[a#101,DEL-c#103,DEL] + 000005:[m#107,SET-m#107,SET] +6: + 000006:[a#1,SET-c#3,SET] + 000007:[f#7,SET-z#9,SET] + +wait-pending-table-stats +000004 +---- +num-entries: 3 +num-deletions: 3 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 6858 +range-deletions-bytes-estimate: 0 + +# By plain file size, 000005 should be picked because it is larger and +# overlaps the same amount of data in L6. However, 000004 has a high +# point-deletions-bytes-estimate, and the compaction picker should pick 000004 +# instead. + +maybe-compact +---- +[JOB 100] compacted(default) L5 [000004] (724B) Score=13.45 + L6 [000006] (13KB) Score=0.92 -> L6 [] (0B), in 1.0s (2.0s total), output rate 0B/s + +# A table containing only range keys is not eligible for elision. +# RANGEKEYDEL or RANGEKEYUNSET. + +define auto-compactions=off +L6 + rangekey:a-b:{(#1,RANGEKEYDEL)} +L6 + rangekey:b-c:{(#2,RANGEKEYUNSET,@1)} +L6 + rangekey:c-d:{(#3,RANGEKEYSET,@1)} +---- +6: + 000004:[a#1,RANGEKEYDEL-b#inf,RANGEKEYDEL] + 000005:[b#2,RANGEKEYUNSET-c#inf,RANGEKEYUNSET] + 000006:[c#3,RANGEKEYSET-d#inf,RANGEKEYSET] + +wait-pending-table-stats +000004 +---- +num-entries: 0 +num-deletions: 0 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +wait-pending-table-stats +000005 +---- +num-entries: 0 +num-deletions: 0 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +wait-pending-table-stats +000006 +---- +num-entries: 0 +num-deletions: 0 +num-range-key-sets: 1 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +maybe-compact +---- +(none) + +# Regression test for cockroachdb/cockroach#90149, exercising reference counting +# on tables that contain a mixture of point, range dels and range keys. +# +# Place a table in L6 that contains a RANGEKEYDEL. Because this table is in the +# bottom of the LSM, and there are no range keys below it, the RANGEKEYDEL is +# eligible for elision (the RANGEDEL too). After the elision, the input table +# should be deleted. In #90149, the table still had a reference count, and +# therefore could not be deleted from the filesystem. + +define auto-compactions=off +L6 + rangekey:a-b:{(#1,RANGEKEYDEL)} + a.SET.2:a + b.SET.3:b + c.SET.4:c + c.RANGEDEL.5:z +---- +6: + 000004:[a#2,SET-z#inf,RANGEDEL] + +wait-pending-table-stats +000004 +---- +num-entries: 3 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 41 + +maybe-compact +---- +[JOB 100] compacted(elision-only) L6 [000004] (894B) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000005] (669B), in 1.0s (2.0s total), output rate 669B/s + +# Close the DB, asserting that the reference counts balance. +close +---- + +# Demonstration of point tombstone weighting. +# +# Construct an LSM with two tables in L6, with a table above each in L5. The +# layout of the tables is such that the range deletion bytes estimate for table +# 000005 is greater than the point deletion bytes estimate for table 000004. +# Without weighting, table 000005 will be selected. + +define auto-compactions=off level-max-bytes=(L5 : 1000) +L5 +a.DEL.101: b.SET.102: +L5 +e.RANGEDEL.107:f f.SET.108: +L6 +a.SET.001: b.SET.002: c.SET.003: +L6 +e.SET.007: f.SET.008: g.SET.009: +---- +5: + 000004:[a#101,DEL-b#102,SET] + 000005:[e#107,RANGEDEL-f#108,SET] +6: + 000006:[a#1,SET-c#3,SET] + 000007:[e#7,SET-g#9,SET] + +wait-pending-table-stats +000004 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 2742 +range-deletions-bytes-estimate: 0 + +wait-pending-table-stats +000005 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 8246 + +maybe-compact +---- +[JOB 100] compacted(default) L5 [000005] (741B) Score=11.82 + L6 [000007] (13KB) Score=1.05 -> L6 [000008] (4.7KB), in 1.0s (2.0s total), output rate 4.7KB/s + +# The same LSM as above. However, this time, with point tombstone weighting at +# 2x, the table with the point tombstone (000004) will be selected as the +# compaction input. + +define auto-compactions=off level-max-bytes=(L5 : 1000) point-tombstone-weight=2 +L5 +a.DEL.101: b.SET.102: +L5 +e.RANGEDEL.107:f f.SET.108: +L6 +a.SET.001: b.SET.002: c.SET.003: +L6 +e.SET.007: f.SET.008: g.SET.009: +---- +5: + 000004:[a#101,DEL-b#102,SET] + 000005:[e#107,RANGEDEL-f#108,SET] +6: + 000006:[a#1,SET-c#3,SET] + 000007:[e#7,SET-g#9,SET] + +wait-pending-table-stats +000004 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 2742 +range-deletions-bytes-estimate: 0 + +wait-pending-table-stats +000005 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 8246 + +maybe-compact +---- +[JOB 100] compacted(default) L5 [000005] (741B) Score=11.82 + L6 [000007] (13KB) Score=1.05 -> L6 [000008] (4.7KB), in 1.0s (2.0s total), output rate 4.7KB/s diff --git a/pebble/testdata/compaction_transform b/pebble/testdata/compaction_transform new file mode 100644 index 0000000..4fd9fd0 --- /dev/null +++ b/pebble/testdata/compaction_transform @@ -0,0 +1,116 @@ + +# Test snapshot striping and coalescing. + +transform snapshots=(5,10,15) disable-elision +a-c:{(#9,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#9,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3)} + +transform snapshots=(5,10,15) disable-elision +a-c:{(#9,RANGEKEYUNSET,@3) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#9,RANGEKEYUNSET,@3) (#4,RANGEKEYSET,@3,foo3)} + +transform snapshots=(5,10,15) disable-elision +a-c:{(#9,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#9,RANGEKEYDEL) (#4,RANGEKEYSET,@3,foo3)} + +transform snapshots=(5,10,15) disable-elision +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3)} + +transform disable-elision +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#11,RANGEKEYDEL)} + +# Test that elision works on the last snapshot stripe. + +transform snapshots=(5,10,15) +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3)} + +transform snapshots=(3,10,15) +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYUNSET,@4) (#2,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#3,RANGEKEYUNSET,@4) (#2,RANGEKEYSET,@3,foo2)} + +transform snapshots=(2,10,15) +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYUNSET,@4) (#2,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5)} + +# The RANGEKEYDEL deletes all underlying keys and there are no snapshots or +# in-use key ranges at play, so all keys should empty out. + +transform +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{} + +# Test RANGEKEYDELs are preserved over in-use key ranges in the last snapshot stripe. +# in-use key ranges cover keys that exist in lower levels of the LSM, so dropping +# range keys in that space could cause correctness issues. + +transform in-use-key-ranges=(b-d) +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#11,RANGEKEYDEL)} + +# Test RANGEKEYSETs are preserved in the non-last snapshot stripe. + +transform in-use-key-ranges=(b-d) snapshots=(8) +a-c:{(#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#11,RANGEKEYDEL) (#4,RANGEKEYSET,@3,foo3)} + +transform +a-c:{(#13,RANGEKEYSET,@3,bar1) (#12,RANGEKEYSET,@2,bar2) (#11,RANGEKEYDEL) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#13,RANGEKEYSET,@3,bar1) (#12,RANGEKEYSET,@2,bar2)} + +# Test RANGEKEYUNSETs are preserved over in-use key ranges. + +transform +a-c:{(#11,RANGEKEYUNSET,@3) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{} + +transform in-use-key-ranges=(b-d) +a-c:{(#11,RANGEKEYUNSET,@3) (#8,RANGEKEYSET,@3,foo5) (#4,RANGEKEYSET,@3,foo3) (#3,RANGEKEYSET,@3,foo2)} +---- +a-c:{(#11,RANGEKEYUNSET,@3)} + +# Test cases where multiple keys have the same sequence number. + +transform +a-c:{(#11,RANGEKEYSET,@3,foo5) (#11,RANGEKEYUNSET,@4) (#11,RANGEKEYDEL)} +---- +a-c:{(#11,RANGEKEYSET,@3,foo5)} + +transform +a-c:{(#11,RANGEKEYSET,@3,foo5) (#11,RANGEKEYUNSET,@3) (#11,RANGEKEYDEL)} +---- +a-c:{(#11,RANGEKEYSET,@3,foo5)} + +# Test that UNSETs and DELs are retained over in-use key ranges. + +transform in-use-key-ranges=(b-d) +a-c:{(#11,RANGEKEYSET,@3,foo5) (#11,RANGEKEYUNSET,@4) (#11,RANGEKEYDEL)} +---- +a-c:{(#11,RANGEKEYSET,@3,foo5) (#11,RANGEKEYUNSET,@4) (#11,RANGEKEYDEL)} + +# Test that sets shadow unset at the same prefix, even if elision is disabled. + +transform in-use-key-ranges=(b-d) +a-c:{(#11,RANGEKEYSET,@3,foo5) (#11,RANGEKEYUNSET,@3) (#11,RANGEKEYDEL)} +---- +a-c:{(#11,RANGEKEYSET,@3,foo5) (#11,RANGEKEYDEL)} + +transform disable-elision +a-c:{(#11,RANGEKEYSET,@3,foo5) (#11,RANGEKEYUNSET,@3) (#11,RANGEKEYDEL) +---- +a-c:{(#11,RANGEKEYSET,@3,foo5) (#11,RANGEKEYDEL)} diff --git a/pebble/testdata/concurrent_excise b/pebble/testdata/concurrent_excise new file mode 100644 index 0000000..135566f --- /dev/null +++ b/pebble/testdata/concurrent_excise @@ -0,0 +1,176 @@ + +reset +---- + +switch 1 +---- +ok + +batch +set d foo +set e bar +---- + +flush +---- + +compact a-z +---- +ok + +switch 2 +---- +ok + +batch +set c fooz +set f foobar +---- + +flush +---- + +compact a-z +---- +ok + +batch +set d foobar +---- + +flush +---- + +lsm +---- +0.0: + 000007:[d#12,SET-d#12,SET] +6: + 000005:[c#10,SET-f#11,SET] + +compact a-z block=c1 +---- +spun off in separate goroutine + +iter +first +next +next +next +next +---- +c: (fooz, .) +d: (foobar, .) +f: (foobar, .) +. +. + +# This excise should cancel the in-flight compaction, causing it to error out +# below. The eventually file-only snapshot should go through because it's not +# waiting on any keys in memtables + +file-only-snapshot s1 + c e +---- +ok + +replicate 1 2 b e +---- +replicated 1 shared SSTs + +unblock c1 +---- +ok + +wait-for-file-only-snapshot s1 +---- +ok + +lsm +---- +6: + 000010:[d#13,SET-d#13,SET] + 000011:[f#11,SET-f#11,SET] + +compact a-z +---- +ok + +wait-for-background-error +---- +pebble: compaction cancelled by a concurrent operation, will retry compaction + +iter +first +next +next +next +next +---- +d: (foo, .) +f: (foobar, .) +. +. +. + +batch +set d fo +set ee foobar +set f3 something +---- + +flush +---- + +compact a-z +---- +ok + +switch 1 +---- +ok + +# The below file-only snapshot should be errored out by the concurrent excise. + +batch +set d something +---- + +flush +---- + +batch +set dd memory +---- + +file-only-snapshot s2 + c e +---- +ok + +iter snapshot=s2 +first +next +next +next +---- +d: (something, .) +dd: (memory, .) +e: (bar, .) +. + +replicate 2 1 c dd +---- +replicated 1 shared SSTs + +wait-for-file-only-snapshot s2 +---- +pebble: snapshot excised before conversion to file-only snapshot + +iter snapshot=s2 +first +next +next +next +---- +pebble: snapshot excised before conversion to file-only snapshot diff --git a/pebble/testdata/db-stage-1/000002.log b/pebble/testdata/db-stage-1/000002.log new file mode 100644 index 0000000000000000000000000000000000000000..4f472ae3ba1024e27358ef759958a74bc5f45d14 GIT binary patch literal 11 OcmZQz00CBJAOQdXFaQYv literal 0 HcmV?d00001 diff --git a/pebble/testdata/db-stage-1/CURRENT b/pebble/testdata/db-stage-1/CURRENT new file mode 100644 index 0000000..feda7d6 --- /dev/null +++ b/pebble/testdata/db-stage-1/CURRENT @@ -0,0 +1 @@ +MANIFEST-000000 diff --git a/pebble/testdata/db-stage-1/LOCK b/pebble/testdata/db-stage-1/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/pebble/testdata/db-stage-1/MANIFEST-000001 b/pebble/testdata/db-stage-1/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..7c30d3195493487fca7e3b34e950ac82be61225b GIT binary patch literal 52 zcmZ1|Uhbv9z{n_-lUkOVlai$8R9TW*o>`pgoS$2eSd>_jU&PGB!k}K^W6s9F$i&3V H%)$u(pWzK} literal 0 HcmV?d00001 diff --git a/pebble/testdata/db-stage-1/OPTIONS-000003 b/pebble/testdata/db-stage-1/OPTIONS-000003 new file mode 100644 index 0000000..370b7b8 --- /dev/null +++ b/pebble/testdata/db-stage-1/OPTIONS-000003 @@ -0,0 +1,48 @@ +[Version] + pebble_version=0.1 + +[Options] + bytes_per_sync=524288 + cache_size=8388608 + cleaner=delete + compaction_debt_concurrency=1073741824 + comparer=leveldb.BytewiseComparator + disable_wal=false + flush_delay_delete_range=0s + flush_delay_range_key=0s + flush_split_bytes=4194304 + format_major_version=13 + l0_compaction_concurrency=10 + l0_compaction_file_threshold=500 + l0_compaction_threshold=4 + l0_stop_writes_threshold=12 + lbase_max_bytes=67108864 + max_concurrent_compactions=1 + max_manifest_file_size=134217728 + max_open_files=1000 + mem_table_size=4194304 + mem_table_stop_writes_threshold=2 + min_deletion_rate=0 + merger=pebble.concatenate + read_compaction_rate=16000 + read_sampling_multiplier=16 + strict_wal_tail=true + table_cache_shards=10 + table_property_collectors=[] + validate_on_ingest=false + wal_dir= + wal_bytes_per_sync=0 + max_writer_concurrency=0 + force_writer_parallelism=false + secondary_cache_size_bytes=0 + create_on_shared=0 + +[Level "0"] + block_restart_interval=16 + block_size=4096 + block_size_threshold=90 + compression=Snappy + filter_policy=none + filter_type=table + index_block_size=4096 + target_file_size=2097152 diff --git a/pebble/testdata/db-stage-1/marker.format-version.000012.013 b/pebble/testdata/db-stage-1/marker.format-version.000012.013 new file mode 100644 index 0000000..e69de29 diff --git a/pebble/testdata/db-stage-1/marker.manifest.000001.MANIFEST-000001 b/pebble/testdata/db-stage-1/marker.manifest.000001.MANIFEST-000001 new file mode 100644 index 0000000..e69de29 diff --git a/pebble/testdata/db-stage-2/000002.log b/pebble/testdata/db-stage-2/000002.log new file mode 100644 index 0000000000000000000000000000000000000000..a253646b76f8f7b0dc17c312489ce57da8d08c40 GIT binary patch literal 170 zcmaFY{MTEQft87Yfq@H%L4Xm2nbY#~ne+2f`pgoS$2eSd>_jU&PGB!k}K^W6s9F$i&3V H%)$u(pWzK} literal 0 HcmV?d00001 diff --git a/pebble/testdata/db-stage-2/OPTIONS-000003 b/pebble/testdata/db-stage-2/OPTIONS-000003 new file mode 100644 index 0000000..370b7b8 --- /dev/null +++ b/pebble/testdata/db-stage-2/OPTIONS-000003 @@ -0,0 +1,48 @@ +[Version] + pebble_version=0.1 + +[Options] + bytes_per_sync=524288 + cache_size=8388608 + cleaner=delete + compaction_debt_concurrency=1073741824 + comparer=leveldb.BytewiseComparator + disable_wal=false + flush_delay_delete_range=0s + flush_delay_range_key=0s + flush_split_bytes=4194304 + format_major_version=13 + l0_compaction_concurrency=10 + l0_compaction_file_threshold=500 + l0_compaction_threshold=4 + l0_stop_writes_threshold=12 + lbase_max_bytes=67108864 + max_concurrent_compactions=1 + max_manifest_file_size=134217728 + max_open_files=1000 + mem_table_size=4194304 + mem_table_stop_writes_threshold=2 + min_deletion_rate=0 + merger=pebble.concatenate + read_compaction_rate=16000 + read_sampling_multiplier=16 + strict_wal_tail=true + table_cache_shards=10 + table_property_collectors=[] + validate_on_ingest=false + wal_dir= + wal_bytes_per_sync=0 + max_writer_concurrency=0 + force_writer_parallelism=false + secondary_cache_size_bytes=0 + create_on_shared=0 + +[Level "0"] + block_restart_interval=16 + block_size=4096 + block_size_threshold=90 + compression=Snappy + filter_policy=none + filter_type=table + index_block_size=4096 + target_file_size=2097152 diff --git a/pebble/testdata/db-stage-2/marker.format-version.000012.013 b/pebble/testdata/db-stage-2/marker.format-version.000012.013 new file mode 100644 index 0000000..e69de29 diff --git a/pebble/testdata/db-stage-2/marker.manifest.000001.MANIFEST-000001 b/pebble/testdata/db-stage-2/marker.manifest.000001.MANIFEST-000001 new file mode 100644 index 0000000..e69de29 diff --git a/pebble/testdata/db-stage-3/000004.sst b/pebble/testdata/db-stage-3/000004.sst new file mode 100644 index 0000000000000000000000000000000000000000..4daa79e302fc10646dd1022b5f8f5906f0eb0d8d GIT binary patch literal 709 zcmah{!EO^V5Vey)Hp$Y`1~`OE5L{!W6j4=CdH@N;g$pNy(8{|z*;w`3mObfa^AGue zp131!d<-AK0sa8&O-YYf@@n?Y_|3d$lL1M&B?qKgt=^ptjdw9FEdUvG7FyGLJ7}R- zHbgXh-hF%jfb?3$(VyKS=lc|Hr$5M3Y9LJ|Fv}}uv`{!fU8WAT0(J>Cb7BK+@~mTZ zcIonzrP2p1jl(<%u&)y876%nn<)I|%(VQc1Jh`pgoS$2eSd>_jU&PGB!k}K^W6s9F$i&3V H%)$u(pWzK} literal 0 HcmV?d00001 diff --git a/pebble/testdata/db-stage-3/MANIFEST-000006 b/pebble/testdata/db-stage-3/MANIFEST-000006 new file mode 100644 index 0000000000000000000000000000000000000000..d920a944374aa394d26aa553033d444fb7c8002d GIT binary patch literal 93 zcmbRD@xo*!21Z7yoYb<^oRlOzr^=Gl^338?=ltA)#G=HK{30eMW_A{aKYM)Jbr~3$ pSee;a_|h3zjCb7BK+@~mTZ zcIonzrP2p1jl(<%u&)y876%nn<)I|%(VQc1JhN11l>70|P%0g8(B4GpFU}v!rE~rJh|_`2(sz04xnvz*1ORTESeLSs}i2 XLaZRf1VJPP49rQ1RS-Ta8;}M7%cu`v literal 0 HcmV?d00001 diff --git a/pebble/testdata/db-stage-4/CURRENT b/pebble/testdata/db-stage-4/CURRENT new file mode 100644 index 0000000..feda7d6 --- /dev/null +++ b/pebble/testdata/db-stage-4/CURRENT @@ -0,0 +1 @@ +MANIFEST-000000 diff --git a/pebble/testdata/db-stage-4/LOCK b/pebble/testdata/db-stage-4/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/pebble/testdata/db-stage-4/MANIFEST-000001 b/pebble/testdata/db-stage-4/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..7c30d3195493487fca7e3b34e950ac82be61225b GIT binary patch literal 52 zcmZ1|Uhbv9z{n_-lUkOVlai$8R9TW*o>`pgoS$2eSd>_jU&PGB!k}K^W6s9F$i&3V H%)$u(pWzK} literal 0 HcmV?d00001 diff --git a/pebble/testdata/db-stage-4/MANIFEST-000006 b/pebble/testdata/db-stage-4/MANIFEST-000006 new file mode 100644 index 0000000000000000000000000000000000000000..309491de5d43870317cd4f73d89a69708fc20f2a GIT binary patch literal 93 zcmbRD@xo*!21Z7yoYb<^oRlOzr^=Gl^338?=ltA)#G=HK{30eMW_A_^neWwCbr~3$ pSee;a_|h3zj 1 +del-range d +---- + +scan +---- +#0,1:1 + +scan range-del +---- +-d:{(#1,RANGEDEL)} diff --git a/pebble/testdata/event_listener b/pebble/testdata/event_listener new file mode 100644 index 0000000..2922840 --- /dev/null +++ b/pebble/testdata/event_listener @@ -0,0 +1,462 @@ +open +---- +mkdir-all: db 0755 +mkdir-all: wal 0755 +open-dir: db +open-dir: wal +lock: db/LOCK +open-dir: db +open-dir: db +open: db/CURRENT +create: db/MANIFEST-000001 +sync: db/MANIFEST-000001 +remove: db/temporary.000001.dbtmp +create: db/temporary.000001.dbtmp +sync: db/temporary.000001.dbtmp +close: db/temporary.000001.dbtmp +rename: db/temporary.000001.dbtmp -> db/CURRENT +sync: db +[JOB 1] MANIFEST created 000001 +open-dir: db +sync: db/MANIFEST-000001 +create: wal/000002.log +sync: wal +[JOB 1] WAL created 000002 +create: db/marker.manifest.000001.MANIFEST-000001 +close: db/marker.manifest.000001.MANIFEST-000001 +sync: db +create: db/marker.format-version.000001.002 +close: db/marker.format-version.000001.002 +sync: db +upgraded to format version: 002 +remove: db/temporary.000000.dbtmp +create: db/temporary.000000.dbtmp +sync: db/temporary.000000.dbtmp +close: db/temporary.000000.dbtmp +rename: db/temporary.000000.dbtmp -> db/CURRENT +create: db/marker.format-version.000002.003 +close: db/marker.format-version.000002.003 +remove: db/marker.format-version.000001.002 +sync: db +upgraded to format version: 003 +create: db/marker.format-version.000003.004 +close: db/marker.format-version.000003.004 +remove: db/marker.format-version.000002.003 +sync: db +upgraded to format version: 004 +create: db/marker.format-version.000004.005 +close: db/marker.format-version.000004.005 +remove: db/marker.format-version.000003.004 +sync: db +upgraded to format version: 005 +create: db/marker.format-version.000005.006 +close: db/marker.format-version.000005.006 +remove: db/marker.format-version.000004.005 +sync: db +upgraded to format version: 006 +create: db/marker.format-version.000006.007 +close: db/marker.format-version.000006.007 +remove: db/marker.format-version.000005.006 +sync: db +upgraded to format version: 007 +create: db/marker.format-version.000007.008 +close: db/marker.format-version.000007.008 +remove: db/marker.format-version.000006.007 +sync: db +upgraded to format version: 008 +create: db/marker.format-version.000008.009 +close: db/marker.format-version.000008.009 +remove: db/marker.format-version.000007.008 +sync: db +upgraded to format version: 009 +create: db/marker.format-version.000009.010 +close: db/marker.format-version.000009.010 +remove: db/marker.format-version.000008.009 +sync: db +upgraded to format version: 010 +create: db/marker.format-version.000010.011 +close: db/marker.format-version.000010.011 +remove: db/marker.format-version.000009.010 +sync: db +upgraded to format version: 011 +create: db/marker.format-version.000011.012 +close: db/marker.format-version.000011.012 +remove: db/marker.format-version.000010.011 +sync: db +upgraded to format version: 012 +create: db/marker.format-version.000012.013 +close: db/marker.format-version.000012.013 +remove: db/marker.format-version.000011.012 +sync: db +upgraded to format version: 013 +create: db/marker.format-version.000013.014 +close: db/marker.format-version.000013.014 +remove: db/marker.format-version.000012.013 +sync: db +upgraded to format version: 014 +create: db/marker.format-version.000014.015 +close: db/marker.format-version.000014.015 +remove: db/marker.format-version.000013.014 +sync: db +upgraded to format version: 015 +create: db/marker.format-version.000015.016 +close: db/marker.format-version.000015.016 +remove: db/marker.format-version.000014.015 +sync: db +upgraded to format version: 016 +create: db/temporary.000003.dbtmp +sync: db/temporary.000003.dbtmp +close: db/temporary.000003.dbtmp +rename: db/temporary.000003.dbtmp -> db/OPTIONS-000003 +sync: db + +flush +---- +sync-data: wal/000002.log +sync-data: wal/000002.log +close: wal/000002.log +create: wal/000004.log +sync: wal +[JOB 4] WAL created 000004 +[JOB 5] flushing 1 memtable (100B) to L0 +create: db/000005.sst +[JOB 5] flushing: sstable created 000005 +sync-data: db/000005.sst +close: db/000005.sst +sync: db +create: db/MANIFEST-000006 +close: db/MANIFEST-000001 +sync: db/MANIFEST-000006 +create: db/marker.manifest.000002.MANIFEST-000006 +close: db/marker.manifest.000002.MANIFEST-000006 +remove: db/marker.manifest.000001.MANIFEST-000001 +sync: db +[JOB 5] MANIFEST created 000006 +[JOB 5] flushed 1 memtable (100B) to L0 [000005] (662B), in 1.0s (2.0s total), output rate 662B/s + +compact +---- +sync-data: wal/000004.log +sync-data: wal/000004.log +close: wal/000004.log +reuseForWrite: wal/000002.log -> wal/000007.log +sync: wal +[JOB 6] WAL created 000007 (recycled 000002) +[JOB 7] flushing 1 memtable (100B) to L0 +create: db/000008.sst +[JOB 7] flushing: sstable created 000008 +sync-data: db/000008.sst +close: db/000008.sst +sync: db +create: db/MANIFEST-000009 +close: db/MANIFEST-000006 +sync: db/MANIFEST-000009 +create: db/marker.manifest.000003.MANIFEST-000009 +close: db/marker.manifest.000003.MANIFEST-000009 +remove: db/marker.manifest.000002.MANIFEST-000006 +sync: db +[JOB 7] MANIFEST created 000009 +[JOB 7] flushed 1 memtable (100B) to L0 [000008] (662B), in 1.0s (2.0s total), output rate 662B/s +remove: db/MANIFEST-000001 +[JOB 7] MANIFEST deleted 000001 +[JOB 8] compacting(default) L0 [000005 000008] (1.3KB) Score=0.00 + L6 [] (0B) Score=0.00; OverlappingRatio: Single 0.00, Multi 0.00 +open: db/000005.sst +read-at(609, 53): db/000005.sst +read-at(572, 37): db/000005.sst +read-at(53, 519): db/000005.sst +read-at(26, 27): db/000005.sst +open: db/000005.sst +close: db/000005.sst +open: db/000008.sst +read-at(609, 53): db/000008.sst +read-at(572, 37): db/000008.sst +read-at(53, 519): db/000008.sst +read-at(26, 27): db/000008.sst +open: db/000008.sst +close: db/000008.sst +open: db/000005.sst +read-at(0, 26): db/000005.sst +open: db/000008.sst +read-at(0, 26): db/000008.sst +close: db/000008.sst +close: db/000005.sst +create: db/000010.sst +[JOB 8] compacting: sstable created 000010 +sync-data: db/000010.sst +close: db/000010.sst +sync: db +create: db/MANIFEST-000011 +close: db/MANIFEST-000009 +sync: db/MANIFEST-000011 +create: db/marker.manifest.000004.MANIFEST-000011 +close: db/marker.manifest.000004.MANIFEST-000011 +remove: db/marker.manifest.000003.MANIFEST-000009 +sync: db +[JOB 8] MANIFEST created 000011 +[JOB 8] compacted(default) L0 [000005 000008] (1.3KB) Score=0.00 + L6 [] (0B) Score=0.00 -> L6 [000010] (662B), in 1.0s (3.0s total), output rate 662B/s +close: db/000005.sst +close: db/000008.sst +remove: db/000005.sst +[JOB 8] sstable deleted 000005 +remove: db/000008.sst +[JOB 8] sstable deleted 000008 +remove: db/MANIFEST-000006 +[JOB 8] MANIFEST deleted 000006 + +disable-file-deletions +---- + +flush +---- +sync-data: wal/000007.log +sync-data: wal/000007.log +close: wal/000007.log +reuseForWrite: wal/000004.log -> wal/000012.log +sync: wal +[JOB 9] WAL created 000012 (recycled 000004) +[JOB 10] flushing 1 memtable (100B) to L0 +create: db/000013.sst +[JOB 10] flushing: sstable created 000013 +sync-data: db/000013.sst +close: db/000013.sst +sync: db +create: db/MANIFEST-000014 +close: db/MANIFEST-000011 +sync: db/MANIFEST-000014 +create: db/marker.manifest.000005.MANIFEST-000014 +close: db/marker.manifest.000005.MANIFEST-000014 +remove: db/marker.manifest.000004.MANIFEST-000011 +sync: db +[JOB 10] MANIFEST created 000014 +[JOB 10] flushed 1 memtable (100B) to L0 [000013] (662B), in 1.0s (2.0s total), output rate 662B/s + +enable-file-deletions +---- +remove: db/MANIFEST-000009 +[JOB 11] MANIFEST deleted 000009 + +ingest +---- +open: ext/0 +read-at(664, 53): ext/0 +read-at(627, 37): ext/0 +read-at(53, 574): ext/0 +read-at(26, 27): ext/0 +read-at(0, 26): ext/0 +close: ext/0 +link: ext/0 -> db/000015.sst +[JOB 12] ingesting: sstable created 000015 +sync: db +open: db/000013.sst +read-at(609, 53): db/000013.sst +read-at(572, 37): db/000013.sst +read-at(53, 519): db/000013.sst +read-at(26, 27): db/000013.sst +read-at(0, 26): db/000013.sst +create: db/MANIFEST-000016 +close: db/MANIFEST-000014 +sync: db/MANIFEST-000016 +create: db/marker.manifest.000006.MANIFEST-000016 +close: db/marker.manifest.000006.MANIFEST-000016 +remove: db/marker.manifest.000005.MANIFEST-000014 +sync: db +[JOB 12] MANIFEST created 000016 +remove: db/MANIFEST-000011 +[JOB 12] MANIFEST deleted 000011 +remove: ext/0 +[JOB 12] ingested L0:000015 (717B) + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 2 1.3KB 0B 0 | 0.40 | 81B | 1 717B | 0 0B | 3 1.9KB | 0B | 2 24.5 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 1 662B 0B 0 | - | 1.3KB | 0 0B | 0 0B | 1 662B | 1.3KB | 1 0.5 +total | 3 2.0KB 0B 0 | - | 825B | 1 717B | 0 0B | 4 3.4KB | 1.3KB | 3 4.2 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (27B) in: 48B written: 108B (125% overhead) +Flushes: 3 +Compactions: 1 estimated debt: 2.0KB in progress: 0 (0B) + default: 1 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 1 (256KB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 6 entries (1.1KB) hit rate: 11.1% +Table cache: 1 entries (800B) hit rate: 40.0% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 1 as flushable: 0 (0B in 0 tables) + +# Set up a scenario where the table to be ingested overlaps with the memtable. +# The table is ingested as a flushable. The flush metrics refect the flushed +# ingestion. + +ingest-flushable +---- +sync-data: wal/000012.log +open: ext/a +read-at(664, 53): ext/a +read-at(627, 37): ext/a +read-at(53, 574): ext/a +read-at(26, 27): ext/a +read-at(0, 26): ext/a +close: ext/a +open: ext/b +read-at(664, 53): ext/b +read-at(627, 37): ext/b +read-at(53, 574): ext/b +read-at(26, 27): ext/b +read-at(0, 26): ext/b +close: ext/b +link: ext/a -> db/000017.sst +[JOB 13] ingesting: sstable created 000017 +link: ext/b -> db/000018.sst +[JOB 13] ingesting: sstable created 000018 +sync: db +sync-data: wal/000012.log +close: wal/000012.log +reuseForWrite: wal/000007.log -> wal/000019.log +sync: wal +[JOB 14] WAL created 000019 (recycled 000007) +sync-data: wal/000019.log +sync-data: wal/000019.log +close: wal/000019.log +create: wal/000020.log +sync: wal +[JOB 15] WAL created 000020 +remove: ext/a +remove: ext/b +[JOB 13] ingested as flushable 000017 (717B), 000018 (717B) +sync-data: wal/000020.log +close: wal/000020.log +create: wal/000021.log +sync: wal +[JOB 16] WAL created 000021 +[JOB 17] flushing 1 memtable (100B) to L0 +create: db/000022.sst +[JOB 17] flushing: sstable created 000022 +sync-data: db/000022.sst +close: db/000022.sst +sync: db +sync: db/MANIFEST-000016 +[JOB 17] flushed 1 memtable (100B) to L0 [000022] (662B), in 1.0s (2.0s total), output rate 662B/s +[JOB 18] flushing 2 ingested tables +create: db/MANIFEST-000023 +close: db/MANIFEST-000016 +sync: db/MANIFEST-000023 +create: db/marker.manifest.000007.MANIFEST-000023 +close: db/marker.manifest.000007.MANIFEST-000023 +remove: db/marker.manifest.000006.MANIFEST-000016 +sync: db +[JOB 18] MANIFEST created 000023 +[JOB 18] flushed 2 ingested flushables L0:000017 (717B) + L6:000018 (717B) in 1.0s (2.0s total), output rate 1.4KB/s +remove: db/MANIFEST-000014 +[JOB 18] MANIFEST deleted 000014 +[JOB 19] flushing 1 memtable (100B) to L0 +sync: db/MANIFEST-000023 +[JOB 19] flush error: pebble: empty table + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 4 2.7KB 0B 0 | 0.80 | 81B | 2 1.4KB | 0 0B | 4 2.6KB | 0B | 4 32.7 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 2 1.3KB 0B 0 | - | 1.3KB | 1 717B | 0 0B | 1 662B | 1.3KB | 1 0.5 +total | 6 4.0KB 0B 0 | - | 2.2KB | 3 2.1KB | 0 0B | 5 5.4KB | 1.3KB | 5 2.5 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (29B) in: 82B written: 110B (34% overhead) +Flushes: 6 +Compactions: 1 estimated debt: 4.0KB in progress: 0 (0B) + default: 1 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (512KB) zombie: 1 (512KB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 12 entries (2.3KB) hit rate: 14.3% +Table cache: 1 entries (800B) hit rate: 50.0% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 1 as flushable: 1 (1.4KB in 2 tables) + +sstables +---- +0: + 13:[a-a] + 15:[a-a] + 22:[a-a] + 17:[a-a] +6: + 10:[a-a] + 18:[b-b] + +checkpoint +---- +mkdir-all: checkpoint 0755 +open-dir: +sync: +close: +open-dir: checkpoint +link: db/OPTIONS-000003 -> checkpoint/OPTIONS-000003 +open-dir: checkpoint +create: checkpoint/marker.format-version.000001.016 +sync-data: checkpoint/marker.format-version.000001.016 +close: checkpoint/marker.format-version.000001.016 +sync: checkpoint +close: checkpoint +link: db/000013.sst -> checkpoint/000013.sst +link: db/000015.sst -> checkpoint/000015.sst +link: db/000022.sst -> checkpoint/000022.sst +link: db/000017.sst -> checkpoint/000017.sst +link: db/000010.sst -> checkpoint/000010.sst +link: db/000018.sst -> checkpoint/000018.sst +open: db/MANIFEST-000023 +create: checkpoint/MANIFEST-000023 +sync-data: checkpoint/MANIFEST-000023 +close: checkpoint/MANIFEST-000023 +close: db/MANIFEST-000023 +open-dir: checkpoint +create: checkpoint/marker.manifest.000001.MANIFEST-000023 +sync-data: checkpoint/marker.manifest.000001.MANIFEST-000023 +close: checkpoint/marker.manifest.000001.MANIFEST-000023 +sync: checkpoint +close: checkpoint +open: wal/000021.log +create: checkpoint/000021.log +sync-data: checkpoint/000021.log +close: checkpoint/000021.log +close: wal/000021.log +sync: checkpoint +close: checkpoint + +enable-file-deletions +---- +pebble: file deletion disablement invariant violated + +close +---- +close: db +close: db/000013.sst +sync-data: wal/000021.log +close: wal/000021.log +close: db/MANIFEST-000023 +close: db +close: db +close: wal +close: db diff --git a/pebble/testdata/excise b/pebble/testdata/excise new file mode 100644 index 0000000..4102e1e --- /dev/null +++ b/pebble/testdata/excise @@ -0,0 +1,339 @@ + +build ext0 format=pebblev2 +set a 1 +set l 2 +---- + +ingest ext0 +---- + +lsm +---- +6: + 000004:[a#10,SET-l#10,SET] + + +batch +set d foo +set f bar +---- + +flush +---- + +lsm +---- +0.0: + 000006:[d#11,SET-f#12,SET] +6: + 000004:[a#10,SET-l#10,SET] + +excise c k +---- +would excise 2 files, use ingest-and-excise to excise. + deleted: L0 000006 + deleted: L6 000004 + added: L6 000007:[a#10,SET-a#10,SET] seqnums:[10-10] points:[a#10,SET-a#10,SET] + added: L6 000008:[l#10,SET-l#10,SET] seqnums:[10-10] points:[l#10,SET-l#10,SET] + + +excise a e +---- +would excise 2 files, use ingest-and-excise to excise. + deleted: L0 000006 + deleted: L6 000004 + added: L0 000009:[f#12,SET-f#12,SET] seqnums:[11-12] points:[f#12,SET-f#12,SET] + added: L6 000010:[l#10,SET-l#10,SET] seqnums:[10-10] points:[l#10,SET-l#10,SET] + +excise e z +---- +would excise 2 files, use ingest-and-excise to excise. + deleted: L0 000006 + deleted: L6 000004 + added: L0 000011:[d#11,SET-d#11,SET] seqnums:[11-12] points:[d#11,SET-d#11,SET] + added: L6 000012:[a#10,SET-a#10,SET] seqnums:[10-10] points:[a#10,SET-a#10,SET] + +excise f l +---- +would excise 2 files, use ingest-and-excise to excise. + deleted: L0 000006 + deleted: L6 000004 + added: L0 000013:[d#11,SET-d#11,SET] seqnums:[11-12] points:[d#11,SET-d#11,SET] + added: L6 000014:[a#10,SET-a#10,SET] seqnums:[10-10] points:[a#10,SET-a#10,SET] + added: L6 000015:[l#10,SET-l#10,SET] seqnums:[10-10] points:[l#10,SET-l#10,SET] + +excise f ll +---- +would excise 2 files, use ingest-and-excise to excise. + deleted: L0 000006 + deleted: L6 000004 + added: L0 000016:[d#11,SET-d#11,SET] seqnums:[11-12] points:[d#11,SET-d#11,SET] + added: L6 000017:[a#10,SET-a#10,SET] seqnums:[10-10] points:[a#10,SET-a#10,SET] + +excise p q +---- +would excise 0 files, use ingest-and-excise to excise. + +lsm +---- +0.0: + 000006:[d#11,SET-f#12,SET] +6: + 000004:[a#10,SET-l#10,SET] + +iter +first +next +next +next +next +---- +a: (1, .) +d: (foo, .) +f: (bar, .) +l: (2, .) +. + +build ext1 format=pebblev2 +set d foo3 +set e bar2 +---- + +ingest-and-excise ext1 excise=c-k +---- + +lsm +---- +6: + 000019:[a#10,SET-a#10,SET] + 000018:[d#13,SET-e#13,SET] + 000020:[l#10,SET-l#10,SET] + +iter +first +next +next +next +next +---- +a: (1, .) +d: (foo3, .) +e: (bar2, .) +l: (2, .) +. + +# More complex cases, with the truncation of file bounds happening at rangedel +# and rangekey bounds. + +reset +---- + +build ext3 format=pebblev2 +range-key-set c f @4 foobar +---- + +ingest ext3 +---- + +build ext4 format=pebblev2 +set b bar +del-range g i +---- + +ingest ext4 +---- + +lsm +---- +0.0: + 000005:[b#11,SET-i#inf,RANGEDEL] +6: + 000004:[c#10,RANGEKEYSET-f#inf,RANGEKEYSET] + +excise f g +---- +would excise 1 files, use ingest-and-excise to excise. + deleted: L0 000005 + added: L0 000006:[b#11,SET-b#11,SET] seqnums:[11-11] points:[b#11,SET-b#11,SET] + added: L0 000007:[g#11,RANGEDEL-i#inf,RANGEDEL] seqnums:[11-11] points:[g#11,RANGEDEL-i#inf,RANGEDEL] + +excise b c +---- +would excise 1 files, use ingest-and-excise to excise. + deleted: L0 000005 + added: L0 000008:[g#11,RANGEDEL-i#inf,RANGEDEL] seqnums:[11-11] points:[g#11,RANGEDEL-i#inf,RANGEDEL] + +excise i j +---- +would excise 0 files, use ingest-and-excise to excise. + +# Excise mid range key. This will not happen in practice, but excise() +# supports it. + +excise c d +---- +would excise 2 files, use ingest-and-excise to excise. + deleted: L0 000005 + deleted: L6 000004 + added: L0 000009:[b#11,SET-b#11,SET] seqnums:[11-11] points:[b#11,SET-b#11,SET] + added: L0 000010:[g#11,RANGEDEL-i#inf,RANGEDEL] seqnums:[11-11] points:[g#11,RANGEDEL-i#inf,RANGEDEL] + added: L6 000011:[d#10,RANGEKEYSET-f#inf,RANGEKEYSET] seqnums:[10-10] ranges:[d#10,RANGEKEYSET-f#inf,RANGEKEYSET] + +reset +---- + +# Create an sstable with a range key set. +batch +set a a +set b b +set d d +range-key-set e ee @1 foo +---- + +flush +---- + +lsm +---- +0.0: + 000005:[a#10,SET-ee#inf,RANGEKEYSET] + +build ext2 +set z z +---- + +ingest-and-excise ext2 excise=b-c +---- + +lsm +---- +0.0: + 000007:[a#10,SET-a#10,SET] + 000008:[d#12,SET-ee#inf,RANGEKEYSET] +6: + 000006:[z#14,SET-z#14,SET] + +# Regression test for https://github.com/cockroachdb/pebble/issues/2947. +reset +---- + +batch +set a a +set b b +set c c +set d d +set e e +set f f +set g g +set h h +set i i +set j j +---- + +flush +---- + +lsm +---- +0.0: + 000005:[a#10,SET-j#19,SET] + +build ext2 +set z z +---- + +ingest-and-excise ext2 excise=d-e +---- + +lsm +---- +0.0: + 000007:[a#10,SET-c#12,SET] + 000008:[e#14,SET-j#19,SET] +6: + 000006:[z#20,SET-z#20,SET] + +build ext3 +set zz zz +---- + +ingest-and-excise ext3 excise=g-h +---- + +# 7, 10, 11 should have the same file backing struct. +lsm +---- +0.0: + 000007:[a#10,SET-c#12,SET] + 000010:[e#14,SET-f#15,SET] + 000011:[h#17,SET-j#19,SET] +6: + 000006:[z#20,SET-z#20,SET] + 000009:[zz#21,SET-zz#21,SET] + +confirm-backing 7 10 11 +---- +file backings are the same + +reopen +---- + +# 7, 10, 11 should still have the same file backing struct even after manifest +# replay. +lsm +---- +0.0: + 000007:[a#10,SET-c#12,SET] + 000010:[e#14,SET-f#15,SET] + 000011:[h#17,SET-j#19,SET] +6: + 000006:[z#20,SET-z#20,SET] + 000009:[zz#21,SET-zz#21,SET] + +confirm-backing 7 10 11 +---- +file backings are the same + +# Excise one boundary, the file backing should still be set. +reset +---- + +batch +set a a +set b b +set c c +set d d +set e e +---- + +flush +---- + +lsm +---- +0.0: + 000005:[a#10,SET-e#14,SET] + +build ext2 +set z z +---- + +ingest-and-excise ext2 excise=d-f +---- + +lsm +---- +0.0: + 000007:[a#10,SET-c#12,SET] +6: + 000006:[z#15,SET-z#15,SET] + +reopen +---- + +lsm +---- +0.0: + 000007:[a#10,SET-c#12,SET] +6: + 000006:[z#15,SET-z#15,SET] diff --git a/pebble/testdata/external_iterator b/pebble/testdata/external_iterator new file mode 100644 index 0000000..589b950 --- /dev/null +++ b/pebble/testdata/external_iterator @@ -0,0 +1,286 @@ +build 1 +set b b +set c c +---- + +build 2 +del-range c z +---- + +# Test that a delete range in a more recent file shadows keys in an +# earlier file. + +iter files=(1) +first +next +next +---- +b: (b, .) +c: (c, .) +. + +iter files=(1) +seek-ge bb +next +---- +c: (c, .) +. + +iter files=(2, 1) fwd-only +first +next +---- +b: (b, .) +. + +build 3 +set a a +set f f +---- + +# Test including an even more recent file with point keys overlapping +# the rangedel. Since the point keys are assigned a higher sequence +# number, they should NOT be shadowed by the rangedel. + +iter files=(3, 2, 1) fwd-only +first +next +next +next +---- +a: (a, .) +b: (b, .) +f: (f, .) +. + +# Test including range keys, and merging the range key state across +# files. Range keys should be interleaved. + +build 4 +range-key-set a c @2 foo +range-key-set c e @3 bar +---- + +build 5 +range-key-del b d +---- + +iter files=(5, 4, 3, 2, 1) fwd-only +first +next +next +next +next +---- +a: (a, [a-b) @2=foo UPDATED) +b: (b, . UPDATED) +d: (., [d-e) @3=bar UPDATED) +f: (f, . UPDATED) +. + +# Test including range keys with empty spans and a merge in between. At no point +# should an empty span be returned. + +build 6 +merge bb ac +---- + +iter files=(6, 5, 4, 3, 2, 1) +seek-lt c +prev +next +next +---- +bb: (ac, .) +b: (b, .) +bb: (ac, .) +d: (., [d-e) @3=bar UPDATED) + +iter files=(6, 5, 4, 3, 2, 1) +seek-ge b +next +prev +prev +next +next +next +---- +b: (b, .) +bb: (ac, .) +b: (b, .) +a: (a, [a-b) @2=foo UPDATED) +b: (b, . UPDATED) +bb: (ac, .) +d: (., [d-e) @3=bar UPDATED) + +# Test range keys that overlap each other with identical state. These +# should be defragmented and exposed as a single range key. + +reset +---- + +build ag +range-key-set a g @5 foo +---- + +build ek +range-key-set e k @5 foo +---- + +iter files=(ag, ek) fwd-only +first +next +---- +a: (., [a-k) @5=foo UPDATED) +. + +# Test range-key masking by creating points, some with suffixes above +# the range key's suffix, some with suffixes below the range key's +# suffix. + +build points +set a@4 v +set c@2 v +set d@9 v +set e@5 v +set k@3 v +set p@4 v +---- + +iter files=(points, ag, ek) mask-suffix=@7 fwd-only +first +next +next +next +next +next +---- +a: (., [a-k) @5=foo UPDATED) +d@9: (v, [a-k) @5=foo) +e@5: (v, [a-k) @5=foo) +k@3: (v, . UPDATED) +p@4: (v, .) +. + +# Test that 'stacked' range keys (eg, multiple defined over the same keyspan at +# varying suffixes) work as expected. + +build stacked +range-key-set a k @4 bar +range-key-set a k @1 bax +---- + +iter files=(points, ag, ek, stacked) fwd-only +first +next +---- +a: (., [a-k) @5=foo, @4=bar, @1=bax UPDATED) +a@4: (v, [a-k) @5=foo, @4=bar, @1=bax) + +# Test mutating the external iterator's options through SetOptions. + +iter files=(points, ag, ek) fwd-only +set-options key-types=point +first +next +set-options lower=e upper=p +first +next +---- +. +a@4: (v, .) +c@2: (v, .) +. +e@5: (v, .) +k@3: (v, .) + +# Test the TrySeekUsingNext optimization that's enabled only for fwd-only +# external iterators. Seed the database with keys like 'a', 'aa', 'aaa', etc so +# that the index block uses a final separator that's beyond all the other live +# keys. + +reset +---- + +build a +set a@3 a@3 +set a@1 a@1 +---- + +build aa +set aa@3 aa@3 +set aa@1 aa@1 +---- + +build aaa +set aaa@3 aaa@3 +set aaa@1 aaa@1 +---- + +build aaaa +set aaaa@3 aaaa@3 +set aaaa@1 aaaa@1 +---- + +build aaaaa +set aaaaa@3 aaaaa@3 +set aaaaa@1 aaaaa@1 +---- + +# Note the absence of fwd-only. This iterator will not use the TrySeekUsingNext +# optimization. + +iter files=(a, aa, aaa, aaaa, aaaaa) +seek-ge a +next +seek-ge aa +next +seek-ge aaa +next +seek-ge aaaa +next +seek-ge aaaaa +next +stats +---- +a@3: (a@3, .) +a@1: (a@1, .) +aa@3: (aa@3, .) +aa@1: (aa@1, .) +aaa@3: (aaa@3, .) +aaa@1: (aaa@1, .) +aaaa@3: (aaaa@3, .) +aaaa@1: (aaaa@1, .) +aaaaa@3: (aaaaa@3, .) +aaaaa@1: (aaaaa@1, .) +stats: (interface (dir, seek, step): (fwd, 5, 5), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 5, 5), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 475B, cached 0B, read-time 0s)), (points: (count 10, key-bytes 50B, value-bytes 50B, tombstoned 0))) + +# Note the inclusion of fwd-only. This iterator will use the TrySeekUsingNext +# optimization and loads ~half the block-bytes as a result. + +iter files=(a, aa, aaa, aaaa, aaaaa) fwd-only +seek-ge a +next +seek-ge aa +next +seek-ge aaa +next +seek-ge aaaa +next +seek-ge aaaaa +next +stats +---- +a@3: (a@3, .) +a@1: (a@1, .) +aa@3: (aa@3, .) +aa@1: (aa@1, .) +aaa@3: (aaa@3, .) +aaa@1: (aaa@1, .) +aaaa@3: (aaaa@3, .) +aaaa@1: (aaaa@1, .) +aaaaa@3: (aaaaa@3, .) +aaaaa@1: (aaaaa@1, .) +stats: (interface (dir, seek, step): (fwd, 5, 5), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 5, 5), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 281B, cached 0B, read-time 0s)), (points: (count 10, key-bytes 50B, value-bytes 50B, tombstoned 0))) diff --git a/pebble/testdata/flushable_batch b/pebble/testdata/flushable_batch new file mode 100644 index 0000000..c8cbc4d --- /dev/null +++ b/pebble/testdata/flushable_batch @@ -0,0 +1,156 @@ +define +a.SET.1:1 +a.SET.2:2 +b.SET.1:1 +b.DEL.2: +c.SET.1:1 +c.MERGE.2:2 +1.RANGEDEL.3: +2.RANGEKEYSET.4: +1.RANGEKEYDEL.3: +2.RANGEKEYUNSET.3: +---- + +# NB: The range keys get fragmented. + +dump seq=0 +---- +a#1,1:2 +a#0,1:1 +b#3,0: +b#2,1:1 +c#5,2:2 +c#4,1:1 +1-3:{(#6,RANGEDEL)} +1-2:{(#8,RANGEKEYDEL)} +2-3:{(#9,RANGEKEYUNSET,3) (#8,RANGEKEYDEL) (#7,RANGEKEYSET,4,4)} +3-4:{(#7,RANGEKEYSET,4,4)} + +dump seq=100 +---- +a#101,1:2 +a#100,1:1 +b#103,0: +b#102,1:1 +c#105,2:2 +c#104,1:1 +1-3:{(#106,RANGEDEL)} +1-2:{(#108,RANGEKEYDEL)} +2-3:{(#109,RANGEKEYUNSET,3) (#108,RANGEKEYDEL) (#107,RANGEKEYSET,4,4)} +3-4:{(#107,RANGEKEYSET,4,4)} + +define +c.SET.1:1 +b.SET.1:1 +a.SET.1:1 +---- + +dump seq=1000 +---- +a#1002,1:1 +b#1001,1:1 +c#1000,1:1 + +iter +first +next +next +next +---- +a:1 +b:1 +c:1 +. + +iter +last +prev +prev +prev +---- +c:1 +b:1 +a:1 +. + +iter lower=b +seek-ge b +prev +---- +b:1 +. + +iter lower=c +seek-lt b +---- +. + +iter lower=e +last +---- +. + +iter upper=1 +first +---- +. + +iter upper=b +first +next +---- +a:1 +. + +iter upper=b +seek-ge b +---- +. + +iter +set-bounds lower=b +seek-ge b +prev +---- +b:1 +. + +iter +set-bounds lower=c +seek-lt b +---- +. + +iter +set-bounds lower=e +last +---- +. + +iter +set-bounds upper=1 +first +---- +. + +iter +set-bounds upper=b +first +next +---- +a:1 +. + +iter +set-bounds upper=b +seek-ge b +---- +. + +iter +first +set-bounds upper=b +seek-ge b +---- +a:1 +. diff --git a/pebble/testdata/flushable_ingest b/pebble/testdata/flushable_ingest new file mode 100644 index 0000000..ea20491 --- /dev/null +++ b/pebble/testdata/flushable_ingest @@ -0,0 +1,627 @@ +# We create multiple SSTs, one of which overlaps with the memtable (scheduling a flush). +# Check that the SSTs get ingested to the lowest levels possible. + +batch +set a 0 +---- + +# The SST below overlaps with memtable and thus should be placed in L0 +# post flush. +build ext1 +set a 1 +---- + +# The SST below overlaps with nothing and thus should be placed in L6 post +# flush. +build ext2 +set b 1 +---- + +# The SST below doesn't overlap with any SSTs in the LSM and thus can be placed +# in L6 post-flush. +build ext3 +set d 1 +---- + +# We block the flush, so the SSTs should still be in the flushable queue. +blockFlush +---- + +ingest ext1 ext2 ext3 +---- + +allowFlush +---- + +lsm +---- + +get +a +b +d +---- +a:1 +b:1 +d:1 + +# We expect 1 WAL for an immutable memtable, 1 file for the ingested ssts, +# one for the mutable memtable. We also expect 3 ssts corresponding to the +# ingested files. +ls +---- +000002.log +000004.sst +000005.sst +000006.sst +000007.log +000008.log +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +ext +marker.format-version.000015.016 +marker.manifest.000001.MANIFEST-000001 + +# Test basic WAL replay +close +---- + +# In this case only the flushable was holding a reference to the sstables. Even +# after the DB is closed, those sstables should still be hanging around. +ls +---- +000002.log +000004.sst +000005.sst +000006.sst +000007.log +000008.log +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +ext +marker.format-version.000015.016 +marker.manifest.000001.MANIFEST-000001 + +open +---- + +# Make sure that the sstables got flushed in the correct order on a WAL replay. +lsm +---- +0.1: + 000004:[a#11,SET-a#11,SET] +0.0: + 000009:[a#10,SET-a#10,SET] + 000005:[b#12,SET-b#12,SET] + 000006:[d#13,SET-d#13,SET] + +get +a +b +d +---- +a:1 +b:1 +d:1 + +reset +---- + +# Repeat the steps above without closing Pebble. Note that the final lsm state +# will be different because WAL replay just placed the files in L0. +batch +set a 0 +---- + +build ext1 +set a 1 +---- + +build ext2 +set b 1 +---- + +build ext3 +set d 1 +---- + +ingest ext1 ext2 ext3 +---- + +lsm +---- +0.1: + 000004:[a#11,SET-a#11,SET] +0.0: + 000009:[a#10,SET-a#10,SET] +6: + 000005:[b#12,SET-b#12,SET] + 000006:[d#13,SET-d#13,SET] + +reset +---- + +# Test multiple overlapping ingests interleaving batch sets, and then flushing. +batch +set a 0 +---- + +build ext4 +set a 1 +---- + +build ext5 +set a 2 +---- + +blockFlush +---- + +ingest ext4 +---- + +allowFlush +---- + +get +a +---- +a:1 + +batch +set b 1 +---- + +get +a +b +---- +a:1 +b:1 + +# Should get ingested into L0 above the memtable flush. +blockFlush +---- + +ingest ext5 +---- + +allowFlush +---- + +get +a +b +---- +a:2 +b:1 + +batch +set c 1 +---- + +flush +---- + +lsm +---- +0.2: + 000007:[a#13,SET-a#13,SET] +0.1: + 000004:[a#11,SET-a#11,SET] +0.0: + 000010:[a#10,SET-a#10,SET] + 000011:[b#12,SET-b#12,SET] + +# Value of a should be the value of a in the second ingested SST. +get +a +b +c +---- +a:2 +b:1 +c:1 + +# Test that non-overlapping ingest still works normally. +reset +---- + +batch +set a 0 +---- + +build ext1 +set b 1 +---- + +build ext2 +set d 1 +---- + +ingest ext1 ext2 +---- + +lsm +---- +6: + 000004:[b#11,SET-b#11,SET] + 000005:[d#12,SET-d#12,SET] + + +# Verify target level of ingestedFlushable. +reset +---- + +batch +set a 0 +---- + +build ext1 +set a 1 +---- + +build ext2 +set b 1 +---- + +build ext3 +set d 1 +---- + +ingest ext1 ext2 ext3 +---- + +lsm +---- +0.1: + 000004:[a#11,SET-a#11,SET] +0.0: + 000009:[a#10,SET-a#10,SET] +6: + 000005:[b#12,SET-b#12,SET] + 000006:[d#13,SET-d#13,SET] + + +batch +set a 3 +---- + +build ext4 +set a 4 +---- + +build ext5 +set b 5 +---- + +ingest ext4 ext5 +---- + +# Looking for the sstable with the key a to go into 0.3, and the sstable with +# key b to go into 0.0. The sstable doesn't go into L5, because L5 isn't open +# yet. +lsm +---- +0.3: + 000010:[a#15,SET-a#15,SET] +0.2: + 000014:[a#14,SET-a#14,SET] +0.1: + 000004:[a#11,SET-a#11,SET] +0.0: + 000009:[a#10,SET-a#10,SET] + 000011:[b#16,SET-b#16,SET] +6: + 000005:[b#12,SET-b#12,SET] + 000006:[d#13,SET-d#13,SET] + +# Testing whether the new mutable memtable with data is flushed correctly during +# WAL replay. +reset +---- + +batch +set a 0 +---- + +# The SST below overlaps with memtable and thus should be placed in L0 +# post flush. +build ext1 +set a 1 +---- + +# The SST below overlaps with nothing and thus should be placed in L6 post +# flush. +build ext2 +set b 1 +---- + +# The SST below doesn't overlap with any SSTs in the LSM and thus can be placed +# in L6 post-flush. +build ext3 +set d 1 +---- + +# We block the flush, so the SSTs should still be in the flushable queue. +blockFlush +---- + +ingest ext1 ext2 ext3 +---- + +# Add another write which should go to the new mutable memtable. +batch +set f 1 +---- + +allowFlush +---- + +lsm +---- + +get +a +b +d +f +---- +a:1 +b:1 +d:1 +f:1 + +# We expect 1 WAL for an immutable memtable, 1 file for the ingested ssts, +# one for the mutable memtable. We also expect 3 ssts corresponding to the +# ingested files. +ls +---- +000002.log +000004.sst +000005.sst +000006.sst +000007.log +000008.log +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +ext +marker.format-version.000015.016 +marker.manifest.000001.MANIFEST-000001 + +close +---- + +# In this case only the memtable was holding a reference to the sstables. Even +# after the DB is closed, those memtables should still be hanging around. +ls +---- +000002.log +000004.sst +000005.sst +000006.sst +000007.log +000008.log +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +ext +marker.format-version.000015.016 +marker.manifest.000001.MANIFEST-000001 + +open +---- + +# Make sure that the sstables got flushed in the correct order on a WAL replay. +lsm +---- +0.1: + 000004:[a#11,SET-a#11,SET] +0.0: + 000009:[a#10,SET-a#10,SET] + 000005:[b#12,SET-b#12,SET] + 000006:[d#13,SET-d#13,SET] + 000010:[f#14,SET-f#14,SET] + +# Check if the new mutable memtable is using a new log file, and that the +# previous log files have been deleted appropriately after the flush. +ls +---- +000004.sst +000005.sst +000006.sst +000009.sst +000010.sst +000011.log +CURRENT +LOCK +MANIFEST-000001 +MANIFEST-000012 +OPTIONS-000013 +ext +marker.format-version.000015.016 +marker.manifest.000002.MANIFEST-000012 + +# Make sure that the new mutable memtable can accept writes. +batch +set h 2 +---- + +get +h +---- +h:2 + +# Test correct WAL replay with read only mode. We essentially want to make sure +# that once a flushable is added to the queue, we create a new mutable memtable +# on top of the flushable. Otherwise, we can invert sequence number invariants. +reset +---- + +batch +set a 0 +---- + +# The SST below overlaps with memtable and thus should be placed in L0 +# post flush. +build ext1 +set a 1 +---- + +# The SST below overlaps with nothing and thus should be placed in L6 post +# flush. +build ext2 +set b 1 +---- + +# The SST below doesn't overlap with any SSTs in the LSM and thus can be placed +# in L6 post-flush. +build ext3 +set d 1 +---- + +# We block the flush, so the SSTs should still be in the flushable queue. +blockFlush +---- + +ingest ext1 ext2 ext3 +---- + +# Add another write which should go to the new mutable memtable. +batch +set a 3 +---- + +allowFlush +---- + +lsm +---- + +get +a +b +d +---- +a:3 +b:1 +d:1 + +close +---- + +open readOnly +---- + +get +a +b +d +---- +a:3 +b:1 +d:1 + +# Test with StrictFS +reset strictMem +---- + +batch +set a 1 +set b 1 +---- + +build ext1 +set a 2 +set b 2 +---- + +blockFlush +---- + +ingest ext1 +---- + +get +a +b +---- +a:2 +b:2 + +ignoreSyncs true +---- + +lsm +---- + +allowFlush +---- + +flush +---- + +# The ingested file is placed above the sstable generated by memtable flush. The +# ingested file has a lower file number, but a higher sequence number as +# expected. +lsm +---- +0.1: + 000004:[a#12,SET-b#12,SET] +0.0: + 000007:[a#10,SET-b#11,SET] + +ls +---- +000002.log +000004.sst +000005.log +000006.log +000007.sst +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +ext +marker.format-version.000015.016 +marker.manifest.000001.MANIFEST-000001 + +close +---- + +# At this point, the changes to the manifest should be lost. Note that 7.sst +# is gone because that file was never synced. +resetToSynced +---- +000002.log +000004.sst +000005.log +000006.log +CURRENT +LOCK +MANIFEST-000001 +OPTIONS-000003 +ext +ext1 +marker.format-version.000015.016 +marker.manifest.000001.MANIFEST-000001 + +ignoreSyncs false +---- + +open +---- + +lsm +---- +0.1: + 000004:[a#12,SET-b#12,SET] +0.0: + 000007:[a#10,SET-b#11,SET] diff --git a/pebble/testdata/format_major_version_pebblev1_migration b/pebble/testdata/format_major_version_pebblev1_migration new file mode 100644 index 0000000..c579a53 --- /dev/null +++ b/pebble/testdata/format_major_version_pebblev1_migration @@ -0,0 +1,170 @@ +# Open the DB at one version prior to the version that enforces Pebblev1 tables. +open version=8 +---- + +format-major-version +---- +008 + +# Confirm the allowable range of table formats. + +min-table-format +---- +(LevelDB) + +max-table-format +---- +(Pebble,v2) + +# Disable automatic compactions while we create the tables. + +disable-automatic-compactions true +---- + +# Create and flush a table. The table is created at the max format version for +# this DB version (i.e. Pebblev2). + +batch +set a a +---- + +flush +---- + +# Ingest an external table written at the max table format for the current +# version (i.e. Pebblev2). + +ingest a format=pebblev2 +set pebblev2 pebblev2 +---- + +# Ingest some external table written at earlier versions (i.e. Pebblev1, +# RocksDBv2, LevelDB). + +ingest b format=pebblev1 +set pebblev1 pebblev1 +---- + +ingest c format=rocksdbv2 +set rocksdbv2 rockdbv2 +---- + +ingest d format=leveldb +set leveldb leveldb +---- + +lsm +---- +0.0: + 000005:[a#10,SET-a#10,SET] +6: + 000009:[leveldb#14,SET-leveldb#14,SET] + 000007:[pebblev1#12,SET-pebblev1#12,SET] + 000006:[pebblev2#11,SET-pebblev2#11,SET] + 000008:[rocksdbv2#13,SET-rocksdbv2#13,SET] + +tally-table-formats +---- +(LevelDB): 1 +(RocksDB,v2): 1 +(Pebble,v1): 1 +(Pebble,v2): 2 +(Pebble,v3): 0 +(Pebble,v4): 0 + +# Upgrade the DB to FormatMinTableFormatPebblev1. + +ratchet-format-major-version 009 +---- + +format-major-version +---- +009 + +# The min table format version has been raised to Pebblev1. + +min-table-format +---- +(Pebble,v1) + +max-table-format +---- +(Pebble,v2) + +# Ingesting a table with a format prior to this version fails. + +ingest e format=rocksdbv2 +set rocksdbv2 rockdbv2 +---- +pebble: table format (RocksDB,v2) is not within range supported at DB format major version 9, ((Pebble,v1),(Pebble,v2)) + +# Upgrade the DB to FormatPrePebblev1Marked. The marked count increases to the +# count of tables at versions pre-Pebblev1 (i.e. two tables). + +ratchet-format-major-version 010 +---- + +format-major-version +---- +010 + +min-table-format +---- +(Pebble,v1) + +max-table-format +---- +(Pebble,v2) + +marked-file-count +---- +2 files marked for compaction + +# Upgrade the DB to FormatPrePebblev1MarkedCompacted. The marked count returns +# to zero. + +disable-automatic-compactions false +---- + +ratchet-format-major-version 014 +---- + +format-major-version +---- +014 + +min-table-format +---- +(Pebble,v1) + +max-table-format +---- +(Pebble,v3) + +marked-file-count +---- +0 files marked for compaction + +# The two tables with older table formats were rewritten with newer table format +# versions (note updated table numbers for the leveldb and rocksdb2 tables). + +lsm +---- +0.0: + 000005:[a#10,SET-a#10,SET] +6: + 000013:[leveldb#0,SET-leveldb#0,SET] + 000007:[pebblev1#12,SET-pebblev1#12,SET] + 000006:[pebblev2#11,SET-pebblev2#11,SET] + 000012:[rocksdbv2#0,SET-rocksdbv2#0,SET] + +# Confirm all tables are at least the minimum supported table format version. + +tally-table-formats +---- +(LevelDB): 0 +(RocksDB,v2): 0 +(Pebble,v1): 1 +(Pebble,v2): 4 +(Pebble,v3): 0 +(Pebble,v4): 0 diff --git a/pebble/testdata/format_major_version_split_user_key_migration b/pebble/testdata/format_major_version_split_user_key_migration new file mode 100644 index 0000000..735e7c0 --- /dev/null +++ b/pebble/testdata/format_major_version_split_user_key_migration @@ -0,0 +1,148 @@ +define +L1 +d.SET.110:d e.SET.140:e +---- +1: + 000004:[d#110,SET-e#140,SET] seqnums:[110-140] points:[d#110,SET-e#140,SET] + +reopen +---- +OK + +# The current public Pebble interface offers no way of constructing a multi-file +# atomic compaction unit, so use the force-ingest command to force an ingestion +# into L1. + +build cd +set c c +set d d +---- + +force-ingest paths=(cd) level=1 +---- +1: + 000008:[c#141,SET-d#141,SET] seqnums:[141-141] points:[c#141,SET-d#141,SET] + 000004:[d#110,SET-e#140,SET] seqnums:[110-140] points:[d#110,SET-e#140,SET] + +format-major-version +---- +005 + +marked-file-count +---- +0 files marked for compaction + +ratchet-format-major-version 006 +---- + +format-major-version +---- +006 + +# Upgrading to format major version 006 should've marked files for compaction. + +marked-file-count +---- +2 files marked for compaction + +reopen +---- +OK + +format-major-version +---- +006 + +# Ensure the files previously marked for compaction are still marked for +# compaction. + +marked-file-count +---- +2 files marked for compaction + +disable-automatic-compactions false +---- + +# Ratcheting to 007 should force compaction of any files still marked for +# compaction. + +ratchet-format-major-version 007 +---- +[JOB 100] compacted(rewrite) L1 [000008 000004] (1.3KB) Score=0.00 + L1 [] (0B) Score=0.00 -> L1 [000013] (649B), in 1.0s (2.0s total), output rate 649B/s + +format-major-version +---- +007 + +lsm +---- +1: + 000013:[c#0,SET-e#0,SET] + +# Reset to a new LSM. + +define +L1 +b.SET.0:b c.SET.5:c +L1 +m.SET.0:m l.SET.5:l +L1 +x.SET.0:x y.SET.5:y +---- +1: + 000004:[b#0,SET-c#5,SET] seqnums:[0-5] points:[b#0,SET-c#5,SET] + 000005:[l#5,SET-m#0,SET] seqnums:[0-5] points:[l#5,SET-m#0,SET] + 000006:[x#0,SET-y#5,SET] seqnums:[0-5] points:[x#0,SET-y#5,SET] + +build ab +set a a +set b b +---- + +build wx +set w w +set x x +---- + +force-ingest paths=(ab, wx) level=1 +---- +1: + 000007:[a#10,SET-b#10,SET] seqnums:[10-10] points:[a#10,SET-b#10,SET] + 000004:[b#0,SET-c#5,SET] seqnums:[0-5] points:[b#0,SET-c#5,SET] + 000005:[l#5,SET-m#0,SET] seqnums:[0-5] points:[l#5,SET-m#0,SET] + 000008:[w#11,SET-x#11,SET] seqnums:[11-11] points:[w#11,SET-x#11,SET] + 000006:[x#0,SET-y#5,SET] seqnums:[0-5] points:[x#0,SET-y#5,SET] + +format-major-version +---- +005 + +ratchet-format-major-version 006 +---- + +format-major-version +---- +006 + +marked-file-count +---- +4 files marked for compaction + +disable-automatic-compactions false +---- + +ratchet-format-major-version 007 +---- +[JOB 100] compacted(rewrite) L1 [000007 000004] (1.3KB) Score=0.00 + L1 [] (0B) Score=0.00 -> L1 [000010] (649B), in 1.0s (2.0s total), output rate 649B/s +[JOB 100] compacted(rewrite) L1 [000008 000006] (1.3KB) Score=0.00 + L1 [] (0B) Score=0.00 -> L1 [000011] (649B), in 1.0s (2.0s total), output rate 649B/s + +lsm +---- +1: + 000010:[a#0,SET-c#0,SET] + 000005:[l#5,SET-m#0,SET] + 000011:[w#0,SET-y#0,SET] + +format-major-version +---- +007 diff --git a/pebble/testdata/frontiers b/pebble/testdata/frontiers new file mode 100644 index 0000000..313f693 --- /dev/null +++ b/pebble/testdata/frontiers @@ -0,0 +1,71 @@ +# NB: The empty line in 'init' configures a frontier with no keys. It should +# never be added to the heap. + +init +b e j +a p n z + +f +---- + +scan +a b c d e f g h j i k l m n o p q r s t u v w x y z +---- +a : { b: "b", p: "p", f: "f" } +b : { e: "e", p: "p", f: "f" } +c : { e: "e", p: "p", f: "f" } +d : { e: "e", p: "p", f: "f" } +e : { f: "f", p: "p", j: "j" } +f : { j: "j", p: "p" } +g : { j: "j", p: "p" } +h : { j: "j", p: "p" } +j : { p: "p" } +i : { p: "p" } +k : { p: "p" } +l : { p: "p" } +m : { p: "p" } +n : { p: "p" } +o : { p: "p" } +p : { z: "z" } +q : { z: "z" } +r : { z: "z" } +s : { z: "z" } +t : { z: "z" } +u : { z: "z" } +v : { z: "z" } +w : { z: "z" } +x : { z: "z" } +y : { z: "z" } +z : { } + +scan +z +---- +z : { } + +scan +a z +---- +a : { b: "b", p: "p", f: "f" } +z : { } + +scan +e +---- +e : { f: "f", p: "p", j: "j" } + +# Test duplicate user keys within a frontier and across individual frontiers. + +init +b e e g +c e z +---- + +scan +a c d f z +---- +a : { b: "b", c: "c" } +c : { e: "e", e: "e" } +d : { e: "e", e: "e" } +f : { g: "g", z: "z" } +z : { } diff --git a/pebble/testdata/indexed_batch_mutation b/pebble/testdata/indexed_batch_mutation new file mode 100644 index 0000000..c3e60ad --- /dev/null +++ b/pebble/testdata/indexed_batch_mutation @@ -0,0 +1,759 @@ +# Set a key within the indexed batch. +new-batch +set foo foo +---- + +# Construct an iterator over the indexed batch. + +new-batch-iter i0 +---- + +# The key we set should be visible. + +iter iter=i0 +first +next +---- +foo: (foo, .) +. + +# Same behavior with a batch-only iterator. +new-batch-only-iter i-bo0 +---- + +iter iter=i-bo0 +first +next +---- +foo: (foo, .) +. + +# Set a new key, while the above iterator is still open. + +mutate +set bar bar +---- + +# The new key should be invisible. + +iter iter=i0 +prev +next +---- +foo: (foo, .) +. + +# Same behavior with a batch-only iterator. +iter iter=i-bo0 +prev +next +---- +foo: (foo, .) +. + +# A set-options operation should refresh the Iterator's view of the batch. The +# bar key should now be visibile. + +iter iter=i0 +set-options +first +next +next +---- +. +bar: (bar, .) +foo: (foo, .) +. + +# Same behavior with a batch-only iterator. +iter iter=i-bo0 +set-options +first +next +next +---- +. +bar: (bar, .) +foo: (foo, .) +. + +# Delete foo with a range deletion. + +mutate +del-range f g +---- + +# Both keys should still be visible. + +iter iter=i0 +prev +prev +---- +foo: (foo, .) +bar: (bar, .) + +# Same behavior with a batch-only iterator. +iter iter=i-bo0 +prev +prev +---- +foo: (foo, .) +bar: (bar, .) + +# After refreshing the iterator's view of the batch, foo should be deleted. + +iter iter=i0 +set-options +seek-ge foo +seek-lt foo +---- +. +. +bar: (bar, .) + +# Same behavior with a batch-only iterator. +iter iter=i-bo0 +set-options +seek-ge foo +seek-lt foo +---- +. +. +bar: (bar, .) + +# Write a range key set and a point key. + +mutate +range-key-set a c @1 boop +set b b +---- + +# The mutations should not be visible. + +iter iter=i0 +prev +next +---- +. +bar: (bar, .) + +# Same behavior with a batch-only iterator. +iter iter=i-bo0 +prev +next +---- +. +bar: (bar, .) + +# But refreshing the batch through a call to SetOptions should surface them. + +iter iter=i0 +set-options +first +next +next +---- +. +a: (., [a-c) @1=boop UPDATED) +b: (b, [a-c) @1=boop) +bar: (bar, [a-c) @1=boop) + +# Same behavior with a batch-only iterator. +iter iter=i-bo0 +set-options +first +next +next +---- +. +a: (., [a-c) @1=boop UPDATED) +b: (b, [a-c) @1=boop) +bar: (bar, [a-c) @1=boop) + +# Remove part of the range key to fragment it. + +mutate +range-key-del ace arc +---- + +iter iter=i0 +next +prev +prev +prev +prev +---- +. +bar: (bar, [a-c) @1=boop UPDATED) +b: (b, [a-c) @1=boop) +a: (., [a-c) @1=boop) +. + +# Same behavior with a batch-only iterator. +iter iter=i-bo0 +next +prev +prev +prev +prev +---- +. +bar: (bar, [a-c) @1=boop UPDATED) +b: (b, [a-c) @1=boop) +a: (., [a-c) @1=boop) +. + +iter iter=i0 +set-options +first +next +next +next +---- +. +a: (., [a-ace) @1=boop UPDATED) +arc: (., [arc-c) @1=boop UPDATED) +b: (b, [arc-c) @1=boop) +bar: (bar, [arc-c) @1=boop) + +# Same behavior with a batch-only iterator. +iter iter=i-bo0 +set-options +first +next +next +next +---- +. +a: (., [a-ace) @1=boop UPDATED) +arc: (., [arc-c) @1=boop UPDATED) +b: (b, [arc-c) @1=boop) +bar: (bar, [arc-c) @1=boop) + +# Create a new indexed batch and a new iterator over it. + +new-batch +set foo foo +---- + +new-batch-iter i1 +---- + +iter iter=i1 +first +next +---- +foo: (foo, .) +. + +# Test interactions with cloned iterators. +# First, apply mutations to the batch. They should remain invisible. + +mutate +set bar bar +range-key-set a z @1 boop +del-range f g +---- + +iter iter=i1 +first +next +---- +foo: (foo, .) +. + +# Clone i1 to create i2. + +clone from=i1 to=i2 refresh-batch=false +---- + +# i1 unchanged. + +iter iter=i1 +first +next +---- +foo: (foo, .) +. + +# i2 sees exactly the same stale state as i1 until SetOptions is called to +# explicitly refresh the view of the underlying batch. + +iter iter=i2 +first +next +set-options +first +next +next +---- +foo: (foo, .) +. +. +a: (., [a-z) @1=boop UPDATED) +bar: (bar, [a-z) @1=boop) +. + +# Clone i1 to create i3, this time passing RefreshBatchView: true. This clone +# should view the updated view of the underlying batch. +clone from=i1 to=i3 refresh-batch=true +---- + +iter iter=i3 +first +next +---- +a: (., [a-z) @1=boop UPDATED) +bar: (bar, [a-z) @1=boop) + +# i1 should still have the old, stale view of the batch. + +iter iter=i1 +first +next +---- +foo: (foo, .) +. + +# Mutate the underlying batch again. + +mutate +set foo foo +range-key-set a z @2 bax +del-range b c +---- + +# The new mutations should be invisible until SetOptions is called. + +iter iter=i1 +first +next +set-options +first +next +next +---- +foo: (foo, .) +. +. +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. + +iter iter=i2 +first +next +next +set-options +first +next +next +---- +a: (., [a-z) @1=boop UPDATED) +bar: (bar, [a-z) @1=boop) +. +. +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. + +# Commit a separate batch to the underlying engine. +batch +range-key-set a z @5 poi +set apple apple +---- + +# The writes to the underlying engine should be invisible. + +iter iter=i1 +first +next +next +---- +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. + +# Clone i1 to create i4. + +clone from=i1 to=i4 refresh-batch=false +---- + +iter iter=i4 +first +next +next +---- +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. + +# Refresh i4's view of its batch. It should still not see the newly committed +# writes. + +iter iter=i4 +set-options +first +next +next +---- +. +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. + +# Create a new iterator i5 over the indexed batch [not a Clone]. It should see +# all committed writes and uncommitted writes. + +new-batch-iter i5 +---- + +iter iter=i5 +first +next +next +next +---- +a: (., [a-z) @5=poi, @2=bax, @1=boop UPDATED) +apple: (apple, [a-z) @5=poi, @2=bax, @1=boop) +foo: (foo, [a-z) @5=poi, @2=bax, @1=boop) +. + +# The batch-only iter only sees the contents of the batch. +new-batch-only-iter i-bo1 +---- + +iter iter=i-bo1 +first +next +next +---- +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. + +# Mutate all the open iterators' underlying batch. + +mutate +range-key-set a z @6 yaya +set c c +---- + +# The iterators should still not see the committed writes, even after refreshing +# to observe more recent batch writes. + +iter iter=i1 +first +next +next +---- +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. + +iter iter=i4 +first +next +next +set-options +first +next +next +---- +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. +. +a: (., [a-z) @6=yaya, @2=bax, @1=boop UPDATED) +c: (c, [a-z) @6=yaya, @2=bax, @1=boop) +foo: (foo, [a-z) @6=yaya, @2=bax, @1=boop) + + +# The batch-only iter sees the more recent batch writes after refreshing. +iter iter=i-bo1 +first +next +next +set-options +first +next +next +next +---- +a: (., [a-z) @2=bax, @1=boop UPDATED) +foo: (foo, [a-z) @2=bax, @1=boop) +. +. +a: (., [a-z) @6=yaya, @2=bax, @1=boop UPDATED) +c: (c, [a-z) @6=yaya, @2=bax, @1=boop) +foo: (foo, [a-z) @6=yaya, @2=bax, @1=boop) +. + +# Test a scenario where constructing an Iterator should NOT use the cached +# fragmented tombstones / range keys, because the new Iterator is a Clone which +# must read at an earlier batch sequence number. + +# Reset and start a new batch. + +reset +---- + +new-batch +set foo foo +---- + +new-batch-iter i1 +---- + +iter iter=i1 +first +next +---- +foo: (foo, .) +. + +# Apply a range deletion and a range key. + +mutate +del-range a z +range-key-set a z @1 foo +---- + +# Create a new iterator which will see both the range deletion and the range +# key, and cache both on the batch so that future iterators constructed over the +# batch do not need to. + +new-batch-iter i2 +---- + +iter iter=i2 +first +next +---- +a: (., [a-z) @1=foo UPDATED) +. + +# Clone the original iterator from before the delete range and the range key +# were created. It should not use the cached fragments of range deletions or +# range keys, and should not see the effects of either. + +clone from=i1 to=i3 refresh-batch=false +---- + +iter iter=i3 +first +next +---- +foo: (foo, .) +. + +reset +---- + +new-batch +range-key-set a c @1 poi +range-key-set b d @2 yaya +---- + +new-batch-iter i1 +---- + +# The batch contains 2 range keys, but the skiplist of fragmented range keys +# contains 3 elements (a-b, b-c, c-d). + +iter iter=i1 +first +next +next +---- +a: (., [a-b) @1=poi UPDATED) +b: (., [b-c) @2=yaya, @1=poi UPDATED) +c: (., [c-d) @2=yaya UPDATED) + +# Add a new range key to the batch. The batch contains 3 internal range keys, +# and the skiplist of fragmented range keys contains 3 elements. + +mutate +range-key-set e f @3 foo +---- + +# Refreshing the iterator's view of the batch through SetOptions should surface +# the new range key. An earlier bug incorrectly compared the number of +# fragmented range keys to the number of internal batch range keys in order to +# determine when to refresh the iterator. + +iter iter=i1 +first +next +next +set-options +first +next +next +next +seek-ge bat +---- +a: (., [a-b) @1=poi UPDATED) +b: (., [b-c) @2=yaya, @1=poi UPDATED) +c: (., [c-d) @2=yaya UPDATED) +. +a: (., [a-b) @1=poi UPDATED) +b: (., [b-c) @2=yaya, @1=poi UPDATED) +c: (., [c-d) @2=yaya UPDATED) +e: (., [e-f) @3=foo UPDATED) +bat: (., [b-c) @2=yaya, @1=poi UPDATED) + +# Mutate the range key under the interleaving iterator's current position in the +# indexed batch. +# +# The last `seek-ge` operation landed on the range key [b-c). The top-level +# *pebble.Iterator needs to step the iterator again to see if there's a +# coincident point key at (`bat`), which would've advanced the interleaving +# iterator to the range key with bounds [c,d), so the underlying interleaving +# iterator is positioned ahead at: +# +# c: (., [c-d) @2=yaya) +# +# If we call set-options to refresh the iterator's view of the indexed batch, +# the range-key-unset [c,d)@2 becomes visible, and the range key that the +# underlying interleaving iterator is positioned over should not be visible. +# +# A bug previously allowed this range key to be visible when seeking into this +# span's bounds (see the optimization in InterleavingIter.SeekGE). Now, the call +# to SetOptions clears the interleaving iterator's positional state to avoid the +# SeekGE optimization. + +mutate +range-key-unset b d @2 +---- + +iter iter=i1 +set-options +seek-ge cat +---- +. +e: (., [e-f) @3=foo UPDATED) + +reset +---- + +batch +range-key-set a e @1 foo +---- + +flush +---- + +new-batch +---- + +new-batch-iter batchiter +---- + +new-db-iter dbiter +---- + +# Test RangeKeyChanged() semantics. +# Seeking to the same prefix returns RangeKeyChanged()=false. +# Seeking to a new prefix returns RangeKeyChanged()=true. +# Seeking to the same prefix with a SetOptions call in between returns +# RangeKeyChanged()=true. + +iter iter=dbiter +seek-prefix-ge b@3 +seek-prefix-ge b@4 +seek-prefix-ge c@3 +seek-prefix-ge d@3 +set-options +seek-prefix-ge d@1 +---- +b@3: (., [b-"b\x00") @1=foo UPDATED) +b@4: (., [b-"b\x00") @1=foo) +c@3: (., [c-"c\x00") @1=foo UPDATED) +d@3: (., [d-"d\x00") @1=foo UPDATED) +. +d@1: (., [d-"d\x00") @1=foo UPDATED) + +# Test the same semantics on a batch iterator. + +iter iter=batchiter +seek-prefix-ge b@3 +seek-prefix-ge b@4 +seek-prefix-ge c@3 +seek-prefix-ge d@3 +set-options +seek-prefix-ge d@1 +---- +b@3: (., [b-"b\x00") @1=foo UPDATED) +b@4: (., [b-"b\x00") @1=foo) +c@3: (., [c-"c\x00") @1=foo UPDATED) +d@3: (., [d-"d\x00") @1=foo UPDATED) +. +d@1: (., [d-"d\x00") @1=foo UPDATED) + +# Test mutating the indexed batch's range keys, overlapping the existing seek +# position. It should not see the new mutations, but after a call to SetOptions +# it should AND it should return RangeKeyChanged()=true. + +mutate +range-key-set d e @2 foo +---- + +iter iter=batchiter +seek-prefix-ge d@2 +set-options +seek-prefix-ge d@2 +---- +d@2: (., [d-"d\x00") @1=foo) +. +d@2: (., [d-"d\x00") @2=foo, @1=foo UPDATED) + +# Test cloning an iterator with a range-key mask block property filter +# configured. If the cloned and the clonee iterators have different suffixes +# configured, their suffixes should be respected. Previously, the +# RangeKeyMasking.Filter option was a footgun, because it was a single mutable +# instance. Cloning the iterator without supplying new iterator options would +# result in two iterators using the same filter. + +reset +---- + +batch +range-key-set a e @5 foo +set b@4 b@4 +---- + +new-db-iter iter-a +---- + +iter iter=iter-a +set-options mask-suffix=@3 mask-filter=true +seek-ge a +next +next +---- +. +a: (., [a-e) @5=foo UPDATED) +b@4: (b@4, [a-e) @5=foo) +. + +clone from=iter-a to=iter-b refresh-batch=false +---- + +iter iter=iter-b +set-options mask-suffix=@6 +seek-ge a +next +---- +. +a: (., [a-e) @5=foo UPDATED) +. + +iter iter=iter-a +seek-ge a +next +next +---- +a: (., [a-e) @5=foo UPDATED) +b@4: (b@4, [a-e) @5=foo) +. diff --git a/pebble/testdata/ingest b/pebble/testdata/ingest new file mode 100644 index 0000000..ee30ab9 --- /dev/null +++ b/pebble/testdata/ingest @@ -0,0 +1,1180 @@ +ingest +---- + +ingest non-existent +---- +open non-existent: file does not exist + +# Elide ingestion of empty sstables. + +build ext0 +---- + +ingest ext0 +---- + +lsm +---- + +build ext0 format=pebblev2 +set a 1 +set b 2 +---- + +ingest ext0 +---- + +lsm +---- +6: + 000006:[a#10,SET-b#10,SET] + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 1 696B 0B 0 | - | 0B | 1 696B | 0 0B | 0 0B | 0B | 1 0.0 +total | 1 696B 0B 0 | - | 696B | 1 696B | 0 0B | 0 696B | 0B | 1 1.0 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (0B) in: 0B written: 0B (0% overhead) +Flushes: 0 +Compactions: 0 estimated debt: 0B in progress: 0 (0B) + default: 0 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 0 (0B) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 6 entries (1.2KB) hit rate: 35.7% +Table cache: 1 entries (800B) hit rate: 50.0% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 1 as flushable: 0 (0B in 0 tables) + + +iter +seek-ge a +next +next +---- +a: (1, .) +b: (2, .) +. + +get +a +b +---- +a:1 +b:2 + +wait-pending-table-stats +000006 +---- +num-entries: 2 +num-deletions: 0 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +build ext1 +set a 3 +del b +---- + +ingest ext1 +---- + +lsm +---- +0.0: + 000007:[a#11,SET-b#11,DEL] +6: + 000006:[a#10,SET-b#10,SET] + +iter +seek-ge a +next +---- +a: (3, .) +. + +get +a +b +---- +a:3 +b: pebble: not found + +build ext2 format=pebblev2 +set a 4 +set b 5 +set c 6 +---- + +ingest ext2 +---- + +lsm +---- +0.1: + 000008:[a#12,SET-c#12,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] +6: + 000006:[a#10,SET-b#10,SET] + +iter +seek-ge a +next +next +---- +a: (4, .) +b: (5, .) +c: (6, .) + +get +a +b +c +---- +a:4 +b:5 +c:6 + +build ext3 +merge b 5 +del c +---- + +ingest ext3 +---- + +lsm +---- +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] +6: + 000006:[a#10,SET-b#10,SET] + +iter +seek-ge a +next +next +---- +a: (4, .) +b: (55, .) +. + +get +a +b +c +---- +a:4 +b:55 +c: pebble: not found + +build ext4 +set x 7 +set y 8 +---- + +ingest ext4 +---- + +lsm +---- +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] +6: + 000006:[a#10,SET-b#10,SET] + 000010:[x#14,SET-y#14,SET] + +iter +seek-lt y +prev +prev +---- +x: (7, .) +b: (55, .) +a: (4, .) + +get +x +y +---- +x:7 +y:8 + +batch +set j 9 +set k 10 +---- + +# Overlap with point keys in memtable, hence memtable will be flushed. + +build ext5 +set k 11 +---- + +ingest ext5 +---- +memtable flushed + +lsm +---- +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] + 000011:[k#17,SET-k#17,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] + 000014:[j#15,SET-k#16,SET] +6: + 000006:[a#10,SET-b#10,SET] + 000010:[x#14,SET-y#14,SET] + +iter +seek-ge j +next +---- +j: (9, .) +k: (11, .) + +get +j +k +---- +j:9 +k:11 + +# No data overlap in memtable, hence it will not be flushed. + +batch +set m 12 +---- + +build ext6 +set n 13 +---- + +ingest ext6 +---- + +lsm +---- +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] + 000011:[k#17,SET-k#17,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] + 000014:[j#15,SET-k#16,SET] +6: + 000006:[a#10,SET-b#10,SET] + 000015:[n#19,SET-n#19,SET] + 000010:[x#14,SET-y#14,SET] + +get +m +n +---- +m:12 +n:13 + +build ext7 format=pebblev2 +del-range a c +del-range x z +---- + +ingest ext7 +---- +memtable flushed + +lsm +---- +0.3: + 000016:[a#20,RANGEDEL-z#inf,RANGEDEL] +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] + 000011:[k#17,SET-k#17,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] + 000014:[j#15,SET-k#16,SET] + 000019:[m#18,SET-m#18,SET] +6: + 000006:[a#10,SET-b#10,SET] + 000015:[n#19,SET-n#19,SET] + 000010:[x#14,SET-y#14,SET] + +get +a +b +c +j +k +m +n +x +y +---- +a: pebble: not found +b: pebble: not found +c: pebble: not found +j:9 +k:11 +m:12 +n:13 +x: pebble: not found +y: pebble: not found + +wait-pending-table-stats +000016 +---- +num-entries: 2 +num-deletions: 2 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 1420 + +# A set operation takes precedence over a range deletion at the same +# sequence number as can occur during ingestion. + +build ext8 +set j 20 +del-range j k +set m 30 +---- + +ingest ext8 +---- + +get +j +k +m +---- +j:20 +k:11 +m:30 + +build ext9 +set a 40 +set f 40 +set g 40 +---- + +ingest ext9 +---- + +lsm +---- +0.4: + 000021:[a#22,SET-g#22,SET] + 000020:[j#21,RANGEDEL-m#21,SET] +0.3: + 000016:[a#20,RANGEDEL-z#inf,RANGEDEL] +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] + 000011:[k#17,SET-k#17,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] + 000014:[j#15,SET-k#16,SET] + 000019:[m#18,SET-m#18,SET] +6: + 000006:[a#10,SET-b#10,SET] + 000015:[n#19,SET-n#19,SET] + 000010:[x#14,SET-y#14,SET] + +# Overlap with sst boundary containing range del sentinel (fileNum 000015) is not considered an overlap since +# range del's end key is exclusive. Hence ext9 gets ingested into L6. + +build ext10 +set z 40 +---- + +# Although ext11 falls into sst boundaries of fileNum 000019, 000015, they don't actually contain any key within ext11's boundary. +# Hence ext11 is allowed to go further down and get ingested into L6. + +build ext11 +set d 40 +---- + +# Overlap with fileNum 000018 is not considered an overlap since ext12's end key is range del sentinel which is exclusive. + +build ext12 +del-range i j +---- + +# Ingesting multiple files into L0 is allowed. + +ingest ext10 ext11 ext12 +---- + +get +z +d +---- +z:40 +d:40 + +lsm +---- +0.4: + 000021:[a#22,SET-g#22,SET] + 000020:[j#21,RANGEDEL-m#21,SET] +0.3: + 000016:[a#20,RANGEDEL-z#inf,RANGEDEL] +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] + 000011:[k#17,SET-k#17,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] + 000014:[j#15,SET-k#16,SET] + 000019:[m#18,SET-m#18,SET] +6: + 000006:[a#10,SET-b#10,SET] + 000023:[d#23,SET-d#23,SET] + 000024:[i#24,RANGEDEL-j#inf,RANGEDEL] + 000015:[n#19,SET-n#19,SET] + 000010:[x#14,SET-y#14,SET] + 000022:[z#25,SET-z#25,SET] + +# No overlap between fileNum 000019 that contains point key f, since f is ingested file's range del sentinel. + +build ext13 +del-range e f +---- + +ingest ext13 +---- + +lsm +---- +0.4: + 000021:[a#22,SET-g#22,SET] + 000020:[j#21,RANGEDEL-m#21,SET] +0.3: + 000016:[a#20,RANGEDEL-z#inf,RANGEDEL] +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] + 000011:[k#17,SET-k#17,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] + 000014:[j#15,SET-k#16,SET] + 000019:[m#18,SET-m#18,SET] +6: + 000006:[a#10,SET-b#10,SET] + 000023:[d#23,SET-d#23,SET] + 000025:[e#26,RANGEDEL-f#inf,RANGEDEL] + 000024:[i#24,RANGEDEL-j#inf,RANGEDEL] + 000015:[n#19,SET-n#19,SET] + 000010:[x#14,SET-y#14,SET] + 000022:[z#25,SET-z#25,SET] + +# Overlap with range delete keys in memtable, hence memtable will be flushed. + +batch +del-range a d +---- + +build ext14 +set b 1 +---- + +ingest ext14 +---- +memtable flushed + +lsm +---- +0.6: + 000026:[b#28,SET-b#28,SET] +0.5: + 000029:[a#27,RANGEDEL-d#inf,RANGEDEL] +0.4: + 000021:[a#22,SET-g#22,SET] + 000020:[j#21,RANGEDEL-m#21,SET] +0.3: + 000016:[a#20,RANGEDEL-z#inf,RANGEDEL] +0.2: + 000009:[b#13,MERGE-c#13,DEL] +0.1: + 000008:[a#12,SET-c#12,SET] + 000011:[k#17,SET-k#17,SET] +0.0: + 000007:[a#11,SET-b#11,DEL] + 000014:[j#15,SET-k#16,SET] + 000019:[m#18,SET-m#18,SET] +6: + 000006:[a#10,SET-b#10,SET] + 000023:[d#23,SET-d#23,SET] + 000025:[e#26,RANGEDEL-f#inf,RANGEDEL] + 000024:[i#24,RANGEDEL-j#inf,RANGEDEL] + 000015:[n#19,SET-n#19,SET] + 000010:[x#14,SET-y#14,SET] + 000022:[z#25,SET-z#25,SET] + +reset +---- + +# Tests to show that keys don't overlap with range delete sentinels. + +batch +set b 1 +---- + +build ext15 +del-range a b +---- + +ingest ext15 +---- + +lsm +---- +6: + 000004:[a#11,RANGEDEL-b#inf,RANGEDEL] + +reset +---- + +batch +del-range b c +---- + +build ext16 +del-range a b +---- + +ingest ext16 +---- + +lsm +---- +6: + 000004:[a#11,RANGEDEL-b#inf,RANGEDEL] + +reset +---- + +# Tests for branch coverage of method overlapWithIterator, +# when levelIter is used and it produces a range del sentinel boundary +# because it finds no overlapping point key. + +# Case 1) levelIter produced boundary is less than ingested file's largest key. + +build ext17 +del-range a b +---- + +ingest ext17 +---- + +build ext18 +set a 10 +set c 10 +---- + +ingest ext18 +---- + +lsm +---- +0.0: + 000005:[a#11,SET-c#11,SET] +6: + 000004:[a#10,RANGEDEL-b#inf,RANGEDEL] + +reset +---- + +# Case 2) levelIter produced boundary is more than ingested file's largest key. + +build ext19 +del-range c d +---- + +ingest ext19 +---- + +build ext20 +set a 10 +set b 10 +---- + +ingest ext20 +---- + +build ext21 +set c 10 +---- + +ingest ext21 +---- + +lsm +---- +0.0: + 000006:[c#12,SET-c#12,SET] +6: + 000005:[a#11,SET-b#11,SET] + 000004:[c#10,RANGEDEL-d#inf,RANGEDEL] + +reset +---- + +# Case 3) levelIter produced boundary is equal to ingested file's largest key, +# where the latter is not a range del sentinel. + +build ext22 +del-range a b +---- + +ingest ext22 +---- + +build ext23 +set a 10 +set b 10 +---- + +ingest ext23 +---- + +lsm +---- +0.0: + 000005:[a#11,SET-b#11,SET] +6: + 000004:[a#10,RANGEDEL-b#inf,RANGEDEL] + +reset +---- + +# Case 4) levelIter produced boundary is equal to ingested file's largest key, +# where the latter is a range del sentinel. + +build ext24 +del-range a b +---- + +ingest ext24 +---- + +build ext25 +del-range a b +---- + +ingest ext25 +---- + +lsm +---- +0.0: + 000005:[a#11,RANGEDEL-b#inf,RANGEDEL] +6: + 000004:[a#10,RANGEDEL-b#inf,RANGEDEL] + +# Check for range key ingestion bug fix in +# https://github.com/cockroachdb/pebble/pull/2082. Without the fix, we expect +# the range key associated with the table ext3 to get elided. This test checks +# that the elision does not happen. +reset +---- + +build ext1 +range-key-set d g 1 val1 +---- + +ingest ext1 +---- + +lsm +---- +6: + 000004:[d#10,RANGEKEYSET-g#inf,RANGEKEYSET] + +build ext2 +range-key-set b e 1 val2 +---- + +ingest ext2 +---- + +lsm +---- +0.0: + 000005:[b#11,RANGEKEYSET-e#inf,RANGEKEYSET] +6: + 000004:[d#10,RANGEKEYSET-g#inf,RANGEKEYSET] + +build ext3 +range-key-del a c +---- + +ingest ext3 +---- + +# Without the fix in #2082 we would expect ext3 file to be ingested into L6. +lsm +---- +0.1: + 000006:[a#12,RANGEKEYDEL-c#inf,RANGEKEYDEL] +0.0: + 000005:[b#11,RANGEKEYSET-e#inf,RANGEKEYSET] +6: + 000004:[d#10,RANGEKEYSET-g#inf,RANGEKEYSET] + +build ext4 +set a a +---- + +ingest ext4 +---- + +lsm +---- +0.2: + 000007:[a#13,SET-a#13,SET] +0.1: + 000006:[a#12,RANGEKEYDEL-c#inf,RANGEKEYDEL] +0.0: + 000005:[b#11,RANGEKEYSET-e#inf,RANGEKEYSET] +6: + 000004:[d#10,RANGEKEYSET-g#inf,RANGEKEYSET] + +compact a aa +---- + +# Without the fix in #2082, we would expect the range key delete a-c to +# get elided as it would be in L6 beneath the b-e range key in L0. +lsm +---- +6: + 000008:[a#0,SET-g#inf,RANGEKEYSET] + +# Shouldn't show results for the b-c range as it must be deleted. +iter +first +next +next +next +---- +a: (a, .) +c: (., [c-e) 1=val2 UPDATED) +e: (., [e-g) 1=val1 UPDATED) +. + +# Keys can have exclusive sentinels. Check that files boundaries which contain +# such keys are ingested ingested into the lowest level possible. +reset +---- + +build ext1 +set c c +set e e +---- + +ingest ext1 +---- + +lsm +---- +6: + 000004:[c#10,SET-e#10,SET] + + +build ext2 +range-key-set a c 1 val1 +---- + +ingest ext2 +---- + +lsm +---- +6: + 000005:[a#11,RANGEKEYSET-c#inf,RANGEKEYSET] + 000004:[c#10,SET-e#10,SET] + +# The following test cases will test that files where the end bound is an +# exclusive sentinel due to range keys are ingested into the correct levels. +build ext3 +set f f +set h h +---- + +ingest ext3 +---- + +lsm +---- +6: + 000005:[a#11,RANGEKEYSET-c#inf,RANGEKEYSET] + 000004:[c#10,SET-e#10,SET] + 000006:[f#12,SET-h#12,SET] + + +build ext4 +range-key-unset eee f 1 +---- + +ingest ext4 +---- + +lsm +---- +6: + 000005:[a#11,RANGEKEYSET-c#inf,RANGEKEYSET] + 000004:[c#10,SET-e#10,SET] + 000007:[eee#13,RANGEKEYUNSET-f#inf,RANGEKEYUNSET] + 000006:[f#12,SET-h#12,SET] + +build ext5 +range-key-set ee eee 1 val3 +---- + +ingest ext5 +---- + +lsm +---- +6: + 000005:[a#11,RANGEKEYSET-c#inf,RANGEKEYSET] + 000004:[c#10,SET-e#10,SET] + 000008:[ee#14,RANGEKEYSET-eee#inf,RANGEKEYSET] + 000007:[eee#13,RANGEKEYUNSET-f#inf,RANGEKEYUNSET] + 000006:[f#12,SET-h#12,SET] + +build ext6 +set x x +set y y +---- + +ingest ext6 +---- + +lsm +---- +6: + 000005:[a#11,RANGEKEYSET-c#inf,RANGEKEYSET] + 000004:[c#10,SET-e#10,SET] + 000008:[ee#14,RANGEKEYSET-eee#inf,RANGEKEYSET] + 000007:[eee#13,RANGEKEYUNSET-f#inf,RANGEKEYUNSET] + 000006:[f#12,SET-h#12,SET] + 000009:[x#15,SET-y#15,SET] + +build ext7 +range-key-del s x +---- + +ingest ext7 +---- + +lsm +---- +6: + 000005:[a#11,RANGEKEYSET-c#inf,RANGEKEYSET] + 000004:[c#10,SET-e#10,SET] + 000008:[ee#14,RANGEKEYSET-eee#inf,RANGEKEYSET] + 000007:[eee#13,RANGEKEYUNSET-f#inf,RANGEKEYUNSET] + 000006:[f#12,SET-h#12,SET] + 000010:[s#16,RANGEKEYDEL-x#inf,RANGEKEYDEL] + 000009:[x#15,SET-y#15,SET] + +reset enable-split +---- + +build ext10 +set a foo +set e bar +---- + +ingest ext10 +---- + +lsm +---- +6: + 000004:[a#10,SET-e#10,SET] + +# The below ingestion should split one existing file. + +build ext11 +set b foobar +set d foobar +---- + +ingest ext11 +---- + +lsm +---- +6: + 000006:[a#10,SET-a#10,SET] + 000005:[b#11,SET-d#11,SET] + 000007:[e#10,SET-e#10,SET] + +iter +first +next +next +next +---- +a: (foo, .) +b: (foobar, .) +d: (foobar, .) +e: (bar, .) + +# This ingestion should not split any files due to data overlap. + +build ext12 +set c foobar +set e baz +---- + +ingest ext12 +---- + +lsm +---- +0.0: + 000008:[c#12,SET-e#12,SET] +6: + 000006:[a#10,SET-a#10,SET] + 000005:[b#11,SET-d#11,SET] + 000007:[e#10,SET-e#10,SET] + +# The below ingestion should fall through one existing file and split another +# file. + +build ext13 +set cc foo +set ccc foooo +---- + +ingest ext13 +---- + +lsm +---- +0.0: + 000008:[c#12,SET-e#12,SET] +6: + 000006:[a#10,SET-a#10,SET] + 000010:[b#11,SET-b#11,SET] + 000009:[cc#13,SET-ccc#13,SET] + 000011:[d#11,SET-d#11,SET] + 000007:[e#10,SET-e#10,SET] + +iter +seek-ge c +next +next +next +next +---- +c: (foobar, .) +cc: (foo, .) +ccc: (foooo, .) +d: (foobar, .) +e: (baz, .) + +# Ingestion splitting doesn't kick in at L0. + +build ext14 +set d updated +set dd new +---- + +ingest ext14 +---- + +lsm +---- +0.1: + 000012:[d#14,SET-dd#14,SET] +0.0: + 000008:[c#12,SET-e#12,SET] +6: + 000006:[a#10,SET-a#10,SET] + 000010:[b#11,SET-b#11,SET] + 000009:[cc#13,SET-ccc#13,SET] + 000011:[d#11,SET-d#11,SET] + 000007:[e#10,SET-e#10,SET] + +iter +seek-lt d +next +next +next +next +---- +ccc: (foooo, .) +d: (updated, .) +dd: (new, .) +e: (baz, .) +. + +# Multi-sstable ingestion batches. This exercises logic to find the appropriate +# file to split for each newly ingested file, as we will be repeatedly splitting +# files into smaller virtual files. + +reset enable-split +---- + +build ext10 +set a foo +set e bar +set g baz +---- + +ingest ext10 +---- + +lsm +---- +6: + 000004:[a#10,SET-g#10,SET] + +build ext11 +set b foobar +set c foobar +---- + +build ext12 +set cc foobar +set d foobar +---- + +# This ingestion should slide in the same gap between keys in ext10. + +ingest ext11 ext12 +---- + +lsm +---- +6: + 000007:[a#10,SET-a#10,SET] + 000005:[b#11,SET-c#11,SET] + 000006:[cc#12,SET-d#12,SET] + 000008:[e#10,SET-g#10,SET] + +# A virtual sstable produced from an ingest split can be ingest split again. + +build ext13 +set ee foooo +set f bar +---- + +ingest ext13 +---- + +lsm +---- +6: + 000007:[a#10,SET-a#10,SET] + 000005:[b#11,SET-c#11,SET] + 000006:[cc#12,SET-d#12,SET] + 000010:[e#10,SET-e#10,SET] + 000009:[ee#13,SET-f#13,SET] + 000011:[g#10,SET-g#10,SET] + +reset enable-split +---- + +build ext10 +set a foo +set e bar +set g baz +---- + +ingest ext10 +---- + +lsm +---- +6: + 000004:[a#10,SET-g#10,SET] + +build ext11 +set b foobar +set c foobar +---- + +build ext12 +set cc foobar +set d foobar +---- + +build ext13 +set ee foooo +set f bar +---- + +# This ingestion should split ext10 twice, and land two files on one side +# of a key in it, and another file on another side of it. + +ingest ext11 ext12 ext13 +---- + +lsm +---- +6: + 000008:[a#10,SET-a#10,SET] + 000005:[b#11,SET-c#11,SET] + 000006:[cc#12,SET-d#12,SET] + 000010:[e#10,SET-e#10,SET] + 000007:[ee#13,SET-f#13,SET] + 000011:[g#10,SET-g#10,SET] + +iter +first +next +next +next +next +next +next +next +next +next +---- +a: (foo, .) +b: (foobar, .) +c: (foobar, .) +cc: (foobar, .) +d: (foobar, .) +e: (bar, .) +ee: (foooo, .) +f: (bar, .) +g: (baz, .) +. diff --git a/pebble/testdata/ingest_external b/pebble/testdata/ingest_external new file mode 100644 index 0000000..1370ac1 --- /dev/null +++ b/pebble/testdata/ingest_external @@ -0,0 +1,122 @@ + +# Simple case. + +build-remote f1 +set a foo +set b bar +set c foobar +---- + +ingest-external +f1,5,a,cc +---- + +lsm +---- +6: + 000004:[a#10,DELSIZED-cc#inf,RANGEDEL] + +iter +first +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (foobar, .) +. + +# Above case but with c left out at ingestion time. + +reset +---- + +build-remote f2 +set a foo +set b bar +set c foobar +---- + +ingest-external +f2,5,a,c +---- + +lsm +---- +6: + 000004:[a#10,DELSIZED-c#inf,RANGEDEL] + +iter +first +next +next +next +---- +a: (foo, .) +b: (bar, .) +. +. + +build-remote f3 +set c foobarbaz +set d haha +set e something +---- + +build-remote f4 +set f foo +set g foo +set h foo +---- + +# This ingestion should error out due to the overlap between file spans. + +ingest-external +f3,10,c,f +f4,10,e,h +---- +pebble: external sstables have overlapping ranges + +ingest-external +f3,10,c,f +f4,10,f,hh +---- + +lsm +---- +6: + 000004:[a#10,DELSIZED-c#inf,RANGEDEL] + 000007:[c#11,DELSIZED-f#inf,RANGEDEL] + 000008:[f#12,DELSIZED-hh#inf,RANGEDEL] + +iter +first +next +next +next +next +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (foobarbaz, .) +d: (haha, .) +e: (something, .) +f: (foo, .) +g: (foo, .) +h: (foo, .) +. + +compact a z +---- + +lsm +---- +6: + 000004:[a#10,DELSIZED-c#inf,RANGEDEL] + 000007:[c#11,DELSIZED-f#inf,RANGEDEL] + 000008:[f#12,DELSIZED-hh#inf,RANGEDEL] diff --git a/pebble/testdata/ingest_load b/pebble/testdata/ingest_load new file mode 100644 index 0000000..3e5d690 --- /dev/null +++ b/pebble/testdata/ingest_load @@ -0,0 +1,152 @@ +load +---- +malformed input: + +load +a.SET.1: +---- +pebble: external sstable has non-zero seqnum: a#1,SET + +load +a.INVALID.0: +---- +pebble: external sstable has corrupted key: a#0,INVALID + +load +a.SET.0: +---- +1: a#0,1-a#0,1 + points: a#0,1-a#0,1 + ranges: #0,0-#0,0 + +load +a.SET.0: +b.SET.0: +---- +1: a#0,1-b#0,1 + points: a#0,1-b#0,1 + ranges: #0,0-#0,0 + +load +a.DEL.0: +---- +1: a#0,0-a#0,0 + points: a#0,0-a#0,0 + ranges: #0,0-#0,0 + +load +a.DEL.0: +b.DEL.0: +---- +1: a#0,0-b#0,0 + points: a#0,0-b#0,0 + ranges: #0,0-#0,0 + +load +a.MERGE.0: +---- +1: a#0,2-a#0,2 + points: a#0,2-a#0,2 + ranges: #0,0-#0,0 + +load +a.MERGE.0: +b.MERGE.0: +---- +1: a#0,2-b#0,2 + points: a#0,2-b#0,2 + ranges: #0,0-#0,0 + +load +a.RANGEDEL.0:b +---- +1: a#0,15-b#72057594037927935,15 + points: a#0,15-b#72057594037927935,15 + ranges: #0,0-#0,0 + +load +a.SET.0: +a.RANGEDEL.0:b +---- +1: a#0,15-b#72057594037927935,15 + points: a#0,15-b#72057594037927935,15 + ranges: #0,0-#0,0 + +load +a.SET.0: +a.RANGEDEL.0:b +---- +1: a#0,15-b#72057594037927935,15 + points: a#0,15-b#72057594037927935,15 + ranges: #0,0-#0,0 + +load +b.SET.0: +a.RANGEDEL.0:b +---- +1: a#0,15-b#0,1 + points: a#0,15-b#0,1 + ranges: #0,0-#0,0 + +# Loading tables at an unsupported table format results in an error. +# Write a table at version 7 (Pebble,v2) into a DB at version 6 (Pebble,v1). +load writer-version=8 db-version=7 +a.SET.1: +---- +pebble: table format (Pebble,v2) is not within range supported at DB format major version 7, ((LevelDB),(Pebble,v1)) + +# Tables with range keys only. + +load writer-version=10 db-version=10 +rangekey: a-z:{(#0,RANGEKEYSET,@1,foo)} +---- +1: a#0,21-z#72057594037927935,21 + points: #0,0-#0,0 + ranges: a#0,21-z#72057594037927935,21 + +# Tables with a mixture of point and range keys. + +load writer-version=10 db-version=10 +a.SET.0: +b.SET.0: +c.SET.0: +rangekey: w-x:{(#0,RANGEKEYSET,@1,foo)} +rangekey: x-y:{(#0,RANGEKEYSET,@2,bar)} +rangekey: y-z:{(#0,RANGEKEYSET,@3,baz)} +---- +1: a#0,1-z#72057594037927935,21 + points: a#0,1-c#0,1 + ranges: w#0,21-z#72057594037927935,21 + +load writer-version=10 db-version=10 +c.SET.0:d +rangekey: a-z:{(#0,RANGEKEYSET,@1,foo)} +---- +1: a#0,21-z#72057594037927935,21 + points: c#0,1-c#0,1 + ranges: a#0,21-z#72057594037927935,21 + +load writer-version=10 db-version=10 +a.SET.0:z +rangekey: c-d:{(#0,RANGEKEYSET,@1,foo)} +---- +1: a#0,1-d#72057594037927935,21 + points: a#0,1-a#0,1 + ranges: c#0,21-d#72057594037927935,21 + +# NB: range dels sort before range keys + +load writer-version=10 db-version=10 +a.RANGEDEL.0:z +rangekey: a-z:{(#0,RANGEKEYSET,@1,foo)} +---- +1: a#0,21-z#72057594037927935,15 + points: a#0,15-z#72057594037927935,15 + ranges: a#0,21-z#72057594037927935,21 + +# Loading tables at an unsupported table format results in an error. + +load writer-version=15 db-version=14 +a.SET.0: +---- +pebble: table format (Pebble,v4) is not within range supported at DB format major version 14, ((Pebble,v1),(Pebble,v3)) diff --git a/pebble/testdata/ingest_memtable_overlaps b/pebble/testdata/ingest_memtable_overlaps new file mode 100644 index 0000000..6f9b387 --- /dev/null +++ b/pebble/testdata/ingest_memtable_overlaps @@ -0,0 +1,175 @@ +define +set a 1 +---- + +overlaps +a-b +b-c +aa-ab +---- +true +false +false + +define +set b 1 +---- + +overlaps +a-b +b-c +---- +true +true + +define +set c 1 +---- + +overlaps +a-b +a-c +---- +false +true + +define +set a 1 +set d 2 +set g 3 +---- + +overlaps +b-c +e-f +b-c e-f +b-c e-g +---- +false +false +false +true + +define +set a 1 +set d 2 +set e 3 +set g 4 +---- + +overlaps +b-c +e-f +b-c e-f +b-c e0-f +---- +false +true +true +false + +define +set a 1 +set c 2 +set d 3 +set g 4 +---- + +overlaps +b-c +e-f +b-c e-f +b-b1 e-f +---- +true +false +true +false + +# The del-range tests are specific to the comparer. + +define default +del-range a c +del-range e g +---- + +overlaps +a-b +b-c +c-d +c-e +f-h +g-h +---- +true +true +false +true +true +false + +define reverse +del-range d b +---- + +overlaps +c-b +b-a +e-d +---- +true +false +true + +define default +set b 1 +---- + +overlaps +a.RANGEDEL.2-b.RANGEDEL.72057594037927935 +---- +false + +define default +del-range b c +---- + +overlaps +a.RANGEDEL.2-b.RANGEDEL.72057594037927935 +---- +false + +define default +del-range a f +del-range b c +---- + +overlaps +d.RANGEDEL.2-e.RANGEDEL.72057594037927935 +---- +true + +define default +range-key-set a f 1 val1 +range-key-set b c 2 val2 +---- + +overlaps +d-e +---- +true + +define default +range-key-set a c 1 val1 +---- + +overlaps +a-c +b-c +a.RANGEDEL.2-b.RANGEDEL.72057594037927935 +d-e +---- +true +true +true +false diff --git a/pebble/testdata/ingest_shared b/pebble/testdata/ingest_shared new file mode 100644 index 0000000..ca7c7dd --- /dev/null +++ b/pebble/testdata/ingest_shared @@ -0,0 +1,1125 @@ + +switch 1 +---- +ok + +build ext0 +set a 1 +set l 2 +---- + +ingest ext0 +---- + +lsm +---- +6: + 000004:[a#10,SET-l#10,SET] + + +batch +set d foo +set f bar +---- + +flush +---- + +compact a-z +---- +ok + +lsm +---- +6: + 000007:[a#0,SET-l#0,SET] + +switch 2 +---- +ok + +iter +first +---- +. + +replicate 1 2 d g +---- +replicated 1 shared SSTs + +lsm +---- +6: + 000005:[d#10,SET-f#10,SET] + +iter +first +next +next +---- +d: (foo, .) +f: (bar, .) +. + +batch +set e bar +set f bar2 +set g bar3 +---- + +iter +first +next +next +next +---- +d: (foo, .) +e: (bar, .) +f: (bar2, .) +g: (bar3, .) + +compact a-z +---- +ok + +iter +first +next +next +next +---- +d: (foo, .) +e: (bar, .) +f: (bar2, .) +g: (bar3, .) + +# Write a new key at f, but don't compact it down. + +batch +set f bar3 +---- + +switch 1 +---- +ok + +lsm +---- +6: + 000007:[a#0,SET-l#0,SET] + +excise e gg +---- +would excise 1 files, use ingest-and-excise to excise. + deleted: L6 000007 + added: L6 000008:[a#0,1-d#0,1] + added: L6 000009:[l#0,1-l#0,1] + +replicate 2 1 e gg +---- +replicated 1 shared SSTs + +iter +first +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, .) + +# Range key masking test. Write some masked keys, then replicate before +# compacting, then compact, then replicate back. + +batch +set h@3 foobar +set i@5 baz +range-key-set g j @4 value +---- + +iter +first +next +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +h@3: (foobar, [g-j) @4=value) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) + +iter mask-filter mask-suffix=@6 +first +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) + +switch 2 +---- +ok + +replicate 1 2 a z +---- +replicated 3 shared SSTs + +iter +first +next +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +h@3: (foobar, [g-j) @4=value) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) + +iter mask-filter mask-suffix=@6 +first +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) + +compact a-z +---- +ok + +replicate 2 1 a z +---- +replicated 3 shared SSTs + +restart +---- +ok, note that the active db has been set to 1 (use 'switch' to change) + +iter +first +next +next +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +h@3: (foobar, [g-j) @4=value) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) +. + +iter mask-filter mask-suffix=@6 +first +next +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) +. + +# Reverse iteration test with masking. + +iter mask-filter mask-suffix=@6 +last +prev +prev +prev +prev +prev +prev +prev +---- +l: (2, .) +i@5: (baz, [g-j) @4=value UPDATED) +g: (bar3, [g-j) @4=value) +f: (bar3, . UPDATED) +e: (bar, .) +d: (foo, .) +a: (1, .) +. + +# Range del tests. + +reset +---- + +switch 1 +---- +ok + +batch +set a@3 o +set b@5 foo +set c@6 bar +set e baz +---- + +flush +---- + +compact a-z +---- +ok + +batch +del-range b d +---- + +flush +---- + +batch +set a@3 abc +set b@7 notdeleted +set c@9 foobar +---- + +flush +---- + +lsm +---- +0.1: + 000009:[a@3#15,SET-c@9#17,SET] +0.0: + 000007:[b#14,RANGEDEL-d#inf,RANGEDEL] +6: + 000005:[a@3#10,SET-e#13,SET] + +iter +first +next +next +next +next +---- +a@3: (abc, .) +b@7: (notdeleted, .) +c@9: (foobar, .) +e: (baz, .) +. + +replicate 1 2 a z +---- +replicated 1 shared SSTs + +switch 2 +---- +ok + +lsm +---- +0.0: + 000004:[a@3#11,SET-d#inf,RANGEDEL] +6: + 000005:[a@3#10,SET-e#10,SET] + +iter +first +next +next +next +next +---- +a@3: (abc, .) +b@7: (notdeleted, .) +c@9: (foobar, .) +e: (baz, .) +. + +# Similar to the above test, except this time we bring the rangedel into +# L5 using an ingestion. + +reset +---- + +switch 1 +---- +ok + +batch +set a@3 o +set b@5 foo +set c@6 bar +set e baz +---- + +flush +---- + +compact a-z +---- +ok + +build s1 +del-range b d +---- + +ingest s1 +---- + +lsm +---- +5: + 000006:[b#14,RANGEDEL-d#inf,RANGEDEL] +6: + 000005:[a@3#10,SET-e#13,SET] + +batch +set a@3 abc +set b@7 notdeleted +set c@9 foobar +---- + +flush +---- + +lsm +---- +0.0: + 000008:[a@3#15,SET-c@9#17,SET] +5: + 000006:[b#14,RANGEDEL-d#inf,RANGEDEL] +6: + 000005:[a@3#10,SET-e#13,SET] + +iter +first +next +next +next +next +next +---- +a@3: (abc, .) +b@7: (notdeleted, .) +c@9: (foobar, .) +e: (baz, .) +. +. + +replicate 1 2 a z +---- +replicated 2 shared SSTs + +switch 2 +---- +ok + +lsm +---- +0.0: + 000004:[a@3#12,SET-c@9#12,SET] +5: + 000005:[b#11,RANGEDEL-d#inf,RANGEDEL] +6: + 000006:[a@3#10,SET-e#10,SET] + +iter +first +next +next +next +next +---- +a@3: (abc, .) +b@7: (notdeleted, .) +c@9: (foobar, .) +e: (baz, .) +. + +# Test for cases where an excise produces a range key on one side and point keys +# on the other. + +reset +---- + +switch 1 +---- +ok + +batch +range-key-set a aaa @3 foo +set d foobar +set e barbaz +---- + +flush +---- + +compact a-z +---- +ok + +lsm +---- +6: + 000005:[a#10,RANGEKEYSET-e#12,SET] + +switch 2 +---- +ok + +batch +set b bcd +set c cde +---- + +flush +---- + +compact a-z +---- +ok + +replicate 2 1 b cc +---- +replicated 1 shared SSTs + +switch 1 +---- +ok + +lsm +---- +6: + 000008:[a#10,RANGEKEYSET-aaa#inf,RANGEKEYSET] + 000007:[b#13,SET-c#13,SET] + 000009:[d#11,SET-e#12,SET] + +iter +first +next +next +next +next +---- +a: (., [a-aaa) @3=foo UPDATED) +b: (bcd, . UPDATED) +c: (cde, .) +d: (foobar, .) +e: (barbaz, .) + +reset +---- + +switch 1 +---- +ok + +batch +set a@3 o +set b@5 foo +set c@6 bar +set e baz +---- + +flush +---- + +compact a-z +---- +ok + +build s2 +del-range bb g +---- + +ingest s2 +---- + +lsm +---- +5: + 000006:[bb#14,RANGEDEL-g#inf,RANGEDEL] +6: + 000005:[a@3#10,SET-e#13,SET] + +switch 2 +---- +ok + +batch +set ff notdeleted +---- + +flush +---- + +compact a-z +---- +ok + +lsm +---- +6: + 000005:[ff#10,SET-ff#10,SET] + +# This replication should truncate the range deletion in pebble instance 1 +# at f, leaving ff undeleted. + +replicate 1 2 b f +---- +replicated 2 shared SSTs + +lsm +---- +5: + 000007:[bb#12,RANGEDEL-f#inf,RANGEDEL] +6: + 000008:[b@5#11,SET-e#11,SET] + 000005:[ff#10,SET-ff#10,SET] + +iter +seek-ge b +next +next +---- +b@5: (foo, .) +ff: (notdeleted, .) +. + +restart +---- +ok, note that the active db has been set to 1 (use 'switch' to change) + +switch 2 +---- +ok + +iter +seek-ge b +next +next +---- +b@5: (foo, .) +ff: (notdeleted, .) +. + +# Same as above, but with a truncated range key instead of a truncated range del. + +reset +---- + +switch 1 +---- +ok + +batch +set a@3 o +set b@5 foo +set c@6 bar +set e baz +---- + +flush +---- + +compact a-z +---- +ok + +build s3 +range-key-set bb g @8 foo +---- + +ingest s3 +---- + +lsm +---- +5: + 000006:[bb#14,RANGEKEYSET-g#inf,RANGEKEYSET] +6: + 000005:[a@3#10,SET-e#13,SET] + +switch 2 +---- +ok + +batch +set ff notcovered +---- + +flush +---- + +compact a-z +---- +ok + +lsm +---- +6: + 000005:[ff#10,SET-ff#10,SET] + +# This replication should truncate the range key in pebble instance 1 +# at f, leaving ff uncovered. + +replicate 1 2 b f +---- +replicated 2 shared SSTs + +lsm +---- +5: + 000007:[bb#12,RANGEKEYSET-f#inf,RANGEKEYSET] +6: + 000008:[b@5#11,SET-e#11,SET] + 000005:[ff#10,SET-ff#10,SET] + +iter +seek-ge b +next +next +next +next +next +---- +b@5: (foo, .) +bb: (., [bb-f) @8=foo UPDATED) +c@6: (bar, [bb-f) @8=foo) +e: (baz, [bb-f) @8=foo) +ff: (notcovered, . UPDATED) +. + + +iter mask-filter mask-suffix=@9 +seek-ge b +next +next +next +next +---- +b@5: (foo, .) +bb: (., [bb-f) @8=foo UPDATED) +e: (baz, [bb-f) @8=foo) +ff: (notcovered, . UPDATED) +. + +# Tests for Eventually file-only snapshots. + +reset +---- + +switch 1 +---- +ok + +batch +set a foo +set b bar +set c baz +---- + +flush +---- + +compact a-z +---- +ok + +switch 2 +---- +ok + +batch +set b foobar +---- + +file-only-snapshot s1 + aa bb + e f +---- +ok + +lsm +---- + +iter snapshot=s1 +first +next +next +---- +b: (foobar, .) +. +. + +# The below call should do a flush. + +wait-for-file-only-snapshot s1 +---- +ok + +lsm +---- +0.0: + 000005:[b#10,SET-b#10,SET] + +iter snapshot=s1 +first +next +next +---- +b: (foobar, .) +. +. + +replicate 1 2 a d +---- +replicated 1 shared SSTs + +iter snapshot=s1 +first +next +next +---- +b: (foobar, .) +. +. + +iter +first +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (baz, .) +. + +switch 1 +---- +ok + +batch +del c +---- + +# The below excise and wait should succeed as the flush will end up transitioning +# the file-only snapshot. + +lsm +---- +6: + 000005:[a#10,SET-c#12,SET] + +file-only-snapshot s2 + a cc +---- +ok + +iter snapshot=s2 +first +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +. +. +. + +flush +---- + +compact a-z +---- +ok + +replicate 2 1 a d +---- +replicated 1 shared SSTs + +wait-for-file-only-snapshot s2 +---- +ok + +iter snapshot=s2 +first +next +next +next +---- +a: (foo, .) +b: (bar, .) +. +. + +iter snapshot=s2 +first +clone +first +next +next +next +---- +a: (foo, .) +. +a: (foo, .) +b: (bar, .) +. +. + +iter +first +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (baz, .) +. +. + +batch +set d foo +set e bar +---- + +flush +---- + +compact a-z +---- +ok + +switch 2 +---- +ok + +flush +---- + +batch +set f foobar +---- + +# The below file-only snapshot is the more challenging case of a partial overlap +# between an excise and a file-only snapshot. In this case the EFOS transition +# blocks on the memtable but the excise proceeds through, causing the EFOS' +# WaitForFileOnlySnapshot() call to error out. Opening iterators also returns +# the same errors. + +file-only-snapshot s3 + c g +---- +ok + +iter snapshot=s3 +first +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (baz, .) +f: (foobar, .) +. + +iter snapshot=s3 +first +next +clone +first +next +next +next +---- +a: (foo, .) +b: (bar, .) +. +a: (foo, .) +b: (bar, .) +c: (baz, .) +f: (foobar, .) + + +replicate 1 2 b e +---- +replicated 2 shared SSTs + +wait-for-file-only-snapshot s3 +---- +pebble: snapshot excised before conversion to file-only snapshot + +iter snapshot=s3 +first +next +next +next +next +---- +pebble: snapshot excised before conversion to file-only snapshot + +iter snapshot=s3 +first +next +clone +first +next +next +next +---- +pebble: snapshot excised before conversion to file-only snapshot + +iter +first +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (baz, .) +d: (foo, .) +f: (foobar, .) + +# The below example tests for a file-only snapshot that overlaps completely +# with an excise right after it. The wait succeeds and snapshot consistency is +# maintained. + +reset +---- + +switch 1 +---- +ok + +batch +set a foo +set b bar +set c baz +---- + +flush +---- + +compact a-z +---- +ok + +switch 2 +---- +ok + +batch +set d foobar +---- + +file-only-snapshot s4 + b e +---- +ok + +iter snapshot=s4 +first +next +next +---- +d: (foobar, .) +. +. + +replicate 1 2 b e +---- +replicated 1 shared SSTs + +wait-for-file-only-snapshot s4 +---- +ok + +iter snapshot=s4 +first +next +next +---- +d: (foobar, .) +. +. + +compact a-z +---- +ok + +iter snapshot=s4 +first +next +next +---- +d: (foobar, .) +. +. + +iter +first +next +next +---- +b: (bar, .) +c: (baz, .) +. diff --git a/pebble/testdata/ingest_shared_lower b/pebble/testdata/ingest_shared_lower new file mode 100644 index 0000000..98b7ba8 --- /dev/null +++ b/pebble/testdata/ingest_shared_lower @@ -0,0 +1,1108 @@ + +switch 1 +---- +ok + +build ext0 +set a 1 +set l 2 +---- + +ingest ext0 +---- + +lsm +---- +6: + 000004:[a#10,SET-l#10,SET] + + +batch +set d foo +set f bar +---- + +flush +---- + +compact a-z +---- +ok + +lsm +---- +6: + 000008:[a#0,SET-l#0,SET] + +switch 2 +---- +ok + +iter +first +---- +. + +replicate 1 2 d g +---- +replicated 1 shared SSTs + +lsm +---- +6: + 000005:[d#10,SET-f#10,SET] + +iter +first +next +next +---- +d: (foo, .) +f: (bar, .) +. + +batch +set e bar +set f bar2 +set g bar3 +---- + +iter +first +next +next +next +---- +d: (foo, .) +e: (bar, .) +f: (bar2, .) +g: (bar3, .) + +compact a-z +---- +ok + +iter +first +next +next +next +---- +d: (foo, .) +e: (bar, .) +f: (bar2, .) +g: (bar3, .) + +# Write a new key at f, but don't compact it down. + +batch +set f bar3 +---- + +switch 1 +---- +ok + +lsm +---- +6: + 000008:[a#0,SET-l#0,SET] + +excise e gg +---- +would excise 1 files, use ingest-and-excise to excise. + deleted: L6 000008 + added: L6 000009:[a#0,1-d#0,1] + added: L6 000010:[l#0,1-l#0,1] + +replicate 2 1 e gg +---- +replicated 1 shared SSTs + +iter +first +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, .) + +# Range key masking test. Write some masked keys, then replicate before +# compacting, then compact, then replicate back. + +batch +set h@3 foobar +set i@5 baz +range-key-set g j @4 value +---- + +iter +first +next +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +h@3: (foobar, [g-j) @4=value) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) + +iter mask-filter mask-suffix=@6 +first +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) + +switch 2 +---- +ok + +replicate 1 2 a z +---- +replicated 3 shared SSTs + +iter +first +next +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +h@3: (foobar, [g-j) @4=value) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) + +iter mask-filter mask-suffix=@6 +first +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) + +compact a-z +---- +ok + +replicate 2 1 a z +---- +replicated 3 shared SSTs + +switch 1 +---- +ok + +iter +first +next +next +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +h@3: (foobar, [g-j) @4=value) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) +. + +iter mask-filter mask-suffix=@6 +first +next +next +next +next +next +next +next +---- +a: (1, .) +d: (foo, .) +e: (bar, .) +f: (bar3, .) +g: (bar3, [g-j) @4=value UPDATED) +i@5: (baz, [g-j) @4=value) +l: (2, . UPDATED) +. + +# Reverse iteration test with masking. + +iter mask-filter mask-suffix=@6 +last +prev +prev +prev +prev +prev +prev +prev +---- +l: (2, .) +i@5: (baz, [g-j) @4=value UPDATED) +g: (bar3, [g-j) @4=value) +f: (bar3, . UPDATED) +e: (bar, .) +d: (foo, .) +a: (1, .) +. + +# Range del tests. + +reset +---- + +switch 1 +---- +ok + +batch +set a@3 o +set b@5 foo +set c@6 bar +set e baz +---- + +flush +---- + +compact a-z +---- +ok + +batch +del-range b d +---- + +flush +---- + +batch +set a@3 abc +set b@7 notdeleted +set c@9 foobar +---- + +flush +---- + +lsm +---- +0.1: + 000010:[a@3#15,SET-c@9#17,SET] +0.0: + 000008:[b#14,RANGEDEL-d#inf,RANGEDEL] +6: + 000006:[a@3#10,SET-e#13,SET] + +iter +first +next +next +next +next +---- +a@3: (abc, .) +b@7: (notdeleted, .) +c@9: (foobar, .) +e: (baz, .) +. + +replicate 1 2 a z +---- +replicated 1 shared SSTs + +switch 2 +---- +ok + +lsm +---- +0.0: + 000004:[a@3#11,SET-d#inf,RANGEDEL] +6: + 000005:[a@3#10,SET-e#10,SET] + +iter +first +next +next +next +next +---- +a@3: (abc, .) +b@7: (notdeleted, .) +c@9: (foobar, .) +e: (baz, .) +. + +# Similar to the above test, except this time we bring the rangedel into +# L5 using an ingestion. + +reset +---- + +switch 1 +---- +ok + +batch +set a@3 o +set b@5 foo +set c@6 bar +set e baz +---- + +flush +---- + +compact a-z +---- +ok + +build s1 +del-range b d +---- + +ingest s1 +---- + +lsm +---- +5: + 000007:[b#14,RANGEDEL-d#inf,RANGEDEL] +6: + 000006:[a@3#10,SET-e#13,SET] + +batch +set a@3 abc +set b@7 notdeleted +set c@9 foobar +---- + +flush +---- + +lsm +---- +0.0: + 000009:[a@3#15,SET-c@9#17,SET] +5: + 000007:[b#14,RANGEDEL-d#inf,RANGEDEL] +6: + 000006:[a@3#10,SET-e#13,SET] + +iter +first +next +next +next +next +next +---- +a@3: (abc, .) +b@7: (notdeleted, .) +c@9: (foobar, .) +e: (baz, .) +. +. + +replicate 1 2 a z +---- +replicated 2 shared SSTs + +switch 2 +---- +ok + +lsm +---- +0.0: + 000004:[a@3#12,SET-c@9#12,SET] +5: + 000005:[b#11,RANGEDEL-d#inf,RANGEDEL] +6: + 000006:[a@3#10,SET-e#10,SET] + +iter +first +next +next +next +next +---- +a@3: (abc, .) +b@7: (notdeleted, .) +c@9: (foobar, .) +e: (baz, .) +. + +# Test for cases where an excise produces a range key on one side and point keys +# on the other. + +reset +---- + +switch 1 +---- +ok + +batch +range-key-set a aaa @3 foo +set d foobar +set e barbaz +---- + +flush +---- + +compact a-z +---- +ok + +lsm +---- +6: + 000006:[a#10,RANGEKEYSET-e#12,SET] + +switch 2 +---- +ok + +batch +set b bcd +set c cde +---- + +flush +---- + +compact a-z +---- +ok + +replicate 2 1 b cc +---- +replicated 1 shared SSTs + +switch 1 +---- +ok + +lsm +---- +6: + 000009:[a#10,RANGEKEYSET-aaa#inf,RANGEKEYSET] + 000008:[b#13,SET-c#13,SET] + 000010:[d#11,SET-e#12,SET] + +iter +first +next +next +next +next +---- +a: (., [a-aaa) @3=foo UPDATED) +b: (bcd, . UPDATED) +c: (cde, .) +d: (foobar, .) +e: (barbaz, .) + +reset +---- + +switch 1 +---- +ok + +batch +set a@3 o +set b@5 foo +set c@6 bar +set e baz +---- + +flush +---- + +compact a-z +---- +ok + +build s2 +del-range bb g +---- + +ingest s2 +---- + +lsm +---- +5: + 000007:[bb#14,RANGEDEL-g#inf,RANGEDEL] +6: + 000006:[a@3#10,SET-e#13,SET] + +switch 2 +---- +ok + +batch +set ff notdeleted +---- + +flush +---- + +compact a-z +---- +ok + +lsm +---- +6: + 000006:[ff#10,SET-ff#10,SET] + +# This replication should truncate the range deletion in pebble instance 1 +# at f, leaving ff undeleted. + +replicate 1 2 b f +---- +replicated 2 shared SSTs + +lsm +---- +5: + 000008:[bb#12,RANGEDEL-f#inf,RANGEDEL] +6: + 000009:[b@5#11,SET-e#11,SET] + 000006:[ff#10,SET-ff#10,SET] + +iter +seek-ge b +next +next +---- +b@5: (foo, .) +ff: (notdeleted, .) +. + +# Same as above, but with a truncated range key instead of a truncated range del. + +reset +---- + +switch 1 +---- +ok + +batch +set a@3 o +set b@5 foo +set c@6 bar +set e baz +---- + +flush +---- + +compact a-z +---- +ok + +build s3 +range-key-set bb g @8 foo +---- + +ingest s3 +---- + +lsm +---- +5: + 000007:[bb#14,RANGEKEYSET-g#inf,RANGEKEYSET] +6: + 000006:[a@3#10,SET-e#13,SET] + +switch 2 +---- +ok + +batch +set ff notcovered +---- + +flush +---- + +compact a-z +---- +ok + +lsm +---- +6: + 000006:[ff#10,SET-ff#10,SET] + +# This replication should truncate the range key in pebble instance 1 +# at f, leaving ff uncovered. + +replicate 1 2 b f +---- +replicated 2 shared SSTs + +lsm +---- +5: + 000008:[bb#12,RANGEKEYSET-f#inf,RANGEKEYSET] +6: + 000009:[b@5#11,SET-e#11,SET] + 000006:[ff#10,SET-ff#10,SET] + +iter +seek-ge b +next +next +next +next +next +---- +b@5: (foo, .) +bb: (., [bb-f) @8=foo UPDATED) +c@6: (bar, [bb-f) @8=foo) +e: (baz, [bb-f) @8=foo) +ff: (notcovered, . UPDATED) +. + + +iter mask-filter mask-suffix=@9 +seek-ge b +next +next +next +next +---- +b@5: (foo, .) +bb: (., [bb-f) @8=foo UPDATED) +e: (baz, [bb-f) @8=foo) +ff: (notcovered, . UPDATED) +. + +# Tests for Eventually file-only snapshots. + +reset +---- + +switch 1 +---- +ok + +batch +set a foo +set b bar +set c baz +---- + +flush +---- + +compact a-z +---- +ok + +switch 2 +---- +ok + +batch +set b foobar +---- + +file-only-snapshot s1 + aa bb + e f +---- +ok + +lsm +---- + +iter snapshot=s1 +first +next +next +---- +b: (foobar, .) +. +. + +# The below call should do a flush. + +wait-for-file-only-snapshot s1 +---- +ok + +lsm +---- +0.0: + 000005:[b#10,SET-b#10,SET] + +iter snapshot=s1 +first +next +next +---- +b: (foobar, .) +. +. + +replicate 1 2 a d +---- +replicated 1 shared SSTs + +iter snapshot=s1 +first +next +next +---- +b: (foobar, .) +. +. + +iter +first +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (baz, .) +. + +switch 1 +---- +ok + +batch +del c +---- + +# The below excise and wait should succeed as the flush will end up transitioning +# the file-only snapshot. + +lsm +---- +6: + 000006:[a#10,SET-c#12,SET] + +file-only-snapshot s2 + a cc +---- +ok + +iter snapshot=s2 +first +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +. +. +. + +flush +---- + +compact a-z +---- +ok + +replicate 2 1 a d +---- +replicated 1 shared SSTs + +wait-for-file-only-snapshot s2 +---- +ok + +iter snapshot=s2 +first +next +next +next +---- +a: (foo, .) +b: (bar, .) +. +. + +iter snapshot=s2 +first +clone +first +next +next +next +---- +a: (foo, .) +. +a: (foo, .) +b: (bar, .) +. +. + +iter +first +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (baz, .) +. +. + +batch +set d foo +set e bar +---- + +flush +---- + +compact a-z +---- +ok + +switch 2 +---- +ok + +flush +---- + +batch +set f foobar +---- + +# The below file-only snapshot is the more challenging case of a partial overlap +# between an excise and a file-only snapshot. In this case the EFOS transition +# blocks on the memtable but the excise proceeds through, causing the EFOS' +# WaitForFileOnlySnapshot() call to error out. Opening iterators also returns +# the same errors. + +file-only-snapshot s3 + c g +---- +ok + +iter snapshot=s3 +first +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (baz, .) +f: (foobar, .) +. + +iter snapshot=s3 +first +next +clone +first +next +next +next +---- +a: (foo, .) +b: (bar, .) +. +a: (foo, .) +b: (bar, .) +c: (baz, .) +f: (foobar, .) + + +replicate 1 2 b e +---- +replicated 2 shared SSTs + +wait-for-file-only-snapshot s3 +---- +pebble: snapshot excised before conversion to file-only snapshot + +iter snapshot=s3 +first +next +next +next +next +---- +pebble: snapshot excised before conversion to file-only snapshot + +iter snapshot=s3 +first +next +clone +first +next +next +next +---- +pebble: snapshot excised before conversion to file-only snapshot + +iter +first +next +next +next +next +---- +a: (foo, .) +b: (bar, .) +c: (baz, .) +d: (foo, .) +f: (foobar, .) + +# The below example tests for a file-only snapshot that overlaps completely +# with an excise right after it. The wait succeeds and snapshot consistency is +# maintained. + +reset +---- + +switch 1 +---- +ok + +batch +set a foo +set b bar +set c baz +---- + +flush +---- + +compact a-z +---- +ok + +switch 2 +---- +ok + +batch +set d foobar +---- + +file-only-snapshot s4 + b e +---- +ok + +iter snapshot=s4 +first +next +next +---- +d: (foobar, .) +. +. + +replicate 1 2 b e +---- +replicated 1 shared SSTs + +wait-for-file-only-snapshot s4 +---- +ok + +iter snapshot=s4 +first +next +next +---- +d: (foobar, .) +. +. + +compact a-z +---- +ok + +iter snapshot=s4 +first +next +next +---- +d: (foobar, .) +. +. + +iter +first +next +next +---- +b: (bar, .) +c: (baz, .) +. diff --git a/pebble/testdata/ingest_sort_and_verify b/pebble/testdata/ingest_sort_and_verify new file mode 100644 index 0000000..3ead5c1 --- /dev/null +++ b/pebble/testdata/ingest_sort_and_verify @@ -0,0 +1,180 @@ +# Default comparator specific tests. + +ingest cmp=default +a.SET.0-b.SET.0 +---- +0: a#0,1-b#0,1 + +ingest cmp=default +a.SET.0-b.SET.0 +c.SET.0-d.SET.0 +e.SET.0-f.SET.0 +---- +0: a#0,1-b#0,1 +1: c#0,1-d#0,1 +2: e#0,1-f#0,1 + +ingest cmp=default +c.SET.0-d.SET.0 +a.SET.0-b.SET.0 +e.SET.0-f.SET.0 +---- +1: a#0,1-b#0,1 +0: c#0,1-d#0,1 +2: e#0,1-f#0,1 + +ingest cmp=default +a.SET.0-b.SET.0 +b.SET.0-d.SET.0 +e.SET.0-f.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=default +c.SET.0-d.SET.0 +d.SET.0-e.SET.0 +a.SET.0-b.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=default +a.SET.1-b.SET.1 +b.SET.0-c.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=default +a.RANGEDEL.0-b.RANGEDEL.72057594037927935 +b.RANGEDEL.0-d.RANGEDEL.72057594037927935 +e.RANGEDEL.0-f.RANGEDEL.72057594037927935 +---- +0: a#0,15-b#72057594037927935,15 +1: b#0,15-d#72057594037927935,15 +2: e#0,15-f#72057594037927935,15 + +ingest cmp=default +a.RANGEDEL.0-b.RANGEDEL.72057594037927935 +c.RANGEDEL.0-e.RANGEDEL.72057594037927935 +e.RANGEDEL.0-f.RANGEDEL.72057594037927935 +---- +0: a#0,15-b#72057594037927935,15 +1: c#0,15-e#72057594037927935,15 +2: e#0,15-f#72057594037927935,15 + +ingest cmp=default +a.RANGEDEL.0-b.RANGEDEL.72057594037927935 +b.RANGEDEL.0-e.RANGEDEL.72057594037927935 +e.RANGEDEL.0-f.RANGEDEL.72057594037927935 +---- +0: a#0,15-b#72057594037927935,15 +1: b#0,15-e#72057594037927935,15 +2: e#0,15-f#72057594037927935,15 + +ingest cmp=default +a.RANGEDEL.0-c.RANGEDEL.72057594037927935 +b.SET.0-d.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=default +b.RANGEDEL.0-d.RANGEDEL.72057594037927935 +a.SET.0-c.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=default +a.RANGEDEL.0-b.RANGEDEL.72057594037927935 +b.SET.0-c.SET.0 +---- +0: a#0,15-b#72057594037927935,15 +1: b#0,1-c#0,1 + +# Reverse comparator specific tests. + +ingest cmp=reverse +b.SET.0-a.SET.0 +---- +0: b#0,1-a#0,1 + +ingest cmp=reverse +f.SET.0-e.SET.0 +d.SET.0-c.SET.0 +b.SET.0-a.SET.0 +---- +0: f#0,1-e#0,1 +1: d#0,1-c#0,1 +2: b#0,1-a#0,1 + +ingest cmp=reverse +f.SET.0-e.SET.0 +b.SET.0-a.SET.0 +d.SET.0-c.SET.0 +---- +0: f#0,1-e#0,1 +2: d#0,1-c#0,1 +1: b#0,1-a#0,1 + +ingest cmp=reverse +f.SET.0-e.SET.0 +d.SET.0-b.SET.0 +b.SET.0-a.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=reverse +b.SET.0-a.SET.0 +e.SET.0-d.SET.0 +d.SET.0-c.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=reverse +c.SET.0-b.SET.0 +b.SET.1-a.SET.1 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=reverse +b.RANGEDEL.0-a.RANGEDEL.72057594037927935 +d.RANGEDEL.0-b.RANGEDEL.72057594037927935 +f.RANGEDEL.0-e.RANGEDEL.72057594037927935 +---- +2: f#0,15-e#72057594037927935,15 +1: d#0,15-b#72057594037927935,15 +0: b#0,15-a#72057594037927935,15 + +ingest cmp=reverse +b.RANGEDEL.0-a.RANGEDEL.72057594037927935 +e.RANGEDEL.0-c.RANGEDEL.72057594037927935 +f.RANGEDEL.0-e.RANGEDEL.72057594037927935 +---- +2: f#0,15-e#72057594037927935,15 +1: e#0,15-c#72057594037927935,15 +0: b#0,15-a#72057594037927935,15 + +ingest cmp=reverse +b.RANGEDEL.0-a.RANGEDEL.72057594037927935 +e.RANGEDEL.0-b.RANGEDEL.72057594037927935 +f.RANGEDEL.0-e.RANGEDEL.72057594037927935 +---- +2: f#0,15-e#72057594037927935,15 +1: e#0,15-b#72057594037927935,15 +0: b#0,15-a#72057594037927935,15 + +ingest cmp=reverse +c.RANGEDEL.0-a.RANGEDEL.72057594037927935 +d.SET.0-b.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=reverse +d.RANGEDEL.0-b.RANGEDEL.72057594037927935 +c.SET.0-a.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges + +ingest cmp=reverse +b.RANGEDEL.0-a.RANGEDEL.72057594037927935 +c.SET.0-b.SET.0 +---- +pebble: local ingestion sstables have overlapping ranges diff --git a/pebble/testdata/ingest_target_level b/pebble/testdata/ingest_target_level new file mode 100644 index 0000000..e6b8edf --- /dev/null +++ b/pebble/testdata/ingest_target_level @@ -0,0 +1,324 @@ +define +---- + +# An empty LSM ingests into the bottom level. +target +a-b +---- +6 + +define +L5 + b.SET.1:1 + c.SET.2:2 +---- +5: + 000004:[b#1,SET-c#2,SET] + +# Overlapping cases. +target +a-b +b-c +c-d +---- +4 +4 +4 + +# Non-overlapping cases: +# - Ingested file lies entirely before the existing file. +# - Ingested file lies entirely after the existing file. +# - Ingested file has no data overlap (falls through the middle of the existing +# file). +target +a-aa +d-e +bb-bb +---- +6 +6 +6 + +define +L0 + b.SET.3:3 + e.SET.4:4 +L0 + d.SET.5:5 + f.SET.6:6 +L0 + x.SET.7:7 + y.SET.8:8 +L3 + g.SET.1:1 + h.SET.2:2 +---- +0.1: + 000005:[d#5,SET-f#6,SET] +0.0: + 000004:[b#3,SET-e#4,SET] + 000006:[x#7,SET-y#8,SET] +3: + 000007:[g#1,SET-h#2,SET] + +# Files overlap with L0. Files ingested into L0. +target +b-c +d-e +---- +0 +0 + +# Files overlap with L3. Files ingested into L2. +target +g-m +---- +2 + +# No overlap. Files ingested into L6. +target +i-m +c-c +---- +6 +6 + +define +L5 + a.SET.4:4 +L5 + c.SET.3:3 +L6 + a.SET.2:2 +L6 + c.SET.1:1 +---- +5: + 000004:[a#4,SET-a#4,SET] + 000005:[c#3,SET-c#3,SET] +6: + 000006:[a#2,SET-a#2,SET] + 000007:[c#1,SET-c#1,SET] + +# The ingested file slips through the gaps in both L5 and L6. +target +b-b +---- +6 + +define +L5 + a.SET.4:4 +L5 + c.SET.3:3 +L6 + a.SET.2:2 +L6 + c.SET.1:1 + compact:a-c +---- +5: + 000004:[a#4,SET-a#4,SET] + 000005:[c#3,SET-c#3,SET] +6: + 000006:[a#2,SET-a#2,SET] + 000007:[c#1,SET-c#1,SET] + +# The ingested file cannot reach L6 as there is a compaction outputting a file +# into the range [a,c]. +target +b-b +---- +5 + +define +L0 + c.SET.4:4 + d.SET.3:3 + d.RANGEDEL.2:g +L2 + a.RANGEDEL.1:g +---- +0.0: + 000004:[c#4,SET-g#inf,RANGEDEL] +2: + 000005:[a#1,RANGEDEL-g#inf,RANGEDEL] + +# Overlapping cases: +# - The ingested file overlaps with with [c,c]. +# - The rangedel over [d,g) keeps the ingested file in L0. +# - Ditto. +target +c-c +d-d +e-e +---- +0 +0 +0 + +# Non-overlapping cases: +# - The ingested file [cc,cc] slips through L0, but is kept at L1 by the +# rangedel in L2. +# - The ingested file is to completely to right of all files. +# - The ingested file is to the left of all files in L0, but is kept at L1 by +# the rangedel in L2. +target +cc-cc +g-g +a-a +---- +1 +6 +1 + +# A more complicated example demonstrating data overlap. +# |--| ingested file: [d-e] - data overlap +# |-| ingested file: [cc-d] - no data overlap +# |--| ingested file: [ee-ff] - no data overlap +# |*--*--*----*------*| existing file: [a-g], points: [a, b, c, dd, g] +# _____________________ +# a b c d e f g +define +L1 + a.SET.0:a + b.SET.0:b + c.SET.0:c + dd.SET.0:dd + g.SET.0:g +---- +1: + 000004:[a#0,SET-g#0,SET] + +# Data overlap. +target +d-e +---- +0 + +# No data overlap. +target +cc-d +ee-ff +---- +6 +6 + +# Range key-point key data overlap will always correctly identify overlap because +# we seek using the combined point and range key bounds of the ingested file +# to determine overlap and don't check the data within the ingested file. +define +L5 + a.SET.0:a + b.SET.0:b + c.SET.0:c +---- +5: + 000004:[a#0,SET-c#0,SET] + +target +rkey:a-c +---- +4 + +# Point key-range key overlap +define +L5 + rangekey:a-c:{(#1,RANGEKEYSET,@t10,foo)} +---- +5: + 000004:[a#1,RANGEKEYSET-c#inf,RANGEKEYSET] + +target +a-c +---- +4 + +# Range key-range key overlap. +define +L5 + rangekey:a-c:{(#1,RANGEKEYSET,@t10,foo)} +---- +5: + 000004:[a#1,RANGEKEYSET-c#inf,RANGEKEYSET] + +target +rkey:a-c +---- +4 + +# Cases with boundary overlap and no data overlap. With suggest-split off +# we get a target level of L0, but with suggest-split on, we get suggested +# a file split. + +define +L6 + a.SET.2:2 + d.SET.3:3 +L6 + f.SET.4:4 + k.SET.6:6 +---- +6: + 000004:[a#2,SET-d#3,SET] + 000005:[f#4,SET-k#6,SET] + +target +b-c +e-g +---- +5 +5 + +target suggest-split +b-c +e-g +---- +6 (split file: 000004) +5 + +target suggest-split +g-i +---- +6 (split file: 000005) + +# suggest-split recognizes and avoids in-progress compactions. + +define +L6 + a.SET.2:2 + d.SET.3:3 +L6 + f.SET.4:4 + k.SET.6:6 + compact:f-k +---- +6: + 000004:[a#2,SET-d#3,SET] + 000005:[f#4,SET-k#6,SET] + +target suggest-split +g-i +---- +5 + +# Ingestion splitting correctly recognizes data overlap in L6, and suggests +# split in L5. + +define +L5 + a.SET.2:2 + e.SET.3:3 +L6 + c.SET.1:1 + k.SET.1:1 +---- +5: + 000004:[a#2,SET-e#3,SET] +6: + 000005:[c#1,SET-k#1,SET] + +target suggest-split +b-c +---- +5 (split file: 000004) diff --git a/pebble/testdata/ingest_update_seqnums b/pebble/testdata/ingest_update_seqnums new file mode 100644 index 0000000..7459fcf --- /dev/null +++ b/pebble/testdata/ingest_update_seqnums @@ -0,0 +1,142 @@ +# Starting sequence number. Each file increments the sequence number. + +starting-seqnum +42 +---- + +# Point keys only (no range dels). + +load +a.SET.0: +b.SET.0: +c.SET.0: +---- +file 0 + +# Point keys only (range del lower bound). + +load +a.RANGEDEL.0:b +c.SET.0: +---- +file 1 + +# Point keys only (range del upper bound). + +load +a.SET.0: +b.RANGEDEL.0:c +---- +file 2 + +# Update the sequence numbers across all three files. +# NB: the sequence numbers are expected to increment by one from the starting +# sequence number, for each file. + +update-files +---- +file 0: + combined: a#42,1-c#42,1 + points: a#42,1-c#42,1 + ranges: #0,0-#0,0 +file 1: + combined: a#43,15-c#43,1 + points: a#43,15-c#43,1 + ranges: #0,0-#0,0 +file 2: + combined: a#44,1-c#72057594037927935,15 + points: a#44,1-c#72057594037927935,15 + ranges: #0,0-#0,0 + +# Reset to the starting sequence number and reset the slice of files. The +# following tests consider a single file at a time. + +# Range keys only. + +reset +---- + +load +rangekey: a-c:{#0,RANGEKEYSET,@1,foo)} +---- +file 0 + +update-files +---- +file 0: + combined: a#42,21-c#72057594037927935,21 + points: #0,0-#0,0 + ranges: a#42,21-c#72057594037927935,21 + +# Combined point and range keys (point key lower and upper bound). + +reset +---- + +load +a.SET.0: +rangekey: b-c:{#0,RANGEKEYSET,@1,foo)} +d.SET.0: +---- +file 0 + +update-files +---- +file 0: + combined: a#42,1-d#42,1 + points: a#42,1-d#42,1 + ranges: b#42,21-c#72057594037927935,21 + +# Combined point and range keys (point key lower and range key upper bound). + +reset +---- + +load +a.SET.0: +rangekey: b-c:{(#0,RANGEKEYSET,@1,foo)} +---- +file 0 + +update-files +---- +file 0: + combined: a#42,1-c#72057594037927935,21 + points: a#42,1-a#42,1 + ranges: b#42,21-c#72057594037927935,21 + +# Combined point and range keys (range key lower and point key upper bound). + +reset +---- + +load +rangekey: a-c:{#0,RANGEKEYSET,@1,foo)} +d.SET.0: +---- +file 0 + +update-files +---- +file 0: + combined: a#42,21-d#42,1 + points: d#42,1-d#42,1 + ranges: a#42,21-c#72057594037927935,21 + +# Combined point and range keys (range key lower and upper bound). + +reset +---- + +load +rangekey: a-d:{#0,RANGEKEYSET,@1,foo)} +c.SET.0: +---- +file 0 + +update-files +---- +file 0: + combined: a#42,21-d#72057594037927935,21 + points: c#42,1-c#42,1 + ranges: a#42,21-d#72057594037927935,21 diff --git a/pebble/testdata/ingested_flushable_api b/pebble/testdata/ingested_flushable_api new file mode 100644 index 0000000..48f8547 --- /dev/null +++ b/pebble/testdata/ingested_flushable_api @@ -0,0 +1,112 @@ +build ext1 +set c c +set e e +---- + +flushable ext1 +---- + +iter +---- +c#0,1 +e#0,1 + +rangekeyIter +---- + +rangedelIter +---- + +containsRangeKey +---- +false + +readyForFlush +---- +true + +reset +---- + +build ext2 +range-key-set d g 1 val1 +---- + +flushable ext2 +---- + +iter +---- + +rangekeyIter +---- +d-g:{(#0,RANGEKEYSET,1,val1)} + +containsRangeKey +---- +true + +rangedelIter +---- + +reset +---- + +build ext3 +del-range a j +del-range o z +---- + +flushable ext3 +---- + +iter +---- + +rangedelIter +---- +a-j:{(#0,RANGEDEL)} +o-z:{(#0,RANGEDEL)} + +rangekeyIter +---- + +readyForFlush +---- +true + +containsRangeKey +---- +false + +reset +---- + +build ext4 +del-range a j +set k kk +range-key-set y z 1 val1 +---- + +flushable ext4 +---- + +iter +---- +k#0,1 + +rangekeyIter +---- +y-z:{(#0,RANGEKEYSET,1,val1)} + +rangedelIter +---- +a-j:{(#0,RANGEDEL)} + +readyForFlush +---- +true + +containsRangeKey +---- +true diff --git a/pebble/testdata/internal_iter_bounds b/pebble/testdata/internal_iter_bounds new file mode 100644 index 0000000..f9fbd10 --- /dev/null +++ b/pebble/testdata/internal_iter_bounds @@ -0,0 +1,36 @@ +define +a.SET.1:1 +a.SET.2:2 +b.SET.1:1 +b.SET.2:2 +c.SET.1:1 +c.SET.2:2 +d.SET.2:2 +---- + +iter lower=b upper=c +seek-ge d +seek-prefix-ge d +seek-lt c +seek-lt b +next +prev +next +next +next +next +next +prev +---- +. +. +b:1 +. +a:2 +. +a:2 +a:1 +b:2 +b:1 +. +d:2 diff --git a/pebble/testdata/internal_iter_next b/pebble/testdata/internal_iter_next new file mode 100644 index 0000000..a66b8ed --- /dev/null +++ b/pebble/testdata/internal_iter_next @@ -0,0 +1,82 @@ +define +a.SET.1:1 +a.SET.2:2 +b.SET.1:1 +b.SET.2:2 +c.SET.1:1 +c.SET.2:2 +---- + +iter +seek-ge a +seek-ge b +seek-ge c +seek-ge d +seek-lt a +seek-lt b +seek-lt c +seek-lt d +seek-prefix-ge a +seek-prefix-ge b +seek-prefix-ge c +seek-prefix-ge d +---- +a:2 +b:2 +c:2 +. +. +a:1 +b:1 +c:1 +a:2 +b:2 +c:2 +. + +iter +first +next +next +prev +prev +prev +next +next +next +next +next +next +prev +prev +next +prev +prev +next +next +next +next +prev +---- +a:2 +a:1 +b:2 +a:1 +a:2 +. +a:2 +a:1 +b:2 +b:1 +c:2 +c:1 +c:2 +b:1 +c:2 +b:1 +b:2 +b:1 +c:2 +c:1 +. +c:1 diff --git a/pebble/testdata/iter_histories/clone b/pebble/testdata/iter_histories/clone new file mode 100644 index 0000000..5fcee11 --- /dev/null +++ b/pebble/testdata/iter_histories/clone @@ -0,0 +1,151 @@ +# Test iterator bounds provided via IterOptions. + +reset +---- + +batch commit +set a a +set b b +set c c +set d d +set f f +range-key-set a ap @6 foo +range-key-set ap c @5 bar +range-key-set cat zoo @3 bax +---- +committed 8 keys + +# Ensure bounds provided at initialization are respected, and propagated to +# cloned iterators. + +combined-iter lower=b upper=e +first +next +next +next +next +clone +first +next +next +next +next +---- +b: (b, [b-c) @5=bar UPDATED) +c: (c, . UPDATED) +cat: (., [cat-e) @3=bax UPDATED) +d: (d, [cat-e) @3=bax) +. +. +b: (b, [b-c) @5=bar UPDATED) +c: (c, . UPDATED) +cat: (., [cat-e) @3=bax UPDATED) +d: (d, [cat-e) @3=bax) +. + +# Ensure bounds provided during clone are propagated to cloned iterators. + +combined-iter lower=b upper=e +first +next +next +next +next +clone lower=a upper=cat key-types=both +first +next +next +next +clone lower=a upper=cat key-types=point +first +next +next +next +---- +b: (b, [b-c) @5=bar UPDATED) +c: (c, . UPDATED) +cat: (., [cat-e) @3=bax UPDATED) +d: (d, [cat-e) @3=bax) +. +. +a: (a, [a-ap) @6=foo UPDATED) +ap: (., [ap-c) @5=bar UPDATED) +b: (b, [ap-c) @5=bar) +c: (c, . UPDATED) +. +a: (a, .) +b: (b, .) +c: (c, .) +. + +# Test cloning an iterator that reads through an indexed batch. + +batch name=batchfoo +del b +set c c2 +range-key-unset b c @5 +---- +wrote 3 keys to batch "batchfoo" + +combined-iter reader=batchfoo name=itera +seek-ge b +seek-ge c +---- +c: (c2, .) +c: (c2, .) + +combined-iter +seek-ge b +seek-ge c +---- +b: (b, [ap-c) @5=bar UPDATED) +c: (c, . UPDATED) + +clone from=itera to=iterb refresh-batch=false +---- + +iter iter=iterb +seek-ge b +seek-ge c +---- +c: (c2, .) +c: (c2, .) + +mutate batch=batchfoo +set c c3 +range-key-set b c @9 final +---- + +iter iter=itera +seek-ge b +seek-ge c +---- +c: (c2, .) +c: (c2, .) + +iter iter=iterb +seek-ge b +seek-ge c +---- +c: (c2, .) +c: (c2, .) + +clone from=iterb to=iterc refresh-batch=false +---- + +iter iter=iterc +seek-ge b +seek-ge c +---- +c: (c2, .) +c: (c2, .) + +clone from=iterb to=iterd refresh-batch=true +---- + +iter iter=iterd +seek-ge b +seek-ge c +---- +b: (., [b-c) @9=final UPDATED) +c: (c3, . UPDATED) diff --git a/pebble/testdata/iter_histories/errors b/pebble/testdata/iter_histories/errors new file mode 100644 index 0000000..07668a9 --- /dev/null +++ b/pebble/testdata/iter_histories/errors @@ -0,0 +1,99 @@ +reset +---- + +batch commit +set a a +set b b +set c c +set d d +---- +committed 4 keys + +# Scan forward + +combined-iter +seek-ge a +next +next +next +next +---- +a: (a, .) +b: (b, .) +c: (c, .) +d: (d, .) +. + +reopen +---- + +combined-iter +first +next +next +next +next +---- +a: (a, .) +b: (b, .) +c: (c, .) +d: (d, .) +. + +reopen enable-table-stats=false inject-errors=((ErrInjected (And Reads (PathMatch "*.sst") (OnIndex 4)))) +---- + +combined-iter +first +first +next +next +next +next +---- +err=pebble: backing file 000004 error: injected error +a: (a, .) +b: (b, .) +c: (c, .) +d: (d, .) +. + +# Regression test for #2994. +# +# Previously, an error while loading an L0 sstable's range key block could +# result in an iterator that would always return the same error. Now, the IO is +# deferred to the first seek. If a seek encounters an IO error, re-seeking the +# iterator should re-attempt the failed IO operation, potentially succeeding if +# the IO error was transient. + +define auto-compactions=off +L0 + a.SET.9:a + rangekey:c-d:{(#0,RANGEKEYSET,@1,foo)} + e@2.SET.2:e@2 +---- +0.0: + 000004:[a#9,SET-e@2#2,SET] + +layout filename=000004.sst +---- + 0 data (38) + 43 index (35) + 83 range-key (29) + 117 properties (645) + 767 meta-index (57) + 829 footer (53) + 882 EOF + +# Inject an error on the first `ReadAt` call on 000004.sst's range key block +# (which is at offset 83). + +reopen auto-compactions=off enable-table-stats=false inject-errors=((ErrInjected (And (PathMatch "000004.sst") (OpFileReadAt 83) (OnIndex 0)))) +---- + +combined-iter +first +first +---- +err=injected error +a: (a, .) diff --git a/pebble/testdata/iter_histories/internal_next b/pebble/testdata/iter_histories/internal_next new file mode 100644 index 0000000..820a1dc --- /dev/null +++ b/pebble/testdata/iter_histories/internal_next @@ -0,0 +1,374 @@ +reset +---- + +# For all prefixes a-z, write 3 keys at timestamps @1, @10, @100. +# This populates a total of 26 * 3 = 78 keys. + +populate keylen=1 timestamps=(1, 10, 100) +---- +wrote 78 keys + +combined-iter +first +next-prefix +internal-next +internal-next +next +next-prefix +internal-next +internal-next +next +internal-next +next +internal-next +---- +a@100: (a@100, .) +b@100: (b@100, .) +. +. +b@10: (b@10, .) +c@100: (c@100, .) +. +. +c@10: (c@10, .) +. +c@1: (c@1, .) +. + +combined-iter +first +next-prefix +can-deterministically-single-delete +can-deterministically-single-delete +next +next-prefix +can-deterministically-single-delete +next +can-deterministically-single-delete +next +can-deterministically-single-delete +---- +a@100: (a@100, .) +b@100: (b@100, .) +true +err: pebble: CanDeterministicallySingleDelete called twice +b@10: (b@10, .) +c@100: (c@100, .) +true +c@10: (c@10, .) +true +c@1: (c@1, .) +true + +# The start boundaries of range keys are interleaved and can cause the internal +# iterator to be advanced ahead to look for a point at the same user key. This +# is one of the few situations in which InternalNext may find the iterator is +# already positioned at iterPosNext. Test this scenario. + +batch commit +range-key-set a b @1 foo +range-key-set bb c @2 bar +---- +committed 2 keys + +combined-iter +first +internal-next +next +internal-next +seek-ge b@10 +internal-next +next +internal-next +internal-next +next +---- +a: (., [a-b) @1=foo UPDATED) +. +a@100: (a@100, [a-b) @1=foo) +. +b@10: (b@10, . UPDATED) +. +b@1: (b@1, .) +. +. +bb: (., [bb-c) @2=bar UPDATED) + +combined-iter +first +can-deterministically-single-delete +next +can-deterministically-single-delete +seek-ge b@10 +can-deterministically-single-delete +next +can-deterministically-single-delete +next +---- +a: (., [a-b) @1=foo UPDATED) +true +a@100: (a@100, [a-b) @1=foo) +true +b@10: (b@10, . UPDATED) +true +b@1: (b@1, .) +true +bb: (., [bb-c) @2=bar UPDATED) + + +reset +---- + +batch commit +set a a +set b b +range-key-set b c @1 foo +set d d +---- +committed 4 keys + +combined-iter +first +internal-next +next +internal-next +next +prev +internal-next +---- +a: (a, .) +. +b: (b, [b-c) @1=foo UPDATED) +. +d: (d, . UPDATED) +b: (b, [b-c) @1=foo UPDATED) +err: switching from reverse to forward via internalNext is prohibited + +combined-iter +first +can-deterministically-single-delete +next +can-deterministically-single-delete +next +prev +can-deterministically-single-delete +---- +a: (a, .) +true +b: (b, [b-c) @1=foo UPDATED) +true +d: (d, . UPDATED) +b: (b, [b-c) @1=foo UPDATED) +err: switching from reverse to forward via internalNext is prohibited + +# Perform a test where we produce two internal versions (both SETs) for each +# user key. Note that this test disables automatic compactions, so the presence +# of the internal keys will be deterministic. + +reset +---- + +populate keylen=1 timestamps=(1, 10, 100) +---- +wrote 78 keys + +flush +---- + +populate keylen=1 timestamps=(1, 10, 100) +---- +wrote 78 keys + +combined-iter +first +next-prefix +internal-next +internal-next +next +next-prefix +internal-next +internal-next +next +internal-next +next +internal-next +---- +a@100: (a@100, .) +b@100: (b@100, .) +SET +. +b@10: (b@10, .) +c@100: (c@100, .) +SET +. +c@10: (c@10, .) +SET +c@1: (c@1, .) +SET + +combined-iter +seek-ge z +internal-next +next +next +internal-next +internal-next +next +internal-next +---- +z@100: (z@100, .) +SET +z@10: (z@10, .) +z@1: (z@1, .) +SET +. +. +. + +combined-iter +first +next-prefix +can-deterministically-single-delete +next +next-prefix +can-deterministically-single-delete +next +can-deterministically-single-delete +next +can-deterministically-single-delete +---- +a@100: (a@100, .) +b@100: (b@100, .) +false +b@10: (b@10, .) +c@100: (c@100, .) +false +c@10: (c@10, .) +false +c@1: (c@1, .) +false + +# Test that a CanDeterministicallySingleDelete is true if the old SETs are all +# deleted by a range delete. + +batch commit +del-range a zzz +---- +committed 1 keys + +populate keylen=1 timestamps=(1, 10, 100) +---- +wrote 78 keys + +combined-iter +first +next-prefix +can-deterministically-single-delete +next +next-prefix +can-deterministically-single-delete +next +can-deterministically-single-delete +next +can-deterministically-single-delete +---- +a@100: (a@100, .) +b@100: (b@100, .) +true +b@10: (b@10, .) +c@100: (c@100, .) +true +c@10: (c@10, .) +true +c@1: (c@1, .) +true + +# Set fmv=FormatDeleteSizedAndObsolete. + +reset format-major-version=15 +---- + +# Test that a SET > SINGLEDEL > SET sequence yields +# CanDeterministicallySingleDelete() = true. This is okay because if the SET +# consumes the SINGLEDEL, it becomes a SETWITHDEL. If a SINGLEDEL consumes a +# SETWITHDEL, it becomes a DEL. + +batch commit +set a a +singledel a +set a a +---- +committed 3 keys + +combined-iter +first +internal-next +internal-next +first +can-deterministically-single-delete +---- +a: (a, .) +SINGLEDEL +SET +a: (a, .) +true + +# Deleting with a DEL[SIZED] should then allow deterministic single delete +# again. + +batch commit +del a +set a a +---- +committed 2 keys + +combined-iter +first +internal-next +internal-next +internal-next +internal-next +internal-next +first +can-deterministically-single-delete +---- +a: (a, .) +DEL +SET +SINGLEDEL +SET +. +a: (a, .) +true + +# The above case tested DEL. Explicitly test DELSIZED by setting the key again, +# then writing a DELSIZED, then another key. + +batch commit +del-sized a 1 +set a a +---- +committed 2 keys + +combined-iter +first +internal-next +internal-next +internal-next +internal-next +internal-next +internal-next +internal-next +first +can-deterministically-single-delete +---- +a: (a, .) +DELSIZED +SET +DEL +SET +SINGLEDEL +SET +. +a: (a, .) +true diff --git a/pebble/testdata/iter_histories/iter_optimizations b/pebble/testdata/iter_histories/iter_optimizations new file mode 100644 index 0000000..33a7969 --- /dev/null +++ b/pebble/testdata/iter_histories/iter_optimizations @@ -0,0 +1,723 @@ +# Test repeated seeks into the same range key, while TrySeekUsingNext=true. +# Test for regression fixed in #1849. + +reset +---- + +batch commit +range-key-set a c @5 boop +range-key-set c e @5 beep +---- +committed 2 keys + +combined-iter +seek-ge a +seek-ge b +---- +a: (., [a-c) @5=boop UPDATED) +b: (., [a-c) @5=boop) + +# Ensure that no-op optimizations do not reuse range key iterator state across +# SetOptions calls. No-op optimizations have the potential to fail to update +# RangeKeyChanged(). + +reset +---- + +batch commit +range-key-set p s @1 foo +---- +committed 1 keys + +combined-iter lower=n@9 upper=x@5 +seek-lt y@3 +set-options lower=n@9 upper=x@5 +seek-lt-limit t o +---- +p: (., [p-s) @1=foo UPDATED) +. +p: valid (., [p-s) @1=foo UPDATED) + +combined-iter lower=n@9 upper=x@5 +seek-ge o +set-options lower=n@9 upper=x@5 +seek-ge oat +---- +p: (., [p-s) @1=foo UPDATED) +. +p: (., [p-s) @1=foo UPDATED) + +combined-iter lower=n@9 upper=x@5 +seek-prefix-ge p@5 +set-options lower=n@9 upper=x@5 +seek-prefix-ge p +---- +p@5: (., [p-"p\x00") @1=foo UPDATED) +. +p: (., [p-"p\x00") @1=foo UPDATED) + +# Regression test for #1963 / cockroachdb/cockroach#88296. +# +# The iterators in this test move their bounds monotonically forward +# [a,b)→[b,e). This enables the sstable iterator optimization for monotonically +# moving bounds (see boundsCmp in sstable/reader.go). With this optimization, +# the first seek after the SetBounds may use the fact that the bounds moved +# forward monotonically to avoid re-seeking within the index. +# +# The test cases below exercise a seek to a key, followed by a seek to a smaller +# key. The second seek should not make use of the bounds optimization because +# doing so may incorrectly skip all keys between the lower bound and the first +# seek key. Previously, the code paths that handled block-property filtering on +# a two-level iterator could leave the iterator in a state such that the second +# seek would improperly also exercise the monotonic bounds optimization. In the +# test cases below, this would result in the key 'b' not being found. Each test +# case exercises a different combination of seek-ge and seek-prefix-ge. + +reset block-size=1 index-block-size=1 +---- + +batch commit +set a a +set b b +set b@4 b@4 +set z@6 z@6 +---- +committed 4 keys + +flush +---- + +combined-iter lower=a upper=b point-key-filter=(1,4) +seek-ge a +set-bounds lower=b upper=e +seek-prefix-ge d@5 +seek-prefix-ge b +---- +a: (a, .) +. +. +b: (b, .) + +combined-iter lower=a upper=b point-key-filter=(1,4) +seek-ge a +set-bounds lower=b upper=e +seek-ge d@5 +seek-prefix-ge b +---- +a: (a, .) +. +. +b: (b, .) + +combined-iter lower=a upper=b point-key-filter=(1,4) +seek-ge a +set-bounds lower=b upper=e +seek-ge d@5 +seek-ge b +---- +a: (a, .) +. +. +b: (b, .) + +combined-iter lower=a upper=b point-key-filter=(1,4) +seek-ge a +set-bounds lower=b upper=e +seek-prefix-ge d@5 +seek-ge b +---- +a: (a, .) +. +. +b: (b, .) + +# Test a similar case with range key masking. The previous bug did not apply to +# this case, because range-key masking never skips blocks on a seek. + +reset block-size=1 index-block-size=1 +---- + +batch commit +set a a +set b b +set b@4 b@4 +set z@6 z@6 +range-key-set a z @9 v +---- +committed 5 keys + +flush +---- + +combined-iter lower=a upper=b mask-suffix=@10 mask-filter +seek-ge a +set-bounds lower=b upper=e +seek-prefix-ge d@5 +seek-ge b +---- +a: (a, [a-b) @9=v UPDATED) +. +d@5: (., [d-"d\x00") @9=v UPDATED) +b: (b, [b-e) @9=v UPDATED) + +# Test TrySeekUsingNext across no-op SetOptions when reading through an indexed +# batch with modifications. The seek-prefix-ges after the first should make use +# of the TrySeekUsingNext optimization. +# +# TODO(jackson): The iterator stats don't signal the use of try-seek-using-next, +# so we inspect lastPositioningOp as a proxy since that's the +# try-seek-using-next prerequisite that previously regressed. Is there a way to +# adapt to this test so that the absence of the try-seek-using-next optimization +# is visible in the iterator statistics? +# +# Regression test for cockroachdb/cockroach#88819. + +reset +---- + +batch commit +set b@5 b@5 +set c@3 c@3 +set d@9 d@9 +set e@8 e@8 +set f@8 f@8 +---- +committed 5 keys + +flush +---- + +batch name=foo +set g@4 g@4 +---- +wrote 1 keys to batch "foo" + +combined-iter reader=foo name=fooiter +inspect lastPositioningOp +seek-prefix-ge b@10 +stats +---- +lastPositioningOp="unknown" +b@5: (b@5, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 119B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +mutate batch=foo +set h@2 h@2 +---- + +iter iter=fooiter +set-options +inspect lastPositioningOp +seek-prefix-ge c@10 +stats +---- +. +lastPositioningOp="seekprefixge" +c@3: (c@3, .) +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 119B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 6B, value-bytes 6B, tombstoned 0))) + +mutate batch=foo +set i@1 i@1 +---- + +iter iter=fooiter +set-options +inspect lastPositioningOp +seek-prefix-ge d@10 +stats +---- +. +lastPositioningOp="seekprefixge" +d@9: (d@9, .) +stats: (interface (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 119B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 9B, value-bytes 9B, tombstoned 0))) + +mutate batch=foo +set j@6 j@6 +---- + +iter iter=fooiter +set-options +inspect lastPositioningOp +seek-prefix-ge e@10 +stats +---- +. +lastPositioningOp="seekprefixge" +e@8: (e@8, .) +stats: (interface (dir, seek, step): (fwd, 4, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 4, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 119B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 12B, value-bytes 12B, tombstoned 0))) + +# Ensure that a case eligible for TrySeekUsingNext across a SetOptions correctly +# sees new batch mutations. The batch iterator should ignore the +# TrySeekUsingNext designation. + +reset +---- + +batch commit +set b@3 b@3 +set c@3 c@3 +---- +committed 2 keys + +batch name=b1 +---- +wrote 0 keys to batch "b1" + +combined-iter name=i1 reader=b1 +seek-prefix-ge b@6 +---- +b@3: (b@3, .) + +mutate batch=b1 +set b@4 b@4 +---- + +iter iter=i1 +set-options +inspect lastPositioningOp +seek-prefix-ge b@5 +---- +. +lastPositioningOp="seekprefixge" +b@4: (b@4, .) + +# Similar case with SeekGE. + +iter iter=i1 +seek-ge b@2 +---- +c@3: (c@3, .) + +mutate batch=b1 +set c@9 c@9 +---- + +iter iter=i1 +set-options +inspect lastPositioningOp +seek-ge b@1 +---- +. +lastPositioningOp="seekge" +c@9: (c@9, .) + +# Test a case similar to the above, but with an intermediate switch to +# range-key-only iteration, so that the batchIter is not re-seeked. + +reset +---- + +batch commit +set b@5 b@5 +set c@3 c@3 +---- +committed 2 keys + +batch name=b1 +---- +wrote 0 keys to batch "b1" + +combined-iter name=i1 reader=b1 +seek-ge b@9 +---- +b@5: (b@5, .) + +mutate batch=b1 +set b@6 b@6 +---- + +iter iter=i1 +set-options key-types=range +seek-ge b@8 +set-options key-types=both +inspect lastPositioningOp +seek-ge b@7 +---- +. +. +. +lastPositioningOp="invalidate" +b@6: (b@6, .) + +reset +---- + +batch commit +set b@2 b@2 +set c@3 c@3 +---- +committed 2 keys + +batch name=b1 +---- +wrote 0 keys to batch "b1" + +combined-iter name=i1 reader=b1 +seek-prefix-ge b@1 +---- +. + +mutate batch=b1 +set c@4 c@4 +---- + +iter iter=i1 +set-options +inspect lastPositioningOp +seek-prefix-ge c@8 +---- +. +lastPositioningOp="seekprefixge" +c@4: (c@4, .) + +# Regression test for #2084. +# +# The optimization added in #2058 began using an enabled TrySeekUsingNext flag +# to avoid re-seeking within a level's file metadata. This optimization was +# dependent on the invariant that the iterator remained positioned at the +# previous seek key, so that a subsequent seek to a larger key does not need to +# backtrack. +# +# This invariant wasn't strictly preserved by the levelIter during SeekPrefixGE +# calls. During a SeekPrefixGE, the sstable iterator may return nil despite the +# existence of sstable keys greater than the seek key if the sstable's bloom +# filter excludes the seek prefix. If the sstable DOES NOT contain any range +# tombstones, the levelIter does not advance to the next file if the file's +# largest bound has a prefix larger than the seek prefix, returning nil, else it +# does advance since the next file could contain the seek prefix. +# +# However, if the file DOES contain range tombstones, the levelIter returns a +# synthetic largest boundary key so that the file remains open until the merging +# iterator passes beyond its bounds. This ensures the file's range deletions' +# effects on other keys are observed. If another level returned a key greater +# than this largest boundary key (eg, because the other level doesn't restrict +# results to the seek prefix), the merging iterator could step beyond the +# level's synthetic boundary key. This step could advance the levelIter to the +# next file, despite its irrelevance to the current prefix. This step would also +# break the invariant that the level iterator remained positioned at the seek +# key. +# +# The bug was fixed by comparing the synthetic boundary key to the seek prefix, +# avoiding ever Next-ing the level iterator beyond the seek prefix. + +# Set 100 bloom-filter bits per key to ensure the bloom-filter exclusivity +# checks successfully exclude prefixes that aren't present. +reset bloom-bits-per-key=100 +---- + +# [a -d) +# b@3 d@1 +batch commit +del-range a d +set b@3 b@3 +set d@1 d@1 +---- +committed 3 keys + +flush +---- + +# c@0 e@0 +batch commit +del c@0 +set e@0 e@0 +---- +committed 2 keys + +flush +---- + +lsm +---- +0.1: + 000007:[c@0#13,DEL-e@0#14,SET] +0.0: + 000005:[a#10,RANGEDEL-d@1#12,SET] + +# The first SeekPrefixGE(b@3) positions each level iterator over their +# respective files and correctly finds b@3. +# +# The second SeekPrefixGE(c@5) seeks in both files. The 0.0 level iterator finds +# that its file does not contain the prefix 'c', so it returns nil. Since the file +# contains a range deletion, it returns a synthetic boundary key with user key +# d@1 to ensure the file stays open until the iterator has moved beyond the +# file's bounds. The seek in level 0.1 finds a key with the prefix 'c': a point +# tombstone c@0#4,DEL. This gets bubbled up to the Iterator, which skips it +# because it's a point tombstone, nexting within 000007 to e@0#5. +# +# Previously, in the bug highlighted by #2084, the merging iterator would then +# see that level 0.0's synthetic boundary key at d@1 was at the top of the heap +# and move to the next file in 0.0. The subsequent call to SeekPrefixGE(d@1, +# TrySeekUsingNext=true) would incorrectly use the current position within the +# 0.0 file metadata (nil), and miss the d@1 key. + +combined-iter +seek-prefix-ge b@3 +seek-prefix-ge c@5 +seek-prefix-ge d@1 +---- +b@3: (b@3, .) +. +d@1: (d@1, .) + + +# Test an instance where unequal application of TrySeekUsingNext optimizations +# among a merging iterator's levels can result in surfacing deleted keys. +# Regression test for #2101. + +reset +---- + +batch commit +set b b +---- +committed 1 keys + +flush +---- + +compact a-h +---- +6: + 000005:[b#10,SET-b#10,SET] + +batch commit +set g g +---- +committed 1 keys + +flush +---- + +compact a-h +---- +6: + 000005:[b#10,SET-b#10,SET] + 000007:[g#11,SET-g#11,SET] + +batch commit +del-range b d +---- +committed 1 keys + +flush +---- + +batch commit +set e e +---- +committed 1 keys + +flush +---- + +lsm +---- +0.0: + 000009:[b#12,RANGEDEL-d#inf,RANGEDEL] + 000011:[e#13,SET-e#13,SET] +6: + 000005:[b#10,SET-b#10,SET] + 000007:[g#11,SET-g#11,SET] + +# The `seek-ge b` could incorrectly return `b` if the level 0.0 levelIter obeys +# the TrySeekUsingNext optimization but the level 6 levelIter does not. The +# TrySeekUsingNext optimization must be applied equally across all the levels of +# a merging iterator. + +combined-iter +seek-ge a +seek-ge b +---- +e: (e, .) +e: (e, .) + +# Regression test for #2118, where a MERGE pushes child iterators to the next +# key, and possibly past a file that contained a range tombstone that we +# should have paused at in a SeekPrefixGE, affecting future TrySeekUsingNexts. +# This test constructs this example (suffixes ignored), where square brackets +# consist of one SST: +# +# L0: [(b, MERGE) (c-d, RANGEDEL)] [(m, DEL)] +# L6: [(c, SET) (c-e, RANGEKEYSET)] [(j, SET)] +# +# We create an iterator with L6 filters enabled and create relatively large +# bloom filter blocks to reduce the false positive rate. Then we SeekPrefixGE(b) +# and end up with the L0 levelIter landing on the (b, MERGE), and the L6 iterator +# is exhausted as no SST filter blocks match the prefix. The top-level iterator +# then Next()s to find the next internal key at b if there is any, we land +# on the pause key at (d, RANGEDELSENTINEL). Crucially since there are no +# more items in the mergingIter heap and the merging iter is set to elide +# range tombstones, we Next() the level iter again as part of the same top-level +# iterator Next(), and land on (m, DEL). The type of the key here doesn't really +# matter. +# +# We then do a SeekPrefixGE(c), and since c > b, in the buggy scenario we +# TrySeekUsingNext. The bottom levelIter correctly finds the sstable containing +# the set, but the upper levelIter is already past the sstable containing the +# rangedel, so it just returns (m, DEL) again, and we surface the (c, SET) that +# should have been deleted. + +reset bloom-bits-per-key=100 +---- + +batch commit +set c@2 foo +range-key-set c e @5 bar +---- +committed 2 keys + +flush +---- + +compact a-z +---- +6: + 000005:[c#11,RANGEKEYSET-e#inf,RANGEKEYSET] + +batch commit +set j k +---- +committed 1 keys + +flush +---- + +compact a-z +---- +6: + 000005:[c#11,RANGEKEYSET-e#inf,RANGEKEYSET] + 000007:[j#12,SET-j#12,SET] + +batch commit +del-range c@2 d +merge b@2 g +---- +committed 2 keys + +flush +---- + +batch commit +del m +---- +committed 1 keys + +flush +---- + +lsm +---- +0.0: + 000009:[b@2#14,MERGE-d#inf,RANGEDEL] + 000011:[m#15,DEL-m#15,DEL] +6: + 000005:[c#11,RANGEKEYSET-e#inf,RANGEKEYSET] + 000007:[j#12,SET-j#12,SET] + +combined-iter upper=z@3 mask-suffix=@3 mask-filter use-l6-filter +seek-prefix-ge b@2 +seek-prefix-ge c@2 +---- +b@2: (g, .) +c@2: (., [c-"c\x00") @5=bar UPDATED) + +# Regression test for Cockroachdb#92205. This test constructs this scenario: +# +# A DEL in a middle level (L0.0) that we SeekPrefixGE directly for. Note that +# this DEL is not deleted by any range deletes; it gets exposed to the +# Iterator. There's a key after this DEL in the L0.0 levelIter, and there's a +# level above it (L0.1) that has a rangedel deleting that key, but not the DEL +# we SeekPrefixGE for. In the lowest level, there's a SET at L6 that is to the +# right of the DEL in L0.0, but is also not deleted by the RANGEDEL in L0.1. +# Our second SeekPrefixGE will be for this SET. Visualization, where square +# brackets are files: +# +# L0.1 [dd-ee#RANGEDEL] +# L0.0 [b#DEL e#SET] +# L6 [d#SET] [f#SET g#SET] +# +# When the Iterator encounters the above DEL internal key in the SeekPrefixGE, it +# calls Iterator.nextUserKey in the Iterator.findNextEntry call that was part of the +# SeekPrefixGE call. While Iterator.findNextEntry has a conditional to exit +# out of the loop if we're in prefix iteration and have gone past the prefix, +# this break only happens _after_ nextUserKey() has run. As a result we Next() +# the levelIter in L0.0, land on e#SET, and the mergingIter realizes that it +# is deleted by the rangedel in a higher level (L0.1). The mergingIter does not +# see d#SET because that sstable was excluded by the bloom filter. We then do a relative +# seek of all levels below L0.1 to ee (the end key of the rangedel), and in that +# process we advance the L6 levelIter to the second file. +# +# When we do the second SeekPrefixGE for d, the outer Iterator thinks d > b and +# so TrySeekUsingNext can work. However, the L6 levelIter has already advanced +# past the file containing d#SET, so we don't surface it even though we should +# have. + +reset bloom-bits-per-key=100 +---- + +batch commit +set d@4 foo +---- +committed 1 keys + +flush +---- + +compact a-f +---- +6: + 000005:[d@4#10,SET-d@4#10,SET] + +batch commit +set f@5 bar +set g@5 baz +---- +committed 2 keys + +flush +---- + +compact e-k +---- +6: + 000005:[d@4#10,SET-d@4#10,SET] + 000007:[f@5#11,SET-g@5#12,SET] + +batch commit +del b@5 +set e@4 foobar +---- +committed 2 keys + +flush +---- + +batch commit +del-range dd ee +---- +committed 1 keys + +flush +---- + +lsm +---- +0.1: + 000011:[dd#15,RANGEDEL-ee#inf,RANGEDEL] +0.0: + 000009:[b@5#13,DEL-e@4#14,SET] +6: + 000005:[d@4#10,SET-d@4#10,SET] + 000007:[f@5#11,SET-g@5#12,SET] + +combined-iter upper=z@3 use-l6-filter +seek-prefix-ge b@6 +seek-prefix-ge d@5 +---- +. +d@4: (foo, .) diff --git a/pebble/testdata/iter_histories/lazy_combined_iteration b/pebble/testdata/iter_histories/lazy_combined_iteration new file mode 100644 index 0000000..4392823 --- /dev/null +++ b/pebble/testdata/iter_histories/lazy_combined_iteration @@ -0,0 +1,279 @@ +# Test a lazy-combined iteration edge case. Consider the LSM: +# +# L5: 000003:[bar.DEL.3, foo.RANGEKEYSET.4] +# L6: 000001:[bar.SET.1] 000002:[bax.RANGEKEYSET.2] +# +# A call to First() seeks the levels to files L5.000003 and L6.000001. +# The L5 levelIter observes that L5.000003 contains the range key with +# start key `foo`, and triggers a switch to combined iteration, setting +# `combinedIterState.key` = `foo`. While switching to combined iteration, the +# iterator must recognize that `foo` > `bar`, and there may yet exist range keys +# that begin before `foo` (in this case `bax`). + +reset +---- + +batch commit +set bar bar +---- +committed 1 keys + +flush +---- + +batch commit +range-key-set bax zoo @1 foo +---- +committed 1 keys + +flush +---- + +batch commit +del bar +range-key-set foo zoo @2 bar +---- +committed 2 keys + +flush +---- + +lsm +---- +0.1: + 000009:[bar#12,DEL-zoo#inf,RANGEKEYSET] +0.0: + 000005:[bar#10,SET-bar#10,SET] + 000007:[bax#11,RANGEKEYSET-zoo#inf,RANGEKEYSET] + +# Assert that First correctly finds [bax,zoo), despite the discovery of +# [foo,zoo) triggering the switch to combined iteration. + +combined-iter +first +next +---- +bax: (., [bax-foo) @1=foo UPDATED) +foo: (., [foo-zoo) @2=bar, @1=foo UPDATED) + +# Test seeking into the middle of a range key during lazy-combined iteration. +# The iterator should surface Key() = the seek key. + +combined-iter +seek-ge bop +---- +bop: (., [bax-foo) @1=foo UPDATED) + +combined-iter +last +---- +foo: (., [foo-zoo) @2=bar, @1=foo UPDATED) + +# Test a lazy combined iterator that must next/prev through fileMetdata when +# skipping through a RANGEDEL. +# +# L5 +# b-----------------------y RANGEDEL +# L6 +# [a] [[d,e)@1] [[l,m)@1] [z] +# +# A SeekGE(k) must surface [l,m)@1 and a SeekLT(k) must surface [d,e)@1. + +reset +---- + +batch commit +set a a +---- +committed 1 keys + +flush +---- + +batch commit +set z z +---- +committed 1 keys + +flush +---- + +batch commit +range-key-set d e @1 foo +---- +committed 1 keys + +flush +---- + +batch commit +range-key-set l m @1 foo +---- +committed 1 keys + +flush +---- + +batch commit +del-range b y +---- +committed 1 keys + +flush +---- + +lsm +---- +0.1: + 000013:[b#14,RANGEDEL-y#inf,RANGEDEL] +0.0: + 000005:[a#10,SET-a#10,SET] + 000009:[d#12,RANGEKEYSET-e#inf,RANGEKEYSET] + 000011:[l#13,RANGEKEYSET-m#inf,RANGEKEYSET] + 000007:[z#11,SET-z#11,SET] + +combined-iter +seek-ge k +next +---- +l: (., [l-m) @1=foo UPDATED) +z: (z, . UPDATED) + +combined-iter +seek-lt k +prev +---- +d: (., [d-e) @1=foo UPDATED) +a: (a, . UPDATED) + + +reset +---- + +batch commit +set a a +set b b +set c c +set e e +range-key-del a f +range-key-unset a f @5 +---- +committed 6 keys + +flush +---- + +wait-table-stats +---- + +# The lazy iterator shouldn't switch to combined iteration when it encounters a +# file that is known to only contain RANGEKEYDELs and RANGEKEYUNSETs. + +combined-iter +is-using-combined +seek-ge a +seek-ge b +is-using-combined +---- +using lazy iterator +a: (a, .) +b: (b, .) +using lazy iterator + +# Write a range key to the memtable. The combined iterator should be forced to +# use non-lazy iteration. + +batch commit +range-key-set m z @5 foo +set s s +---- +committed 2 keys + +combined-iter +is-using-combined +seek-ge a +is-using-combined +seek-ge n +is-using-combined +---- +using combined (non-lazy) iterator +a: (a, .) +using combined (non-lazy) iterator +n: (., [m-z) @5=foo UPDATED) +using combined (non-lazy) iterator + +flush +---- + +# Now that the range key is flushed, a switch to combined iteration should only +# happen once the sstable containing the set is encountered. + +combined-iter +is-using-combined +seek-ge a +is-using-combined +seek-ge n +is-using-combined +---- +using lazy iterator +a: (a, .) +using lazy iterator +n: (., [m-z) @5=foo UPDATED) +using combined (non-lazy) iterator + +# Regression tests for #2210 metamorphic test failure. +# +# Lazy-combined iteration depends on individual point level iterators triggering +# a switch to combined iteration when they observe a file containing relevant +# range keys. Previously, this switch did not happen if the observed range +# keys all lied outside the current iteration prefix. +# +# This made it possible for a level to become positioned beyond the file +# containing range keys, without ever triggering the switch to combined +# iteration. A subsequent seek that made use of the TrySeekUsingNext +# optimization would never observe the file containing range keys, and omit the +# range keys. + +define +L6 + bax.DEL.9: +L6 + rangekey:c-d:{(#0,RANGEKEYSET,@1,foo)} +L6 + d@2.SET.2:v +---- +6: + 000004:[bax#9,DEL-bax#9,DEL] + 000005:[c#0,RANGEKEYSET-d#inf,RANGEKEYSET] + 000006:[d@2#2,SET-d@2#2,SET] + +combined-iter +seek-prefix-ge bax +seek-prefix-ge cat +---- +. +cat: (., [cat-"cat\x00") @1=foo UPDATED) + +# Another regression test for the #2210 metamorphic test failure, this one using +# a MERGE key to force the Iterator to step the internal iterator beyond the +# range key file. + +define +L6 + bax.MERGE.9:v +L6 + rangekey:c-d:{(#0,RANGEKEYSET,@1,foo)} +L6 + d@2.SET.2:v +---- +6: + 000004:[bax#9,MERGE-bax#9,MERGE] + 000005:[c#0,RANGEKEYSET-d#inf,RANGEKEYSET] + 000006:[d@2#2,SET-d@2#2,SET] + +combined-iter +seek-prefix-ge bax +seek-prefix-ge cat +---- +bax: (v, .) +cat: (., [cat-"cat\x00") @1=foo UPDATED) diff --git a/pebble/testdata/iter_histories/merge b/pebble/testdata/iter_histories/merge new file mode 100644 index 0000000..d9d8ec8 --- /dev/null +++ b/pebble/testdata/iter_histories/merge @@ -0,0 +1,37 @@ +# Test semantics of reading merge keys through the database and through a batch, +# both with an iterator and with Get. + +reset merger=appender +---- + +batch name=bar +merge k bar +---- +wrote 1 keys to batch "bar" + +batch commit +merge k foo +---- +committed 1 keys + +combined-iter +seek-ge k +---- +k: (foo, .) + +get +k +---- +k: foo + +combined-iter reader=bar +seek-ge k +seek-prefix-ge k +---- +k: (foobar, .) +k: (foobar, .) + +get reader=bar +k +---- +k: foobar diff --git a/pebble/testdata/iter_histories/next_prefix b/pebble/testdata/iter_histories/next_prefix new file mode 100644 index 0000000..cc74d47 --- /dev/null +++ b/pebble/testdata/iter_histories/next_prefix @@ -0,0 +1,292 @@ +reset +---- + +# For all prefixes a-z, write 3 keys at timestamps @1, @10, @100. +# This populates a total of 26 * 3 = 78 keys. + +populate keylen=1 timestamps=(1, 10, 100) +---- +wrote 78 keys + +combined-iter +first +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +---- +a@100: (a@100, .) +b@100: (b@100, .) +c@100: (c@100, .) +d@100: (d@100, .) +e@100: (e@100, .) +f@100: (f@100, .) +g@100: (g@100, .) +h@100: (h@100, .) +i@100: (i@100, .) +j@100: (j@100, .) +k@100: (k@100, .) +l@100: (l@100, .) +m@100: (m@100, .) +n@100: (n@100, .) +o@100: (o@100, .) +p@100: (p@100, .) + +combined-iter +seek-ge n@30 +next-prefix +next +next +next-prefix +---- +n@10: (n@10, .) +o@100: (o@100, .) +o@10: (o@10, .) +o@1: (o@1, .) +p@100: (p@100, .) + +combined-iter +seek-prefix-ge p@210 +next-prefix +---- +p@100: (p@100, .) +. + +combined-iter +seek-ge p@210 +next-prefix +seek-ge p@210 +next +next-prefix +seek-ge p@210 +next +next +next-prefix +---- +p@100: (p@100, .) +q@100: (q@100, .) +p@100: (p@100, .) +p@10: (p@10, .) +q@100: (q@100, .) +p@100: (p@100, .) +p@10: (p@10, .) +p@1: (p@1, .) +q@100: (q@100, .) + +reset target-file-size=1 +---- + +populate keylen=1 timestamps=(1, 10, 100) +---- +wrote 78 keys + +flush +---- + +combined-iter +first +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +next-prefix +---- +a@100: (a@100, .) +b@100: (b@100, .) +c@100: (c@100, .) +d@100: (d@100, .) +e@100: (e@100, .) +f@100: (f@100, .) +g@100: (g@100, .) +h@100: (h@100, .) +i@100: (i@100, .) +j@100: (j@100, .) +k@100: (k@100, .) +l@100: (l@100, .) +m@100: (m@100, .) +n@100: (n@100, .) +o@100: (o@100, .) +p@100: (p@100, .) + +combined-iter +seek-ge n@30 +next-prefix +next +next +next-prefix +---- +n@10: (n@10, .) +o@100: (o@100, .) +o@10: (o@10, .) +o@1: (o@1, .) +p@100: (p@100, .) + +combined-iter +seek-prefix-ge p@210 +next-prefix +---- +p@100: (p@100, .) +. + +combined-iter +seek-ge p@210 +next-prefix +seek-ge p@210 +next +next-prefix +seek-ge p@210 +next +next +next-prefix +---- +p@100: (p@100, .) +q@100: (q@100, .) +p@100: (p@100, .) +p@10: (p@10, .) +q@100: (q@100, .) +p@100: (p@100, .) +p@10: (p@10, .) +p@1: (p@1, .) +q@100: (q@100, .) + +batch commit +range-key-set p r @1 foo +---- +committed 1 keys + +combined-iter +seek-ge p@210 +next-prefix +---- +p@210: (., [p-r) @1=foo UPDATED) +q@100: (q@100, [p-r) @1=foo) + +combined-iter +seek-ge p@210 +next-prefix +seek-ge p@210 +next +next-prefix +seek-ge p@210 +next +next +next-prefix +---- +p@210: (., [p-r) @1=foo UPDATED) +q@100: (q@100, [p-r) @1=foo) +p@210: (., [p-r) @1=foo) +p@100: (p@100, [p-r) @1=foo) +q@100: (q@100, [p-r) @1=foo) +p@210: (., [p-r) @1=foo) +p@100: (p@100, [p-r) @1=foo) +p@10: (p@10, [p-r) @1=foo) +q@100: (q@100, [p-r) @1=foo) + +# Test an iterator that is positioned on a range key start of a prefix, and the +# next key is a point key with that same prefix. The interleaving iterator must +# correctly handle this case and advance the point key iterator. +combined-iter +seek-ge p +next-prefix +---- +p: (., [p-r) @1=foo UPDATED) +q@100: (q@100, [p-r) @1=foo) + +# Test switching directions via NextPrefix. +combined-iter +seek-ge p@100 +prev +next-prefix +---- +p@100: (p@100, [p-r) @1=foo UPDATED) +p: (., [p-r) @1=foo) +q@100: (q@100, [p-r) @1=foo) + +# Test switching directions via NextPrefix when the internal iterator is +# exhausted (in the reverse direction), but the Iterator is not. +# eg, i.pos = iterPosPrev and i.iterKey == nil. +combined-iter +seek-ge a@10 +prev +next-prefix +---- +a@10: (a@10, .) +a@100: (a@100, .) +b@100: (b@100, .) + +reset +---- + +populate keylen=1 timestamps=(1, 10, 100) +---- +wrote 78 keys + +flush +---- + +lsm +---- +0.0: + 000005:[a@100#12,SET-z@1#85,SET] + +# Test for https://github.com/cockroachdb/pebble/issues/2260. Triggered the +# bug. The second call to first would return c@100 instead of the correct key, +# b@1. +combined-iter upper=b@1 +first +next-prefix +next-prefix +set-bounds lower=b@1 upper=d +first +next +first +---- +a@100: (a@100, .) +err=NextPrefix not permitted with upper bound b@1 +err=NextPrefix not permitted with upper bound b@1 +. +b@1: (b@1, .) +c@100: (c@100, .) +b@1: (b@1, .) + +# Did not trigger https://github.com/cockroachdb/pebble/issues/2260 since +# Iterator.NextPrefix first does a Next. So the second call to NextPrefix +# returned after the Next, since the upper bound was reached, which left the +# Iterator positioned at b@1. +combined-iter upper=b@10 +first +next-prefix +next-prefix +set-bounds lower=b@1 upper=d +first +next +first +---- +a@100: (a@100, .) +err=NextPrefix not permitted with upper bound b@10 +err=NextPrefix not permitted with upper bound b@10 +. +b@1: (b@1, .) +c@100: (c@100, .) +b@1: (b@1, .) diff --git a/pebble/testdata/iter_histories/prefix_iteration b/pebble/testdata/iter_histories/prefix_iteration new file mode 100644 index 0000000..2481d3d --- /dev/null +++ b/pebble/testdata/iter_histories/prefix_iteration @@ -0,0 +1,335 @@ +# Regression test for a bug discovered in #1878. +# A lazy-combined iterator triggers combined iteration during an initial +# seek-prefix-ge call. The initial seek-prefix-ge call avoids defragmenting +# fragments beyond the initial fragment [c,f). A subsequent seek-ge that seeks +# within the bounds of the initial fragment [c,f) must not fall into the +# optimization that reuses the span without reseeking the keypsan iterator, +# because the span is not defragmented. +# +# In the bug surfaced by #1878, the initial seek-prefix-ge that switched to +# combined iteration failed to record that the iterator was now in prefix mode, +# allowing the subsequent seek-ge to incorrectly reuse the existing span. + +reset +---- + +batch commit +range-key-set a c @5 foo +---- +committed 1 keys + +flush +---- + +batch commit +range-key-set c f @5 foo +---- +committed 1 keys + +flush +---- + +batch commit +range-key-set f m @5 foo +---- +committed 1 keys + +flush +---- + +lsm +---- +0.0: + 000005:[a#10,RANGEKEYSET-c#inf,RANGEKEYSET] + 000007:[c#11,RANGEKEYSET-f#inf,RANGEKEYSET] + 000009:[f#12,RANGEKEYSET-m#inf,RANGEKEYSET] + +combined-iter +seek-prefix-ge d@5 +seek-ge d +---- +d@5: (., [d-"d\x00") @5=foo UPDATED) +d: (., [a-m) @5=foo UPDATED) + +# Test that repeated SeekPrefixGEs correctly return truncated spans with +# RangeKeyChanged() -> UPDATED. + +combined-iter +seek-prefix-ge c@5 +seek-prefix-ge d@5 +seek-ge d@7 +seek-prefix-ge d@7 +---- +c@5: (., [c-"c\x00") @5=foo UPDATED) +d@5: (., [d-"d\x00") @5=foo UPDATED) +d@7: (., [a-m) @5=foo UPDATED) +d@7: (., [d-"d\x00") @5=foo UPDATED) + +# Test a LSM with range keys fragmented within a prefix. +# This is a regression test for cockroachdb/cockroach#86102. + +reset target-file-size=1 +---- + +batch commit +range-key-set a c @1 bar +range-key-set c e @1 foo +set c@9 c@9 +set c@8 c@8 +set c@7 c@7 +set c@6 c@6 +set c@5 c@5 +set c@4 c@4 +set c@3 c@3 +set c@2 c@2 +set d@0 d@0 +range-key-set y z @1 foo +set z z +---- +committed 13 keys + +flush +---- + +lsm +---- +0.0: + 000005:[a#10,RANGEKEYSET-c@8#inf,RANGEKEYSET] + 000006:[c@8#13,SET-c@7#inf,RANGEKEYSET] + 000007:[c@7#14,SET-c@6#inf,RANGEKEYSET] + 000008:[c@6#15,SET-c@5#inf,RANGEKEYSET] + 000009:[c@5#16,SET-c@4#inf,RANGEKEYSET] + 000010:[c@4#17,SET-c@3#inf,RANGEKEYSET] + 000011:[c@3#18,SET-c@2#inf,RANGEKEYSET] + 000012:[c@2#19,SET-d@0#inf,RANGEKEYSET] + 000013:[d@0#20,SET-e#inf,RANGEKEYSET] + 000014:[y#21,RANGEKEYSET-z#22,SET] + +# The first seek-prefix-ge y@1 converts the iterator from lazy combined iterator +# to combined iteration. +# +# The second seek-prefix-ge d@1 does not fully defragment the range key. The +# underlying range key is defragmented to [c@2,e). This incomplete +# defragmentation is still hidden from the user at this point, since the range +# key is truncated to [d,d\x00). +# +# The third seek-prefix-ge c@0 seeks to a key that falls within the +# range key currently defragmented on interleaving iterator. A previous bug +# would use this span without defragmenting the span to include the full +# span of the prefix [c,c\x00). + +combined-iter +seek-prefix-ge y@1 +seek-prefix-ge d@1 +seek-prefix-ge c@0 +---- +y@1: (., [y-"y\x00") @1=foo UPDATED) +d@1: (., [d-"d\x00") @1=foo UPDATED) +c@0: (., [c-"c\x00") @1=foo UPDATED) + +# Test a LSM with range keys fragmented within a prefix. +# This is a regression test for cockroachdb/cockroach#86102. + +reset +---- + +ingest ext1 +range-key-set a c@8 @1 bar +set c@9 c@9 +---- + +ingest ext2 +range-key-set c@8 e @1 bar +set c@8 c@8 +set c@7 c@7 +set c@6 c@6 +set c@5 c@5 +set c@4 c@4 +set c@3 c@3 +set c@2 c@2 +---- + +ingest ext2 +range-key-set y z @1 foo +set z z +---- + +lsm +---- +6: + 000004:[a#10,RANGEKEYSET-c@8#inf,RANGEKEYSET] + 000005:[c@8#11,RANGEKEYSET-e#inf,RANGEKEYSET] + 000006:[y#12,RANGEKEYSET-z#12,SET] + + +# The first seek-prefix-ge y@1 converts the iterator from lazy combined iterator +# to combined iteration. +# +# The second seek-prefix-ge d@1 does not fully defragment the range key. The +# underlying range key is defragmented to [a,c@8). This incomplete +# defragmentation is still hidden from the user at this point, since the range +# key is truncated to [a,a\x00). +# +# The third seek-prefix-ge c@10 seeks to a key that falls within the +# range key currently defragmented on interleaving iterator. A previous bug +# would use this span without defragmenting the span to include the full +# span of the prefix [c,c\x00). + +combined-iter +seek-prefix-ge y@1 +seek-prefix-ge a@1 +seek-prefix-ge c@10 +---- +y@1: (., [y-"y\x00") @1=foo UPDATED) +a@1: (., [a-"a\x00") @1=bar UPDATED) +c@10: (., [c-"c\x00") @1=bar UPDATED) + +# Regression test for an invariant violation in the range key defragmenting +# iterator during prefix iteration. [Related to #1893]. There is a lot of +# subtlety here. Do not modify this test case without verifying that it still +# exercises the right conditions. +# +# Normally during forward iteration, if a switch to lazy-combined iteration is +# triggered, the lazy-combined iterator establishes a seek key for the range key +# iterator such that the seek key is: +# 1. greater than or equal to the key at previous iterator position. +# 2. less than or equal to the first range key with a start key greater than +# or equal to the previous iterator position. +# These invariants are important so that the range key iterator is positioned +# appropriately after the switch to combined iteration and no range keys are +# missed. +# +# Parts of the iterator stack depend on the above invariants. For example, +# during forward iteration the BoundedIter only checks span start keys against +# iterator bounds and the configured prefix, with the expectation that the seek +# is always already greater than or equal to the lower bound. In turn, the +# DefragmentingIter indirectly relies on the same invariant, because it requires +# a consistent view of the fragments. If the BoundedIter returns a span in one +# direction, but skips it when iterating back, the defragmenting iterator will +# end up on a different fragment. +# +# This test exercises a case in which previously, during prefix iteration, it +# was possible for the switch to lazy-combined iteration to trigger using a seek +# key k, such that there exist range key fragments between the current iterator +# position and k (violating the 2nd invariant up above). +# +# The sequence of events is: +# 1. SeekPrefixGE("b@9") = 'b@4': +# a. This seek positions the two levels, L0 and L6. The L0 iterator seeks +# to file 000006. This file does not contain any keys with the prefix +# "b", and the bloom filter must succeed in excluding the file. Since the +# file contains a range deletion, SeekPrefixGE returns the level's +# largest point key (`d#inf,RANGEDEL`) to ensure the file stays open until +# the iterator advances past the range deletion. +# b. In L6, the level iterator seeks to 000004 which contains a key with +# the prefix, returning 'b@4'. +# 2. Next(): +# a. Next advances the the L6 iterator to file 000005. This file contains a +# range key [e,f)@1=bar, which updates the lazy-combined iterator's +# state, recording the earliest observed range key as 'e'. The L6 level +# iterator then returns the file single point key 'c'. +# b. The merging iterator checks whether point key 'c' is deleted by any +# range key deletions. It is. It's deleted by L0's [c,d) range deletion. +# The merging iterator then seeks the iterator to the tombstone's end +# key 'd'. +# c. After seeking, the range deletion sentinel d is at the top of the +# heap. At this point, the merging iterator checks whether the keyspace +# of the prefix has been exceeded, and it has. It returns nil. +# 3. Switch to combined iteration: +# a. The Next has completed and triggered combined iteration. The only file +# containing range keys that was observed was 000005, containing the +# range key [e,f). The switch to combined iteration seeks the keyspan +# iterator to 'e'. Note that the iterator never observed L0's [d,e) +# range key that precedes [e,f) in the keyspace. +# b. Seeking the keyspan iterator calls DefragmentingIter.SeekLT('e'), +# which lands on the [d,e) fragment. This fragment does NOT check to see +# if the span starts at a prefix greater than the current prefix 'b', +# because only bounds in the direction of iteration are check. +# c. The DefragmentingIter observes disappearing range key fragments when +# it switches directions, as a result of (b). +# + +# Use 100-bits per key to ensure the bloom filter provides total recall. +reset bloom-bits-per-key=100 +---- + +# Ingest L6 files: +# +# 000004: b@4 +# 000005: c, [e,f)@1=bar + +ingest ext1 +set b@4 b@4 +---- + +ingest ext1 +set c c +range-key-set e f @1 bar +---- + +# Ingest L0 files: +# +# 000006: a, del-range(c, d) +# 000007: [d,e)@1=bar + +ingest ext2 +set a a +del-range c d +---- + +ingest ext3 +range-key-set d e @1 bar +---- + +lsm +---- +0.0: + 000006:[a#12,SET-d#inf,RANGEDEL] + 000007:[d#13,RANGEKEYSET-e#inf,RANGEKEYSET] +6: + 000004:[b@4#10,SET-b@4#10,SET] + 000005:[c#11,SET-f#inf,RANGEKEYSET] + +combined-iter +seek-prefix-ge b@9 +next +---- +b@4: (b@4, .) +. + +# Regression test for #2151. +# +# This test consists of two SeekPrefixGEs for ascending keys, which results in +# TrySeekUsingNext()=true for the second seek. The entirety of both seeked +# prefixes is deleted by the range deletion [b-d). The iterator being used is +# created from a snapshot at sequence number #4. At that sequence number, the +# iterator observes the range deletion and all of L6's point keys, but none of +# the point keys in L5. +# +# Previously, a bug existed where the SeekPrefixGE("b@9") would cause the +# iterator to next beyond the L5 sstable. The subsequent SeekPrefixGE with +# TrySeekUsingNext would mistakenly miss the range deletion [b-d) because it had +# already proceeded beyond the file. + +define snapshots=(4) +L5 + b.RANGEDEL.3:d + b@9.SET.9:v + c@9.SET.9:v + d@9.SET.9:v +L6 + b@2.SET.2:v + c@2.SET.2:v + d@2.SET.2:v +---- +5: + 000004:[b#3,RANGEDEL-d@9#9,SET] +6: + 000005:[b@2#2,SET-d@2#2,SET] + +combined-iter snapshot=4 +seek-prefix-ge b@9 +seek-prefix-ge c@9 +---- +. +. diff --git a/pebble/testdata/iter_histories/range_key_changed b/pebble/testdata/iter_histories/range_key_changed new file mode 100644 index 0000000..f4e6d64 --- /dev/null +++ b/pebble/testdata/iter_histories/range_key_changed @@ -0,0 +1,274 @@ +reset +---- + +populate keylen=2 timestamps=(1,3,5) +---- +wrote 2106 keys + +batch commit +range-key-set a d @1 foo +range-key-set d f @1 foo +range-key-set f g @2 bar +---- +committed 3 keys + +combined-iter +seek-ge ba@4 +next +next +prev +prev +seek-ge ba@1 +seek-ge ca@1 +seek-ge a +prev +next +prev +seek-ge a +seek-ge dog +seek-ge foo +seek-ge f +prev +next +seek-lt f@1 +---- +ba@4: (., [a-f) @1=foo UPDATED) +ba@3: (ba@3, [a-f) @1=foo) +ba@1: (ba@1, [a-f) @1=foo) +ba@3: (ba@3, [a-f) @1=foo) +ba@5: (ba@5, [a-f) @1=foo) +ba@1: (ba@1, [a-f) @1=foo) +ca@1: (ca@1, [a-f) @1=foo) +a: (., [a-f) @1=foo) +. +a: (., [a-f) @1=foo UPDATED) +. +a: (., [a-f) @1=foo UPDATED) +dog: (., [a-f) @1=foo) +foo: (., [f-g) @2=bar UPDATED) +f: (., [f-g) @2=bar) +ez@1: (ez@1, [a-f) @1=foo UPDATED) +f: (., [f-g) @2=bar UPDATED) +f@3: (f@3, [f-g) @2=bar) + +combined-iter +seek-prefix-ge ba@9 +seek-prefix-ge ba@5 +seek-prefix-ge ba@3 +next +seek-prefix-ge da@5 +next +next +next +seek-prefix-ge da@5 +---- +ba@9: (., [ba-"ba\x00") @1=foo UPDATED) +ba@5: (ba@5, [ba-"ba\x00") @1=foo) +ba@3: (ba@3, [ba-"ba\x00") @1=foo) +ba@1: (ba@1, [ba-"ba\x00") @1=foo) +da@5: (da@5, [da-"da\x00") @1=foo UPDATED) +da@3: (da@3, [da-"da\x00") @1=foo) +da@1: (da@1, [da-"da\x00") @1=foo) +. +da@5: (da@5, [da-"da\x00") @1=foo UPDATED) + +# Regression test for #1947 — Test a no-op call to SetBounds. Even if the +# underlying iterator doesn't need to be invalidated because the bounds didn't +# change, a subsequent Seek that finds the same range key must still report +# RangeKeyChanged() -> true. + +reset +---- + +batch commit +range-key-set a d @1 foo +---- +committed 1 keys + +combined-iter lower=a upper=z +last +set-bounds lower=a upper=z +last +set-bounds lower=a upper=z +first +set-bounds lower=a upper=z +seek-ge a +set-bounds lower=a upper=z +seek-lt z +set-bounds lower=a upper=z +seek-prefix-ge a +set-bounds lower=a upper=z +seek-prefix-ge a +---- +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-"a\x00") @1=foo UPDATED) +. +a: (., [a-"a\x00") @1=foo UPDATED) + +combined-iter lower=a upper=z +last +set-options lower=a upper=z +last +set-options lower=a upper=z +first +set-options lower=a upper=z +seek-ge a +set-options lower=a upper=z +seek-lt z +set-options lower=a upper=z +seek-prefix-ge a +set-options lower=a upper=z +seek-prefix-ge a +---- +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-"a\x00") @1=foo UPDATED) +. +a: (., [a-"a\x00") @1=foo UPDATED) + +# Regression test for #1950 — Test a no-op call to SeekGE/SeekLT after a +# SetBounds/SetOptions noop. The SetBounds/SetOptions noop made the iterator +# appear to be invalidated, but the internal iterator state was preserved. +# However, if the previous iterator state had a range key, this range key must +# be considered changed for the purpose of calculating RangeKeyChanged(). + +combined-iter lower=a upper=z +seek-lt z +set-bounds lower=a upper=z +seek-lt y +seek-ge 1 +set-bounds lower=a upper=z +seek-ge a +---- +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +a: (., [a-d) @1=foo) +. +a: (., [a-d) @1=foo UPDATED) + +combined-iter lower=a upper=z +seek-lt z +set-options lower=a upper=z +seek-lt y +seek-ge 1 +set-options lower=a upper=z +seek-ge a +---- +a: (., [a-d) @1=foo UPDATED) +. +a: (., [a-d) @1=foo UPDATED) +a: (., [a-d) @1=foo) +. +a: (., [a-d) @1=foo UPDATED) + + +# Similar to the above regression, test that a no-op correctly returns +# RangeKeyChanged()=false if there's no intervening SetOptions/SetBounds call. + +combined-iter lower=a upper=z +seek-lt z +seek-lt y +set-bounds lower=a upper=z +seek-ge 1 +seek-ge a +---- +a: (., [a-d) @1=foo UPDATED) +a: (., [a-d) @1=foo) +. +a: (., [a-d) @1=foo UPDATED) +a: (., [a-d) @1=foo) + +combined-iter lower=a upper=z +seek-lt z +seek-lt y +set-options lower=a upper=z +seek-ge 1 +seek-ge a +---- +a: (., [a-d) @1=foo UPDATED) +a: (., [a-d) @1=foo) +. +a: (., [a-d) @1=foo UPDATED) +a: (., [a-d) @1=foo) + +# Regression test for #1980. An iterator with RangeKeyChanged()=true that is +# then reconfigured to iterate over point keys should always return +# RangeKeyChanged()=false. + +reset +---- + +batch commit +range-key-set a b @1 foo +set c c +---- +committed 2 keys + +combined-iter +seek-ge a +set-options key-types=point +seek-ge c +---- +a: (., [a-b) @1=foo UPDATED) +. +c: (c, .) + +combined-iter +seek-ge a +set-options key-types=point +seek-prefix-ge c +---- +a: (., [a-b) @1=foo UPDATED) +. +c: (c, .) + +combined-iter +seek-ge a +set-options key-types=point +seek-lt cat +---- +a: (., [a-b) @1=foo UPDATED) +. +c: (c, .) + +combined-iter +seek-ge a +set-options key-types=point +last +---- +a: (., [a-b) @1=foo UPDATED) +. +c: (c, .) + +batch commit +range-key-del a b +range-key-set d e @1 foo +---- +committed 2 keys + +combined-iter +seek-ge d +set-options key-types=point +first +---- +d: (., [d-e) @1=foo UPDATED) +. +c: (c, .) diff --git a/pebble/testdata/iter_histories/range_key_masking b/pebble/testdata/iter_histories/range_key_masking new file mode 100644 index 0000000..f13c71a --- /dev/null +++ b/pebble/testdata/iter_histories/range_key_masking @@ -0,0 +1,243 @@ +reset +---- + +batch commit +range-key-set a d @8 boop +set a@2 a@2 +set a@3 a@3 +set a@9 a@9 +set a@10 a@10 +set b b +---- +committed 6 keys + +combined-iter +seek-prefix-ge a +next +next +next +next +next +---- +a: (., [a-"a\x00") @8=boop UPDATED) +a@10: (a@10, [a-"a\x00") @8=boop) +a@9: (a@9, [a-"a\x00") @8=boop) +a@3: (a@3, [a-"a\x00") @8=boop) +a@2: (a@2, [a-"a\x00") @8=boop) +. + +# Perform the above iteration with range-key masking enabled at a suffix equal +# to the range key's. The [a,d)@8 range key should serve as a masking, obscuring +# the points a@3 and a@2. + +combined-iter mask-suffix=@8 +seek-prefix-ge a +next +next +next +---- +a: (., [a-"a\x00") @8=boop UPDATED) +a@10: (a@10, [a-"a\x00") @8=boop) +a@9: (a@9, [a-"a\x00") @8=boop) +. + +# Perform the same thing but with a mask suffix below the range key's. All the +# points should be visible again. +# +# Then use SetOptions to raise the mask. The masked points should disappear. + +combined-iter mask-suffix=@7 +seek-prefix-ge a +next +next +next +next +next +set-options key-types=both mask-suffix=@8 +seek-prefix-ge a +next +next +next +---- +a: (., [a-"a\x00") @8=boop UPDATED) +a@10: (a@10, [a-"a\x00") @8=boop) +a@9: (a@9, [a-"a\x00") @8=boop) +a@3: (a@3, [a-"a\x00") @8=boop) +a@2: (a@2, [a-"a\x00") @8=boop) +. +. +a: (., [a-"a\x00") @8=boop UPDATED) +a@10: (a@10, [a-"a\x00") @8=boop) +a@9: (a@9, [a-"a\x00") @8=boop) +. + +# Test that switching out of prefix iteration correctly expands the bounds +# beyond the scope of the previous prefix. + +combined-iter +seek-prefix-ge a +next +seek-ge a@3 +---- +a: (., [a-"a\x00") @8=boop UPDATED) +a@10: (a@10, [a-"a\x00") @8=boop) +a@3: (a@3, [a-d) @8=boop UPDATED) + +# Test a range key masking case where the range key is not immediately +# masking point keys, but masks point keys once positioned beneath it. + +reset +---- + +batch commit +range-key-set d e @5 boop +set a@1 a1 +set b@3 b3 +set d@3 d3 +---- +committed 4 keys + +combined-iter mask-suffix=@9 +first +next +next +next +---- +a@1: (a1, .) +b@3: (b3, .) +d: (., [d-e) @5=boop UPDATED) +. + +# Test a broad range key that masks all the point keys. + +reset block-size=20 +---- + +batch commit +range-key-set a z @5 boop +set a@1 foo +set b@3 foo +set c@3 foo +set d@1 foo +set e@3 foo +set f@3 foo +set g@2 foo +set h@2 foo +set i@2 foo +set j@2 foo +set k@0 foo +set l@2 foo +set m@1 foo +set n@3 foo +set o@4 foo +set p@2 foo +set q@2 foo +set r@1 foo +set s@2 foo +set t@3 foo +set u@2 foo +set v@0 foo +set w@0 foo +set x@2 foo +set y@4 foo +---- +committed 26 keys + +flush +---- + +combined-iter mask-suffix=@9 +first +next +stats +---- +a: (., [a-z) @5=boop UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 1.1KB, cached 0B, read-time 0s)), (points: (count 25, key-bytes 75B, value-bytes 75B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 25, skipped 25))) + +# Repeat the above test, but with an iterator that uses a block-property filter +# mask. The internal stats should reflect fewer bytes read and fewer points +# visited by the internal iterators. + +combined-iter mask-suffix=@9 mask-filter +first +next +stats +---- +a: (., [a-z) @5=boop UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 514B, cached 514B, read-time 0s)), (points: (count 2, key-bytes 6B, value-bytes 6B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 2, skipped 2))) + +# Perform a similar comparison in reverse. + +combined-iter mask-suffix=@9 +last +prev +stats +---- +a: (., [a-z) @5=boop UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 1.1KB, cached 1.1KB, read-time 0s)), (points: (count 25, key-bytes 75B, value-bytes 75B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 25, skipped 25))) + +combined-iter mask-suffix=@9 mask-filter +last +prev +stats +---- +a: (., [a-z) @5=boop UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 514B, cached 514B, read-time 0s)), (points: (count 2, key-bytes 6B, value-bytes 6B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 2, skipped 2))) + +# Perform similar comparisons with seeks. + +combined-iter mask-suffix=@9 +seek-ge m +next +stats +---- +m: (., [a-z) @5=boop UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 789B, cached 789B, read-time 0s)), (points: (count 13, key-bytes 39B, value-bytes 39B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 13, skipped 13))) + +combined-iter mask-suffix=@9 mask-filter +seek-ge m +next +stats +---- +m: (., [a-z) @5=boop UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 514B, cached 514B, read-time 0s)), (points: (count 2, key-bytes 6B, value-bytes 6B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 2, skipped 2))) + +combined-iter mask-suffix=@9 +seek-lt m +prev +stats +---- +a: (., [a-z) @5=boop UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 789B, cached 789B, read-time 0s)), (points: (count 12, key-bytes 36B, value-bytes 36B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 12, skipped 12))) + +combined-iter mask-suffix=@9 mask-filter +seek-lt m +prev +stats +---- +a: (., [a-z) @5=boop UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 539B, cached 539B, read-time 0s)), (points: (count 2, key-bytes 6B, value-bytes 6B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 2, skipped 2))) diff --git a/pebble/testdata/iter_histories/range_keys_simple b/pebble/testdata/iter_histories/range_keys_simple new file mode 100644 index 0000000..6748821 --- /dev/null +++ b/pebble/testdata/iter_histories/range_keys_simple @@ -0,0 +1,508 @@ +reset +---- + +# Use the key string as the value so that it's easy to tell when we surface the +# wrong value. + +batch commit +set a a +set b b +set c c +set d d +range-key-set b c @5 boop +range-key-set cat dog @3 beep +---- +committed 6 keys + +# Scan forward + +combined-iter +seek-ge a +next +next +next +next +next +---- +a: (a, .) +b: (b, [b-c) @5=boop UPDATED) +c: (c, . UPDATED) +cat: (., [cat-dog) @3=beep UPDATED) +d: (d, [cat-dog) @3=beep) +. + +# Do the above forward iteration but with a mask suffix. The results should be +# identical despite range keys serving as masks, because none of the point keys +# have suffixes. + +combined-iter mask-suffix=@9 +seek-ge a +next +next +next +next +next +---- +a: (a, .) +b: (b, [b-c) @5=boop UPDATED) +c: (c, . UPDATED) +cat: (., [cat-dog) @3=beep UPDATED) +d: (d, [cat-dog) @3=beep) +. + +# Scan backward + +combined-iter +seek-lt z +prev +prev +prev +prev +prev +---- +d: (d, [cat-dog) @3=beep UPDATED) +cat: (., [cat-dog) @3=beep) +c: (c, . UPDATED) +b: (b, [b-c) @5=boop UPDATED) +a: (a, . UPDATED) +. + +combined-iter +seek-ge ace +seek-ge b +seek-ge c +seek-ge cab +seek-ge cat +seek-ge d +seek-ge day +seek-ge dog +---- +b: (b, [b-c) @5=boop UPDATED) +b: (b, [b-c) @5=boop) +c: (c, . UPDATED) +cat: (., [cat-dog) @3=beep UPDATED) +cat: (., [cat-dog) @3=beep) +d: (d, [cat-dog) @3=beep) +day: (., [cat-dog) @3=beep) +. + +combined-iter +seek-lt 1 +seek-lt ace +seek-lt b +seek-lt c +seek-lt cab +seek-lt cat +seek-lt d +seek-lt day +seek-lt dog +seek-lt zebra +---- +. +a: (a, .) +a: (a, .) +b: (b, [b-c) @5=boop UPDATED) +c: (c, . UPDATED) +c: (c, .) +cat: (., [cat-dog) @3=beep UPDATED) +d: (d, [cat-dog) @3=beep) +d: (d, [cat-dog) @3=beep) +d: (d, [cat-dog) @3=beep) + +rangekey-iter +first +next +next +set-bounds lower=bat upper=catatonic +first +next +next +---- +b [b-c) @5=boop UPDATED +cat [cat-dog) @3=beep UPDATED +. +. +bat [bat-c) @5=boop UPDATED +cat [cat-catatonic) @3=beep UPDATED +. + +rangekey-iter +seek-ge bat +---- +bat [b-c) @5=boop UPDATED + +# Delete 'b': The Iterator should still stop at b because of the range key +# with a start boundary at b. + +batch commit +del b +---- +committed 1 keys + +combined-iter +seek-ge b +seek-ge ace +---- +b: (., [b-c) @5=boop UPDATED) +b: (., [b-c) @5=boop) + +rangekey-iter +seek-ge b +seek-ge ace +---- +b [b-c) @5=boop UPDATED +b [b-c) @5=boop + +# Delete the b-c range key and the beginning of the cat-dog range key, +# truncating it to now begin at 'd'. + +batch name=indexed +range-key-del b d +---- +wrote 1 keys to batch "indexed" + +# Reading through the indexed batch, we should see the beginning of the cat-dog +# range key now beginning at 'd'. + +combined-iter reader=indexed +seek-ge b +next +---- +c: (c, .) +d: (d, [d-dog) @3=beep UPDATED) + +commit batch=indexed +---- +committed 1 keys + +# Reading through the database after applying the batch, we should still see the +# beginning of the cat-dog range key now beginning at 'd'. + +combined-iter +seek-ge b +next +---- +c: (c, .) +d: (d, [d-dog) @3=beep UPDATED) + +# Reading through the database after flushing, we should still see the +# beginning of the cat-dog range key now beginning at 'd'. + +flush +---- + +combined-iter +seek-ge b +next +---- +c: (c, .) +d: (d, [d-dog) @3=beep UPDATED) + + +reset +---- + +batch commit +range-key-set c d @1 boop +range-key-set apple c @3 beep +range-key-set ace apple @3 beep +set a a1 +set b b1 +set c c1 +del a +set b b2 +set c c2 +---- +committed 9 keys + +# Test that reverse iteration surfaces range key start boundaries alongside +# point keys at the same key, and defragments logically equivalent ranges. + +combined-iter +last +prev +prev +prev +---- +c: (c2, [c-d) @1=boop UPDATED) +b: (b2, [ace-c) @3=beep UPDATED) +ace: (., [ace-c) @3=beep) +. + +# Test that forward iteration surfaces range key start boundaries alongside +# point keys at the same key, and defragments logically equivalent ranges. + +combined-iter +first +next +next +next +---- +ace: (., [ace-c) @3=beep UPDATED) +b: (b2, [ace-c) @3=beep) +c: (c2, [c-d) @1=boop UPDATED) +. + +# NB: seek-prefix-ge truncates bounds to the prefix. + +combined-iter +seek-prefix-ge b +next +---- +b: (b2, [b-"b\x00") @3=beep UPDATED) +. + +reset +---- + +# For all prefixes a, aa, ab, ... zz, write 3 keys at timestamps @1, @10, @100. +# This populates a total of (26**2 + 26) * 3 = 2106 keys. + +populate keylen=2 timestamps=(1, 10, 100) +---- +wrote 2106 keys + +batch commit +range-key-set b c @5 beep +range-key-unset c d @1 +range-key-del d e +---- +committed 3 keys + +flush +---- + +metrics +---- +Metrics.Keys.RangeKeySetsCount = 1 + +combined-iter +seek-ge az +next +next +next +next +next +seek-ge bz@10 +next +next +---- +az@100: (az@100, .) +az@10: (az@10, .) +az@1: (az@1, .) +b: (., [b-c) @5=beep UPDATED) +b@100: (b@100, [b-c) @5=beep) +b@10: (b@10, [b-c) @5=beep) +bz@10: (bz@10, [b-c) @5=beep) +bz@1: (bz@1, [b-c) @5=beep) +c@100: (c@100, . UPDATED) + +# Perform the same iteration with all range keys serving as masks. The bz@1 +# point key should be elided. + +combined-iter mask-suffix=@100 +seek-ge az +next +next +next +next +next +seek-ge bz@10 +next +next +---- +az@100: (az@100, .) +az@10: (az@10, .) +az@1: (az@1, .) +b: (., [b-c) @5=beep UPDATED) +b@100: (b@100, [b-c) @5=beep) +b@10: (b@10, [b-c) @5=beep) +bz@10: (bz@10, [b-c) @5=beep) +c@100: (c@100, . UPDATED) +c@10: (c@10, .) + +# Ensure that a cloned iterator includes range keys. + +combined-iter +seek-ge bz@10 +clone +seek-ge bz@10 +---- +bz@10: (bz@10, [b-c) @5=beep UPDATED) +. +bz@10: (bz@10, [b-c) @5=beep UPDATED) + +# Within a batch, later writes overwrite earlier writes. Here, the range-key-del +# of [bat, bus) overwrites the earlier writes of [b,c) and [b,e). + +batch commit +range-key-set b c @5 beep +range-key-set b e @1 bop +range-key-set c z @1000 boop +range-key-del bat bus +---- +committed 4 keys + +flush +---- + +lsm +---- +0.1: + 000008:[b#2120,RANGEKEYSET-z#inf,RANGEKEYSET] +0.0: + 000006:[a@100#12,SET-zz@1#2113,SET] + +scan-rangekeys +---- +[b, bat) + @5=beep, @1=bop +[bus, c) + @5=beep, @1=bop +[c, e) + @1000=boop, @1=bop +[e, z) + @1000=boop + +# NB: There are now 8 range key sets in the database. See the 7 range keys in +# the above scan-rangekeys. Additionally, the sstable flushed earlier up above +# included a rangekeyset [b,c) @5=beep. + +metrics +---- +Metrics.Keys.RangeKeySetsCount = 8 + + +combined-iter +seek-prefix-ge ca +next +seek-prefix-ge ca@100 +---- +ca: (., [ca-"ca\x00") @1000=boop, @1=bop UPDATED) +ca@100: (ca@100, [ca-"ca\x00") @1000=boop, @1=bop) +ca@100: (ca@100, [ca-"ca\x00") @1000=boop, @1=bop) + + +# Perform the same iteration as above, but with @1000 range-key masking. The +# previously encountered point keys should be elided. + +combined-iter mask-suffix=@1000 +seek-prefix-ge ca +next +seek-prefix-ge ca@100 +---- +ca: (., [ca-"ca\x00") @1000=boop, @1=bop UPDATED) +. +ca@100: (., [ca-"ca\x00") @1000=boop, @1=bop UPDATED) + +# Test masked, non-prefixed iteration. We should see the range keys, but all the +# points should be masked except those beginning with z which were excluded by +# the range key's exclusive z end bound. + +combined-iter mask-suffix=@1000 +seek-ge ca +next +next +next +next +next +---- +ca: (., [c-e) @1000=boop, @1=bop UPDATED) +e: (., [e-z) @1000=boop UPDATED) +z@100: (z@100, . UPDATED) +z@10: (z@10, .) +z@1: (z@1, .) +za@100: (za@100, .) + +# Applying range keys to a DB running with a version that doesn't support them +# results in an error. Range keys were added in version 7. +reset format-major-version=6 +---- + +batch commit +range-key-set a z @5 boop +---- +pebble: batch requires at least format major version 8 (current: 6) + +# Constructing iterator over range keys on a DB that doesn't support them +# results in an error. + +reset format-major-version=6 +---- + +combined-iter +---- +pebble: range keys require at least format major version 8 (current: 6) + +# Test Prev-ing back over a synthetic range key marker. Synthetic range-key +# markers (the keys interleaved at 'c' during a SeekGE(c) when there's a +# straddling range key) are ephemeral, and Prev-ing back must move back the +# appropriate number of times. + +reset +---- + +batch commit +set a a +range-key-set b e @1 foo +---- +committed 2 keys + +flush +---- + +combined-iter +seek-ge b +prev +seek-ge c +prev +---- +b: (., [b-e) @1=foo UPDATED) +a: (a, . UPDATED) +c: (., [b-e) @1=foo UPDATED) +b: (., [b-e) @1=foo) + +define +L6 +a.RANGEDEL.3:z +rangekey:b-d:{(#5,RANGEKEYSET,@2,foo)} +---- +6: + 000004:[a#3,RANGEDEL-z#inf,RANGEDEL] + +combined-iter +seek-lt apple +---- +. + +combined-iter +seek-ge apple +seek-ge z +seek-lt apple +seek-lt z +first +last +---- +b: (., [b-d) @2=foo UPDATED) +. +. +b: (., [b-d) @2=foo UPDATED) +b: (., [b-d) @2=foo) +b: (., [b-d) @2=foo) + +combined-iter +seek-ge apple +prev +last +next +prev +seek-lt c +prev +---- +b: (., [b-d) @2=foo UPDATED) +. +b: (., [b-d) @2=foo UPDATED) +. +b: (., [b-d) @2=foo UPDATED) +b: (., [b-d) @2=foo) +. diff --git a/pebble/testdata/iter_histories/set_options b/pebble/testdata/iter_histories/set_options new file mode 100644 index 0000000..f344773 --- /dev/null +++ b/pebble/testdata/iter_histories/set_options @@ -0,0 +1,56 @@ +# Ensure bounds and key-types provided through SetOptions are respected. + +reset +---- + +batch commit +set a a +set b b +set c c +set d d +set f f +range-key-set a ap @6 foo +range-key-set ap c @5 bar +range-key-set cat zoo @3 bax +---- +committed 8 keys + +combined-iter lower=b upper=e +first +next +next +next +next +set-options lower=a upper=cat key-types=both +first +next +next +next +set-options lower=a upper=cat key-types=point +first +next +next +next +---- +b: (b, [b-c) @5=bar UPDATED) +c: (c, . UPDATED) +cat: (., [cat-e) @3=bax UPDATED) +d: (d, [cat-e) @3=bax) +. +. +a: (a, [a-ap) @6=foo UPDATED) +ap: (., [ap-c) @5=bar UPDATED) +b: (b, [ap-c) @5=bar) +c: (c, . UPDATED) +. +a: (a, .) +b: (b, .) +c: (c, .) +. + +flush +---- + +metrics +---- +Metrics.Keys.RangeKeySetsCount = 3 diff --git a/pebble/testdata/iter_histories/skip_point b/pebble/testdata/iter_histories/skip_point new file mode 100644 index 0000000..1ed27a7 --- /dev/null +++ b/pebble/testdata/iter_histories/skip_point @@ -0,0 +1,115 @@ +reset +---- + +populate keylen=1 timestamps=(1, 10, 100) +---- +wrote 78 keys + +# With a filter [20,30) all keys should be hidden, in both forward and reverse +# iteration directions. + +combined-iter point-key-filter=(20,30) +first +last +seek-ge d +seek-lt m +---- +. +. +. +. + +# With a filter [1,2) only the keys @1 should be visible. +# Test forward direction. + +combined-iter point-key-filter=(1,2) +first +next +next +next +next +next +next +next +next +next +---- +a@1: (a@1, .) +b@1: (b@1, .) +c@1: (c@1, .) +d@1: (d@1, .) +e@1: (e@1, .) +f@1: (f@1, .) +g@1: (g@1, .) +h@1: (h@1, .) +i@1: (i@1, .) +j@1: (j@1, .) + + +# With a filter [1,2) only the keys @1 should be visible. +# And reverse direction. + +combined-iter point-key-filter=(1,2) +last +prev +prev +prev +prev +prev +prev +prev +prev +prev +---- +z@1: (z@1, .) +y@1: (y@1, .) +x@1: (x@1, .) +w@1: (w@1, .) +v@1: (v@1, .) +u@1: (u@1, .) +t@1: (t@1, .) +s@1: (s@1, .) +r@1: (r@1, .) +q@1: (q@1, .) + +# With an expansive filter, all keys should be visible. + +combined-iter point-key-filter=(0,1000) +first +next +seek-ge m +prev +seek-lt m +next +last +---- +a@100: (a@100, .) +a@10: (a@10, .) +m@100: (m@100, .) +l@1: (l@1, .) +l@1: (l@1, .) +m@100: (m@100, .) +z@1: (z@1, .) + +# Test the case where a range key [a,z) is truncated to a seek key +# at which there exists a point key, but the point key should be skipped. +# The seek should stop at the seek key, but show no visible point key. + +batch commit +range-key-set a z @5 boop +---- +committed 1 keys + +combined-iter point-key-filter=(9,12) +seek-ge c@1 +---- +c@1: (., [a-z) @5=boop UPDATED) + + +# Try the same scenario, but this time with a filter that should NOT skip the +# point key. + +combined-iter point-key-filter=(1,12) +seek-ge c@1 +---- +c@1: (c@1, [a-z) @5=boop UPDATED) diff --git a/pebble/testdata/iter_histories/stats_no_invariants b/pebble/testdata/iter_histories/stats_no_invariants new file mode 100644 index 0000000..fb64b72 --- /dev/null +++ b/pebble/testdata/iter_histories/stats_no_invariants @@ -0,0 +1,159 @@ +reset +---- + +# Use the key string as the value so that it's easy to tell when we surface the +# wrong value. + +batch commit +set a a +set b b +set c c +set d d +range-key-set b c @5 boop +range-key-set cat dog @3 beep +---- +committed 6 keys + +flush +---- + +# Scan forward + +combined-iter +stats +seek-ge a +next +stats +next +next +next +next +stats +---- +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 0, 0)) +a: (a, .) +b: (b, [b-c) @5=boop UPDATED) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 89B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))), +(range-key-stats: (count 1), (contained points: (count 1, skipped 0))) +c: (c, . UPDATED) +cat: (., [cat-dog) @3=beep UPDATED) +d: (d, [cat-dog) @3=beep) +. +stats: (interface (dir, seek, step): (fwd, 1, 5), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 6), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 89B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))), +(range-key-stats: (count 2), (contained points: (count 2, skipped 0))) + +# Do the above forward iteration but with a mask suffix. The results should be +# identical despite range keys serving as masks, because none of the point keys +# have suffixes. + +combined-iter mask-suffix=@9 +seek-ge a +next +next +next +next +next +stats +---- +a: (a, .) +b: (b, [b-c) @5=boop UPDATED) +c: (c, . UPDATED) +cat: (., [cat-dog) @3=beep UPDATED) +d: (d, [cat-dog) @3=beep) +. +stats: (interface (dir, seek, step): (fwd, 1, 5), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 6), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 89B, cached 89B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))), +(range-key-stats: (count 2), (contained points: (count 2, skipped 0))) + +# Scan backward + +combined-iter +seek-lt z +prev +prev +prev +prev +prev +stats +---- +d: (d, [cat-dog) @3=beep UPDATED) +cat: (., [cat-dog) @3=beep) +c: (c, . UPDATED) +b: (b, [b-c) @5=boop UPDATED) +a: (a, . UPDATED) +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 5)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 6)), +(internal-stats: (block-bytes: (total 89B, cached 89B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))), +(range-key-stats: (count 2), (contained points: (count 2, skipped 0))) + +combined-iter +seek-ge ace +seek-ge b +seek-ge c +seek-ge cab +seek-ge cat +seek-ge d +seek-ge day +seek-ge dog +stats +---- +b: (b, [b-c) @5=boop UPDATED) +b: (b, [b-c) @5=boop) +c: (c, . UPDATED) +cat: (., [cat-dog) @3=beep UPDATED) +cat: (., [cat-dog) @3=beep) +d: (d, [cat-dog) @3=beep) +day: (., [cat-dog) @3=beep) +. +stats: (interface (dir, seek, step): (fwd, 8, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 6, 4), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 89B, cached 89B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))), +(range-key-stats: (count 2), (contained points: (count 3, skipped 0))) + +combined-iter +seek-lt 1 +seek-lt ace +seek-lt b +seek-lt c +seek-lt cab +seek-lt cat +seek-lt d +seek-lt day +seek-lt dog +seek-lt zebra +stats +---- +. +a: (a, .) +a: (a, .) +b: (b, [b-c) @5=boop UPDATED) +c: (c, . UPDATED) +c: (c, .) +cat: (., [cat-dog) @3=beep UPDATED) +d: (d, [cat-dog) @3=beep) +d: (d, [cat-dog) @3=beep) +d: (d, [cat-dog) @3=beep) +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 10, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 10, 10)), +(internal-stats: (block-bytes: (total 267B, cached 267B, read-time 0s)), (points: (count 15, key-bytes 15B, value-bytes 15B, tombstoned 0))), +(range-key-stats: (count 2), (contained points: (count 6, skipped 0))) + +rangekey-iter +first +next +next +set-bounds lower=bat upper=catatonic +first +next +next +stats +---- +b [b-c) @5=boop UPDATED +cat [cat-dog) @3=beep UPDATED +. +. +bat [bat-c) @5=boop UPDATED +cat [cat-catatonic) @3=beep UPDATED +. +stats: (interface (dir, seek, step): (fwd, 2, 4), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 4), (rev, 0, 0)), +(range-key-stats: (count 4), (contained points: (count 0, skipped 0))) diff --git a/pebble/testdata/iter_histories/with_limit b/pebble/testdata/iter_histories/with_limit new file mode 100644 index 0000000..359f0b2 --- /dev/null +++ b/pebble/testdata/iter_histories/with_limit @@ -0,0 +1,206 @@ +reset +---- + +batch commit +range-key-set x z @5 boop +---- +committed 1 keys + +combined-iter +last +next +prev +---- +x: (., [x-z) @5=boop UPDATED) +. +x: (., [x-z) @5=boop UPDATED) + +# Test limited reverse iteration. The seek-lt-limit z y must see the [x-z) range +# key because it covers a key within the range [y, z). The range key start +# boundary isn't until x. + +combined-iter +seek-lt-limit z y +next +prev-limit y +---- +x: valid (., [x-z) @5=boop UPDATED) +. +x: valid (., [x-z) @5=boop UPDATED) + +# Test limited forward iteration. Since range keys are interleaved at the start +# boundaries, the iterator is guaranteed to encounter covering range keys +# without any special casing in the implementation. + +combined-iter +seek-ge-limit w y +prev +next-limit y +---- +x: valid (., [x-z) @5=boop UPDATED) +. +x: valid (., [x-z) @5=boop UPDATED) + +# Test another limited backward iteration case where there exists a deleted +# point key and the underlying internalIterator is Prev'd to a key beyond the +# limit. This should still surface the covering range key. + +batch commit +del yy +---- +committed 1 keys + +combined-iter +seek-lt-limit z y +next +prev-limit y +---- +x: valid (., [x-z) @5=boop UPDATED) +. +x: valid (., [x-z) @5=boop UPDATED) + +# Test a case during limited reverse iteration where a range key covers a +# portion of the keyspace within the limit. The iterator should NOT pause and +# should surface the range key. + +reset +---- + +batch commit +del b +range-key-set a d @1 foo +---- +committed 2 keys + +flush +---- + +combined-iter +seek-ge z +prev-limit c +---- +. +a: valid (., [a-d) @1=foo UPDATED) + +# Test a case during limited reverse iteration where there exists a range key +# but it ends before the limit. The iterator should pause. + +reset +---- + +batch commit +del b +range-key-set a c @1 foo +---- +committed 2 keys + +combined-iter +seek-ge z +prev-limit c +---- +. +. at-limit + +# Test at-limit interactions with RangeKeyChanged(). +# Regression test for #1963. + +reset +---- + +# Construct a range key and points such that there are deleted keys that a +# -WithLimit iterator operation may pause at both at the beginning and end of +# the range key's bounds. +# +# * b.DEL * cat.SET * dog.DEL +# |-------------------------------------------------------------------) [a,e)@1→foo +# a b c d e + +batch commit +del b +set cat cat +del dog +range-key-set a e @1 foo +---- +committed 4 keys + +combined-iter +seek-ge-limit a bat +seek-ge-limit a bat +seek-ge-limit apple bat +seek-ge-limit cow zoo +---- +a: valid (., [a-e) @1=foo UPDATED) +a: valid (., [a-e) @1=foo) +apple: valid (., [a-e) @1=foo) +cow: valid (., [a-e) @1=foo) + +combined-iter +seek-ge a +next-limit bat +next-limit dog +next-limit zoo +---- +a: (., [a-e) @1=foo UPDATED) +. at-limit +cat: valid (cat, [a-e) @1=foo UPDATED) +. exhausted + +combined-iter +seek-lt-limit f e +seek-lt-limit e d +seek-lt-limit e d +seek-lt-limit f e +seek-lt-limit e d +---- +. at-limit +cat: valid (cat, [a-e) @1=foo UPDATED) +cat: valid (cat, [a-e) @1=foo) +. at-limit +cat: valid (cat, [a-e) @1=foo UPDATED) + +# Add a new dz.SET key. +# +# * b.DEL * cat.SET * dog.DEL * dz.SET +# |-------------------------------------------------------------------) [a,e)@1→foo +# a b c d e + +batch commit +set dz dz +---- +committed 1 keys + +combined-iter +seek-ge dz +prev-limit e +prev-limit dy +prev-limit c +prev +next +---- +dz: (dz, [a-e) @1=foo UPDATED) +. at-limit +cat: valid (cat, [a-e) @1=foo UPDATED) +a: valid (., [a-e) @1=foo) +. +a: (., [a-e) @1=foo UPDATED) + +# Test enforcing limits even while skipping point keys. + +reset +---- + +batch commit +set b@9 b@9 +set c@2 c@2 +set d@8 d@8 +---- +committed 3 keys + +combined-iter point-key-filter=(6,10) +last +prev-limit c +seek-ge-limit c@9 d +---- +d@8: (d@8, .) +. at-limit +. at-limit diff --git a/pebble/testdata/iterator b/pebble/testdata/iterator new file mode 100644 index 0000000..cad4f0d --- /dev/null +++ b/pebble/testdata/iterator @@ -0,0 +1,1706 @@ +define +a.SET.1:b +---- + +iter seq=2 +seek-ge a +next +prev +---- +a: (b, .) +. +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +seek-ge b +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)) + +iter seq=2 +seek-lt a +---- +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)) + + +define +a.SET.2:c +a.SET.1:b +---- + +iter seq=2 +seek-ge a +next +prev +---- +a: (b, .) +. +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=3 +seek-ge a +next +prev +---- +a: (c, .) +. +a: (c, .) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 1, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge a +next +prev +next +---- +a: (b, .) +. +err=pebble: unsupported reverse prefix iteration +err=pebble: unsupported reverse prefix iteration +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge a +next +---- +a: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + + +define +a.DEL.2: +a.SET.1:b +---- + +iter seq=3 +seek-ge a +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +seek-ge 1 +next +---- +a: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 1B, tombstoned 0))) + +iter seq=3 +seek-lt b +---- +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +seek-lt b +prev +next +---- +a: (b, .) +. +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 2B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge a +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge 1 +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 0B, tombstoned 0))) + +define +a.DEL.2: +a.SET.1:b +b.SET.3:c +---- + +iter seq=4 +seek-ge a +next +---- +b: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 2B, tombstoned 0))) + +iter seq=3 +seek-ge a +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +seek-ge a +---- +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 1B, tombstoned 0))) + +iter seq=4 +seek-prefix-ge a +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 2B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge a +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge a +---- +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge a +seek-prefix-ge b +---- +a: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 2B, tombstoned 0))) + +define +a.DEL.3: +a.SET.1:b +b.DEL.3: +b.SET.2:c +c.SET.3:d +---- + +iter seq=4 +seek-prefix-ge a +seek-prefix-ge b +seek-prefix-ge c +---- +. +. +c: (d, .) +stats: (interface (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 3, 4), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 7, key-bytes 7B, value-bytes 4B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge a +seek-prefix-ge b +seek-prefix-ge c +---- +a: (b, .) +b: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 3B, tombstoned 0))) + +iter seq=3 +seek-ge a +seek-ge b +seek-ge c +---- +a: (b, .) +b: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 3B, tombstoned 0))) + +define +a.SET.1:a +b.SET.2:b +c.SET.3:c +---- + +iter seq=4 +seek-ge a +next +next +next +---- +a: (a, .) +b: (b, .) +c: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=4 +seek-ge b +next +---- +b: (b, .) +c: (c, .) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=4 +seek-ge c +---- +c: (c, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=4 +seek-lt a +---- +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)) + +iter seq=4 +seek-lt b +prev +next +---- +a: (a, .) +. +a: (a, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=4 +seek-lt c +prev +prev +next +---- +b: (b, .) +a: (a, .) +. +a: (a, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 1, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + + +iter seq=4 +seek-lt d +prev +prev +prev +next +---- +c: (c, .) +b: (b, .) +a: (a, .) +. +a: (a, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 3)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 1, 3)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=4 +seek-prefix-ge a +next +---- +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=4 +seek-prefix-ge b +next +---- +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + + +iter seq=4 +seek-prefix-ge c +next +---- +c: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + + +iter seq=4 +seek-prefix-ge d +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)) + +iter seq=4 +seek-prefix-ge a +seek-prefix-ge c +seek-prefix-ge b +---- +a: (a, .) +c: (c, .) +b: (b, .) +stats: (interface (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +define +a.SET.b2:b +b.SET.2:c +---- + +iter seq=2 +seek-ge a +next +prev +---- +a: (b, .) +. +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=2 +seek-ge b +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +seek-lt a +---- +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)) + +iter seq=2 +seek-lt b +prev +next +---- +a: (b, .) +. +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +seek-lt c +prev +next +---- +a: (b, .) +. +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge a +next +---- +a: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge b +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + + +define +a.SET.1:a +aa.SET.2:aa +aaa.SET.3:aaa +b.SET.4:b +---- + +iter seq=5 +seek-prefix-ge a +next +---- +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge a +next +---- +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aa +---- +aa: (aa, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aa +next +---- +aa: (aa, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aa +next +---- +aa: (aa, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aaa +next +---- +aaa: (aaa, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aaa +---- +aaa: (aaa, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge b +next +---- +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aa +last +prev +prev +prev +prev +---- +aa: (aa, .) +b: (b, .) +aaa: (aaa, .) +aa: (aa, .) +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 1, 4)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 1, 4)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 9B, value-bytes 9B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aa +first +next +next +next +next +---- +aa: (aa, .) +a: (a, .) +aa: (aa, .) +aaa: (aaa, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 4), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 4), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 9B, value-bytes 9B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aaa +seek-ge aa +next +next +next +---- +aaa: (aaa, .) +aa: (aa, .) +aaa: (aaa, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 3), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 3), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 9B, value-bytes 9B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aaa +seek-ge aaa +next +next +---- +aaa: (aaa, .) +aaa: (aaa, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 7B, value-bytes 7B, tombstoned 0))) + +iter seq=5 +seek-prefix-ge aaa +seek-lt aa +next +next +next +next +---- +aaa: (aaa, .) +a: (a, .) +aa: (aa, .) +aaa: (aaa, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 4), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 2, 4), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 6, key-bytes 11B, value-bytes 11B, tombstoned 0))) + + +iter seq=5 +seek-prefix-ge aaa +seek-lt b +next +next +---- +aaa: (aaa, .) +aaa: (aaa, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 12B, value-bytes 12B, tombstoned 0))) + +iter seq=4 +seek-prefix-ge a +seek-prefix-ge aa +seek-prefix-ge aaa +seek-prefix-ge b +---- +a: (a, .) +aa: (aa, .) +aaa: (aaa, .) +. +stats: (interface (dir, seek, step): (fwd, 4, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 4, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 7B, value-bytes 7B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge aaa +seek-prefix-ge b +seek-prefix-ge a +seek-prefix-ge aa +seek-prefix-ge aaa +---- +. +. +a: (a, .) +aa: (aa, .) +. +stats: (interface (dir, seek, step): (fwd, 5, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 5, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 7, key-bytes 12B, value-bytes 12B, tombstoned 0))) + +define +bb.DEL.2: +bb.SET.1:1 +bb2.SET.3:2 +---- + +iter seq=4 +seek-prefix-ge bb +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 7B, value-bytes 2B, tombstoned 0))) + + +define +a.MERGE.3:d +a.MERGE.2:c +a.SET.1:b +b.MERGE.2:b +b.MERGE.1:a +---- + +iter seq=4 +seek-ge a +next +next +prev +---- +a: (bcd, .) +b: (ab, .) +. +b: (ab, .) +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 5), (rev, 1, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 8, key-bytes 8B, value-bytes 8B, tombstoned 0))) + +iter seq=3 +seek-ge a +next +---- +a: (bc, .) +b: (ab, .) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 4), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=2 +seek-ge a +next +---- +a: (b, .) +b: (a, .) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=4 +seek-lt c +prev +prev +next +---- +b: (ab, .) +a: (bcd, .) +. +a: (bcd, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 1, 5)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 8, key-bytes 8B, value-bytes 8B, tombstoned 0))) + +iter seq=3 +seek-lt c +prev +---- +b: (ab, .) +a: (bc, .) +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 4)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=2 +seek-lt c +prev +---- +b: (a, .) +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=4 +seek-ge a +next +prev +next +---- +a: (bcd, .) +b: (ab, .) +a: (bcd, .) +b: (ab, .) +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 10), (rev, 1, 5)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 15, key-bytes 15B, value-bytes 15B, tombstoned 0))) + +iter seq=3 +seek-ge a +next +prev +next +---- +a: (bc, .) +b: (ab, .) +a: (bc, .) +b: (ab, .) +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 8), (rev, 1, 4)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 15, key-bytes 15B, value-bytes 15B, tombstoned 0))) + +iter seq=2 +seek-ge a +next +prev +next +---- +a: (b, .) +b: (a, .) +a: (b, .) +b: (a, .) +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 4), (rev, 1, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 15, key-bytes 15B, value-bytes 15B, tombstoned 0))) + +iter seq=4 +seek-lt c +prev +next +prev +---- +b: (ab, .) +a: (bcd, .) +b: (ab, .) +a: (bcd, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 5), (rev, 2, 10)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 15, key-bytes 15B, value-bytes 15B, tombstoned 0))) + +iter seq=3 +seek-lt c +prev +next +prev +---- +b: (ab, .) +a: (bc, .) +b: (ab, .) +a: (bc, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 4), (rev, 2, 8)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 15, key-bytes 15B, value-bytes 15B, tombstoned 0))) + +iter seq=2 +seek-lt c +prev +next +prev +---- +b: (a, .) +a: (b, .) +b: (a, .) +a: (b, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 2)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 2, 4)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 15, key-bytes 15B, value-bytes 15B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge a +next +---- +a: (bc, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge a +next +---- +a: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=4 +seek-prefix-ge a +next +---- +a: (bcd, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge a +next +---- +a: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge a +next +---- +a: (bc, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge c +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)) + +iter seq=3 +seek-prefix-ge 1 +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge a +---- +a: (bc, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + + +define +a.MERGE.3:d +a.MERGE.2:c +a.MERGE.1:b +aa.MERGE.2:b +aa.MERGE.1:a +b.MERGE.2:b +b.MERGE.1:a +---- + +iter seq=3 +seek-prefix-ge a +next +next +---- +a: (bc, .) +. +. +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 5B, value-bytes 4B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge a +next +next +---- +a: (b, .) +. +. +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 5B, value-bytes 4B, tombstoned 0))) + +iter seq=4 +seek-prefix-ge a +next +---- +a: (bcd, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 5B, value-bytes 4B, tombstoned 0))) + +iter seq=2 +seek-prefix-ge a +next +---- +a: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 5B, value-bytes 4B, tombstoned 0))) + +iter seq=3 +seek-prefix-ge aa +next +---- +aa: (ab, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 5B, value-bytes 3B, tombstoned 0))) + +iter seq=4 +seek-prefix-ge aa +---- +aa: (ab, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 5B, value-bytes 3B, tombstoned 0))) + +define +a.SET.1:a +b.SET.1:b +c.SET.1:c +d.SET.1:d +---- + +iter seq=2 lower=a +seek-ge a +first +prev +---- +a: (a, .) +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 lower=b +seek-ge a +first +prev +---- +b: (b, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 lower=c +seek-ge a +first +prev +---- +c: (c, .) +c: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 lower=d +seek-ge a +first +prev +---- +d: (d, .) +d: (d, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 lower=e +seek-ge a +first +---- +. +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)) + +iter seq=2 upper=d +seek-lt d +last +next +---- +c: (c, .) +c: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 2, 0)), (internal (dir, seek, step): (fwd, 0, 2), (rev, 2, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=2 upper=c +seek-lt d +last +next +---- +b: (b, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 2, 0)), (internal (dir, seek, step): (fwd, 0, 2), (rev, 2, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=2 upper=b +seek-lt d +last +next +---- +a: (a, .) +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 2, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 2, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=2 upper=a +seek-lt d +last +---- +. +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 2, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 2, 0)) + +iter seq=2 lower=b upper=c +seek-ge a +next +---- +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +set-bounds lower=a +seek-ge a +first +prev +---- +. +a: (a, .) +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +set-bounds lower=b +seek-ge a +first +prev +---- +. +b: (b, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +set-bounds lower=c +seek-ge a +first +prev +---- +. +c: (c, .) +c: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +set-bounds lower=d +seek-ge a +first +prev +---- +. +d: (d, .) +d: (d, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +set-bounds lower=e +seek-ge a +first +---- +. +. +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)) + +iter seq=2 +set-bounds upper=d +seek-lt d +last +next +---- +. +c: (c, .) +c: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 2, 0)), (internal (dir, seek, step): (fwd, 0, 2), (rev, 2, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=2 +set-bounds upper=c +seek-lt d +last +next +---- +. +b: (b, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 2, 0)), (internal (dir, seek, step): (fwd, 0, 2), (rev, 2, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +iter seq=2 +set-bounds upper=b +seek-lt d +last +next +---- +. +a: (a, .) +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 2, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 2, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=2 +set-bounds upper=a +seek-lt d +last +---- +. +. +. +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 2, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 2, 0)) + +iter seq=2 +set-bounds lower=a +seek-lt d +next +next +---- +. +c: (c, .) +d: (d, .) +. +stats: (interface (dir, seek, step): (fwd, 0, 2), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 0, 3), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 4B, tombstoned 0))) + +iter seq=2 +set-bounds lower=b upper=c +seek-ge a +next +---- +. +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +set-bounds lower=b +seek-ge a +set-bounds lower=b upper=z +seek-ge a +---- +. +b: (b, .) +. +b: (b, .) +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +seek-ge a +set-bounds upper=e +---- +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +set-bounds lower=b +seek-ge a +set-bounds upper=e +---- +. +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +set-bounds lower=b +first +---- +. +b: (b, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +set-bounds upper=b +first +---- +. +a: (a, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=2 +set-bounds lower=b +last +---- +. +d: (d, .) +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +set-bounds upper=b +last +---- +. +a: (a, .) +stats: (interface (dir, seek, step): (fwd, 0, 0), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +# The prev call after "set-bounds upper=c" will assume that the iterator +# is exhausted due to having stepped up to c. Which means prev should step +# back to below c, hence returning b. +iter seq=2 +last +next +set-bounds upper=c +prev +---- +d: (d, .) +. +. +b: (b, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 1)), (internal (dir, seek, step): (fwd, 0, 2), (rev, 2, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 5, key-bytes 5B, value-bytes 5B, tombstoned 0))) + +# The next call after "set-bounds lower=b" will assume that the iterator +# is exhausted due to having stepped below b. Which means next should step +# up to b (or higher), hence returning b. +iter seq=2 +first +prev +set-bounds lower=b +next +---- +a: (a, .) +. +. +b: (b, .) +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 +set-bounds lower=b +seek-lt c +next +---- +. +b: (b, .) +c: (c, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 1, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=2 +set-bounds upper=d +seek-ge c +prev +---- +. +c: (c, .) +b: (b, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 2)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +define +a.SET.1:a +aa.SET.1:aa +aaa.SET.1:aaa +b.SET.1:b +---- + +iter seq=2 lower=a +seek-prefix-ge a +first +prev +---- +a: (a, .) +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 1)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + + +iter seq=2 lower=aa +seek-prefix-ge a +---- +err=pebble: SeekPrefixGE supplied with key outside of lower bound +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 0, 0), (rev, 0, 0)) + +iter seq=2 lower=a upper=aa +seek-prefix-ge a +next +---- +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) + +iter seq=2 lower=a upper=aaa +seek-prefix-ge a +next +---- +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=2 lower=a upper=b +seek-prefix-ge a +next +---- +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=2 lower=a upper=c +seek-prefix-ge a +next +---- +a: (a, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 3B, value-bytes 3B, tombstoned 0))) + +iter seq=2 lower=a upper=aaa +seek-prefix-ge aa +---- +aa: (aa, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +iter seq=2 lower=a upper=aaa +seek-prefix-ge aa +next +---- +aa: (aa, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +define +a.SET.1:a +b.SET.2:b +---- + +iter seq=4 +first +next +next +---- +a: (a, .) +b: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +define +a.SINGLEDEL.1: +---- + +iter seq=2 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 0B, tombstoned 0))) + +define +a.SINGLEDEL.2: +a.SINGLEDEL.1: +---- + +iter seq=3 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 0B, tombstoned 0))) + +define +a.SINGLEDEL.2: +a.DEL.1: +---- + +iter seq=3 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 0B, tombstoned 0))) + +define +a.SINGLEDEL.2: +a.MERGE.1: +---- + +iter seq=3 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 0B, tombstoned 0))) + +define +a.SINGLEDEL.2: +a.SET.1:b +---- + +iter seq=3 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 1B, tombstoned 0))) + +define +a.SET.2:b +a.SINGLEDEL.1: +---- + +iter seq=3 +first +next +---- +a: (b, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 1B, tombstoned 0))) + +define +a.SINGLEDEL.2: +a.SET.1:b +b.SET.3:c +---- + +iter seq=4 +first +next +---- +b: (c, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 2B, tombstoned 0))) + +define +a.SINGLEDEL.3: +a.SET.2:b +a.SET.1:a +---- + +iter +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 2B, tombstoned 0))) + +define +a.SINGLEDEL.3: +a.MERGE.2:b +a.MERGE.1:a +---- + +iter seq=4 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 3, key-bytes 3B, value-bytes 2B, tombstoned 0))) + +define +a.SINGLEDEL.4: +a.SET.3:val +a.SINGLEDEL.2: +a.SET.1:val +---- + +iter seq=5 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 4), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 6B, tombstoned 0))) + +define +a.SINGLEDEL.4: +a.SET.3:val +a.DEL.2: +a.SET.1:val +---- + +iter seq=5 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 4), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 6B, tombstoned 0))) + +define +a.SINGLEDEL.4: +a.SET.3:c +a.MERGE.2:b +a.SET.1:a +---- + +iter seq=5 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 4), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 4, key-bytes 4B, value-bytes 3B, tombstoned 0))) + +define +a.SINGLEDEL.3: +a.SET.1:val +---- + +iter seq=4 +first +---- +. +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 3B, tombstoned 0))) + +# Exercise iteration with limits, when there are no deletes. +define +a.SET.1:a +b.SET.1:b +c.SET.1:c +d.SET.1:d +---- + +iter seq=2 +seek-ge-limit a b +next-limit b +prev-limit a +next-limit b +next-limit b +seek-lt-limit d d +prev-limit d +next-limit e +prev-limit d +prev-limit c +prev-limit b +prev-limit a +prev-limit a +next-limit a +next-limit b +---- +a: valid (a, .) +. at-limit +a: valid (a, .) +. at-limit +. at-limit +. at-limit +. at-limit +d: valid (d, .) +. at-limit +c: valid (c, .) +b: valid (b, .) +a: valid (a, .) +. exhausted +. at-limit +a: valid (a, .) +stats: (interface (dir, seek, step): (fwd, 1, 6), (rev, 1, 7)), (internal (dir, seek, step): (fwd, 3, 3), (rev, 1, 6)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 11, key-bytes 11B, value-bytes 11B, tombstoned 0))) + +# Exercise iteration with limits when we have deletes. + +define +a.SET.1:a +b.DEL.3: +b.SET.2:b +c.DEL.3: +c.SET.2:c +d.SET.1:d +---- + +iter seq=4 +seek-ge-limit a b +next-limit b +prev-limit a +prev-limit a +next-limit b +next-limit b +next-limit b +prev-limit a +next-limit c +prev-limit b +next-limit c +next-limit d +next-limit e +next-limit e +prev-limit d +next-limit e +---- +a: valid (a, .) +. at-limit +a: valid (a, .) +. exhausted +a: valid (a, .) +. at-limit +. at-limit +a: valid (a, .) +. at-limit +. at-limit +. at-limit +. at-limit +d: valid (d, .) +. exhausted +d: valid (d, .) +. exhausted +stats: (interface (dir, seek, step): (fwd, 1, 10), (rev, 0, 5)), (internal (dir, seek, step): (fwd, 3, 13), (rev, 1, 8)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 21, key-bytes 21B, value-bytes 14B, tombstoned 0))) + +iter seq=4 +seek-ge-limit b d +next-limit d +prev-limit b +next-limit e +---- +. at-limit +. at-limit +. at-limit +d: valid (d, .) +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 1, 9), (rev, 0, 5)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 15, key-bytes 15B, value-bytes 9B, tombstoned 0))) + +iter seq=4 +seek-lt-limit d c +prev-limit c +prev-limit b +prev-limit a +prev-limit a +next-limit b +---- +. at-limit +. at-limit +. at-limit +a: valid (a, .) +. exhausted +a: valid (a, .) +stats: (interface (dir, seek, step): (fwd, 0, 1), (rev, 1, 4)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 1, 5)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 6, key-bytes 6B, value-bytes 4B, tombstoned 0))) + +# NB: Zero values are skipped by deletable merger. +define merger=deletable +a.MERGE.1:1 +a.MERGE.2:2 +a.MERGE.3:-1 +a.MERGE.4:-2 +b.MERGE.4:-3 +b.MERGE.3:3 +b.MERGE.2:2 +b.MERGE.1:-2 +---- + +iter seq=5 +seek-ge a +next +next +prev +prev +---- +. +. +. +. +. +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 2)), (internal (dir, seek, step): (fwd, 1, 8), (rev, 1, 8)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 16, key-bytes 16B, value-bytes 24B, tombstoned 0))) + +iter seq=4 +seek-ge a +next +next +prev +prev +---- +a: (2, .) +b: (3, .) +. +b: (3, .) +a: (2, .) +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 2)), (internal (dir, seek, step): (fwd, 1, 6), (rev, 1, 6)), +(internal-stats: (block-bytes: (total 0B, cached 0B, read-time 0s)), (points: (count 16, key-bytes 16B, value-bytes 24B, tombstoned 0))) diff --git a/pebble/testdata/iterator_block_interval_filter b/pebble/testdata/iterator_block_interval_filter new file mode 100644 index 0000000..8e46099 --- /dev/null +++ b/pebble/testdata/iterator_block_interval_filter @@ -0,0 +1,306 @@ +# Block size is 1, so each block contains one key, and two level index is used +# since the lower index blocks have only one key. + +# Build a table with a single interval collector with id=2 and 2 character +# suffix. The keys in the table are in the interval [1,7). +build id_offset=(2,0) +set a01 a +set b02 b +set c03 c +set d04 d +set e05 e +set f06 f +---- +0.0: + 000005:[a01#10,SET-f06#15,SET] + +# Iterate without a filter. +iter +first +next +next +next +next +next +next +---- +a01: (a, .) +b02: (b, .) +c03: (c, .) +d04: (d, .) +e05: (e, .) +f06: (f, .) +. + +# Iterate with a filter interval [1,2) that selects a key at the beginning of +# the file. +iter id_lower_upper=(2,1,2) +first +next +prev +prev +next +next +last +seek-lt f +seek-ge a +seek-ge b +prev +---- +a01: (a, .) +. +a01: (a, .) +. +a01: (a, .) +. +a01: (a, .) +a01: (a, .) +a01: (a, .) +. +a01: (a, .) + +# Iterate with a filter interval [3,5) that selects keys in the middle of the +# file. +iter id_lower_upper=(2,3,5) +first +next +next +last +prev +prev +seek-lt f +prev +next +prev +prev +last +seek-ge c +seek-ge d +next +prev +prev +prev +---- +c03: (c, .) +d04: (d, .) +. +d04: (d, .) +c03: (c, .) +. +d04: (d, .) +c03: (c, .) +d04: (d, .) +c03: (c, .) +. +d04: (d, .) +c03: (c, .) +d04: (d, .) +. +d04: (d, .) +c03: (c, .) +. + +# Iterate with a filter interval [6,8) that selects a key at the end of the +# file. +iter id_lower_upper=(2,6,8) +first +next +prev +prev +next +last +prev +seek-lt g +seek-ge b +---- +f06: (f, .) +. +f06: (f, .) +. +f06: (f, .) +f06: (f, .) +. +f06: (f, .) +f06: (f, .) + +iter id_lower_upper=(2,2,2) +first +last +seek-ge a +seek-lt g +---- +. +. +. +. + +# Iterate with a filter interval [7,9) that is after the interval in the file. +iter id_lower_upper=(2, 7, 9) +first +last +seek-ge a +seek-lt g +---- +. +. +. +. + +# Iterate with a filter interval [0,1) that is before the interval in the +# file. +iter id_lower_upper=(2, 0, 1) +first +last +seek-ge a +seek-lt g +---- +. +. +. +. + +# Iterate with a filter id=3, which is unknown to the file, so all blocks are +# visible. +iter id_lower_upper=(3, 1, 2) +first +next +next +next +next +next +next +---- +a01: (a, .) +b02: (b, .) +c03: (c, .) +d04: (d, .) +e05: (e, .) +f06: (f, .) +. + +# Build a table with two interval collectors: +# - id=3 and 2 character suffix. The keys in the table are in the interval +# [1,6). +# - id=5 and 2 characters offset by 2 from the suffix. The keys in the table +# are in the interval [6,11). +build id_offset=(3,0) id_offset=(5,2) +set a1001 a +set b0902 b +set c0803 c +set d0704 d +set e0605 e +---- +0.0: + 000005:[a1001#10,SET-e0605#14,SET] + +# Iterate without a filter. +iter +first +next +next +next +next +next +---- +a1001: (a, .) +b0902: (b, .) +c0803: (c, .) +d0704: (d, .) +e0605: (e, .) +. + +# Iterate with filter id=5, interval [7,9). +iter id_lower_upper=(5,7,9) +first +next +next +prev +prev +---- +c0803: (c, .) +d0704: (d, .) +. +d0704: (d, .) +c0803: (c, .) + +# Iterate with filter id=5, interval [7,9), and an unknown filter id. The +# result should only be affected by the filter id=5. +iter id_lower_upper=(5,7,9) id_lower_upper=(10,0,1) +first +next +next +prev +prev +---- +c0803: (c, .) +d0704: (d, .) +. +d0704: (d, .) +c0803: (c, .) + +# Iterate with filter id=3, interval [4,5) and filter id=5, interval [7,9). +# The set of blocks admitted by these two filters are intersecting, but not +# identical. Key c0803, which is allowed by the latter is not allowed by the +# former, and hence omitted. +iter id_lower_upper=(3,4,5) id_lower_upper=(5,7,9) +first +next +prev +prev +---- +d0704: (d, .) +. +d0704: (d, .) +. + +# Repeat the above test, but calling set-options before iteration to set the +# same filter. The results should be identical. +iter id_lower_upper=(3,4,5) id_lower_upper=(5,7,9) +set-options point-filters=reuse +first +next +prev +prev +---- +. +d0704: (d, .) +. +d0704: (d, .) +. + +# Repeat the above test, but calling set-options before iteration to remove the +# filter. +iter id_lower_upper=(3,4,5) id_lower_upper=(5,7,9) +set-options point-filters=none +first +next +prev +prev +---- +. +a1001: (a, .) +b0902: (b, .) +a1001: (a, .) +. + + +# Iterate with filter id=3 and id=5, where the two admitted sets are +# non-empty, but the intersection is empty. +iter id_lower_upper=(3,4,5) id_lower_upper=(5,8,9) +first +---- +. + +# Iterate with filter id=3 and id=5, where filter id=5 set is empty, so the +# intersection is empty. +iter id_lower_upper=(3,4,5) id_lower_upper=(5,11,12) +first +---- +. + +# Iterate with filter id=3 and id=5, where filter id=3 set is empty, so the +# intersection is empty. +iter id_lower_upper=(3,6,7) id_lower_upper=(5,7,9) +first +---- +. diff --git a/pebble/testdata/iterator_bounds_lifetimes b/pebble/testdata/iterator_bounds_lifetimes new file mode 100644 index 0000000..7799c25 --- /dev/null +++ b/pebble/testdata/iterator_bounds_lifetimes @@ -0,0 +1,83 @@ +new-iter label=first lower=bar upper=foo +---- +first: ("bar", "foo") boundsBufIdx=1 + +iter label=first +first +next +---- +bb@29: (bb@29, .) +bc@30: (bc@30, .) + +# Clone an iterator from the original iterator. The clone should have its own +# copy of the bounds. + +clone from=first to=second +---- +first: ("bar", "foo") boundsBufIdx=1 +second: ("bar", "foo") boundsBufIdx=1 + +iter label=second +last +prev +---- +fo@150: (fo@150, .) +fn@149: (fn@149, .) + +# Changing the bounds on the original should leave the clone's bounds unchanged. + +set-bounds label=first lower=boop +---- +first: ("boop", ) boundsBufIdx=0 +second: ("bar", "foo") boundsBufIdx=1 + +iter label=first +seek-ge goop +---- +gp@178: (gp@178, .) + +iter label=second +prev +---- +fm@148: (fm@148, .) + +set-bounds label=first lower=boop upper=bop +---- +first: ("boop", "bop") boundsBufIdx=1 +second: ("bar", "foo") boundsBufIdx=1 + +# Changing the bounds on the clone should leave the original's bounds unchanged. + +set-options label=second lower=a upper=z +---- +first: ("boop", "bop") boundsBufIdx=1 +second: ("a", "z") boundsBufIdx=0 + +# Test no-op set-options. The boundsBufIdx should remain unchanged, reflecting +# that the bounds were not copied again. + +set-options label=second lower=a upper=z +---- +first: ("boop", "bop") boundsBufIdx=1 +second: ("a", "z") boundsBufIdx=0 + +# Test SetOptions with unchanged bounds but changes to other options. SetOptions +# should hold onto the existing bounds buffers. The boundsBufIdx should still +# remain unchanged, reflecting that the bounds were not copied. + +set-options label=second lower=a upper=z key-types=both +---- +first: ("boop", "bop") boundsBufIdx=1 +second: ("a", "z") boundsBufIdx=0 + +iter label=second +seek-ge foo +---- +fp@151: (fp@151, .) + +close label=first +---- +second: ("a", "z") boundsBufIdx=0 + +close label=second +---- diff --git a/pebble/testdata/iterator_next_prev b/pebble/testdata/iterator_next_prev new file mode 100644 index 0000000..3121be3 --- /dev/null +++ b/pebble/testdata/iterator_next_prev @@ -0,0 +1,278 @@ +build ext1 +merge a 1 +set c 2 +---- + +ingest ext1 +---- +6: + 000004:[a#10,MERGE-c#10,SET] + +build ext2 +del-range b c +---- + +ingest ext2 +---- +0.0: + 000005:[b#11,RANGEDEL-c#inf,RANGEDEL] +6: + 000004:[a#10,MERGE-c#10,SET] + +# Regression test for a bug where range tombstones were not properly +# ignored by Iterator.prevUserKey when switching from forward to +# reverse iteration. In the forward direction, the Iterator sees the +# keys: +# +# a#1,MERGE +# c#1,SET +# +# Due to the synthetic boundary key generated for sstable 5, in the +# reverse direction Iterator sees the keys: +# +# c#1,SET +# b#2,RANGEDEL +# a#1,MERGE +# +# Normally the record b#2,RANGEDEL is skipped by Iterator during +# iteration, but logic to do so was missing from Iterator.prevUserKey. +# The result was that prev could return the same key that iterator was +# currently pointed at. + +iter +first +prev +---- +a: (1, .) +. + +reset +---- + +build ext1 +set t 1 +merge z 2 +---- + +ingest ext1 +---- +6: + 000004:[t#10,SET-z#10,MERGE] + +build ext2 +del-range x y +---- + +ingest ext2 +---- +0.0: + 000005:[x#11,RANGEDEL-y#inf,RANGEDEL] +6: + 000004:[t#10,SET-z#10,MERGE] + +# Regression test for a bug where range tombstones were not properly +# ignored by Iterator.nextUserKey when switching from reverse to +# forward iteration. In the reverse direction, the Iterator sees the +# keys: +# +# z#1,MERGE +# t#1,SET +# +# Due to the synthetic boundary key generated for sstable 5, in the +# forward direction Iterator sees the keys: +# +# t#1,SET +# y#72057594037927935,RANGEDEL +# z#1,MERGE +# +# Normally the record y#72057594037927935,RANGEDEL is skipped by +# Iterator during iteration, but logic to do so was missing from +# Iterator.nextUserKey. The result was that next could return the same +# key that iterator was currently pointed at. + +iter +last +next +---- +z: (2, .) +. + +# Verify that switching from reverse iteration to forward iteration +# properly skips over range tombstones at the start of forward +# iteration. + +reset +---- + +build ext1 +set e e +---- + +ingest ext1 +---- +6: + 000004:[e#10,SET-e#10,SET] + +build ext2 +set b b +del-range c d +---- + +ingest ext2 +---- +6: + 000005:[b#11,SET-d#inf,RANGEDEL] + 000004:[e#10,SET-e#10,SET] + +# The scenario requires iteration at a snapshot. The "last" operation +# will exhaust the mergingIter looking backwards for visible +# records. The subsequent "next" will seek-ge(lower) which will skip +# over the "b" record and find the boundary key due to the range +# deletion sentinel. + +iter seq=11 +set-bounds lower=c upper=f +last +next +---- +. +e: (e, .) +. + +# Test that the cloned iterator sees all the keys. +iter +set-bounds lower=a upper=f +first +next +next +clone +seek-ge a +next +next +---- +. +b: (b, .) +e: (e, .) +. +. +b: (b, .) +e: (e, .) +. + +# Test that the cloned iterator respects the original bounds. +iter +set-bounds lower=a upper=d +first +next +clone +seek-ge a +next +---- +. +b: (b, .) +. +. +b: (b, .) +. + +# Test that a cloned iterator set with new bounds, respects the new bounds and +# options. +iter +set-bounds lower=a upper=d +first +next +clone lower=a upper=z key-types=both +seek-ge a +next +---- +. +b: (b, .) +. +. +b: (b, .) +e: (e, .) + +# Test that the cloned iterator respects the seq num. +iter seq=11 +set-bounds lower=a upper=f +first +next +clone +last +prev +---- +. +e: (e, .) +. +. +e: (e, .) +. + +# Verify that switching from forward iteration to reverse iteration +# properly skips over range tombstones at the end of reverse +# iteration. + +reset +---- + +build ext1 +merge a a +---- + +ingest ext1 +---- +6: + 000004:[a#10,MERGE-a#10,MERGE] + +build ext2 +set e e +del-range c d +---- + +ingest ext2 +---- +6: + 000004:[a#10,MERGE-a#10,MERGE] + 000005:[c#11,RANGEDEL-e#11,SET] + +iter seq=11 +set-bounds lower=a upper=e +first +prev +---- +. +a: (a, .) +. + +reset +---- + +# Test demonstrating inadvertent exposure of ordering effects of the +# InternalKeyKind numbering. We build an sst with a (del/singledel, set) pair +# for two user keys. When ingested, all 4 keys have the same seqnum. The set +# overrides the del, and the singedel overrides the set. +# +# The test input setup looks peculiar because the build uses an indexed batch, +# and iterates over it to write to the sst, so we need to place the set after +# the del, and the singledel after the set in order for the batch ordering to +# be one that is suitable for feeding into the sstable writer. All 4 keys are +# being written to the sst (notice the bounds in the ingest). + +build ext1 +del a +set a 1 +set b 2 +singledel b +---- + +ingest ext1 +---- +6: + 000004:[a#10,SET-b#10,SET] + +iter +first +next +---- +a: (1, .) +. diff --git a/pebble/testdata/iterator_read_sampling b/pebble/testdata/iterator_read_sampling new file mode 100644 index 0000000..889a95d --- /dev/null +++ b/pebble/testdata/iterator_read_sampling @@ -0,0 +1,351 @@ +# Test with overlapping keys across levels, should pick top level to compact after allowed-seeks goes to 0 +# Verify that Iterator.First(), Iterator.SeekGE() and Iterator.Next() call maybe sample read. +define auto-compactions=off +L0 + a.SET.4:4 +L1 + a.SET.3:3 +L2 + d.SET.2:2 +L3 + d.SET.1:1 +---- +0.0: + 000004:[a#4,SET-a#4,SET] +1: + 000005:[a#3,SET-a#3,SET] +2: + 000006:[d#2,SET-d#2,SET] +3: + 000007:[d#1,SET-d#1,SET] + +set allowed-seeks=2 +---- + + +iter +first +---- +a: (4, .) + +iter-read-compactions +---- +(none) + +iter +first +---- +a: (4, .) + +iter-read-compactions +---- +(level: 0, start: a, end: a) + +read-compactions +---- +(none) + +close-iter +---- + +read-compactions +---- +(level: 0, start: a, end: a) + +iter +seek-ge d +---- +d: (2, .) + +iter +prev +---- +a: (4, .) + +iter +next +---- +d: (2, .) + +iter-read-compactions +---- +(level: 2, start: d, end: d) + +close-iter +---- + +read-compactions +---- +(level: 0, start: a, end: a) +(level: 2, start: d, end: d) + + + +# Verify that Iterator.Last(), Iterator.SeekLT() and Iterator.Prev() call maybe sample read. +define auto-compactions=off +L0 + a.SET.4:4 + c.SET.8:8 +L1 + a.SET.3:3 + c.SET.9:9 +L2 + d.SET.2:2 + l.SET.7:7 +L3 + d.SET.1:1 + l.SET.8:8 +---- +0.0: + 000004:[a#4,SET-c#8,SET] +1: + 000005:[a#3,SET-c#9,SET] +2: + 000006:[d#2,SET-l#7,SET] +3: + 000007:[d#1,SET-l#8,SET] + +set allowed-seeks=2 +---- + + +iter +last +---- +l: (8, .) + +iter-read-compactions +---- +(none) + +iter +last +---- +l: (8, .) + +iter-read-compactions +---- +(level: 2, start: d, end: l) + +read-compactions +---- +(none) + +close-iter +---- + +read-compactions +---- +(level: 2, start: d, end: l) + +iter +seek-lt d +---- +c: (9, .) + +iter +next +---- +d: (2, .) + +iter +prev +---- +c: (9, .) + +iter-read-compactions +---- +(level: 0, start: a, end: c) + +close-iter +---- + +read-compactions +---- +(level: 2, start: d, end: l) +(level: 0, start: a, end: c) + + +# For Iterator.Last(), Iterator.SeekLT() and Iterator.Prev(), if the key is the first key of the file or +# the only key, sampling skips it because the iterator has already moved past it. +define auto-compactions=off +L0 + a.SET.4:4 +L1 + a.SET.3:3 +L2 + d.SET.2:2 +L3 + d.SET.1:1 +---- +0.0: + 000004:[a#4,SET-a#4,SET] +1: + 000005:[a#3,SET-a#3,SET] +2: + 000006:[d#2,SET-d#2,SET] +3: + 000007:[d#1,SET-d#1,SET] + +set allowed-seeks=2 +---- + + +iter +last +---- +d: (2, .) + +iter-read-compactions +---- +(none) + +iter +last +---- +d: (2, .) + +iter-read-compactions +---- +(none) + +read-compactions +---- +(none) + +close-iter +---- + +read-compactions +---- +(none) + +iter +seek-lt d +---- +a: (4, .) + +iter +next +---- +d: (2, .) + +iter +prev +---- +a: (4, .) + +iter-read-compactions +---- +(none) + +close-iter +---- + +read-compactions +---- +(none) + + + + +# Test with no overlapping keys across levels, should not pick any compaction +define auto-compactions=off +L0 + a.SET.4:4 +L1 + b.SET.3:3 +L2 + c.SET.2:2 +L3 + d.SET.1:1 +---- +0.0: + 000004:[a#4,SET-a#4,SET] +1: + 000005:[b#3,SET-b#3,SET] +2: + 000006:[c#2,SET-c#2,SET] +3: + 000007:[d#1,SET-d#1,SET] + +set allowed-seeks=3 +---- + +iter +first +---- +a: (4, .) + +iter +first +---- +a: (4, .) + +iter +first +---- +a: (4, .) + +iter-read-compactions +---- +(none) + +close-iter +---- + +read-compactions +---- +(none) + +# Test to see if the allowedSeeks associated with a file +# is reset once it hits 0. +define auto-compactions=off +L0 + a.SET.4:4 + c.SET.8:8 +L1 + a.SET.3:3 + c.SET.9:9 +L2 + d.SET.2:2 + l.SET.7:7 +L3 + d.SET.1:1 + l.SET.8:8 +---- +0.0: + 000004:[a#4,SET-c#8,SET] +1: + 000005:[a#3,SET-c#9,SET] +2: + 000006:[d#2,SET-l#7,SET] +3: + 000007:[d#1,SET-l#8,SET] + +set allowed-seeks=1 +---- + +iter +last +---- +l: (8, .) + +iter-read-compactions +---- +(level: 2, start: d, end: l) + +close-iter +---- + +read-compactions +---- +(level: 2, start: d, end: l) + +# The allowedSeeks on this file should have been reset. +# Since the value of allowedSeeks determined +# by the code is 100, we check if allowed-seeks has been +# reset to 100. +show allowed-seeks=(000006,) +---- +100 diff --git a/pebble/testdata/iterator_seek_opt b/pebble/testdata/iterator_seek_opt new file mode 100644 index 0000000..73eda49 --- /dev/null +++ b/pebble/testdata/iterator_seek_opt @@ -0,0 +1,317 @@ + +define auto-compactions=off +L0 + a.SET.4:4 +L1 + a.SET.3:3 +L2 + d.SET.2:2 +L3 + b.SET.1:1 + c.SET.1:1 + d.SET.1:1 + e.SET.1:1 +---- +0.0: + 000004:[a#4,SET-a#4,SET] +1: + 000005:[a#3,SET-a#3,SET] +2: + 000006:[d#2,SET-d#2,SET] +3: + 000007:[b#1,SET-e#1,SET] + +# Simple case: three successive seeks, at increasing keys. Should use +# trySeekUsingNext. + +iter +seek-ge a +---- +a: (4, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)) +SeekGEs with trySeekUsingNext: 0 +SeekPrefixGEs with trySeekUsingNext: 0 + +iter +seek-ge b +---- +b: (1, .) +stats: (interface (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 2, 0), (rev, 0, 0)) +SeekGEs with trySeekUsingNext: 2 +SeekPrefixGEs with trySeekUsingNext: 0 + +iter +seek-ge c +---- +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 3, 0), (rev, 0, 0)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 0 + +# Seek at a lower key. Should not call with trySeekUsingNext = true. + +iter +seek-ge bb +---- +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 4, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 4, 0), (rev, 0, 0)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 0 + +# Seek at a greater key than last seek, but lands on the same key. Should +# not call internalIterator at all. + +iter +seek-ge bbb +---- +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 5, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 4, 0), (rev, 0, 0)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 0 + +# A step followed by a seek should not call with trySeekUsingNext = true. + +iter +next +seek-ge e +---- +d: (2, .) +e: (1, .) +stats: (interface (dir, seek, step): (fwd, 6, 1), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 5, 1), (rev, 0, 0)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 0 + +iter +prev +seek-ge b +---- +d: (2, .) +b: (1, .) +stats: (interface (dir, seek, step): (fwd, 7, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 6, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 0 + +# SeekPrefixGE simple case. + +iter +seek-prefix-ge a +---- +a: (4, .) +stats: (interface (dir, seek, step): (fwd, 8, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 7, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 0 + +iter +seek-prefix-ge b +---- +b: (1, .) +stats: (interface (dir, seek, step): (fwd, 9, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 8, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 2 + +iter +seek-prefix-ge c +---- +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 10, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 9, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 4 + +# Seek at a lower key. Should not call with trySeekUsingNext = true. + +iter +seek-prefix-ge bb +---- +. +stats: (interface (dir, seek, step): (fwd, 11, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 10, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 4 + +# Shifting bounds followed by SeekGEs. The one immediately after a bounds change +# does not use trySeekUsingNext, but successive ones do (while still respecting +# bounds). + +iter +set-bounds lower=a upper=aa +seek-ge a +---- +. +a: (4, .) +stats: (interface (dir, seek, step): (fwd, 12, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 11, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 4 + +iter +set-bounds lower=a upper=c +seek-ge b +---- +. +b: (1, .) +stats: (interface (dir, seek, step): (fwd, 13, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 12, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 4 +SeekPrefixGEs with trySeekUsingNext: 4 + +iter +seek-ge bb +---- +. +stats: (interface (dir, seek, step): (fwd, 14, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 13, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 5 +SeekPrefixGEs with trySeekUsingNext: 4 + +iter +set-bounds lower=a upper=d +seek-ge bbb +---- +. +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 15, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 14, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 5 +SeekPrefixGEs with trySeekUsingNext: 4 + +iter +seek-ge cc +---- +. +stats: (interface (dir, seek, step): (fwd, 16, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 15, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 6 +SeekPrefixGEs with trySeekUsingNext: 4 + +# Shifting bounds, with non-overlapping and monotonic bounds. A set-bounds sits +# between every two seeks. We don't call trySeekUsingNext=true when the bounds +# are set to unequal bounds, but the results are still correct and within +# bounds. We do call trySeekUsingNext=true when the set bounds are identical. + +iter +set-bounds lower=a upper=c +seek-ge b +---- +. +b: (1, .) +stats: (interface (dir, seek, step): (fwd, 17, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 16, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 6 +SeekPrefixGEs with trySeekUsingNext: 4 + +iter +set-bounds lower=c upper=e +seek-ge c +---- +. +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 18, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 17, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 6 +SeekPrefixGEs with trySeekUsingNext: 4 + +# NB: Equal bounds. + +iter +set-bounds lower=c upper=e +seek-ge d +---- +. +d: (2, .) +stats: (interface (dir, seek, step): (fwd, 19, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 18, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 8 +SeekPrefixGEs with trySeekUsingNext: 4 + +iter +set-bounds lower=a upper=c +seek-prefix-ge b +---- +. +b: (1, .) +stats: (interface (dir, seek, step): (fwd, 20, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 19, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 8 +SeekPrefixGEs with trySeekUsingNext: 4 + +iter +set-bounds lower=c upper=e +seek-prefix-ge c +---- +. +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 21, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 20, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 8 +SeekPrefixGEs with trySeekUsingNext: 4 + +# NB: Equal bounds. + +iter +set-bounds lower=c upper=e +seek-prefix-ge d +---- +. +d: (2, .) +stats: (interface (dir, seek, step): (fwd, 22, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 21, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 8 +SeekPrefixGEs with trySeekUsingNext: 6 + +# Shifting bounds, with non-overlapping and monotonic bounds, but using +# SetOptions. A set-options sits between every two seeks. We don't call +# trySeekUsingNext=true when the bounds are set to unequal bounds, but the +# results are still correct and within bounds. We do call trySeekUsingNext=true +# when the set bounds are identical. + +iter +set-options lower=a upper=c +seek-ge b +---- +. +b: (1, .) +stats: (interface (dir, seek, step): (fwd, 23, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 22, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 8 +SeekPrefixGEs with trySeekUsingNext: 6 + +iter +set-options lower=c upper=e +seek-ge c +---- +. +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 24, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 23, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 8 +SeekPrefixGEs with trySeekUsingNext: 6 + +# NB: Equal bounds. + +iter +set-options lower=c upper=e +seek-ge d +---- +. +d: (2, .) +stats: (interface (dir, seek, step): (fwd, 25, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 24, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 10 +SeekPrefixGEs with trySeekUsingNext: 6 + +iter +set-options lower=a upper=c +seek-prefix-ge b +---- +. +b: (1, .) +stats: (interface (dir, seek, step): (fwd, 26, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 25, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 10 +SeekPrefixGEs with trySeekUsingNext: 6 + +iter +set-options lower=c upper=e +seek-prefix-ge c +---- +. +c: (1, .) +stats: (interface (dir, seek, step): (fwd, 27, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 26, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 10 +SeekPrefixGEs with trySeekUsingNext: 6 + +# NB: Equal bounds. + +iter +set-options lower=c upper=e +seek-prefix-ge d +---- +. +d: (2, .) +stats: (interface (dir, seek, step): (fwd, 28, 1), (rev, 0, 1)), (internal (dir, seek, step): (fwd, 27, 1), (rev, 0, 3)) +SeekGEs with trySeekUsingNext: 10 +SeekPrefixGEs with trySeekUsingNext: 8 diff --git a/pebble/testdata/iterator_seek_opt_errors b/pebble/testdata/iterator_seek_opt_errors new file mode 100644 index 0000000..631dc04 --- /dev/null +++ b/pebble/testdata/iterator_seek_opt_errors @@ -0,0 +1,87 @@ +define +a.SET.1:a +b.SET.1:b +c.SET.1:c +d.SET.1:d +---- + +# Exercise noop optimization with no errors + +iter +seek-ge aa +seek-ge aa +seek-ge aaa +seek-ge b +seek-ge bb +---- +b: (b, .) +b: (b, .) +b: (b, .) +b: (b, .) +c: (c, .) + +iter +seek-lt ddd +seek-lt ddd +seek-lt dd +seek-lt d +seek-lt c +---- +d: (d, .) +d: (d, .) +d: (d, .) +c: (c, .) +b: (b, .) + +# Exercise errors which should prevent seek optimizations. + +iter seek-error=(0,1) +seek-ge a +seek-ge b +seek-ge c +seek-ge d +---- +err=injecting error +err=injecting error +c: (c, .) +d: (d, .) + +iter seek-error=(1) +seek-ge d +seek-ge a +seek-ge b +seek-ge b +---- +d: (d, .) +err=injecting error +b: (b, .) +b: (b, .) + +iter seek-error=(0,1) +seek-lt e +seek-lt d +seek-lt c +seek-lt b +---- +err=injecting error +err=injecting error +b: (b, .) +a: (a, .) + +iter seek-error=(1) +seek-lt b +seek-lt e +seek-lt e +---- +a: (a, .) +err=injecting error +d: (d, .) + +iter seek-error=(1) +seek-prefix-ge d +seek-prefix-ge a +seek-prefix-ge b +---- +d: (d, .) +err=injecting error +b: (b, .) diff --git a/pebble/testdata/iterator_stats b/pebble/testdata/iterator_stats new file mode 100644 index 0000000..c6375b4 --- /dev/null +++ b/pebble/testdata/iterator_stats @@ -0,0 +1,80 @@ +build ext1 +merge a 1 +set c 2 +---- + +ingest ext1 +---- +6: + 000004:[a#10,MERGE-c#10,SET] + +iter +first +next +next +stats +---- +a: (1, .) +c: (2, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 57B, cached 57B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +# Perform the same operation again with a new iterator. It should yield +# identical statistics. + +iter +first +next +next +stats +---- +a: (1, .) +c: (2, .) +. +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 57B, cached 57B, read-time 0s)), (points: (count 2, key-bytes 2B, value-bytes 2B, tombstoned 0))) + +build ext2 +set d@10 d10 +set d@9 d9 +set d@8 d8 +set e@20 e20 +set e@18 e18 +---- + +ingest ext2 +---- +6: + 000004:[a#10,MERGE-c#10,SET] + 000005:[d@10#11,SET-e@18#11,SET] + +iter +seek-ge c +stats +next +next +stats +next +stats +next +stats +next +stats +---- +c: (2, .) +stats: (interface (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 0), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 57B, cached 57B, read-time 0s)), (points: (count 1, key-bytes 1B, value-bytes 1B, tombstoned 0))) +d@10: (d10, .) +d@9: (d9, .) +stats: (interface (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 2), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 157B, cached 147B, read-time 0s)), (points: (count 3, key-bytes 8B, value-bytes 6B, tombstoned 0)), (separated: (count 1, bytes 2B, fetched 2B))) +d@8: (d8, .) +stats: (interface (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 3), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 157B, cached 147B, read-time 0s)), (points: (count 4, key-bytes 11B, value-bytes 8B, tombstoned 0)), (separated: (count 2, bytes 4B, fetched 4B))) +e@20: (e20, .) +stats: (interface (dir, seek, step): (fwd, 1, 4), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 4), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 157B, cached 147B, read-time 0s)), (points: (count 5, key-bytes 15B, value-bytes 11B, tombstoned 0)), (separated: (count 2, bytes 4B, fetched 4B))) +e@18: (e18, .) +stats: (interface (dir, seek, step): (fwd, 1, 5), (rev, 0, 0)), (internal (dir, seek, step): (fwd, 1, 5), (rev, 0, 0)), +(internal-stats: (block-bytes: (total 157B, cached 147B, read-time 0s)), (points: (count 6, key-bytes 19B, value-bytes 13B, tombstoned 0)), (separated: (count 3, bytes 7B, fetched 7B))) diff --git a/pebble/testdata/iterator_table_filter b/pebble/testdata/iterator_table_filter new file mode 100644 index 0000000..9eeffea --- /dev/null +++ b/pebble/testdata/iterator_table_filter @@ -0,0 +1,66 @@ +define +L0 + a.SET.4:4 +L1 + a.SET.3:3 +L2 + a.SET.2:2 +L3 + a.SET.1:1 +---- +0.0: + 000004:[a#4,SET-a#4,SET] +1: + 000005:[a#3,SET-a#3,SET] +2: + 000006:[a#2,SET-a#2,SET] +3: + 000007:[a#1,SET-a#1,SET] + +iter +first +---- +a: (4, .) + +# Only scan tables with min-seq-num < filter. + +iter filter=5 +first +---- +a: (4, .) + +iter filter=4 +first +---- +a: (3, .) + +iter filter=3 +first +---- +a: (2, .) + +iter filter=2 +first +---- +a: (1, .) + +iter filter=1 +first +---- +. + +# Set-options that reuses the filter should still see the filter apply. +# Set-options that removes the filter should not. + +iter filter=4 +first +set-options table-filter=reuse +first +set-options table-filter=none +first +---- +a: (3, .) +. +a: (3, .) +. +a: (4, .) diff --git a/pebble/testdata/level_checker b/pebble/testdata/level_checker new file mode 100644 index 0000000..d5788ed --- /dev/null +++ b/pebble/testdata/level_checker @@ -0,0 +1,358 @@ +# Format for define command: +# Levels are ordered from higher to lower, and each new level starts with an L +# Each level is defined using an even number of lines where every pair of lines represents +# a file. The files within a level are ordered from smaller to larger keys. +# Each file is defined using: the first line specifies the smallest and largest internal +# keys and the second line the point key-value pairs in the sstable in order. The rangedel +# key-value pairs should also be in increasing order relative to the other rangedel pairs. +# The largest file key can take the form of .RANGEDEL.72057594037927935, which +# represents the range deletion sentinel. +# +# Many of the correct case definitions are borrowed from merging_iter since it defines +# some tricky configurations. + +# The untruncated range tombstone at the higher level does not overlap with the lower level once +# we consider the file boundaries, which is why its lower sequence number is ok. +define +L +a.SET.30 e.RANGEDEL.72057594037927935 +a.SET.30:30 c.SET.27:27 a.RANGEDEL.8:f +L +e.SET.10 g.SET.20 +e.SET.10:10 g.SET.20:20 e.RANGEDEL.8:f +---- +Level 1 + file 0: [a#30,1-e#72057594037927935,15] +Level 2 + file 0: [e#10,1-g#20,1] + +check +---- + +# The untruncated range tombstone at the higher level does not overlap with the g point at the +# lower level once we consider the file boundaries, which is why its lower sequence number is ok. +define +L +a.SET.15 f.SET.16 +a.SET.15:15 c.SET.13:13 f.SET.16:16 a.RANGEDEL.12:h +L +e.SET.10 g.SET.15 +e.SET.10:10 g.SET.15:15 +---- +Level 1 + file 0: [a#15,1-f#16,1] +Level 2 + file 0: [e#10,1-g#15,1] + +check +---- + +# The untruncated range tombstones in both levels do not overlap once we consider the file +# boundaries (which are non-overlapping). +define +L +c.SET.30 f.RANGEDEL.0 +c.SET.30:30 d.SET.27:27 a.RANGEDEL.8:f +L +a.SET.10 c.RANGEDEL.72057594037927935 +a.SET.10:10 b.SET.12:12 a.RANGEDEL.8:f +---- +Level 1 + file 0: [c#30,1-f#0,15] +Level 2 + file 0: [a#10,1-c#72057594037927935,15] + +check +---- + +# The range tombstone in the higher level does not overlap with the b point in the lower level, +# which has a higher sequence number, when we consider the file boundaries. +define +L +c.SET.15 g.SET.16 +c.SET.15:15 f.SET.13:13 g.SET.16:16 a.RANGEDEL.12:h +L +b.SET.14 d.SET.10 +b.SET.14:14 d.SET.10:10 +---- +Level 1 + file 0: [c#15,1-g#16,1] +Level 2 + file 0: [b#14,1-d#10,1] + +check +---- + +# The two files do not overlap despite the seeming overlap of the range tombstones. +define +L +a.SET.30 e.RANGEDEL.72057594037927935 +a.SET.30:30 c.SET.27:27 a.RANGEDEL.8:g +L +e.SET.10 g.SET.20 +e.SET.10:10 g.SET.20:20 e.RANGEDEL.8:g +---- +Level 1 + file 0: [a#30,1-e#72057594037927935,15] +Level 2 + file 0: [e#10,1-g#20,1] + +check +---- + +define +L +a.SET.30 e.RANGEDEL.72057594037927935 +a.SET.30:30 c.SET.27:27 a.RANGEDEL.8:g +L +a.SET.10 g.SET.20 +a.SET.10:10 c.SET.28:28 g.SET.20:20 +---- +Level 1 + file 0: [a#30,1-e#72057594037927935,15] +Level 2 + file 0: [a#10,1-g#20,1] + +check +---- +found InternalKey c#27,SET in L1: fileNum=000010 and InternalKey c#28,SET in L2: fileNum=000011 + +# The sentinel key for the RANGEDEL should not violate g having a higher seq num at a +# lower level. +define +L +a.SET.30 g.RANGEDEL.72057594037927935 +a.SET.30:30 c.SET.27:27 a.RANGEDEL.8:g +L +g.SET.10 j.SET.20 +g.SET.10:10 j.SET.20:20 +---- +Level 1 + file 0: [a#30,1-g#72057594037927935,15] +Level 2 + file 0: [g#10,1-j#20,1] + +check +---- + +define +L +a.SET.30 g.SET.8 +a.SET.30:30 c.SET.27:27 a.RANGEDEL.8:g g.SET.8:8 +L +g.SET.10 j.SET.20 +g.SET.10:10 j.SET.20:20 +---- +Level 1 + file 0: [a#30,1-g#8,1] +Level 2 + file 0: [g#10,1-j#20,1] + +check +---- +found InternalKey g#8,SET in L1: fileNum=000014 and InternalKey g#10,SET in L2: fileNum=000015 + +define +L +a.SET.30 g.SET.30 +a.SET.30:30 c.SET.8:8 g.SET.30:30 +L +a.SET.10 j.SET.20 +a.SET.10:10 j.SET.20:20 b.RANGEDEL.10:g +---- +Level 1 + file 0: [a#30,1-g#30,1] +Level 2 + file 0: [a#10,1-j#20,1] + +check +---- +tombstone b-g:{(#10,RANGEDEL)} in L2: fileNum=000017 deletes key c#8,SET in L1: fileNum=000016 + +define +L +a.RANGEDEL.8 c.RANGEDEL.72057594037927935 +a.RANGEDEL.8:c +L +a.RANGEDEL.6 d.RANGEDEL.72057594037927935 +a.RANGEDEL.6:d b.RANGEDEL.10:c +---- +Level 1 + file 0: [a#8,15-c#72057594037927935,15] +Level 2 + file 0: [a#6,15-d#72057594037927935,15] + +check +---- +encountered tombstone b-c:{(#8,RANGEDEL)} in L1: fileNum=000018 that has a lower seqnum than the same tombstone in L2: fileNum=000019 + +# Check incorrect ordering of point keys in an sstable. +define disable-key-order-checks +L +a.SET.3 e.SET.4 +e.SET.4:e a.SET.3:a +L +d.SET.1 f.SET.2 +d.SET.1:d f.SET.2:f +---- +Level 1 + file 0: [a#3,1-e#4,1] +Level 2 + file 0: [d#1,1-f#2,1] + +check +---- +out of order keys e#4,SET >= a#3,SET in L1: fileNum=000020 + +# Check successive sstables on a level are ordered. +define disable-key-order-checks +L +a.SET.1 b.SET.2 +a.SET.1:a b.SET.2:b +b.SET.3 c.SET.4 +b.SET.3:b c.SET.4:c +---- +Level 1 + file 0: [a#1,1-b#2,1] + file 1: [b#3,1-c#4,1] + +check +---- +out of order keys b#2,SET >= b#3,SET in L1: fileNum=000023 + +# Check range delete keys are fragmented and ordered in an sstable having +# rangeDelV2 formatted range delete blocks. + +# Case 1: Fragmented but not ordered. +define write-unfragmented disable-key-order-checks +L +a.RANGEDEL.1 g.RANGEDEL.72057594037927935 +d.RANGEDEL.2:e d.RANGEDEL.1:e f.RANGEDEL.3:g a.RANGEDEL.4:b +---- +Level 1 + file 0: [a#1,15-g#72057594037927935,15] + +check +---- +unordered or unfragmented range delete tombstones f-g:{(#3,RANGEDEL)}, a-b:{(#4,RANGEDEL)} in L1: fileNum=000024 + +# Case 2: Ordered but not fragmented. +define write-unfragmented disable-key-order-checks +L +a.RANGEDEL.1 d.RANGEDEL.72057594037927935 +a.RANGEDEL.1:d b.RANGEDEL.2:c +---- +Level 1 + file 0: [a#1,15-d#72057594037927935,15] + +check +---- +unordered or unfragmented range delete tombstones a-d:{(#1,RANGEDEL)}, b-c:{(#2,RANGEDEL)} in L1: fileNum=000025 + +# Case 3: Verify check is done before truncation. +define write-unfragmented disable-key-order-checks +L +a.RANGEDEL.1 b.RANGEDEL.72057594037927935 +a.RANGEDEL.1:z d.RANGEDEL.2:e +---- +Level 1 + file 0: [a#1,15-b#72057594037927935,15] + +check +---- +unordered or unfragmented range delete tombstones a-z:{(#1,RANGEDEL)}, d-e:{(#2,RANGEDEL)} in L1: fileNum=000026 + +# Merge record processing. + +# Case 1: Latest versions of a key are MERGE records and processing one of +# them fails. +define +L +a.MERGE.10 a.MERGE.9 +a.MERGE.10:10 a.MERGE.9:fail-merge +---- +Level 1 + file 0: [a#10,2-a#9,2] + +check merger=fail-merger +---- +merge processing error on key a#9,MERGE in L1: fileNum=000027: merge failed + +# Case 2: Last checked key is a MERGE record. +define +L +a.MERGE.10 a.MERGE.9 +a.MERGE.10:10 a.MERGE.9:fail-finish +---- +Level 1 + file 0: [a#10,2-a#9,2] + +check merger=fail-merger +---- +merge processing error on key a#9,MERGE in L1: fileNum=000028: finish failed + +# Case 3: MERGE records succeeded by newer versions of a key are also +# processed. +define +L +a.MERGE.10 a.SINGLEDEL.3 +a.MERGE.10:10 a.MERGE.9:9 a.SET.8:8 a.MERGE.7:7 a.MERGE.6:6 a.DEL.5: a.MERGE.4:fail-finish a.SINGLEDEL.3: +---- +Level 1 + file 0: [a#10,2-a#3,7] + +check merger=fail-merger +---- +merge processing error on key a#3,SINGLEDEL in L1: fileNum=000029: finish failed + +# Case 4: Finish processing on key change. +define +L +a.MERGE.10 b.SET.11 +a.MERGE.10:10 a.MERGE.9:fail-finish b.SET.11:11 +---- +Level 1 + file 0: [a#10,2-b#11,1] + +check merger=fail-merger +---- +merge processing error on key b#11,SET in L1: fileNum=000030: finish failed + +# Case 5: SET finishes MERGE record processing. +define +L +a.MERGE.10 a.SET.9 +a.MERGE.10:10 a.SET.9:fail-finish +---- +Level 1 + file 0: [a#10,2-a#9,1] + +check merger=fail-merger +---- +merge processing error on key a#9,SET in L1: fileNum=000031: finish failed + +# Case 6: DEL finishes MERGE record processing. +define +L +a.MERGE.10 a.DEL.9 +a.MERGE.10:fail-finish a.DEL.9: +---- +Level 1 + file 0: [a#10,2-a#9,0] + +check merger=fail-merger +---- +merge processing error on key a#9,DEL in L1: fileNum=000032: finish failed + +# Case 7: SINGLEDEL finishes MERGE record processing. +define +L +a.MERGE.10 a.SINGLEDEL.9 +a.MERGE.10:fail-finish a.SINGLEDEL.9: +---- +Level 1 + file 0: [a#10,2-a#9,7] + +check merger=fail-merger +---- +merge processing error on key a#9,SINGLEDEL in L1: fileNum=000033: finish failed diff --git a/pebble/testdata/level_iter b/pebble/testdata/level_iter new file mode 100644 index 0000000..3728027 --- /dev/null +++ b/pebble/testdata/level_iter @@ -0,0 +1,500 @@ +define +a.SET.1:1 b.SET.2:2 +c.SET.3:3 d.SET.4:4 +dd.SET.5:5 +---- + +iter +seek-ge a +next +next +next +next +next +---- +a#1,1:1 +b#2,1:2 +c#3,1:3 +d#4,1:4 +dd#5,1:5 +. + +iter +seek-ge b +next +next +next +next +---- +b#2,1:2 +c#3,1:3 +d#4,1:4 +dd#5,1:5 +. + +iter +seek-ge c +next +next +next +---- +c#3,1:3 +d#4,1:4 +dd#5,1:5 +. + +iter +seek-ge d +next +next +---- +d#4,1:4 +dd#5,1:5 +. + +iter +seek-ge dd +next +---- +dd#5,1:5 +. + +iter +seek-ge e +---- +. + +iter +seek-lt a +---- +. + +iter +seek-lt b +prev +---- +a#1,1:1 +. + +iter +seek-lt c +prev +prev +---- +b#2,1:2 +a#1,1:1 +. + +iter +seek-lt d +prev +prev +prev +---- +c#3,1:3 +b#2,1:2 +a#1,1:1 +. + +iter +seek-lt e +prev +prev +prev +prev +prev +---- +dd#5,1:5 +d#4,1:4 +c#3,1:3 +b#2,1:2 +a#1,1:1 +. + +iter +seek-prefix-ge a +next +---- +a#1,1:1 +b#2,1:2 + +iter +seek-prefix-ge d +next +next +---- +d#4,1:4 +dd#5,1:5 +. + +iter +seek-prefix-ge dd +next +---- +dd#5,1:5 +. + +iter +seek-prefix-ge d +next +prev +prev +---- +d#4,1:4 +dd#5,1:5 +d#4,1:4 +c#3,1:3 + +iter +seek-prefix-ge d +prev +---- +d#4,1:4 +c#3,1:3 + +iter +seek-prefix-ge dd +prev +---- +dd#5,1:5 +d#4,1:4 + +iter lower=a +seek-ge a +first +---- +a#1,1:1 +a#1,1:1 + +iter +set-bounds lower=a +seek-ge a +first +---- +a#1,1:1 +a#1,1:1 + +iter +set-bounds lower=dd upper=f +seek-lt dc +set-bounds lower=a upper=f +seek-lt dc +prev +prev +prev +prev +---- +. +d#4,1:4 +c#3,1:3 +b#2,1:2 +a#1,1:1 +. + +iter +set-bounds lower=a upper=b +seek-ge c +set-bounds lower=a upper=f +seek-ge c +next +next +next +---- +. +c#3,1:3 +d#4,1:4 +dd#5,1:5 +. + +# levelIter trims lower/upper bound in the options passed to sstables. +load a +---- +[,] + +load b lower=aa upper=bb +---- +[aa,] + +load b lower=aa upper=c +---- +[aa,] + +load c lower=b upper=d +---- +[,d] + +load c lower=b upper=e +---- +[,] + +# levelIter only checks lower bound when loading sstables. +iter lower=b +seek-ge a +first +---- +a#1,1:1 +a#1,1:1 + +iter lower=c +seek-ge a +first +---- +c#3,1:3 +c#3,1:3 + +iter +set-bounds lower=b +seek-ge a +first +---- +a#1,1:1 +a#1,1:1 + +iter +set-bounds lower=c +seek-ge a +first +---- +c#3,1:3 +c#3,1:3 + +# levelIter only checks lower bound when loading sstables. +iter lower=d +seek-ge a +first +---- +c#3,1:3 +c#3,1:3 + +iter lower=e +seek-ge a +first +---- +. +. + +iter upper=e +seek-lt e +last +---- +dd#5,1:5 +dd#5,1:5 + +iter +set-bounds lower=d +seek-ge a +first +---- +c#3,1:3 +c#3,1:3 + +iter +set-bounds lower=e +seek-ge a +first +---- +. +. + +iter +set-bounds upper=e +seek-lt e +last +---- +dd#5,1:5 +dd#5,1:5 + +# levelIter only checks upper bound when loading sstables. +iter upper=d +seek-lt e +last +---- +d#4,1:4 +d#4,1:4 + +iter upper=c +seek-lt e +last +---- +b#2,1:2 +b#2,1:2 + +iter +set-bounds upper=d +seek-lt e +last +---- +d#4,1:4 +d#4,1:4 + +iter +set-bounds upper=c +seek-lt e +last +---- +b#2,1:2 +b#2,1:2 + +# levelIter only checks upper bound when loading sstables. +iter upper=b +seek-lt e +last +---- +b#2,1:2 +b#2,1:2 + +iter upper=a +seek-lt e +last +---- +. +. + +iter upper=dd +seek-prefix-ge d +next +---- +d#4,1:4 +. + +iter +set-bounds upper=b +seek-lt e +last +---- +b#2,1:2 +b#2,1:2 + +iter +set-bounds upper=a +seek-lt e +last +---- +. +. + +iter +set-bounds upper=dd +seek-prefix-ge d +next +---- +d#4,1:4 +. + +iter upper=e +seek-prefix-ge d +next +next +---- +d#4,1:4 +dd#5,1:5 +. + +iter lower=dd +seek-prefix-ge d +next +---- +dd#5,1:5 +. + +iter lower=d +seek-prefix-ge dd +prev +---- +dd#5,1:5 +d#4,1:4 + +iter lower=c +seek-prefix-ge dd +prev +---- +dd#5,1:5 +d#4,1:4 + +iter lower=c +seek-lt c +---- +. + +iter +seek-lt c +set-bounds lower=c +seek-lt c +---- +b#2,1:2 +. + +iter upper=c +seek-ge c +---- +. + +iter +seek-ge c +set-bounds upper=c +seek-ge c +---- +c#3,1:3 +. + +# The behavior of next/prev after set-bounds is undefined. We're just +# asserting the current behavior. + +# The lower bound is beyond the current table's bounds. + +iter +seek-ge c +set-bounds lower=e +next +---- +c#3,1:3 +. + +# The lower bound lies within the current table's bounds. + +iter +seek-ge c +set-bounds lower=d +next +---- +c#3,1:3 +d#4,1:4 + +# The upper bound is before the current table's bounds. + +iter +seek-ge d +set-bounds upper=c +prev +---- +d#4,1:4 +. + +# The upper bound lies within the current table's bounds. + +iter +seek-ge d +set-bounds upper=cc +prev +---- +d#4,1:4 +c#3,1:3 + +# Setting bounds should update the table bounds, allowing a subsequent +# seek-ge/seek-lt to see the boundary keys. + +iter +seek-ge d +set-bounds lower=cc +seek-lt d +---- +d#4,1:4 +. + +iter +seek-ge c +set-bounds upper=cc +seek-ge d +---- +c#3,1:3 +. diff --git a/pebble/testdata/level_iter_boundaries b/pebble/testdata/level_iter_boundaries new file mode 100644 index 0000000..319baf6 --- /dev/null +++ b/pebble/testdata/level_iter_boundaries @@ -0,0 +1,382 @@ +build +a.RANGEDEL.1:c +b.RANGEDEL.2:d +---- +0: a#1,15-d#72057594037927935,15 + +iter +first +next +last +prev +---- +d#72057594037927935,15: +. +a#1,15: +. + +iter +seek-ge c +seek-ge d +seek-lt b +prev +---- +d#72057594037927935,15: +. +a#1,15: +. + +iter +seek-prefix-ge c +seek-prefix-ge d +seek-lt b +prev +---- +d#72057594037927935,15: +. +a#1,15: +. + +iter +seek-ge e +seek-lt a +---- +. +. + +iter +seek-prefix-ge e +seek-lt a +---- +. +. + +clear +---- + +build +a.SET.1:a +---- +0: a#1,1-a#1,1 + +build +b.RANGEDEL.2:c +---- +0: a#1,1-a#1,1 +1: b#2,15-c#72057594037927935,15 + +build +c.SET.3:c +---- +0: a#1,1-a#1,1 +1: b#2,15-c#72057594037927935,15 +2: c#3,1-c#3,1 + +iter +first +next +next +next +---- +a#1,1:a +c#72057594037927935,15: +c#3,1:c +. + +iter +last +prev +prev +prev +---- +c#3,1:c +b#2,15: +a#1,1:a +. + +clear +---- + +build +a.SET.1:b +b.RANGEDEL.2:c +---- +0: a#1,1-c#72057594037927935,15 + +iter +first +next +next +---- +a#1,1:b +c#72057594037927935,15: +. + +iter +last +prev +---- +a#1,1:b +. + +clear +---- + +build +a.RANGEDEL.1:b +c.SET.2:c +---- +0: a#1,15-c#2,1 + +iter +first +next +---- +c#2,1:c +. + +iter +last +prev +prev +---- +c#2,1:c +a#1,15: +. + +# Regression test to check that Seek{GE,LT} work properly in certain +# cases when then levelIter is positioned at a boundary key. + +clear +---- + +build +d.SET.3:d +c.RANGEDEL.2:e +---- +0: c#2,15-e#72057594037927935,15 + +iter +seek-ge d +next +seek-ge d +next +seek-lt e +prev +seek-ge d +prev +seek-lt e +---- +d#3,1:d +e#72057594037927935,15: +d#3,1:d +e#72057594037927935,15: +d#3,1:d +c#2,15: +d#3,1:d +c#2,15: +d#3,1:d + +# Regression test to check that Seek{GE,LT}, First, and Last do not +# have iteration bounds affected by SeekPrefixGE. Previously, +# SeekPrefixGE adjusted the iteration upper bound which would leak +# over to other positioning operations. While SeekPrefixGE no longer +# has this behavior, it is good to check the iteration bounds handling +# regardless. + +clear +---- + +build +b.SET.4:b +d.SET.3:d +---- +0: b#4,1-d#3,1 + +iter +seek-prefix-ge c +seek-ge d +next +---- +. +d#3,1:d +. + +iter +seek-prefix-ge c +seek-lt e +next +---- +. +d#3,1:d +. + +iter +seek-prefix-ge c +first +next +next +---- +. +b#4,1:b +d#3,1:d +. + +iter +seek-prefix-ge c +last +next +---- +. +d#3,1:d +. + +clear +---- + +build +a.SET.3:z +d.SET.4:z +---- +0: a#3,1-d#4,1 + +build +e.SET.5:z +f.SET.6:z +g.RANGEDEL.2:h +---- +0: a#3,1-d#4,1 +1: e#5,1-h#72057594037927935,15 + +build +j.SET.6:z +---- +0: a#3,1-d#4,1 +1: e#5,1-h#72057594037927935,15 +2: j#6,1-j#6,1 + +# Test cases to check that when the bounds are contained within a file, iterating +# beyond the bounds does not cause the levelIter to change to the next/prev file. +# This is not a correctness issue, but is a useful performance optimization and +# we want to verify that the code does what we want it to. +iter save +set-bounds lower=a upper=b +seek-ge a +next +---- +a#3,1:z +. + +file-pos +---- +file 0 + +iter save continue +seek-prefix-ge a +next +---- +a#3,1:z +. + +file-pos +---- +file 0 + +iter save continue +set-bounds lower=b upper=c +seek-ge b +---- +. + +file-pos +---- +file 0 + +iter save continue +seek-prefix-ge b +---- +. + +file-pos +---- +file 0 + +# Seek to an earlier position just as a sanity check. +iter save continue +set-bounds lower=a upper=b +seek-ge a +next +---- +a#3,1:z +. + +file-pos +---- +file 0 + +iter save continue +set-bounds lower=d upper=e +seek-ge d +next +---- +d#4,1:z +. + +file-pos +---- +file 1 + +iter save continue +seek-prefix-ge d +next +---- +d#4,1:z +. + +file-pos +---- +file 1 + +iter save continue +set-bounds lower=e upper=f +seek-ge e +next +next +---- +e#5,1:z +f#72057594037927935,15: +. + +file-pos +---- +file 1 + +iter save continue +seek-lt f +prev +prev +---- +e#5,1:z +. +. + +file-pos +---- +file 0 + +iter save continue +set-bounds lower=f upper=g +seek-lt g +prev +prev +---- +f#6,1:z +f#72057594037927935,15: +. + +file-pos +---- +file 1 + +iter continue +---- diff --git a/pebble/testdata/level_iter_seek b/pebble/testdata/level_iter_seek new file mode 100644 index 0000000..0557080 --- /dev/null +++ b/pebble/testdata/level_iter_seek @@ -0,0 +1,391 @@ +# Note that this test file uses a levelIterTestIter which combines a +# point iterator and a range-del iterator, returning both results in a +# single key: +# +# /# + +# Verify that SeekGE, SeekLT, Next, and Prev all pause at table +# boundaries in the presence of lower/upper bounds and range +# tombstones. Verify that SeekPrefixGE pauses at a table boundary in +# the presence of range tombstones. + +build +a.SET.9:a +b.SET.8:b +---- +0: a#9,1-b#8,1 + +build +c.SET.7:c +d.RANGEDEL.6:e +f.SET.5:f +---- +0: a#9,1-b#8,1 +1: c#7,1-f#5,1 + +build +g.SET.4:g +h.SET.3:h +---- +0: a#9,1-b#8,1 +1: c#7,1-f#5,1 +2: g#4,1-h#3,1 + +iter +seek-ge d +---- +f/d-e:{(#6,RANGEDEL)}#5,1:f + +iter +set-bounds upper=d +seek-ge d +---- +d/d-e:{(#6,RANGEDEL)}#72057594037927935,15: + +iter +set-bounds upper=d +seek-ge c +next +prev +next +next +---- +c/d-e:{(#6,RANGEDEL)}#7,1:c +d#72057594037927935,15: +c#7,1:c +d#72057594037927935,15: +. + +# There is no point key with d, but since there is a rangedel, levelIter returns +# the boundary key using the largest key, f, in the file. +iter +seek-prefix-ge d +---- +f/d-e:{(#6,RANGEDEL)}#5,1: + +# Tests a sequence of SeekPrefixGE with monotonically increasing keys, some of +# which are present and some not (so fail the bloom filter match). The seek to +# cc returns a boundary key. +iter +seek-prefix-ge aa +seek-prefix-ge c +seek-prefix-ge cc +seek-prefix-ge f +seek-prefix-ge g +seek-prefix-ge gg +seek-prefix-ge h +---- +./#0,0: +c/d-e:{(#6,RANGEDEL)}#7,1:c +f/d-e:{(#6,RANGEDEL)}#5,1: +f/#5,1:f +g/#4,1:g +./#0,0: +h/#3,1:h + +# Test that when sequentially iterate through all 3 files, the stats +# accumulate as we close a file and switch to the next one. Also, while in the +# middle of the first file, a reset-stats propagates to the underlying +# iterators, and when done iterating, a reset-stats does reset the local +# state. +iter +seek-ge a +stats +reset-stats +stats +next +stats +next +stats +next +stats +next +stats +next +stats +next +stats +reset-stats +stats +---- +a/#9,1:a +{BlockBytes:56 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +b#8,1:b +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +c#7,1:c +{BlockBytes:56 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +f#5,1:f +{BlockBytes:56 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +g#4,1:g +{BlockBytes:112 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +h#3,1:h +{BlockBytes:112 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +. +{BlockBytes:112 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +iter +set-bounds lower=d +seek-lt d +---- +d/d-e:{(#6,RANGEDEL)}#72057594037927935,15: + +iter +set-bounds lower=d +seek-lt g +prev +next +prev +prev +---- +f/d-e:{(#6,RANGEDEL)}#5,1:f +d#72057594037927935,15: +f#5,1:f +d#72057594037927935,15: +. + +# Verify that First() in the presence of an upper-bound pauses at the +# table containing the upper-bound. + +clear +---- + +build +d.RANGEDEL.6:e +f.SET.5:f +---- +0: d#6,15-f#5,1 + +iter +set-bounds upper=f +first +---- +f#72057594037927935,15: + +# Verify that Last() in the presence of a lower-bound pauses at the +# table containing the lower-bound. + +clear +---- + +build +c.SET.7:c +d.RANGEDEL.6:e +---- +0: c#7,1-e#72057594037927935,15 + +iter +set-bounds lower=c +last +---- +c#7,1:c + +# Verify that a seek to a file with range tombstones as boundaries pauses on +# those boundaries. + +clear +---- + +build +a.RANGEDEL.5:b +c.SET.7:c +d.RANGEDEL.6:e +---- +0: a#5,15-e#72057594037927935,15 + +build +f.SET.8:f +g.SET.9:g +---- +0: a#5,15-e#72057594037927935,15 +1: f#8,1-g#9,1 + +iter +seek-ge d +prev +next +next +---- +e/d-e:{(#6,RANGEDEL)}#72057594037927935,15: +c#7,1:c +e#72057594037927935,15: +f#8,1:f + +iter +seek-lt b +next +prev +prev +---- +a/a-b:{(#5,RANGEDEL)}#5,15: +c#7,1:c +a#5,15: +. + +# Verify that prev when positioned at the largest boundary returns the +# last key. + +clear +---- + +build +a.SET.1:a +b.SET.1:b +d.RANGEDEL.2:e +---- +0: a#1,1-e#72057594037927935,15 + +iter +seek-lt c +seek-ge d +prev +---- +b/#1,1:b +e/d-e:{(#2,RANGEDEL)}#72057594037927935,15: +b#1,1:b + +# Verify that next when positioned at the smallest boundary returns +# the first key. + +clear +---- + +build +a.RANGEDEL.1:b +d.SET.2:d +e.SET.2:e +---- +0: a#1,15-e#2,1 + +iter +seek-ge d +seek-lt d +next +---- +d/#2,1:d +a/a-b:{(#1,RANGEDEL)}#1,15: +d#2,1:d + +# Verify SeekPrefixGE correctness with trySeekUsingNext=true +clear +---- + +build +a.SET.1:a +b.SET.2:b +c.RANGEDEL.4:e +---- +0: a#1,1-e#72057594037927935,15 + +build +e.SET.4:e +f.SINGLEDEL.5: +f.SET.4:f +g.SET.6:g +h.SINGLEDEL.7: +---- +0: a#1,1-e#72057594037927935,15 +1: e#4,1-h#7,7 + +build +h.SET.6:h +i.SET.6:i +---- +0: a#1,1-e#72057594037927935,15 +1: e#4,1-h#7,7 +2: h#6,1-i#6,1 + +build +j.SET.7:j +---- +0: a#1,1-e#72057594037927935,15 +1: e#4,1-h#7,7 +2: h#6,1-i#6,1 +3: j#7,1-j#7,1 + +# Seeks to immediately following keys. +iter +seek-prefix-ge a false +seek-prefix-ge a true +seek-prefix-ge b true +next +seek-prefix-ge c false +seek-prefix-ge d true +seek-prefix-ge f true +seek-prefix-ge g true +seek-prefix-ge h true +seek-prefix-ge i true +seek-prefix-ge j true +---- +a/c-e:{(#4,RANGEDEL)}#1,1:a +a/c-e:{(#4,RANGEDEL)}#1,1:a +b/c-e:{(#4,RANGEDEL)}#2,1:b +e#72057594037927935,15: +e/c-e:{(#4,RANGEDEL)}#72057594037927935,15: +e/c-e:{(#4,RANGEDEL)}#72057594037927935,15: +f/#5,7: +g/#6,1:g +h/#7,7: +i/#6,1:i +j/#7,1:j + +# Seeks to keys that are in the next file, so cannot use Next. +iter +seek-prefix-ge a false +seek-prefix-ge e true +seek-prefix-ge i true +seek-prefix-ge j true +---- +a/c-e:{(#4,RANGEDEL)}#1,1:a +e/#4,1:e +i/#6,1:i +j/#7,1:j + +# Verify that we do not open files that do not have point keys. + +clear +---- + +build +a.SET.9:a +b.SET.8:b +---- +0: a#9,1-b#8,1 + +build +c.SET.7:c +d.RANGEDEL.6:e +f.SET.5:f +---- +0: a#9,1-b#8,1 +1: c#7,1-f#5,1 + +build format=pebblev2 +g.RANGEKEYDEL.6:h +---- +0: a#9,1-b#8,1 +1: c#7,1-f#5,1 +2: g#6,19-h#72057594037927935,19 + +build +i.SET.4:i +j.SET.3:j +---- +0: a#9,1-b#8,1 +1: c#7,1-f#5,1 +2: g#6,19-h#72057594037927935,19 +3: i#4,1-j#3,1 + +iter +seek-ge f +next +---- +f/#5,1:f +i#4,1:i + +# The below count should be 2, as we skip over the rangekey-only file. + +iters-created +---- +2 diff --git a/pebble/testdata/make-db.go b/pebble/testdata/make-db.go new file mode 100644 index 0000000..060935a --- /dev/null +++ b/pebble/testdata/make-db.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "log" + "os" + "strconv" + + "github.com/cockroachdb/pebble" +) + +const version = pebble.FormatFlushableIngest + +func usage() { + fmt.Fprintf(os.Stderr, "usage: %s [1,2,3,4]\n", os.Args[0]) + os.Exit(1) +} + +func main() { + if len(os.Args) != 2 { + usage() + } + // The program consists of up to 4 stages. If stage is in the range [1, 4], + // the program will exit after the stage'th stage. + // 1. create an empty DB. + // 2. add some key/value pairs. + // 3. close and re-open the DB, which forces a compaction. + // 4. add some more key/value pairs. + stage, err := strconv.Atoi(os.Args[1]) + if err != nil || stage < 1 || stage > 4 { + usage() + } + dbName := fmt.Sprintf("db-stage-%d", stage) + opts := &pebble.Options{ + FormatMajorVersion: version, + } + + fmt.Printf("Stage 1\n") + db, err := pebble.Open(dbName, opts) + if err != nil { + log.Fatal(err) + } + defer func() { + if db != nil { + db.Close() + } + }() + + if stage < 2 { + return + } + fmt.Printf("Stage 2\n") + + if err := db.Set([]byte("foo"), []byte("one"), pebble.Sync); err != nil { + log.Fatal(err) + } + if err := db.Set([]byte("bar"), []byte("two"), pebble.Sync); err != nil { + log.Fatal(err) + } + if err := db.Set([]byte("baz"), []byte("three"), pebble.Sync); err != nil { + log.Fatal(err) + } + if err := db.Set([]byte("foo"), []byte("four"), pebble.Sync); err != nil { + log.Fatal(err) + } + if err := db.Delete([]byte("bar"), pebble.Sync); err != nil { + log.Fatal(err) + } + + if stage < 3 { + return + } + fmt.Printf("Stage 3\n") + + db.Close() + db = nil + db, err = pebble.Open(dbName, opts) + if err != nil { + log.Fatal(err) + } + + if stage < 4 { + return + } + fmt.Printf("Stage 4\n") + + if err := db.Set([]byte("foo"), []byte("five"), pebble.Sync); err != nil { + log.Fatal(err) + } + if err := db.Set([]byte("quux"), []byte("six"), pebble.Sync); err != nil { + log.Fatal(err) + } + if err := db.Delete([]byte("baz"), pebble.Sync); err != nil { + log.Fatal(err) + } +} diff --git a/pebble/testdata/manual_compaction b/pebble/testdata/manual_compaction new file mode 100644 index 0000000..cd3ad5f --- /dev/null +++ b/pebble/testdata/manual_compaction @@ -0,0 +1,1236 @@ +batch +set a 1 +set b 2 +---- + +compact a-b +---- +6: + 000005:[a#10,SET-b#11,SET] + +batch +set c 3 +set d 4 +---- + +compact c-d +---- +6: + 000005:[a#10,SET-b#11,SET] + 000007:[c#12,SET-d#13,SET] + +batch +set b 5 +set c 6 +---- + +compact a-d +---- +6: + 000010:[a#0,SET-d#0,SET] + +# This also tests flushing a memtable that only contains range +# deletions. + +batch +del-range a e +---- + +compact a-d +---- + +# Test that a multi-output-file compaction generates non-overlapping files. + +define target-file-sizes=(100, 1) +L0 + b.SET.1:v +L0 + a.SET.2:v +---- +0.0: + 000005:[a#2,SET-a#2,SET] + 000004:[b#1,SET-b#1,SET] + +compact a-b +---- +1: + 000006:[a#0,SET-a#0,SET] + 000007:[b#0,SET-b#0,SET] + +# A range tombstone extends past the grandparent file boundary used to limit the +# size of future compactions. Verify the range tombstone is split at that file +# boundary. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.3:v +L2 + a.RANGEDEL.2:e +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +---- +1: + 000004:[a#3,SET-a#3,SET] +2: + 000005:[a#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +wait-pending-table-stats +000005 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 1552 + +compact a-e L1 +---- +2: + 000008:[a#3,SET-c#inf,RANGEDEL] + 000009:[c#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +wait-pending-table-stats +000008 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 776 + +# Same as above, except range tombstone covers multiple grandparent file boundaries. + +define target-file-sizes=(1, 1, 1, 1) format-major-version=1 +L1 + a.SET.3:v +L2 + a.RANGEDEL.2:g +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +L3 + e.SET.0:v + f.SET.1:v +L3 + f.SET.0:v + g.SET.0:v +---- +1: + 000004:[a#3,SET-a#3,SET] +2: + 000005:[a#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + 000009:[f#0,SET-g#0,SET] + +compact a-e L1 +---- +2: + 000010:[a#3,SET-c#inf,RANGEDEL] + 000011:[c#2,RANGEDEL-e#inf,RANGEDEL] + 000012:[e#2,RANGEDEL-f#inf,RANGEDEL] + 000013:[f#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + 000009:[f#0,SET-g#0,SET] + +# A range tombstone covers multiple grandparent file boundaries between point keys, +# rather than after all point keys. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.3:v + h.SET.3:v +L2 + a.RANGEDEL.2:g +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +L3 + e.SET.0:v + f.SET.1:v +---- +1: + 000004:[a#3,SET-h#3,SET] +2: + 000005:[a#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + +compact a-e L1 +---- +2: + 000009:[a#3,SET-c#inf,RANGEDEL] + 000010:[c#2,RANGEDEL-h#3,SET] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + +# A range tombstone is the first and only item output by a compaction, and it +# extends past the grandparent file boundary used to limit the size of future +# compactions. Verify the range tombstone is split at that file boundary. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.RANGEDEL.3:e +L2 + a.SET.2:v +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +---- +1: + 000004:[a#3,RANGEDEL-e#inf,RANGEDEL] +2: + 000005:[a#2,SET-a#2,SET] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +compact a-e L1 +---- +2: + 000008:[a#3,RANGEDEL-c#inf,RANGEDEL] + 000009:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +# An elided range tombstone is the first item encountered by a compaction, +# and the grandparent limit set by it extends to the next item, also a range +# tombstone. The first item should be elided, and the second item should +# reset the grandparent limit. + +define target-file-sizes=(100, 100, 100, 100) +L1 + a.RANGEDEL.4:d +L1 + grandparent.RANGEDEL.2:z + h.SET.3:v +L2 + grandparent.SET.1:v +L3 + grandparent.SET.0:v +L3 + m.SET.0:v +---- +1: + 000004:[a#4,RANGEDEL-d#inf,RANGEDEL] + 000005:[grandparent#2,RANGEDEL-z#inf,RANGEDEL] +2: + 000006:[grandparent#1,SET-grandparent#1,SET] +3: + 000007:[grandparent#0,SET-grandparent#0,SET] + 000008:[m#0,SET-m#0,SET] + +compact a-h L1 +---- +2: + 000009:[grandparent#2,RANGEDEL-m#inf,RANGEDEL] + 000010:[m#2,RANGEDEL-z#inf,RANGEDEL] +3: + 000007:[grandparent#0,SET-grandparent#0,SET] + 000008:[m#0,SET-m#0,SET] + +# Setup such that grandparent overlap limit is exceeded multiple times at the same user key ("b"). +# Ensures the compaction output files are non-overlapping. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.2:v + c.SET.2:v +L2 + a.RANGEDEL.3:c +L3 + b.SET.2:v +L3 + b.SET.1:v +L3 + b.SET.0:v +---- +1: + 000004:[a#2,SET-c#2,SET] +2: + 000005:[a#3,RANGEDEL-c#inf,RANGEDEL] +3: + 000006:[b#2,SET-b#2,SET] + 000007:[b#1,SET-b#1,SET] + 000008:[b#0,SET-b#0,SET] + +compact a-c L1 +---- +2: + 000009:[a#3,RANGEDEL-b#inf,RANGEDEL] + 000010:[b#3,RANGEDEL-c#2,SET] +3: + 000006:[b#2,SET-b#2,SET] + 000007:[b#1,SET-b#1,SET] + 000008:[b#0,SET-b#0,SET] + +# Regression test for a bug where compaction would stop process range +# tombstones for an input level upon finding an sstable in the input +# level with no range tombstones. In the scenario below, sstable 6 +# does not contain any range tombstones while sstable 7 does. Both are +# compacted together with sstable 5. + +reset +---- + +batch +set a 1 +set b 1 +set c 1 +set d 1 +set z 1 +---- + +compact a-z +---- +6: + 000005:[a#10,SET-z#14,SET] + +build ext1 +set a 2 +---- + +build ext2 +set b 2 +del-range c z +---- + +ingest ext1 ext2 +---- +0.0: + 000006:[a#15,SET-a#15,SET] + 000007:[b#16,SET-z#inf,RANGEDEL] +6: + 000005:[a#10,SET-z#14,SET] + +iter +first +next +next +next +---- +a: (2, .) +b: (2, .) +z: (1, .) +. + +compact a-z +---- +6: + 000008:[a#0,SET-z#0,SET] + +iter +first +next +next +next +---- +a: (2, .) +b: (2, .) +z: (1, .) +. + +# Regresion test for a bug in sstable smallest boundary generation +# where the smallest key for an sstable was set to a key "larger" than +# the start key of the first range tombstone. This in turn fouled up +# the processing logic of range tombstones used by mergingIter which +# allowed stepping out of an sstable even though it contained a range +# tombstone that covered keys in lower levels. + +define target-file-sizes=(1, 1, 1, 1) +L0 + c.SET.4:4 +L1 + a.SET.3:3 +L2 + a.RANGEDEL.2:e +L3 + b.SET.1:1 +---- +0.0: + 000004:[c#4,SET-c#4,SET] +1: + 000005:[a#3,SET-a#3,SET] +2: + 000006:[a#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000007:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +0.0: + 000004:[c#4,SET-c#4,SET] +2: + 000008:[a#3,SET-b#inf,RANGEDEL] + 000009:[b#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000007:[b#1,SET-b#1,SET] + +# We should only see a:3 and c:4 at this point. + +iter +first +next +next +---- +a: (3, .) +c: (4, .) +. + +# The bug allowed seeing b:1 during reverse iteration. + +iter +last +prev +prev +---- +c: (4, .) +a: (3, .) +. + +# This is a similar scenario to the one above. In older versions of Pebble this +# case necessitated adjusting the seqnum of the range tombstone to +# prev.LargestKey.SeqNum-1. We no longer allow user keys to be split across +# sstables, and the seqnum adjustment is no longer necessary. +# +# Note the target-file-size of 26 is specially tailored to get the +# desired compaction output. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.2:2 + b.RANGEDEL.3:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +last +prev +---- +a: (4, .) +. +a: (4, .) +. + +# Similar to the preceding scenario, except the range tombstone has +# the same seqnum as the largest key in the preceding file. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.3:3 + b.RANGEDEL.3:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +next +last +prev +prev +---- +a: (4, .) +b: (3, .) +. +b: (3, .) +a: (4, .) +. + +# Similar to the preceding scenario, except the range tombstone has +# a smaller seqnum than the largest key in the preceding file. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.4:4 + b.RANGEDEL.2:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#4,SET-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#4,SET-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +next +last +prev +prev +---- +a: (4, .) +b: (4, .) +. +b: (4, .) +a: (4, .) +. + +# Test a scenario where the last point key in an sstable has a seqnum +# of 0. + +define target-file-sizes=(1, 1, 26) snapshots=(2) +L1 + a.SET.3:3 + b.RANGEDEL.3:e + b.SET.0:0 +L3 + a.RANGEDEL.2:b +L3 + c.SET.0:0 + d.SET.0:0 +---- +1: + 000004:[a#3,SET-e#inf,RANGEDEL] +3: + 000005:[a#2,RANGEDEL-b#inf,RANGEDEL] + 000006:[c#0,SET-d#0,SET] + +iter +last +prev +---- +a: (3, .) +. + +compact a-e L1 +---- +2: + 000007:[a#3,SET-c#inf,RANGEDEL] + 000008:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000005:[a#2,RANGEDEL-b#inf,RANGEDEL] + 000006:[c#0,SET-d#0,SET] + +iter +last +prev +---- +a: (3, .) +. + +# Test a scenario where the last point key in an sstable before the +# grandparent limit is reached has a seqnum of 0. We want to cut the +# sstable after the next point key is added, rather than continuing to +# add keys indefinitely (or till the size limit is reached). + +define target-file-sizes=(100, 1, 52) snapshots=(2) +L1 + a.SET.3:3 + b.RANGEDEL.3:e + b.SET.0:0 + c.SET.3:1 + d.SET.1:1 +L3 + c.RANGEDEL.2:d +---- +1: + 000004:[a#3,SET-e#inf,RANGEDEL] +3: + 000005:[c#2,RANGEDEL-d#inf,RANGEDEL] + +compact a-f L1 +---- +2: + 000006:[a#3,SET-c#inf,RANGEDEL] + 000007:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000005:[c#2,RANGEDEL-d#inf,RANGEDEL] + +# Test a scenario where we the last point key in an sstable has a +# seqnum of 0, but there is another range tombstone later in the +# compaction. This scenario was previously triggering an assertion due +# to the rangedel.Fragmenter being finished prematurely. + +define target-file-sizes=(1, 1, 1) +L1 + a.SET.0:0 + c.RANGEDEL.1:d +L3 + b.SET.0:0 +---- +1: + 000004:[a#0,SET-d#inf,RANGEDEL] +3: + 000005:[b#0,SET-b#0,SET] + +compact a-e L1 +---- +2: + 000006:[a#0,SET-a#0,SET] +3: + 000005:[b#0,SET-b#0,SET] + +define target-file-sizes=(1, 1, 1, 1) +L0 + b.SET.1:v +L0 + a.SET.2:v +---- +0.0: + 000005:[a#2,SET-a#2,SET] + 000004:[b#1,SET-b#1,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=b +---- + +async-compact a-b L0 +---- +manual compaction blocked until ongoing finished +1: + 000006:[a#0,SET-a#0,SET] + 000007:[b#0,SET-b#0,SET] + +compact a-b L1 +---- +2: + 000008:[a#0,SET-a#0,SET] + 000009:[b#0,SET-b#0,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=b +---- + +async-compact a-b L2 +---- +manual compaction blocked until ongoing finished +3: + 000010:[a#0,SET-a#0,SET] + 000011:[b#0,SET-b#0,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=b +---- + +set-concurrent-compactions num=2 +---- + +async-compact a-b L3 +---- +manual compaction did not block for ongoing +4: + 000012:[a#0,SET-a#0,SET] + 000013:[b#0,SET-b#0,SET] + +remove-ongoing-compaction +---- + +add-ongoing-compaction startLevel=4 outputLevel=5 start=a end=b +---- + +async-compact a-b L4 +---- +manual compaction blocked until ongoing finished +5: + 000014:[a#0,SET-a#0,SET] + 000015:[b#0,SET-b#0,SET] + +# Test of a scenario where consecutive elided range tombstones and grandparent +# boundaries could result in an invariant violation in the rangedel fragmenter. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.RANGEDEL.4:b + c.RANGEDEL.4:d + e.RANGEDEL.4:f +L1 + g.RANGEDEL.6:h + i.RANGEDEL.4:j +L1 + k.RANGEDEL.5:q + m.RANGEDEL.4:q +L2 + a.SET.2:foo +L3 + a.SET.1:foo + c.SET.1:foo +L3 + ff.SET.1:v +L3 + k.SET.1:foo +---- +1: + 000004:[a#4,RANGEDEL-f#inf,RANGEDEL] + 000005:[g#6,RANGEDEL-j#inf,RANGEDEL] + 000006:[k#5,RANGEDEL-q#inf,RANGEDEL] +2: + 000007:[a#2,SET-a#2,SET] +3: + 000008:[a#1,SET-c#1,SET] + 000009:[ff#1,SET-ff#1,SET] + 000010:[k#1,SET-k#1,SET] + +compact a-q L1 +---- +2: + 000011:[a#4,RANGEDEL-d#inf,RANGEDEL] + 000012:[k#5,RANGEDEL-m#inf,RANGEDEL] +3: + 000008:[a#1,SET-c#1,SET] + 000009:[ff#1,SET-ff#1,SET] + 000010:[k#1,SET-k#1,SET] + +# Test a case where a new output file is started, there are no previous output +# files, there are no additional keys (key = nil) and the rangedel fragmenter +# is non-empty. +define target-file-sizes=(1, 1, 1) +L1 + a.RANGEDEL.10:b + d.RANGEDEL.9:e + q.RANGEDEL.8:r +L2 + g.RANGEDEL.7:h +L3 + q.SET.6:6 +---- +1: + 000004:[a#10,RANGEDEL-r#inf,RANGEDEL] +2: + 000005:[g#7,RANGEDEL-h#inf,RANGEDEL] +3: + 000006:[q#6,SET-q#6,SET] + +compact a-r L1 +---- +2: + 000007:[q#8,RANGEDEL-r#inf,RANGEDEL] +3: + 000006:[q#6,SET-q#6,SET] + +define target-file-sizes=(100, 100, 100) +L1 + a.RANGEDEL.10:b + b.SET.0:foo + d.RANGEDEL.0:e + j.SET.10:foo +L2 + f.RANGEDEL.7:g +L3 + c.SET.6:6 +L3 + c.SET.5:5 +L3 + c.SET.4:4 +L4 + a.SET.0:0 + f.SET.0:0 +---- +1: + 000004:[a#10,RANGEDEL-j#10,SET] +2: + 000005:[f#7,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[c#6,SET-c#6,SET] + 000007:[c#5,SET-c#5,SET] + 000008:[c#4,SET-c#4,SET] +4: + 000009:[a#0,SET-f#0,SET] + +compact a-r L1 +---- +2: + 000010:[a#10,RANGEDEL-b#0,SET] + 000011:[d#0,RANGEDEL-j#10,SET] +3: + 000006:[c#6,SET-c#6,SET] + 000007:[c#5,SET-c#5,SET] + 000008:[c#4,SET-c#4,SET] +4: + 000009:[a#0,SET-f#0,SET] + +# Test a snapshot that separates a range deletion from all the data that it +# deletes. Ensure that we respect the target-file-size and split into multiple +# outputs. + +define target-file-sizes=(1, 1, 1) snapshots=(14) +L1 + a.RANGEDEL.15:z + b.SET.11:foo + c.SET.11:foo +L2 + c.SET.0:foo + d.SET.0:foo +---- +1: + 000004:[a#15,RANGEDEL-z#inf,RANGEDEL] +2: + 000005:[c#0,SET-d#0,SET] + +sstable-properties file=000004 +snapshot-pinned-keys +---- +snapshot-pinned-keys: + pebble.num.snapshot-pinned-keys: 2 + pebble.raw.snapshot-pinned-keys.size: 18 + +compact a-z L1 +---- +2: + 000006:[a#15,RANGEDEL-c#inf,RANGEDEL] + 000007:[c#15,RANGEDEL-d#inf,RANGEDEL] + 000008:[d#15,RANGEDEL-z#inf,RANGEDEL] + +sstable-properties file=000006 +snapshot-pinned-keys +---- +snapshot-pinned-keys: + pebble.num.snapshot-pinned-keys: 1 + pebble.raw.snapshot-pinned-keys.size: 9 + +sstable-properties file=000007 +snapshot-pinned-keys +---- +snapshot-pinned-keys: + pebble.num.snapshot-pinned-keys: 1 + pebble.raw.snapshot-pinned-keys.size: 9 + +sstable-properties file=000008 +snapshot-pinned-keys +---- +snapshot-pinned-keys: + pebble.num.snapshot-pinned-keys: 1 + pebble.raw.snapshot-pinned-keys.size: 9 + +# Test an interaction between a range deletion that will be elided with +# output splitting. Ensure that the output is still split (previous versions +# of the code did not, because of intricacies around preventing a zero +# sequence number in an output's largest key). + +define target-file-sizes=(1, 1, 1) +L1 + a.RANGEDEL.10:z + b.SET.11:foo + c.SET.11:foo +L2 + c.SET.0:foo + d.SET.0:foo +---- +1: + 000004:[a#10,RANGEDEL-z#inf,RANGEDEL] +2: + 000005:[c#0,SET-d#0,SET] + +compact a-z L1 +---- +2: + 000006:[b#0,SET-b#0,SET] + 000007:[c#0,SET-c#0,SET] + +define target-file-sizes=(1, 1, 1, 1) +L0 + a.SET.3:v + b.SET.2:v +L2 + a.SET.1:v +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v +---- +0.0: + 000004:[a#3,SET-b#2,SET] +2: + 000005:[a#1,SET-a#1,SET] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-c#0,SET] + +set-concurrent-compactions num=3 +---- + +compact a-c parallel hide-file-num +---- +4: + [a#0,SET-a#0,SET] + [b#0,SET-b#0,SET] + [c#0,SET-c#0,SET] + +define target-file-sizes=(1, 1, 1, 1) +L0 + a.SET.3:v + b.SET.2:v +L0 + a.SET.2:v + c.SET.2:v +L2 + a.SET.1:v + b.SET.1:v +L2 + c.SET.1:v +L2 + d.SET.0:v +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v +---- +0.1: + 000004:[a#3,SET-b#2,SET] +0.0: + 000005:[a#2,SET-c#2,SET] +2: + 000006:[a#1,SET-b#1,SET] + 000007:[c#1,SET-c#1,SET] + 000008:[d#0,SET-d#0,SET] +3: + 000009:[a#0,SET-b#0,SET] + 000010:[c#0,SET-c#0,SET] + +set-concurrent-compactions num=2 +---- + +compact a-c L0 parallel +---- +1: + 000011:[a#3,SET-a#3,SET] + 000012:[b#2,SET-b#2,SET] + 000013:[c#2,SET-c#2,SET] +2: + 000006:[a#1,SET-b#1,SET] + 000007:[c#1,SET-c#1,SET] + 000008:[d#0,SET-d#0,SET] +3: + 000009:[a#0,SET-b#0,SET] + 000010:[c#0,SET-c#0,SET] + +add-ongoing-compaction startLevel=3 outputLevel=4 start=a end=d +---- + +# We allow 2 maximum concurrent compactions. The operation below generates +# 2 concurrent compactions (a-b, c) from L1 to L2. With 1 ongoing compaction with +# output level L4, there is no conflict and thus the concurrent compactions should +# be queued up and executed sequentially. We ensure that the compactions finish and +# that the final result of the compactions is correct. + +async-compact a-d L1 parallel +---- +manual compaction did not block for ongoing +2: + 000014:[a#3,SET-a#3,SET] + 000015:[b#2,SET-b#2,SET] + 000016:[c#2,SET-c#2,SET] + 000008:[d#0,SET-d#0,SET] +3: + 000009:[a#0,SET-b#0,SET] + 000010:[c#0,SET-c#0,SET] + +remove-ongoing-compaction +---- + +set-concurrent-compactions num=3 +---- + +compact a-d parallel hide-file-num +---- +4: + [a#0,SET-a#0,SET] + [b#0,SET-b#0,SET] + [c#0,SET-c#0,SET] + [d#0,SET-d#0,SET] + +# Create a contrived compaction that forces point key and rangedel iterators +# to stay in sync to emit a correct view of visible and deleted keys. Note that +# Pebble no longer produces range tombstones that go outside a file's bounds, +# but past versions of pebble did, and we should still be able to handle those +# well. + +define target-file-sizes=(1, 1, 1, 1, 1, 1) snapshots=(390) +L3 start=tmgc.MERGE.391 end=tmgc.MERGE.391 + tmgc.MERGE.391:foo + tmgc.RANGEDEL.331:udkatvs +L3 start=tmgc.MERGE.384 end=tmgc.MERGE.384 + tmgc.MERGE.384:bar + tmgc.RANGEDEL.383:tvsalezade + tmgc.RANGEDEL.331:tvsalezade +L3 start=tmgc.RANGEDEL.383 end=tvsalezade.RANGEDEL.72057594037927935 + tmgc.RANGEDEL.383:tvsalezade + tmgc.SET.375:baz + tmgc.RANGEDEL.356:tvsalezade +---- +3: + 000004:[tmgc#391,MERGE-tmgc#391,MERGE] + 000005:[tmgc#384,MERGE-tmgc#384,MERGE] + 000006:[tmgc#383,RANGEDEL-tvsalezade#inf,RANGEDEL] + +compact a-z L3 +---- +4: + 000007:[tmgc#391,MERGE-tmgc#384,MERGE] + +# baz should NOT be visible in the value. + +iter +first +next +next +---- +tmgc: (barfoo, .) +. +. + +# Test split user keys containing RANGEDELs. +# Note that this manual compaction is multilevel! + +define +L4 + b.SET.10:b10 +L5 start=b.SET.9 end=b.SET.8 + b.SET.9:b9 + b.SET.8:b8 + b.RANGEDEL.1:z +L5 start=b.SET.7 end=b.SET.6 + b.SET.7:b7 + b.SET.6:b6 + b.RANGEDEL.1:z +L5 start=b.SET.5 end=z.RANGEDEL.72057594037927935 + b.SET.5:b5 + b.SET.4:b4 + b.RANGEDEL.1:z +L6 + b.SET.0:b0 + bat.SET.0:bat + cat.SET.0:cat +---- +4: + 000004:[b#10,SET-b#10,SET] +5: + 000005:[b#9,SET-b#8,SET] + 000006:[b#7,SET-b#6,SET] + 000007:[b#5,SET-z#inf,RANGEDEL] +6: + 000008:[b#0,SET-cat#0,SET] + +compact a-z +---- +6: + 000009:[b#0,SET-b#0,SET] + +iter +first +next +---- +b: (b10, .) +. + +# Ensure an untruncated range tombstone (eg, written by an earlier version of +# Pebble, Cockroach v21.2 or earlier) cannot delete newer keys outside the +# containing file's bounds. +# +# Regression test for cockroachdb/cockroach#89777. + +define target-file-sizes=(1) +L5 start=g.RANGEDEL.1 end=z.RANGEDEL.72057594037927935 + a.RANGEDEL.1:z + m.SET.1:bar +L6 + m.SET.0:bax +L6 start=a.SET.1 end=g.RANGEDEL.72057594037927935 + a.SET.1:foo + a.RANGEDEL.1:z +---- +5: + 000004:[g#1,RANGEDEL-z#inf,RANGEDEL] +6: + 000006:[a#1,SET-g#inf,RANGEDEL] + 000005:[m#0,SET-m#0,SET] + +# Compacting g-z should result in the elision of the range tombstone in the g-z +# span. + +compact g-z +---- +6: + 000006:[a#1,SET-g#inf,RANGEDEL] + 000007:[m#0,SET-m#0,SET] + +# Write a bunch of keys within the keyspace [g,z), and flush them. + +batch +set b b +set h h +set i i +set j j +set k k +set m m +set q q +set y y +---- + +flush +---- +0.0: + 000009:[b#10,SET-b#10,SET] + 000010:[h#11,SET-h#11,SET] + 000011:[i#12,SET-i#12,SET] + 000012:[j#13,SET-j#13,SET] + 000013:[k#14,SET-k#14,SET] + 000014:[m#15,SET-m#15,SET] + 000015:[q#16,SET-q#16,SET] + 000016:[y#17,SET-y#17,SET] +6: + 000006:[a#1,SET-g#inf,RANGEDEL] + 000007:[m#0,SET-m#0,SET] + +# Compact g-z to zero the sequence numbers of the newer keys. + +compact g-z +---- +0.0: + 000009:[b#10,SET-b#10,SET] +6: + 000006:[a#1,SET-g#inf,RANGEDEL] + 000048:[h#0,SET-k#0,SET] + 000049:[m#0,SET-y#0,SET] + +batch +set t t +---- + +flush +---- +0.0: + 000009:[b#10,SET-b#10,SET] + 000051:[t#18,SET-t#18,SET] +6: + 000006:[a#1,SET-g#inf,RANGEDEL] + 000048:[h#0,SET-k#0,SET] + 000049:[m#0,SET-y#0,SET] + +# Compact everything. The batch-committed keys with zeroed sequence numbers (eg, +# h, i, j, k, m, q, y) should all still exist because the a-z tombstone in +# 000006 should be limited in how far it expands. + +compact a-z +---- +6: + 000062:[a#0,SET-i#0,SET] + 000063:[j#0,SET-q#0,SET] + 000064:[t#0,SET-y#0,SET] + +iter +first +next +next +next +next +next +next +next +next +next +---- +a: (foo, .) +b: (b, .) +h: (h, .) +i: (i, .) +j: (j, .) +k: (k, .) +m: (m, .) +q: (q, .) +t: (t, .) +y: (y, .) + +# Ensure an untruncated range tombstone (eg, written by an earlier version of +# Pebble, Cockroach v21.2 or earlier) cannot expand beyond its previous +# truncated bounds. +# +# Regression test for cockroachdb/cockroach#89777. + +define snapshots=(2) +L5 start=g.RANGEDEL.3 end=n.RANGEDEL.72057594037927935 + a.RANGEDEL.3:z + m.SET.1:bar +L5 + s.SET.0:bax +L6 + q.SET.0:foo +---- +5: + 000004:[g#3,RANGEDEL-n#inf,RANGEDEL] + 000005:[s#0,SET-s#0,SET] +6: + 000006:[q#0,SET-q#0,SET] + +# Compacting all the files in a single expansion should not expand the +# untruncated range tombstone to the larger untruncated bounds. The rangedel +# should remain bounded as [a,n). + +compact a-z +---- +6: + 000007:[g#3,RANGEDEL-s#0,SET] diff --git a/pebble/testdata/manual_compaction_file_boundaries b/pebble/testdata/manual_compaction_file_boundaries new file mode 100644 index 0000000..1ab48ee --- /dev/null +++ b/pebble/testdata/manual_compaction_file_boundaries @@ -0,0 +1,518 @@ +# Test the file-size grandparent boundary alignment heuristic. This test sets up +# L3 with a file at each of 'a', 'b', ..., 'z'. It also creates a single file in +# L2 spanning a-z. Then, it commits, flushes and compacts into L1 keys 'a@1', +# 'aa@1', 'ab@1', ..., 'zz@1'. Finally, it tests compacting L1 into L2. +# +# With L3 as the grandparent level, the alignment heuristic should attempt to +# align the output files with grandparent's boundaries. Each output file should +# have a key range formed by the prefix of a single letter. + +define target-file-sizes=(5000, 5000, 5000, 5000) +L2 + a.SET.101: + z.SET.102: +L3 + a.SET.001: +L3 + b.SET.002: +L3 + c.SET.003: +L3 + d.SET.004: +L3 + e.SET.005: +L3 + f.SET.006: +L3 + g.SET.007: +L3 + h.SET.008: +L3 + i.SET.009: +L3 + j.SET.010: +L3 + k.SET.011: +L3 + l.SET.012: +L3 + m.SET.013: +L3 + n.SET.014: +L3 + o.SET.015: +L3 + p.SET.016: +L3 + q.SET.017: +L3 + r.SET.018: +L3 + s.SET.019: +L3 + t.SET.020: +L3 + u.SET.021: +L3 + v.SET.022: +L3 + w.SET.023: +L3 + x.SET.024: +L3 + y.SET.025: +L3 + z.SET.026: +---- +2: + 000004:[a#101,SET-z#102,SET] +3: + 000005:[a#1,SET-a#1,SET] + 000006:[b#2,SET-b#2,SET] + 000007:[c#3,SET-c#3,SET] + 000008:[d#4,SET-d#4,SET] + 000009:[e#5,SET-e#5,SET] + 000010:[f#6,SET-f#6,SET] + 000011:[g#7,SET-g#7,SET] + 000012:[h#8,SET-h#8,SET] + 000013:[i#9,SET-i#9,SET] + 000014:[j#10,SET-j#10,SET] + 000015:[k#11,SET-k#11,SET] + 000016:[l#12,SET-l#12,SET] + 000017:[m#13,SET-m#13,SET] + 000018:[n#14,SET-n#14,SET] + 000019:[o#15,SET-o#15,SET] + 000020:[p#16,SET-p#16,SET] + 000021:[q#17,SET-q#17,SET] + 000022:[r#18,SET-r#18,SET] + 000023:[s#19,SET-s#19,SET] + 000024:[t#20,SET-t#20,SET] + 000025:[u#21,SET-u#21,SET] + 000026:[v#22,SET-v#22,SET] + 000027:[w#23,SET-w#23,SET] + 000028:[x#24,SET-x#24,SET] + 000029:[y#25,SET-y#25,SET] + 000030:[z#26,SET-z#26,SET] + +populate keylen=2 vallen=200 timestamps=(1) +---- +wrote 702 keys + +flush +---- +0.0: + 000033:[a@1#103,SET-aw@1#126,SET] + 000034:[ax@1#127,SET-bt@1#150,SET] + 000035:[bu@1#151,SET-cq@1#174,SET] + 000036:[cr@1#175,SET-dn@1#198,SET] + 000037:[do@1#199,SET-ek@1#222,SET] + 000038:[el@1#223,SET-fh@1#246,SET] + 000039:[fi@1#247,SET-ge@1#270,SET] + 000040:[gf@1#271,SET-hb@1#294,SET] + 000041:[hc@1#295,SET-hz@1#318,SET] + 000042:[i@1#319,SET-iw@1#342,SET] + 000043:[ix@1#343,SET-jt@1#366,SET] + 000044:[ju@1#367,SET-kq@1#390,SET] + 000045:[kr@1#391,SET-ln@1#414,SET] + 000046:[lo@1#415,SET-mk@1#438,SET] + 000047:[ml@1#439,SET-nh@1#462,SET] + 000048:[ni@1#463,SET-oe@1#486,SET] + 000049:[of@1#487,SET-pb@1#510,SET] + 000050:[pc@1#511,SET-pz@1#534,SET] + 000051:[q@1#535,SET-qw@1#558,SET] + 000052:[qx@1#559,SET-rt@1#582,SET] + 000053:[ru@1#583,SET-sq@1#606,SET] + 000054:[sr@1#607,SET-tn@1#630,SET] + 000055:[to@1#631,SET-uk@1#654,SET] + 000056:[ul@1#655,SET-vh@1#678,SET] + 000057:[vi@1#679,SET-we@1#702,SET] + 000058:[wf@1#703,SET-xb@1#726,SET] + 000059:[xc@1#727,SET-xz@1#750,SET] + 000060:[y@1#751,SET-yw@1#774,SET] + 000061:[yx@1#775,SET-zt@1#798,SET] + 000062:[zu@1#799,SET-zz@1#804,SET] +2: + 000004:[a#101,SET-z#102,SET] +3: + 000005:[a#1,SET-a#1,SET] + 000006:[b#2,SET-b#2,SET] + 000007:[c#3,SET-c#3,SET] + 000008:[d#4,SET-d#4,SET] + 000009:[e#5,SET-e#5,SET] + 000010:[f#6,SET-f#6,SET] + 000011:[g#7,SET-g#7,SET] + 000012:[h#8,SET-h#8,SET] + 000013:[i#9,SET-i#9,SET] + 000014:[j#10,SET-j#10,SET] + 000015:[k#11,SET-k#11,SET] + 000016:[l#12,SET-l#12,SET] + 000017:[m#13,SET-m#13,SET] + 000018:[n#14,SET-n#14,SET] + 000019:[o#15,SET-o#15,SET] + 000020:[p#16,SET-p#16,SET] + 000021:[q#17,SET-q#17,SET] + 000022:[r#18,SET-r#18,SET] + 000023:[s#19,SET-s#19,SET] + 000024:[t#20,SET-t#20,SET] + 000025:[u#21,SET-u#21,SET] + 000026:[v#22,SET-v#22,SET] + 000027:[w#23,SET-w#23,SET] + 000028:[x#24,SET-x#24,SET] + 000029:[y#25,SET-y#25,SET] + 000030:[z#26,SET-z#26,SET] + +compact a-zz L0 +---- +1: + 000063:[a@1#103,SET-aw@1#126,SET] + 000064:[ax@1#127,SET-bt@1#150,SET] + 000065:[bu@1#151,SET-cq@1#174,SET] + 000066:[cr@1#175,SET-dn@1#198,SET] + 000067:[do@1#199,SET-ek@1#222,SET] + 000068:[el@1#223,SET-fh@1#246,SET] + 000069:[fi@1#247,SET-ge@1#270,SET] + 000070:[gf@1#271,SET-hb@1#294,SET] + 000071:[hc@1#295,SET-hz@1#318,SET] + 000072:[i@1#319,SET-iw@1#342,SET] + 000073:[ix@1#343,SET-jt@1#366,SET] + 000074:[ju@1#367,SET-kq@1#390,SET] + 000075:[kr@1#391,SET-ln@1#414,SET] + 000076:[lo@1#415,SET-mk@1#438,SET] + 000077:[ml@1#439,SET-nh@1#462,SET] + 000078:[ni@1#463,SET-oe@1#486,SET] + 000079:[of@1#487,SET-pb@1#510,SET] + 000080:[pc@1#511,SET-pz@1#534,SET] + 000081:[q@1#535,SET-qw@1#558,SET] + 000082:[qx@1#559,SET-rt@1#582,SET] + 000083:[ru@1#583,SET-sq@1#606,SET] + 000084:[sr@1#607,SET-tn@1#630,SET] + 000085:[to@1#631,SET-uk@1#654,SET] + 000086:[ul@1#655,SET-vh@1#678,SET] + 000087:[vi@1#679,SET-we@1#702,SET] + 000088:[wf@1#703,SET-xb@1#726,SET] + 000089:[xc@1#727,SET-xz@1#750,SET] + 000090:[y@1#751,SET-yw@1#774,SET] + 000091:[yx@1#775,SET-zt@1#798,SET] + 000092:[zu@1#799,SET-zz@1#804,SET] +2: + 000004:[a#101,SET-z#102,SET] +3: + 000005:[a#1,SET-a#1,SET] + 000006:[b#2,SET-b#2,SET] + 000007:[c#3,SET-c#3,SET] + 000008:[d#4,SET-d#4,SET] + 000009:[e#5,SET-e#5,SET] + 000010:[f#6,SET-f#6,SET] + 000011:[g#7,SET-g#7,SET] + 000012:[h#8,SET-h#8,SET] + 000013:[i#9,SET-i#9,SET] + 000014:[j#10,SET-j#10,SET] + 000015:[k#11,SET-k#11,SET] + 000016:[l#12,SET-l#12,SET] + 000017:[m#13,SET-m#13,SET] + 000018:[n#14,SET-n#14,SET] + 000019:[o#15,SET-o#15,SET] + 000020:[p#16,SET-p#16,SET] + 000021:[q#17,SET-q#17,SET] + 000022:[r#18,SET-r#18,SET] + 000023:[s#19,SET-s#19,SET] + 000024:[t#20,SET-t#20,SET] + 000025:[u#21,SET-u#21,SET] + 000026:[v#22,SET-v#22,SET] + 000027:[w#23,SET-w#23,SET] + 000028:[x#24,SET-x#24,SET] + 000029:[y#25,SET-y#25,SET] + 000030:[z#26,SET-z#26,SET] + +# Perform the actual test. Compacting L1 into L2 should use L3's boundaries to +# inform compaction output splitting. +# +compact a-zz L1 +---- +2: + 000093:[a#101,SET-az@1#129,SET] + 000094:[b@1#130,SET-bz@1#156,SET] + 000095:[c@1#157,SET-cz@1#183,SET] + 000096:[d@1#184,SET-dz@1#210,SET] + 000097:[e@1#211,SET-ez@1#237,SET] + 000098:[f@1#238,SET-fz@1#264,SET] + 000099:[g@1#265,SET-gz@1#291,SET] + 000100:[h@1#292,SET-hz@1#318,SET] + 000101:[i@1#319,SET-iz@1#345,SET] + 000102:[j@1#346,SET-jz@1#372,SET] + 000103:[k@1#373,SET-kz@1#399,SET] + 000104:[l@1#400,SET-lz@1#426,SET] + 000105:[m@1#427,SET-mz@1#453,SET] + 000106:[n@1#454,SET-nz@1#480,SET] + 000107:[o@1#481,SET-oz@1#507,SET] + 000108:[p@1#508,SET-pz@1#534,SET] + 000109:[q@1#535,SET-qz@1#561,SET] + 000110:[r@1#562,SET-rz@1#588,SET] + 000111:[s@1#589,SET-sz@1#615,SET] + 000112:[t@1#616,SET-tz@1#642,SET] + 000113:[u@1#643,SET-uz@1#669,SET] + 000114:[v@1#670,SET-vz@1#696,SET] + 000115:[w@1#697,SET-wz@1#723,SET] + 000116:[x@1#724,SET-xz@1#750,SET] + 000117:[y@1#751,SET-yz@1#777,SET] + 000118:[z#102,SET-zr@1#796,SET] + 000119:[zs@1#797,SET-zz@1#804,SET] +3: + 000005:[a#1,SET-a#1,SET] + 000006:[b#2,SET-b#2,SET] + 000007:[c#3,SET-c#3,SET] + 000008:[d#4,SET-d#4,SET] + 000009:[e#5,SET-e#5,SET] + 000010:[f#6,SET-f#6,SET] + 000011:[g#7,SET-g#7,SET] + 000012:[h#8,SET-h#8,SET] + 000013:[i#9,SET-i#9,SET] + 000014:[j#10,SET-j#10,SET] + 000015:[k#11,SET-k#11,SET] + 000016:[l#12,SET-l#12,SET] + 000017:[m#13,SET-m#13,SET] + 000018:[n#14,SET-n#14,SET] + 000019:[o#15,SET-o#15,SET] + 000020:[p#16,SET-p#16,SET] + 000021:[q#17,SET-q#17,SET] + 000022:[r#18,SET-r#18,SET] + 000023:[s#19,SET-s#19,SET] + 000024:[t#20,SET-t#20,SET] + 000025:[u#21,SET-u#21,SET] + 000026:[v#22,SET-v#22,SET] + 000027:[w#23,SET-w#23,SET] + 000028:[x#24,SET-x#24,SET] + 000029:[y#25,SET-y#25,SET] + 000030:[z#26,SET-z#26,SET] + +file-sizes +---- +L2: + 000093:[a#101,1-az@1#129,1]: 7472 bytes (7.3KB) + 000094:[b@1#130,1-bz@1#156,1]: 6465 bytes (6.3KB) + 000095:[c@1#157,1-cz@1#183,1]: 6465 bytes (6.3KB) + 000096:[d@1#184,1-dz@1#210,1]: 6465 bytes (6.3KB) + 000097:[e@1#211,1-ez@1#237,1]: 6465 bytes (6.3KB) + 000098:[f@1#238,1-fz@1#264,1]: 6465 bytes (6.3KB) + 000099:[g@1#265,1-gz@1#291,1]: 6465 bytes (6.3KB) + 000100:[h@1#292,1-hz@1#318,1]: 6465 bytes (6.3KB) + 000101:[i@1#319,1-iz@1#345,1]: 6465 bytes (6.3KB) + 000102:[j@1#346,1-jz@1#372,1]: 6465 bytes (6.3KB) + 000103:[k@1#373,1-kz@1#399,1]: 6465 bytes (6.3KB) + 000104:[l@1#400,1-lz@1#426,1]: 6465 bytes (6.3KB) + 000105:[m@1#427,1-mz@1#453,1]: 6465 bytes (6.3KB) + 000106:[n@1#454,1-nz@1#480,1]: 6465 bytes (6.3KB) + 000107:[o@1#481,1-oz@1#507,1]: 6465 bytes (6.3KB) + 000108:[p@1#508,1-pz@1#534,1]: 6465 bytes (6.3KB) + 000109:[q@1#535,1-qz@1#561,1]: 6464 bytes (6.3KB) + 000110:[r@1#562,1-rz@1#588,1]: 6465 bytes (6.3KB) + 000111:[s@1#589,1-sz@1#615,1]: 6465 bytes (6.3KB) + 000112:[t@1#616,1-tz@1#642,1]: 6465 bytes (6.3KB) + 000113:[u@1#643,1-uz@1#669,1]: 6465 bytes (6.3KB) + 000114:[v@1#670,1-vz@1#696,1]: 6465 bytes (6.3KB) + 000115:[w@1#697,1-wz@1#723,1]: 6465 bytes (6.3KB) + 000116:[x@1#724,1-xz@1#750,1]: 6465 bytes (6.3KB) + 000117:[y@1#751,1-yz@1#777,1]: 6465 bytes (6.3KB) + 000118:[z#102,1-zr@1#796,1]: 5752 bytes (5.6KB) + 000119:[zs@1#797,1-zz@1#804,1]: 2346 bytes (2.3KB) +L3: + 000005:[a#1,1-a#1,1]: 10638 bytes (10KB) + 000006:[b#2,1-b#2,1]: 10638 bytes (10KB) + 000007:[c#3,1-c#3,1]: 10638 bytes (10KB) + 000008:[d#4,1-d#4,1]: 10638 bytes (10KB) + 000009:[e#5,1-e#5,1]: 10638 bytes (10KB) + 000010:[f#6,1-f#6,1]: 10638 bytes (10KB) + 000011:[g#7,1-g#7,1]: 10638 bytes (10KB) + 000012:[h#8,1-h#8,1]: 10638 bytes (10KB) + 000013:[i#9,1-i#9,1]: 10638 bytes (10KB) + 000014:[j#10,1-j#10,1]: 10638 bytes (10KB) + 000015:[k#11,1-k#11,1]: 10638 bytes (10KB) + 000016:[l#12,1-l#12,1]: 10638 bytes (10KB) + 000017:[m#13,1-m#13,1]: 10638 bytes (10KB) + 000018:[n#14,1-n#14,1]: 10638 bytes (10KB) + 000019:[o#15,1-o#15,1]: 10638 bytes (10KB) + 000020:[p#16,1-p#16,1]: 10638 bytes (10KB) + 000021:[q#17,1-q#17,1]: 10638 bytes (10KB) + 000022:[r#18,1-r#18,1]: 10638 bytes (10KB) + 000023:[s#19,1-s#19,1]: 10638 bytes (10KB) + 000024:[t#20,1-t#20,1]: 10638 bytes (10KB) + 000025:[u#21,1-u#21,1]: 10638 bytes (10KB) + 000026:[v#22,1-v#22,1]: 10638 bytes (10KB) + 000027:[w#23,1-w#23,1]: 10638 bytes (10KB) + 000028:[x#24,1-x#24,1]: 10638 bytes (10KB) + 000029:[y#25,1-y#25,1]: 10638 bytes (10KB) + 000030:[z#26,1-z#26,1]: 10638 bytes (10KB) + +# Test a scenario where there exists a grandparent file (in L3), but the L1->L2 +# compaction doesn't reach it until late in the compaction. The output file +# should be split at 2x the target file size (~10K), despite not being aligned +# with a grandparent. +# +# Additionally, when the compaction does reach the grandparent's start bound, +# the compaction should NOT split the output if the current output is less than +# 0.5x the target file size (~2.5K). +# +# Lastly, once past the final grandparent, the compaction should optimize for +# cutting as close to file size as possible, resulting in an output file ~5K. + +define target-file-sizes=(5000, 5000, 5000, 5000) +L1 + a.SET.201: + b.SET.202: + c.SET.203: + d.SET.204: + e.SET.205: + f.SET.206: + g.SET.207: + h.SET.208: + i.SET.209: + j.SET.210: + k.SET.211: + l.SET.212: + m.SET.213: + n.SET.214: + o.SET.215: +L2 + a.SET.101: + z.SET.102: +L3 + m.SET.001: +---- +1: + 000004:[a#201,SET-o#215,SET] +2: + 000005:[a#101,SET-z#102,SET] +3: + 000006:[m#1,SET-m#1,SET] + +compact a-zz L1 +---- +2: + 000007:[a#201,SET-j#210,SET] + 000008:[k#211,SET-o#215,SET] + 000009:[z#102,SET-z#102,SET] +3: + 000006:[m#1,SET-m#1,SET] + +file-sizes +---- +L2: + 000007:[a#201,1-j#210,1]: 10811 bytes (11KB) + 000008:[k#211,1-o#215,1]: 5723 bytes (5.6KB) + 000009:[z#102,1-z#102,1]: 643 bytes (643B) +L3: + 000006:[m#1,1-m#1,1]: 10638 bytes (10KB) + +# Test the file-size splitter's adaptive tolerance for early-splitting at a +# grandparent boundary. The L1->L2 compaction has many opportunities to split at +# a grandparent boundary at file sizes ≥ 2.5K. Because it's seen more than 8 +# grandparent boundaries, waits until file size is ≥ 90% of the target file size +# (eg, ~4.5K). + +define target-file-sizes=(5000, 5000, 5000, 5000) +L1 + a.SET.201: + b.SET.202: + c.SET.203: + d.SET.204: + e.SET.205: + f.SET.206: + g.SET.207: + h.SET.208: + i.SET.209: + j.SET.210: + k.SET.211: + l.SET.212: + m.SET.213: + n.SET.214: + o.SET.215: +L2 + a.SET.101: + z.SET.102: +L3 + a.SET.001: +L3 + ab.SET.002: +L3 + ac.SET.003: +L3 + ad.SET.004: +L3 + ad.SET.005: +L3 + ad.SET.006: +L3 + ad.SET.007: +L3 + ad.SET.008: +L3 + c.SET.009: +L3 + d.SET.010: +L3 + e.SET.011: +L3 + f.SET.012: +L3 + m.SET.013: +---- +1: + 000004:[a#201,SET-o#215,SET] +2: + 000005:[a#101,SET-z#102,SET] +3: + 000006:[a#1,SET-a#1,SET] + 000007:[ab#2,SET-ab#2,SET] + 000008:[ac#3,SET-ac#3,SET] + 000013:[ad#8,SET-ad#8,SET] + 000012:[ad#7,SET-ad#7,SET] + 000011:[ad#6,SET-ad#6,SET] + 000010:[ad#5,SET-ad#5,SET] + 000009:[ad#4,SET-ad#4,SET] + 000014:[c#9,SET-c#9,SET] + 000015:[d#10,SET-d#10,SET] + 000016:[e#11,SET-e#11,SET] + 000017:[f#12,SET-f#12,SET] + 000018:[m#13,SET-m#13,SET] + +compact a-zz L1 +---- +2: + 000019:[a#201,SET-e#205,SET] + 000020:[f#206,SET-l#212,SET] + 000021:[m#213,SET-z#102,SET] +3: + 000006:[a#1,SET-a#1,SET] + 000007:[ab#2,SET-ab#2,SET] + 000008:[ac#3,SET-ac#3,SET] + 000013:[ad#8,SET-ad#8,SET] + 000012:[ad#7,SET-ad#7,SET] + 000011:[ad#6,SET-ad#6,SET] + 000010:[ad#5,SET-ad#5,SET] + 000009:[ad#4,SET-ad#4,SET] + 000014:[c#9,SET-c#9,SET] + 000015:[d#10,SET-d#10,SET] + 000016:[e#11,SET-e#11,SET] + 000017:[f#12,SET-f#12,SET] + 000018:[m#13,SET-m#13,SET] + +file-sizes +---- +L2: + 000019:[a#201,1-e#205,1]: 5723 bytes (5.6KB) + 000020:[f#206,1-l#212,1]: 7749 bytes (7.6KB) + 000021:[m#213,1-z#102,1]: 3686 bytes (3.6KB) +L3: + 000006:[a#1,1-a#1,1]: 1638 bytes (1.6KB) + 000007:[ab#2,1-ab#2,1]: 1639 bytes (1.6KB) + 000008:[ac#3,1-ac#3,1]: 1639 bytes (1.6KB) + 000013:[ad#8,1-ad#8,1]: 1639 bytes (1.6KB) + 000012:[ad#7,1-ad#7,1]: 1639 bytes (1.6KB) + 000011:[ad#6,1-ad#6,1]: 1639 bytes (1.6KB) + 000010:[ad#5,1-ad#5,1]: 1639 bytes (1.6KB) + 000009:[ad#4,1-ad#4,1]: 1639 bytes (1.6KB) + 000014:[c#9,1-c#9,1]: 1638 bytes (1.6KB) + 000015:[d#10,1-d#10,1]: 1638 bytes (1.6KB) + 000016:[e#11,1-e#11,1]: 1638 bytes (1.6KB) + 000017:[f#12,1-f#12,1]: 1638 bytes (1.6KB) + 000018:[m#13,1-m#13,1]: 1638 bytes (1.6KB) diff --git a/pebble/testdata/manual_compaction_file_boundaries_delsized b/pebble/testdata/manual_compaction_file_boundaries_delsized new file mode 100644 index 0000000..61a2b31 --- /dev/null +++ b/pebble/testdata/manual_compaction_file_boundaries_delsized @@ -0,0 +1,520 @@ +# Test the file-size grandparent boundary alignment heuristic. This test sets up +# L3 with a file at each of 'a', 'b', ..., 'z'. It also creates a single file in +# L2 spanning a-z. Then, it commits, flushes and compacts into L1 keys 'a@1', +# 'aa@1', 'ab@1', ..., 'zz@1'. Finally, it tests compacting L1 into L2. +# +# With L3 as the grandparent level, the alignment heuristic should attempt to +# align the output files with grandparent's boundaries. Each output file should +# have a key range formed by the prefix of a single letter. + +define target-file-sizes=(5000, 5000, 5000, 5000) +L2 + a.SET.101: + z.SET.102: +L3 + a.SET.001: +L3 + b.SET.002: +L3 + c.SET.003: +L3 + d.SET.004: +L3 + e.SET.005: +L3 + f.SET.006: +L3 + g.SET.007: +L3 + h.SET.008: +L3 + i.SET.009: +L3 + j.SET.010: +L3 + k.SET.011: +L3 + l.SET.012: +L3 + m.SET.013: +L3 + n.SET.014: +L3 + o.SET.015: +L3 + p.SET.016: +L3 + q.SET.017: +L3 + r.SET.018: +L3 + s.SET.019: +L3 + t.SET.020: +L3 + u.SET.021: +L3 + v.SET.022: +L3 + w.SET.023: +L3 + x.SET.024: +L3 + y.SET.025: +L3 + z.SET.026: +---- +2: + 000004:[a#101,SET-z#102,SET] +3: + 000005:[a#1,SET-a#1,SET] + 000006:[b#2,SET-b#2,SET] + 000007:[c#3,SET-c#3,SET] + 000008:[d#4,SET-d#4,SET] + 000009:[e#5,SET-e#5,SET] + 000010:[f#6,SET-f#6,SET] + 000011:[g#7,SET-g#7,SET] + 000012:[h#8,SET-h#8,SET] + 000013:[i#9,SET-i#9,SET] + 000014:[j#10,SET-j#10,SET] + 000015:[k#11,SET-k#11,SET] + 000016:[l#12,SET-l#12,SET] + 000017:[m#13,SET-m#13,SET] + 000018:[n#14,SET-n#14,SET] + 000019:[o#15,SET-o#15,SET] + 000020:[p#16,SET-p#16,SET] + 000021:[q#17,SET-q#17,SET] + 000022:[r#18,SET-r#18,SET] + 000023:[s#19,SET-s#19,SET] + 000024:[t#20,SET-t#20,SET] + 000025:[u#21,SET-u#21,SET] + 000026:[v#22,SET-v#22,SET] + 000027:[w#23,SET-w#23,SET] + 000028:[x#24,SET-x#24,SET] + 000029:[y#25,SET-y#25,SET] + 000030:[z#26,SET-z#26,SET] + +populate keylen=2 vallen=200 timestamps=(1) +---- +wrote 702 keys + +flush +---- +0.0: + 000033:[a@1#103,SET-av@1#125,SET] + 000034:[aw@1#126,SET-br@1#148,SET] + 000035:[bs@1#149,SET-cn@1#171,SET] + 000036:[co@1#172,SET-dj@1#194,SET] + 000037:[dk@1#195,SET-ef@1#217,SET] + 000038:[eg@1#218,SET-fb@1#240,SET] + 000039:[fc@1#241,SET-fy@1#263,SET] + 000040:[fz@1#264,SET-gu@1#286,SET] + 000041:[gv@1#287,SET-hq@1#309,SET] + 000042:[hr@1#310,SET-im@1#332,SET] + 000043:[in@1#333,SET-ji@1#355,SET] + 000044:[jj@1#356,SET-ke@1#378,SET] + 000045:[kf@1#379,SET-la@1#401,SET] + 000046:[lb@1#402,SET-lx@1#424,SET] + 000047:[ly@1#425,SET-mt@1#447,SET] + 000048:[mu@1#448,SET-np@1#470,SET] + 000049:[nq@1#471,SET-ol@1#493,SET] + 000050:[om@1#494,SET-ph@1#516,SET] + 000051:[pi@1#517,SET-qd@1#539,SET] + 000052:[qe@1#540,SET-r@1#562,SET] + 000053:[ra@1#563,SET-rw@1#585,SET] + 000054:[rx@1#586,SET-ss@1#608,SET] + 000055:[st@1#609,SET-to@1#631,SET] + 000056:[tp@1#632,SET-uk@1#654,SET] + 000057:[ul@1#655,SET-vg@1#677,SET] + 000058:[vh@1#678,SET-wc@1#700,SET] + 000059:[wd@1#701,SET-wz@1#723,SET] + 000060:[x@1#724,SET-xv@1#746,SET] + 000061:[xw@1#747,SET-yr@1#769,SET] + 000062:[ys@1#770,SET-zn@1#792,SET] + 000063:[zo@1#793,SET-zz@1#804,SET] +2: + 000004:[a#101,SET-z#102,SET] +3: + 000005:[a#1,SET-a#1,SET] + 000006:[b#2,SET-b#2,SET] + 000007:[c#3,SET-c#3,SET] + 000008:[d#4,SET-d#4,SET] + 000009:[e#5,SET-e#5,SET] + 000010:[f#6,SET-f#6,SET] + 000011:[g#7,SET-g#7,SET] + 000012:[h#8,SET-h#8,SET] + 000013:[i#9,SET-i#9,SET] + 000014:[j#10,SET-j#10,SET] + 000015:[k#11,SET-k#11,SET] + 000016:[l#12,SET-l#12,SET] + 000017:[m#13,SET-m#13,SET] + 000018:[n#14,SET-n#14,SET] + 000019:[o#15,SET-o#15,SET] + 000020:[p#16,SET-p#16,SET] + 000021:[q#17,SET-q#17,SET] + 000022:[r#18,SET-r#18,SET] + 000023:[s#19,SET-s#19,SET] + 000024:[t#20,SET-t#20,SET] + 000025:[u#21,SET-u#21,SET] + 000026:[v#22,SET-v#22,SET] + 000027:[w#23,SET-w#23,SET] + 000028:[x#24,SET-x#24,SET] + 000029:[y#25,SET-y#25,SET] + 000030:[z#26,SET-z#26,SET] + +compact a-zz L0 +---- +1: + 000064:[a@1#103,SET-av@1#125,SET] + 000065:[aw@1#126,SET-br@1#148,SET] + 000066:[bs@1#149,SET-cn@1#171,SET] + 000067:[co@1#172,SET-dj@1#194,SET] + 000068:[dk@1#195,SET-ef@1#217,SET] + 000069:[eg@1#218,SET-fb@1#240,SET] + 000070:[fc@1#241,SET-fy@1#263,SET] + 000071:[fz@1#264,SET-gu@1#286,SET] + 000072:[gv@1#287,SET-hq@1#309,SET] + 000073:[hr@1#310,SET-im@1#332,SET] + 000074:[in@1#333,SET-ji@1#355,SET] + 000075:[jj@1#356,SET-ke@1#378,SET] + 000076:[kf@1#379,SET-la@1#401,SET] + 000077:[lb@1#402,SET-lx@1#424,SET] + 000078:[ly@1#425,SET-mt@1#447,SET] + 000079:[mu@1#448,SET-np@1#470,SET] + 000080:[nq@1#471,SET-ol@1#493,SET] + 000081:[om@1#494,SET-ph@1#516,SET] + 000082:[pi@1#517,SET-qd@1#539,SET] + 000083:[qe@1#540,SET-r@1#562,SET] + 000084:[ra@1#563,SET-rw@1#585,SET] + 000085:[rx@1#586,SET-ss@1#608,SET] + 000086:[st@1#609,SET-to@1#631,SET] + 000087:[tp@1#632,SET-uk@1#654,SET] + 000088:[ul@1#655,SET-vg@1#677,SET] + 000089:[vh@1#678,SET-wc@1#700,SET] + 000090:[wd@1#701,SET-wz@1#723,SET] + 000091:[x@1#724,SET-xv@1#746,SET] + 000092:[xw@1#747,SET-yr@1#769,SET] + 000093:[ys@1#770,SET-zn@1#792,SET] + 000094:[zo@1#793,SET-zz@1#804,SET] +2: + 000004:[a#101,SET-z#102,SET] +3: + 000005:[a#1,SET-a#1,SET] + 000006:[b#2,SET-b#2,SET] + 000007:[c#3,SET-c#3,SET] + 000008:[d#4,SET-d#4,SET] + 000009:[e#5,SET-e#5,SET] + 000010:[f#6,SET-f#6,SET] + 000011:[g#7,SET-g#7,SET] + 000012:[h#8,SET-h#8,SET] + 000013:[i#9,SET-i#9,SET] + 000014:[j#10,SET-j#10,SET] + 000015:[k#11,SET-k#11,SET] + 000016:[l#12,SET-l#12,SET] + 000017:[m#13,SET-m#13,SET] + 000018:[n#14,SET-n#14,SET] + 000019:[o#15,SET-o#15,SET] + 000020:[p#16,SET-p#16,SET] + 000021:[q#17,SET-q#17,SET] + 000022:[r#18,SET-r#18,SET] + 000023:[s#19,SET-s#19,SET] + 000024:[t#20,SET-t#20,SET] + 000025:[u#21,SET-u#21,SET] + 000026:[v#22,SET-v#22,SET] + 000027:[w#23,SET-w#23,SET] + 000028:[x#24,SET-x#24,SET] + 000029:[y#25,SET-y#25,SET] + 000030:[z#26,SET-z#26,SET] + +# Perform the actual test. Compacting L1 into L2 should use L3's boundaries to +# inform compaction output splitting. +# +compact a-zz L1 +---- +2: + 000095:[a#101,SET-az@1#129,SET] + 000096:[b@1#130,SET-bz@1#156,SET] + 000097:[c@1#157,SET-cz@1#183,SET] + 000098:[d@1#184,SET-dz@1#210,SET] + 000099:[e@1#211,SET-ez@1#237,SET] + 000100:[f@1#238,SET-fz@1#264,SET] + 000101:[g@1#265,SET-gz@1#291,SET] + 000102:[h@1#292,SET-hz@1#318,SET] + 000103:[i@1#319,SET-iz@1#345,SET] + 000104:[j@1#346,SET-jz@1#372,SET] + 000105:[k@1#373,SET-kz@1#399,SET] + 000106:[l@1#400,SET-lz@1#426,SET] + 000107:[m@1#427,SET-mz@1#453,SET] + 000108:[n@1#454,SET-nz@1#480,SET] + 000109:[o@1#481,SET-oz@1#507,SET] + 000110:[p@1#508,SET-pz@1#534,SET] + 000111:[q@1#535,SET-qz@1#561,SET] + 000112:[r@1#562,SET-rz@1#588,SET] + 000113:[s@1#589,SET-sz@1#615,SET] + 000114:[t@1#616,SET-tz@1#642,SET] + 000115:[u@1#643,SET-uz@1#669,SET] + 000116:[v@1#670,SET-vz@1#696,SET] + 000117:[w@1#697,SET-wz@1#723,SET] + 000118:[x@1#724,SET-xz@1#750,SET] + 000119:[y@1#751,SET-yz@1#777,SET] + 000120:[z#102,SET-zr@1#796,SET] + 000121:[zs@1#797,SET-zz@1#804,SET] +3: + 000005:[a#1,SET-a#1,SET] + 000006:[b#2,SET-b#2,SET] + 000007:[c#3,SET-c#3,SET] + 000008:[d#4,SET-d#4,SET] + 000009:[e#5,SET-e#5,SET] + 000010:[f#6,SET-f#6,SET] + 000011:[g#7,SET-g#7,SET] + 000012:[h#8,SET-h#8,SET] + 000013:[i#9,SET-i#9,SET] + 000014:[j#10,SET-j#10,SET] + 000015:[k#11,SET-k#11,SET] + 000016:[l#12,SET-l#12,SET] + 000017:[m#13,SET-m#13,SET] + 000018:[n#14,SET-n#14,SET] + 000019:[o#15,SET-o#15,SET] + 000020:[p#16,SET-p#16,SET] + 000021:[q#17,SET-q#17,SET] + 000022:[r#18,SET-r#18,SET] + 000023:[s#19,SET-s#19,SET] + 000024:[t#20,SET-t#20,SET] + 000025:[u#21,SET-u#21,SET] + 000026:[v#22,SET-v#22,SET] + 000027:[w#23,SET-w#23,SET] + 000028:[x#24,SET-x#24,SET] + 000029:[y#25,SET-y#25,SET] + 000030:[z#26,SET-z#26,SET] + +file-sizes +---- +L2: + 000095:[a#101,1-az@1#129,1]: 7528 bytes (7.4KB) + 000096:[b@1#130,1-bz@1#156,1]: 6520 bytes (6.4KB) + 000097:[c@1#157,1-cz@1#183,1]: 6520 bytes (6.4KB) + 000098:[d@1#184,1-dz@1#210,1]: 6520 bytes (6.4KB) + 000099:[e@1#211,1-ez@1#237,1]: 6520 bytes (6.4KB) + 000100:[f@1#238,1-fz@1#264,1]: 6520 bytes (6.4KB) + 000101:[g@1#265,1-gz@1#291,1]: 6520 bytes (6.4KB) + 000102:[h@1#292,1-hz@1#318,1]: 6520 bytes (6.4KB) + 000103:[i@1#319,1-iz@1#345,1]: 6520 bytes (6.4KB) + 000104:[j@1#346,1-jz@1#372,1]: 6520 bytes (6.4KB) + 000105:[k@1#373,1-kz@1#399,1]: 6520 bytes (6.4KB) + 000106:[l@1#400,1-lz@1#426,1]: 6520 bytes (6.4KB) + 000107:[m@1#427,1-mz@1#453,1]: 6520 bytes (6.4KB) + 000108:[n@1#454,1-nz@1#480,1]: 6520 bytes (6.4KB) + 000109:[o@1#481,1-oz@1#507,1]: 6520 bytes (6.4KB) + 000110:[p@1#508,1-pz@1#534,1]: 6520 bytes (6.4KB) + 000111:[q@1#535,1-qz@1#561,1]: 6519 bytes (6.4KB) + 000112:[r@1#562,1-rz@1#588,1]: 6520 bytes (6.4KB) + 000113:[s@1#589,1-sz@1#615,1]: 6520 bytes (6.4KB) + 000114:[t@1#616,1-tz@1#642,1]: 6520 bytes (6.4KB) + 000115:[u@1#643,1-uz@1#669,1]: 6520 bytes (6.4KB) + 000116:[v@1#670,1-vz@1#696,1]: 6520 bytes (6.4KB) + 000117:[w@1#697,1-wz@1#723,1]: 6520 bytes (6.4KB) + 000118:[x@1#724,1-xz@1#750,1]: 6520 bytes (6.4KB) + 000119:[y@1#751,1-yz@1#777,1]: 6520 bytes (6.4KB) + 000120:[z#102,1-zr@1#796,1]: 5800 bytes (5.7KB) + 000121:[zs@1#797,1-zz@1#804,1]: 2382 bytes (2.3KB) +L3: + 000005:[a#1,1-a#1,1]: 10667 bytes (10KB) + 000006:[b#2,1-b#2,1]: 10667 bytes (10KB) + 000007:[c#3,1-c#3,1]: 10667 bytes (10KB) + 000008:[d#4,1-d#4,1]: 10667 bytes (10KB) + 000009:[e#5,1-e#5,1]: 10667 bytes (10KB) + 000010:[f#6,1-f#6,1]: 10667 bytes (10KB) + 000011:[g#7,1-g#7,1]: 10667 bytes (10KB) + 000012:[h#8,1-h#8,1]: 10667 bytes (10KB) + 000013:[i#9,1-i#9,1]: 10667 bytes (10KB) + 000014:[j#10,1-j#10,1]: 10667 bytes (10KB) + 000015:[k#11,1-k#11,1]: 10667 bytes (10KB) + 000016:[l#12,1-l#12,1]: 10667 bytes (10KB) + 000017:[m#13,1-m#13,1]: 10667 bytes (10KB) + 000018:[n#14,1-n#14,1]: 10667 bytes (10KB) + 000019:[o#15,1-o#15,1]: 10667 bytes (10KB) + 000020:[p#16,1-p#16,1]: 10667 bytes (10KB) + 000021:[q#17,1-q#17,1]: 10667 bytes (10KB) + 000022:[r#18,1-r#18,1]: 10667 bytes (10KB) + 000023:[s#19,1-s#19,1]: 10667 bytes (10KB) + 000024:[t#20,1-t#20,1]: 10667 bytes (10KB) + 000025:[u#21,1-u#21,1]: 10667 bytes (10KB) + 000026:[v#22,1-v#22,1]: 10667 bytes (10KB) + 000027:[w#23,1-w#23,1]: 10667 bytes (10KB) + 000028:[x#24,1-x#24,1]: 10667 bytes (10KB) + 000029:[y#25,1-y#25,1]: 10667 bytes (10KB) + 000030:[z#26,1-z#26,1]: 10667 bytes (10KB) + +# Test a scenario where there exists a grandparent file (in L3), but the L1->L2 +# compaction doesn't reach it until late in the compaction. The output file +# should be split at 2x the target file size (~10K), despite not being aligned +# with a grandparent. +# +# Additionally, when the compaction does reach the grandparent's start bound, +# the compaction should NOT split the output if the current output is less than +# 0.5x the target file size (~2.5K). +# +# Lastly, once past the final grandparent, the compaction should optimize for +# cutting as close to file size as possible, resulting in an output file ~5K. + +define target-file-sizes=(5000, 5000, 5000, 5000) +L1 + a.SET.201: + b.SET.202: + c.SET.203: + d.SET.204: + e.SET.205: + f.SET.206: + g.SET.207: + h.SET.208: + i.SET.209: + j.SET.210: + k.SET.211: + l.SET.212: + m.SET.213: + n.SET.214: + o.SET.215: +L2 + a.SET.101: + z.SET.102: +L3 + m.SET.001: +---- +1: + 000004:[a#201,SET-o#215,SET] +2: + 000005:[a#101,SET-z#102,SET] +3: + 000006:[m#1,SET-m#1,SET] + +compact a-zz L1 +---- +2: + 000007:[a#201,SET-j#210,SET] + 000008:[k#211,SET-o#215,SET] + 000009:[z#102,SET-z#102,SET] +3: + 000006:[m#1,SET-m#1,SET] + +file-sizes +---- +L2: + 000007:[a#201,1-j#210,1]: 10849 bytes (11KB) + 000008:[k#211,1-o#215,1]: 5756 bytes (5.6KB) + 000009:[z#102,1-z#102,1]: 672 bytes (672B) +L3: + 000006:[m#1,1-m#1,1]: 10667 bytes (10KB) + +# Test the file-size splitter's adaptive tolerance for early-splitting at a +# grandparent boundary. The L1->L2 compaction has many opportunities to split at +# a grandparent boundary at file sizes ≥ 2.5K. Because it's seen more than 8 +# grandparent boundaries, waits until file size is ≥ 90% of the target file size +# (eg, ~4.5K). + +define target-file-sizes=(5000, 5000, 5000, 5000) +L1 + a.SET.201: + b.SET.202: + c.SET.203: + d.SET.204: + e.SET.205: + f.SET.206: + g.SET.207: + h.SET.208: + i.SET.209: + j.SET.210: + k.SET.211: + l.SET.212: + m.SET.213: + n.SET.214: + o.SET.215: +L2 + a.SET.101: + z.SET.102: +L3 + a.SET.001: +L3 + ab.SET.002: +L3 + ac.SET.003: +L3 + ad.SET.004: +L3 + ae.SET.005: +L3 + af.SET.006: +L3 + ag.SET.007: +L3 + ah.SET.008: +L3 + c.SET.009: +L3 + d.SET.010: +L3 + e.SET.011: +L3 + f.SET.012: +L3 + m.SET.013: +---- +1: + 000004:[a#201,SET-o#215,SET] +2: + 000005:[a#101,SET-z#102,SET] +3: + 000006:[a#1,SET-a#1,SET] + 000007:[ab#2,SET-ab#2,SET] + 000008:[ac#3,SET-ac#3,SET] + 000009:[ad#4,SET-ad#4,SET] + 000010:[ae#5,SET-ae#5,SET] + 000011:[af#6,SET-af#6,SET] + 000012:[ag#7,SET-ag#7,SET] + 000013:[ah#8,SET-ah#8,SET] + 000014:[c#9,SET-c#9,SET] + 000015:[d#10,SET-d#10,SET] + 000016:[e#11,SET-e#11,SET] + 000017:[f#12,SET-f#12,SET] + 000018:[m#13,SET-m#13,SET] + +compact a-zz L1 +---- +2: + 000019:[a#201,SET-e#205,SET] + 000020:[f#206,SET-l#212,SET] + 000021:[m#213,SET-z#102,SET] +3: + 000006:[a#1,SET-a#1,SET] + 000007:[ab#2,SET-ab#2,SET] + 000008:[ac#3,SET-ac#3,SET] + 000009:[ad#4,SET-ad#4,SET] + 000010:[ae#5,SET-ae#5,SET] + 000011:[af#6,SET-af#6,SET] + 000012:[ag#7,SET-ag#7,SET] + 000013:[ah#8,SET-ah#8,SET] + 000014:[c#9,SET-c#9,SET] + 000015:[d#10,SET-d#10,SET] + 000016:[e#11,SET-e#11,SET] + 000017:[f#12,SET-f#12,SET] + 000018:[m#13,SET-m#13,SET] + +file-sizes +---- +L2: + 000019:[a#201,1-e#205,1]: 5756 bytes (5.6KB) + 000020:[f#206,1-l#212,1]: 7784 bytes (7.6KB) + 000021:[m#213,1-z#102,1]: 3718 bytes (3.6KB) +L3: + 000006:[a#1,1-a#1,1]: 1667 bytes (1.6KB) + 000007:[ab#2,1-ab#2,1]: 1668 bytes (1.6KB) + 000008:[ac#3,1-ac#3,1]: 1668 bytes (1.6KB) + 000009:[ad#4,1-ad#4,1]: 1668 bytes (1.6KB) + 000010:[ae#5,1-ae#5,1]: 1668 bytes (1.6KB) + 000011:[af#6,1-af#6,1]: 1668 bytes (1.6KB) + 000012:[ag#7,1-ag#7,1]: 1668 bytes (1.6KB) + 000013:[ah#8,1-ah#8,1]: 1668 bytes (1.6KB) + 000014:[c#9,1-c#9,1]: 1667 bytes (1.6KB) + 000015:[d#10,1-d#10,1]: 1667 bytes (1.6KB) + 000016:[e#11,1-e#11,1]: 1667 bytes (1.6KB) + 000017:[f#12,1-f#12,1]: 1667 bytes (1.6KB) + 000018:[m#13,1-m#13,1]: 1667 bytes (1.6KB) diff --git a/pebble/testdata/manual_compaction_multilevel b/pebble/testdata/manual_compaction_multilevel new file mode 100644 index 0000000..7dad58e --- /dev/null +++ b/pebble/testdata/manual_compaction_multilevel @@ -0,0 +1,121 @@ +# This set of tests validates that manually executed multi level compactions work +# The multilevel compaction tests mainly live in +# /testdata/compaction_setup_inputs_multilevel_write_amp + +# A vanilla multi level compaction +define level-max-bytes=(L2 : 5) auto-compactions=off +L1 + a.SET.3:v b.SET.2:v +L2 + a.SET.2:v c.SET.4:v +L3 + c.SET.3:v d.SET.2:v +L4 + c.SET.2:v d.SET.1:v +---- +1: + 000004:[a#3,SET-b#2,SET] +2: + 000005:[a#2,SET-c#4,SET] +3: + 000006:[c#3,SET-d#2,SET] +4: + 000007:[c#2,SET-d#1,SET] + +compact a-b L1 +---- +3: + 000008:[a#3,SET-d#2,SET] +4: + 000007:[c#2,SET-d#1,SET] + +# Conduct a multi level compaction with no output level files +define level-max-bytes=(L2 : 5) auto-compactions=off +L1 + a.SET.3:v b.SET.2:v +L2 + a.SET.2:v c.SET.4:v +L4 + c.SET.2:v d.SET.1:v +---- +1: + 000004:[a#3,SET-b#2,SET] +2: + 000005:[a#2,SET-c#4,SET] +4: + 000006:[c#2,SET-d#1,SET] + +compact a-b L1 +---- +3: + 000007:[a#3,SET-c#4,SET] +4: + 000006:[c#2,SET-d#1,SET] + +# No multilevel compaction because a move to L2 results in less writeamp than the ML compaction +# which includes the file in L3. +define level-max-bytes=(L2 : 5) auto-compactions=off +L1 + a.SET.3:v b.SET.2:v +L3 + a.SET.2:v c.SET.4:v +L4 + c.SET.2:v d.SET.1:v +---- +1: + 000004:[a#3,SET-b#2,SET] +3: + 000005:[a#2,SET-c#4,SET] +4: + 000006:[c#2,SET-d#1,SET] + +compact a-b L1 +---- +2: + 000004:[a#3,SET-b#2,SET] +3: + 000005:[a#2,SET-c#4,SET] +4: + 000006:[c#2,SET-d#1,SET] + +# Conduct a multi input compaction without intermediate or output level, basically a move. +define level-max-bytes=(L2 : 5) auto-compactions=off multi-input-level +L1 + a.SET.3:v b.SET.2:v +L4 + c.SET.2:v d.SET.1:v +---- +1: + 000004:[a#3,SET-b#2,SET] +4: + 000005:[c#2,SET-d#1,SET] + +compact a-b L1 +---- +3: + 000004:[a#3,SET-b#2,SET] +4: + 000005:[c#2,SET-d#1,SET] + +# Don't conduct a multi level compaction on L0. +define level-max-bytes=(L1 : 5) auto-compactions=off multi-input-level +L0 + a.SET.1:v b.SET.2:v +L1 + a.SET.3:v c.SET.4:v +L2 + c.SET.2:v d.SET.2:v +---- +0.0: + 000004:[a#1,SET-b#2,SET] +1: + 000005:[a#3,SET-c#4,SET] +2: + 000006:[c#2,SET-d#2,SET] + +compact a-b L0 +---- +1: + 000007:[a#3,SET-c#4,SET] +2: + 000006:[c#2,SET-d#2,SET] diff --git a/pebble/testdata/manual_compaction_range_keys b/pebble/testdata/manual_compaction_range_keys new file mode 100644 index 0000000..bd283e4 --- /dev/null +++ b/pebble/testdata/manual_compaction_range_keys @@ -0,0 +1,48 @@ + +# Test compaction of range keys. + +define target-file-sizes=(1, 1, 1, 1) +L0 + rangekey:a-c:{(#4,RANGEKEYSET,@2,foo)} + a.SET.3:b +L2 + a.SET.2:v +L3 + a.SET.0:v + b.SET.0:v + rangekey:b-c:{(#1,RANGEKEYSET,@2,bar)} +L3 + c.SET.0:v +---- +0.0: + 000004:[a#4,RANGEKEYSET-c#inf,RANGEKEYSET] seqnums:[3-4] points:[a#3,SET-a#3,SET] ranges:[a#4,RANGEKEYSET-c#inf,RANGEKEYSET] +2: + 000005:[a#2,SET-a#2,SET] seqnums:[2-2] points:[a#2,SET-a#2,SET] +3: + 000006:[a#0,SET-c#inf,RANGEKEYSET] seqnums:[0-1] points:[a#0,SET-b#0,SET] ranges:[b#1,RANGEKEYSET-c#inf,RANGEKEYSET] + 000007:[c#0,SET-c#0,SET] seqnums:[0-0] points:[c#0,SET-c#0,SET] + +compact a-d L0 +---- +1: + 000008:[a#4,RANGEKEYSET-c#inf,RANGEKEYSET] seqnums:[3-4] points:[a#3,SET-a#3,SET] ranges:[a#4,RANGEKEYSET-c#inf,RANGEKEYSET] +2: + 000005:[a#2,SET-a#2,SET] seqnums:[2-2] points:[a#2,SET-a#2,SET] +3: + 000006:[a#0,SET-c#inf,RANGEKEYSET] seqnums:[0-1] points:[a#0,SET-b#0,SET] ranges:[b#1,RANGEKEYSET-c#inf,RANGEKEYSET] + 000007:[c#0,SET-c#0,SET] seqnums:[0-0] points:[c#0,SET-c#0,SET] + +compact a-d L1 +---- +2: + 000009:[a#4,RANGEKEYSET-c#inf,RANGEKEYSET] seqnums:[3-4] points:[a#3,SET-a#3,SET] ranges:[a#4,RANGEKEYSET-c#inf,RANGEKEYSET] +3: + 000006:[a#0,SET-c#inf,RANGEKEYSET] seqnums:[0-1] points:[a#0,SET-b#0,SET] ranges:[b#1,RANGEKEYSET-c#inf,RANGEKEYSET] + 000007:[c#0,SET-c#0,SET] seqnums:[0-0] points:[c#0,SET-c#0,SET] + +compact a-d L2 +---- +3: + 000010:[a#4,RANGEKEYSET-b#inf,RANGEKEYSET] seqnums:[0-4] points:[a#0,SET-a#0,SET] ranges:[a#4,RANGEKEYSET-b#inf,RANGEKEYSET] + 000011:[b#4,RANGEKEYSET-c#inf,RANGEKEYSET] seqnums:[0-4] points:[b#0,SET-b#0,SET] ranges:[b#4,RANGEKEYSET-c#inf,RANGEKEYSET] + 000007:[c#0,SET-c#0,SET] seqnums:[0-0] points:[c#0,SET-c#0,SET] diff --git a/pebble/testdata/manual_compaction_set_with_del b/pebble/testdata/manual_compaction_set_with_del new file mode 100644 index 0000000..95dc46c --- /dev/null +++ b/pebble/testdata/manual_compaction_set_with_del @@ -0,0 +1,863 @@ +batch +set a 1 +set b 2 +---- + +compact a-b +---- +6: + 000005:[a#10,SET-b#11,SET] + +batch +set c 3 +set d 4 +---- + +compact c-d +---- +6: + 000005:[a#10,SET-b#11,SET] + 000007:[c#12,SET-d#13,SET] + +batch +set b 5 +set c 6 +---- + +compact a-d +---- +6: + 000010:[a#0,SET-d#0,SET] + +# This also tests flushing a memtable that only contains range +# deletions. + +batch +del-range a e +---- + +compact a-d +---- + +# Test that a multi-output-file compaction generates non-overlapping files. + +define target-file-sizes=(100, 1) +L0 + b.SET.1:v +L0 + a.SET.2:v +---- +0.0: + 000005:[a#2,SET-a#2,SET] + 000004:[b#1,SET-b#1,SET] + +compact a-b +---- +1: + 000006:[a#0,SET-a#0,SET] + 000007:[b#0,SET-b#0,SET] + +# A range tombstone extends past the grandparent file boundary used to limit the +# size of future compactions. Verify the range tombstone is split at that file +# boundary. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.3:v +L2 + a.RANGEDEL.2:e +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +---- +1: + 000004:[a#3,SET-a#3,SET] +2: + 000005:[a#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +wait-pending-table-stats +000005 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 1278 + +compact a-e L1 +---- +2: + 000008:[a#3,SETWITHDEL-c#inf,RANGEDEL] + 000009:[c#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +wait-pending-table-stats +000008 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 639 + +# Same as above, except range tombstone covers multiple grandparent file boundaries. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.3:v +L2 + a.RANGEDEL.2:g +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +L3 + e.SET.0:v + f.SET.1:v +L3 + f.SET.0:v + g.SET.0:v +---- +1: + 000004:[a#3,SET-a#3,SET] +2: + 000005:[a#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + 000009:[f#0,SET-g#0,SET] + +compact a-e L1 +---- +2: + 000010:[a#3,SETWITHDEL-c#inf,RANGEDEL] + 000011:[c#2,RANGEDEL-e#inf,RANGEDEL] + 000012:[e#2,RANGEDEL-f#inf,RANGEDEL] + 000013:[f#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + 000009:[f#0,SET-g#0,SET] + +# A range tombstone covers multiple grandparent file boundaries between point keys, +# rather than after all point keys. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.3:v + h.SET.3:v +L2 + a.RANGEDEL.2:g +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +L3 + e.SET.0:v + f.SET.1:v +---- +1: + 000004:[a#3,SET-h#3,SET] +2: + 000005:[a#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + +compact a-e L1 +---- +2: + 000009:[a#3,SETWITHDEL-c#inf,RANGEDEL] + 000010:[c#2,RANGEDEL-h#3,SET] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + +# A range tombstone is the first and only item output by a compaction, and it +# extends past the grandparent file boundary used to limit the size of future +# compactions. Verify the range tombstone is split at that file boundary. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.RANGEDEL.3:e +L2 + a.SET.2:v +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +---- +1: + 000004:[a#3,RANGEDEL-e#inf,RANGEDEL] +2: + 000005:[a#2,SET-a#2,SET] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +compact a-e L1 +---- +2: + 000008:[a#3,RANGEDEL-c#inf,RANGEDEL] + 000009:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +# An elided range tombstone is the first item encountered by a compaction, +# and the grandparent limit set by it extends to the next item, also a range +# tombstone. The first item should be elided, and the second item should +# reset the grandparent limit. + +define target-file-sizes=(100, 100, 100, 100) +L1 + a.RANGEDEL.4:d +L1 + grandparent.RANGEDEL.2:z + h.SET.3:v +L2 + grandparent.SET.1:v +L3 + grandparent.SET.0:v +L3 + m.SET.0:v +---- +1: + 000004:[a#4,RANGEDEL-d#inf,RANGEDEL] + 000005:[grandparent#2,RANGEDEL-z#inf,RANGEDEL] +2: + 000006:[grandparent#1,SET-grandparent#1,SET] +3: + 000007:[grandparent#0,SET-grandparent#0,SET] + 000008:[m#0,SET-m#0,SET] + +compact a-h L1 +---- +2: + 000009:[grandparent#2,RANGEDEL-m#inf,RANGEDEL] + 000010:[m#2,RANGEDEL-z#inf,RANGEDEL] +3: + 000007:[grandparent#0,SET-grandparent#0,SET] + 000008:[m#0,SET-m#0,SET] + +# Setup such that grandparent overlap limit is exceeded multiple times at the same user key ("b"). +# Ensures the compaction output files are non-overlapping. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.2:v + c.SET.2:v +L2 + a.RANGEDEL.3:c +L3 + b.SET.2:v +L3 + b.SET.1:v +L3 + b.SET.0:v +---- +1: + 000004:[a#2,SET-c#2,SET] +2: + 000005:[a#3,RANGEDEL-c#inf,RANGEDEL] +3: + 000006:[b#2,SET-b#2,SET] + 000007:[b#1,SET-b#1,SET] + 000008:[b#0,SET-b#0,SET] + +compact a-c L1 +---- +2: + 000009:[a#3,RANGEDEL-b#inf,RANGEDEL] + 000010:[b#3,RANGEDEL-c#2,SET] +3: + 000006:[b#2,SET-b#2,SET] + 000007:[b#1,SET-b#1,SET] + 000008:[b#0,SET-b#0,SET] + +# Regression test for a bug where compaction would stop process range +# tombstones for an input level upon finding an sstable in the input +# level with no range tombstones. In the scenario below, sstable 6 +# does not contain any range tombstones while sstable 7 does. Both are +# compacted together with sstable 5. + +reset +---- + +batch +set a 1 +set b 1 +set c 1 +set d 1 +set z 1 +---- + +compact a-z +---- +6: + 000005:[a#10,SET-z#14,SET] + +build ext1 +set a 2 +---- + +build ext2 +set b 2 +del-range c z +---- + +ingest ext1 ext2 +---- +0.0: + 000006:[a#15,SET-a#15,SET] + 000007:[b#16,SET-z#inf,RANGEDEL] +6: + 000005:[a#10,SET-z#14,SET] + +iter +first +next +next +next +---- +a: (2, .) +b: (2, .) +z: (1, .) +. + +compact a-z +---- +6: + 000008:[a#0,SET-z#0,SET] + +iter +first +next +next +next +---- +a: (2, .) +b: (2, .) +z: (1, .) +. + +# Regresion test for a bug in sstable smallest boundary generation +# where the smallest key for an sstable was set to a key "larger" than +# the start key of the first range tombstone. This in turn fouled up +# the processing logic of range tombstones used by mergingIter which +# allowed stepping out of an sstable even though it contained a range +# tombstone that covered keys in lower levels. + +define target-file-sizes=(1, 1, 1, 1) +L0 + c.SET.4:4 +L1 + a.SET.3:3 +L2 + a.RANGEDEL.2:e +L3 + b.SET.1:1 +---- +0.0: + 000004:[c#4,SET-c#4,SET] +1: + 000005:[a#3,SET-a#3,SET] +2: + 000006:[a#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000007:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +0.0: + 000004:[c#4,SET-c#4,SET] +2: + 000008:[a#3,SETWITHDEL-b#inf,RANGEDEL] + 000009:[b#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000007:[b#1,SET-b#1,SET] + +# We should only see a:3 and c:4 at this point. + +iter +first +next +next +---- +a: (3, .) +c: (4, .) +. + +# The bug allowed seeing b:1 during reverse iteration. + +iter +last +prev +prev +---- +c: (4, .) +a: (3, .) +. + +# This is a similar scenario to the one above. In older versions of Pebble this +# case necessitated adjusting the seqnum of the range tombstone to +# prev.LargestKey.SeqNum-1. We no longer allow user keys to be split across +# sstables, and the seqnum adjustment is no longer necessary. +# +# Note the target-file-size of 26 is specially tailored to get the +# desired compaction output. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.2:2 + b.RANGEDEL.3:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +last +prev +---- +a: (4, .) +. +a: (4, .) +. + +# Similar to the preceding scenario, except the range tombstone has +# the same seqnum as the largest key in the preceding file. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.3:3 + b.RANGEDEL.3:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +next +last +prev +prev +---- +a: (4, .) +b: (3, .) +. +b: (3, .) +a: (4, .) +. + +# Similar to the preceding scenario, except the range tombstone has +# a smaller seqnum than the largest key in the preceding file. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.4:4 + b.RANGEDEL.2:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#4,SET-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#4,SET-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +next +last +prev +prev +---- +a: (4, .) +b: (4, .) +. +b: (4, .) +a: (4, .) +. + +# Test a scenario where the last point key in an sstable has a seqnum +# of 0. + +define target-file-sizes=(1, 1, 26) snapshots=(2) +L1 + a.SET.3:3 + b.RANGEDEL.3:e + b.SET.0:0 +L3 + a.RANGEDEL.2:b +L3 + c.SET.0:0 + d.SET.0:0 +---- +1: + 000004:[a#3,SET-e#inf,RANGEDEL] +3: + 000005:[a#2,RANGEDEL-b#inf,RANGEDEL] + 000006:[c#0,SET-d#0,SET] + +iter +last +prev +---- +a: (3, .) +. + +compact a-e L1 +---- +2: + 000007:[a#3,SET-c#inf,RANGEDEL] + 000008:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000005:[a#2,RANGEDEL-b#inf,RANGEDEL] + 000006:[c#0,SET-d#0,SET] + +iter +last +prev +---- +a: (3, .) +. + +# Test a scenario where the last point key in an sstable before the +# grandparent limit is reached has a seqnum of 0. We want to cut the +# sstable after the next point key is added, rather than continuing to +# add keys indefinitely (or till the size limit is reached). + +define target-file-sizes=(100, 1, 52) snapshots=(2) +L1 + a.SET.3:3 + b.RANGEDEL.3:e + b.SET.0:0 + c.SET.3:1 + d.SET.1:1 +L3 + c.RANGEDEL.2:d +---- +1: + 000004:[a#3,SET-e#inf,RANGEDEL] +3: + 000005:[c#2,RANGEDEL-d#inf,RANGEDEL] + +compact a-f L1 +---- +2: + 000006:[a#3,SET-c#inf,RANGEDEL] + 000007:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000005:[c#2,RANGEDEL-d#inf,RANGEDEL] + + +# Test a scenario where we the last point key in an sstable has a +# seqnum of 0, but there is another range tombstone later in the +# compaction. This scenario was previously triggering an assertion due +# to the rangedel.Fragmenter being finished prematurely. + +define target-file-sizes=(1, 1, 1) +L1 + a.SET.0:0 + c.RANGEDEL.1:d +L3 + b.SET.0:0 +---- +1: + 000004:[a#0,SET-d#inf,RANGEDEL] +3: + 000005:[b#0,SET-b#0,SET] + +compact a-e L1 +---- +2: + 000006:[a#0,SET-a#0,SET] +3: + 000005:[b#0,SET-b#0,SET] + +define target-file-sizes=(1, 1, 1, 1) +L0 + b.SET.1:v +L0 + a.SET.2:v +---- +0.0: + 000005:[a#2,SET-a#2,SET] + 000004:[b#1,SET-b#1,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z +---- + +async-compact a-b L0 +---- +manual compaction blocked until ongoing finished +1: + 000006:[a#0,SET-a#0,SET] + 000007:[b#0,SET-b#0,SET] + +compact a-b L1 +---- +2: + 000008:[a#0,SET-a#0,SET] + 000009:[b#0,SET-b#0,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z +---- + +async-compact a-b L2 +---- +manual compaction blocked until ongoing finished +3: + 000010:[a#0,SET-a#0,SET] + 000011:[b#0,SET-b#0,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z +---- + +set-concurrent-compactions num=2 +---- + +async-compact a-b L3 +---- +manual compaction did not block for ongoing +4: + 000012:[a#0,SET-a#0,SET] + 000013:[b#0,SET-b#0,SET] + +remove-ongoing-compaction +---- + +add-ongoing-compaction startLevel=4 outputLevel=5 start=a end=b +---- + +async-compact a-b L4 +---- +manual compaction blocked until ongoing finished +5: + 000014:[a#0,SET-a#0,SET] + 000015:[b#0,SET-b#0,SET] + +# Test of a scenario where consecutive elided range tombstones and grandparent +# boundaries could result in an invariant violation in the rangedel fragmenter. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.RANGEDEL.4:b + c.RANGEDEL.4:d + e.RANGEDEL.4:f +L1 + g.RANGEDEL.6:h + i.RANGEDEL.4:j +L1 + k.RANGEDEL.5:q + m.RANGEDEL.4:q +L2 + a.SET.2:foo +L3 + a.SET.1:foo + c.SET.1:foo +L3 + ff.SET.1:v +L3 + k.SET.1:foo +---- +1: + 000004:[a#4,RANGEDEL-f#inf,RANGEDEL] + 000005:[g#6,RANGEDEL-j#inf,RANGEDEL] + 000006:[k#5,RANGEDEL-q#inf,RANGEDEL] +2: + 000007:[a#2,SET-a#2,SET] +3: + 000008:[a#1,SET-c#1,SET] + 000009:[ff#1,SET-ff#1,SET] + 000010:[k#1,SET-k#1,SET] + +compact a-q L1 +---- +2: + 000011:[a#4,RANGEDEL-d#inf,RANGEDEL] + 000012:[k#5,RANGEDEL-m#inf,RANGEDEL] +3: + 000008:[a#1,SET-c#1,SET] + 000009:[ff#1,SET-ff#1,SET] + 000010:[k#1,SET-k#1,SET] + +# Test a case where a new output file is started, there are no previous output +# files, there are no additional keys (key = nil) and the rangedel fragmenter +# is non-empty. +define target-file-sizes=(1, 1, 1) +L1 + a.RANGEDEL.10:b + d.RANGEDEL.9:e + q.RANGEDEL.8:r +L2 + g.RANGEDEL.7:h +L3 + q.SET.6:6 +---- +1: + 000004:[a#10,RANGEDEL-r#inf,RANGEDEL] +2: + 000005:[g#7,RANGEDEL-h#inf,RANGEDEL] +3: + 000006:[q#6,SET-q#6,SET] + +compact a-r L1 +---- +2: + 000007:[q#8,RANGEDEL-r#inf,RANGEDEL] +3: + 000006:[q#6,SET-q#6,SET] + +define target-file-sizes=(100, 100, 100) +L1 + a.RANGEDEL.10:b + b.SET.0:foo + d.RANGEDEL.0:e + j.SET.10:foo +L2 + f.RANGEDEL.7:g +L3 + c.SET.6:6 +L3 + c.SET.5:5 +L3 + c.SET.4:4 +L4 + a.SET.0:0 + f.SET.0:0 +---- +1: + 000004:[a#10,RANGEDEL-j#10,SET] +2: + 000005:[f#7,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[c#6,SET-c#6,SET] + 000007:[c#5,SET-c#5,SET] + 000008:[c#4,SET-c#4,SET] +4: + 000009:[a#0,SET-f#0,SET] + +compact a-r L1 +---- +2: + 000010:[a#10,RANGEDEL-b#0,SET] + 000011:[d#0,RANGEDEL-j#10,SET] +3: + 000006:[c#6,SET-c#6,SET] + 000007:[c#5,SET-c#5,SET] + 000008:[c#4,SET-c#4,SET] +4: + 000009:[a#0,SET-f#0,SET] + +# Test a snapshot that separates a range deletion from all the data that it +# deletes. Ensure that we respect the target-file-size and split into multiple +# outputs. + +define target-file-sizes=(1, 1, 1) snapshots=(14) +L1 + a.RANGEDEL.15:z + b.SET.11:foo + c.SET.11:foo +L2 + c.SET.0:foo + d.SET.0:foo +---- +1: + 000004:[a#15,RANGEDEL-z#inf,RANGEDEL] +2: + 000005:[c#0,SET-d#0,SET] + +compact a-z L1 +---- +2: + 000006:[a#15,RANGEDEL-c#inf,RANGEDEL] + 000007:[c#15,RANGEDEL-d#inf,RANGEDEL] + 000008:[d#15,RANGEDEL-z#inf,RANGEDEL] + +# Test an interaction between a range deletion that will be elided with +# output splitting. Ensure that the output is still split (previous versions +# of the code did not, because of intricacies around preventing a zero +# sequence number in an output's largest key). + +define target-file-sizes=(1, 1, 1) +L1 + a.RANGEDEL.10:z + b.SET.11:foo + c.SET.11:foo +L2 + c.SET.0:foo + d.SET.0:foo +---- +1: + 000004:[a#10,RANGEDEL-z#inf,RANGEDEL] +2: + 000005:[c#0,SET-d#0,SET] + +compact a-z L1 +---- +2: + 000006:[b#0,SET-b#0,SET] + 000007:[c#0,SET-c#0,SET] diff --git a/pebble/testdata/manual_compaction_set_with_del_sstable_Pebblev4 b/pebble/testdata/manual_compaction_set_with_del_sstable_Pebblev4 new file mode 100644 index 0000000..5807ec5 --- /dev/null +++ b/pebble/testdata/manual_compaction_set_with_del_sstable_Pebblev4 @@ -0,0 +1,811 @@ +batch +set a 1 +set b 2 +---- + +compact a-b +---- +6: + 000005:[a#10,SET-b#11,SET] + +batch +set c 3 +set d 4 +---- + +compact c-d +---- +6: + 000005:[a#10,SET-b#11,SET] + 000007:[c#12,SET-d#13,SET] + +batch +set b 5 +set c 6 +---- + +compact a-d +---- +6: + 000010:[a#0,SET-d#0,SET] + +# This also tests flushing a memtable that only contains range +# deletions. + +batch +del-range a e +---- + +compact a-d +---- + +# Test that a multi-output-file compaction generates non-overlapping files. + +define target-file-sizes=(100, 1) +L0 + b.SET.1:v +L0 + a.SET.2:v +---- +0.0: + 000005:[a#2,SET-a#2,SET] + 000004:[b#1,SET-b#1,SET] + +compact a-b +---- +1: + 000006:[a#0,SET-a#0,SET] + 000007:[b#0,SET-b#0,SET] + +# A range tombstone extends past the grandparent file boundary used to limit the +# size of future compactions. Verify the range tombstone is split at that file +# boundary. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.3:v +L2 + a.RANGEDEL.2:e +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +---- +1: + 000004:[a#3,SET-a#3,SET] +2: + 000005:[a#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +wait-pending-table-stats +000005 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 1334 + +compact a-e L1 +---- +2: + 000008:[a#3,SETWITHDEL-c#inf,RANGEDEL] + 000009:[c#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +wait-pending-table-stats +000008 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 667 + +# Same as above, except range tombstone covers multiple grandparent file boundaries. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.3:v +L2 + a.RANGEDEL.2:g +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +L3 + e.SET.0:v + f.SET.1:v +L3 + g.SET.1:v + g.SET.0:v +---- +1: + 000004:[a#3,SET-a#3,SET] +2: + 000005:[a#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + 000009:[g#1,SET-g#1,SET] + +compact a-e L1 +---- +2: + 000010:[a#3,SETWITHDEL-c#inf,RANGEDEL] + 000011:[c#2,RANGEDEL-e#inf,RANGEDEL] + 000012:[e#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + 000009:[g#1,SET-g#1,SET] + +# A range tombstone covers multiple grandparent file boundaries between point keys, +# rather than after all point keys. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.SET.3:v + h.SET.3:v +L2 + a.RANGEDEL.2:g +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +L3 + e.SET.0:v + f.SET.1:v +---- +1: + 000004:[a#3,SET-h#3,SET] +2: + 000005:[a#2,RANGEDEL-g#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + +compact a-e L1 +---- +2: + 000009:[a#3,SETWITHDEL-c#inf,RANGEDEL] + 000010:[c#2,RANGEDEL-h#3,SET] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + 000008:[e#0,SET-f#1,SET] + +# A range tombstone is the first and only item output by a compaction, and it +# extends past the grandparent file boundary used to limit the size of future +# compactions. Verify the range tombstone is split at that file boundary. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.RANGEDEL.3:e +L2 + a.SET.2:v +L3 + a.SET.0:v + b.SET.0:v +L3 + c.SET.0:v + d.SET.0:v +---- +1: + 000004:[a#3,RANGEDEL-e#inf,RANGEDEL] +2: + 000005:[a#2,SET-a#2,SET] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +compact a-e L1 +---- +2: + 000008:[a#3,RANGEDEL-c#inf,RANGEDEL] + 000009:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[a#0,SET-b#0,SET] + 000007:[c#0,SET-d#0,SET] + +# An elided range tombstone is the first item encountered by a compaction, +# and the grandparent limit set by it extends to the next item, also a range +# tombstone. The first item should be elided, and the second item should +# reset the grandparent limit. + +define target-file-sizes=(100, 100, 100, 100) +L1 + a.RANGEDEL.4:d +L1 + grandparent.RANGEDEL.2:z + h.SET.3:v +L2 + grandparent.SET.1:v +L3 + grandparent.SET.0:v +L3 + m.SET.0:v +---- +1: + 000004:[a#4,RANGEDEL-d#inf,RANGEDEL] + 000005:[grandparent#2,RANGEDEL-z#inf,RANGEDEL] +2: + 000006:[grandparent#1,SET-grandparent#1,SET] +3: + 000007:[grandparent#0,SET-grandparent#0,SET] + 000008:[m#0,SET-m#0,SET] + +compact a-h L1 +---- +2: + 000009:[grandparent#2,RANGEDEL-m#inf,RANGEDEL] + 000010:[m#2,RANGEDEL-z#inf,RANGEDEL] +3: + 000007:[grandparent#0,SET-grandparent#0,SET] + 000008:[m#0,SET-m#0,SET] + +# Regression test for a bug where compaction would stop process range +# tombstones for an input level upon finding an sstable in the input +# level with no range tombstones. In the scenario below, sstable 6 +# does not contain any range tombstones while sstable 7 does. Both are +# compacted together with sstable 5. + +reset +---- + +batch +set a 1 +set b 1 +set c 1 +set d 1 +set z 1 +---- + +compact a-z +---- +6: + 000005:[a#10,SET-z#14,SET] + +build ext1 +set a 2 +---- + +build ext2 +set b 2 +del-range c z +---- + +ingest ext1 ext2 +---- +0.0: + 000006:[a#15,SET-a#15,SET] + 000007:[b#16,SET-z#inf,RANGEDEL] +6: + 000005:[a#10,SET-z#14,SET] + +iter +first +next +next +next +---- +a: (2, .) +b: (2, .) +z: (1, .) +. + +compact a-z +---- +6: + 000008:[a#0,SET-z#0,SET] + +iter +first +next +next +next +---- +a: (2, .) +b: (2, .) +z: (1, .) +. + +# Regresion test for a bug in sstable smallest boundary generation +# where the smallest key for an sstable was set to a key "larger" than +# the start key of the first range tombstone. This in turn fouled up +# the processing logic of range tombstones used by mergingIter which +# allowed stepping out of an sstable even though it contained a range +# tombstone that covered keys in lower levels. + +define target-file-sizes=(1, 1, 1, 1) +L0 + c.SET.4:4 +L1 + a.SET.3:3 +L2 + a.RANGEDEL.2:e +L3 + b.SET.1:1 +---- +0.0: + 000004:[c#4,SET-c#4,SET] +1: + 000005:[a#3,SET-a#3,SET] +2: + 000006:[a#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000007:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +0.0: + 000004:[c#4,SET-c#4,SET] +2: + 000008:[a#3,SETWITHDEL-b#inf,RANGEDEL] + 000009:[b#2,RANGEDEL-e#inf,RANGEDEL] +3: + 000007:[b#1,SET-b#1,SET] + +# We should only see a:3 and c:4 at this point. + +iter +first +next +next +---- +a: (3, .) +c: (4, .) +. + +# The bug allowed seeing b:1 during reverse iteration. + +iter +last +prev +prev +---- +c: (4, .) +a: (3, .) +. + +# This is a similar scenario to the one above. In older versions of Pebble this +# case necessitated adjusting the seqnum of the range tombstone to +# prev.LargestKey.SeqNum-1. We no longer allow user keys to be split across +# sstables, and the seqnum adjustment is no longer necessary. +# +# Note the target-file-size of 26 is specially tailored to get the +# desired compaction output. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.2:2 + b.RANGEDEL.3:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +last +prev +---- +a: (4, .) +. +a: (4, .) +. + +# Similar to the preceding scenario, except the range tombstone has +# the same seqnum as the largest key in the preceding file. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.3:3 + b.RANGEDEL.3:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +next +last +prev +prev +---- +a: (4, .) +b: (3, .) +. +b: (3, .) +a: (4, .) +. + +# Similar to the preceding scenario, except the range tombstone has +# a smaller seqnum than the largest key in the preceding file. + +define target-file-sizes=(26, 26, 26, 26) snapshots=(1, 2, 3) +L1 + a.SET.4:4 +L1 + b.SET.4:4 + b.RANGEDEL.2:e +L3 + b.SET.1:1 +---- +1: + 000004:[a#4,SET-a#4,SET] + 000005:[b#4,SET-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +compact a-e L1 +---- +2: + 000007:[a#4,SET-a#4,SET] + 000008:[b#4,SET-e#inf,RANGEDEL] +3: + 000006:[b#1,SET-b#1,SET] + +iter +first +next +next +last +prev +prev +---- +a: (4, .) +b: (4, .) +. +b: (4, .) +a: (4, .) +. + +# Test a scenario where the last point key in an sstable has a seqnum +# of 0. + +define target-file-sizes=(1, 1, 26) snapshots=(2) +L1 + a.SET.3:3 + b.RANGEDEL.3:e + b.SET.0:0 +L3 + a.RANGEDEL.2:b +L3 + c.SET.0:0 + d.SET.0:0 +---- +1: + 000004:[a#3,SET-e#inf,RANGEDEL] +3: + 000005:[a#2,RANGEDEL-b#inf,RANGEDEL] + 000006:[c#0,SET-d#0,SET] + +iter +last +prev +---- +a: (3, .) +. + +compact a-e L1 +---- +2: + 000007:[a#3,SET-c#inf,RANGEDEL] + 000008:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000005:[a#2,RANGEDEL-b#inf,RANGEDEL] + 000006:[c#0,SET-d#0,SET] + +iter +last +prev +---- +a: (3, .) +. + +# Test a scenario where the last point key in an sstable before the +# grandparent limit is reached has a seqnum of 0. We want to cut the +# sstable after the next point key is added, rather than continuing to +# add keys indefinitely (or till the size limit is reached). + +define target-file-sizes=(100, 1, 52) snapshots=(2) +L1 + a.SET.3:3 + b.RANGEDEL.3:e + b.SET.0:0 + c.SET.3:1 + d.SET.1:1 +L3 + c.RANGEDEL.2:d +---- +1: + 000004:[a#3,SET-e#inf,RANGEDEL] +3: + 000005:[c#2,RANGEDEL-d#inf,RANGEDEL] + +compact a-f L1 +---- +2: + 000006:[a#3,SET-c#inf,RANGEDEL] + 000007:[c#3,RANGEDEL-e#inf,RANGEDEL] +3: + 000005:[c#2,RANGEDEL-d#inf,RANGEDEL] + + +# Test a scenario where we the last point key in an sstable has a +# seqnum of 0, but there is another range tombstone later in the +# compaction. This scenario was previously triggering an assertion due +# to the rangedel.Fragmenter being finished prematurely. + +define target-file-sizes=(1, 1, 1) +L1 + a.SET.0:0 + c.RANGEDEL.1:d +L3 + b.SET.0:0 +---- +1: + 000004:[a#0,SET-d#inf,RANGEDEL] +3: + 000005:[b#0,SET-b#0,SET] + +compact a-e L1 +---- +2: + 000006:[a#0,SET-a#0,SET] +3: + 000005:[b#0,SET-b#0,SET] + +define target-file-sizes=(1, 1, 1, 1) +L0 + b.SET.1:v +L0 + a.SET.2:v +---- +0.0: + 000005:[a#2,SET-a#2,SET] + 000004:[b#1,SET-b#1,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z +---- + +async-compact a-b L0 +---- +manual compaction blocked until ongoing finished +1: + 000006:[a#0,SET-a#0,SET] + 000007:[b#0,SET-b#0,SET] + +compact a-b L1 +---- +2: + 000008:[a#0,SET-a#0,SET] + 000009:[b#0,SET-b#0,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z +---- + +async-compact a-b L2 +---- +manual compaction blocked until ongoing finished +3: + 000010:[a#0,SET-a#0,SET] + 000011:[b#0,SET-b#0,SET] + +add-ongoing-compaction startLevel=0 outputLevel=1 start=a end=z +---- + +set-concurrent-compactions num=2 +---- + +async-compact a-b L3 +---- +manual compaction did not block for ongoing +4: + 000012:[a#0,SET-a#0,SET] + 000013:[b#0,SET-b#0,SET] + +remove-ongoing-compaction +---- + +add-ongoing-compaction startLevel=4 outputLevel=5 start=a end=b +---- + +async-compact a-b L4 +---- +manual compaction blocked until ongoing finished +5: + 000014:[a#0,SET-a#0,SET] + 000015:[b#0,SET-b#0,SET] + +# Test of a scenario where consecutive elided range tombstones and grandparent +# boundaries could result in an invariant violation in the rangedel fragmenter. + +define target-file-sizes=(1, 1, 1, 1) +L1 + a.RANGEDEL.4:b + c.RANGEDEL.4:d + e.RANGEDEL.4:f +L1 + g.RANGEDEL.6:h + i.RANGEDEL.4:j +L1 + k.RANGEDEL.5:q + m.RANGEDEL.4:q +L2 + a.SET.2:foo +L3 + a.SET.1:foo + c.SET.1:foo +L3 + ff.SET.1:v +L3 + k.SET.1:foo +---- +1: + 000004:[a#4,RANGEDEL-f#inf,RANGEDEL] + 000005:[g#6,RANGEDEL-j#inf,RANGEDEL] + 000006:[k#5,RANGEDEL-q#inf,RANGEDEL] +2: + 000007:[a#2,SET-a#2,SET] +3: + 000008:[a#1,SET-c#1,SET] + 000009:[ff#1,SET-ff#1,SET] + 000010:[k#1,SET-k#1,SET] + +compact a-q L1 +---- +2: + 000011:[a#4,RANGEDEL-d#inf,RANGEDEL] + 000012:[k#5,RANGEDEL-m#inf,RANGEDEL] +3: + 000008:[a#1,SET-c#1,SET] + 000009:[ff#1,SET-ff#1,SET] + 000010:[k#1,SET-k#1,SET] + +# Test a case where a new output file is started, there are no previous output +# files, there are no additional keys (key = nil) and the rangedel fragmenter +# is non-empty. +define target-file-sizes=(1, 1, 1) +L1 + a.RANGEDEL.10:b + d.RANGEDEL.9:e + q.RANGEDEL.8:r +L2 + g.RANGEDEL.7:h +L3 + q.SET.6:6 +---- +1: + 000004:[a#10,RANGEDEL-r#inf,RANGEDEL] +2: + 000005:[g#7,RANGEDEL-h#inf,RANGEDEL] +3: + 000006:[q#6,SET-q#6,SET] + +compact a-r L1 +---- +2: + 000007:[q#8,RANGEDEL-r#inf,RANGEDEL] +3: + 000006:[q#6,SET-q#6,SET] + +# Test a snapshot that separates a range deletion from all the data that it +# deletes. Ensure that we respect the target-file-size and split into multiple +# outputs. + +define target-file-sizes=(1, 1, 1) snapshots=(14) +L1 + a.RANGEDEL.15:z + b.SET.11:foo + c.SET.11:foo +L2 + c.SET.0:foo + d.SET.0:foo +---- +1: + 000004:[a#15,RANGEDEL-z#inf,RANGEDEL] +2: + 000005:[c#0,SET-d#0,SET] + +compact a-z L1 +---- +2: + 000006:[a#15,RANGEDEL-c#inf,RANGEDEL] + 000007:[c#15,RANGEDEL-d#inf,RANGEDEL] + 000008:[d#15,RANGEDEL-z#inf,RANGEDEL] + +# Test an interaction between a range deletion that will be elided with +# output splitting. Ensure that the output is still split (previous versions +# of the code did not, because of intricacies around preventing a zero +# sequence number in an output's largest key). + +define target-file-sizes=(1, 1, 1) +L1 + a.RANGEDEL.10:z + b.SET.11:foo + c.SET.11:foo +L2 + c.SET.0:foo + d.SET.0:foo +---- +1: + 000004:[a#10,RANGEDEL-z#inf,RANGEDEL] +2: + 000005:[c#0,SET-d#0,SET] + +compact a-z L1 +---- +2: + 000006:[b#0,SET-b#0,SET] + 000007:[c#0,SET-c#0,SET] + +define snapshots=(10) +L1 + a.MERGE.15:a15 +L2 + a.SET.5:a5 +---- +1: + 000004:[a#15,MERGE-a#15,MERGE] +2: + 000005:[a#5,SET-a#5,SET] + +compact a-z +---- +3: + 000006:[a#15,MERGE-a#0,SET] + +# Fix for #2705. When snapshotPinned was used to set force obsolete, the +# merged value would be a15 since the SET was incorrectly ignored. +iter +first +next +---- +a: (a5a15, .) +. diff --git a/pebble/testdata/manual_flush b/pebble/testdata/manual_flush new file mode 100644 index 0000000..1cb0d98 --- /dev/null +++ b/pebble/testdata/manual_flush @@ -0,0 +1,77 @@ +batch +set a 1 +set b 2 +---- + +# The first L0 table can have its seqnums zeroed. +flush +---- +0.0: + 000005:[a#10,SET-b#11,SET] + +reset +---- + +batch +set a 1 +set b 2 +del a +del b +---- + +flush +---- +0.0: + 000005:[a#12,DEL-b#13,DEL] + +batch +set a 3 +---- + +# A second (overlapping) L0 table will have non-zero seqnums. +flush +---- +0.1: + 000007:[a#14,SET-a#14,SET] +0.0: + 000005:[a#12,DEL-b#13,DEL] + +batch +set c 4 +---- + +# A third (non-overlapping) L0 table will have non-zero seqnums. +flush +---- +0.1: + 000007:[a#14,SET-a#14,SET] +0.0: + 000005:[a#12,DEL-b#13,DEL] + 000009:[c#15,SET-c#15,SET] + +reset +---- + +batch +set a 1 +set b 2 +del-range a c +---- + +flush +---- +0.0: + 000005:[a#12,RANGEDEL-c#inf,RANGEDEL] + +reset +---- + +batch +set a 1 +set b 2 +---- + +async-flush +---- +0.0: + 000005:[a#10,SET-b#11,SET] diff --git a/pebble/testdata/marked_for_compaction b/pebble/testdata/marked_for_compaction new file mode 100644 index 0000000..25a6181 --- /dev/null +++ b/pebble/testdata/marked_for_compaction @@ -0,0 +1,28 @@ +define +L0 + c.SET.11:foo +L1 + c.SET.0:foo + d.SET.0:foo +---- +0.0: + 000004:[c#11,SET-c#11,SET] seqnums:[11-11] points:[c#11,SET-c#11,SET] +1: + 000005:[c#0,SET-d#0,SET] seqnums:[0-0] points:[c#0,SET-d#0,SET] + +mark-for-compaction file=000005 +---- +marked L1.000005 + +mark-for-compaction file=000004 +---- +marked L0.000004 + +maybe-compact +---- +[JOB 100] compacted(rewrite) L1 [000005] (670B) Score=0.00 + L1 [] (0B) Score=0.00 -> L1 [000006] (670B), in 1.0s (2.0s total), output rate 670B/s +[JOB 100] compacted(rewrite) L0 [000004] (665B) Score=0.00 + L0 [] (0B) Score=0.00 -> L0 [000007] (665B), in 1.0s (2.0s total), output rate 665B/s +0.0: + 000007:[c#11,SET-c#11,SET] seqnums:[11-11] points:[c#11,SET-c#11,SET] +1: + 000006:[c#0,SET-d#0,SET] seqnums:[0-0] points:[c#0,SET-d#0,SET] diff --git a/pebble/testdata/merging_iter b/pebble/testdata/merging_iter new file mode 100644 index 0000000..e106170 --- /dev/null +++ b/pebble/testdata/merging_iter @@ -0,0 +1,592 @@ +# Format for define command: +# Levels are ordered from higher to lower, and each new level starts with an L +# Each level is defined using an even number of lines where every pair of lines represents +# a file. The files within a level are ordered from smaller to larger keys. +# Each file is defined using: the first line specifies the smallest and largest internal +# keys and the second line the point key-value pairs in the sstable in order. The rangedel +# key-value pairs should also be in increasing order relative to the other rangedel pairs. +# The largest file key can take the form of .RANGEDEL.72057594037927935, which +# represents the range deletion sentinel. + +# The rangedel should not delete any points in any sstable. The two files were involved in a +# compaction and then the second file got moved to a lower level. +define +L +a.SET.30 e.RANGEDEL.72057594037927935 +a.SET.30:30 c.SET.27:27 a.RANGEDEL.8:f +L +e.SET.10 g.SET.20 +e.SET.10:10 g.SET.20:20 e.RANGEDEL.8:f +---- +1: + 000000:[a#30,SET-e#inf,RANGEDEL] +2: + 000001:[e#10,SET-g#20,SET] + +# isNextEntryDeleted() should not allow the rangedel to act on the points in the lower sstable +# that are after it. +iter +first +next +next +next +next +stats +reset-stats +stats +---- +a#30,1:30 +c#27,1:27 +e#10,1:10 +g#20,1:20 +. +{BlockBytes:116 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:5 ValueBytes:8 PointCount:5 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} + +# seekGE() should not allow the rangedel to act on points in the lower sstable that are after it. +iter +seek-ge d +next +next +---- +e#10,1:10 +g#20,1:20 +. + +# isPrevEntryDeleted() should not allow the rangedel to act on the points in the lower sstable +# that are after it. +iter +last +prev +prev +prev +---- +g#20,1:20 +e#10,1:10 +c#27,1:27 +a#30,1:30 + +# seekLT() should not allow the rangedel to act on points in the lower sstable that are after it. +iter +seek-lt h +prev +prev +prev +---- +g#20,1:20 +e#10,1:10 +c#27,1:27 +a#30,1:30 + +# We keep the rangedel alive by having a point in the higher level past the first point in the +# lower level. This rangedel hides that first point in the lower level but we should not seek to +# h and hide the second point. +define +L +a.SET.15 f.SET.16 +a.SET.15:15 c.SET.13:13 f.SET.16:16 a.RANGEDEL.12:h +L +e.SET.10 g.SET.15 +e.SET.10:10 g.SET.15:15 +---- +1: + 000002:[a#15,SET-f#16,SET] +2: + 000003:[e#10,SET-g#15,SET] + +iter +first +next +next +next +---- +a#15,1:15 +c#13,1:13 +f#16,1:16 +g#15,1:15 + +iter +seek-ge d +next +---- +f#16,1:16 +g#15,1:15 + +iter +last +prev +prev +prev +---- +g#15,1:15 +f#16,1:16 +c#13,1:13 +a#15,1:15 + +# The rangedel should not delete any points in any sstable. The two files were involved in an +# compaction and then the first file got moved to a lower level. +define +L +c.SET.30 f.RANGEDEL.0 +c.SET.30:30 d.SET.27:27 a.RANGEDEL.8:f +L +a.SET.10 c.RANGEDEL.72057594037927935 +a.SET.10:10 b.SET.12:12 a.RANGEDEL.8:f +---- +1: + 000004:[c#30,SET-f#0,RANGEDEL] +2: + 000005:[a#10,SET-c#inf,RANGEDEL] + +# isNextEntryDeleted() should not allow the rangedel to act on the points in the lower sstable +# that are before it. +iter +first +next +next +next +---- +a#10,1:10 +b#12,1:12 +c#30,1:30 +d#27,1:27 + +# seekGE() should not allow the rangedel to act on points in the lower sstable that are before it. +iter +seek-ge a +next +next +next +---- +a#10,1:10 +b#12,1:12 +c#30,1:30 +d#27,1:27 + +# isPrevEntryDeleted() should not allow the rangedel to act on the points in the lower sstable +# that are before it. +iter +last +prev +prev +prev +---- +d#27,1:27 +c#30,1:30 +b#12,1:12 +a#10,1:10 + +# seekLT() should not allow the rangedel to act on points in the lower sstable that are before it. +iter +seek-lt e +prev +prev +prev +---- +d#27,1:27 +c#30,1:30 +b#12,1:12 +a#10,1:10 + +# We keep the rangedel alive in prev iteration by having a point in the higher level before +# the last point in the lower level. This rangedel hides that first point in the lower level +# but we should not seek to a and hide the second point. +define +L +c.SET.15 g.SET.16 +c.SET.15:15 f.SET.13:13 g.SET.16:16 a.RANGEDEL.12:h +L +b.SET.14 d.SET.10 +b.SET.14:14 d.SET.10:10 +---- +1: + 000006:[c#15,SET-g#16,SET] +2: + 000007:[b#14,SET-d#10,SET] + +iter +last +prev +prev +prev +---- +g#16,1:16 +f#13,1:13 +c#15,1:15 +b#14,1:14 + +iter +seek-lt f +prev +---- +c#15,1:15 +b#14,1:14 + +# The rangedel should not delete anything. +define +L +a.SET.30 e.RANGEDEL.72057594037927935 +a.SET.30:30 c.SET.27:27 a.RANGEDEL.8:g +L +e.SET.10 g.SET.20 +e.SET.10:10 g.SET.20:20 e.RANGEDEL.8:g +---- +1: + 000008:[a#30,SET-e#inf,RANGEDEL] +2: + 000009:[e#10,SET-g#20,SET] + +# When doing seek-lt f, the rangedel should not apply to e in the lower sstable. This is the +# reason we cannot just use largest user key to constrain the rangedel and we need to +# know whether it is the sentinel key. +iter +seek-lt f +prev +prev +---- +e#10,1:10 +c#27,1:27 +a#30,1:30 + +iter +seek-ge e +next +---- +e#10,1:10 +g#20,1:20 + +iter +first +seek-ge e +next +---- +a#30,1:30 +e#10,1:10 +g#20,1:20 + +iter +first +next +next +next +next +---- +a#30,1:30 +c#27,1:27 +e#10,1:10 +g#20,1:20 +. + +# Verify that switching directions respects lower/upper bound. + +define +L +a.SET.9 d.SET.6 +a.SET.9:9 b.SET.8:8 c.SET.7:7 d.SET.6:6 +---- +1: + 000010:[a#9,SET-d#6,SET] + +# Verify the lower bound is respected in switchToMinHeap() when the +# heap is empty. + +iter +set-bounds lower=c +seek-ge c +prev +prev +next +---- +c#7,1:7 +. +. +c#7,1:7 + +# Verify the upper bound is respected in switchToMaxHeap() when the +# heap is empty. + +iter +set-bounds upper=c +seek-lt c +next +next +prev +---- +b#8,1:8 +. +. +b#8,1:8 + +# Verify the lower bound is respected in switchToMinHeap() when the +# heap is not empty. + +define +L +a.SET.9 d.SET.6 +a.SET.9:9 b.SET.8:8 c.SET.7:7 d.SET.6:6 +L +c.SET.5 f.SET.2 +c.SET.5:5 d.SET.4:4 e.SET.3:3 f.SET.2:2 +---- +1: + 000011:[a#9,SET-d#6,SET] +2: + 000012:[c#5,SET-f#2,SET] + +iter +set-bounds lower=d +seek-ge d +prev +prev +next +next +---- +d#6,1:6 +. +. +d#6,1:6 +d#4,1:4 + +# Check the behavior of reverse prefix iteration. + +iter +seek-prefix-ge d +prev +next +---- +d#6,1:6 +err=pebble: unsupported reverse prefix iteration +err=pebble: unsupported reverse prefix iteration + +# Verify the upper bound is respected in switchToMaxHeap() when the +# heap is not empty. + +define +L +c.SET.9 f.SET.6 +c.SET.9:9 d.SET.8:8 e.SET.7:7 f.SET.6:6 +L +a.SET.5 d.SET.2 +a.SET.5:5 b.SET.4:4 c.SET.3:3 d.SET.2:2 +---- +1: + 000013:[c#9,SET-f#6,SET] +2: + 000014:[a#5,SET-d#2,SET] + +iter +set-bounds upper=d +seek-lt d +next +next +prev +prev +---- +c#3,1:3 +. +. +c#3,1:3 +c#9,1:9 + +# Verify that the tombstone for the current level is updated correctly +# when we advance the iterator on the level and step into a new +# sstable. In the scenario below, the keys "c" and "d" should not show +# up in the iteration output. + +define +L +a.SET.2 a.SET.2 +a.SET.2:2 +c.RANGEDEL.4 e.RANGEDEL.72057594037927935 +c.RANGEDEL.4:e +f.SET.3 f.SET.3 +f.SET.3:3 +L +a.SET.0 f.SET.0 +a.SET.0:1 b.SET.0:1 c.SET.0:1 d.SET.0:1 e.SET.0:1 f.SET.0:1 +---- +1: + 000015:[a#2,SET-a#2,SET] + 000016:[c#4,RANGEDEL-e#inf,RANGEDEL] + 000017:[f#3,SET-f#3,SET] +2: + 000018:[a#0,SET-f#0,SET] + +iter +first +next +next +next +next +next +next +next +---- +a#2,1:2 +a#0,1:1 +b#0,1:1 +e#0,1:1 +f#3,1:3 +f#0,1:1 +. +. + +iter +last +prev +prev +prev +prev +prev +prev +prev +---- +f#0,1:1 +f#3,1:3 +e#0,1:1 +b#0,1:1 +a#0,1:1 +a#2,1:2 +. +. + +# Verify the upper bound is respected when switching directions at a RANGEDEL +# boundary. + +define +L +kq.RANGEDEL.100 p.RANGEDEL.72057594037927935 +kq.RANGEDEL.100:p +L +b.SET.90 o.SET.65 +b.SET.90:90 cat.SET.70:70 g.SET.80:80 o.SET.65:65 +L +a.SET.41 z.RANGEDEL.72057594037927935 +a.SET.41:41 koujdlp.MERGE.37:37 ok.SET.46:46 v.SET.43:43 v.RANGEDEL.19:z +---- +1: + 000019:[kq#100,RANGEDEL-p#inf,RANGEDEL] +2: + 000020:[b#90,SET-o#65,SET] +3: + 000021:[a#41,SET-z#inf,RANGEDEL] + +iter +set-bounds upper=n +seek-ge krgywquurww +prev +---- +. +koujdlp#37,2:37 + +# Verify the lower bound is respected when switching directions at a RANGEDEL +# boundary. + +define +L +a.SET.103 jyk.RANGEDEL.72057594037927935 +a.SET.103:103 imd.SET.793:793 iwoeionch.SET.792:792 c.RANGEDEL.101:jyk +L +b.SET.90 o.SET.65 +b.SET.90:90 cat.SET.70:70 g.SET.80:80 o.SET.65:65 +L +all.SET.0 zk.SET.722 +all.SET.0:0 c.SET.0:0 zk.SET.722:722 +---- +1: + 000022:[a#103,SET-jyk#inf,RANGEDEL] +2: + 000023:[b#90,SET-o#65,SET] +3: + 000024:[all#0,SET-zk#722,SET] + +iter +set-bounds lower=cz upper=jd +seek-lt jd +next +---- +iwoeionch#792,1:792 +. + +# Exercise the early stopping behavior for prefix iteration when encountering +# range deletion tombstones. Keys a, d are not deleted, while the rest are. +define +L +a.SET.10 d.SET.10 +a.SET.10:a10 b.SET.10:b10 c.SET.10:c10 d.SET.10:d10 b.RANGEDEL.12:d +---- +1: + 000025:[a#10,SET-d#10,SET] + +iter +first +next +next +---- +a#10,1:a10 +d#10,1:d10 +. + +# The seek to c finds d since iteration cannot stop at c as it matches the +# prefix, and when it steps to d, it finds d is not deleted. Note that +# mergingIter is an InternalIterator and does not need to guarantee prefix +# match -- that is job of the higher-level Iterator. So "seek-prefix-ge c" is +# allowed to return d. +iter +seek-prefix-ge a false +seek-prefix-ge aa true +seek-prefix-ge b true +seek-prefix-ge c true +seek-prefix-ge d true +---- +a#10,1:a10 +. +. +d#10,1:d10 +d#10,1:d10 + +iter +seek-prefix-ge a false +next +seek-prefix-ge b false +seek-prefix-ge d true +next +---- +a#10,1:a10 +. +. +d#10,1:d10 +. + +# Create a sstable which has a range tombstone that covers 4 points in the +# same sstable. This tests the PointsCoveredByRangeTombstones and PointCount +# stats. +define +L +a.SET.30 g.RANGEDEL.72057594037927935 +a.SET.30:30 a.RANGEDEL.20:g b.SET.19:19 c.SET.18:18 d.SET.17:17 e.SET.16:16 f.SET.21:21 +---- +1: + 000026:[a#30,SET-g#inf,RANGEDEL] + +iter +first +stats +reset-stats +stats +next +stats +next +stats +next +stats +---- +a#30,1:30 +{BlockBytes:97 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:1 ValueBytes:2 PointCount:1 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:0 ValueBytes:0 PointCount:0 PointsCoveredByRangeTombstones:0 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +f#21,1:21 +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:5 ValueBytes:10 PointCount:5 PointsCoveredByRangeTombstones:4 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +. +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:6 ValueBytes:10 PointCount:6 PointsCoveredByRangeTombstones:4 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} +. +{BlockBytes:0 BlockBytesInCache:0 BlockReadDuration:0s KeyBytes:6 ValueBytes:10 PointCount:6 PointsCoveredByRangeTombstones:4 SeparatedPointValue:{Count:0 ValueBytes:0 ValueBytesFetched:0}} diff --git a/pebble/testdata/merging_iter_seek b/pebble/testdata/merging_iter_seek new file mode 100644 index 0000000..2205da2 --- /dev/null +++ b/pebble/testdata/merging_iter_seek @@ -0,0 +1,303 @@ +define +a0.SET.0:0 +a1.SET.1:1 +a2.SET.2:2 +---- + +iter +seek-ge a0 +next +next +next +---- +a0:0 +a1:1 +a2:2 +. + +iter +seek-ge a1 +next +next +---- +a1:1 +a2:2 +. + +iter +seek-ge a2 +next +---- +a2:2 +. + +iter +seek-ge a3 +---- +. + +iter +seek-lt a0 +---- +. + +iter +seek-lt a1 +prev +---- +a0:0 +. + +iter +seek-lt a2 +prev +prev +---- +a1:1 +a0:0 +. + +iter +seek-lt a3 +prev +prev +prev +---- +a2:2 +a1:1 +a0:0 +. + +iter +seek-prefix-ge a0 +next +---- +a0:0 +a1:1 + +iter +seek-prefix-ge a0 +prev +---- +a0:0 +err=pebble: unsupported reverse prefix iteration + +iter +seek-prefix-ge a0 +first +next +next +next +---- +a0:0 +a0:0 +a1:1 +a2:2 +. + +iter +seek-prefix-ge a0 +last +next +---- +a0:0 +a2:2 +. + +iter +seek-prefix-ge a0 +seek-ge a0 +next +next +next +---- +a0:0 +a0:0 +a1:1 +a2:2 +. + +iter +seek-prefix-ge a0 +seek-lt a2 +next +next +---- +a0:0 +a1:1 +a2:2 +. + +iter +seek-prefix-ge a1 +last +prev +prev +prev +---- +a1:1 +a2:2 +a1:1 +a0:0 +. + +iter +seek-prefix-ge a1 +first +prev +---- +a1:1 +a0:0 +. + +define +a0.SET.0:0 b3.SET.3:3 +a1.SET.1:1 +a2.SET.2:2 +---- + +iter +seek-ge a2 +next +next +---- +a2:2 +b3:3 +. + +iter +seek-lt a2 +prev +prev +---- +a1:1 +a0:0 +. + +define +a.SET.0:0 b.SET.3:3 +aa.SET.1:1 +aaa.SET.2:2 +---- + +iter +seek-prefix-ge a +next +seek-prefix-ge aaa +next +---- +a:0 +aa:1 +aaa:2 +b:3 + +iter +seek-prefix-ge aa +prev +---- +aa:1 +err=pebble: unsupported reverse prefix iteration + +iter +seek-prefix-ge aa +next +prev +---- +aa:1 +aaa:2 +err=pebble: unsupported reverse prefix iteration + +iter +seek-prefix-ge aa +next +prev +---- +aa:1 +aaa:2 +err=pebble: unsupported reverse prefix iteration + +iter +seek-prefix-ge aaa +next +---- +aaa:2 +b:3 + +iter +seek-prefix-ge aaa +prev +---- +aaa:2 +err=pebble: unsupported reverse prefix iteration + +iter +seek-prefix-ge b +prev +---- +b:3 +err=pebble: unsupported reverse prefix iteration + +iter +seek-prefix-ge b +next +---- +b:3 +. + +iter +seek-prefix-ge aa +last +prev +prev +prev +prev +---- +aa:1 +b:3 +aaa:2 +aa:1 +a:0 +. + +iter +seek-prefix-ge aa +first +next +next +next +next +---- +aa:1 +a:0 +aa:1 +aaa:2 +b:3 +. + +iter +seek-prefix-ge aa +seek-ge a +next +next +next +next +---- +aa:1 +a:0 +aa:1 +aaa:2 +b:3 +. + +iter +seek-prefix-ge aa +seek-lt aaa +next +next +next +---- +aa:1 +aa:1 +aaa:2 +b:3 +. diff --git a/pebble/testdata/metrics b/pebble/testdata/metrics new file mode 100644 index 0000000..1dbfbb1 --- /dev/null +++ b/pebble/testdata/metrics @@ -0,0 +1,720 @@ +example +---- + | | | | ingested | moved | written | | amp | multilevel +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w | top in read +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+----------+------------------ + 0 | 101 102B 0B 101 | 103.0 | 104B | 112 104B | 113 106B | 221 217B | 107B | 1 2.1 | 104B 104B 104B + 1 | 201 202B 0B 201 | 203.0 | 204B | 212 204B | 213 206B | 421 417B | 207B | 2 2.0 | 204B 204B 204B + 2 | 301 302B 0B 301 | 303.0 | 304B | 312 304B | 313 306B | 621 617B | 307B | 3 2.0 | 304B 304B 304B + 3 | 401 402B 0B 401 | 403.0 | 404B | 412 404B | 413 406B | 821 817B | 407B | 4 2.0 | 404B 404B 404B + 4 | 501 502B 0B 501 | 503.0 | 504B | 512 504B | 513 506B | 1.0K 1017B | 507B | 5 2.0 | 504B 504B 504B + 5 | 601 602B 0B 601 | 603.0 | 604B | 612 604B | 613 606B | 1.2K 1.2KB | 607B | 6 2.0 | 604B 604B 604B + 6 | 701 702B 0B 701 | - | 704B | 712 704B | 713 706B | 1.4K 1.4KB | 707B | 7 2.0 | 704B 704B 704B +total | 2.8K 2.7KB 0B 2.8K | - | 2.8KB | 2.9K 2.8KB | 2.9K 2.8KB | 5.7K 8.4KB | 2.8KB | 28 3.0 | 2.8KB 2.8KB 2.8KB +--------------------------------------------------------------------------------------------------------------------------------------- +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: 1 (2.0MB) +Virtual tables: 2807 (2.8KB) +Block cache: 2 entries (1B) hit rate: 42.9% +Table cache: 18 entries (17B) hit rate: 48.7% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 4 earliest seq num: 1024 +Table iters: 21 +Filter utility: 47.4% +Ingestions: 27 as flushable: 36 (34B in 35 tables) + +batch +set a 1 +---- + +iter-new a category=a qos=non-latency +---- + +flush +---- +0.0: + 000005:[a#10,SET-a#10,SET] + +# iter b references both a memtable and sstable 5. + +iter-new b category=b qos=latency +---- + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 1 661B 0B 0 | 0.25 | 28B | 0 0B | 0 0B | 1 661B | 0B | 1 23.6 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 0 0B 0B 0 | - | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 +total | 1 661B 0B 0 | - | 56B | 0 0B | 0 0B | 1 717B | 0B | 1 12.8 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (28B) in: 17B written: 56B (229% overhead) +Flushes: 1 +Compactions: 0 estimated debt: 0B in progress: 0 (0B) + default: 0 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 1 (256KB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 3 entries (556B) hit rate: 0.0% +Table cache: 1 entries (800B) hit rate: 0.0% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 1 +Filter utility: 0.0% +Ingestions: 0 as flushable: 0 (0B in 0 tables) + +disk-usage +---- +1.9KB + +batch +set b 2 +---- + +flush +---- +0.0: + 000005:[a#10,SET-a#10,SET] + 000007:[b#11,SET-b#11,SET] + +# iter c references both a memtable and sstables 5 and 7. + +iter-new c category=c qos=non-latency +---- + +compact a-z +---- +6: + 000008:[a#0,SET-b#0,SET] + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 56B | 0 0B | 0 0B | 2 1.3KB | 0B | 0 23.6 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 1 667B 0B 0 | - | 1.3KB | 0 0B | 0 0B | 1 667B | 1.3KB | 1 0.5 +total | 1 667B 0B 0 | - | 84B | 0 0B | 0 0B | 3 2.0KB | 1.3KB | 1 24.7 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (28B) in: 34B written: 84B (147% overhead) +Flushes: 2 +Compactions: 1 estimated debt: 0B in progress: 0 (0B) + default: 1 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 2 (512KB) +Zombie tables: 2 (1.3KB) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 5 entries (1.1KB) hit rate: 42.9% +Table cache: 2 entries (1.6KB) hit rate: 66.7% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 2 +Filter utility: 0.0% +Ingestions: 0 as flushable: 0 (0B in 0 tables) +Iter category stats: + pebble-compaction, non-latency: {BlockBytes:132 BlockBytesInCache:88} + +disk-usage +---- +3.3KB + +# Closing iter a will release one of the zombie memtables. + +iter-close a +---- + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 56B | 0 0B | 0 0B | 2 1.3KB | 0B | 0 23.6 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 1 667B 0B 0 | - | 1.3KB | 0 0B | 0 0B | 1 667B | 1.3KB | 1 0.5 +total | 1 667B 0B 0 | - | 84B | 0 0B | 0 0B | 3 2.0KB | 1.3KB | 1 24.7 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (28B) in: 34B written: 84B (147% overhead) +Flushes: 2 +Compactions: 1 estimated debt: 0B in progress: 0 (0B) + default: 1 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 2 (512KB) +Zombie tables: 2 (1.3KB) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 5 entries (1.1KB) hit rate: 42.9% +Table cache: 2 entries (1.6KB) hit rate: 66.7% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 2 +Filter utility: 0.0% +Ingestions: 0 as flushable: 0 (0B in 0 tables) +Iter category stats: + pebble-compaction, non-latency: {BlockBytes:132 BlockBytesInCache:88} + +# Closing iter c will release one of the zombie sstables. The other +# zombie sstable is still referenced by iter b. + +iter-close c +---- + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 56B | 0 0B | 0 0B | 2 1.3KB | 0B | 0 23.6 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 1 667B 0B 0 | - | 1.3KB | 0 0B | 0 0B | 1 667B | 1.3KB | 1 0.5 +total | 1 667B 0B 0 | - | 84B | 0 0B | 0 0B | 3 2.0KB | 1.3KB | 1 24.7 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (28B) in: 34B written: 84B (147% overhead) +Flushes: 2 +Compactions: 1 estimated debt: 0B in progress: 0 (0B) + default: 1 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 2 (512KB) +Zombie tables: 1 (661B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 3 entries (556B) hit rate: 42.9% +Table cache: 1 entries (800B) hit rate: 66.7% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 1 +Filter utility: 0.0% +Ingestions: 0 as flushable: 0 (0B in 0 tables) +Iter category stats: + c, non-latency: {BlockBytes:44 BlockBytesInCache:44} + pebble-compaction, non-latency: {BlockBytes:132 BlockBytesInCache:88} + +disk-usage +---- +2.7KB + +# Closing iter b will release the last zombie sstable and the last zombie memtable. + +iter-close b +---- + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 56B | 0 0B | 0 0B | 2 1.3KB | 0B | 0 23.6 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 1 667B 0B 0 | - | 1.3KB | 0 0B | 0 0B | 1 667B | 1.3KB | 1 0.5 +total | 1 667B 0B 0 | - | 84B | 0 0B | 0 0B | 3 2.0KB | 1.3KB | 1 24.7 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (28B) in: 34B written: 84B (147% overhead) +Flushes: 2 +Compactions: 1 estimated debt: 0B in progress: 0 (0B) + default: 1 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 1 (256KB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 0 entries (0B) hit rate: 42.9% +Table cache: 0 entries (0B) hit rate: 66.7% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 0 as flushable: 0 (0B in 0 tables) +Iter category stats: + b, latency: {BlockBytes:44 BlockBytesInCache:0} + c, non-latency: {BlockBytes:44 BlockBytesInCache:44} + pebble-compaction, non-latency: {BlockBytes:132 BlockBytesInCache:88} + +disk-usage +---- +2.0KB + +additional-metrics +---- +block bytes written: + __level___data-block__value-block + 0 54B 0B + 1 0B 0B + 2 0B 0B + 3 0B 0B + 4 0B 0B + 5 0B 0B + 6 33B 0B + +batch +set c@20 c20 +set c@19 c19 +set c@18 c18 +set c@17 c17 +set c@16 c16 +set c@15 c15 +set c@14 c14 +---- + +flush +---- +0.0: + 000010:[c@20#12,SET-c@18#14,SET] + 000011:[c@17#15,SET-c@15#17,SET] + 000012:[c@14#18,SET-c@14#18,SET] +6: + 000008:[a#0,SET-b#0,SET] + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 3 2.2KB 38B 0 | 0.25 | 149B | 0 0B | 0 0B | 5 3.5KB | 0B | 1 24.2 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 1 667B 0B 0 | - | 1.3KB | 0 0B | 0 0B | 1 667B | 1.3KB | 1 0.5 +total | 4 2.9KB 38B 0 | - | 242B | 0 0B | 0 0B | 6 4.4KB | 1.3KB | 2 18.6 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (93B) in: 116B written: 242B (109% overhead) +Flushes: 3 +Compactions: 1 estimated debt: 2.9KB in progress: 0 (0B) + default: 1 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 1 (256KB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 0 entries (0B) hit rate: 42.9% +Table cache: 0 entries (0B) hit rate: 66.7% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 0 as flushable: 0 (0B in 0 tables) +Iter category stats: + b, latency: {BlockBytes:44 BlockBytesInCache:0} + c, non-latency: {BlockBytes:44 BlockBytesInCache:44} + pebble-compaction, non-latency: {BlockBytes:132 BlockBytesInCache:88} + +additional-metrics +---- +block bytes written: + __level___data-block__value-block + 0 198B 38B + 1 0B 0B + 2 0B 0B + 3 0B 0B + 4 0B 0B + 5 0B 0B + 6 33B 0B + +compact a-z +---- +6: + 000008:[a#0,SET-b#0,SET] + 000013:[c@20#0,SET-c@16#0,SET] + 000014:[c@15#0,SET-c@14#0,SET] + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 149B | 0 0B | 0 0B | 5 3.5KB | 0B | 0 24.2 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 3 2.2KB 41B 0 | - | 3.5KB | 0 0B | 0 0B | 3 2.2KB | 3.5KB | 1 0.6 +total | 3 2.2KB 41B 0 | - | 242B | 0 0B | 0 0B | 8 6.0KB | 3.5KB | 1 25.3 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (93B) in: 116B written: 242B (109% overhead) +Flushes: 3 +Compactions: 2 estimated debt: 0B in progress: 0 (0B) + default: 2 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 1 (256KB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 0 entries (0B) hit rate: 27.3% +Table cache: 0 entries (0B) hit rate: 58.3% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 0 as flushable: 0 (0B in 0 tables) +Iter category stats: + b, latency: {BlockBytes:44 BlockBytesInCache:0} + c, non-latency: {BlockBytes:44 BlockBytesInCache:44} + pebble-compaction, non-latency: {BlockBytes:411 BlockBytesInCache:154} + +additional-metrics +---- +block bytes written: + __level___data-block__value-block + 0 198B 38B + 1 0B 0B + 2 0B 0B + 3 0B 0B + 4 0B 0B + 5 0B 0B + 6 143B 41B + +# Flushable ingestion metrics. This requires there be data in a memtable that +# would overlap with the ingested table(s). Delayed flushes are disabled here to +# prevent the ingestion from immediately triggering a flush of the memtable. +# Instead, we wish to flush manually _after_ the ingestion of the two tables has +# completed, linking the two tables into the flushable queue. + +delay-flush +enable +---- + +batch +set d d +set e e +set f f +---- + +build ext1.sst +set d d +---- + +build ext2.sst +set e e +---- + +ingest ext1.sst ext2.sst +---- + +build ext3.sst +set f f +---- + +ingest ext3.sst +---- + +delay-flush +disable +---- + +flush +---- +0.1: + 000015:[d#22,SET-d#22,SET] + 000016:[e#23,SET-e#23,SET] + 000019:[f#24,SET-f#24,SET] +0.0: + 000023:[d#19,SET-f#21,SET] +6: + 000008:[a#0,SET-b#0,SET] + 000013:[c@20#0,SET-c@16#0,SET] + 000014:[c@15#0,SET-c@14#0,SET] + +# We expect the ingested-as-flushable count to be three (one for each ingested +# table). The unknown category in the iter category stats is because of a gap +# in instrumentation for checking overlap with an existing flushable ingest, +# where we open and close a point iterator when constructing a range-del +# iterator. +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 4 2.8KB 0B 0 | 0.50 | 149B | 3 2.1KB | 0 0B | 6 4.2KB | 0B | 2 28.8 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 3 2.2KB 41B 0 | - | 3.5KB | 0 0B | 0 0B | 3 2.2KB | 3.5KB | 1 0.6 +total | 7 5.0KB 41B 0 | - | 2.3KB | 3 2.1KB | 0 0B | 9 8.7KB | 3.5KB | 3 3.8 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (26B) in: 176B written: 175B (-1% overhead) +Flushes: 8 +Compactions: 2 estimated debt: 5.0KB in progress: 0 (0B) + default: 2 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (1.0MB) zombie: 1 (1.0MB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 12 entries (2.4KB) hit rate: 31.1% +Table cache: 3 entries (2.3KB) hit rate: 57.9% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 0 as flushable: 2 (2.1KB in 3 tables) +Iter category stats: + _unknown, latency: {BlockBytes:88 BlockBytesInCache:88} + b, latency: {BlockBytes:44 BlockBytesInCache:0} + c, non-latency: {BlockBytes:44 BlockBytesInCache:44} + pebble-compaction, non-latency: {BlockBytes:411 BlockBytesInCache:154} + pebble-ingest, latency: {BlockBytes:192 BlockBytesInCache:128} + +batch +set g g +set h h +set i i +set j j +set k k +set l l +set m m +---- + +flush +---- +0.1: + 000015:[d#22,SET-d#22,SET] + 000016:[e#23,SET-e#23,SET] + 000019:[f#24,SET-f#24,SET] +0.0: + 000023:[d#19,SET-f#21,SET] + 000025:[g#25,SET-i#27,SET] + 000026:[j#28,SET-l#30,SET] + 000027:[m#31,SET-m#31,SET] +6: + 000008:[a#0,SET-b#0,SET] + 000013:[c@20#0,SET-c@16#0,SET] + 000014:[c@15#0,SET-c@14#0,SET] + +metrics +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 7 4.7KB 0B 0 | 0.50 | 207B | 3 2.1KB | 0 0B | 9 6.2KB | 0B | 2 30.5 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 3 2.2KB 41B 0 | - | 3.5KB | 0 0B | 0 0B | 3 2.2KB | 3.5KB | 1 0.6 +total | 10 7.0KB 41B 0 | - | 2.4KB | 3 2.1KB | 0 0B | 12 11KB | 3.5KB | 3 4.6 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (58B) in: 223B written: 265B (19% overhead) +Flushes: 9 +Compactions: 2 estimated debt: 7.0KB in progress: 0 (0B) + default: 2 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (1.0MB) zombie: 1 (1.0MB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 12 entries (2.4KB) hit rate: 31.1% +Table cache: 3 entries (2.3KB) hit rate: 57.9% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 0 as flushable: 2 (2.1KB in 3 tables) +Iter category stats: + _unknown, latency: {BlockBytes:88 BlockBytesInCache:88} + b, latency: {BlockBytes:44 BlockBytesInCache:0} + c, non-latency: {BlockBytes:44 BlockBytesInCache:44} + pebble-compaction, non-latency: {BlockBytes:411 BlockBytesInCache:154} + pebble-ingest, latency: {BlockBytes:192 BlockBytesInCache:128} + +build ext1 +set z z +---- + +ingest-and-excise ext1 excise=i-k +---- + +# sstable 29, 30 were created as virtual when i-k was excised. +lsm +---- +0.1: + 000015:[d#22,SET-d#22,SET] + 000016:[e#23,SET-e#23,SET] + 000019:[f#24,SET-f#24,SET] +0.0: + 000023:[d#19,SET-f#21,SET] + 000029:[g#25,SET-h#26,SET] + 000030:[k#29,SET-l#30,SET] + 000027:[m#31,SET-m#31,SET] +6: + 000008:[a#0,SET-b#0,SET] + 000013:[c@20#0,SET-c@16#0,SET] + 000014:[c@15#0,SET-c@14#0,SET] + 000028:[z#32,SET-z#32,SET] + +# There should be 2 backing tables. Note that tiny sstables have inaccurate +# virtual sstable sizes. +metrics-value +num-backing +backing-size +num-virtual +num-virtual 0 +virtual-size +---- +2 +1.3KB +2 +2 +102B + +metrics zero-cache-hits-misses +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 7 3.5KB 0B 2 | 0.50 | 207B | 3 2.1KB | 0 0B | 9 6.2KB | 0B | 2 30.5 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 4 2.9KB 41B 0 | - | 3.5KB | 1 716B | 0 0B | 3 2.2KB | 3.5KB | 1 0.6 +total | 11 6.4KB 41B 2 | - | 3.1KB | 4 2.8KB | 0 0B | 12 12KB | 3.5KB | 3 3.7 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (58B) in: 223B written: 265B (19% overhead) +Flushes: 9 +Compactions: 2 estimated debt: 6.4KB in progress: 0 (0B) + default: 2 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (1.0MB) zombie: 1 (1.0MB) +Zombie tables: 0 (0B) +Backing tables: 2 (1.3KB) +Virtual tables: 2 (102B) +Block cache: 21 entries (4.1KB) hit rate: 0.0% +Table cache: 5 entries (3.9KB) hit rate: 0.0% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 1 as flushable: 2 (2.1KB in 3 tables) +Iter category stats: + _unknown, latency: {BlockBytes:0 BlockBytesInCache:0} + b, latency: {BlockBytes:44 BlockBytesInCache:0} + c, non-latency: {BlockBytes:44 BlockBytesInCache:44} + pebble-compaction, non-latency: {BlockBytes:411 BlockBytesInCache:154} + pebble-ingest, latency: {BlockBytes:328 BlockBytesInCache:128} + +# Virtualize a virtual sstable. +build ext1 +set zz zz +---- + +ingest-and-excise ext1 excise=k-l +---- + +# sstable 32 created when k-l was excised, but no new backing file should be +# created. +lsm +---- +0.1: + 000015:[d#22,SET-d#22,SET] + 000016:[e#23,SET-e#23,SET] + 000019:[f#24,SET-f#24,SET] +0.0: + 000023:[d#19,SET-f#21,SET] + 000029:[g#25,SET-h#26,SET] + 000032:[l#30,SET-l#30,SET] + 000027:[m#31,SET-m#31,SET] +6: + 000008:[a#0,SET-b#0,SET] + 000013:[c@20#0,SET-c@16#0,SET] + 000014:[c@15#0,SET-c@14#0,SET] + 000028:[z#32,SET-z#32,SET] + 000031:[zz#33,SET-zz#33,SET] + +metrics-value +num-backing +backing-size +num-virtual +num-virtual 0 +virtual-size +---- +2 +1.3KB +2 +2 +102B + +compact a-z +---- +6: + 000008:[a#0,SET-b#0,SET] + 000013:[c@20#0,SET-c@16#0,SET] + 000014:[c@15#0,SET-c@14#0,SET] + 000033:[d#0,SET-m#0,SET] + 000028:[z#32,SET-z#32,SET] + 000031:[zz#33,SET-zz#33,SET] + +# Virtual sstables metrics should be gone after the compaction. +metrics-value +num-backing +backing-size +num-virtual +num-virtual 0 +virtual-size +---- +0 +0B +0 +0 +0B + +metrics zero-cache-hits-misses +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 207B | 3 2.1KB | 0 0B | 9 6.2KB | 0B | 0 30.5 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 6 4.3KB 41B 0 | - | 7.0KB | 2 1.4KB | 0 0B | 4 2.9KB | 7.0KB | 1 0.4 +total | 6 4.3KB 41B 0 | - | 3.8KB | 5 3.5KB | 0 0B | 13 13KB | 7.0KB | 1 3.4 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (58B) in: 223B written: 265B (19% overhead) +Flushes: 9 +Compactions: 3 estimated debt: 0B in progress: 0 (0B) + default: 3 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (1.0MB) zombie: 1 (1.0MB) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 6 entries (1.2KB) hit rate: 0.0% +Table cache: 0 entries (0B) hit rate: 0.0% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 2 as flushable: 2 (2.1KB in 3 tables) +Iter category stats: + _unknown, latency: {BlockBytes:0 BlockBytesInCache:0} + b, latency: {BlockBytes:44 BlockBytesInCache:0} + c, non-latency: {BlockBytes:44 BlockBytesInCache:44} + pebble-compaction, non-latency: {BlockBytes:941 BlockBytesInCache:640} + pebble-ingest, latency: {BlockBytes:400 BlockBytesInCache:200} diff --git a/pebble/testdata/point_collapsing_iter b/pebble/testdata/point_collapsing_iter new file mode 100644 index 0000000..da98864 --- /dev/null +++ b/pebble/testdata/point_collapsing_iter @@ -0,0 +1,67 @@ + +define +a.SET.5:foo +b.SET.6:foo +b.DEL.4: +c.SET.7:bar +c.SET.5:foo +---- + +iter +first +next +next +next +next +---- +a#5,1:foo +b#6,1:foo +c#7,1:bar +. +. + +# Ensure that we pause at (and return) rangedel start points correctly. + +define +a.RANGEDEL.4:b +a.SET.5:foo +b.RANGEDEL.3:c +b.SET.6:foo +b.DEL.4: +c.SET.7:bar +c.SET.5:foo +---- + +iter +seek-ge b +next +next +---- +b#72057594037927935,15: +b#6,1:foo +c#7,1:bar + +# More rangedel elision tests + +define +a.RANGEDEL.4:b +a.SET.5:foo +b.RANGEDEL.4:c +b.SET.3:foo +b.DEL.2: +c.SET.7:bar +c.SET.5:foo +---- + +iter +seek-ge a +next +next +next +next +---- +a#72057594037927935,15: +a#5,1:foo +b#72057594037927935,15: +c#7,1:bar +. diff --git a/pebble/testdata/range_del b/pebble/testdata/range_del new file mode 100644 index 0000000..14102e0 --- /dev/null +++ b/pebble/testdata/range_del @@ -0,0 +1,1543 @@ +# 1 memtable. + +define +mem + a.SET.10:b + a.SET.12:c + a.SET.14:d + b.MERGE.10:b + b.MERGE.12:c + b.MERGE.14:d + b.RANGEDEL.15:c + b.MERGE.16:e + c.SET.10:b + c.SET.12:c + c.SET.14:d +---- +mem: 1 + +get seq=11 +a +b +c +---- +a:b +b:b +c:b + +get seq=13 +a +b +c +---- +a:c +b:bc +c:c + +get seq=15 +a +b +c +---- +a:d +b:bcd +c:d + +get seq=16 +a +b +c +---- +a:d +b: pebble: not found +c:d + +get seq=17 +a +b +c +---- +a:d +b:e +c:d + +get seq=15 +a +b +c +---- +a:d +b:bcd +c:d + +iter seq=15 +first +next +next +next +seek-ge a +seek-ge b +seek-ge c +seek-ge d +last +prev +prev +prev +seek-lt a +seek-lt b +seek-lt c +seek-lt d +---- +a: (d, .) +b: (bcd, .) +c: (d, .) +. +a: (d, .) +b: (bcd, .) +c: (d, .) +. +c: (d, .) +b: (bcd, .) +a: (d, .) +. +. +a: (d, .) +b: (bcd, .) +c: (d, .) + +iter seq=16 +first +next +next +seek-ge a +seek-ge b +seek-ge c +seek-ge d +last +prev +prev +seek-lt a +seek-lt b +seek-lt c +seek-lt d +---- +a: (d, .) +c: (d, .) +. +a: (d, .) +c: (d, .) +c: (d, .) +. +c: (d, .) +a: (d, .) +. +. +a: (d, .) +a: (d, .) +c: (d, .) + +# Multiple memtables. + +define +mem + a.SET.10:b + b.MERGE.10:b + c.SET.10:b +mem + a.SET.12:c + b.MERGE.12:c + c.SET.12:c +mem + a.SET.14:d + b.MERGE.14:d + c.SET.14:d +mem + b.RANGEDEL.15:c +mem + b.MERGE.16:e +---- +mem: 5 + +get seq=11 +a +b +c +---- +a:b +b:b +c:b + +get seq=13 +a +b +c +---- +a:c +b:bc +c:c + +get seq=15 +a +b +c +---- +a:d +b:bcd +c:d + +get seq=16 +a +b +c +---- +a:d +b: pebble: not found +c:d + +get seq=17 +a +b +c +---- +a:d +b:e +c:d + +get seq=15 +a +b +c +---- +a:d +b:bcd +c:d + +iter seq=15 +first +next +next +next +seek-ge a +seek-ge b +seek-ge c +seek-ge d +last +prev +prev +prev +seek-lt a +seek-lt b +seek-lt c +seek-lt d +---- +a: (d, .) +b: (bcd, .) +c: (d, .) +. +a: (d, .) +b: (bcd, .) +c: (d, .) +. +c: (d, .) +b: (bcd, .) +a: (d, .) +. +. +a: (d, .) +b: (bcd, .) +c: (d, .) + +iter seq=16 +first +next +next +seek-ge a +seek-ge b +seek-ge c +seek-ge d +last +prev +prev +seek-lt a +seek-lt b +seek-lt c +seek-lt d +---- +a: (d, .) +c: (d, .) +. +a: (d, .) +c: (d, .) +c: (d, .) +. +c: (d, .) +a: (d, .) +. +. +a: (d, .) +a: (d, .) +c: (d, .) + +# Overlapping range deletions in the same memtable. + +define +mem + a.SET.10:1 + a.SET.12:2 + a.SET.14:3 + a.SET.16:4 + b.SET.10:1 + b.SET.12:2 + b.SET.14:3 + b.SET.16:4 + c.SET.10:1 + c.SET.12:2 + c.SET.14:3 + c.SET.16:4 + d.SET.10:1 + d.SET.12:2 + d.SET.14:3 + d.SET.16:4 + a.RANGEDEL.11:b + b.RANGEDEL.13:c + b.RANGEDEL.11:c + c.RANGEDEL.15:d + c.RANGEDEL.13:d + c.RANGEDEL.11:d +---- +mem: 1 + +get seq=11 +a +b +c +d +---- +a:1 +b:1 +c:1 +d:1 + +get seq=12 +a +b +c +d +---- +a: pebble: not found +b: pebble: not found +c: pebble: not found +d:1 + +get seq=14 +a +b +c +d +---- +a:2 +b: pebble: not found +c: pebble: not found +d:2 + +get seq=16 +a +b +c +d +---- +a:3 +b:3 +c: pebble: not found +d:3 + +get seq=18 +a +b +c +d +---- +a:4 +b:4 +c:4 +d:4 + +iter seq=11 +first +next +next +next +next +last +prev +prev +prev +prev +---- +a: (1, .) +b: (1, .) +c: (1, .) +d: (1, .) +. +d: (1, .) +c: (1, .) +b: (1, .) +a: (1, .) +. + +iter seq=12 +first +next +last +prev +---- +d: (1, .) +. +d: (1, .) +. + +iter seq=14 +first +next +next +last +prev +prev +---- +a: (2, .) +d: (2, .) +. +d: (2, .) +a: (2, .) +. + +iter seq=16 +first +next +next +next +last +prev +prev +prev +---- +a: (3, .) +b: (3, .) +d: (3, .) +. +d: (3, .) +b: (3, .) +a: (3, .) +. + +iter seq=18 +first +next +next +next +next +last +prev +prev +prev +prev +---- +a: (4, .) +b: (4, .) +c: (4, .) +d: (4, .) +. +d: (4, .) +c: (4, .) +b: (4, .) +a: (4, .) +. + +# Overlapping range deletions in different memtables. Note that the +# range tombstones are not fragmented in this case. + +define +mem + a.SET.10:1 + b.SET.10:1 + c.SET.10:1 + d.SET.10:1 +mem + a.SET.12:2 + b.SET.12:2 + c.SET.12:2 + d.SET.12:2 + a.RANGEDEL.11:d +mem + a.SET.14:3 + b.SET.14:3 + c.SET.14:3 + d.SET.14:3 + b.RANGEDEL.13:d +mem + a.SET.16:4 + b.SET.16:4 + c.SET.16:4 + d.SET.16:4 + c.RANGEDEL.13:d +---- +mem: 4 + +get seq=11 +a +b +c +d +---- +a:1 +b:1 +c:1 +d:1 + +get seq=12 +a +b +c +d +---- +a: pebble: not found +b: pebble: not found +c: pebble: not found +d:1 + +get seq=14 +a +b +c +d +---- +a:2 +b: pebble: not found +c: pebble: not found +d:2 + +get seq=16 +a +b +c +d +---- +a:3 +b:3 +c: pebble: not found +d:3 + +get seq=18 +a +b +c +d +---- +a:4 +b:4 +c:4 +d:4 + +iter seq=11 +first +next +next +next +next +last +prev +prev +prev +prev +---- +a: (1, .) +b: (1, .) +c: (1, .) +d: (1, .) +. +d: (1, .) +c: (1, .) +b: (1, .) +a: (1, .) +. + +iter seq=12 +first +next +last +prev +---- +d: (1, .) +. +d: (1, .) +. + +iter seq=14 +first +next +next +last +prev +prev +---- +a: (2, .) +d: (2, .) +. +d: (2, .) +a: (2, .) +. + +iter seq=16 +first +next +next +next +last +prev +prev +prev +---- +a: (3, .) +b: (3, .) +d: (3, .) +. +d: (3, .) +b: (3, .) +a: (3, .) +. + +iter seq=18 +first +next +next +next +next +last +prev +prev +prev +prev +---- +a: (4, .) +b: (4, .) +c: (4, .) +d: (4, .) +. +d: (4, .) +c: (4, .) +b: (4, .) +a: (4, .) +. + +# User-key that spans tables in a level. + +define +L1 + a.SET.12:3 +L1 + a.SET.11:2 +L1 + a.SET.10:1 +---- +mem: 1 +1: + 000004:[a#12,SET-a#12,SET] + 000005:[a#11,SET-a#11,SET] + 000006:[a#10,SET-a#10,SET] + +get seq=10 +a +---- +a: pebble: not found + +get seq=11 +a +---- +a:1 + +get seq=12 +a +---- +a:2 + +get seq=13 +a +---- +a:3 + +iter seq=11 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (1, .) +a: (1, .) +. +a: (1, .) +. +a: (1, .) + +iter seq=12 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (2, .) +a: (2, .) +. +a: (2, .) +. +a: (2, .) + +iter seq=13 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (3, .) +a: (3, .) +. +a: (3, .) +. +a: (3, .) + +define +L1 + a.MERGE.12:3 +L1 + a.MERGE.11:2 +L1 + a.MERGE.10:1 +---- +mem: 1 +1: + 000004:[a#12,MERGE-a#12,MERGE] + 000005:[a#11,MERGE-a#11,MERGE] + 000006:[a#10,MERGE-a#10,MERGE] + +get seq=10 +a +---- +a: pebble: not found + +get seq=11 +a +---- +a:1 + +get seq=12 +a +---- +a:12 + +get seq=13 +a +---- +a:123 + +iter seq=11 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (1, .) +a: (1, .) +. +a: (1, .) +. +a: (1, .) + +iter seq=12 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (12, .) +a: (12, .) +. +a: (12, .) +. +a: (12, .) + +iter seq=13 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (123, .) +a: (123, .) +. +a: (123, .) +. +a: (123, .) + +# User-key spread across multiple levels. + +define +mem + a.MERGE.13:4 +L1 + a.MERGE.12:3 +L2 + a.MERGE.11:2 +L3 + a.MERGE.10:1 +---- +mem: 1 +1: + 000004:[a#12,MERGE-a#12,MERGE] +2: + 000005:[a#11,MERGE-a#11,MERGE] +3: + 000006:[a#10,MERGE-a#10,MERGE] + +get seq=10 +a +---- +a: pebble: not found + +get seq=11 +a +---- +a:1 + +get seq=12 +a +---- +a:12 + +get seq=13 +a +---- +a:123 + +get seq=14 +a +---- +a:1234 + +iter seq=11 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (1, .) +a: (1, .) +. +a: (1, .) +. +a: (1, .) + +iter seq=12 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (12, .) +a: (12, .) +. +a: (12, .) +. +a: (12, .) + +iter seq=13 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (123, .) +a: (123, .) +. +a: (123, .) +. +a: (123, .) + +iter seq=14 +first +seek-ge a +seek-ge b +last +seek-lt a +seek-lt b +---- +a: (1234, .) +a: (1234, .) +. +a: (1234, .) +. +a: (1234, .) + +# Range deletions on multiple levels. +define +L0 + a.SET.13:4 + b.SET.13:4 + d.SET.13:4 + c.RANGEDEL.13:d +L1 + a.SET.12:3 + d.SET.12:3 + b.RANGEDEL.12:d +L2 + d.SET.11:2 + a.RANGEDEL.11:d +L3 + a.SET.10:1 + b.SET.10:1 + c.SET.10:1 + d.SET.10:1 +---- +mem: 1 +0.0: + 000004:[a#13,SET-d#13,SET] +1: + 000005:[a#12,SET-d#12,SET] +2: + 000006:[a#11,RANGEDEL-d#11,SET] +3: + 000007:[a#10,SET-d#10,SET] + +get seq=11 +a +b +c +d +---- +a:1 +b:1 +c:1 +d:1 + +get seq=12 +a +b +c +d +---- +a: pebble: not found +b: pebble: not found +c: pebble: not found +d:2 + +get seq=13 +a +b +c +d +---- +a:3 +b: pebble: not found +c: pebble: not found +d:3 + +get seq=14 +a +b +c +d +---- +a:4 +b:4 +c: pebble: not found +d:4 + +iter seq=11 +first +next +next +next +last +prev +prev +prev +---- +a: (1, .) +b: (1, .) +c: (1, .) +d: (1, .) +d: (1, .) +c: (1, .) +b: (1, .) +a: (1, .) + +iter seq=12 +first +last +---- +d: (2, .) +d: (2, .) + +iter seq=13 +first +next +last +prev +---- +a: (3, .) +d: (3, .) +d: (3, .) +a: (3, .) + +iter seq=14 +first +next +next +last +prev +prev +---- +a: (4, .) +b: (4, .) +d: (4, .) +d: (4, .) +b: (4, .) +a: (4, .) + +# Range deletions spanning tables within a level. + +define +mem + a.SET.12:3 + b.SET.12:3 + c.SET.12:3 + d.SET.12:3 +L1 + a.RANGEDEL.11:b +L1 + b.RANGEDEL.11:c +L1 + c.RANGEDEL.11:d +L2 + a.SET.10:1 + b.SET.10:1 + c.SET.10:1 + d.SET.10:1 +---- +mem: 1 +1: + 000004:[a#11,RANGEDEL-b#inf,RANGEDEL] + 000005:[b#11,RANGEDEL-c#inf,RANGEDEL] + 000006:[c#11,RANGEDEL-d#inf,RANGEDEL] +2: + 000007:[a#10,SET-d#10,SET] + +get seq=11 +a +b +c +d +---- +a:1 +b:1 +c:1 +d:1 + +get seq=12 +a +b +c +d +---- +a: pebble: not found +b: pebble: not found +c: pebble: not found +d:1 + +get seq=13 +a +b +c +d +---- +a:3 +b:3 +c:3 +d:3 + +iter seq=11 +first +next +next +next +last +prev +prev +prev +---- +a: (1, .) +b: (1, .) +c: (1, .) +d: (1, .) +d: (1, .) +c: (1, .) +b: (1, .) +a: (1, .) + +iter seq=12 +first +last +---- +d: (1, .) +d: (1, .) + +iter seq=13 +first +next +next +next +last +prev +prev +prev +---- +a: (3, .) +b: (3, .) +c: (3, .) +d: (3, .) +d: (3, .) +c: (3, .) +b: (3, .) +a: (3, .) + +# Invalid LSM structure (range deletion at newer level covers newer +# write at an older level). This LSM structure is not generated +# naturally, but tested here to show the level-by-level nature of Get. + +define +L1 + a.RANGEDEL.10:b +L2 + a.SET.11:2 +---- +mem: 1 +1: + 000004:[a#10,RANGEDEL-b#inf,RANGEDEL] +2: + 000005:[a#11,SET-a#11,SET] + +get seq=12 +a +---- +a: pebble: not found + +# A range tombstone straddles two SSTs. One is compacted to a lower level. Its +# keys that are newer than the range tombstone should not disappear. +# +# Uses a snapshot to prevent range tombstone from being elided when it gets +# compacted to the bottommost level. + +define target-file-sizes=(100, 1) snapshots=(1) +L0 + a.RANGEDEL.10:e +L0 + a.SET.11:v +L0 + c.SET.12:v +---- +mem: 1 +0.1: + 000005:[a#11,SET-a#11,SET] + 000006:[c#12,SET-c#12,SET] +0.0: + 000004:[a#10,RANGEDEL-e#inf,RANGEDEL] + +compact a-e +---- +1: + 000007:[a#11,SET-c#inf,RANGEDEL] + 000008:[c#12,SET-e#inf,RANGEDEL] + +compact d-e +---- +1: + 000007:[a#11,SET-c#inf,RANGEDEL] +2: + 000008:[c#12,SET-e#inf,RANGEDEL] + +iter seq=13 +seek-ge b +next +---- +c: (v, .) +. + +# Reverse the above test: compact the left file containing the split range +# tombstone downwards, and iterate from right to left. + +define target-file-sizes=(100, 1) snapshots=(1) +L0 + a.RANGEDEL.10:e +L0 + a.SET.11:v +L0 + c.SET.12:v +---- +mem: 1 +0.1: + 000005:[a#11,SET-a#11,SET] + 000006:[c#12,SET-c#12,SET] +0.0: + 000004:[a#10,RANGEDEL-e#inf,RANGEDEL] + +compact a-e +---- +1: + 000007:[a#11,SET-c#inf,RANGEDEL] + 000008:[c#12,SET-e#inf,RANGEDEL] + +compact a-b +---- +1: + 000008:[c#12,SET-e#inf,RANGEDEL] +2: + 000007:[a#11,SET-c#inf,RANGEDEL] + +iter seq=13 +seek-lt d +prev +prev +---- +c: (v, .) +a: (v, .) +. + +# A range tombstone straddles two sstables. One is compacted two +# levels lower. The other is compacted one level lower. The one that +# is compacted one level lower should not see its boundaries expand +# causing it to delete more keys. A snapshot is used to prevent range +# tombstone from being elided when it gets compacted to the bottommost +# level. + +define target-file-sizes=(100, 1) snapshots=(1) +L0 + a.RANGEDEL.10:e +L0 + a.SET.11:v +L0 + c.SET.12:v +L2 + d.SET.0:v +---- +mem: 1 +0.1: + 000005:[a#11,SET-a#11,SET] + 000006:[c#12,SET-c#12,SET] +0.0: + 000004:[a#10,RANGEDEL-e#inf,RANGEDEL] +2: + 000007:[d#0,SET-d#0,SET] + +compact a-b +---- +1: + 000008:[a#11,SET-c#inf,RANGEDEL] + 000009:[c#12,SET-d#inf,RANGEDEL] + 000010:[d#10,RANGEDEL-e#inf,RANGEDEL] +2: + 000007:[d#0,SET-d#0,SET] + +compact d-e +---- +1: + 000008:[a#11,SET-c#inf,RANGEDEL] + 000009:[c#12,SET-d#inf,RANGEDEL] +3: + 000011:[d#10,RANGEDEL-e#inf,RANGEDEL] + +get seq=13 +c +---- +c:v + +compact a-b L1 +---- +1: + 000009:[c#12,SET-d#inf,RANGEDEL] +2: + 000008:[a#11,SET-c#inf,RANGEDEL] +3: + 000011:[d#10,RANGEDEL-e#inf,RANGEDEL] + +get seq=13 +c +---- +c:v + +# A slight variation on the scenario above where a range tombstone is +# expanded past the boundaries of its "atomic compaction unit". + +define target-file-sizes=(100, 1) snapshots=(1) +L0 + a.RANGEDEL.10:e +L0 + a.SET.11:v +L0 + c.SET.12:v +L0 + f.SET.13:v +L2 + d.SET.0:v +---- +mem: 1 +0.1: + 000005:[a#11,SET-a#11,SET] + 000006:[c#12,SET-c#12,SET] +0.0: + 000004:[a#10,RANGEDEL-e#inf,RANGEDEL] + 000007:[f#13,SET-f#13,SET] +2: + 000008:[d#0,SET-d#0,SET] + +compact a-b +---- +0.0: + 000007:[f#13,SET-f#13,SET] +1: + 000009:[a#11,SET-c#inf,RANGEDEL] + 000010:[c#12,SET-d#inf,RANGEDEL] + 000011:[d#10,RANGEDEL-e#inf,RANGEDEL] +2: + 000008:[d#0,SET-d#0,SET] + +compact d-e +---- +0.0: + 000007:[f#13,SET-f#13,SET] +1: + 000009:[a#11,SET-c#inf,RANGEDEL] + 000010:[c#12,SET-d#inf,RANGEDEL] +3: + 000012:[d#10,RANGEDEL-e#inf,RANGEDEL] + +get seq=13 +c +---- +c:v + +compact f-f L0 +---- +1: + 000009:[a#11,SET-c#inf,RANGEDEL] + 000010:[c#12,SET-d#inf,RANGEDEL] + 000007:[f#13,SET-f#13,SET] +3: + 000012:[d#10,RANGEDEL-e#inf,RANGEDEL] + +compact a-f L1 +---- +2: + 000013:[a#11,SET-c#inf,RANGEDEL] + 000014:[c#12,SET-d#inf,RANGEDEL] + 000015:[f#13,SET-f#13,SET] +3: + 000012:[d#10,RANGEDEL-e#inf,RANGEDEL] + +get seq=13 +c +---- +c:v + +define +L0 + a.RANGEDEL.12:f +L0 + a.RANGEDEL.13:c + c.RANGEDEL.13:f +L1 + b.RANGEDEL.11:e +L2 + c.RANGEDEL.10:d +---- +mem: 1 +0.1: + 000005:[a#13,RANGEDEL-f#inf,RANGEDEL] +0.0: + 000004:[a#12,RANGEDEL-f#inf,RANGEDEL] +1: + 000006:[b#11,RANGEDEL-e#inf,RANGEDEL] +2: + 000007:[c#10,RANGEDEL-d#inf,RANGEDEL] + +wait-pending-table-stats +000007 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +wait-pending-table-stats +000006 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 836 + +wait-pending-table-stats +000004 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 1672 + +wait-pending-table-stats +000005 +---- +num-entries: 2 +num-deletions: 2 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 1672 + + +# Range deletions with varying overlap. +define +L0 + a.SET.13:4 + b.SET.13:4 + d.SET.13:4 + c.RANGEDEL.13:d +L1 + a.SET.12:3 + d.SET.12:3 + b.RANGEDEL.12:d +L2 + d.SET.11:2 + a.RANGEDEL.11:d +L3 + a.SET.10:1 + b.SET.10:1 + c.SET.10:1 + d.SET.10:1 +---- +mem: 1 +0.0: + 000004:[a#13,SET-d#13,SET] +1: + 000005:[a#12,SET-d#12,SET] +2: + 000006:[a#11,RANGEDEL-d#11,SET] +3: + 000007:[a#10,SET-d#10,SET] + +wait-pending-table-stats +000007 +---- +num-entries: 4 +num-deletions: 0 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +wait-pending-table-stats +000006 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 42 + +wait-pending-table-stats +000005 +---- +num-entries: 3 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 68 + +wait-pending-table-stats +000004 +---- +num-entries: 4 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 100 + +# Multiple Range deletions in a table. +define +L0 + a.RANGEDEL.15:d + e.RANGEDEL.15:z +L0 + a.RANGEDEL.14:d +L0 + e.RANGEDEL.13:z +L1 + a.SET.11:1 + b.SET.11:1 + c.SET.11:1 +L2 + x.SET.10:2 +---- +mem: 1 +0.1: + 000004:[a#15,RANGEDEL-z#inf,RANGEDEL] +0.0: + 000005:[a#14,RANGEDEL-d#inf,RANGEDEL] + 000006:[e#13,RANGEDEL-z#inf,RANGEDEL] +1: + 000007:[a#11,SET-c#11,SET] +2: + 000008:[x#10,SET-x#10,SET] + +wait-pending-table-stats +000005 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 782 + +wait-pending-table-stats +000006 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 771 + +wait-pending-table-stats +000004 +---- +num-entries: 2 +num-deletions: 2 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 1553 diff --git a/pebble/testdata/read_compaction_queue b/pebble/testdata/read_compaction_queue new file mode 100644 index 0000000..54644d6 --- /dev/null +++ b/pebble/testdata/read_compaction_queue @@ -0,0 +1,627 @@ +# Verify these invariantLs: +# No overlapping ranges. +# Size should reflect what's currently in the queue. +# Empty queue should return nil. +# Oldest element should be removed first. + +# Remove from empty queue. +# Removing from the empty queue should return nil. +create +---- +(success) + +print-size +---- +0 + +remove-compaction +---- +(nil) + +# Add to empty queue. +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +print-size +---- +1 + +remove-compaction +---- +L5: a-e 1 + +print-size +---- +0 + +remove-compaction +---- +(nil) + +# No intersection in a non-full queue. +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +print-queue +---- +L5: a-e 1 + +add-compaction +L5: g-k 2 +---- + +print-queue +---- +L5: a-e 1 +L5: g-k 2 + +print-size +---- +2 + +remove-compaction +---- +L5: a-e 1 + +print-size +---- +1 + +remove-compaction +---- +L5: g-k 2 + +print-size +---- +0 + +# No intersection in a full queue. +# This adds a compaction once the queue is already full. +# This new compaction doesn't intersect with any compaction +# already in the queue, so the oldest element is evicted. +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +add-compaction +L5: f-g 2 +---- + +add-compaction +L5: k-m 3 +---- + +add-compaction +L5: n-o 4 +---- + +add-compaction +L5: p-r 5 +---- + +print-size +---- +5 + +print-queue +---- +L5: a-e 1 +L5: f-g 2 +L5: k-m 3 +L5: n-o 4 +L5: p-r 5 + +add-compaction +L4: t-u 6 +---- + +print-queue +---- +L5: f-g 2 +L5: k-m 3 +L5: n-o 4 +L5: p-r 5 +L4: t-u 6 + +print-size +---- +5 + + +# One intersection in a non-full queue. +# Try intersections with intervals at +# various positions in the queue, and examine +# the state of the queue after. +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +add-compaction +L5: f-g 2 +---- + +add-compaction +L5: k-m 3 +---- + +add-compaction +L5: n-w 4 +---- + +# +print-size +---- +4 + +print-queue +---- +L5: a-e 1 +L5: f-g 2 +L5: k-m 3 +L5: n-w 4 + +# Add interval which intersects with the first +# interval in the queue. +add-compaction +L4: c-d 5 +---- + +print-queue +---- +L5: f-g 2 +L5: k-m 3 +L5: n-w 4 +L4: c-d 5 + +print-size +---- +4 + +# Add an interval which intersects with the last +# element in the queue. +add-compaction +L3: a-d 6 +---- + +print-queue +---- +L5: f-g 2 +L5: k-m 3 +L5: n-w 4 +L3: a-d 6 + +print-size +---- +4 + +# Add an interval which intersects with an element in the +# middle of the queue. +add-compaction +L3: u-z 7 +---- + +print-queue +---- +L5: f-g 2 +L5: k-m 3 +L3: a-d 6 +L3: u-z 7 + +print-size +---- +4 + +remove-compaction +---- +L5: f-g 2 + +remove-compaction +---- +L5: k-m 3 + +remove-compaction +---- +L3: a-d 6 + +remove-compaction +---- +L3: u-z 7 + +remove-compaction +---- +(nil) + +# One intersection in a full queue. +# We're doing some tests with full/non-full queues +# because the logic for those cases is sometimes different. +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +add-compaction +L5: f-g 2 +---- + +add-compaction +L5: k-m 3 +---- + +add-compaction +L5: n-w 4 +---- + +add-compaction +L5: x-z 5 +---- + +print-size +---- +5 + +print-queue +---- +L5: a-e 1 +L5: f-g 2 +L5: k-m 3 +L5: n-w 4 +L5: x-z 5 + +# Add interval which intersects with the first +# interval in the queue. +add-compaction +L4: c-d 6 +---- + +print-queue +---- +L5: f-g 2 +L5: k-m 3 +L5: n-w 4 +L5: x-z 5 +L4: c-d 6 + +print-size +---- +5 + +# Add an interval which intersects with the last +# element in the queue. +add-compaction +L3: a-d 6 +---- + +print-queue +---- +L5: f-g 2 +L5: k-m 3 +L5: n-w 4 +L5: x-z 5 +L3: a-d 6 + +print-size +---- +5 + +# Add an interval which intersects with an element in the +# middle of the queue. +add-compaction +L3: u-z 7 +---- + +print-queue +---- +L5: f-g 2 +L5: k-m 3 +L3: a-d 6 +L3: u-z 7 + +print-size +---- +4 + +remove-compaction +---- +L5: f-g 2 + +remove-compaction +---- +L5: k-m 3 + +remove-compaction +---- +L3: a-d 6 + +remove-compaction +---- +L3: u-z 7 + +remove-compaction +---- +(nil) + +# More than one intersection in a non-full queue. +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +add-compaction +L5: f-g 2 +---- + +add-compaction +L5: k-m 3 +---- + +add-compaction +L5: n-w 4 +---- + +# +print-size +---- +4 + +print-queue +---- +L5: a-e 1 +L5: f-g 2 +L5: k-m 3 +L5: n-w 4 + +# Add an interval with intersects with two elements of the queue. +add-compaction +L4: f-m 5 +---- + +print-queue +---- +L5: a-e 1 +L5: n-w 4 +L4: f-m 5 + +print-size +---- +3 + +# Add an interval which clears the entire queue. +add-compaction +L3: a-z 6 +---- + +print-queue +---- +L3: a-z 6 + +print-size +---- +1 + +remove-compaction +---- +L3: a-z 6 + + +remove-compaction +---- +(nil) + +print-size +---- +0 + +# More than one intersection in a full queue. +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +add-compaction +L5: f-g 2 +---- + +add-compaction +L5: k-m 3 +---- + +add-compaction +L5: n-w 4 +---- + +add-compaction +L3: y-z 5 +---- + +add-compaction +L2: a-z 6 +---- + +print-queue +---- +L2: a-z 6 + +print-size +---- +1 + +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +add-compaction +L5: f-g 2 +---- + +add-compaction +L5: k-m 3 +---- + +add-compaction +L5: n-w 4 +---- + +add-compaction +L3: y-z 5 +---- + +# Test multiple overlap which doesn't cover the entire full queue. + +add-compaction +L2: o-y 6 +---- + +print-queue +---- +L5: a-e 1 +L5: f-g 2 +L5: k-m 3 +L2: o-y 6 + +remove-compaction +---- +L5: a-e 1 + +remove-compaction +---- +L5: f-g 2 + +remove-compaction +---- +L5: k-m 3 + +remove-compaction +---- +L2: o-y 6 + +remove-compaction +---- +(nil) + +# Test a queue which becomes full, then empty, then fills up again. +create +---- +(success) + +add-compaction +L5: a-e 1 +---- + +add-compaction +L5: f-g 2 +---- + +add-compaction +L5: k-m 3 +---- + +add-compaction +L5: n-w 4 +---- + +add-compaction +L3: y-z 5 +---- + +add-compaction +L2: o-y 6 +---- + +print-queue +---- +L5: a-e 1 +L5: f-g 2 +L5: k-m 3 +L2: o-y 6 + +remove-compaction +---- +L5: a-e 1 + +remove-compaction +---- +L5: f-g 2 + +remove-compaction +---- +L5: k-m 3 + +remove-compaction +---- +L2: o-y 6 + +remove-compaction +---- +(nil) + +add-compaction +L5: a-e 1 +---- + +add-compaction +L5: f-g 2 +---- + +add-compaction +L5: k-m 3 +---- + +add-compaction +L5: n-w 4 +---- + +print-queue +---- +L5: a-e 1 +L5: f-g 2 +L5: k-m 3 +L5: n-w 4 + +print-size +---- +4 + +# Test overlap once we refill the queue. +add-compaction +L4: b-l 5 +---- + +print-queue +---- +L5: n-w 4 +L4: b-l 5 + +print-size +---- +2 diff --git a/pebble/testdata/rocksdb-ingest-only/000003.log b/pebble/testdata/rocksdb-ingest-only/000003.log new file mode 100644 index 0000000..e69de29 diff --git a/pebble/testdata/rocksdb-ingest-only/000006.sst b/pebble/testdata/rocksdb-ingest-only/000006.sst new file mode 100644 index 0000000000000000000000000000000000000000..1b98083a748c8433ef5b7a155c4a2357253cba22 GIT binary patch literal 1187 zcmaJ=O>Z1U5Ut*9yuF+CcMQmiqCI#eP)N&3ER1AXGRF1+2^2vj4hXG!W@>la?&%(P z_w4RO2qgz3IKmAfenQTiIdeep6S(jPKsiG7?3!FiTAGildR<-hUiEM61bSSamwJZE z=E=FLrqE_xw$WzoP6o=1&;KFbd=cGs?86_Qy$d&Z>as)xFLF7H zAqsA^8(f*W0ei61bT+;4gi191h(vnKM39L_p-hG|5vl?se0z-*4u@(chDrx?MAE(O zO*S1{f?iw*I2Z;*)@b106gPz6->ca>3_utt+==nf7aa0}d9`vZwL8j6$ zNUDH7dLH|b4WgS+%DnKSiCtz9((W>cGM*%Y$`N?Gk;x#1SC8>2Yu{j~k>(+`>4D$o zI3og#)C2FsfapLm(l&`UUK+a~7q(l?3A)%12@HIBdrq7vtX64t#rttdbSiVKt=dm9(>waVYB(6LZcalF|?PNbu?CDkr((Do2wKBP+pWCU91vT<~eSyyv8O{mn5ba z8EWbYO=>fmZN9>CbR!hDKqpP+Ve?vxlrg2&MR0tTnKBnO4fSYv)@?B~!723DyQ_@$ zjPNey#9ASs4OVA#Z|Y%~)3|kWF)~D$lPP5vM&|f`;&;|hVr)4Hh0>UXT$Sx^$qH=|hfjd^~!)rbDI zA+^8I>E#MNWv8(5-_0qgGOC<-`I9|ciV5$~6E%mQzx-jJea|2M!d{Q&t5@9d{MWy} K`suwtzWx{1Yho1u literal 0 HcmV?d00001 diff --git a/pebble/testdata/rocksdb-ingest-only/CURRENT b/pebble/testdata/rocksdb-ingest-only/CURRENT new file mode 100644 index 0000000..875cf23 --- /dev/null +++ b/pebble/testdata/rocksdb-ingest-only/CURRENT @@ -0,0 +1 @@ +MANIFEST-000007 diff --git a/pebble/testdata/rocksdb-ingest-only/IDENTITY b/pebble/testdata/rocksdb-ingest-only/IDENTITY new file mode 100644 index 0000000..b11f8d2 --- /dev/null +++ b/pebble/testdata/rocksdb-ingest-only/IDENTITY @@ -0,0 +1 @@ +160040b17ab41758-6d6a810cd585edc2 \ No newline at end of file diff --git a/pebble/testdata/rocksdb-ingest-only/LOCK b/pebble/testdata/rocksdb-ingest-only/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/pebble/testdata/rocksdb-ingest-only/MANIFEST-000001 b/pebble/testdata/rocksdb-ingest-only/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..d89025ae6eb479dde681d12e61bc276d185fa292 GIT binary patch literal 13 UcmX?daDFr!10xdyGZPB~03Fi;k^lez literal 0 HcmV?d00001 diff --git a/pebble/testdata/rocksdb-ingest-only/MANIFEST-000007 b/pebble/testdata/rocksdb-ingest-only/MANIFEST-000007 new file mode 100644 index 0000000000000000000000000000000000000000..4b500e23263c02e9880f569bf307c8a5547ede75 GIT binary patch literal 135 zcmbR2ea2mkfss)pIX^kOC_gbdBR)Alw;-`7u_V7}#`=h5Obm=n40~%jGuapzIT@Ha zSQwTu)h+dfaZ=dW7IP{jW|ihtq-N&lCS_$-mNP&AqYx(}lqpw{lbcvlT#%lXTU3fE H&ASET=>DEL=>SET=>SINGLEDEL. +define target-file-sizes=(1, 1, 1, 1, 1) +L1 + a.SINGLEDEL.10: +L2 + a.SET.9:v3 +L3 + a.DEL.8: +L4 + a.SET.7:v2 +L5 + a.SET.6:v1 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +2: + 000005:[a#9,SET-a#9,SET] +3: + 000006:[a#8,DEL-a#8,DEL] +4: + 000007:[a#7,SET-a#7,SET] +5: + 000008:[a#6,SET-a#6,SET] + +# No data. +iter +first +---- +. + +# Compact away the DEL. +compact a-b L2 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000009:[a#9,SET-a#9,SET] +4: + 000007:[a#7,SET-a#7,SET] +5: + 000008:[a#6,SET-a#6,SET] + +# No data. +iter +first +---- +. + +# Do two compactions to compact away the SINGLEDEL and 1 SET. +compact a-b L1 +---- +2: + 000010:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000009:[a#9,SET-a#9,SET] +4: + 000007:[a#7,SET-a#7,SET] +5: + 000008:[a#6,SET-a#6,SET] + +compact a-b L2 +---- +4: + 000007:[a#7,SET-a#7,SET] +5: + 000008:[a#6,SET-a#6,SET] + +# Deleted data reappears. +iter +first +---- +a: (v2, .) diff --git a/pebble/testdata/singledel_manual_compaction_set_with_del b/pebble/testdata/singledel_manual_compaction_set_with_del new file mode 100644 index 0000000..afeff65 --- /dev/null +++ b/pebble/testdata/singledel_manual_compaction_set_with_del @@ -0,0 +1,282 @@ +# This is not actually a manual compaction test, and simply uses manual +# compaction to demonstrate single delete semantics when used with +# set-with-delete. + +# Define a sequence of SET=>SET=>DEL=>SET=>SINGLEDEL. +define target-file-sizes=(1, 1, 1, 1, 1) +L1 + a.SINGLEDEL.10: +L2 + a.SET.9:v3 +L3 + a.DEL.8: +L4 + a.SET.7:v2 +L5 + a.SET.6:v1 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +2: + 000005:[a#9,SET-a#9,SET] +3: + 000006:[a#8,DEL-a#8,DEL] +4: + 000007:[a#7,SET-a#7,SET] +5: + 000008:[a#6,SET-a#6,SET] + +# No data. +iter +first +---- +. + +# Compact away the DEL. +compact a-b L2 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000009:[a#9,SETWITHDEL-a#9,SETWITHDEL] +4: + 000007:[a#7,SET-a#7,SET] +5: + 000008:[a#6,SET-a#6,SET] + +# No data. +iter +first +---- +. + +# Do two compactions to compact away the SINGLEDEL and 1 SET. +compact a-b L1 +---- +2: + 000010:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000009:[a#9,SETWITHDEL-a#9,SETWITHDEL] +4: + 000007:[a#7,SET-a#7,SET] +5: + 000008:[a#6,SET-a#6,SET] + +compact a-b L2 +---- +3: + 000011:[a#10,DEL-a#10,DEL] +4: + 000007:[a#7,SET-a#7,SET] +5: + 000008:[a#6,SET-a#6,SET] + +# Deleted data is not resurrected. +iter +first +---- +. + +# Define a sequence of SET=>SINGLEDEL=>SET=>SINGLEDEL. +define target-file-sizes=(1, 1, 1, 1, 1) +L1 + a.SINGLEDEL.10: +L2 + a.SET.9:v3 +L3 + a.SINGLEDEL.8: +L4 + a.SET.7:v2 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +2: + 000005:[a#9,SET-a#9,SET] +3: + 000006:[a#8,SINGLEDEL-a#8,SINGLEDEL] +4: + 000007:[a#7,SET-a#7,SET] + +# No data. +iter +first +---- +. + +# Compact away the older SINGLEDEL. +compact a-b L2 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000008:[a#9,SETWITHDEL-a#9,SETWITHDEL] +4: + 000007:[a#7,SET-a#7,SET] + +# No data. +iter +first +---- +. + +# Do two compactions to compact away the newer SINGLEDEL and 1 SET. +compact a-b L1 +---- +2: + 000009:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000008:[a#9,SETWITHDEL-a#9,SETWITHDEL] +4: + 000007:[a#7,SET-a#7,SET] + +compact a-b L2 +---- +3: + 000010:[a#10,DEL-a#10,DEL] +4: + 000007:[a#7,SET-a#7,SET] + +# Deleted data is not resurrected. +iter +first +---- +. + +# Define a sequence of SET=>DEL=>SET=>SINGLEDEL, such that the DEL and +# SINGLEDEL meet in a compaction. Disable multilevel compaction to exercise the proper test case. +define snapshots=(9) disable-multi-level +L1 + a.SINGLEDEL.10: +L2 + a.SET.9:v3 +L3 + a.DEL.8: +L4 + a.SET.7:v2 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +2: + 000005:[a#9,SET-a#9,SET] +3: + 000006:[a#8,DEL-a#8,DEL] +4: + 000007:[a#7,SET-a#7,SET] + +# No data. +iter +first +---- +. + +# Compact L2 and L3. The snapshot prevents the DEL=>SET from being collapsed. +compact a-b L2 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000008:[a#9,SET-a#8,DEL] +4: + 000007:[a#7,SET-a#7,SET] + +# No data. +iter +first +---- +. + +close-snapshots +---- + +compact a-b L1 +---- +2: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000008:[a#9,SET-a#8,DEL] +4: + 000007:[a#7,SET-a#7,SET] + +# The DEL survives. +compact a-b L2 +---- +3: + 000009:[a#8,DEL-a#8,DEL] +4: + 000007:[a#7,SET-a#7,SET] + +# No data +iter +first +---- +. + +# Define a sequence of SET=>SINGLEDEL=>SET=>SINGLEDEL, such that the two +# SINGLEDELs meet in a compaction. +# To test surface the right test case, disable multi level compaction. +define snapshots=(9) disable-multi-level +L1 + a.SINGLEDEL.10: +L2 + a.SET.9:v3 +L3 + a.SINGLEDEL.8: +L4 + a.SET.7:v2 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +2: + 000005:[a#9,SET-a#9,SET] +3: + 000006:[a#8,SINGLEDEL-a#8,SINGLEDEL] +4: + 000007:[a#7,SET-a#7,SET] + +# No data. +iter +first +---- +. + +# Compact L2 and L3. The snapshot prevents the SINGLEDEL=>SET from being collapsed. +compact a-b L2 +---- +1: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000008:[a#9,SET-a#8,SINGLEDEL] +4: + 000007:[a#7,SET-a#7,SET] + +# No data. +iter +first +---- +. + +close-snapshots +---- + +compact a-b L1 +---- +2: + 000004:[a#10,SINGLEDEL-a#10,SINGLEDEL] +3: + 000008:[a#9,SET-a#8,SINGLEDEL] +4: + 000007:[a#7,SET-a#7,SET] + +# The SINGLEDEL survives. +compact a-b L2 +---- +3: + 000009:[a#8,SINGLEDEL-a#8,SINGLEDEL] +4: + 000007:[a#7,SET-a#7,SET] + +# No data +iter +first +---- +. diff --git a/pebble/testdata/snapshot b/pebble/testdata/snapshot new file mode 100644 index 0000000..9399812 --- /dev/null +++ b/pebble/testdata/snapshot @@ -0,0 +1,180 @@ +define +set a 1 +snapshot 1 +set b 2 +snapshot 2 +set c 3 +snapshot 3 +---- + +iter snapshot=1 +first +next +prev +---- +a:1 +. +a:1 + +iter snapshot=2 +first +next +next +prev +---- +a:1 +b:2 +. +b:2 + +iter snapshot=3 +first +next +next +next +prev +---- +a:1 +b:2 +c:3 +. +c:3 + +define +set a 1 +snapshot 1 +set a 2 +snapshot 2 +set a 3 +snapshot 3 +---- + +iter snapshot=1 +first +next +prev +---- +a:1 +. +a:1 + +iter snapshot=2 +first +next +prev +---- +a:2 +. +a:2 + +iter snapshot=3 +first +next +prev +---- +a:3 +. +a:3 + +define +set a 1 +snapshot 1 +set a 2 +snapshot 2 +set a 3 +snapshot 3 +compact a-b +---- + +iter snapshot=1 +first +next +prev +---- +a:1 +. +a:1 + +iter snapshot=2 +first +next +prev +---- +a:2 +. +a:2 + +iter snapshot=3 +first +next +prev +---- +a:3 +. +a:3 + +define +merge a 1 +snapshot 1 +merge a 2 +snapshot 2 +merge a 3 +snapshot 3 +compact a-b +---- + +iter snapshot=1 +first +next +prev +---- +a:1 +. +a:1 + +iter snapshot=2 +first +next +prev +---- +a:12 +. +a:12 + +iter snapshot=3 +first +next +prev +---- +a:123 +. +a:123 + +# Fix for #2705. levelIter encounters two files where the first has seqnum +# below snapshot seqnum, so obsolete points can be hidden. The second file has +# a more recent seqnum so obsolete points cannot be hidden. But the in-place +# modification of the filters slice was causing obsolete points to be hidden +# in the second file. +define block-size=1 +set a 1 +compact a-b +set c 2 +snapshot 1 +set c 3 +compact c-d +---- + +db-state +---- +6: + 000005:[a#10,SET-a#10,SET] + 000007:[c#12,SET-c#11,SET] + +iter snapshot=1 +first +next +next +---- +a:1 +c:2 +. diff --git a/pebble/testdata/sstable_key_compare b/pebble/testdata/sstable_key_compare new file mode 100644 index 0000000..cc06afd --- /dev/null +++ b/pebble/testdata/sstable_key_compare @@ -0,0 +1,96 @@ +cmp +a.SET.4 a.SET.4 +a.SET.4 b.SET.4 +a.SET.4 a.SET.3 +a.SET.3 a.SET.4 +cat.SET.3 cat.MERGE.4 +dog.SET.3 cat.MERGE.4 +---- + a#4,SET = a#4,SET + a#4,SET < b#4,SET + a#4,SET = a#3,SET + a#3,SET = a#4,SET + cat#3,SET = cat#4,MERGE + dog#3,SET > cat#4,MERGE + +cmp +a.SET.4 a.RANGEDEL.72057594037927935 +a.RANGEDEL.72057594037927935 a.SET.4 +cat.SET.4 a.RANGEDEL.72057594037927935 +a.SET.4 cat.RANGEDEL.72057594037927935 +cat.RANGEDEL.2 cat.SET.3 +cat.RANGEDEL.2 cat.RANGEDEL.3 +cat.RANGEDEL.2 cat.RANGEDEL.72057594037927935 +---- + a#4,SET > a#inf,RANGEDEL + a#inf,RANGEDEL < a#4,SET + cat#4,SET > a#inf,RANGEDEL + a#4,SET < cat#inf,RANGEDEL + cat#2,RANGEDEL = cat#3,SET + cat#2,RANGEDEL = cat#3,RANGEDEL + cat#2,RANGEDEL > cat#inf,RANGEDEL + +cmp +a.RANGEKEYSET.5 a.SET.3 +a.RANGEKEYSET.5 a.RANGEDEL.3 +a.RANGEKEYSET.5 a.RANGEDEL.72057594037927935 +a.RANGEKEYSET.72057594037927935 a.RANGEDEL.72057594037927935 +a.RANGEKEYSET.72057594037927935 a.RANGEKEYSET.5 +a.RANGEKEYSET.5 a.RANGEKEYSET.72057594037927935 +---- + a#5,RANGEKEYSET = a#3,SET + a#5,RANGEKEYSET = a#3,RANGEDEL + a#5,RANGEKEYSET > a#inf,RANGEDEL + a#inf,RANGEKEYSET = a#inf,RANGEDEL + a#inf,RANGEKEYSET < a#5,RANGEKEYSET + a#5,RANGEKEYSET > a#inf,RANGEKEYSET + +cmp +a.RANGEKEYUNSET.5 a.RANGEKEYUNSET.72057594037927935 +a.RANGEKEYUNSET.72057594037927935 a.RANGEKEYUNSET.5 +foo.RANGEKEYUNSET.72057594037927935 a.RANGEKEYUNSET.5 +a.SET.5 a.RANGEKEYUNSET.72057594037927935 +a.RANGEKEYUNSET.72057594037927935 a.SET.5 +a.RANGEKEYUNSET.72057594037927935 a.RANGEDEL.72057594037927935 +a.RANGEDEL.72057594037927935 a.RANGEKEYUNSET.72057594037927935 +a.RANGEKEYUNSET.72057594037927935 a.RANGEKEYSET.72057594037927935 +a.RANGEKEYSET.72057594037927935 a.RANGEKEYUNSET.72057594037927935 +---- + a#5,RANGEKEYUNSET > a#inf,RANGEKEYUNSET + a#inf,RANGEKEYUNSET < a#5,RANGEKEYUNSET + foo#inf,RANGEKEYUNSET > a#5,RANGEKEYUNSET + a#5,SET > a#inf,RANGEKEYUNSET + a#inf,RANGEKEYUNSET < a#5,SET + a#inf,RANGEKEYUNSET = a#inf,RANGEDEL + a#inf,RANGEDEL = a#inf,RANGEKEYUNSET + a#inf,RANGEKEYUNSET = a#inf,RANGEKEYSET + a#inf,RANGEKEYSET = a#inf,RANGEKEYUNSET + +cmp +a.RANGEKEYDEL.5 a.RANGEKEYDEL.72057594037927935 +a.RANGEKEYDEL.72057594037927935 a.RANGEKEYDEL.5 +foo.RANGEKEYDEL.72057594037927935 a.RANGEKEYDEL.5 +a.SET.5 a.RANGEKEYDEL.72057594037927935 +a.RANGEKEYDEL.72057594037927935 a.SET.5 +a.RANGEKEYDEL.72057594037927935 a.RANGEDEL.72057594037927935 +a.RANGEDEL.72057594037927935 a.RANGEKEYDEL.72057594037927935 +a.RANGEKEYDEL.72057594037927935 a.RANGEKEYSET.72057594037927935 +a.RANGEKEYSET.72057594037927935 a.RANGEKEYDEL.72057594037927935 +a.RANGEKEYUNSET.72057594037927935 a.RANGEKEYDEL.72057594037927935 +a.RANGEKEYDEL.72057594037927935 a.RANGEKEYUNSET.72057594037927935 +a.RANGEKEYDEL.72057594037927935 a.RANGEKEYSET.72057594037927935 +a.RANGEKEYSET.72057594037927935 a.RANGEKEYDEL.72057594037927935 +---- + a#5,RANGEKEYDEL > a#inf,RANGEKEYDEL + a#inf,RANGEKEYDEL < a#5,RANGEKEYDEL + foo#inf,RANGEKEYDEL > a#5,RANGEKEYDEL + a#5,SET > a#inf,RANGEKEYDEL + a#inf,RANGEKEYDEL < a#5,SET + a#inf,RANGEKEYDEL = a#inf,RANGEDEL + a#inf,RANGEDEL = a#inf,RANGEKEYDEL + a#inf,RANGEKEYDEL = a#inf,RANGEKEYSET + a#inf,RANGEKEYSET = a#inf,RANGEKEYDEL + a#inf,RANGEKEYUNSET = a#inf,RANGEKEYDEL + a#inf,RANGEKEYDEL = a#inf,RANGEKEYUNSET + a#inf,RANGEKEYDEL = a#inf,RANGEKEYSET + a#inf,RANGEKEYSET = a#inf,RANGEKEYDEL diff --git a/pebble/testdata/table_stats b/pebble/testdata/table_stats new file mode 100644 index 0000000..a14ef69 --- /dev/null +++ b/pebble/testdata/table_stats @@ -0,0 +1,911 @@ +batch +set a 1 +set b 2 +del c +---- + +flush +---- +0.0: + 000005:[a#10,SET-c#12,DEL] + +wait-pending-table-stats +000005 +---- +num-entries: 3 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 64 +range-deletions-bytes-estimate: 0 + +compact a-c +---- +6: + 000005:[a#10,SET-c#12,DEL] + +batch +del-range a c +---- + +flush +---- +0.0: + 000007:[a#13,RANGEDEL-c#inf,RANGEDEL] +6: + 000005:[a#10,SET-c#12,DEL] + +wait-pending-table-stats +000007 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 51 + +reopen +---- + +# After re-opening the database, the table stats collector should eventually +# load 000007's stats. + +wait-loaded-initial +---- +[JOB 2] all initial table stats loaded + +wait-pending-table-stats +000007 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 51 + +compact a-c +---- + +# Test a file that is moved by a compaction before its table stats are +# collected. The stats collector should silently skip the first pending file, +# but the second entry from the move compaction should cause the file's stats +# to be loaded. + +disable +---- + +batch +set a 1 +set b 2 +---- + +flush +---- +0.0: + 000012:[a#14,SET-b#15,SET] + +compact a-c +---- +6: + 000012:[a#14,SET-b#15,SET] + +enable +---- + +wait-pending-table-stats +000012 +---- +num-entries: 2 +num-deletions: 0 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +# Test a file that is deleted by a compaction before its table stats are +# collected. The stats collector should just silently skip the pending file. + +disable +---- + +batch +del-range a c +---- + +flush +---- +0.0: + 000014:[a#16,RANGEDEL-c#inf,RANGEDEL] +6: + 000012:[a#14,SET-b#15,SET] + +compact a-c +---- + +enable +---- + +wait-pending-table-stats +000014 +---- +(not found) + +# Test range tombstones that need to be truncated to file bounds. The +# grandparent limit and small target file size ensures that our manual +# compaction of L4->L5 will split the range tombstone across several files. + +define target-file-sizes=(100, 1) +L4 + a.RANGEDEL.8:f +L5 + b.SET.7:v +L6 + a.SET.1:v +L6 + b.SET.2:v +L6 + c.SET.3:v +L6 + d.SET.4:v +L6 + e.SET.5:v +---- +4: + 000004:[a#8,RANGEDEL-f#inf,RANGEDEL] +5: + 000005:[b#7,SET-b#7,SET] +6: + 000006:[a#1,SET-a#1,SET] + 000007:[b#2,SET-b#2,SET] + 000008:[c#3,SET-c#3,SET] + 000009:[d#4,SET-d#4,SET] + 000010:[e#5,SET-e#5,SET] + +compact a-b L4 +---- +5: + 000011:[a#8,RANGEDEL-b#inf,RANGEDEL] + 000012:[b#8,RANGEDEL-c#inf,RANGEDEL] + 000013:[c#8,RANGEDEL-d#inf,RANGEDEL] + 000014:[d#8,RANGEDEL-e#inf,RANGEDEL] + 000015:[e#8,RANGEDEL-f#inf,RANGEDEL] +6: + 000006:[a#1,SET-a#1,SET] + 000007:[b#2,SET-b#2,SET] + 000008:[c#3,SET-c#3,SET] + 000009:[d#4,SET-d#4,SET] + 000010:[e#5,SET-e#5,SET] + +wait-pending-table-stats +000011 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 632 + +wait-pending-table-stats +000012 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 632 + +# A table in L6 with two point keys blocks, each covered by distinct range dels. +# The deletion estimate takes into account the contribution from both deleted +# blocks. Note that the snapshot is required to allow the hint to be computed. +define block-size=1 snapshots=(10) +L6 + e.SET.5:e a.RANGEDEL.15:f m.SET.5:m g.RANGEDEL.15:z +---- +6: + 000004:[a#15,RANGEDEL-z#inf,RANGEDEL] + +wait-pending-table-stats +000004 +---- +num-entries: 4 +num-deletions: 2 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 68 + +# Hints that partially overlap tables in lower levels only count blocks that are +# contained within the hint. +# +# L0 |-| 000004: a.RANGEDEL:b +# L1 |---| 000005: d.RANGEDEL:f +# L2 x x 000006: Two blocks [a, d] +# L2 x x 000007: Two blocks [e, h] +# ------------------- +# a b c d e f g h + +define block-size=1 +L0 + a.RANGEDEL.2:b +L1 + d.RANGEDEL.1:f +L2 + a.SET.0:a d.SET.0:d +L2 + e.SET.0:e h.SET.0:h +---- +0.0: + 000004:[a#2,RANGEDEL-b#inf,RANGEDEL] +1: + 000005:[d#1,RANGEDEL-f#inf,RANGEDEL] +2: + 000006:[a#0,SET-d#0,SET] + 000007:[e#0,SET-h#0,SET] + +# Table 000004 deletes the first block in table 000006. +wait-pending-table-stats +000004 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 33 + +# Table 000005 deletes the second block in table 000006 (containing 'd') and the +# first block in table 000007 (containing 'e'). +wait-pending-table-stats +000005 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 66 + +# Test the interaction between point and range key deletions. + +define +---- + +# Start with a table that contains point and range keys, but no range dels or +# range key dels. +batch +set a a +range-key-set a b @1 foo +range-key-unset a b @2 +---- + +flush +---- +0.0: + 000005:[a#12,RANGEKEYUNSET-b#inf,RANGEKEYSET] + +# Add a table that contains only point keys, to the right of the existing table. +batch +set c c +---- + +flush +---- +0.0: + 000005:[a#12,RANGEKEYUNSET-b#inf,RANGEKEYSET] + 000007:[c#13,SET-c#13,SET] + +compact a-c +---- +6: + 000008:[a#11,RANGEKEYSET-b#inf,RANGEKEYSET] + 000009:[c#0,SET-c#0,SET] + +# Add a table that contains a RANGEKEYDEL covering the first table in L6. +batch +range-key-del a b +---- + +flush +---- +0.0: + 000011:[a#14,RANGEKEYDEL-b#inf,RANGEKEYDEL] +6: + 000008:[a#11,RANGEKEYSET-b#inf,RANGEKEYSET] + 000009:[c#0,SET-c#0,SET] + +# Add one more table containing a RANGEDEL. +batch +del-range a c +---- + +flush +---- +0.1: + 000013:[a#15,RANGEDEL-c#inf,RANGEDEL] +0.0: + 000011:[a#14,RANGEKEYDEL-b#inf,RANGEKEYDEL] +6: + 000008:[a#11,RANGEKEYSET-b#inf,RANGEKEYSET] + 000009:[c#0,SET-c#0,SET] + +# Compute stats on the table containing range key del. It should not show an +# estimate for deleted point keys as there are no tables below it that contain +# only range keys. +wait-pending-table-stats +000011 +---- +num-entries: 0 +num-deletions: 0 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +# Compute stats on the table containing the range del. It should show an +# estimate for deleted point keys, as a table below it (000008) contains point +# keys. Note that even though table 000008 contains range keys, the range del +# estimates are non-zero, as this number is agnostic of range keys. +wait-pending-table-stats +000013 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 778 + +# Drop a range del and a range key del over the entire keyspace. This table can +# delete everything underneath it. +ingest ext1 +del-range a z +range-key-del a z +---- +0.2: + 000014:[a#16,RANGEKEYDEL-z#inf,RANGEDEL] +0.1: + 000013:[a#15,RANGEDEL-c#inf,RANGEDEL] +0.0: + 000011:[a#14,RANGEKEYDEL-b#inf,RANGEKEYDEL] +6: + 000008:[a#11,RANGEKEYSET-b#inf,RANGEKEYSET] + 000009:[c#0,SET-c#0,SET] + +compact a-z +---- + +# Ingest another sstable with range tombstones again, but this time into an +# empty LSM. The table should ingest into L6. Its table stats should reflect +# that its range tombstones cannot delete any of the data contained within the +# file itself. +ingest ext1 +del-range a z +range-key-del a z +set d d +set e e +set f f +---- +6: + 000015:[a#17,RANGEKEYDEL-z#inf,RANGEDEL] + +wait-pending-table-stats +000015 +---- +num-entries: 4 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +# A hint for exclusively range key deletions that covers a table with point keys +# should not contain an estimate for point keys. + +define +---- + +# A table with point keys. +batch +set b b +---- + +flush +---- +0.0: + 000005:[b#10,SET-b#10,SET] + +# A table with a mixture of point and range keys. +batch +set c c +range-key-set d d @1 foo +---- + +flush +---- +0.0: + 000005:[b#10,SET-b#10,SET] + 000007:[c#11,SET-c#11,SET] + +compact a-z +---- +6: + 000008:[b#0,SET-b#0,SET] + 000009:[c#0,SET-c#0,SET] + +# The table with the range key del, that spans the previous two tables. +batch +range-key-del a z +---- + +flush +---- +0.0: + 000011:[a#13,RANGEKEYDEL-z#inf,RANGEKEYDEL] +6: + 000008:[b#0,SET-b#0,SET] + 000009:[c#0,SET-c#0,SET] + +# The hint on table 000011 does estimates zero size for range deleted point +# keys. +wait-pending-table-stats +000011 +---- +num-entries: 0 +num-deletions: 0 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +# A hint from a range del that covers a table with only range keys should not +# contain an estimate for the range keys. + +define +L4 + a.RANGEDEL.4:c +L5 + a.RANGEDEL.2:e + b.SET.3:b +L6 + rangekey:c-d:{(#1,RANGEKEYSET,@1,foo)} +---- +4: + 000004:[a#4,RANGEDEL-c#inf,RANGEDEL] +5: + 000005:[a#2,RANGEDEL-e#inf,RANGEDEL] +6: + 000006:[c#1,RANGEKEYSET-d#inf,RANGEKEYSET] + +# The table in L5 should not contain an estimate for the table below it, which +# contains only range keys. +wait-pending-table-stats +000005 +---- +num-entries: 2 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +# The table in L4 can delete the table in L5, which contains point keys. The +# estimate is only partial, as the range del does not fully overlap the table. +wait-pending-table-stats +000004 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 26 + +# Test point tombstone compensation that uses DELSIZED keys. + +define format-major-version=15 +L6 + bar.SET.0: + bax.SET.0: + foo.SET.0: + moo.SET.0: +---- +6: + 000004:[bar#0,SET-moo#0,SET] + +batch +set a apple +set b banana +set c coconut +del-sized foo 100000 +del moo +---- + +flush +---- +0.0: + 000006:[a#10,SET-moo#14,DEL] +6: + 000004:[bar#0,SET-moo#0,SET] + +metric keys.missized-tombstones-count +---- +keys.missized-tombstones-count: 0 + +# The foo DELSIZED tombstone should cause the +# `pebble.raw.point-tombstone.value.size` property to be 100000 + len(foo) = +# 100003. + +properties file=000006 +num.deletions +deleted.keys +raw.point-tombstone +---- +num.deletions: + pebble.num.deletions.sized: 1 +deleted.keys: + rocksdb.deleted.keys: 2 +raw.point-tombstone: + pebble.raw.point-tombstone.key.size: 6 + pebble.raw.point-tombstone.value.size: 100003 + +# And the size hint should then appear in the point-deletions-bytes-estimate, +# scaled according to the computed 'compression ratio'. + +wait-pending-table-stats +000006 +---- +num-entries: 5 +num-deletions: 2 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 112732 +range-deletions-bytes-estimate: 0 + +# Try a missized point tombstone. It should appear in the Metrics after the +# flush that will elide the a.SET. + +batch +set a boop +del-sized a 10000 +---- + +flush +---- +0.1: + 000008:[a#16,DEL-a#16,DEL] +0.0: + 000006:[a#10,SET-moo#14,DEL] +6: + 000004:[bar#0,SET-moo#0,SET] + +metric keys.missized-tombstones-count +---- +keys.missized-tombstones-count: 1 + +# Virtual sstables tests. Note that these tests are just for sanity checking +# purposes. Small sstables lead to inaccurate values during extrapolation. +define format-major-version=16 +---- + +batch +set a 1 +set b 2 +del d +---- + +flush +---- +0.0: + 000005:[a#10,SET-d#12,DEL] + +metadata-stats file=5 +---- +size: 726 + +# Just grab the physical sstable properties as these are used to construct the +# virtual sstable properties. +properties file=5 +rocksdb +pebble +---- +rocksdb: + rocksdb.num.entries: 3 + rocksdb.raw.key.size: 27 + rocksdb.raw.value.size: 2 + rocksdb.deleted.keys: 1 + rocksdb.num.range-deletions: 0 + rocksdb.comparator: pebble.internal.testkeys + rocksdb.compression: Snappy + rocksdb.compression_options: window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; + rocksdb.data.size: 53 + rocksdb.filter.size: 0 + rocksdb.index.size: 27 + rocksdb.block.based.table.index.type: 0 + rocksdb.merge.operator: pebble.concatenate + rocksdb.num.data.blocks: 1 + rocksdb.merge.operands: 0 + rocksdb.prefix.extractor.name: nullptr + rocksdb.block.based.table.prefix.filtering: false + rocksdb.property.collectors: [obsolete-key] + rocksdb.block.based.table.whole.key.filtering: false +pebble: + pebble.raw.point-tombstone.key.size: 1 + rocksdb.comparator: pebble.internal.testkeys + rocksdb.merge.operator: pebble.concatenate + +build ext1 +set f f +---- + +ingest-and-excise ext1 excise=b-c +---- + +lsm +---- +0.0: + 000007:[a#10,SET-a#10,SET] + 000008:[d#12,DEL-d#12,DEL] +6: + 000006:[f#13,SET-f#13,SET] + +metadata-stats file=7 +---- +size: 53 + +metadata-stats file=8 +---- +size: 53 + +# Note that the backing file size is much larger than the virtual file sizes. +# For tiny sstables, the metadata contained in the sstable is much larger than +# the actual sizes. + +# While sstable 8 has no point tombstones, because of the nature of extrapolation +# both file 7 and file 8 will have a point tombstone key size property. Because +# of this both the files have a point deletion bytes estimate. +properties file=7 +---- +rocksdb.num.entries: 1 +rocksdb.raw.key.size: 2 +rocksdb.raw.value.size: 1 +pebble.raw.point-tombstone.key.size: 1 +rocksdb.deleted.keys: 1 + +properties file=8 +---- +rocksdb.num.entries: 1 +rocksdb.raw.key.size: 2 +rocksdb.raw.value.size: 1 +pebble.raw.point-tombstone.key.size: 1 +rocksdb.deleted.keys: 1 + +wait-pending-table-stats +000007 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 53 +range-deletions-bytes-estimate: 0 + +wait-pending-table-stats +000008 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 53 +range-deletions-bytes-estimate: 0 + +# Create an sstable with a range key set. +batch +set a a +set b b +set d d +range-key-set e ee @1 foo +---- + +flush +---- +0.1: + 000010:[a#14,SET-ee#inf,RANGEKEYSET] +0.0: + 000007:[a#10,SET-a#10,SET] + 000008:[d#12,DEL-d#12,DEL] +6: + 000006:[f#13,SET-f#13,SET] + +properties file=10 +rocksdb +pebble +---- +rocksdb: + rocksdb.num.entries: 3 + rocksdb.raw.key.size: 27 + rocksdb.raw.value.size: 3 + rocksdb.deleted.keys: 0 + rocksdb.num.range-deletions: 0 + rocksdb.comparator: pebble.internal.testkeys + rocksdb.compression: Snappy + rocksdb.compression_options: window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; + rocksdb.data.size: 47 + rocksdb.filter.size: 0 + rocksdb.index.size: 27 + rocksdb.block.based.table.index.type: 0 + rocksdb.merge.operator: pebble.concatenate + rocksdb.num.data.blocks: 1 + rocksdb.merge.operands: 0 + rocksdb.prefix.extractor.name: nullptr + rocksdb.block.based.table.prefix.filtering: false + rocksdb.property.collectors: [obsolete-key] + rocksdb.block.based.table.whole.key.filtering: false +pebble: + pebble.num.range-key-dels: 0 + pebble.num.range-key-sets: 1 + rocksdb.comparator: pebble.internal.testkeys + rocksdb.merge.operator: pebble.concatenate + pebble.num.range-key-unsets: 0 + pebble.raw.range-key.key.size: 9 + pebble.raw.range-key.value.size: 10 + +metadata-stats file=10 +---- +size: 828 + +build ext2 +set z z +---- + +ingest-and-excise ext2 excise=b-c +---- + +lsm +---- +0.1: + 000012:[a#14,SET-a#14,SET] + 000013:[d#16,SET-ee#inf,RANGEKEYSET] +0.0: + 000007:[a#10,SET-a#10,SET] + 000008:[d#12,DEL-d#12,DEL] +6: + 000006:[f#13,SET-f#13,SET] + 000011:[z#18,SET-z#18,SET] + +metadata-stats file=12 +---- +size: 47 + +metadata-stats file=13 +---- +size: 47 + +# range key sets shows up for both files. This is expected. +properties file=12 +---- +rocksdb.num.entries: 1 +rocksdb.raw.key.size: 2 +rocksdb.raw.value.size: 1 +pebble.num.range-key-sets: 1 + +properties file=13 +---- +rocksdb.num.entries: 1 +rocksdb.raw.key.size: 2 +rocksdb.raw.value.size: 1 +pebble.num.range-key-sets: 1 + +wait-pending-table-stats +000012 +---- +num-entries: 1 +num-deletions: 0 +num-range-key-sets: 1 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +wait-pending-table-stats +000013 +---- +num-entries: 1 +num-deletions: 0 +num-range-key-sets: 1 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 0 + +# Create an sstable with range deletes to view the range delete byte estimates. + +# Compact everything to L6. Range deletion bytes estimate doesn't account for +# bytes in L0. +compact a-z +---- +6: + 000014:[a#0,SET-a#0,SET] + 000015:[d#0,SETWITHDEL-d#0,SETWITHDEL] + 000016:[e#17,RANGEKEYSET-ee#inf,RANGEKEYSET] + 000006:[f#13,SET-f#13,SET] + 000011:[z#18,SET-z#18,SET] + +batch +del-range a e +---- + +flush +---- +0.0: + 000018:[a#19,RANGEDEL-e#inf,RANGEDEL] +6: + 000014:[a#0,SET-a#0,SET] + 000015:[d#0,SETWITHDEL-d#0,SETWITHDEL] + 000016:[e#17,RANGEKEYSET-ee#inf,RANGEKEYSET] + 000006:[f#13,SET-f#13,SET] + 000011:[z#18,SET-z#18,SET] + +properties file=18 +rocksdb +pebble +---- +rocksdb: + rocksdb.num.entries: 1 + rocksdb.raw.key.size: 9 + rocksdb.raw.value.size: 1 + rocksdb.deleted.keys: 1 + rocksdb.num.range-deletions: 1 + rocksdb.comparator: pebble.internal.testkeys + rocksdb.compression: Snappy + rocksdb.compression_options: window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; + rocksdb.data.size: 13 + rocksdb.filter.size: 0 + rocksdb.index.size: 29 + rocksdb.block.based.table.index.type: 0 + rocksdb.merge.operator: pebble.concatenate + rocksdb.num.data.blocks: 1 + rocksdb.merge.operands: 0 + rocksdb.prefix.extractor.name: nullptr + rocksdb.block.based.table.prefix.filtering: false + rocksdb.property.collectors: [obsolete-key] + rocksdb.block.based.table.whole.key.filtering: false +pebble: + rocksdb.comparator: pebble.internal.testkeys + rocksdb.merge.operator: pebble.concatenate + +build ext3 +set zz zz +---- + +ingest-and-excise ext3 excise=b-c +---- + +lsm +---- +0.0: + 000020:[a#19,RANGEDEL-b#inf,RANGEDEL] + 000021:[c#19,RANGEDEL-e#inf,RANGEDEL] +6: + 000014:[a#0,SET-a#0,SET] + 000015:[d#0,SETWITHDEL-d#0,SETWITHDEL] + 000016:[e#17,RANGEKEYSET-ee#inf,RANGEKEYSET] + 000006:[f#13,SET-f#13,SET] + 000011:[z#18,SET-z#18,SET] + 000019:[zz#20,SET-zz#20,SET] + +properties file=20 +---- +rocksdb.num.entries: 1 +rocksdb.raw.key.size: 1 +rocksdb.raw.value.size: 1 +rocksdb.deleted.keys: 1 +rocksdb.num.range-deletions: 1 + +properties file=21 +---- +rocksdb.num.entries: 1 +rocksdb.raw.key.size: 1 +rocksdb.raw.value.size: 1 +rocksdb.deleted.keys: 1 +rocksdb.num.range-deletions: 1 + +wait-pending-table-stats +000020 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 661 + +wait-pending-table-stats +000021 +---- +num-entries: 1 +num-deletions: 1 +num-range-key-sets: 0 +point-deletions-bytes-estimate: 0 +range-deletions-bytes-estimate: 660 diff --git a/pebble/testdata/table_stats_deletion_iter b/pebble/testdata/table_stats_deletion_iter new file mode 100644 index 0000000..d1cbba4 --- /dev/null +++ b/pebble/testdata/table_stats_deletion_iter @@ -0,0 +1,199 @@ +# The example used in the documentation for tableRangedDeletionIter. An iterator +# over the following table: +# +# +# |---------| |---------| |-------| RANGEKEYDELs +# |-----------|-------------| |-----| RANGEDELs +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z +# +# yields the following spans: +# +# 1 3 1 3 2 1 3 2 +# |-----|---------|-----|---|-----| |---|-|-----| +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z +# +# where: +# - 1: homogenous span with range dels only +# - 2: homogenous span with range key dels only +# - 3: heterogeneous span with range dels and range key dels + +build +a-g:{(#0,RANGEDEL)} +g-n:{(#0,RANGEDEL)} +t-w:{(#0,RANGEDEL)} +d-i:{(#0,RANGEKEYDEL)} +l-q:{(#0,RANGEKEYDEL)} +v-z:{(#0,RANGEKEYDEL)} +---- +000000:[a#0,RANGEDEL-z#inf,RANGEKEYDEL] + +spans +---- +a-d:{(#0,RANGEDEL)} +d-i:{(#0,RANGEKEYDEL) (#0,RANGEDEL)} +i-l:{(#0,RANGEDEL)} +l-n:{(#0,RANGEKEYDEL) (#0,RANGEDEL)} +n-q:{(#0,RANGEKEYDEL)} +t-v:{(#0,RANGEDEL)} +v-w:{(#0,RANGEKEYDEL) (#0,RANGEDEL)} +w-z:{(#0,RANGEKEYDEL)} + +# The iterator elides anything other than range deletes and range key deletes. + +# |---------------RANGEKEYUNSET----------------------| +# |---------------RANGEKEYSET------------------------| +# |---------------RANGEKEYDEL------------------------| +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z +# +# Becomes: +# |---------------RANGEKEYDEL------------------------| +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z + +build +a-z:{(#0,RANGEKEYDEL) (#0,RANGEKEYSET,@1,foo) (#0,RANGEKEYUNSET,@2)} +---- +000000:[a#0,RANGEKEYSET-z#inf,RANGEKEYDEL] + +spans +---- +a-z:{(#0,RANGEKEYDEL)} + +# The same as the above, except that the RANGEKEYDEL is at a lower sequence +# number than the SET and UNSET. The three keys exist at different sequence +# numbers, with the DEL at the bottom to avoid the perceived affects of +# shadowing. + +build +a-z:{(#3,RANGEKEYSET,@1,foo) (#2,RANGEKEYUNSET,@2) (#1,RANGEKEYDEL)} +---- +000000:[a#3,RANGEKEYSET-z#inf,RANGEKEYDEL] + +spans +---- +a-z:{(#1,RANGEKEYDEL)} + +# |-RANGEKEYSET-|-RANGEKEYUNSET-|-RANGEKEYDEL--------| +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z +# +# Becomes: +# |-RANGEKEYDEL-------| +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z + +build +a-h:{(#0,RANGEKEYSET,@1,foo)} +h-p:{(#0,RANGEKEYUNSET,@1)} +p-z:{(#0,RANGEKEYDEL)} +---- +000000:[a#0,RANGEKEYSET-z#inf,RANGEKEYDEL] + +spans +---- +p-z:{(#0,RANGEKEYDEL)} + +# |-RANGEKEYSET-|-RANGEKEYUNSET-|-RANGEKEYDEL--------| +# |--RANGEDEL-----------------------------| +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z +# +# Becomes: +# +# |---------------1-------------|----3----|----2----| +# __________________________________________________________ +# a b c d e f g h i j k l m n o p q r s t u v w x y z + +build +a-u:{(#0,RANGEDEL)} +a-h:{(#0,RANGEKEYSET,@1,foo)} +h-p:{(#0,RANGEKEYUNSET,@1)} +p-z:{(#0,RANGEKEYDEL)} +---- +000000:[a#0,RANGEKEYSET-z#inf,RANGEKEYDEL] + +spans +---- +a-p:{(#0,RANGEDEL)} +p-u:{(#0,RANGEKEYDEL) (#0,RANGEDEL)} +u-z:{(#0,RANGEKEYDEL)} + +# An iterator over a table with neither range dels or range key dels present +# yields no spans. + +build +a-m:{(#0,RANGEKEYSET,@1,foo)} +m-z:{(#0,RANGEKEYUNSET,@1)} +---- +000000:[a#0,RANGEKEYSET-z#inf,RANGEKEYUNSET] + +spans +---- +(none) + +# Abutting spans that are merged emit only the largest and smallest key. + +build +a-c:{(#4,RANGEDEL) (#3,RANGEDEL) (#1,RANGEDEL)} +c-d:{(#9,RANGEDEL) (#7,RANGEDEL) (#6,RANGEDEL) (#2,RANGEDEL)} +---- +000000:[a#4,RANGEDEL-d#inf,RANGEDEL] + +spans +---- +a-d:{(#9,RANGEDEL) (#1,RANGEDEL)} + +# The same as above, but for range key dels. + +build +a-c:{(#4,RANGEKEYDEL) (#3,RANGEKEYDEL) (#1,RANGEKEYDEL)} +c-d:{(#9,RANGEKEYDEL) (#7,RANGEKEYDEL) (#6,RANGEKEYDEL) (#2,RANGEKEYDEL)} +---- +000000:[a#4,RANGEKEYDEL-d#inf,RANGEKEYDEL] + +spans +---- +a-d:{(#9,RANGEKEYDEL) (#1,RANGEKEYDEL)} + +# The same as above, but for mixed key kinds. The resulting span has four keys, +# given the respective largest / smallest keys from the range del and range key +# del spans are interleaved. + +build +a-c:{(#4,RANGEDEL) (#2,RANGEDEL)} +a-c:{(#3,RANGEKEYDEL) (#1,RANGEKEYDEL)} +---- +000000:[a#4,RANGEDEL-c#inf,RANGEDEL] + +spans +---- +a-c:{(#4,RANGEDEL) (#3,RANGEKEYDEL) (#2,RANGEDEL) (#1,RANGEKEYDEL)} + +# "Hetrogenous spans" (i.e. spans containing both range dels and range key dels) +# are preserved when abutting spans are merged. This tests that the reducing +# function does not "lose information" of the merged spans, effectively +# resulting in the span switching types from a heterogeneous span to a +# homogenous span. + +# 10 |-------|---|---| RANGEDELs +# 5 |-------|---| RANGEKEYDELs +# 3 |-------|---|---| RANGEDELs +# _____________________ +# a b c d e + +build +a-c:{(#10,RANGEDEL) (#3,RANGEDEL)} +c-d:{(#10,RANGEDEL) (#3,RANGEDEL)} +d-e:{(#10,RANGEDEL) (#3,RANGEDEL)} +a-c:{(#5,RANGEKEYDEL)} +c-d:{(#5,RANGEKEYDEL)} +---- +000000:[a#10,RANGEDEL-e#inf,RANGEDEL] + +spans +---- +a-d:{(#10,RANGEDEL) (#5,RANGEKEYDEL) (#3,RANGEDEL)} +d-e:{(#10,RANGEDEL) (#3,RANGEDEL)} diff --git a/pebble/testdata/version_check_consistency b/pebble/testdata/version_check_consistency new file mode 100644 index 0000000..a8ca9b3 --- /dev/null +++ b/pebble/testdata/version_check_consistency @@ -0,0 +1,70 @@ +build +000001:10 +000002:20 +000003:30 +---- +open test/000001.sst: file does not exist + +check-consistency +---- +OK + +check-consistency +L0 + 000005:10 +---- +L0: 000005: file 000005 (type 2) unknown to the objstorage provider: file does not exist + +check-consistency +L0 + 000001:10 +---- +L0: 000001: file 000001 (type 2) unknown to the objstorage provider: file does not exist + +check-consistency +L0 + 000001:11 +---- +L0: 000001: file 000001 (type 2) unknown to the objstorage provider: file does not exist + +check-consistency redact +L0 + 000001:11 +---- +L0: 000001: file 000001 (type 2) unknown to the objstorage provider: file does not exist + +check-consistency +L0 + 000001:10 +L1 + 000002:20 +L2 + 000003:30 +---- +L0: 000001: file 000001 (type 2) unknown to the objstorage provider: file does not exist +L1: 000002: file 000002 (type 2) unknown to the objstorage provider: file does not exist +L2: 000003: file 000003 (type 2) unknown to the objstorage provider: file does not exist + +check-consistency +L0 + 000001:11 +L1 + 000002:22 +L2 + 000003:33 +---- +L0: 000001: file 000001 (type 2) unknown to the objstorage provider: file does not exist +L1: 000002: file 000002 (type 2) unknown to the objstorage provider: file does not exist +L2: 000003: file 000003 (type 2) unknown to the objstorage provider: file does not exist + +check-consistency redact +L0 + 000001:11 +L1 + 000002:22 +L2 + 000004:30 +---- +L0: 000001: file 000001 (type 2) unknown to the objstorage provider: file does not exist +L1: 000002: file 000002 (type 2) unknown to the objstorage provider: file does not exist +L2: 000004: file 000004 (type 2) unknown to the objstorage provider: file does not exist diff --git a/pebble/tool/data/d3.v5.min.js b/pebble/tool/data/d3.v5.min.js new file mode 100644 index 0000000..a75674c --- /dev/null +++ b/pebble/tool/data/d3.v5.min.js @@ -0,0 +1,2 @@ +// https://d3js.org Version 5.1.0. Copyright 2018 Mike Bostock. +(function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})})(this,function(t){"use strict";function n(t,n){return tn?1:t>=n?0:NaN}function e(t){return 1===t.length&&(t=function(t){return function(e,r){return n(t(e),r)}}(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}function r(t,n){return[t,n]}function i(t){return null===t?NaN:+t}function o(t,n){var e,r,o=t.length,a=0,u=-1,f=0,c=0;if(null==n)for(;++u1)return c/(a-1)}function a(t,n){var e=o(t,n);return e?Math.sqrt(e):e}function u(t,n){var e,r,i,o=t.length,a=-1;if(null==n){for(;++a=e)for(r=i=e;++ae&&(r=e),i=e)for(r=i=e;++ae&&(r=e),i0)return[t];if((r=n0)for(t=Math.ceil(t/a),n=Math.floor(n/a),o=new Array(i=Math.ceil(n-t+1));++u=0?(o>=es?10:o>=rs?5:o>=is?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=es?10:o>=rs?5:o>=is?2:1)}function d(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=es?i*=10:o>=rs?i*=5:o>=is&&(i*=2),n=1)return+e(t[r-1],r-1,t);var r,o=(r-1)*n,a=Math.floor(o),u=+e(t[a],a,t);return u+(+e(t[a+1],a+1,t)-u)*(o-a)}}function g(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++or&&(r=e)}else for(;++o=e)for(r=e;++or&&(r=e);return r}function y(t){for(var n,e,r,i=t.length,o=-1,a=0;++o=0;)for(n=(r=t[i]).length;--n>=0;)e[--a]=r[n];return e}function _(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++oe&&(r=e)}else for(;++o=e)for(r=e;++oe&&(r=e);return r}function b(t){if(!(i=t.length))return[];for(var n=-1,e=_(t,m),r=new Array(e);++n=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),ds.hasOwnProperty(n)?{space:ds[n],local:t}:t}function C(t){var n=k(t);return(n.local?function(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}:function(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===hs&&n.documentElement.namespaceURI===hs?n.createElement(t):n.createElementNS(e,t)}})(n)}function P(){}function z(t){return null==t?P:function(){return this.querySelector(t)}}function R(){return[]}function L(t){return null==t?R:function(){return this.querySelectorAll(t)}}function D(t){return new Array(t.length)}function U(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function q(t,n,e,r,i,o){for(var a,u=0,f=n.length,c=o.length;un?1:t>=n?0:NaN}function B(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function F(t,n){return t.style.getPropertyValue(n)||B(t).getComputedStyle(t,null).getPropertyValue(n)}function I(t){return t.trim().split(/^|\s+/)}function j(t){return t.classList||new H(t)}function H(t){this._node=t,this._names=I(t.getAttribute("class")||"")}function X(t,n){for(var e=j(t),r=-1,i=n.length;++r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Ns.exec(t))?Ct(parseInt(n[1],16)):(n=Ss.exec(t))?new Lt(n[1],n[2],n[3],1):(n=Es.exec(t))?new Lt(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=ks.exec(t))?Pt(n[1],n[2],n[3],n[4]):(n=Cs.exec(t))?Pt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Ps.exec(t))?Dt(n[1],n[2]/100,n[3]/100,1):(n=zs.exec(t))?Dt(n[1],n[2]/100,n[3]/100,n[4]):Rs.hasOwnProperty(t)?Ct(Rs[t]):"transparent"===t?new Lt(NaN,NaN,NaN,0):null}function Ct(t){return new Lt(t>>16&255,t>>8&255,255&t,1)}function Pt(t,n,e,r){return r<=0&&(t=n=e=NaN),new Lt(t,n,e,r)}function zt(t){return t instanceof Et||(t=kt(t)),t?(t=t.rgb(),new Lt(t.r,t.g,t.b,t.opacity)):new Lt}function Rt(t,n,e,r){return 1===arguments.length?zt(t):new Lt(t,n,e,null==r?1:r)}function Lt(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Dt(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new qt(t,n,e,r)}function Ut(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof qt)return new qt(t.h,t.s,t.l,t.opacity);if(t instanceof Et||(t=kt(t)),!t)return new qt;if(t instanceof qt)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,f=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&f<1?0:a,new qt(a,u,f,t.opacity)}(t):new qt(t,n,e,null==r?1:r)}function qt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Ot(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Yt(t){if(t instanceof Ft)return new Ft(t.l,t.a,t.b,t.opacity);if(t instanceof $t){if(isNaN(t.h))return new Ft(t.l,0,0,t.opacity);var n=t.h*Ls;return new Ft(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof Lt||(t=zt(t));var e,r,i=Xt(t.r),o=Xt(t.g),a=Xt(t.b),u=It((.2225045*i+.7168786*o+.0606169*a)/qs);return i===o&&o===a?e=r=u:(e=It((.4360747*i+.3850649*o+.1430804*a)/Us),r=It((.0139322*i+.0971045*o+.7141733*a)/Os)),new Ft(116*u-16,500*(e-u),200*(u-r),t.opacity)}function Bt(t,n,e,r){return 1===arguments.length?Yt(t):new Ft(t,n,e,null==r?1:r)}function Ft(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function It(t){return t>Is?Math.pow(t,1/3):t/Fs+Ys}function jt(t){return t>Bs?t*t*t:Fs*(t-Ys)}function Ht(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Xt(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Gt(t){if(t instanceof $t)return new $t(t.h,t.c,t.l,t.opacity);if(t instanceof Ft||(t=Yt(t)),0===t.a&&0===t.b)return new $t(NaN,0,t.l,t.opacity);var n=Math.atan2(t.b,t.a)*Ds;return new $t(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Vt(t,n,e,r){return 1===arguments.length?Gt(t):new $t(t,n,e,null==r?1:r)}function $t(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}function Wt(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof Zt)return new Zt(t.h,t.s,t.l,t.opacity);t instanceof Lt||(t=zt(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=($s*r+Gs*n-Vs*e)/($s+Gs-Vs),o=r-i,a=(Xs*(e-i)-js*o)/Hs,u=Math.sqrt(a*a+o*o)/(Xs*i*(1-i)),f=u?Math.atan2(a,o)*Ds-120:NaN;return new Zt(f<0?f+360:f,u,i,t.opacity)}(t):new Zt(t,n,e,null==r?1:r)}function Zt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Qt(t,n,e,r,i){var o=t*t,a=o*t;return((1-3*t+3*o-a)*n+(4-6*o+3*a)*e+(1+3*t+3*o-3*a)*r+a*i)/6}function Jt(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r180||e<-180?e-360*Math.round(e/360):e):tn(isNaN(t)?n:t)}function rn(t){return 1==(t=+t)?on:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):tn(isNaN(n)?e:n)}}function on(t,n){var e=n-t;return e?nn(t,e):tn(isNaN(t)?n:t)}function an(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,f.push({i:a,x:cn(e,r)})),o=ol.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:cn(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,f),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:cn(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,f),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:cn(t,e)},{i:u-2,x:cn(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,f),o=a=null,function(t){for(var n,e=-1,r=f.length;++e=0&&n._call.call(null,t),n=n._next;--ml}function Nn(){Tl=(Al=Sl.now())+Nl,ml=xl=0;try{Tn()}finally{ml=0,function(){var t,n,e=Ks,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:Ks=n);tl=t,En(r)}(),Tl=0}}function Sn(){var t=Sl.now(),n=t-Al;n>Ml&&(Nl-=n,Al=t)}function En(t){if(!ml){xl&&(xl=clearTimeout(xl));t-Tl>24?(t<1/0&&(xl=setTimeout(Nn,t-Sl.now()-Nl)),wl&&(wl=clearInterval(wl))):(wl||(Al=Sl.now(),wl=setInterval(Sn,Ml)),ml=1,El(Nn))}}function kn(t,n,e){var r=new Mn;return n=null==n?0:+n,r.restart(function(e){r.stop(),t(e+n)},n,e),r}function Cn(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};(function(t,n,e){function r(f){var c,s,l,h;if(e.state!==zl)return o();for(c in u)if((h=u[c]).name===e.name){if(h.state===Ll)return kn(r);h.state===Dl?(h.state=ql,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete u[c]):+cPl)throw new Error("too late; already scheduled");return e}function zn(t,n){var e=Rn(t,n);if(e.state>Rl)throw new Error("too late; already started");return e}function Rn(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Ln(t,n){var e,r,i,o=t.__transition,a=!0;if(o){n=null==n?null:n+"";for(i in o)(e=o[i]).name===n?(r=e.state>Rl&&e.stateMath.abs(t[1]-U[1])?x=!0:m=!0),U=t,b=!0,Wn(),o()}function o(){var t;switch(y=U[0]-D[0],_=U[1]-D[1],A){case hh:case lh:T&&(y=Math.max(C-u,Math.min(z-d,y)),c=u+y,p=d+y),N&&(_=Math.max(P-l,Math.min(R-v,_)),h=l+_,g=v+_);break;case dh:T<0?(y=Math.max(C-u,Math.min(z-u,y)),c=u+y,p=d):T>0&&(y=Math.max(C-d,Math.min(z-d,y)),c=u,p=d+y),N<0?(_=Math.max(P-l,Math.min(R-l,_)),h=l+_,g=v):N>0&&(_=Math.max(P-v,Math.min(R-v,_)),h=l,g=v+_);break;case ph:T&&(c=Math.max(C,Math.min(z,u-y*T)),p=Math.max(C,Math.min(z,d+y*T))),N&&(h=Math.max(P,Math.min(R,l-_*N)),g=Math.max(P,Math.min(R,v+_*N)))}p0&&(u=c-y),N<0?v=g-_:N>0&&(l=h-_),A=hh,Y.attr("cursor",_h.selection),o());break;default:return}Wn()},!0).on("keyup.brush",function(){switch(t.event.keyCode){case 16:L&&(m=x=L=!1,o());break;case 18:A===ph&&(T<0?d=p:T>0&&(u=c),N<0?v=g:N>0&&(l=h),A=dh,o());break;case 32:A===hh&&(t.event.altKey?(T&&(d=p-y*T,u=c+y*T),N&&(v=g-_*N,l=h+_*N),A=ph):(T<0?d=p:T>0&&(u=c),N<0?v=g:N>0&&(l=h),A=dh),Y.attr("cursor",_h[M]),o());break;default:return}Wn()},!0).on("mousemove.brush",e,!0).on("mouseup.brush",a,!0);_t(t.event.view)}$n(),Ln(w),r.call(w),q.start()}}function u(){var t=this.__brush||{selection:null};return t.extent=c.apply(this,arguments),t.dim=n,t}var f,c=Jn,s=Qn,l=N(e,"start","brush","end"),h=6;return e.move=function(t,e){t.selection?t.on("start.brush",function(){i(this,arguments).beforestart().start()}).on("interrupt.brush end.brush",function(){i(this,arguments).end()}).tween("brush",function(){function t(t){a.selection=1===t&&te(c)?null:s(t),r.call(o),u.brush()}var o=this,a=o.__brush,u=i(o,arguments),f=a.selection,c=n.input("function"==typeof e?e.apply(this,arguments):e,a.extent),s=hn(f,c);return f&&c?t:t(1)}):t.each(function(){var t=arguments,o=this.__brush,a=n.input("function"==typeof e?e.apply(this,t):e,o.extent),u=i(this,t).beforestart();Ln(this),o.selection=null==a||te(a)?null:a,r.call(this),u.start().brush().end()})},o.prototype={beforestart:function(){return 1==++this.active&&(this.state.emitter=this,this.starting=!0),this},start:function(){return this.starting&&(this.starting=!1,this.emit("start")),this},brush:function(){return this.emit("brush"),this},end:function(){return 0==--this.active&&(delete this.state.emitter,this.emit("end")),this},emit:function(t){ot(new function(t,n,e){this.target=t,this.type=n,this.selection=e}(e,t,n.output(this.state.selection)),l.apply,l,[t,this.that,this.args])}},e.extent=function(t){return arguments.length?(c="function"==typeof t?t:Vn([[+t[0][0],+t[0][1]],[+t[1][0],+t[1][1]]]),e):c},e.filter=function(t){return arguments.length?(s="function"==typeof t?t:Vn(!!t),e):s},e.handleSize=function(t){return arguments.length?(h=+t,e):h},e.on=function(){var t=l.on.apply(l,arguments);return t===l?e:t},e}function ee(t){return function(){return t}}function re(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function ie(){return new re}function oe(t){return t.source}function ae(t){return t.target}function ue(t){return t.radius}function fe(t){return t.startAngle}function ce(t){return t.endAngle}function se(){}function le(t,n){var e=new se;if(t instanceof se)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++ir!=d>r&&e<(h-c)*(r-s)/(d-s)+c&&(i=-i)}return i}(t,n[r]))return e;return 0}function xe(){}function we(){function t(t){var e=a(t);if(Array.isArray(e))e=e.slice().sort(_e);else{var r=u(t),i=r[0],o=r[1];e=d(i,o,e),e=s(Math.floor(i/e)*e,Math.floor(o/e)*e,e)}return e.map(function(e){return n(t,e)})}function n(t,n){var r=[],a=[];return function(t,n,r){function a(t){var n,i,o=[t[0][0]+u,t[0][1]+f],a=[t[1][0]+u,t[1][1]+f],c=e(o),s=e(a);(n=p[c])?(i=d[s])?(delete p[n.end],delete d[i.start],n===i?(n.ring.push(a),r(n.ring)):d[n.start]=p[i.end]={start:n.start,end:i.end,ring:n.ring.concat(i.ring)}):(delete p[n.end],n.ring.push(a),p[n.end=s]=n):(n=d[s])?(i=p[c])?(delete d[n.start],delete p[i.end],n===i?(n.ring.push(a),r(n.ring)):d[i.start]=p[n.end]={start:i.start,end:n.end,ring:i.ring.concat(n.ring)}):(delete d[n.start],n.ring.unshift(o),d[n.start=c]=n):d[c]=p[s]={start:c,end:s,ring:[o,a]}}var u,f,c,s,l,h,d=new Array,p=new Array;u=f=-1,s=t[0]>=n,Dh[s<<1].forEach(a);for(;++u=n,Dh[c|s<<1].forEach(a);Dh[s<<0].forEach(a);for(;++f=n,l=t[f*i]>=n,Dh[s<<1|l<<2].forEach(a);++u=n,h=l,l=t[f*i+u+1]>=n,Dh[c|s<<1|l<<2|h<<3].forEach(a);Dh[s|l<<3].forEach(a)}u=-1,l=t[f*i]>=n,Dh[l<<2].forEach(a);for(;++u=n,Dh[l<<2|h<<3].forEach(a);Dh[l<<3].forEach(a)}(t,n,function(e){f(e,t,n),function(t){for(var n=0,e=t.length,r=t[e-1][1]*t[0][0]-t[e-1][0]*t[0][1];++n0?r.push([e]):a.push(e)}),a.forEach(function(t){for(var n,e=0,i=r.length;e0&&a0&&u0&&r>0))throw new Error("invalid size");return i=e,o=r,t},t.thresholds=function(n){return arguments.length?(a="function"==typeof n?n:Array.isArray(n)?be(Lh.call(n)):be(n),t):a},t.smooth=function(n){return arguments.length?(f=n?r:xe,t):f===r},t}function Me(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(f-=t.data[u-o+a*r]),n.data[u-e+a*r]=f/Math.min(u+1,r-1+o-u,o))}function Ae(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(f-=t.data[a+(u-o)*r]),n.data[a+(u-e)*r]=f/Math.min(u+1,i-1+o-u,o))}function Te(t){return t[0]}function Ne(t){return t[1]}function Se(t){return new Function("d","return {"+t.map(function(t,n){return JSON.stringify(t)+": d["+n+"]"}).join(",")+"}")}function Ee(t){function n(t,n){function e(){if(c)return qh;if(s)return s=!1,Uh;var n,e,r=u;if(t.charCodeAt(r)===Oh){for(;u++=a?c=!0:(e=t.charCodeAt(u++))===Yh?s=!0:e===Bh&&(s=!0,t.charCodeAt(u)===Yh&&++u),t.slice(r+1,n-1).replace(/""/g,'"')}for(;u=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a,i=d,!(d=d[l=s<<1|c]))return i[l]=p,t;if(u=+t._x.call(null,d.data),f=+t._y.call(null,d.data),n===u&&e===f)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(c=n>=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a}while((l=s<<1|c)==(h=(f>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function Ye(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Be(t){return t[0]}function Fe(t){return t[1]}function Ie(t,n,e){var r=new je(null==n?Be:n,null==e?Fe:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function je(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function He(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}function Xe(t){return t.x+t.vx}function Ge(t){return t.y+t.vy}function Ve(t){return t.index}function $e(t,n){var e=t.get(n);if(!e)throw new Error("missing: "+n);return e}function We(t){return t.x}function Ze(t){return t.y}function Qe(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,r=t.slice(0,e);return[r.length>1?r[0]+r.slice(2):r,+t.slice(e+1)]}function Je(t){return(t=Qe(Math.abs(t)))?t[1]:NaN}function Ke(t,n){var e=Qe(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}function tr(t){return new nr(t)}function nr(t){if(!(n=ud.exec(t)))throw new Error("invalid format: "+t);var n,e=n[1]||" ",r=n[2]||">",i=n[3]||"-",o=n[4]||"",a=!!n[5],u=n[6]&&+n[6],f=!!n[7],c=n[8]&&+n[8].slice(1),s=n[9]||"";"n"===s?(f=!0,s="g"):ad[s]||(s=""),(a||"0"===e&&"="===r)&&(a=!0,e="0",r="="),this.fill=e,this.align=r,this.sign=i,this.symbol=o,this.zero=a,this.width=u,this.comma=f,this.precision=c,this.type=s}function er(t){return t}function rr(t){function n(t){function n(t){var n,r,a,s=g,m=y;if("c"===v)m=_(t)+m,t="";else{var x=(t=+t)<0;if(t=_(Math.abs(t),p),x&&0==+t&&(x=!1),s=(x?"("===c?c:"-":"-"===c||"("===c?"":c)+s,m=("s"===v?cd[8+rd/3]:"")+m+(x&&"("===c?")":""),b)for(n=-1,r=t.length;++n(a=t.charCodeAt(n))||a>57){m=(46===a?i+t.slice(n+1):t.slice(n))+m,t=t.slice(0,n);break}}d&&!l&&(t=e(t,1/0));var w=s.length+t.length+m.length,M=w>1)+s+t+m+M.slice(w);break;default:t=M+s+t+m}return o(t)}var u=(t=tr(t)).fill,f=t.align,c=t.sign,s=t.symbol,l=t.zero,h=t.width,d=t.comma,p=t.precision,v=t.type,g="$"===s?r[0]:"#"===s&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",y="$"===s?r[1]:/[%p]/.test(v)?a:"",_=ad[v],b=!v||/[defgprs%]/.test(v);return p=null==p?v?6:12:/[gprs]/.test(v)?Math.max(1,Math.min(21,p)):Math.max(0,Math.min(20,p)),n.toString=function(){return t+""},n}var e=t.grouping&&t.thousands?function(t,n){return function(e,r){for(var i=e.length,o=[],a=0,u=t[0],f=0;i>0&&u>0&&(f+u+1>r&&(u=Math.max(1,r-f)),o.push(e.substring(i-=u,i+u)),!((f+=u+1)>r));)u=t[a=(a+1)%t.length];return o.reverse().join(n)}}(t.grouping,t.thousands):er,r=t.currency,i=t.decimal,o=t.numerals?function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}}(t.numerals):er,a=t.percent||"%";return{format:n,formatPrefix:function(t,e){var r=n((t=tr(t),t.type="f",t)),i=3*Math.max(-8,Math.min(8,Math.floor(Je(e)/3))),o=Math.pow(10,-i),a=cd[8+i/3];return function(t){return r(o*t)+a}}}}function ir(n){return fd=rr(n),t.format=fd.format,t.formatPrefix=fd.formatPrefix,fd}function or(t){return Math.max(0,-Je(Math.abs(t)))}function ar(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Je(n)/3)))-Je(Math.abs(t)))}function ur(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Je(n)-Je(t))+1}function fr(){return new cr}function cr(){this.reset()}function sr(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}function lr(t){return t>1?0:t<-1?Hd:Math.acos(t)}function hr(t){return t>1?Xd:t<-1?-Xd:Math.asin(t)}function dr(t){return(t=ip(t/2))*t}function pr(){}function vr(t,n){t&&cp.hasOwnProperty(t.type)&&cp[t.type](t,n)}function gr(t,n,e){var r,i=-1,o=t.length-e;for(n.lineStart();++i=0?1:-1,i=r*e,o=Kd(n),a=ip(n),u=pd*a,f=dd*o+u*Kd(i),c=u*r*ip(i);sp.add(Jd(c,f)),hd=t,dd=o,pd=a}function Mr(t){return[Jd(t[1],t[0]),hr(t[2])]}function Ar(t){var n=t[0],e=t[1],r=Kd(e);return[r*Kd(n),r*ip(n),ip(e)]}function Tr(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Nr(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function Sr(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function Er(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function kr(t){var n=ap(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function Cr(t,n){Md.push(Ad=[vd=t,yd=t]),n_d&&(_d=n)}function Pr(t,n){var e=Ar([t*Wd,n*Wd]);if(wd){var r=Nr(wd,e),i=Nr([r[1],-r[0],0],r);kr(i),i=Mr(i);var o,a=t-bd,u=a>0?1:-1,f=i[0]*$d*u,c=Zd(a)>180;c^(u*bd_d&&(_d=o):(f=(f+360)%360-180,c^(u*bd_d&&(_d=n))),c?tqr(vd,yd)&&(yd=t):qr(t,yd)>qr(vd,yd)&&(vd=t):yd>=vd?(tyd&&(yd=t)):t>bd?qr(vd,t)>qr(vd,yd)&&(yd=t):qr(t,yd)>qr(vd,yd)&&(vd=t)}else Md.push(Ad=[vd=t,yd=t]);n_d&&(_d=n),wd=e,bd=t}function zr(){pp.point=Pr}function Rr(){Ad[0]=vd,Ad[1]=yd,pp.point=Cr,wd=null}function Lr(t,n){if(wd){var e=t-bd;dp.add(Zd(e)>180?e+(e>0?360:-360):e)}else md=t,xd=n;hp.point(t,n),Pr(t,n)}function Dr(){hp.lineStart()}function Ur(){Lr(md,xd),hp.lineEnd(),Zd(dp)>Id&&(vd=-(yd=180)),Ad[0]=vd,Ad[1]=yd,wd=null}function qr(t,n){return(n-=t)<0?n+360:n}function Or(t,n){return t[0]-n[0]}function Yr(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nHd?t-Vd:t<-Hd?t+Vd:t,n]}function Kr(t,n,e){return(t%=Vd)?n||e?Qr(ni(t),ei(n,e)):ni(t):n||e?ei(n,e):Jr}function ti(t){return function(n,e){return n+=t,[n>Hd?n-Vd:n<-Hd?n+Vd:n,e]}}function ni(t){var n=ti(t);return n.invert=ti(-t),n}function ei(t,n){function e(t,n){var e=Kd(n),u=Kd(t)*e,f=ip(t)*e,c=ip(n),s=c*r+u*i;return[Jd(f*o-s*a,u*r-c*i),hr(s*o+f*a)]}var r=Kd(t),i=ip(t),o=Kd(n),a=ip(n);return e.invert=function(t,n){var e=Kd(n),u=Kd(t)*e,f=ip(t)*e,c=ip(n),s=c*o-f*a;return[Jd(f*o+c*a,u*r+s*i),hr(s*r-u*i)]},e}function ri(t){function n(n){return n=t(n[0]*Wd,n[1]*Wd),n[0]*=$d,n[1]*=$d,n}return t=Kr(t[0]*Wd,t[1]*Wd,t.length>2?t[2]*Wd:0),n.invert=function(n){return n=t.invert(n[0]*Wd,n[1]*Wd),n[0]*=$d,n[1]*=$d,n},n}function ii(t,n,e,r,i,o){if(e){var a=Kd(n),u=ip(n),f=r*e;null==i?(i=n+r*Vd,o=n-f/2):(i=oi(a,i),o=oi(a,o),(r>0?io)&&(i+=r*Vd));for(var c,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function ui(t,n){return Zd(t[0]-n[0])=0;--o)i.point((s=c[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}c=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function si(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,T=A*M,N=T>Hd,S=v*x;if(Sp.add(Jd(S*A*ip(T),g*w+S*Kd(T))),a+=N?M+A*Vd:M,N^d>=e^b>=e){var E=Nr(Ar(h),Ar(_));kr(E);var k=Nr(o,E);kr(k);var C=(N^M>=0?-1:1)*hr(k[2]);(r>C||r===C&&(E[0]||E[1]))&&(u+=N^M>=0?1:-1)}}return(a<-Id||a0){for(b||(i.polygonStart(),b=!0),i.lineStart(),t=0;t1&&2&o&&a.push(a.pop().concat(a.shift())),d.push(a.filter(di))}var h,d,p,v=n(i),g=ai(),_=n(g),b=!1,m={point:o,lineStart:u,lineEnd:f,polygonStart:function(){m.point=c,m.lineStart=s,m.lineEnd=l,d=[],h=[]},polygonEnd:function(){m.point=o,m.lineStart=u,m.lineEnd=f,d=y(d);var t=li(h,r);d.length?(b||(i.polygonStart(),b=!0),ci(d,pi,t,e,i)):t&&(b||(i.polygonStart(),b=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),b&&(i.polygonEnd(),b=!1),d=h=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}};return m}}function di(t){return t.length>1}function pi(t,n){return((t=t.x)[0]<0?t[1]-Xd-Id:Xd-t[1])-((n=n.x)[0]<0?n[1]-Xd-Id:Xd-n[1])}function vi(t){function n(t,n){return Kd(t)*Kd(n)>i}function e(t,n,e){var r=[1,0,0],o=Nr(Ar(t),Ar(n)),a=Tr(o,o),u=o[0],f=a-u*u;if(!f)return!e&&t;var c=i*a/f,s=-i*u/f,l=Nr(r,o),h=Er(r,c);Sr(h,Er(o,s));var d=l,p=Tr(h,d),v=Tr(d,d),g=p*p-v*(Tr(h,h)-1);if(!(g<0)){var y=ap(g),_=Er(d,(-p-y)/v);if(Sr(_,h),_=Mr(_),!e)return _;var b,m=t[0],x=n[0],w=t[1],M=n[1];x0^_[1]<(Zd(_[0]-m)Hd^(m<=_[0]&&_[0]<=x)){var N=Er(d,(-p+y)/v);return Sr(N,h),[_,Mr(N)]}}}function r(n,e){var r=a?t:Hd-t,i=0;return n<-r?i|=1:n>r&&(i|=2),e<-r?i|=4:e>r&&(i|=8),i}var i=Kd(t),o=6*Wd,a=i>0,u=Zd(i)>Id;return hi(n,function(t){var i,o,f,c,s;return{lineStart:function(){c=f=!1,s=1},point:function(l,h){var d,p=[l,h],v=n(l,h),g=a?v?0:r(l,h):v?r(l+(l<0?Hd:-Hd),h):0;if(!i&&(c=f=v)&&t.lineStart(),v!==f&&(!(d=e(i,p))||ui(i,d)||ui(p,d))&&(p[0]+=Id,p[1]+=Id,v=n(p[0],p[1])),v!==f)s=0,v?(t.lineStart(),d=e(p,i),t.point(d[0],d[1])):(d=e(i,p),t.point(d[0],d[1]),t.lineEnd()),i=d;else if(u&&i&&a^v){var y;g&o||!(y=e(p,i,!0))||(s=0,a?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1])))}!v||i&&ui(i,p)||t.point(p[0],p[1]),i=p,f=v,o=g},lineEnd:function(){f&&t.lineEnd(),i=null},clean:function(){return s|(c&&f)<<1}}},function(n,e,r,i){ii(i,t,o,r,n,e)},a?[0,-t]:[-Hd,t-Hd])}function gi(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,c){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||f(i,o)<0^u>0)do{c.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else c.point(o[0],o[1])}function a(r,i){return Zd(r[0]-t)0?0:3:Zd(r[0]-e)0?2:1:Zd(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return f(t.x,n.x)}function f(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){function f(t,n){i(t,n)&&w.point(t,n)}function c(o,a){var u=i(o,a);if(l&&h.push([o,a]),m)d=o,p=a,v=u,m=!1,u&&(w.lineStart(),w.point(o,a));else if(u&&b)w.point(o,a);else{var f=[g=Math.max(Cp,Math.min(kp,g)),_=Math.max(Cp,Math.min(kp,_))],c=[o=Math.max(Cp,Math.min(kp,o)),a=Math.max(Cp,Math.min(kp,a))];!function(t,n,e,r,i,o){var a,u=t[0],f=t[1],c=0,s=1,l=n[0]-u,h=n[1]-f;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>c&&(c=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>c&&(c=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>c&&(c=a)}if(a=o-f,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>c&&(c=a)}else if(h>0){if(a0&&(t[0]=u+c*l,t[1]=f+c*h),s<1&&(n[0]=u+s*l,n[1]=f+s*h),!0}}}}}(f,c,t,n,e,r)?u&&(w.lineStart(),w.point(o,a),x=!1):(b||(w.lineStart(),w.point(f[0],f[1])),w.point(c[0],c[1]),u||w.lineEnd(),x=!1)}g=o,_=a,b=u}var s,l,h,d,p,v,g,_,b,m,x,w=a,M=ai(),A={point:f,lineStart:function(){A.point=c,l&&l.push(h=[]),m=!0,b=!1,g=_=NaN},lineEnd:function(){s&&(c(d,p),v&&b&&M.rejoin(),s.push(M.result())),A.point=f,b&&w.lineEnd()},polygonStart:function(){w=M,s=[],l=[],x=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=l.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=x&&n,i=(s=y(s)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&ci(s,u,n,o,a),a.polygonEnd()),w=a,s=l=h=null}};return A}}function yi(){zp.point=zp.lineEnd=pr}function _i(t,n){gp=t*=Wd,yp=ip(n*=Wd),_p=Kd(n),zp.point=bi}function bi(t,n){t*=Wd;var e=ip(n*=Wd),r=Kd(n),i=Zd(t-gp),o=Kd(i),a=r*ip(i),u=_p*e-yp*r*o,f=yp*e+_p*r*o;Pp.add(Jd(ap(a*a+u*u),f)),gp=t,yp=e,_p=r}function mi(t){return Pp.reset(),_r(t,zp),+Pp}function xi(t,n){return Rp[0]=t,Rp[1]=n,mi(Lp)}function wi(t,n){return!(!t||!Up.hasOwnProperty(t.type))&&Up[t.type](t,n)}function Mi(t,n){return 0===xi(t,n)}function Ai(t,n){var e=xi(t[0],t[1]);return xi(t[0],n)+xi(n,t[1])<=e+Id}function Ti(t,n){return!!li(t.map(Ni),Si(n))}function Ni(t){return(t=t.map(Si)).pop(),t}function Si(t){return[t[0]*Wd,t[1]*Wd]}function Ei(t,n,e){var r=s(t,n-Id,e).concat(n);return function(t){return r.map(function(n){return[t,n]})}}function ki(t,n,e){var r=s(t,n-Id,e).concat(n);return function(t){return r.map(function(n){return[n,t]})}}function Ci(){function t(){return{type:"MultiLineString",coordinates:n()}}function n(){return s(tp(o/y)*y,i,y).map(d).concat(s(tp(c/_)*_,f,_).map(p)).concat(s(tp(r/v)*v,e,v).filter(function(t){return Zd(t%y)>Id}).map(l)).concat(s(tp(u/g)*g,a,g).filter(function(t){return Zd(t%_)>Id}).map(h))}var e,r,i,o,a,u,f,c,l,h,d,p,v=10,g=v,y=90,_=360,b=2.5;return t.lines=function(){return n().map(function(t){return{type:"LineString",coordinates:t}})},t.outline=function(){return{type:"Polygon",coordinates:[d(o).concat(p(f).slice(1),d(i).reverse().slice(1),p(c).reverse().slice(1))]}},t.extent=function(n){return arguments.length?t.extentMajor(n).extentMinor(n):t.extentMinor()},t.extentMajor=function(n){return arguments.length?(o=+n[0][0],i=+n[1][0],c=+n[0][1],f=+n[1][1],o>i&&(n=o,o=i,i=n),c>f&&(n=c,c=f,f=n),t.precision(b)):[[o,c],[i,f]]},t.extentMinor=function(n){return arguments.length?(r=+n[0][0],e=+n[1][0],u=+n[0][1],a=+n[1][1],r>e&&(n=r,r=e,e=n),u>a&&(n=u,u=a,a=n),t.precision(b)):[[r,u],[e,a]]},t.step=function(n){return arguments.length?t.stepMajor(n).stepMinor(n):t.stepMinor()},t.stepMajor=function(n){return arguments.length?(y=+n[0],_=+n[1],t):[y,_]},t.stepMinor=function(n){return arguments.length?(v=+n[0],g=+n[1],t):[v,g]},t.precision=function(n){return arguments.length?(b=+n,l=Ei(u,a,90),h=ki(r,e,b),d=Ei(c,f,90),p=ki(o,i,b),t):b},t.extentMajor([[-180,-90+Id],[180,90-Id]]).extentMinor([[-180,-80-Id],[180,80+Id]])}function Pi(t){return t}function zi(){Yp.point=Ri}function Ri(t,n){Yp.point=Li,bp=xp=t,mp=wp=n}function Li(t,n){Op.add(wp*t-xp*n),xp=t,wp=n}function Di(){Li(bp,mp)}function Ui(t,n){Xp+=t,Gp+=n,++Vp}function qi(){tv.point=Oi}function Oi(t,n){tv.point=Yi,Ui(Tp=t,Np=n)}function Yi(t,n){var e=t-Tp,r=n-Np,i=ap(e*e+r*r);$p+=i*(Tp+t)/2,Wp+=i*(Np+n)/2,Zp+=i,Ui(Tp=t,Np=n)}function Bi(){tv.point=Ui}function Fi(){tv.point=ji}function Ii(){Hi(Mp,Ap)}function ji(t,n){tv.point=Hi,Ui(Mp=Tp=t,Ap=Np=n)}function Hi(t,n){var e=t-Tp,r=n-Np,i=ap(e*e+r*r);$p+=i*(Tp+t)/2,Wp+=i*(Np+n)/2,Zp+=i,Qp+=(i=Np*t-Tp*n)*(Tp+t),Jp+=i*(Np+n),Kp+=3*i,Ui(Tp=t,Np=n)}function Xi(t){this._context=t}function Gi(t,n){uv.point=Vi,ev=iv=t,rv=ov=n}function Vi(t,n){iv-=t,ov-=n,av.add(ap(iv*iv+ov*ov)),iv=t,ov=n}function $i(){this._string=[]}function Wi(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function Zi(t){return function(n){var e=new Qi;for(var r in t)e[r]=t[r];return e.stream=n,e}}function Qi(){}function Ji(t,n,e){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),_r(e,t.stream(Hp)),n(Hp.result()),null!=r&&t.clipExtent(r),t}function Ki(t,n,e){return Ji(t,function(e){var r=n[1][0]-n[0][0],i=n[1][1]-n[0][1],o=Math.min(r/(e[1][0]-e[0][0]),i/(e[1][1]-e[0][1])),a=+n[0][0]+(r-o*(e[1][0]+e[0][0]))/2,u=+n[0][1]+(i-o*(e[1][1]+e[0][1]))/2;t.scale(150*o).translate([a,u])},e)}function to(t,n,e){return Ki(t,[[0,0],n],e)}function no(t,n,e){return Ji(t,function(e){var r=+n,i=r/(e[1][0]-e[0][0]),o=(r-i*(e[1][0]+e[0][0]))/2,a=-i*e[0][1];t.scale(150*i).translate([o,a])},e)}function eo(t,n,e){return Ji(t,function(e){var r=+n,i=r/(e[1][1]-e[0][1]),o=-i*e[0][0],a=(r-i*(e[1][1]+e[0][1]))/2;t.scale(150*i).translate([o,a])},e)}function ro(t,n){return+n?function(t,n){function e(r,i,o,a,u,f,c,s,l,h,d,p,v,g){var y=c-r,_=s-i,b=y*y+_*_;if(b>4*n&&v--){var m=a+h,x=u+d,w=f+p,M=ap(m*m+x*x+w*w),A=hr(w/=M),T=Zd(Zd(w)-1)n||Zd((y*k+_*C)/b-.5)>.3||a*h+u*d+f*p2?t[2]%360*Wd:0,e()):[b*$d,m*$d,x*$d]},n.angle=function(t){return arguments.length?(w=t%360*Wd,e()):w*$d},n.precision=function(t){return arguments.length?(c=ro(s,S=t*t),r()):ap(S)},n.fitExtent=function(t,e){return Ki(n,t,e)},n.fitSize=function(t,e){return to(n,t,e)},n.fitWidth=function(t,e){return no(n,t,e)},n.fitHeight=function(t,e){return eo(n,t,e)},function(){return i=t.apply(this,arguments),n.invert=i.invert&&function(t){return(t=l.invert(t[0],t[1]))&&[t[0]*$d,t[1]*$d]},e()}}function uo(t){var n=0,e=Hd/3,r=ao(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*Wd,e=t[1]*Wd):[n*$d,e*$d]},i}function fo(t,n){function e(t,n){var e=ap(o-2*i*ip(n))/i;return[e*ip(t*=i),a-e*Kd(t)]}var r=ip(t),i=(r+ip(n))/2;if(Zd(i)0?n<-Xd+Id&&(n=-Xd+Id):n>Xd-Id&&(n=Xd-Id);var e=o/rp(go(n),i);return[e*ip(i*t),o-e*Kd(i*t)]}var r=Kd(t),i=t===n?ip(t):ep(r/Kd(n))/ep(go(n)/go(t)),o=r*rp(go(t),i)/i;return i?(e.invert=function(t,n){var e=o-n,r=op(i)*ap(t*t+e*e);return[Jd(t,Zd(e))/i*op(e),2*Qd(rp(o/r,1/i))-Xd]},e):po}function _o(t,n){return[t,n]}function bo(t,n){function e(t,n){var e=o-n,r=i*t;return[e*ip(r),o-e*Kd(r)]}var r=Kd(t),i=t===n?ip(t):(r-Kd(n))/(n-t),o=r/i+t;return Zd(i)=0;)n+=e[r].value;else n=1;t.value=n}function Co(t,n){var e,r,i,o,a,u=new Lo(t),f=+t.value&&(u.value=t.value),c=[u];for(null==n&&(n=Po);e=c.pop();)if(f&&(e.value=+e.data.value),(i=n(e.data))&&(a=i.length))for(e.children=new Array(a),o=a-1;o>=0;--o)c.push(r=e.children[o]=new Lo(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(Ro)}function Po(t){return t.children}function zo(t){t.data=t.data.data}function Ro(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Lo(t){this.data=t,this.depth=this.height=0,this.parent=null}function Do(t){for(var n,e,r=0,i=(t=function(t){for(var n,e,r=t.length;r;)e=Math.random()*r--|0,n=t[r],t[r]=t[e],t[e]=n;return t}(dv.call(t))).length,o=[];r0&&e*e>r*r+i*i}function Oo(t,n){for(var e=0;e(a*=a)?(r=(c+a-i)/(2*c),o=Math.sqrt(Math.max(0,a/c-r*r)),e.x=t.x-r*u-o*f,e.y=t.y-r*f+o*u):(r=(c+i-a)/(2*c),o=Math.sqrt(Math.max(0,i/c-r*r)),e.x=n.x+r*u-o*f,e.y=n.y+r*f+o*u)):(e.x=n.x+e.r,e.y=n.y)}function Io(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function jo(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function Ho(t){this._=t,this.next=null,this.previous=null}function Xo(t){if(!(i=t.length))return 0;var n,e,r,i,o,a,u,f,c,s,l;if(n=t[0],n.x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Fo(e,n,r=t[2]),n=new Ho(n),e=new Ho(e),r=new Ho(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(u=3;uh&&(h=u),g=s*s*v,(d=Math.max(h/g,g/l))>p){s-=u;break}p=d}y.push(a={value:s,dice:f1&&la(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function pa(){return Math.random()}function va(t){function n(n){var o=n+"",a=e.get(o);if(!a){if(i!==kv)return i;e.set(o,a=r.push(n))}return t[(a-1)%t.length]}var e=le(),r=[],i=kv;return t=null==t?[]:Ev.call(t),n.domain=function(t){if(!arguments.length)return r.slice();r=[],e=le();for(var i,o,a=-1,u=t.length;++a2?wa:xa,o=a=null,r}function r(n){return(o||(o=i(u,f,s?function(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=n?0:t>=e?1:r(t)}}}(t):t,c)))(+n)}var i,o,a,u=Cv,f=Cv,c=hn,s=!1;return r.invert=function(t){return(a||(a=i(f,u,ma,s?function(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:r(t)}}}(n):n)))(+t)},r.domain=function(t){return arguments.length?(u=Sv.call(t,ba),e()):u.slice()},r.range=function(t){return arguments.length?(f=Ev.call(t),e()):f.slice()},r.rangeRound=function(t){return f=Ev.call(t),c=dn,e()},r.clamp=function(t){return arguments.length?(s=!!t,e()):s},r.interpolate=function(t){return arguments.length?(c=t,e()):c},e()}function Ta(n){var e=n.domain;return n.ticks=function(t){var n=e();return l(n[0],n[n.length-1],null==t?10:t)},n.tickFormat=function(n,r){return function(n,e,r){var i,o=n[0],a=n[n.length-1],u=d(o,a,null==e?10:e);switch((r=tr(null==r?",f":r)).type){case"s":var f=Math.max(Math.abs(o),Math.abs(a));return null!=r.precision||isNaN(i=ar(u,f))||(r.precision=i),t.formatPrefix(r,f);case"":case"e":case"g":case"p":case"r":null!=r.precision||isNaN(i=ur(u,Math.max(Math.abs(o),Math.abs(a))))||(r.precision=i-("e"===r.type));break;case"f":case"%":null!=r.precision||isNaN(i=or(u))||(r.precision=i-2*("%"===r.type))}return t.format(r)}(e(),n,r)},n.nice=function(t){null==t&&(t=10);var r,i=e(),o=0,a=i.length-1,u=i[o],f=i[a];return f0?r=h(u=Math.floor(u/r)*r,f=Math.ceil(f/r)*r,t):r<0&&(r=h(u=Math.ceil(u*r)/r,f=Math.floor(f*r)/r,t)),r>0?(i[o]=Math.floor(u/r)*r,i[a]=Math.ceil(f/r)*r,e(i)):r<0&&(i[o]=Math.ceil(u*r)/r,i[a]=Math.floor(f*r)/r,e(i)),n},n}function Na(){var t=Aa(ma,cn);return t.copy=function(){return Ma(t,Na())},Ta(t)}function Sa(){function t(t){return+t}var n=[0,1];return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=Sv.call(e,ba),t):n.slice()},t.copy=function(){return Sa().domain(n)},Ta(t)}function Ea(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a0){for(;df)break;g.push(h)}}else for(;d=1;--s)if(!((h=c*s)f)break;g.push(h)}}else g=l(d,p,Math.min(p-d,v)).map(a);return n?g.reverse():g},e.tickFormat=function(n,r){if(null==r&&(r=10===i?".0e":","),"function"!=typeof r&&(r=t.format(r)),n===1/0)return r;null==n&&(n=10);var u=Math.max(1,i*n/e.ticks().length);return function(t){var n=t/a(Math.round(o(t)));return n*i0?o[n-1]:r[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},t.copy=function(){return Ya().domain([e,r]).range(a)},Ta(t)}function Ba(){function t(t){if(t<=t)return e[Qc(n,t,0,r)]}var n=[.5],e=[0,1],r=1;return t.domain=function(i){return arguments.length?(n=Ev.call(i),r=Math.min(n.length,e.length-1),t):n.slice()},t.range=function(i){return arguments.length?(e=Ev.call(i),r=Math.min(n.length,e.length-1),t):e.slice()},t.invertExtent=function(t){var r=e.indexOf(t);return[n[r-1],n[r]]},t.copy=function(){return Ba().domain(n).range(e)},t}function Fa(t,n,e,r){function i(n){return t(n=new Date(+n)),n}return i.floor=i,i.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},i.round=function(t){var n=i(t),e=i.ceil(t);return t-n0))return u;do{u.push(a=new Date(+e)),n(e,o),t(e)}while(a=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return Pv.setTime(+n),zv.setTime(+r),t(Pv),t(zv),Math.floor(e(Pv,zv))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}function Ia(t){return Fa(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Dv)/Uv})}function ja(t){return Fa(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Uv})}function Ha(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Xa(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function Ga(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function Va(t){function n(t,n){return function(e){var r,i,o,a=[],u=-1,f=0,c=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in a||(a.w=1),"Z"in a?(i=(o=(i=Xa(Ga(a.y))).getUTCDay())>4||0===o?gg.ceil(i):gg(i),i=dg.offset(i,7*(a.V-1)),a.y=i.getUTCFullYear(),a.m=i.getUTCMonth(),a.d=i.getUTCDate()+(a.w+6)%7):(i=(o=(i=n(Ga(a.y))).getDay())>4||0===o?Gv.ceil(i):Gv(i),i=jv.offset(i,7*(a.V-1)),a.y=i.getFullYear(),a.m=i.getMonth(),a.d=i.getDate()+(a.w+6)%7)}else("W"in a||"U"in a)&&("w"in a||(a.w="u"in a?a.u%7:"W"in a?1:0),o="Z"in a?Xa(Ga(a.y)).getUTCDay():n(Ga(a.y)).getDay(),a.m=0,a.d="W"in a?(a.w+6)%7+7*a.W-(o+5)%7:a.w+7*a.U-(o+6)%7);return"Z"in a?(a.H+=a.Z/100|0,a.M+=a.Z%100,Xa(a)):n(a)}}function r(t,n,e,r){for(var i,o,a=0,u=n.length,f=e.length;a=f)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=A[i in Lg?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}var i=t.dateTime,o=t.date,a=t.time,u=t.periods,f=t.days,c=t.shortDays,s=t.months,l=t.shortMonths,h=Za(u),d=Qa(u),p=Za(f),v=Qa(f),g=Za(c),y=Qa(c),_=Za(s),b=Qa(s),m=Za(l),x=Qa(l),w={a:function(t){return c[t.getDay()]},A:function(t){return f[t.getDay()]},b:function(t){return l[t.getMonth()]},B:function(t){return s[t.getMonth()]},c:null,d:yu,e:yu,f:wu,H:_u,I:bu,j:mu,L:xu,m:Mu,M:Au,p:function(t){return u[+(t.getHours()>=12)]},Q:Ju,s:Ku,S:Tu,u:Nu,U:Su,V:Eu,w:ku,W:Cu,x:null,X:null,y:Pu,Y:zu,Z:Ru,"%":Qu},M={a:function(t){return c[t.getUTCDay()]},A:function(t){return f[t.getUTCDay()]},b:function(t){return l[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:Lu,e:Lu,f:Yu,H:Du,I:Uu,j:qu,L:Ou,m:Bu,M:Fu,p:function(t){return u[+(t.getUTCHours()>=12)]},Q:Ju,s:Ku,S:Iu,u:ju,U:Hu,V:Xu,w:Gu,W:Vu,x:null,X:null,y:$u,Y:Wu,Z:Zu,"%":Qu},A={a:function(t,n,e){var r=g.exec(n.slice(e));return r?(t.w=y[r[0].toLowerCase()],e+r[0].length):-1},A:function(t,n,e){var r=p.exec(n.slice(e));return r?(t.w=v[r[0].toLowerCase()],e+r[0].length):-1},b:function(t,n,e){var r=m.exec(n.slice(e));return r?(t.m=x[r[0].toLowerCase()],e+r[0].length):-1},B:function(t,n,e){var r=_.exec(n.slice(e));return r?(t.m=b[r[0].toLowerCase()],e+r[0].length):-1},c:function(t,n,e){return r(t,i,n,e)},d:uu,e:uu,f:du,H:cu,I:cu,j:fu,L:hu,m:au,M:su,p:function(t,n,e){var r=h.exec(n.slice(e));return r?(t.p=d[r[0].toLowerCase()],e+r[0].length):-1},Q:vu,s:gu,S:lu,u:Ka,U:tu,V:nu,w:Ja,W:eu,x:function(t,n,e){return r(t,o,n,e)},X:function(t,n,e){return r(t,a,n,e)},y:iu,Y:ru,Z:ou,"%":pu};return w.x=n(o,w),w.X=n(a,w),w.c=n(i,w),M.x=n(o,M),M.X=n(a,M),M.c=n(i,M),{format:function(t){var e=n(t+="",w);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",Ha);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",M);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,Xa);return n.toString=function(){return t},n}}}function $a(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function ou(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function au(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function uu(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function fu(t,n,e){var r=Dg.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function cu(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function su(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function lu(t,n,e){var r=Dg.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function hu(t,n,e){var r=Dg.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function du(t,n,e){var r=Dg.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function pu(t,n,e){var r=Ug.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function vu(t,n,e){var r=Dg.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function gu(t,n,e){var r=Dg.exec(n.slice(e));return r?(t.Q=1e3*+r[0],e+r[0].length):-1}function yu(t,n){return $a(t.getDate(),n,2)}function _u(t,n){return $a(t.getHours(),n,2)}function bu(t,n){return $a(t.getHours()%12||12,n,2)}function mu(t,n){return $a(1+jv.count(ug(t),t),n,3)}function xu(t,n){return $a(t.getMilliseconds(),n,3)}function wu(t,n){return xu(t,n)+"000"}function Mu(t,n){return $a(t.getMonth()+1,n,2)}function Au(t,n){return $a(t.getMinutes(),n,2)}function Tu(t,n){return $a(t.getSeconds(),n,2)}function Nu(t){var n=t.getDay();return 0===n?7:n}function Su(t,n){return $a(Xv.count(ug(t),t),n,2)}function Eu(t,n){var e=t.getDay();return t=e>=4||0===e?Wv(t):Wv.ceil(t),$a(Wv.count(ug(t),t)+(4===ug(t).getDay()),n,2)}function ku(t){return t.getDay()}function Cu(t,n){return $a(Gv.count(ug(t),t),n,2)}function Pu(t,n){return $a(t.getFullYear()%100,n,2)}function zu(t,n){return $a(t.getFullYear()%1e4,n,4)}function Ru(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+$a(n/60|0,"0",2)+$a(n%60,"0",2)}function Lu(t,n){return $a(t.getUTCDate(),n,2)}function Du(t,n){return $a(t.getUTCHours(),n,2)}function Uu(t,n){return $a(t.getUTCHours()%12||12,n,2)}function qu(t,n){return $a(1+dg.count(Pg(t),t),n,3)}function Ou(t,n){return $a(t.getUTCMilliseconds(),n,3)}function Yu(t,n){return Ou(t,n)+"000"}function Bu(t,n){return $a(t.getUTCMonth()+1,n,2)}function Fu(t,n){return $a(t.getUTCMinutes(),n,2)}function Iu(t,n){return $a(t.getUTCSeconds(),n,2)}function ju(t){var n=t.getUTCDay();return 0===n?7:n}function Hu(t,n){return $a(vg.count(Pg(t),t),n,2)}function Xu(t,n){var e=t.getUTCDay();return t=e>=4||0===e?bg(t):bg.ceil(t),$a(bg.count(Pg(t),t)+(4===Pg(t).getUTCDay()),n,2)}function Gu(t){return t.getUTCDay()}function Vu(t,n){return $a(gg.count(Pg(t),t),n,2)}function $u(t,n){return $a(t.getUTCFullYear()%100,n,2)}function Wu(t,n){return $a(t.getUTCFullYear()%1e4,n,4)}function Zu(){return"+0000"}function Qu(){return"%"}function Ju(t){return+t}function Ku(t){return Math.floor(+t/1e3)}function tf(n){return zg=Va(n),t.timeFormat=zg.format,t.timeParse=zg.parse,t.utcFormat=zg.utcFormat,t.utcParse=zg.utcParse,zg}function nf(t){return new Date(t)}function ef(t){return t instanceof Date?+t:+new Date(+t)}function rf(t,n,r,i,o,a,u,f,c){function s(e){return(u(e)=1?m_:t<=-1?-m_:Math.asin(t)}function lf(t){return t.innerRadius}function hf(t){return t.outerRadius}function df(t){return t.startAngle}function pf(t){return t.endAngle}function vf(t){return t&&t.padAngle}function gf(t,n,e,r,i,o,a){var u=t-e,f=n-r,c=(a?o:-o)/y_(u*u+f*f),s=c*f,l=-c*u,h=t+s,d=n+l,p=e+s,v=r+l,g=(h+p)/2,y=(d+v)/2,_=p-h,b=v-d,m=_*_+b*b,x=i-o,w=h*v-p*d,M=(b<0?-1:1)*y_(p_(0,x*x*m-w*w)),A=(w*b-_*M)/m,T=(-w*_-b*M)/m,N=(w*b+_*M)/m,S=(-w*_+b*M)/m,E=A-g,k=T-y,C=N-g,P=S-y;return E*E+k*k>C*C+P*P&&(A=N,T=S),{cx:A,cy:T,x01:-s,y01:-l,x11:A*(i/x-1),y11:T*(i/x-1)}}function yf(t){this._context=t}function _f(t){return new yf(t)}function bf(t){return t[0]}function mf(t){return t[1]}function xf(){function t(t){var u,f,c,s=t.length,l=!1;for(null==i&&(a=o(c=ie())),u=0;u<=s;++u)!(u=s;--l)c.point(g[l],y[l]);c.lineEnd(),c.areaEnd()}v&&(g[n]=+e(h,n,t),y[n]=+i(h,n,t),c.point(r?+r(h,n,t):g[n],o?+o(h,n,t):y[n]))}if(d)return c=null,d+""||null}function n(){return xf().defined(a).curve(f).context(u)}var e=bf,r=null,i=cf(0),o=mf,a=cf(!0),u=null,f=_f,c=null;return t.x=function(n){return arguments.length?(e="function"==typeof n?n:cf(+n),r=null,t):e},t.x0=function(n){return arguments.length?(e="function"==typeof n?n:cf(+n),t):e},t.x1=function(n){return arguments.length?(r=null==n?null:"function"==typeof n?n:cf(+n),t):r},t.y=function(n){return arguments.length?(i="function"==typeof n?n:cf(+n),o=null,t):i},t.y0=function(n){return arguments.length?(i="function"==typeof n?n:cf(+n),t):i},t.y1=function(n){return arguments.length?(o=null==n?null:"function"==typeof n?n:cf(+n),t):o},t.lineX0=t.lineY0=function(){return n().x(e).y(i)},t.lineY1=function(){return n().x(e).y(o)},t.lineX1=function(){return n().x(r).y(i)},t.defined=function(n){return arguments.length?(a="function"==typeof n?n:cf(!!n),t):a},t.curve=function(n){return arguments.length?(f=n,null!=u&&(c=f(u)),t):f},t.context=function(n){return arguments.length?(null==n?u=c=null:c=f(u=n),t):u},t}function Mf(t,n){return nt?1:n>=t?0:NaN}function Af(t){return t}function Tf(t){this._curve=t}function Nf(t){function n(n){return new Tf(t(n))}return n._curve=t,n}function Sf(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Nf(t)):n()._curve},t}function Ef(){return Sf(xf().curve(w_))}function kf(){var t=wf().curve(w_),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Sf(e())},delete t.lineX0,t.lineEndAngle=function(){return Sf(r())},delete t.lineX1,t.lineInnerRadius=function(){return Sf(i())},delete t.lineY0,t.lineOuterRadius=function(){return Sf(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Nf(t)):n()._curve},t}function Cf(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}function Pf(t){return t.source}function zf(t){return t.target}function Rf(t){function n(){var n,u=M_.call(arguments),f=e.apply(this,u),c=r.apply(this,u);if(a||(a=n=ie()),t(a,+i.apply(this,(u[0]=f,u)),+o.apply(this,u),+i.apply(this,(u[0]=c,u)),+o.apply(this,u)),n)return a=null,n+""||null}var e=Pf,r=zf,i=bf,o=mf,a=null;return n.source=function(t){return arguments.length?(e=t,n):e},n.target=function(t){return arguments.length?(r=t,n):r},n.x=function(t){return arguments.length?(i="function"==typeof t?t:cf(+t),n):i},n.y=function(t){return arguments.length?(o="function"==typeof t?t:cf(+t),n):o},n.context=function(t){return arguments.length?(a=null==t?null:t,n):a},n}function Lf(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n=(n+r)/2,e,n,i,r,i)}function Df(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n,e=(e+i)/2,r,e,r,i)}function Uf(t,n,e,r,i){var o=Cf(n,e),a=Cf(n,e=(e+i)/2),u=Cf(r,e),f=Cf(r,i);t.moveTo(o[0],o[1]),t.bezierCurveTo(a[0],a[1],u[0],u[1],f[0],f[1])}function qf(){}function Of(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function Yf(t){this._context=t}function Bf(t){this._context=t}function Ff(t){this._context=t}function If(t,n){this._basis=new Yf(t),this._beta=n}function jf(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Hf(t,n){this._context=t,this._k=(1-n)/6}function Xf(t,n){this._context=t,this._k=(1-n)/6}function Gf(t,n){this._context=t,this._k=(1-n)/6}function Vf(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>__){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,f=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/f,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/f}if(t._l23_a>__){var c=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*c+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*c+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function $f(t,n){this._context=t,this._alpha=n}function Wf(t,n){this._context=t,this._alpha=n}function Zf(t,n){this._context=t,this._alpha=n}function Qf(t){this._context=t}function Jf(t){return t<0?-1:1}function Kf(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(Jf(o)+Jf(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function tc(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function nc(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function ec(t){this._context=t}function rc(t){this._context=new ic(t)}function ic(t){this._context=t}function oc(t){this._context=t}function ac(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function sc(t,n){return t[n]}function lc(t){var n=t.map(hc);return cc(t).sort(function(t,e){return n[t]-n[e]})}function hc(t){for(var n,e=0,r=-1,i=t.length;++r0)){if(o/=h,h<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=r-f,h||!(o<0)){if(o/=h,h<0){if(o>l)return;o>s&&(s=o)}else if(h>0){if(o0)){if(o/=d,d<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=i-c,d||!(o<0)){if(o/=d,d<0){if(o>l)return;o>s&&(s=o)}else if(d>0){if(o0||l<1)||(s>0&&(t[0]=[f+s*h,c+s*d]),l<1&&(t[1]=[f+l*h,c+l*d]),!0)}}}}}function Tc(t,n,e,r,i){var o=t[1];if(o)return!0;var a,u,f=t[0],c=t.left,s=t.right,l=c[0],h=c[1],d=s[0],p=s[1],v=(l+d)/2,g=(h+p)/2;if(p===h){if(v=r)return;if(l>d){if(f){if(f[1]>=i)return}else f=[v,e];o=[v,i]}else{if(f){if(f[1]1)if(l>d){if(f){if(f[1]>=i)return}else f=[(e-u)/a,e];o=[(i-u)/a,i]}else{if(f){if(f[1]=r)return}else f=[n,a*n+u];o=[r,a*r+u]}else{if(f){if(f[0]=-eb)){var d=f*f+c*c,p=s*s+l*l,v=(l*d-c*p)/h,g=(f*p-s*d)/h,y=K_.pop()||new function(){yc(this),this.x=this.y=this.arc=this.site=this.cy=null};y.arc=t,y.site=i,y.x=v+a,y.y=(y.cy=g+u)+Math.sqrt(v*v+g*g),t.circle=y;for(var _=null,b=Q_._;b;)if(y.ynb)u=u.L;else{if(!((i=o-function(t,n){var e=t.N;if(e)return Dc(e,n);var r=t.site;return r[1]===n?r[0]:1/0}(u,a))>nb)){r>-nb?(n=u.P,e=u):i>-nb?(n=u,e=u.N):n=e=u;break}if(!u.R){n=u;break}u=u.R}(function(t){Z_[t.index]={site:t,halfedges:[]}})(t);var f=Pc(t);if(W_.insert(n,f),n||e){if(n===e)return Cc(n),e=Pc(n.site),W_.insert(f,e),f.edge=e.edge=xc(n.site,f.site),kc(n),void kc(e);if(e){Cc(n),Cc(e);var c=n.site,s=c[0],l=c[1],h=t[0]-s,d=t[1]-l,p=e.site,v=p[0]-s,g=p[1]-l,y=2*(h*g-d*v),_=h*h+d*d,b=v*v+g*g,m=[(g*_-d*b)/y+s,(h*b-v*_)/y+l];Mc(e.edge,c,p,m),f.edge=xc(c,t,null,m),e.edge=xc(t,p,null,m),kc(n),kc(e)}else f.edge=xc(n.site,f.site)}}function Dc(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var a=t.P;if(!a)return-1/0;var u=(e=a.site)[0],f=e[1],c=f-n;if(!c)return u;var s=u-r,l=1/o-1/c,h=s/c;return l?(-h+Math.sqrt(h*h-2*l*(s*s/(-2*c)-f+c/2+i-o/2)))/l+r:(r+u)/2}function Uc(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function qc(t,n){return n[1]-t[1]||n[0]-t[0]}function Oc(t,n){var e,r,i,o=t.sort(qc).pop();for(J_=[],Z_=new Array(t.length),W_=new gc,Q_=new gc;;)if(i=$_,o&&(!i||o[1]nb||Math.abs(i[0][1]-i[1][1])>nb)||delete J_[o]})(a,u,f,c),function(t,n,e,r){var i,o,a,u,f,c,s,l,h,d,p,v,g=Z_.length,y=!0;for(i=0;inb||Math.abs(v-h)>nb)&&(f.splice(u,0,J_.push(wc(a,d,Math.abs(p-t)nb?[t,Math.abs(l-t)nb?[Math.abs(h-r)nb?[e,Math.abs(l-e)nb?[Math.abs(h-n)r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}var Zc=e(n),Qc=Zc.right,Jc=Zc.left,Kc=Array.prototype,ts=Kc.slice,ns=Kc.map,es=Math.sqrt(50),rs=Math.sqrt(10),is=Math.sqrt(2),os=Array.prototype.slice,as=1,us=2,fs=3,cs=4,ss=1e-6,ls={value:function(){}};S.prototype=N.prototype={constructor:S,on:function(t,n){var e,r=this._,i=function(t,n){return t.trim().split(/^|\s+/).map(function(t){var e="",r=t.indexOf(".");if(r>=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}(t+"",r),o=-1,a=i.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o0)for(var e,r,i=new Array(e),o=0;o=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var bs={};if(t.event=null,"undefined"!=typeof document){"onmouseenter"in document.documentElement||(bs={mouseenter:"mouseover",mouseleave:"mouseout"})}var ms=[null];ut.prototype=ft.prototype={constructor:ut,select:function(t){"function"!=typeof t&&(t=z(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=m&&(m=b+1);!(_=g[m])&&++m=0;)(r=i[o])&&(a&&a!==r.nextSibling&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=Y);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?function(t){return function(){this.style.removeProperty(t)}}:"function"==typeof n?function(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}:function(t,n,e){return function(){this.style.setProperty(t,n,e)}})(t,n,null==e?"":e)):F(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?function(t){return function(){delete this[t]}}:"function"==typeof n?function(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}:function(t,n){return function(){this[t]=n}})(t,n)):this.node()[t]},classed:function(t,n){var e=I(t+"");if(arguments.length<2){for(var r=j(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?it:rt,null==e&&(e=!1),r=0;r=240?t-240:t+120,i,r),Ot(t,i,r),Ot(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Ls=Math.PI/180,Ds=180/Math.PI,Us=.96422,qs=1,Os=.82521,Ys=4/29,Bs=6/29,Fs=3*Bs*Bs,Is=Bs*Bs*Bs;Nt(Ft,Bt,St(Et,{brighter:function(t){return new Ft(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Ft(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return n=Us*jt(n),t=qs*jt(t),e=Os*jt(e),new Lt(Ht(3.1338561*n-1.6168667*t-.4906146*e),Ht(-.9787684*n+1.9161415*t+.033454*e),Ht(.0719453*n-.2289914*t+1.4052427*e),this.opacity)}})),Nt($t,Vt,St(Et,{brighter:function(t){return new $t(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new $t(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return Yt(this).rgb()}}));var js=-.29227,Hs=-.90649,Xs=1.97294,Gs=Xs*Hs,Vs=1.78277*Xs,$s=1.78277*js- -.14861*Hs;Nt(Zt,Wt,St(Et,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new Zt(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new Zt(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Ls,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new Lt(255*(n+e*(-.14861*r+1.78277*i)),255*(n+e*(js*r+Hs*i)),255*(n+e*(Xs*r)),this.opacity)}}));var Ws,Zs,Qs,Js,Ks,tl,nl=function t(n){function e(t,n){var e=r((t=Rt(t)).r,(n=Rt(n)).r),i=r(t.g,n.g),o=r(t.b,n.b),a=on(t.opacity,n.opacity);return function(n){return t.r=e(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}var r=rn(n);return e.gamma=t,e}(1),el=an(Jt),rl=an(Kt),il=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,ol=new RegExp(il.source,"g"),al=180/Math.PI,ul={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1},fl=vn(function(t){return"none"===t?ul:(Ws||(Ws=document.createElement("DIV"),Zs=document.documentElement,Qs=document.defaultView),Ws.style.transform=t,t=Qs.getComputedStyle(Zs.appendChild(Ws),null).getPropertyValue("transform"),Zs.removeChild(Ws),t=t.slice(7,-1).split(","),pn(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))},"px, ","px)","deg)"),cl=vn(function(t){return null==t?ul:(Js||(Js=document.createElementNS("http://www.w3.org/2000/svg","g")),Js.setAttribute("transform",t),(t=Js.transform.baseVal.consolidate())?(t=t.matrix,pn(t.a,t.b,t.c,t.d,t.e,t.f)):ul)},", ",")",")"),sl=Math.SQRT2,ll=2,hl=4,dl=1e-12,pl=_n(en),vl=_n(on),gl=bn(en),yl=bn(on),_l=mn(en),bl=mn(on),ml=0,xl=0,wl=0,Ml=1e3,Al=0,Tl=0,Nl=0,Sl="object"==typeof performance&&performance.now?performance:Date,El="object"==typeof window&&window.requestAnimationFrame?window.requestAnimationFrame.bind(window):function(t){setTimeout(t,17)};Mn.prototype=An.prototype={constructor:Mn,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?xn():+e)+(null==n?0:+n),this._next||tl===this||(tl?tl._next=this:Ks=this,tl=this),this._call=t,this._time=e,En()},stop:function(){this._call&&(this._call=null,this._time=1/0,En())}};var kl=N("start","end","interrupt"),Cl=[],Pl=0,zl=1,Rl=2,Ll=3,Dl=4,Ul=5,ql=6,Ol=ft.prototype.constructor,Yl=0,Bl=ft.prototype;qn.prototype=On.prototype={constructor:qn,select:function(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=z(t));for(var r=this._groups,i=r.length,o=new Array(i),a=0;a=0&&(t=t.slice(0,n)),!t||"start"===t})}(n)?Pn:zn;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=k(t),r="transform"===e?cl:Un;return this.attrTween(t,"function"==typeof n?(e.local?function(t,n,e){var r,i,o;return function(){var a,u=e(this);if(null!=u)return(a=this.getAttributeNS(t.space,t.local))===u?null:a===r&&u===i?o:o=n(r=a,i=u);this.removeAttributeNS(t.space,t.local)}}:function(t,n,e){var r,i,o;return function(){var a,u=e(this);if(null!=u)return(a=this.getAttribute(t))===u?null:a===r&&u===i?o:o=n(r=a,i=u);this.removeAttribute(t)}})(e,r,Dn(this,"attr."+t,n)):null==n?(e.local?function(t){return function(){this.removeAttributeNS(t.space,t.local)}}:function(t){return function(){this.removeAttribute(t)}})(e):(e.local?function(t,n,e){var r,i;return function(){var o=this.getAttributeNS(t.space,t.local);return o===e?null:o===r?i:i=n(r=o,e)}}:function(t,n,e){var r,i;return function(){var o=this.getAttribute(t);return o===e?null:o===r?i:i=n(r=o,e)}})(e,r,n+""))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=k(t);return this.tween(e,(r.local?function(t,n){function e(){var e=this,r=n.apply(e,arguments);return r&&function(n){e.setAttributeNS(t.space,t.local,r(n))}}return e._value=n,e}:function(t,n){function e(){var e=this,r=n.apply(e,arguments);return r&&function(n){e.setAttribute(t,r(n))}}return e._value=n,e})(r,n))},style:function(t,n,e){var r="transform"==(t+="")?fl:Un;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=F(this,t),a=(this.style.removeProperty(t),F(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,function(t){return function(){this.style.removeProperty(t)}}(t)):this.styleTween(t,"function"==typeof n?function(t,n,e){var r,i,o;return function(){var a=F(this,t),u=e(this);return null==u&&(this.style.removeProperty(t),u=F(this,t)),a===u?null:a===r&&u===i?o:o=n(r=a,i=u)}}(t,r,Dn(this,"style."+t,n)):function(t,n,e){var r,i;return function(){var o=F(this,t);return o===e?null:o===r?i:i=n(r=o,e)}}(t,r,n+""),e)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Dn(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Rn(this.node(),e).tween,o=0,a=i.length;o1e-6)if(Math.abs(s*u-f*c)>1e-6&&i){var h=e-o,d=r-a,p=u*u+f*f,v=h*h+d*d,g=Math.sqrt(p),y=Math.sqrt(l),_=i*Math.tan((Ch-Math.acos((p+l-v)/(2*g*y)))/2),b=_/y,m=_/g;Math.abs(b-1)>1e-6&&(this._+="L"+(t+b*c)+","+(n+b*s)),this._+="A"+i+","+i+",0,0,"+ +(s*h>c*d)+","+(this._x1=t+m*u)+","+(this._y1=n+m*f)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n;var a=(e=+e)*Math.cos(r),u=e*Math.sin(r),f=t+a,c=n+u,s=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+f+","+c:(Math.abs(this._x1-f)>1e-6||Math.abs(this._y1-c)>1e-6)&&(this._+="L"+f+","+c),e&&(l<0&&(l=l%Ph+Ph),l>zh?this._+="A"+e+","+e+",0,1,"+s+","+(t-a)+","+(n-u)+"A"+e+","+e+",0,1,"+s+","+(this._x1=f)+","+(this._y1=c):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Ch)+","+s+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};se.prototype=le.prototype={constructor:se,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var Rh=le.prototype;ge.prototype=ye.prototype={constructor:ge,has:Rh.has,add:function(t){return t+="",this["$"+t]=t,this},remove:Rh.remove,clear:Rh.clear,values:Rh.keys,size:Rh.size,empty:Rh.empty,each:Rh.each};var Lh=Array.prototype.slice,Dh=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]],Uh={},qh={},Oh=34,Yh=10,Bh=13,Fh=Ee(","),Ih=Fh.parse,jh=Fh.parseRows,Hh=Fh.format,Xh=Fh.formatRows,Gh=Ee("\t"),Vh=Gh.parse,$h=Gh.parseRows,Wh=Gh.format,Zh=Gh.formatRows,Qh=Re(Ih),Jh=Re(Vh),Kh=De("application/xml"),td=De("text/html"),nd=De("image/svg+xml"),ed=Ie.prototype=je.prototype;ed.copy=function(){var t,n,e=new je(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=He(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=He(n));return e},ed.add=function(t){var n=+this._x.call(null,t),e=+this._y.call(null,t);return Oe(this.cover(n,e),n,e,t)},ed.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),f=1/0,c=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));for(st||t>i||r>n||n>o))return this;var a,u,f=i-e,c=this._root;switch(u=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do{a=new Array(4),a[u]=c,c=a}while(f*=2,i=e+f,o=r+f,t>i||n>o);break;case 1:do{a=new Array(4),a[u]=c,c=a}while(f*=2,e=i-f,o=r+f,e>t||n>o);break;case 2:do{a=new Array(4),a[u]=c,c=a}while(f*=2,i=e+f,r=o-f,t>i||r>n);break;case 3:do{a=new Array(4),a[u]=c,c=a}while(f*=2,e=i-f,r=o-f,e>t||r>n)}this._root&&this._root.length&&(this._root=c)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this},ed.data=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},ed.extent=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},ed.find=function(t,n,e){var r,i,o,a,u,f,c,s=this._x0,l=this._y0,h=this._x1,d=this._y1,p=[],v=this._root;for(v&&p.push(new Ye(v,s,l,h,d)),null==e?e=1/0:(s=t-e,l=n-e,h=t+e,d=n+e,e*=e);f=p.pop();)if(!(!(v=f.node)||(i=f.x0)>h||(o=f.y0)>d||(a=f.x1)=y)<<1|t>=g)&&(f=p[p.length-1],p[p.length-1]=p[p.length-1-c],p[p.length-1-c]=f)}else{var _=t-+this._x.call(null,v.data),b=n-+this._y.call(null,v.data),m=_*_+b*b;if(m=(u=(p+g)/2))?p=u:g=u,(s=a>=(f=(v+y)/2))?v=f:y=f,n=d,!(d=d[l=s<<1|c]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},ed.removeAll=function(t){for(var n=0,e=t.length;n0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Ke(100*t,n)},r:Ke,s:function(t,n){var e=Qe(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(rd=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Qe(t,Math.max(0,n+o-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},ud=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i;tr.prototype=nr.prototype,nr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var fd,cd=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];ir({decimal:".",thousands:",",grouping:[3],currency:["$",""]}),cr.prototype={constructor:cr,reset:function(){this.s=this.t=0},add:function(t){sr(Fd,t,this.t),sr(this,Fd.s,this.s),this.s?this.t+=Fd.t:this.s=Fd.t},valueOf:function(){return this.s}};var sd,ld,hd,dd,pd,vd,gd,yd,_d,bd,md,xd,wd,Md,Ad,Td,Nd,Sd,Ed,kd,Cd,Pd,zd,Rd,Ld,Dd,Ud,qd,Od,Yd,Bd,Fd=new cr,Id=1e-6,jd=1e-12,Hd=Math.PI,Xd=Hd/2,Gd=Hd/4,Vd=2*Hd,$d=180/Hd,Wd=Hd/180,Zd=Math.abs,Qd=Math.atan,Jd=Math.atan2,Kd=Math.cos,tp=Math.ceil,np=Math.exp,ep=Math.log,rp=Math.pow,ip=Math.sin,op=Math.sign||function(t){return t>0?1:t<0?-1:0},ap=Math.sqrt,up=Math.tan,fp={Feature:function(t,n){vr(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++rId?_d=90:dp<-Id&&(gd=-90),Ad[0]=vd,Ad[1]=yd}},vp={sphere:pr,point:Br,lineStart:Ir,lineEnd:Xr,polygonStart:function(){vp.lineStart=Gr,vp.lineEnd=Vr},polygonEnd:function(){vp.lineStart=Ir,vp.lineEnd=Xr}};Jr.invert=Jr;var gp,yp,_p,bp,mp,xp,wp,Mp,Ap,Tp,Np,Sp=fr(),Ep=hi(function(){return!0},function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?Hd:-Hd,f=Zd(o-e);Zd(f-Hd)0?Xd:-Xd),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&f>=Hd&&(Zd(e-i)Id?Qd((ip(n)*(o=Kd(r))*ip(e)-ip(r)*(i=Kd(n))*ip(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}},function(t,n,e,r){var i;if(null==t)i=e*Xd,r.point(-Hd,i),r.point(0,i),r.point(Hd,i),r.point(Hd,0),r.point(Hd,-i),r.point(0,-i),r.point(-Hd,-i),r.point(-Hd,0),r.point(-Hd,i);else if(Zd(t[0]-n[0])>Id){var o=t[0]Ip&&(Ip=t),njp&&(jp=n)},lineStart:pr,lineEnd:pr,polygonStart:pr,polygonEnd:pr,result:function(){var t=[[Bp,Fp],[Ip,jp]];return Ip=jp=-(Fp=Bp=1/0),t}},Xp=0,Gp=0,Vp=0,$p=0,Wp=0,Zp=0,Qp=0,Jp=0,Kp=0,tv={point:Ui,lineStart:qi,lineEnd:Bi,polygonStart:function(){tv.lineStart=Fi,tv.lineEnd=Ii},polygonEnd:function(){tv.point=Ui,tv.lineStart=qi,tv.lineEnd=Bi},result:function(){var t=Kp?[Qp/Kp,Jp/Kp]:Zp?[$p/Zp,Wp/Zp]:Vp?[Xp/Vp,Gp/Vp]:[NaN,NaN];return Xp=Gp=Vp=$p=Wp=Zp=Qp=Jp=Kp=0,t}};Xi.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,Vd)}},result:pr};var nv,ev,rv,iv,ov,av=fr(),uv={point:pr,lineStart:function(){uv.point=Gi},lineEnd:function(){nv&&Vi(ev,rv),uv.point=pr},polygonStart:function(){nv=!0},polygonEnd:function(){nv=null},result:function(){var t=+av;return av.reset(),t}};$i.prototype={_radius:4.5,_circle:Wi(4.5),pointRadius:function(t){return(t=+t)!==this._radius&&(this._radius=t,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:null==this._circle&&(this._circle=Wi(this._radius)),this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}return null}},Qi.prototype={constructor:Qi,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var fv=16,cv=Kd(30*Wd),sv=Zi({point:function(t,n){this.stream.point(t*Wd,n*Wd)}}),lv=lo(function(t){return ap(2/(1+t))});lv.invert=ho(function(t){return 2*hr(t/2)});var hv=lo(function(t){return(t=lr(t))&&t/ip(t)});hv.invert=ho(function(t){return t}),po.invert=function(t,n){return[t,2*Qd(np(n))-Xd]},_o.invert=_o,mo.invert=ho(Qd),wo.invert=function(t,n){var e,r=n,i=25;do{var o=r*r,a=o*o;r-=e=(r*(1.007226+o*(.015085+a*(.028874*o-.044475-.005916*a)))-n)/(1.007226+o*(.045255+a*(.259866*o-.311325-.005916*11*a)))}while(Zd(e)>Id&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},Mo.invert=ho(hr),Ao.invert=ho(function(t){return 2*Qd(t)}),To.invert=function(t,n){return[-n,2*Qd(np(t))-Xd]},Lo.prototype=Co.prototype={constructor:Lo,count:function(){return this.eachAfter(ko)},each:function(t){var n,e,r,i,o=this,a=[o];do{for(n=a.reverse(),a=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},sum:function(t){return this.eachAfter(function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e})},sort:function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){var t=[];return this.each(function(n){t.push(n)}),t},leaves:function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},links:function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n},copy:function(){return Co(this).eachBefore(zo)}};var dv=Array.prototype.slice,pv="$",vv={depth:-1},gv={};fa.prototype=Object.create(Lo.prototype);var yv=(1+Math.sqrt(5))/2,_v=function t(n){function e(t,e,r,i,o){sa(n,t,e,r,i,o)}return e.ratio=function(n){return t((n=+n)>1?n:1)},e}(yv),bv=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,f,c,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(yv),mv=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(pa),xv=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(pa),wv=function t(n){function e(){var t=xv.source(n).apply(this,arguments);return function(){return Math.exp(t())}}return e.source=t,e}(pa),Mv=function t(n){function e(t){return function(){for(var e=0,r=0;r0?t>1?Fa(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Rv:null};var Lv=Rv.range,Dv=6e4,Uv=6048e5,qv=Fa(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),Ov=qv.range,Yv=Fa(function(t){t.setTime(Math.floor(t/Dv)*Dv)},function(t,n){t.setTime(+t+n*Dv)},function(t,n){return(n-t)/Dv},function(t){return t.getMinutes()}),Bv=Yv.range,Fv=Fa(function(t){var n=t.getTimezoneOffset()*Dv%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),Iv=Fv.range,jv=Fa(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Dv)/864e5},function(t){return t.getDate()-1}),Hv=jv.range,Xv=Ia(0),Gv=Ia(1),Vv=Ia(2),$v=Ia(3),Wv=Ia(4),Zv=Ia(5),Qv=Ia(6),Jv=Xv.range,Kv=Gv.range,tg=Vv.range,ng=$v.range,eg=Wv.range,rg=Zv.range,ig=Qv.range,og=Fa(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ag=og.range,ug=Fa(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});ug.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Fa(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var fg=ug.range,cg=Fa(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Dv)},function(t,n){return(n-t)/Dv},function(t){return t.getUTCMinutes()}),sg=cg.range,lg=Fa(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),hg=lg.range,dg=Fa(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),pg=dg.range,vg=ja(0),gg=ja(1),yg=ja(2),_g=ja(3),bg=ja(4),mg=ja(5),xg=ja(6),wg=vg.range,Mg=gg.range,Ag=yg.range,Tg=_g.range,Ng=bg.range,Sg=mg.range,Eg=xg.range,kg=Fa(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),Cg=kg.range,Pg=Fa(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Pg.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Fa(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var zg,Rg=Pg.range,Lg={"-":"",_:" ",0:"0"},Dg=/^\s*\d+/,Ug=/^%/,qg=/[\\^$*+?|[\]().{}]/g;tf({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Og="%Y-%m-%dT%H:%M:%S.%LZ",Yg=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat(Og),Bg=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse(Og),Fg=1e3,Ig=60*Fg,jg=60*Ig,Hg=24*jg,Xg=7*Hg,Gg=30*Hg,Vg=365*Hg,$g=af("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Wg=af("7fc97fbeaed4fdc086ffff99386cb0f0027fbf5b17666666"),Zg=af("1b9e77d95f027570b3e7298a66a61ee6ab02a6761d666666"),Qg=af("a6cee31f78b4b2df8a33a02cfb9a99e31a1cfdbf6fff7f00cab2d66a3d9affff99b15928"),Jg=af("fbb4aeb3cde3ccebc5decbe4fed9a6ffffcce5d8bdfddaecf2f2f2"),Kg=af("b3e2cdfdcdaccbd5e8f4cae4e6f5c9fff2aef1e2cccccccc"),ty=af("e41a1c377eb84daf4a984ea3ff7f00ffff33a65628f781bf999999"),ny=af("66c2a5fc8d628da0cbe78ac3a6d854ffd92fe5c494b3b3b3"),ey=af("8dd3c7ffffb3bebadafb807280b1d3fdb462b3de69fccde5d9d9d9bc80bdccebc5ffed6f"),ry=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(af),iy=uf(ry),oy=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(af),ay=uf(oy),uy=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(af),fy=uf(uy),cy=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(af),sy=uf(cy),ly=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(af),hy=uf(ly),dy=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(af),py=uf(dy),vy=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(af),gy=uf(vy),yy=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(af),_y=uf(yy),by=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(af),my=uf(by),xy=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(af),wy=uf(xy),My=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(af),Ay=uf(My),Ty=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(af),Ny=uf(Ty),Sy=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(af),Ey=uf(Sy),ky=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(af),Cy=uf(ky),Py=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(af),zy=uf(Py),Ry=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(af),Ly=uf(Ry),Dy=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(af),Uy=uf(Dy),qy=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(af),Oy=uf(qy),Yy=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(af),By=uf(Yy),Fy=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(af),Iy=uf(Fy),jy=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(af),Hy=uf(jy),Xy=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(af),Gy=uf(Xy),Vy=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(af),$y=uf(Vy),Wy=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(af),Zy=uf(Wy),Qy=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(af),Jy=uf(Qy),Ky=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(af),t_=uf(Ky),n_=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(af),e_=uf(n_),r_=bl(Wt(300,.5,0),Wt(-240,.5,1)),i_=bl(Wt(-100,.75,.35),Wt(80,1.5,.8)),o_=bl(Wt(260,.75,.35),Wt(80,1.5,.8)),a_=Wt(),u_=ff(af("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),f_=ff(af("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),c_=ff(af("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),s_=ff(af("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921")),l_=Math.abs,h_=Math.atan2,d_=Math.cos,p_=Math.max,v_=Math.min,g_=Math.sin,y_=Math.sqrt,__=1e-12,b_=Math.PI,m_=b_/2,x_=2*b_;yf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var w_=Nf(_f);Tf.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var M_=Array.prototype.slice,A_={draw:function(t,n){var e=Math.sqrt(n/b_);t.moveTo(e,0),t.arc(0,0,e,0,x_)}},T_={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},N_=Math.sqrt(1/3),S_=2*N_,E_={draw:function(t,n){var e=Math.sqrt(n/S_),r=e*N_;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},k_=Math.sin(b_/10)/Math.sin(7*b_/10),C_=Math.sin(x_/10)*k_,P_=-Math.cos(x_/10)*k_,z_={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=C_*e,i=P_*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var a=x_*o/5,u=Math.cos(a),f=Math.sin(a);t.lineTo(f*e,-u*e),t.lineTo(u*r-f*i,f*r+u*i)}t.closePath()}},R_={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},L_=Math.sqrt(3),D_={draw:function(t,n){var e=-Math.sqrt(n/(3*L_));t.moveTo(0,2*e),t.lineTo(-L_*e,-e),t.lineTo(L_*e,-e),t.closePath()}},U_=Math.sqrt(3)/2,q_=1/Math.sqrt(12),O_=3*(q_/2+1),Y_={draw:function(t,n){var e=Math.sqrt(n/O_),r=e/2,i=e*q_,o=r,a=e*q_+e,u=-o,f=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,f),t.lineTo(-.5*r-U_*i,U_*r+-.5*i),t.lineTo(-.5*o-U_*a,U_*o+-.5*a),t.lineTo(-.5*u-U_*f,U_*u+-.5*f),t.lineTo(-.5*r+U_*i,-.5*i-U_*r),t.lineTo(-.5*o+U_*a,-.5*a-U_*o),t.lineTo(-.5*u+U_*f,-.5*f-U_*u),t.closePath()}},B_=[A_,T_,E_,R_,z_,D_,Y_];Yf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Of(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Of(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Bf.prototype={areaStart:qf,areaEnd:qf,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:Of(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},Ff.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:Of(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},If.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,f=-1;++f<=e;)r=f/e,this._basis.point(this._beta*t[f]+(1-this._beta)*(i+r*a),this._beta*n[f]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var F_=function t(n){function e(t){return 1===n?new Yf(t):new If(t,n)}return e.beta=function(n){return t(+n)},e}(.85);Hf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:jf(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:jf(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var I_=function t(n){function e(t){return new Hf(t,n)}return e.tension=function(n){return t(+n)},e}(0);Xf.prototype={areaStart:qf,areaEnd:qf,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:jf(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var j_=function t(n){function e(t){return new Xf(t,n)}return e.tension=function(n){return t(+n)},e}(0);Gf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:jf(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var H_=function t(n){function e(t){return new Gf(t,n)}return e.tension=function(n){return t(+n)},e}(0);$f.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Vf(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var X_=function t(n){function e(t){return n?new $f(t,n):new Hf(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Wf.prototype={areaStart:qf,areaEnd:qf,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Vf(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var G_=function t(n){function e(t){return n?new Wf(t,n):new Xf(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Zf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Vf(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var V_=function t(n){function e(t){return n?new Zf(t,n):new Gf(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Qf.prototype={areaStart:qf,areaEnd:qf,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}},ec.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:nc(this,this._t0,tc(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,nc(this,tc(this,e=Kf(this,t,n)),e);break;default:nc(this,this._t0,e=Kf(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(rc.prototype=Object.create(ec.prototype)).point=function(t,n){ec.prototype.point.call(this,n,t)},ic.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,r,i,o){this._context.bezierCurveTo(n,t,r,e,o,i)}},oc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,e=t.length;if(e)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===e)this._context.lineTo(t[1],n[1]);else for(var r=ac(t),i=ac(n),o=0,a=1;a=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}},gc.prototype={constructor:gc,insert:function(t,n){var e,r,i;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=mc(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)e===(r=e.U).L?(i=r.R)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.R&&(_c(this,e),e=(t=e).U),e.C=!1,r.C=!0,bc(this,r)):(i=r.L)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.L&&(bc(this,e),e=(t=e).U),e.C=!1,r.C=!0,_c(this,r)),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,r,i=t.U,o=t.L,a=t.R;if(e=o?a?mc(a):o:a,i?i.L===t?i.L=e:i.R=e:this._=e,o&&a?(r=e.C,e.C=t.C,e.L=o,o.U=e,e!==a?(i=e.U,e.U=t.U,t=e.R,i.L=t,e.R=a,a.U=e):(e.U=i,i=e,t=e.R)):(r=t.C,t=e),t&&(t.U=i),!r)if(t&&t.C)t.C=!1;else{do{if(t===this._)break;if(t===i.L){if((n=i.R).C&&(n.C=!1,i.C=!0,_c(this,i),n=i.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,bc(this,n),n=i.R),n.C=i.C,i.C=n.R.C=!1,_c(this,i),t=this._;break}}else if((n=i.L).C&&(n.C=!1,i.C=!0,bc(this,i),n=i.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,_c(this,n),n=i.L),n.C=i.C,i.C=n.L.C=!1,bc(this,i),t=this._;break}n.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};var $_,W_,Z_,Q_,J_,K_=[],tb=[],nb=1e-6,eb=1e-12;Oc.prototype={constructor:Oc,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return Sc(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,r){if(o=(i=e.halfedges).length)for(var i,o,a,u=e.site,f=-1,c=n[i[o-1]],s=c.left===u?c.right:c.left;++f=u)return null;var f=t-i.site[0],c=n-i.site[1],s=f*f+c*c;do{i=o.cells[r=a],a=null,i.halfedges.forEach(function(e){var r=o.edges[e],u=r.left;if(u!==i.site&&u||(u=r.right)){var f=t-u[0],c=n-u[1],l=f*f+c*c;lt?1:n>=t?0:NaN},t.deviation=a,t.extent=u,t.histogram=function(){function t(t){var i,o,a=t.length,u=new Array(a);for(i=0;il;)h.pop(),--p;var v,g=new Array(p+1);for(i=0;i<=p;++i)(v=g[i]=[]).x0=i>0?h[i-1]:c,v.x1=i=o.length)return null!=e&&n.sort(e),null!=r?r(n):n;for(var f,c,s,l=-1,h=n.length,d=o[i++],p=le(),v=a();++lo.length)return t;var i,u=a[e-1];return null!=r&&e>=o.length?i=t.entries():(i=[],t.each(function(t,r){i.push({key:r,values:n(t,e)})})),null!=u?i.sort(function(t,n){return u(t.key,n.key)}):i}var e,r,i,o=[],a=[];return i={object:function(n){return t(n,0,he,de)},map:function(n){return t(n,0,pe,ve)},entries:function(e){return n(t(e,0,pe,ve),0)},key:function(t){return o.push(t),i},sortKeys:function(t){return a[o.length-1]=t,i},sortValues:function(t){return e=t,i},rollup:function(t){return r=t,i}}},t.set=ye,t.map=le,t.keys=function(t){var n=[];for(var e in t)n.push(e);return n},t.values=function(t){var n=[];for(var e in t)n.push(t[e]);return n},t.entries=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},t.color=kt,t.rgb=Rt,t.hsl=Ut,t.lab=Bt,t.hcl=Vt,t.lch=function(t,n,e,r){return 1===arguments.length?Gt(t):new $t(e,n,t,null==r?1:r)},t.gray=function(t,n){return new Ft(t,0,0,null==n?1:n)},t.cubehelix=Wt,t.contours=we,t.contourDensity=function(){function t(t){var e=new Float32Array(v*y),r=new Float32Array(v*y);t.forEach(function(t,n,r){var i=a(t,n,r)+p>>h,o=u(t,n,r)+p>>h;i>=0&&i=0&&o>h),Ae({width:v,height:y,data:r},{width:v,height:y,data:e},l>>h),Me({width:v,height:y,data:e},{width:v,height:y,data:r},l>>h),Ae({width:v,height:y,data:r},{width:v,height:y,data:e},l>>h),Me({width:v,height:y,data:e},{width:v,height:y,data:r},l>>h),Ae({width:v,height:y,data:r},{width:v,height:y,data:e},l>>h);var i=_(e);if(!Array.isArray(i)){var o=g(e);i=d(0,o,i),(i=s(0,Math.floor(o/i)*i,i)).shift()}return we().thresholds(i).size([v,y])(e).map(n)}function n(t){return t.value*=Math.pow(2,-2*h),t.coordinates.forEach(e),t}function e(t){t.forEach(r)}function r(t){t.forEach(i)}function i(t){t[0]=t[0]*Math.pow(2,h)-p,t[1]=t[1]*Math.pow(2,h)-p}function o(){return p=3*l,v=f+2*p>>h,y=c+2*p>>h,t}var a=Te,u=Ne,f=960,c=500,l=20,h=2,p=3*l,v=f+2*p>>h,y=c+2*p>>h,_=be(20);return t.x=function(n){return arguments.length?(a="function"==typeof n?n:be(+n),t):a},t.y=function(n){return arguments.length?(u="function"==typeof n?n:be(+n),t):u},t.size=function(t){if(!arguments.length)return[f,c];var n=Math.ceil(t[0]),e=Math.ceil(t[1]);if(!(n>=0||n>=0))throw new Error("invalid size");return f=n,c=e,o()},t.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return h=Math.floor(Math.log(t)/Math.LN2),o()},t.thresholds=function(n){return arguments.length?(_="function"==typeof n?n:Array.isArray(n)?be(Lh.call(n)):be(n),t):_},t.bandwidth=function(t){if(!arguments.length)return Math.sqrt(l*(l+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return l=Math.round((Math.sqrt(4*t*t+1)-1)/2),o()},t},t.dispatch=N,t.drag=function(){function n(t){t.on("mousedown.drag",e).filter(g).on("touchstart.drag",o).on("touchmove.drag",a).on("touchend.drag touchcancel.drag",u).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function e(){if(!h&&d.apply(this,arguments)){var n=f("mouse",p.apply(this,arguments),pt,this,arguments);n&&(ct(t.event.view).on("mousemove.drag",r,!0).on("mouseup.drag",i,!0),_t(t.event.view),gt(),l=!1,c=t.event.clientX,s=t.event.clientY,n("start"))}}function r(){if(yt(),!l){var n=t.event.clientX-c,e=t.event.clientY-s;l=n*n+e*e>m}y.mouse("drag")}function i(){ct(t.event.view).on("mousemove.drag mouseup.drag",null),bt(t.event.view,l),yt(),y.mouse("end")}function o(){if(d.apply(this,arguments)){var n,e,r=t.event.changedTouches,i=p.apply(this,arguments),o=r.length;for(n=0;nf+d||ic+d||or.index){var p=f-u.x-u.vx,v=c-u.y-u.vy,g=p*p+v*v;gt.r&&(t.r=t[n].r)}function r(){if(i){var n,e,r=i.length;for(o=new Array(r),n=0;n=s)){(t.data!==o||t.next)&&(0===i&&(i=qe(),d+=i*i),0===f&&(f=qe(),d+=f*f),d1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,a,u,f,c=0,s=t.length;for(null==r?r=1/0:r*=r,c=0;c1?(d.on(t,n),o):d.on(t)}}},t.forceX=function(t){function n(t){for(var n,e=0,a=r.length;eqr(r[0],r[1])&&(r[1]=i[1]),qr(i[0],r[1])>qr(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=qr(r[1],i[0]))>a&&(a=u,vd=i[0],yd=r[1])}return Md=Ad=null,vd===1/0||gd===1/0?[[NaN,NaN],[NaN,NaN]]:[[vd,gd],[yd,_d]]},t.geoCentroid=function(t){Td=Nd=Sd=Ed=kd=Cd=Pd=zd=Rd=Ld=Dd=0,_r(t,vp);var n=Rd,e=Ld,r=Dd,i=n*n+e*e+r*r;return i=.12&&i<.234&&r>=-.425&&r<-.214?c:i>=.166&&i<.234&&r>=-.214&&r<-.115?s:f).invert(t)},t.stream=function(t){return e&&r===t?e:e=function(t){var n=t.length;return{point:function(e,r){for(var i=-1;++i2?t[2]+90:90]):(t=e(),[t[0],t[1],t[2]-90])},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=To,t.geoRotation=ri,t.geoStream=_r,t.geoTransform=function(t){return{stream:Zi(t)}},t.cluster=function(){function t(t){var o,a=0;t.eachAfter(function(t){var e=t.children;e?(t.x=function(t){return t.reduce(So,0)/t.length}(e),t.y=function(t){return 1+t.reduce(Eo,0)}(e)):(t.x=o?a+=n(t,o):0,t.y=0,o=t)});var u=function(t){for(var n;n=t.children;)t=n[0];return t}(t),f=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(t),c=u.x-n(u,f)/2,s=f.x+n(f,u)/2;return t.eachAfter(i?function(n){n.x=(n.x-t.x)*e,n.y=(t.y-n.y)*r}:function(n){n.x=(n.x-c)/(s-c)*e,n.y=(1-(t.y?n.y/t.y:1))*r})}var n=No,e=1,r=1,i=!1;return t.separation=function(e){return arguments.length?(n=e,t):n},t.size=function(n){return arguments.length?(i=!1,e=+n[0],r=+n[1],t):i?null:[e,r]},t.nodeSize=function(n){return arguments.length?(i=!0,e=+n[0],r=+n[1],t):i?[e,r]:null},t},t.hierarchy=Co,t.pack=function(){function t(t){return t.x=e/2,t.y=r/2,n?t.eachBefore(Zo(n)).eachAfter(Qo(i,.5)).eachBefore(Jo(1)):t.eachBefore(Zo(Wo)).eachAfter(Qo(Vo,1)).eachAfter(Qo(i,t.r/Math.min(e,r))).eachBefore(Jo(Math.min(e,r)/(2*t.r))),t}var n=null,e=1,r=1,i=Vo;return t.radius=function(e){return arguments.length?(n=function(t){return null==t?null:Go(t)}(e),t):n},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i="function"==typeof n?n:$o(+n),t):i},t},t.packSiblings=function(t){return Xo(t),t},t.packEnclose=Do,t.partition=function(){function t(t){var o=t.height+1;return t.x0=t.y0=r,t.x1=n,t.y1=e/o,t.eachBefore(function(t,n){return function(e){e.children&&ta(e,e.x0,t*(e.depth+1)/n,e.x1,t*(e.depth+2)/n);var i=e.x0,o=e.y0,a=e.x1-r,u=e.y1-r;a0)throw new Error("cycle");return o}var n=na,e=ea;return t.id=function(e){return arguments.length?(n=Go(e),t):n},t.parentId=function(n){return arguments.length?(e=Go(n),t):e},t},t.tree=function(){function t(t){var f=function(t){for(var n,e,r,i,o,a=new fa(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new fa(r[i],i)),e.parent=n;return(a.parent=new fa(null,0)).children=[a],a}(t);if(f.eachAfter(n),f.parent.m=-f.z,f.eachBefore(e),u)t.eachBefore(r);else{var c=t,s=t,l=t;t.eachBefore(function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)});var h=c===s?1:i(c,s)/2,d=h-c.x,p=o/(s.x+h+d),v=a/(l.depth||1);t.eachBefore(function(t){t.x=(t.x+d)*p,t.y=t.depth*v})}return t}function n(t){var n=t.children,e=t.parent.children,r=t.i?e[t.i-1]:null;if(n){(function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)})(t);var o=(n[0].z+n[n.length-1].z)/2;r?(t.z=r.z+i(t._,r._),t.m=t.z-o):t.z=o}else r&&(t.z=r.z+i(t._,r._));t.parent.A=function(t,n,e){if(n){for(var r,o=t,a=t,u=n,f=o.parent.children[0],c=o.m,s=a.m,l=u.m,h=f.m;u=oa(u),o=ia(o),u&&o;)f=ia(f),(a=oa(a)).a=t,(r=u.z+l-o.z-c+i(u._,o._))>0&&(aa(ua(u,t,e),t,r),c+=r,s+=r),l+=u.m,c+=o.m,h+=f.m,s+=a.m;u&&!oa(a)&&(a.t=u,a.m+=l-s),o&&!ia(f)&&(f.t=o,f.m+=c-h,e=t)}return e}(t,r,t.parent.A||e[0])}function e(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function r(t){t.x*=o,t.y=t.depth*a}var i=ra,o=1,a=1,u=null;return t.separation=function(n){return arguments.length?(i=n,t):i},t.size=function(n){return arguments.length?(u=!1,o=+n[0],a=+n[1],t):u?null:[o,a]},t.nodeSize=function(n){return arguments.length?(u=!0,o=+n[0],a=+n[1],t):u?[o,a]:null},t},t.treemap=function(){function t(t){return t.x0=t.y0=0,t.x1=i,t.y1=o,t.eachBefore(n),a=[0],r&&t.eachBefore(Ko),t}function n(t){var n=a[t.depth],r=t.x0+n,i=t.y0+n,o=t.x1-n,h=t.y1-n;o=n-1){var c=f[t];return c.x0=r,c.y0=i,c.x1=a,void(c.y1=u)}for(var l=s[t],h=e/2+l,d=t+1,p=n-1;d>>1;s[v]u-i){var _=(r*y+a*g)/e;o(t,d,g,r,i,_,u),o(d,n,y,_,i,a,u)}else{var b=(i*y+u*g)/e;o(t,d,g,r,i,a,b),o(d,n,y,r,b,a,u)}}var a,u,f=t.children,c=f.length,s=new Array(c+1);for(s[0]=u=a=0;a=0;--n)c.push(t[r[o[n]][2]]);for(n=+u;nu!=c>u&&a<(f-e)*(u-r)/(c-r)+e&&(s=!s),f=e,c=r;return s},t.polygonLength=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],a=o[0],u=o[1],f=0;++r1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return a_.h=360*t-100,a_.s=1.5-1.5*n,a_.l=.8-.9*n,a_+""},t.interpolateWarm=i_,t.interpolateCool=o_,t.interpolateViridis=u_,t.interpolateMagma=f_,t.interpolateInferno=c_,t.interpolatePlasma=s_,t.create=function(t){return ct(C(t).call(document.documentElement))},t.creator=C,t.local=st,t.matcher=ys,t.mouse=pt,t.namespace=k,t.namespaces=ds,t.clientPoint=dt,t.select=ct,t.selectAll=function(t){return"string"==typeof t?new ut([document.querySelectorAll(t)],[document.documentElement]):new ut([null==t?[]:t],ms)},t.selection=ft,t.selector=z,t.selectorAll=L,t.style=F,t.touch=vt,t.touches=function(t,n){null==n&&(n=ht().touches);for(var e=0,r=n?n.length:0,i=new Array(r);eh;if(f||(f=t=ie()),l__)if(p>x_-__)f.moveTo(l*d_(h),l*g_(h)),f.arc(0,0,l,h,d,!v),s>__&&(f.moveTo(s*d_(d),s*g_(d)),f.arc(0,0,s,d,h,v));else{var g,y,_=h,b=d,m=h,x=d,w=p,M=p,A=u.apply(this,arguments)/2,T=A>__&&(i?+i.apply(this,arguments):y_(s*s+l*l)),N=v_(l_(l-s)/2,+r.apply(this,arguments)),S=N,E=N;if(T>__){var k=sf(T/s*g_(A)),C=sf(T/l*g_(A));(w-=2*k)>__?(k*=v?1:-1,m+=k,x-=k):(w=0,m=x=(h+d)/2),(M-=2*C)>__?(C*=v?1:-1,_+=C,b-=C):(M=0,_=b=(h+d)/2)}var P=l*d_(_),z=l*g_(_),R=s*d_(x),L=s*g_(x);if(N>__){var D=l*d_(b),U=l*g_(b),q=s*d_(m),O=s*g_(m);if(p__?function(t,n,e,r,i,o,a,u){var f=e-t,c=r-n,s=a-i,l=u-o,h=(s*(n-o)-l*(t-i))/(l*f-s*c);return[t+h*f,n+h*c]}(P,z,q,O,D,U,R,L):[R,L],B=P-Y[0],F=z-Y[1],I=D-Y[0],j=U-Y[1],H=1/g_(function(t){return t>1?0:t<-1?b_:Math.acos(t)}((B*I+F*j)/(y_(B*B+F*F)*y_(I*I+j*j)))/2),X=y_(Y[0]*Y[0]+Y[1]*Y[1]);S=v_(N,(s-X)/(H-1)),E=v_(N,(l-X)/(H+1))}}M>__?E>__?(g=gf(q,O,P,z,l,E,v),y=gf(D,U,R,L,l,E,v),f.moveTo(g.cx+g.x01,g.cy+g.y01),E__&&w>__?S>__?(g=gf(R,L,D,U,s,-S,v),y=gf(P,z,q,O,s,-S,v),f.lineTo(g.cx+g.x01,g.cy+g.y01),S0&&(d+=l);for(null!=e?p.sort(function(t,n){return e(v[t],v[n])}):null!=r&&p.sort(function(n,e){return r(t[n],t[e])}),u=0,c=d?(y-h*b)/d:0;u0?l*c:0)+b,v[f]={data:t[f],index:u,value:l,startAngle:g,endAngle:s,padAngle:_};return v}var n=Af,e=Mf,r=null,i=cf(0),o=cf(x_),a=cf(0);return t.value=function(e){return arguments.length?(n="function"==typeof e?e:cf(+e),t):n},t.sortValues=function(n){return arguments.length?(e=n,r=null,t):e},t.sort=function(n){return arguments.length?(r=n,e=null,t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:cf(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:cf(+n),t):o},t.padAngle=function(n){return arguments.length?(a="function"==typeof n?n:cf(+n),t):a},t},t.areaRadial=kf,t.radialArea=kf,t.lineRadial=Ef,t.radialLine=Ef,t.pointRadial=Cf,t.linkHorizontal=function(){return Rf(Lf)},t.linkVertical=function(){return Rf(Df)},t.linkRadial=function(){var t=Rf(Uf);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.symbol=function(){function t(){var t;if(r||(r=t=ie()),n.apply(this,arguments).draw(r,+e.apply(this,arguments)),t)return r=null,t+""||null}var n=cf(A_),e=cf(64),r=null;return t.type=function(e){return arguments.length?(n="function"==typeof e?e:cf(e),t):n},t.size=function(n){return arguments.length?(e="function"==typeof n?n:cf(+n),t):e},t.context=function(n){return arguments.length?(r=null==n?null:n,t):r},t},t.symbols=B_,t.symbolCircle=A_,t.symbolCross=T_,t.symbolDiamond=E_,t.symbolSquare=R_,t.symbolStar=z_,t.symbolTriangle=D_,t.symbolWye=Y_,t.curveBasisClosed=function(t){return new Bf(t)},t.curveBasisOpen=function(t){return new Ff(t)},t.curveBasis=function(t){return new Yf(t)},t.curveBundle=F_,t.curveCardinalClosed=j_,t.curveCardinalOpen=H_,t.curveCardinal=I_,t.curveCatmullRomClosed=G_,t.curveCatmullRomOpen=V_,t.curveCatmullRom=X_,t.curveLinearClosed=function(t){return new Qf(t)},t.curveLinear=_f,t.curveMonotoneX=function(t){return new ec(t)},t.curveMonotoneY=function(t){return new rc(t)},t.curveNatural=function(t){return new oc(t)},t.curveStep=function(t){return new uc(t,.5)},t.curveStepAfter=function(t){return new uc(t,1)},t.curveStepBefore=function(t){return new uc(t,0)},t.stack=function(){function t(t){var o,a,u=n.apply(this,arguments),f=t.length,c=u.length,s=new Array(c);for(o=0;o0){for(var e,r,i,o=0,a=t[0].length;o1)for(var e,r,i,o,a,u,f=0,c=t[n[0]].length;f=0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):r[0]=o},t.stackOffsetNone=fc,t.stackOffsetSilhouette=function(t,n){if((e=t.length)>0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;azl&&e.name===n)return new qn([[t]],sh,n,+r)}return null},t.interrupt=Ln,t.voronoi=function(){function t(t){return new Oc(t.map(function(r,i){var o=[Math.round(n(r,i,t)/nb)*nb,Math.round(e(r,i,t)/nb)*nb];return o.index=i,o.data=r,o}),r)}var n=pc,e=vc,r=null;return t.polygons=function(n){return t(n).polygons()},t.links=function(n){return t(n).links()},t.triangles=function(n){return t(n).triangles()},t.x=function(e){return arguments.length?(n="function"==typeof e?e:dc(+e),t):n},t.y=function(n){return arguments.length?(e="function"==typeof n?n:dc(+n),t):e},t.extent=function(n){return arguments.length?(r=null==n?null:[[+n[0][0],+n[0][1]],[+n[1][0],+n[1][1]]],t):r&&[[r[0][0],r[0][1]],[r[1][0],r[1][1]]]},t.size=function(n){return arguments.length?(r=null==n?null:[[0,0],[+n[0],+n[1]]],t):r&&[r[1][0]-r[0][0],r[1][1]-r[0][1]]},t},t.zoom=function(){function n(t){t.property("__zoom",Gc).on("wheel.zoom",f).on("mousedown.zoom",c).on("dblclick.zoom",s).filter(m).on("touchstart.zoom",l).on("touchmove.zoom",h).on("touchend.zoom touchcancel.zoom",d).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function e(t,n){return(n=Math.max(x[0],Math.min(x[1],n)))===t.k?t:new Bc(n,t.x,t.y)}function r(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new Bc(t.k,r,i)}function i(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function o(t,n,e){t.on("start.zoom",function(){a(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){a(this,arguments).end()}).tween("zoom",function(){var t=arguments,r=a(this,t),o=y.apply(this,t),u=e||i(o),f=Math.max(o[1][0]-o[0][0],o[1][1]-o[0][1]),c=this.__zoom,s="function"==typeof n?n.apply(this,t):n,l=A(c.invert(u).concat(f/c.k),s.invert(u).concat(f/s.k));return function(t){if(1===t)t=s;else{var n=l(t),e=f/n[2];t=new Bc(e,u[0]-n[0]*e,u[1]-n[1]*e)}r.zoom(null,t)}})}function a(t,n){for(var e,r=0,i=T.length;rC}n.zoom("mouse",_(r(n.that.__zoom,n.mouse[0]=pt(n.that),n.mouse[1]),n.extent,w))},!0).on("mouseup.zoom",function(){e.on("mousemove.zoom mouseup.zoom",null),bt(t.event.view,n.moved),jc(),n.end()},!0),i=pt(this),o=t.event.clientX,u=t.event.clientY;_t(t.event.view),Ic(),n.mouse=[i,this.__zoom.invert(i)],Ln(this),n.start()}}function s(){if(g.apply(this,arguments)){var i=this.__zoom,a=pt(this),u=i.invert(a),f=i.k*(t.event.shiftKey?.5:2),c=_(r(e(i,f),a,u),y.apply(this,arguments),w);jc(),M>0?ct(this).transition().duration(M).call(o,c,a):ct(this).call(n.transform,c)}}function l(){if(g.apply(this,arguments)){var n,e,r,i,o=a(this,arguments),u=t.event.changedTouches,f=u.length;for(Ic(),e=0;e {version.onCheckboxChange(d3.event.target.checked)}); + checkboxContainer.append("label") + .attr("for", "flatten-sublevels") + .text("Show sublevels"); + h.append("svg").attr("id", "slider"); + c.append("svg").attr("id", "vis"); +} + +let vis = d3.select("#vis"); + +function renderHelp() { + vis + .append("text") + .attr("class", "help") + .attr("x", 10) + .attr("y", levelOffsets[6] + 30) + .text( + "(space: start/stop, left-arrow[+shift]: step-back, right-arrow[+shift]: step-forward)" + ); +} + +function renderReason() { + return vis + .append("text") + .attr("class", "reason") + .attr("x", 10) + .attr("y", 16); +} + +let reason = renderReason(); + +let index = d3.select("#index"); + +// Pretty formatting of a number in human readable units. +function humanize(s) { + const iecSuffixes = [" B", " KB", " MB", " GB", " TB", " PB", " EB"]; + if (s < 10) { + return "" + s; + } + let e = Math.floor(Math.log(s) / Math.log(1024)); + let suffix = iecSuffixes[Math.floor(e)]; + let val = Math.floor(s / Math.pow(1024, e) * 10 + 0.5) / 10; + return val.toFixed(val < 10 ? 1 : 0) + suffix; +} + +function generateLevelOffsets() { + return levelHeights.map((v, i) => + levelHeights.slice(0, i + 1).reduce((sum, elem) => sum + elem, offsetStart) + ); +} + +function styleWidth(e) { + let width = +e.style("width").slice(0, -2); + return Math.round(Number(width)); +} + +function styleHeight(e) { + let height = +e.style("height").slice(0, -2); + return Math.round(Number(height)); +} + +let sliderX, sliderHandle; +let offsetSliderX; + +// The version object holds the current LSM state. +let version = { + levels: [[], [], [], [], [], [], []], + sublevels: [], + numSublevels: 0, + showSublevels: false, + // Generated after every change using setLevelsInfo(). + levelsInfo: [], + // The version edit index. + index: -1, + + init: function() { + for (let edit of data.Edits) { + if (edit.Sublevels === null || edit.Sublevels === undefined) { + continue; + } + for (let [file, sublevel] of Object.entries(edit.Sublevels)) { + if (sublevel >= this.numSublevels) { + this.numSublevels = sublevel + 1; + } + } + } + for (let i = 0; i < this.numSublevels; i++) { + this.sublevels.push([]); + } + d3.select("#checkbox-container label") + .text("Show sublevels (" + this.numSublevels.toString() + ")"); + this.setHeights(); + this.setLevelsInfo(); + renderHelp(); + }, + + setHeights: function() { + // Update the height of level 0 to account for the number of sublevels, + // if there are any. + if (this.numSublevels > 0 && this.showSublevels === true) { + levelHeights[0] = sublevelHeight * this.numSublevels; + } else { + levelHeights[0] = sublevelHeight; + } + levelOffsets = generateLevelOffsets(); + vis.style("height", levelOffsets[6] + 100); + }, + + onCheckboxChange: function(value) { + this.showSublevels = value; + vis.selectAll("*") + .remove(); + reason = renderReason(); + this.setHeights(); + this.setLevelsInfo(); + renderHelp(); + + this.render(true); + this.updateSize(); + }, + + // Set the version edit index. This steps either forward or + // backward through the version edits, applying or unapplying each + // edit. + set: function(index) { + let prevIndex = this.index; + if (index < 0) { + index = 0; + } else if (index >= data.Edits.length) { + index = data.Edits.length - 1; + } + if (index == this.index) { + return; + } + + // If the current edit index is less than the target index, + // step forward applying edits. + for (; this.index < index; this.index++) { + let edit = data.Edits[this.index + 1]; + for (let level in edit.Deleted) { + this.remove(level, edit.Deleted[level]); + } + for (let level in edit.Added) { + this.add(level, edit.Added[level]); + } + } + + // If the current edit index is greater than the target index, + // step backward unapplying edits. + for (; this.index > index; this.index--) { + let edit = data.Edits[this.index]; + for (let level in edit.Added) { + this.remove(level, edit.Added[level]); + } + for (let level in edit.Deleted) { + this.add(level, edit.Deleted[level]); + } + } + + // Build the sublevels from this.levels[0]. They need to be rebuilt from + // scratch each time there's a change to L0. + this.sublevels = []; + while(this.sublevels.length < this.numSublevels) { + this.sublevels.push([]); + } + for (let file of this.levels[0]) { + let sublevel = null; + for (let i = index; i >= 0 && (sublevel === null || sublevel === undefined); i--) { + if (data.Edits[i].Sublevels == null || data.Edits[i].Sublevels == undefined) { + continue; + } + sublevel = data.Edits[i].Sublevels[file]; + } + this.sublevels[sublevel].push(file); + } + + // Sort the levels. + for (let i in this.levels) { + if (i == 0) { + for (let j in this.sublevels) { + this.sublevels[j].sort(function(a, b) { + let fa = data.Files[a]; + let fb = data.Files[b]; + if (fa.Smallest < fb.Smallest) { + return -1; + } + if (fa.Smallest > fb.Smallest) { + return +1; + } + return 0; + }); + } + this.levels[i].sort(function(a, b) { + let fa = data.Files[a]; + let fb = data.Files[b]; + if (fa.LargestSeqNum < fb.LargestSeqNum) { + return -1; + } + if (fa.LargestSeqNum > fb.LargestSeqNum) { + return +1; + } + if (fa.SmallestSeqNum < fb.SmallestSeqNum) { + return -1; + } + if (fa.SmallestSeqNum > fb.SmallestSeqNum) { + return +1; + } + return a < b; + }); + } else { + this.levels[i].sort(function(a, b) { + let fa = data.Files[a]; + let fb = data.Files[b]; + if (fa.Smallest < fb.Smallest) { + return -1; + } + if (fa.Smallest > fb.Smallest) { + return +1; + } + return 0; + }); + } + } + + this.updateLevelsInfo(); + this.render(prevIndex === -1); + }, + + // Add the specified sstables to the specifed level. + add: function(level, fileNums) { + for (let i = 0; i < fileNums.length; i++) { + this.levels[level].push(fileNums[i]); + } + }, + + // Remove the specified sstables from the specifed level. + remove: function(level, fileNums) { + let l = this.levels[level]; + for (let i = 0; i < l.length; i++) { + if (fileNums.indexOf(l[i]) != -1) { + l[i] = l[l.length - 1]; + l.pop(); + i--; + } + } + }, + + // Return the size of the sstables in a level. + size: function(level, sublevel) { + if (level == 0 && sublevel !== null && sublevel !== undefined) { + return this.sublevels[sublevel].reduce( + (sum, elem) => sum + data.Files[elem].Size, + 0 + ); + } + return (this.levels[level] || []).reduce( + (sum, elem) => sum + data.Files[elem].Size, + 0 + ); + }, + + // Returns the height to use for an sstable. + height: function(fileNum) { + let meta = data.Files[fileNum]; + return Math.ceil((meta.Size + 1024.0 * 1024.0 - 1) / (1024.0 * 1024.0)); + }, + + scale: function(level) { + return levelWidth < this.levelsInfo[level].files.length + ? levelWidth / this.levelsInfo[level].files.length + : 1; + }, + + // Return a summary of the count and size of the specified sstables. + summarize: function(level, fileNums) { + let count = 0; + let size = 0; + for (let fileNum of fileNums) { + count++; + size += data.Files[fileNum].Size; + } + return count + " @ " + "L" + level + " (" + humanize(size) + ")"; + }, + + // Return a textual description of a version edit. + describe: function(edit) { + let s = edit.Reason; + + if (edit.Deleted) { + let sep = " "; + for (let i = 0; i < 7; i++) { + if (edit.Deleted[i]) { + s += sep + this.summarize(i, edit.Deleted[i]); + sep = " + "; + } + } + } + + if (edit.Added) { + let sep = " => "; + for (let i = 0; i < 7; i++) { + if (edit.Added[i]) { + s += sep + this.summarize(i, edit.Added[i]); + sep = " + "; + } + } + } + + return s; + }, + + setLevelsInfo: function() { + let sublevelCount = this.numSublevels; + let levelsInfo = []; + let levelsStart = 1; + if (this.showSublevels === true) { + levelsInfo = this.sublevels.map((files, sublevel) => ({ + files: files, + levelString: "L0." + sublevel.toString(), + levelDisplayString: (sublevel === this.numSublevels - 1 ? + "L0." : "    .") + sublevel.toString(), + levelClass: "L0-" + sublevel.toString(), + level: 0, + offset: offsetStart + (sublevelHeight * (sublevelCount - sublevel)), + height: sublevelHeight, + size: humanize(this.size(0, sublevel)), + })); + if (levelsInfo.length === 0) { + levelsStart = 0; + } + levelsInfo.reverse(); + } else { + levelsStart = 0; + } + + levelsInfo = levelsInfo.concat(this.levels.slice(levelsStart).map((files, level) => ({ + files: files, + levelString: "L" + (level+levelsStart).toString(), + levelDisplayString: "L" + (level+levelsStart).toString(), + levelClass: "L" + (level+levelsStart).toString(), + level: level, + offset: levelOffsets[level+levelsStart], + height: levelHeights[level+levelsStart], + size: humanize(this.size(level+levelsStart)), + }))); + this.levelsInfo = levelsInfo; + }, + + updateLevelsInfo: function() { + let levelsStart = 1; + if (this.showSublevels === true) { + this.sublevels.forEach((files, sublevel) => { + this.levelsInfo[this.numSublevels - (sublevel + 1)].files = files; + this.levelsInfo[this.numSublevels - (sublevel + 1)].size = humanize(this.size(0, sublevel)); + }); + if (this.numSublevels === 0) { + levelsStart = 0; + } + } else { + levelsStart = 0; + } + + this.levels.slice(levelsStart).forEach((files, level) => { + let sublevelOffset = this.showSublevels === true ? this.numSublevels : 0; + this.levelsInfo[sublevelOffset + level].files = files; + this.levelsInfo[sublevelOffset + level].size = humanize(this.size(levelsStart + level)); + }); + }, + + render: function(redraw) { + let version = this; + + vis.interrupt(); + + // Render the edit info. + let info = "[" + this.describe(data.Edits[this.index]) + "]"; + reason.text(info); + + // Render the text for each level: sstable count and size. + vis + .selectAll("text.levels") + .data(this.levelsInfo) + .enter() + .append("text") + .attr("class", "levels") + .attr("x", 10) + .attr("y", d => d.offset) + .html(d => d.levelDisplayString); + vis + .selectAll("text.counts") + .data(this.levelsInfo) + .text((d, i) => d.files.length) + .enter() + .append("text") + .attr("class", "counts") + .attr("text-anchor", "end") + .attr("x", 55) + .attr("y", d => d.offset) + .text(d => d.files.length); + vis + .selectAll("text.sizes") + .data(this.levelsInfo) + .text((d, i) => d.size) + .enter() + .append("text") + .attr("class", "sizes") + .attr("text-anchor", "end") + .attr("x", 100) + .attr("y", (d, i) => d.offset) + .text(d => d.size); + + // Render each of the levels. Each level is composed of an + // outer group which provides a clipping recentangle, an inner + // group defining the coordinate system, an overlap rectangle + // to capture mouse events, an indicator rectangle used to + // display sstable overlaps, and the per-sstable rectangles. + for (let i in this.levelsInfo) { + let g, clipG; + if (redraw === false) { + g = vis + .selectAll("g.clip" + this.levelsInfo[i].levelClass) + .select("g") + .data([i]); + clipG = g + .enter() + .append("g") + .attr("class", "clipRect clip" + this.levelsInfo[i].levelClass) + .attr("clip-path", "url(#" + this.levelsInfo[i].levelClass + ")"); + } else { + clipG = vis + .append("g") + .attr("class", "clipRect clip" + this.levelsInfo[i].levelClass) + .attr("clip-path", "url(#" + this.levelsInfo[i].levelClass + ")") + .data([i]); + g = clipG + .append("g"); + } + clipG + .append("g") + .attr( + "transform", + "translate(" + + lineStart + + "," + + this.levelsInfo[i].offset + + ") scale(1,-1)" + ); + clipG.append("rect").attr("class", "indicator"); + + // Define the overlap rectangle for capturing mouse events. + clipG + .append("rect") + .attr("x", lineStart) + .attr("y", this.levelsInfo[i].offset - this.levelsInfo[i].height) + .attr("width", levelWidth) + .attr("height", this.levelsInfo[i].height) + .attr("opacity", 0) + .attr("pointer-events", "all") + .on("mousemove", i => version.onMouseMove(i)) + .on("mouseout", function() { + reason.text(info); + vis.selectAll("rect.indicator").attr("fill", "none"); + }); + + // Scale each level to fit within the display. + let s = this.scale(i); + g.attr( + "transform", + "translate(" + + lineStart + + "," + + this.levelsInfo[i].offset + + ") scale(" + + s + + "," + + -(1 / s) + + ")" + ); + + // Render the sstables for the level. + let level = g.selectAll("rect." + this.levelsInfo[i].levelClass).data(this.levelsInfo[i].files); + level.attr("fill", fileNum => (data.Files[fileNum].Virtual?"#8A9":"#555")).attr("x", (fileNum, i) => i); + level + .enter() + .append("rect") + .attr("class", this.levelsInfo[i].levelClass + " sstable") + .attr("id", fileNum => fileNum) + .attr("fill", fileNum => (data.Files[fileNum].Virtual?"orange":"red")) + .attr("x", (fileNum, i) => i) + .attr("y", 0) + .attr("width", 1) + .attr("height", fileNum => version.height(fileNum)); + level.exit().remove(); + } + + sliderHandle.attr("cx", sliderX(version.index)); + index.node().value = version.index + data.StartEdit; + }, + + onMouseMove: function(i) { + i = Number(i); + if (Number.isNaN(i) || i >= this.levelsInfo.length || this.levelsInfo[i].files.length === 0) { + return; + } + + // The mouse coordinates are relative to the + // SVG element. Adjust to be relative to the + // level position. + let mousex = d3.mouse(vis.node())[0] - lineStart; + let index = Math.round(mousex / this.scale(i)); + if (index < 0) { + index = 0; + } else if (index >= this.levelsInfo[i].files.length) { + index = this.levelsInfo[i].files.length - 1; + } + let fileNum = this.levelsInfo[i].files[index]; + let meta = data.Files[fileNum]; + + // Find the start and end index of the tables + // that overlap with filenum. + let overlapInfo = ""; + for (let j = 1; j < this.levelsInfo.length; j++) { + if (this.levelsInfo[i].files.length === 0) { + continue; + } + let indicator = vis.select("g.clip" + this.levelsInfo[j].levelClass + " rect.indicator"); + indicator + .attr("fill", "black") + .attr("opacity", 0.3) + .attr("y", this.levelsInfo[j].offset - this.levelsInfo[j].height) + .attr("height", this.levelsInfo[j].height); + if (j === i) { + continue; + } + let fileNums = this.levelsInfo[j].files; + for (let k in fileNums) { + let other = data.Files[fileNums[k]]; + if (other.Largest < meta.Smallest) { + continue; + } + let s = this.scale(j); + let t = k; + for (; k < fileNums.length; k++) { + let other = data.Files[fileNums[k]]; + if (other.Smallest >= meta.Largest) { + break; + } + } + if (k === t) { + indicator.attr("x", lineStart + s * t).attr("width", s); + } else { + indicator + .attr("x", lineStart + s * t) + .attr("width", Math.max(0.5, s * (k - t))); + } + if (i + 1 === j && k > t) { + let overlapSize = this.levelsInfo[j].files + .slice(t, k) + .reduce((sum, elem) => sum + data.Files[elem].Size, 0); + + overlapInfo = + " overlaps " + + (k - t) + + " @ " + + this.levelsInfo[j].levelString + + " (" + + humanize(overlapSize) + + ")"; + } + break; + } + } + + reason.text( + "[" + + this.levelsInfo[i].levelString + + (data.Files[fileNum].Virtual? " v":" ") + + fileNum + + " (" + + humanize(data.Files[fileNum].Size) + + ")" + + overlapInfo + + " <" + + data.Keys[data.Files[fileNum].Smallest].Pretty + + ", " + + data.Keys[data.Files[fileNum].Largest].Pretty + + ">" + + "]" + ); + + vis + .select("g.clip" + this.levelsInfo[i].levelClass + " rect.indicator") + .attr("x", lineStart + this.scale(i) * index) + .attr("width", 1); + }, + + // Recalculate structures related to the page width. + updateSize: function() { + let svg = d3.select("#slider").html(""); + + let margin = { right: 10, left: 10 }; + + let width = styleWidth(d3.select("#slider")) - margin.left - margin.right, + height = styleHeight(svg); + + sliderX = d3 + .scaleLinear() + .domain([0, data.Edits.length - 1]) + .range([0, width]) + .clamp(true); + + // Used only to generate offset ticks for slider. + // sliderX is used to index into the data.Edits array (0-indexed). + offsetSliderX = d3 + .scaleLinear() + .domain([data.StartEdit, data.StartEdit + data.Edits.length - 1]) + .range([0, width]); + + let slider = svg + .append("g") + .attr("class", "slider") + .attr("transform", "translate(" + margin.left + "," + height / 2 + ")"); + + slider + .append("line") + .attr("class", "track") + .attr("x1", sliderX.range()[0]) + .attr("x2", sliderX.range()[1]) + .select(function() { + return this.parentNode.appendChild(this.cloneNode(true)); + }) + .attr("class", "track-inset") + .select(function() { + return this.parentNode.appendChild(this.cloneNode(true)); + }) + .attr("class", "track-overlay") + .call( + d3 + .drag() + .on("start.interrupt", function() { + slider.interrupt(); + }) + .on("start drag", function() { + version.set(Math.round(sliderX.invert(d3.event.x))); + }) + ); + + slider + .insert("g", ".track-overlay") + .attr("class", "ticks") + .attr("transform", "translate(0," + 18 + ")") + .selectAll("text") + .data(offsetSliderX.ticks(10)) + .enter() + .append("text") + .attr("x", offsetSliderX) + .attr("text-anchor", "middle") + .text(function(d) { + return d; + }); + + sliderHandle = slider + .insert("circle", ".track-overlay") + .attr("class", "handle") + .attr("r", 9) + .attr("cx", sliderX(version.index)); + + levelWidth = styleWidth(vis) - 10 - lineStart; + let lineEnd = lineStart + levelWidth; + + vis + .selectAll("line") + .data(this.levelsInfo) + .attr("x2", lineEnd) + .enter() + .append("line") + .attr("x1", lineStart) + .attr("x2", lineEnd) + .attr("y1", d => d.offset) + .attr("y2", d => d.offset) + .attr("stroke", "#ddd"); + + vis + .selectAll("defs clipPath rect") + .data(this.levelsInfo) + .attr("width", lineEnd - lineStart) + .enter() + .append("defs") + .append("clipPath") + .attr("id", d => d.levelClass) + .append("rect") + .attr("x", lineStart) + .attr("y", d => d.offset - d.height) + .attr("width", lineEnd - lineStart) + .attr("height", d => d.height); + }, +}; + +window.onload = function() { + version.init(); + version.updateSize(); + version.set(0); +}; + +window.addEventListener("resize", function() { + version.updateSize(); + version.render(); +}); + +let timer; + +function startPlayback(increment) { + timer = d3.timer(function() { + let lastIndex = version.index; + version.set(version.index + increment); + if (lastIndex == version.index) { + timer.stop(); + timer = null; + } + }); +} + +function stopPlayback() { + if (timer == null) { + return false; + } + timer.stop(); + timer = null; + return true; +} + +document.addEventListener("keydown", function(e) { + switch (e.keyCode) { + case 37: // left arrow + stopPlayback(); + version.set(version.index - (e.shiftKey ? 10 : 1)); + return; + case 39: // right arrow + stopPlayback(); + version.set(version.index + (e.shiftKey ? 10 : 1)); + return; + case 32: // space + if (stopPlayback()) { + return; + } + startPlayback(1); + return; + } +}); + +index.on("input", function() { + if (!isNaN(+this.value)) { + const val = Number(this.value) - data.StartEdit; + if (val >= 0) { + version.set(val); + } + } +}); diff --git a/pebble/tool/data_test.go b/pebble/tool/data_test.go new file mode 100644 index 0000000..0f18737 --- /dev/null +++ b/pebble/tool/data_test.go @@ -0,0 +1,136 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/vfs" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func runTests(t *testing.T, path string) { + paths, err := filepath.Glob(path) + require.NoError(t, err) + + root := filepath.Dir(path) + for { + next := filepath.Dir(root) + if next == "." { + break + } + root = next + } + + normalize := func(name string) string { + if os.PathSeparator == '/' { + return name + } + return strings.Replace(name, "/", string(os.PathSeparator), -1) + } + + for _, path := range paths { + name, err := filepath.Rel(root, path) + require.NoError(t, err) + + fs := vfs.NewMem() + t.Run(name, func(t *testing.T) { + datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { + args := []string{d.Cmd} + for _, arg := range d.CmdArgs { + args = append(args, arg.String()) + } + args = append(args, strings.Fields(d.Input)...) + + // The testdata files contain paths with "/" path separators, but we + // might be running on a system with a different path separator + // (e.g. Windows). Copy the input data into a mem filesystem which + // always uses "/" for the path separator. + for i := range args { + src := normalize(args[i]) + dest := vfs.Default.PathBase(src) + if ok, err := vfs.Clone(vfs.Default, fs, src, dest); err != nil { + return err.Error() + } else if ok { + args[i] = fs.PathBase(args[i]) + } + } + + var buf bytes.Buffer + var secs int64 + timeNow = func() time.Time { secs++; return time.Unix(secs, 0) } + + defer func() { + timeNow = time.Now + }() + + // Register a test comparer and merger so that we can check the + // behavior of tools when the comparer and merger do not match. + comparer := func() *Comparer { + c := *base.DefaultComparer + c.Name = "test-comparer" + c.FormatKey = func(key []byte) fmt.Formatter { + return fmtFormatter{ + fmt: "test formatter: %s", + v: key, + } + } + c.FormatValue = func(_, value []byte) fmt.Formatter { + return fmtFormatter{ + fmt: "test value formatter: %s", + v: value, + } + } + return &c + }() + altComparer := func() *Comparer { + c := *base.DefaultComparer + c.Name = "alt-comparer" + return &c + }() + merger := func() *Merger { + m := *base.DefaultMerger + m.Name = "test-merger" + return &m + }() + openErrEnhancer := func(err error) error { + if errors.Is(err, base.ErrCorruption) { + return base.CorruptionErrorf("%v\nCustom message in case of corruption error.", err) + } + return err + } + + tool := New( + DefaultComparer(comparer), + Comparers(altComparer, testkeys.Comparer), + Mergers(merger), + FS(fs), + OpenErrEnhancer(openErrEnhancer), + ) + + c := &cobra.Command{} + c.AddCommand(tool.Commands...) + c.SetArgs(args) + c.SetOut(&buf) + c.SetErr(&buf) + if err := c.Execute(); err != nil { + return err.Error() + } + return buf.String() + }) + }) + } +} diff --git a/pebble/tool/db.go b/pebble/tool/db.go new file mode 100644 index 0000000..dd78daf --- /dev/null +++ b/pebble/tool/db.go @@ -0,0 +1,777 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import ( + "context" + "fmt" + "io" + "text/tabwriter" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/manifest" + "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/tool/logs" + "github.com/spf13/cobra" +) + +// dbT implements db-level tools, including both configuration state and the +// commands themselves. +type dbT struct { + Root *cobra.Command + Check *cobra.Command + Checkpoint *cobra.Command + Get *cobra.Command + Logs *cobra.Command + LSM *cobra.Command + Properties *cobra.Command + Scan *cobra.Command + Set *cobra.Command + Space *cobra.Command + IOBench *cobra.Command + + // Configuration. + opts *pebble.Options + comparers sstable.Comparers + mergers sstable.Mergers + openErrEnhancer func(error) error + + // Flags. + comparerName string + mergerName string + fmtKey keyFormatter + fmtValue valueFormatter + start key + end key + count int64 + allLevels bool + ioCount int + ioParallelism int + ioSizes string + verbose bool +} + +func newDB( + opts *pebble.Options, + comparers sstable.Comparers, + mergers sstable.Mergers, + openErrEnhancer func(error) error, +) *dbT { + d := &dbT{ + opts: opts, + comparers: comparers, + mergers: mergers, + openErrEnhancer: openErrEnhancer, + } + d.fmtKey.mustSet("quoted") + d.fmtValue.mustSet("[%x]") + + d.Root = &cobra.Command{ + Use: "db", + Short: "DB introspection tools", + } + d.Check = &cobra.Command{ + Use: "check

", + Short: "verify checksums and metadata", + Long: ` +Verify sstable, manifest, and WAL checksums. Requires that the specified +database not be in use by another process. +`, + Args: cobra.ExactArgs(1), + Run: d.runCheck, + } + d.Checkpoint = &cobra.Command{ + Use: "checkpoint ", + Short: "create a checkpoint", + Long: ` +Creates a Pebble checkpoint in the specified destination directory. A checkpoint +is a point-in-time snapshot of DB state. Requires that the specified +database not be in use by another process. +`, + Args: cobra.ExactArgs(2), + Run: d.runCheckpoint, + } + d.Get = &cobra.Command{ + Use: "get ", + Short: "get value for a key", + Long: ` +Gets a value for a key, if it exists in DB. Prints a "not found" error if key +does not exist. Requires that the specified database not be in use by another +process. +`, + Args: cobra.ExactArgs(2), + Run: d.runGet, + } + d.Logs = logs.NewCmd() + d.LSM = &cobra.Command{ + Use: "lsm ", + Short: "print LSM structure", + Long: ` +Print the structure of the LSM tree. Requires that the specified database not +be in use by another process. +`, + Args: cobra.ExactArgs(1), + Run: d.runLSM, + } + d.Properties = &cobra.Command{ + Use: "properties ", + Short: "print aggregated sstable properties", + Long: ` +Print SSTable properties, aggregated per level of the LSM. +`, + Args: cobra.ExactArgs(1), + Run: d.runProperties, + } + d.Scan = &cobra.Command{ + Use: "scan ", + Short: "print db records", + Long: ` +Print the records in the DB. Requires that the specified database not be in use +by another process. +`, + Args: cobra.ExactArgs(1), + Run: d.runScan, + } + d.Set = &cobra.Command{ + Use: "set ", + Short: "set a value for a key", + Long: ` +Adds a new key/value to the DB. Requires that the specified database +not be in use by another process. +`, + Args: cobra.ExactArgs(3), + Run: d.runSet, + } + d.Space = &cobra.Command{ + Use: "space ", + Short: "print filesystem space used", + Long: ` +Print the estimated filesystem space usage for the inclusive-inclusive range +specified by --start and --end. Requires that the specified database not be in +use by another process. +`, + Args: cobra.ExactArgs(1), + Run: d.runSpace, + } + d.IOBench = &cobra.Command{ + Use: "io-bench ", + Short: "perform sstable IO benchmark", + Long: ` +Run a random IO workload with various IO sizes against the sstables in the +specified database. +`, + Args: cobra.ExactArgs(1), + Run: d.runIOBench, + } + + d.Root.AddCommand(d.Check, d.Checkpoint, d.Get, d.Logs, d.LSM, d.Properties, d.Scan, d.Set, d.Space, d.IOBench) + d.Root.PersistentFlags().BoolVarP(&d.verbose, "verbose", "v", false, "verbose output") + + for _, cmd := range []*cobra.Command{d.Check, d.Checkpoint, d.Get, d.LSM, d.Properties, d.Scan, d.Set, d.Space} { + cmd.Flags().StringVar( + &d.comparerName, "comparer", "", "comparer name (use default if empty)") + cmd.Flags().StringVar( + &d.mergerName, "merger", "", "merger name (use default if empty)") + } + + for _, cmd := range []*cobra.Command{d.Scan, d.Space} { + cmd.Flags().Var( + &d.start, "start", "start key for the range") + cmd.Flags().Var( + &d.end, "end", "end key for the range") + } + + d.Scan.Flags().Var( + &d.fmtKey, "key", "key formatter") + for _, cmd := range []*cobra.Command{d.Scan, d.Get} { + cmd.Flags().Var( + &d.fmtValue, "value", "value formatter") + } + + d.Scan.Flags().Int64Var( + &d.count, "count", 0, "key count for scan (0 is unlimited)") + + d.IOBench.Flags().BoolVar( + &d.allLevels, "all-levels", false, "if set, benchmark all levels (default is only L5/L6)") + d.IOBench.Flags().IntVar( + &d.ioCount, "io-count", 10000, "number of IOs (per IO size) to benchmark") + d.IOBench.Flags().IntVar( + &d.ioParallelism, "io-parallelism", 16, "number of goroutines issuing IO") + d.IOBench.Flags().StringVar( + &d.ioSizes, "io-sizes-kb", "4,16,64,128,256,512,1024", "comma separated list of IO sizes in KB") + + return d +} + +func (d *dbT) loadOptions(dir string) error { + ls, err := d.opts.FS.List(dir) + if err != nil || len(ls) == 0 { + // NB: We don't return the error here as we prefer to return the error from + // pebble.Open. Another way to put this is that a non-existent directory is + // not a failure in loading the options. + return nil + } + + hooks := &pebble.ParseHooks{ + NewComparer: func(name string) (*pebble.Comparer, error) { + if c := d.comparers[name]; c != nil { + return c, nil + } + return nil, errors.Errorf("unknown comparer %q", errors.Safe(name)) + }, + NewMerger: func(name string) (*pebble.Merger, error) { + if m := d.mergers[name]; m != nil { + return m, nil + } + return nil, errors.Errorf("unknown merger %q", errors.Safe(name)) + }, + SkipUnknown: func(name, value string) bool { + return true + }, + } + + // TODO(peter): RocksDB sometimes leaves multiple OPTIONS files in + // existence. We parse all of them as the comparer and merger shouldn't be + // changing. We could parse only the first or the latest. Not clear if this + // matters. + var dbOpts pebble.Options + for _, filename := range ls { + ft, _, ok := base.ParseFilename(d.opts.FS, filename) + if !ok { + continue + } + switch ft { + case base.FileTypeOptions: + err := func() error { + f, err := d.opts.FS.Open(d.opts.FS.PathJoin(dir, filename)) + if err != nil { + return err + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return err + } + + if err := dbOpts.Parse(string(data), hooks); err != nil { + return err + } + return nil + }() + if err != nil { + return err + } + } + } + + if dbOpts.Comparer != nil { + d.opts.Comparer = dbOpts.Comparer + } + if dbOpts.Merger != nil { + d.opts.Merger = dbOpts.Merger + } + return nil +} + +type openOption interface { + apply(opts *pebble.Options) +} + +func (d *dbT) openDB(dir string, openOptions ...openOption) (*pebble.DB, error) { + db, err := d.openDBInternal(dir, openOptions...) + if err != nil { + if d.openErrEnhancer != nil { + err = d.openErrEnhancer(err) + } + return nil, err + } + return db, nil +} + +func (d *dbT) openDBInternal(dir string, openOptions ...openOption) (*pebble.DB, error) { + if err := d.loadOptions(dir); err != nil { + return nil, errors.Wrap(err, "error loading options") + } + if d.comparerName != "" { + d.opts.Comparer = d.comparers[d.comparerName] + if d.opts.Comparer == nil { + return nil, errors.Errorf("unknown comparer %q", errors.Safe(d.comparerName)) + } + } + if d.mergerName != "" { + d.opts.Merger = d.mergers[d.mergerName] + if d.opts.Merger == nil { + return nil, errors.Errorf("unknown merger %q", errors.Safe(d.mergerName)) + } + } + opts := *d.opts + for _, opt := range openOptions { + opt.apply(&opts) + } + opts.Cache = pebble.NewCache(128 << 20 /* 128 MB */) + defer opts.Cache.Unref() + return pebble.Open(dir, &opts) +} + +func (d *dbT) closeDB(stderr io.Writer, db *pebble.DB) { + if err := db.Close(); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + } +} + +func (d *dbT) runCheck(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr() + db, err := d.openDB(args[0]) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer d.closeDB(stderr, db) + + var stats pebble.CheckLevelsStats + if err := db.CheckLevels(&stats); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + } + fmt.Fprintf(stdout, "checked %d %s and %d %s\n", + stats.NumPoints, makePlural("point", stats.NumPoints), stats.NumTombstones, makePlural("tombstone", int64(stats.NumTombstones))) +} + +type nonReadOnly struct{} + +func (n nonReadOnly) apply(opts *pebble.Options) { + opts.ReadOnly = false + // Increase the L0 compaction threshold to reduce the likelihood of an + // unintended compaction changing test output. + opts.L0CompactionThreshold = 10 +} + +func (d *dbT) runCheckpoint(cmd *cobra.Command, args []string) { + stderr := cmd.ErrOrStderr() + db, err := d.openDB(args[0], nonReadOnly{}) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer d.closeDB(stderr, db) + destDir := args[1] + + if err := db.Checkpoint(destDir); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + } +} + +func (d *dbT) runGet(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr() + db, err := d.openDB(args[0]) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer d.closeDB(stderr, db) + var k key + if err := k.Set(args[1]); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + + val, closer, err := db.Get(k) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer func() { + if closer != nil { + closer.Close() + } + }() + if val != nil { + fmt.Fprintf(stdout, "%s\n", d.fmtValue.fn(k, val)) + } +} + +func (d *dbT) runLSM(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr() + db, err := d.openDB(args[0]) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer d.closeDB(stderr, db) + + fmt.Fprintf(stdout, "%s", db.Metrics()) +} + +func (d *dbT) runScan(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr() + db, err := d.openDB(args[0]) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer d.closeDB(stderr, db) + + // Update the internal formatter if this comparator has one specified. + if d.opts.Comparer != nil { + d.fmtKey.setForComparer(d.opts.Comparer.Name, d.comparers) + d.fmtValue.setForComparer(d.opts.Comparer.Name, d.comparers) + } + + start := timeNow() + fmtKeys := d.fmtKey.spec != "null" + fmtValues := d.fmtValue.spec != "null" + var count int64 + + iter, _ := db.NewIter(&pebble.IterOptions{ + UpperBound: d.end, + }) + for valid := iter.SeekGE(d.start); valid; valid = iter.Next() { + if fmtKeys || fmtValues { + needDelimiter := false + if fmtKeys { + fmt.Fprintf(stdout, "%s", d.fmtKey.fn(iter.Key())) + needDelimiter = true + } + if fmtValues { + if needDelimiter { + stdout.Write([]byte{' '}) + } + fmt.Fprintf(stdout, "%s", d.fmtValue.fn(iter.Key(), iter.Value())) + } + stdout.Write([]byte{'\n'}) + } + + count++ + if d.count > 0 && count >= d.count { + break + } + } + + if err := iter.Close(); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + } + + elapsed := timeNow().Sub(start) + + fmt.Fprintf(stdout, "scanned %d %s in %0.1fs\n", + count, makePlural("record", count), elapsed.Seconds()) +} + +func (d *dbT) runSpace(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr() + db, err := d.openDB(args[0]) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer d.closeDB(stdout, db) + + bytes, err := db.EstimateDiskUsage(d.start, d.end) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + fmt.Fprintf(stdout, "%d\n", bytes) +} + +func (d *dbT) runProperties(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr() + dirname := args[0] + err := func() error { + desc, err := pebble.Peek(dirname, d.opts.FS) + if err != nil { + return err + } else if !desc.Exists { + return oserror.ErrNotExist + } + manifestFilename := d.opts.FS.PathBase(desc.ManifestFilename) + + // Replay the manifest to get the current version. + f, err := d.opts.FS.Open(desc.ManifestFilename) + if err != nil { + return errors.Wrapf(err, "pebble: could not open MANIFEST file %q", manifestFilename) + } + defer f.Close() + + cmp := base.DefaultComparer + var bve manifest.BulkVersionEdit + bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata) + rr := record.NewReader(f, 0 /* logNum */) + for { + r, err := rr.Next() + if err == io.EOF { + break + } + if err != nil { + return errors.Wrapf(err, "pebble: reading manifest %q", manifestFilename) + } + var ve manifest.VersionEdit + err = ve.Decode(r) + if err != nil { + return err + } + if err := bve.Accumulate(&ve); err != nil { + return err + } + if ve.ComparerName != "" { + cmp = d.comparers[ve.ComparerName] + d.fmtKey.setForComparer(ve.ComparerName, d.comparers) + d.fmtValue.setForComparer(ve.ComparerName, d.comparers) + } + } + v, err := bve.Apply( + nil /* version */, cmp.Compare, d.fmtKey.fn, d.opts.FlushSplitBytes, + d.opts.Experimental.ReadCompactionRate, nil, /* zombies */ + manifest.AllowSplitUserKeys, + ) + if err != nil { + return err + } + + objProvider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(d.opts.FS, dirname)) + if err != nil { + return err + } + defer objProvider.Close() + + // Load and aggregate sstable properties. + tw := tabwriter.NewWriter(stdout, 2, 1, 4, ' ', 0) + var total props + var all []props + for _, l := range v.Levels { + iter := l.Iter() + var level props + for t := iter.First(); t != nil; t = iter.Next() { + if t.Virtual { + // TODO(bananabrick): Handle virtual sstables here. We don't + // really have any stats or properties at this point. Maybe + // we could approximate some of these properties for virtual + // sstables by first grabbing properties for the backing + // physical sstable, and then extrapolating. + continue + } + err := d.addProps(objProvider, t.PhysicalMeta(), &level) + if err != nil { + return err + } + } + all = append(all, level) + total.update(level) + } + all = append(all, total) + + fmt.Fprintln(tw, "\tL0\tL1\tL2\tL3\tL4\tL5\tL6\tTOTAL") + + fmt.Fprintf(tw, "count\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n", + propArgs(all, func(p *props) interface{} { return p.Count })...) + + fmt.Fprintln(tw, "seq num\t\t\t\t\t\t\t\t") + fmt.Fprintf(tw, " smallest\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n", + propArgs(all, func(p *props) interface{} { return p.SmallestSeqNum })...) + fmt.Fprintf(tw, " largest\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n", + propArgs(all, func(p *props) interface{} { return p.LargestSeqNum })...) + + fmt.Fprintln(tw, "size\t\t\t\t\t\t\t\t") + fmt.Fprintf(tw, " data\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.DataSize) })...) + fmt.Fprintf(tw, " blocks\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n", + propArgs(all, func(p *props) interface{} { return p.NumDataBlocks })...) + fmt.Fprintf(tw, " index\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.IndexSize) })...) + fmt.Fprintf(tw, " blocks\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n", + propArgs(all, func(p *props) interface{} { return p.NumIndexBlocks })...) + fmt.Fprintf(tw, " top-level\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.TopLevelIndexSize) })...) + fmt.Fprintf(tw, " filter\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.FilterSize) })...) + fmt.Fprintf(tw, " raw-key\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.RawKeySize) })...) + fmt.Fprintf(tw, " raw-value\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.RawValueSize) })...) + fmt.Fprintf(tw, " pinned-key\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.SnapshotPinnedKeySize) })...) + fmt.Fprintf(tw, " pinned-value\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.SnapshotPinnedValueSize) })...) + fmt.Fprintf(tw, " point-del-key-size\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.RawPointTombstoneKeySize) })...) + fmt.Fprintf(tw, " point-del-value-size\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.RawPointTombstoneValueSize) })...) + + fmt.Fprintln(tw, "records\t\t\t\t\t\t\t\t") + fmt.Fprintf(tw, " set\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { + return humanize.Count.Uint64(p.NumEntries - p.NumDeletions - p.NumMergeOperands) + })...) + fmt.Fprintf(tw, " delete\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumDeletions - p.NumRangeDeletions) })...) + fmt.Fprintf(tw, " delete-sized\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumSizedDeletions) })...) + fmt.Fprintf(tw, " range-delete\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumRangeDeletions) })...) + fmt.Fprintf(tw, " range-key-sets\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumRangeKeySets) })...) + fmt.Fprintf(tw, " range-key-unsets\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumRangeKeyUnSets) })...) + fmt.Fprintf(tw, " range-key-deletes\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumRangeKeyDeletes) })...) + fmt.Fprintf(tw, " merge\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumMergeOperands) })...) + fmt.Fprintf(tw, " pinned\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.SnapshotPinnedKeys) })...) + + if err := tw.Flush(); err != nil { + return err + } + return nil + }() + if err != nil { + fmt.Fprintln(stderr, err) + } +} + +func (d *dbT) runSet(cmd *cobra.Command, args []string) { + stderr := cmd.ErrOrStderr() + db, err := d.openDB(args[0], nonReadOnly{}) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer d.closeDB(stderr, db) + var k, v key + if err := k.Set(args[1]); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + if err := v.Set(args[2]); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + + if err := db.Set(k, v, nil); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + } +} + +func propArgs(props []props, getProp func(*props) interface{}) []interface{} { + args := make([]interface{}, 0, len(props)) + for _, p := range props { + args = append(args, getProp(&p)) + } + return args +} + +type props struct { + Count uint64 + SmallestSeqNum uint64 + LargestSeqNum uint64 + DataSize uint64 + FilterSize uint64 + IndexSize uint64 + NumDataBlocks uint64 + NumIndexBlocks uint64 + NumDeletions uint64 + NumSizedDeletions uint64 + NumEntries uint64 + NumMergeOperands uint64 + NumRangeDeletions uint64 + NumRangeKeySets uint64 + NumRangeKeyUnSets uint64 + NumRangeKeyDeletes uint64 + RawKeySize uint64 + RawPointTombstoneKeySize uint64 + RawPointTombstoneValueSize uint64 + RawValueSize uint64 + SnapshotPinnedKeys uint64 + SnapshotPinnedKeySize uint64 + SnapshotPinnedValueSize uint64 + TopLevelIndexSize uint64 +} + +func (p *props) update(o props) { + p.Count += o.Count + if o.SmallestSeqNum != 0 && (o.SmallestSeqNum < p.SmallestSeqNum || p.SmallestSeqNum == 0) { + p.SmallestSeqNum = o.SmallestSeqNum + } + if o.LargestSeqNum > p.LargestSeqNum { + p.LargestSeqNum = o.LargestSeqNum + } + p.DataSize += o.DataSize + p.FilterSize += o.FilterSize + p.IndexSize += o.IndexSize + p.NumDataBlocks += o.NumDataBlocks + p.NumIndexBlocks += o.NumIndexBlocks + p.NumDeletions += o.NumDeletions + p.NumSizedDeletions += o.NumSizedDeletions + p.NumEntries += o.NumEntries + p.NumMergeOperands += o.NumMergeOperands + p.NumRangeDeletions += o.NumRangeDeletions + p.NumRangeKeySets += o.NumRangeKeySets + p.NumRangeKeyUnSets += o.NumRangeKeyUnSets + p.NumRangeKeyDeletes += o.NumRangeKeyDeletes + p.RawKeySize += o.RawKeySize + p.RawPointTombstoneKeySize += o.RawPointTombstoneKeySize + p.RawPointTombstoneValueSize += o.RawPointTombstoneValueSize + p.RawValueSize += o.RawValueSize + p.SnapshotPinnedKeySize += o.SnapshotPinnedKeySize + p.SnapshotPinnedValueSize += o.SnapshotPinnedValueSize + p.SnapshotPinnedKeys += o.SnapshotPinnedKeys + p.TopLevelIndexSize += o.TopLevelIndexSize +} + +func (d *dbT) addProps( + objProvider objstorage.Provider, m manifest.PhysicalFileMeta, p *props, +) error { + ctx := context.Background() + f, err := objProvider.OpenForReading(ctx, base.FileTypeTable, m.FileBacking.DiskFileNum, objstorage.OpenOptions{}) + if err != nil { + return err + } + r, err := sstable.NewReader(f, sstable.ReaderOptions{}, d.mergers, d.comparers) + if err != nil { + _ = f.Close() + return err + } + p.update(props{ + Count: 1, + SmallestSeqNum: m.SmallestSeqNum, + LargestSeqNum: m.LargestSeqNum, + DataSize: r.Properties.DataSize, + FilterSize: r.Properties.FilterSize, + IndexSize: r.Properties.IndexSize, + NumDataBlocks: r.Properties.NumDataBlocks, + NumIndexBlocks: 1 + r.Properties.IndexPartitions, + NumDeletions: r.Properties.NumDeletions, + NumSizedDeletions: r.Properties.NumSizedDeletions, + NumEntries: r.Properties.NumEntries, + NumMergeOperands: r.Properties.NumMergeOperands, + NumRangeDeletions: r.Properties.NumRangeDeletions, + NumRangeKeySets: r.Properties.NumRangeKeySets, + NumRangeKeyUnSets: r.Properties.NumRangeKeyUnsets, + NumRangeKeyDeletes: r.Properties.NumRangeKeyDels, + RawKeySize: r.Properties.RawKeySize, + RawPointTombstoneKeySize: r.Properties.RawPointTombstoneKeySize, + RawPointTombstoneValueSize: r.Properties.RawPointTombstoneValueSize, + RawValueSize: r.Properties.RawValueSize, + SnapshotPinnedKeySize: r.Properties.SnapshotPinnedKeySize, + SnapshotPinnedValueSize: r.Properties.SnapshotPinnedValueSize, + SnapshotPinnedKeys: r.Properties.SnapshotPinnedKeys, + TopLevelIndexSize: r.Properties.TopLevelIndexSize, + }) + return r.Close() +} + +func makePlural(singular string, count int64) string { + if count > 1 { + return fmt.Sprintf("%ss", singular) + } + return singular +} diff --git a/pebble/tool/db_io_bench.go b/pebble/tool/db_io_bench.go new file mode 100644 index 0000000..a419882 --- /dev/null +++ b/pebble/tool/db_io_bench.go @@ -0,0 +1,316 @@ +// 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 tool + +import ( + "context" + "fmt" + "io" + "math" + "math/rand" + "slices" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/spf13/cobra" +) + +type benchIO struct { + readableIdx int + ofs int64 + size int + // elapsed time for the IO, filled out by performIOs. + elapsed time.Duration +} + +const maxIOSize = 1024 * 1024 + +// runIOBench runs an IO benchmark against the current sstables of a database. +// The workload is random IO, with various IO sizes. The main goal of the +// benchmark is to establish the relationship between IO size and latency, +// especially against shared object storage. +func (d *dbT) runIOBench(cmd *cobra.Command, args []string) { + stdout := cmd.OutOrStdout() + + ioSizes, err := parseIOSizes(d.ioSizes) + if err != nil { + fmt.Fprintf(stdout, "error parsing io-sizes: %s\n", err) + return + } + + db, err := d.openDB(args[0]) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + defer d.closeDB(stdout, db) + + readables, err := d.openBenchTables(db) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + + defer func() { + for _, r := range readables { + r.Close() + } + }() + + ios := genBenchIOs(stdout, readables, d.ioCount, ioSizes) + + levels := "L5,L6" + if d.allLevels { + levels = "all" + } + fmt.Fprintf(stdout, "IO count: %d Parallelism: %d Levels: %s\n", d.ioCount, d.ioParallelism, levels) + + var wg sync.WaitGroup + wg.Add(d.ioParallelism) + remainingIOs := ios + for i := 0; i < d.ioParallelism; i++ { + // We want to distribute the IOs among d.ioParallelism goroutines. At each + // step, we look at the number of IOs remaining and take the average (across + // the goroutines that are left); this deals with any rounding issues. + n := len(remainingIOs) / (d.ioParallelism - i) + go func(workerIdx int, ios []benchIO) { + defer wg.Done() + if err := performIOs(readables, ios); err != nil { + fmt.Fprintf(stdout, "worker %d encountered error: %v", workerIdx, err) + } + }(i, remainingIOs[:n]) + remainingIOs = remainingIOs[n:] + } + wg.Wait() + + elapsed := make([]time.Duration, d.ioCount) + for _, ioSize := range ioSizes { + elapsed = elapsed[:0] + for i := range ios { + if ios[i].size == ioSize { + elapsed = append(elapsed, ios[i].elapsed) + } + } + fmt.Fprintf(stdout, "%4dKB -- %s\n", ioSize/1024, getStats(elapsed)) + } +} + +// genBenchIOs generates IOs for each given size. All IOs (across all +// sizes) are in random order. +func genBenchIOs( + stdout io.Writer, readables []objstorage.Readable, count int, sizes []int, +) []benchIO { + // size[i] is the size of the object, in blocks of maxIOSize. + size := make([]int, len(readables)) + // sum[i] is the sum (size[0] + ... + size[i]). + sum := make([]int, len(readables)) + total := 0 + for i, r := range readables { + size[i] = int(r.Size() / maxIOSize) + total += size[i] + sum[i] = total + } + fmt.Fprintf(stdout, "Opened %d objects; total size %d MB.\n", len(readables), total*maxIOSize/(1024*1024)) + + // To avoid a lot of overlap between the reads, the total size should be a + // factor larger than the size we will actually read (for the largest IO + // size). + const sizeFactor = 2 + if total*maxIOSize < count*sizes[len(sizes)-1]*sizeFactor { + fmt.Fprintf(stdout, "Warning: store too small for the given IO count and sizes.\n") + } + + // Choose how many IOs we do for each object, by selecting a random block + // across all file blocks. + // The choice of objects will be the same across all IO sizes. + b := make([]int, count) + for i := range b { + b[i] = rand.Intn(total) + } + // For each b[i], find the index such that sum[idx-1] <= b < sum[idx]. + // Sorting b makes this easier: we can "merge" the sorted arrays b and sum. + sort.Ints(b) + rIdx := make([]int, count) + currIdx := 0 + for i := range b { + for b[i] >= sum[currIdx] { + currIdx++ + } + rIdx[i] = currIdx + } + + res := make([]benchIO, 0, count*len(sizes)) + for _, ioSize := range sizes { + for _, idx := range rIdx { + // Random ioSize aligned offset. + ofs := ioSize * rand.Intn(size[idx]*maxIOSize/ioSize) + + res = append(res, benchIO{ + readableIdx: idx, + ofs: int64(ofs), + size: ioSize, + }) + } + } + rand.Shuffle(len(res), func(i, j int) { + res[i], res[j] = res[j], res[i] + }) + return res +} + +// openBenchTables opens the sstables for the benchmark and returns them as a +// list of Readables. +// +// By default, only L5/L6 sstables are used; all levels are used if the +// allLevels flag is set. +// +// Note that only sstables that are at least maxIOSize (1MB) are used. +func (d *dbT) openBenchTables(db *pebble.DB) ([]objstorage.Readable, error) { + tables, err := db.SSTables() + if err != nil { + return nil, err + } + startLevel := 5 + if d.allLevels { + startLevel = 0 + } + + var nums []base.DiskFileNum + numsMap := make(map[base.DiskFileNum]struct{}) + for l := startLevel; l < len(tables); l++ { + for _, t := range tables[l] { + n := t.BackingSSTNum.DiskFileNum() + if _, ok := numsMap[n]; !ok { + nums = append(nums, n) + numsMap[n] = struct{}{} + } + } + } + + p := db.ObjProvider() + var res []objstorage.Readable + for _, n := range nums { + r, err := p.OpenForReading(context.Background(), base.FileTypeTable, n, objstorage.OpenOptions{}) + if err != nil { + for _, r := range res { + _ = r.Close() + } + return nil, err + } + if r.Size() < maxIOSize { + _ = r.Close() + continue + } + res = append(res, r) + } + if len(res) == 0 { + return nil, errors.Errorf("no sstables (with size at least %d)", maxIOSize) + } + + return res, nil +} + +// parseIOSizes parses a comma-separated list of IO sizes, in KB. +func parseIOSizes(sizes string) ([]int, error) { + var res []int + for _, s := range strings.Split(sizes, ",") { + n, err := strconv.Atoi(s) + if err != nil { + return nil, err + } + ioSize := n * 1024 + if ioSize > maxIOSize { + return nil, errors.Errorf("IO sizes over %d not supported", maxIOSize) + } + if maxIOSize%ioSize != 0 { + return nil, errors.Errorf("IO size must be a divisor of %d", maxIOSize) + } + res = append(res, ioSize) + } + if len(res) == 0 { + return nil, errors.Errorf("no IO sizes specified") + } + sort.Ints(res) + return res, nil +} + +// performIOs performs the given list of IOs and populates the elapsed fields. +func performIOs(readables []objstorage.Readable, ios []benchIO) error { + ctx := context.Background() + rh := make([]objstorage.ReadHandle, len(readables)) + for i := range rh { + rh[i] = readables[i].NewReadHandle(ctx) + } + defer func() { + for i := range rh { + rh[i].Close() + } + }() + + buf := make([]byte, maxIOSize) + startTime := time.Now() + var firstErr error + var nOtherErrs int + for i := range ios { + if err := rh[ios[i].readableIdx].ReadAt(ctx, buf[:ios[i].size], ios[i].ofs); err != nil { + if firstErr == nil { + firstErr = err + } else { + nOtherErrs++ + } + } + endTime := time.Now() + ios[i].elapsed = endTime.Sub(startTime) + startTime = endTime + } + if nOtherErrs > 0 { + return errors.Errorf("%v; plus %d more errors", firstErr, nOtherErrs) + } + return firstErr +} + +// getStats calculates various statistics given a list of elapsed times. +func getStats(d []time.Duration) string { + slices.Sort(d) + + factor := 1.0 / float64(len(d)) + var mean float64 + for i := range d { + mean += float64(d[i]) * factor + } + var variance float64 + for i := range d { + delta := float64(d[i]) - mean + variance += delta * delta * factor + } + + toStr := func(d time.Duration) string { + if d < 10*time.Millisecond { + return fmt.Sprintf("%1.2fms", float64(d)/float64(time.Millisecond)) + } + if d < 100*time.Millisecond { + return fmt.Sprintf("%2.1fms", float64(d)/float64(time.Millisecond)) + } + return fmt.Sprintf("%4dms", d/time.Millisecond) + } + + return fmt.Sprintf( + "avg %s stddev %s p10 %s p50 %s p90 %s p95 %s p99 %s", + toStr(time.Duration(mean)), + toStr(time.Duration(math.Sqrt(variance))), + toStr(d[len(d)*10/100]), + toStr(d[len(d)*50/100]), + toStr(d[len(d)*90/100]), + toStr(d[len(d)*95/100]), + toStr(d[len(d)*99/100]), + ) +} diff --git a/pebble/tool/db_test.go b/pebble/tool/db_test.go new file mode 100644 index 0000000..b3ce5f0 --- /dev/null +++ b/pebble/tool/db_test.go @@ -0,0 +1,11 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import "testing" + +func TestDB(t *testing.T) { + runTests(t, "testdata/db_*") +} diff --git a/pebble/tool/find.go b/pebble/tool/find.go new file mode 100644 index 0000000..5a18ec1 --- /dev/null +++ b/pebble/tool/find.go @@ -0,0 +1,641 @@ +// Copyright 2020 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 tool + +import ( + "bytes" + "cmp" + "fmt" + "io" + "slices" + "sort" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/internal/private" + "github.com/cockroachdb/pebble/internal/rangedel" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/sstable" + "github.com/spf13/cobra" +) + +type findRef struct { + key base.InternalKey + value []byte + fileNum base.FileNum +} + +// findT implements the find tool. +// +// TODO(bananabrick): Add support for virtual sstables in this tool. Currently, +// the tool will work because we're parsing files from disk, so virtual sstables +// will never be added to findT.tables. The manifest could contain information +// about virtual sstables. This is fine because the manifest is only used to +// compute the findT.editRefs, and editRefs is only used if a file in +// findT.tables contains a key. Of course, the tool won't be completely +// accurate without dealing with virtual sstable case. +type findT struct { + Root *cobra.Command + + // Configuration. + opts *pebble.Options + comparers sstable.Comparers + mergers sstable.Mergers + + // Flags. + comparerName string + fmtKey keyFormatter + fmtValue valueFormatter + verbose bool + + // Map from file num to path on disk. + files map[base.DiskFileNum]string + // Map from file num to version edit index which references the file num. + editRefs map[base.FileNum][]int + // List of version edits. + edits []manifest.VersionEdit + // Sorted list of WAL file nums. + logs []base.DiskFileNum + // Sorted list of manifest file nums. + manifests []base.DiskFileNum + // Sorted list of table file nums. + tables []base.FileNum + // Set of tables that contains references to the search key. + tableRefs map[base.FileNum]bool + // Map from file num to table metadata. + tableMeta map[base.FileNum]*manifest.FileMetadata + // List of error messages for SSTables that could not be decoded. + errors []string +} + +func newFind( + opts *pebble.Options, + comparers sstable.Comparers, + defaultComparer string, + mergers sstable.Mergers, +) *findT { + f := &findT{ + opts: opts, + comparers: comparers, + mergers: mergers, + } + f.fmtKey.mustSet("quoted") + f.fmtValue.mustSet("[%x]") + + f.Root = &cobra.Command{ + Use: "find ", + Short: "find references to the specified key", + Long: ` +Find references to the specified key and any range tombstones that contain the +key. This includes references to the key in WAL files and sstables, and the +provenance of the sstables (flushed, ingested, compacted). +`, + Args: cobra.ExactArgs(2), + Run: f.run, + } + + f.Root.Flags().BoolVarP( + &f.verbose, "verbose", "v", false, "verbose output") + f.Root.Flags().StringVar( + &f.comparerName, "comparer", defaultComparer, "comparer name") + f.Root.Flags().Var( + &f.fmtKey, "key", "key formatter") + f.Root.Flags().Var( + &f.fmtValue, "value", "value formatter") + return f +} + +func (f *findT) run(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + var key key + if err := key.Set(args[1]); err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + + if err := f.findFiles(stdout, stderr, args[0]); err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + f.readManifests(stdout) + + f.opts.Comparer = f.comparers[f.comparerName] + if f.opts.Comparer == nil { + fmt.Fprintf(stderr, "unknown comparer %q", f.comparerName) + return + } + f.fmtKey.setForComparer(f.opts.Comparer.Name, f.comparers) + f.fmtValue.setForComparer(f.opts.Comparer.Name, f.comparers) + + refs := f.search(stdout, key) + var lastFileNum base.FileNum + for i := range refs { + r := &refs[i] + if lastFileNum != r.fileNum { + lastFileNum = r.fileNum + fmt.Fprintf(stdout, "%s", f.opts.FS.PathBase(f.files[r.fileNum.DiskFileNum()])) + if m := f.tableMeta[r.fileNum]; m != nil { + fmt.Fprintf(stdout, " ") + formatKeyRange(stdout, f.fmtKey, &m.Smallest, &m.Largest) + } + fmt.Fprintf(stdout, "\n") + if p := f.tableProvenance(r.fileNum); p != "" { + fmt.Fprintf(stdout, " (%s)\n", p) + } + } + fmt.Fprintf(stdout, " ") + formatKeyValue(stdout, f.fmtKey, f.fmtValue, &r.key, r.value) + } + + for _, errorMsg := range f.errors { + fmt.Fprint(stdout, errorMsg) + } +} + +// Find all of the manifests, logs, and tables in the specified directory. +func (f *findT) findFiles(stdout, stderr io.Writer, dir string) error { + f.files = make(map[base.DiskFileNum]string) + f.editRefs = make(map[base.FileNum][]int) + f.logs = nil + f.manifests = nil + f.tables = nil + f.tableMeta = make(map[base.FileNum]*manifest.FileMetadata) + + if _, err := f.opts.FS.Stat(dir); err != nil { + return err + } + + walk(stderr, f.opts.FS, dir, func(path string) { + ft, fileNum, ok := base.ParseFilename(f.opts.FS, path) + if !ok { + return + } + switch ft { + case base.FileTypeLog: + f.logs = append(f.logs, fileNum) + case base.FileTypeManifest: + f.manifests = append(f.manifests, fileNum) + case base.FileTypeTable: + f.tables = append(f.tables, fileNum.FileNum()) + default: + return + } + f.files[fileNum] = path + }) + + slices.Sort(f.logs) + slices.Sort(f.manifests) + slices.Sort(f.tables) + + if f.verbose { + fmt.Fprintf(stdout, "%s\n", dir) + fmt.Fprintf(stdout, "%5d %s\n", len(f.manifests), makePlural("manifest", int64(len(f.manifests)))) + fmt.Fprintf(stdout, "%5d %s\n", len(f.logs), makePlural("log", int64(len(f.logs)))) + fmt.Fprintf(stdout, "%5d %s\n", len(f.tables), makePlural("sstable", int64(len(f.tables)))) + } + return nil +} + +// Read the manifests and populate the editRefs map which is used to determine +// the provenance and metadata of tables. +func (f *findT) readManifests(stdout io.Writer) { + for _, fileNum := range f.manifests { + func() { + path := f.files[fileNum] + mf, err := f.opts.FS.Open(path) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + defer mf.Close() + + if f.verbose { + fmt.Fprintf(stdout, "%s\n", path) + } + + rr := record.NewReader(mf, 0 /* logNum */) + for { + r, err := rr.Next() + if err != nil { + if err != io.EOF { + fmt.Fprintf(stdout, "%s: %s\n", path, err) + } + break + } + + var ve manifest.VersionEdit + if err := ve.Decode(r); err != nil { + fmt.Fprintf(stdout, "%s: %s\n", path, err) + break + } + i := len(f.edits) + f.edits = append(f.edits, ve) + + if ve.ComparerName != "" { + f.comparerName = ve.ComparerName + } + if num := ve.MinUnflushedLogNum.FileNum(); num != 0 { + f.editRefs[num] = append(f.editRefs[num], i) + } + for df := range ve.DeletedFiles { + f.editRefs[df.FileNum] = append(f.editRefs[df.FileNum], i) + } + for _, nf := range ve.NewFiles { + // The same file can be deleted and added in a single version edit + // which indicates a "move" compaction. Only add the edit to the list + // once. + refs := f.editRefs[nf.Meta.FileNum] + if n := len(refs); n == 0 || refs[n-1] != i { + f.editRefs[nf.Meta.FileNum] = append(refs, i) + } + if _, ok := f.tableMeta[nf.Meta.FileNum]; !ok { + f.tableMeta[nf.Meta.FileNum] = nf.Meta + } + } + } + }() + } + + if f.verbose { + fmt.Fprintf(stdout, "%5d %s\n", len(f.edits), makePlural("edit", int64(len(f.edits)))) + } +} + +// Search the logs and sstables for references to the specified key. +func (f *findT) search(stdout io.Writer, key []byte) []findRef { + refs := f.searchLogs(stdout, key, nil) + refs = f.searchTables(stdout, key, refs) + + // For a given file (log or table) the references are already in the correct + // order. We simply want to order the references by fileNum using a stable + // sort. + // + // TODO(peter): I'm not sure if this is perfectly correct with regards to log + // files and ingested sstables, but it is close enough and doing something + // better is onerous. Revisit if this ever becomes problematic (e.g. if we + // allow finding more than one key at a time). + // + // An example of the problem with logs and ingestion (which can only occur + // with distinct keys). If I write key "a" to a log, I can then ingested key + // "b" without causing "a" to be flushed. Then I can write key "c" to the + // log. Ideally, we'd show the key "a" from the log, then the key "b" from + // the ingested sstable, then key "c" from the log. + slices.SortStableFunc(refs, func(a, b findRef) int { + return cmp.Compare(a.fileNum, b.fileNum) + }) + return refs +} + +// Search the logs for references to the specified key. +func (f *findT) searchLogs(stdout io.Writer, searchKey []byte, refs []findRef) []findRef { + cmp := f.opts.Comparer.Compare + for _, fileNum := range f.logs { + _ = func() (err error) { + path := f.files[fileNum] + lf, err := f.opts.FS.Open(path) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + defer lf.Close() + + if f.verbose { + fmt.Fprintf(stdout, "%s", path) + defer fmt.Fprintf(stdout, "\n") + } + defer func() { + switch err { + case record.ErrZeroedChunk: + if f.verbose { + fmt.Fprintf(stdout, ": EOF [%s] (may be due to WAL preallocation)", err) + } + case record.ErrInvalidChunk: + if f.verbose { + fmt.Fprintf(stdout, ": EOF [%s] (may be due to WAL recycling)", err) + } + default: + if err != io.EOF { + if f.verbose { + fmt.Fprintf(stdout, ": %s", err) + } else { + fmt.Fprintf(stdout, "%s: %s\n", path, err) + } + } + } + }() + + var b pebble.Batch + var buf bytes.Buffer + rr := record.NewReader(lf, fileNum) + for { + r, err := rr.Next() + if err == nil { + buf.Reset() + _, err = io.Copy(&buf, r) + } + if err != nil { + return err + } + + b = pebble.Batch{} + if err := b.SetRepr(buf.Bytes()); err != nil { + fmt.Fprintf(stdout, "%s: corrupt log file: %v", path, err) + continue + } + seqNum := b.SeqNum() + for r := b.Reader(); ; seqNum++ { + kind, ukey, value, ok, err := r.Next() + if !ok { + if err != nil { + fmt.Fprintf(stdout, "%s: corrupt log file: %v", path, err) + break + } + break + } + ikey := base.MakeInternalKey(ukey, seqNum, kind) + switch kind { + case base.InternalKeyKindDelete, + base.InternalKeyKindDeleteSized, + base.InternalKeyKindSet, + base.InternalKeyKindMerge, + base.InternalKeyKindSingleDelete, + base.InternalKeyKindSetWithDelete: + if cmp(searchKey, ikey.UserKey) != 0 { + continue + } + case base.InternalKeyKindRangeDelete: + // Output tombstones that contain or end with the search key. + t := rangedel.Decode(ikey, value, nil) + if !t.Contains(cmp, searchKey) && cmp(t.End, searchKey) != 0 { + continue + } + default: + continue + } + + refs = append(refs, findRef{ + key: ikey.Clone(), + value: append([]byte(nil), value...), + fileNum: fileNum.FileNum(), + }) + } + } + }() + } + return refs +} + +// Search the tables for references to the specified key. +func (f *findT) searchTables(stdout io.Writer, searchKey []byte, refs []findRef) []findRef { + cache := pebble.NewCache(128 << 20 /* 128 MB */) + defer cache.Unref() + + f.tableRefs = make(map[base.FileNum]bool) + for _, fileNum := range f.tables { + _ = func() (err error) { + path := f.files[fileNum.DiskFileNum()] + tf, err := f.opts.FS.Open(path) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + + m := f.tableMeta[fileNum] + if f.verbose { + fmt.Fprintf(stdout, "%s", path) + if m != nil && m.SmallestSeqNum == m.LargestSeqNum { + fmt.Fprintf(stdout, ": global seqnum: %d", m.LargestSeqNum) + } + defer fmt.Fprintf(stdout, "\n") + } + defer func() { + switch { + case err != nil: + if f.verbose { + fmt.Fprintf(stdout, ": %v", err) + } else { + fmt.Fprintf(stdout, "%s: %v\n", path, err) + } + } + }() + + opts := sstable.ReaderOptions{ + Cache: cache, + Comparer: f.opts.Comparer, + Filters: f.opts.Filters, + } + readable, err := sstable.NewSimpleReadable(tf) + if err != nil { + return err + } + r, err := sstable.NewReader(readable, opts, f.comparers, f.mergers, + private.SSTableRawTombstonesOpt.(sstable.ReaderOption)) + if err != nil { + f.errors = append(f.errors, fmt.Sprintf("Unable to decode sstable %s, %s", f.files[fileNum.DiskFileNum()], err.Error())) + // Ensure the error only gets printed once. + err = nil + return + } + defer r.Close() + + if m != nil && m.SmallestSeqNum == m.LargestSeqNum { + r.Properties.GlobalSeqNum = m.LargestSeqNum + } + + iter, err := r.NewIter(nil, nil) + if err != nil { + return err + } + defer iter.Close() + key, value := iter.SeekGE(searchKey, base.SeekGEFlagsNone) + + // We configured sstable.Reader to return raw tombstones which requires a + // bit more work here to put them in a form that can be iterated in + // parallel with the point records. + rangeDelIter, err := func() (keyspan.FragmentIterator, error) { + iter, err := r.NewRawRangeDelIter() + if err != nil { + return nil, err + } + if iter == nil { + return keyspan.NewIter(r.Compare, nil), nil + } + defer iter.Close() + + var tombstones []keyspan.Span + for t := iter.First(); t != nil; t = iter.Next() { + if !t.Contains(r.Compare, searchKey) { + continue + } + tombstones = append(tombstones, t.ShallowClone()) + } + + slices.SortFunc(tombstones, func(a, b keyspan.Span) int { + return r.Compare(a.Start, b.Start) + }) + return keyspan.NewIter(r.Compare, tombstones), nil + }() + if err != nil { + return err + } + + defer rangeDelIter.Close() + rangeDel := rangeDelIter.First() + + foundRef := false + for key != nil || rangeDel != nil { + if key != nil && + (rangeDel == nil || r.Compare(key.UserKey, rangeDel.Start) < 0) { + if r.Compare(searchKey, key.UserKey) != 0 { + key, value = nil, base.LazyValue{} + continue + } + v, _, err := value.Value(nil) + if err != nil { + return err + } + refs = append(refs, findRef{ + key: key.Clone(), + value: append([]byte(nil), v...), + fileNum: fileNum, + }) + key, value = iter.Next() + } else { + // Use rangedel.Encode to add a reference for each key + // within the span. + err := rangedel.Encode(rangeDel, func(k base.InternalKey, v []byte) error { + refs = append(refs, findRef{ + key: k.Clone(), + value: append([]byte(nil), v...), + fileNum: fileNum, + }) + return nil + }) + if err != nil { + return err + } + rangeDel = rangeDelIter.Next() + } + foundRef = true + } + + if foundRef { + f.tableRefs[fileNum] = true + } + return nil + }() + } + return refs +} + +// Determine the provenance of the specified table. We search the version edits +// for the first edit which created the table, and then analyze the edit to +// determine if it was a compaction, flush, or ingestion. Returns an empty +// string if the provenance of a table cannot be determined. +func (f *findT) tableProvenance(fileNum base.FileNum) string { + editRefs := f.editRefs[fileNum] + for len(editRefs) > 0 { + ve := f.edits[editRefs[0]] + editRefs = editRefs[1:] + for _, nf := range ve.NewFiles { + if fileNum != nf.Meta.FileNum { + continue + } + + var buf bytes.Buffer + switch { + case len(ve.DeletedFiles) > 0: + // A version edit with deleted files is a compaction. The deleted + // files are the inputs to the compaction. We're going to + // reconstruct the input files and display those inputs that + // contain the search key (i.e. are list in refs) and use an + // ellipsis to indicate when there were other inputs that have + // been elided. + var sourceLevels []int + levels := make(map[int][]base.FileNum) + for df := range ve.DeletedFiles { + files := levels[df.Level] + if len(files) == 0 { + sourceLevels = append(sourceLevels, df.Level) + } + levels[df.Level] = append(files, df.FileNum) + } + + sort.Ints(sourceLevels) + if sourceLevels[len(sourceLevels)-1] != nf.Level { + sourceLevels = append(sourceLevels, nf.Level) + } + + sep := " " + fmt.Fprintf(&buf, "compacted") + for _, level := range sourceLevels { + files := levels[level] + slices.Sort(files) + + fmt.Fprintf(&buf, "%sL%d [", sep, level) + sep = "" + elided := false + for _, fileNum := range files { + if f.tableRefs[fileNum] { + fmt.Fprintf(&buf, "%s%s", sep, fileNum) + sep = " " + } else { + elided = true + } + } + if elided { + fmt.Fprintf(&buf, "%s...", sep) + } + fmt.Fprintf(&buf, "]") + sep = " + " + } + + case ve.MinUnflushedLogNum != 0: + // A version edit with a min-unflushed log indicates a flush + // operation. + fmt.Fprintf(&buf, "flushed to L%d", nf.Level) + + case nf.Meta.SmallestSeqNum == nf.Meta.LargestSeqNum: + // If the smallest and largest seqnum are the same, the file was + // ingested. Note that this can also occur for a flushed sstable + // that contains only a single key, though that would have + // already been captured above. + fmt.Fprintf(&buf, "ingested to L%d", nf.Level) + + default: + // The provenance of the table is unclear. This is usually due to + // the MANIFEST rolling over and taking a snapshot of the LSM + // state. + fmt.Fprintf(&buf, "added to L%d", nf.Level) + } + + // After a table is created, it can be moved to a different level via a + // move compaction. This is indicated by a version edit that deletes the + // table from one level and adds the same table to a different + // level. Loop over the remaining version edits for the table looking for + // such moves. + for len(editRefs) > 0 { + ve := f.edits[editRefs[0]] + editRefs = editRefs[1:] + for _, nf := range ve.NewFiles { + if fileNum == nf.Meta.FileNum { + for df := range ve.DeletedFiles { + if fileNum == df.FileNum { + fmt.Fprintf(&buf, ", moved to L%d", nf.Level) + break + } + } + break + } + } + } + + return buf.String() + } + } + return "" +} diff --git a/pebble/tool/find_test.go b/pebble/tool/find_test.go new file mode 100644 index 0000000..6a6c887 --- /dev/null +++ b/pebble/tool/find_test.go @@ -0,0 +1,11 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import "testing" + +func TestFind(t *testing.T) { + runTests(t, "testdata/find") +} diff --git a/pebble/tool/logs/compaction.go b/pebble/tool/logs/compaction.go new file mode 100644 index 0000000..cffe1da --- /dev/null +++ b/pebble/tool/logs/compaction.go @@ -0,0 +1,1230 @@ +// Copyright 2021 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 logs + +import ( + "bufio" + "bytes" + "cmp" + "fmt" + "math" + "os" + "path/filepath" + "regexp" + "slices" + "sort" + "strconv" + "strings" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/spf13/cobra" +) + +const numLevels = manifest.NumLevels + +var ( + // Captures a common logging prefix that can be used as the context for the + // surrounding information captured by other expressions. Example: + // + // I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [T1,n5,pebble,s5] ... + // + logContextPattern = regexp.MustCompile( + `^.*` + + /* Timestamp */ `(?P\d{6} \d{2}:\d{2}:\d{2}.\d{6}).*` + + /* Node / Store */ `\[(T(\d+|\?),)?n(?P\d+|\?).*,s(?P\d+|\?).*?\].*`, + ) + logContextPatternTimestampIdx = logContextPattern.SubexpIndex("timestamp") + logContextPatternNodeIdx = logContextPattern.SubexpIndex("node") + logContextPatternStoreIdx = logContextPattern.SubexpIndex("store") + + // Matches either a compaction or a memtable flush log line. + // + // A compaction start / end line resembles: + // "[JOB X] compact(ed|ing)" + // + // A memtable flush start / end line resembles: + // "[JOB X] flush(ed|ing)" + // + // An ingested sstable flush looks like: + // "[JOB 226] flushed 6 ingested flushables" + sentinelPattern = regexp.MustCompile(`\[JOB.*(?Pcompact|flush|ingest)(?Ped|ing)[^:]`) + sentinelPatternPrefixIdx = sentinelPattern.SubexpIndex("prefix") + sentinelPatternSuffixIdx = sentinelPattern.SubexpIndex("suffix") + + // Example compaction start and end log lines: + // 23.1 and older: + // I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n5,pebble,s5] 1216510 [JOB 284925] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) + // I211215 14:26:56.318543 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n5,pebble,s5] 1216554 [JOB 284925] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s + // current: + // I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n5,pebble,s5] 1216510 [JOB 284925] compacting(default) L2 [442555] (4.2MB) + L3 [445853] (8.4MB) + // I211215 14:26:56.318543 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n5,pebble,s5] 1216554 [JOB 284925] compacted(default) L2 [442555] (4.2MB) + L3 [445853] (8.4MB) -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s + // + // NOTE: we use the log timestamp to compute the compaction duration rather + // than the Pebble log output. + compactionPattern = regexp.MustCompile( + `^.*` + + /* Job ID */ `\[JOB (?P\d+)]\s` + + /* Start / end */ `compact(?Ped|ing)` + + + /* Compaction type */ + `\((?P.*?)\)\s` + + /* Optional annotation*/ `?(\s*\[(?P.*?)\]\s*)?` + + + /* Start / end level */ + `(?PL(?P\d).*?(?:.*(?:\+|->)\sL(?P\d))?` + + /* Bytes */ + `(?:.*?\((?P[0-9.]+( [BKMGTPE]|[KMGTPE]?B))\))` + + /* Score */ + `?(\s*(Score=\d+(\.\d+)))?)`, + ) + compactionPatternJobIdx = compactionPattern.SubexpIndex("job") + compactionPatternSuffixIdx = compactionPattern.SubexpIndex("suffix") + compactionPatternTypeIdx = compactionPattern.SubexpIndex("type") + compactionPatternLevels = compactionPattern.SubexpIndex("levels") + compactionPatternFromIdx = compactionPattern.SubexpIndex("from") + compactionPatternToIdx = compactionPattern.SubexpIndex("to") + compactionPatternBytesIdx = compactionPattern.SubexpIndex("bytes") + + // Example memtable flush log lines: + // 23.1 and older: + // I211213 16:23:48.903751 21136 3@vendor/github.com/cockroachdb/pebble/event.go:599 â‹® [n9,pebble,s9] 24 [JOB 10] flushing 2 memtables to L0 + // I211213 16:23:49.134464 21136 3@vendor/github.com/cockroachdb/pebble/event.go:603 â‹® [n9,pebble,s9] 26 [JOB 10] flushed 2 memtables to L0 [1535806] (1.3 M), in 0.2s, output rate 5.8 M/s + // current: + // I211213 16:23:48.903751 21136 + // 3@vendor/github.com/cockroachdb/pebble/event.go:599 â‹® [n9,pebble,s9] 24 [JOB 10] flushing 2 memtables (1.4MB) to L0 + // I211213 16:23:49.134464 21136 + // 3@vendor/github.com/cockroachdb/pebble/event.go:603 â‹® [n9,pebble,s9] 26 [JOB 10] flushed 2 memtables (1.4MB) to L0 [1535806] (1.3MB), in 0.2s, output rate 5.8MB/s + // + // NOTE: we use the log timestamp to compute the flush duration rather than + // the Pebble log output. + flushPattern = regexp.MustCompile( + `^..*` + + /* Job ID */ `\[JOB (?P\d+)]\s` + + /* Compaction type */ `flush(?Ped|ing)\s` + + /* Memtable count; size (23.2+) */ `\d+ memtables? (\([^)]+\))?` + + /* SSTable Bytes */ `(?:.*?\((?P[0-9.]+( [BKMGTPE]|[KMGTPE]?B))\))?`, + ) + flushPatternSuffixIdx = flushPattern.SubexpIndex("suffix") + flushPatternJobIdx = flushPattern.SubexpIndex("job") + flushPatternBytesIdx = flushPattern.SubexpIndex("bytes") + + // Example ingested log lines: + // 23.1 and older: + // I220228 16:01:22.487906 18476248525 3@vendor/github.com/cockroachdb/pebble/ingest.go:637 â‹® [n24,pebble,s24] 33430782 [JOB 10211226] ingested L0:21818678 (1.8 K), L0:21818683 (1.2 K), L0:21818679 (1.6 K), L0:21818680 (1.1 K), L0:21818681 (1.1 K), L0:21818682 (160 M) + // current: + // I220228 16:01:22.487906 18476248525 3@vendor/github.com/cockroachdb/pebble/ingest.go:637 â‹® [n24,pebble,s24] 33430782 [JOB 10211226] ingested L0:21818678 (1.8KB), L0:21818683 (1.2KB), L0:21818679 (1.6KB), L0:21818680 (1.1KB), L0:21818681 (1.1KB), L0:21818682 (160MB) + // + ingestedPattern = regexp.MustCompile( + `^.*` + + /* Job ID */ `\[JOB (?P\d+)]\s` + + /* ingested */ `ingested\s`) + ingestedPatternJobIdx = ingestedPattern.SubexpIndex("job") + ingestedFilePattern = regexp.MustCompile( + `L` + + /* Level */ `(?P\d):` + + /* File number */ `(?P\d+)\s` + + /* Bytes */ `\((?P[0-9.]+( [BKMGTPE]|[KMGTPE]?B))\)`) + ingestedFilePatternLevelIdx = ingestedFilePattern.SubexpIndex("level") + ingestedFilePatternFileIdx = ingestedFilePattern.SubexpIndex("file") + ingestedFilePatternBytesIdx = ingestedFilePattern.SubexpIndex("bytes") + + // flushable ingestions + // + // I230831 04:13:28.824280 3780 3@pebble/event.go:685 â‹® [n10,s10,pebble] 365 [JOB 226] flushed 6 ingested flushables L0:024334 (1.5KB) + L0:024339 (1.0KB) + L0:024335 (1.9KB) + L0:024336 (1.1KB) + L0:024337 (1.1KB) + L0:024338 (12KB) in 0.0s (0.0s total), output rate 67MB/s + flushableIngestedPattern = regexp.MustCompile( + `^.*` + + /* Job ID */ `\[JOB (?P\d+)]\s` + + /* match ingested flushable */ `flushed \d ingested flushable`) + flushableIngestedPatternJobIdx = flushableIngestedPattern.SubexpIndex("job") + + // Example read-amp log line: + // 23.1 and older: + // total 31766 188 G - 257 G 187 G 48 K 3.6 G 744 536 G 49 K 278 G 5 2.1 + // current: + // total | 1 639B 0B | - | 84B | 0 0B | 0 0B | 3 1.9KB | 1.2KB | 1 23.7 + readAmpPattern = regexp.MustCompile( + /* Read amp */ `(?:^|\+)(?:\s{2}total|total \|).*?\s(?P\d+)\s.{4,7}$`, + ) + readAmpPatternValueIdx = readAmpPattern.SubexpIndex("value") +) + +const ( + // timeFmt matches the Cockroach log timestamp format. + // See: https://github.com/cockroachdb/cockroach/blob/master/pkg/util/log/format_crdb_v2.go + timeFmt = "060102 15:04:05.000000" + + // timeFmtSlim is similar to timeFmt, except that it strips components with a + // lower granularity than a minute. + timeFmtSlim = "060102 15:04" + + // timeFmtHrMinSec prints only the hour, minute and second of the time. + timeFmtHrMinSec = "15:04:05" +) + +// compactionType is the type of compaction. It tracks the types in +// compaction.go. We copy the values here to avoid exporting the types in +// compaction.go. +type compactionType uint8 + +const ( + compactionTypeDefault compactionType = iota + compactionTypeFlush + compactionTypeMove + compactionTypeDeleteOnly + compactionTypeElisionOnly + compactionTypeRead +) + +// String implements fmt.Stringer. +func (c compactionType) String() string { + switch c { + case compactionTypeDefault: + return "default" + case compactionTypeMove: + return "move" + case compactionTypeDeleteOnly: + return "delete-only" + case compactionTypeElisionOnly: + return "elision-only" + case compactionTypeRead: + return "read" + default: + panic(errors.Newf("unknown compaction type: %s", c)) + } +} + +// parseCompactionType parses the given compaction type string and returns a +// compactionType. +func parseCompactionType(s string) (t compactionType, err error) { + switch s { + case "default": + t = compactionTypeDefault + case "move": + t = compactionTypeMove + case "delete-only": + t = compactionTypeDeleteOnly + case "elision-only": + t = compactionTypeElisionOnly + case "read": + t = compactionTypeRead + default: + err = errors.Newf("unknown compaction type: %s", s) + } + return +} + +// compactionStart is a compaction start event. +type compactionStart struct { + ctx logContext + jobID int + cType compactionType + fromLevel int + toLevel int + inputBytes uint64 +} + +// parseCompactionStart converts the given regular expression sub-matches for a +// compaction start log line into a compactionStart event. +func parseCompactionStart(matches []string) (compactionStart, error) { + var start compactionStart + + // Parse job ID. + jobID, err := strconv.Atoi(matches[compactionPatternJobIdx]) + if err != nil { + return start, errors.Newf("could not parse jobID: %s", err) + } + + // Parse compaction type. + cType, err := parseCompactionType(matches[compactionPatternTypeIdx]) + if err != nil { + return start, err + } + + // Parse input bytes. + inputBytes, err := sumInputBytes(matches[compactionPatternLevels]) + if err != nil { + return start, errors.Newf("could not sum input bytes: %s", err) + } + + // Parse from-level. + from, err := strconv.Atoi(matches[compactionPatternFromIdx]) + if err != nil { + return start, errors.Newf("could not parse from-level: %s", err) + } + + // Parse to-level. For deletion and elision compactions, set the same level. + to := from + if cType != compactionTypeElisionOnly && cType != compactionTypeDeleteOnly { + to, err = strconv.Atoi(matches[compactionPatternToIdx]) + if err != nil { + return start, errors.Newf("could not parse to-level: %s", err) + } + } + + start = compactionStart{ + jobID: jobID, + cType: cType, + fromLevel: from, + toLevel: to, + inputBytes: inputBytes, + } + + return start, nil +} + +// compactionEnd is a compaction end event. +type compactionEnd struct { + jobID int + writtenBytes uint64 + // TODO(jackson): Parse and include the aggregate size of input + // sstables. It may be instructive, because compactions that drop + // keys write less data than they remove from the input level. +} + +// parseCompactionEnd converts the given regular expression sub-matches for a +// compaction end log line into a compactionEnd event. +func parseCompactionEnd(matches []string) (compactionEnd, error) { + var end compactionEnd + + // Parse job ID. + jobID, err := strconv.Atoi(matches[compactionPatternJobIdx]) + if err != nil { + return end, errors.Newf("could not parse jobID: %s", err) + } + end = compactionEnd{jobID: jobID} + + // Optionally, if we have compacted bytes. + if matches[compactionPatternBytesIdx] != "" { + end.writtenBytes = unHumanize(matches[compactionPatternBytesIdx]) + } + + return end, nil +} + +// parseFlushStart converts the given regular expression sub-matches for a +// memtable flush start log line into a compactionStart event. +func parseFlushStart(matches []string) (compactionStart, error) { + var start compactionStart + // Parse job ID. + jobID, err := strconv.Atoi(matches[flushPatternJobIdx]) + if err != nil { + return start, errors.Newf("could not parse jobID: %s", err) + } + c := compactionStart{ + jobID: jobID, + cType: compactionTypeFlush, + fromLevel: -1, + toLevel: 0, + } + return c, nil +} + +// parseFlushEnd converts the given regular expression sub-matches for a +// memtable flush end log line into a compactionEnd event. +func parseFlushEnd(matches []string) (compactionEnd, error) { + var end compactionEnd + + // Parse job ID. + jobID, err := strconv.Atoi(matches[flushPatternJobIdx]) + if err != nil { + return end, errors.Newf("could not parse jobID: %s", err) + } + end = compactionEnd{jobID: jobID} + + // Optionally, if we have flushed bytes. + if matches[flushPatternBytesIdx] != "" { + end.writtenBytes = unHumanize(matches[flushPatternBytesIdx]) + } + + return end, nil +} + +// event describes an aggregated event (eg, start and end events +// combined if necessary). +type event struct { + nodeID int + storeID int + jobID int + timeStart time.Time + timeEnd time.Time + compaction *compaction + ingest *ingest +} + +// compaction represents an aggregated compaction event (i.e. the combination of +// a start and end event). +type compaction struct { + cType compactionType + fromLevel int + toLevel int + inputBytes uint64 + outputBytes uint64 +} + +// ingest describes the completion of an ingest. +type ingest struct { + files []ingestedFile +} + +type ingestedFile struct { + level int + fileNum int + sizeBytes uint64 +} + +// readAmp represents a read-amp event. +type readAmp struct { + ctx logContext + readAmp int +} + +type nodeStoreJob struct { + node, store, job int +} + +func (n nodeStoreJob) String() string { + return fmt.Sprintf("(node=%d,store=%d,job=%d)", n.node, n.store, n.job) +} + +type errorEvent struct { + path string + line string + err error +} + +// logEventCollector keeps track of open compaction events and read-amp events +// over the course of parsing log line events. Completed compaction events are +// added to the collector once a matching start and end pair are encountered. +// Read-amp events are added as they are encountered (the have no start / end +// concept). +type logEventCollector struct { + ctx logContext + m map[nodeStoreJob]compactionStart + events []event + readAmps []readAmp + errors []errorEvent +} + +// newEventCollector instantiates a new logEventCollector. +func newEventCollector() *logEventCollector { + return &logEventCollector{ + m: make(map[nodeStoreJob]compactionStart), + } +} + +// addError records an error encountered during log parsing. +func (c *logEventCollector) addError(path, line string, err error) { + c.errors = append(c.errors, errorEvent{path: path, line: line, err: err}) +} + +// addCompactionStart adds a new compactionStart to the collector. The event is +// tracked by its job ID. +func (c *logEventCollector) addCompactionStart(start compactionStart) error { + key := nodeStoreJob{c.ctx.node, c.ctx.store, start.jobID} + if _, ok := c.m[key]; ok { + return errors.Newf("start event already seen for %s", key) + } + start.ctx = c.ctx + c.m[key] = start + return nil +} + +// addCompactionEnd completes the compaction event for the given compactionEnd. +func (c *logEventCollector) addCompactionEnd(end compactionEnd) { + key := nodeStoreJob{c.ctx.node, c.ctx.store, end.jobID} + start, ok := c.m[key] + if !ok { + _, _ = fmt.Fprintf( + os.Stderr, + "compaction end event missing start event for %s; skipping\n", key, + ) + return + } + + // Remove the job from the collector once it has been matched. + delete(c.m, key) + + c.events = append(c.events, event{ + nodeID: start.ctx.node, + storeID: start.ctx.store, + jobID: start.jobID, + timeStart: start.ctx.timestamp, + timeEnd: c.ctx.timestamp, + compaction: &compaction{ + cType: start.cType, + fromLevel: start.fromLevel, + toLevel: start.toLevel, + inputBytes: start.inputBytes, + outputBytes: end.writtenBytes, + }, + }) +} + +// addReadAmp adds the readAmp event to the collector. +func (c *logEventCollector) addReadAmp(ra readAmp) { + ra.ctx = c.ctx + c.readAmps = append(c.readAmps, ra) +} + +// logContext captures the metadata of log lines. +type logContext struct { + timestamp time.Time + node, store int +} + +// saveContext saves the given logContext in the collector. +func (c *logEventCollector) saveContext(ctx logContext) { + c.ctx = ctx +} + +// level is a level in the LSM. The WAL is level -1. +type level int + +// String implements fmt.Stringer. +func (l level) String() string { + if l == -1 { + return "WAL" + } + return "L" + strconv.Itoa(int(l)) +} + +// fromTo is a map key for (from, to) level tuples. +type fromTo struct { + from, to level +} + +// compactionTypeCount is a mapping from compaction type to count. +type compactionTypeCount map[compactionType]int + +// windowSummary summarizes events in a window of time between a start and end +// time. The window tracks: +// - for each compaction type: counts, total bytes compacted, and total duration. +// - total ingested bytes for each level +// - read amp magnitudes +type windowSummary struct { + nodeID, storeID int + tStart, tEnd time.Time + eventCount int + flushedCount int + flushedBytes uint64 + flushedTime time.Duration + compactionCounts map[fromTo]compactionTypeCount + compactionBytesIn map[fromTo]uint64 + compactionBytesOut map[fromTo]uint64 + compactionBytesMoved map[fromTo]uint64 + compactionBytesDel map[fromTo]uint64 + compactionTime map[fromTo]time.Duration + ingestedCount [numLevels]int + ingestedBytes [numLevels]uint64 + readAmps []readAmp + longRunning []event +} + +// String implements fmt.Stringer, returning a formatted window summary. +func (s windowSummary) String() string { + type fromToCount struct { + ft fromTo + counts compactionTypeCount + bytesIn uint64 + bytesOut uint64 + bytesMoved uint64 + bytesDel uint64 + duration time.Duration + } + var pairs []fromToCount + for k, v := range s.compactionCounts { + pairs = append(pairs, fromToCount{ + ft: k, + counts: v, + bytesIn: s.compactionBytesIn[k], + bytesOut: s.compactionBytesOut[k], + bytesMoved: s.compactionBytesMoved[k], + bytesDel: s.compactionBytesDel[k], + duration: s.compactionTime[k], + }) + } + slices.SortFunc(pairs, func(l, r fromToCount) int { + if v := cmp.Compare(l.ft.from, r.ft.from); v != 0 { + return v + } + return cmp.Compare(l.ft.to, r.ft.to) + }) + + nodeID, storeID := "?", "?" + if s.nodeID != -1 { + nodeID = strconv.Itoa(s.nodeID) + } + if s.storeID != -1 { + storeID = strconv.Itoa(s.storeID) + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("node: %s, store: %s\n", nodeID, storeID)) + sb.WriteString(fmt.Sprintf(" from: %s\n", s.tStart.Format(timeFmtSlim))) + sb.WriteString(fmt.Sprintf(" to: %s\n", s.tEnd.Format(timeFmtSlim))) + var count, sum int + for _, ra := range s.readAmps { + count++ + sum += ra.readAmp + } + sb.WriteString(fmt.Sprintf(" r-amp: %.1f\n", float64(sum)/float64(count))) + + // Print flush+ingest statistics. + { + var headerWritten bool + maybeWriteHeader := func() { + if !headerWritten { + sb.WriteString("_kind______from______to_____________________________________count___bytes______time\n") + headerWritten = true + } + } + + if s.flushedCount > 0 { + maybeWriteHeader() + fmt.Fprintf(&sb, "%-7s %7s %7d %7s %9s\n", + "flush", "L0", s.flushedCount, humanize.Bytes.Uint64(s.flushedBytes), + s.flushedTime.Truncate(time.Second)) + } + + count := s.flushedCount + sum := s.flushedBytes + totalTime := s.flushedTime + for l := 0; l < len(s.ingestedBytes); l++ { + if s.ingestedCount[l] == 0 { + continue + } + maybeWriteHeader() + fmt.Fprintf(&sb, "%-7s %7s %7d %7s\n", + "ingest", fmt.Sprintf("L%d", l), s.ingestedCount[l], humanize.Bytes.Uint64(s.ingestedBytes[l])) + count += s.ingestedCount[l] + sum += s.ingestedBytes[l] + } + if headerWritten { + fmt.Fprintf(&sb, "total %7d %7s %9s\n", + count, humanize.Bytes.Uint64(sum), totalTime.Truncate(time.Second), + ) + } + } + + // Print compactions statistics. + if len(s.compactionCounts) > 0 { + sb.WriteString("_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time\n") + var totalDef, totalMove, totalElision, totalDel int + var totalBytesIn, totalBytesOut, totalBytesMoved, totalBytesDel uint64 + var totalTime time.Duration + for _, p := range pairs { + def := p.counts[compactionTypeDefault] + move := p.counts[compactionTypeMove] + elision := p.counts[compactionTypeElisionOnly] + del := p.counts[compactionTypeDeleteOnly] + total := def + move + elision + del + + str := fmt.Sprintf("%-7s %7s %7s %7d %7d %7d %7d %7d %7s %7s %7s %7s %9s\n", + "compact", p.ft.from, p.ft.to, def, move, elision, del, total, + humanize.Bytes.Uint64(p.bytesIn), humanize.Bytes.Uint64(p.bytesOut), + humanize.Bytes.Uint64(p.bytesMoved), humanize.Bytes.Uint64(p.bytesDel), + p.duration.Truncate(time.Second)) + sb.WriteString(str) + + totalDef += def + totalMove += move + totalElision += elision + totalDel += del + totalBytesIn += p.bytesIn + totalBytesOut += p.bytesOut + totalBytesMoved += p.bytesMoved + totalBytesDel += p.bytesDel + totalTime += p.duration + } + sb.WriteString(fmt.Sprintf("total %19d %7d %7d %7d %7d %7s %7s %7s %7s %9s\n", + totalDef, totalMove, totalElision, totalDel, s.eventCount, + humanize.Bytes.Uint64(totalBytesIn), humanize.Bytes.Uint64(totalBytesOut), + humanize.Bytes.Uint64(totalBytesMoved), humanize.Bytes.Uint64(totalBytesDel), + totalTime.Truncate(time.Second))) + } + + // (Optional) Long running events. + if len(s.longRunning) > 0 { + sb.WriteString("long-running events (descending runtime):\n") + sb.WriteString("_kind________from________to_______job______type_____start_______end____dur(s)_____bytes:\n") + for _, e := range s.longRunning { + c := e.compaction + kind := "compact" + if c.fromLevel == -1 { + kind = "flush" + } + sb.WriteString(fmt.Sprintf("%-7s %9s %9s %9d %9s %9s %9s %9.0f %9s\n", + kind, level(c.fromLevel), level(c.toLevel), e.jobID, c.cType, + e.timeStart.Format(timeFmtHrMinSec), e.timeEnd.Format(timeFmtHrMinSec), + e.timeEnd.Sub(e.timeStart).Seconds(), humanize.Bytes.Uint64(c.outputBytes))) + } + } + + return sb.String() +} + +// windowSummarySlice is a slice of windowSummary that sorts in order of start +// time, node, then store. +type windowsSummarySlice []windowSummary + +func (s windowsSummarySlice) Len() int { + return len(s) +} + +func (s windowsSummarySlice) Less(i, j int) bool { + if !s[i].tStart.Equal(s[j].tStart) { + return s[i].tStart.Before(s[j].tStart) + } + if s[i].nodeID != s[j].nodeID { + return s[i].nodeID < s[j].nodeID + } + return s[i].storeID < s[j].storeID +} + +func (s windowsSummarySlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// eventSlice is a slice of events that sorts in order of node, store, +// then event start time. +type eventSlice []event + +func (s eventSlice) Len() int { + return len(s) +} + +func (s eventSlice) Less(i, j int) bool { + if s[i].nodeID != s[j].nodeID { + return s[i].nodeID < s[j].nodeID + } + if s[i].storeID != s[j].storeID { + return s[i].storeID < s[j].storeID + } + return s[i].timeStart.Before(s[j].timeStart) +} + +func (s eventSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// readAmpSlice is a slice of readAmp events that sorts in order of node, store, +// then read amp event start time. +type readAmpSlice []readAmp + +func (r readAmpSlice) Len() int { + return len(r) +} + +func (r readAmpSlice) Less(i, j int) bool { + // Sort by node, store, then read-amp. + if r[i].ctx.node != r[j].ctx.node { + return r[i].ctx.node < r[j].ctx.node + } + if r[i].ctx.store != r[j].ctx.store { + return r[i].ctx.store < r[j].ctx.store + } + return r[i].ctx.timestamp.Before(r[j].ctx.timestamp) +} + +func (r readAmpSlice) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +// aggregator combines compaction and read-amp events within windows of fixed +// duration and returns one aggregated windowSummary struct per window. +type aggregator struct { + window time.Duration + events []event + readAmps []readAmp + longRunningLimit time.Duration +} + +// newAggregator returns a new aggregator. +func newAggregator( + window, longRunningLimit time.Duration, events []event, readAmps []readAmp, +) *aggregator { + return &aggregator{ + window: window, + events: events, + readAmps: readAmps, + longRunningLimit: longRunningLimit, + } +} + +// aggregate aggregates the events into windows, returning the windowSummary for +// each interval. +func (a *aggregator) aggregate() []windowSummary { + if len(a.events) == 0 { + return nil + } + + // Sort the event and read-amp slices by start time. + sort.Sort(eventSlice(a.events)) + sort.Sort(readAmpSlice(a.readAmps)) + + initWindow := func(e event) *windowSummary { + start := e.timeStart.Truncate(a.window) + return &windowSummary{ + nodeID: e.nodeID, + storeID: e.storeID, + tStart: start, + tEnd: start.Add(a.window), + compactionCounts: make(map[fromTo]compactionTypeCount), + compactionBytesIn: make(map[fromTo]uint64), + compactionBytesOut: make(map[fromTo]uint64), + compactionBytesMoved: make(map[fromTo]uint64), + compactionBytesDel: make(map[fromTo]uint64), + compactionTime: make(map[fromTo]time.Duration), + } + } + + var windows []windowSummary + var j int // index for read-amps + finishWindow := func(cur *windowSummary) { + // Collect read-amp values for the previous window. + var readAmps []readAmp + for j < len(a.readAmps) { + ra := a.readAmps[j] + + // Skip values before the current window. + if ra.ctx.node < cur.nodeID || + ra.ctx.store < cur.storeID || + ra.ctx.timestamp.Before(cur.tStart) { + j++ + continue + } + + // We've passed over the current window. Stop. + if ra.ctx.node > cur.nodeID || + ra.ctx.store > cur.storeID || + ra.ctx.timestamp.After(cur.tEnd) { + break + } + + // Collect this read-amp value. + readAmps = append(readAmps, ra) + j++ + } + cur.readAmps = readAmps + + // Sort long running compactions in descending order of duration. + slices.SortFunc(cur.longRunning, func(l, r event) int { + return cmp.Compare(l.timeEnd.Sub(l.timeStart), r.timeEnd.Sub(r.timeStart)) + }) + + // Add the completed window to the set of windows. + windows = append(windows, *cur) + } + + // Move through the compactions, collecting relevant compactions into the same + // window. Windows have the same node and store, and a compaction start time + // within a given range. + i := 0 + curWindow := initWindow(a.events[i]) + for ; ; i++ { + // No more windows. Complete the current window. + if i == len(a.events) { + finishWindow(curWindow) + break + } + e := a.events[i] + + // If we're at the start of a new interval, finalize the current window and + // start a new one. + if curWindow.nodeID != e.nodeID || + curWindow.storeID != e.storeID || + e.timeStart.After(curWindow.tEnd) { + finishWindow(curWindow) + curWindow = initWindow(e) + } + + switch { + case e.ingest != nil: + // Update ingest stats. + for _, f := range e.ingest.files { + curWindow.ingestedCount[f.level]++ + curWindow.ingestedBytes[f.level] += f.sizeBytes + } + case e.compaction != nil && e.compaction.cType == compactionTypeFlush: + // Update flush stats. + f := e.compaction + curWindow.flushedCount++ + curWindow.flushedBytes += f.outputBytes + curWindow.flushedTime += e.timeEnd.Sub(e.timeStart) + case e.compaction != nil: + // Update compaction stats. + c := e.compaction + // Update compaction counts. + ft := fromTo{level(c.fromLevel), level(c.toLevel)} + m, ok := curWindow.compactionCounts[ft] + if !ok { + m = make(compactionTypeCount) + curWindow.compactionCounts[ft] = m + } + m[c.cType]++ + curWindow.eventCount++ + + // Update compacted bytes in / out / moved / deleted. + switch c.cType { + case compactionTypeMove: + curWindow.compactionBytesMoved[ft] += c.inputBytes + case compactionTypeDeleteOnly: + curWindow.compactionBytesDel[ft] += c.inputBytes + default: + curWindow.compactionBytesIn[ft] += c.inputBytes + curWindow.compactionBytesOut[ft] += c.outputBytes + } + + // Update compaction time. + _, ok = curWindow.compactionTime[ft] + if !ok { + curWindow.compactionTime[ft] = 0 + } + curWindow.compactionTime[ft] += e.timeEnd.Sub(e.timeStart) + + } + // Add "long-running" events. Those that start in this window + // that have duration longer than the window interval. + if e.timeEnd.Sub(e.timeStart) > a.longRunningLimit { + curWindow.longRunning = append(curWindow.longRunning, e) + } + } + + // Windows are added in order of (node, store, time). Re-sort the windows by + // (time, node, store) for better presentation. + sort.Sort(windowsSummarySlice(windows)) + + return windows +} + +// parseLog parses the log file with the given path, using the given parse +// function to collect events in the given logEventCollector. parseLog +// returns a non-nil error if an I/O error was encountered while reading +// the log file. Parsing errors are accumulated in the +// logEventCollector. +func parseLog(path string, b *logEventCollector) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + s := bufio.NewScanner(f) + for s.Scan() { + line := s.Text() + // Store the log context for the current line, if we have one. + if err := parseLogContext(line, b); err != nil { + return err + } + + // First check for a flush or compaction. + matches := sentinelPattern.FindStringSubmatch(line) + if matches != nil { + // Determine which regexp to apply by testing the first letter of the prefix. + var err error + switch matches[sentinelPatternPrefixIdx][0] { + case 'c': + err = parseCompaction(line, b) + case 'f': + err = parseFlush(line, b) + case 'i': + err = parseIngest(line, b) + default: + err = errors.Newf("unexpected line: neither compaction nor flush: %s", line) + } + if err != nil { + b.addError(path, line, err) + } + continue + } + + // Else check for an LSM debug line. + if err = parseReadAmp(line, b); err != nil { + b.addError(path, line, err) + continue + } + } + return s.Err() +} + +// parseLogContext extracts contextual information from the log line (e.g. the +// timestamp, node and store). +func parseLogContext(line string, b *logEventCollector) error { + matches := logContextPattern.FindStringSubmatch(line) + if matches == nil { + return nil + } + + // Parse start time. + t, err := time.Parse(timeFmt, matches[logContextPatternTimestampIdx]) + if err != nil { + return errors.Newf("could not parse timestamp: %s", err) + } + + // Parse node and store. + nodeID, err := strconv.Atoi(matches[logContextPatternNodeIdx]) + if err != nil { + if matches[logContextPatternNodeIdx] != "?" { + return errors.Newf("could not parse node ID: %s", err) + } + nodeID = -1 + } + + storeID, err := strconv.Atoi(matches[logContextPatternStoreIdx]) + if err != nil { + if matches[logContextPatternStoreIdx] != "?" { + return errors.Newf("could not parse store ID: %s", err) + } + storeID = -1 + } + + b.saveContext(logContext{ + timestamp: t, + node: nodeID, + store: storeID, + }) + return nil +} + +// parseCompaction parses and collects Pebble compaction events. +func parseCompaction(line string, b *logEventCollector) error { + matches := compactionPattern.FindStringSubmatch(line) + if matches == nil { + return nil + } + + // "compacting": implies start line. + if matches[compactionPatternSuffixIdx] == "ing" { + start, err := parseCompactionStart(matches) + if err != nil { + return err + } + if err := b.addCompactionStart(start); err != nil { + return err + } + return nil + } + + // "compacted": implies end line. + end, err := parseCompactionEnd(matches) + if err != nil { + return err + } + + b.addCompactionEnd(end) + return nil +} + +// parseFlush parses and collects Pebble memtable flush events. +func parseFlush(line string, b *logEventCollector) error { + matches := flushPattern.FindStringSubmatch(line) + if matches == nil { + return nil + } + + if matches[flushPatternSuffixIdx] == "ing" { + start, err := parseFlushStart(matches) + if err != nil { + return err + } + return b.addCompactionStart(start) + } + + end, err := parseFlushEnd(matches) + if err != nil { + return err + } + + b.addCompactionEnd(end) + return nil +} + +func parseIngestDuringFlush(line string, b *logEventCollector) error { + matches := flushableIngestedPattern.FindStringSubmatch(line) + if matches == nil { + return nil + } + // Parse job ID. + jobID, err := strconv.Atoi(matches[flushableIngestedPatternJobIdx]) + if err != nil { + return errors.Newf("could not parse jobID: %s", err) + } + return parseRemainingIngestLogLine(jobID, line, b) +} + +// parseIngest parses and collects Pebble ingest complete events. +func parseIngest(line string, b *logEventCollector) error { + matches := ingestedPattern.FindStringSubmatch(line) + if matches == nil { + // Try and parse the other kind of ingest. + return parseIngestDuringFlush(line, b) + } + // Parse job ID. + jobID, err := strconv.Atoi(matches[ingestedPatternJobIdx]) + if err != nil { + return errors.Newf("could not parse jobID: %s", err) + } + return parseRemainingIngestLogLine(jobID, line, b) +} + +// parses the level, filenum, and bytes for the files which were ingested. +func parseRemainingIngestLogLine(jobID int, line string, b *logEventCollector) error { + fileMatches := ingestedFilePattern.FindAllStringSubmatch(line, -1) + files := make([]ingestedFile, len(fileMatches)) + for i := range fileMatches { + level, err := strconv.Atoi(fileMatches[i][ingestedFilePatternLevelIdx]) + if err != nil { + return errors.Newf("could not parse level: %s", err) + } + fileNum, err := strconv.Atoi(fileMatches[i][ingestedFilePatternFileIdx]) + if err != nil { + return errors.Newf("could not parse file number: %s", err) + } + files[i] = ingestedFile{ + level: level, + fileNum: fileNum, + sizeBytes: unHumanize(fileMatches[i][ingestedFilePatternBytesIdx]), + } + } + b.events = append(b.events, event{ + nodeID: b.ctx.node, + storeID: b.ctx.store, + jobID: jobID, + timeStart: b.ctx.timestamp, + timeEnd: b.ctx.timestamp, + ingest: &ingest{ + files: files, + }, + }) + return nil +} + +// parseReadAmp attempts to parse the current line as a read amp value +func parseReadAmp(line string, b *logEventCollector) error { + matches := readAmpPattern.FindStringSubmatch(line) + if matches == nil { + return nil + } + val, err := strconv.Atoi(matches[readAmpPatternValueIdx]) + if err != nil { + return errors.Newf("could not parse read amp: %s", err) + } + b.addReadAmp(readAmp{ + readAmp: val, + }) + return nil +} + +// runCompactionLogs is runnable function of the top-level cobra.Command that +// parses and collects Pebble compaction events and LSM information. +func runCompactionLogs(cmd *cobra.Command, args []string) error { + // The args contain a list of log files to read. + files := args + + // Scan the log files collecting start and end compaction lines. + b := newEventCollector() + for _, file := range files { + err := parseLog(file, b) + // parseLog returns an error only on I/O errors, which we + // immediately exit with. + if err != nil { + return err + } + } + + window, err := cmd.Flags().GetDuration("window") + if err != nil { + return err + } + + longRunningLimit, err := cmd.Flags().GetDuration("long-running-limit") + if err != nil { + return err + } + if longRunningLimit == 0 { + // Off by default. Set to infinite duration. + longRunningLimit = time.Duration(math.MaxInt64) + } + + // Aggregate the lines. + a := newAggregator(window, longRunningLimit, b.events, b.readAmps) + summaries := a.aggregate() + for _, s := range summaries { + fmt.Printf("%s\n", s) + } + + // After the summaries, print accumulated parsing errors to stderr. + for _, e := range b.errors { + fmt.Fprintf(os.Stderr, "-\n%s: %s\nError: %s\n", filepath.Base(e.path), e.line, e.err) + } + return nil +} + +// unHumanize performs the opposite of humanize.Bytes.Uint64 (e.g. "10B", +// "10MB") or the 23.1 humanize.IEC.Uint64 (e.g. "10 B", "10 M"), converting a +// human-readable value into a raw number of bytes. +func unHumanize(s string) uint64 { + if len(s) < 2 || !(s[0] >= '0' && s[0] <= '9') { + panic(errors.Newf("invalid bytes value %q", s)) + } + if s[len(s)-1] == 'B' { + s = s[:len(s)-1] + } + + multiplier := uint64(1) + switch s[len(s)-1] { + case 'K': + multiplier = 1 << 10 + case 'M': + multiplier = 1 << 20 + case 'G': + multiplier = 1 << 30 + case 'T': + multiplier = 1 << 40 + case 'P': + multiplier = 1 << 50 + case 'E': + multiplier = 1 << 60 + } + if multiplier != 1 { + s = s[:len(s)-1] + } + if s[len(s)-1] == ' ' { + s = s[:len(s)-1] + } + val, err := strconv.ParseFloat(s, 64) + if err != nil { + panic(fmt.Sprintf("parsing %s: %v", s, err)) + } + + return uint64(val * float64(multiplier)) +} + +// sumInputBytes takes a string as input and returns the sum of the +// human-readable sizes, as an integer number of bytes. +func sumInputBytes(s string) (total uint64, _ error) { + var ( + open bool + b bytes.Buffer + ) + for _, c := range s { + switch c { + case '(': + open = true + case ')': + total += unHumanize(b.String()) + b.Reset() + open = false + default: + if open { + b.WriteRune(c) + } + } + } + return +} diff --git a/pebble/tool/logs/compaction_test.go b/pebble/tool/logs/compaction_test.go new file mode 100644 index 0000000..36c3a80 --- /dev/null +++ b/pebble/tool/logs/compaction_test.go @@ -0,0 +1,467 @@ +// Copyright 2021 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 logs + +import ( + "bytes" + "fmt" + "math" + "os" + "path/filepath" + "regexp" + "testing" + "time" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" +) + +const ( + compactionStartLine23_1 = `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n5,pebble,s6] 1216510 [JOB 284925] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M)` + compactionStartLine = `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n5,pebble,s6] 1216510 [JOB 284925] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05;` + + compactionEndLine23_1 = `I211215 14:26:56.318543 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n5,pebble,s6] 1216554 [JOB 284925] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s` + compactionEndLine = `I211215 14:26:56.318543 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n5,pebble,s6] 1216554 [JOB 284925] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s` + + compactionMultiLevelStartLine = `I211215 14:26:56.318543 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n5,pebble,s6] 1216554 [JOB 11] compacting(default) [multilevel] L2 [250858] (2.1MB) Score=1.09 + L3 [247985 247989 247848] (17MB) Score=0.99 + L4 [250817 250834 238396] (28MB) Score=1.00; OverlappingRatio: Single 3.77, Multi 1.46;` + compactionMultiLevelEndline = `I211215 14:26:56.318543 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n5,pebble,s6] 1216554 [JOB 11] compacted(default) [multilevel] L2 [250858] (2.1MB) Score=1.09 + L3 [247985 247989 247848] (17MB) Score=0.99 + L4 [250817 250834 238396] (28MB) Score=1.00 -> L4 [250859 250860 250861 250862 250863] (46MB), in 0.2s (0.2s total), output rate 185MB/s` + + flushStartLine = `I211213 16:23:48.903751 21136 3@vendor/github.com/cockroachdb/pebble/event.go:599 â‹® [n9,pebble,s8] 24 [JOB 10] flushing 2 memtables to L0` + + flushEndLine23_1 = `I211213 16:23:49.134464 21136 3@vendor/github.com/cockroachdb/pebble/event.go:603 â‹® [n9,pebble,s8] 26 [JOB 10] flushed 2 memtables to L0 [1535806] (1.3 M), in 0.2s, output rate 5.8 M/s` + flushEndLine = `I211213 16:23:49.134464 21136 3@vendor/github.com/cockroachdb/pebble/event.go:603 â‹® [n9,pebble,s8] 26 [JOB 10] flushed 2 memtables to L0 [1535806] (1.3MB), in 0.2s, output rate 5.8MB/s` + + readAmpLine23_1 = ` total 31766 188 G - 257 G 187 G 48 K 3.6 G 744 536 G 49 K 278 G 5 2.1` + readAmpLine = `total | 32K 188GB 0B | - | 257GB | 48K 187GB | 744 3.6GB | 49K 536GB | 278GB | 5 2.1` + + compactionStartNoNodeStoreLine23_1 = `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n?,pebble,s?] 1216510 [JOB 284925] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M)` + compactionStartNoNodeStoreLine = `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n?,pebble,s?] 1216510 [JOB 284925] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4MB)` + flushStartNoNodeStoreLine = `I211213 16:23:48.903751 21136 3@vendor/github.com/cockroachdb/pebble/event.go:599 â‹® [n?,pebble,s?] 24 [JOB 10] flushing 2 memtables to L0` + + flushableIngestionLine23_1 = `I230831 04:13:28.824280 3780 3@pebble/event.go:685 â‹® [n10,s10,pebble] 365 [JOB 226] flushed 6 ingested flushables L0:024334 (1.5 K) + L0:024339 (1.0 K) + L0:024335 (1.9 K) + L0:024336 (1.1 K) + L0:024337 (1.1 K) + L0:024338 (12 K) in 0.0s (0.0s total), output rate 67 M/s` + flushableIngestionLine = `I230831 04:13:28.824280 3780 3@pebble/event.go:685 â‹® [n10,s10,pebble] 365 [JOB 226] flushed 6 ingested flushables L0:024334 (1.5KB) + L0:024339 (1.0KB) + L0:024335 (1.9KB) + L0:024336 (1.1KB) + L0:024337 (1.1KB) + L0:024338 (12KB) in 0.0s (0.0s total), output rate 67MB/s` +) + +func TestCompactionLogs_Regex(t *testing.T) { + tcs := []struct { + name string + re *regexp.Regexp + line string + matches map[int]string + }{ + { + name: "compaction start - sentinel", + re: sentinelPattern, + line: compactionStartLine23_1, + matches: map[int]string{ + sentinelPatternPrefixIdx: "compact", + sentinelPatternSuffixIdx: "ing", + }, + }, + { + name: "compaction start - sentinel", + re: sentinelPattern, + line: compactionStartLine, + matches: map[int]string{ + sentinelPatternPrefixIdx: "compact", + sentinelPatternSuffixIdx: "ing", + }, + }, + { + name: "compaction end - sentinel", + re: sentinelPattern, + line: compactionEndLine23_1, + matches: map[int]string{ + sentinelPatternPrefixIdx: "compact", + sentinelPatternSuffixIdx: "ed", + }, + }, + { + name: "compaction end - sentinel", + re: sentinelPattern, + line: compactionEndLine, + matches: map[int]string{ + sentinelPatternPrefixIdx: "compact", + sentinelPatternSuffixIdx: "ed", + }, + }, + { + name: "flush start - sentinel", + re: sentinelPattern, + line: flushStartLine, + matches: map[int]string{ + sentinelPatternPrefixIdx: "flush", + sentinelPatternSuffixIdx: "ing", + }, + }, + { + name: "flush end - sentinel", + re: sentinelPattern, + line: flushEndLine23_1, + matches: map[int]string{ + sentinelPatternPrefixIdx: "flush", + sentinelPatternSuffixIdx: "ed", + }, + }, + { + name: "flush end - sentinel", + re: sentinelPattern, + line: flushEndLine, + matches: map[int]string{ + sentinelPatternPrefixIdx: "flush", + sentinelPatternSuffixIdx: "ed", + }, + }, + { + name: "compaction start", + re: compactionPattern, + line: compactionStartLine23_1, + matches: map[int]string{ + compactionPatternJobIdx: "284925", + compactionPatternSuffixIdx: "ing", + compactionPatternTypeIdx: "default", + compactionPatternFromIdx: "2", + compactionPatternToIdx: "3", + compactionPatternLevels: "L2 [442555] (4.2 M) + L3 [445853] (8.4 M)", + }, + }, + { + name: "compaction start", + re: compactionPattern, + line: compactionStartLine, + matches: map[int]string{ + compactionPatternJobIdx: "284925", + compactionPatternSuffixIdx: "ing", + compactionPatternTypeIdx: "default", + compactionPatternFromIdx: "2", + compactionPatternToIdx: "3", + compactionPatternLevels: "L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99", + }, + }, + { + name: "compaction start multilevel", + re: compactionPattern, + line: compactionMultiLevelStartLine, + matches: map[int]string{ + compactionPatternJobIdx: "11", + compactionPatternSuffixIdx: "ing", + compactionPatternTypeIdx: "default", + compactionPatternFromIdx: "2", + compactionPatternToIdx: "4", + compactionPatternLevels: "L2 [250858] (2.1MB) Score=1.09 + L3 [247985 247989 247848] (17MB) Score=0.99 + L4 [250817 250834 238396] (28MB) Score=1.00", + }, + }, + { + name: "compaction start - no node / store", + re: compactionPattern, + line: compactionStartNoNodeStoreLine, + matches: map[int]string{ + compactionPatternJobIdx: "284925", + compactionPatternSuffixIdx: "ing", + compactionPatternTypeIdx: "default", + compactionPatternFromIdx: "2", + compactionPatternToIdx: "3", + }, + }, + { + name: "compaction end", + re: compactionPattern, + line: compactionEndLine, + matches: map[int]string{ + compactionPatternJobIdx: "284925", + compactionPatternSuffixIdx: "ed", + compactionPatternTypeIdx: "default", + compactionPatternFromIdx: "2", + compactionPatternToIdx: "3", + compactionPatternBytesIdx: "13MB", + }, + }, + { + name: "compaction end", + re: compactionPattern, + line: compactionMultiLevelEndline, + matches: map[int]string{ + compactionPatternJobIdx: "11", + compactionPatternSuffixIdx: "ed", + compactionPatternTypeIdx: "default", + compactionPatternFromIdx: "2", + compactionPatternToIdx: "4", + compactionPatternBytesIdx: "46MB", + }, + }, + { + name: "flush start", + re: flushPattern, + line: flushStartLine, + matches: map[int]string{ + flushPatternJobIdx: "10", + flushPatternSuffixIdx: "ing", + flushPatternBytesIdx: "", + }, + }, + { + name: "flush start - no node / store", + re: flushPattern, + line: flushStartNoNodeStoreLine, + matches: map[int]string{ + flushPatternJobIdx: "10", + flushPatternSuffixIdx: "ing", + flushPatternBytesIdx: "", + }, + }, + { + name: "flush end", + re: flushPattern, + line: flushEndLine, + matches: map[int]string{ + flushPatternJobIdx: "10", + flushPatternSuffixIdx: "ed", + flushPatternBytesIdx: "1.3MB", + }, + }, + { + name: "read amp suffix", + re: readAmpPattern, + line: readAmpLine, + matches: map[int]string{ + readAmpPatternValueIdx: "5", + }, + }, + { + name: "read amp suffix 23.1", + re: readAmpPattern, + line: readAmpLine23_1, + matches: map[int]string{ + readAmpPatternValueIdx: "5", + }, + }, + { + name: "ingestion during flush job 23.1", + re: flushableIngestedPattern, + line: flushableIngestionLine23_1, + matches: map[int]string{ + flushableIngestedPatternJobIdx: "226", + }, + }, + { + name: "ingestion during flush 23.1", + re: ingestedFilePattern, + line: flushableIngestionLine23_1, + matches: map[int]string{ + // Just looking at the first match for these. + ingestedFilePatternLevelIdx: "0", + ingestedFilePatternFileIdx: "024334", + ingestedFilePatternBytesIdx: "1.5 K", + }, + }, + { + name: "ingestion during flush job", + re: flushableIngestedPattern, + line: flushableIngestionLine, + matches: map[int]string{ + flushableIngestedPatternJobIdx: "226", + }, + }, + { + name: "ingestion during flush", + re: ingestedFilePattern, + line: flushableIngestionLine, + matches: map[int]string{ + // Just looking at the first match for these. + ingestedFilePatternLevelIdx: "0", + ingestedFilePatternFileIdx: "024334", + ingestedFilePatternBytesIdx: "1.5KB", + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + matches := tc.re.FindStringSubmatch(tc.line) + require.NotNil(t, matches) + for idx, want := range tc.matches { + require.Equal(t, want, matches[idx]) + } + }) + } +} + +func TestParseLogContext(t *testing.T) { + testCases := []struct { + line string + timestamp string + node int + store int + }{ + { + line: `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n5,pebble,s6] foo`, + timestamp: "211215 14:26:56.012382", + node: 5, + store: 6, + }, + { + line: `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [T?,n5,pebble,s6] foo`, + timestamp: "211215 14:26:56.012382", + node: 5, + store: 6, + }, + { + line: `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [T15,n5,pebble,s6] foo`, + timestamp: "211215 14:26:56.012382", + node: 5, + store: 6, + }, + { + line: `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [T1,n?,pebble,s6] foo`, + timestamp: "211215 14:26:56.012382", + node: -1, + store: 6, + }, + { + line: `I211215 14:26:56.012382 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n5,pebble,s?] foo`, + timestamp: "211215 14:26:56.012382", + node: 5, + store: -1, + }, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + var b logEventCollector + require.NoError(t, parseLogContext(tc.line, &b)) + expT, err := time.Parse(timeFmt, tc.timestamp) + require.NoError(t, err) + require.Equal(t, expT, b.ctx.timestamp) + require.Equal(t, tc.node, b.ctx.node) + require.Equal(t, tc.store, b.ctx.store) + }) + } +} + +func TestCompactionLogs(t *testing.T) { + dir := t.TempDir() + datadriven.Walk(t, "testdata", func(t *testing.T, path string) { + var c *logEventCollector + var logNum int + resetFn := func() { + c = newEventCollector() + logNum = 0 + } + resetFn() + datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "log": + basename := fmt.Sprintf("%d.log", logNum) + logNum++ + f, err := os.Create(filepath.Join(dir, basename)) + if err != nil { + panic(err) + } + if _, err = f.WriteString(td.Input); err != nil { + panic(err) + } + _ = f.Close() + + if err = parseLog(f.Name(), c); err != nil { + return err.Error() + } + return basename + + case "summarize": + window := 1 * time.Minute + longRunning := time.Duration(math.MaxInt64) + + var err error + for _, cmdArg := range td.CmdArgs { + switch cmdArg.Key { + case "window": + window, err = time.ParseDuration(cmdArg.Vals[0]) + if err != nil { + panic(errors.Newf("could not parse window: %s", err)) + } + + case "long-running": + longRunning, err = time.ParseDuration(cmdArg.Vals[0]) + if err != nil { + panic(errors.Newf("could not parse long-running: %s", err)) + } + + default: + panic(errors.Newf("unknown arg %q", cmdArg.Key)) + } + } + + a := newAggregator(window, longRunning, c.events, c.readAmps) + windows := a.aggregate() + + var b bytes.Buffer + for _, w := range windows { + b.WriteString(w.String()) + } + + return b.String() + + case "reset": + resetFn() + return "" + + default: + return fmt.Sprintf("unknown command %q", td.Cmd) + } + }) + }) +} + +func TestParseInputBytes(t *testing.T) { + testCases := []struct { + s string + want uint64 + }{ + // Test both 23.1 and current formats. + { + "(10 B)", + 10, + }, + { + "(10B)", + 10, + }, + { + "(10 M)", + 10 << 20, + }, + { + "(10MB)", + 10 << 20, + }, + { + "(10 M) + (20 M)", + 30 << 20, + }, + { + "(10MB) + (20MB)", + 30 << 20, + }, + { + "(10 M) + (20 M) + (30 M)", + 60 << 20, + }, + { + "(10MB) + (20MB) + (30MB)", + 60 << 20, + }, + { + "foo", + 0, + }, + } + for _, tc := range testCases { + t.Run(tc.s, func(t *testing.T) { + got, err := sumInputBytes(tc.s) + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/pebble/tool/logs/testdata/compactions-23-1 b/pebble/tool/logs/testdata/compactions-23-1 new file mode 100644 index 0000000..718b286 --- /dev/null +++ b/pebble/tool/logs/testdata/compactions-23-1 @@ -0,0 +1,499 @@ +# Single compaction and flush pair for a single node / store combination. +# +# Use a combination of [n1,pebble,s1] and [n1,s1,pebble] to mimic the two +# formats we see in production. + + +log +I211215 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I211215 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,s1,pebble] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s + +I211215 00:01:10.000000 21136 3@vendor/github.com/cockroachdb/pebble/event.go:599 â‹® [n1,s1,pebble] 24 [JOB 2] flushing 2 memtables to L0 +I211215 00:01:20.000000 21136 3@vendor/github.com/cockroachdb/pebble/event.go:603 â‹® [n1,pebble,s1] 26 [JOB 2] flushed 2 memtables to L0 [1535806] (1.3 M), in 0.2s, output rate 5.8 M/s +---- +0.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:00 + to: 211215 00:01 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +flush L0 1 1.3MB 10s +total 1 1.3MB 10s + +# Same as the previous case, except that the start and end events are are split +# across multiple files (one log line per file). + +reset +---- + +log +I211215 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,bars,s1,foos] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +---- +0.log + +log +I211215 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,s1,foos] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +1.log + +log +I211215 00:01:10.000000 21136 3@vendor/github.com/cockroachdb/pebble/event.go:599 â‹® [n1,s1] 24 [JOB 2] flushing 2 memtables to L0 +---- +2.log + +log +I211215 00:01:20.000000 21136 3@vendor/github.com/cockroachdb/pebble/event.go:603 â‹® [n1,pebble,s1] 26 [JOB 2] flushed 2 memtables to L0 [1535806] (1.3 M), in 0.2s, output rate 5.8 M/s +---- +3.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:00 + to: 211215 00:01 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +flush L0 1 1.3MB 10s +total 1 1.3MB 10s + +# Read amplification from the Cockroach log, one within an existing window, +# another outside of the existing window. The latter is not included. + +reset +---- + +log +I211215 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I211215 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +0.log + +log +I211215 00:00:15.000000 434 kv/kvserver/store.go:3251 â‹® [n1,s1] 31356 +__level_____count____size___score______in__ingest(sz_cnt)____move(sz_cnt)___write(sz_cnt)____read___r-amp___w-amp + WAL 1 54 M - 65 G - - - - 70 G - - - 1.1 + 0 0 0 B 0.00 70 G 77 M 133 0 B 0 24 G 19 K 4.2 G 0 0.3 + 1 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 + 2 14 34 M 0.96 18 G 0 B 0 17 M 10 49 G 14 K 55 G 1 2.7 + 3 42 207 M 0.96 12 G 0 B 0 939 M 280 43 G 7.3 K 46 G 1 3.4 + 4 264 1.5 G 0.99 9.1 G 18 M 6 824 M 152 31 G 4.5 K 35 G 1 3.4 + 5 7474 23 G 1.00 2.8 G 116 G 26 K 1.8 G 301 3.2 G 604 3.2 G 1 1.2 + 6 23972 164 G - 98 G 70 G 22 K 1.6 K 1 129 G 3.8 K 135 G 1 1.3 + total 31766 188 G - 257 G 187 G 48 K 3.6 G 744 536 G 49 K 278 G 5 2.1 +I211215 00:01:15.000000 434 kv/kvserver/store.go:3251 â‹® [n1,s1] 31356 +__level_____count____size___score______in__ingest(sz_cnt)____move(sz_cnt)___write(sz_cnt)____read___r-amp___w-amp + WAL 1 35 M - 65 G - - - - 70 G - - - 1.1 + 0 0 0 B 0.00 70 G 77 M 133 0 B 0 24 G 19 K 4.2 G 0 0.3 + 1 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 + 2 14 34 M 0.95 18 G 0 B 0 17 M 10 49 G 14 K 55 G 1 2.7 + 3 42 207 M 0.96 12 G 0 B 0 939 M 280 43 G 7.3 K 46 G 1 3.4 + 4 264 1.5 G 0.99 9.1 G 18 M 6 824 M 152 31 G 4.5 K 35 G 1 3.4 + 5 7474 23 G 1.00 2.8 G 116 G 26 K 1.8 G 301 3.2 G 604 3.2 G 1 1.2 + 6 23972 164 G - 98 G 70 G 22 K 1.6 K 1 129 G 3.8 K 135 G 1 1.3 + total 31766 188 G - 257 G 187 G 48 K 3.6 G 744 536 G 49 K 278 G 5 2.1 +---- +1.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:00 + to: 211215 00:01 + r-amp: 5.0 +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Long running compaction. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,s1,pebble] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I211215 00:03:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +0.log + +summarize long-running=1m +---- +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 2m10s +total 1 0 0 0 1 13MB 13MB 0B 0B 2m10s +long-running events (descending runtime): +_kind________from________to_______job______type_____start_______end____dur(s)_____bytes: +compact L2 L3 1 default 00:01:10 00:03:20 130 13MB + +# Single node, multiple stores. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s + +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s2] 1216510 [JOB 2] compacting(default) L3 [442555] (4.2 M) + L4 [445853] (8.4 M) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s2] 1216554 [JOB 2] compacted(default) L3 [442555] (4.2 M) + L4 [445853] (8.4 M) -> L4 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +0.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 2 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L3 L4 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Multiple nodes, single stores. Two separate pebble logs. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +0.log + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n2,pebble,s1] 1216510 [JOB 1] compacting(default) L3 [442555] (4.2 M) + L4 [445853] (8.4 M) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n2,pebble,s1] 1216554 [JOB 1] compacted(default) L3 [442555] (4.2 M) + L4 [445853] (8.4 M) -> L4 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +1.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 2, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L3 L4 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Multiple nodes, multiple stores. Two separate pebble logs. Output is sorted by +# (time, node, store). + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s + +I211215 00:02:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s2] 1216510 [JOB 2] compacting(default) L1 [442555] (4.2 M) + L2 [445853] (8.4 M) +I211215 00:02:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s2] 1216554 [JOB 2] compacted(default) L1 [442555] (4.2 M) + L2 [445853] (8.4 M) -> L2 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +0.log + +log +I211215 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n2,pebble,s1] 1216510 [JOB 1] compacting(default) L3 [442555] (4.2 M) + L4 [445853] (8.4 M) +I211215 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n2,pebble,s1] 1216554 [JOB 1] compacted(default) L3 [442555] (4.2 M) + L4 [445853] (8.4 M) -> L4 [445883 445887] (13 M), in 0.3s, output rate 42 M/s + +I211215 00:02:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n2,pebble,s2] 1216510 [JOB 2] compacting(default) L4 [442555] (4.2 M) + L5 [445853] (8.4 M) +I211215 00:02:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n2,pebble,s2] 1216554 [JOB 2] compacted(default) L4 [442555] (4.2 M) + L5 [445853] (8.4 M) -> L5 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +1.log + +summarize +---- +node: 2, store: 1 + from: 211215 00:00 + to: 211215 00:01 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L3 L4 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 2 + from: 211215 00:02 + to: 211215 00:03 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L1 L2 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 2, store: 2 + from: 211215 00:02 + to: 211215 00:03 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L4 L5 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Log lines with an absent node / store are aggregated. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n?,pebble,s?] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n?,pebble,s?] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +0.log + +log +I211215 00:01:15.000000 434 kv/kvserver/store.go:3251 â‹® [n?,s?] 31356 +__level_____count____size___score______in__ingest(sz_cnt)____move(sz_cnt)___write(sz_cnt)____read___r-amp___w-amp + WAL 1 54 M - 65 G - - - - 70 G - - - 1.1 + 0 0 0 B 0.00 70 G 77 M 133 0 B 0 24 G 19 K 4.2 G 0 0.3 + 1 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 + 2 14 34 M 0.96 18 G 0 B 0 17 M 10 49 G 14 K 55 G 1 2.7 + 3 42 207 M 0.96 12 G 0 B 0 939 M 280 43 G 7.3 K 46 G 1 3.4 + 4 264 1.5 G 0.99 9.1 G 18 M 6 824 M 152 31 G 4.5 K 35 G 1 3.4 + 5 7474 23 G 1.00 2.8 G 116 G 26 K 1.8 G 301 3.2 G 604 3.2 G 1 1.2 + 6 23972 164 G - 98 G 70 G 22 K 1.6 K 1 129 G 3.8 K 135 G 1 1.3 + total 31766 188 G - 257 G 187 G 48 K 3.6 G 744 536 G 49 K 278 G 5 2.1 +---- +1.log + +summarize +---- +node: ?, store: ? + from: 211215 00:01 + to: 211215 00:02 + r-amp: 5.0 +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# The same Job ID interleaved for multiple nodes / stores. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I211215 00:02:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n2,pebble,s2] 1216510 [JOB 1] compacting(default) L4 [442555] (4.2 M) + L5 [445853] (8.4 M) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +I211215 00:02:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n2,pebble,s2] 1216554 [JOB 1] compacted(default) L4 [442555] (4.2 M) + L5 [445853] (8.4 M) -> L5 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +0.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 2, store: 2 + from: 211215 00:02 + to: 211215 00:03 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L4 L5 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Read amp matching should remain backwards compatible. + +reset +---- + +log +I220301 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) +I220301 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] compacted(default) L2 [442555] (4.2 M) + L3 [445853] (8.4 M) -> L3 [445883 445887] (13 M), in 0.3s, output rate 42 M/s +---- +0.log + +log +I220301 00:00:30.000000 200 1@gossip/gossip.go:1500 â‹® [n1] 74 node has connected to cluster via gossip +I220301 00:00:30.000000 200 kv/kvserver/stores.go:269 â‹® [n1] 75 wrote 0 node addresses to persistent storage +I220301 00:00:30.000000 319 2@server/status/runtime.go:569 â‹® [n1] 76 runtime stats: 154 MiB RSS, 273 goroutines (stacks: 2.5 MiB), 42 MiB/71 MiB Go alloc/total (heap fragmentation: 11 MiB, heap reserved: 3.9 MiB, heap released: 4.2 MiB), 3.2 MiB/5.6 MiB CGO alloc/total (0.0 CGO/sec), 0.0/0.0 %(u/s)time, 0.0 %gc (0x), 425 KiB/500 KiB (r/w)net +I220301 00:00:30.000000 319 2@server/status/runtime.go:569 â‹® [n1] 77 runtime stats: 159 MiB RSS, 266 goroutines (stacks: 3.3 MiB), 42 MiB/78 MiB Go alloc/total (heap fragmentation: 12 MiB, heap reserved: 6.7 MiB, heap released: 64 MiB), 4.4 MiB/6.8 MiB CGO alloc/total (0.4 CGO/sec), 2.9/2.1 %(u/s)time, 0.0 %gc (0x), 335 KiB/323 KiB (r/w)net +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +__level_____count____size___score______in__ingest(sz_cnt)____move(sz_cnt)___write(sz_cnt)____read___r-amp___w-amp +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + WAL 3 779 K - 773 K - - - - 779 K - - - 1.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 0 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 1 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 2 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 3 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 4 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 5 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 6 0 0 B - 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + total 0 0 B - 779 K 0 B 0 0 B 0 779 K 0 0 B 1 1.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + flush 0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +compact 0 0 B 0 B (size == estimated-debt, in = in-progress-bytes) +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + memtbl 3 1.8 M +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +zmemtbl 0 0 B +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + ztbl 0 0 B +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + bcache 0 0 B 0.0% (score == hit-rate) +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + tcache 0 0 B 0.0% (score == hit-rate) +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + titers 0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + filter - - 0.0% (score == utility) +---- +1.log + +summarize +---- +node: 1, store: 1 + from: 220301 00:00 + to: 220301 00:01 + r-amp: 1.0 +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +reset +---- + +log +I220228 14:44:31.497272 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1366 â‹® [n24,pebble,s24] 33267888 [JOB 10197855] flushing 1 memtable to L0 +I220228 14:44:31.497485 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267889 [JOB 10197855] flushing: sstable created 21731018 +I220228 14:44:31.527038 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267892 [JOB 10197855] flushing: sstable created 21731020 +I220228 14:44:31.542944 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267894 [JOB 10197855] flushing: sstable created 21731021 +I220228 14:44:31.553581 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267895 [JOB 10197855] flushing: sstable created 21731022 +I220228 14:44:31.554585 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267896 [JOB 10197855] flushing: sstable created 21731023 +I220228 14:44:31.569928 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267897 [JOB 10197855] flushing: sstable created 21731024 +I220228 14:44:31.624309 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267899 [JOB 10197855] flushing: sstable created 21731025 +I220228 14:44:31.685531 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267901 [JOB 10197855] flushing: sstable created 21731026 +I220228 14:44:31.686009 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267902 [JOB 10197855] flushing: sstable created 21731027 +I220228 14:44:31.686415 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267903 [JOB 10197855] flushing: sstable created 21731028 +I220228 14:44:31.780892 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267906 [JOB 10197855] flushing: sstable created 21731030 +I220228 14:44:31.790911 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267907 [JOB 10197855] flushing: sstable created 21731031 +I220228 14:44:31.904614 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267911 [JOB 10197855] flushing: sstable created 21731033 +I220228 14:44:31.905835 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267912 [JOB 10197855] flushing: sstable created 21731034 +I220228 14:44:31.906860 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267913 [JOB 10197855] flushing: sstable created 21731035 +I220228 14:44:31.907602 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267914 [JOB 10197855] flushing: sstable created 21731036 +I220228 14:44:32.019173 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267920 [JOB 10197855] flushing: sstable created 21731037 +I220228 14:44:32.019714 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267921 [JOB 10197855] flushing: sstable created 21731038 +I220228 14:44:32.020161 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267922 [JOB 10197855] flushing: sstable created 21731039 +I220228 14:44:32.100117 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267925 [JOB 10197855] flushing: sstable created 21731040 +I220228 14:44:32.100609 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267926 [JOB 10197855] flushing: sstable created 21731041 +I220228 14:44:32.101065 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267927 [JOB 10197855] flushing: sstable created 21731042 +I220228 14:44:32.101494 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267928 [JOB 10197855] flushing: sstable created 21731043 +I220228 14:44:32.102569 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267929 [JOB 10197855] flushing: sstable created 21731044 +I220228 14:44:32.106284 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267930 [JOB 10197855] flushing: sstable created 21731045 +I220228 14:44:32.138686 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1423 â‹® [n24,pebble,s24] 33267931 [JOB 10197855] flushed 1 memtable to L0 [21731018 21731020 21731021 21731022 21731023 21731024 21731025 21731026 21731027 21731028 21731030 21731031 21731033 21731034 21731035 21731036 21731037 21731038 21731039 21731040 21731041 21731042 21731043 21731044 21731045] (19 M), in 0.6s, output rate 31 M/s +---- +0.log + +summarize +---- +node: 24, store: 24 + from: 220228 14:44 + to: 220228 14:45 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +flush L0 1 19MB 0s +total 1 19MB 0s + +reset +---- + +log +I220228 16:01:22.487906 18476248525 3@vendor/github.com/cockroachdb/pebble/ingest.go:637 â‹® [n24,pebble,s24] 33430782 [JOB 10211226] ingested L0:21818678 (1.8 K), L0:21818683 (1.2 K), L0:21818679 (1.6 K), L0:21818680 (1.1 K), L0:21818681 (1.1 K), L0:21818682 (160 M) +45127:I220228 15:58:45.538681 18475981755 3@vendor/github.com/cockroachdb/pebble/ingest.go:637 â‹® [n24,pebble,s24] 33424719 [JOB 10210743] ingested L0:21814543 (1.4 K), L0:21814548 (1.2 K), L5:21814544 (1.4 K), L5:21814545 (1.1 K), L5:21814546 (1.1 K), L0:21814547 (140 M) +---- +0.log + +summarize +---- +node: 24, store: 24 + from: 220228 15:58 + to: 220228 15:59 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +ingest L0 3 140MB +ingest L5 3 3.6KB +total 6 140MB 0s +node: 24, store: 24 + from: 220228 16:01 + to: 220228 16:02 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +ingest L0 6 160MB +total 6 160MB 0s + +reset +---- + +log +I220907 00:27:21.579807 15082709999 3@vendor/github.com/cockroachdb/pebble/event.go:587 â‹® [n15,pebble,s15] 2736197 [JOB 743692] compacting(delete-only) L6 [18323385] (11 M) +I220907 00:27:21.580169 15082709999 3@vendor/github.com/cockroachdb/pebble/event.go:591 â‹® [n15,pebble,s15] 2736198 [JOB 743692] compacted(delete-only) L6 [18323385] (11 M) -> L6 [] (0 B), in 0.0s, output rate 0 B/s + +I220907 00:27:21.631145 15082710355 3@vendor/github.com/cockroachdb/pebble/event.go:587 â‹® [n15,pebble,s15] 2736201 [JOB 743694] compacting(default) L5 [18323582] (1.8 K) + L6 [17770912] (128 M) +I220907 00:27:22.729839 15082710355 3@vendor/github.com/cockroachdb/pebble/event.go:591 â‹® [n15,pebble,s15] 2736208 [JOB 743694] compacted(default) L5 [18323582] (1.8 K) + L6 [17770912] (128 M) -> L6 [18323586] (3.6 M), in 1.1s, output rate 3.3 M/s + +I220907 00:27:21.630546 15082710354 3@vendor/github.com/cockroachdb/pebble/event.go:587 â‹® [n15,pebble,s15] 2736199 [JOB 743693] compacting(move) L5 [18323585] (4.0 M) + L6 [] (0 B) +I220907 00:27:21.631002 15082710354 3@vendor/github.com/cockroachdb/pebble/event.go:591 â‹® [n15,pebble,s15] 2736200 [JOB 743693] compacted(move) L5 [18323585] (4.0 M) + L6 [] (0 B) -> L6 [18323585] (4.0 M), in 0.0s, output rate 50 G/s +---- +0.log + +summarize +---- +node: 15, store: 15 + from: 220907 00:27 + to: 220907 00:28 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L5 L6 1 1 0 0 2 128MB 3.6MB 4.0MB 0B 1s +compact L6 L6 0 0 0 1 1 0B 0B 0B 11MB 0s +total 1 1 0 1 3 128MB 3.6MB 4.0MB 11MB 1s + +reset +---- + +log +I230831 04:13:28.824280 3780 3@pebble/event.go:685 â‹® [n10,s10,pebble] 365 [JOB 226] flushed 6 ingested flushables L0:024334 (1.5 K) + L0:024339 (1.0 K) + L0:024335 (1.9 K) + L0:024336 (1.1 K) + L0:024337 (1.1 K) + L0:024338 (12 K) in 0.0s (0.0s total), output rate 67 M/s + +I230831 04:13:28.689575 3717 3@pebble/event.go:685 â‹® [n10,s10,pebble] 345 [JOB 219] flushed 6 ingested flushables L0:024323 (1.5 K) + L0:024328 (1.0 K) + L0:024324 (2.0 K) + L2:024325 (1.1 K) + L2:024326 (1.1 K) + L0:024327 (54 K) in 0.0s (0.0s total), output rate 152 M/s +---- +0.log + +summarize +---- +node: 10, store: 10 + from: 230831 04:13 + to: 230831 04:14 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +ingest L0 10 77KB +ingest L2 2 2.2KB +total 12 79KB 0s diff --git a/pebble/tool/logs/testdata/compactions-latest b/pebble/tool/logs/testdata/compactions-latest new file mode 100644 index 0000000..532b331 --- /dev/null +++ b/pebble/tool/logs/testdata/compactions-latest @@ -0,0 +1,499 @@ +# Single compaction and flush pair for a single node / store combination. +# +# Use a combination of [n1,pebble,s1] and [n1,s1,pebble] to mimic the two +# formats we see in production. + + +log +I211215 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,s1,pebble] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s + +I211215 00:01:10.000000 21136 3@vendor/github.com/cockroachdb/pebble/event.go:599 â‹® [n1,s1,pebble] 24 [JOB 2] flushing 2 memtables (1.5MB) to L0 +I211215 00:01:20.000000 21136 3@vendor/github.com/cockroachdb/pebble/event.go:603 â‹® [n1,pebble,s1] 26 [JOB 2] flushed 2 memtables (1.5MB) to L0 [1535806] (1.3MB), in 0.2s, output rate 5.8MB/s +---- +0.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:00 + to: 211215 00:01 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +flush L0 1 1.3MB 10s +total 1 1.3MB 10s + +# Same as the previous case, except that the start and end events are are split +# across multiple files (one log line per file). + +reset +---- + +log +I211215 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,bars,s1,foos] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +---- +0.log + +log +I211215 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,s1,foos] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +1.log + +log +I211215 00:01:10.000000 21136 3@vendor/github.com/cockroachdb/pebble/event.go:599 â‹® [n1,s1] 24 [JOB 2] flushing 2 memtables (1.5MB) to L0 +---- +2.log + +log +I211215 00:01:20.000000 21136 3@vendor/github.com/cockroachdb/pebble/event.go:603 â‹® [n1,pebble,s1] 26 [JOB 2] flushed 2 memtables (1.5MB) to L0 [1535806] (1.3MB), in 0.2s, output rate 5.8MB/s +---- +3.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:00 + to: 211215 00:01 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +flush L0 1 1.3MB 10s +total 1 1.3MB 10s + +# Read amplification from the Cockroach log, one within an existing window, +# another outside of the existing window. The latter is not included. + +reset +---- + +log +I211215 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +0.log + +log +I211215 00:00:15.000000 434 kv/kvserver/store.go:3251 â‹® [n1,s1] 31356 + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 70GB | 133 77MB | 0 0B | 19K 24GB | 4.2GB | 0 0.3 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 14 34MB 0B 0 | 0.96 | 18GB | 0 0B | 10 17MB | 14K 49GB | 55GB | 1 2.7 + 3 | 42 207MB 0B 0 | 0.96 | 12GB | 0 0B | 280 939MB | 7.3K 43GB | 46GB | 1 3.4 + 4 | 264 1.5GB 0B 0 | 0.99 | 9.1GB | 6 18MB | 152 824MB | 4.5K 31GB | 35GB | 1 3.4 + 5 | 7.5K 23GB 0B 0 | 1.00 | 2.8GB | 26K 116GB | 301 1.8GB | 604 3.2GB | 3.2GB | 1 1.2 + 6 | 24K 164GB 0B 0 | - | 98GB | 22K 70GB | 1 1.6KB | 3.8K 128GB | 135GB | 1 1.3 +total | 32K 188GB 0B 0 | - | 257GB | 48K 187GB | 744 3.6GB | 49K 536GB | 278GB | 5 2.1 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (54MB) in: 65GB written: 70B (8% overhead) +---- +1.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:00 + to: 211215 00:01 + r-amp: 5.0 +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Long running compaction. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,s1,pebble] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:03:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +0.log + +summarize long-running=1m +---- +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 2m10s +total 1 0 0 0 1 13MB 13MB 0B 0B 2m10s +long-running events (descending runtime): +_kind________from________to_______job______type_____start_______end____dur(s)_____bytes: +compact L2 L3 1 default 00:01:10 00:03:20 130 13MB + +# Single node, multiple stores. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s + +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s2] 1216510 [JOB 2] compacting(default) L3 [442555] (4.2MB) + L4 [445853] (8.4MB) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s2] 1216554 [JOB 2] compacted(default) L3 [442555] (4.2MB) + L4 [445853] (8.4MB) -> L4 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +0.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 2 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L3 L4 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Multiple nodes, single stores. Two separate pebble logs. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +0.log + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n2,pebble,s1] 1216510 [JOB 1] compacting(default) L3 [442555] (4.2MB) + L4 [445853] (8.4MB) +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n2,pebble,s1] 1216554 [JOB 1] compacted(default) L3 [442555] (4.2MB) + L4 [445853] (8.4MB) -> L4 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +1.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 2, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L3 L4 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Multiple nodes, multiple stores. Two separate pebble logs. Output is sorted by +# (time, node, store). + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s + +I211215 00:02:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s2] 1216510 [JOB 2] compacting(default) L1 [442555] (4.2MB) + L2 [445853] (8.4MB) +I211215 00:02:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s2] 1216554 [JOB 2] compacted(default) L1 [442555] (4.2MB) + L2 [445853] (8.4MB) -> L2 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +0.log + +log +I211215 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n2,pebble,s1] 1216510 [JOB 1] compacting(default) L3 [442555] (4.2MB) + L4 [445853] (8.4MB) +I211215 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n2,pebble,s1] 1216554 [JOB 1] compacted(default) L3 [442555] (4.2MB) + L4 [445853] (8.4MB) -> L4 [445883 445887] (13MB), in 0.3s, output rate 42MB/s + +I211215 00:02:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n2,pebble,s2] 1216510 [JOB 2] compacting(default) L4 [442555] (4.2MB) + L5 [445853] (8.4MB) +I211215 00:02:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n2,pebble,s2] 1216554 [JOB 2] compacted(default) L4 [442555] (4.2MB) + L5 [445853] (8.4MB) -> L5 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +1.log + +summarize +---- +node: 2, store: 1 + from: 211215 00:00 + to: 211215 00:01 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L3 L4 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 1, store: 2 + from: 211215 00:02 + to: 211215 00:03 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L1 L2 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 2, store: 2 + from: 211215 00:02 + to: 211215 00:03 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L4 L5 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Log lines with an absent node / store are aggregated. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n?,pebble,s?] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n?,pebble,s?] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +0.log + +log +I211215 00:01:15.000000 434 kv/kvserver/store.go:3251 â‹® [n?,s?] 31356 + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 0 0B 0B 0 | 0.00 | 70GB | 133 77MB | 0 0B | 19K 24GB | 4.2GB | 0 0.3 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 14 34MB 0B 0 | 0.96 | 18GB | 0 0B | 10 17MB | 14K 49GB | 55GB | 1 2.7 + 3 | 42 207MB 0B 0 | 0.96 | 12GB | 0 0B | 280 939MB | 7.3K 43GB | 46GB | 1 3.4 + 4 | 264 1.5GB 0B 0 | 0.99 | 9.1GB | 6 18MB | 152 824MB | 4.5K 31GB | 35GB | 1 3.4 + 5 | 7.5K 23GB 0B 0 | 1.00 | 2.8GB | 26K 116GB | 301 1.8GB | 604 3.2GB | 3.2GB | 1 1.2 + 6 | 24K 164GB 0B 0 | - | 98GB | 22K 70GB | 1 1.6KB | 3.8K 128GB | 135GB | 1 1.3 +total | 32K 188GB 0B 0 | - | 257GB | 48K 187GB | 744 3.6GB | 49K 536GB | 278GB | 5 2.1 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (54MB) in: 65GB written: 70B (8% overhead) +---- +1.log + +summarize +---- +node: ?, store: ? + from: 211215 00:01 + to: 211215 00:02 + r-amp: 5.0 +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# The same Job ID interleaved for multiple nodes / stores. + +reset +---- + +log +I211215 00:01:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:02:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n2,pebble,s2] 1216510 [JOB 1] compacting(default) L4 [442555] (4.2MB) Score=1.01 + L5 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I211215 00:01:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +I211215 00:02:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n2,pebble,s2] 1216554 [JOB 1] compacted(default) L4 [442555] (4.2MB) + L5 [445853] (8.4MB) -> L5 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +0.log + +summarize +---- +node: 1, store: 1 + from: 211215 00:01 + to: 211215 00:02 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s +node: 2, store: 2 + from: 211215 00:02 + to: 211215 00:03 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L4 L5 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +# Read amp matching should remain backwards compatible. + +reset +---- + +log +I220301 00:00:10.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1845 â‹® [n1,pebble,s1] 1216510 [JOB 1] compacting(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=0.99; OverlappingRatio: Single 8.03, Multi 25.05 +I220301 00:00:20.000000 51831533 3@vendor/github.com/cockroachdb/pebble/compaction.go:1886 â‹® [n1,pebble,s1] 1216554 [JOB 1] [JOB 1] compacted(default) L2 [442555] (4.2MB) Score=1.01 + L3 [445853] (8.4MB) Score=1.01 -> L3 [445883 445887] (13MB), in 0.3s, output rate 42MB/s +---- +0.log + +log +I220301 00:00:30.000000 200 1@gossip/gossip.go:1500 â‹® [n1] 74 node has connected to cluster via gossip +I220301 00:00:30.000000 200 kv/kvserver/stores.go:269 â‹® [n1] 75 wrote 0 node addresses to persistent storage +I220301 00:00:30.000000 319 2@server/status/runtime.go:569 â‹® [n1] 76 runtime stats: 154 MiB RSS, 273 goroutines (stacks: 2.5 MiB), 42 MiB/71 MiB Go alloc/total (heap fragmentation: 11 MiB, heap reserved: 3.9 MiB, heap released: 4.2 MiB), 3.2 MiB/5.6 MiB CGO alloc/total (0.0 CGO/sec), 0.0/0.0 %(u/s)time, 0.0 %gc (0x), 425 KiB/500 KiB (r/w)net +I220301 00:00:30.000000 319 2@server/status/runtime.go:569 â‹® [n1] 77 runtime stats: 159 MiB RSS, 266 goroutines (stacks: 3.3 MiB), 42 MiB/78 MiB Go alloc/total (heap fragmentation: 12 MiB, heap reserved: 6.7 MiB, heap released: 64 MiB), 4.4 MiB/6.8 MiB CGO alloc/total (0.4 CGO/sec), 2.9/2.1 %(u/s)time, 0.0 %gc (0x), 335 KiB/323 KiB (r/w)net +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + | | | | ingested | moved | written | | amp +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 0 | 0 0B 0B 0 | 0.00 | 56B | 0 0B | 0 0B | 2 1.2KB | 0B | 0 22.6 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + 6 | 1 639B 0B 0 | - | 1.2KB | 0 0B | 0 0B | 1 639B | 1.2KB | 1 0.5 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +total | 1 639B 0B 0 | - | 84B | 0 0B | 0 0B | 3 1.9KB | 1.2KB | 1 23.7 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +------------------------------------------------------------------------------------------------------------------- +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +WAL: 1 files (28B) in: 34B written: 84B (147% overhead) +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Flushes: 2 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Compactions: 1 estimated debt: 0B in progress: 0 (0B) +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 + default: 1 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +MemTables: 1 (256KB) zombie: 2 (512KB) +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Zombie tables: 2 (1.2KB) +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Block cache: 5 entries (1.0KB) hit rate: 42.9% +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Table cache: 2 entries (1.6KB) hit rate: 66.7% +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Snapshots: 0 earliest seq num: 0 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Table iters: 2 +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Filter utility: 0.0% +I220301 00:00:30.000000 315 kv/kvserver/store.go:2713 â‹® [n1,s1] 78 +Ingestions: 0 as flushable: 0 (0B in 0 tables) +---- +1.log + +summarize +---- +node: 1, store: 1 + from: 220301 00:00 + to: 220301 00:01 + r-amp: 1.0 +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L2 L3 1 0 0 0 1 13MB 13MB 0B 0B 10s +total 1 0 0 0 1 13MB 13MB 0B 0B 10s + +reset +---- + +log +I220228 14:44:31.497272 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1366 â‹® [n24,pebble,s24] 33267888 [JOB 10197855] flushing 1 memtable (64MB) to L0 +I220228 14:44:31.497485 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267889 [JOB 10197855] flushing: sstable created 21731018 +I220228 14:44:31.527038 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267892 [JOB 10197855] flushing: sstable created 21731020 +I220228 14:44:31.542944 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267894 [JOB 10197855] flushing: sstable created 21731021 +I220228 14:44:31.553581 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267895 [JOB 10197855] flushing: sstable created 21731022 +I220228 14:44:31.554585 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267896 [JOB 10197855] flushing: sstable created 21731023 +I220228 14:44:31.569928 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267897 [JOB 10197855] flushing: sstable created 21731024 +I220228 14:44:31.624309 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267899 [JOB 10197855] flushing: sstable created 21731025 +I220228 14:44:31.685531 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267901 [JOB 10197855] flushing: sstable created 21731026 +I220228 14:44:31.686009 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267902 [JOB 10197855] flushing: sstable created 21731027 +I220228 14:44:31.686415 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267903 [JOB 10197855] flushing: sstable created 21731028 +I220228 14:44:31.780892 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267906 [JOB 10197855] flushing: sstable created 21731030 +I220228 14:44:31.790911 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267907 [JOB 10197855] flushing: sstable created 21731031 +I220228 14:44:31.904614 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267911 [JOB 10197855] flushing: sstable created 21731033 +I220228 14:44:31.905835 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267912 [JOB 10197855] flushing: sstable created 21731034 +I220228 14:44:31.906860 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267913 [JOB 10197855] flushing: sstable created 21731035 +I220228 14:44:31.907602 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267914 [JOB 10197855] flushing: sstable created 21731036 +I220228 14:44:32.019173 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267920 [JOB 10197855] flushing: sstable created 21731037 +I220228 14:44:32.019714 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267921 [JOB 10197855] flushing: sstable created 21731038 +I220228 14:44:32.020161 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267922 [JOB 10197855] flushing: sstable created 21731039 +I220228 14:44:32.100117 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267925 [JOB 10197855] flushing: sstable created 21731040 +I220228 14:44:32.100609 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267926 [JOB 10197855] flushing: sstable created 21731041 +I220228 14:44:32.101065 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267927 [JOB 10197855] flushing: sstable created 21731042 +I220228 14:44:32.101494 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267928 [JOB 10197855] flushing: sstable created 21731043 +I220228 14:44:32.102569 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267929 [JOB 10197855] flushing: sstable created 21731044 +I220228 14:44:32.106284 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1985 â‹® [n24,pebble,s24] 33267930 [JOB 10197855] flushing: sstable created 21731045 +I220228 14:44:32.138686 18460916022 3@vendor/github.com/cockroachdb/pebble/compaction.go:1423 â‹® [n24,pebble,s24] 33267931 [JOB 10197855] flushed 1 memtable to L0 [21731018 21731020 21731021 21731022 21731023 21731024 21731025 21731026 21731027 21731028 21731030 21731031 21731033 21731034 21731035 21731036 21731037 21731038 21731039 21731040 21731041 21731042 21731043 21731044 21731045] (19MB), in 0.6s, output rate 31MB/s +---- +0.log + +summarize +---- +node: 24, store: 24 + from: 220228 14:44 + to: 220228 14:45 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +flush L0 1 19MB 0s +total 1 19MB 0s + +reset +---- + +log +I220228 16:01:22.487906 18476248525 3@vendor/github.com/cockroachdb/pebble/ingest.go:637 â‹® [n24,pebble,s24] 33430782 [JOB 10211226] ingested L0:21818678 (1.8KB), L0:21818683 (1.2KB), L0:21818679 (1.6KB), L0:21818680 (1.1KB), L0:21818681 (1.1KB), L0:21818682 (160MB) +45127:I220228 15:58:45.538681 18475981755 3@vendor/github.com/cockroachdb/pebble/ingest.go:637 â‹® [n24,pebble,s24] 33424719 [JOB 10210743] ingested L0:21814543 (1.4KB), L0:21814548 (1.2KB), L5:21814544 (1.4KB), L5:21814545 (1.1KB), L5:21814546 (1.1KB), L0:21814547 (140MB) +---- +0.log + +summarize +---- +node: 24, store: 24 + from: 220228 15:58 + to: 220228 15:59 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +ingest L0 3 140MB +ingest L5 3 3.6KB +total 6 140MB 0s +node: 24, store: 24 + from: 220228 16:01 + to: 220228 16:02 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +ingest L0 6 160MB +total 6 160MB 0s + +reset +---- + +log +I220907 00:27:21.579807 15082709999 3@vendor/github.com/cockroachdb/pebble/event.go:587 â‹® [n15,pebble,s15] 2736197 [JOB 743692] compacting(delete-only) L6 [18323385] (11MB) +I220907 00:27:21.580169 15082709999 3@vendor/github.com/cockroachdb/pebble/event.go:591 â‹® [n15,pebble,s15] 2736198 [JOB 743692] compacted(delete-only) L6 [18323385] (11MB) -> L6 [] (0B), in 0.0s, output rate 0 B/s + +I220907 00:27:21.631145 15082710355 3@vendor/github.com/cockroachdb/pebble/event.go:587 â‹® [n15,pebble,s15] 2736201 [JOB 743694] compacting(default) L5 [18323582] (1.8KB) + L6 [17770912] (128MB) +I220907 00:27:22.729839 15082710355 3@vendor/github.com/cockroachdb/pebble/event.go:591 â‹® [n15,pebble,s15] 2736208 [JOB 743694] compacted(default) L5 [18323582] (1.8KB) + L6 [17770912] (128MB) -> L6 [18323586] (3.6MB), in 1.1s, output rate 3.3MB/s + +I220907 00:27:21.630546 15082710354 3@vendor/github.com/cockroachdb/pebble/event.go:587 â‹® [n15,pebble,s15] 2736199 [JOB 743693] compacting(move) L5 [18323585] (4.0MB) + L6 [] (0B) +I220907 00:27:21.631002 15082710354 3@vendor/github.com/cockroachdb/pebble/event.go:591 â‹® [n15,pebble,s15] 2736200 [JOB 743693] compacted(move) L5 [18323585] (4.0MB) + L6 [] (0B) -> L6 [18323585] (4.0MB), in 0.0s, output rate 50GB/s +---- +0.log + +summarize +---- +node: 15, store: 15 + from: 220907 00:27 + to: 220907 00:28 + r-amp: NaN +_kind______from______to___default____move___elide__delete___count___in(B)__out(B)__mov(B)__del(B)______time +compact L5 L6 1 1 0 0 2 128MB 3.6MB 4.0MB 0B 1s +compact L6 L6 0 0 0 1 1 0B 0B 0B 11MB 0s +total 1 1 0 1 3 128MB 3.6MB 4.0MB 11MB 1s + +reset +---- + +log +I230831 04:13:28.824280 3780 3@pebble/event.go:685 â‹® [n10,s10,pebble] 365 [JOB 226] flushed 6 ingested flushables L0:024334 (1.5KB) + L0:024339 (1.0KB) + L0:024335 (1.9KB) + L0:024336 (1.1KB) + L0:024337 (1.1KB) + L0:024338 (12KB) in 0.0s (0.0s total), output rate 67MB/s + +I230831 04:13:28.689575 3717 3@pebble/event.go:685 â‹® [n10,s10,pebble] 345 [JOB 219] flushed 6 ingested flushables L0:024323 (1.5KB) + L0:024328 (1.0KB) + L0:024324 (2.0KB) + L2:024325 (1.1KB) + L2:024326 (1.1KB) + L0:024327 (54KB) in 0.0s (0.0s total), output rate 152MB/s +---- +0.log + +summarize +---- +node: 10, store: 10 + from: 230831 04:13 + to: 230831 04:14 + r-amp: NaN +_kind______from______to_____________________________________count___bytes______time +ingest L0 10 77KB +ingest L2 2 2.2KB +total 12 79KB 0s diff --git a/pebble/tool/logs/tool.go b/pebble/tool/logs/tool.go new file mode 100644 index 0000000..35c0783 --- /dev/null +++ b/pebble/tool/logs/tool.go @@ -0,0 +1,32 @@ +// Copyright 2021 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 logs + +import ( + "time" + + "github.com/spf13/cobra" +) + +// NewCmd returns a new cobra.Command for parsing logs. +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "logs", + Short: "Scan and summarize logs", + } + + compactionCmd := &cobra.Command{ + Use: "compactions", + Short: "Scan and summarize compaction logs", + RunE: runCompactionLogs, + } + compactionCmd.Flags().Duration( + "window", 10*time.Minute, "time window in which to aggregate compactions") + compactionCmd.Flags().Duration( + "long-running-limit", 0, "log compactions with runtime greater than the limit") + + cmd.AddCommand(compactionCmd) + return cmd +} diff --git a/pebble/tool/lsm.go b/pebble/tool/lsm.go new file mode 100644 index 0000000..013a80c --- /dev/null +++ b/pebble/tool/lsm.go @@ -0,0 +1,439 @@ +// Copyright 2020 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 tool + +import ( + "encoding/json" + "fmt" + "io" + "log" + "math" + "slices" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/sstable" + "github.com/spf13/cobra" +) + +//go:generate ./make_lsm_data.sh + +type lsmFileMetadata struct { + Size uint64 + Smallest int // ID of smallest key + Largest int // ID of largest key + SmallestSeqNum uint64 + LargestSeqNum uint64 + Virtual bool +} + +type lsmVersionEdit struct { + // Reason for the edit: flushed, ingested, compacted, added. + Reason string + // Map from level to files added to the level. + Added map[int][]base.FileNum `json:",omitempty"` + // Map from level to files deleted from the level. + Deleted map[int][]base.FileNum `json:",omitempty"` + // L0 sublevels for any files with changed sublevels so far. + Sublevels map[base.FileNum]int `json:",omitempty"` +} + +type lsmKey struct { + Pretty string + SeqNum uint64 + Kind int +} + +type lsmState struct { + Manifest string + Edits []lsmVersionEdit `json:",omitempty"` + Files map[base.FileNum]lsmFileMetadata `json:",omitempty"` + Keys []lsmKey `json:",omitempty"` + StartEdit int64 +} + +type lsmT struct { + Root *cobra.Command + + // Configuration. + opts *pebble.Options + comparers sstable.Comparers + + fmtKey keyFormatter + embed bool + pretty bool + startEdit int64 + endEdit int64 + editCount int64 + + cmp *base.Comparer + state lsmState + keyMap map[lsmKey]int +} + +func newLSM(opts *pebble.Options, comparers sstable.Comparers) *lsmT { + l := &lsmT{ + opts: opts, + comparers: comparers, + } + l.fmtKey.mustSet("quoted") + + l.Root = &cobra.Command{ + Use: "lsm ", + Short: "LSM visualization tool", + Long: ` +Visualize the evolution of an LSM from the version edits in a MANIFEST. + +Given an input MANIFEST, output an HTML file containing a visualization showing +the evolution of the LSM. Each version edit in the MANIFEST becomes a single +step in the visualization. The 7 levels of the LSM are depicted with each +sstable represented as a 1-pixel wide rectangle. The height of the rectangle is +proportional to the size (in bytes) of the sstable. The sstables are displayed +in the same order as they occur in the LSM. Note that the sstables from +different levels are NOT aligned according to their start and end keys (doing so +is also interesting, but it works against using the area of the rectangle to +indicate size). +`, + Args: cobra.ExactArgs(1), + RunE: l.runLSM, + } + + l.Root.Flags().Var(&l.fmtKey, "key", "key formatter") + l.Root.Flags().BoolVar(&l.embed, "embed", true, "embed javascript in HTML (disable for development)") + l.Root.Flags().BoolVar(&l.pretty, "pretty", false, "pretty JSON output") + l.Root.Flags().Int64Var(&l.startEdit, "start-edit", 0, "starting edit # to include in visualization") + l.Root.Flags().Int64Var(&l.endEdit, "end-edit", math.MaxInt64, "ending edit # to include in visualization") + l.Root.Flags().Int64Var(&l.editCount, "edit-count", math.MaxInt64, "count of edits to include in visualization") + return l +} + +func (l *lsmT) isFlagSet(name string) bool { + return l.Root.Flags().Changed(name) +} + +func (l *lsmT) validateFlags() error { + if l.isFlagSet("edit-count") { + if l.isFlagSet("start-edit") && l.isFlagSet("end-edit") { + return errors.Errorf("edit-count cannot be provided with both start-edit and end-edit") + } else if l.isFlagSet("end-edit") { + return errors.Errorf("cannot use edit-count with end-edit, use start-edit and end-edit instead") + } + } + + if l.startEdit > l.endEdit { + return errors.Errorf("start-edit cannot be after end-edit") + } + + return nil +} + +func (l *lsmT) runLSM(cmd *cobra.Command, args []string) error { + err := l.validateFlags() + if err != nil { + return err + } + + edits := l.readManifest(args[0]) + if edits == nil { + return nil + } + + if l.startEdit > 0 { + edits, err = l.coalesceEdits(edits) + if err != nil { + return err + } + } + if l.endEdit < int64(len(edits)) { + edits = edits[:l.endEdit-l.startEdit+1] + } + if l.editCount < int64(len(edits)) { + edits = edits[:l.editCount] + } + + l.buildKeys(edits) + err = l.buildEdits(edits) + if err != nil { + return err + } + + w := l.Root.OutOrStdout() + + fmt.Fprintf(w, ` + + + + +`) + if l.embed { + fmt.Fprintf(w, "\n", lsmDataCSS) + } else { + fmt.Fprintf(w, "\n") + } + fmt.Fprintf(w, "\n\n") + if l.embed { + fmt.Fprintf(w, "\n") + } else { + fmt.Fprintf(w, "\n") + } + fmt.Fprintf(w, "\n") + if l.embed { + fmt.Fprintf(w, "\n", lsmDataJS) + } else { + fmt.Fprintf(w, "\n") + } + fmt.Fprintf(w, "\n\n") + + return nil +} + +func (l *lsmT) readManifest(path string) []*manifest.VersionEdit { + f, err := l.opts.FS.Open(path) + if err != nil { + fmt.Fprintf(l.Root.OutOrStderr(), "%s\n", err) + return nil + } + defer f.Close() + + l.state.Manifest = path + + var edits []*manifest.VersionEdit + w := l.Root.OutOrStdout() + rr := record.NewReader(f, 0 /* logNum */) + for i := 0; ; i++ { + r, err := rr.Next() + if err != nil { + if err != io.EOF { + fmt.Fprintf(w, "%s\n", err) + } + break + } + + ve := &manifest.VersionEdit{} + err = ve.Decode(r) + if err != nil { + fmt.Fprintf(w, "%s\n", err) + break + } + edits = append(edits, ve) + + if ve.ComparerName != "" { + l.cmp = l.comparers[ve.ComparerName] + if l.cmp == nil { + fmt.Fprintf(w, "%d: unknown comparer %q\n", i, ve.ComparerName) + return nil + } + l.fmtKey.setForComparer(ve.ComparerName, l.comparers) + } else if l.cmp == nil { + l.cmp = base.DefaultComparer + } + } + return edits +} + +func (l *lsmT) buildKeys(edits []*manifest.VersionEdit) { + var keys []base.InternalKey + for _, ve := range edits { + for i := range ve.NewFiles { + nf := &ve.NewFiles[i] + keys = append(keys, nf.Meta.Smallest) + keys = append(keys, nf.Meta.Largest) + } + } + + l.keyMap = make(map[lsmKey]int) + + slices.SortFunc(keys, func(a, b base.InternalKey) int { + return base.InternalCompare(l.cmp.Compare, a, b) + }) + + for i := range keys { + k := &keys[i] + if i > 0 && base.InternalCompare(l.cmp.Compare, keys[i-1], keys[i]) == 0 { + continue + } + j := len(l.state.Keys) + l.state.Keys = append(l.state.Keys, lsmKey{ + Pretty: fmt.Sprint(l.fmtKey.fn(k.UserKey)), + SeqNum: k.SeqNum(), + Kind: int(k.Kind()), + }) + l.keyMap[lsmKey{string(k.UserKey), k.SeqNum(), int(k.Kind())}] = j + } +} + +func (l *lsmT) buildEdits(edits []*manifest.VersionEdit) error { + l.state.Edits = nil + l.state.StartEdit = l.startEdit + l.state.Files = make(map[base.FileNum]lsmFileMetadata) + var currentFiles [manifest.NumLevels][]*manifest.FileMetadata + + backings := make(map[base.DiskFileNum]*manifest.FileBacking) + + for _, ve := range edits { + for _, i := range ve.CreatedBackingTables { + backings[i.DiskFileNum] = i + } + if len(ve.DeletedFiles) == 0 && len(ve.NewFiles) == 0 { + continue + } + + edit := lsmVersionEdit{ + Reason: l.reason(ve), + Added: make(map[int][]base.FileNum), + Deleted: make(map[int][]base.FileNum), + } + + for j := range ve.NewFiles { + nf := &ve.NewFiles[j] + if b, ok := backings[nf.BackingFileNum]; ok && nf.Meta.Virtual { + nf.Meta.FileBacking = b + } + if _, ok := l.state.Files[nf.Meta.FileNum]; !ok { + l.state.Files[nf.Meta.FileNum] = lsmFileMetadata{ + Size: nf.Meta.Size, + Smallest: l.findKey(nf.Meta.Smallest), + Largest: l.findKey(nf.Meta.Largest), + SmallestSeqNum: nf.Meta.SmallestSeqNum, + LargestSeqNum: nf.Meta.LargestSeqNum, + Virtual: nf.Meta.Virtual, + } + } + edit.Added[nf.Level] = append(edit.Added[nf.Level], nf.Meta.FileNum) + currentFiles[nf.Level] = append(currentFiles[nf.Level], nf.Meta) + } + + for df := range ve.DeletedFiles { + edit.Deleted[df.Level] = append(edit.Deleted[df.Level], df.FileNum) + for j, f := range currentFiles[df.Level] { + if f.FileNum == df.FileNum { + copy(currentFiles[df.Level][j:], currentFiles[df.Level][j+1:]) + currentFiles[df.Level] = currentFiles[df.Level][:len(currentFiles[df.Level])-1] + } + } + } + + v := manifest.NewVersion(l.cmp.Compare, l.fmtKey.fn, 0, currentFiles) + edit.Sublevels = make(map[base.FileNum]int) + for sublevel, files := range v.L0SublevelFiles { + iter := files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if len(l.state.Edits) > 0 { + lastEdit := l.state.Edits[len(l.state.Edits)-1] + if sublevel2, ok := lastEdit.Sublevels[f.FileNum]; ok && sublevel == sublevel2 { + continue + } + } + edit.Sublevels[f.FileNum] = sublevel + } + } + l.state.Edits = append(l.state.Edits, edit) + } + + if l.state.Edits == nil { + return errors.Errorf("there are no edits in [start-edit, end-edit], which add or delete files") + } + return nil +} + +func (l *lsmT) coalesceEdits(edits []*manifest.VersionEdit) ([]*manifest.VersionEdit, error) { + if l.startEdit >= int64(len(edits)) { + return nil, errors.Errorf("start-edit is more than the number of edits, %d", len(edits)) + } + + be := manifest.BulkVersionEdit{} + be.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata) + + // Coalesce all edits from [0, l.startEdit) into a BulkVersionEdit. + for _, ve := range edits[:l.startEdit] { + err := be.Accumulate(ve) + if err != nil { + return nil, err + } + } + + startingEdit := edits[l.startEdit] + var beNewFiles []manifest.NewFileEntry + beDeletedFiles := make(map[manifest.DeletedFileEntry]*manifest.FileMetadata) + + for level, deletedFiles := range be.Deleted { + for _, file := range deletedFiles { + dfe := manifest.DeletedFileEntry{ + Level: level, + FileNum: file.FileNum, + } + beDeletedFiles[dfe] = file + } + } + + // Filter out added files that were also deleted in the BulkVersionEdit. + for level, newFiles := range be.Added { + for _, file := range newFiles { + dfe := manifest.DeletedFileEntry{ + Level: level, + FileNum: file.FileNum, + } + + if _, ok := beDeletedFiles[dfe]; !ok { + beNewFiles = append(beNewFiles, manifest.NewFileEntry{ + Level: level, + Meta: file, + }) + } + } + } + startingEdit.NewFiles = append(beNewFiles, startingEdit.NewFiles...) + + edits = edits[l.startEdit:] + return edits, nil +} + +func (l *lsmT) findKey(key base.InternalKey) int { + return l.keyMap[lsmKey{string(key.UserKey), key.SeqNum(), int(key.Kind())}] +} + +func (l *lsmT) reason(ve *manifest.VersionEdit) string { + if len(ve.DeletedFiles) > 0 { + return "compacted" + } + if ve.MinUnflushedLogNum != 0 { + return "flushed" + } + for i := range ve.NewFiles { + nf := &ve.NewFiles[i] + if nf.Meta.SmallestSeqNum == nf.Meta.LargestSeqNum { + return "ingested" + } + } + return "added" +} + +func (l *lsmT) formatJSON(v interface{}) string { + if l.pretty { + return l.prettyJSON(v) + } + return l.uglyJSON(v) +} + +func (l *lsmT) uglyJSON(v interface{}) string { + data, err := json.Marshal(v) + if err != nil { + log.Fatal(err) + } + return string(data) +} + +func (l *lsmT) prettyJSON(v interface{}) string { + data, err := json.MarshalIndent(v, "", "\t") + if err != nil { + log.Fatal(err) + } + return string(data) +} diff --git a/pebble/tool/lsm_data.go b/pebble/tool/lsm_data.go new file mode 100644 index 0000000..ea34cd2 --- /dev/null +++ b/pebble/tool/lsm_data.go @@ -0,0 +1,906 @@ +// Code generated by make_lsm_data.sh; DO NOT EDIT. + +package tool + +var lsmDataCSS = ` +body { + margin: 0 0 0 0; +} + +.counts { + font: 10px sans-serif; +} + +.help { + font: 12px sans-serif; +} + +.levels { + font: 12px sans-serif; +} + +.reason { + font: 12px sans-serif; +} + +.sizes { + font: 10px sans-serif; +} + +.ticks { + font: 10px sans-serif; +} + +.track, +.track-inset, +.track-overlay { + stroke-linecap: round; +} + +.track { + stroke: #000; + stroke-opacity: 0.3; + stroke-width: 10px; +} + +.track-inset { + stroke: #ddd; + stroke-width: 8px; +} + +.track-overlay { + pointer-events: stroke; + stroke-width: 50px; + stroke: transparent; + cursor: crosshair; +} + +.handle { + fill: #fff; + stroke: #000; + stroke-opacity: 0.5; + stroke-width: 1.25px; +} + +#container { + height: 100vh; + width: 100%; + overflow-y: scroll; +} + +#header { + display: block; +} + +#index { + position: relative; + margin: 11px 10px 10px 10px; + width: 50px; +} + +#index-container { + float: left; + width: 76px; + height: 40px; +} + +#checkbox-container { + float: right; + width: 170px; + height: 40px; + padding-top: 10px; +} + +#slider { + background-color: #fff; + height: 40px; + width: calc(100% - 256px); +} + +#vis { + display: block; + background-color: #fff; + height: calc(100% - 40px); + width: 100%; +} +` + +var lsmDataJS = ` +// TODO(peter): +// +// - interactions +// - mouse wheel: horizontal zoom +// - click/drag: horizontal pan + +"use strict"; + +// The heights of each level. The first few levels are given smaller +// heights to account for the increasing target file size. +// +// TODO(peter): Use the TargetFileSizes specified in the OPTIONS file. +let levelHeights = [16, 16, 16, 16, 32, 64, 128]; +const offsetStart = 24; +let levelOffsets = generateLevelOffsets(); +const lineStart = 105; +const sublevelHeight = 16; +let levelWidth = 0; + +{ + // Create the base DOM elements. + let c = d3 + .select("body") + .append("div") + .attr("id", "container"); + let h = c.append("div").attr("id", "header"); + h + .append("div") + .attr("id", "index-container") + .append("input") + .attr("type", "text") + .attr("id", "index") + .attr("autocomplete", "off"); + let checkboxContainer = h + .append("div") + .attr("id", "checkbox-container"); + checkboxContainer.append("input") + .attr("type", "checkbox") + .attr("id", "flatten-sublevels") + .on("change", () => {version.onCheckboxChange(d3.event.target.checked)}); + checkboxContainer.append("label") + .attr("for", "flatten-sublevels") + .text("Show sublevels"); + h.append("svg").attr("id", "slider"); + c.append("svg").attr("id", "vis"); +} + +let vis = d3.select("#vis"); + +function renderHelp() { + vis + .append("text") + .attr("class", "help") + .attr("x", 10) + .attr("y", levelOffsets[6] + 30) + .text( + "(space: start/stop, left-arrow[+shift]: step-back, right-arrow[+shift]: step-forward)" + ); +} + +function renderReason() { + return vis + .append("text") + .attr("class", "reason") + .attr("x", 10) + .attr("y", 16); +} + +let reason = renderReason(); + +let index = d3.select("#index"); + +// Pretty formatting of a number in human readable units. +function humanize(s) { + const iecSuffixes = [" B", " KB", " MB", " GB", " TB", " PB", " EB"]; + if (s < 10) { + return "" + s; + } + let e = Math.floor(Math.log(s) / Math.log(1024)); + let suffix = iecSuffixes[Math.floor(e)]; + let val = Math.floor(s / Math.pow(1024, e) * 10 + 0.5) / 10; + return val.toFixed(val < 10 ? 1 : 0) + suffix; +} + +function generateLevelOffsets() { + return levelHeights.map((v, i) => + levelHeights.slice(0, i + 1).reduce((sum, elem) => sum + elem, offsetStart) + ); +} + +function styleWidth(e) { + let width = +e.style("width").slice(0, -2); + return Math.round(Number(width)); +} + +function styleHeight(e) { + let height = +e.style("height").slice(0, -2); + return Math.round(Number(height)); +} + +let sliderX, sliderHandle; +let offsetSliderX; + +// The version object holds the current LSM state. +let version = { + levels: [[], [], [], [], [], [], []], + sublevels: [], + numSublevels: 0, + showSublevels: false, + // Generated after every change using setLevelsInfo(). + levelsInfo: [], + // The version edit index. + index: -1, + + init: function() { + for (let edit of data.Edits) { + if (edit.Sublevels === null || edit.Sublevels === undefined) { + continue; + } + for (let [file, sublevel] of Object.entries(edit.Sublevels)) { + if (sublevel >= this.numSublevels) { + this.numSublevels = sublevel + 1; + } + } + } + for (let i = 0; i < this.numSublevels; i++) { + this.sublevels.push([]); + } + d3.select("#checkbox-container label") + .text("Show sublevels (" + this.numSublevels.toString() + ")"); + this.setHeights(); + this.setLevelsInfo(); + renderHelp(); + }, + + setHeights: function() { + // Update the height of level 0 to account for the number of sublevels, + // if there are any. + if (this.numSublevels > 0 && this.showSublevels === true) { + levelHeights[0] = sublevelHeight * this.numSublevels; + } else { + levelHeights[0] = sublevelHeight; + } + levelOffsets = generateLevelOffsets(); + vis.style("height", levelOffsets[6] + 100); + }, + + onCheckboxChange: function(value) { + this.showSublevels = value; + vis.selectAll("*") + .remove(); + reason = renderReason(); + this.setHeights(); + this.setLevelsInfo(); + renderHelp(); + + this.render(true); + this.updateSize(); + }, + + // Set the version edit index. This steps either forward or + // backward through the version edits, applying or unapplying each + // edit. + set: function(index) { + let prevIndex = this.index; + if (index < 0) { + index = 0; + } else if (index >= data.Edits.length) { + index = data.Edits.length - 1; + } + if (index == this.index) { + return; + } + + // If the current edit index is less than the target index, + // step forward applying edits. + for (; this.index < index; this.index++) { + let edit = data.Edits[this.index + 1]; + for (let level in edit.Deleted) { + this.remove(level, edit.Deleted[level]); + } + for (let level in edit.Added) { + this.add(level, edit.Added[level]); + } + } + + // If the current edit index is greater than the target index, + // step backward unapplying edits. + for (; this.index > index; this.index--) { + let edit = data.Edits[this.index]; + for (let level in edit.Added) { + this.remove(level, edit.Added[level]); + } + for (let level in edit.Deleted) { + this.add(level, edit.Deleted[level]); + } + } + + // Build the sublevels from this.levels[0]. They need to be rebuilt from + // scratch each time there's a change to L0. + this.sublevels = []; + while(this.sublevels.length < this.numSublevels) { + this.sublevels.push([]); + } + for (let file of this.levels[0]) { + let sublevel = null; + for (let i = index; i >= 0 && (sublevel === null || sublevel === undefined); i--) { + if (data.Edits[i].Sublevels == null || data.Edits[i].Sublevels == undefined) { + continue; + } + sublevel = data.Edits[i].Sublevels[file]; + } + this.sublevels[sublevel].push(file); + } + + // Sort the levels. + for (let i in this.levels) { + if (i == 0) { + for (let j in this.sublevels) { + this.sublevels[j].sort(function(a, b) { + let fa = data.Files[a]; + let fb = data.Files[b]; + if (fa.Smallest < fb.Smallest) { + return -1; + } + if (fa.Smallest > fb.Smallest) { + return +1; + } + return 0; + }); + } + this.levels[i].sort(function(a, b) { + let fa = data.Files[a]; + let fb = data.Files[b]; + if (fa.LargestSeqNum < fb.LargestSeqNum) { + return -1; + } + if (fa.LargestSeqNum > fb.LargestSeqNum) { + return +1; + } + if (fa.SmallestSeqNum < fb.SmallestSeqNum) { + return -1; + } + if (fa.SmallestSeqNum > fb.SmallestSeqNum) { + return +1; + } + return a < b; + }); + } else { + this.levels[i].sort(function(a, b) { + let fa = data.Files[a]; + let fb = data.Files[b]; + if (fa.Smallest < fb.Smallest) { + return -1; + } + if (fa.Smallest > fb.Smallest) { + return +1; + } + return 0; + }); + } + } + + this.updateLevelsInfo(); + this.render(prevIndex === -1); + }, + + // Add the specified sstables to the specifed level. + add: function(level, fileNums) { + for (let i = 0; i < fileNums.length; i++) { + this.levels[level].push(fileNums[i]); + } + }, + + // Remove the specified sstables from the specifed level. + remove: function(level, fileNums) { + let l = this.levels[level]; + for (let i = 0; i < l.length; i++) { + if (fileNums.indexOf(l[i]) != -1) { + l[i] = l[l.length - 1]; + l.pop(); + i--; + } + } + }, + + // Return the size of the sstables in a level. + size: function(level, sublevel) { + if (level == 0 && sublevel !== null && sublevel !== undefined) { + return this.sublevels[sublevel].reduce( + (sum, elem) => sum + data.Files[elem].Size, + 0 + ); + } + return (this.levels[level] || []).reduce( + (sum, elem) => sum + data.Files[elem].Size, + 0 + ); + }, + + // Returns the height to use for an sstable. + height: function(fileNum) { + let meta = data.Files[fileNum]; + return Math.ceil((meta.Size + 1024.0 * 1024.0 - 1) / (1024.0 * 1024.0)); + }, + + scale: function(level) { + return levelWidth < this.levelsInfo[level].files.length + ? levelWidth / this.levelsInfo[level].files.length + : 1; + }, + + // Return a summary of the count and size of the specified sstables. + summarize: function(level, fileNums) { + let count = 0; + let size = 0; + for (let fileNum of fileNums) { + count++; + size += data.Files[fileNum].Size; + } + return count + " @ " + "L" + level + " (" + humanize(size) + ")"; + }, + + // Return a textual description of a version edit. + describe: function(edit) { + let s = edit.Reason; + + if (edit.Deleted) { + let sep = " "; + for (let i = 0; i < 7; i++) { + if (edit.Deleted[i]) { + s += sep + this.summarize(i, edit.Deleted[i]); + sep = " + "; + } + } + } + + if (edit.Added) { + let sep = " => "; + for (let i = 0; i < 7; i++) { + if (edit.Added[i]) { + s += sep + this.summarize(i, edit.Added[i]); + sep = " + "; + } + } + } + + return s; + }, + + setLevelsInfo: function() { + let sublevelCount = this.numSublevels; + let levelsInfo = []; + let levelsStart = 1; + if (this.showSublevels === true) { + levelsInfo = this.sublevels.map((files, sublevel) => ({ + files: files, + levelString: "L0." + sublevel.toString(), + levelDisplayString: (sublevel === this.numSublevels - 1 ? + "L0." : "    .") + sublevel.toString(), + levelClass: "L0-" + sublevel.toString(), + level: 0, + offset: offsetStart + (sublevelHeight * (sublevelCount - sublevel)), + height: sublevelHeight, + size: humanize(this.size(0, sublevel)), + })); + if (levelsInfo.length === 0) { + levelsStart = 0; + } + levelsInfo.reverse(); + } else { + levelsStart = 0; + } + + levelsInfo = levelsInfo.concat(this.levels.slice(levelsStart).map((files, level) => ({ + files: files, + levelString: "L" + (level+levelsStart).toString(), + levelDisplayString: "L" + (level+levelsStart).toString(), + levelClass: "L" + (level+levelsStart).toString(), + level: level, + offset: levelOffsets[level+levelsStart], + height: levelHeights[level+levelsStart], + size: humanize(this.size(level+levelsStart)), + }))); + this.levelsInfo = levelsInfo; + }, + + updateLevelsInfo: function() { + let levelsStart = 1; + if (this.showSublevels === true) { + this.sublevels.forEach((files, sublevel) => { + this.levelsInfo[this.numSublevels - (sublevel + 1)].files = files; + this.levelsInfo[this.numSublevels - (sublevel + 1)].size = humanize(this.size(0, sublevel)); + }); + if (this.numSublevels === 0) { + levelsStart = 0; + } + } else { + levelsStart = 0; + } + + this.levels.slice(levelsStart).forEach((files, level) => { + let sublevelOffset = this.showSublevels === true ? this.numSublevels : 0; + this.levelsInfo[sublevelOffset + level].files = files; + this.levelsInfo[sublevelOffset + level].size = humanize(this.size(levelsStart + level)); + }); + }, + + render: function(redraw) { + let version = this; + + vis.interrupt(); + + // Render the edit info. + let info = "[" + this.describe(data.Edits[this.index]) + "]"; + reason.text(info); + + // Render the text for each level: sstable count and size. + vis + .selectAll("text.levels") + .data(this.levelsInfo) + .enter() + .append("text") + .attr("class", "levels") + .attr("x", 10) + .attr("y", d => d.offset) + .html(d => d.levelDisplayString); + vis + .selectAll("text.counts") + .data(this.levelsInfo) + .text((d, i) => d.files.length) + .enter() + .append("text") + .attr("class", "counts") + .attr("text-anchor", "end") + .attr("x", 55) + .attr("y", d => d.offset) + .text(d => d.files.length); + vis + .selectAll("text.sizes") + .data(this.levelsInfo) + .text((d, i) => d.size) + .enter() + .append("text") + .attr("class", "sizes") + .attr("text-anchor", "end") + .attr("x", 100) + .attr("y", (d, i) => d.offset) + .text(d => d.size); + + // Render each of the levels. Each level is composed of an + // outer group which provides a clipping recentangle, an inner + // group defining the coordinate system, an overlap rectangle + // to capture mouse events, an indicator rectangle used to + // display sstable overlaps, and the per-sstable rectangles. + for (let i in this.levelsInfo) { + let g, clipG; + if (redraw === false) { + g = vis + .selectAll("g.clip" + this.levelsInfo[i].levelClass) + .select("g") + .data([i]); + clipG = g + .enter() + .append("g") + .attr("class", "clipRect clip" + this.levelsInfo[i].levelClass) + .attr("clip-path", "url(#" + this.levelsInfo[i].levelClass + ")"); + } else { + clipG = vis + .append("g") + .attr("class", "clipRect clip" + this.levelsInfo[i].levelClass) + .attr("clip-path", "url(#" + this.levelsInfo[i].levelClass + ")") + .data([i]); + g = clipG + .append("g"); + } + clipG + .append("g") + .attr( + "transform", + "translate(" + + lineStart + + "," + + this.levelsInfo[i].offset + + ") scale(1,-1)" + ); + clipG.append("rect").attr("class", "indicator"); + + // Define the overlap rectangle for capturing mouse events. + clipG + .append("rect") + .attr("x", lineStart) + .attr("y", this.levelsInfo[i].offset - this.levelsInfo[i].height) + .attr("width", levelWidth) + .attr("height", this.levelsInfo[i].height) + .attr("opacity", 0) + .attr("pointer-events", "all") + .on("mousemove", i => version.onMouseMove(i)) + .on("mouseout", function() { + reason.text(info); + vis.selectAll("rect.indicator").attr("fill", "none"); + }); + + // Scale each level to fit within the display. + let s = this.scale(i); + g.attr( + "transform", + "translate(" + + lineStart + + "," + + this.levelsInfo[i].offset + + ") scale(" + + s + + "," + + -(1 / s) + + ")" + ); + + // Render the sstables for the level. + let level = g.selectAll("rect." + this.levelsInfo[i].levelClass).data(this.levelsInfo[i].files); + level.attr("fill", fileNum => (data.Files[fileNum].Virtual?"#8A9":"#555")).attr("x", (fileNum, i) => i); + level + .enter() + .append("rect") + .attr("class", this.levelsInfo[i].levelClass + " sstable") + .attr("id", fileNum => fileNum) + .attr("fill", fileNum => (data.Files[fileNum].Virtual?"orange":"red")) + .attr("x", (fileNum, i) => i) + .attr("y", 0) + .attr("width", 1) + .attr("height", fileNum => version.height(fileNum)); + level.exit().remove(); + } + + sliderHandle.attr("cx", sliderX(version.index)); + index.node().value = version.index + data.StartEdit; + }, + + onMouseMove: function(i) { + i = Number(i); + if (Number.isNaN(i) || i >= this.levelsInfo.length || this.levelsInfo[i].files.length === 0) { + return; + } + + // The mouse coordinates are relative to the + // SVG element. Adjust to be relative to the + // level position. + let mousex = d3.mouse(vis.node())[0] - lineStart; + let index = Math.round(mousex / this.scale(i)); + if (index < 0) { + index = 0; + } else if (index >= this.levelsInfo[i].files.length) { + index = this.levelsInfo[i].files.length - 1; + } + let fileNum = this.levelsInfo[i].files[index]; + let meta = data.Files[fileNum]; + + // Find the start and end index of the tables + // that overlap with filenum. + let overlapInfo = ""; + for (let j = 1; j < this.levelsInfo.length; j++) { + if (this.levelsInfo[i].files.length === 0) { + continue; + } + let indicator = vis.select("g.clip" + this.levelsInfo[j].levelClass + " rect.indicator"); + indicator + .attr("fill", "black") + .attr("opacity", 0.3) + .attr("y", this.levelsInfo[j].offset - this.levelsInfo[j].height) + .attr("height", this.levelsInfo[j].height); + if (j === i) { + continue; + } + let fileNums = this.levelsInfo[j].files; + for (let k in fileNums) { + let other = data.Files[fileNums[k]]; + if (other.Largest < meta.Smallest) { + continue; + } + let s = this.scale(j); + let t = k; + for (; k < fileNums.length; k++) { + let other = data.Files[fileNums[k]]; + if (other.Smallest >= meta.Largest) { + break; + } + } + if (k === t) { + indicator.attr("x", lineStart + s * t).attr("width", s); + } else { + indicator + .attr("x", lineStart + s * t) + .attr("width", Math.max(0.5, s * (k - t))); + } + if (i + 1 === j && k > t) { + let overlapSize = this.levelsInfo[j].files + .slice(t, k) + .reduce((sum, elem) => sum + data.Files[elem].Size, 0); + + overlapInfo = + " overlaps " + + (k - t) + + " @ " + + this.levelsInfo[j].levelString + + " (" + + humanize(overlapSize) + + ")"; + } + break; + } + } + + reason.text( + "[" + + this.levelsInfo[i].levelString + + (data.Files[fileNum].Virtual? " v":" ") + + fileNum + + " (" + + humanize(data.Files[fileNum].Size) + + ")" + + overlapInfo + + " <" + + data.Keys[data.Files[fileNum].Smallest].Pretty + + ", " + + data.Keys[data.Files[fileNum].Largest].Pretty + + ">" + + "]" + ); + + vis + .select("g.clip" + this.levelsInfo[i].levelClass + " rect.indicator") + .attr("x", lineStart + this.scale(i) * index) + .attr("width", 1); + }, + + // Recalculate structures related to the page width. + updateSize: function() { + let svg = d3.select("#slider").html(""); + + let margin = { right: 10, left: 10 }; + + let width = styleWidth(d3.select("#slider")) - margin.left - margin.right, + height = styleHeight(svg); + + sliderX = d3 + .scaleLinear() + .domain([0, data.Edits.length - 1]) + .range([0, width]) + .clamp(true); + + // Used only to generate offset ticks for slider. + // sliderX is used to index into the data.Edits array (0-indexed). + offsetSliderX = d3 + .scaleLinear() + .domain([data.StartEdit, data.StartEdit + data.Edits.length - 1]) + .range([0, width]); + + let slider = svg + .append("g") + .attr("class", "slider") + .attr("transform", "translate(" + margin.left + "," + height / 2 + ")"); + + slider + .append("line") + .attr("class", "track") + .attr("x1", sliderX.range()[0]) + .attr("x2", sliderX.range()[1]) + .select(function() { + return this.parentNode.appendChild(this.cloneNode(true)); + }) + .attr("class", "track-inset") + .select(function() { + return this.parentNode.appendChild(this.cloneNode(true)); + }) + .attr("class", "track-overlay") + .call( + d3 + .drag() + .on("start.interrupt", function() { + slider.interrupt(); + }) + .on("start drag", function() { + version.set(Math.round(sliderX.invert(d3.event.x))); + }) + ); + + slider + .insert("g", ".track-overlay") + .attr("class", "ticks") + .attr("transform", "translate(0," + 18 + ")") + .selectAll("text") + .data(offsetSliderX.ticks(10)) + .enter() + .append("text") + .attr("x", offsetSliderX) + .attr("text-anchor", "middle") + .text(function(d) { + return d; + }); + + sliderHandle = slider + .insert("circle", ".track-overlay") + .attr("class", "handle") + .attr("r", 9) + .attr("cx", sliderX(version.index)); + + levelWidth = styleWidth(vis) - 10 - lineStart; + let lineEnd = lineStart + levelWidth; + + vis + .selectAll("line") + .data(this.levelsInfo) + .attr("x2", lineEnd) + .enter() + .append("line") + .attr("x1", lineStart) + .attr("x2", lineEnd) + .attr("y1", d => d.offset) + .attr("y2", d => d.offset) + .attr("stroke", "#ddd"); + + vis + .selectAll("defs clipPath rect") + .data(this.levelsInfo) + .attr("width", lineEnd - lineStart) + .enter() + .append("defs") + .append("clipPath") + .attr("id", d => d.levelClass) + .append("rect") + .attr("x", lineStart) + .attr("y", d => d.offset - d.height) + .attr("width", lineEnd - lineStart) + .attr("height", d => d.height); + }, +}; + +window.onload = function() { + version.init(); + version.updateSize(); + version.set(0); +}; + +window.addEventListener("resize", function() { + version.updateSize(); + version.render(); +}); + +let timer; + +function startPlayback(increment) { + timer = d3.timer(function() { + let lastIndex = version.index; + version.set(version.index + increment); + if (lastIndex == version.index) { + timer.stop(); + timer = null; + } + }); +} + +function stopPlayback() { + if (timer == null) { + return false; + } + timer.stop(); + timer = null; + return true; +} + +document.addEventListener("keydown", function(e) { + switch (e.keyCode) { + case 37: // left arrow + stopPlayback(); + version.set(version.index - (e.shiftKey ? 10 : 1)); + return; + case 39: // right arrow + stopPlayback(); + version.set(version.index + (e.shiftKey ? 10 : 1)); + return; + case 32: // space + if (stopPlayback()) { + return; + } + startPlayback(1); + return; + } +}); + +index.on("input", function() { + if (!isNaN(+this.value)) { + const val = Number(this.value) - data.StartEdit; + if (val >= 0) { + version.set(val); + } + } +}); +` diff --git a/pebble/tool/make_incorrect_manifests.go b/pebble/tool/make_incorrect_manifests.go new file mode 100644 index 0000000..90aa87d --- /dev/null +++ b/pebble/tool/make_incorrect_manifests.go @@ -0,0 +1,61 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +//go:build make_incorrect_manifests +// +build make_incorrect_manifests + +// Run using: go run -tags make_incorrect_manifests ./tool/make_incorrect_manifests.go +package main + +import ( + "log" + + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/vfs" +) + +func writeVE(writer *record.Writer, ve *manifest.VersionEdit) { + w, err := writer.Next() + if err != nil { + log.Fatal(err) + } + err = ve.Encode(w) + if err != nil { + log.Fatal(err) + } +} + +func makeManifest1() { + fs := vfs.Default + f, err := fs.Create("tool/testdata/MANIFEST-invalid") + if err != nil { + log.Fatal(err) + } + writer := record.NewWriter(f) + var ve manifest.VersionEdit + ve.ComparerName = "leveldb.BytewiseComparator" + ve.MinUnflushedLogNum = 2 + ve.NextFileNum = 5 + ve.LastSeqNum = 20 + ve.NewFiles = []manifest.NewFileEntry{ + {Level: 6, Meta: &manifest.FileMetadata{ + FileNum: 1, SmallestSeqNum: 2, LargestSeqNum: 5}}} + writeVE(writer, &ve) + + ve.MinUnflushedLogNum = 3 + ve.NewFiles = []manifest.NewFileEntry{ + {Level: 6, Meta: &manifest.FileMetadata{ + FileNum: 2, SmallestSeqNum: 1, LargestSeqNum: 4}}} + writeVE(writer, &ve) + + err = writer.Close() + if err != nil { + log.Fatal(err) + } +} + +func main() { + makeManifest1() +} diff --git a/pebble/tool/make_lsm_data.sh b/pebble/tool/make_lsm_data.sh new file mode 100755 index 0000000..6156ddf --- /dev/null +++ b/pebble/tool/make_lsm_data.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +dest=lsm_data.go +do="DO" +not="NOT" +edit="EDIT" + +cat > ${dest} <> ${dest} + +cat >> ${dest} <> ${dest} + +cat>> ${dest} <", + Short: "print manifest contents", + Long: ` +Print the contents of the MANIFEST files. +`, + Args: cobra.MinimumNArgs(1), + Run: m.runDump, + } + m.Dump.Flags().Var(&m.fmtKey, "key", "key formatter") + m.Dump.Flags().Var(&m.filterStart, "filter-start", "start key filters out all version edits that only reference sstables containing keys strictly before the given key") + m.Dump.Flags().Var(&m.filterEnd, "filter-end", "end key filters out all version edits that only reference sstables containing keys at or strictly after the given key") + m.Root.AddCommand(m.Dump) + m.Root.PersistentFlags().BoolVarP(&m.verbose, "verbose", "v", false, "verbose output") + + // Add summarize command + m.Summarize = &cobra.Command{ + Use: "summarize ", + Short: "summarize manifest contents", + Long: ` +Summarize the edits to the MANIFEST files over time. +`, + Args: cobra.MinimumNArgs(1), + Run: m.runSummarize, + } + m.Root.AddCommand(m.Summarize) + m.Summarize.Flags().DurationVar( + &m.summarizeDur, "dur", time.Hour, "bucket duration as a Go duration string (eg, '1h', '15m')") + + // Add check command + m.Check = &cobra.Command{ + Use: "check ", + Short: "check manifest contents", + Long: ` +Check the contents of the MANIFEST files. +`, + Args: cobra.MinimumNArgs(1), + Run: m.runCheck, + } + m.Root.AddCommand(m.Check) + m.Check.Flags().Var( + &m.fmtKey, "key", "key formatter") + + return m +} + +func (m *manifestT) printLevels(cmp base.Compare, stdout io.Writer, v *manifest.Version) { + for level := range v.Levels { + if level == 0 && len(v.L0SublevelFiles) > 0 && !v.Levels[level].Empty() { + for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- { + fmt.Fprintf(stdout, "--- L0.%d ---\n", sublevel) + v.L0SublevelFiles[sublevel].Each(func(f *manifest.FileMetadata) { + if !anyOverlapFile(cmp, f, m.filterStart, m.filterEnd) { + return + } + fmt.Fprintf(stdout, " %s:%d", f.FileNum, f.Size) + formatSeqNumRange(stdout, f.SmallestSeqNum, f.LargestSeqNum) + formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest) + fmt.Fprintf(stdout, "\n") + }) + } + continue + } + fmt.Fprintf(stdout, "--- L%d ---\n", level) + iter := v.Levels[level].Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if !anyOverlapFile(cmp, f, m.filterStart, m.filterEnd) { + continue + } + fmt.Fprintf(stdout, " %s:%d", f.FileNum, f.Size) + formatSeqNumRange(stdout, f.SmallestSeqNum, f.LargestSeqNum) + formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest) + fmt.Fprintf(stdout, "\n") + } + } +} + +func (m *manifestT) runDump(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + for _, arg := range args { + func() { + f, err := m.opts.FS.Open(arg) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer f.Close() + + fmt.Fprintf(stdout, "%s\n", arg) + + var bve manifest.BulkVersionEdit + bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata) + var comparer *base.Comparer + var editIdx int + rr := record.NewReader(f, 0 /* logNum */) + for { + offset := rr.Offset() + r, err := rr.Next() + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + break + } + + var ve manifest.VersionEdit + err = ve.Decode(r) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + break + } + if err := bve.Accumulate(&ve); err != nil { + fmt.Fprintf(stdout, "%s\n", err) + break + } + + if comparer != nil && !anyOverlap(comparer.Compare, &ve, m.filterStart, m.filterEnd) { + continue + } + + empty := true + fmt.Fprintf(stdout, "%d/%d\n", offset, editIdx) + if ve.ComparerName != "" { + empty = false + fmt.Fprintf(stdout, " comparer: %s", ve.ComparerName) + comparer = m.comparers[ve.ComparerName] + if comparer == nil { + fmt.Fprintf(stdout, " (unknown)") + } + fmt.Fprintf(stdout, "\n") + m.fmtKey.setForComparer(ve.ComparerName, m.comparers) + } + if ve.MinUnflushedLogNum != 0 { + empty = false + fmt.Fprintf(stdout, " log-num: %d\n", ve.MinUnflushedLogNum) + } + if ve.ObsoletePrevLogNum != 0 { + empty = false + fmt.Fprintf(stdout, " prev-log-num: %d\n", ve.ObsoletePrevLogNum) + } + if ve.NextFileNum != 0 { + empty = false + fmt.Fprintf(stdout, " next-file-num: %d\n", ve.NextFileNum) + } + if ve.LastSeqNum != 0 { + empty = false + fmt.Fprintf(stdout, " last-seq-num: %d\n", ve.LastSeqNum) + } + entries := make([]manifest.DeletedFileEntry, 0, len(ve.DeletedFiles)) + for df := range ve.DeletedFiles { + empty = false + entries = append(entries, df) + } + slices.SortFunc(entries, func(a, b manifest.DeletedFileEntry) int { + if v := cmp.Compare(a.Level, b.Level); v != 0 { + return v + } + return cmp.Compare(a.FileNum, b.FileNum) + }) + for _, df := range entries { + fmt.Fprintf(stdout, " deleted: L%d %s\n", df.Level, df.FileNum) + } + for _, nf := range ve.NewFiles { + empty = false + fmt.Fprintf(stdout, " added: L%d %s:%d", + nf.Level, nf.Meta.FileNum, nf.Meta.Size) + formatSeqNumRange(stdout, nf.Meta.SmallestSeqNum, nf.Meta.LargestSeqNum) + formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest) + if nf.Meta.CreationTime != 0 { + fmt.Fprintf(stdout, " (%s)", + time.Unix(nf.Meta.CreationTime, 0).UTC().Format(time.RFC3339)) + } + fmt.Fprintf(stdout, "\n") + } + if empty { + // NB: An empty version edit can happen if we log a version edit with + // a zero field. RocksDB does this with a version edit that contains + // `LogNum == 0`. + fmt.Fprintf(stdout, " \n") + } + editIdx++ + } + + if comparer != nil { + v, err := bve.Apply( + nil /* version */, comparer.Compare, m.fmtKey.fn, 0, + m.opts.Experimental.ReadCompactionRate, + nil /* zombies */, manifest.AllowSplitUserKeys, + ) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + m.printLevels(comparer.Compare, stdout, v) + } + }() + } +} + +func anyOverlap(cmp base.Compare, ve *manifest.VersionEdit, start, end key) bool { + if start == nil && end == nil { + return true + } + for _, df := range ve.DeletedFiles { + if anyOverlapFile(cmp, df, start, end) { + return true + } + } + for _, nf := range ve.NewFiles { + if anyOverlapFile(cmp, nf.Meta, start, end) { + return true + } + } + return false +} + +func anyOverlapFile(cmp base.Compare, f *manifest.FileMetadata, start, end key) bool { + if f == nil { + return true + } + if start != nil { + if v := cmp(f.Largest.UserKey, start); v < 0 { + return false + } else if f.Largest.IsExclusiveSentinel() && v == 0 { + return false + } + } + if end != nil && cmp(f.Smallest.UserKey, end) >= 0 { + return false + } + return true +} + +func (m *manifestT) runSummarize(cmd *cobra.Command, args []string) { + for _, arg := range args { + err := m.runSummarizeOne(cmd.OutOrStdout(), arg) + if err != nil { + fmt.Fprintf(cmd.OutOrStderr(), "%s\n", err) + } + } +} + +func (m *manifestT) runSummarizeOne(stdout io.Writer, arg string) error { + f, err := m.opts.FS.Open(arg) + if err != nil { + return err + } + defer f.Close() + fmt.Fprintf(stdout, "%s\n", arg) + + type summaryBucket struct { + bytesAdded [manifest.NumLevels]uint64 + bytesCompactOut [manifest.NumLevels]uint64 + } + var ( + bve manifest.BulkVersionEdit + newestOverall time.Time + oldestOverall time.Time // oldest after initial version edit + buckets = map[time.Time]*summaryBucket{} + metadatas = map[base.FileNum]*manifest.FileMetadata{} + ) + bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata) + rr := record.NewReader(f, 0 /* logNum */) + for i := 0; ; i++ { + r, err := rr.Next() + if err == io.EOF { + break + } else if err != nil { + return err + } + + var ve manifest.VersionEdit + err = ve.Decode(r) + if err != nil { + return err + } + if err := bve.Accumulate(&ve); err != nil { + return err + } + + veNewest, veOldest := newestOverall, newestOverall + for _, nf := range ve.NewFiles { + _, seen := metadatas[nf.Meta.FileNum] + metadatas[nf.Meta.FileNum] = nf.Meta + if nf.Meta.CreationTime == 0 { + continue + } + + t := time.Unix(nf.Meta.CreationTime, 0).UTC() + if veNewest.Before(t) { + veNewest = t + } + // Only update the oldest if we haven't already seen this + // file; it might've been moved in which case the sstable's + // creation time is from when it was originally created. + if veOldest.After(t) && !seen { + veOldest = t + } + } + // Ratchet up the most recent timestamp we've seen. + if newestOverall.Before(veNewest) { + newestOverall = veNewest + } + + if i == 0 || newestOverall.IsZero() { + continue + } + // Update oldestOverall once, when we encounter the first version edit + // at index >= 1. It should be approximately the start time of the + // manifest. + if !newestOverall.IsZero() && oldestOverall.IsZero() { + oldestOverall = newestOverall + } + + bucketKey := newestOverall.Truncate(m.summarizeDur) + b := buckets[bucketKey] + if b == nil { + b = &summaryBucket{} + buckets[bucketKey] = b + } + + // Increase `bytesAdded` for any version edits that only add files. + // These are either flushes or ingests. + if len(ve.NewFiles) > 0 && len(ve.DeletedFiles) == 0 { + for _, nf := range ve.NewFiles { + b.bytesAdded[nf.Level] += nf.Meta.Size + } + continue + } + + // Increase `bytesCompactOut` for the input level of any compactions + // that remove bytes from a level (excluding intra-L0 compactions). + // compactions. + destLevel := -1 + if len(ve.NewFiles) > 0 { + destLevel = ve.NewFiles[0].Level + } + for dfe := range ve.DeletedFiles { + if dfe.Level != destLevel { + b.bytesCompactOut[dfe.Level] += metadatas[dfe.FileNum].Size + } + } + } + + formatUint64 := func(v uint64, _ time.Duration) string { + if v == 0 { + return "." + } + return humanize.Bytes.Uint64(v).String() + } + formatRate := func(v uint64, dur time.Duration) string { + if v == 0 { + return "." + } + secs := dur.Seconds() + if secs == 0 { + secs = 1 + } + return humanize.Bytes.Uint64(uint64(float64(v)/secs)).String() + "/s" + } + + if newestOverall.IsZero() { + fmt.Fprintf(stdout, "(no timestamps)\n") + } else { + // NB: bt begins unaligned with the bucket duration (m.summarizeDur), + // but after the first bucket will always be aligned. + for bi, bt := 0, oldestOverall; !bt.After(newestOverall); bi, bt = bi+1, bt.Truncate(m.summarizeDur).Add(m.summarizeDur) { + // Truncate the start time to calculate the bucket key, and + // retrieve the appropriate bucket. + bk := bt.Truncate(m.summarizeDur) + var bucket summaryBucket + if buckets[bk] != nil { + bucket = *buckets[bk] + } + + if bi%10 == 0 { + fmt.Fprintf(stdout, " ") + fmt.Fprintf(stdout, "_______L0_______L1_______L2_______L3_______L4_______L5_______L6_____TOTAL\n") + } + fmt.Fprintf(stdout, "%s\n", bt.Format(time.RFC3339)) + + // Compute the bucket duration. It may < `m.summarizeDur` if this is + // the first or last bucket. + bucketEnd := bt.Truncate(m.summarizeDur).Add(m.summarizeDur) + if bucketEnd.After(newestOverall) { + bucketEnd = newestOverall + } + dur := bucketEnd.Sub(bt) + + stats := []struct { + label string + format func(uint64, time.Duration) string + vals [manifest.NumLevels]uint64 + }{ + {"Ingest+Flush", formatUint64, bucket.bytesAdded}, + {"Ingest+Flush", formatRate, bucket.bytesAdded}, + {"Compact (out)", formatUint64, bucket.bytesCompactOut}, + {"Compact (out)", formatRate, bucket.bytesCompactOut}, + } + for _, stat := range stats { + var sum uint64 + for _, v := range stat.vals { + sum += v + } + fmt.Fprintf(stdout, "%20s %8s %8s %8s %8s %8s %8s %8s %8s\n", + stat.label, + stat.format(stat.vals[0], dur), + stat.format(stat.vals[1], dur), + stat.format(stat.vals[2], dur), + stat.format(stat.vals[3], dur), + stat.format(stat.vals[4], dur), + stat.format(stat.vals[5], dur), + stat.format(stat.vals[6], dur), + stat.format(sum, dur)) + } + } + fmt.Fprintf(stdout, "%s\n", newestOverall.Format(time.RFC3339)) + } + + dur := newestOverall.Sub(oldestOverall) + fmt.Fprintf(stdout, "---\n") + fmt.Fprintf(stdout, "Estimated start time: %s\n", oldestOverall.Format(time.RFC3339)) + fmt.Fprintf(stdout, "Estimated end time: %s\n", newestOverall.Format(time.RFC3339)) + fmt.Fprintf(stdout, "Estimated duration: %s\n", dur.String()) + + return nil +} + +func (m *manifestT) runCheck(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + ok := true + for _, arg := range args { + func() { + f, err := m.opts.FS.Open(arg) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + ok = false + return + } + defer f.Close() + + var v *manifest.Version + var cmp *base.Comparer + rr := record.NewReader(f, 0 /* logNum */) + // Contains the FileMetadata needed by BulkVersionEdit.Apply. + // It accumulates the additions since later edits contain + // deletions of earlier added files. + addedByFileNum := make(map[base.FileNum]*manifest.FileMetadata) + for { + offset := rr.Offset() + r, err := rr.Next() + if err != nil { + if err == io.EOF { + break + } + fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", arg, offset, err) + ok = false + break + } + + var ve manifest.VersionEdit + err = ve.Decode(r) + if err != nil { + fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", arg, offset, err) + ok = false + break + } + var bve manifest.BulkVersionEdit + bve.AddedByFileNum = addedByFileNum + if err := bve.Accumulate(&ve); err != nil { + fmt.Fprintf(stderr, "%s\n", err) + ok = false + return + } + + empty := true + if ve.ComparerName != "" { + empty = false + cmp = m.comparers[ve.ComparerName] + if cmp == nil { + fmt.Fprintf(stdout, "%s: offset: %d comparer %s not found", + arg, offset, ve.ComparerName) + ok = false + break + } + m.fmtKey.setForComparer(ve.ComparerName, m.comparers) + } + empty = empty && ve.MinUnflushedLogNum == 0 && ve.ObsoletePrevLogNum == 0 && + ve.LastSeqNum == 0 && len(ve.DeletedFiles) == 0 && + len(ve.NewFiles) == 0 + if empty { + continue + } + // TODO(sbhola): add option to Apply that reports all errors instead of + // one error. + newv, err := bve.Apply(v, cmp.Compare, m.fmtKey.fn, 0, m.opts.Experimental.ReadCompactionRate, nil /* zombies */, manifest.AllowSplitUserKeys) + if err != nil { + fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", + arg, offset, err) + fmt.Fprintf(stdout, "Version state before failed Apply\n") + m.printLevels(cmp.Compare, stdout, v) + fmt.Fprintf(stdout, "Version edit that failed\n") + for df := range ve.DeletedFiles { + fmt.Fprintf(stdout, " deleted: L%d %s\n", df.Level, df.FileNum) + } + for _, nf := range ve.NewFiles { + fmt.Fprintf(stdout, " added: L%d %s:%d", + nf.Level, nf.Meta.FileNum, nf.Meta.Size) + formatSeqNumRange(stdout, nf.Meta.SmallestSeqNum, nf.Meta.LargestSeqNum) + formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest) + fmt.Fprintf(stdout, "\n") + } + ok = false + break + } + v = newv + } + }() + } + if ok { + fmt.Fprintf(stdout, "OK\n") + } +} diff --git a/pebble/tool/manifest_test.go b/pebble/tool/manifest_test.go new file mode 100644 index 0000000..26ebca3 --- /dev/null +++ b/pebble/tool/manifest_test.go @@ -0,0 +1,11 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import "testing" + +func TestManifest(t *testing.T) { + runTests(t, "testdata/manifest_*") +} diff --git a/pebble/tool/remotecat.go b/pebble/tool/remotecat.go new file mode 100644 index 0000000..01eabb9 --- /dev/null +++ b/pebble/tool/remotecat.go @@ -0,0 +1,132 @@ +// 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 tool + +import ( + "fmt" + "io" + "slices" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat" + "github.com/cockroachdb/pebble/record" + "github.com/spf13/cobra" +) + +// remoteCatalogT implements tools for the remote object catalog. +type remoteCatalogT struct { + Root *cobra.Command + Dump *cobra.Command + + verbose bool + opts *pebble.Options +} + +func newRemoteCatalog(opts *pebble.Options) *remoteCatalogT { + m := &remoteCatalogT{ + opts: opts, + } + + m.Root = &cobra.Command{ + Use: "remotecat", + Short: "remote object catalog introspection tools", + } + + // Add dump command + m.Dump = &cobra.Command{ + Use: "dump ", + Short: "print remote object catalog contents", + Long: ` +Print the contents of the REMOTE-OBJ-CATALOG files. +`, + Args: cobra.MinimumNArgs(1), + Run: m.runDump, + } + m.Dump.Flags().BoolVarP(&m.verbose, "verbose", "v", false, "show each record in the catalog") + m.Root.AddCommand(m.Dump) + + return m +} + +func (m *remoteCatalogT) runDump(cmd *cobra.Command, args []string) { + for _, arg := range args { + err := m.runDumpOne(cmd.OutOrStdout(), arg) + if err != nil { + fmt.Fprintf(cmd.OutOrStderr(), "%s\n", err) + } + } +} + +func (m *remoteCatalogT) runDumpOne(stdout io.Writer, filename string) error { + f, err := m.opts.FS.Open(filename) + if err != nil { + return err + } + + var creatorID objstorage.CreatorID + objects := make(map[base.DiskFileNum]remoteobjcat.RemoteObjectMetadata) + + fmt.Fprintf(stdout, "%s\n", filename) + var editIdx int + rr := record.NewReader(f, 0 /* logNum */) + for { + offset := rr.Offset() + r, err := rr.Next() + if err == io.EOF { + break + } else if err != nil { + return err + } + + var ve remoteobjcat.VersionEdit + err = ve.Decode(r) + if err != nil { + return err + } + + if m.verbose { + fmt.Fprintf(stdout, "%d/%d\n", offset, editIdx) + if ve.CreatorID.IsSet() { + fmt.Fprintf(stdout, " CreatorID: %s\n", ve.CreatorID) + } + if len(ve.NewObjects) > 0 { + fmt.Fprintf(stdout, " NewObjects:\n") + for _, m := range ve.NewObjects { + fmt.Fprintf( + stdout, " %s CreatorID: %s CreatorFileNum: %s Locator: %q CustomObjectName: %q\n", + m.FileNum, m.CreatorID, m.CreatorFileNum, m.Locator, m.CustomObjectName, + ) + } + } + if len(ve.DeletedObjects) > 0 { + fmt.Fprintf(stdout, " DeletedObjects:\n") + for _, n := range ve.DeletedObjects { + fmt.Fprintf(stdout, " %s\n", n) + } + } + } + editIdx++ + if err := ve.Apply(&creatorID, objects); err != nil { + return err + } + } + fmt.Fprintf(stdout, "CreatorID: %v\n", creatorID) + var filenums []base.DiskFileNum + for n := range objects { + filenums = append(filenums, n) + } + slices.Sort(filenums) + fmt.Fprintf(stdout, "Objects:\n") + for _, n := range filenums { + m := objects[n] + fmt.Fprintf( + stdout, " %s CreatorID: %s CreatorFileNum: %s Locator: %q CustomObjectName: %q\n", + n, m.CreatorID, m.CreatorFileNum, m.Locator, m.CustomObjectName, + ) + } + return nil +} diff --git a/pebble/tool/remotecat_test.go b/pebble/tool/remotecat_test.go new file mode 100644 index 0000000..378be35 --- /dev/null +++ b/pebble/tool/remotecat_test.go @@ -0,0 +1,11 @@ +// 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 tool + +import "testing" + +func TestRemotecat(t *testing.T) { + runTests(t, "testdata/remotecat") +} diff --git a/pebble/tool/sstable.go b/pebble/tool/sstable.go new file mode 100644 index 0000000..1e34877 --- /dev/null +++ b/pebble/tool/sstable.go @@ -0,0 +1,564 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "slices" + "text/tabwriter" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/private" + "github.com/cockroachdb/pebble/internal/rangedel" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/spf13/cobra" +) + +// sstableT implements sstable-level tools, including both configuration state +// and the commands themselves. +type sstableT struct { + Root *cobra.Command + Check *cobra.Command + Layout *cobra.Command + Properties *cobra.Command + Scan *cobra.Command + Space *cobra.Command + + // Configuration and state. + opts *pebble.Options + comparers sstable.Comparers + mergers sstable.Mergers + + // Flags. + fmtKey keyFormatter + fmtValue valueFormatter + start key + end key + filter key + count int64 + verbose bool +} + +func newSSTable( + opts *pebble.Options, comparers sstable.Comparers, mergers sstable.Mergers, +) *sstableT { + s := &sstableT{ + opts: opts, + comparers: comparers, + mergers: mergers, + } + s.fmtKey.mustSet("quoted") + s.fmtValue.mustSet("[%x]") + + s.Root = &cobra.Command{ + Use: "sstable", + Short: "sstable introspection tools", + } + s.Check = &cobra.Command{ + Use: "check ", + Short: "verify checksums and metadata", + Long: ``, + Args: cobra.MinimumNArgs(1), + Run: s.runCheck, + } + s.Layout = &cobra.Command{ + Use: "layout ", + Short: "print sstable block and record layout", + Long: ` +Print the layout for the sstables. The -v flag controls whether record layout +is displayed or omitted. +`, + Args: cobra.MinimumNArgs(1), + Run: s.runLayout, + } + s.Properties = &cobra.Command{ + Use: "properties ", + Short: "print sstable properties", + Long: ` +Print the properties for the sstables. The -v flag controls whether the +properties are pretty-printed or displayed in a verbose/raw format. +`, + Args: cobra.MinimumNArgs(1), + Run: s.runProperties, + } + s.Scan = &cobra.Command{ + Use: "scan ", + Short: "print sstable records", + Long: ` +Print the records in the sstables. The sstables are scanned in command line +order which means the records will be printed in that order. Raw range +tombstones are displayed interleaved with point records. +`, + Args: cobra.MinimumNArgs(1), + Run: s.runScan, + } + s.Space = &cobra.Command{ + Use: "space ", + Short: "print filesystem space used", + Long: ` +Print the estimated space usage in the specified files for the +inclusive-inclusive range specified by --start and --end. +`, + Args: cobra.MinimumNArgs(1), + Run: s.runSpace, + } + + s.Root.AddCommand(s.Check, s.Layout, s.Properties, s.Scan, s.Space) + s.Root.PersistentFlags().BoolVarP(&s.verbose, "verbose", "v", false, "verbose output") + + s.Check.Flags().Var( + &s.fmtKey, "key", "key formatter") + s.Layout.Flags().Var( + &s.fmtKey, "key", "key formatter") + s.Layout.Flags().Var( + &s.fmtValue, "value", "value formatter") + s.Scan.Flags().Var( + &s.fmtKey, "key", "key formatter") + s.Scan.Flags().Var( + &s.fmtValue, "value", "value formatter") + for _, cmd := range []*cobra.Command{s.Scan, s.Space} { + cmd.Flags().Var( + &s.start, "start", "start key for the range") + cmd.Flags().Var( + &s.end, "end", "end key for the range") + } + s.Scan.Flags().Var( + &s.filter, "filter", "only output records with matching prefix or overlapping range tombstones") + s.Scan.Flags().Int64Var( + &s.count, "count", 0, "key count for scan (0 is unlimited)") + + return s +} + +func (s *sstableT) newReader(f vfs.File) (*sstable.Reader, error) { + readable, err := sstable.NewSimpleReadable(f) + if err != nil { + return nil, err + } + o := sstable.ReaderOptions{ + Cache: pebble.NewCache(128 << 20 /* 128 MB */), + Comparer: s.opts.Comparer, + Filters: s.opts.Filters, + } + defer o.Cache.Unref() + return sstable.NewReader(readable, o, s.comparers, s.mergers, + private.SSTableRawTombstonesOpt.(sstable.ReaderOption)) +} + +func (s *sstableT) runCheck(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + s.foreachSstable(stderr, args, func(arg string) { + f, err := s.opts.FS.Open(arg) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + + fmt.Fprintf(stdout, "%s\n", arg) + + r, err := s.newReader(f) + + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + defer r.Close() + + // Update the internal formatter if this comparator has one specified. + s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers) + s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers) + + iter, err := r.NewIter(nil, nil) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + + // If a split function is defined for the comparer, verify that + // SeekPrefixGE can find every key in the table. + var prefixIter sstable.Iterator + if r.Split != nil { + var err error + prefixIter, err = r.NewIter(nil, nil) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + } + + var lastKey base.InternalKey + for key, _ := iter.First(); key != nil; key, _ = iter.Next() { + if base.InternalCompare(r.Compare, lastKey, *key) >= 0 { + fmt.Fprintf(stdout, "WARNING: OUT OF ORDER KEYS!\n") + if s.fmtKey.spec != "null" { + fmt.Fprintf(stdout, " %s >= %s\n", + lastKey.Pretty(s.fmtKey.fn), key.Pretty(s.fmtKey.fn)) + } + } + lastKey.Trailer = key.Trailer + lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...) + + if prefixIter != nil { + n := r.Split(key.UserKey) + prefix := key.UserKey[:n] + key2, _ := prefixIter.SeekPrefixGE(prefix, key.UserKey, base.SeekGEFlagsNone) + if key2 == nil { + fmt.Fprintf(stdout, "WARNING: PREFIX ITERATION FAILURE!\n") + if s.fmtKey.spec != "null" { + fmt.Fprintf(stdout, " %s not found\n", key.Pretty(s.fmtKey.fn)) + } + } + } + } + + if err := iter.Close(); err != nil { + fmt.Fprintf(stdout, "%s\n", err) + } + if prefixIter != nil { + if err := prefixIter.Close(); err != nil { + fmt.Fprintf(stdout, "%s\n", err) + } + } + }) +} + +func (s *sstableT) runLayout(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + s.foreachSstable(stderr, args, func(arg string) { + f, err := s.opts.FS.Open(arg) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + + fmt.Fprintf(stdout, "%s\n", arg) + + r, err := s.newReader(f) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + defer r.Close() + + // Update the internal formatter if this comparator has one specified. + s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers) + s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers) + + l, err := r.Layout() + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + fmtRecord := func(key *base.InternalKey, value []byte) { + formatKeyValue(stdout, s.fmtKey, s.fmtValue, key, value) + } + if s.fmtKey.spec == "null" && s.fmtValue.spec == "null" { + fmtRecord = nil + } + l.Describe(stdout, s.verbose, r, fmtRecord) + }) +} + +func (s *sstableT) runProperties(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + s.foreachSstable(stderr, args, func(arg string) { + f, err := s.opts.FS.Open(arg) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + + fmt.Fprintf(stdout, "%s\n", arg) + + r, err := s.newReader(f) + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + return + } + defer r.Close() + + if s.verbose { + fmt.Fprintf(stdout, "%s", r.Properties.String()) + return + } + + stat, err := f.Stat() + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + + formatNull := func(s string) string { + switch s { + case "", "nullptr": + return "-" + } + return s + } + + tw := tabwriter.NewWriter(stdout, 2, 1, 2, ' ', 0) + fmt.Fprintf(tw, "size\t\n") + fmt.Fprintf(tw, " file\t%s\n", humanize.Bytes.Int64(stat.Size())) + fmt.Fprintf(tw, " data\t%s\n", humanize.Bytes.Uint64(r.Properties.DataSize)) + fmt.Fprintf(tw, " blocks\t%d\n", r.Properties.NumDataBlocks) + fmt.Fprintf(tw, " index\t%s\n", humanize.Bytes.Uint64(r.Properties.IndexSize)) + fmt.Fprintf(tw, " blocks\t%d\n", 1+r.Properties.IndexPartitions) + fmt.Fprintf(tw, " top-level\t%s\n", humanize.Bytes.Uint64(r.Properties.TopLevelIndexSize)) + fmt.Fprintf(tw, " filter\t%s\n", humanize.Bytes.Uint64(r.Properties.FilterSize)) + fmt.Fprintf(tw, " raw-key\t%s\n", humanize.Bytes.Uint64(r.Properties.RawKeySize)) + fmt.Fprintf(tw, " raw-value\t%s\n", humanize.Bytes.Uint64(r.Properties.RawValueSize)) + fmt.Fprintf(tw, " pinned-key\t%d\n", r.Properties.SnapshotPinnedKeySize) + fmt.Fprintf(tw, " pinned-val\t%d\n", r.Properties.SnapshotPinnedValueSize) + fmt.Fprintf(tw, " point-del-key-size\t%d\n", r.Properties.RawPointTombstoneKeySize) + fmt.Fprintf(tw, " point-del-value-size\t%d\n", r.Properties.RawPointTombstoneValueSize) + fmt.Fprintf(tw, "records\t%d\n", r.Properties.NumEntries) + fmt.Fprintf(tw, " set\t%d\n", r.Properties.NumEntries- + (r.Properties.NumDeletions+r.Properties.NumMergeOperands)) + fmt.Fprintf(tw, " delete\t%d\n", r.Properties.NumPointDeletions()) + fmt.Fprintf(tw, " delete-sized\t%d\n", r.Properties.NumSizedDeletions) + fmt.Fprintf(tw, " range-delete\t%d\n", r.Properties.NumRangeDeletions) + fmt.Fprintf(tw, " range-key-set\t%d\n", r.Properties.NumRangeKeySets) + fmt.Fprintf(tw, " range-key-unset\t%d\n", r.Properties.NumRangeKeyUnsets) + fmt.Fprintf(tw, " range-key-delete\t%d\n", r.Properties.NumRangeKeyDels) + fmt.Fprintf(tw, " merge\t%d\n", r.Properties.NumMergeOperands) + fmt.Fprintf(tw, " global-seq-num\t%d\n", r.Properties.GlobalSeqNum) + fmt.Fprintf(tw, " pinned\t%d\n", r.Properties.SnapshotPinnedKeys) + fmt.Fprintf(tw, "index\t\n") + fmt.Fprintf(tw, " key\t") + fmt.Fprintf(tw, " value\t") + fmt.Fprintf(tw, "comparer\t%s\n", r.Properties.ComparerName) + fmt.Fprintf(tw, "merger\t%s\n", formatNull(r.Properties.MergerName)) + fmt.Fprintf(tw, "filter\t%s\n", formatNull(r.Properties.FilterPolicyName)) + fmt.Fprintf(tw, " prefix\t%t\n", r.Properties.PrefixFiltering) + fmt.Fprintf(tw, " whole-key\t%t\n", r.Properties.WholeKeyFiltering) + fmt.Fprintf(tw, "compression\t%s\n", r.Properties.CompressionName) + fmt.Fprintf(tw, " options\t%s\n", r.Properties.CompressionOptions) + fmt.Fprintf(tw, "user properties\t\n") + fmt.Fprintf(tw, " collectors\t%s\n", r.Properties.PropertyCollectorNames) + keys := make([]string, 0, len(r.Properties.UserProperties)) + for key := range r.Properties.UserProperties { + keys = append(keys, key) + } + slices.Sort(keys) + for _, key := range keys { + fmt.Fprintf(tw, " %s\t%s\n", key, r.Properties.UserProperties[key]) + } + tw.Flush() + }) +} + +func (s *sstableT) runScan(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + s.foreachSstable(stderr, args, func(arg string) { + f, err := s.opts.FS.Open(arg) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + + // In filter-mode, we prefix ever line that is output with the sstable + // filename. + var prefix string + if s.filter == nil { + fmt.Fprintf(stdout, "%s\n", arg) + } else { + prefix = fmt.Sprintf("%s: ", arg) + } + + r, err := s.newReader(f) + if err != nil { + fmt.Fprintf(stdout, "%s%s\n", prefix, err) + return + } + defer r.Close() + + // Update the internal formatter if this comparator has one specified. + s.fmtKey.setForComparer(r.Properties.ComparerName, s.comparers) + s.fmtValue.setForComparer(r.Properties.ComparerName, s.comparers) + + iter, err := r.NewIter(nil, s.end) + if err != nil { + fmt.Fprintf(stderr, "%s%s\n", prefix, err) + return + } + defer iter.Close() + key, value := iter.SeekGE(s.start, base.SeekGEFlagsNone) + + // We configured sstable.Reader to return raw tombstones which requires a + // bit more work here to put them in a form that can be iterated in + // parallel with the point records. + rangeDelIter, err := func() (keyspan.FragmentIterator, error) { + iter, err := r.NewRawRangeDelIter() + if err != nil { + return nil, err + } + if iter == nil { + return keyspan.NewIter(r.Compare, nil), nil + } + defer iter.Close() + + var tombstones []keyspan.Span + for t := iter.First(); t != nil; t = iter.Next() { + if s.end != nil && r.Compare(s.end, t.Start) <= 0 { + // The range tombstone lies after the scan range. + continue + } + if r.Compare(s.start, t.End) >= 0 { + // The range tombstone lies before the scan range. + continue + } + tombstones = append(tombstones, t.ShallowClone()) + } + + slices.SortFunc(tombstones, func(a, b keyspan.Span) int { + return r.Compare(a.Start, b.Start) + }) + return keyspan.NewIter(r.Compare, tombstones), nil + }() + if err != nil { + fmt.Fprintf(stdout, "%s%s\n", prefix, err) + return + } + + defer rangeDelIter.Close() + rangeDel := rangeDelIter.First() + count := s.count + + var lastKey base.InternalKey + for key != nil || rangeDel != nil { + if key != nil && (rangeDel == nil || r.Compare(key.UserKey, rangeDel.Start) < 0) { + // The filter specifies a prefix of the key. + // + // TODO(peter): Is using prefix comparison like this kosher for all + // comparers? Probably not, but it is for common ones such as the + // Pebble default and CockroachDB's comparer. + if s.filter == nil || bytes.HasPrefix(key.UserKey, s.filter) { + fmt.Fprint(stdout, prefix) + v, _, err := value.Value(nil) + if err != nil { + fmt.Fprintf(stdout, "%s%s\n", prefix, err) + return + } + formatKeyValue(stdout, s.fmtKey, s.fmtValue, key, v) + + } + if base.InternalCompare(r.Compare, lastKey, *key) >= 0 { + fmt.Fprintf(stdout, "%s WARNING: OUT OF ORDER KEYS!\n", prefix) + } + lastKey.Trailer = key.Trailer + lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...) + key, value = iter.Next() + } else { + // If a filter is specified, we want to output any range tombstone + // which overlaps the prefix. The comparison on the start key is + // somewhat complex. Consider the tombstone [aaa,ccc). We want to + // output this tombstone if filter is "aa", and if it "bbb". + if s.filter == nil || + ((r.Compare(s.filter, rangeDel.Start) >= 0 || + bytes.HasPrefix(rangeDel.Start, s.filter)) && + r.Compare(s.filter, rangeDel.End) < 0) { + fmt.Fprint(stdout, prefix) + if err := rangedel.Encode(rangeDel, func(k base.InternalKey, v []byte) error { + formatKeyValue(stdout, s.fmtKey, s.fmtValue, &k, v) + return nil + }); err != nil { + fmt.Fprintf(stdout, "%s\n", err) + os.Exit(1) + } + } + rangeDel = rangeDelIter.Next() + } + + if count > 0 { + count-- + if count == 0 { + break + } + } + } + + // Handle range keys. + rkIter, err := r.NewRawRangeKeyIter() + if err != nil { + fmt.Fprintf(stdout, "%s\n", err) + os.Exit(1) + } + if rkIter != nil { + defer rkIter.Close() + for span := rkIter.SeekGE(s.start); span != nil; span = rkIter.Next() { + // By default, emit the key, unless there is a filter. + emit := s.filter == nil + // Skip spans that start after the end key (if provided). End keys are + // exclusive, e.g. [a, b), so we consider the interval [b, +inf). + if s.end != nil && r.Compare(span.Start, s.end) >= 0 { + emit = false + } + // Filters override the provided start / end bounds, if provided. + if s.filter != nil && bytes.HasPrefix(span.Start, s.filter) { + // In filter mode, each line is prefixed with the filename. + fmt.Fprint(stdout, prefix) + emit = true + } + if emit { + formatSpan(stdout, s.fmtKey, s.fmtValue, span) + } + } + } + + if err := iter.Close(); err != nil { + fmt.Fprintf(stdout, "%s\n", err) + } + }) +} + +func (s *sstableT) runSpace(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + s.foreachSstable(stderr, args, func(arg string) { + f, err := s.opts.FS.Open(arg) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + r, err := s.newReader(f) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer r.Close() + + bytes, err := r.EstimateDiskUsage(s.start, s.end) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + fmt.Fprintf(stdout, "%s: %d\n", arg, bytes) + }) +} + +func (s *sstableT) foreachSstable(stderr io.Writer, args []string, fn func(arg string)) { + // Loop over args, invoking fn for each file. Each directory is recursively + // listed and fn is invoked on any file with an .sst or .ldb suffix. + for _, arg := range args { + info, err := s.opts.FS.Stat(arg) + if err != nil || !info.IsDir() { + fn(arg) + continue + } + walk(stderr, s.opts.FS, arg, func(path string) { + switch filepath.Ext(path) { + case ".sst", ".ldb": + fn(path) + } + }) + } +} diff --git a/pebble/tool/sstable_test.go b/pebble/tool/sstable_test.go new file mode 100644 index 0000000..3b56e35 --- /dev/null +++ b/pebble/tool/sstable_test.go @@ -0,0 +1,11 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import "testing" + +func TestSSTable(t *testing.T) { + runTests(t, "testdata/sstable_*") +} diff --git a/pebble/tool/testdata/MANIFEST-invalid b/pebble/tool/testdata/MANIFEST-invalid new file mode 100644 index 0000000000000000000000000000000000000000..36ce72ad1400f0d02d4e6baadc5f504d9adb269c GIT binary patch literal 130 zcmd=8V;N_~z{n_-lUkOVlai$8R9TW*o>`pgoS$2eSd>_jU&O@3%*rB?!p6wJ!2ktt W1{3R^WuNyGGJqLk023Ai7+C;Gl^c%$ literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/REMOTE-OBJ-CATALOG b/pebble/tool/testdata/REMOTE-OBJ-CATALOG new file mode 100644 index 0000000000000000000000000000000000000000..59786d539de322b9ae1d2c9c0516ee98045671e6 GIT binary patch literal 82 zcmZS8)^KKEVEnT_telB~k(pWRxlIla10xVHGcvF+r{(7}{Cr~1t^pKcVr1ol3V|dU afb68iB37Q%ijvf#yu=*6;^GnpCPn~zKM)`Q literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/bad-magic.sst b/pebble/tool/testdata/bad-magic.sst new file mode 100644 index 0000000000000000000000000000000000000000..dcb82095cade7a37734034920f68e6792645b54e GIT binary patch literal 986 zcmaKqziSjh6vyA*uW($Ii^+uqLBUFKjOlCwY9TfOv5+7lv$t>WhRn_^GqZQM=>rlF zf`5XASfsPCw-L0k41$&5e;|S&=$l<{ZzGt6&oaEhrM2)Tha6kns;J_wrS*^Lb% z^jfjjL8~;yIy3$h_}^|9-@}!b(ZhWkg)EeGU?H~{F~>uROsEKF%;hQiaJU-$KQuKM ziy4cBbZCScZNc?(qNig`<@YgPB;17cDq*N)mM9kUM97?p2w-Dr-ukRh@dP7`c_tn7 zHx@BT$_;ngNSvUg-R|TLr^4c0J@>#$ryg$5TA|fL#nUv0)pq4G$Q?PD6A2!^`S`gF z%hj9_cbr*qfIDzuVMK|JB&6+Z04tTfl%@!W+ODKp8O$WAksAZ7EmE{exMLGEel~!9 zzS)-tq= z(7NIdy);d@ST+0m7I5YaA5uH!Z)x6hvoV)8HGWc;GrCfVKF~gXQWMhfX;I#9dAYWw zw@ZQ>sBl$a1Nh!f%+@rC$B3>pB8xKE_SG4Yxh zPz?yxgB#Fq{xdeovGcOy{ge-I+IzGI-NtD7clp7s%H^JKod(}JmyVX!yzTstn!=Ay L-w)s3{`%|>nXD(X literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/corrupt-options-db/OPTIONS-000002 b/pebble/tool/testdata/corrupt-options-db/OPTIONS-000002 new file mode 100644 index 0000000..bd4a478 --- /dev/null +++ b/pebble/tool/testdata/corrupt-options-db/OPTIONS-000002 @@ -0,0 +1 @@ +blargle diff --git a/pebble/tool/testdata/corrupted.sst b/pebble/tool/testdata/corrupted.sst new file mode 100644 index 0000000000000000000000000000000000000000..1dffb44aad9d3434b60e3a4182ec869af1fff386 GIT binary patch literal 986 zcmaKqziSjh6vyA*uW($Ii^+uqLBUFKjOlCwY9TfOv5+7lv$t>WhRn_^GqZQM=>rlF zf`5XASfsPCw-L0k41$&5e;|k;=$l<{ZzGt6&oaEiIpAmj$xP<)MU`ygnRXE!#8 z&}+q72d&Z+>&*C5;D5Vad=FPzMi2LG6tYm#frZ>+#2gPLGNB@zF_)+4!{KW1|IpN6 zEM_bg(xDM*v<27CiJp!%mEXsFk#G~%tAwGJS)y3X6CraZB7lvhdF!)2#S@G$=9zTR z-&n*XDL33{BXNR~cDs{1oC=F~_1pt1oqD)IYlT)16;IO~R@;@&Aa~?oP9%8v=Hur! zELU?z+;L{b0q($sg%Kq>l90Bu0jyN^Qko(hYP*tZWiXSdMs5tSwn)(?;f_tv_}KvZ zm8ZNJ-+HqapP(5b(zomK4&Bk2X)c8qV&ge=d`at{mNli$@r$u?c<>lIz z-ag?n!$Qlhf0@^aJ>rOXMSLKB607viw~2eiQ({8AAWn#P#24ZhF=zlV;y#fQ$HZ%5 zKs6v#4{kue`OnxS$Ii=+_ftN=Y46b^S1LpY6?F- LeLsAA`|GnmosK89 literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/db_check b/pebble/tool/testdata/db_check new file mode 100644 index 0000000..7cdb409 --- /dev/null +++ b/pebble/tool/testdata/db_check @@ -0,0 +1,44 @@ +db check +---- +accepts 1 arg(s), received 0 + +db check +non-existent +---- +error opening database at "non-existent": pebble: database "non-existent" does not exist + +db check +./testdata/corrupt-options-db +---- +error loading options: invalid key=value syntax: "blargle" +Custom message in case of corruption error. + + +db check +../testdata/db-stage-4 +---- +checked 6 points and 0 tombstone + +db check +../testdata/db-stage-4 +--comparer=foo +---- +unknown comparer "foo" + +db check +../testdata/db-stage-4 +--comparer=test-comparer +---- +pebble: manifest file "MANIFEST-000006" for DB "db-stage-4": comparer name from file "leveldb.BytewiseComparator" != comparer name from Options "test-comparer" + +db check +../testdata/db-stage-4 +--merger=foo +---- +unknown merger "foo" + +db check +../testdata/db-stage-4 +--merger=test-merger +---- +pebble: merger name from file "pebble.concatenate" != merger name from options "test-merger" diff --git a/pebble/tool/testdata/db_checkpoint b/pebble/tool/testdata/db_checkpoint new file mode 100644 index 0000000..0641605 --- /dev/null +++ b/pebble/tool/testdata/db_checkpoint @@ -0,0 +1,18 @@ +db checkpoint +---- +accepts 2 arg(s), received 0 + +db checkpoint +non-existent +---- +accepts 2 arg(s), received 1 + +db checkpoint +../testdata/db-stage-4 +../testdata/db-checkpoint1 +---- + +db check +../testdata/db-checkpoint1 +---- +checked 6 points and 0 tombstone diff --git a/pebble/tool/testdata/db_get_set b/pebble/tool/testdata/db_get_set new file mode 100644 index 0000000..72b4fef --- /dev/null +++ b/pebble/tool/testdata/db_get_set @@ -0,0 +1,58 @@ +db check ../testdata/db-stage-4 +---- +checked 6 points and 0 tombstone + +db get +../testdata/db-stage-4 +---- +accepts 2 arg(s), received 1 + +db get +../testdata/db-stage-4 +key1 +---- +pebble: not found + +db set +../testdata/db-stage-4 +key1 +value1 +---- + +db get +../testdata/db-stage-4 +key1 +---- +[76616c756531] + +db check ../testdata/db-stage-4 +---- +checked 7 points and 0 tombstone + +# 0x6b657941 = "key1", so this test case verifies that +# hex decoding works too. + +db get +../testdata/db-stage-4 +hex:6b657931 +---- +[76616c756531] + +db set +../testdata/db-stage-4 +hex:6b657931 +hex:1b1b1b +---- + +db get +../testdata/db-stage-4 +hex:6b657931 +---- +[1b1b1b] + +db get +../testdata/db-stage-4 +hex:6b657931 +--value=quoted +---- +\x1b\x1b\x1b diff --git a/pebble/tool/testdata/db_lsm b/pebble/tool/testdata/db_lsm new file mode 100644 index 0000000..1967c7f --- /dev/null +++ b/pebble/tool/testdata/db_lsm @@ -0,0 +1,39 @@ +db lsm +---- +accepts 1 arg(s), received 0 + +db lsm +non-existent +---- +error opening database at "non-existent": pebble: database "non-existent" does not exist + +db lsm +../testdata/db-stage-4 +---- + | | | | ingested | moved | written | | amp +level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- + 0 | 1 709B 0B 0 | 0.50 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 1 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 2 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 3 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 4 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 5 | 0 0B 0B 0 | 0.00 | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 + 6 | 0 0B 0B 0 | - | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 +total | 1 709B 0B 0 | - | 0B | 0 0B | 0 0B | 0 0B | 0B | 0 0.0 +------------------------------------------------------------------------------------------------------------------- +WAL: 1 files (0B) in: 0B written: 0B (0% overhead) +Flushes: 0 +Compactions: 0 estimated debt: 0B in progress: 0 (0B) + default: 0 delete: 0 elision: 0 move: 0 read: 0 rewrite: 0 multi-level: 0 +MemTables: 1 (256KB) zombie: 0 (0B) +Zombie tables: 0 (0B) +Backing tables: 0 (0B) +Virtual tables: 0 (0B) +Block cache: 0 entries (0B) hit rate: 0.0% +Table cache: 0 entries (0B) hit rate: 0.0% +Secondary cache: 0 entries (0B) hit rate: 0.0% +Snapshots: 0 earliest seq num: 0 +Table iters: 0 +Filter utility: 0.0% +Ingestions: 0 as flushable: 0 (0B in 0 tables) diff --git a/pebble/tool/testdata/db_properties b/pebble/tool/testdata/db_properties new file mode 100644 index 0000000..1bb3a3b --- /dev/null +++ b/pebble/tool/testdata/db_properties @@ -0,0 +1,72 @@ +db properties +---- +accepts 1 arg(s), received 0 + +db properties +non-existent +---- +open non-existent/: file does not exist + +db properties +../testdata/db-stage-4 +---- + L0 L1 L2 L3 L4 L5 L6 TOTAL +count 1 0 0 0 0 0 0 1 +seq num + smallest 12 0 0 0 0 0 0 12 + largest 14 0 0 0 0 0 0 14 +size + data 62B 0B 0B 0B 0B 0B 0B 62B + blocks 1 0 0 0 0 0 0 1 + index 27B 0B 0B 0B 0B 0B 0B 27B + blocks 1 0 0 0 0 0 0 1 + top-level 0B 0B 0B 0B 0B 0B 0B 0B + filter 0B 0B 0B 0B 0B 0B 0B 0B + raw-key 33B 0B 0B 0B 0B 0B 0B 33B + raw-value 9B 0B 0B 0B 0B 0B 0B 9B + pinned-key 0B 0B 0B 0B 0B 0B 0B 0B + pinned-value 0B 0B 0B 0B 0B 0B 0B 0B + point-del-key-size 3B 0B 0B 0B 0B 0B 0B 3B + point-del-value-size 0B 0B 0B 0B 0B 0B 0B 0B +records + set 2 0 0 0 0 0 0 2 + delete 1 0 0 0 0 0 0 1 + delete-sized 0 0 0 0 0 0 0 0 + range-delete 0 0 0 0 0 0 0 0 + range-key-sets 0 0 0 0 0 0 0 0 + range-key-unsets 0 0 0 0 0 0 0 0 + range-key-deletes 0 0 0 0 0 0 0 0 + merge 0 0 0 0 0 0 0 0 + pinned 0 0 0 0 0 0 0 0 + +db properties +./testdata/mixed +---- + L0 L1 L2 L3 L4 L5 L6 TOTAL +count 1 0 0 0 0 0 0 1 +seq num + smallest 1 0 0 0 0 0 0 1 + largest 29 0 0 0 0 0 0 29 +size + data 236B 0B 0B 0B 0B 0B 0B 236B + blocks 1 0 0 0 0 0 0 1 + index 29B 0B 0B 0B 0B 0B 0B 29B + blocks 1 0 0 0 0 0 0 1 + top-level 0B 0B 0B 0B 0B 0B 0B 0B + filter 0B 0B 0B 0B 0B 0B 0B 0B + raw-key 286B 0B 0B 0B 0B 0B 0B 286B + raw-value 0B 0B 0B 0B 0B 0B 0B 0B + pinned-key 0B 0B 0B 0B 0B 0B 0B 0B + pinned-value 0B 0B 0B 0B 0B 0B 0B 0B + point-del-key-size 0B 0B 0B 0B 0B 0B 0B 0B + point-del-value-size 0B 0B 0B 0B 0B 0B 0B 0B +records + set 26 0 0 0 0 0 0 26 + delete 0 0 0 0 0 0 0 0 + delete-sized 0 0 0 0 0 0 0 0 + range-delete 0 0 0 0 0 0 0 0 + range-key-sets 1 0 0 0 0 0 0 1 + range-key-unsets 1 0 0 0 0 0 0 1 + range-key-deletes 1 0 0 0 0 0 0 1 + merge 0 0 0 0 0 0 0 0 + pinned 0 0 0 0 0 0 0 0 diff --git a/pebble/tool/testdata/db_scan b/pebble/tool/testdata/db_scan new file mode 100644 index 0000000..fb17ff8 --- /dev/null +++ b/pebble/tool/testdata/db_scan @@ -0,0 +1,88 @@ +db scan +---- +accepts 1 arg(s), received 0 + +db scan +non-existent +---- +error opening database at "non-existent": pebble: database "non-existent" does not exist + +db scan +./testdata/corrupt-options-db +---- +error loading options: invalid key=value syntax: "blargle" +Custom message in case of corruption error. + +db scan +../testdata/db-stage-4 +---- +foo [66697665] +quux [736978] +scanned 2 records in 1.0s + +db scan +../testdata/db-stage-4 +--comparer=foo +---- +unknown comparer "foo" + +db scan +../testdata/db-stage-4 +--comparer=test-comparer +---- +pebble: manifest file "MANIFEST-000006" for DB "db-stage-4": comparer name from file "leveldb.BytewiseComparator" != comparer name from Options "test-comparer" + +db scan +../testdata/db-stage-4 +--merger=foo +---- +unknown merger "foo" + +db scan +../testdata/db-stage-4 +--merger=test-merger +---- +pebble: merger name from file "pebble.concatenate" != merger name from options "test-merger" + +db scan +../testdata/db-stage-4 +--key=%x +--value=size +---- +666f6f <4> +71757578 <3> +scanned 2 records in 1.0s + +db scan +../testdata/db-stage-4 +--key=%x +--value=null +--start=quux +---- +71757578 +scanned 1 record in 1.0s + +db scan +../testdata/db-stage-4 +--key=null +--value=size +--end=quux +---- +<4> +scanned 1 record in 1.0s + +db scan +../testdata/db-stage-4 +--key=null +--value=null +---- +scanned 2 records in 1.0s + + +db scan +../testdata/db-stage-4 +--key=null +--value=null +--count=1 +---- +scanned 1 record in 1.0s diff --git a/pebble/tool/testdata/db_space b/pebble/tool/testdata/db_space new file mode 100644 index 0000000..e073b34 --- /dev/null +++ b/pebble/tool/testdata/db_space @@ -0,0 +1,38 @@ +db space +---- +accepts 1 arg(s), received 0 + +# covers the whole 4.sst + +db space --start=a --end=z +../testdata/db-stage-4 +---- +709 + +# covers from left of 4.sst to its only data block + +db space --start=a --end=bar +../testdata/db-stage-4 +---- +62 + +# covers from 4.sst's only data block to its right + +db space --start=foo --end=z +../testdata/db-stage-4 +---- +62 + +# covers non-overlapping range to left of 4.sst + +db space --start=a --end=a +../testdata/db-stage-4 +---- +0 + +# covers non-overlapping range to right of 4.sst + +db space --start=z --end=z +../testdata/db-stage-4 +---- +0 diff --git a/pebble/tool/testdata/find b/pebble/tool/testdata/find new file mode 100644 index 0000000..c18bc35 --- /dev/null +++ b/pebble/tool/testdata/find @@ -0,0 +1,140 @@ +find +---- +accepts 2 arg(s), received 0 + +find +non-existent +key +---- +stat non-existent: file does not exist + +find +testdata/find-db +aaa +---- +000002.log + aaa#1,SET [31] +000004.log + aaa#8,DEL [] +000005.sst [aaa#1,SET-ccc#5,MERGE] + (flushed to L0, moved to L6) + aaa#1,SET [31] +000008.sst [aaa#0,SET-ccc#0,MERGE] + (compacted L0 [...] + L6 [000005]) + aaa#0,SET [31] +000010.sst [aaa#8,DEL-eee#inf,RANGEDEL] + (flushed to L0) + aaa#8,DEL [] +000011.sst [aaa#8,DEL-eee#inf,RANGEDEL] + (compacted L0 [000010] + L6 [000008 ...]) + aaa#8,DEL [] + aaa#0,SET [31] + +find +testdata/find-db +bbb +--key=%x +--value=pretty:test-comparer +---- +000002.log + 626262#2,SET test value formatter: 2 +000004.log + 626262-656565#10,RANGEDEL +000005.sst [616161#1,SET-636363#5,MERGE] + (flushed to L0, moved to L6) + 626262#2,SET test value formatter: 2 +000006.sst [626262#6,SET-636363#6,SET] + (ingested to L0) + 626262#6,SET test value formatter: 22 +000008.sst [616161#0,SET-636363#0,MERGE] + (compacted L0 [000006] + L6 [000005]) + 626262#6,SET test value formatter: 22 + 626262#0,SET test value formatter: 2 +000010.sst [616161#8,DEL-656565#inf,RANGEDEL] + (flushed to L0) + 626262-656565#10,RANGEDEL +000011.sst [616161#8,DEL-656565#inf,RANGEDEL] + (compacted L0 [000010] + L6 [000008 ...]) + 626262-656565#10,RANGEDEL + 626262#6,SET test value formatter: 22 + 626262#0,SET test value formatter: 2 + +find +testdata/find-db +hex:636363 +--value=null +---- +000002.log + ccc#3,MERGE + ccc#4,MERGE + ccc#5,MERGE +000004.log + ccc#9,SINGLEDEL + bbb-eee#10,RANGEDEL +000005.sst [aaa#1,SET-ccc#5,MERGE] + (flushed to L0, moved to L6) + ccc#5,MERGE +000006.sst [bbb#6,SET-ccc#6,SET] + (ingested to L0) + ccc#6,SET +000008.sst [aaa#0,SET-ccc#0,MERGE] + (compacted L0 [000006] + L6 [000005]) + ccc#6,SET + ccc#0,MERGE +000010.sst [aaa#8,DEL-eee#inf,RANGEDEL] + (flushed to L0) + bbb-eee#10,RANGEDEL +000011.sst [aaa#8,DEL-eee#inf,RANGEDEL] + (compacted L0 [000010] + L6 [000008 ...]) + bbb-eee#10,RANGEDEL + ccc#6,SET + ccc#0,MERGE + +find +testdata/find-db +ddd +-v +---- +find-db + 1 manifest + 3 logs + 6 sstables +find-db/MANIFEST-000001 + 10 edits +find-db/archive/000002.log +find-db/archive/000004.log +find-db/000009.log +find-db/archive/000005.sst +find-db/archive/000006.sst: global seqnum: 6 +find-db/archive/000007.sst: global seqnum: 7 +find-db/archive/000008.sst +find-db/archive/000010.sst +find-db/archive/000011.sst +000004.log + bbb-eee#10,RANGEDEL +000007.sst [ddd#7,SET-ddd#7,SET] + (ingested to L6) + ddd#7,SET [3333] +000010.sst [aaa#8,DEL-eee#inf,RANGEDEL] + (flushed to L0) + bbb-eee#10,RANGEDEL +000011.sst [aaa#8,DEL-eee#inf,RANGEDEL] + (compacted L0 [000010] + L6 [000007 ...]) + bbb-eee#10,RANGEDEL + ddd#7,SET [3333] + +find +testdata/find-db +eee +---- +000004.log + bbb-eee#10,RANGEDEL + +find +testdata/find-mixed +hex:636363 +--value=null +---- +000002.sst + test formatter: ccc#0,SET +Unable to decode sstable find-mixed/000001.sst, pebble/table: invalid table (file size is too small) diff --git a/pebble/tool/testdata/find-db/000009.log b/pebble/tool/testdata/find-db/000009.log new file mode 100644 index 0000000000000000000000000000000000000000..9fd4bbac28b55f6a56205f725d34a6d01524076b GIT binary patch literal 11 OcmZQz00CAmAOQdXOaKr7 literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/find-db/CURRENT b/pebble/tool/testdata/find-db/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/pebble/tool/testdata/find-db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/pebble/tool/testdata/find-db/LOCK b/pebble/tool/testdata/find-db/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/pebble/tool/testdata/find-db/MANIFEST-000001 b/pebble/tool/testdata/find-db/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..c3aa9f4a31ebf59f4ff51fcdf61380b96bd68ad5 GIT binary patch literal 422 zcmcbvxmQ|3j-q)6Ek!7?fL(8flL-=HWt=& z2G$8|+=+>ajEoFmz@40&%)|;|GqSR=KDs`mnT_$2n{{Haj~^ z$KFqU8x4UvI9b@)7(fQ4vvEv^cnG460qh|t8>j;6Aw9|VL{K1bGIO(Vr896H0_$Pm zfZCOsn#%tl0ywx}dU~yt+)RLW@vv~Qu>q|Ek{s!5+)eDD@WZARr~+!;K1=OFc9>Rf E0Fvlc?f?J) literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/find-db/OPTIONS-000003 b/pebble/tool/testdata/find-db/OPTIONS-000003 new file mode 100644 index 0000000..54d3bc2 --- /dev/null +++ b/pebble/tool/testdata/find-db/OPTIONS-000003 @@ -0,0 +1,36 @@ +[Version] + pebble_version=0.1 + +[Options] + bytes_per_sync=524288 + cache_size=8388608 + cleaner=archive + comparer=alt-comparer + delete_range_flush_delay=0s + disable_wal=false + flush_split_bytes=4194304 + l0_compaction_concurrency=10 + l0_compaction_threshold=4 + l0_stop_writes_threshold=12 + lbase_max_bytes=67108864 + max_concurrent_compactions=1 + max_manifest_file_size=134217728 + max_open_files=1000 + mem_table_size=4194304 + mem_table_stop_writes_threshold=2 + min_compaction_rate=4194304 + min_flush_rate=1048576 + merger=test-merger + strict_wal_tail=true + table_property_collectors=[] + wal_dir= + wal_bytes_per_sync=0 + +[Level "0"] + block_restart_interval=16 + block_size=4096 + compression=Snappy + filter_policy=none + filter_type=table + index_block_size=4096 + target_file_size=2097152 diff --git a/pebble/tool/testdata/find-db/archive/000002.log b/pebble/tool/testdata/find-db/archive/000002.log new file mode 100644 index 0000000000000000000000000000000000000000..6a11fe78bb7845c9127b33fa1087abba3aacd17d GIT binary patch literal 161 zcmZ3iKkb1q11l2)0|O%vg8+zTWKK*>WHi*v+Qko*1*rf5h-^|)5~I<27THLsEHgwB mEX%~4oSe*P?0m094JylmA#1WIz-J#+mK8(R6e7Y3G8F)k>kth7hN=jl*O-+S}v9bbb01SN!82|tP literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/find-db/archive/000005.sst b/pebble/tool/testdata/find-db/archive/000005.sst new file mode 100644 index 0000000000000000000000000000000000000000..757f66395a9ab024c3a08893c8dc6d354925bd90 GIT binary patch literal 784 zcmah{zi$&U6t?q&TyjU+6178Fg2WoDZ3-FE4D4(S3<#Zk=aNQ_7bfR+U@2_tP4q&wBVv9ocZp(L6j6F)zm6Yg`MQe zd!UovEe*zSC7(!Imh~t~Q&n2$o2{EcZPScfvzQ#8&c^~aAQsb;*~8gy8P}IkaT1pJRgtLB(L4b8~ zICcg_F>Z*5EE)`P)uf03xw7Y|e;X@ZP)jzTJsA}3<3>WS4mV_NKzl4S>|o#F?(hI_ zs8~Ct4PhW$W01LGz)&>A1?UwJRf49H89ZvoPBur)PC`aUJ6Ns@66OuNGdSsCkXCar z?PLeLe=C?cTnJJk_dyBf0K&E!zWcrFokHbQ%3TK{X#sLAL}~S4ujTvOnz(fe@M-hx zdE`W++W_KO+)D=6EW8&^FkBv+K7NnxwEvt}sRNXo?djLf%?*BfGrhuZyW_J1ANr5^ PYybKA>gBsf-(UX*N6YJC literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/find-db/archive/000006.sst b/pebble/tool/testdata/find-db/archive/000006.sst new file mode 100644 index 0000000000000000000000000000000000000000..80773f2ac796ba2c04ddbefc136b612183e0270c GIT binary patch literal 817 zcmah{y>1gh5Z?6%53V7_+4Q8wVZOiICFlPzyYCVqRuIq9zsGbLQkN95>q(6nK;a**OwD zj9`PjHCM57K1LOQEtE-u53jWeA_W^YYAdjTP6fTFO}BcLprqBMn=K+!pcnp{H20{n z8TV{gM4|(_RX&I+9a}oEVFc2x(J2$Q@zYsNwZwJqn!o??X3h?>FiUiTfhWyq1*t$0;2>1luO>qbkNwH z0xDv8iXE8d8R(@_xwox;CG__N!z)k_p54n=5o*oXB}iZ~?6h_)f>#Z~g@YxdpD!-S q{p#IS=C*{w*)HDK|0Lzh=O5_T`uKPlLjSR_g&$wOzxjOe?d@;gc=ksC literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/find-db/archive/000007.sst b/pebble/tool/testdata/find-db/archive/000007.sst new file mode 100644 index 0000000000000000000000000000000000000000..9e8c240e012fbfc6064219f0dbe396db49fea6c7 GIT binary patch literal 808 zcmah{J8u**5Vr5Z?LD|8oD!i!L08b86*xqYPKQKIiKq}-dDoMT6|cRqeUD8I5)w50 z5bB7Eng;#=6~6&Q3FF-(&@s*KjAy=?@0%h0h7f|1+Gexxt-QDIKl~uw2JHQ+joiF? zias`k`Z(<=+A19>;7a|wVi#UWa zOX|OI(1D!@Dcu3Jz(XhIWez0jQo%iEPF}!aw@*QVM>&w4Bhl>$Hpp9Z6+7o+Q~_9~ zOcH!J*CvP*Y}{$Bzy>-E^rAkk^{PNgZ%8*aB2%Ck1^W=nr~Z7QqWj)d%}#aruedX#Tm%!d6f?+rxM5tE8{b a9=)WW+mpkC5c-b=?4Mu0zq)+QuSZGbsKE2ilpYY85Bkn(1i& za55CI1Tj4rA0Hi0hQ_*HFfXQilVL{JG2^L?lfr`Wk#l2N?87dL%fE4uo*nZkVT3hM zKZ#hL0SZFQsHMd4b2tfG7z9`+hht|@(83$yC5oDDyxu@WfSlV?6yCr}XVj7eODYylX+szYHyC897%&vHaRz!0MCG8VWCr)EEhn3! zW=AEWgASI9jFkJ9w=+0uV31aGF!iW|UELf;t}m2QB3GaUa}COCZTN0CE^i8Jr&8)B z5J@wT3n6l=o4tykZwuniDZs<#>9fFzMptDM&*C_0Ub66BaQ(bq+ea7ZX7%cM<+^O; kc6iHKF#sy8I*s)@lI`)t7WFyoM%#%ObXvyy&1Q&f*bB~!<8G) za=!!9uHP_Vt<3dv&5NS!$4QbG4)=DAJ>=^omCjDb4<1cM3Ra+|506fcPbMSlFc&P! z>EUFQ@l7J+%q3~*zq!~LgZw{rEo3KLRcZ!f-8NiVv>LzfA5d3>Af{)-+V ljByJ;hF?gp-amOvKh|Cj!%O${e1gh5Z*oi-T5w#9V-zejf)6T;7wwOq=+KHGawKOg45pZ#2nfCVRs$-(m;bm z&tpK(3(!y@S_%pt0U;r1VD_8@Bnn2_+nfLSzS$=O;uFp}aY=&)HJ8$ccb$+O;yY0k z(Wci8Mx%PGw{c}ed>Y4bZ=<_cb8C)w?FJd|-o!W|1Z{aMo8vJ-q4qRyJ^evE2YSC2 zLhfF;$h*(a-w?ls#h2UGNC*Jf%O;Yo`ujfFtgAde(IR4z#2*%M4T2dSNnlAPV9rcg zR5|NiS|~V9=Il6;22_$ww%7Y~_BhAT6Da>OyWK{d%W0aiW1c3mWQnk|wc2iw7F=!7Vq}`$Og^NkZCgw5mmT0b0ZrCX(3x>F>bU~qIC(&%GLD^cQ zQ>V7WMrVzlaydn&H4`AQF=TNrKoGY>Q&1CNc>#)NLeqZDN7@jY8CD+UF4Jt9vTELH zcbWz*3Yk#}Xg64xpH)RHiu*zP9EH`EE}vJY`FcWDE}vJSs!}-HEBdKUdd|hWtHum* t%;{CK@~?G-W)%C`!c()#&yUX@(XWkrM{8F5k2vW6{^8q;x3|B%{0;nK0@?ro literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/find-mixed/000001.sst b/pebble/tool/testdata/find-mixed/000001.sst new file mode 100644 index 0000000..9977a28 --- /dev/null +++ b/pebble/tool/testdata/find-mixed/000001.sst @@ -0,0 +1 @@ +invalid diff --git a/pebble/tool/testdata/find-mixed/000002.sst b/pebble/tool/testdata/find-mixed/000002.sst new file mode 100644 index 0000000000000000000000000000000000000000..80773f2ac796ba2c04ddbefc136b612183e0270c GIT binary patch literal 817 zcmah{y>1gh5Z?6%53V7_+4Q8wVZOiICFlPzyYCVqRuIq9zsGbLQkN95>q(6nK;a**OwD zj9`PjHCM57K1LOQEtE-u53jWeA_W^YYAdjTP6fTFO}BcLprqBMn=K+!pcnp{H20{n z8TV{gM4|(_RX&I+9a}oEVFc2x(J2$Q@zYsNwZwJqn!o??X3h?>FiUiTfhWyq1*t$0;2>1luO>qbkNwH z0xDv8iXE8d8R(@_xwox;CG__N!z)k_p54n=5o*oXB}iZ~?6h_)f>#Z~g@YxdpD!-S q{p#IS=C*{w*)HDK|0Lzh=O5_T`uKPlLjSR_g&$wOzxjOe?d@;gc=ksC literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/manifest_dump b/pebble/tool/testdata/manifest_dump new file mode 100644 index 0000000..5ec7f4b --- /dev/null +++ b/pebble/tool/testdata/manifest_dump @@ -0,0 +1,399 @@ +manifest dump +---- +requires at least 1 arg(s), only received 0 + +manifest dump +../testdata/db-stage-2/MANIFEST-000001 +---- +MANIFEST-000001 +0/0 + comparer: leveldb.BytewiseComparator + next-file-num: 2 +39/1 + log-num: 2 + next-file-num: 3 + last-seq-num: 9 +EOF +--- L0 --- +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump +../testdata/db-stage-4/MANIFEST-000006 +---- +MANIFEST-000006 +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 7 +41/1 + log-num: 5 + next-file-num: 6 + last-seq-num: 14 + added: L0 000004:709<#12-#14>[bar#14,DEL-foo#13,SET] (2023-12-04T17:57:25Z) +EOF +--- L0.0 --- + 000004:709<#12-#14>[bar#14,DEL-foo#13,SET] +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump --filter-start=zoo +../testdata/db-stage-4/MANIFEST-000006 +---- +MANIFEST-000006 +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 7 +EOF +--- L0.0 --- +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump --filter-end=a +../testdata/db-stage-4/MANIFEST-000006 +---- +MANIFEST-000006 +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 7 +EOF +--- L0.0 --- +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump --filter-start=a --filter-end=d +../testdata/db-stage-4/MANIFEST-000006 +---- +MANIFEST-000006 +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 7 +41/1 + log-num: 5 + next-file-num: 6 + last-seq-num: 14 + added: L0 000004:709<#12-#14>[bar#14,DEL-foo#13,SET] (2023-12-04T17:57:25Z) +EOF +--- L0.0 --- + 000004:709<#12-#14>[bar#14,DEL-foo#13,SET] +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump +../testdata/db-stage-4/MANIFEST-000006 +--key=%x +---- +MANIFEST-000006 +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 7 +41/1 + log-num: 5 + next-file-num: 6 + last-seq-num: 14 + added: L0 000004:709<#12-#14>[626172#14,DEL-666f6f#13,SET] (2023-12-04T17:57:25Z) +EOF +--- L0.0 --- + 000004:709<#12-#14>[626172#14,DEL-666f6f#13,SET] +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump +../testdata/db-stage-4/MANIFEST-000006 +--key=null +---- +MANIFEST-000006 +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 7 +41/1 + log-num: 5 + next-file-num: 6 + last-seq-num: 14 + added: L0 000004:709<#12-#14> (2023-12-04T17:57:25Z) +EOF +--- L0.0 --- + 000004:709<#12-#14> +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump +../testdata/db-stage-4/MANIFEST-000006 +--key=pretty +---- +MANIFEST-000006 +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 7 +41/1 + log-num: 5 + next-file-num: 6 + last-seq-num: 14 + added: L0 000004:709<#12-#14>[bar#14,DEL-foo#13,SET] (2023-12-04T17:57:25Z) +EOF +--- L0.0 --- + 000004:709<#12-#14>[bar#14,DEL-foo#13,SET] +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump +../testdata/db-stage-4/MANIFEST-000006 +--key=pretty:test-comparer +---- +MANIFEST-000006 +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 7 +41/1 + log-num: 5 + next-file-num: 6 + last-seq-num: 14 + added: L0 000004:709<#12-#14>[test formatter: bar#14,DEL-test formatter: foo#13,SET] (2023-12-04T17:57:25Z) +EOF +--- L0.0 --- + 000004:709<#12-#14>[test formatter: bar#14,DEL-test formatter: foo#13,SET] +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest check +---- +requires at least 1 arg(s), only received 0 + +manifest check +../testdata/db-stage-1/MANIFEST-000001 +---- +OK + +manifest check +../testdata/db-stage-2/MANIFEST-000001 +---- +OK + +manifest check +../testdata/db-stage-3/MANIFEST-000005 +---- +open ../testdata/db-stage-3/MANIFEST-000005: file does not exist + +manifest check +../testdata/db-stage-4/MANIFEST-000006 +---- +OK + +manifest dump +./testdata/MANIFEST-invalid +---- +MANIFEST-invalid +0/0 + comparer: leveldb.BytewiseComparator + log-num: 2 + next-file-num: 5 + last-seq-num: 20 + added: L6 000001:0<#2-#5>[#0,DEL-#0,DEL] +65/1 + comparer: leveldb.BytewiseComparator + log-num: 3 + next-file-num: 5 + last-seq-num: 20 + added: L6 000002:0<#1-#4>[#0,DEL-#0,DEL] +EOF +pebble: files 000002 and 000001 collided on sort keys + +manifest check +./testdata/MANIFEST-invalid +---- +MANIFEST-invalid: offset: 65 err: pebble: files 000002 and 000001 collided on sort keys +Version state before failed Apply +--- L0 --- +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + 000001:0<#2-#5>[#0,DEL-#0,DEL] +Version edit that failed + added: L6 000002:0<#1-#4>[#0,DEL-#0,DEL] + +manifest dump +./testdata/find-db/MANIFEST-000001 +---- +MANIFEST-000001 +0/0 + comparer: alt-comparer + next-file-num: 2 +25/1 + log-num: 2 + next-file-num: 3 +36/2 + log-num: 4 + next-file-num: 6 + last-seq-num: 5 + added: L0 000005:784<#1-#5>[aaa#1,SET-ccc#5,MERGE] (2021-04-01T20:24:02Z) +88/3 + next-file-num: 6 + last-seq-num: 5 + deleted: L0 000005 + added: L6 000005:784<#1-#5>[aaa#1,SET-ccc#5,MERGE] (2021-04-01T20:24:02Z) +141/4 + next-file-num: 7 + last-seq-num: 6 + added: L0 000006:817<#6-#6>[bbb#6,SET-ccc#6,SET] (2021-04-01T20:24:02Z) +191/5 + next-file-num: 8 + last-seq-num: 7 + added: L6 000007:808<#7-#7>[ddd#7,SET-ddd#7,SET] (2021-04-01T20:24:02Z) +241/6 + next-file-num: 9 + last-seq-num: 7 + deleted: L0 000006 + deleted: L6 000005 + added: L6 000008:791<#0-#6>[aaa#0,SET-ccc#0,MERGE] (2021-04-01T20:24:02Z) +297/7 + log-num: 9 + next-file-num: 11 + last-seq-num: 10 + added: L0 000010:834<#8-#10>[aaa#8,DEL-eee#inf,RANGEDEL] (2021-04-01T20:24:02Z) +349/8 + next-file-num: 12 + last-seq-num: 10 + deleted: L0 000010 + deleted: L6 000007 + deleted: L6 000008 + added: L6 000011:898<#0-#10>[aaa#8,DEL-eee#inf,RANGEDEL] (2021-04-01T20:24:02Z) +408/9 + next-file-num: 12 + last-seq-num: 10 + deleted: L6 000011 +EOF +--- L0 --- +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest dump --filter-start=bat --filter-end=cat +./testdata/find-db/MANIFEST-000001 +---- +MANIFEST-000001 +0/0 + comparer: alt-comparer + next-file-num: 2 +36/1 + log-num: 4 + next-file-num: 6 + last-seq-num: 5 + added: L0 000005:784<#1-#5>[aaa#1,SET-ccc#5,MERGE] (2021-04-01T20:24:02Z) +88/2 + next-file-num: 6 + last-seq-num: 5 + deleted: L0 000005 + added: L6 000005:784<#1-#5>[aaa#1,SET-ccc#5,MERGE] (2021-04-01T20:24:02Z) +141/3 + next-file-num: 7 + last-seq-num: 6 + added: L0 000006:817<#6-#6>[bbb#6,SET-ccc#6,SET] (2021-04-01T20:24:02Z) +241/4 + next-file-num: 9 + last-seq-num: 7 + deleted: L0 000006 + deleted: L6 000005 + added: L6 000008:791<#0-#6>[aaa#0,SET-ccc#0,MERGE] (2021-04-01T20:24:02Z) +297/5 + log-num: 9 + next-file-num: 11 + last-seq-num: 10 + added: L0 000010:834<#8-#10>[aaa#8,DEL-eee#inf,RANGEDEL] (2021-04-01T20:24:02Z) +349/6 + next-file-num: 12 + last-seq-num: 10 + deleted: L0 000010 + deleted: L6 000007 + deleted: L6 000008 + added: L6 000011:898<#0-#10>[aaa#8,DEL-eee#inf,RANGEDEL] (2021-04-01T20:24:02Z) +408/7 + next-file-num: 12 + last-seq-num: 10 + deleted: L6 000011 +EOF +--- L0 --- +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- + +manifest check +./testdata/mixed/MANIFEST-000001 +---- +OK + +manifest dump +./testdata/mixed/MANIFEST-000001 +---- +MANIFEST-000001 +0/0 + comparer: pebble.internal.testkeys + next-file-num: 2 +37/1 + log-num: 2 + next-file-num: 3 +48/2 + log-num: 4 + next-file-num: 6 + last-seq-num: 29 + added: L0 000005:1166<#1-#29>[a#29,RANGEKEYDEL-z@1#26,SET] (2022-08-12T23:21:28Z) +EOF +--- L0.0 --- + 000005:1166<#1-#29>[a#29,RANGEKEYDEL-z@1#26,SET] +--- L1 --- +--- L2 --- +--- L3 --- +--- L4 --- +--- L5 --- +--- L6 --- diff --git a/pebble/tool/testdata/manifest_summarize b/pebble/tool/testdata/manifest_summarize new file mode 100644 index 0000000..a307f8f --- /dev/null +++ b/pebble/tool/testdata/manifest_summarize @@ -0,0 +1,45 @@ +manifest summarize +---- +requires at least 1 arg(s), only received 0 + +manifest summarize +../testdata/db-stage-2/MANIFEST-000001 +---- +MANIFEST-000001 +(no timestamps) +--- +Estimated start time: 0001-01-01T00:00:00Z +Estimated end time: 0001-01-01T00:00:00Z +Estimated duration: 0s + +manifest summarize +./testdata/find-db/MANIFEST-000001 +---- +MANIFEST-000001 + _______L0_______L1_______L2_______L3_______L4_______L5_______L6_____TOTAL +2021-04-01T20:24:02Z + Ingest+Flush 2.4KB . . . . . 808B 3.2KB + Ingest+Flush 2.4KB/s . . . . . 808B/s 3.2KB/s + Compact (out) 2.4KB . . . . . 898B 3.3KB + Compact (out) 2.4KB/s . . . . . 898B/s 3.3KB/s +2021-04-01T20:24:02Z +--- +Estimated start time: 2021-04-01T20:24:02Z +Estimated end time: 2021-04-01T20:24:02Z +Estimated duration: 0s + +manifest summarize +./testdata/mixed/MANIFEST-000001 +---- +MANIFEST-000001 + _______L0_______L1_______L2_______L3_______L4_______L5_______L6_____TOTAL +2022-08-12T23:21:28Z + Ingest+Flush 1.1KB . . . . . . 1.1KB + Ingest+Flush 1.1KB/s . . . . . . 1.1KB/s + Compact (out) . . . . . . . . + Compact (out) . . . . . . . . +2022-08-12T23:21:28Z +--- +Estimated start time: 2022-08-12T23:21:28Z +Estimated end time: 2022-08-12T23:21:28Z +Estimated duration: 0s diff --git a/pebble/tool/testdata/mixed/000002.log b/pebble/tool/testdata/mixed/000002.log new file mode 100644 index 0000000000000000000000000000000000000000..04a0fbed4fbd7866ae99683e218f2ea84257723a GIT binary patch literal 214 zcmW;G%MpS=5QO0ws3ifk$_qhg1SUWbi}(OVo+=QV2SaoA@2dW)kG&he`A=LVB5DoE ztQz~{-YoEhC7$tuSG-|`HQw=o4L-5O4tpGM#0g*c#t+W8;A(EwC%Q(qHtJnihg7{5 J;k$^7jX!DV9WVd@ literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/mixed/000004.log b/pebble/tool/testdata/mixed/000004.log new file mode 100644 index 0000000000000000000000000000000000000000..d674fcb9c7e6b5b95b5e735579fadc843e169376 GIT binary patch literal 53 zcmXq?ZaGnlft7`Ufk6(4K>);NWKMK2Vi09aWMiyiaxi8PVN7HNu}p*+6B&~LqP_-p literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/mixed/000005.sst b/pebble/tool/testdata/mixed/000005.sst new file mode 100644 index 0000000000000000000000000000000000000000..10a342078cdf7dd48f20f517a4a9a5b7365c2c95 GIT binary patch literal 1166 zcmaKrJ8u*_6vyq^Bs;UOY_e=f!Yc&@O{|1_1w|AoUw{TmgjSxNlZ}bTGuYnjZYZhv z2Xv*43M3>%K}iRH&qgd2Bc1u#$M*4g`CGKZnoJ(;bIuaJ*<>k( zQPRE&?GVFSzthdYC6K^n@EBYHM_?U10b@{s3HTUnfTv&+)L;wD!8T~X4p@MzUStarkaEq=fX-eqTN%P?PjDM?=Nvj9$evLQ;q5y+{(#fpTJ(>)Hz-0H~5*K1WN_(=(jP|yBd~#A?YcRi1 z$KB;&p~txq$1+!XCR7$uQ*XqZc`2>*g}n$y8CWt>3s26&h-=kXEIFqN^T5cmob}?t zpeQ}&ZUTRb$skqU9q#V`d9b7DjP&6jx9{G$d$8j??9ymENn?y+>#7839oWuaS~DJRB`Y_P>TC>{!Pt+R1a7%ZZyxS7nb$S7lR zhv#HRB#M$y`OL9iZGDl$VPS>{-9X}19@?YwyQ-(-tvV;;ts=MGT18WY#({aNQp#|k z&>3=HRd616zkX3?nj%|6TqKOl$&9rwy=|;T!3{qXD6yun>>fUfTT$q@5L*c5@2K@_ zn}zn7zlV3v`dh49yQqHU%>ajv9TNfsgL|ceDH;D QhUxFWaN_GfFW&_D0rKcLVE_OC literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/mixed/CURRENT b/pebble/tool/testdata/mixed/CURRENT new file mode 100644 index 0000000..feda7d6 --- /dev/null +++ b/pebble/tool/testdata/mixed/CURRENT @@ -0,0 +1 @@ +MANIFEST-000000 diff --git a/pebble/tool/testdata/mixed/LOCK b/pebble/tool/testdata/mixed/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/pebble/tool/testdata/mixed/MANIFEST-000001 b/pebble/tool/testdata/mixed/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..eae2a21a776ed9144bea3fac454204e4c4b35341 GIT binary patch literal 121 zcmZqlt4Nk(U}TgiNKH!0N!82DD@iTNOU%(LNi8nPPOU6vW@2HO;84QJ!obMH#LV2% zH+PW}kjcW##v+@+z}m;j%AM$7$jHb52HaIZwiJZTnJ6p^WmSp(hX6)dHr5{pZck@p F1OWcr9w`6- literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/mixed/OPTIONS-000003 b/pebble/tool/testdata/mixed/OPTIONS-000003 new file mode 100644 index 0000000..7de3573 --- /dev/null +++ b/pebble/tool/testdata/mixed/OPTIONS-000003 @@ -0,0 +1,44 @@ +[Version] + pebble_version=0.1 + +[Options] + bytes_per_sync=524288 + cache_size=8388608 + cleaner=delete + compaction_debt_concurrency=1073741824 + comparer=pebble.internal.testkeys + delete_range_flush_delay=0s + disable_wal=false + flush_split_bytes=4194304 + format_major_version=11 + l0_compaction_concurrency=10 + l0_compaction_file_threshold=500 + l0_compaction_threshold=4 + l0_stop_writes_threshold=12 + lbase_max_bytes=67108864 + max_concurrent_compactions=1 + max_manifest_file_size=134217728 + max_open_files=1000 + mem_table_size=4194304 + mem_table_stop_writes_threshold=2 + min_deletion_rate=0 + merger=pebble.concatenate + read_compaction_rate=16000 + read_sampling_multiplier=16 + strict_wal_tail=true + table_cache_shards=16 + table_property_collectors=[] + validate_on_ingest=false + wal_dir= + wal_bytes_per_sync=0 + max_writer_concurrency=0 + force_writer_parallelism=false + +[Level "0"] + block_restart_interval=16 + block_size=4096 + compression=Snappy + filter_policy=none + filter_type=table + index_block_size=4096 + target_file_size=2097152 diff --git a/pebble/tool/testdata/mixed/main.go b/pebble/tool/testdata/mixed/main.go new file mode 100644 index 0000000..a4ba3b5 --- /dev/null +++ b/pebble/tool/testdata/mixed/main.go @@ -0,0 +1,107 @@ +// Command main generates test fixtures for a DB containing a mixture of point +// and range keys. +// +// Upon running this command, the DB directory will contain: +// +// 1. A single SSTable (000005.sst), containing: +// a. 26 point keys, a@1 through z@1. +// b. a RANGEKEYSET [a, z)@1 +// c. a RANGEKEYUNSET [a, z)@2 +// d. a RANGEKEYDEL [a, b) +// 2. A WAL for an unflushed memtable containing: +// a. A single point key a@2. +// b. a RANGEKEYSET [a, z)@3 +// c. a RANGEKEYUNSET [a, z)@4 +// d. a RANGEKEYDEL [a, b) +package main + +import ( + "io/fs" + "os" + "path/filepath" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/testkeys" + "github.com/cockroachdb/pebble/vfs" +) + +const outDir = "./tool/testdata/mixed" + +func main() { + err := filepath.WalkDir(outDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if filepath.Base(path) == "main.go" || d.IsDir() { + return nil + } + return os.Remove(path) + }) + if err != nil { + panic(err) + } + lel := pebble.MakeLoggingEventListener(pebble.DefaultLogger) + + opts := &pebble.Options{ + FS: vfs.Default, + Comparer: testkeys.Comparer, + FormatMajorVersion: pebble.FormatNewest, + EventListener: &lel, + DisableAutomaticCompactions: true, + } + db, err := pebble.Open(outDir, opts) + if err != nil { + panic(err) + } + + writePoint := func(b *pebble.Batch, k []byte) { + if err := b.Set(k, nil, nil); err != nil { + panic(err) + } + } + writeRangeKeySet := func(b *pebble.Batch, start, end, suffix []byte) { + if err := b.RangeKeySet(start, end, suffix, nil, nil); err != nil { + panic(err) + } + } + writeRangeKeyUnset := func(b *pebble.Batch, start, end, suffix []byte) { + if err := b.RangeKeyUnset(start, end, suffix, nil); err != nil { + panic(err) + } + } + writeRangeKeyDel := func(b *pebble.Batch, start, end []byte) { + if err := b.RangeKeyDelete(start, end, nil); err != nil { + panic(err) + } + } + + // Write some point and range keys. + ks := testkeys.Alpha(1) + b := db.NewBatch() + for i := int64(0); i < ks.Count(); i++ { + writePoint(b, testkeys.KeyAt(ks, i, 1)) + } + start, end := testkeys.Key(ks, 0), testkeys.Key(ks, ks.Count()-1) + writeRangeKeySet(b, start, end, testkeys.Suffix(1)) // SET [a, z)@1 + writeRangeKeyUnset(b, start, end, testkeys.Suffix(2)) // UNSET [a, z)@2 + writeRangeKeyDel(b, start, testkeys.Key(ks, 1)) // DEL [a, b) + if err := b.Commit(nil); err != nil { + panic(err) + } + + // Flush memtables. + if err := db.Flush(); err != nil { + panic(err) + } + + // Write one more point and range key into a memtable before closing, so the + // WAL is not empty. + b = db.NewBatch() + writePoint(b, testkeys.KeyAt(ks, 0, 2)) + writeRangeKeySet(b, start, end, testkeys.Suffix(3)) // SET [a, z)@3 + writeRangeKeyUnset(b, start, end, testkeys.Suffix(4)) // UNSET [a, z)@4 + writeRangeKeyDel(b, start, testkeys.Key(ks, 1)) // DEL [a, b) + if err := b.Commit(nil); err != nil { + panic(err) + } +} diff --git a/pebble/tool/testdata/mixed/marker.format-version.000010.011 b/pebble/tool/testdata/mixed/marker.format-version.000010.011 new file mode 100644 index 0000000..e69de29 diff --git a/pebble/tool/testdata/mixed/marker.manifest.000001.MANIFEST-000001 b/pebble/tool/testdata/mixed/marker.manifest.000001.MANIFEST-000001 new file mode 100644 index 0000000..e69de29 diff --git a/pebble/tool/testdata/out-of-order.sst b/pebble/tool/testdata/out-of-order.sst new file mode 100644 index 0000000000000000000000000000000000000000..af39d5f22f6e851226a9c0de21d990f60e59d4a8 GIT binary patch literal 833 zcmah{&2AGh5Vn(s?55lFk3=Z^D3!QDt@0y!2sr=`z=0z|EAM);wd%DO+i9}BBk=${ z1Fyh=BU}*@Pk;~-2aX_dgz=^o+!)D{=kJ@DZ|3Gj5))3z2908pUrk6hNG=gVXm~e% ze1ApaR=)jvCFBZP8yETXdH9Z8U$Z7Zb|Pb$!Vk;11HnAc6fmg;ESaw>2sxuIT3MLM zC7Vg*!Ae~mjCSeb$YAI()c=^Htxj&#ywq&QOQ~umMX28GMWeFfmV09roPffr4{Hw# z>EMw$_axm57MzntKheCZ>fIA_@N@?|Py?$ZqMU9g-wFVbMfD5?M2L5EpI2#FJFfrm=W%MwTwQ^7rFPM*SEyiP%Z zM{;mhN1|&HERhAxRqC8ikrQBrGFi~!xi*2aAfxWu3D_`B3$((oorG}FJK?xp;-jM z1fv8v-)w%x)}VWKA4rVlITm7?m!RiLRo?ctTcHhKV_l^nxZTrd5mL?<4M;Og(mNX_ zXbpeiipl%OAKsFk_W7w+wt>Rl?!WE?e-qUHT8!wo&UkMaO#hLvj9(vrzIgNC%ga9` C(Etnp literal 0 HcmV?d00001 diff --git a/pebble/tool/testdata/remotecat b/pebble/tool/testdata/remotecat new file mode 100644 index 0000000..0f7ab2c --- /dev/null +++ b/pebble/tool/testdata/remotecat @@ -0,0 +1,33 @@ +remotecat dump +---- +requires at least 1 arg(s), only received 0 + +remotecat dump +./testdata/REMOTE-OBJ-CATALOG +---- +REMOTE-OBJ-CATALOG +CreatorID: 3 +Objects: + 000002 CreatorID: 5 CreatorFileNum: 000010 Locator: "foo" CustomObjectName: "" + 000003 CreatorID: 0 CreatorFileNum: 000000 Locator: "bar" CustomObjectName: "external.sst" + +remotecat dump --verbose +./testdata/REMOTE-OBJ-CATALOG +---- +REMOTE-OBJ-CATALOG +0/0 +7/1 + CreatorID: 3 +16/2 + NewObjects: + 000001 CreatorID: 3 CreatorFileNum: 000001 Locator: "foo" CustomObjectName: "" +35/3 + NewObjects: + 000002 CreatorID: 5 CreatorFileNum: 000010 Locator: "foo" CustomObjectName: "" + 000003 CreatorID: 0 CreatorFileNum: 000000 Locator: "bar" CustomObjectName: "external.sst" + DeletedObjects: + 000001 +CreatorID: 3 +Objects: + 000002 CreatorID: 5 CreatorFileNum: 000010 Locator: "foo" CustomObjectName: "" + 000003 CreatorID: 0 CreatorFileNum: 000000 Locator: "bar" CustomObjectName: "external.sst" diff --git a/pebble/tool/testdata/sstable_check b/pebble/tool/testdata/sstable_check new file mode 100644 index 0000000..c25772d --- /dev/null +++ b/pebble/tool/testdata/sstable_check @@ -0,0 +1,59 @@ +sstable check +../sstable/testdata/h.sst +---- +h.sst + +sstable check +testdata/out-of-order.sst +---- +out-of-order.sst +WARNING: OUT OF ORDER KEYS! + c#0,SET >= b#0,SET + +sstable check +--key=%x +testdata/out-of-order.sst +---- +out-of-order.sst +WARNING: OUT OF ORDER KEYS! + 63#0,SET >= 62#0,SET + +sstable check +--key=pretty +testdata/out-of-order.sst +---- +out-of-order.sst +WARNING: OUT OF ORDER KEYS! + c#0,SET >= b#0,SET + +sstable check +--key=pretty:test-comparer +testdata/out-of-order.sst +---- +out-of-order.sst +WARNING: OUT OF ORDER KEYS! + test formatter: c#0,SET >= test formatter: b#0,SET + +sstable check +--key=null +testdata/out-of-order.sst +---- +out-of-order.sst +WARNING: OUT OF ORDER KEYS! + +sstable check +testdata/corrupted.sst +---- +corrupted.sst +pebble/table: invalid table 000000 (checksum mismatch at 0/57) + +sstable check +testdata/bad-magic.sst +---- +bad-magic.sst +pebble/table: invalid table (bad magic number: 0xf6cff485b741e288) + +sstable check +./testdata/mixed/000005.sst +---- +000005.sst diff --git a/pebble/tool/testdata/sstable_layout b/pebble/tool/testdata/sstable_layout new file mode 100644 index 0000000..9c4ea7b --- /dev/null +++ b/pebble/tool/testdata/sstable_layout @@ -0,0 +1,3894 @@ +sstable layout +---- +requires at least 1 arg(s), only received 0 + +sstable layout +../sstable/testdata/h.sst +---- +h.sst + 0 data (1094) + 1099 data (1057) + 2161 data (1074) + 3240 data (1051) + 4296 data (1046) + 5347 data (1091) + 6443 data (996) + 7444 data (1060) + 8509 data (1051) + 9565 data (1016) + 10586 data (1026) + 11617 data (1100) + 12722 data (1025) + 13752 data (156) + 13913 index (245) + 14163 range-del (421) + 14589 properties (719) + 15313 meta-index (61) + 15379 footer (53) + 15432 EOF + +sstable layout +../sstable/testdata/h.ldb +---- +h.ldb + 0 data (1094) + 1099 data (1057) + 2161 data (1074) + 3240 data (1051) + 4296 data (1046) + 5347 data (1091) + 6443 data (996) + 7444 data (1060) + 8509 data (1051) + 9565 data (1016) + 10586 data (1026) + 11617 data (1100) + 12722 data (1025) + 13752 data (156) + 13913 index (245) + 14163 range-del (421) + 14589 properties (673) + 15267 meta-index (61) + 15333 leveldb-footer (48) + 15381 EOF + +sstable layout +../sstable/testdata/h.table-bloom.no-compression.sst +---- +h.table-bloom.no-compression.sst + 0 data (2041) + 2046 data (2044) + 4095 data (2039) + 6139 data (2036) + 8180 data (2032) + 10217 data (2042) + 12264 data (2039) + 14308 data (2037) + 16350 data (2029) + 18384 data (2040) + 20429 data (2030) + 22464 data (2035) + 24504 data (2036) + 26545 data (249) + 26799 filter (2245) + 29049 index (325) + 29379 range-del (421) + 29805 properties (717) + 30527 meta-index (112) + 30644 footer (53) + 30697 EOF + +sstable layout +../sstable/testdata/h.no-compression.two_level_index.sst +---- +h.no-compression.two_level_index.sst + 0 data (2041) + 2046 data (2044) + 4095 data (2039) + 6139 data (2036) + 8180 data (2032) + 10217 data (2042) + 12264 data (2039) + 14308 data (2037) + 16350 data (2029) + 18384 data (2040) + 20429 data (2030) + 22464 data (2035) + 24504 data (2036) + 26545 data (249) + 26799 index (120) + 26924 index (118) + 27047 index (95) + 27147 top-index (70) + 27222 range-del (421) + 27648 properties (765) + 28418 meta-index (63) + 28486 footer (53) + 28539 EOF + +sstable layout +-v +--value=pretty:test-comparer +../sstable/testdata/h.no-compression.two_level_index.sst +---- +h.no-compression.two_level_index.sst + 0 data (2041) + 0 record (14 = 3 [0] + 9 + 2) [restart] + a#0,SET test value formatter: 97 + 14 record (17 = 3 [1] + 13 + 1) + aboard#0,SET test value formatter: 2 + 31 record (14 = 3 [3] + 10 + 1) + about#0,SET test value formatter: 2 + 45 record (14 = 3 [3] + 10 + 1) + above#0,SET test value formatter: 1 + 59 record (16 = 3 [2] + 12 + 1) + abroad#0,SET test value formatter: 1 + 75 record (16 = 3 [2] + 12 + 1) + absurd#0,SET test value formatter: 1 + 91 record (16 = 3 [2] + 12 + 1) + abused#0,SET test value formatter: 1 + 107 record (17 = 3 [1] + 13 + 1) + accord#0,SET test value formatter: 1 + 124 record (15 = 3 [4] + 11 + 1) + account#0,SET test value formatter: 1 + 139 record (22 = 3 [2] + 18 + 1) + achievements#0,SET test value formatter: 1 + 161 record (18 = 3 [2] + 14 + 1) + acquaint#0,SET test value formatter: 1 + 179 record (13 = 3 [2] + 9 + 1) + act#0,SET test value formatter: 5 + 192 record (15 = 3 [3] + 11 + 1) + action#0,SET test value formatter: 1 + 207 record (13 = 3 [6] + 9 + 1) + actions#0,SET test value formatter: 1 + 220 record (19 = 3 [1] + 15 + 1) + addition#0,SET test value formatter: 1 + 239 record (16 = 3 [3] + 12 + 1) + address#0,SET test value formatter: 1 + 255 record (17 = 3 [0] + 13 + 1) [restart] + adieu#0,SET test value formatter: 4 + 272 record (20 = 3 [2] + 16 + 1) + admiration#0,SET test value formatter: 1 + 292 record (18 = 3 [2] + 14 + 1) + adoption#0,SET test value formatter: 1 + 310 record (20 = 3 [2] + 16 + 1) + adulterate#0,SET test value formatter: 1 + 330 record (19 = 3 [2] + 15 + 1) + advantage#0,SET test value formatter: 1 + 349 record (15 = 3 [3] + 11 + 1) + advice#0,SET test value formatter: 1 + 364 record (17 = 3 [1] + 13 + 1) + affair#0,SET test value formatter: 2 + 381 record (18 = 3 [3] + 14 + 1) + affection#0,SET test value formatter: 3 + 399 record (15 = 3 [2] + 11 + 1) + after#0,SET test value formatter: 1 + 414 record (16 = 3 [5] + 12 + 1) + afternoon#0,SET test value formatter: 1 + 430 record (17 = 3 [1] + 12 + 2) + again#0,SET test value formatter: 13 + 447 record (14 = 3 [5] + 10 + 1) + against#0,SET test value formatter: 5 + 461 record (13 = 3 [1] + 9 + 1) + ah#0,SET test value formatter: 2 + 474 record (14 = 3 [1] + 10 + 1) + air#0,SET test value formatter: 5 + 488 record (13 = 3 [3] + 9 + 1) + airs#0,SET test value formatter: 1 + 501 record (15 = 3 [1] + 11 + 1) + alas#0,SET test value formatter: 1 + 516 record (16 = 3 [0] + 11 + 2) [restart] + all#0,SET test value formatter: 36 + 532 record (15 = 3 [3] + 11 + 1) + alleys#0,SET test value formatter: 1 + 547 record (14 = 3 [3] + 10 + 1) + allow#0,SET test value formatter: 1 + 561 record (16 = 3 [2] + 12 + 1) + almost#0,SET test value formatter: 4 + 577 record (15 = 3 [2] + 11 + 1) + alone#0,SET test value formatter: 4 + 592 record (13 = 3 [4] + 9 + 1) + along#0,SET test value formatter: 2 + 605 record (17 = 3 [2] + 13 + 1) + already#0,SET test value formatter: 1 + 622 record (16 = 3 [2] + 12 + 1) + always#0,SET test value formatter: 1 + 638 record (13 = 3 [1] + 9 + 1) + am#0,SET test value formatter: 9 + 651 record (16 = 3 [2] + 12 + 1) + amazed#0,SET test value formatter: 1 + 667 record (19 = 3 [2] + 15 + 1) + ambiguous#0,SET test value formatter: 1 + 686 record (17 = 3 [4] + 13 + 1) + ambitious#0,SET test value formatter: 1 + 703 record (14 = 3 [1] + 9 + 2) + an#0,SET test value formatter: 13 + 717 record (15 = 3 [2] + 9 + 3) + and#0,SET test value formatter: 227 + 732 record (15 = 3 [2] + 11 + 1) + angel#0,SET test value formatter: 1 + 747 record (13 = 3 [5] + 9 + 1) + angels#0,SET test value formatter: 1 + 760 record (17 = 3 [0] + 13 + 1) [restart] + anger#0,SET test value formatter: 1 + 777 record (14 = 3 [3] + 10 + 1) + angry#0,SET test value formatter: 1 + 791 record (17 = 3 [2] + 13 + 1) + another#0,SET test value formatter: 1 + 808 record (16 = 3 [2] + 12 + 1) + answer#0,SET test value formatter: 4 + 824 record (15 = 3 [2] + 11 + 1) + antic#0,SET test value formatter: 1 + 839 record (13 = 3 [2] + 9 + 1) + any#0,SET test value formatter: 6 + 852 record (18 = 3 [1] + 14 + 1) + apparel#0,SET test value formatter: 1 + 870 record (17 = 3 [5] + 13 + 1) + apparition#0,SET test value formatter: 2 + 887 record (15 = 3 [3] + 11 + 1) + appear#0,SET test value formatter: 4 + 902 record (13 = 3 [6] + 9 + 1) + appears#0,SET test value formatter: 1 + 915 record (16 = 3 [4] + 12 + 1) + appetite#0,SET test value formatter: 1 + 931 record (16 = 3 [3] + 12 + 1) + approve#0,SET test value formatter: 1 + 947 record (13 = 3 [2] + 9 + 1) + apt#0,SET test value formatter: 1 + 960 record (15 = 3 [1] + 10 + 2) + are#0,SET test value formatter: 21 + 975 record (13 = 3 [2] + 9 + 1) + arm#0,SET test value formatter: 2 + 988 record (14 = 3 [3] + 10 + 1) + armed#0,SET test value formatter: 2 + 1002 record (18 = 3 [0] + 14 + 1) [restart] + armour#0,SET test value formatter: 1 + 1020 record (13 = 3 [3] + 9 + 1) + arms#0,SET test value formatter: 2 + 1033 record (16 = 3 [2] + 12 + 1) + arrant#0,SET test value formatter: 1 + 1049 record (13 = 3 [2] + 9 + 1) + art#0,SET test value formatter: 6 + 1062 record (15 = 3 [3] + 11 + 1) + artery#0,SET test value formatter: 1 + 1077 record (16 = 3 [3] + 12 + 1) + article#0,SET test value formatter: 1 + 1093 record (13 = 3 [7] + 9 + 1) + articles#0,SET test value formatter: 1 + 1106 record (14 = 3 [1] + 9 + 2) + as#0,SET test value formatter: 56 + 1120 record (15 = 3 [2] + 11 + 1) + aside#0,SET test value formatter: 1 + 1135 record (16 = 3 [2] + 12 + 1) + asking#0,SET test value formatter: 1 + 1151 record (16 = 3 [2] + 12 + 1) + assail#0,SET test value formatter: 1 + 1167 record (18 = 3 [3] + 14 + 1) + assistant#0,SET test value formatter: 1 + 1185 record (15 = 3 [3] + 11 + 1) + assume#0,SET test value formatter: 2 + 1200 record (14 = 3 [1] + 9 + 2) + at#0,SET test value formatter: 18 + 1214 record (20 = 3 [2] + 16 + 1) + attendants#0,SET test value formatter: 1 + 1234 record (13 = 3 [5] + 9 + 1) + attent#0,SET test value formatter: 1 + 1247 record (21 = 3 [0] + 17 + 1) [restart] + attribute#0,SET test value formatter: 1 + 1268 record (19 = 3 [1] + 15 + 1) + audience#0,SET test value formatter: 1 + 1287 record (15 = 3 [2] + 11 + 1) + aught#0,SET test value formatter: 2 + 1302 record (20 = 3 [2] + 16 + 1) + auspicious#0,SET test value formatter: 1 + 1322 record (16 = 3 [1] + 12 + 1) + avoid#0,SET test value formatter: 1 + 1338 record (15 = 3 [3] + 11 + 1) + avouch#0,SET test value formatter: 1 + 1353 record (16 = 3 [1] + 12 + 1) + awake#0,SET test value formatter: 1 + 1369 record (13 = 3 [3] + 9 + 1) + away#0,SET test value formatter: 7 + 1382 record (16 = 3 [2] + 12 + 1) + awhile#0,SET test value formatter: 2 + 1398 record (13 = 3 [1] + 9 + 1) + ay#0,SET test value formatter: 7 + 1411 record (16 = 3 [0] + 12 + 1) + baby#0,SET test value formatter: 1 + 1427 record (14 = 3 [2] + 10 + 1) + back#0,SET test value formatter: 1 + 1441 record (15 = 3 [2] + 11 + 1) + baked#0,SET test value formatter: 1 + 1456 record (14 = 3 [2] + 10 + 1) + bark#0,SET test value formatter: 1 + 1470 record (13 = 3 [3] + 9 + 1) + barr#0,SET test value formatter: 1 + 1483 record (14 = 3 [2] + 10 + 1) + base#0,SET test value formatter: 1 + 1497 record (17 = 3 [0] + 13 + 1) [restart] + baser#0,SET test value formatter: 1 + 1514 record (15 = 3 [2] + 11 + 1) + bawds#0,SET test value formatter: 1 + 1529 record (14 = 3 [1] + 9 + 2) + be#0,SET test value formatter: 42 + 1543 record (14 = 3 [2] + 10 + 1) + bear#0,SET test value formatter: 5 + 1557 record (13 = 3 [4] + 9 + 1) + beard#0,SET test value formatter: 1 + 1570 record (15 = 3 [4] + 11 + 1) + bearers#0,SET test value formatter: 1 + 1585 record (13 = 3 [4] + 9 + 1) + bears#0,SET test value formatter: 1 + 1598 record (14 = 3 [3] + 10 + 1) + beast#0,SET test value formatter: 2 + 1612 record (16 = 3 [3] + 12 + 1) + beating#0,SET test value formatter: 1 + 1628 record (15 = 3 [3] + 11 + 1) + beauty#0,SET test value formatter: 1 + 1643 record (15 = 3 [3] + 11 + 1) + beaver#0,SET test value formatter: 1 + 1658 record (17 = 3 [2] + 13 + 1) + beckons#0,SET test value formatter: 2 + 1675 record (13 = 3 [2] + 9 + 1) + bed#0,SET test value formatter: 4 + 1688 record (14 = 3 [2] + 10 + 1) + been#0,SET test value formatter: 4 + 1702 record (16 = 3 [3] + 12 + 1) + beetles#0,SET test value formatter: 1 + 1718 record (18 = 3 [2] + 14 + 1) + befitted#0,SET test value formatter: 1 + 1736 record (18 = 3 [0] + 14 + 1) [restart] + before#0,SET test value formatter: 6 + 1754 record (13 = 3 [2] + 9 + 1) + beg#0,SET test value formatter: 1 + 1767 record (16 = 3 [3] + 12 + 1) + beguile#0,SET test value formatter: 1 + 1783 record (16 = 3 [2] + 12 + 1) + behold#0,SET test value formatter: 1 + 1799 record (15 = 3 [4] + 11 + 1) + behoves#0,SET test value formatter: 1 + 1814 record (15 = 3 [2] + 11 + 1) + being#0,SET test value formatter: 4 + 1829 record (16 = 3 [2] + 12 + 1) + belief#0,SET test value formatter: 1 + 1845 record (14 = 3 [5] + 10 + 1) + believe#0,SET test value formatter: 6 + 1859 record (13 = 3 [3] + 9 + 1) + bell#0,SET test value formatter: 1 + 1872 record (14 = 3 [2] + 10 + 1) + bend#0,SET test value formatter: 2 + 1886 record (16 = 3 [3] + 12 + 1) + beneath#0,SET test value formatter: 5 + 1902 record (15 = 3 [4] + 11 + 1) + benefit#0,SET test value formatter: 1 + 1917 record (19 = 3 [2] + 14 + 2) + bernardo#0,SET test value formatter: 30 + 1936 record (17 = 3 [2] + 13 + 1) + beseech#0,SET test value formatter: 2 + 1953 record (17 = 3 [3] + 13 + 1) + besmirch#0,SET test value formatter: 1 + 1970 record (13 = 3 [3] + 9 + 1) + best#0,SET test value formatter: 5 + 1983 record (18 = 3 [0] + 14 + 1) [restart] + beteem#0,SET test value formatter: 1 + 2001 [restart 0] + 2005 [restart 255] + 2009 [restart 516] + 2013 [restart 760] + 2017 [restart 1002] + 2021 [restart 1247] + 2025 [restart 1497] + 2029 [restart 1736] + 2033 [restart 1983] + 2041 [trailer compression=none checksum=0x13c15fb] + 2046 data (2044) + 2046 record (21 = 3 [0] + 17 + 1) [restart] + bethought#0,SET test value formatter: 1 + 2067 record (15 = 3 [3] + 11 + 1) + better#0,SET test value formatter: 2 + 2082 record (16 = 3 [3] + 12 + 1) + between#0,SET test value formatter: 2 + 2098 record (16 = 3 [2] + 12 + 1) + beware#0,SET test value formatter: 2 + 2114 record (16 = 3 [2] + 12 + 1) + beyond#0,SET test value formatter: 1 + 2130 record (14 = 3 [1] + 10 + 1) + bid#0,SET test value formatter: 2 + 2144 record (14 = 3 [2] + 10 + 1) + bird#0,SET test value formatter: 2 + 2158 record (14 = 3 [3] + 10 + 1) + birth#0,SET test value formatter: 3 + 2172 record (15 = 3 [2] + 11 + 1) + bites#0,SET test value formatter: 1 + 2187 record (15 = 3 [3] + 11 + 1) + bitter#0,SET test value formatter: 1 + 2202 record (16 = 3 [1] + 12 + 1) + black#0,SET test value formatter: 1 + 2218 record (14 = 3 [3] + 10 + 1) + blast#0,SET test value formatter: 1 + 2232 record (17 = 3 [5] + 13 + 1) + blastments#0,SET test value formatter: 1 + 2249 record (13 = 3 [5] + 9 + 1) + blasts#0,SET test value formatter: 1 + 2262 record (15 = 3 [3] + 11 + 1) + blazes#0,SET test value formatter: 1 + 2277 record (14 = 3 [4] + 10 + 1) + blazon#0,SET test value formatter: 1 + 2291 record (20 = 3 [0] + 16 + 1) [restart] + blessing#0,SET test value formatter: 3 + 2311 record (15 = 3 [2] + 11 + 1) + blood#0,SET test value formatter: 7 + 2326 record (17 = 3 [3] + 13 + 1) + blossoms#0,SET test value formatter: 1 + 2343 record (14 = 3 [3] + 10 + 1) + blows#0,SET test value formatter: 1 + 2357 record (16 = 3 [1] + 12 + 1) + bodes#0,SET test value formatter: 1 + 2373 record (13 = 3 [3] + 9 + 1) + body#0,SET test value formatter: 5 + 2386 record (15 = 3 [2] + 11 + 1) + bonds#0,SET test value formatter: 1 + 2401 record (14 = 3 [3] + 10 + 1) + bones#0,SET test value formatter: 1 + 2415 record (14 = 3 [2] + 10 + 1) + book#0,SET test value formatter: 1 + 2429 record (13 = 3 [4] + 9 + 1) + books#0,SET test value formatter: 1 + 2442 record (14 = 3 [2] + 10 + 1) + born#0,SET test value formatter: 2 + 2456 record (17 = 3 [3] + 13 + 1) + borrower#0,SET test value formatter: 1 + 2473 record (15 = 3 [6] + 11 + 1) + borrowing#0,SET test value formatter: 1 + 2488 record (15 = 3 [2] + 11 + 1) + bosom#0,SET test value formatter: 1 + 2503 record (14 = 3 [2] + 10 + 1) + both#0,SET test value formatter: 3 + 2517 record (15 = 3 [2] + 11 + 1) + bound#0,SET test value formatter: 2 + 2532 record (21 = 3 [0] + 17 + 1) [restart] + bounteous#0,SET test value formatter: 1 + 2553 record (13 = 3 [2] + 9 + 1) + bow#0,SET test value formatter: 1 + 2566 record (13 = 3 [2] + 9 + 1) + boy#0,SET test value formatter: 2 + 2579 record (16 = 3 [1] + 12 + 1) + brain#0,SET test value formatter: 2 + 2595 record (13 = 3 [3] + 9 + 1) + bray#0,SET test value formatter: 1 + 2608 record (15 = 3 [3] + 11 + 1) + brazen#0,SET test value formatter: 1 + 2623 record (16 = 3 [2] + 12 + 1) + breach#0,SET test value formatter: 1 + 2639 record (13 = 3 [4] + 9 + 1) + break#0,SET test value formatter: 3 + 2652 record (15 = 3 [5] + 11 + 1) + breaking#0,SET test value formatter: 1 + 2667 record (14 = 3 [4] + 10 + 1) + breath#0,SET test value formatter: 1 + 2681 record (15 = 3 [6] + 11 + 1) + breathing#0,SET test value formatter: 1 + 2696 record (15 = 3 [2] + 11 + 1) + brief#0,SET test value formatter: 1 + 2711 record (14 = 3 [3] + 10 + 1) + bring#0,SET test value formatter: 1 + 2725 record (17 = 3 [2] + 13 + 1) + brokers#0,SET test value formatter: 1 + 2742 record (16 = 3 [3] + 12 + 1) + brother#0,SET test value formatter: 6 + 2758 record (13 = 3 [3] + 9 + 1) + brow#0,SET test value formatter: 1 + 2771 record (17 = 3 [0] + 13 + 1) [restart] + bruit#0,SET test value formatter: 1 + 2788 record (15 = 3 [1] + 11 + 1) + bulk#0,SET test value formatter: 1 + 2803 record (16 = 3 [2] + 12 + 1) + buried#0,SET test value formatter: 1 + 2819 record (14 = 3 [3] + 10 + 1) + burns#0,SET test value formatter: 2 + 2833 record (13 = 3 [4] + 9 + 1) + burnt#0,SET test value formatter: 1 + 2846 record (14 = 3 [3] + 10 + 1) + burst#0,SET test value formatter: 2 + 2860 record (18 = 3 [2] + 14 + 1) + business#0,SET test value formatter: 4 + 2878 record (14 = 3 [2] + 9 + 2) + but#0,SET test value formatter: 58 + 2892 record (16 = 3 [3] + 12 + 1) + buttons#0,SET test value formatter: 1 + 2908 record (13 = 3 [2] + 9 + 1) + buy#0,SET test value formatter: 1 + 2921 record (14 = 3 [1] + 9 + 2) + by#0,SET test value formatter: 31 + 2935 record (16 = 3 [0] + 12 + 1) + call#0,SET test value formatter: 4 + 2951 record (19 = 3 [3] + 15 + 1) + calumnious#0,SET test value formatter: 1 + 2970 record (14 = 3 [2] + 10 + 1) + came#0,SET test value formatter: 2 + 2984 record (13 = 3 [2] + 9 + 1) + can#0,SET test value formatter: 5 + 2997 record (15 = 3 [3] + 11 + 1) + canker#0,SET test value formatter: 1 + 3012 record (18 = 3 [0] + 14 + 1) [restart] + cannon#0,SET test value formatter: 2 + 3030 record (13 = 3 [5] + 9 + 1) + cannot#0,SET test value formatter: 3 + 3043 record (14 = 3 [3] + 10 + 1) + canon#0,SET test value formatter: 1 + 3057 record (16 = 3 [5] + 12 + 1) + canonized#0,SET test value formatter: 1 + 3073 record (14 = 3 [3] + 10 + 1) + canst#0,SET test value formatter: 2 + 3087 record (13 = 3 [2] + 9 + 1) + cap#0,SET test value formatter: 1 + 3100 record (19 = 3 [2] + 15 + 1) + carefully#0,SET test value formatter: 1 + 3119 record (17 = 3 [3] + 13 + 1) + carriage#0,SET test value formatter: 1 + 3136 record (16 = 3 [4] + 12 + 1) + carrying#0,SET test value formatter: 1 + 3152 record (14 = 3 [3] + 10 + 1) + carve#0,SET test value formatter: 1 + 3166 record (14 = 3 [2] + 10 + 1) + cast#0,SET test value formatter: 3 + 3180 record (14 = 3 [4] + 10 + 1) + castle#0,SET test value formatter: 2 + 3194 record (15 = 3 [2] + 11 + 1) + catch#0,SET test value formatter: 1 + 3209 record (16 = 3 [2] + 12 + 1) + cautel#0,SET test value formatter: 1 + 3225 record (15 = 3 [4] + 11 + 1) + caution#0,SET test value formatter: 1 + 3240 record (21 = 3 [1] + 17 + 1) + celebrated#0,SET test value formatter: 1 + 3261 record (21 = 3 [0] + 17 + 1) [restart] + celestial#0,SET test value formatter: 1 + 3282 record (18 = 3 [3] + 14 + 1) + cellarage#0,SET test value formatter: 1 + 3300 record (17 = 3 [2] + 13 + 1) + censure#0,SET test value formatter: 2 + 3317 record (19 = 3 [2] + 15 + 1) + cerements#0,SET test value formatter: 1 + 3336 record (16 = 3 [3] + 12 + 1) + certain#0,SET test value formatter: 1 + 3352 record (18 = 3 [1] + 14 + 1) + chances#0,SET test value formatter: 1 + 3370 record (14 = 3 [4] + 10 + 1) + change#0,SET test value formatter: 1 + 3384 record (18 = 3 [3] + 14 + 1) + character#0,SET test value formatter: 1 + 3402 record (14 = 3 [4] + 10 + 1) + charge#0,SET test value formatter: 3 + 3416 record (16 = 3 [4] + 12 + 1) + chariest#0,SET test value formatter: 1 + 3432 record (17 = 3 [5] + 13 + 1) + charitable#0,SET test value formatter: 1 + 3449 record (13 = 3 [4] + 9 + 1) + charm#0,SET test value formatter: 1 + 3462 record (15 = 3 [3] + 11 + 1) + chaste#0,SET test value formatter: 1 + 3477 record (15 = 3 [2] + 11 + 1) + cheer#0,SET test value formatter: 1 + 3492 record (15 = 3 [2] + 11 + 1) + chief#0,SET test value formatter: 2 + 3507 record (15 = 3 [5] + 11 + 1) + chiefest#0,SET test value formatter: 1 + 3522 record (18 = 3 [0] + 14 + 1) [restart] + choice#0,SET test value formatter: 2 + 3540 record (15 = 3 [3] + 11 + 1) + choose#0,SET test value formatter: 1 + 3555 record (24 = 3 [1] + 20 + 1) + circumscribed#0,SET test value formatter: 1 + 3579 record (17 = 3 [7] + 13 + 1) + circumstance#0,SET test value formatter: 2 + 3596 record (15 = 3 [1] + 11 + 1) + clad#0,SET test value formatter: 1 + 3611 record (17 = 3 [3] + 13 + 1) + claudius#0,SET test value formatter: 8 + 3628 record (17 = 3 [2] + 13 + 1) + clearly#0,SET test value formatter: 1 + 3645 record (14 = 3 [3] + 10 + 1) + clepe#0,SET test value formatter: 1 + 3659 record (15 = 3 [2] + 11 + 1) + cliff#0,SET test value formatter: 1 + 3674 record (19 = 3 [3] + 15 + 1) + climatures#0,SET test value formatter: 1 + 3693 record (15 = 3 [2] + 11 + 1) + cloak#0,SET test value formatter: 1 + 3708 record (15 = 3 [3] + 11 + 1) + clouds#0,SET test value formatter: 2 + 3723 record (15 = 3 [1] + 11 + 1) + cock#0,SET test value formatter: 5 + 3738 record (14 = 3 [2] + 10 + 1) + cold#0,SET test value formatter: 2 + 3752 record (14 = 3 [4] + 10 + 1) + coldly#0,SET test value formatter: 1 + 3766 record (19 = 3 [3] + 15 + 1) + colleagued#0,SET test value formatter: 1 + 3785 record (18 = 3 [0] + 14 + 1) [restart] + colour#0,SET test value formatter: 1 + 3803 record (16 = 3 [2] + 12 + 1) + combat#0,SET test value formatter: 1 + 3819 record (14 = 3 [6] + 10 + 1) + combated#0,SET test value formatter: 1 + 3833 record (16 = 3 [4] + 12 + 1) + combined#0,SET test value formatter: 1 + 3849 record (14 = 3 [3] + 9 + 2) + come#0,SET test value formatter: 17 + 3863 record (13 = 3 [4] + 9 + 1) + comes#0,SET test value formatter: 7 + 3876 record (13 = 3 [5] + 9 + 1) + comest#0,SET test value formatter: 1 + 3889 record (16 = 3 [3] + 12 + 1) + comfort#0,SET test value formatter: 1 + 3905 record (15 = 3 [3] + 11 + 1) + coming#0,SET test value formatter: 1 + 3920 record (16 = 3 [3] + 12 + 1) + command#0,SET test value formatter: 1 + 3936 record (16 = 3 [7] + 12 + 1) + commandment#0,SET test value formatter: 1 + 3952 record (15 = 3 [4] + 11 + 1) + commend#0,SET test value formatter: 2 + 3967 record (16 = 3 [7] + 12 + 1) + commendable#0,SET test value formatter: 1 + 3983 record (14 = 3 [4] + 10 + 1) + common#0,SET test value formatter: 4 + 3997 record (16 = 3 [3] + 12 + 1) + compact#0,SET test value formatter: 1 + 4013 record (17 = 3 [4] + 13 + 1) + competent#0,SET test value formatter: 1 + 4030 record (20 = 3 [0] + 16 + 1) [restart] + complete#0,SET test value formatter: 1 + 4050 [restart 2046] + 4054 [restart 2291] + 4058 [restart 2532] + 4062 [restart 2771] + 4066 [restart 3012] + 4070 [restart 3261] + 4074 [restart 3522] + 4078 [restart 3785] + 4082 [restart 4030] + 4090 [trailer compression=none checksum=0x334c5e54] + 4095 data (2039) + 4095 record (22 = 3 [0] + 18 + 1) [restart] + complexion#0,SET test value formatter: 1 + 4117 record (20 = 3 [4] + 16 + 1) + compulsatory#0,SET test value formatter: 1 + 4137 record (16 = 3 [3] + 12 + 1) + comrade#0,SET test value formatter: 1 + 4153 record (17 = 3 [2] + 13 + 1) + conceal#0,SET test value formatter: 1 + 4170 record (20 = 3 [3] + 16 + 1) + condolement#0,SET test value formatter: 1 + 4190 record (16 = 3 [3] + 12 + 1) + confess#0,SET test value formatter: 1 + 4206 record (15 = 3 [4] + 11 + 1) + confine#0,SET test value formatter: 1 + 4221 record (13 = 3 [7] + 9 + 1) + confined#0,SET test value formatter: 1 + 4234 record (18 = 3 [3] + 14 + 1) + conqueror#0,SET test value formatter: 1 + 4252 record (16 = 3 [3] + 12 + 1) + consent#0,SET test value formatter: 3 + 4268 record (18 = 3 [4] + 14 + 1) + constantly#0,SET test value formatter: 1 + 4286 record (19 = 3 [3] + 15 + 1) + contagious#0,SET test value formatter: 1 + 4305 record (18 = 3 [4] + 14 + 1) + contracted#0,SET test value formatter: 1 + 4323 record (15 = 3 [5] + 11 + 1) + contrive#0,SET test value formatter: 1 + 4338 record (21 = 3 [3] + 17 + 1) + conveniently#0,SET test value formatter: 1 + 4359 record (14 = 3 [4] + 10 + 1) + convoy#0,SET test value formatter: 1 + 4373 record (18 = 3 [0] + 14 + 1) [restart] + copied#0,SET test value formatter: 1 + 4391 record (19 = 3 [2] + 15 + 1) + cornelius#0,SET test value formatter: 4 + 4410 record (19 = 3 [3] + 15 + 1) + coronation#0,SET test value formatter: 1 + 4429 record (19 = 3 [3] + 15 + 1) + corruption#0,SET test value formatter: 1 + 4448 record (14 = 3 [3] + 10 + 1) + corse#0,SET test value formatter: 2 + 4462 record (16 = 3 [2] + 12 + 1) + costly#0,SET test value formatter: 1 + 4478 record (15 = 3 [2] + 11 + 1) + couch#0,SET test value formatter: 1 + 4493 record (14 = 3 [3] + 10 + 1) + could#0,SET test value formatter: 2 + 4507 record (20 = 3 [3] + 16 + 1) + countenance#0,SET test value formatter: 2 + 4527 record (14 = 3 [5] + 10 + 1) + country#0,SET test value formatter: 1 + 4541 record (15 = 3 [7] + 11 + 1) + countrymen#0,SET test value formatter: 1 + 4556 record (15 = 3 [3] + 11 + 1) + couple#0,SET test value formatter: 1 + 4571 record (15 = 3 [3] + 11 + 1) + course#0,SET test value formatter: 2 + 4586 record (13 = 3 [6] + 9 + 1) + courses#0,SET test value formatter: 1 + 4599 record (13 = 3 [4] + 9 + 1) + court#0,SET test value formatter: 1 + 4612 record (16 = 3 [5] + 12 + 1) + courteous#0,SET test value formatter: 1 + 4628 record (20 = 3 [0] + 16 + 1) [restart] + courtier#0,SET test value formatter: 1 + 4648 record (15 = 3 [3] + 11 + 1) + cousin#0,SET test value formatter: 2 + 4663 record (18 = 3 [2] + 14 + 1) + covenant#0,SET test value formatter: 1 + 4681 record (16 = 3 [1] + 12 + 1) + crack#0,SET test value formatter: 1 + 4697 record (17 = 3 [2] + 13 + 1) + credent#0,SET test value formatter: 1 + 4714 record (17 = 3 [3] + 13 + 1) + crescent#0,SET test value formatter: 1 + 4731 record (13 = 3 [3] + 9 + 1) + crew#0,SET test value formatter: 2 + 4744 record (15 = 3 [2] + 11 + 1) + cried#0,SET test value formatter: 1 + 4759 record (13 = 3 [4] + 9 + 1) + cries#0,SET test value formatter: 1 + 4772 record (15 = 3 [3] + 11 + 1) + crimes#0,SET test value formatter: 1 + 4787 record (15 = 3 [2] + 11 + 1) + cross#0,SET test value formatter: 1 + 4802 record (16 = 3 [3] + 12 + 1) + crowing#0,SET test value formatter: 1 + 4818 record (13 = 3 [4] + 9 + 1) + crown#0,SET test value formatter: 2 + 4831 record (13 = 3 [4] + 9 + 1) + crows#0,SET test value formatter: 1 + 4844 record (15 = 3 [2] + 11 + 1) + crust#0,SET test value formatter: 1 + 4859 record (15 = 3 [1] + 11 + 1) + curd#0,SET test value formatter: 1 + 4874 record (18 = 3 [0] + 14 + 1) [restart] + cursed#0,SET test value formatter: 2 + 4892 record (16 = 3 [2] + 12 + 1) + custom#0,SET test value formatter: 3 + 4908 record (15 = 3 [6] + 11 + 1) + customary#0,SET test value formatter: 1 + 4923 record (13 = 3 [2] + 9 + 1) + cut#0,SET test value formatter: 1 + 4936 record (14 = 3 [0] + 9 + 2) + d#0,SET test value formatter: 54 + 4950 record (16 = 3 [1] + 12 + 1) + daily#0,SET test value formatter: 1 + 4966 record (19 = 3 [2] + 15 + 1) + dalliance#0,SET test value formatter: 1 + 4985 record (14 = 3 [2] + 10 + 1) + damn#0,SET test value formatter: 1 + 4999 record (14 = 3 [4] + 10 + 1) + damned#0,SET test value formatter: 2 + 5013 record (14 = 3 [2] + 10 + 1) + dane#0,SET test value formatter: 3 + 5027 record (15 = 3 [3] + 11 + 1) + danger#0,SET test value formatter: 1 + 5042 record (15 = 3 [2] + 11 + 1) + dared#0,SET test value formatter: 1 + 5057 record (13 = 3 [4] + 9 + 1) + dares#0,SET test value formatter: 1 + 5070 record (18 = 3 [2] + 14 + 1) + daughter#0,SET test value formatter: 2 + 5088 record (17 = 3 [2] + 13 + 1) + dawning#0,SET test value formatter: 1 + 5105 record (13 = 3 [2] + 9 + 1) + day#0,SET test value formatter: 8 + 5118 record (16 = 3 [0] + 12 + 1) [restart] + days#0,SET test value formatter: 1 + 5134 record (15 = 3 [1] + 11 + 1) + dead#0,SET test value formatter: 7 + 5149 record (13 = 3 [3] + 9 + 1) + dear#0,SET test value formatter: 4 + 5162 record (15 = 3 [4] + 11 + 1) + dearest#0,SET test value formatter: 2 + 5177 record (14 = 3 [4] + 10 + 1) + dearly#0,SET test value formatter: 1 + 5191 record (14 = 3 [3] + 10 + 1) + death#0,SET test value formatter: 6 + 5205 record (17 = 3 [2] + 13 + 1) + decline#0,SET test value formatter: 1 + 5222 record (14 = 3 [2] + 10 + 1) + deed#0,SET test value formatter: 1 + 5236 record (13 = 3 [4] + 9 + 1) + deeds#0,SET test value formatter: 1 + 5249 record (13 = 3 [3] + 9 + 1) + deep#0,SET test value formatter: 1 + 5262 record (18 = 3 [2] + 14 + 1) + defeated#0,SET test value formatter: 1 + 5280 record (14 = 3 [4] + 10 + 1) + defect#0,SET test value formatter: 1 + 5294 record (14 = 3 [4] + 10 + 1) + defend#0,SET test value formatter: 1 + 5308 record (18 = 3 [2] + 14 + 1) + dejected#0,SET test value formatter: 1 + 5326 record (17 = 3 [2] + 13 + 1) + delated#0,SET test value formatter: 1 + 5343 record (16 = 3 [3] + 12 + 1) + delight#0,SET test value formatter: 1 + 5359 record (19 = 3 [0] + 15 + 1) [restart] + deliver#0,SET test value formatter: 2 + 5378 record (22 = 3 [2] + 18 + 1) + demonstrated#0,SET test value formatter: 1 + 5400 record (18 = 3 [2] + 13 + 2) + denmark#0,SET test value formatter: 13 + 5418 record (15 = 3 [3] + 11 + 1) + denote#0,SET test value formatter: 1 + 5433 record (16 = 3 [2] + 12 + 1) + depart#0,SET test value formatter: 1 + 5449 record (16 = 3 [3] + 12 + 1) + depends#0,SET test value formatter: 1 + 5465 record (16 = 3 [3] + 12 + 1) + deprive#0,SET test value formatter: 1 + 5481 record (16 = 3 [2] + 12 + 1) + design#0,SET test value formatter: 1 + 5497 record (14 = 3 [4] + 10 + 1) + desire#0,SET test value formatter: 6 + 5511 record (18 = 3 [3] + 14 + 1) + desperate#0,SET test value formatter: 1 + 5529 record (15 = 3 [8] + 11 + 1) + desperation#0,SET test value formatter: 1 + 5544 record (13 = 3 [2] + 9 + 1) + dew#0,SET test value formatter: 3 + 5557 record (13 = 3 [3] + 9 + 1) + dews#0,SET test value formatter: 1 + 5570 record (19 = 3 [2] + 15 + 1) + dexterity#0,SET test value formatter: 1 + 5589 record (15 = 3 [1] + 10 + 2) + did#0,SET test value formatter: 14 + 5604 record (14 = 3 [3] + 10 + 1) + didst#0,SET test value formatter: 1 + 5618 record (15 = 3 [0] + 11 + 1) [restart] + die#0,SET test value formatter: 1 + 5633 record (13 = 3 [3] + 9 + 1) + died#0,SET test value formatter: 1 + 5646 record (13 = 3 [3] + 9 + 1) + diet#0,SET test value formatter: 1 + 5659 record (17 = 3 [2] + 13 + 1) + dignity#0,SET test value formatter: 1 + 5676 record (16 = 3 [2] + 12 + 1) + direct#0,SET test value formatter: 1 + 5692 record (14 = 3 [3] + 10 + 1) + dirge#0,SET test value formatter: 1 + 5706 record (22 = 3 [2] + 18 + 1) + disappointed#0,SET test value formatter: 1 + 5728 record (17 = 3 [4] + 13 + 1) + disasters#0,SET test value formatter: 1 + 5745 record (18 = 3 [3] + 14 + 1) + disclosed#0,SET test value formatter: 1 + 5763 record (17 = 3 [4] + 13 + 1) + discourse#0,SET test value formatter: 1 + 5780 record (18 = 3 [4] + 14 + 1) + discretion#0,SET test value formatter: 1 + 5798 record (17 = 3 [3] + 13 + 1) + disjoint#0,SET test value formatter: 1 + 5815 record (17 = 3 [3] + 13 + 1) + dispatch#0,SET test value formatter: 2 + 5832 record (19 = 3 [4] + 15 + 1) + disposition#0,SET test value formatter: 3 + 5851 record (18 = 3 [3] + 14 + 1) + distilled#0,SET test value formatter: 1 + 5869 record (16 = 3 [6] + 12 + 1) + distilment#0,SET test value formatter: 1 + 5885 record (22 = 3 [0] + 18 + 1) [restart] + distracted#0,SET test value formatter: 1 + 5907 record (16 = 3 [2] + 12 + 1) + divide#0,SET test value formatter: 1 + 5923 record (14 = 3 [1] + 9 + 2) + do#0,SET test value formatter: 36 + 5937 record (14 = 3 [2] + 10 + 1) + does#0,SET test value formatter: 3 + 5951 record (14 = 3 [2] + 10 + 1) + dole#0,SET test value formatter: 1 + 5965 record (14 = 3 [2] + 10 + 1) + done#0,SET test value formatter: 3 + 5979 record (14 = 3 [2] + 10 + 1) + doom#0,SET test value formatter: 1 + 5993 record (16 = 3 [4] + 12 + 1) + doomsday#0,SET test value formatter: 1 + 6009 record (14 = 3 [2] + 10 + 1) + doth#0,SET test value formatter: 7 + 6023 record (16 = 3 [2] + 12 + 1) + double#0,SET test value formatter: 2 + 6039 record (13 = 3 [4] + 9 + 1) + doubt#0,SET test value formatter: 4 + 6052 record (15 = 3 [5] + 11 + 1) + doubtful#0,SET test value formatter: 1 + 6067 record (14 = 3 [2] + 10 + 1) + down#0,SET test value formatter: 7 + 6081 record (17 = 3 [1] + 13 + 1) + drains#0,SET test value formatter: 1 + 6098 [restart 4095] + 6102 [restart 4373] + 6106 [restart 4628] + 6110 [restart 4874] + 6114 [restart 5118] + 6118 [restart 5359] + 6122 [restart 5618] + 6126 [restart 5885] + 6134 [trailer compression=none checksum=0xa3c2c00f] + 6139 data (2036) + 6139 record (16 = 3 [0] + 12 + 1) [restart] + dram#0,SET test value formatter: 1 + 6155 record (17 = 3 [3] + 13 + 1) + draughts#0,SET test value formatter: 1 + 6172 record (13 = 3 [3] + 9 + 1) + draw#0,SET test value formatter: 1 + 6185 record (13 = 3 [4] + 9 + 1) + draws#0,SET test value formatter: 1 + 6198 record (15 = 3 [2] + 11 + 1) + dread#0,SET test value formatter: 1 + 6213 record (14 = 3 [5] + 10 + 1) + dreaded#0,SET test value formatter: 1 + 6227 record (15 = 3 [5] + 11 + 1) + dreadful#0,SET test value formatter: 2 + 6242 record (13 = 3 [4] + 9 + 1) + dream#0,SET test value formatter: 1 + 6255 record (13 = 3 [5] + 9 + 1) + dreamt#0,SET test value formatter: 1 + 6268 record (15 = 3 [2] + 11 + 1) + drink#0,SET test value formatter: 1 + 6283 record (13 = 3 [5] + 9 + 1) + drinks#0,SET test value formatter: 1 + 6296 record (18 = 3 [2] + 14 + 1) + dropping#0,SET test value formatter: 1 + 6314 record (13 = 3 [8] + 9 + 1) + droppings#0,SET test value formatter: 1 + 6327 record (14 = 3 [2] + 10 + 1) + drum#0,SET test value formatter: 1 + 6341 record (18 = 3 [3] + 14 + 1) + drunkards#0,SET test value formatter: 1 + 6359 record (15 = 3 [1] + 11 + 1) + dull#0,SET test value formatter: 1 + 6374 record (18 = 3 [0] + 14 + 1) [restart] + duller#0,SET test value formatter: 1 + 6392 record (13 = 3 [4] + 9 + 1) + dulls#0,SET test value formatter: 1 + 6405 record (14 = 3 [2] + 10 + 1) + dumb#0,SET test value formatter: 2 + 6419 record (14 = 3 [2] + 10 + 1) + dust#0,SET test value formatter: 1 + 6433 record (16 = 3 [2] + 12 + 1) + duties#0,SET test value formatter: 1 + 6449 record (13 = 3 [3] + 9 + 1) + duty#0,SET test value formatter: 7 + 6462 record (19 = 3 [1] + 15 + 1) + dwelling#0,SET test value formatter: 1 + 6481 record (14 = 3 [1] + 10 + 1) + dye#0,SET test value formatter: 1 + 6495 record (13 = 3 [0] + 9 + 1) + e#0,SET test value formatter: 1 + 6508 record (15 = 3 [1] + 11 + 1) + each#0,SET test value formatter: 5 + 6523 record (15 = 3 [2] + 11 + 1) + eager#0,SET test value formatter: 2 + 6538 record (14 = 3 [2] + 10 + 1) + eale#0,SET test value formatter: 1 + 6552 record (13 = 3 [2] + 9 + 1) + ear#0,SET test value formatter: 5 + 6565 record (13 = 3 [3] + 9 + 1) + ears#0,SET test value formatter: 3 + 6578 record (14 = 3 [3] + 10 + 1) + earth#0,SET test value formatter: 9 + 6592 record (14 = 3 [5] + 10 + 1) + earthly#0,SET test value formatter: 1 + 6606 record (16 = 3 [0] + 12 + 1) [restart] + ease#0,SET test value formatter: 2 + 6622 record (13 = 3 [3] + 9 + 1) + east#0,SET test value formatter: 1 + 6635 record (16 = 3 [4] + 12 + 1) + eastward#0,SET test value formatter: 1 + 6651 record (18 = 3 [1] + 14 + 1) + eclipse#0,SET test value formatter: 1 + 6669 record (15 = 3 [1] + 11 + 1) + edge#0,SET test value formatter: 1 + 6684 record (17 = 3 [1] + 13 + 1) + effect#0,SET test value formatter: 2 + 6701 record (17 = 3 [1] + 13 + 1) + eleven#0,SET test value formatter: 1 + 6718 record (14 = 3 [2] + 10 + 1) + else#0,SET test value formatter: 4 + 6732 record (17 = 3 [3] + 13 + 1) + elsinore#0,SET test value formatter: 2 + 6749 record (17 = 3 [1] + 13 + 1) + embark#0,SET test value formatter: 1 + 6766 record (16 = 3 [2] + 12 + 1) + empire#0,SET test value formatter: 1 + 6782 record (17 = 3 [2] + 13 + 1) + emulate#0,SET test value formatter: 1 + 6799 record (13 = 3 [1] + 9 + 1) + en#0,SET test value formatter: 2 + 6812 record (19 = 3 [2] + 15 + 1) + encounter#0,SET test value formatter: 1 + 6831 record (17 = 3 [3] + 13 + 1) + encumber#0,SET test value formatter: 1 + 6848 record (13 = 3 [2] + 9 + 1) + end#0,SET test value formatter: 1 + 6861 record (17 = 3 [0] + 13 + 1) [restart] + enemy#0,SET test value formatter: 1 + 6878 record (16 = 3 [2] + 12 + 1) + enmity#0,SET test value formatter: 1 + 6894 record (16 = 3 [2] + 12 + 1) + enough#0,SET test value formatter: 1 + 6910 record (16 = 3 [2] + 11 + 2) + enter#0,SET test value formatter: 12 + 6926 record (17 = 3 [5] + 13 + 1) + enterprise#0,SET test value formatter: 1 + 6943 record (20 = 3 [5] + 16 + 1) + entertainment#0,SET test value formatter: 1 + 6963 record (17 = 3 [3] + 13 + 1) + entrance#0,SET test value formatter: 1 + 6980 record (17 = 3 [4] + 13 + 1) + entreated#0,SET test value formatter: 1 + 6997 record (17 = 3 [7] + 13 + 1) + entreatments#0,SET test value formatter: 1 + 7014 record (16 = 3 [1] + 12 + 1) + equal#0,SET test value formatter: 1 + 7030 record (13 = 3 [1] + 9 + 1) + er#0,SET test value formatter: 5 + 7043 record (13 = 3 [2] + 9 + 1) + ere#0,SET test value formatter: 4 + 7056 record (18 = 3 [2] + 14 + 1) + ergrowth#0,SET test value formatter: 1 + 7074 record (18 = 3 [2] + 14 + 1) + ermaster#0,SET test value formatter: 1 + 7092 record (16 = 3 [2] + 12 + 1) + erring#0,SET test value formatter: 1 + 7108 record (18 = 3 [2] + 14 + 1) + eruption#0,SET test value formatter: 1 + 7126 record (19 = 3 [0] + 15 + 1) [restart] + erwhelm#0,SET test value formatter: 1 + 7145 record (17 = 3 [1] + 13 + 1) + esteem#0,SET test value formatter: 1 + 7162 record (13 = 3 [1] + 9 + 1) + et#0,SET test value formatter: 1 + 7175 record (17 = 3 [2] + 13 + 1) + eternal#0,SET test value formatter: 1 + 7192 record (15 = 3 [5] + 11 + 1) + eternity#0,SET test value formatter: 1 + 7207 record (15 = 3 [1] + 11 + 1) + even#0,SET test value formatter: 8 + 7222 record (14 = 3 [4] + 10 + 1) + events#0,SET test value formatter: 1 + 7236 record (13 = 3 [3] + 9 + 1) + ever#0,SET test value formatter: 6 + 7249 record (19 = 3 [4] + 15 + 1) + everlasting#0,SET test value formatter: 1 + 7268 record (13 = 3 [4] + 9 + 1) + every#0,SET test value formatter: 3 + 7281 record (18 = 3 [1] + 14 + 1) + exactly#0,SET test value formatter: 1 + 7299 record (19 = 3 [2] + 15 + 1) + excellent#0,SET test value formatter: 1 + 7318 record (16 = 3 [2] + 12 + 1) + exeunt#0,SET test value formatter: 8 + 7334 record (14 = 3 [2] + 10 + 1) + exit#0,SET test value formatter: 6 + 7348 record (17 = 3 [2] + 13 + 1) + express#0,SET test value formatter: 2 + 7365 record (17 = 3 [2] + 13 + 1) + extinct#0,SET test value formatter: 1 + 7382 record (20 = 3 [0] + 16 + 1) [restart] + extorted#0,SET test value formatter: 1 + 7402 record (20 = 3 [3] + 16 + 1) + extravagant#0,SET test value formatter: 1 + 7422 record (14 = 3 [1] + 10 + 1) + eye#0,SET test value formatter: 6 + 7436 record (13 = 3 [3] + 9 + 1) + eyes#0,SET test value formatter: 7 + 7449 record (16 = 3 [0] + 12 + 1) + face#0,SET test value formatter: 2 + 7465 record (15 = 3 [2] + 11 + 1) + faded#0,SET test value formatter: 1 + 7480 record (14 = 3 [2] + 10 + 1) + fail#0,SET test value formatter: 1 + 7494 record (13 = 3 [3] + 9 + 1) + fair#0,SET test value formatter: 3 + 7507 record (13 = 3 [4] + 9 + 1) + fairy#0,SET test value formatter: 1 + 7520 record (14 = 3 [3] + 10 + 1) + faith#0,SET test value formatter: 4 + 7534 record (17 = 3 [2] + 13 + 1) + falling#0,SET test value formatter: 1 + 7551 record (14 = 3 [3] + 10 + 1) + false#0,SET test value formatter: 1 + 7565 record (18 = 3 [2] + 14 + 1) + familiar#0,SET test value formatter: 1 + 7583 record (15 = 3 [2] + 11 + 1) + fancy#0,SET test value formatter: 1 + 7598 record (16 = 3 [3] + 12 + 1) + fantasy#0,SET test value formatter: 2 + 7614 record (13 = 3 [2] + 9 + 1) + far#0,SET test value formatter: 2 + 7627 record (16 = 3 [0] + 12 + 1) [restart] + fare#0,SET test value formatter: 2 + 7643 record (16 = 3 [4] + 12 + 1) + farewell#0,SET test value formatter: 8 + 7659 record (17 = 3 [2] + 13 + 1) + fashion#0,SET test value formatter: 3 + 7676 record (13 = 3 [3] + 9 + 1) + fast#0,SET test value formatter: 2 + 7689 record (13 = 3 [2] + 9 + 1) + fat#0,SET test value formatter: 1 + 7702 record (13 = 3 [3] + 9 + 1) + fate#0,SET test value formatter: 2 + 7715 record (13 = 3 [4] + 9 + 1) + fates#0,SET test value formatter: 1 + 7728 record (16 = 3 [3] + 11 + 2) + father#0,SET test value formatter: 28 + 7744 record (13 = 3 [6] + 9 + 1) + fathers#0,SET test value formatter: 1 + 7757 record (15 = 3 [4] + 11 + 1) + fathoms#0,SET test value formatter: 1 + 7772 record (15 = 3 [2] + 11 + 1) + fault#0,SET test value formatter: 4 + 7787 record (16 = 3 [2] + 12 + 1) + favour#0,SET test value formatter: 2 + 7803 record (15 = 3 [1] + 11 + 1) + fear#0,SET test value formatter: 9 + 7818 record (15 = 3 [4] + 11 + 1) + fearful#0,SET test value formatter: 1 + 7833 record (13 = 3 [2] + 9 + 1) + fed#0,SET test value formatter: 1 + 7846 record (13 = 3 [2] + 9 + 1) + fee#0,SET test value formatter: 1 + 7859 record (16 = 3 [0] + 12 + 1) [restart] + fell#0,SET test value formatter: 2 + 7875 record (14 = 3 [4] + 10 + 1) + fellow#0,SET test value formatter: 2 + 7889 record (13 = 3 [2] + 9 + 1) + few#0,SET test value formatter: 3 + 7902 record (14 = 3 [1] + 10 + 1) + fie#0,SET test value formatter: 4 + 7916 record (15 = 3 [3] + 11 + 1) + fierce#0,SET test value formatter: 1 + 7931 record (16 = 3 [2] + 12 + 1) + figure#0,SET test value formatter: 3 + 7947 record (16 = 3 [2] + 12 + 1) + filial#0,SET test value formatter: 1 + 7963 record (14 = 3 [2] + 10 + 1) + find#0,SET test value formatter: 2 + 7977 record (16 = 3 [3] + 12 + 1) + fingers#0,SET test value formatter: 1 + 7993 record (14 = 3 [2] + 10 + 1) + fire#0,SET test value formatter: 4 + 8007 record (13 = 3 [4] + 9 + 1) + fires#0,SET test value formatter: 1 + 8020 record (14 = 3 [3] + 10 + 1) + first#0,SET test value formatter: 1 + 8034 record (13 = 3 [2] + 9 + 1) + fit#0,SET test value formatter: 2 + 8047 record (13 = 3 [3] + 9 + 1) + fits#0,SET test value formatter: 1 + 8060 record (16 = 3 [3] + 12 + 1) + fitting#0,SET test value formatter: 1 + 8076 record (13 = 3 [2] + 9 + 1) + fix#0,SET test value formatter: 2 + 8089 record (18 = 3 [0] + 14 + 1) [restart] + flames#0,SET test value formatter: 1 + 8107 record (13 = 3 [3] + 9 + 1) + flat#0,SET test value formatter: 1 + 8120 record (15 = 3 [2] + 11 + 1) + flesh#0,SET test value formatter: 2 + 8135 [restart 6139] + 8139 [restart 6374] + 8143 [restart 6606] + 8147 [restart 6861] + 8151 [restart 7126] + 8155 [restart 7382] + 8159 [restart 7627] + 8163 [restart 7859] + 8167 [restart 8089] + 8175 [trailer compression=none checksum=0xc4a3b6e0] + 8180 data (2032) + 8180 record (17 = 3 [0] + 13 + 1) [restart] + flood#0,SET test value formatter: 1 + 8197 record (17 = 3 [3] + 13 + 1) + flourish#0,SET test value formatter: 1 + 8214 record (18 = 3 [2] + 14 + 1) + flushing#0,SET test value formatter: 1 + 8232 record (14 = 3 [1] + 10 + 1) + foe#0,SET test value formatter: 1 + 8246 record (16 = 3 [2] + 12 + 1) + follow#0,SET test value formatter: 9 + 8262 record (13 = 3 [6] + 9 + 1) + follows#0,SET test value formatter: 1 + 8275 record (14 = 3 [2] + 10 + 1) + fond#0,SET test value formatter: 1 + 8289 record (14 = 3 [2] + 10 + 1) + food#0,SET test value formatter: 1 + 8303 record (13 = 3 [3] + 9 + 1) + fool#0,SET test value formatter: 1 + 8316 record (13 = 3 [4] + 9 + 1) + fools#0,SET test value formatter: 1 + 8329 record (13 = 3 [3] + 9 + 1) + foot#0,SET test value formatter: 1 + 8342 record (14 = 3 [2] + 9 + 2) + for#0,SET test value formatter: 45 + 8356 record (15 = 3 [3] + 11 + 1) + forbid#0,SET test value formatter: 1 + 8371 record (15 = 3 [3] + 11 + 1) + forced#0,SET test value formatter: 1 + 8386 record (16 = 3 [3] + 12 + 1) + foreign#0,SET test value formatter: 1 + 8402 record (19 = 3 [4] + 15 + 1) + foreknowing#0,SET test value formatter: 1 + 8421 record (20 = 3 [0] + 16 + 1) [restart] + foresaid#0,SET test value formatter: 1 + 8441 record (16 = 3 [3] + 12 + 1) + forfeit#0,SET test value formatter: 1 + 8457 record (15 = 3 [3] + 11 + 1) + forged#0,SET test value formatter: 1 + 8472 record (13 = 3 [5] + 9 + 1) + forget#0,SET test value formatter: 1 + 8485 record (13 = 3 [3] + 9 + 1) + form#0,SET test value formatter: 4 + 8498 record (13 = 3 [4] + 9 + 1) + forms#0,SET test value formatter: 2 + 8511 record (14 = 3 [3] + 10 + 1) + forth#0,SET test value formatter: 3 + 8525 record (17 = 3 [4] + 13 + 1) + fortified#0,SET test value formatter: 1 + 8542 record (17 = 3 [5] + 13 + 1) + fortinbras#0,SET test value formatter: 6 + 8559 record (13 = 3 [4] + 9 + 1) + forts#0,SET test value formatter: 1 + 8572 record (15 = 3 [4] + 11 + 1) + fortune#0,SET test value formatter: 1 + 8587 record (16 = 3 [3] + 12 + 1) + forward#0,SET test value formatter: 1 + 8603 record (16 = 3 [2] + 12 + 1) + fought#0,SET test value formatter: 1 + 8619 record (13 = 3 [3] + 9 + 1) + foul#0,SET test value formatter: 6 + 8632 record (18 = 3 [1] + 14 + 1) + frailty#0,SET test value formatter: 1 + 8650 record (14 = 3 [3] + 10 + 1) + frame#0,SET test value formatter: 1 + 8664 record (18 = 3 [0] + 14 + 1) [restart] + france#0,SET test value formatter: 3 + 8682 record (17 = 3 [5] + 12 + 2) + francisco#0,SET test value formatter: 10 + 8699 record (14 = 3 [2] + 10 + 1) + free#0,SET test value formatter: 1 + 8713 record (14 = 3 [4] + 10 + 1) + freely#0,SET test value formatter: 1 + 8727 record (14 = 3 [4] + 10 + 1) + freeze#0,SET test value formatter: 1 + 8741 record (16 = 3 [3] + 12 + 1) + fretful#0,SET test value formatter: 1 + 8757 record (16 = 3 [2] + 12 + 1) + friend#0,SET test value formatter: 3 + 8773 record (15 = 3 [6] + 11 + 1) + friending#0,SET test value formatter: 1 + 8788 record (13 = 3 [6] + 9 + 1) + friends#0,SET test value formatter: 5 + 8801 record (15 = 3 [2] + 10 + 2) + from#0,SET test value formatter: 21 + 8816 record (14 = 3 [3] + 10 + 1) + frown#0,SET test value formatter: 1 + 8830 record (17 = 3 [5] + 13 + 1) + frowningly#0,SET test value formatter: 1 + 8847 record (18 = 3 [2] + 14 + 1) + fruitful#0,SET test value formatter: 1 + 8865 record (15 = 3 [1] + 11 + 1) + full#0,SET test value formatter: 2 + 8880 record (17 = 3 [2] + 13 + 1) + funeral#0,SET test value formatter: 3 + 8897 record (17 = 3 [2] + 13 + 1) + furnish#0,SET test value formatter: 1 + 8914 record (19 = 3 [0] + 15 + 1) [restart] + further#0,SET test value formatter: 4 + 8933 record (17 = 3 [0] + 13 + 1) + gaged#0,SET test value formatter: 1 + 8950 record (16 = 3 [2] + 12 + 1) + gainst#0,SET test value formatter: 2 + 8966 record (13 = 3 [3] + 9 + 1) + gait#0,SET test value formatter: 1 + 8979 record (16 = 3 [2] + 12 + 1) + galled#0,SET test value formatter: 1 + 8995 record (13 = 3 [4] + 9 + 1) + galls#0,SET test value formatter: 1 + 9008 record (14 = 3 [2] + 10 + 1) + gape#0,SET test value formatter: 1 + 9022 record (17 = 3 [2] + 13 + 1) + garbage#0,SET test value formatter: 1 + 9039 record (15 = 3 [3] + 11 + 1) + garden#0,SET test value formatter: 1 + 9054 record (15 = 3 [2] + 11 + 1) + gates#0,SET test value formatter: 1 + 9069 record (15 = 3 [2] + 11 + 1) + gaudy#0,SET test value formatter: 1 + 9084 record (18 = 3 [1] + 14 + 1) + general#0,SET test value formatter: 1 + 9102 record (15 = 3 [5] + 11 + 1) + generous#0,SET test value formatter: 1 + 9117 record (15 = 3 [3] + 11 + 1) + gentle#0,SET test value formatter: 1 + 9132 record (15 = 3 [6] + 11 + 1) + gentlemen#0,SET test value formatter: 5 + 9147 record (18 = 3 [2] + 14 + 1) + gertrude#0,SET test value formatter: 4 + 9165 record (15 = 3 [0] + 11 + 1) [restart] + get#0,SET test value formatter: 1 + 9180 record (17 = 3 [1] + 12 + 2) + ghost#0,SET test value formatter: 26 + 9197 record (17 = 3 [1] + 13 + 1) + gibber#0,SET test value formatter: 1 + 9214 record (15 = 3 [2] + 11 + 1) + gifts#0,SET test value formatter: 3 + 9229 record (14 = 3 [2] + 10 + 1) + gins#0,SET test value formatter: 1 + 9243 record (14 = 3 [2] + 10 + 1) + girl#0,SET test value formatter: 1 + 9257 record (15 = 3 [2] + 10 + 2) + give#0,SET test value formatter: 13 + 9272 record (13 = 3 [4] + 9 + 1) + given#0,SET test value formatter: 4 + 9285 record (15 = 3 [3] + 11 + 1) + giving#0,SET test value formatter: 3 + 9300 record (15 = 3 [1] + 11 + 1) + glad#0,SET test value formatter: 2 + 9315 record (18 = 3 [2] + 14 + 1) + glimpses#0,SET test value formatter: 1 + 9333 record (15 = 3 [2] + 11 + 1) + globe#0,SET test value formatter: 1 + 9348 record (13 = 3 [3] + 9 + 1) + glow#0,SET test value formatter: 1 + 9361 record (14 = 3 [1] + 9 + 2) + go#0,SET test value formatter: 15 + 9375 record (16 = 3 [2] + 12 + 1) + goblin#0,SET test value formatter: 1 + 9391 record (13 = 3 [2] + 9 + 1) + god#0,SET test value formatter: 8 + 9404 record (16 = 3 [0] + 12 + 1) [restart] + goes#0,SET test value formatter: 3 + 9420 record (15 = 3 [2] + 11 + 1) + going#0,SET test value formatter: 1 + 9435 record (14 = 3 [2] + 10 + 1) + gone#0,SET test value formatter: 4 + 9449 record (15 = 3 [2] + 10 + 2) + good#0,SET test value formatter: 20 + 9464 record (14 = 3 [4] + 10 + 1) + goodly#0,SET test value formatter: 1 + 9478 record (16 = 3 [1] + 12 + 1) + grace#0,SET test value formatter: 6 + 9494 record (13 = 3 [5] + 9 + 1) + graces#0,SET test value formatter: 1 + 9507 record (16 = 3 [4] + 12 + 1) + gracious#0,SET test value formatter: 2 + 9523 record (16 = 3 [3] + 12 + 1) + grapple#0,SET test value formatter: 1 + 9539 record (14 = 3 [3] + 10 + 1) + grave#0,SET test value formatter: 1 + 9553 record (13 = 3 [5] + 9 + 1) + graves#0,SET test value formatter: 1 + 9566 record (15 = 3 [2] + 11 + 1) + great#0,SET test value formatter: 1 + 9581 record (16 = 3 [5] + 12 + 1) + greatness#0,SET test value formatter: 1 + 9597 record (14 = 3 [3] + 10 + 1) + green#0,SET test value formatter: 2 + 9611 record (16 = 3 [4] + 12 + 1) + greeting#0,SET test value formatter: 1 + 9627 record (15 = 3 [2] + 11 + 1) + grief#0,SET test value formatter: 3 + 9642 record (20 = 3 [0] + 16 + 1) [restart] + grizzled#0,SET test value formatter: 1 + 9662 record (15 = 3 [2] + 11 + 1) + gross#0,SET test value formatter: 2 + 9677 record (15 = 3 [3] + 11 + 1) + ground#0,SET test value formatter: 3 + 9692 record (13 = 3 [3] + 9 + 1) + grow#0,SET test value formatter: 2 + 9705 record (13 = 3 [4] + 9 + 1) + grown#0,SET test value formatter: 1 + 9718 record (13 = 3 [4] + 9 + 1) + grows#0,SET test value formatter: 2 + 9731 record (16 = 3 [1] + 12 + 1) + guard#0,SET test value formatter: 1 + 9747 record (16 = 3 [2] + 12 + 1) + guilty#0,SET test value formatter: 2 + 9763 record (14 = 3 [0] + 10 + 1) + ha#0,SET test value formatter: 1 + 9777 record (15 = 3 [2] + 11 + 1) + habit#0,SET test value formatter: 2 + 9792 record (14 = 3 [2] + 9 + 2) + had#0,SET test value formatter: 13 + 9806 record (14 = 3 [2] + 10 + 1) + hail#0,SET test value formatter: 1 + 9820 record (13 = 3 [3] + 9 + 1) + hair#0,SET test value formatter: 1 + 9833 record (16 = 3 [2] + 12 + 1) + hallow#0,SET test value formatter: 1 + 9849 record (18 = 3 [2] + 12 + 3) + hamlet#0,SET test value formatter: 100 + 9867 record (14 = 3 [2] + 10 + 1) + hand#0,SET test value formatter: 5 + 9881 record (17 = 3 [0] + 13 + 1) [restart] + hands#0,SET test value formatter: 4 + 9898 record (13 = 3 [3] + 9 + 1) + hang#0,SET test value formatter: 2 + 9911 record (13 = 3 [2] + 9 + 1) + hap#0,SET test value formatter: 1 + 9924 record (16 = 3 [3] + 12 + 1) + happily#0,SET test value formatter: 1 + 9940 record (20 = 3 [2] + 16 + 1) + harbingers#0,SET test value formatter: 1 + 9960 record (13 = 3 [3] + 9 + 1) + hard#0,SET test value formatter: 2 + 9973 record (13 = 3 [4] + 9 + 1) + hardy#0,SET test value formatter: 1 + 9986 record (15 = 3 [3] + 11 + 1) + harrow#0,SET test value formatter: 1 + 10001 record (13 = 3 [6] + 9 + 1) + harrows#0,SET test value formatter: 1 + 10014 record (13 = 3 [2] + 9 + 1) + has#0,SET test value formatter: 3 + 10027 record (13 = 3 [3] + 9 + 1) + hast#0,SET test value formatter: 4 + 10040 record (13 = 3 [4] + 9 + 1) + haste#0,SET test value formatter: 7 + 10053 record (15 = 3 [2] + 11 + 1) + hatch#0,SET test value formatter: 1 + 10068 record (14 = 3 [3] + 9 + 2) + hath#0,SET test value formatter: 15 + 10082 record (15 = 3 [2] + 10 + 2) + have#0,SET test value formatter: 31 + 10097 record (15 = 3 [3] + 11 + 1) + havior#0,SET test value formatter: 1 + 10112 record (15 = 3 [0] + 10 + 2) [restart] + he#0,SET test value formatter: 34 + 10127 record (14 = 3 [2] + 10 + 1) + head#0,SET test value formatter: 6 + 10141 record (14 = 3 [4] + 10 + 1) + headed#0,SET test value formatter: 1 + 10155 record (17 = 3 [4] + 13 + 1) + headshake#0,SET test value formatter: 1 + 10172 [restart 8180] + 10176 [restart 8421] + 10180 [restart 8664] + 10184 [restart 8914] + 10188 [restart 9165] + 10192 [restart 9404] + 10196 [restart 9642] + 10200 [restart 9881] + 10204 [restart 10112] + 10212 [trailer compression=none checksum=0x23db05a] + 10217 data (2042) + 10217 record (18 = 3 [0] + 14 + 1) [restart] + health#0,SET test value formatter: 3 + 10235 record (13 = 3 [3] + 9 + 1) + hear#0,SET test value formatter: 9 + 10248 record (13 = 3 [4] + 9 + 1) + heard#0,SET test value formatter: 4 + 10261 record (15 = 3 [4] + 11 + 1) + hearing#0,SET test value formatter: 1 + 10276 record (13 = 3 [4] + 9 + 1) + hears#0,SET test value formatter: 2 + 10289 record (14 = 3 [5] + 10 + 1) + hearsed#0,SET test value formatter: 1 + 10303 record (14 = 3 [4] + 9 + 2) + heart#0,SET test value formatter: 10 + 10317 record (15 = 3 [5] + 11 + 1) + heartily#0,SET test value formatter: 3 + 10332 record (13 = 3 [5] + 9 + 1) + hearts#0,SET test value formatter: 1 + 10345 record (13 = 3 [3] + 9 + 1) + heat#0,SET test value formatter: 1 + 10358 record (16 = 3 [3] + 11 + 2) + heaven#0,SET test value formatter: 21 + 10374 record (13 = 3 [6] + 9 + 1) + heavens#0,SET test value formatter: 1 + 10387 record (13 = 3 [4] + 9 + 1) + heavy#0,SET test value formatter: 1 + 10400 record (17 = 3 [2] + 13 + 1) + hebenon#0,SET test value formatter: 1 + 10417 record (16 = 3 [2] + 12 + 1) + height#0,SET test value formatter: 1 + 10433 record (14 = 3 [2] + 10 + 1) + held#0,SET test value formatter: 1 + 10447 record (16 = 3 [0] + 12 + 1) [restart] + hell#0,SET test value formatter: 3 + 10463 record (13 = 3 [3] + 9 + 1) + help#0,SET test value formatter: 2 + 10476 record (13 = 3 [2] + 9 + 1) + her#0,SET test value formatter: 8 + 10489 record (17 = 3 [3] + 13 + 1) + heraldry#0,SET test value formatter: 1 + 10506 record (17 = 3 [3] + 13 + 1) + hercules#0,SET test value formatter: 1 + 10523 record (14 = 3 [3] + 9 + 2) + here#0,SET test value formatter: 11 + 10537 record (17 = 3 [4] + 13 + 1) + hereafter#0,SET test value formatter: 1 + 10554 record (14 = 3 [4] + 10 + 1) + herein#0,SET test value formatter: 3 + 10568 record (14 = 3 [1] + 10 + 1) + hic#0,SET test value formatter: 1 + 10582 record (17 = 3 [2] + 13 + 1) + hideous#0,SET test value formatter: 1 + 10599 record (14 = 3 [2] + 10 + 1) + hies#0,SET test value formatter: 1 + 10613 record (14 = 3 [2] + 10 + 1) + high#0,SET test value formatter: 2 + 10627 record (14 = 3 [4] + 10 + 1) + higher#0,SET test value formatter: 1 + 10641 record (14 = 3 [2] + 10 + 1) + hill#0,SET test value formatter: 1 + 10655 record (13 = 3 [4] + 9 + 1) + hillo#0,SET test value formatter: 2 + 10668 record (14 = 3 [2] + 9 + 2) + him#0,SET test value formatter: 21 + 10682 record (19 = 3 [0] + 15 + 1) [restart] + himself#0,SET test value formatter: 3 + 10701 record (14 = 3 [2] + 9 + 2) + his#0,SET test value formatter: 57 + 10715 record (16 = 3 [2] + 12 + 1) + hither#0,SET test value formatter: 1 + 10731 record (14 = 3 [6] + 10 + 1) + hitherto#0,SET test value formatter: 1 + 10745 record (13 = 3 [1] + 9 + 1) + ho#0,SET test value formatter: 5 + 10758 record (14 = 3 [2] + 10 + 1) + hold#0,SET test value formatter: 9 + 10772 record (15 = 3 [4] + 11 + 1) + holding#0,SET test value formatter: 1 + 10787 record (13 = 3 [4] + 9 + 1) + holds#0,SET test value formatter: 2 + 10800 record (14 = 3 [3] + 10 + 1) + holla#0,SET test value formatter: 1 + 10814 record (13 = 3 [3] + 9 + 1) + holy#0,SET test value formatter: 1 + 10827 record (16 = 3 [2] + 12 + 1) + honest#0,SET test value formatter: 2 + 10843 record (15 = 3 [3] + 11 + 1) + honour#0,SET test value formatter: 5 + 10858 record (16 = 3 [6] + 12 + 1) + honourable#0,SET test value formatter: 1 + 10874 record (15 = 3 [2] + 11 + 1) + hoops#0,SET test value formatter: 1 + 10889 record (18 = 3 [2] + 13 + 2) + horatio#0,SET test value formatter: 85 + 10907 record (17 = 3 [3] + 13 + 1) + horrible#0,SET test value formatter: 4 + 10924 record (20 = 3 [0] + 16 + 1) [restart] + horridly#0,SET test value formatter: 1 + 10944 record (14 = 3 [2] + 10 + 1) + host#0,SET test value formatter: 1 + 10958 record (13 = 3 [2] + 9 + 1) + hot#0,SET test value formatter: 1 + 10971 record (14 = 3 [2] + 10 + 1) + hour#0,SET test value formatter: 6 + 10985 record (14 = 3 [3] + 10 + 1) + house#0,SET test value formatter: 2 + 10999 record (13 = 3 [2] + 9 + 1) + how#0,SET test value formatter: 7 + 11012 record (18 = 3 [3] + 14 + 1) + howsoever#0,SET test value formatter: 1 + 11030 record (17 = 3 [1] + 13 + 1) + humbly#0,SET test value formatter: 1 + 11047 record (17 = 3 [2] + 13 + 1) + hundred#0,SET test value formatter: 1 + 11064 record (19 = 3 [2] + 15 + 1) + husbandry#0,SET test value formatter: 1 + 11083 record (19 = 3 [1] + 15 + 1) + hyperion#0,SET test value formatter: 1 + 11102 record (15 = 3 [0] + 9 + 3) + i#0,SET test value formatter: 124 + 11117 record (14 = 3 [1] + 10 + 1) + ice#0,SET test value formatter: 1 + 11131 record (14 = 3 [1] + 9 + 2) + if#0,SET test value formatter: 22 + 11145 record (20 = 3 [1] + 16 + 1) + ignorance#0,SET test value formatter: 1 + 11165 record (13 = 3 [1] + 9 + 1) + ii#0,SET test value formatter: 1 + 11178 record (15 = 3 [0] + 11 + 1) [restart] + iii#0,SET test value formatter: 1 + 11193 record (17 = 3 [1] + 13 + 1) + illume#0,SET test value formatter: 1 + 11210 record (16 = 3 [4] + 12 + 1) + illusion#0,SET test value formatter: 1 + 11226 record (16 = 3 [1] + 12 + 1) + image#0,SET test value formatter: 1 + 11242 record (19 = 3 [4] + 15 + 1) + imagination#0,SET test value formatter: 1 + 11261 record (19 = 3 [2] + 15 + 1) + immediate#0,SET test value formatter: 1 + 11280 record (17 = 3 [3] + 13 + 1) + imminent#0,SET test value formatter: 1 + 11297 record (17 = 3 [3] + 13 + 1) + immortal#0,SET test value formatter: 1 + 11314 record (16 = 3 [2] + 12 + 1) + impart#0,SET test value formatter: 3 + 11330 record (16 = 3 [6] + 12 + 1) + impartment#0,SET test value formatter: 1 + 11346 record (17 = 3 [4] + 13 + 1) + impatient#0,SET test value formatter: 1 + 11363 record (22 = 3 [3] + 18 + 1) + imperfections#0,SET test value formatter: 1 + 11385 record (15 = 3 [5] + 11 + 1) + imperial#0,SET test value formatter: 1 + 11400 record (16 = 3 [3] + 12 + 1) + impious#0,SET test value formatter: 1 + 11416 record (19 = 3 [3] + 15 + 1) + implements#0,SET test value formatter: 1 + 11435 record (19 = 3 [4] + 15 + 1) + implorators#0,SET test value formatter: 1 + 11454 record (21 = 3 [0] + 17 + 1) [restart] + importing#0,SET test value formatter: 1 + 11475 record (16 = 3 [6] + 12 + 1) + importuned#0,SET test value formatter: 1 + 11491 record (15 = 3 [8] + 11 + 1) + importunity#0,SET test value formatter: 1 + 11506 record (16 = 3 [4] + 12 + 1) + impotent#0,SET test value formatter: 1 + 11522 record (16 = 3 [3] + 12 + 1) + impress#0,SET test value formatter: 1 + 11538 record (15 = 3 [1] + 9 + 3) + in#0,SET test value formatter: 118 + 11553 record (16 = 3 [2] + 12 + 1) + incest#0,SET test value formatter: 1 + 11569 record (16 = 3 [6] + 12 + 1) + incestuous#0,SET test value formatter: 2 + 11585 record (18 = 3 [3] + 14 + 1) + incorrect#0,SET test value formatter: 1 + 11603 record (17 = 3 [3] + 13 + 1) + increase#0,SET test value formatter: 1 + 11620 record (16 = 3 [2] + 12 + 1) + indeed#0,SET test value formatter: 8 + 11636 record (17 = 3 [2] + 13 + 1) + infants#0,SET test value formatter: 1 + 11653 record (17 = 3 [3] + 13 + 1) + infinite#0,SET test value formatter: 1 + 11670 record (18 = 3 [3] + 14 + 1) + influence#0,SET test value formatter: 1 + 11688 record (15 = 3 [3] + 11 + 1) + inform#0,SET test value formatter: 1 + 11703 record (21 = 3 [2] + 17 + 1) + inheritance#0,SET test value formatter: 1 + 11724 record (16 = 3 [0] + 12 + 1) [restart] + inky#0,SET test value formatter: 1 + 11740 record (17 = 3 [2] + 13 + 1) + instant#0,SET test value formatter: 2 + 11757 record (20 = 3 [4] + 16 + 1) + instrumental#0,SET test value formatter: 1 + 11777 record (16 = 3 [2] + 12 + 1) + intent#0,SET test value formatter: 1 + 11793 record (13 = 3 [6] + 9 + 1) + intents#0,SET test value formatter: 1 + 11806 record (13 = 3 [3] + 9 + 1) + into#0,SET test value formatter: 5 + 11819 record (15 = 3 [2] + 11 + 1) + inurn#0,SET test value formatter: 1 + 11834 record (21 = 3 [2] + 17 + 1) + investments#0,SET test value formatter: 1 + 11855 record (16 = 3 [3] + 12 + 1) + invites#0,SET test value formatter: 1 + 11871 record (21 = 3 [3] + 17 + 1) + invulnerable#0,SET test value formatter: 1 + 11892 record (16 = 3 [2] + 12 + 1) + inward#0,SET test value formatter: 1 + 11908 record (14 = 3 [1] + 9 + 2) + is#0,SET test value formatter: 62 + 11922 record (15 = 3 [2] + 11 + 1) + issue#0,SET test value formatter: 1 + 11937 record (15 = 3 [1] + 9 + 3) + it#0,SET test value formatter: 126 + 11952 record (13 = 3 [2] + 9 + 1) + its#0,SET test value formatter: 1 + 11965 record (15 = 3 [3] + 11 + 1) + itself#0,SET test value formatter: 9 + 11980 record (14 = 3 [0] + 10 + 1) [restart] + iv#0,SET test value formatter: 1 + 11994 record (16 = 3 [0] + 12 + 1) + jaws#0,SET test value formatter: 1 + 12010 record (16 = 3 [1] + 12 + 1) + jelly#0,SET test value formatter: 1 + 12026 record (17 = 3 [1] + 13 + 1) + jocund#0,SET test value formatter: 1 + 12043 record (15 = 3 [2] + 11 + 1) + joint#0,SET test value formatter: 2 + 12058 record (16 = 3 [5] + 12 + 1) + jointress#0,SET test value formatter: 1 + 12074 record (13 = 3 [2] + 9 + 1) + joy#0,SET test value formatter: 1 + 12087 record (19 = 3 [1] + 15 + 1) + judgment#0,SET test value formatter: 1 + 12106 record (15 = 3 [2] + 11 + 1) + juice#0,SET test value formatter: 1 + 12121 record (16 = 3 [2] + 12 + 1) + julius#0,SET test value formatter: 1 + 12137 record (14 = 3 [2] + 10 + 1) + jump#0,SET test value formatter: 1 + 12151 record (16 = 3 [0] + 12 + 1) + keep#0,SET test value formatter: 3 + 12167 record (13 = 3 [4] + 9 + 1) + keeps#0,SET test value formatter: 1 + 12180 record (14 = 3 [2] + 10 + 1) + kept#0,SET test value formatter: 1 + 12194 record (16 = 3 [2] + 12 + 1) + kettle#0,SET test value formatter: 1 + 12210 record (13 = 3 [2] + 9 + 1) + key#0,SET test value formatter: 1 + 12223 [restart 10217] + 12227 [restart 10447] + 12231 [restart 10682] + 12235 [restart 10924] + 12239 [restart 11178] + 12243 [restart 11454] + 12247 [restart 11724] + 12251 [restart 11980] + 12259 [trailer compression=none checksum=0x935ea812] + 12264 data (2039) + 12264 record (15 = 3 [0] + 11 + 1) [restart] + kin#0,SET test value formatter: 1 + 12279 record (13 = 3 [3] + 9 + 1) + kind#0,SET test value formatter: 1 + 12292 record (14 = 3 [3] + 9 + 2) + king#0,SET test value formatter: 23 + 12306 record (15 = 3 [4] + 11 + 1) + kingdom#0,SET test value formatter: 1 + 12321 record (16 = 3 [1] + 12 + 1) + knave#0,SET test value formatter: 1 + 12337 record (14 = 3 [2] + 10 + 1) + knew#0,SET test value formatter: 1 + 12351 record (17 = 3 [2] + 13 + 1) + knotted#0,SET test value formatter: 1 + 12368 record (14 = 3 [3] + 9 + 2) + know#0,SET test value formatter: 17 + 12382 record (13 = 3 [4] + 9 + 1) + known#0,SET test value formatter: 2 + 12395 record (13 = 3 [4] + 9 + 1) + knows#0,SET test value formatter: 1 + 12408 record (20 = 3 [0] + 16 + 1) + labourer#0,SET test value formatter: 1 + 12428 record (16 = 3 [6] + 12 + 1) + laboursome#0,SET test value formatter: 1 + 12444 record (14 = 3 [2] + 10 + 1) + lack#0,SET test value formatter: 1 + 12458 record (13 = 3 [4] + 9 + 1) + lacks#0,SET test value formatter: 1 + 12471 record (18 = 3 [2] + 13 + 2) + laertes#0,SET test value formatter: 16 + 12489 record (14 = 3 [2] + 10 + 1) + land#0,SET test value formatter: 2 + 12503 record (17 = 3 [0] + 13 + 1) [restart] + lands#0,SET test value formatter: 3 + 12520 record (16 = 3 [2] + 12 + 1) + larger#0,SET test value formatter: 1 + 12536 record (14 = 3 [2] + 10 + 1) + last#0,SET test value formatter: 3 + 12550 record (15 = 3 [4] + 11 + 1) + lasting#0,SET test value formatter: 1 + 12565 record (14 = 3 [2] + 10 + 1) + late#0,SET test value formatter: 3 + 12579 record (13 = 3 [2] + 9 + 1) + law#0,SET test value formatter: 2 + 12592 record (16 = 3 [3] + 12 + 1) + lawless#0,SET test value formatter: 1 + 12608 record (13 = 3 [2] + 9 + 1) + lay#0,SET test value formatter: 1 + 12621 record (15 = 3 [2] + 11 + 1) + lazar#0,SET test value formatter: 1 + 12636 record (15 = 3 [1] + 11 + 1) + lead#0,SET test value formatter: 1 + 12651 record (14 = 3 [3] + 10 + 1) + least#0,SET test value formatter: 2 + 12665 record (14 = 3 [3] + 10 + 1) + leave#0,SET test value formatter: 8 + 12679 record (14 = 3 [5] + 10 + 1) + leavens#0,SET test value formatter: 1 + 12693 record (14 = 3 [2] + 10 + 1) + left#0,SET test value formatter: 1 + 12707 record (17 = 3 [2] + 13 + 1) + leisure#0,SET test value formatter: 1 + 12724 record (14 = 3 [2] + 10 + 1) + lend#0,SET test value formatter: 1 + 12738 record (18 = 3 [0] + 14 + 1) [restart] + lender#0,SET test value formatter: 1 + 12756 record (13 = 3 [4] + 9 + 1) + lends#0,SET test value formatter: 1 + 12769 record (15 = 3 [3] + 11 + 1) + length#0,SET test value formatter: 1 + 12784 record (18 = 3 [2] + 14 + 1) + leperous#0,SET test value formatter: 1 + 12802 record (14 = 3 [2] + 10 + 1) + less#0,SET test value formatter: 2 + 12816 record (14 = 3 [4] + 10 + 1) + lesson#0,SET test value formatter: 1 + 12830 record (14 = 3 [2] + 9 + 2) + let#0,SET test value formatter: 23 + 12844 record (14 = 3 [3] + 10 + 1) + lethe#0,SET test value formatter: 1 + 12858 record (13 = 3 [3] + 9 + 1) + lets#0,SET test value formatter: 1 + 12871 record (16 = 3 [2] + 12 + 1) + levies#0,SET test value formatter: 1 + 12887 record (18 = 3 [2] + 14 + 1) + lewdness#0,SET test value formatter: 1 + 12905 record (20 = 3 [1] + 16 + 1) + libertine#0,SET test value formatter: 1 + 12925 record (14 = 3 [2] + 10 + 1) + lids#0,SET test value formatter: 1 + 12939 record (18 = 3 [2] + 14 + 1) + liegemen#0,SET test value formatter: 1 + 12957 record (13 = 3 [3] + 9 + 1) + lies#0,SET test value formatter: 1 + 12970 record (14 = 3 [2] + 10 + 1) + life#0,SET test value formatter: 7 + 12984 record (18 = 3 [0] + 14 + 1) [restart] + lifted#0,SET test value formatter: 1 + 13002 record (15 = 3 [2] + 11 + 1) + light#0,SET test value formatter: 1 + 13017 record (15 = 3 [5] + 11 + 1) + lightest#0,SET test value formatter: 1 + 13032 record (15 = 3 [2] + 10 + 2) + like#0,SET test value formatter: 23 + 13047 record (14 = 3 [2] + 10 + 1) + link#0,SET test value formatter: 1 + 13061 record (14 = 3 [2] + 10 + 1) + lion#0,SET test value formatter: 1 + 13075 record (14 = 3 [2] + 10 + 1) + lips#0,SET test value formatter: 1 + 13089 record (16 = 3 [2] + 12 + 1) + liquid#0,SET test value formatter: 1 + 13105 record (14 = 3 [2] + 10 + 1) + list#0,SET test value formatter: 6 + 13119 record (13 = 3 [4] + 9 + 1) + lists#0,SET test value formatter: 1 + 13132 record (16 = 3 [2] + 12 + 1) + little#0,SET test value formatter: 3 + 13148 record (14 = 3 [2] + 10 + 1) + live#0,SET test value formatter: 3 + 13162 record (14 = 3 [4] + 10 + 1) + livery#0,SET test value formatter: 1 + 13176 record (13 = 3 [4] + 9 + 1) + lives#0,SET test value formatter: 1 + 13189 record (14 = 3 [1] + 9 + 2) + ll#0,SET test value formatter: 18 + 13203 record (13 = 3 [1] + 9 + 1) + lo#0,SET test value formatter: 1 + 13216 record (16 = 3 [0] + 12 + 1) [restart] + loan#0,SET test value formatter: 1 + 13232 record (18 = 3 [3] + 14 + 1) + loathsome#0,SET test value formatter: 1 + 13250 record (14 = 3 [2] + 10 + 1) + lock#0,SET test value formatter: 1 + 13264 record (13 = 3 [4] + 9 + 1) + locks#0,SET test value formatter: 1 + 13277 record (15 = 3 [2] + 11 + 1) + lodge#0,SET test value formatter: 1 + 13292 record (15 = 3 [2] + 11 + 1) + lofty#0,SET test value formatter: 1 + 13307 record (14 = 3 [2] + 10 + 1) + long#0,SET test value formatter: 4 + 13321 record (14 = 3 [4] + 10 + 1) + longer#0,SET test value formatter: 3 + 13335 record (15 = 3 [2] + 10 + 2) + look#0,SET test value formatter: 10 + 13350 record (13 = 3 [4] + 9 + 1) + looks#0,SET test value formatter: 2 + 13363 record (14 = 3 [3] + 10 + 1) + loose#0,SET test value formatter: 1 + 13377 record (15 = 3 [2] + 10 + 2) + lord#0,SET test value formatter: 60 + 13392 record (13 = 3 [4] + 9 + 1) + lords#0,SET test value formatter: 1 + 13405 record (15 = 3 [5] + 11 + 1) + lordship#0,SET test value formatter: 1 + 13420 record (14 = 3 [2] + 10 + 1) + lose#0,SET test value formatter: 2 + 13434 record (13 = 3 [4] + 9 + 1) + loses#0,SET test value formatter: 1 + 13447 record (16 = 3 [0] + 12 + 1) [restart] + loss#0,SET test value formatter: 1 + 13463 record (13 = 3 [3] + 9 + 1) + lost#0,SET test value formatter: 5 + 13476 record (14 = 3 [2] + 10 + 1) + loud#0,SET test value formatter: 1 + 13490 record (14 = 3 [2] + 10 + 1) + love#0,SET test value formatter: 8 + 13504 record (13 = 3 [4] + 9 + 1) + loves#0,SET test value formatter: 5 + 13517 record (15 = 3 [3] + 11 + 1) + loving#0,SET test value formatter: 2 + 13532 record (15 = 3 [1] + 11 + 1) + lust#0,SET test value formatter: 2 + 13547 record (16 = 3 [2] + 12 + 1) + luxury#0,SET test value formatter: 1 + 13563 record (13 = 3 [0] + 9 + 1) + m#0,SET test value formatter: 2 + 13576 record (16 = 3 [1] + 12 + 1) + madam#0,SET test value formatter: 4 + 13592 record (13 = 3 [3] + 9 + 1) + made#0,SET test value formatter: 8 + 13605 record (16 = 3 [3] + 12 + 1) + madness#0,SET test value formatter: 1 + 13621 record (14 = 3 [2] + 10 + 1) + maid#0,SET test value formatter: 1 + 13635 record (14 = 3 [4] + 10 + 1) + maiden#0,SET test value formatter: 1 + 13649 record (13 = 3 [3] + 9 + 1) + main#0,SET test value formatter: 2 + 13662 record (20 = 3 [2] + 16 + 1) + majestical#0,SET test value formatter: 1 + 13682 record (19 = 3 [0] + 15 + 1) [restart] + majesty#0,SET test value formatter: 1 + 13701 record (14 = 3 [2] + 10 + 1) + make#0,SET test value formatter: 8 + 13715 record (13 = 3 [4] + 9 + 1) + makes#0,SET test value formatter: 2 + 13728 record (15 = 3 [3] + 11 + 1) + making#0,SET test value formatter: 2 + 13743 record (19 = 3 [2] + 15 + 1) + malicious#0,SET test value formatter: 1 + 13762 record (14 = 3 [2] + 9 + 2) + man#0,SET test value formatter: 11 + 13776 record (15 = 3 [3] + 11 + 1) + manner#0,SET test value formatter: 1 + 13791 record (13 = 3 [6] + 9 + 1) + manners#0,SET test value formatter: 1 + 13804 record (15 = 3 [3] + 11 + 1) + mantle#0,SET test value formatter: 1 + 13819 record (13 = 3 [3] + 9 + 1) + many#0,SET test value formatter: 2 + 13832 record (16 = 3 [2] + 12 + 1) + marble#0,SET test value formatter: 1 + 13848 record (19 = 3 [3] + 14 + 2) + marcellus#0,SET test value formatter: 46 + 13867 record (13 = 3 [4] + 9 + 1) + march#0,SET test value formatter: 2 + 13880 record (13 = 3 [3] + 9 + 1) + mark#0,SET test value formatter: 2 + 13893 record (17 = 3 [3] + 13 + 1) + marriage#0,SET test value formatter: 3 + 13910 record (14 = 3 [5] + 10 + 1) + married#0,SET test value formatter: 2 + 13924 record (18 = 3 [0] + 14 + 1) [restart] + marrow#0,SET test value formatter: 1 + 13942 record (13 = 3 [4] + 9 + 1) + marry#0,SET test value formatter: 3 + 13955 record (13 = 3 [3] + 9 + 1) + mart#0,SET test value formatter: 1 + 13968 record (15 = 3 [4] + 11 + 1) + martial#0,SET test value formatter: 1 + 13983 record (15 = 3 [3] + 11 + 1) + marvel#0,SET test value formatter: 1 + 13998 record (15 = 3 [2] + 11 + 1) + matin#0,SET test value formatter: 1 + 14013 record (15 = 3 [3] + 11 + 1) + matter#0,SET test value formatter: 1 + 14028 record (14 = 3 [2] + 9 + 2) + may#0,SET test value formatter: 19 + 14042 record (14 = 3 [1] + 9 + 2) + me#0,SET test value formatter: 47 + 14056 record (14 = 3 [2] + 10 + 1) + mean#0,SET test value formatter: 2 + 14070 record (13 = 3 [4] + 9 + 1) + means#0,SET test value formatter: 2 + 14083 record (14 = 3 [3] + 10 + 1) + meats#0,SET test value formatter: 1 + 14097 record (20 = 3 [2] + 16 + 1) + meditation#0,SET test value formatter: 1 + 14117 record (14 = 3 [2] + 10 + 1) + meet#0,SET test value formatter: 3 + 14131 record (15 = 3 [4] + 11 + 1) + meeting#0,SET test value formatter: 1 + 14146 record (14 = 3 [2] + 10 + 1) + melt#0,SET test value formatter: 1 + 14160 record (18 = 3 [0] + 14 + 1) [restart] + memory#0,SET test value formatter: 5 + 14178 record (13 = 3 [2] + 9 + 1) + men#0,SET test value formatter: 3 + 14191 record (15 = 3 [2] + 11 + 1) + mercy#0,SET test value formatter: 2 + 14206 record (13 = 3 [3] + 9 + 1) + mere#0,SET test value formatter: 1 + 14219 record (14 = 3 [4] + 10 + 1) + merely#0,SET test value formatter: 1 + 14233 record (17 = 3 [2] + 13 + 1) + message#0,SET test value formatter: 1 + 14250 record (13 = 3 [2] + 9 + 1) + met#0,SET test value formatter: 1 + 14263 [restart 12264] + 14267 [restart 12503] + 14271 [restart 12738] + 14275 [restart 12984] + 14279 [restart 13216] + 14283 [restart 13447] + 14287 [restart 13682] + 14291 [restart 13924] + 14295 [restart 14160] + 14303 [trailer compression=none checksum=0xb57d9fa6] + 14308 data (2037) + 14308 record (20 = 3 [0] + 16 + 1) [restart] + methinks#0,SET test value formatter: 2 + 14328 record (17 = 3 [4] + 13 + 1) + methought#0,SET test value formatter: 1 + 14345 record (15 = 3 [3] + 11 + 1) + mettle#0,SET test value formatter: 1 + 14360 record (17 = 3 [1] + 13 + 1) + middle#0,SET test value formatter: 1 + 14377 record (15 = 3 [2] + 11 + 1) + might#0,SET test value formatter: 7 + 14392 record (16 = 3 [5] + 12 + 1) + mightiest#0,SET test value formatter: 1 + 14408 record (14 = 3 [2] + 10 + 1) + milk#0,SET test value formatter: 1 + 14422 record (14 = 3 [2] + 10 + 1) + mind#0,SET test value formatter: 6 + 14436 record (13 = 3 [3] + 9 + 1) + mine#0,SET test value formatter: 6 + 14449 record (18 = 3 [3] + 14 + 1) + ministers#0,SET test value formatter: 1 + 14467 record (15 = 3 [3] + 11 + 1) + minute#0,SET test value formatter: 1 + 14482 record (13 = 3 [6] + 9 + 1) + minutes#0,SET test value formatter: 1 + 14495 record (15 = 3 [2] + 11 + 1) + mirth#0,SET test value formatter: 1 + 14510 record (15 = 3 [1] + 11 + 1) + mock#0,SET test value formatter: 1 + 14525 record (15 = 3 [4] + 11 + 1) + mockery#0,SET test value formatter: 1 + 14540 record (18 = 3 [2] + 14 + 1) + moderate#0,SET test value formatter: 1 + 14558 record (18 = 3 [0] + 14 + 1) [restart] + moiety#0,SET test value formatter: 1 + 14576 record (14 = 3 [3] + 10 + 1) + moist#0,SET test value formatter: 1 + 14590 record (14 = 3 [2] + 10 + 1) + mole#0,SET test value formatter: 2 + 14604 record (16 = 3 [2] + 12 + 1) + moment#0,SET test value formatter: 1 + 14620 record (15 = 3 [2] + 11 + 1) + month#0,SET test value formatter: 3 + 14635 record (13 = 3 [5] + 9 + 1) + months#0,SET test value formatter: 1 + 14648 record (15 = 3 [2] + 11 + 1) + moods#0,SET test value formatter: 1 + 14663 record (13 = 3 [3] + 9 + 1) + moon#0,SET test value formatter: 2 + 14676 record (15 = 3 [2] + 10 + 2) + more#0,SET test value formatter: 19 + 14691 record (13 = 3 [3] + 9 + 1) + morn#0,SET test value formatter: 3 + 14704 record (15 = 3 [4] + 11 + 1) + morning#0,SET test value formatter: 3 + 14719 record (15 = 3 [2] + 10 + 2) + most#0,SET test value formatter: 28 + 14734 record (14 = 3 [2] + 10 + 1) + mote#0,SET test value formatter: 1 + 14748 record (15 = 3 [3] + 11 + 1) + mother#0,SET test value formatter: 5 + 14763 record (15 = 3 [3] + 11 + 1) + motion#0,SET test value formatter: 1 + 14778 record (14 = 3 [4] + 10 + 1) + motive#0,SET test value formatter: 2 + 14792 record (17 = 3 [0] + 13 + 1) [restart] + mourn#0,SET test value formatter: 1 + 14809 record (15 = 3 [5] + 11 + 1) + mourning#0,SET test value formatter: 1 + 14824 record (14 = 3 [3] + 10 + 1) + mouse#0,SET test value formatter: 1 + 14838 record (14 = 3 [3] + 10 + 1) + mouth#0,SET test value formatter: 1 + 14852 record (15 = 3 [2] + 11 + 1) + moved#0,SET test value formatter: 1 + 14867 record (15 = 3 [1] + 11 + 1) + much#0,SET test value formatter: 9 + 14882 record (16 = 3 [2] + 12 + 1) + murder#0,SET test value formatter: 3 + 14898 record (15 = 3 [2] + 10 + 2) + must#0,SET test value formatter: 14 + 14913 record (15 = 3 [1] + 9 + 3) + my#0,SET test value formatter: 126 + 14928 record (16 = 3 [2] + 12 + 1) + myself#0,SET test value formatter: 4 + 14944 record (16 = 3 [0] + 12 + 1) + name#0,SET test value formatter: 2 + 14960 record (17 = 3 [2] + 13 + 1) + nations#0,SET test value formatter: 1 + 14977 record (14 = 3 [4] + 10 + 1) + native#0,SET test value formatter: 2 + 14991 record (16 = 3 [3] + 12 + 1) + natural#0,SET test value formatter: 2 + 15007 record (14 = 3 [5] + 9 + 2) + nature#0,SET test value formatter: 13 + 15021 record (13 = 3 [2] + 9 + 1) + nay#0,SET test value formatter: 7 + 15034 record (14 = 3 [0] + 10 + 1) [restart] + ne#0,SET test value formatter: 1 + 15048 record (14 = 3 [2] + 10 + 1) + near#0,SET test value formatter: 3 + 15062 record (21 = 3 [2] + 17 + 1) + necessaries#0,SET test value formatter: 1 + 15083 record (14 = 3 [2] + 10 + 1) + need#0,SET test value formatter: 1 + 15097 record (15 = 3 [4] + 11 + 1) + needful#0,SET test value formatter: 1 + 15112 record (13 = 3 [4] + 9 + 1) + needs#0,SET test value formatter: 1 + 15125 record (17 = 3 [2] + 13 + 1) + neither#0,SET test value formatter: 1 + 15142 record (16 = 3 [2] + 12 + 1) + nemean#0,SET test value formatter: 1 + 15158 record (16 = 3 [2] + 12 + 1) + nephew#0,SET test value formatter: 1 + 15174 record (16 = 3 [3] + 12 + 1) + neptune#0,SET test value formatter: 1 + 15190 record (15 = 3 [2] + 11 + 1) + nerve#0,SET test value formatter: 1 + 15205 record (15 = 3 [2] + 11 + 1) + never#0,SET test value formatter: 6 + 15220 record (13 = 3 [2] + 9 + 1) + new#0,SET test value formatter: 1 + 15233 record (13 = 3 [3] + 9 + 1) + news#0,SET test value formatter: 2 + 15246 record (17 = 3 [1] + 12 + 2) + night#0,SET test value formatter: 22 + 15263 record (14 = 3 [5] + 10 + 1) + nighted#0,SET test value formatter: 1 + 15277 record (19 = 3 [0] + 15 + 1) [restart] + nightly#0,SET test value formatter: 1 + 15296 record (13 = 3 [5] + 9 + 1) + nights#0,SET test value formatter: 3 + 15309 record (15 = 3 [2] + 11 + 1) + niobe#0,SET test value formatter: 1 + 15324 record (17 = 3 [2] + 13 + 1) + nipping#0,SET test value formatter: 1 + 15341 record (14 = 3 [1] + 9 + 2) + no#0,SET test value formatter: 28 + 15355 record (18 = 3 [2] + 14 + 1) + nobility#0,SET test value formatter: 1 + 15373 record (14 = 3 [3] + 10 + 1) + noble#0,SET test value formatter: 5 + 15387 record (14 = 3 [2] + 10 + 1) + none#0,SET test value formatter: 2 + 15401 record (14 = 3 [2] + 9 + 2) + nor#0,SET test value formatter: 14 + 15415 record (15 = 3 [3] + 11 + 1) + norway#0,SET test value formatter: 5 + 15430 record (14 = 3 [2] + 9 + 2) + not#0,SET test value formatter: 80 + 15444 record (13 = 3 [3] + 9 + 1) + note#0,SET test value formatter: 2 + 15457 record (16 = 3 [3] + 12 + 1) + nothing#0,SET test value formatter: 2 + 15473 record (14 = 3 [2] + 9 + 2) + now#0,SET test value formatter: 19 + 15487 record (14 = 3 [0] + 9 + 2) + o#0,SET test value formatter: 30 + 15501 record (15 = 3 [1] + 11 + 1) + oath#0,SET test value formatter: 1 + 15516 record (16 = 3 [0] + 12 + 1) [restart] + obey#0,SET test value formatter: 3 + 15532 record (16 = 3 [2] + 12 + 1) + object#0,SET test value formatter: 1 + 15548 record (20 = 3 [2] + 16 + 1) + obligation#0,SET test value formatter: 1 + 15568 record (20 = 3 [2] + 16 + 1) + obsequious#0,SET test value formatter: 1 + 15588 record (18 = 3 [4] + 14 + 1) + observance#0,SET test value formatter: 1 + 15606 record (13 = 3 [8] + 9 + 1) + observant#0,SET test value formatter: 1 + 15619 record (16 = 3 [7] + 12 + 1) + observation#0,SET test value formatter: 1 + 15635 record (18 = 3 [3] + 14 + 1) + obstinate#0,SET test value formatter: 1 + 15653 record (19 = 3 [1] + 15 + 1) + occasion#0,SET test value formatter: 1 + 15672 record (14 = 3 [1] + 10 + 1) + odd#0,SET test value formatter: 1 + 15686 record (15 = 3 [1] + 9 + 3) + of#0,SET test value formatter: 176 + 15701 record (13 = 3 [2] + 9 + 1) + off#0,SET test value formatter: 6 + 15714 record (16 = 3 [3] + 12 + 1) + offence#0,SET test value formatter: 2 + 15730 record (13 = 3 [5] + 9 + 1) + offend#0,SET test value formatter: 1 + 15743 record (14 = 3 [6] + 10 + 1) + offended#0,SET test value formatter: 1 + 15757 record (13 = 3 [4] + 9 + 1) + offer#0,SET test value formatter: 2 + 15770 record (15 = 3 [0] + 11 + 1) [restart] + oft#0,SET test value formatter: 7 + 15785 record (14 = 3 [1] + 10 + 1) + old#0,SET test value formatter: 4 + 15799 record (15 = 3 [1] + 11 + 1) + omen#0,SET test value formatter: 1 + 15814 record (14 = 3 [1] + 9 + 2) + on#0,SET test value formatter: 25 + 15828 record (14 = 3 [2] + 10 + 1) + once#0,SET test value formatter: 8 + 15842 record (13 = 3 [2] + 9 + 1) + one#0,SET test value formatter: 6 + 15855 record (15 = 3 [1] + 11 + 1) + oped#0,SET test value formatter: 1 + 15870 record (13 = 3 [3] + 9 + 1) + open#0,SET test value formatter: 1 + 15883 record (18 = 3 [2] + 13 + 2) + ophelia#0,SET test value formatter: 15 + 15901 record (17 = 3 [2] + 13 + 1) + opinion#0,SET test value formatter: 1 + 15918 record (17 = 3 [2] + 13 + 1) + opposed#0,SET test value formatter: 1 + 15935 record (17 = 3 [5] + 13 + 1) + opposition#0,SET test value formatter: 1 + 15952 record (16 = 3 [3] + 12 + 1) + oppress#0,SET test value formatter: 1 + 15968 record (14 = 3 [1] + 9 + 2) + or#0,SET test value formatter: 28 + 15982 record (17 = 3 [2] + 13 + 1) + orchard#0,SET test value formatter: 2 + 15999 record (18 = 3 [2] + 14 + 1) + ordnance#0,SET test value formatter: 1 + 16017 record (18 = 3 [0] + 14 + 1) [restart] + origin#0,SET test value formatter: 1 + 16035 record (16 = 3 [1] + 12 + 1) + other#0,SET test value formatter: 4 + 16051 record (15 = 3 [1] + 10 + 2) + our#0,SET test value formatter: 45 + 16066 record (16 = 3 [3] + 12 + 1) + ourself#0,SET test value formatter: 2 + 16082 record (15 = 3 [6] + 11 + 1) + ourselves#0,SET test value formatter: 1 + 16097 record (13 = 3 [2] + 9 + 1) + out#0,SET test value formatter: 8 + 16110 record (14 = 3 [1] + 10 + 1) + own#0,SET test value formatter: 6 + 16124 record (16 = 3 [3] + 12 + 1) + ownself#0,SET test value formatter: 1 + 16140 record (16 = 3 [0] + 12 + 1) + pale#0,SET test value formatter: 4 + 16156 record (13 = 3 [4] + 9 + 1) + pales#0,SET test value formatter: 1 + 16169 record (13 = 3 [3] + 9 + 1) + palm#0,SET test value formatter: 1 + 16182 record (13 = 3 [4] + 9 + 1) + palmy#0,SET test value formatter: 1 + 16195 record (16 = 3 [2] + 12 + 1) + pardon#0,SET test value formatter: 1 + 16211 record (14 = 3 [3] + 10 + 1) + parle#0,SET test value formatter: 1 + 16225 record (13 = 3 [5] + 9 + 1) + parley#0,SET test value formatter: 1 + 16238 record (13 = 3 [3] + 9 + 1) + part#0,SET test value formatter: 6 + 16251 record (22 = 3 [0] + 18 + 1) [restart] + particular#0,SET test value formatter: 6 + 16273 record (15 = 3 [5] + 11 + 1) + partisan#0,SET test value formatter: 1 + 16288 record (17 = 3 [2] + 13 + 1) + passeth#0,SET test value formatter: 1 + 16305 [restart 14308] + 16309 [restart 14558] + 16313 [restart 14792] + 16317 [restart 15034] + 16321 [restart 15277] + 16325 [restart 15516] + 16329 [restart 15770] + 16333 [restart 16017] + 16337 [restart 16251] + 16345 [trailer compression=none checksum=0x88e82f4b] + 16350 data (2029) + 16350 record (19 = 3 [0] + 15 + 1) [restart] + passing#0,SET test value formatter: 1 + 16369 record (13 = 3 [3] + 9 + 1) + past#0,SET test value formatter: 1 + 16382 record (15 = 3 [4] + 11 + 1) + pastors#0,SET test value formatter: 1 + 16397 record (14 = 3 [2] + 10 + 1) + path#0,SET test value formatter: 1 + 16411 record (16 = 3 [3] + 12 + 1) + patrick#0,SET test value formatter: 1 + 16427 record (13 = 3 [2] + 9 + 1) + pay#0,SET test value formatter: 1 + 16440 record (13 = 3 [1] + 9 + 1) + pe#0,SET test value formatter: 1 + 16453 record (15 = 3 [2] + 11 + 1) + peace#0,SET test value formatter: 2 + 16468 record (17 = 3 [2] + 13 + 1) + peevish#0,SET test value formatter: 1 + 16485 record (19 = 3 [2] + 15 + 1) + perchance#0,SET test value formatter: 2 + 16504 record (16 = 3 [3] + 12 + 1) + perform#0,SET test value formatter: 1 + 16520 record (15 = 3 [4] + 11 + 1) + perfume#0,SET test value formatter: 1 + 16535 record (16 = 3 [3] + 12 + 1) + perhaps#0,SET test value formatter: 1 + 16551 record (17 = 3 [3] + 13 + 1) + perilous#0,SET test value formatter: 1 + 16568 record (18 = 3 [3] + 14 + 1) + permanent#0,SET test value formatter: 1 + 16586 record (19 = 3 [3] + 15 + 1) + pernicious#0,SET test value formatter: 1 + 16605 record (20 = 3 [0] + 16 + 1) [restart] + persever#0,SET test value formatter: 1 + 16625 record (14 = 3 [4] + 10 + 1) + person#0,SET test value formatter: 1 + 16639 record (14 = 3 [6] + 10 + 1) + personal#0,SET test value formatter: 1 + 16653 record (13 = 3 [6] + 9 + 1) + persons#0,SET test value formatter: 1 + 16666 record (18 = 3 [3] + 14 + 1) + perturbed#0,SET test value formatter: 1 + 16684 record (16 = 3 [2] + 12 + 1) + pester#0,SET test value formatter: 1 + 16700 record (18 = 3 [2] + 14 + 1) + petition#0,SET test value formatter: 1 + 16718 record (14 = 3 [3] + 10 + 1) + petty#0,SET test value formatter: 1 + 16732 record (21 = 3 [1] + 17 + 1) + philosophy#0,SET test value formatter: 1 + 16753 record (16 = 3 [2] + 12 + 1) + phrase#0,SET test value formatter: 3 + 16769 record (16 = 3 [1] + 12 + 1) + piece#0,SET test value formatter: 1 + 16785 record (13 = 3 [2] + 9 + 1) + pin#0,SET test value formatter: 1 + 16798 record (16 = 3 [2] + 12 + 1) + pioner#0,SET test value formatter: 1 + 16814 record (14 = 3 [3] + 10 + 1) + pious#0,SET test value formatter: 1 + 16828 record (14 = 3 [2] + 10 + 1) + pith#0,SET test value formatter: 1 + 16842 record (13 = 3 [3] + 9 + 1) + pity#0,SET test value formatter: 1 + 16855 record (17 = 3 [0] + 13 + 1) [restart] + place#0,SET test value formatter: 3 + 16872 record (14 = 3 [3] + 10 + 1) + plain#0,SET test value formatter: 1 + 16886 record (16 = 3 [3] + 12 + 1) + planets#0,SET test value formatter: 1 + 16902 record (17 = 3 [3] + 13 + 1) + platform#0,SET test value formatter: 5 + 16919 record (17 = 3 [3] + 13 + 1) + plausive#0,SET test value formatter: 1 + 16936 record (13 = 3 [3] + 9 + 1) + play#0,SET test value formatter: 2 + 16949 record (16 = 3 [2] + 12 + 1) + please#0,SET test value formatter: 1 + 16965 record (15 = 3 [3] + 11 + 1) + pledge#0,SET test value formatter: 1 + 16980 record (16 = 3 [1] + 12 + 1) + point#0,SET test value formatter: 2 + 16996 record (17 = 3 [2] + 13 + 1) + polacks#0,SET test value formatter: 1 + 17013 record (13 = 3 [3] + 9 + 1) + pole#0,SET test value formatter: 1 + 17026 record (18 = 3 [3] + 13 + 2) + polonius#0,SET test value formatter: 13 + 17044 record (19 = 3 [2] + 15 + 1) + ponderous#0,SET test value formatter: 1 + 17063 record (14 = 3 [2] + 10 + 1) + pooh#0,SET test value formatter: 1 + 17077 record (13 = 3 [3] + 9 + 1) + poor#0,SET test value formatter: 9 + 17090 record (17 = 3 [2] + 13 + 1) + porches#0,SET test value formatter: 1 + 17107 record (22 = 3 [0] + 18 + 1) [restart] + porpentine#0,SET test value formatter: 1 + 17129 record (19 = 3 [3] + 15 + 1) + portentous#0,SET test value formatter: 1 + 17148 record (17 = 3 [2] + 13 + 1) + possess#0,SET test value formatter: 1 + 17165 record (13 = 3 [5] + 9 + 1) + posset#0,SET test value formatter: 1 + 17178 record (13 = 3 [3] + 9 + 1) + post#0,SET test value formatter: 3 + 17191 record (14 = 3 [2] + 10 + 1) + pour#0,SET test value formatter: 1 + 17205 record (15 = 3 [2] + 11 + 1) + power#0,SET test value formatter: 3 + 17220 record (15 = 3 [1] + 11 + 1) + pray#0,SET test value formatter: 7 + 17235 record (15 = 3 [4] + 11 + 1) + prayers#0,SET test value formatter: 1 + 17250 record (19 = 3 [2] + 15 + 1) + preceding#0,SET test value formatter: 1 + 17269 record (15 = 3 [5] + 11 + 1) + precepts#0,SET test value formatter: 1 + 17284 record (16 = 3 [4] + 12 + 1) + precurse#0,SET test value formatter: 1 + 17300 record (21 = 3 [3] + 17 + 1) + preparations#0,SET test value formatter: 1 + 17321 record (17 = 3 [3] + 13 + 1) + presence#0,SET test value formatter: 1 + 17338 record (13 = 3 [6] + 9 + 1) + present#0,SET test value formatter: 1 + 17351 record (17 = 3 [4] + 13 + 1) + pressures#0,SET test value formatter: 1 + 17368 record (16 = 3 [0] + 12 + 1) [restart] + prey#0,SET test value formatter: 1 + 17384 record (15 = 3 [2] + 11 + 1) + prick#0,SET test value formatter: 2 + 17399 record (14 = 3 [3] + 10 + 1) + pride#0,SET test value formatter: 1 + 17413 record (17 = 3 [3] + 13 + 1) + primrose#0,SET test value formatter: 1 + 17430 record (13 = 3 [4] + 9 + 1) + primy#0,SET test value formatter: 1 + 17443 record (15 = 3 [3] + 11 + 1) + prince#0,SET test value formatter: 1 + 17458 record (15 = 3 [3] + 11 + 1) + prison#0,SET test value formatter: 1 + 17473 record (16 = 3 [3] + 12 + 1) + private#0,SET test value formatter: 1 + 17489 record (13 = 3 [4] + 9 + 1) + privy#0,SET test value formatter: 1 + 17502 record (19 = 3 [2] + 15 + 1) + probation#0,SET test value formatter: 1 + 17521 record (16 = 3 [3] + 12 + 1) + process#0,SET test value formatter: 1 + 17537 record (17 = 3 [4] + 13 + 1) + proclaims#0,SET test value formatter: 1 + 17554 record (17 = 3 [3] + 13 + 1) + prodigal#0,SET test value formatter: 2 + 17571 record (17 = 3 [3] + 13 + 1) + prologue#0,SET test value formatter: 1 + 17588 record (16 = 3 [3] + 12 + 1) + promise#0,SET test value formatter: 1 + 17604 record (20 = 3 [3] + 16 + 1) + pronouncing#0,SET test value formatter: 1 + 17624 record (21 = 3 [0] + 17 + 1) [restart] + prophetic#0,SET test value formatter: 1 + 17645 record (19 = 3 [4] + 15 + 1) + proportions#0,SET test value formatter: 1 + 17664 record (14 = 3 [5] + 10 + 1) + propose#0,SET test value formatter: 1 + 17678 record (15 = 3 [1] + 11 + 1) + puff#0,SET test value formatter: 1 + 17693 record (14 = 3 [2] + 10 + 1) + pure#0,SET test value formatter: 1 + 17707 record (15 = 3 [3] + 11 + 1) + purged#0,SET test value formatter: 1 + 17722 record (16 = 3 [3] + 12 + 1) + purpose#0,SET test value formatter: 1 + 17738 record (14 = 3 [3] + 10 + 1) + purse#0,SET test value formatter: 1 + 17752 record (16 = 3 [4] + 12 + 1) + pursuest#0,SET test value formatter: 1 + 17768 record (13 = 3 [2] + 9 + 1) + put#0,SET test value formatter: 2 + 17781 record (13 = 3 [3] + 9 + 1) + puts#0,SET test value formatter: 1 + 17794 record (19 = 3 [0] + 15 + 1) + quarrel#0,SET test value formatter: 1 + 17813 record (15 = 3 [2] + 11 + 1) + queen#0,SET test value formatter: 7 + 17828 record (17 = 3 [3] + 13 + 1) + question#0,SET test value formatter: 2 + 17845 record (16 = 3 [8] + 12 + 1) + questionable#0,SET test value formatter: 1 + 17861 record (21 = 3 [2] + 17 + 1) + quicksilver#0,SET test value formatter: 1 + 17882 record (17 = 3 [0] + 13 + 1) [restart] + quiet#0,SET test value formatter: 1 + 17899 record (14 = 3 [5] + 10 + 1) + quietly#0,SET test value formatter: 1 + 17913 record (15 = 3 [3] + 11 + 1) + quills#0,SET test value formatter: 1 + 17928 record (19 = 3 [0] + 15 + 1) + radiant#0,SET test value formatter: 1 + 17947 record (14 = 3 [2] + 10 + 1) + rank#0,SET test value formatter: 2 + 17961 record (14 = 3 [4] + 10 + 1) + rankly#0,SET test value formatter: 1 + 17975 record (14 = 3 [2] + 10 + 1) + rate#0,SET test value formatter: 1 + 17989 record (17 = 3 [3] + 13 + 1) + ratified#0,SET test value formatter: 1 + 18006 record (13 = 3 [1] + 9 + 1) + re#0,SET test value formatter: 2 + 18019 record (17 = 3 [2] + 13 + 1) + reaches#0,SET test value formatter: 1 + 18036 record (13 = 3 [3] + 9 + 1) + rear#0,SET test value formatter: 1 + 18049 record (15 = 3 [3] + 11 + 1) + reason#0,SET test value formatter: 5 + 18064 record (16 = 3 [2] + 12 + 1) + rebels#0,SET test value formatter: 1 + 18080 record (18 = 3 [2] + 14 + 1) + reckless#0,SET test value formatter: 1 + 18098 record (17 = 3 [4] + 13 + 1) + reckoning#0,SET test value formatter: 1 + 18115 record (13 = 3 [4] + 9 + 1) + recks#0,SET test value formatter: 1 + 18128 record (19 = 3 [0] + 15 + 1) [restart] + records#0,SET test value formatter: 1 + 18147 record (15 = 3 [4] + 11 + 1) + recover#0,SET test value formatter: 1 + 18162 record (13 = 3 [2] + 9 + 1) + red#0,SET test value formatter: 1 + 18175 record (13 = 3 [3] + 9 + 1) + rede#0,SET test value formatter: 1 + 18188 record (15 = 3 [2] + 11 + 1) + reels#0,SET test value formatter: 1 + 18203 record (16 = 3 [2] + 12 + 1) + relief#0,SET test value formatter: 1 + 18219 record (15 = 3 [5] + 11 + 1) + relieved#0,SET test value formatter: 1 + 18234 record (16 = 3 [2] + 12 + 1) + remain#0,SET test value formatter: 1 + 18250 record (17 = 3 [3] + 13 + 1) + remember#0,SET test value formatter: 6 + 18267 record (17 = 3 [6] + 13 + 1) + remembrance#0,SET test value formatter: 1 + 18284 record (15 = 3 [3] + 11 + 1) + remove#0,SET test value formatter: 1 + 18299 record (13 = 3 [6] + 9 + 1) + removed#0,SET test value formatter: 1 + 18312 record (16 = 3 [2] + 12 + 1) + render#0,SET test value formatter: 1 + 18328 record (15 = 3 [2] + 11 + 1) + reply#0,SET test value formatter: 1 + 18343 [restart 16350] + 18347 [restart 16605] + 18351 [restart 16855] + 18355 [restart 17107] + 18359 [restart 17368] + 18363 [restart 17624] + 18367 [restart 17882] + 18371 [restart 18128] + 18379 [trailer compression=none checksum=0xa251cc65] + 18384 data (2040) + 18384 record (18 = 3 [0] + 14 + 1) [restart] + report#0,SET test value formatter: 1 + 18402 record (17 = 3 [2] + 13 + 1) + request#0,SET test value formatter: 1 + 18419 record (15 = 3 [4] + 11 + 1) + requite#0,SET test value formatter: 1 + 18434 record (17 = 3 [2] + 13 + 1) + reserve#0,SET test value formatter: 1 + 18451 record (18 = 3 [3] + 14 + 1) + resolutes#0,SET test value formatter: 1 + 18469 record (14 = 3 [5] + 10 + 1) + resolve#0,SET test value formatter: 1 + 18483 record (13 = 3 [3] + 9 + 1) + rest#0,SET test value formatter: 2 + 18496 record (20 = 3 [2] + 16 + 1) + retrograde#0,SET test value formatter: 1 + 18516 record (15 = 3 [3] + 11 + 1) + return#0,SET test value formatter: 2 + 18531 record (16 = 3 [2] + 12 + 1) + reveal#0,SET test value formatter: 1 + 18547 record (13 = 3 [4] + 9 + 1) + revel#0,SET test value formatter: 1 + 18560 record (15 = 3 [4] + 11 + 1) + revenge#0,SET test value formatter: 3 + 18575 record (16 = 3 [3] + 12 + 1) + revisit#0,SET test value formatter: 1 + 18591 record (18 = 3 [1] + 14 + 1) + rhenish#0,SET test value formatter: 1 + 18609 record (15 = 3 [1] + 11 + 1) + rich#0,SET test value formatter: 1 + 18624 record (13 = 3 [2] + 9 + 1) + rid#0,SET test value formatter: 1 + 18637 record (17 = 3 [0] + 13 + 1) [restart] + right#0,SET test value formatter: 3 + 18654 record (14 = 3 [2] + 10 + 1) + rise#0,SET test value formatter: 1 + 18668 record (16 = 3 [2] + 12 + 1) + rivals#0,SET test value formatter: 1 + 18684 record (14 = 3 [3] + 10 + 1) + river#0,SET test value formatter: 1 + 18698 record (15 = 3 [1] + 11 + 1) + roar#0,SET test value formatter: 1 + 18713 record (16 = 3 [2] + 12 + 1) + romage#0,SET test value formatter: 1 + 18729 record (13 = 3 [4] + 9 + 1) + roman#0,SET test value formatter: 1 + 18742 record (13 = 3 [3] + 9 + 1) + rome#0,SET test value formatter: 1 + 18755 record (14 = 3 [2] + 10 + 1) + room#0,SET test value formatter: 2 + 18769 record (14 = 3 [3] + 10 + 1) + roots#0,SET test value formatter: 1 + 18783 record (16 = 3 [2] + 12 + 1) + rotten#0,SET test value formatter: 1 + 18799 record (17 = 3 [2] + 13 + 1) + roughly#0,SET test value formatter: 1 + 18816 record (14 = 3 [3] + 10 + 1) + rouse#0,SET test value formatter: 2 + 18830 record (15 = 3 [2] + 11 + 1) + royal#0,SET test value formatter: 2 + 18845 record (16 = 3 [1] + 12 + 1) + ruled#0,SET test value formatter: 1 + 18861 record (17 = 3 [2] + 13 + 1) + running#0,SET test value formatter: 1 + 18878 record (18 = 3 [0] + 14 + 1) [restart] + russet#0,SET test value formatter: 1 + 18896 record (14 = 3 [0] + 9 + 2) + s#0,SET test value formatter: 39 + 18910 record (16 = 3 [1] + 12 + 1) + sable#0,SET test value formatter: 1 + 18926 record (16 = 3 [2] + 12 + 1) + safety#0,SET test value formatter: 2 + 18942 record (14 = 3 [2] + 10 + 1) + said#0,SET test value formatter: 3 + 18956 record (13 = 3 [3] + 9 + 1) + sail#0,SET test value formatter: 1 + 18969 record (14 = 3 [3] + 10 + 1) + saint#0,SET test value formatter: 1 + 18983 record (14 = 3 [2] + 10 + 1) + salt#0,SET test value formatter: 1 + 18997 record (14 = 3 [2] + 10 + 1) + same#0,SET test value formatter: 5 + 19011 record (20 = 3 [2] + 16 + 1) + sanctified#0,SET test value formatter: 1 + 19031 record (14 = 3 [2] + 10 + 1) + sate#0,SET test value formatter: 1 + 19045 record (14 = 3 [3] + 10 + 1) + satyr#0,SET test value formatter: 1 + 19059 record (17 = 3 [2] + 13 + 1) + saviour#0,SET test value formatter: 1 + 19076 record (13 = 3 [2] + 9 + 1) + saw#0,SET test value formatter: 6 + 19089 record (13 = 3 [3] + 9 + 1) + saws#0,SET test value formatter: 1 + 19102 record (14 = 3 [2] + 9 + 2) + say#0,SET test value formatter: 11 + 19116 record (18 = 3 [0] + 14 + 1) [restart] + saying#0,SET test value formatter: 1 + 19134 record (13 = 3 [3] + 9 + 1) + says#0,SET test value formatter: 3 + 19147 record (16 = 3 [1] + 12 + 1) + scale#0,SET test value formatter: 1 + 19163 record (16 = 3 [3] + 12 + 1) + scandal#0,SET test value formatter: 1 + 19179 record (15 = 3 [4] + 11 + 1) + scanter#0,SET test value formatter: 1 + 19194 record (15 = 3 [3] + 11 + 1) + scapes#0,SET test value formatter: 1 + 19209 record (17 = 3 [3] + 13 + 1) + scarcely#0,SET test value formatter: 1 + 19226 record (15 = 3 [2] + 11 + 1) + scene#0,SET test value formatter: 5 + 19241 record (13 = 3 [4] + 9 + 1) + scent#0,SET test value formatter: 1 + 19254 record (17 = 3 [2] + 13 + 1) + scholar#0,SET test value formatter: 1 + 19271 record (13 = 3 [7] + 9 + 1) + scholars#0,SET test value formatter: 1 + 19284 record (14 = 3 [4] + 10 + 1) + school#0,SET test value formatter: 1 + 19298 record (15 = 3 [2] + 11 + 1) + scope#0,SET test value formatter: 2 + 19313 record (14 = 3 [1] + 10 + 1) + sea#0,SET test value formatter: 3 + 19327 record (13 = 3 [3] + 9 + 1) + seal#0,SET test value formatter: 2 + 19340 record (15 = 3 [3] + 11 + 1) + season#0,SET test value formatter: 4 + 19355 record (16 = 3 [0] + 12 + 1) [restart] + seat#0,SET test value formatter: 1 + 19371 record (16 = 3 [2] + 12 + 1) + second#0,SET test value formatter: 1 + 19387 record (16 = 3 [3] + 12 + 1) + secrecy#0,SET test value formatter: 1 + 19403 record (13 = 3 [5] + 9 + 1) + secret#0,SET test value formatter: 1 + 19416 record (13 = 3 [6] + 9 + 1) + secrets#0,SET test value formatter: 1 + 19429 record (15 = 3 [3] + 11 + 1) + secure#0,SET test value formatter: 2 + 19444 record (16 = 3 [2] + 12 + 1) + seduce#0,SET test value formatter: 1 + 19460 record (13 = 3 [2] + 9 + 1) + see#0,SET test value formatter: 7 + 19473 record (13 = 3 [3] + 9 + 1) + seed#0,SET test value formatter: 1 + 19486 record (15 = 3 [3] + 11 + 1) + seeing#0,SET test value formatter: 1 + 19501 record (13 = 3 [3] + 9 + 1) + seek#0,SET test value formatter: 1 + 19514 record (13 = 3 [3] + 9 + 1) + seem#0,SET test value formatter: 2 + 19527 record (15 = 3 [4] + 11 + 1) + seeming#0,SET test value formatter: 1 + 19542 record (13 = 3 [4] + 9 + 1) + seems#0,SET test value formatter: 3 + 19555 record (13 = 3 [3] + 9 + 1) + seen#0,SET test value formatter: 8 + 19568 record (16 = 3 [2] + 12 + 1) + seized#0,SET test value formatter: 1 + 19584 record (18 = 3 [0] + 14 + 1) [restart] + select#0,SET test value formatter: 1 + 19602 record (13 = 3 [3] + 9 + 1) + self#0,SET test value formatter: 1 + 19615 record (15 = 3 [2] + 11 + 1) + sense#0,SET test value formatter: 1 + 19630 record (16 = 3 [4] + 12 + 1) + sensible#0,SET test value formatter: 1 + 19646 record (13 = 3 [3] + 9 + 1) + sent#0,SET test value formatter: 1 + 19659 record (19 = 3 [2] + 15 + 1) + sepulchre#0,SET test value formatter: 1 + 19678 record (17 = 3 [2] + 13 + 1) + serious#0,SET test value formatter: 1 + 19695 record (16 = 3 [3] + 12 + 1) + serpent#0,SET test value formatter: 2 + 19711 record (16 = 3 [3] + 12 + 1) + servant#0,SET test value formatter: 1 + 19727 record (13 = 3 [7] + 9 + 1) + servants#0,SET test value formatter: 1 + 19740 record (15 = 3 [4] + 11 + 1) + service#0,SET test value formatter: 1 + 19755 record (13 = 3 [2] + 9 + 1) + set#0,SET test value formatter: 4 + 19768 record (16 = 3 [1] + 12 + 1) + shake#0,SET test value formatter: 2 + 19784 record (15 = 3 [3] + 10 + 2) + shall#0,SET test value formatter: 22 + 19799 record (13 = 3 [4] + 9 + 1) + shalt#0,SET test value formatter: 1 + 19812 record (14 = 3 [3] + 10 + 1) + shame#0,SET test value formatter: 1 + 19826 record (20 = 3 [0] + 16 + 1) [restart] + shameful#0,SET test value formatter: 1 + 19846 record (14 = 3 [3] + 10 + 1) + shape#0,SET test value formatter: 2 + 19860 record (13 = 3 [5] + 9 + 1) + shapes#0,SET test value formatter: 1 + 19873 record (14 = 3 [3] + 10 + 1) + shark#0,SET test value formatter: 1 + 19887 record (13 = 3 [2] + 9 + 1) + she#0,SET test value formatter: 6 + 19900 record (16 = 3 [3] + 12 + 1) + sheeted#0,SET test value formatter: 1 + 19916 record (13 = 3 [5] + 9 + 1) + sheets#0,SET test value formatter: 1 + 19929 record (15 = 3 [2] + 11 + 1) + shift#0,SET test value formatter: 1 + 19944 record (20 = 3 [3] + 16 + 1) + shipwrights#0,SET test value formatter: 1 + 19964 record (15 = 3 [2] + 11 + 1) + shoes#0,SET test value formatter: 1 + 19979 record (13 = 3 [3] + 9 + 1) + shot#0,SET test value formatter: 2 + 19992 record (15 = 3 [3] + 11 + 1) + should#0,SET test value formatter: 6 + 20007 record (14 = 3 [6] + 10 + 1) + shoulder#0,SET test value formatter: 1 + 20021 record (14 = 3 [6] + 10 + 1) + shouldst#0,SET test value formatter: 1 + 20035 record (13 = 3 [3] + 9 + 1) + show#0,SET test value formatter: 6 + 20048 record (13 = 3 [4] + 9 + 1) + shows#0,SET test value formatter: 2 + 20061 record (20 = 3 [0] + 16 + 1) [restart] + shrewdly#0,SET test value formatter: 1 + 20081 record (15 = 3 [3] + 11 + 1) + shrill#0,SET test value formatter: 1 + 20096 record (15 = 3 [3] + 11 + 1) + shrunk#0,SET test value formatter: 1 + 20111 record (15 = 3 [1] + 11 + 1) + sick#0,SET test value formatter: 2 + 20126 record (14 = 3 [2] + 10 + 1) + side#0,SET test value formatter: 1 + 20140 record (15 = 3 [2] + 11 + 1) + sight#0,SET test value formatter: 3 + 20155 record (17 = 3 [2] + 13 + 1) + silence#0,SET test value formatter: 1 + 20172 record (15 = 3 [3] + 11 + 1) + silver#0,SET test value formatter: 1 + 20187 record (16 = 3 [2] + 12 + 1) + simple#0,SET test value formatter: 1 + 20203 record (13 = 3 [2] + 9 + 1) + sin#0,SET test value formatter: 1 + 20216 record (14 = 3 [3] + 10 + 1) + since#0,SET test value formatter: 1 + 20230 record (15 = 3 [3] + 11 + 1) + sinews#0,SET test value formatter: 1 + 20245 record (16 = 3 [3] + 12 + 1) + singeth#0,SET test value formatter: 1 + 20261 record (13 = 3 [2] + 9 + 1) + sir#0,SET test value formatter: 3 + 20274 record (13 = 3 [3] + 9 + 1) + sirs#0,SET test value formatter: 1 + 20287 record (16 = 3 [2] + 12 + 1) + sister#0,SET test value formatter: 3 + 20303 record (15 = 3 [0] + 11 + 1) [restart] + sit#0,SET test value formatter: 4 + 20318 record (13 = 3 [3] + 9 + 1) + sits#0,SET test value formatter: 2 + 20331 record (17 = 3 [1] + 13 + 1) + skirts#0,SET test value formatter: 1 + 20348 record (18 = 3 [1] + 14 + 1) + slander#0,SET test value formatter: 1 + 20366 record (18 = 3 [3] + 14 + 1) + slaughter#0,SET test value formatter: 1 + 20384 [restart 18384] + 20388 [restart 18637] + 20392 [restart 18878] + 20396 [restart 19116] + 20400 [restart 19355] + 20404 [restart 19584] + 20408 [restart 19826] + 20412 [restart 20061] + 20416 [restart 20303] + 20424 [trailer compression=none checksum=0x8953c396] + 20429 data (2030) + 20429 record (16 = 3 [0] + 12 + 1) [restart] + slay#0,SET test value formatter: 1 + 20445 record (17 = 3 [2] + 13 + 1) + sledded#0,SET test value formatter: 1 + 20462 record (14 = 3 [3] + 10 + 1) + sleep#0,SET test value formatter: 1 + 20476 record (15 = 3 [5] + 11 + 1) + sleeping#0,SET test value formatter: 3 + 20491 record (14 = 3 [2] + 10 + 1) + slow#0,SET test value formatter: 2 + 20505 record (16 = 3 [1] + 12 + 1) + smile#0,SET test value formatter: 2 + 20521 record (13 = 3 [5] + 9 + 1) + smiles#0,SET test value formatter: 1 + 20534 record (15 = 3 [4] + 11 + 1) + smiling#0,SET test value formatter: 2 + 20549 record (16 = 3 [2] + 12 + 1) + smooth#0,SET test value formatter: 1 + 20565 record (14 = 3 [3] + 10 + 1) + smote#0,SET test value formatter: 1 + 20579 record (14 = 3 [1] + 9 + 2) + so#0,SET test value formatter: 48 + 20593 record (13 = 3 [2] + 9 + 1) + soe#0,SET test value formatter: 1 + 20606 record (14 = 3 [2] + 10 + 1) + soft#0,SET test value formatter: 2 + 20620 record (14 = 3 [2] + 10 + 1) + soil#0,SET test value formatter: 2 + 20634 record (17 = 3 [2] + 13 + 1) + soldier#0,SET test value formatter: 1 + 20651 record (13 = 3 [7] + 9 + 1) + soldiers#0,SET test value formatter: 1 + 20664 record (18 = 3 [0] + 14 + 1) [restart] + solemn#0,SET test value formatter: 2 + 20682 record (14 = 3 [3] + 10 + 1) + solid#0,SET test value formatter: 1 + 20696 record (15 = 3 [2] + 10 + 2) + some#0,SET test value formatter: 13 + 20711 record (17 = 3 [4] + 13 + 1) + something#0,SET test value formatter: 3 + 20728 record (15 = 3 [5] + 11 + 1) + sometime#0,SET test value formatter: 1 + 20743 record (13 = 3 [8] + 9 + 1) + sometimes#0,SET test value formatter: 1 + 20756 record (16 = 3 [4] + 12 + 1) + somewhat#0,SET test value formatter: 1 + 20772 record (13 = 3 [2] + 9 + 1) + son#0,SET test value formatter: 3 + 20785 record (14 = 3 [3] + 10 + 1) + songs#0,SET test value formatter: 1 + 20799 record (14 = 3 [2] + 10 + 1) + sore#0,SET test value formatter: 1 + 20813 record (15 = 3 [3] + 11 + 1) + sorrow#0,SET test value formatter: 3 + 20828 record (13 = 3 [4] + 9 + 1) + sorry#0,SET test value formatter: 1 + 20841 record (13 = 3 [3] + 9 + 1) + sort#0,SET test value formatter: 1 + 20854 record (14 = 3 [2] + 10 + 1) + soul#0,SET test value formatter: 8 + 20868 record (13 = 3 [4] + 9 + 1) + souls#0,SET test value formatter: 1 + 20881 record (14 = 3 [3] + 10 + 1) + sound#0,SET test value formatter: 2 + 20895 record (20 = 3 [0] + 16 + 1) [restart] + sounding#0,SET test value formatter: 1 + 20915 record (15 = 3 [3] + 11 + 1) + source#0,SET test value formatter: 1 + 20930 record (21 = 3 [2] + 17 + 1) + sovereignty#0,SET test value formatter: 1 + 20951 record (17 = 3 [1] + 12 + 2) + speak#0,SET test value formatter: 27 + 20968 record (15 = 3 [5] + 11 + 1) + speaking#0,SET test value formatter: 1 + 20983 record (15 = 3 [3] + 11 + 1) + speech#0,SET test value formatter: 1 + 20998 record (13 = 3 [4] + 9 + 1) + speed#0,SET test value formatter: 1 + 21011 record (14 = 3 [3] + 10 + 1) + spend#0,SET test value formatter: 1 + 21025 record (17 = 3 [2] + 13 + 1) + spheres#0,SET test value formatter: 1 + 21042 record (16 = 3 [2] + 12 + 1) + spirit#0,SET test value formatter: 8 + 21058 record (13 = 3 [6] + 9 + 1) + spirits#0,SET test value formatter: 1 + 21071 record (14 = 3 [3] + 10 + 1) + spite#0,SET test value formatter: 1 + 21085 record (15 = 3 [2] + 11 + 1) + spoke#0,SET test value formatter: 1 + 21100 record (16 = 3 [2] + 12 + 1) + spring#0,SET test value formatter: 2 + 21116 record (14 = 3 [6] + 10 + 1) + springes#0,SET test value formatter: 1 + 21130 record (17 = 3 [1] + 13 + 1) + squeak#0,SET test value formatter: 1 + 21147 record (14 = 3 [0] + 10 + 1) [restart] + st#0,SET test value formatter: 4 + 21161 record (15 = 3 [2] + 11 + 1) + stale#0,SET test value formatter: 1 + 21176 record (13 = 3 [4] + 9 + 1) + stalk#0,SET test value formatter: 1 + 21189 record (13 = 3 [5] + 9 + 1) + stalks#0,SET test value formatter: 1 + 21202 record (14 = 3 [3] + 10 + 1) + stamp#0,SET test value formatter: 1 + 21216 record (14 = 3 [3] + 10 + 1) + stand#0,SET test value formatter: 5 + 21230 record (13 = 3 [5] + 9 + 1) + stands#0,SET test value formatter: 1 + 21243 record (13 = 3 [3] + 9 + 1) + star#0,SET test value formatter: 3 + 21256 record (13 = 3 [4] + 9 + 1) + stars#0,SET test value formatter: 2 + 21269 record (13 = 3 [4] + 9 + 1) + start#0,SET test value formatter: 1 + 21282 record (14 = 3 [5] + 10 + 1) + started#0,SET test value formatter: 1 + 21296 record (14 = 3 [3] + 10 + 1) + state#0,SET test value formatter: 8 + 21310 record (14 = 3 [5] + 10 + 1) + stately#0,SET test value formatter: 1 + 21324 record (15 = 3 [4] + 11 + 1) + station#0,SET test value formatter: 1 + 21339 record (13 = 3 [3] + 9 + 1) + stay#0,SET test value formatter: 7 + 21352 record (15 = 3 [2] + 11 + 1) + steel#0,SET test value formatter: 2 + 21367 record (17 = 3 [0] + 13 + 1) [restart] + steep#0,SET test value formatter: 1 + 21384 record (17 = 3 [3] + 13 + 1) + sterling#0,SET test value formatter: 1 + 21401 record (17 = 3 [2] + 13 + 1) + stiffly#0,SET test value formatter: 1 + 21418 record (14 = 3 [3] + 10 + 1) + still#0,SET test value formatter: 8 + 21432 record (14 = 3 [3] + 10 + 1) + sting#0,SET test value formatter: 2 + 21446 record (13 = 3 [3] + 9 + 1) + stir#0,SET test value formatter: 2 + 21459 record (16 = 3 [4] + 12 + 1) + stirring#0,SET test value formatter: 1 + 21475 record (15 = 3 [2] + 11 + 1) + stole#0,SET test value formatter: 1 + 21490 record (16 = 3 [3] + 12 + 1) + stomach#0,SET test value formatter: 1 + 21506 record (14 = 3 [3] + 10 + 1) + stood#0,SET test value formatter: 2 + 21520 record (13 = 3 [3] + 9 + 1) + stop#0,SET test value formatter: 1 + 21533 record (14 = 3 [3] + 10 + 1) + story#0,SET test value formatter: 1 + 21547 record (17 = 3 [2] + 13 + 1) + strange#0,SET test value formatter: 6 + 21564 record (13 = 3 [7] + 9 + 1) + stranger#0,SET test value formatter: 1 + 21577 record (16 = 3 [3] + 12 + 1) + streets#0,SET test value formatter: 1 + 21593 record (15 = 3 [3] + 11 + 1) + strict#0,SET test value formatter: 1 + 21608 record (18 = 3 [0] + 14 + 1) [restart] + strike#0,SET test value formatter: 2 + 21626 record (16 = 3 [3] + 12 + 1) + strokes#0,SET test value formatter: 1 + 21642 record (14 = 3 [4] + 10 + 1) + strong#0,SET test value formatter: 1 + 21656 record (15 = 3 [3] + 11 + 1) + struck#0,SET test value formatter: 2 + 21671 record (22 = 3 [2] + 18 + 1) + stubbornness#0,SET test value formatter: 1 + 21693 record (16 = 3 [3] + 12 + 1) + student#0,SET test value formatter: 1 + 21709 record (14 = 3 [3] + 10 + 1) + stung#0,SET test value formatter: 1 + 21723 record (18 = 3 [1] + 14 + 1) + subject#0,SET test value formatter: 3 + 21741 record (18 = 3 [3] + 14 + 1) + substance#0,SET test value formatter: 1 + 21759 record (15 = 3 [2] + 10 + 2) + such#0,SET test value formatter: 10 + 21774 record (16 = 3 [2] + 12 + 1) + sudden#0,SET test value formatter: 1 + 21790 record (14 = 3 [2] + 10 + 1) + suit#0,SET test value formatter: 1 + 21804 record (13 = 3 [4] + 9 + 1) + suits#0,SET test value formatter: 3 + 21817 record (20 = 3 [2] + 16 + 1) + sulphurous#0,SET test value formatter: 1 + 21837 record (16 = 3 [2] + 12 + 1) + summit#0,SET test value formatter: 1 + 21853 record (15 = 3 [4] + 11 + 1) + summons#0,SET test value formatter: 1 + 21868 record (15 = 3 [0] + 11 + 1) [restart] + sun#0,SET test value formatter: 2 + 21883 record (15 = 3 [3] + 11 + 1) + sunday#0,SET test value formatter: 1 + 21898 record (20 = 3 [2] + 16 + 1) + suppliance#0,SET test value formatter: 1 + 21918 record (16 = 3 [4] + 12 + 1) + supposal#0,SET test value formatter: 1 + 21934 record (16 = 3 [4] + 12 + 1) + suppress#0,SET test value formatter: 1 + 21950 record (14 = 3 [2] + 10 + 1) + sure#0,SET test value formatter: 1 + 21964 record (18 = 3 [3] + 14 + 1) + surprised#0,SET test value formatter: 1 + 21982 record (18 = 3 [3] + 14 + 1) + surrender#0,SET test value formatter: 1 + 22000 record (17 = 3 [3] + 13 + 1) + survivor#0,SET test value formatter: 1 + 22017 record (21 = 3 [2] + 17 + 1) + suspiration#0,SET test value formatter: 1 + 22038 record (16 = 3 [3] + 12 + 1) + sustain#0,SET test value formatter: 1 + 22054 record (21 = 3 [1] + 17 + 1) + swaggering#0,SET test value formatter: 1 + 22075 record (16 = 3 [2] + 11 + 2) + swear#0,SET test value formatter: 10 + 22091 record (14 = 3 [4] + 10 + 1) + sweaty#0,SET test value formatter: 1 + 22105 record (14 = 3 [3] + 10 + 1) + sweep#0,SET test value formatter: 1 + 22119 record (13 = 3 [4] + 9 + 1) + sweet#0,SET test value formatter: 2 + 22132 record (17 = 3 [0] + 13 + 1) [restart] + swift#0,SET test value formatter: 2 + 22149 record (16 = 3 [3] + 12 + 1) + swinish#0,SET test value formatter: 1 + 22165 record (15 = 3 [2] + 11 + 1) + sword#0,SET test value formatter: 5 + 22180 record (13 = 3 [4] + 9 + 1) + sworn#0,SET test value formatter: 2 + 22193 record (14 = 3 [0] + 9 + 2) + t#0,SET test value formatter: 18 + 22207 record (13 = 3 [1] + 9 + 1) + ta#0,SET test value formatter: 1 + 22220 record (15 = 3 [2] + 11 + 1) + table#0,SET test value formatter: 1 + 22235 record (13 = 3 [5] + 9 + 1) + tables#0,SET test value formatter: 2 + 22248 record (15 = 3 [2] + 11 + 1) + taint#0,SET test value formatter: 1 + 22263 record (15 = 3 [2] + 10 + 2) + take#0,SET test value formatter: 10 + 22278 record (13 = 3 [4] + 9 + 1) + taken#0,SET test value formatter: 1 + 22291 record (13 = 3 [4] + 9 + 1) + takes#0,SET test value formatter: 3 + 22304 record (14 = 3 [2] + 10 + 1) + tale#0,SET test value formatter: 1 + 22318 record (13 = 3 [3] + 9 + 1) + talk#0,SET test value formatter: 1 + 22331 record (14 = 3 [2] + 10 + 1) + task#0,SET test value formatter: 1 + 22345 record (13 = 3 [2] + 9 + 1) + tax#0,SET test value formatter: 1 + 22358 record (17 = 3 [0] + 13 + 1) [restart] + teach#0,SET test value formatter: 2 + 22375 record (14 = 3 [3] + 10 + 1) + tears#0,SET test value formatter: 2 + 22389 record (14 = 3 [2] + 10 + 1) + tell#0,SET test value formatter: 9 + 22403 record (16 = 3 [2] + 12 + 1) + temple#0,SET test value formatter: 1 + 22419 [restart 20429] + 22423 [restart 20664] + 22427 [restart 20895] + 22431 [restart 21147] + 22435 [restart 21367] + 22439 [restart 21608] + 22443 [restart 21868] + 22447 [restart 22132] + 22451 [restart 22358] + 22459 [trailer compression=none checksum=0x53c39637] + 22464 data (2035) + 22464 record (17 = 3 [0] + 13 + 1) [restart] + tempt#0,SET test value formatter: 1 + 22481 record (17 = 3 [2] + 13 + 1) + tenable#0,SET test value formatter: 1 + 22498 record (18 = 3 [4] + 14 + 1) + tenantless#0,SET test value formatter: 1 + 22516 record (13 = 3 [3] + 9 + 1) + tend#0,SET test value formatter: 1 + 22529 record (14 = 3 [4] + 10 + 1) + tender#0,SET test value formatter: 2 + 22543 record (13 = 3 [6] + 9 + 1) + tenders#0,SET test value formatter: 3 + 22556 record (14 = 3 [2] + 10 + 1) + term#0,SET test value formatter: 2 + 22570 record (13 = 3 [4] + 9 + 1) + terms#0,SET test value formatter: 2 + 22583 record (16 = 3 [2] + 12 + 1) + tether#0,SET test value formatter: 1 + 22599 record (15 = 3 [3] + 11 + 1) + tetter#0,SET test value formatter: 1 + 22614 record (16 = 3 [1] + 11 + 2) + than#0,SET test value formatter: 15 + 22630 record (14 = 3 [4] + 10 + 1) + thanks#0,SET test value formatter: 2 + 22644 record (14 = 3 [3] + 9 + 2) + that#0,SET test value formatter: 83 + 22658 record (13 = 3 [3] + 9 + 1) + thaw#0,SET test value formatter: 1 + 22671 record (15 = 3 [2] + 9 + 3) + the#0,SET test value formatter: 237 + 22686 record (14 = 3 [3] + 9 + 2) + thee#0,SET test value formatter: 23 + 22700 record (18 = 3 [0] + 13 + 2) [restart] + their#0,SET test value formatter: 10 + 22718 record (14 = 3 [3] + 9 + 2) + them#0,SET test value formatter: 10 + 22732 record (13 = 3 [4] + 9 + 1) + theme#0,SET test value formatter: 1 + 22745 record (14 = 3 [3] + 9 + 2) + then#0,SET test value formatter: 15 + 22759 record (15 = 3 [3] + 10 + 2) + there#0,SET test value formatter: 18 + 22774 record (16 = 3 [5] + 12 + 1) + therefore#0,SET test value formatter: 4 + 22790 record (14 = 3 [5] + 10 + 1) + thereto#0,SET test value formatter: 1 + 22804 record (15 = 3 [3] + 10 + 2) + these#0,SET test value formatter: 13 + 22819 record (14 = 3 [3] + 10 + 1) + thews#0,SET test value formatter: 1 + 22833 record (14 = 3 [3] + 9 + 2) + they#0,SET test value formatter: 14 + 22847 record (14 = 3 [2] + 10 + 1) + thin#0,SET test value formatter: 1 + 22861 record (13 = 3 [4] + 9 + 1) + thine#0,SET test value formatter: 3 + 22874 record (13 = 3 [4] + 9 + 1) + thing#0,SET test value formatter: 6 + 22887 record (13 = 3 [5] + 9 + 1) + things#0,SET test value formatter: 3 + 22900 record (14 = 3 [4] + 9 + 2) + think#0,SET test value formatter: 16 + 22914 record (15 = 3 [5] + 11 + 1) + thinking#0,SET test value formatter: 1 + 22929 record (17 = 3 [0] + 13 + 1) [restart] + third#0,SET test value formatter: 1 + 22946 record (14 = 3 [3] + 9 + 2) + this#0,SET test value formatter: 67 + 22960 record (16 = 3 [2] + 12 + 1) + thorns#0,SET test value formatter: 1 + 22976 record (13 = 3 [5] + 9 + 1) + thorny#0,SET test value formatter: 1 + 22989 record (14 = 3 [3] + 10 + 1) + those#0,SET test value formatter: 7 + 23003 record (14 = 3 [3] + 9 + 2) + thou#0,SET test value formatter: 28 + 23017 record (15 = 3 [4] + 10 + 2) + though#0,SET test value formatter: 10 + 23032 record (13 = 3 [6] + 9 + 1) + thought#0,SET test value formatter: 2 + 23045 record (13 = 3 [7] + 9 + 1) + thoughts#0,SET test value formatter: 4 + 23058 record (16 = 3 [2] + 12 + 1) + thrice#0,SET test value formatter: 1 + 23074 record (14 = 3 [4] + 10 + 1) + thrift#0,SET test value formatter: 2 + 23088 record (15 = 3 [3] + 11 + 1) + throat#0,SET test value formatter: 1 + 23103 record (14 = 3 [4] + 10 + 1) + throne#0,SET test value formatter: 2 + 23117 record (15 = 3 [4] + 11 + 1) + through#0,SET test value formatter: 3 + 23132 record (13 = 3 [4] + 9 + 1) + throw#0,SET test value formatter: 1 + 23145 record (17 = 3 [2] + 13 + 1) + thunder#0,SET test value formatter: 1 + 23162 record (16 = 3 [0] + 12 + 1) [restart] + thus#0,SET test value formatter: 9 + 23178 record (14 = 3 [2] + 9 + 2) + thy#0,SET test value formatter: 36 + 23192 record (16 = 3 [3] + 12 + 1) + thyself#0,SET test value formatter: 1 + 23208 record (15 = 3 [1] + 11 + 1) + till#0,SET test value formatter: 4 + 23223 record (15 = 3 [2] + 10 + 2) + time#0,SET test value formatter: 10 + 23238 record (13 = 3 [4] + 9 + 1) + times#0,SET test value formatter: 1 + 23251 record (14 = 3 [2] + 9 + 2) + tis#0,SET test value formatter: 22 + 23265 record (15 = 3 [1] + 9 + 3) + to#0,SET test value formatter: 192 + 23280 record (13 = 3 [2] + 9 + 1) + toe#0,SET test value formatter: 1 + 23293 record (18 = 3 [2] + 14 + 1) + together#0,SET test value formatter: 7 + 23311 record (15 = 3 [2] + 11 + 1) + toils#0,SET test value formatter: 1 + 23326 record (14 = 3 [2] + 10 + 1) + told#0,SET test value formatter: 2 + 23340 record (16 = 3 [2] + 12 + 1) + tongue#0,SET test value formatter: 4 + 23356 record (13 = 3 [2] + 9 + 1) + too#0,SET test value formatter: 9 + 23369 record (13 = 3 [2] + 9 + 1) + top#0,SET test value formatter: 1 + 23382 record (20 = 3 [2] + 16 + 1) + tormenting#0,SET test value formatter: 1 + 23402 record (20 = 3 [0] + 16 + 1) [restart] + touching#0,SET test value formatter: 3 + 23422 record (16 = 3 [2] + 12 + 1) + toward#0,SET test value formatter: 4 + 23438 record (13 = 3 [2] + 9 + 1) + toy#0,SET test value formatter: 1 + 23451 record (13 = 3 [3] + 9 + 1) + toys#0,SET test value formatter: 1 + 23464 record (19 = 3 [1] + 15 + 1) + traduced#0,SET test value formatter: 1 + 23483 record (16 = 3 [3] + 12 + 1) + tragedy#0,SET test value formatter: 1 + 23499 record (15 = 3 [3] + 11 + 1) + trains#0,SET test value formatter: 1 + 23514 record (18 = 3 [4] + 14 + 1) + traitorous#0,SET test value formatter: 1 + 23532 record (18 = 3 [3] + 14 + 1) + trappings#0,SET test value formatter: 1 + 23550 record (16 = 3 [2] + 12 + 1) + treads#0,SET test value formatter: 1 + 23566 record (16 = 3 [4] + 12 + 1) + treasure#0,SET test value formatter: 2 + 23582 record (16 = 3 [3] + 12 + 1) + tremble#0,SET test value formatter: 1 + 23598 record (15 = 3 [2] + 11 + 1) + tried#0,SET test value formatter: 1 + 23613 record (17 = 3 [3] + 13 + 1) + trifling#0,SET test value formatter: 1 + 23630 record (16 = 3 [3] + 12 + 1) + triumph#0,SET test value formatter: 1 + 23646 record (16 = 3 [3] + 12 + 1) + trivial#0,SET test value formatter: 1 + 23662 record (19 = 3 [0] + 15 + 1) [restart] + trouble#0,SET test value formatter: 1 + 23681 record (13 = 3 [7] + 9 + 1) + troubles#0,SET test value formatter: 1 + 23694 record (16 = 3 [2] + 12 + 1) + truant#0,SET test value formatter: 2 + 23710 record (13 = 3 [3] + 9 + 1) + true#0,SET test value formatter: 5 + 23723 record (17 = 3 [4] + 13 + 1) + truepenny#0,SET test value formatter: 1 + 23740 record (14 = 3 [3] + 10 + 1) + truly#0,SET test value formatter: 1 + 23754 record (16 = 3 [3] + 12 + 1) + trumpet#0,SET test value formatter: 2 + 23770 record (13 = 3 [7] + 9 + 1) + trumpets#0,SET test value formatter: 1 + 23783 record (18 = 3 [3] + 14 + 1) + truncheon#0,SET test value formatter: 1 + 23801 record (16 = 3 [3] + 12 + 1) + truster#0,SET test value formatter: 1 + 23817 record (14 = 3 [3] + 10 + 1) + truth#0,SET test value formatter: 2 + 23831 record (15 = 3 [1] + 11 + 1) + tush#0,SET test value formatter: 2 + 23846 record (17 = 3 [1] + 13 + 1) + twelve#0,SET test value formatter: 3 + 23863 record (14 = 3 [3] + 10 + 1) + twere#0,SET test value formatter: 1 + 23877 record (15 = 3 [2] + 11 + 1) + twice#0,SET test value formatter: 2 + 23892 record (14 = 3 [3] + 10 + 1) + twill#0,SET test value formatter: 2 + 23906 record (17 = 3 [0] + 13 + 1) [restart] + twixt#0,SET test value formatter: 1 + 23923 record (13 = 3 [2] + 9 + 1) + two#0,SET test value formatter: 5 + 23936 record (18 = 3 [0] + 14 + 1) + ubique#0,SET test value formatter: 1 + 23954 record (17 = 3 [1] + 13 + 1) + unanel#0,SET test value formatter: 1 + 23971 record (15 = 3 [2] + 11 + 1) + uncle#0,SET test value formatter: 5 + 23986 record (17 = 3 [2] + 13 + 1) + undergo#0,SET test value formatter: 1 + 24003 record (17 = 3 [5] + 13 + 1) + understand#0,SET test value formatter: 1 + 24020 record (15 = 3 [10] + 11 + 1) + understanding#0,SET test value formatter: 2 + 24035 record (21 = 3 [2] + 17 + 1) + uneffectual#0,SET test value formatter: 1 + 24056 record (19 = 3 [2] + 15 + 1) + unfledged#0,SET test value formatter: 1 + 24075 record (15 = 3 [3] + 11 + 1) + unfold#0,SET test value formatter: 3 + 24090 record (16 = 3 [4] + 12 + 1) + unforced#0,SET test value formatter: 1 + 24106 record (18 = 3 [5] + 14 + 1) + unfortified#0,SET test value formatter: 1 + 24124 record (20 = 3 [2] + 16 + 1) + ungracious#0,SET test value formatter: 1 + 24144 record (16 = 3 [2] + 12 + 1) + unhand#0,SET test value formatter: 1 + 24160 record (15 = 3 [3] + 11 + 1) + unholy#0,SET test value formatter: 1 + 24175 record (20 = 3 [0] + 16 + 1) [restart] + unhousel#0,SET test value formatter: 1 + 24195 record (20 = 3 [2] + 16 + 1) + unimproved#0,SET test value formatter: 1 + 24215 record (17 = 3 [2] + 13 + 1) + unmanly#0,SET test value formatter: 1 + 24232 record (14 = 3 [4] + 10 + 1) + unmask#0,SET test value formatter: 1 + 24246 record (15 = 3 [5] + 11 + 1) + unmaster#0,SET test value formatter: 1 + 24261 record (14 = 3 [3] + 10 + 1) + unmix#0,SET test value formatter: 1 + 24275 record (19 = 3 [2] + 15 + 1) + unnatural#0,SET test value formatter: 2 + 24294 record (22 = 3 [2] + 18 + 1) + unprevailing#0,SET test value formatter: 1 + 24316 record (20 = 3 [4] + 16 + 1) + unprofitable#0,SET test value formatter: 1 + 24336 record (21 = 3 [5] + 17 + 1) + unproportioned#0,SET test value formatter: 1 + 24357 record (21 = 3 [2] + 17 + 1) + unrighteous#0,SET test value formatter: 1 + 24378 record (18 = 3 [2] + 14 + 1) + unschool#0,SET test value formatter: 1 + 24396 record (17 = 3 [3] + 13 + 1) + unsifted#0,SET test value formatter: 1 + 24413 record (14 = 3 [2] + 10 + 1) + unto#0,SET test value formatter: 4 + 24427 record (18 = 3 [2] + 14 + 1) + unvalued#0,SET test value formatter: 1 + 24445 record (18 = 3 [2] + 14 + 1) + unweeded#0,SET test value formatter: 1 + 24463 [restart 22464] + 24467 [restart 22700] + 24471 [restart 22929] + 24475 [restart 23162] + 24479 [restart 23402] + 24483 [restart 23662] + 24487 [restart 23906] + 24491 [restart 24175] + 24499 [trailer compression=none checksum=0xe12c134e] + 24504 data (2036) + 24504 record (15 = 3 [0] + 10 + 2) [restart] + up#0,SET test value formatter: 10 + 24519 record (19 = 3 [2] + 15 + 1) + uphoarded#0,SET test value formatter: 1 + 24538 record (15 = 3 [2] + 10 + 2) + upon#0,SET test value formatter: 18 + 24553 record (14 = 3 [1] + 9 + 2) + us#0,SET test value formatter: 19 + 24567 record (13 = 3 [2] + 9 + 1) + use#0,SET test value formatter: 1 + 24580 record (13 = 3 [3] + 9 + 1) + uses#0,SET test value formatter: 1 + 24593 record (15 = 3 [2] + 11 + 1) + usurp#0,SET test value formatter: 1 + 24608 record (13 = 3 [0] + 9 + 1) + v#0,SET test value formatter: 1 + 24621 record (17 = 3 [1] + 13 + 1) + vailed#0,SET test value formatter: 1 + 24638 record (13 = 3 [3] + 9 + 1) + vain#0,SET test value formatter: 1 + 24651 record (17 = 3 [2] + 13 + 1) + valiant#0,SET test value formatter: 2 + 24668 record (16 = 3 [2] + 12 + 1) + vanish#0,SET test value formatter: 1 + 24684 record (19 = 3 [3] + 15 + 1) + vanquisher#0,SET test value formatter: 1 + 24703 record (14 = 3 [2] + 10 + 1) + vast#0,SET test value formatter: 1 + 24717 record (15 = 3 [1] + 11 + 1) + very#0,SET test value formatter: 9 + 24732 record (15 = 3 [1] + 11 + 1) + vial#0,SET test value formatter: 1 + 24747 record (19 = 3 [0] + 15 + 1) [restart] + vicious#0,SET test value formatter: 1 + 24766 record (16 = 3 [2] + 12 + 1) + vigour#0,SET test value formatter: 1 + 24782 record (14 = 3 [2] + 10 + 1) + vile#0,SET test value formatter: 1 + 24796 record (16 = 3 [3] + 12 + 1) + villain#0,SET test value formatter: 5 + 24812 record (18 = 3 [2] + 14 + 1) + violence#0,SET test value formatter: 2 + 24830 record (13 = 3 [5] + 9 + 1) + violet#0,SET test value formatter: 1 + 24843 record (16 = 3 [2] + 12 + 1) + virtue#0,SET test value formatter: 3 + 24859 record (13 = 3 [6] + 9 + 1) + virtues#0,SET test value formatter: 1 + 24872 record (15 = 3 [5] + 11 + 1) + virtuous#0,SET test value formatter: 1 + 24887 record (16 = 3 [2] + 12 + 1) + visage#0,SET test value formatter: 1 + 24903 record (15 = 3 [3] + 11 + 1) + vision#0,SET test value formatter: 1 + 24918 record (13 = 3 [4] + 9 + 1) + visit#0,SET test value formatter: 2 + 24931 record (16 = 3 [1] + 12 + 1) + voice#0,SET test value formatter: 5 + 24947 record (19 = 3 [2] + 15 + 1) + voltimand#0,SET test value formatter: 4 + 24966 record (15 = 3 [3] + 11 + 1) + volume#0,SET test value formatter: 1 + 24981 record (13 = 3 [2] + 9 + 1) + vow#0,SET test value formatter: 1 + 24994 record (16 = 3 [0] + 12 + 1) [restart] + vows#0,SET test value formatter: 3 + 25010 record (17 = 3 [1] + 13 + 1) + vulgar#0,SET test value formatter: 2 + 25027 record (16 = 3 [0] + 12 + 1) + wake#0,SET test value formatter: 1 + 25043 record (14 = 3 [2] + 10 + 1) + walk#0,SET test value formatter: 6 + 25057 record (13 = 3 [4] + 9 + 1) + walks#0,SET test value formatter: 1 + 25070 record (15 = 3 [2] + 11 + 1) + wants#0,SET test value formatter: 1 + 25085 record (13 = 3 [2] + 9 + 1) + war#0,SET test value formatter: 1 + 25098 record (16 = 3 [3] + 12 + 1) + warlike#0,SET test value formatter: 2 + 25114 record (16 = 3 [3] + 12 + 1) + warning#0,SET test value formatter: 1 + 25130 record (16 = 3 [3] + 12 + 1) + warrant#0,SET test value formatter: 1 + 25146 record (13 = 3 [3] + 9 + 1) + wars#0,SET test value formatter: 1 + 25159 record (13 = 3 [3] + 9 + 1) + wary#0,SET test value formatter: 1 + 25172 record (14 = 3 [2] + 9 + 2) + was#0,SET test value formatter: 17 + 25186 record (16 = 3 [3] + 12 + 1) + wassail#0,SET test value formatter: 1 + 25202 record (16 = 3 [2] + 11 + 2) + watch#0,SET test value formatter: 12 + 25218 record (15 = 3 [5] + 11 + 1) + watchman#0,SET test value formatter: 1 + 25233 record (17 = 3 [0] + 13 + 1) [restart] + waves#0,SET test value formatter: 3 + 25250 record (15 = 3 [2] + 11 + 1) + waxes#0,SET test value formatter: 2 + 25265 record (13 = 3 [2] + 9 + 1) + way#0,SET test value formatter: 2 + 25278 record (13 = 3 [3] + 9 + 1) + ways#0,SET test value formatter: 1 + 25291 record (14 = 3 [1] + 9 + 2) + we#0,SET test value formatter: 34 + 25305 record (14 = 3 [2] + 10 + 1) + weak#0,SET test value formatter: 1 + 25319 record (14 = 3 [3] + 10 + 1) + wears#0,SET test value formatter: 1 + 25333 record (13 = 3 [4] + 9 + 1) + weary#0,SET test value formatter: 1 + 25346 record (17 = 3 [2] + 13 + 1) + wedding#0,SET test value formatter: 1 + 25363 record (14 = 3 [2] + 10 + 1) + weed#0,SET test value formatter: 1 + 25377 record (13 = 3 [3] + 9 + 1) + week#0,SET test value formatter: 1 + 25390 record (15 = 3 [2] + 11 + 1) + weigh#0,SET test value formatter: 2 + 25405 record (15 = 3 [5] + 11 + 1) + weighing#0,SET test value formatter: 1 + 25420 record (17 = 3 [2] + 13 + 1) + welcome#0,SET test value formatter: 3 + 25437 record (14 = 3 [3] + 9 + 2) + well#0,SET test value formatter: 14 + 25451 record (14 = 3 [2] + 10 + 1) + went#0,SET test value formatter: 1 + 25465 record (16 = 3 [0] + 12 + 1) [restart] + were#0,SET test value formatter: 3 + 25481 record (14 = 3 [2] + 10 + 1) + west#0,SET test value formatter: 1 + 25495 record (16 = 3 [4] + 12 + 1) + westward#0,SET test value formatter: 1 + 25511 record (16 = 3 [1] + 12 + 1) + wharf#0,SET test value formatter: 1 + 25527 record (14 = 3 [3] + 9 + 2) + what#0,SET test value formatter: 42 + 25541 record (18 = 3 [4] + 14 + 1) + whatsoever#0,SET test value formatter: 1 + 25559 record (14 = 3 [2] + 10 + 1) + when#0,SET test value formatter: 8 + 25573 record (14 = 3 [4] + 10 + 1) + whence#0,SET test value formatter: 1 + 25587 record (14 = 3 [3] + 10 + 1) + where#0,SET test value formatter: 9 + 25601 record (16 = 3 [5] + 12 + 1) + wherefore#0,SET test value formatter: 1 + 25617 record (14 = 3 [5] + 10 + 1) + wherein#0,SET test value formatter: 4 + 25631 record (14 = 3 [5] + 10 + 1) + whereof#0,SET test value formatter: 2 + 25645 record (16 = 3 [3] + 12 + 1) + whether#0,SET test value formatter: 1 + 25661 record (16 = 3 [2] + 11 + 2) + which#0,SET test value formatter: 16 + 25677 record (14 = 3 [3] + 10 + 1) + while#0,SET test value formatter: 2 + 25691 record (13 = 3 [5] + 9 + 1) + whiles#0,SET test value formatter: 1 + 25704 record (18 = 3 [0] + 14 + 1) [restart] + whilst#0,SET test value formatter: 1 + 25722 record (17 = 3 [3] + 13 + 1) + whirling#0,SET test value formatter: 1 + 25739 record (16 = 3 [3] + 12 + 1) + whisper#0,SET test value formatter: 1 + 25755 record (13 = 3 [2] + 9 + 1) + who#0,SET test value formatter: 8 + 25768 record (14 = 3 [3] + 10 + 1) + whole#0,SET test value formatter: 3 + 25782 record (16 = 3 [5] + 12 + 1) + wholesome#0,SET test value formatter: 2 + 25798 record (14 = 3 [3] + 10 + 1) + whose#0,SET test value formatter: 8 + 25812 record (14 = 3 [2] + 9 + 2) + why#0,SET test value formatter: 13 + 25826 record (17 = 3 [1] + 13 + 1) + wicked#0,SET test value formatter: 3 + 25843 record (14 = 3 [2] + 10 + 1) + wide#0,SET test value formatter: 1 + 25857 record (14 = 3 [2] + 10 + 1) + wife#0,SET test value formatter: 1 + 25871 record (14 = 3 [2] + 10 + 1) + wild#0,SET test value formatter: 1 + 25885 record (14 = 3 [3] + 9 + 2) + will#0,SET test value formatter: 25 + 25899 record (15 = 3 [4] + 11 + 1) + willing#0,SET test value formatter: 1 + 25914 record (14 = 3 [7] + 10 + 1) + willingly#0,SET test value formatter: 1 + 25928 record (13 = 3 [3] + 9 + 1) + wilt#0,SET test value formatter: 1 + 25941 record (16 = 3 [0] + 12 + 1) [restart] + wind#0,SET test value formatter: 2 + 25957 record (13 = 3 [4] + 9 + 1) + winds#0,SET test value formatter: 2 + 25970 record (13 = 3 [4] + 9 + 1) + windy#0,SET test value formatter: 1 + 25983 record (14 = 3 [3] + 10 + 1) + wings#0,SET test value formatter: 1 + 25997 record (14 = 3 [2] + 10 + 1) + wipe#0,SET test value formatter: 1 + 26011 record (16 = 3 [2] + 12 + 1) + wisdom#0,SET test value formatter: 1 + 26027 record (13 = 3 [6] + 9 + 1) + wisdoms#0,SET test value formatter: 1 + 26040 record (15 = 3 [3] + 11 + 1) + wisest#0,SET test value formatter: 1 + 26055 record (15 = 3 [3] + 11 + 1) + wishes#0,SET test value formatter: 1 + 26070 record (13 = 3 [2] + 9 + 1) + wit#0,SET test value formatter: 2 + 26083 record (14 = 3 [3] + 10 + 1) + witch#0,SET test value formatter: 1 + 26097 record (17 = 3 [5] + 13 + 1) + witchcraft#0,SET test value formatter: 1 + 26114 record (14 = 3 [3] + 9 + 2) + with#0,SET test value formatter: 65 + 26128 record (14 = 3 [4] + 10 + 1) + withal#0,SET test value formatter: 2 + 26142 record (15 = 3 [4] + 10 + 2) + within#0,SET test value formatter: 11 + 26157 record (15 = 3 [4] + 11 + 1) + without#0,SET test value formatter: 3 + 26172 record (19 = 3 [0] + 15 + 1) [restart] + witness#0,SET test value formatter: 1 + 26191 record (19 = 3 [3] + 15 + 1) + wittenberg#0,SET test value formatter: 4 + 26210 record (14 = 3 [1] + 10 + 1) + woe#0,SET test value formatter: 3 + 26224 record (15 = 3 [2] + 11 + 1) + woman#0,SET test value formatter: 2 + 26239 record (13 = 3 [3] + 9 + 1) + womb#0,SET test value formatter: 1 + 26252 record (13 = 3 [2] + 9 + 1) + won#0,SET test value formatter: 1 + 26265 record (15 = 3 [3] + 11 + 1) + wonder#0,SET test value formatter: 1 + 26280 record (15 = 3 [6] + 11 + 1) + wonderful#0,SET test value formatter: 1 + 26295 record (16 = 3 [4] + 12 + 1) + wondrous#0,SET test value formatter: 1 + 26311 record (13 = 3 [3] + 9 + 1) + wont#0,SET test value formatter: 1 + 26324 record (19 = 3 [2] + 15 + 1) + woodcocks#0,SET test value formatter: 1 + 26343 record (14 = 3 [2] + 10 + 1) + word#0,SET test value formatter: 3 + 26357 record (13 = 3 [4] + 9 + 1) + words#0,SET test value formatter: 2 + 26370 record (13 = 3 [3] + 9 + 1) + wore#0,SET test value formatter: 1 + 26383 record (13 = 3 [3] + 9 + 1) + work#0,SET test value formatter: 2 + 26396 record (14 = 3 [3] + 10 + 1) + world#0,SET test value formatter: 3 + 26410 record (16 = 3 [0] + 12 + 1) [restart] + worm#0,SET test value formatter: 1 + 26426 record (14 = 3 [3] + 10 + 1) + worth#0,SET test value formatter: 1 + 26440 record (13 = 3 [5] + 9 + 1) + worthy#0,SET test value formatter: 1 + 26453 record (16 = 3 [2] + 11 + 2) + would#0,SET test value formatter: 14 + 26469 record (14 = 3 [5] + 10 + 1) + wouldst#0,SET test value formatter: 3 + 26483 record (17 = 3 [1] + 13 + 1) + wretch#0,SET test value formatter: 1 + 26500 [restart 24504] + 26504 [restart 24747] + 26508 [restart 24994] + 26512 [restart 25233] + 26516 [restart 25465] + 26520 [restart 25704] + 26524 [restart 25941] + 26528 [restart 26172] + 26532 [restart 26410] + 26540 [trailer compression=none checksum=0x99c293d8] + 26545 data (249) + 26545 record (16 = 3 [0] + 12 + 1) [restart] + writ#0,SET test value formatter: 2 + 26561 record (15 = 3 [4] + 11 + 1) + writing#0,SET test value formatter: 1 + 26576 record (15 = 3 [2] + 11 + 1) + wrong#0,SET test value formatter: 1 + 26591 record (15 = 3 [2] + 11 + 1) + wrung#0,SET test value formatter: 1 + 26606 record (15 = 3 [0] + 11 + 1) + yea#0,SET test value formatter: 1 + 26621 record (13 = 3 [2] + 9 + 1) + yes#0,SET test value formatter: 4 + 26634 record (20 = 3 [3] + 16 + 1) + yesternight#0,SET test value formatter: 1 + 26654 record (13 = 3 [2] + 9 + 1) + yet#0,SET test value formatter: 7 + 26667 record (19 = 3 [1] + 15 + 1) + yielding#0,SET test value formatter: 1 + 26686 record (14 = 3 [1] + 10 + 1) + yon#0,SET test value formatter: 1 + 26700 record (13 = 3 [3] + 9 + 1) + yond#0,SET test value formatter: 1 + 26713 record (15 = 3 [2] + 9 + 3) + you#0,SET test value formatter: 110 + 26728 record (14 = 3 [3] + 10 + 1) + young#0,SET test value formatter: 6 + 26742 record (14 = 3 [3] + 9 + 2) + your#0,SET test value formatter: 49 + 26756 record (16 = 3 [4] + 12 + 1) + yourself#0,SET test value formatter: 7 + 26772 record (14 = 3 [3] + 10 + 1) + youth#0,SET test value formatter: 5 + 26786 [restart 26545] + 26794 [trailer compression=none checksum=0x2bb2856] + 26799 index (120) + 26799 block:0/2041 [restart] + 26817 block:2046/2044 [restart] + 26839 block:4095/2039 [restart] + 26858 block:6139/2036 [restart] + 26876 block:8180/2032 [restart] + 26895 [restart 26799] + 26899 [restart 26817] + 26903 [restart 26839] + 26907 [restart 26858] + 26911 [restart 26876] + 26919 [trailer compression=none checksum=0x4b1bc52e] + 26924 index (118) + 26924 block:10217/2042 [restart] + 26941 block:12264/2039 [restart] + 26959 block:14308/2037 [restart] + 26979 block:16350/2029 [restart] + 26998 block:18384/2040 [restart] + 27018 [restart 26924] + 27022 [restart 26941] + 27026 [restart 26959] + 27030 [restart 26979] + 27034 [restart 26998] + 27042 [trailer compression=none checksum=0xe1dd6a77] + 27047 index (95) + 27047 block:20429/2030 [restart] + 27068 block:22464/2035 [restart] + 27086 block:24504/2036 [restart] + 27105 block:26545/249 [restart] + 27122 [restart 27047] + 27126 [restart 27068] + 27130 [restart 27086] + 27134 [restart 27105] + 27142 [trailer compression=none checksum=0x3b1313e3] + 27147 top-index (70) + 27147 block:26799/120 [restart] + 27166 block:26924/118 [restart] + 27185 block:27047/95 [restart] + 27201 [restart 27147] + 27205 [restart 27166] + 27209 [restart 27185] + 27217 [trailer compression=none checksum=0xd20fdc47] + 27222 range-del (421) + 27222 record (13 = 3 [0] + 9 + 1) [restart] + a-a#0,RANGEDEL + 27235 record (23 = 3 [0] + 13 + 7) [restart] + beard-bearers#0,RANGEDEL + 27258 record (24 = 3 [0] + 16 + 5) [restart] + carriage-carve#0,RANGEDEL + 27282 record (21 = 3 [0] + 13 + 5) [restart] + cross-crows#0,RANGEDEL + 27303 record (23 = 3 [0] + 14 + 6) [restart] + duller-duties#0,RANGEDEL + 27326 record (21 = 3 [0] + 14 + 4) [restart] + fierce-fire#0,RANGEDEL + 27347 record (21 = 3 [0] + 13 + 5) [restart] + grace-great#0,RANGEDEL + 27368 record (17 = 3 [0] + 11 + 3) [restart] + how-ice#0,RANGEDEL + 27385 record (20 = 3 [0] + 12 + 5) [restart] + lead-lends#0,RANGEDEL + 27405 record (18 = 3 [0] + 12 + 3) [restart] + meet-met#0,RANGEDEL + 27423 record (17 = 3 [0] + 10 + 4) [restart] + of-once#0,RANGEDEL + 27440 record (25 = 3 [0] + 16 + 6) [restart] + precurse-prison#0,RANGEDEL + 27465 record (15 = 3 [0] + 9 + 3) [restart] + s-saw#0,RANGEDEL + 27480 record (19 = 3 [0] + 12 + 4) [restart] + slay-soil#0,RANGEDEL + 27499 record (24 = 3 [0] + 16 + 5) [restart] + suppress-sword#0,RANGEDEL + 27523 record (23 = 3 [0] + 16 + 4) [restart] + traduced-true#0,RANGEDEL + 27546 record (25 = 3 [0] + 15 + 7) [restart] + warning-wedding#0,RANGEDEL + 27571 [restart 27222] + 27575 [restart 27235] + 27579 [restart 27258] + 27583 [restart 27282] + 27587 [restart 27303] + 27591 [restart 27326] + 27595 [restart 27347] + 27599 [restart 27368] + 27603 [restart 27385] + 27607 [restart 27405] + 27611 [restart 27423] + 27615 [restart 27440] + 27619 [restart 27465] + 27623 [restart 27480] + 27627 [restart 27499] + 27631 [restart 27523] + 27635 [restart 27546] + 27643 [trailer compression=none checksum=0xb93b31c5] + 27648 properties (765) + 27648 rocksdb.block.based.table.index.type (43) [restart] + 27691 rocksdb.block.based.table.prefix.filtering (20) + 27711 rocksdb.block.based.table.whole.key.filtering (23) + 27734 rocksdb.column.family.id (24) + 27758 rocksdb.comparator (37) + 27795 rocksdb.compression (23) + 27818 rocksdb.compression_options (106) + 27924 rocksdb.creation.time (16) + 27940 rocksdb.data.size (15) + 27955 rocksdb.deleted.keys (15) + 27970 rocksdb.external_sst_file.global_seqno (41) + 28011 rocksdb.external_sst_file.version (14) + 28025 rocksdb.filter.size (15) + 28040 rocksdb.fixed.key.length (18) + 28058 rocksdb.format.version (17) + 28075 rocksdb.index.key.is.user.key (25) + 28100 rocksdb.index.partitions (14) + 28114 rocksdb.index.size (9) + 28123 rocksdb.index.value.is.delta.encoded (26) + 28149 rocksdb.merge.operands (18) + 28167 rocksdb.merge.operator (13) + 28180 rocksdb.num.data.blocks (19) + 28199 rocksdb.num.entries (12) + 28211 rocksdb.num.range-deletions (19) + 28230 rocksdb.oldest.key.time (19) + 28249 rocksdb.prefix.extractor.name (31) + 28280 rocksdb.property.collectors (47) + 28327 rocksdb.raw.key.size (18) + 28345 rocksdb.raw.value.size (15) + 28360 rocksdb.top-level.index.size (24) + 28384 test.key-count (21) + 28405 [restart 27648] + 28413 [trailer compression=none checksum=0x8542f94f] + 28418 meta-index (63) + 28418 rocksdb.properties block:27648/765 [restart] + 28444 rocksdb.range_del block:27222/421 [restart] + 28469 [restart 28418] + 28473 [restart 28444] + 28481 [trailer compression=none checksum=0xabc8467e] + 28486 footer (53) + 28486 checksum type: crc32c + 28487 meta: offset=28418, length=63 + 28491 index: offset=27147, length=70 + 28495 [padding] + 28527 version: 2 + 28531 magic number: 0xf7cff485b741e288 + 28539 EOF + +sstable layout +-v +testdata/out-of-order.sst +---- +out-of-order.sst + 0 data (28) + 0 record (12 = 3 [0] + 9 + 0) [restart] + a#0,SET [] + 12 record (12 = 3 [0] + 9 + 0) + c#0,SET [] + 24 record (12 = 3 [0] + 9 + 0) + b#0,SET [] + WARNING: OUT OF ORDER KEYS! + 36 [restart 0] + 28 [trailer compression=snappy checksum=0x94ebf32b] + 33 index (22) + 33 block:0/28 [restart] + 47 [restart 33] + 55 [trailer compression=none checksum=0xc316e0d2] + 60 properties (678) + 60 rocksdb.block.based.table.index.type (43) [restart] + 103 rocksdb.block.based.table.prefix.filtering (20) + 123 rocksdb.block.based.table.whole.key.filtering (23) + 146 rocksdb.column.family.id (24) + 170 rocksdb.comparator (37) + 207 rocksdb.compression (16) + 223 rocksdb.compression_options (106) + 329 rocksdb.creation.time (16) + 345 rocksdb.data.size (13) + 358 rocksdb.deleted.keys (15) + 373 rocksdb.external_sst_file.global_seqno (41) + 414 rocksdb.external_sst_file.version (14) + 428 rocksdb.filter.size (15) + 443 rocksdb.fixed.key.length (18) + 461 rocksdb.format.version (17) + 478 rocksdb.index.key.is.user.key (25) + 503 rocksdb.index.size (8) + 511 rocksdb.index.value.is.delta.encoded (26) + 537 rocksdb.merge.operands (18) + 555 rocksdb.merge.operator (24) + 579 rocksdb.num.data.blocks (19) + 598 rocksdb.num.entries (11) + 609 rocksdb.num.range-deletions (19) + 628 rocksdb.oldest.key.time (19) + 647 rocksdb.prefix.extractor.name (31) + 678 rocksdb.property.collectors (22) + 700 rocksdb.raw.key.size (16) + 716 rocksdb.raw.value.size (14) + 730 [restart 60] + 738 [trailer compression=none checksum=0xbbcd4fc4] + 743 meta-index (32) + 743 rocksdb.properties block:60/678 [restart] + 767 [restart 743] + 775 [trailer compression=none checksum=0x1a67e403] + 780 footer (53) + 780 checksum type: crc32c + 781 meta: offset=743, length=32 + 784 index: offset=33, length=22 + 786 [padding] + 821 version: 2 + 825 magic number: 0xf7cff485b741e288 + 833 EOF + +sstable layout +./testdata/mixed/000005.sst +---- +000005.sst + 0 data (231) + 236 index (24) + 265 range-key (64) + 334 properties (710) + 1049 meta-index (59) + 1113 footer (53) + 1166 EOF diff --git a/pebble/tool/testdata/sstable_properties b/pebble/tool/testdata/sstable_properties new file mode 100644 index 0000000..4dada20 --- /dev/null +++ b/pebble/tool/testdata/sstable_properties @@ -0,0 +1,229 @@ +sstable properties +---- +requires at least 1 arg(s), only received 0 + +sstable properties +../sstable/testdata/h.sst +---- +h.sst +size + file 15KB + data 14KB + blocks 14 + index 325B + blocks 1 + top-level 0B + filter 0B + raw-key 23KB + raw-value 1.9KB + pinned-key 0 + pinned-val 0 + point-del-key-size 0 + point-del-value-size 0 +records 1727 + set 1710 + delete 0 + delete-sized 0 + range-delete 17 + range-key-set 0 + range-key-unset 0 + range-key-delete 0 + merge 0 + global-seq-num 0 + pinned 0 +index + key value comparer leveldb.BytewiseComparator +merger - +filter - + prefix false + whole-key false +compression Snappy + options window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; +user properties + collectors [KeyCountPropertyCollector] + test.key-count 1727 + +sstable properties +../sstable/testdata/h.ldb +---- +h.ldb +size + file 15KB + data 14KB + blocks 14 + index 325B + blocks 1 + top-level 0B + filter 0B + raw-key 23KB + raw-value 1.9KB + pinned-key 0 + pinned-val 0 + point-del-key-size 0 + point-del-value-size 0 +records 1727 + set 1710 + delete 0 + delete-sized 0 + range-delete 17 + range-key-set 0 + range-key-unset 0 + range-key-delete 0 + merge 0 + global-seq-num 0 + pinned 0 +index + key value comparer leveldb.BytewiseComparator +merger - +filter - + prefix false + whole-key false +compression Snappy + options window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; +user properties + collectors [] + +sstable properties +../sstable/testdata/h.no-compression.two_level_index.sst +---- +h.no-compression.two_level_index.sst +size + file 28KB + data 26KB + blocks 14 + index 408B + blocks 4 + top-level 70B + filter 0B + raw-key 23KB + raw-value 1.9KB + pinned-key 0 + pinned-val 0 + point-del-key-size 0 + point-del-value-size 0 +records 1727 + set 1710 + delete 0 + delete-sized 0 + range-delete 17 + range-key-set 0 + range-key-unset 0 + range-key-delete 0 + merge 0 + global-seq-num 0 + pinned 0 +index + key value comparer leveldb.BytewiseComparator +merger - +filter - + prefix false + whole-key false +compression NoCompression + options window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; +user properties + collectors [KeyCountPropertyCollector] + test.key-count 1727 + +sstable properties +-v +../sstable/testdata/h.no-compression.two_level_index.sst +---- +h.no-compression.two_level_index.sst +rocksdb.num.entries: 1727 +rocksdb.raw.key.size: 23938 +rocksdb.raw.value.size: 1912 +rocksdb.deleted.keys: 17 +rocksdb.num.range-deletions: 17 +rocksdb.comparator: leveldb.BytewiseComparator +rocksdb.compression: NoCompression +rocksdb.compression_options: window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; +rocksdb.data.size: 26799 +rocksdb.external_sst_file.version: 2 +rocksdb.filter.size: 0 +rocksdb.external_sst_file.global_seqno: 0 +rocksdb.index.partitions: 3 +rocksdb.index.size: 408 +rocksdb.block.based.table.index.type: 2 +rocksdb.merge.operator: nullptr +rocksdb.num.data.blocks: 14 +rocksdb.merge.operands: 0 +rocksdb.prefix.extractor.name: nullptr +rocksdb.block.based.table.prefix.filtering: false +rocksdb.property.collectors: [KeyCountPropertyCollector] +rocksdb.top-level.index.size: 70 +rocksdb.block.based.table.whole.key.filtering: false +test.key-count: 1727 + +# Test for properties in SSTs made by the db itself. Should not contain +# rocksdb.external_sst_file.* . See +# https://github.com/cockroachdb/pebble/issues/532 +sstable properties +-v +testdata/find-db/archive/000011.sst +---- +000011.sst +rocksdb.num.entries: 8 +rocksdb.raw.key.size: 88 +rocksdb.raw.value.size: 13 +rocksdb.deleted.keys: 2 +rocksdb.num.range-deletions: 1 +rocksdb.comparator: alt-comparer +rocksdb.compression: Snappy +rocksdb.compression_options: window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; +rocksdb.data.size: 90 +rocksdb.filter.size: 0 +rocksdb.index.size: 27 +rocksdb.block.based.table.index.type: 0 +rocksdb.merge.operator: test-merger +rocksdb.num.data.blocks: 1 +rocksdb.merge.operands: 1 +rocksdb.prefix.extractor.name: nullptr +rocksdb.block.based.table.prefix.filtering: false +rocksdb.property.collectors: [] +rocksdb.block.based.table.whole.key.filtering: false + +sstable properties +testdata/bad-magic.sst +---- +bad-magic.sst +pebble/table: invalid table (bad magic number: 0xf6cff485b741e288) + +sstable properties +testdata/mixed/000005.sst +---- +000005.sst +size + file 1.1KB + data 236B + blocks 1 + index 29B + blocks 1 + top-level 0B + filter 0B + raw-key 286B + raw-value 0B + pinned-key 0 + pinned-val 0 + point-del-key-size 0 + point-del-value-size 0 +records 26 + set 26 + delete 0 + delete-sized 0 + range-delete 0 + range-key-set 1 + range-key-unset 1 + range-key-delete 1 + merge 0 + global-seq-num 0 + pinned 0 +index + key value comparer pebble.internal.testkeys +merger pebble.concatenate +filter - + prefix false + whole-key false +compression Snappy + options window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0; +user properties + collectors [] diff --git a/pebble/tool/testdata/sstable_scan b/pebble/tool/testdata/sstable_scan new file mode 100644 index 0000000..75164c2 --- /dev/null +++ b/pebble/tool/testdata/sstable_scan @@ -0,0 +1,402 @@ +sstable scan +--start=arm +--end=aside +../sstable/testdata/h.sst +---- +h.sst +arm#0,SET [32] +armed#0,SET [32] +armour#0,SET [31] +arms#0,SET [32] +arrant#0,SET [31] +art#0,SET [36] +artery#0,SET [31] +article#0,SET [31] +articles#0,SET [31] +as#0,SET [3536] + +sstable scan +--end=abused +../sstable/testdata/h.sst +---- +h.sst +a-a#0,RANGEDEL +a#0,SET [3937] +aboard#0,SET [32] +about#0,SET [32] +above#0,SET [31] +abroad#0,SET [31] +absurd#0,SET [31] + +sstable scan +--start=you +../sstable/testdata/h.sst +---- +h.sst +you#0,SET [313130] +young#0,SET [36] +your#0,SET [3439] +yourself#0,SET [37] +youth#0,SET [35] + +sstable scan +--key=%x +--value=null +--start=you +../sstable/testdata/h.sst +---- +h.sst +796f75#0,SET +796f756e67#0,SET +796f7572#0,SET +796f757273656c66#0,SET +796f757468#0,SET + +sstable scan +--key=%q +--value=null +--start=hex:796f75 +--end=raw:yourself +../sstable/testdata/h.sst +---- +h.sst +"you"#0,SET +"young"#0,SET +"your"#0,SET + +sstable scan +--key=null +--value=[%x] +--start=hex:796f75 +--end=raw:yourself +../sstable/testdata/h.sst +---- +h.sst +[313130] +[36] +[3439] + +sstable scan +--key=pretty +--value=[%x] +--start=hex:796f75 +--end=raw:yourself +../sstable/testdata/h.sst +---- +h.sst +you#0,SET [313130] +young#0,SET [36] +your#0,SET [3439] + +sstable scan +--key=pretty +--value=[%x] +--start=hex:796f75 +--end=raw:yourself +../sstable/testdata/h.sst +---- +h.sst +you#0,SET [313130] +young#0,SET [36] +your#0,SET [3439] + +sstable scan +--key=pretty +--value=pretty +--start=hex:796f75 +--end=raw:yourself +../sstable/testdata/h.sst +---- +h.sst +you#0,SET 110 +young#0,SET 6 +your#0,SET 49 + +sstable scan +--key=pretty:test-comparer +--value=pretty:test-comparer +--start=hex:796f75 +--end=raw:yourself +../sstable/testdata/h.sst +---- +h.sst +test formatter: you#0,SET test value formatter: 110 +test formatter: young#0,SET test value formatter: 6 +test formatter: your#0,SET test value formatter: 49 + +# Start and end scan keys lie within range tombstones. +sstable scan +--start=beards +--end=carrying +../sstable/testdata/h.sst +---- +h.sst +beard-bearers#0,RANGEDEL +bearers#0,SET [31] +bears#0,SET [31] +beast#0,SET [32] +beating#0,SET [31] +beauty#0,SET [31] +beaver#0,SET [31] +beckons#0,SET [32] +bed#0,SET [34] +been#0,SET [34] +beetles#0,SET [31] +befitted#0,SET [31] +before#0,SET [36] +beg#0,SET [31] +beguile#0,SET [31] +behold#0,SET [31] +behoves#0,SET [31] +being#0,SET [34] +belief#0,SET [31] +believe#0,SET [36] +bell#0,SET [31] +bend#0,SET [32] +beneath#0,SET [35] +benefit#0,SET [31] +bernardo#0,SET [3330] +beseech#0,SET [32] +besmirch#0,SET [31] +best#0,SET [35] +beteem#0,SET [31] +bethought#0,SET [31] +better#0,SET [32] +between#0,SET [32] +beware#0,SET [32] +beyond#0,SET [31] +bid#0,SET [32] +bird#0,SET [32] +birth#0,SET [33] +bites#0,SET [31] +bitter#0,SET [31] +black#0,SET [31] +blast#0,SET [31] +blastments#0,SET [31] +blasts#0,SET [31] +blazes#0,SET [31] +blazon#0,SET [31] +blessing#0,SET [33] +blood#0,SET [37] +blossoms#0,SET [31] +blows#0,SET [31] +bodes#0,SET [31] +body#0,SET [35] +bonds#0,SET [31] +bones#0,SET [31] +book#0,SET [31] +books#0,SET [31] +born#0,SET [32] +borrower#0,SET [31] +borrowing#0,SET [31] +bosom#0,SET [31] +both#0,SET [33] +bound#0,SET [32] +bounteous#0,SET [31] +bow#0,SET [31] +boy#0,SET [32] +brain#0,SET [32] +bray#0,SET [31] +brazen#0,SET [31] +breach#0,SET [31] +break#0,SET [33] +breaking#0,SET [31] +breath#0,SET [31] +breathing#0,SET [31] +brief#0,SET [31] +bring#0,SET [31] +brokers#0,SET [31] +brother#0,SET [36] +brow#0,SET [31] +bruit#0,SET [31] +bulk#0,SET [31] +buried#0,SET [31] +burns#0,SET [32] +burnt#0,SET [31] +burst#0,SET [32] +business#0,SET [34] +but#0,SET [3538] +buttons#0,SET [31] +buy#0,SET [31] +by#0,SET [3331] +call#0,SET [34] +calumnious#0,SET [31] +came#0,SET [32] +can#0,SET [35] +canker#0,SET [31] +cannon#0,SET [32] +cannot#0,SET [33] +canon#0,SET [31] +canonized#0,SET [31] +canst#0,SET [32] +cap#0,SET [31] +carefully#0,SET [31] +carriage-carve#0,RANGEDEL +carriage#0,SET [31] + +# Start scan key lies on range tombstone end key. +sstable scan +--start=bearers +--end=bears +../sstable/testdata/h.sst +---- +h.sst +bearers#0,SET [31] + +# End scan key lies on range tombstone start key. +sstable scan +--start=bear +--end=beard +../sstable/testdata/h.sst +---- +h.sst +bear#0,SET [35] + +# Count that only includes point records. +sstable scan +--start=armed +--count=3 +../sstable/testdata/h.sst +---- +h.sst +armed#0,SET [32] +armour#0,SET [31] +arms#0,SET [32] + +# Count that includes point records and range tombstones. +sstable scan +--start=beards +--count=2 +../sstable/testdata/h.sst +---- +h.sst +beard-bearers#0,RANGEDEL +bearers#0,SET [31] + +sstable scan +testdata/out-of-order.sst +---- +out-of-order.sst +a#0,SET [] +c#0,SET [] +b#0,SET [] + WARNING: OUT OF ORDER KEYS! + +sstable scan +--filter=arms +../sstable/testdata/h.sst +---- +h.sst: arms#0,SET [32] + +sstable scan +--filter=bear +../sstable/testdata/h.sst +---- +h.sst: bear#0,SET [35] +h.sst: beard-bearers#0,RANGEDEL +h.sst: beard#0,SET [31] +h.sst: bearers#0,SET [31] +h.sst: bears#0,SET [31] + +sstable scan +--filter=beards +../sstable/testdata/h.sst +---- +h.sst: beard-bearers#0,RANGEDEL + +sstable scan +--filter=beard +../sstable/testdata/ +---- +testdata/h.block-bloom.no-compression.sst: beard-bearers#0,RANGEDEL +testdata/h.block-bloom.no-compression.sst: beard#0,SET [31] +testdata/h.ldb: beard-bearers#0,RANGEDEL +testdata/h.ldb: beard#0,SET [31] +testdata/h.no-compression.sst: beard-bearers#0,RANGEDEL +testdata/h.no-compression.sst: beard#0,SET [31] +testdata/h.no-compression.two_level_index.sst: beard-bearers#0,RANGEDEL +testdata/h.no-compression.two_level_index.sst: beard#0,SET [31] +testdata/h.sst: beard-bearers#0,RANGEDEL +testdata/h.sst: beard#0,SET [31] +testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst: beard-bearers#0,RANGEDEL +testdata/h.table-bloom.no-compression.prefix_extractor.no_whole_key_filter.sst: beard#0,SET [31] +testdata/h.table-bloom.no-compression.sst: beard-bearers#0,RANGEDEL +testdata/h.table-bloom.no-compression.sst: beard#0,SET [31] +testdata/h.table-bloom.sst: beard-bearers#0,RANGEDEL +testdata/h.table-bloom.sst: beard#0,SET [31] +testdata/h.zstd-compression.sst: beard-bearers#0,RANGEDEL +testdata/h.zstd-compression.sst: beard#0,SET [31] + +sstable scan +--filter=beard +--start=boar +../sstable/testdata/h.sst +---- + +sstable scan +./testdata/mixed/000005.sst +---- +000005.sst +a@1#1,SET [] +b@1#2,SET [] +c@1#3,SET [] +d@1#4,SET [] +e@1#5,SET [] +f@1#6,SET [] +g@1#7,SET [] +h@1#8,SET [] +i@1#9,SET [] +j@1#10,SET [] +k@1#11,SET [] +l@1#12,SET [] +m@1#13,SET [] +n@1#14,SET [] +o@1#15,SET [] +p@1#16,SET [] +q@1#17,SET [] +r@1#18,SET [] +s@1#19,SET [] +t@1#20,SET [] +u@1#21,SET [] +v@1#22,SET [] +w@1#23,SET [] +x@1#24,SET [] +y@1#25,SET [] +z@1#26,SET [] +[a-b): + #29,RANGEKEYDEL +[b-z): + #28,RANGEKEYUNSET: @2 + #27,RANGEKEYSET: @1 [] + +sstable scan +--start=b +--end=e +./testdata/mixed/000005.sst +---- +000005.sst +b@1#2,SET [] +c@1#3,SET [] +d@1#4,SET [] +[b-z): + #28,RANGEKEYUNSET: @2 + #27,RANGEKEYSET: @1 [] + +sstable scan +--filter=a +./testdata/mixed/000005.sst +---- +000005.sst: a@1#1,SET [] +000005.sst: [a-b): + #29,RANGEKEYDEL + +sstable scan +--filter=b +--start=b +--end=d +./testdata/mixed/000005.sst +---- +000005.sst: b@1#2,SET [] +000005.sst: [b-z): + #28,RANGEKEYUNSET: @2 + #27,RANGEKEYSET: @1 [] diff --git a/pebble/tool/testdata/sstable_space b/pebble/tool/testdata/sstable_space new file mode 100644 index 0000000..cfec706 --- /dev/null +++ b/pebble/tool/testdata/sstable_space @@ -0,0 +1,68 @@ +sstable space +---- +requires at least 1 arg(s), only received 0 + +# first key in first data block + +sstable space --start=a --end=a +../sstable/testdata/h.sst +---- +h.sst: 1099 + +# first data block through last key in first data block + +sstable space --start=a --end=beteem +../sstable/testdata/h.sst +---- +h.sst: 1099 + +# last key in first data block through first key in second data block + +sstable space --start=beteem --end=bethought +../sstable/testdata/h.sst +---- +h.sst: 2161 + +# last key in first data block through last key in last data block + +sstable space --start=beteem --end=youth +../sstable/testdata/h.sst +---- +h.sst: 13913 + +# second last key in last data block through last key in last data block + +sstable space --start=yourself --end=youth +../sstable/testdata/h.sst +---- +h.sst: 161 + +# Two-level index: first key in first data block + +sstable space --start=a --end=a +../sstable/testdata/h.no-compression.two_level_index.sst +---- +h.no-compression.two_level_index.sst: 2046 + +# Two-level index: last key in last data block + +sstable space --start=youth --end=youth +../sstable/testdata/h.no-compression.two_level_index.sst +---- +h.no-compression.two_level_index.sst: 254 + +# Two-level index: last key in first top-level index partition and first key +# in second top-level index partition + +sstable space --start=headshake --end=health +../sstable/testdata/h.no-compression.two_level_index.sst +---- +h.no-compression.two_level_index.sst: 4084 + +# Two-level index: last key in first top-level index partition through last +# key in last data block. + +sstable space --start=headshake --end=youth +../sstable/testdata/h.no-compression.two_level_index.sst +---- +h.no-compression.two_level_index.sst: 18619 diff --git a/pebble/tool/testdata/wal_dump b/pebble/tool/testdata/wal_dump new file mode 100644 index 0000000..9e8af91 --- /dev/null +++ b/pebble/tool/testdata/wal_dump @@ -0,0 +1,118 @@ +wal dump +---- +requires at least 1 arg(s), only received 0 + +wal dump +../testdata/db-stage-2/000002.log +---- +000002.log +0(21) seq=10 count=1 + SET(test formatter: foo,test value formatter: one) +32(21) seq=11 count=1 + SET(test formatter: bar,test value formatter: two) +64(23) seq=12 count=1 + SET(test formatter: baz,test value formatter: three) +98(22) seq=13 count=1 + SET(test formatter: foo,test value formatter: four) +131(17) seq=14 count=1 + DEL(test formatter: bar) +EOF + +wal dump +../testdata/db-stage-2/000002.log +--key=pretty:leveldb.BytewiseComparator +--value=size +---- +000002.log +0(21) seq=10 count=1 + SET(foo,<3>) +32(21) seq=11 count=1 + SET(bar,<3>) +64(23) seq=12 count=1 + SET(baz,<5>) +98(22) seq=13 count=1 + SET(foo,<4>) +131(17) seq=14 count=1 + DEL(bar) +EOF + +wal dump +--key=pretty:leveldb.BytewiseComparator +--value=size +../testdata/db-stage-4/000005.log +---- +000005.log +0(22) seq=15 count=1 + SET(foo,<4>) +33(22) seq=16 count=1 + SET(quux,<3>) +66(17) seq=17 count=1 + DEL(baz) +EOF + +wal dump +../testdata/db-stage-4/000005.log +--key=%x +--value=%x +---- +000005.log +0(22) seq=15 count=1 + SET(666f6f,66697665) +33(22) seq=16 count=1 + SET(71757578,736978) +66(17) seq=17 count=1 + DEL(62617a) +EOF + +wal dump +../testdata/db-stage-4/000005.log +--key=pretty:leveldb.BytewiseComparator +--value=pretty:test-comparer +---- +000005.log +0(22) seq=15 count=1 + SET(foo,test value formatter: five) +33(22) seq=16 count=1 + SET(quux,test value formatter: six) +66(17) seq=17 count=1 + DEL(baz) +EOF + +wal dump +../testdata/db-stage-4/000005.log +--key=pretty:test-comparer +--value=%x +---- +000005.log +0(22) seq=15 count=1 + SET(test formatter: foo,66697665) +33(22) seq=16 count=1 + SET(test formatter: quux,736978) +66(17) seq=17 count=1 + DEL(test formatter: baz) +EOF + +wal dump +../testdata/db-stage-4/000005.log +--key=pretty:leveldb.BytewiseComparator +--value=quoted +---- +000005.log +0(22) seq=15 count=1 + SET(foo,five) +33(22) seq=16 count=1 + SET(quux,six) +66(17) seq=17 count=1 + DEL(baz) +EOF + +wal dump +./testdata/mixed/000004.log +---- +000004.log +0(42) seq=30 count=4 + SET(test formatter: a@2,test value formatter: ) + RANGEKEYSET(test formatter: a-test formatter: z:{(#31,RANGEKEYSET,@3)}) + RANGEKEYUNSET(test formatter: a-test formatter: z:{(#32,RANGEKEYUNSET,@4)}) + RANGEKEYDEL(test formatter: a-test formatter: b:{(#33,RANGEKEYDEL)}) +EOF diff --git a/pebble/tool/tool.go b/pebble/tool/tool.go new file mode 100644 index 0000000..4895b76 --- /dev/null +++ b/pebble/tool/tool.go @@ -0,0 +1,152 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import ( + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/bloom" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/spf13/cobra" +) + +// Comparer exports the base.Comparer type. +type Comparer = base.Comparer + +// FilterPolicy exports the base.FilterPolicy type. +type FilterPolicy = base.FilterPolicy + +// Merger exports the base.Merger type. +type Merger = base.Merger + +// T is the container for all of the introspection tools. +type T struct { + Commands []*cobra.Command + db *dbT + find *findT + lsm *lsmT + manifest *manifestT + remotecat *remoteCatalogT + sstable *sstableT + wal *walT + opts pebble.Options + comparers sstable.Comparers + mergers sstable.Mergers + defaultComparer string + openErrEnhancer func(error) error +} + +// A Option configures the Pebble introspection tool. +type Option func(*T) + +// Comparers may be passed to New to register comparers for use by +// the introspesction tools. +func Comparers(cmps ...*Comparer) Option { + return func(t *T) { + for _, c := range cmps { + t.comparers[c.Name] = c + } + } +} + +// DefaultComparer registers a comparer for use by the introspection tools and +// sets it as the default. +func DefaultComparer(c *Comparer) Option { + return func(t *T) { + t.comparers[c.Name] = c + t.defaultComparer = c.Name + } +} + +// Mergers may be passed to New to register mergers for use by the +// introspection tools. +func Mergers(mergers ...*Merger) Option { + return func(t *T) { + for _, m := range mergers { + t.mergers[m.Name] = m + } + } +} + +// Filters may be passed to New to register filter policies for use by the +// introspection tools. +func Filters(filters ...FilterPolicy) Option { + return func(t *T) { + for _, f := range filters { + t.opts.Filters[f.Name()] = f + } + } +} + +// FS sets the filesystem implementation to use by the introspection tools. +func FS(fs vfs.FS) Option { + return func(t *T) { + t.opts.FS = fs + } +} + +// OpenErrEnhancer sets a function that enhances an error encountered when the +// tool opens a database; used to provide the user additional context, for +// example that a corruption error might be caused by encryption at rest not +// being configured properly. +func OpenErrEnhancer(fn func(error) error) Option { + return func(t *T) { + t.openErrEnhancer = fn + } +} + +// New creates a new introspection tool. +func New(opts ...Option) *T { + t := &T{ + opts: pebble.Options{ + Filters: make(map[string]FilterPolicy), + FS: vfs.Default, + ReadOnly: true, + }, + comparers: make(sstable.Comparers), + mergers: make(sstable.Mergers), + defaultComparer: base.DefaultComparer.Name, + } + + opts = append(opts, + Comparers(base.DefaultComparer), + Filters(bloom.FilterPolicy(10)), + Mergers(base.DefaultMerger)) + + for _, opt := range opts { + opt(t) + } + + t.db = newDB(&t.opts, t.comparers, t.mergers, t.openErrEnhancer) + t.find = newFind(&t.opts, t.comparers, t.defaultComparer, t.mergers) + t.lsm = newLSM(&t.opts, t.comparers) + t.manifest = newManifest(&t.opts, t.comparers) + t.remotecat = newRemoteCatalog(&t.opts) + t.sstable = newSSTable(&t.opts, t.comparers, t.mergers) + t.wal = newWAL(&t.opts, t.comparers, t.defaultComparer) + t.Commands = []*cobra.Command{ + t.db.Root, + t.find.Root, + t.lsm.Root, + t.manifest.Root, + t.remotecat.Root, + t.sstable.Root, + t.wal.Root, + } + return t +} + +// ConfigureSharedStorage updates the shared storage options. +func (t *T) ConfigureSharedStorage( + s remote.StorageFactory, + createOnShared remote.CreateOnSharedStrategy, + createOnSharedLocator remote.Locator, +) { + t.opts.Experimental.RemoteStorage = s + t.opts.Experimental.CreateOnShared = createOnShared + t.opts.Experimental.CreateOnSharedLocator = createOnSharedLocator +} diff --git a/pebble/tool/util.go b/pebble/tool/util.go new file mode 100644 index 0000000..e315424 --- /dev/null +++ b/pebble/tool/util.go @@ -0,0 +1,325 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import ( + "encoding/hex" + "fmt" + "io" + "sort" + "strings" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" +) + +var timeNow = time.Now + +type key []byte + +func (k *key) String() string { + return string(*k) +} + +func (k *key) Type() string { + return "key" +} + +func (k *key) Set(v string) error { + switch { + case strings.HasPrefix(v, "hex:"): + v = strings.TrimPrefix(v, "hex:") + b, err := hex.DecodeString(v) + if err != nil { + return err + } + *k = key(b) + + case strings.HasPrefix(v, "raw:"): + *k = key(strings.TrimPrefix(v, "raw:")) + + default: + *k = key(v) + } + return nil +} + +type keyFormatter struct { + spec string + fn base.FormatKey + setByUser bool + comparer string +} + +func (f *keyFormatter) String() string { + return f.spec +} + +func (f *keyFormatter) Type() string { + return "keyFormatter" +} + +func (f *keyFormatter) Set(spec string) error { + f.spec = spec + f.setByUser = true + switch spec { + case "null": + f.fn = formatKeyNull + case "quoted": + f.fn = formatKeyQuoted + case "pretty": + // Using "pretty" defaults to base.FormatBytes (just like formatKeyQuoted), + // except with the ability of having the comparer-provided formatter + // overwrite f.fn if there is one specified. We determine whether to do + // that overwrite through setByUser. + f.fn = formatKeyQuoted + f.setByUser = false + case "size": + f.fn = formatKeySize + default: + if strings.HasPrefix(spec, "pretty:") { + // Usage: pretty: + f.comparer = spec[7:] + f.fn = formatKeyQuoted + return nil + } + if strings.Count(spec, "%") != 1 { + return errors.Errorf("unknown formatter: %q", errors.Safe(spec)) + } + f.fn = func(v []byte) fmt.Formatter { + return fmtFormatter{f.spec, v} + } + } + return nil +} + +func (f *keyFormatter) mustSet(spec string) { + if err := f.Set(spec); err != nil { + panic(err) + } + f.setByUser = false +} + +// Sets the appropriate formatter function for this comparer. +func (f *keyFormatter) setForComparer(comparerName string, comparers sstable.Comparers) { + if f.setByUser && len(f.comparer) == 0 { + // User specified a different formatter, no-op. + return + } + + if len(f.comparer) > 0 { + // User specified a comparer to reference for formatting, which takes + // precedence. + comparerName = f.comparer + } else if len(comparerName) == 0 { + return + } + + if cmp := comparers[comparerName]; cmp != nil && cmp.FormatKey != nil { + f.fn = cmp.FormatKey + } +} + +type valueFormatter struct { + spec string + fn base.FormatValue + setByUser bool + comparer string +} + +func (f *valueFormatter) String() string { + return f.spec +} + +func (f *valueFormatter) Type() string { + return "valueFormatter" +} + +func (f *valueFormatter) Set(spec string) error { + f.spec = spec + f.setByUser = true + switch spec { + case "null": + f.fn = formatValueNull + case "quoted": + f.fn = formatValueQuoted + case "pretty": + // Using "pretty" defaults to base.FormatBytes (just like + // formatValueQuoted), except with the ability of having the + // comparer-provided formatter overwrite f.fn if there is one specified. We + // determine whether to do that overwrite through setByUser. + f.fn = formatValueQuoted + f.setByUser = false + case "size": + f.fn = formatValueSize + default: + if strings.HasPrefix(spec, "pretty:") { + // Usage: pretty: + f.comparer = spec[7:] + f.fn = formatValueQuoted + return nil + } + if strings.Count(spec, "%") != 1 { + return errors.Errorf("unknown formatter: %q", errors.Safe(spec)) + } + f.fn = func(k, v []byte) fmt.Formatter { + return fmtFormatter{f.spec, v} + } + } + return nil +} + +func (f *valueFormatter) mustSet(spec string) { + if err := f.Set(spec); err != nil { + panic(err) + } + f.setByUser = false +} + +// Sets the appropriate formatter function for this comparer. +func (f *valueFormatter) setForComparer(comparerName string, comparers sstable.Comparers) { + if f.setByUser && len(f.comparer) == 0 { + // User specified a different formatter, no-op. + return + } + + if len(f.comparer) > 0 { + // User specified a comparer to reference for formatting, which takes + // precedence. + comparerName = f.comparer + } else if len(comparerName) == 0 { + return + } + + if cmp := comparers[comparerName]; cmp != nil && cmp.FormatValue != nil { + f.fn = cmp.FormatValue + } +} + +type fmtFormatter struct { + fmt string + v []byte +} + +func (f fmtFormatter) Format(s fmt.State, c rune) { + fmt.Fprintf(s, f.fmt, f.v) +} + +type nullFormatter struct{} + +func (nullFormatter) Format(s fmt.State, c rune) { +} + +func formatKeyNull(v []byte) fmt.Formatter { + return nullFormatter{} +} + +func formatValueNull(k, v []byte) fmt.Formatter { + return nullFormatter{} +} + +func formatKeyQuoted(v []byte) fmt.Formatter { + return base.FormatBytes(v) +} + +func formatValueQuoted(k, v []byte) fmt.Formatter { + return base.FormatBytes(v) +} + +type sizeFormatter []byte + +func (v sizeFormatter) Format(s fmt.State, c rune) { + fmt.Fprintf(s, "<%d>", len(v)) +} + +func formatKeySize(v []byte) fmt.Formatter { + return sizeFormatter(v) +} + +func formatValueSize(k, v []byte) fmt.Formatter { + return sizeFormatter(v) +} + +func formatKey(w io.Writer, fmtKey keyFormatter, key *base.InternalKey) bool { + if fmtKey.spec == "null" { + return false + } + fmt.Fprintf(w, "%s", key.Pretty(fmtKey.fn)) + return true +} + +func formatSeqNumRange(w io.Writer, start, end uint64) { + fmt.Fprintf(w, "<#%d-#%d>", start, end) +} + +func formatKeyRange(w io.Writer, fmtKey keyFormatter, start, end *base.InternalKey) { + if fmtKey.spec == "null" { + return + } + fmt.Fprintf(w, "[%s-%s]", start.Pretty(fmtKey.fn), end.Pretty(fmtKey.fn)) +} + +func formatKeyValue( + w io.Writer, fmtKey keyFormatter, fmtValue valueFormatter, key *base.InternalKey, value []byte, +) { + if key.Kind() == base.InternalKeyKindRangeDelete { + if fmtKey.spec != "null" { + fmt.Fprintf(w, "%s-%s#%d,%s", + fmtKey.fn(key.UserKey), fmtKey.fn(value), + key.SeqNum(), key.Kind()) + } + } else { + needDelimiter := formatKey(w, fmtKey, key) + if fmtValue.spec != "null" { + if needDelimiter { + w.Write([]byte{' '}) + } + fmt.Fprintf(w, "%s", fmtValue.fn(key.UserKey, value)) + } + } + w.Write([]byte{'\n'}) +} + +func formatSpan(w io.Writer, fmtKey keyFormatter, fmtValue valueFormatter, s *keyspan.Span) { + if fmtKey.spec != "null" { + fmt.Fprintf(w, "[%s-%s):\n", fmtKey.fn(s.Start), fmtKey.fn(s.End)) + for _, k := range s.Keys { + fmt.Fprintf(w, " #%d,%s", k.SeqNum(), k.Kind()) + switch k.Kind() { + case base.InternalKeyKindRangeKeySet: + fmt.Fprintf(w, ": %s %s", k.Suffix, fmtValue.fn(s.Start, k.Value)) + case base.InternalKeyKindRangeKeyUnset: + fmt.Fprintf(w, ": %s", k.Suffix) + } + w.Write([]byte{'\n'}) + } + } +} + +func walk(stderr io.Writer, fs vfs.FS, dir string, fn func(path string)) { + paths, err := fs.List(dir) + if err != nil { + fmt.Fprintf(stderr, "%s: %v\n", dir, err) + return + } + sort.Strings(paths) + for _, part := range paths { + path := fs.PathJoin(dir, part) + info, err := fs.Stat(path) + if err != nil { + fmt.Fprintf(stderr, "%s: %v\n", path, err) + continue + } + if info.IsDir() { + walk(stderr, fs, path, fn) + } else { + fn(path) + } + } +} diff --git a/pebble/tool/wal.go b/pebble/tool/wal.go new file mode 100644 index 0000000..2d3775f --- /dev/null +++ b/pebble/tool/wal.go @@ -0,0 +1,171 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/rangekey" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/sstable" + "github.com/spf13/cobra" +) + +// walT implements WAL-level tools, including both configuration state and the +// commands themselves. +type walT struct { + Root *cobra.Command + Dump *cobra.Command + + opts *pebble.Options + fmtKey keyFormatter + fmtValue valueFormatter + + defaultComparer string + comparers sstable.Comparers + verbose bool +} + +func newWAL(opts *pebble.Options, comparers sstable.Comparers, defaultComparer string) *walT { + w := &walT{ + opts: opts, + } + w.fmtKey.mustSet("quoted") + w.fmtValue.mustSet("size") + w.comparers = comparers + w.defaultComparer = defaultComparer + + w.Root = &cobra.Command{ + Use: "wal", + Short: "WAL introspection tools", + } + w.Dump = &cobra.Command{ + Use: "dump ", + Short: "print WAL contents", + Long: ` +Print the contents of the WAL files. +`, + Args: cobra.MinimumNArgs(1), + Run: w.runDump, + } + + w.Root.AddCommand(w.Dump) + w.Root.PersistentFlags().BoolVarP(&w.verbose, "verbose", "v", false, "verbose output") + + w.Dump.Flags().Var( + &w.fmtKey, "key", "key formatter") + w.Dump.Flags().Var( + &w.fmtValue, "value", "value formatter") + return w +} + +func (w *walT) runDump(cmd *cobra.Command, args []string) { + stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() + w.fmtKey.setForComparer(w.defaultComparer, w.comparers) + w.fmtValue.setForComparer(w.defaultComparer, w.comparers) + + for _, arg := range args { + func() { + // Parse the filename in order to extract the file number. This is + // necessary in case WAL recycling was used (which it is usually is). If + // we can't parse the filename or it isn't a log file, we'll plow ahead + // anyways (which will likely fail when we try to read the file). + _, fileNum, ok := base.ParseFilename(w.opts.FS, arg) + if !ok { + fileNum = base.FileNum(0).DiskFileNum() + } + + f, err := w.opts.FS.Open(arg) + if err != nil { + fmt.Fprintf(stderr, "%s\n", err) + return + } + defer f.Close() + + fmt.Fprintf(stdout, "%s\n", arg) + + var b pebble.Batch + var buf bytes.Buffer + rr := record.NewReader(f, fileNum) + for { + offset := rr.Offset() + r, err := rr.Next() + if err == nil { + buf.Reset() + _, err = io.Copy(&buf, r) + } + if err != nil { + // It is common to encounter a zeroed or invalid chunk due to WAL + // preallocation and WAL recycling. We need to distinguish these + // errors from EOF in order to recognize that the record was + // truncated, but want to otherwise treat them like EOF. + switch err { + case record.ErrZeroedChunk: + fmt.Fprintf(stdout, "EOF [%s] (may be due to WAL preallocation)\n", err) + case record.ErrInvalidChunk: + fmt.Fprintf(stdout, "EOF [%s] (may be due to WAL recycling)\n", err) + default: + fmt.Fprintf(stdout, "%s\n", err) + } + return + } + + b = pebble.Batch{} + if err := b.SetRepr(buf.Bytes()); err != nil { + fmt.Fprintf(stdout, "corrupt batch within log file %q: %v", arg, err) + return + } + fmt.Fprintf(stdout, "%d(%d) seq=%d count=%d\n", + offset, len(b.Repr()), b.SeqNum(), b.Count()) + for r, idx := b.Reader(), 0; ; idx++ { + kind, ukey, value, ok, err := r.Next() + if !ok { + if err != nil { + fmt.Fprintf(stdout, "corrupt batch within log file %q: %v", arg, err) + } + break + } + fmt.Fprintf(stdout, " %s(", kind) + switch kind { + case base.InternalKeyKindDelete: + fmt.Fprintf(stdout, "%s", w.fmtKey.fn(ukey)) + case base.InternalKeyKindSet: + fmt.Fprintf(stdout, "%s,%s", w.fmtKey.fn(ukey), w.fmtValue.fn(ukey, value)) + case base.InternalKeyKindMerge: + fmt.Fprintf(stdout, "%s,%s", w.fmtKey.fn(ukey), w.fmtValue.fn(ukey, value)) + case base.InternalKeyKindLogData: + fmt.Fprintf(stdout, "<%d>", len(value)) + case base.InternalKeyKindIngestSST: + fileNum, _ := binary.Uvarint(ukey) + fmt.Fprintf(stdout, "%s", base.FileNum(fileNum)) + case base.InternalKeyKindSingleDelete: + fmt.Fprintf(stdout, "%s", w.fmtKey.fn(ukey)) + case base.InternalKeyKindSetWithDelete: + fmt.Fprintf(stdout, "%s", w.fmtKey.fn(ukey)) + case base.InternalKeyKindRangeDelete: + fmt.Fprintf(stdout, "%s,%s", w.fmtKey.fn(ukey), w.fmtKey.fn(value)) + case base.InternalKeyKindRangeKeySet, base.InternalKeyKindRangeKeyUnset, base.InternalKeyKindRangeKeyDelete: + ik := base.MakeInternalKey(ukey, b.SeqNum()+uint64(idx), kind) + s, err := rangekey.Decode(ik, value, nil) + if err != nil { + fmt.Fprintf(stdout, "%s: error decoding %s", w.fmtKey.fn(ukey), err) + } else { + fmt.Fprintf(stdout, "%s", s.Pretty(w.fmtKey.fn)) + } + case base.InternalKeyKindDeleteSized: + v, _ := binary.Uvarint(value) + fmt.Fprintf(stdout, "%s,%d", w.fmtKey.fn(ukey), v) + } + fmt.Fprintf(stdout, ")\n") + } + } + }() + } +} diff --git a/pebble/tool/wal_test.go b/pebble/tool/wal_test.go new file mode 100644 index 0000000..f5de8eb --- /dev/null +++ b/pebble/tool/wal_test.go @@ -0,0 +1,11 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package tool + +import "testing" + +func TestWAL(t *testing.T) { + runTests(t, "testdata/wal_*") +} diff --git a/pebble/version_set.go b/pebble/version_set.go new file mode 100644 index 0000000..35dd0e7 --- /dev/null +++ b/pebble/version_set.go @@ -0,0 +1,1009 @@ +// Copyright 2012 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 ( + "bytes" + "fmt" + "io" + "sync" + "sync/atomic" + + "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/internal/manifest" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/atomicfs" +) + +const numLevels = manifest.NumLevels + +const manifestMarkerName = `manifest` + +// Provide type aliases for the various manifest structs. +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 +type versionList = manifest.VersionList + +// versionSet manages a collection of immutable versions, and manages the +// creation of a new version from the most recent version. A new version is +// created from an existing version by applying a version edit which is just +// 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 { + // 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 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 atomic.Int64 + + // Immutable fields. + dirname string + // Set to DB.mu. + 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 + + // Mutable fields. + versions versionList + picker compactionPicker + + metrics Metrics + + // A pointer to versionSet.addObsoleteLocked. Avoids allocating a new closure + // on the creation of every version. + 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[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. + minUnflushedLogNum base.DiskFileNum + + // The next file number. A single counter is used to assign file + // numbers for the WAL, MANIFEST, sstable, and OPTIONS files. + nextFileNum uint64 + + // The current manifest file number. + manifestFileNum base.DiskFileNum + manifestMarker *atomicfs.Marker + + manifestFile vfs.File + manifest *record.Writer + setCurrent func(base.DiskFileNum) error + getFormatMajorVersion func() FormatMajorVersion + + writing bool + writerCond sync.Cond + // State for deciding when to write a snapshot. Protected by mu. + rotationHelper record.RotationHelper +} + +func (vs *versionSet) init( + dirname string, + opts *Options, + marker *atomicfs.Marker, + setCurrent func(base.DiskFileNum) error, + getFMV func() FormatMajorVersion, + mu *sync.Mutex, +) { + vs.dirname = dirname + vs.mu = mu + vs.writerCond.L = mu + vs.opts = opts + vs.fs = opts.FS + vs.cmp = opts.Comparer.Compare + vs.cmpName = opts.Comparer.Name + vs.dynamicBaseLevel = true + vs.versions.Init(mu) + vs.obsoleteFn = vs.addObsoleteLocked + 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 + vs.getFormatMajorVersion = getFMV +} + +// create creates a version set for a fresh DB. +func (vs *versionSet) create( + jobID int, + dirname string, + opts *Options, + marker *atomicfs.Marker, + setCurrent func(base.DiskFileNum) error, + getFormatMajorVersion func() FormatMajorVersion, + mu *sync.Mutex, +) error { + vs.init(dirname, opts, marker, setCurrent, getFormatMajorVersion, mu) + newVersion := &version{} + vs.append(newVersion) + var err error + + 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.getNextDiskFileNum() + err = vs.createManifest(vs.dirname, vs.manifestFileNum, vs.minUnflushedLogNum, vs.nextFileNum) + if err == nil { + if err = vs.manifest.Flush(); err != nil { + vs.opts.Logger.Fatalf("MANIFEST flush failed: %v", err) + } + } + if err == nil { + if err = vs.manifestFile.Sync(); err != nil { + vs.opts.Logger.Fatalf("MANIFEST sync failed: %v", err) + } + } + if err == nil { + // NB: setCurrent is responsible for syncing the data directory. + if err = vs.setCurrent(vs.manifestFileNum); err != nil { + vs.opts.Logger.Fatalf("MANIFEST set current failed: %v", err) + } + } + + vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{ + JobID: jobID, + Path: base.MakeFilepath(vs.fs, vs.dirname, fileTypeManifest, vs.manifestFileNum), + FileNum: vs.manifestFileNum, + Err: err, + }) + if err != nil { + return err + } + return nil +} + +// load loads the version set from the manifest file. +func (vs *versionSet) load( + dirname string, + opts *Options, + manifestFileNum base.DiskFileNum, + marker *atomicfs.Marker, + setCurrent func(base.DiskFileNum) error, + getFormatMajorVersion func() FormatMajorVersion, + mu *sync.Mutex, +) error { + vs.init(dirname, opts, marker, setCurrent, getFormatMajorVersion, mu) + + vs.manifestFileNum = manifestFileNum + manifestPath := base.MakeFilepath(opts.FS, dirname, fileTypeManifest, vs.manifestFileNum) + manifestFilename := opts.FS.PathBase(manifestPath) + + // Read the versionEdits in the manifest file. + var bve bulkVersionEdit + bve.AddedByFileNum = make(map[base.FileNum]*fileMetadata) + manifest, err := vs.fs.Open(manifestPath) + if err != nil { + return errors.Wrapf(err, "pebble: could not open manifest file %q for DB %q", + errors.Safe(manifestFilename), dirname) + } + defer manifest.Close() + rr := record.NewReader(manifest, 0 /* logNum */) + for { + r, err := rr.Next() + if err == io.EOF || record.IsInvalidRecord(err) { + break + } + if err != nil { + return errors.Wrapf(err, "pebble: error when loading manifest file %q", + errors.Safe(manifestFilename)) + } + var ve versionEdit + err = ve.Decode(r) + if err != nil { + // Break instead of returning an error if the record is corrupted + // or invalid. + if err == io.EOF || record.IsInvalidRecord(err) { + break + } + return err + } + if ve.ComparerName != "" { + if ve.ComparerName != vs.cmpName { + return errors.Errorf("pebble: manifest file %q for DB %q: "+ + "comparer name from file %q != comparer name from Options %q", + errors.Safe(manifestFilename), dirname, errors.Safe(ve.ComparerName), errors.Safe(vs.cmpName)) + } + } + if err := bve.Accumulate(&ve); err != nil { + return err + } + if ve.MinUnflushedLogNum != 0 { + vs.minUnflushedLogNum = ve.MinUnflushedLogNum + } + if ve.NextFileNum != 0 { + vs.nextFileNum = ve.NextFileNum + } + if ve.LastSeqNum != 0 { + // logSeqNum is the _next_ sequence number that will be assigned, + // while LastSeqNum is the last assigned sequence number. Note that + // this behaviour mimics that in RocksDB; the first sequence number + // assigned is one greater than the one present in the manifest + // (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. + // + // 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 + // function and could have only updated it to some other non-zero value, + // so it cannot be 0 here. + if vs.minUnflushedLogNum == 0 { + if vs.nextFileNum >= 2 { + // We either have a freshly created DB, or a DB created by RocksDB + // that has not had a single flushed SSTable yet. This is because + // RocksDB bumps up nextFileNum in this case without bumping up + // minUnflushedLogNum, even if WALs with non-zero file numbers are + // present in the directory. + } else { + return base.CorruptionErrorf("pebble: malformed manifest file %q for DB %q", + errors.Safe(manifestFilename), dirname) + } + } + vs.markFileNumUsed(vs.minUnflushedLogNum) + + // 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 + } + newVersion.L0Sublevels.InitCompactingFileInfo(nil /* in-progress compactions */) + vs.append(newVersion) + + for i := range vs.metrics.Levels { + l := &vs.metrics.Levels[i] + l.NumFiles = int64(newVersion.Levels[i].Len()) + files := newVersion.Levels[i].Slice() + l.Size = int64(files.SizeSum()) + } + + vs.picker = newCompactionPicker(newVersion, vs.opts, nil) + return nil +} + +func (vs *versionSet) close() error { + if vs.manifestFile != nil { + if err := vs.manifestFile.Close(); err != nil { + return err + } + } + if vs.manifestMarker != nil { + if err := vs.manifestMarker.Close(); err != nil { + return err + } + } + return nil +} + +// logLock locks the manifest for writing. The lock must be released by either +// a call to logUnlock or logAndApply. +// +// DB.mu must be held when calling this method, but the mutex may be dropped and +// re-acquired during the course of this method. +func (vs *versionSet) logLock() { + // Wait for any existing writing to the manifest to complete, then mark the + // manifest as busy. + for vs.writing { + vs.writerCond.Wait() + } + vs.writing = true +} + +// logUnlock releases the lock for manifest writing. +// +// DB.mu must be held when calling this method. +func (vs *versionSet) logUnlock() { + if !vs.writing { + vs.opts.Logger.Fatalf("MANIFEST not locked for writing") + } + vs.writing = false + 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. +// +// DB.mu must be held when calling this method and will be released temporarily +// while performing file I/O. Requires that the manifest is locked for writing +// (see logLock). Will unconditionally release the manifest lock (via +// logUnlock) even if an error occurs. +// +// inProgressCompactions is called while DB.mu is held, to get the list of +// in-progress compactions. +func (vs *versionSet) logAndApply( + jobID int, + ve *versionEdit, + metrics map[int]*LevelMetrics, + forceRotation bool, + inProgressCompactions func() []compactionInfo, +) error { + if !vs.writing { + vs.opts.Logger.Fatalf("MANIFEST not locked for writing") + } + defer vs.logUnlock() + + if ve.MinUnflushedLogNum != 0 { + if ve.MinUnflushedLogNum < vs.minUnflushedLogNum || + vs.nextFileNum <= uint64(ve.MinUnflushedLogNum) { + panic(fmt.Sprintf("pebble: inconsistent versionEdit minUnflushedLogNum %d", + ve.MinUnflushedLogNum)) + } + } + + // This is the next manifest filenum, but if the current file is too big we + // will write this ve to the next file which means what ve encodes is the + // current filenum and not the next one. + // + // TODO(sbhola): figure out why this is correct and update comment. + ve.NextFileNum = vs.nextFileNum + + // LastSeqNum is set to the current upper bound on the assigned sequence + // numbers. Note that this is exactly the behavior of RocksDB. LastSeqNum is + // used to initialize versionSet.logSeqNum and versionSet.visibleSeqNum on + // replay. It must be higher than or equal to any than any sequence number + // written to an sstable, including sequence numbers in ingested files. + // Note that LastSeqNum is not (and cannot be) the minimum unflushed sequence + // number. This is fallout from ingestion which allows a sequence number X to + // be assigned to an ingested sstable even though sequence number X-1 resides + // 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 := vs.logSeqNum.Load() + ve.LastSeqNum = logSeqNum - 1 + if logSeqNum == 0 { + // logSeqNum is initialized to 1 in Open() if there are no previous WAL + // or manifest records, so this case should never happen. + vs.opts.Logger.Fatalf("logSeqNum must be a positive integer: %d", logSeqNum) + } + + 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 + // is true, or the current one is too large. + // + // For largeness, we do not exclusively use MaxManifestFileSize size + // threshold since we have had incidents where due to either large keys or + // large numbers of files, each edit results in a snapshot + write of the + // edit. This slows the system down since each flush or compaction is + // writing a new manifest snapshot. The primary goal of the size-based + // rollover logic is to ensure that when reopening a DB, the number of edits + // that need to be replayed on top of the snapshot is "sane". Rolling over + // to a new manifest after each edit is not relevant to that goal. + // + // Consider the following cases: + // - The number of live files F in the DB is roughly stable: after writing + // the snapshot (with F files), say we require that there be enough edits + // such that the cumulative number of files in those edits, E, be greater + // than F. This will ensure that the total amount of time in logAndApply + // that is spent in snapshot writing is ~50%. + // + // - The number of live files F in the DB is shrinking drastically, say from + // F to F/10: This can happen for various reasons, like wide range + // tombstones, or large numbers of smaller than usual files that are being + // merged together into larger files. And say the new files generated + // during this shrinkage is insignificant compared to F/10, and so for + // this example we will assume it is effectively 0. After this shrinking, + // E = 0.9F, and so if we used the previous snapshot file count, F, as the + // threshold that needs to be exceeded, we will further delay the snapshot + // writing. Which means on DB reopen we will need to replay 0.9F edits to + // get to a version with 0.1F files. It would be better to create a new + // snapshot when E exceeds the number of files in the current version. + // + // - The number of live files F in the DB is growing via perfect ingests + // into L6: Say we wrote the snapshot when there were F files and now we + // have 10F files, so E = 9F. We will further delay writing a new + // snapshot. This case can be critiqued as contrived, but we consider it + // nonetheless. + // + // The logic below uses the min of the last snapshot file count and the file + // count in the current version. + vs.rotationHelper.AddRecord(int64(len(ve.DeletedFiles) + len(ve.NewFiles))) + sizeExceeded := vs.manifest.Size() >= vs.opts.MaxManifestFileSize + requireRotation := forceRotation || vs.manifest == nil + + var nextSnapshotFilecount int64 + for i := range vs.metrics.Levels { + nextSnapshotFilecount += vs.metrics.Levels[i].NumFiles + } + if sizeExceeded && !requireRotation { + requireRotation = vs.rotationHelper.ShouldRotate(nextSnapshotFilecount) + } + var newManifestFileNum base.DiskFileNum + var prevManifestFileSize uint64 + if requireRotation { + newManifestFileNum = vs.getNextDiskFileNum() + prevManifestFileSize = uint64(vs.manifest.Size()) + } + + // Grab certain values before releasing vs.mu, in case createManifest() needs + // to be called. + minUnflushedLogNum := vs.minUnflushedLogNum + nextFileNum := vs.nextFileNum + + var zombies map[base.DiskFileNum]uint64 + if err := func() error { + vs.mu.Unlock() + defer vs.mu.Lock() + + var err error + 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") + } + + if newManifestFileNum != 0 { + 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), + FileNum: newManifestFileNum, + Err: err, + }) + return errors.Wrap(err, "MANIFEST create failed") + } + } + + w, err := vs.manifest.Next() + 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 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 + // and ensures it is synced. + if err := ve.Encode(w); err != nil { + return errors.Wrap(err, "MANIFEST write failed") + } + if err := vs.manifest.Flush(); err != nil { + return errors.Wrap(err, "MANIFEST flush failed") + } + if err := vs.manifestFile.Sync(); err != nil { + return errors.Wrap(err, "MANIFEST sync failed") + } + if newManifestFileNum != 0 { + // NB: setCurrent is responsible for syncing the data directory. + if err := vs.setCurrent(newManifestFileNum); err != nil { + return errors.Wrap(err, "MANIFEST set current failed") + } + vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{ + JobID: jobID, + Path: base.MakeFilepath(vs.fs, vs.dirname, fileTypeManifest, newManifestFileNum), + FileNum: newManifestFileNum, + }) + } + return nil + }(); err != nil { + // Any error encountered during any of the operations in the previous + // closure are considered fatal. Treating such errors as fatal is preferred + // to attempting to unwind various file and b-tree reference counts, and + // re-generating L0 sublevel metadata. This may change in the future, if + // certain manifest / WAL operations become retryable. For more context, see + // #1159 and #1792. + vs.opts.Logger.Fatalf("%s", err) + return err + } + + if requireRotation { + // Successfully rotated. + vs.rotationHelper.Rotate(nextSnapshotFilecount) + } + // Now that DB.mu is held again, initialize compacting file info in + // L0Sublevels. + inProgress := inProgressCompactions() + + newVersion.L0Sublevels.InitCompactingFileInfo(inProgressL0Compactions(inProgress)) + + // Update the zombie tables set first, as installation of the new version + // will unref the previous version which could result in addObsoleteLocked + // being called. + for fileNum, size := range zombies { + vs.zombieTables[fileNum] = size + } + + // Install the new version. + vs.append(newVersion) + if ve.MinUnflushedLogNum != 0 { + vs.minUnflushedLogNum = ve.MinUnflushedLogNum + } + if newManifestFileNum != 0 { + if vs.manifestFileNum != 0 { + vs.obsoleteManifests = append(vs.obsoleteManifests, fileInfo{ + fileNum: vs.manifestFileNum, + fileSize: prevManifestFileSize, + }) + } + vs.manifestFileNum = newManifestFileNum + } + + for level, update := range metrics { + vs.metrics.Levels[level].Add(update) + } + 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 { + 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) + if !vs.dynamicBaseLevel { + vs.picker.forceBaseLevel1() + } + return nil +} + +func (vs *versionSet) incrementCompactions( + kind compactionKind, extraLevels []*compactionLevel, pickerMetrics compactionPickerMetrics, +) { + switch kind { + case compactionKindDefault: + vs.metrics.Compact.Count++ + vs.metrics.Compact.DefaultCount++ + + case compactionKindFlush, compactionKindIngestedFlushable: + vs.metrics.Flush.Count++ + + case compactionKindMove: + vs.metrics.Compact.Count++ + vs.metrics.Compact.MoveCount++ + + case compactionKindDeleteOnly: + vs.metrics.Compact.Count++ + vs.metrics.Compact.DeleteOnlyCount++ + + case compactionKindElisionOnly: + vs.metrics.Compact.Count++ + vs.metrics.Compact.ElisionOnlyCount++ + + case compactionKindRead: + vs.metrics.Compact.Count++ + vs.metrics.Compact.ReadCount++ + + case compactionKindRewrite: + vs.metrics.Compact.Count++ + vs.metrics.Compact.RewriteCount++ + } + if len(extraLevels) > 0 { + vs.metrics.Compact.MultiLevelCount++ + } +} + +func (vs *versionSet) incrementCompactionBytes(numBytes int64) { + vs.atomicInProgressBytes.Add(numBytes) +} + +// createManifest creates a manifest file that contains a snapshot of vs. +func (vs *versionSet) createManifest( + dirname string, fileNum, minUnflushedLogNum base.DiskFileNum, nextFileNum uint64, +) (err error) { + var ( + filename = base.MakeFilepath(vs.fs, dirname, fileTypeManifest, fileNum) + manifestFile vfs.File + manifest *record.Writer + ) + defer func() { + if manifest != nil { + manifest.Close() + } + if manifestFile != nil { + manifestFile.Close() + } + if err != nil { + vs.fs.Remove(filename) + } + }() + manifestFile, err = vs.fs.Create(filename) + if err != nil { + return err + } + manifest = record.NewWriter(manifestFile) + + 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() { + snapshot.NewFiles = append(snapshot.NewFiles, newFileEntry{ + 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, + ) + } + } + } + + // When creating a version snapshot for an existing DB, this snapshot VersionEdit will be + // immediately followed by another VersionEdit (being written in logAndApply()). That + // VersionEdit always contains a LastSeqNum, so we don't need to include that in the snapshot. + // But it does not necessarily include MinUnflushedLogNum, NextFileNum, so we initialize those + // using the corresponding fields in the versionSet (which came from the latest preceding + // VersionEdit that had those fields). + snapshot.MinUnflushedLogNum = minUnflushedLogNum + snapshot.NextFileNum = nextFileNum + + w, err1 := manifest.Next() + if err1 != nil { + return err1 + } + if err := snapshot.Encode(w); err != nil { + return err + } + + if vs.manifest != nil { + vs.manifest.Close() + vs.manifest = nil + } + if vs.manifestFile != nil { + if err := vs.manifestFile.Close(); err != nil { + return err + } + vs.manifestFile = nil + } + + vs.manifest, manifest = manifest, nil + vs.manifestFile, manifestFile = manifestFile, nil + return nil +} + +func (vs *versionSet) markFileNumUsed(fileNum base.DiskFileNum) { + if vs.nextFileNum <= uint64(fileNum) { + vs.nextFileNum = uint64(fileNum + 1) + } +} + +func (vs *versionSet) getNextFileNum() base.FileNum { + x := vs.nextFileNum + vs.nextFileNum++ + return base.FileNum(x) +} + +func (vs *versionSet) getNextDiskFileNum() base.DiskFileNum { + x := vs.nextFileNum + vs.nextFileNum++ + return base.DiskFileNum(x) +} + +func (vs *versionSet) append(v *version) { + if v.Refs() != 0 { + panic("pebble: version should be unreferenced") + } + if !vs.versions.Empty() { + vs.versions.Back().UnrefLocked() + } + v.Deleted = vs.obsoleteFn + v.Ref() + vs.versions.PushBack(v) +} + +func (vs *versionSet) currentVersion() *version { + return vs.versions.Back() +} + +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.FileBacking.DiskFileNum] = struct{}{} + } + } + if v == current { + break + } + } +} + +// 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 []*fileBacking) { + if len(obsolete) == 0 { + return + } + + 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[fi.fileNum]; !ok { + vs.opts.Logger.Fatalf("MANIFEST obsolete table %s not marked as zombie", fi.fileNum) + } + } + + 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 []*fileBacking) { + vs.mu.Lock() + defer vs.mu.Unlock() + vs.addObsoleteLocked(obsolete) +} + +func (vs *versionSet) updateObsoleteTableMetricsLocked() { + vs.metrics.Table.ObsoleteCount = int64(len(vs.obsoleteTables)) + vs.metrics.Table.ObsoleteSize = 0 + for _, fi := range vs.obsoleteTables { + vs.metrics.Table.ObsoleteSize += fi.fileSize + } +} + +func setCurrentFunc( + vers FormatMajorVersion, marker *atomicfs.Marker, fs vfs.FS, dirname string, dir vfs.File, +) func(base.DiskFileNum) error { + if vers < formatVersionedManifestMarker { + // Pebble versions before `formatVersionedManifestMarker` used + // the CURRENT file to signal which MANIFEST is current. Ignore + // the filename read during LocateMarker. + return func(manifestFileNum base.DiskFileNum) error { + if err := setCurrentFile(dirname, fs, manifestFileNum); err != nil { + return err + } + if err := dir.Sync(); err != nil { + // This is a panic here, rather than higher in the call + // stack, for parity with the atomicfs.Marker behavior. + // A panic is always necessary because failed Syncs are + // unrecoverable. + panic(errors.Wrap(err, "fatal: MANIFEST dirsync failed")) + } + return nil + } + } + return setCurrentFuncMarker(marker, fs, dirname) +} + +func setCurrentFuncMarker( + marker *atomicfs.Marker, fs vfs.FS, dirname string, +) func(base.DiskFileNum) error { + return func(manifestFileNum base.DiskFileNum) error { + return marker.Move(base.MakeFilename(fileTypeManifest, manifestFileNum)) + } +} + +func findCurrentManifest( + vers FormatMajorVersion, fs vfs.FS, dirname string, +) (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 + // marker should succeed even if the marker has never been placed. + var filename string + marker, filename, err = atomicfs.LocateMarker(fs, dirname, manifestMarkerName) + if err != nil { + return nil, base.FileNum(0).DiskFileNum(), false, err + } + + if vers < formatVersionedManifestMarker { + // Pebble versions before `formatVersionedManifestMarker` used + // the CURRENT file to signal which MANIFEST is current. Ignore + // the filename read during LocateMarker. + + manifestNum, err = readCurrentFile(fs, dirname) + if oserror.IsNotExist(err) { + return marker, base.FileNum(0).DiskFileNum(), false, nil + } else if err != nil { + return marker, base.FileNum(0).DiskFileNum(), false, err + } + return marker, manifestNum, true, nil + } + + // The current format major version is >= + // formatVersionedManifestMarker indicating that the + // atomicfs.Marker is the source of truth on the current manifest. + + if filename == "" { + // The marker hasn't been set yet. This database doesn't exist. + return marker, base.FileNum(0).DiskFileNum(), false, nil + } + + var ok bool + _, manifestNum, ok = base.ParseFilename(fs, filename) + if !ok { + 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) (base.DiskFileNum, error) { + // Read the CURRENT file to find the current manifest file. + current, err := fs.Open(base.MakeFilepath(fs, dirname, fileTypeCurrent, base.FileNum(0).DiskFileNum())) + if err != nil { + 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 base.FileNum(0).DiskFileNum(), err + } + n := stat.Size() + if n == 0 { + return base.FileNum(0).DiskFileNum(), errors.Errorf("pebble: CURRENT file for DB %q is empty", dirname) + } + if n > 4096 { + 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 base.FileNum(0).DiskFileNum(), err + } + if b[n-1] != '\n' { + 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 base.FileNum(0).DiskFileNum(), base.CorruptionErrorf("pebble: MANIFEST name %q is malformed", errors.Safe(b)) + } + return manifestFileNum, nil +} + +func newFileMetrics(newFiles []manifest.NewFileEntry) map[int]*LevelMetrics { + m := map[int]*LevelMetrics{} + for _, nf := range newFiles { + lm := m[nf.Level] + if lm == nil { + lm = &LevelMetrics{} + m[nf.Level] = lm + } + lm.NumFiles++ + lm.Size += int64(nf.Meta.Size) + } + return m +} diff --git a/pebble/version_set_test.go b/pebble/version_set_test.go new file mode 100644 index 0000000..d0059db --- /dev/null +++ b/pebble/version_set_test.go @@ -0,0 +1,509 @@ +// Copyright 2020 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 ( + "io" + "testing" + "time" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/sstable" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func writeAndIngest(t *testing.T, mem vfs.FS, d *DB, k InternalKey, v []byte, filename string) { + path := mem.PathJoin("ext", filename) + f, err := mem.Create(path) + require.NoError(t, err) + w := sstable.NewWriter(objstorageprovider.NewFileWritable(f), sstable.WriterOptions{}) + require.NoError(t, w.Add(k, v)) + require.NoError(t, w.Close()) + require.NoError(t, d.Ingest([]string{path})) +} + +// d.mu should be help. logLock should not be held. +func checkBackingSize(t *testing.T, d *DB) { + d.mu.versions.logLock() + var backingSizeSum uint64 + for _, backing := range d.mu.versions.backingState.fileBackingMap { + backingSizeSum += backing.Size + } + require.Equal(t, backingSizeSum, d.mu.versions.backingState.fileBackingSize) + d.mu.versions.logUnlock() +} + +// TestLatestRefCounting sanity checks the ref counting implementation for +// FileMetadata.latestRefs, and makes sure that the zombie table implementation +// works when the version edit contains virtual sstables. It also checks that +// we're adding the physical sstable to the obsolete tables list iff the file is +// truly obsolete. +func TestLatestRefCounting(t *testing.T) { + mem := vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + + opts := &Options{ + FS: mem, + MaxManifestFileSize: 1, + DisableAutomaticCompactions: true, + FormatMajorVersion: FormatVirtualSSTables, + } + d, err := Open("", opts) + require.NoError(t, err) + + err = d.Set([]byte{'a'}, []byte{'a'}, nil) + require.NoError(t, err) + err = d.Set([]byte{'b'}, []byte{'b'}, nil) + require.NoError(t, err) + + err = d.Flush() + require.NoError(t, err) + + iter := d.mu.versions.currentVersion().Levels[0].Iter() + var f *fileMetadata = iter.First() + require.NotNil(t, f) + require.Equal(t, 1, int(f.LatestRefs())) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + + // Grab some new file nums. + d.mu.Lock() + f1 := FileNum(d.mu.versions.nextFileNum) + f2 := f1 + 1 + d.mu.versions.nextFileNum += 2 + d.mu.Unlock() + + m1 := &manifest.FileMetadata{ + FileBacking: f.FileBacking, + FileNum: f1, + CreationTime: time.Now().Unix(), + Size: f.Size / 2, + SmallestSeqNum: f.SmallestSeqNum, + LargestSeqNum: f.LargestSeqNum, + Smallest: base.MakeInternalKey([]byte{'a'}, f.Smallest.SeqNum(), InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte{'a'}, f.Smallest.SeqNum(), InternalKeyKindSet), + HasPointKeys: true, + Virtual: true, + } + + m2 := &manifest.FileMetadata{ + FileBacking: f.FileBacking, + FileNum: f2, + CreationTime: time.Now().Unix(), + Size: f.Size - m1.Size, + SmallestSeqNum: f.SmallestSeqNum, + LargestSeqNum: f.LargestSeqNum, + Smallest: base.MakeInternalKey([]byte{'b'}, f.Largest.SeqNum(), InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte{'b'}, f.Largest.SeqNum(), InternalKeyKindSet), + HasPointKeys: true, + Virtual: true, + } + + m1.LargestPointKey = m1.Largest + m1.SmallestPointKey = m1.Smallest + + m2.LargestPointKey = m2.Largest + m2.SmallestPointKey = m2.Smallest + + m1.ValidateVirtual(f) + d.checkVirtualBounds(m1) + m2.ValidateVirtual(f) + d.checkVirtualBounds(m2) + + fileMetrics := func(ve *versionEdit) map[int]*LevelMetrics { + metrics := newFileMetrics(ve.NewFiles) + for de, f := range ve.DeletedFiles { + lm := metrics[de.Level] + if lm == nil { + lm = &LevelMetrics{} + metrics[de.Level] = lm + } + metrics[de.Level].NumFiles-- + metrics[de.Level].Size -= int64(f.Size) + } + return metrics + } + + d.mu.Lock() + defer d.mu.Unlock() + applyVE := func(ve *versionEdit) error { + d.mu.versions.logLock() + jobID := d.mu.nextJobID + d.mu.nextJobID++ + + err := d.mu.versions.logAndApply(jobID, ve, fileMetrics(ve), false, func() []compactionInfo { + return d.getInProgressCompactionInfoLocked(nil) + }) + d.updateReadStateLocked(nil) + return err + } + + // Virtualize f. + ve := manifest.VersionEdit{} + d1 := manifest.DeletedFileEntry{Level: 0, FileNum: f.FileNum} + n1 := manifest.NewFileEntry{Level: 0, Meta: m1} + n2 := manifest.NewFileEntry{Level: 0, Meta: m2} + + ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata) + ve.DeletedFiles[d1] = f + ve.NewFiles = append(ve.NewFiles, n1) + ve.NewFiles = append(ve.NewFiles, n2) + ve.CreatedBackingTables = append(ve.CreatedBackingTables, f.FileBacking) + + require.NoError(t, applyVE(&ve)) + // 2 latestRefs from 2 virtual sstables in the latest version which refer + // to the physical sstable. + require.Equal(t, 2, int(m1.LatestRefs())) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap)) + _, ok := d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum] + require.True(t, ok) + require.Equal(t, f.Size, m2.FileBacking.VirtualizedSize.Load()) + checkBackingSize(t, d) + + // Make sure that f is not present in zombie list, because it is not yet a + // zombie. + require.Equal(t, 0, len(d.mu.versions.zombieTables)) + + // Delete the virtual sstable m1. + ve = manifest.VersionEdit{} + d1 = manifest.DeletedFileEntry{Level: 0, FileNum: m1.FileNum} + ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata) + ve.DeletedFiles[d1] = m1 + require.NoError(t, applyVE(&ve)) + + // Only one virtual sstable in the latest version, confirm that the latest + // version ref counting is correct. + require.Equal(t, 1, int(m2.LatestRefs())) + require.Equal(t, 0, len(d.mu.versions.zombieTables)) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap)) + _, ok = d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum] + require.True(t, ok) + require.Equal(t, m2.Size, m2.FileBacking.VirtualizedSize.Load()) + checkBackingSize(t, d) + + // Move m2 from L0 to L6 to test the move compaction case. + ve = manifest.VersionEdit{} + d1 = manifest.DeletedFileEntry{Level: 0, FileNum: m2.FileNum} + n1 = manifest.NewFileEntry{Level: 6, Meta: m2} + ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata) + ve.DeletedFiles[d1] = m2 + ve.NewFiles = append(ve.NewFiles, n1) + require.NoError(t, applyVE(&ve)) + checkBackingSize(t, d) + + require.Equal(t, 1, int(m2.LatestRefs())) + require.Equal(t, 0, len(d.mu.versions.zombieTables)) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap)) + _, ok = d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum] + require.True(t, ok) + require.Equal(t, m2.Size, m2.FileBacking.VirtualizedSize.Load()) + + // Delete m2 from L6. + ve = manifest.VersionEdit{} + d1 = manifest.DeletedFileEntry{Level: 6, FileNum: m2.FileNum} + ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata) + ve.DeletedFiles[d1] = m2 + require.NoError(t, applyVE(&ve)) + checkBackingSize(t, d) + + // All virtual sstables are gone. + require.Equal(t, 0, int(m2.LatestRefs())) + require.Equal(t, 1, len(d.mu.versions.zombieTables)) + require.Equal(t, f.Size, d.mu.versions.zombieTables[f.FileBacking.DiskFileNum]) + require.Equal(t, 0, len(d.mu.versions.backingState.fileBackingMap)) + _, ok = d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum] + require.False(t, ok) + require.Equal(t, 0, int(m2.FileBacking.VirtualizedSize.Load())) + checkBackingSize(t, d) +} + +// TODO(bananabrick): Convert TestLatestRefCounting and this test into a single +// datadriven test. +func TestVirtualSSTableManifestReplay(t *testing.T) { + mem := vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + + opts := &Options{ + FormatMajorVersion: FormatVirtualSSTables, + FS: mem, + MaxManifestFileSize: 1, + DisableAutomaticCompactions: true, + } + d, err := Open("", opts) + require.NoError(t, err) + + err = d.Set([]byte{'a'}, []byte{'a'}, nil) + require.NoError(t, err) + err = d.Set([]byte{'b'}, []byte{'b'}, nil) + require.NoError(t, err) + + err = d.Flush() + require.NoError(t, err) + + iter := d.mu.versions.currentVersion().Levels[0].Iter() + var f *fileMetadata = iter.First() + require.NotNil(t, f) + require.Equal(t, 1, int(f.LatestRefs())) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + + // Grab some new file nums. + d.mu.Lock() + f1 := FileNum(d.mu.versions.nextFileNum) + f2 := f1 + 1 + d.mu.versions.nextFileNum += 2 + d.mu.Unlock() + + m1 := &manifest.FileMetadata{ + FileBacking: f.FileBacking, + FileNum: f1, + CreationTime: time.Now().Unix(), + Size: f.Size / 2, + SmallestSeqNum: f.SmallestSeqNum, + LargestSeqNum: f.LargestSeqNum, + Smallest: base.MakeInternalKey([]byte{'a'}, f.Smallest.SeqNum(), InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte{'a'}, f.Smallest.SeqNum(), InternalKeyKindSet), + HasPointKeys: true, + Virtual: true, + } + + m2 := &manifest.FileMetadata{ + FileBacking: f.FileBacking, + FileNum: f2, + CreationTime: time.Now().Unix(), + Size: f.Size - m1.Size, + SmallestSeqNum: f.SmallestSeqNum, + LargestSeqNum: f.LargestSeqNum, + Smallest: base.MakeInternalKey([]byte{'b'}, f.Largest.SeqNum(), InternalKeyKindSet), + Largest: base.MakeInternalKey([]byte{'b'}, f.Largest.SeqNum(), InternalKeyKindSet), + HasPointKeys: true, + Virtual: true, + } + + m1.LargestPointKey = m1.Largest + m1.SmallestPointKey = m1.Smallest + m1.Stats.NumEntries = 1 + + m2.LargestPointKey = m2.Largest + m2.SmallestPointKey = m2.Smallest + m2.Stats.NumEntries = 1 + + m1.ValidateVirtual(f) + d.checkVirtualBounds(m1) + m2.ValidateVirtual(f) + d.checkVirtualBounds(m2) + + fileMetrics := func(ve *versionEdit) map[int]*LevelMetrics { + metrics := newFileMetrics(ve.NewFiles) + for de, f := range ve.DeletedFiles { + lm := metrics[de.Level] + if lm == nil { + lm = &LevelMetrics{} + metrics[de.Level] = lm + } + metrics[de.Level].NumFiles-- + metrics[de.Level].Size -= int64(f.Size) + } + return metrics + } + + d.mu.Lock() + applyVE := func(ve *versionEdit) error { + d.mu.versions.logLock() + jobID := d.mu.nextJobID + d.mu.nextJobID++ + + err := d.mu.versions.logAndApply(jobID, ve, fileMetrics(ve), false, func() []compactionInfo { + return d.getInProgressCompactionInfoLocked(nil) + }) + d.updateReadStateLocked(nil) + return err + } + + // Virtualize f. + ve := manifest.VersionEdit{} + d1 := manifest.DeletedFileEntry{Level: 0, FileNum: f.FileNum} + n1 := manifest.NewFileEntry{Level: 0, Meta: m1} + n2 := manifest.NewFileEntry{Level: 0, Meta: m2} + + ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata) + ve.DeletedFiles[d1] = f + ve.NewFiles = append(ve.NewFiles, n1) + ve.NewFiles = append(ve.NewFiles, n2) + ve.CreatedBackingTables = append(ve.CreatedBackingTables, f.FileBacking) + + require.NoError(t, applyVE(&ve)) + checkBackingSize(t, d) + d.mu.Unlock() + + require.Equal(t, 2, int(m1.LatestRefs())) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap)) + _, ok := d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum] + require.True(t, ok) + require.Equal(t, f.Size, m2.FileBacking.VirtualizedSize.Load()) + + // Snapshot version edit will be written to a new manifest due to the flush. + d.Set([]byte{'c'}, []byte{'c'}, nil) + d.Flush() + + require.NoError(t, d.Close()) + d, err = Open("", opts) + require.NoError(t, err) + + d.mu.Lock() + it := d.mu.versions.currentVersion().Levels[0].Iter() + var virtualFile *fileMetadata + for f := it.First(); f != nil; f = it.Next() { + if f.Virtual { + virtualFile = f + break + } + } + + require.Equal(t, 2, int(virtualFile.LatestRefs())) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + require.Equal(t, 1, len(d.mu.versions.backingState.fileBackingMap)) + _, ok = d.mu.versions.backingState.fileBackingMap[f.FileBacking.DiskFileNum] + require.True(t, ok) + require.Equal(t, f.Size, virtualFile.FileBacking.VirtualizedSize.Load()) + checkBackingSize(t, d) + d.mu.Unlock() + + // Will cause the virtual sstables to be deleted, and the file backing should + // also be removed. + d.Compact([]byte{'a'}, []byte{'z'}, false) + + d.mu.Lock() + virtualFile = nil + it = d.mu.versions.currentVersion().Levels[0].Iter() + for f := it.First(); f != nil; f = it.Next() { + if f.Virtual { + virtualFile = f + break + } + } + require.Nil(t, virtualFile) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + require.Equal(t, 0, len(d.mu.versions.backingState.fileBackingMap)) + checkBackingSize(t, d) + d.mu.Unlock() + + // Close and restart to make sure that the new snapshot written during + // compaction doesn't have the file backing. + require.NoError(t, d.Close()) + d, err = Open("", opts) + require.NoError(t, err) + + d.mu.Lock() + virtualFile = nil + it = d.mu.versions.currentVersion().Levels[0].Iter() + for f := it.First(); f != nil; f = it.Next() { + if f.Virtual { + virtualFile = f + break + } + } + require.Nil(t, virtualFile) + require.Equal(t, 0, len(d.mu.versions.obsoleteTables)) + require.Equal(t, 0, len(d.mu.versions.backingState.fileBackingMap)) + checkBackingSize(t, d) + d.mu.Unlock() + require.NoError(t, d.Close()) +} + +func TestVersionSetCheckpoint(t *testing.T) { + mem := vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + + opts := &Options{ + FS: mem, + MaxManifestFileSize: 1, + } + d, err := Open("", opts) + require.NoError(t, err) + + // Multiple manifest files are created such that the latest one must have a correct snapshot + // of the preceding state for the DB to be opened correctly and see the written data. + // Snapshot has no files, so first edit will cause manifest rotation. + writeAndIngest(t, mem, d, base.MakeInternalKey([]byte("a"), 0, InternalKeyKindSet), []byte("b"), "a") + // Snapshot has no files, and manifest has an edit from the previous ingest, + // so this second ingest will cause manifest rotation. + writeAndIngest(t, mem, d, base.MakeInternalKey([]byte("c"), 0, InternalKeyKindSet), []byte("d"), "c") + require.NoError(t, d.Close()) + d, err = Open("", opts) + require.NoError(t, err) + checkValue := func(k string, expected string) { + v, closer, err := d.Get([]byte(k)) + require.NoError(t, err) + require.Equal(t, expected, string(v)) + closer.Close() + } + checkValue("a", "b") + checkValue("c", "d") + require.NoError(t, d.Close()) +} + +func TestVersionSetSeqNums(t *testing.T) { + mem := vfs.NewMem() + require.NoError(t, mem.MkdirAll("ext", 0755)) + + opts := &Options{ + FS: mem, + MaxManifestFileSize: 1, + } + d, err := Open("", opts) + require.NoError(t, err) + + // Snapshot has no files, so first edit will cause manifest rotation. + writeAndIngest(t, mem, d, base.MakeInternalKey([]byte("a"), 0, InternalKeyKindSet), []byte("b"), "a") + // Snapshot has no files, and manifest has an edit from the previous ingest, + // so this second ingest will cause manifest rotation. + writeAndIngest(t, mem, d, base.MakeInternalKey([]byte("c"), 0, InternalKeyKindSet), []byte("d"), "c") + require.NoError(t, d.Close()) + d, err = Open("", opts) + require.NoError(t, err) + defer d.Close() + d.TestOnlyWaitForCleaning() + + // Check that the manifest has the correct LastSeqNum, equalling the highest + // observed SeqNum. + filenames, err := mem.List("") + require.NoError(t, err) + var manifest vfs.File + for _, filename := range filenames { + fileType, _, ok := base.ParseFilename(mem, filename) + if ok && fileType == fileTypeManifest { + manifest, err = mem.Open(filename) + require.NoError(t, err) + } + } + require.NotNil(t, manifest) + defer manifest.Close() + rr := record.NewReader(manifest, 0 /* logNum */) + lastSeqNum := uint64(0) + for { + r, err := rr.Next() + if err == io.EOF { + break + } + require.NoError(t, err) + var ve versionEdit + err = ve.Decode(r) + require.NoError(t, err) + if ve.LastSeqNum != 0 { + lastSeqNum = ve.LastSeqNum + } + } + // 2 ingestions happened, so LastSeqNum should equal base.SeqNumStart + 1. + require.Equal(t, uint64(11), lastSeqNum) + // logSeqNum is always one greater than the last assigned sequence number. + require.Equal(t, d.mu.versions.logSeqNum.Load(), lastSeqNum+1) +} diff --git a/pebble/vfs/atomicfs/marker.go b/pebble/vfs/atomicfs/marker.go new file mode 100644 index 0000000..6bdc506 --- /dev/null +++ b/pebble/vfs/atomicfs/marker.go @@ -0,0 +1,241 @@ +// Copyright 2021 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 atomicfs + +import ( + "fmt" + "strconv" + "strings" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/vfs" +) + +// ReadMarker looks up the current state of a marker returning just the +// current value of the marker. Callers that may need to move the marker +// to a new value should use LocateMarker. +func ReadMarker(fs vfs.FS, dir, markerName string) (string, error) { + state, err := scanForMarker(fs, dir, markerName) + if err != nil { + return "", err + } + return state.value, nil +} + +// LocateMarker loads the current state of a marker. It returns a handle +// to the Marker that may be used to move the marker and the +// current value of the marker. +func LocateMarker(fs vfs.FS, dir, markerName string) (*Marker, string, error) { + state, err := scanForMarker(fs, dir, markerName) + if err != nil { + return nil, "", err + } + dirFD, err := fs.OpenDir(dir) + if err != nil { + return nil, "", err + } + return &Marker{ + fs: fs, + dir: dir, + dirFD: dirFD, + name: markerName, + filename: state.filename, + iter: state.iter, + obsoleteFiles: state.obsolete, + }, state.value, nil +} + +type scannedState struct { + // filename is the latest marker file found (the one with the highest iter value). + filename string + iter uint64 + value string + // obsolete is a list of earlier markers that were found. + obsolete []string +} + +func scanForMarker(fs vfs.FS, dir, markerName string) (scannedState, error) { + ls, err := fs.List(dir) + if err != nil { + return scannedState{}, err + } + var state scannedState + for _, filename := range ls { + if !strings.HasPrefix(filename, `marker.`) { + continue + } + // Any filenames with the `marker.` prefix are required to be + // well-formed and parse as markers. + name, iter, value, err := parseMarkerFilename(filename) + if err != nil { + return scannedState{}, err + } + if name != markerName { + continue + } + + if state.filename == "" || state.iter < iter { + if state.filename != "" { + state.obsolete = append(state.obsolete, state.filename) + } + state.filename = filename + state.iter = iter + state.value = value + } else { + state.obsolete = append(state.obsolete, filename) + } + } + return state, nil +} + +// A Marker provides an interface for maintaining a single string value on the +// filesystem. The marker may be atomically moved from value to value. +// +// The implementation creates a new marker file for each new value, embedding +// the value in the marker filename. +// +// Marker is not safe for concurrent use. Multiple processes may not read or +// move the same marker simultaneously. A Marker may only be constructed through +// LocateMarker. +// +// Marker names must be unique within the directory. +type Marker struct { + fs vfs.FS + dir string + dirFD vfs.File + // name identifies the marker. + name string + // filename contains the entire filename of the current marker. It + // has a format of `marker...`. It's not + // necessarily in sync with iter, since filename is only updated + // when the marker is successfully moved. + filename string + // iter holds the current iteration value. It matches the iteration + // value encoded within filename, if filename is non-empty. Iter is + // monotonically increasing over the lifetime of a marker. Actual + // marker files will always have a positive iter value. + iter uint64 + // obsoleteFiles holds a list of marker files discovered by LocateMarker that + // are old values for this marker. These files may exist in certain error + // cases or crashes (e.g. if the deletion of the previous marker file failed + // during Move). + obsoleteFiles []string +} + +func markerFilename(name string, iter uint64, value string) string { + return fmt.Sprintf("marker.%s.%06d.%s", name, iter, value) +} + +func parseMarkerFilename(s string) (name string, iter uint64, value string, err error) { + // Check for and remove the `marker.` prefix. + if !strings.HasPrefix(s, `marker.`) { + return "", 0, "", errors.Newf("invalid marker filename: %q", s) + } + s = s[len(`marker.`):] + + // Extract the marker's name. + i := strings.IndexByte(s, '.') + if i == -1 { + return "", 0, "", errors.Newf("invalid marker filename: %q", s) + } + name = s[:i] + s = s[i+1:] + + // Extract the marker's iteration number. + i = strings.IndexByte(s, '.') + if i == -1 { + return "", 0, "", errors.Newf("invalid marker filename: %q", s) + } + iter, err = strconv.ParseUint(s[:i], 10, 64) + if err != nil { + return "", 0, "", errors.Newf("invalid marker filename: %q", s) + } + + // Everything after the iteration's `.` delimiter is the value. + s = s[i+1:] + + return name, iter, s, nil +} + +// Close releases all resources in use by the marker. +func (a *Marker) Close() error { + return a.dirFD.Close() +} + +// Move atomically moves the marker to a new value. +// +// If Move returns a nil error, the new marker value is guaranteed to be +// persisted to stable storage. If Move returns an error, the current +// value of the marker may be the old value or the new value. Callers +// may retry a Move error. +// +// If an error occurs while syncing the directory, Move panics. +func (a *Marker) Move(newValue string) error { + a.iter++ + dstFilename := markerFilename(a.name, a.iter, newValue) + dstPath := a.fs.PathJoin(a.dir, dstFilename) + oldFilename := a.filename + + // Create the new marker. + f, err := a.fs.Create(dstPath) + if err != nil { + // On a distributed filesystem, an error doesn't guarantee that + // the file wasn't created. A retry of the same Move call will + // use a new iteration value, and try to a create a new file. If + // the errored invocation was actually successful in creating + // the file, we'll leak a file. That's okay, because the next + // time the marker is located we'll add it to the obsolete files + // list. + // + // Note that the unconditional increment of `a.iter` means that + // `a.iter` and `a.filename` are not necessarily in sync, + // because `a.filename` is only updated on success. + return err + } + a.filename = dstFilename + if err := f.Close(); err != nil { + return err + } + + // Remove the now defunct file. If an error is surfaced, we record + // the file as an obsolete file. The file's presence does not + // affect correctness, and it will be cleaned up the next time + // RemoveObsolete is called, either by this process or the next. + if oldFilename != "" { + if err := a.fs.Remove(a.fs.PathJoin(a.dir, oldFilename)); err != nil && !oserror.IsNotExist(err) { + a.obsoleteFiles = append(a.obsoleteFiles, oldFilename) + } + } + + // Sync the directory to ensure marker movement is synced. + if err := a.dirFD.Sync(); err != nil { + // Fsync errors are unrecoverable. + // See https://wiki.postgresql.org/wiki/Fsync_Errors and + // https://danluu.com/fsyncgate. + panic(errors.WithStack(err)) + } + return nil +} + +// NextIter returns the next iteration number that the marker will use. +// Clients may use this number for formulating new values that are +// unused. +func (a *Marker) NextIter() uint64 { + return a.iter + 1 +} + +// RemoveObsolete removes any obsolete files discovered while locating +// the marker or files unable to be removed during Move. +func (a *Marker) RemoveObsolete() error { + for i, filename := range a.obsoleteFiles { + if err := a.fs.Remove(a.fs.PathJoin(a.dir, filename)); err != nil && !oserror.IsNotExist(err) { + a.obsoleteFiles = a.obsoleteFiles[i:] + return err + } + } + a.obsoleteFiles = nil + return nil +} diff --git a/pebble/vfs/atomicfs/marker_test.go b/pebble/vfs/atomicfs/marker_test.go new file mode 100644 index 0000000..f43c90d --- /dev/null +++ b/pebble/vfs/atomicfs/marker_test.go @@ -0,0 +1,295 @@ +// Copyright 2021 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 atomicfs + +import ( + "bytes" + "fmt" + "os" + "sort" + "strconv" + "strings" + "sync/atomic" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/vfs" + "github.com/cockroachdb/pebble/vfs/errorfs" + "github.com/stretchr/testify/require" +) + +func TestMarker_FilenameRoundtrip(t *testing.T) { + filenames := []string{ + "marker.foo.000003.MANIFEST-000021", + "marker.bar.000003.MANIFEST-000021", + "marker.version.000003.1", + "marker.version.000003.1.2.3.4", + "marker.current.500000.MANIFEST-000001", + "marker.current.18446744073709551615.MANIFEST-000001", + } + for _, testFilename := range filenames { + t.Run(testFilename, func(t *testing.T) { + name, iter, value, err := parseMarkerFilename(testFilename) + require.NoError(t, err) + + filename := markerFilename(name, iter, value) + require.Equal(t, testFilename, filename) + }) + } +} + +func TestMarker_Parsefilename(t *testing.T) { + testCases := map[string]func(require.TestingT, error, ...interface{}){ + "marker.current.000003.MANIFEST-000021": require.NoError, + "marker.current.10.MANIFEST-000021": require.NoError, + "marker.v.10.1.2.3.4": require.NoError, + "marker.name.18446744073709551615.value": require.NoError, + "marke.current.000003.MANIFEST-000021": require.Error, + "marker.current.foo.MANIFEST-000021": require.Error, + "marker.current.ffffff.MANIFEST-000021": require.Error, + } + for filename, assert := range testCases { + t.Run(filename, func(t *testing.T) { + _, _, _, err := parseMarkerFilename(filename) + assert(t, err) + }) + } +} + +func TestMarker(t *testing.T) { + markers := map[string]*Marker{} + memFS := vfs.NewMem() + + var buf bytes.Buffer + datadriven.RunTest(t, "testdata/marker", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "list": + ls, err := memFS.List(td.CmdArgs[0].String()) + if err != nil { + return err.Error() + } + sort.Strings(ls) + buf.Reset() + for _, filename := range ls { + fmt.Fprintln(&buf, filename) + } + return buf.String() + + case "locate": + var dir, marker string + td.ScanArgs(t, "dir", &dir) + td.ScanArgs(t, "marker", &marker) + m, v, err := LocateMarker(memFS, dir, marker) + if err != nil { + return err.Error() + } + p := memFS.PathJoin(dir, marker) + if oldMarker := markers[p]; oldMarker != nil { + if err := oldMarker.Close(); err != nil { + return err.Error() + } + } + + markers[p] = m + return v + + case "mkdir-all": + if len(td.CmdArgs) != 1 { + return "usage: mkdir-all " + } + if err := memFS.MkdirAll(td.CmdArgs[0].String(), os.ModePerm); err != nil { + return err.Error() + } + return "" + + case "move": + var dir, marker string + td.ScanArgs(t, "dir", &dir) + td.ScanArgs(t, "marker", &marker) + m := markers[memFS.PathJoin(dir, marker)] + require.NotNil(t, m) + err := m.Move(td.Input) + if err != nil { + return err.Error() + } + return "" + + case "next-iter": + var dir, marker string + td.ScanArgs(t, "dir", &dir) + td.ScanArgs(t, "marker", &marker) + m := markers[memFS.PathJoin(dir, marker)] + require.NotNil(t, m) + return fmt.Sprintf("%d", m.NextIter()) + + case "read": + var dir, marker string + td.ScanArgs(t, "dir", &dir) + td.ScanArgs(t, "marker", &marker) + v, err := ReadMarker(memFS, dir, marker) + if err != nil { + return err.Error() + } + return v + + case "remove-obsolete": + var dir, marker string + td.ScanArgs(t, "dir", &dir) + td.ScanArgs(t, "marker", &marker) + m := markers[memFS.PathJoin(dir, marker)] + require.NotNil(t, m) + obsoleteCount := len(m.obsoleteFiles) + require.NoError(t, m.RemoveObsolete()) + removedCount := obsoleteCount - len(m.obsoleteFiles) + return fmt.Sprintf("Removed %d files.", removedCount) + + case "touch": + for _, filename := range strings.Split(td.Input, "\n") { + f, err := memFS.Create(filename) + if err != nil { + return err.Error() + } + if err := f.Close(); err != nil { + return err.Error() + } + } + return "" + + default: + panic(fmt.Sprintf("unknown command %q", td.Cmd)) + } + }) +} + +func TestMarker_StrictSync(t *testing.T) { + // Use an in-memory FS that strictly enforces syncs. + mem := vfs.NewStrictMem() + syncDir := func(dir string) { + fdir, err := mem.OpenDir(dir) + require.NoError(t, err) + require.NoError(t, fdir.Sync()) + require.NoError(t, fdir.Close()) + } + + require.NoError(t, mem.MkdirAll("foo", os.ModePerm)) + syncDir("") + m, v, err := LocateMarker(mem, "foo", "bar") + require.NoError(t, err) + require.Equal(t, "", v) + require.NoError(t, m.Move("hello")) + require.NoError(t, m.Close()) + + // Discard any unsynced writes to make sure we set up the test + // preconditions correctly. + mem.ResetToSyncedState() + m, v, err = LocateMarker(mem, "foo", "bar") + require.NoError(t, err) + require.Equal(t, "hello", v) + require.NoError(t, m.Move("hello-world")) + require.NoError(t, m.Close()) + + // Discard any unsynced writes. + mem.ResetToSyncedState() + m, v, err = LocateMarker(mem, "foo", "bar") + require.NoError(t, err) + require.Equal(t, "hello-world", v) + require.NoError(t, m.Close()) +} + +// TestMarker_FaultTolerance attempts a series of operations on atomic +// markers, injecting errors at successively higher indexed operations. +// It completes when an error is never injected, because the index is +// higher than the number of filesystem operations performed by the +// test. +func TestMarker_FaultTolerance(t *testing.T) { + done := false + for i := 1; !done && i < 1000; i++ { + t.Run(strconv.Itoa(i), func(t *testing.T) { + var count atomic.Int32 + count.Store(int32(i)) + inj := errorfs.InjectorFunc(func(op errorfs.Op) error { + // Don't inject on Sync errors. They're fatal. + if op.Kind == errorfs.OpFileSync { + return nil + } + if v := count.Add(-1); v == 0 { + return errorfs.ErrInjected + } + return nil + }) + + mem := vfs.NewMem() + fs := errorfs.Wrap(mem, inj) + markers := map[string]*Marker{} + ops := []struct { + op string + name string + value string + }{ + {op: "locate", name: "foo", value: ""}, + {op: "locate", name: "foo", value: ""}, + {op: "locate", name: "bar", value: ""}, + {op: "rm-obsolete", name: "foo"}, + {op: "move", name: "bar", value: "california"}, + {op: "rm-obsolete", name: "bar"}, + {op: "move", name: "bar", value: "california"}, + {op: "move", name: "bar", value: "new-york"}, + {op: "locate", name: "bar", value: "new-york"}, + {op: "move", name: "bar", value: "california"}, + {op: "rm-obsolete", name: "bar"}, + {op: "locate", name: "bar", value: "california"}, + {op: "move", name: "foo", value: "connecticut"}, + {op: "locate", name: "foo", value: "connecticut"}, + } + + for _, op := range ops { + runOp := func() error { + switch op.op { + case "locate": + m, v, err := LocateMarker(fs, "", op.name) + if err != nil { + return err + } + require.NotNil(t, m) + require.Equal(t, op.value, v) + if existingMarker := markers[op.name]; existingMarker != nil { + require.NoError(t, existingMarker.Close()) + } + markers[op.name] = m + return nil + case "move": + m := markers[op.name] + require.NotNil(t, m) + return m.Move(op.value) + case "rm-obsolete": + m := markers[op.name] + require.NotNil(t, m) + return m.RemoveObsolete() + default: + panic("unreachable") + } + } + + // Run the operation, if it fails with the injected + // error, retry it exactly once. The retry should always + // succeed. + err := runOp() + if errors.Is(err, errorfs.ErrInjected) { + err = runOp() + } + require.NoError(t, err) + } + + for _, m := range markers { + require.NoError(t, m.Close()) + } + + // Stop if the number of operations in the test case is + // fewer than `i`. + done = count.Load() > 0 + }) + } +} diff --git a/pebble/vfs/atomicfs/testdata/marker b/pebble/vfs/atomicfs/testdata/marker new file mode 100644 index 0000000..4e50b44 --- /dev/null +++ b/pebble/vfs/atomicfs/testdata/marker @@ -0,0 +1,136 @@ +# Errors if the containing directory does not exist. +locate dir=bar marker=foo +---- +open bar/: file does not exist + +mkdir-all data +---- + +read dir=data marker=foo +---- + +# Loads a nonexistent marker correctly. +locate dir=data marker=foo +---- + +next-iter dir=data marker=foo +---- +1 + +next-iter dir=data marker=foo +---- +1 + +# The directory should still be empty. +list data +---- + +# Moving the marker for the first time should create a marker file. +move dir=data marker=foo +MANIFEST-000010 +---- + +list data +---- +marker.foo.000001.MANIFEST-000010 + +next-iter dir=data marker=foo +---- +2 + +read dir=data marker=foo +---- +MANIFEST-000010 + +# Moving the marker should move the existing marker file. +move dir=data marker=foo +MANIFEST-000016 +---- + +next-iter dir=data marker=foo +---- +3 + +list data +---- +marker.foo.000002.MANIFEST-000016 + +read dir=data marker=foo +---- +MANIFEST-000016 + +# Create non-marker files. +touch +data/MANIFEST-000016 +data/CURRENT +data/000004.sst +---- + +# Re-locate the marker. It should be unchanged. +locate dir=data marker=foo +---- +MANIFEST-000016 + +# Locate a new marker. +locate dir=data marker=bar +---- + +move dir=data marker=bar +MANIFEST-000016 +---- + +list data +---- +000004.sst +CURRENT +MANIFEST-000016 +marker.bar.000001.MANIFEST-000016 +marker.foo.000002.MANIFEST-000016 + +move dir=data marker=foo +MANIFEST-000021 +---- + +list data +---- +000004.sst +CURRENT +MANIFEST-000016 +marker.bar.000001.MANIFEST-000016 +marker.foo.000003.MANIFEST-000021 + +touch +data/marker.bar.000009.MANIFEST-000099 +---- + +list data +---- +000004.sst +CURRENT +MANIFEST-000016 +marker.bar.000001.MANIFEST-000016 +marker.bar.000009.MANIFEST-000099 +marker.foo.000003.MANIFEST-000021 + +locate dir=data marker=bar +---- +MANIFEST-000099 + +remove-obsolete dir=data marker=bar +---- +Removed 1 files. + +list data +---- +000004.sst +CURRENT +MANIFEST-000016 +marker.bar.000009.MANIFEST-000099 +marker.foo.000003.MANIFEST-000021 + +read dir=data marker=bar +---- +MANIFEST-000099 + +read dir=data marker=garbage +---- diff --git a/pebble/vfs/clone.go b/pebble/vfs/clone.go new file mode 100644 index 0000000..5d6edf2 --- /dev/null +++ b/pebble/vfs/clone.go @@ -0,0 +1,137 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package vfs + +import ( + "io" + "sort" + + "github.com/cockroachdb/errors/oserror" +) + +type cloneOpts struct { + skip func(string) bool + sync bool + tryLink bool +} + +// A CloneOption configures the behavior of Clone. +type CloneOption func(*cloneOpts) + +// CloneSkip configures Clone to skip files for which the provided function +// returns true when passed the file's path. +func CloneSkip(fn func(string) bool) CloneOption { + return func(co *cloneOpts) { co.skip = fn } +} + +// CloneSync configures Clone to sync files and directories. +var CloneSync CloneOption = func(o *cloneOpts) { o.sync = true } + +// CloneTryLink configures Clone to link files to the destination if the source and +// destination filesystems are the same. If the source and destination +// filesystems are not the same or the filesystem does not support linking, then +// Clone falls back to copying. +var CloneTryLink CloneOption = func(o *cloneOpts) { o.tryLink = true } + +// Clone recursively copies a directory structure from srcFS to dstFS. srcPath +// specifies the path in srcFS to copy from and must be compatible with the +// srcFS path format. dstDir is the target directory in dstFS and must be +// compatible with the dstFS path format. Returns (true,nil) on a successful +// copy, (false,nil) if srcPath does not exist, and (false,err) if an error +// occurred. +func Clone(srcFS, dstFS FS, srcPath, dstPath string, opts ...CloneOption) (bool, error) { + var o cloneOpts + for _, opt := range opts { + opt(&o) + } + + srcFile, err := srcFS.Open(srcPath) + if err != nil { + if oserror.IsNotExist(err) { + // Ignore non-existent errors. Those will translate into non-existent + // files in the destination filesystem. + return false, nil + } + return false, err + } + defer srcFile.Close() + + stat, err := srcFile.Stat() + if err != nil { + return false, err + } + + if stat.IsDir() { + if err := dstFS.MkdirAll(dstPath, 0755); err != nil { + return false, err + } + list, err := srcFS.List(srcPath) + if err != nil { + return false, err + } + // Sort the paths so we get deterministic test output. + sort.Strings(list) + for _, name := range list { + if o.skip != nil && o.skip(srcFS.PathJoin(srcPath, name)) { + continue + } + _, err := Clone(srcFS, dstFS, srcFS.PathJoin(srcPath, name), dstFS.PathJoin(dstPath, name), opts...) + if err != nil { + return false, err + } + } + + if o.sync { + dir, err := dstFS.OpenDir(dstPath) + if err != nil { + return false, err + } + if err := dir.Sync(); err != nil { + return false, err + } + if err := dir.Close(); err != nil { + return false, err + } + } + + return true, nil + } + + // If the source and destination filesystems are the same and the user + // specified they'd prefer to link if possible, try to use a hardlink, + // falling back to copying if it fails. + if srcFS == dstFS && o.tryLink { + if err := LinkOrCopy(srcFS, srcPath, dstPath); oserror.IsNotExist(err) { + // Clone's semantics are such that it returns (false,nil) if the + // source does not exist. + return false, nil + } else if err != nil { + return false, err + } else { + return true, nil + } + } + + data, err := io.ReadAll(srcFile) + if err != nil { + return false, err + } + dstFile, err := dstFS.Create(dstPath) + if err != nil { + return false, err + } + if _, err = dstFile.Write(data); err != nil { + return false, err + } + if o.sync { + if err := dstFile.Sync(); err != nil { + return false, err + } + } + if err := dstFile.Close(); err != nil { + return false, err + } + return true, nil +} diff --git a/pebble/vfs/default_linux.go b/pebble/vfs/default_linux.go new file mode 100644 index 0000000..ec29074 --- /dev/null +++ b/pebble/vfs/default_linux.go @@ -0,0 +1,132 @@ +// 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 +// +build linux + +package vfs + +import ( + "os" + "syscall" + + "github.com/cockroachdb/errors" + "golang.org/x/sys/unix" +) + +func wrapOSFileImpl(f *os.File) File { + lf := &linuxFile{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 &linuxDir{f}, nil +} + +// Assert that linuxFile and linuxDir implement vfs.File. +var ( + _ File = (*linuxDir)(nil) + _ File = (*linuxFile)(nil) +) + +type linuxDir struct { + *os.File +} + +func (d *linuxDir) Prefetch(offset int64, length int64) error { return nil } +func (d *linuxDir) Preallocate(offset, length int64) error { return nil } +func (d *linuxDir) SyncData() error { return d.Sync() } +func (d *linuxDir) SyncTo(offset int64) (fullSync bool, err error) { return false, nil } + +type linuxFile struct { + *os.File + fd uintptr + useSyncRange bool +} + +func (f *linuxFile) Prefetch(offset int64, length int64) error { + _, _, err := unix.Syscall(unix.SYS_READAHEAD, uintptr(f.fd), uintptr(offset), uintptr(length)) + return err +} + +func (f *linuxFile) Preallocate(offset, length int64) error { + return unix.Fallocate(int(f.fd), unix.FALLOC_FL_KEEP_SIZE, offset, length) +} + +func (f *linuxFile) SyncData() error { + return unix.Fdatasync(int(f.fd)) +} + +func (f *linuxFile) SyncTo(offset int64) (fullSync bool, err error) { + if !f.useSyncRange { + // Use fdatasync, which does provide persistence guarantees but won't + // update all file metadata. From the `fdatasync` man page: + // + // fdatasync() is similar to fsync(), but does not flush modified + // metadata unless that metadata is needed in order to allow a + // subsequent data retrieval to be correctly handled. For example, + // changes to st_atime or st_mtime (respectively, time of last access + // and time of last modification; see stat(2)) do not require flushing + // because they are not necessary for a subsequent data read to be + // handled correctly. On the other hand, a change to the file size + // (st_size, as made by say ftruncate(2)), would require a metadata + // flush. + if err = unix.Fdatasync(int(f.fd)); err != nil { + return false, err + } + return true, nil + } + + const ( + waitBefore = 0x1 + write = 0x2 + // waitAfter = 0x4 + ) + + // By specifying write|waitBefore for the flags, we're instructing + // SyncFileRange to a) wait for any outstanding data being written to finish, + // and b) to queue any other dirty data blocks in the range [0,offset] for + // writing. The actual writing of this data will occur asynchronously. The + // use of `waitBefore` is to limit how much dirty data is allowed to + // accumulate. Linux sometimes behaves poorly when a large amount of dirty + // data accumulates, impacting other I/O operations. + return false, unix.SyncFileRange(int(f.fd), 0, offset, write|waitBefore) +} + +type syncFileRange func(fd int, off int64, n int64, flags int) (err error) + +// sync_file_range depends on both the filesystem, and the broader kernel +// support. In particular, Windows Subsystem for Linux does not support +// sync_file_range, even when used with ext{2,3,4}. syncRangeSmokeTest performs +// a test of of sync_file_range, returning false on ENOSYS, and true otherwise. +func syncRangeSmokeTest(fd uintptr, syncFn syncFileRange) bool { + err := syncFn(int(fd), 0 /* offset */, 0 /* nbytes */, 0 /* flags */) + return err != unix.ENOSYS +} + +func isSyncRangeSupported(fd uintptr) bool { + var stat unix.Statfs_t + if err := unix.Fstatfs(int(fd), &stat); err != nil { + return false + } + + // Allowlist which filesystems we allow using sync_file_range with as some + // filesystems treat that syscall as a noop (notably ZFS). A allowlist is + // used instead of a denylist in order to have a more graceful failure mode + // in case a filesystem we haven't tested is encountered. Currently only + // ext2/3/4 are known to work properly. + const extMagic = 0xef53 + switch stat.Type { + case extMagic: + return syncRangeSmokeTest(fd, unix.SyncFileRange) + } + return false +} diff --git a/pebble/vfs/default_unix.go b/pebble/vfs/default_unix.go new file mode 100644 index 0000000..836185e --- /dev/null +++ b/pebble/vfs/default_unix.go @@ -0,0 +1,49 @@ +// 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 darwin || dragonfly || freebsd || netbsd || openbsd || solaris +// +build darwin dragonfly freebsd netbsd openbsd solaris + +package vfs + +import ( + "os" + "syscall" + + "github.com/cockroachdb/errors" +) + +func wrapOSFileImpl(osFile *os.File) File { + return &unixFile{File: osFile, fd: osFile.Fd()} +} + +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 &unixFile{f, InvalidFd}, nil +} + +// Assert that unixFile implements vfs.File. +var _ File = (*unixFile)(nil) + +type unixFile struct { + *os.File + fd uintptr +} + +func (*unixFile) Prefetch(offset int64, length int64) error { return nil } +func (*unixFile) Preallocate(offset, length int64) error { return nil } + +func (f *unixFile) SyncData() error { + return f.Sync() +} + +func (f *unixFile) SyncTo(int64) (fullSync bool, err error) { + if err = f.Sync(); err != nil { + return false, err + } + return true, nil +} diff --git a/pebble/vfs/default_windows.go b/pebble/vfs/default_windows.go new file mode 100644 index 0000000..f314193 --- /dev/null +++ b/pebble/vfs/default_windows.go @@ -0,0 +1,61 @@ +// 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 windows +// +build windows + +package vfs + +import ( + "os" + "syscall" + + "github.com/cockroachdb/errors" +) + +func wrapOSFileImpl(f *os.File) File { + return &windowsFile{f} +} + +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 &windowsDir{f}, nil +} + +// Assert that windowsFile and windowsDir implement vfs.File. +var ( + _ File = (*windowsFile)(nil) + _ File = (*windowsDir)(nil) +) + +type windowsDir struct { + *os.File +} + +func (*windowsDir) Prefetch(offset int64, length int64) error { return nil } +func (*windowsDir) Preallocate(off, length int64) error { return nil } + +// Silently ignore Sync() on Windows. This is the same behavior as +// RocksDB. See port/win/io_win.cc:WinDirectory::Fsync(). +func (*windowsDir) Sync() error { return nil } +func (*windowsDir) SyncData() error { return nil } +func (*windowsDir) SyncTo(length int64) (fullSync bool, err error) { return false, nil } + +type windowsFile struct { + *os.File +} + +func (*windowsFile) Prefetch(offset int64, length int64) error { return nil } +func (*windowsFile) Preallocate(offset, length int64) error { return nil } + +func (f *windowsFile) SyncData() error { return f.Sync() } +func (f *windowsFile) SyncTo(length int64) (fullSync bool, err error) { + if err = f.Sync(); err != nil { + return false, err + } + return true, nil +} diff --git a/pebble/vfs/disk_full.go b/pebble/vfs/disk_full.go new file mode 100644 index 0000000..16f6a5d --- /dev/null +++ b/pebble/vfs/disk_full.go @@ -0,0 +1,453 @@ +// Copyright 2021 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 vfs + +import ( + "io" + "os" + "sync" + "sync/atomic" + "syscall" + + "github.com/cockroachdb/errors" +) + +// OnDiskFull wraps the provided FS with an FS that examines returned errors, +// looking for ENOSPC errors. It invokes the provided callback when the +// underlying filesystem returns an error signifying the storage is out of +// disk space. +// +// All new writes to the filesystem are blocked while the callback executes, +// so care must be taken to avoid expensive work from within the callback. +// +// Once the callback completes, any write-oriented operations that encountered +// ENOSPC are retried exactly once. Once the callback completes, it will not +// be invoked again until a new operation that began after the callback +// returned encounters an ENOSPC error. +// +// OnDiskFull may be used to automatically manage a ballast file, which is +// removed from the filesystem from within the callback. Note that if managing +// a ballast, the caller should maintain a reference to the inner FS and +// remove the ballast on the unwrapped FS. +func OnDiskFull(fs FS, fn func()) FS { + newFS := &enospcFS{inner: fs} + newFS.mu.Cond.L = &newFS.mu.Mutex + newFS.mu.onDiskFull = fn + return newFS +} + +type enospcFS 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() + } +} + +// Unwrap returns the underlying FS. This may be called by vfs.Root to access +// the underlying filesystem. +func (fs *enospcFS) Unwrap() FS { + return fs.inner +} + +// waitUntilReady is called before every FS or File operation that +// might return ENOSPC. If an ENOSPC was encountered and the corresponding +// invocation of the `onDiskFull` callback has not yet returned, +// waitUntilReady blocks until the callback returns. The returned generation +// is always even. +func (fs *enospcFS) waitUntilReady() uint32 { + 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. + return gen + } + + // We're currently handling an ENOSPC error. Wait on the condition + // variable until we're not handling an ENOSPC. + fs.mu.Lock() + defer fs.mu.Unlock() + + // Load the generation again with fs.mu locked. + gen = fs.generation.Load() + for gen%2 == 1 { + fs.mu.Wait() + gen = fs.generation.Load() + } + return gen +} + +func (fs *enospcFS) handleENOSPC(gen uint32) { + fs.mu.Lock() + defer fs.mu.Unlock() + + 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 + // responsible for invoking the callback. + if currentGeneration == gen { + // 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. + fs.generation.Store(gen + 1) + + func() { + // Drop the mutex while we invoke the callback, re-acquiring + // afterwards. + fs.mu.Unlock() + defer fs.mu.Lock() + fs.mu.onDiskFull() + }() + + // Update the current generation again to an even number, indicating + // that the callback has completed for the write generation `gen`. + fs.generation.Store(gen + 2) + fs.mu.Broadcast() + return + } + + // The current generation has already been incremented, so either the + // callback is currently being run by another goroutine or it's already + // completed. Wait for it complete if it hasn't already. + // + // The current generation may be updated multiple times, including to an + // odd number signifying a later write generation has already encountered + // ENOSPC. In that case, the callback was not able to remedy the full disk + // and waiting is unlikely to be helpful. Continuing to wait risks + // blocking an unbounded number of generations. Retrying and bubbling the + // ENOSPC up might be helpful if we can abort a large compaction that + // started before we became more selective about compaction picking, so + // this loop only waits for this write generation's callback and no + // subsequent generations' callbacks. + for currentGeneration == gen+1 { + fs.mu.Wait() + currentGeneration = fs.generation.Load() + } +} + +func (fs *enospcFS) Create(name string) (File, error) { + gen := fs.waitUntilReady() + + f, err := fs.inner.Create(name) + + if err != nil && isENOSPC(err) { + fs.handleENOSPC(gen) + f, err = fs.inner.Create(name) + } + if f != nil { + f = &enospcFile{ + fs: fs, + inner: f, + } + } + return f, err +} + +func (fs *enospcFS) Link(oldname, newname string) error { + gen := fs.waitUntilReady() + + err := fs.inner.Link(oldname, newname) + + if err != nil && isENOSPC(err) { + fs.handleENOSPC(gen) + err = fs.inner.Link(oldname, newname) + } + return err +} + +func (fs *enospcFS) Open(name string, opts ...OpenOption) (File, error) { + f, err := fs.inner.Open(name, opts...) + if f != nil { + f = &enospcFile{ + fs: fs, + inner: f, + } + } + 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 { + f = &enospcFile{ + fs: fs, + inner: f, + } + } + return f, err +} + +func (fs *enospcFS) Remove(name string) error { + gen := fs.waitUntilReady() + + err := fs.inner.Remove(name) + + if err != nil && isENOSPC(err) { + fs.handleENOSPC(gen) + err = fs.inner.Remove(name) + } + return err +} + +func (fs *enospcFS) RemoveAll(name string) error { + gen := fs.waitUntilReady() + + err := fs.inner.RemoveAll(name) + + if err != nil && isENOSPC(err) { + fs.handleENOSPC(gen) + err = fs.inner.RemoveAll(name) + } + return err +} + +func (fs *enospcFS) Rename(oldname, newname string) error { + gen := fs.waitUntilReady() + + err := fs.inner.Rename(oldname, newname) + + if err != nil && isENOSPC(err) { + fs.handleENOSPC(gen) + err = fs.inner.Rename(oldname, newname) + } + return err +} + +func (fs *enospcFS) ReuseForWrite(oldname, newname string) (File, error) { + gen := fs.waitUntilReady() + + f, err := fs.inner.ReuseForWrite(oldname, newname) + + if err != nil && isENOSPC(err) { + fs.handleENOSPC(gen) + f, err = fs.inner.ReuseForWrite(oldname, newname) + } + + if f != nil { + f = &enospcFile{ + fs: fs, + inner: f, + } + } + return f, err +} + +func (fs *enospcFS) MkdirAll(dir string, perm os.FileMode) error { + gen := fs.waitUntilReady() + + err := fs.inner.MkdirAll(dir, perm) + + if err != nil && isENOSPC(err) { + fs.handleENOSPC(gen) + err = fs.inner.MkdirAll(dir, perm) + } + return err +} + +func (fs *enospcFS) Lock(name string) (io.Closer, error) { + gen := fs.waitUntilReady() + + closer, err := fs.inner.Lock(name) + + if err != nil && isENOSPC(err) { + fs.handleENOSPC(gen) + closer, err = fs.inner.Lock(name) + } + return closer, err +} + +func (fs *enospcFS) List(dir string) ([]string, error) { + return fs.inner.List(dir) +} + +func (fs *enospcFS) Stat(name string) (os.FileInfo, error) { + return fs.inner.Stat(name) +} + +func (fs *enospcFS) PathBase(path string) string { + return fs.inner.PathBase(path) +} + +func (fs *enospcFS) PathJoin(elem ...string) string { + return fs.inner.PathJoin(elem...) +} + +func (fs *enospcFS) PathDir(path string) string { + return fs.inner.PathDir(path) +} + +func (fs *enospcFS) GetDiskUsage(path string) (DiskUsage, error) { + return fs.inner.GetDiskUsage(path) +} + +type enospcFile struct { + fs *enospcFS + inner File +} + +var _ File = (*enospcFile)(nil) + +func (f *enospcFile) Close() error { + return f.inner.Close() +} + +func (f *enospcFile) Read(p []byte) (n int, err error) { + return f.inner.Read(p) +} + +func (f *enospcFile) ReadAt(p []byte, off int64) (n int, err error) { + return f.inner.ReadAt(p, off) +} + +func (f *enospcFile) Write(p []byte) (n int, err error) { + gen := f.fs.waitUntilReady() + + n, err = f.inner.Write(p) + + if err != nil && isENOSPC(err) { + f.fs.handleENOSPC(gen) + var n2 int + n2, err = f.inner.Write(p[n:]) + n += n2 + } + 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) +} + +func (f *enospcFile) Preallocate(offset, length int64) error { + return f.inner.Preallocate(offset, length) +} + +func (f *enospcFile) Stat() (os.FileInfo, error) { + return f.inner.Stat() +} + +func (f *enospcFile) Sync() error { + gen := f.fs.waitUntilReady() + + err := f.inner.Sync() + + if err != nil && isENOSPC(err) { + f.fs.handleENOSPC(gen) + + // NB: It is NOT safe to retry the Sync. See the PostgreSQL + // 'fsyncgate' discussion. A successful Sync after a failed one does + // not provide any guarantees and (always?) loses the unsynced writes. + // We need to bubble the error up and hope we weren't syncing a WAL or + // MANIFEST, because we'll have no choice but to crash. Errors while + // syncing an sstable will result in a failed flush/compaction, and + // the relevant sstable(s) will be marked as obsolete and deleted. + // See: https://lwn.net/Articles/752063/ + } + return err +} + +func (f *enospcFile) SyncData() error { + return f.inner.SyncData() +} + +func (f *enospcFile) SyncTo(length int64) (fullSync bool, err error) { + return f.inner.SyncTo(length) +} + +func (f *enospcFile) Fd() uintptr { + return f.inner.Fd() +} + +var _ FS = (*enospcFS)(nil) + +func isENOSPC(err error) bool { + err = errors.UnwrapAll(err) + e, ok := err.(syscall.Errno) + return ok && e == syscall.ENOSPC +} diff --git a/pebble/vfs/disk_full_test.go b/pebble/vfs/disk_full_test.go new file mode 100644 index 0000000..d56fc55 --- /dev/null +++ b/pebble/vfs/disk_full_test.go @@ -0,0 +1,254 @@ +// Copyright 2021 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 vfs + +import ( + "io" + "os" + "sync" + "sync/atomic" + "syscall" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" +) + +var filesystemWriteOps = map[string]func(FS) error{ + "Create": func(fs FS) error { + _, err := fs.Create("foo") + return err + }, + "Lock": func(fs FS) error { + _, err := fs.Lock("foo") + return err + }, + "ReuseForWrite": func(fs FS) error { + _, err := fs.ReuseForWrite("foo", "bar") + return err + }, + "Link": func(fs FS) error { return fs.Link("foo", "bar") }, + "MkdirAll": func(fs FS) error { return fs.MkdirAll("foo", os.ModePerm) }, + "Remove": func(fs FS) error { return fs.Remove("foo") }, + "RemoveAll": func(fs FS) error { return fs.RemoveAll("foo") }, + "Rename": func(fs FS) error { return fs.Rename("foo", "bar") }, +} + +func TestOnDiskFull_FS(t *testing.T) { + for name, fn := range filesystemWriteOps { + t.Run(name, func(t *testing.T) { + innerFS := &enospcMockFS{} + innerFS.enospcs.Store(1) + var callbackInvocations int + fs := OnDiskFull(innerFS, func() { + callbackInvocations++ + }) + + // Call this vfs.FS method on the wrapped filesystem. The first + // call should return ENOSPC. Our registered callback should be + // invoked, then the method should be retried and return a nil + // error. + require.NoError(t, fn(fs)) + require.Equal(t, 1, callbackInvocations) + // The inner filesystem should be invoked twice because of the + // retry. + require.Equal(t, uint32(2), innerFS.invocations.Load()) + }) + } +} + +func TestOnDiskFull_File(t *testing.T) { + t.Run("Write", func(t *testing.T) { + innerFS := &enospcMockFS{bytesWritten: 6} + var callbackInvocations int + fs := OnDiskFull(innerFS, func() { + callbackInvocations++ + }) + + f, err := fs.Create("foo") + require.NoError(t, err) + + // The next Write should ENOSPC. + innerFS.enospcs.Store(1) + + // Call the Write method on the wrapped file. The first call should return + // ENOSPC, but also that six bytes were written. Our registered callback + // should be invoked, then Write should be retried and return a nil error + // and five bytes written. + n, err := f.Write([]byte("hello world")) + require.NoError(t, err) + require.Equal(t, 11, n) + require.Equal(t, 1, callbackInvocations) + // The inner filesystem should be invoked 3 times. Once during Create + // and twice during Write. + require.Equal(t, uint32(3), innerFS.invocations.Load()) + }) + t.Run("Sync", func(t *testing.T) { + innerFS := &enospcMockFS{bytesWritten: 6} + var callbackInvocations int + fs := OnDiskFull(innerFS, func() { + callbackInvocations++ + }) + + f, err := fs.Create("foo") + require.NoError(t, err) + + // The next Sync should ENOSPC. The callback should be invoked, but a + // Sync cannot be retried. + innerFS.enospcs.Store(1) + + err = f.Sync() + require.Error(t, err) + require.Equal(t, 1, callbackInvocations) + // The inner filesystem should be invoked 2 times. Once during Create + // and once during Sync. + require.Equal(t, uint32(2), innerFS.invocations.Load()) + }) +} + +func TestOnDiskFull_Concurrent(t *testing.T) { + innerFS := &enospcMockFS{ + opDelay: 10 * time.Millisecond, + } + innerFS.enospcs.Store(10) + var callbackInvocations atomic.Int32 + fs := OnDiskFull(innerFS, func() { + callbackInvocations.Add(1) + }) + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _, err := fs.Create("foo") + // They all should succeed on retry. + require.NoError(t, err) + }() + } + wg.Wait() + // Since all operations should start before the first one returns an + // ENOSPC, the callback should only be invoked once. + require.Equal(t, int32(1), callbackInvocations.Load()) + require.Equal(t, uint32(20), innerFS.invocations.Load()) +} + +type enospcMockFS struct { + FS + opDelay time.Duration + bytesWritten int + enospcs atomic.Int32 + invocations atomic.Uint32 +} + +func (fs *enospcMockFS) maybeENOSPC() error { + fs.invocations.Add(1) + v := fs.enospcs.Add(-1) + + // Sleep before returning so that tests may issue concurrent writes that + // fall into the same write generation. + time.Sleep(fs.opDelay) + + if v >= 0 { + // Wrap the error to test error unwrapping. + err := &os.PathError{Op: "mock", Path: "mock", Err: syscall.ENOSPC} + return errors.Wrap(err, "uh oh") + } + return nil +} + +func (fs *enospcMockFS) Create(name string) (File, error) { + if err := fs.maybeENOSPC(); err != nil { + return nil, err + } + return &enospcMockFile{fs: fs}, nil +} + +func (fs *enospcMockFS) Link(oldname, newname string) error { + if err := fs.maybeENOSPC(); err != nil { + return err + } + return nil +} + +func (fs *enospcMockFS) Remove(name string) error { + if err := fs.maybeENOSPC(); err != nil { + return err + } + return nil +} + +func (fs *enospcMockFS) RemoveAll(name string) error { + if err := fs.maybeENOSPC(); err != nil { + return err + } + return nil +} + +func (fs *enospcMockFS) Rename(oldname, newname string) error { + if err := fs.maybeENOSPC(); err != nil { + return err + } + return nil +} + +func (fs *enospcMockFS) ReuseForWrite(oldname, newname string) (File, error) { + if err := fs.maybeENOSPC(); err != nil { + return nil, err + } + return &enospcMockFile{fs: fs}, nil +} + +func (fs *enospcMockFS) MkdirAll(dir string, perm os.FileMode) error { + if err := fs.maybeENOSPC(); err != nil { + return err + } + return nil +} + +func (fs *enospcMockFS) Lock(name string) (io.Closer, error) { + if err := fs.maybeENOSPC(); err != nil { + return nil, err + } + return nil, nil +} + +type enospcMockFile struct { + fs *enospcMockFS + File +} + +func (f *enospcMockFile) Write(b []byte) (int, error) { + + if err := f.fs.maybeENOSPC(); err != nil { + n := len(b) + if f.fs.bytesWritten < n { + n = f.fs.bytesWritten + } + return n, err + } + return len(b), nil +} + +func (f *enospcMockFile) Sync() error { + return f.fs.maybeENOSPC() +} + +// BenchmarkOnDiskFull benchmarks the overhead of the OnDiskFull filesystem +// wrapper during a Write when there is no ENOSPC. +func BenchmarkOnDiskFull(b *testing.B) { + fs := OnDiskFull(NewMem(), func() {}) + + f, err := fs.Create("foo") + require.NoError(b, err) + defer func() { require.NoError(b, f.Close()) }() + + payload := []byte("hello world") + for i := 0; i < b.N; i++ { + _, err := f.Write(payload) + require.NoError(b, err) + } +} diff --git a/pebble/vfs/disk_health.go b/pebble/vfs/disk_health.go new file mode 100644 index 0000000..3df4017 --- /dev/null +++ b/pebble/vfs/disk_health.go @@ -0,0 +1,806 @@ +// Copyright 2020 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 vfs + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/redact" +) + +const ( + // preallocatedSlotCount is the default number of slots available for + // concurrent filesystem operations. The slot count may be exceeded, but + // each additional slot will incur an additional allocation. We choose 16 + // here with the expectation that it is significantly more than required in + // practice. See the comment above the diskHealthCheckingFS type definition. + preallocatedSlotCount = 16 + // deltaBits is the number of bits in the packed 64-bit integer used for + // identifying a delta from the file creation time in milliseconds. + deltaBits = 40 + // writeSizeBits is the number of bits in the packed 64-bit integer used for + // identifying the size of the write operation, if the operation is sized. See + // writeSizePrecision below for precision of size. + writeSizeBits = 20 + // Track size of writes at kilobyte precision. See comment above lastWritePacked for more. + writeSizePrecision = 1024 +) + +// Variables to enable testing. +var ( + // defaultTickInterval is the default interval between two ticks of each + // diskHealthCheckingFile loop iteration. + defaultTickInterval = 2 * time.Second +) + +// OpType is the type of IO operation being monitored by a +// diskHealthCheckingFile. +type OpType uint8 + +// The following OpTypes is limited to the subset of file system operations that +// a diskHealthCheckingFile supports (namely writes and syncs). +const ( + OpTypeUnknown OpType = iota + OpTypeWrite + OpTypeSync + OpTypeSyncData + OpTypeSyncTo + OpTypeCreate + OpTypeLink + OpTypeMkdirAll + OpTypePreallocate + OpTypeRemove + OpTypeRemoveAll + OpTypeRename + OpTypeReuseForWrite + // Note: opTypeMax is just used in tests. It must appear last in the list + // of OpTypes. + opTypeMax +) + +// String implements fmt.Stringer. +func (o OpType) String() string { + switch o { + case OpTypeWrite: + return "write" + case OpTypeSync: + return "sync" + case OpTypeSyncData: + return "syncdata" + case OpTypeSyncTo: + return "syncto" + case OpTypeCreate: + return "create" + case OpTypeLink: + return "link" + case OpTypeMkdirAll: + return "mkdirall" + case OpTypePreallocate: + return "preallocate" + case OpTypeRemove: + return "remove" + case OpTypeRemoveAll: + return "removall" + case OpTypeRename: + return "rename" + case OpTypeReuseForWrite: + return "reuseforwrite" + case OpTypeUnknown: + return "unknown" + default: + panic(fmt.Sprintf("vfs: unknown op type: %d", o)) + } +} + +// diskHealthCheckingFile is a File wrapper to detect slow disk operations, and +// call onSlowDisk if a disk operation is seen to exceed diskSlowThreshold. +// +// This struct creates a goroutine (in startTicker()) that, at every tick +// interval, sees if there's a disk operation taking longer than the specified +// duration. This setup is preferable to creating a new timer at every disk +// operation, as it reduces overhead per disk operation. +type diskHealthCheckingFile struct { + file File + onSlowDisk func(opType OpType, writeSizeInBytes int, duration time.Duration) + diskSlowThreshold time.Duration + tickInterval time.Duration + + stopper chan struct{} + // lastWritePacked is a 64-bit unsigned int. The most significant + // 40 bits represent an delta (in milliseconds) from the creation + // time of the diskHealthCheckingFile. The next most significant 20 bits + // represent the size of the write in KBs, if the write has a size. (If + // it doesn't, the 20 bits are zeroed). The least significant four bits + // contains the OpType. + // + // The use of 40 bits for an delta provides ~34 years of effective + // monitoring time before the uint wraps around, at millisecond precision. + // ~34 years of process uptime "ought to be enough for anybody". Millisecond + // writeSizePrecision is sufficient, given that we are monitoring for writes that take + // longer than one millisecond. + // + // The use of 20 bits for the size in KBs allows representing sizes up + // to nearly one GB. If the write is larger than that, we round down to ~one GB. + // + // The use of four bits for OpType allows for 16 operation types. + // + // NB: this packing scheme is not persisted, and is therefore safe to adjust + // across process boundaries. + lastWritePacked atomic.Uint64 + createTimeNanos int64 +} + +// newDiskHealthCheckingFile instantiates a new diskHealthCheckingFile, with the +// specified time threshold and event listener. +func newDiskHealthCheckingFile( + file File, + diskSlowThreshold time.Duration, + onSlowDisk func(OpType OpType, writeSizeInBytes int, duration time.Duration), +) *diskHealthCheckingFile { + return &diskHealthCheckingFile{ + file: file, + onSlowDisk: onSlowDisk, + diskSlowThreshold: diskSlowThreshold, + tickInterval: defaultTickInterval, + + stopper: make(chan struct{}), + createTimeNanos: time.Now().UnixNano(), + } +} + +// startTicker starts a new goroutine with a ticker to monitor disk operations. +// Can only be called if the ticker goroutine isn't running already. +func (d *diskHealthCheckingFile) startTicker() { + if d.diskSlowThreshold == 0 { + return + } + + go func() { + ticker := time.NewTicker(d.tickInterval) + defer ticker.Stop() + + for { + select { + case <-d.stopper: + return + + case <-ticker.C: + packed := d.lastWritePacked.Load() + if packed == 0 { + continue + } + delta, writeSize, op := unpack(packed) + lastWrite := time.Unix(0, d.createTimeNanos+delta.Nanoseconds()) + now := time.Now() + if lastWrite.Add(d.diskSlowThreshold).Before(now) { + // diskSlowThreshold was exceeded. Call the passed-in + // listener. + d.onSlowDisk(op, writeSize, now.Sub(lastWrite)) + } + } + } + }() +} + +// stopTicker stops the goroutine started in startTicker. +func (d *diskHealthCheckingFile) stopTicker() { + close(d.stopper) +} + +// Fd implements (vfs.File).Fd. +func (d *diskHealthCheckingFile) Fd() uintptr { + return d.file.Fd() +} + +// Read implements (vfs.File).Read +func (d *diskHealthCheckingFile) Read(p []byte) (int, error) { + return d.file.Read(p) +} + +// ReadAt implements (vfs.File).ReadAt +func (d *diskHealthCheckingFile) ReadAt(p []byte, off int64) (int, error) { + return d.file.ReadAt(p, off) +} + +// Write implements the io.Writer interface. +func (d *diskHealthCheckingFile) Write(p []byte) (n int, err error) { + d.timeDiskOp(OpTypeWrite, int64(len(p)), func() { + n, err = d.file.Write(p) + }, time.Now().UnixNano()) + 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) + }, time.Now().UnixNano()) + return n, err +} + +// Close implements the io.Closer interface. +func (d *diskHealthCheckingFile) Close() error { + d.stopTicker() + return d.file.Close() +} + +// Prefetch implements (vfs.File).Prefetch. +func (d *diskHealthCheckingFile) Prefetch(offset, length int64) error { + return d.file.Prefetch(offset, length) +} + +// Preallocate implements (vfs.File).Preallocate. +func (d *diskHealthCheckingFile) Preallocate(off, n int64) (err error) { + d.timeDiskOp(OpTypePreallocate, n, func() { + err = d.file.Preallocate(off, n) + }, time.Now().UnixNano()) + return err +} + +// Stat implements (vfs.File).Stat. +func (d *diskHealthCheckingFile) Stat() (os.FileInfo, error) { + return d.file.Stat() +} + +// Sync implements the io.Syncer interface. +func (d *diskHealthCheckingFile) Sync() (err error) { + d.timeDiskOp(OpTypeSync, 0, func() { + err = d.file.Sync() + }, time.Now().UnixNano()) + return err +} + +// SyncData implements (vfs.File).SyncData. +func (d *diskHealthCheckingFile) SyncData() (err error) { + d.timeDiskOp(OpTypeSyncData, 0, func() { + err = d.file.SyncData() + }, time.Now().UnixNano()) + return err +} + +// SyncTo implements (vfs.File).SyncTo. +func (d *diskHealthCheckingFile) SyncTo(length int64) (fullSync bool, err error) { + d.timeDiskOp(OpTypeSyncTo, length, func() { + fullSync, err = d.file.SyncTo(length) + }, time.Now().UnixNano()) + return fullSync, err +} + +// timeDiskOp runs the specified closure and makes its timing visible to the +// monitoring goroutine, in case it exceeds one of the slow disk durations. +// opType should always be set. writeSizeInBytes should be set if the write +// operation is sized. If not, it should be set to zero. +// +// The start time is taken as a parameter in the form of nanoseconds since the +// unix epoch so that it appears in stack traces during crashes (if GOTRACEBACK +// is set appropriately), aiding postmortem debugging. +func (d *diskHealthCheckingFile) timeDiskOp( + opType OpType, writeSizeInBytes int64, op func(), startNanos int64, +) { + if d == nil { + op() + return + } + + delta := time.Duration(startNanos - d.createTimeNanos) + packed := pack(delta, writeSizeInBytes, opType) + if d.lastWritePacked.Swap(packed) != 0 { + panic("concurrent write operations detected on file") + } + defer func() { + if d.lastWritePacked.Swap(0) != packed { + panic("concurrent write operations detected on file") + } + }() + op() +} + +// Note the slight lack of symmetry between pack & unpack. pack takes an int64 for writeSizeInBytes, since +// callers of pack use an int64. This is dictated by the vfs interface. unpack OTOH returns an int. This is +// safe because the packing scheme implies we only actually need 32 bits. +func pack(delta time.Duration, writeSizeInBytes int64, opType OpType) uint64 { + // We have no guarantee of clock monotonicity. If we have a small regression + // in the clock, we set deltaMillis to zero, so we can still catch the operation + // if happens to be slow. + deltaMillis := delta.Milliseconds() + if deltaMillis < 0 { + deltaMillis = 0 + } + // As of 3/7/2023, the use of 40 bits for an delta provides ~34 years + // of effective monitoring time before the uint wraps around, at millisecond + // precision. + if deltaMillis > 1< writeSizeCeiling { + writeSize = writeSizeCeiling + } + + return uint64(deltaMillis)<<(64-deltaBits) | uint64(writeSize)<<(64-deltaBits-writeSizeBits) | uint64(opType) +} + +func unpack(packed uint64) (delta time.Duration, writeSizeInBytes int, opType OpType) { + delta = time.Duration(packed>>(64-deltaBits)) * time.Millisecond + wz := int64(packed>>(64-deltaBits-writeSizeBits)) & ((1 << writeSizeBits) - 1) * writeSizePrecision + // Given the packing scheme, converting wz to an int will not truncate anything. + writeSizeInBytes = int(wz) + opType = OpType(packed & 0xf) + return delta, writeSizeInBytes, opType +} + +// diskHealthCheckingDir implements disk-health checking for directories. Unlike +// other files, we allow directories to receive concurrent write operations +// (Syncs are the only write operations supported by a directory.) Since the +// diskHealthCheckingFile's timeDiskOp can only track a single in-flight +// operation at a time, we time the operation using the filesystem-level +// timeFilesystemOp function instead. +type diskHealthCheckingDir struct { + File + name string + fs *diskHealthCheckingFS +} + +// Sync implements the io.Syncer interface. +func (d *diskHealthCheckingDir) Sync() (err error) { + d.fs.timeFilesystemOp(d.name, OpTypeSync, func() { + err = d.File.Sync() + }, time.Now().UnixNano()) + return err +} + +// DiskSlowInfo captures info about detected slow operations on the vfs. +type DiskSlowInfo struct { + // Path of file being written to. + Path string + // Operation being performed on the file. + OpType OpType + // Size of write in bytes, if the write is sized. + WriteSize int + // Duration that has elapsed since this disk operation started. + Duration time.Duration +} + +func (i DiskSlowInfo) String() string { + return redact.StringWithoutMarkers(i) +} + +// SafeFormat implements redact.SafeFormatter. +func (i DiskSlowInfo) SafeFormat(w redact.SafePrinter, _ rune) { + 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. +// It times disk write operations in two ways: +// +// 1. Wrapping vfs.Files. +// +// The bulk of write I/O activity is file writing and syncing, invoked through +// the `vfs.File` interface. This VFS wraps all files open for writing with a +// special diskHealthCheckingFile implementation of the vfs.File interface. See +// above for the implementation. +// +// 2. Monitoring filesystem metadata operations. +// +// Filesystem metadata operations (create, link, remove, rename, etc) are also +// sources of disk writes. Unlike a vfs.File which requires Write and Sync calls +// to be sequential, a vfs.FS may receive these filesystem metadata operations +// in parallel. To accommodate this parallelism, the diskHealthCheckingFS's +// write-oriented filesystem operations record their start times into a 'slot' +// on the filesystem. A single long-running goroutine periodically scans the +// slots looking for slow operations. +// +// The number of slots on a diskHealthCheckingFS grows to a working set of the +// maximum concurrent filesystem operations. This is expected to be very few +// for these reasons: +// 1. Pebble has limited write concurrency. Flushes, compactions and WAL +// rotations are the primary sources of filesystem metadata operations. With +// the default max-compaction concurrency, these operations require at most 5 +// concurrent slots if all 5 perform a filesystem metadata operation +// simultaneously. +// 2. Pebble's limited concurrent I/O writers spend most of their time +// performing file I/O, not performing the filesystem metadata operations that +// require recording a slot on the diskHealthCheckingFS. +// 3. In CockroachDB, each additional store/Pebble instance has its own vfs.FS +// which provides a separate goroutine and set of slots. +// 4. In CockroachDB, many of the additional sources of filesystem metadata +// operations (like encryption-at-rest) are sequential with respect to Pebble's +// threads. +type diskHealthCheckingFS struct { + tickInterval time.Duration + diskSlowThreshold time.Duration + onSlowDisk func(DiskSlowInfo) + fs FS + mu struct { + sync.Mutex + tickerRunning bool + stopper chan struct{} + inflight []*slot + } + // prealloc preallocates the memory for mu.inflight slots and the slice + // itself. The contained fields are not accessed directly except by + // WithDiskHealthChecks when initializing mu.inflight. The number of slots + // in d.mu.inflight will grow to the maximum number of concurrent file + // metadata operations (create, remove, link, etc). If the number of + // concurrent operations never exceeds preallocatedSlotCount, we'll never + // incur an additional allocation. + prealloc struct { + slots [preallocatedSlotCount]slot + slotPtrSlice [preallocatedSlotCount]*slot + } +} + +type slot struct { + name string + opType OpType + startNanos atomic.Int64 +} + +// diskHealthCheckingFS implements FS. +var _ FS = (*diskHealthCheckingFS)(nil) + +// WithDiskHealthChecks wraps an FS and ensures that all write-oriented +// operations on the FS are wrapped with disk health detection checks. Disk +// operations that are observed to take longer than diskSlowThreshold trigger an +// onSlowDisk call. +// +// A threshold of zero disables disk-health checking. +func WithDiskHealthChecks( + innerFS FS, diskSlowThreshold time.Duration, onSlowDisk func(info DiskSlowInfo), +) (FS, io.Closer) { + if diskSlowThreshold == 0 { + return innerFS, noopCloser{} + } + + fs := &diskHealthCheckingFS{ + fs: innerFS, + tickInterval: defaultTickInterval, + diskSlowThreshold: diskSlowThreshold, + onSlowDisk: onSlowDisk, + } + fs.mu.stopper = make(chan struct{}) + // The fs holds preallocated slots and a preallocated array of slot pointers + // with equal length. Initialize the inflight slice to use a slice backed by + // the preallocated array with each slot initialized to a preallocated slot. + fs.mu.inflight = fs.prealloc.slotPtrSlice[:] + for i := range fs.mu.inflight { + fs.mu.inflight[i] = &fs.prealloc.slots[i] + } + return fs, fs +} + +// timeFilesystemOp executes the provided closure, which should perform a +// singular filesystem operation of a type matching opType on the named file. It +// records the provided start time such that the long-lived disk-health checking +// goroutine can observe if the operation is blocked for an inordinate time. +// +// The start time is taken as a parameter in the form of nanoseconds since the +// unix epoch so that it appears in stack traces during crashes (if GOTRACEBACK +// is set appropriately), aiding postmortem debugging. +func (d *diskHealthCheckingFS) timeFilesystemOp( + name string, opType OpType, op func(), startNanos int64, +) { + if d == nil { + op() + return + } + + // Record this operation's start time on the FS, so that the long-running + // goroutine can monitor the filesystem operation. + // + // The diskHealthCheckingFile implementation uses a single field that is + // atomically updated, taking advantage of the fact that writes to a single + // vfs.File handle are not performed in parallel. The vfs.FS however may + // receive write filesystem operations in parallel. To accommodate this + // parallelism, writing goroutines append their start time to a + // mutex-protected vector. On ticks, the long-running goroutine scans the + // vector searching for start times older than the slow-disk threshold. When + // a writing goroutine completes its operation, it atomically overwrites its + // slot to signal completion. + var s *slot + func() { + d.mu.Lock() + defer d.mu.Unlock() + + // If there's no long-running goroutine to monitor this filesystem + // operation, start one. + if !d.mu.tickerRunning { + d.startTickerLocked() + } + + for i := 0; i < len(d.mu.inflight); i++ { + 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 + s.startNanos.Store(startNanos) + break + } + } + // If we didn't find any unused slots, create a new slot and append it. + // This slot will exist forever. The number of slots will grow to the + // maximum number of concurrent filesystem operations over the lifetime + // of the process. Only operations that grow the number of slots must + // incur an allocation. + if s == nil { + s = &slot{ + name: name, + opType: opType, + } + s.startNanos.Store(startNanos) + d.mu.inflight = append(d.mu.inflight, s) + } + }() + + op() + + // Signal completion by zeroing the start time. + s.startNanos.Store(0) +} + +// startTickerLocked starts a new goroutine with a ticker to monitor disk +// filesystem operations. Requires d.mu and !d.mu.tickerRunning. +func (d *diskHealthCheckingFS) startTickerLocked() { + d.mu.tickerRunning = true + stopper := d.mu.stopper + go func() { + ticker := time.NewTicker(d.tickInterval) + defer ticker.Stop() + type exceededSlot struct { + name string + opType OpType + startNanos int64 + } + var exceededSlots []exceededSlot + + for { + select { + case <-ticker.C: + // Scan the inflight slots for any slots recording a start + // time older than the diskSlowThreshold. + exceededSlots = exceededSlots[:0] + d.mu.Lock() + now := time.Now() + for i := range d.mu.inflight { + 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. + inflightOp := exceededSlot{ + name: d.mu.inflight[i].name, + opType: d.mu.inflight[i].opType, + startNanos: nanos, + } + exceededSlots = append(exceededSlots, inflightOp) + } + } + d.mu.Unlock() + for i := range exceededSlots { + d.onSlowDisk( + DiskSlowInfo{ + Path: exceededSlots[i].name, + OpType: exceededSlots[i].opType, + WriteSize: 0, // writes at the fs level are not sized + Duration: now.Sub(time.Unix(0, exceededSlots[i].startNanos)), + }) + } + case <-stopper: + return + } + } + }() +} + +// Close implements io.Closer. Close stops the long-running goroutine that +// monitors for slow filesystem metadata operations. Close may be called +// multiple times. If the filesystem is used after Close has been called, a new +// long-running goroutine will be created. +func (d *diskHealthCheckingFS) Close() error { + d.mu.Lock() + if !d.mu.tickerRunning { + // Nothing to stop. + d.mu.Unlock() + return nil + } + + // Grab the stopper so we can request the long-running goroutine to stop. + // Replace the stopper in case this FS is reused. It's possible to Close and + // reuse a disk-health checking FS. This is to accommodate the on-by-default + // behavior in Pebble, and the possibility that users may continue to use + // the Pebble default FS beyond the lifetime of a single DB. + stopper := d.mu.stopper + d.mu.stopper = make(chan struct{}) + d.mu.tickerRunning = false + d.mu.Unlock() + + // Ask the long-running goroutine to stop. This is a synchronous channel + // send. + stopper <- struct{}{} + close(stopper) + return nil +} + +// Create implements the FS interface. +func (d *diskHealthCheckingFS) Create(name string) (File, error) { + var f File + var err error + d.timeFilesystemOp(name, OpTypeCreate, func() { + f, err = d.fs.Create(name) + }, time.Now().UnixNano()) + if err != nil { + return f, err + } + if d.diskSlowThreshold == 0 { + return f, nil + } + checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(opType OpType, writeSizeInBytes int, duration time.Duration) { + d.onSlowDisk( + DiskSlowInfo{ + Path: name, + OpType: opType, + WriteSize: writeSizeInBytes, + Duration: duration, + }) + }) + checkingFile.startTicker() + return checkingFile, nil +} + +// GetDiskUsage implements the FS interface. +func (d *diskHealthCheckingFS) GetDiskUsage(path string) (DiskUsage, error) { + return d.fs.GetDiskUsage(path) +} + +// Link implements the FS interface. +func (d *diskHealthCheckingFS) Link(oldname, newname string) error { + var err error + d.timeFilesystemOp(newname, OpTypeLink, func() { + err = d.fs.Link(oldname, newname) + }, time.Now().UnixNano()) + return err +} + +// List implements the FS interface. +func (d *diskHealthCheckingFS) List(dir string) ([]string, error) { + return d.fs.List(dir) +} + +// Lock implements the FS interface. +func (d *diskHealthCheckingFS) Lock(name string) (io.Closer, error) { + return d.fs.Lock(name) +} + +// MkdirAll implements the FS interface. +func (d *diskHealthCheckingFS) MkdirAll(dir string, perm os.FileMode) error { + var err error + d.timeFilesystemOp(dir, OpTypeMkdirAll, func() { + err = d.fs.MkdirAll(dir, perm) + }, time.Now().UnixNano()) + return err +} + +// Open implements the FS interface. +func (d *diskHealthCheckingFS) Open(name string, opts ...OpenOption) (File, error) { + 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) + if err != nil { + return f, err + } + // Directories opened with OpenDir must be opened with health checking, + // because they may be explicitly synced. + return &diskHealthCheckingDir{ + File: f, + name: name, + fs: d, + }, nil +} + +// PathBase implements the FS interface. +func (d *diskHealthCheckingFS) PathBase(path string) string { + return d.fs.PathBase(path) +} + +// PathJoin implements the FS interface. +func (d *diskHealthCheckingFS) PathJoin(elem ...string) string { + return d.fs.PathJoin(elem...) +} + +// PathDir implements the FS interface. +func (d *diskHealthCheckingFS) PathDir(path string) string { + return d.fs.PathDir(path) +} + +// Remove implements the FS interface. +func (d *diskHealthCheckingFS) Remove(name string) error { + var err error + d.timeFilesystemOp(name, OpTypeRemove, func() { + err = d.fs.Remove(name) + }, time.Now().UnixNano()) + return err +} + +// RemoveAll implements the FS interface. +func (d *diskHealthCheckingFS) RemoveAll(name string) error { + var err error + d.timeFilesystemOp(name, OpTypeRemoveAll, func() { + err = d.fs.RemoveAll(name) + }, time.Now().UnixNano()) + return err +} + +// Rename implements the FS interface. +func (d *diskHealthCheckingFS) Rename(oldname, newname string) error { + var err error + d.timeFilesystemOp(newname, OpTypeRename, func() { + err = d.fs.Rename(oldname, newname) + }, time.Now().UnixNano()) + return err +} + +// ReuseForWrite implements the FS interface. +func (d *diskHealthCheckingFS) ReuseForWrite(oldname, newname string) (File, error) { + var f File + var err error + d.timeFilesystemOp(newname, OpTypeReuseForWrite, func() { + f, err = d.fs.ReuseForWrite(oldname, newname) + }, time.Now().UnixNano()) + if err != nil { + return f, err + } + if d.diskSlowThreshold == 0 { + return f, nil + } + checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(opType OpType, writeSizeInBytes int, duration time.Duration) { + d.onSlowDisk( + DiskSlowInfo{ + Path: newname, + OpType: opType, + WriteSize: writeSizeInBytes, + Duration: duration, + }) + }) + checkingFile.startTicker() + return checkingFile, nil +} + +// Stat implements the FS interface. +func (d *diskHealthCheckingFS) Stat(name string) (os.FileInfo, error) { + return d.fs.Stat(name) +} + +type noopCloser struct{} + +func (noopCloser) Close() error { return nil } diff --git a/pebble/vfs/disk_health_test.go b/pebble/vfs/disk_health_test.go new file mode 100644 index 0000000..7bb2058 --- /dev/null +++ b/pebble/vfs/disk_health_test.go @@ -0,0 +1,571 @@ +// Copyright 2020 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 vfs + +import ( + "io" + "math" + "os" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" +) + +type mockFile struct { + syncAndWriteDuration time.Duration +} + +func (m mockFile) Close() error { + return nil +} + +func (m mockFile) Read(p []byte) (n int, err error) { + panic("unimplemented") +} + +func (m mockFile) ReadAt(p []byte, off int64) (n int, err error) { + panic("unimplemented") +} + +func (m mockFile) Write(p []byte) (n int, err error) { + time.Sleep(m.syncAndWriteDuration) + return len(p), nil +} + +func (m mockFile) WriteAt(p []byte, ofs int64) (n int, err error) { + time.Sleep(m.syncAndWriteDuration) + return len(p), nil +} + +func (m mockFile) Prefetch(offset, length int64) error { + panic("unimplemented") +} + +func (m mockFile) Preallocate(int64, int64) error { + time.Sleep(m.syncAndWriteDuration) + return nil +} + +func (m mockFile) Stat() (os.FileInfo, error) { + panic("unimplemented") +} + +func (m mockFile) Fd() uintptr { + return InvalidFd +} + +func (m mockFile) Sync() error { + time.Sleep(m.syncAndWriteDuration) + return nil +} + +func (m mockFile) SyncData() error { + time.Sleep(m.syncAndWriteDuration) + return nil +} + +func (m mockFile) SyncTo(int64) (fullSync bool, err error) { + time.Sleep(m.syncAndWriteDuration) + return false, nil +} + +var _ File = &mockFile{} + +type mockFS struct { + create func(string) (File, error) + link func(string, string) error + list func(string) ([]string, error) + lock func(string) (io.Closer, error) + mkdirAll func(string, os.FileMode) error + open func(string, ...OpenOption) (File, error) + openDir func(string) (File, error) + pathBase func(string) string + pathJoin func(...string) string + pathDir func(string) string + remove func(string) error + removeAll func(string) error + rename func(string, string) error + reuseForWrite func(string, string) (File, error) + stat func(string) (os.FileInfo, error) + getDiskUsage func(string) (DiskUsage, error) +} + +func (m mockFS) Create(name string) (File, error) { + if m.create == nil { + panic("unimplemented") + } + return m.create(name) +} + +func (m mockFS) Link(oldname, newname string) error { + if m.link == nil { + panic("unimplemented") + } + return m.link(oldname, newname) +} + +func (m mockFS) Open(name string, opts ...OpenOption) (File, error) { + if m.open == nil { + panic("unimplemented") + } + return m.open(name, opts...) +} + +func (m mockFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) { + panic("unimplemented") +} + +func (m mockFS) OpenDir(name string) (File, error) { + if m.openDir == nil { + panic("unimplemented") + } + return m.openDir(name) +} + +func (m mockFS) Remove(name string) error { + if m.remove == nil { + panic("unimplemented") + } + return m.remove(name) +} + +func (m mockFS) RemoveAll(name string) error { + if m.removeAll == nil { + panic("unimplemented") + } + return m.removeAll(name) +} + +func (m mockFS) Rename(oldname, newname string) error { + if m.rename == nil { + panic("unimplemented") + } + return m.rename(oldname, newname) +} + +func (m mockFS) ReuseForWrite(oldname, newname string) (File, error) { + if m.reuseForWrite == nil { + panic("unimplemented") + } + return m.reuseForWrite(oldname, newname) +} + +func (m mockFS) MkdirAll(dir string, perm os.FileMode) error { + if m.mkdirAll == nil { + panic("unimplemented") + } + return m.mkdirAll(dir, perm) +} + +func (m mockFS) Lock(name string) (io.Closer, error) { + if m.lock == nil { + panic("unimplemented") + } + return m.lock(name) +} + +func (m mockFS) List(dir string) ([]string, error) { + if m.list == nil { + panic("unimplemented") + } + return m.list(dir) +} + +func (m mockFS) Stat(name string) (os.FileInfo, error) { + if m.stat == nil { + panic("unimplemented") + } + return m.stat(name) +} + +func (m mockFS) PathBase(path string) string { + if m.pathBase == nil { + panic("unimplemented") + } + return m.pathBase(path) +} + +func (m mockFS) PathJoin(elem ...string) string { + if m.pathJoin == nil { + panic("unimplemented") + } + return m.pathJoin(elem...) +} + +func (m mockFS) PathDir(path string) string { + if m.pathDir == nil { + panic("unimplemented") + } + return m.pathDir(path) +} + +func (m mockFS) GetDiskUsage(path string) (DiskUsage, error) { + if m.getDiskUsage == nil { + panic("unimplemented") + } + return m.getDiskUsage(path) +} + +var _ FS = &mockFS{} + +func TestDiskHealthChecking_File(t *testing.T) { + oldTickInterval := defaultTickInterval + defaultTickInterval = time.Millisecond + if runtime.GOOS == "windows" { + t.Skipf("skipped on windows due to unreliable runtimes") + } + + defer func() { defaultTickInterval = oldTickInterval }() + + const ( + slowThreshold = 50 * time.Millisecond + ) + + fiveKB := make([]byte, 5*writeSizePrecision) + testCases := []struct { + op OpType + writeSize int + writeDuration time.Duration + fn func(f File) + createWriteDelta time.Duration + }{ + { + op: OpTypeWrite, + writeSize: 5 * writeSizePrecision, // five KB + writeDuration: 100 * time.Millisecond, + fn: func(f File) { f.Write(fiveKB) }, + }, + { + op: OpTypeSync, + writeSize: 0, + writeDuration: 100 * time.Millisecond, + fn: func(f File) { f.Sync() }, + }, + } + for _, tc := range testCases { + t.Run(tc.op.String(), func(t *testing.T) { + diskSlow := make(chan DiskSlowInfo, 3) + mockFS := &mockFS{create: func(name string) (File, error) { + return mockFile{syncAndWriteDuration: tc.writeDuration}, nil + }} + fs, closer := WithDiskHealthChecks(mockFS, slowThreshold, + func(info DiskSlowInfo) { + diskSlow <- info + }) + defer closer.Close() + dhFile, _ := fs.Create("test") + defer dhFile.Close() + + // Writing after file creation tests computation of delta between file + // creation time & write time. + time.Sleep(tc.createWriteDelta) + + tc.fn(dhFile) + + select { + case i := <-diskSlow: + d := i.Duration + if d.Seconds() < slowThreshold.Seconds() { + t.Fatalf("expected %0.1f to be greater than threshold %0.1f", d.Seconds(), slowThreshold.Seconds()) + } + require.Equal(t, tc.writeSize, i.WriteSize) + require.Equal(t, tc.op, i.OpType) + case <-time.After(10 * time.Second): + t.Fatal("disk stall detector did not detect slow disk operation") + } + }) + } +} + +func TestDiskHealthChecking_NotTooManyOps(t *testing.T) { + numBitsForOpType := 64 - deltaBits - writeSizeBits + numOpTypesAllowed := int(math.Pow(2, float64(numBitsForOpType))) + numOpTypes := int(opTypeMax) + require.LessOrEqual(t, numOpTypes, numOpTypesAllowed) +} + +func TestDiskHealthChecking_File_PackingAndUnpacking(t *testing.T) { + testCases := []struct { + desc string + delta time.Duration + writeSize int64 + opType OpType + wantDelta time.Duration + wantWriteSize int + }{ + // Write op with write size in bytes. + { + desc: "write, sized op", + delta: 3000 * time.Millisecond, + writeSize: 1024, // 1 KB. + opType: OpTypeWrite, + wantDelta: 3000 * time.Millisecond, + wantWriteSize: 1024, + }, + // Sync op. No write size. Max-ish delta that packing scheme can handle. + { + desc: "sync, no write size", + delta: 34 * time.Hour * 24 * 365, + writeSize: 0, + opType: OpTypeSync, + wantDelta: 34 * time.Hour * 24 * 365, + wantWriteSize: 0, + }, + // Delta is negative (e.g. due to clock sync). Set to + // zero. + { + desc: "delta negative", + delta: -5, + writeSize: 5120, // 5 KB + opType: OpTypeWrite, + wantDelta: 0, + wantWriteSize: 5120, + }, + // Write size in bytes is larger than can fit in 20 bits. + // Round down to max that can fit in 20 bits. + { + desc: "write size truncated", + delta: 231 * time.Millisecond, + writeSize: 2097152000, // too big! + opType: OpTypeWrite, + wantDelta: 231 * time.Millisecond, + wantWriteSize: 1073740800, // (2^20-1) * writeSizePrecision ~= a bit less than one GB + }, + // Write size in bytes is max representable less than the ceiling. + { + desc: "write size barely not truncated", + delta: 231 * time.Millisecond, + writeSize: 1073739776, // max representable less than the ceiling + opType: OpTypeWrite, + wantDelta: 231 * time.Millisecond, + wantWriteSize: 1073739776, // since can fit, unchanged + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + packed := pack(tc.delta, tc.writeSize, tc.opType) + gotDelta, gotWriteSize, gotOpType := unpack(packed) + + require.Equal(t, tc.wantDelta, gotDelta) + require.Equal(t, tc.wantWriteSize, gotWriteSize) + require.Equal(t, tc.opType, gotOpType) + }) + } +} + +func TestDiskHealthChecking_File_Underflow(t *testing.T) { + f := &mockFile{} + hcFile := newDiskHealthCheckingFile(f, 1*time.Second, func(opType OpType, writeSizeInBytes int, duration time.Duration) { + // We expect to panic before sending the event. + t.Fatalf("unexpected slow disk event") + }) + defer hcFile.Close() + + t.Run("too large delta leads to panic", func(t *testing.T) { + // Given the packing scheme, 35 years of process uptime will lead to a delta + // that is too large to fit in the packed int64. + tCreate := time.Now().Add(-35 * time.Hour * 24 * 365) + hcFile.createTimeNanos = tCreate.UnixNano() + + // Assert that the time since tCreate (in milliseconds) is indeed greater + // than the max delta that can fit. + require.True(t, time.Since(tCreate).Milliseconds() > 1<: An error by itself is an injector that injects an error every +// time. +// - ( ) is an injector that injects an error only when +// the operation satisfies the predicate. +// +// Predicates: +// - Reads is a constant predicate that evalutes to true iff the operation is a +// read operation (eg, Open, Read, ReadAt, Stat) +// - Writes is a constant predicate that evaluates to true iff the operation is +// a write operation (eg, Create, Rename, Write, WriteAt, etc). +// - (PathMatch ) is a predicate that evalutes to true iff the +// operation's file path matches the provided shell pattern. +// - (OnIndex ) is a predicate that evaluates to true only on the n-th +// invocation. +// - (And [PREDICATE]...) is a predicate that evaluates to true +// iff all the provided predicates evaluate to true. And short circuits on +// the first predicate to evaluate to false. +// - (Or [PREDICATE]...) is a predicate that evaluates to true iff +// at least one of the provided predicates evaluates to true. Or short +// circuits on the first predicate to evaluate to true. +// - (Not ) is a predicate that evaluates to true iff its provided +// predicates evaluates to false. +// - (Randomly [INTEGER]) is a predicate that pseudorandomly evaluates +// to true. The probability of evaluating to true is determined by the +// required float argument (must be ≤1). The optional second parameter is a +// pseudorandom seed, for adjusting the deterministic randomness. +// - Operation-specific: +// (OpFileReadAt ) is a predicate that evaluates to true iff +// an operation is a file ReadAt call with an offset that's exactly equal. +// +// Example: (ErrInjected (And (PathMatch "*.sst") (OnIndex 5))) is a rule set +// that will inject an error on the 5-th I/O operation involving an sstable. +func NewParser() *Parser { + p := &Parser{ + predicates: dsl.NewPredicateParser[Op](), + injectors: dsl.NewParser[Injector](), + } + p.predicates.DefineConstant("Reads", func() dsl.Predicate[Op] { return Reads }) + p.predicates.DefineConstant("Writes", func() dsl.Predicate[Op] { return Writes }) + p.predicates.DefineFunc("PathMatch", + func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] { + pattern := s.ConsumeString() + s.Consume(token.RPAREN) + return PathMatch(pattern) + }) + p.predicates.DefineFunc("OpFileReadAt", + func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] { + return parseFileReadAtOp(s) + }) + p.predicates.DefineFunc("Randomly", + func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] { + return parseRandomly(s) + }) + p.AddError(ErrInjected) + return p +} + +// A Parser parses the error-injecting DSL. It may be extended to include +// additional errors through AddError. +type Parser struct { + predicates *dsl.Parser[dsl.Predicate[Op]] + injectors *dsl.Parser[Injector] +} + +// Parse parses the error injection DSL, returning the parsed injector. +func (p *Parser) Parse(s string) (Injector, error) { + return p.injectors.Parse(s) +} + +// AddError defines a new error that may be used within the DSL parsed by +// Parse and will inject the provided error. +func (p *Parser) AddError(le LabelledError) { + // Define the error both as a constant that unconditionally injects the + // error, and as a function that injects the error only if the provided + // predicate evaluates to true. + p.injectors.DefineConstant(le.Label, func() Injector { return le }) + p.injectors.DefineFunc(le.Label, + func(_ *dsl.Parser[Injector], s *dsl.Scanner) Injector { + pred := p.predicates.ParseFromPos(s, s.Scan()) + s.Consume(token.RPAREN) + return le.If(pred) + }) +} + +// LabelledError is an error that also implements Injector, unconditionally +// injecting itself. It implements String() by returning its label. It +// implements Error() by returning its underlying error. +type LabelledError struct { + error + Label string + predicate Predicate +} + +// String implements fmt.Stringer. +func (le LabelledError) String() string { + if le.predicate == nil { + return le.Label + } + return fmt.Sprintf("(%s %s)", le.Label, le.predicate.String()) +} + +// MaybeError implements Injector. +func (le LabelledError) MaybeError(op Op) error { + if le.predicate == nil || le.predicate.Evaluate(op) { + return le + } + return nil +} + +// If returns an Injector that returns the receiver error if the provided +// predicate evalutes to true. +func (le LabelledError) If(p Predicate) Injector { + le.predicate = p + return le +} + +func parseFileReadAtOp(s *dsl.Scanner) *opFileReadAt { + lit := s.Consume(token.INT).Lit + off, err := strconv.ParseInt(lit, 10, 64) + if err != nil { + panic(err) + } + s.Consume(token.RPAREN) + return &opFileReadAt{offset: off} +} + +func parseRandomly(s *dsl.Scanner) Predicate { + lit := s.Consume(token.FLOAT).Lit + p, err := strconv.ParseFloat(lit, 64) + if err != nil { + panic(err) + } else if p > 1.0 { + // NB: It's not possible for p to be less than zero because we don't + // try to parse the '-' token. + panic(errors.Newf("errorfs: Randomly proability p must be within p ≤ 1.0")) + } + + var seed int64 + tok := s.Scan() + switch tok.Kind { + case token.RPAREN: + case token.INT: + seed, err = strconv.ParseInt(tok.Lit, 10, 64) + if err != nil { + panic(err) + } + s.Consume(token.RPAREN) + default: + panic(errors.Errorf("errorfs: unexpected token %s; expected RPAREN | FLOAT", tok.String())) + } + return Randomly(p, seed) +} diff --git a/pebble/vfs/errorfs/errorfs.go b/pebble/vfs/errorfs/errorfs.go new file mode 100644 index 0000000..b357fcf --- /dev/null +++ b/pebble/vfs/errorfs/errorfs.go @@ -0,0 +1,518 @@ +// Copyright 2020 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 errorfs + +import ( + "fmt" + "io" + "os" + "strings" + "sync/atomic" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/internal/dsl" + "github.com/cockroachdb/pebble/vfs" +) + +// ErrInjected is an error artificially injected for testing fs error paths. +var ErrInjected = LabelledError{ + error: errors.New("injected error"), + Label: "ErrInjected", +} + +// Op describes a filesystem operation. +type Op struct { + // Kind describes the particular kind of operation being performed. + Kind OpKind + // Path is the path of the file of the file being operated on. + Path string + // Offset is the offset of an operation. It's set for OpFileReadAt and + // OpFileWriteAt operations. + Offset int64 +} + +// OpKind is an enum describing the type of operation. +type OpKind int + +const ( + // OpCreate describes a create file operation. + OpCreate OpKind = iota + // OpLink describes a hardlink operation. + OpLink + // OpOpen describes a file open operation. + OpOpen + // OpOpenDir describes a directory open operation. + OpOpenDir + // OpRemove describes a remove file operation. + OpRemove + // OpRemoveAll describes a recursive remove operation. + OpRemoveAll + // OpRename describes a rename operation. + OpRename + // OpReuseForWrite describes a reuse for write operation. + OpReuseForWrite + // OpMkdirAll describes a make directory including parents operation. + OpMkdirAll + // OpLock describes a lock file operation. + OpLock + // OpList describes a list directory operation. + OpList + // OpFilePreallocate describes a file preallocate operation. + OpFilePreallocate + // OpStat describes a path-based stat operation. + OpStat + // OpGetDiskUsage describes a disk usage operation. + OpGetDiskUsage + // OpFileClose describes a close file operation. + OpFileClose + // OpFileRead describes a file read operation. + OpFileRead + // OpFileReadAt describes a file seek read operation. + OpFileReadAt + // OpFileWrite describes a file write operation. + OpFileWrite + // OpFileWriteAt describes a file seek write operation. + OpFileWriteAt + // OpFileStat describes a file stat operation. + OpFileStat + // OpFileSync describes a file sync operation. + OpFileSync + // OpFileFlush describes a file flush operation. + OpFileFlush +) + +// ReadOrWrite returns the operation's kind. +func (o OpKind) ReadOrWrite() OpReadWrite { + switch o { + case OpOpen, OpOpenDir, OpList, OpStat, OpGetDiskUsage, OpFileRead, OpFileReadAt, OpFileStat: + return OpIsRead + case OpCreate, OpLink, OpRemove, OpRemoveAll, OpRename, OpReuseForWrite, OpMkdirAll, OpLock, OpFileClose, OpFileWrite, OpFileWriteAt, OpFileSync, OpFileFlush, OpFilePreallocate: + return OpIsWrite + default: + panic(fmt.Sprintf("unrecognized op %v\n", o)) + } +} + +// OpReadWrite is an enum describing whether an operation is a read or write +// operation. +type OpReadWrite int + +const ( + // OpIsRead describes read operations. + OpIsRead OpReadWrite = iota + // OpIsWrite describes write operations. + OpIsWrite +) + +// String implements fmt.Stringer. +func (kind OpReadWrite) String() string { + switch kind { + case OpIsRead: + return "Reads" + case OpIsWrite: + return "Writes" + default: + panic(fmt.Sprintf("unrecognized OpKind %d", kind)) + } +} + +// OnIndex is a convenience function for constructing a dsl.OnIndex for use with +// an error-injecting filesystem. +func OnIndex(index int32) *InjectIndex { + return &InjectIndex{dsl.OnIndex[Op](index)} +} + +// InjectIndex implements Injector, injecting an error at a specific index. +type InjectIndex struct { + *dsl.Index[Op] +} + +// MaybeError implements the Injector interface. +// +// TODO(jackson): Remove this implementation and update callers to compose it +// with other injectors. +func (ii *InjectIndex) MaybeError(op Op) error { + if !ii.Evaluate(op) { + return nil + } + return ErrInjected +} + +// InjectorFunc implements the Injector interface for a function with +// MaybeError's signature. +type InjectorFunc func(Op) error + +// String implements fmt.Stringer. +func (f InjectorFunc) String() string { return "" } + +// MaybeError implements the Injector interface. +func (f InjectorFunc) MaybeError(op Op) error { return f(op) } + +// Injector injects errors into FS operations. +type Injector interface { + fmt.Stringer + // MaybeError is invoked by an errorfs before an operation is executed. It + // is passed an enum indicating the type of operation and a path of the + // subject file or directory. If the operation takes two paths (eg, + // Rename, Link), the original source path is provided. + MaybeError(op Op) error +} + +// Any returns an injector that injects an error if any of the provided +// injectors inject an error. The error returned by the first injector to return +// an error is used. +func Any(injectors ...Injector) Injector { + return anyInjector(injectors) +} + +type anyInjector []Injector + +func (a anyInjector) String() string { + var sb strings.Builder + sb.WriteString("(Any") + for _, inj := range a { + sb.WriteString(" ") + sb.WriteString(inj.String()) + } + sb.WriteString(")") + return sb.String() +} + +func (a anyInjector) MaybeError(op Op) error { + for _, inj := range a { + if err := inj.MaybeError(op); err != nil { + return err + } + } + return nil +} + +// Counter wraps an Injector, counting the number of errors injected. It may be +// used in tests to ensure that when an error is injected, the error is +// surfaced through the user interface. +type Counter struct { + Injector + atomic.Uint64 +} + +// String implements fmt.Stringer. +func (c *Counter) String() string { + return c.Injector.String() +} + +// MaybeError implements Injector. +func (c *Counter) MaybeError(op Op) error { + err := c.Injector.MaybeError(op) + if err != nil { + c.Uint64.Add(1) + } + return err +} + +// Toggle wraps an Injector. By default, Toggle injects nothing. When toggled on +// through its On method, it begins injecting errors when the contained injector +// injects them. It may be returned to its original state through Off. +type Toggle struct { + Injector + on atomic.Bool +} + +// String implements fmt.Stringer. +func (t *Toggle) String() string { + return t.Injector.String() +} + +// MaybeError implements Injector. +func (t *Toggle) MaybeError(op Op) error { + if !t.on.Load() { + return nil + } + return t.Injector.MaybeError(op) +} + +// On enables error injection. +func (t *Toggle) On() { t.on.Store(true) } + +// Off disables error injection. +func (t *Toggle) Off() { t.on.Store(false) } + +// FS implements vfs.FS, injecting errors into +// its operations. +type FS struct { + fs vfs.FS + inj Injector +} + +// Wrap wraps an existing vfs.FS implementation, returning a new +// vfs.FS implementation that shadows operations to the provided FS. +// It uses the provided Injector for deciding when to inject errors. +// If an error is injected, FS propagates the error instead of +// shadowing the operation. +func Wrap(fs vfs.FS, inj Injector) *FS { + return &FS{ + fs: fs, + inj: inj, + } +} + +// WrapFile wraps an existing vfs.File, returning a new vfs.File that shadows +// operations to the provided vfs.File. It uses the provided Injector for +// deciding when to inject errors. If an error is injected, the file +// propagates the error instead of shadowing the operation. +func WrapFile(f vfs.File, inj Injector) vfs.File { + return &errorFile{file: f, inj: inj} +} + +// Unwrap returns the FS implementation underlying fs. +// See pebble/vfs.Root. +func (fs *FS) Unwrap() vfs.FS { + return fs.fs +} + +// Create implements FS.Create. +func (fs *FS) Create(name string) (vfs.File, error) { + if err := fs.inj.MaybeError(Op{Kind: OpCreate, Path: name}); err != nil { + return nil, err + } + f, err := fs.fs.Create(name) + if err != nil { + return nil, err + } + return &errorFile{name, f, fs.inj}, nil +} + +// Link implements FS.Link. +func (fs *FS) Link(oldname, newname string) error { + if err := fs.inj.MaybeError(Op{Kind: OpLink, Path: oldname}); err != nil { + return err + } + return fs.fs.Link(oldname, newname) +} + +// Open implements FS.Open. +func (fs *FS) Open(name string, opts ...vfs.OpenOption) (vfs.File, error) { + if err := fs.inj.MaybeError(Op{Kind: OpOpen, Path: name}); err != nil { + return nil, err + } + f, err := fs.fs.Open(name) + if err != nil { + return nil, err + } + ef := &errorFile{name, f, fs.inj} + for _, opt := range opts { + opt.Apply(ef) + } + return ef, nil +} + +// OpenReadWrite implements FS.OpenReadWrite. +func (fs *FS) OpenReadWrite(name string, opts ...vfs.OpenOption) (vfs.File, error) { + if err := fs.inj.MaybeError(Op{Kind: OpOpen, Path: name}); err != nil { + return nil, err + } + f, err := fs.fs.OpenReadWrite(name) + if err != nil { + return nil, err + } + ef := &errorFile{name, f, fs.inj} + for _, opt := range opts { + opt.Apply(ef) + } + return ef, nil +} + +// OpenDir implements FS.OpenDir. +func (fs *FS) OpenDir(name string) (vfs.File, error) { + if err := fs.inj.MaybeError(Op{Kind: OpOpenDir, Path: name}); err != nil { + return nil, err + } + f, err := fs.fs.OpenDir(name) + if err != nil { + return nil, err + } + return &errorFile{name, f, fs.inj}, nil +} + +// GetDiskUsage implements FS.GetDiskUsage. +func (fs *FS) GetDiskUsage(path string) (vfs.DiskUsage, error) { + if err := fs.inj.MaybeError(Op{Kind: OpGetDiskUsage, Path: path}); err != nil { + return vfs.DiskUsage{}, err + } + return fs.fs.GetDiskUsage(path) +} + +// PathBase implements FS.PathBase. +func (fs *FS) PathBase(p string) string { + return fs.fs.PathBase(p) +} + +// PathDir implements FS.PathDir. +func (fs *FS) PathDir(p string) string { + return fs.fs.PathDir(p) +} + +// PathJoin implements FS.PathJoin. +func (fs *FS) PathJoin(elem ...string) string { + return fs.fs.PathJoin(elem...) +} + +// Remove implements FS.Remove. +func (fs *FS) Remove(name string) error { + if _, err := fs.fs.Stat(name); oserror.IsNotExist(err) { + return nil + } + + if err := fs.inj.MaybeError(Op{Kind: OpRemove, Path: name}); err != nil { + return err + } + return fs.fs.Remove(name) +} + +// RemoveAll implements FS.RemoveAll. +func (fs *FS) RemoveAll(fullname string) error { + if err := fs.inj.MaybeError(Op{Kind: OpRemoveAll, Path: fullname}); err != nil { + return err + } + return fs.fs.RemoveAll(fullname) +} + +// Rename implements FS.Rename. +func (fs *FS) Rename(oldname, newname string) error { + if err := fs.inj.MaybeError(Op{Kind: OpRename, Path: oldname}); err != nil { + return err + } + return fs.fs.Rename(oldname, newname) +} + +// ReuseForWrite implements FS.ReuseForWrite. +func (fs *FS) ReuseForWrite(oldname, newname string) (vfs.File, error) { + if err := fs.inj.MaybeError(Op{Kind: OpReuseForWrite, Path: oldname}); err != nil { + return nil, err + } + return fs.fs.ReuseForWrite(oldname, newname) +} + +// MkdirAll implements FS.MkdirAll. +func (fs *FS) MkdirAll(dir string, perm os.FileMode) error { + if err := fs.inj.MaybeError(Op{Kind: OpMkdirAll, Path: dir}); err != nil { + return err + } + return fs.fs.MkdirAll(dir, perm) +} + +// Lock implements FS.Lock. +func (fs *FS) Lock(name string) (io.Closer, error) { + if err := fs.inj.MaybeError(Op{Kind: OpLock, Path: name}); err != nil { + return nil, err + } + return fs.fs.Lock(name) +} + +// List implements FS.List. +func (fs *FS) List(dir string) ([]string, error) { + if err := fs.inj.MaybeError(Op{Kind: OpList, Path: dir}); err != nil { + return nil, err + } + return fs.fs.List(dir) +} + +// Stat implements FS.Stat. +func (fs *FS) Stat(name string) (os.FileInfo, error) { + if err := fs.inj.MaybeError(Op{Kind: OpStat, Path: name}); err != nil { + return nil, err + } + return fs.fs.Stat(name) +} + +// errorFile implements vfs.File. The interface is implemented on the pointer +// type to allow pointer equality comparisons. +type errorFile struct { + path string + file vfs.File + inj Injector +} + +func (f *errorFile) Close() error { + // We don't inject errors during close as those calls should never fail in + // practice. + return f.file.Close() +} + +func (f *errorFile) Read(p []byte) (int, error) { + if err := f.inj.MaybeError(Op{Kind: OpFileRead, Path: f.path}); err != nil { + return 0, err + } + return f.file.Read(p) +} + +func (f *errorFile) ReadAt(p []byte, off int64) (int, error) { + if err := f.inj.MaybeError(Op{ + Kind: OpFileReadAt, + Path: f.path, + Offset: off, + }); err != nil { + return 0, err + } + return f.file.ReadAt(p, off) +} + +func (f *errorFile) Write(p []byte) (int, error) { + if err := f.inj.MaybeError(Op{Kind: OpFileWrite, Path: f.path}); err != nil { + return 0, err + } + return f.file.Write(p) +} + +func (f *errorFile) WriteAt(p []byte, off int64) (int, error) { + if err := f.inj.MaybeError(Op{ + Kind: OpFileWriteAt, + Path: f.path, + Offset: off, + }); err != nil { + return 0, err + } + return f.file.WriteAt(p, off) +} + +func (f *errorFile) Stat() (os.FileInfo, error) { + if err := f.inj.MaybeError(Op{Kind: OpFileStat, Path: f.path}); err != nil { + return nil, err + } + return f.file.Stat() +} + +func (f *errorFile) Prefetch(offset, length int64) error { + // TODO(radu): Consider error injection. + return f.file.Prefetch(offset, length) +} + +func (f *errorFile) Preallocate(offset, length int64) error { + if err := f.inj.MaybeError(Op{Kind: OpFilePreallocate, Path: f.path}); err != nil { + return err + } + return f.file.Preallocate(offset, length) +} + +func (f *errorFile) Sync() error { + if err := f.inj.MaybeError(Op{Kind: OpFileSync, Path: f.path}); err != nil { + return err + } + return f.file.Sync() +} + +func (f *errorFile) SyncData() error { + // TODO(jackson): Consider error injection. + return f.file.SyncData() +} + +func (f *errorFile) SyncTo(length int64) (fullSync bool, err error) { + // TODO(jackson): Consider error injection. + return f.file.SyncTo(length) +} + +func (f *errorFile) Fd() uintptr { + return f.file.Fd() +} diff --git a/pebble/vfs/errorfs/errorfs_test.go b/pebble/vfs/errorfs/errorfs_test.go new file mode 100644 index 0000000..af9b6ba --- /dev/null +++ b/pebble/vfs/errorfs/errorfs_test.go @@ -0,0 +1,34 @@ +// 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 errorfs + +import ( + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" +) + +func TestErrorFS(t *testing.T) { + var sb strings.Builder + datadriven.RunTest(t, "testdata/errorfs", func(t *testing.T, td *datadriven.TestData) string { + sb.Reset() + switch td.Cmd { + case "parse-dsl": + for _, l := range strings.Split(strings.TrimSpace(td.Input), "\n") { + inj, err := ParseDSL(l) + if err != nil { + fmt.Fprintf(&sb, "parsing err: %s\n", err) + } else { + fmt.Fprintf(&sb, "%s\n", inj.String()) + } + } + return sb.String() + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} diff --git a/pebble/vfs/errorfs/testdata/errorfs b/pebble/vfs/errorfs/testdata/errorfs new file mode 100644 index 0000000..8c4e336 --- /dev/null +++ b/pebble/vfs/errorfs/testdata/errorfs @@ -0,0 +1,75 @@ +parse-dsl +ErrInjected +(ErrInjected Reads) +(ErrInjected (PathMatch "foo/*.sst")) +(ErrInjected (OnIndex 1)) +(ErrInjected (Or Reads Writes)) +(ErrInjected (And (PathMatch "foo/bar/*.sst") (OnIndex 1))) +(ErrInjected (Or (OnIndex 2) (PathMatch "*.sst"))) +(ErrInjected (And Reads (PathMatch "*.sst"))) +(ErrInjected (Or Writes (PathMatch "*.sst"))) +---- +ErrInjected +(ErrInjected Reads) +(ErrInjected (PathMatch "foo/*.sst")) +(ErrInjected (OnIndex 1)) +(ErrInjected (Or Reads Writes)) +(ErrInjected (And (PathMatch "foo/bar/*.sst") (OnIndex 1))) +(ErrInjected (Or (OnIndex 2) (PathMatch "*.sst"))) +(ErrInjected (And Reads (PathMatch "*.sst"))) +(ErrInjected (Or Writes (PathMatch "*.sst"))) + +parse-dsl +errInjected +ErrInjected() +(ErrInjected (PathMatch foo/*.sst)) +(alwoes (PathMatch "foo/*.sst")) +(ErrInjected (PathMatch "foo/*.sst" "")) +(ErrInjected PathMatch "foo/*.sst") +(ErrInjected (OnIndex ErrInjected)) +(Or ErrInjected ErrInjected ErrInjected +(And ErrInjected ErrInjected ErrInjected) +(Or 1 4 5) +(ErrInjected (OnIndex foo)) +(ErrInjected (OnIndex 9223372036854775807)) +---- +parsing err: dsl: unknown constant "errInjected" +parsing err: dsl: unexpected token ( at pos 12; expected EOF +parsing err: dsl: unexpected token (IDENT, "foo") at pos 25; expected STRING +parsing err: dsl: unknown func "alwoes" +parsing err: dsl: unexpected token (STRING, "\"\"") at pos 37; expected ) +parsing err: dsl: unknown constant "PathMatch" +parsing err: dsl: unexpected token (IDENT, "ErrInjected") at pos 23; expected INT +parsing err: dsl: unknown func "Or" +parsing err: dsl: unknown func "And" +parsing err: dsl: unknown func "Or" +parsing err: dsl: unexpected token (IDENT, "foo") at pos 23; expected INT +parsing err: strconv.ParseInt: parsing "9223372036854775807": value out of range + +parse-dsl +(ErrInjected (OpFileReadAt _)) +(ErrInjected (OpFileReadAt foo)) +(ErrInjected (OpFileReadAt 1052363)) +---- +parsing err: dsl: unexpected token (IDENT, "_") at pos 28; expected INT +parsing err: dsl: unexpected token (IDENT, "foo") at pos 28; expected INT +(ErrInjected (FileReadAt 1052363)) + +parse-dsl +(ErrInjected (Randomly 0)) +(ErrInjected (Randomly 0.1)) +(ErrInjected (Randomly 0.2 18520850252)) +(ErrInjected (Randomly 1.2 18520850252)) +(ErrInjected (Randomly -0.3 18520850252)) +(ErrInjected (Randomly 18520850252 0.25)) +(ErrInjected (And (PathMatch "*.sst") (Randomly 0.05 185957252))) +(ErrInjected (And (PathMatch "*.sst") (Randomly 0.05))) +---- +parsing err: dsl: unexpected token (INT, "0") at pos 24; expected FLOAT +(ErrInjected (Randomly 0.10)) +(ErrInjected (Randomly 0.20 18520850252)) +parsing err: errorfs: Randomly proability p must be within p ≤ 1.0 +parsing err: dsl: unexpected token - at pos 24; expected FLOAT +parsing err: dsl: unexpected token (INT, "18520850252") at pos 24; expected FLOAT +(ErrInjected (And (PathMatch "*.sst") (Randomly 0.05 185957252))) +(ErrInjected (And (PathMatch "*.sst") (Randomly 0.05))) diff --git a/pebble/vfs/errors_unix.go b/pebble/vfs/errors_unix.go new file mode 100644 index 0000000..2d05e14 --- /dev/null +++ b/pebble/vfs/errors_unix.go @@ -0,0 +1,21 @@ +// Copyright 2020 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 darwin || dragonfly || freebsd || linux || openbsd || netbsd +// +build darwin dragonfly freebsd linux openbsd netbsd + +package vfs + +import ( + "github.com/cockroachdb/errors" + "golang.org/x/sys/unix" +) + +var errNotEmpty = unix.ENOTEMPTY + +// IsNoSpaceError returns true if the given error indicates that the disk is +// out of space. +func IsNoSpaceError(err error) bool { + return errors.Is(err, unix.ENOSPC) +} diff --git a/pebble/vfs/errors_unix_test.go b/pebble/vfs/errors_unix_test.go new file mode 100644 index 0000000..7a1c1dc --- /dev/null +++ b/pebble/vfs/errors_unix_test.go @@ -0,0 +1,21 @@ +// Copyright 2020 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 darwin || dragonfly || freebsd || linux || openbsd || netbsd +// +build darwin dragonfly freebsd linux openbsd netbsd + +package vfs + +import ( + "testing" + + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func TestIsNoSpaceError(t *testing.T) { + err := errors.WithStack(unix.ENOSPC) + require.True(t, IsNoSpaceError(err)) +} diff --git a/pebble/vfs/errors_windows.go b/pebble/vfs/errors_windows.go new file mode 100644 index 0000000..65920fc --- /dev/null +++ b/pebble/vfs/errors_windows.go @@ -0,0 +1,22 @@ +// Copyright 2020 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 windows +// +build windows + +package vfs + +import ( + "github.com/cockroachdb/errors" + "golang.org/x/sys/windows" +) + +var errNotEmpty = windows.ERROR_DIR_NOT_EMPTY + +// IsNoSpaceError returns true if the given error indicates that the disk is +// out of space. +func IsNoSpaceError(err error) bool { + return errors.Is(err, windows.ERROR_DISK_FULL) || + errors.Is(err, windows.ERROR_HANDLE_DISK_FULL) +} diff --git a/pebble/vfs/fadvise_generic.go b/pebble/vfs/fadvise_generic.go new file mode 100644 index 0000000..d6b9b41 --- /dev/null +++ b/pebble/vfs/fadvise_generic.go @@ -0,0 +1,16 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +//go:build !linux +// +build !linux + +package vfs + +func fadviseRandom(f uintptr) error { + return nil +} + +func fadviseSequential(f uintptr) error { + return nil +} diff --git a/pebble/vfs/fadvise_linux.go b/pebble/vfs/fadvise_linux.go new file mode 100644 index 0000000..6bb4db1 --- /dev/null +++ b/pebble/vfs/fadvise_linux.go @@ -0,0 +1,20 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +//go:build linux +// +build linux + +package vfs + +import "golang.org/x/sys/unix" + +// Calls Fadvise with FADV_RANDOM to disable readahead on a file descriptor. +func fadviseRandom(f uintptr) error { + return unix.Fadvise(int(f), 0, 0, unix.FADV_RANDOM) +} + +// Calls Fadvise with FADV_SEQUENTIAL to enable readahead on a file descriptor. +func fadviseSequential(f uintptr) error { + return unix.Fadvise(int(f), 0, 0, unix.FADV_SEQUENTIAL) +} diff --git a/pebble/vfs/fd_test.go b/pebble/vfs/fd_test.go new file mode 100644 index 0000000..d86aba5 --- /dev/null +++ b/pebble/vfs/fd_test.go @@ -0,0 +1,36 @@ +// Copyright 2021 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 vfs + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestFileWrappersHaveFd(t *testing.T) { + // Use the real filesystem so that we can test vfs.Default, which returns + // files with Fd(). + tmpf, err := os.CreateTemp("", "pebble-db-fd-file") + require.NoError(t, err) + filename := tmpf.Name() + defer os.Remove(filename) + + // File wrapper case 1: Check if diskHealthCheckingFile has Fd(). + fs2, closer := WithDiskHealthChecks(Default, 10*time.Second, + func(info DiskSlowInfo) {}) + defer closer.Close() + f2, err := fs2.Open(filename) + require.NoError(t, err) + require.NotZero(t, f2.Fd()) + require.NotEqual(t, f2.Fd(), InvalidFd) + // File wrapper case 2: Check if syncingFile has Fd(). + f3 := NewSyncingFile(f2, SyncingFileOptions{BytesPerSync: 8 << 10 /* 8 KB */}) + require.NotZero(t, f3.Fd()) + require.NotEqual(t, f3.Fd(), InvalidFd) + require.NoError(t, f2.Close()) +} diff --git a/pebble/vfs/file_lock_generic.go b/pebble/vfs/file_lock_generic.go new file mode 100644 index 0000000..6177da6 --- /dev/null +++ b/pebble/vfs/file_lock_generic.go @@ -0,0 +1,20 @@ +// Copyright 2012 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 !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows +// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows + +package vfs + +import ( + "io" + "runtime" + + "github.com/cockroachdb/errors" +) + +func (defFS) Lock(name string) (io.Closer, error) { + return nil, errors.Errorf("pebble: file locking is not implemented on %s/%s", + errors.Safe(runtime.GOOS), errors.Safe(runtime.GOARCH)) +} diff --git a/pebble/vfs/file_lock_test.go b/pebble/vfs/file_lock_test.go new file mode 100644 index 0000000..0b8cf11 --- /dev/null +++ b/pebble/vfs/file_lock_test.go @@ -0,0 +1,106 @@ +// Copyright 2014 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 vfs_test + +import ( + "bytes" + "flag" + "os" + "os/exec" + "testing" + + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +var lockFilename = flag.String("lockfile", "", "File to lock. A non-empty value implies a child process.") + +func spawn(prog, filename string) ([]byte, error) { + return exec.Command(prog, "-lockfile", filename, "-test.v", + "-test.run=TestLock$").CombinedOutput() +} + +// TestLock locks a file, spawns a second process that attempts to grab the +// lock to verify it fails. +// Then it closes the lock, and spawns a third copy to verify it can be +// relocked. +func TestLock(t *testing.T) { + child := *lockFilename != "" + var filename string + if child { + filename = *lockFilename + } else { + f, err := os.CreateTemp("", "golang-pebble-db-testlock-") + require.NoError(t, err) + + filename = f.Name() + // NB: On Windows, locking will fail if the file is already open by the + // current process, so we close the lockfile here. + require.NoError(t, f.Close()) + defer os.Remove(filename) + } + + // Avoid truncating an existing, non-empty file. + fi, err := os.Stat(filename) + if err == nil && fi.Size() != 0 { + t.Fatalf("The file %s is not empty", filename) + } + + t.Logf("Locking: %s", filename) + lock, err := vfs.Default.Lock(filename) + if err != nil { + t.Fatalf("Could not lock %s: %v", filename, err) + } + + if !child { + t.Logf("Spawning child, should fail to grab lock.") + out, err := spawn(os.Args[0], filename) + if err == nil { + t.Fatalf("Attempt to grab open lock should have failed.\n%s", out) + } + if !bytes.Contains(out, []byte("Could not lock")) { + t.Fatalf("Child failed with unexpected output: %s", out) + } + t.Logf("Child failed to grab lock as expected.") + } + + t.Logf("Unlocking %s", filename) + if err := lock.Close(); err != nil { + t.Fatalf("Could not unlock %s: %v", filename, err) + } + + if !child { + t.Logf("Spawning child, should successfully grab lock.") + if out, err := spawn(os.Args[0], filename); err != nil { + t.Fatalf("Attempt to re-open lock should have succeeded: %v\n%s", + err, out) + } + t.Logf("Child grabbed lock.") + } +} + +func TestLockSameProcess(t *testing.T) { + f, err := os.CreateTemp("", "pebble-testlocksameprocess-") + require.NoError(t, err) + filename := f.Name() + + // NB: On Windows, locking will fail if the file is already open by the + // current process, so we close the lockfile here. + require.NoError(t, f.Close()) + defer os.Remove(filename) + + lock1, err := vfs.Default.Lock(filename) + require.NoError(t, err) + + // Locking the file again from within the same process should fail. + // On Unix, Lock should detect the file in the global map of + // process-locked files. + // On Windows, locking will fail since the file is already open by the + // current process. + _, err = vfs.Default.Lock(filename) + require.Error(t, err) + + require.NoError(t, lock1.Close()) +} diff --git a/pebble/vfs/file_lock_unix.go b/pebble/vfs/file_lock_unix.go new file mode 100644 index 0000000..4d05e8c --- /dev/null +++ b/pebble/vfs/file_lock_unix.go @@ -0,0 +1,70 @@ +// Copyright 2014 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 darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package vfs + +import ( + "io" + "os" + "sync" + + "github.com/cockroachdb/errors" + "golang.org/x/sys/unix" +) + +var lockedFiles struct { + mu struct { + sync.Mutex + files map[string]bool + } +} + +// lockCloser hides all of an os.File's methods, except for Close. +type lockCloser struct { + name string + f *os.File +} + +func (l lockCloser) Close() error { + lockedFiles.mu.Lock() + defer lockedFiles.mu.Unlock() + if !lockedFiles.mu.files[l.name] { + panic(errors.Errorf("lock file %q is not locked", l.name)) + } + delete(lockedFiles.mu.files, l.name) + + return l.f.Close() +} + +func (defaultFS) Lock(name string) (io.Closer, error) { + lockedFiles.mu.Lock() + defer lockedFiles.mu.Unlock() + if lockedFiles.mu.files == nil { + lockedFiles.mu.files = map[string]bool{} + } + if lockedFiles.mu.files[name] { + return nil, errors.New("lock held by current process") + } + + f, err := os.Create(name) + if err != nil { + return nil, err + } + spec := unix.Flock_t{ + Type: unix.F_WRLCK, + Whence: io.SeekStart, + Start: 0, + Len: 0, // 0 means to lock the entire file. + Pid: int32(os.Getpid()), + } + if err := unix.FcntlFlock(f.Fd(), unix.F_SETLK, &spec); err != nil { + f.Close() + return nil, err + } + lockedFiles.mu.files[name] = true + return lockCloser{name, f}, nil +} diff --git a/pebble/vfs/file_lock_windows.go b/pebble/vfs/file_lock_windows.go new file mode 100644 index 0000000..31fd17a --- /dev/null +++ b/pebble/vfs/file_lock_windows.go @@ -0,0 +1,42 @@ +// Copyright 2013 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 windows +// +build windows + +package vfs + +import ( + "io" + + "golang.org/x/sys/windows" +) + +// lockCloser hides all of an windows.Handle's methods, except for Close. +type lockCloser struct { + fd windows.Handle +} + +func (l lockCloser) Close() error { + return windows.Close(l.fd) +} + +// Lock locks the given file. On Windows, Locking will fail if the file is +// already open by the current process. +func (defaultFS) Lock(name string) (io.Closer, error) { + p, err := windows.UTF16PtrFromString(name) + if err != nil { + return nil, err + } + fd, err := windows.CreateFile(p, + windows.GENERIC_READ|windows.GENERIC_WRITE, + 0, nil, windows.CREATE_ALWAYS, + windows.FILE_ATTRIBUTE_NORMAL, + 0, + ) + if err != nil { + return nil, err + } + return lockCloser{fd: fd}, nil +} diff --git a/pebble/vfs/logging_fs.go b/pebble/vfs/logging_fs.go new file mode 100644 index 0000000..820c081 --- /dev/null +++ b/pebble/vfs/logging_fs.go @@ -0,0 +1,157 @@ +// Copyright 2021 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 vfs + +import ( + "io" + "os" +) + +// WithLogging wraps an FS and logs filesystem modification operations to the +// given logFn. +func WithLogging(fs FS, logFn LogFn) FS { + return &loggingFS{ + FS: fs, + logFn: logFn, + } +} + +// LogFn is a function that is used to capture a log when WithLogging is used. +type LogFn func(fmt string, args ...interface{}) + +type loggingFS struct { + FS + logFn LogFn +} + +var _ FS = (*loggingFS)(nil) + +func (fs *loggingFS) Create(name string) (File, error) { + fs.logFn("create: %s", name) + f, err := fs.FS.Create(name) + if err != nil { + return nil, err + } + 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) +} + +func (fs *loggingFS) OpenDir(name string) (File, error) { + fs.logFn("open-dir: %s", name) + f, err := fs.FS.OpenDir(name) + if err != nil { + return nil, err + } + return newLoggingFile(f, name, fs.logFn), nil +} + +func (fs *loggingFS) Rename(oldname, newname string) error { + fs.logFn("rename: %s -> %s", oldname, newname) + return fs.FS.Rename(oldname, newname) +} + +func (fs *loggingFS) ReuseForWrite(oldname, newname string) (File, error) { + fs.logFn("reuseForWrite: %s -> %s", oldname, newname) + f, err := fs.FS.ReuseForWrite(oldname, newname) + if err != nil { + return nil, err + } + return newLoggingFile(f, newname, fs.logFn), nil +} + +func (fs *loggingFS) MkdirAll(dir string, perm os.FileMode) error { + fs.logFn("mkdir-all: %s %#o", dir, perm) + return fs.FS.MkdirAll(dir, perm) +} + +func (fs *loggingFS) Lock(name string) (io.Closer, error) { + fs.logFn("lock: %s", name) + return fs.FS.Lock(name) +} + +func (fs loggingFS) Remove(name string) error { + fs.logFn("remove: %s", name) + err := fs.FS.Remove(name) + return err +} + +func (fs loggingFS) RemoveAll(name string) error { + fs.logFn("remove-all: %s", name) + err := fs.FS.RemoveAll(name) + return err +} + +type loggingFile struct { + File + name string + logFn LogFn +} + +var _ File = (*loggingFile)(nil) + +func newLoggingFile(f File, name string, logFn LogFn) *loggingFile { + return &loggingFile{ + File: f, + name: name, + logFn: logFn, + } +} + +func (f *loggingFile) Close() error { + f.logFn("close: %s", f.name) + return f.File.Close() +} + +func (f *loggingFile) Sync() error { + f.logFn("sync: %s", f.name) + return f.File.Sync() +} + +func (f *loggingFile) SyncData() error { + f.logFn("sync-data: %s", f.name) + return f.File.SyncData() +} + +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/pebble/vfs/mem_fs.go b/pebble/vfs/mem_fs.go new file mode 100644 index 0000000..ea2be17 --- /dev/null +++ b/pebble/vfs/mem_fs.go @@ -0,0 +1,832 @@ +// Copyright 2012 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 vfs // import "github.com/cockroachdb/pebble/vfs" + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "sort" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/shims/slices" +) + +const sep = "/" + +// NewMem returns a new memory-backed FS implementation. +func NewMem() *MemFS { + return &MemFS{ + root: newRootMemNode(), + } +} + +// NewStrictMem returns a "strict" memory-backed FS implementation. The behaviour is strict wrt +// needing a Sync() call on files or directories for the state changes to be finalized. Any +// changes that are not finalized are visible to reads until MemFS.ResetToSyncedState() is called, +// at which point they are discarded and no longer visible. +// +// Expected usage: +// +// strictFS := NewStrictMem() +// db := Open(..., &Options{FS: strictFS}) +// // Do and commit various operations. +// ... +// // Prevent any more changes to finalized state. +// strictFS.SetIgnoreSyncs(true) +// // This will finish any ongoing background flushes, compactions but none of these writes will +// // be finalized since syncs are being ignored. +// db.Close() +// // Discard unsynced state. +// strictFS.ResetToSyncedState() +// // Allow changes to finalized state. +// strictFS.SetIgnoreSyncs(false) +// // Open the DB. This DB should have the same state as if the earlier strictFS operations and +// // db.Close() were not called. +// db := Open(..., &Options{FS: strictFS}) +func NewStrictMem() *MemFS { + return &MemFS{ + root: newRootMemNode(), + strict: true, + } +} + +// NewMemFile returns a memory-backed File implementation. The memory-backed +// file takes ownership of data. +func NewMemFile(data []byte) File { + n := &memNode{} + n.refs.Store(1) + n.mu.data = data + n.mu.modTime = time.Now() + return &memFile{ + n: n, + read: true, + } +} + +// MemFS implements FS. +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 + // open files. In tests meant to exercise this behavior, this flag can be + // set to error if removing an open file. + windowsSemantics bool +} + +var _ FS = &MemFS{} + +// UseWindowsSemantics configures whether the MemFS implements Windows-style +// semantics, in particular with respect to whether any of an open file's links +// may be removed. Windows semantics default to off. +func (y *MemFS) UseWindowsSemantics(windowsSemantics bool) { + y.mu.Lock() + defer y.mu.Unlock() + y.windowsSemantics = windowsSemantics +} + +// String dumps the contents of the MemFS. +func (y *MemFS) String() string { + y.mu.Lock() + defer y.mu.Unlock() + + s := new(bytes.Buffer) + y.root.dump(s, 0) + return s.String() +} + +// SetIgnoreSyncs sets the MemFS.ignoreSyncs field. See the usage comment with NewStrictMem() for +// details. +func (y *MemFS) SetIgnoreSyncs(ignoreSyncs bool) { + if !y.strict { + panic("SetIgnoreSyncs can only be used on a strict MemFS") + } + y.mu.Lock() + y.ignoreSyncs = ignoreSyncs + y.mu.Unlock() +} + +// ResetToSyncedState discards state in the FS that is not synced. See the usage comment with +// NewStrictMem() for details. +func (y *MemFS) ResetToSyncedState() { + if !y.strict { + panic("ResetToSyncedState can only be used on a strict MemFS") + } + y.mu.Lock() + y.root.resetToSyncedState() + y.mu.Unlock() +} + +// walk walks the directory tree for the fullname, calling f at each step. If +// f returns an error, the walk will be aborted and return that same error. +// +// Each walk is atomic: y's mutex is held for the entire operation, including +// all calls to f. +// +// dir is the directory at that step, frag is the name fragment, and final is +// whether it is the final step. For example, walking "/foo/bar/x" will result +// in 3 calls to f: +// - "/", "foo", false +// - "/foo/", "bar", false +// - "/foo/bar/", "x", true +// +// Similarly, walking "/y/z/", with a trailing slash, will result in 3 calls to f: +// - "/", "y", false +// - "/y/", "z", false +// - "/y/z/", "", true +func (y *MemFS) walk(fullname string, f func(dir *memNode, frag string, final bool) error) error { + y.mu.Lock() + defer y.mu.Unlock() + + // For memfs, the current working directory is the same as the root directory, + // so we strip off any leading "/"s to make fullname a relative path, and + // the walk starts at y.root. + for len(fullname) > 0 && fullname[0] == sep[0] { + fullname = fullname[1:] + } + dir := y.root + + for { + frag, remaining := fullname, "" + i := strings.IndexRune(fullname, rune(sep[0])) + final := i < 0 + if !final { + frag, remaining = fullname[:i], fullname[i+1:] + for len(remaining) > 0 && remaining[0] == sep[0] { + remaining = remaining[1:] + } + } + if err := f(dir, frag, final); err != nil { + return err + } + if final { + break + } + child := dir.children[frag] + if child == nil { + return &os.PathError{ + Op: "open", + Path: fullname, + Err: oserror.ErrNotExist, + } + } + if !child.isDir { + return &os.PathError{ + Op: "open", + Path: fullname, + Err: errors.New("not a directory"), + } + } + dir, fullname = child, remaining + } + return nil +} + +// Create implements FS.Create. +func (y *MemFS) Create(fullname string) (File, error) { + var ret *memFile + err := y.walk(fullname, func(dir *memNode, frag string, final bool) error { + if final { + if frag == "" { + return errors.New("pebble/vfs: empty file name") + } + n := &memNode{name: frag} + dir.children[frag] = n + ret = &memFile{ + n: n, + fs: y, + read: true, + write: true, + } + } + return nil + }) + if err != nil { + return nil, err + } + ret.n.refs.Add(1) + return ret, nil +} + +// Link implements FS.Link. +func (y *MemFS) Link(oldname, newname string) error { + var n *memNode + err := y.walk(oldname, func(dir *memNode, frag string, final bool) error { + if final { + if frag == "" { + return errors.New("pebble/vfs: empty file name") + } + n = dir.children[frag] + } + return nil + }) + if err != nil { + return err + } + if n == nil { + return &os.LinkError{ + Op: "link", + Old: oldname, + New: newname, + Err: oserror.ErrNotExist, + } + } + return y.walk(newname, func(dir *memNode, frag string, final bool) error { + if final { + if frag == "" { + return errors.New("pebble/vfs: empty file name") + } + if _, ok := dir.children[frag]; ok { + return &os.LinkError{ + Op: "link", + Old: oldname, + New: newname, + Err: oserror.ErrExist, + } + } + dir.children[frag] = n + } + return nil + }) +} + +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 { + if frag == "" { + ret = &memFile{ + n: dir, + fs: y, + } + return nil + } + if n := dir.children[frag]; n != nil { + ret = &memFile{ + n: n, + fs: y, + read: true, + write: openForWrite, + } + } + } + return nil + }) + if err != nil { + return nil, err + } + if ret == nil { + return nil, &os.PathError{ + Op: "open", + Path: fullname, + Err: oserror.ErrNotExist, + } + } + 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, 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, false /* openForWrite */) +} + +// Remove implements FS.Remove. +func (y *MemFS) Remove(fullname string) error { + return y.walk(fullname, func(dir *memNode, frag string, final bool) error { + if final { + if frag == "" { + return errors.New("pebble/vfs: empty file name") + } + child, ok := dir.children[frag] + if !ok { + return oserror.ErrNotExist + } + if y.windowsSemantics { + // Disallow removal of open files/directories which implements + // 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 := child.refs.Load(); n > 0 { + return oserror.ErrInvalid + } + } + if len(child.children) > 0 { + return errNotEmpty + } + delete(dir.children, frag) + } + return nil + }) +} + +// RemoveAll implements FS.RemoveAll. +func (y *MemFS) RemoveAll(fullname string) error { + err := y.walk(fullname, func(dir *memNode, frag string, final bool) error { + if final { + if frag == "" { + return errors.New("pebble/vfs: empty file name") + } + _, ok := dir.children[frag] + if !ok { + return nil + } + delete(dir.children, frag) + } + return nil + }) + // Match os.RemoveAll which returns a nil error even if the parent + // directories don't exist. + if oserror.IsNotExist(err) { + err = nil + } + return err +} + +// Rename implements FS.Rename. +func (y *MemFS) Rename(oldname, newname string) error { + var n *memNode + err := y.walk(oldname, func(dir *memNode, frag string, final bool) error { + if final { + if frag == "" { + return errors.New("pebble/vfs: empty file name") + } + n = dir.children[frag] + delete(dir.children, frag) + } + return nil + }) + if err != nil { + return err + } + if n == nil { + return &os.PathError{ + Op: "open", + Path: oldname, + Err: oserror.ErrNotExist, + } + } + return y.walk(newname, func(dir *memNode, frag string, final bool) error { + if final { + if frag == "" { + return errors.New("pebble/vfs: empty file name") + } + dir.children[frag] = n + n.name = frag + } + return nil + }) +} + +// ReuseForWrite implements FS.ReuseForWrite. +func (y *MemFS) ReuseForWrite(oldname, newname string) (File, error) { + if err := y.Rename(oldname, newname); err != nil { + return nil, err + } + f, err := y.Open(newname) + if err != nil { + return nil, err + } + y.mu.Lock() + defer y.mu.Unlock() + + mf := f.(*memFile) + mf.read = false + mf.write = true + return f, nil +} + +// MkdirAll implements FS.MkdirAll. +func (y *MemFS) MkdirAll(dirname string, perm os.FileMode) error { + return y.walk(dirname, func(dir *memNode, frag string, final bool) error { + if frag == "" { + if final { + return nil + } + return errors.New("pebble/vfs: empty file name") + } + child := dir.children[frag] + if child == nil { + dir.children[frag] = &memNode{ + name: frag, + children: make(map[string]*memNode), + isDir: true, + } + return nil + } + if !child.isDir { + return &os.PathError{ + Op: "open", + Path: dirname, + Err: errors.New("not a directory"), + } + } + return nil + }) +} + +// 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. 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. +func (y *MemFS) List(dirname string) ([]string, error) { + if !strings.HasSuffix(dirname, sep) { + dirname += sep + } + var ret []string + err := y.walk(dirname, func(dir *memNode, frag string, final bool) error { + if final { + if frag != "" { + panic("unreachable") + } + ret = make([]string, 0, len(dir.children)) + for s := range dir.children { + ret = append(ret, s) + } + } + return nil + }) + return ret, err +} + +// Stat implements FS.Stat. +func (y *MemFS) Stat(name string) (os.FileInfo, error) { + f, err := y.Open(name) + if err != nil { + if pe, ok := err.(*os.PathError); ok { + pe.Op = "stat" + } + return nil, err + } + defer f.Close() + return f.Stat() +} + +// PathBase implements FS.PathBase. +func (*MemFS) PathBase(p string) string { + // Note that MemFS uses forward slashes for its separator, hence the use of + // path.Base, not filepath.Base. + return path.Base(p) +} + +// PathJoin implements FS.PathJoin. +func (*MemFS) PathJoin(elem ...string) string { + // Note that MemFS uses forward slashes for its separator, hence the use of + // path.Join, not filepath.Join. + return path.Join(elem...) +} + +// PathDir implements FS.PathDir. +func (*MemFS) PathDir(p string) string { + // Note that MemFS uses forward slashes for its separator, hence the use of + // path.Dir, not filepath.Dir. + return path.Dir(p) +} + +// GetDiskUsage implements FS.GetDiskUsage. +func (*MemFS) GetDiskUsage(string) (DiskUsage, error) { + return DiskUsage{}, ErrUnsupported +} + +// memNode holds a file's data or a directory's children, and implements os.FileInfo. +type memNode struct { + name string + isDir bool + refs atomic.Int32 + + // Mutable state. + // - For a file: data, syncedDate, modTime: A file is only being mutated by a single goroutine, + // but there can be concurrent readers e.g. DB.Checkpoint() which can read WAL or MANIFEST + // files that are being written to. Additionally Sync() calls can be concurrent with writing. + // - For a directory: children and syncedChildren. Concurrent writes are possible, and + // these are protected using MemFS.mu. + mu struct { + sync.Mutex + data []byte + syncedData []byte + modTime time.Time + } + + children map[string]*memNode + syncedChildren map[string]*memNode +} + +func newRootMemNode() *memNode { + return &memNode{ + name: "/", // set the name to match what file systems do + children: make(map[string]*memNode), + isDir: true, + } +} + +func (f *memNode) IsDir() bool { + return f.isDir +} + +func (f *memNode) ModTime() time.Time { + f.mu.Lock() + defer f.mu.Unlock() + return f.mu.modTime +} + +func (f *memNode) Mode() os.FileMode { + if f.isDir { + return os.ModeDir | 0755 + } + return 0755 +} + +func (f *memNode) Name() string { + return f.name +} + +func (f *memNode) Size() int64 { + f.mu.Lock() + defer f.mu.Unlock() + return int64(len(f.mu.data)) +} + +func (f *memNode) Sys() interface{} { + return nil +} + +func (f *memNode) dump(w *bytes.Buffer, level int) { + if f.isDir { + w.WriteString(" ") + } else { + f.mu.Lock() + fmt.Fprintf(w, "%8d ", len(f.mu.data)) + f.mu.Unlock() + } + for i := 0; i < level; i++ { + w.WriteString(" ") + } + w.WriteString(f.name) + if !f.isDir { + w.WriteByte('\n') + return + } + if level > 0 { // deal with the fact that the root's name is already "/" + w.WriteByte(sep[0]) + } + w.WriteByte('\n') + names := make([]string, 0, len(f.children)) + for name := range f.children { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + f.children[name].dump(w, level+1) + } +} + +func (f *memNode) resetToSyncedState() { + if f.isDir { + f.children = make(map[string]*memNode) + for k, v := range f.syncedChildren { + f.children[k] = v + } + for _, v := range f.children { + v.resetToSyncedState() + } + } else { + f.mu.Lock() + f.mu.data = slices.Clone(f.mu.syncedData) + f.mu.Unlock() + } +} + +// memFile is a reader or writer of a node's data, and implements File. +type memFile struct { + n *memNode + fs *MemFS // nil for a standalone memFile + rpos int + wpos int + read, write bool +} + +var _ File = (*memFile)(nil) + +func (f *memFile) Close() error { + if n := f.n.refs.Add(-1); n < 0 { + panic(fmt.Sprintf("pebble: close of unopened file: %d", n)) + } + f.n = nil + return nil +} + +func (f *memFile) Read(p []byte) (int, error) { + if !f.read { + return 0, errors.New("pebble/vfs: file was not opened for reading") + } + if f.n.isDir { + return 0, errors.New("pebble/vfs: cannot read a directory") + } + f.n.mu.Lock() + defer f.n.mu.Unlock() + if f.rpos >= len(f.n.mu.data) { + return 0, io.EOF + } + n := copy(p, f.n.mu.data[f.rpos:]) + f.rpos += n + return n, nil +} + +func (f *memFile) ReadAt(p []byte, off int64) (int, error) { + if !f.read { + return 0, errors.New("pebble/vfs: file was not opened for reading") + } + if f.n.isDir { + return 0, errors.New("pebble/vfs: cannot read a directory") + } + f.n.mu.Lock() + defer f.n.mu.Unlock() + if off >= int64(len(f.n.mu.data)) { + return 0, io.EOF + } + 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) { + 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() + if f.wpos+len(p) <= len(f.n.mu.data) { + n := copy(f.n.mu.data[f.wpos:f.wpos+len(p)], p) + if n != len(p) { + panic("stuff") + } + } else { + f.n.mu.data = append(f.n.mu.data[:f.wpos], p...) + } + f.wpos += len(p) + + if invariants.Enabled { + // Mutate the input buffer to flush out bugs in Pebble which expect the + // input buffer to be unmodified. + for i := range p { + p[i] ^= 0xff + } + } + 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 } + +func (f *memFile) Stat() (os.FileInfo, error) { + return f.n, nil +} + +func (f *memFile) Sync() error { + if f.fs == nil || !f.fs.strict { + return nil + } + f.fs.mu.Lock() + defer f.fs.mu.Unlock() + if f.fs.ignoreSyncs { + return nil + } + if f.n.isDir { + f.n.syncedChildren = make(map[string]*memNode) + for k, v := range f.n.children { + f.n.syncedChildren[k] = v + } + } else { + f.n.mu.Lock() + f.n.mu.syncedData = slices.Clone(f.n.mu.data) + f.n.mu.Unlock() + } + return nil +} + +func (f *memFile) SyncData() error { + return f.Sync() +} + +func (f *memFile) SyncTo(length int64) (fullSync bool, err error) { + // NB: This SyncTo implementation lies, with its return values claiming it + // synced the data up to `length`. When fullSync=false, SyncTo provides no + // durability guarantees, so this can help surface bugs where we improperly + // rely on SyncTo providing durability. + return false, nil +} + +func (f *memFile) Fd() uintptr { + return InvalidFd +} + +// Flush is a no-op and present only to prevent buffering at higher levels +// (e.g. it prevents sstable.Writer from using a bufio.Writer). +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/pebble/vfs/mem_fs_test.go b/pebble/vfs/mem_fs_test.go new file mode 100644 index 0000000..592b392 --- /dev/null +++ b/pebble/vfs/mem_fs_test.go @@ -0,0 +1,460 @@ +// Copyright 2012 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 vfs + +import ( + "fmt" + "io" + "os" + "sort" + "strconv" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/stretchr/testify/require" +) + +func runTestCases(t *testing.T, testCases []string, fs *MemFS) { + var f File + for _, tc := range testCases { + s := strings.Split(tc, " ")[1:] + + saveF := s[0] == "f" && s[1] == "=" + if saveF { + s = s[2:] + } + + fails := s[len(s)-1] == "fails" + if fails { + s = s[:len(s)-1] + } + + var ( + fi os.FileInfo + g File + err error + ) + switch s[0] { + case "create": + g, err = fs.Create(s[1]) + case "link": + err = fs.Link(s[1], s[2]) + case "open": + g, err = fs.Open(s[1]) + case "openDir": + g, err = fs.OpenDir(s[1]) + case "mkdirall": + err = fs.MkdirAll(s[1], 0755) + case "remove": + err = fs.Remove(s[1]) + case "rename": + err = fs.Rename(s[1], s[2]) + case "reuseForWrite": + g, err = fs.ReuseForWrite(s[1], s[2]) + case "resetToSynced": + fs.ResetToSyncedState() + case "ignoreSyncs": + fs.SetIgnoreSyncs(true) + case "stopIgnoringSyncs": + fs.SetIgnoreSyncs(false) + case "f.write": + _, err = f.Write([]byte(s[1])) + case "f.sync": + err = f.Sync() + case "f.read": + n, _ := strconv.Atoi(s[1]) + buf := make([]byte, n) + _, err = io.ReadFull(f, buf) + if err != nil { + break + } + if got, want := string(buf), s[3]; got != want { + t.Fatalf("%q: got %q, want %q", tc, got, want) + } + case "f.readat": + n, _ := strconv.Atoi(s[1]) + off, _ := strconv.Atoi(s[2]) + buf := make([]byte, n) + _, err = f.ReadAt(buf, int64(off)) + if err != nil { + break + } + if got, want := string(buf), s[4]; got != want { + t.Fatalf("%q: got %q, want %q", tc, got, want) + } + case "f.close": + f, err = nil, f.Close() + case "f.stat.name": + fi, err = f.Stat() + if err != nil { + break + } + if got, want := fi.Name(), s[2]; got != want { + t.Fatalf("%q: got %q, want %q", tc, got, want) + } + default: + t.Fatalf("bad test case: %q", tc) + } + + if saveF { + f, g = g, nil + } else if g != nil { + g.Close() + } + + if fails { + if err == nil { + t.Fatalf("%q: got nil error, want non-nil", tc) + } + } else { + if err != nil { + t.Fatalf("%q: %v", tc, err) + } + } + } + + // Both "" and "/" are allowed to be used to refer to the root of the FS + // for the purposes of cloning. + checkClonedIsEquivalent(t, fs, "") + checkClonedIsEquivalent(t, fs, "/") +} + +// Test that the FS can be cloned and that the clone serializes identically. +func checkClonedIsEquivalent(t *testing.T, fs *MemFS, path string) { + t.Helper() + clone := NewMem() + cloned, err := Clone(fs, clone, path, path) + require.NoError(t, err) + require.True(t, cloned) + require.Equal(t, fs.String(), clone.String()) +} + +func TestBasics(t *testing.T) { + fs := NewMem() + testCases := []string{ + // Create a top-level file. + "1a: create /foo", + // Create a child of that file. It should fail, since /foo is not a directory. + "2a: create /foo/x fails", + // Create a third-level file. It should fail, since /bar has not been created. + // Similarly, opening that file should fail. + "3a: create /bar/baz/y fails", + "3b: open /bar/baz/y fails", + // Make the /bar/baz directory; create a third-level file. Creation should now succeed. + "4a: mkdirall /bar/baz", + "4b: f = create /bar/baz/y", + "4c: f.stat.name == y", + // Write some data; read it back. + "5a: f.write abcde", + "5b: f.close", + "5c: f = open /bar/baz/y", + "5d: f.read 5 == abcde", + "5e: f.readat 2 1 == bc", + "5f: f.close", + // Link /bar/baz/y to /bar/baz/z. We should be able to read from both files + // and remove them independently. + "6a: link /bar/baz/y /bar/baz/z", + "6b: f = open /bar/baz/z", + "6c: f.read 5 == abcde", + "6d: f.close", + "6e: remove /bar/baz/z", + "6f: f = open /bar/baz/y", + "6g: f.read 5 == abcde", + "6h: f.close", + // Remove the file twice. The first should succeed, the second should fail. + "7a: remove /bar/baz/y", + "7b: remove /bar/baz/y fails", + "7c: open /bar/baz/y fails", + // Rename /foo to /goo. Trying to open /foo should succeed before the rename and + // fail afterwards, and vice versa for /goo. + "8a: open /foo", + "8b: open /goo fails", + "8c: rename /foo /goo", + "8d: open /foo fails", + "8e: open /goo", + // Create /bar/baz/z and rename /bar/baz to /bar/caz. + "9a: create /bar/baz/z", + "9b: open /bar/baz/z", + "9c: open /bar/caz/z fails", + "9d: rename /bar/baz /bar/caz", + "9e: open /bar/baz/z fails", + "9f: open /bar/caz/z", + // ReuseForWrite + "10a: reuseForWrite /bar/caz/z /bar/z", + "10b: open /bar/caz/z fails", + "10c: open /bar/z", + // Opening the root directory works. + "11a: f = open /", + "11b: f.stat.name == /", + } + runTestCases(t, testCases, fs) +} + +func TestList(t *testing.T) { + fs := NewMem() + + dirnames := []string{ + "/bar", + "/foo/2", + } + for _, dirname := range dirnames { + err := fs.MkdirAll(dirname, 0755) + if err != nil { + t.Fatalf("MkdirAll %q: %v", dirname, err) + } + } + + filenames := []string{ + "/a", + "/bar/baz", + "/foo/0", + "/foo/1", + "/foo/2/a", + "/foo/2/b", + "/foo/3", + "/foot", + } + for _, filename := range filenames { + f, err := fs.Create(filename) + if err != nil { + t.Fatalf("Create %q: %v", filename, err) + } + if err := f.Close(); err != nil { + t.Fatalf("Close %q: %v", filename, err) + } + } + + { + got := fs.String() + const want = ` / + 0 a + bar/ + 0 baz + foo/ + 0 0 + 0 1 + 2/ + 0 a + 0 b + 0 3 + 0 foot +` + if got != want { + t.Fatalf("String:\n----got----\n%s----want----\n%s", got, want) + } + } + + testCases := []string{ + "/:a bar foo foot", + "/bar:baz", + "/bar/:baz", + "/baz:", + "/baz/:", + "/foo:0 1 2 3", + "/foo/:0 1 2 3", + "/foo/1:", + "/foo/1/:", + "/foo/2:a b", + "/foo/2/:a b", + "/foot:", + "/foot/:", + } + for _, tc := range testCases { + s := strings.Split(tc, ":") + list, _ := fs.List(s[0]) + sort.Strings(list) + got := strings.Join(list, " ") + want := s[1] + if got != want { + t.Errorf("List %q: got %q, want %q", s[0], got, want) + } + } +} + +func TestMemFile(t *testing.T) { + want := "foo" + f := NewMemFile([]byte(want)) + buf, err := io.ReadAll(f) + if err != nil { + t.Fatalf("%v", err) + } + if got := string(buf); got != want { + t.Fatalf("got %q, want %q", got, want) + } +} + +func TestStrictFS(t *testing.T) { + fs := NewStrictMem() + testCases := []string{ + // Created file disappears if directory is not synced. + "1a: create /foo", + "1b: open /foo", + "1c: resetToSynced", + "1d: open /foo fails", + + // Create directory and a file in it and write and read from it. + "2a: mkdirall /bar", + "2b: f = create /bar/y", + "2c: f.stat.name == y", + // Write some data; read it back. + "2d: f.write abcde", + "2e: f.close", + "2f: f = open /bar/y", + "2g: f.read 5 == abcde", + "2h: f.close", + "2i: open /bar", + + // Resetting causes both the directory and file to disappear. + "3a: resetToSynced", + "3b: openDir /bar fails", + "3c: open /bar/y fails", + + // Create the directory and file again. Link the file to another file in the same dir, + // and to a file in the root dir. Sync the root dir. After reset, the created dir and the + // file in the root dir are the only ones visible. + "4a: mkdirall /bar", + "4b: create /bar/y", + "4c: f = openDir /", + "4d: f.sync", + "4e: f.close", + "4f: link /bar/y /bar/z", + "4g: link /bar/y /z", + "4h: f = openDir /", + "4i: f.sync", + "4j: f.close", + "4k: resetToSynced", + "4l: openDir /bar", + "4m: open /bar/y fails", + "4n: open /bar/z fails", + "4o: open /z", + + // Create the file in the directory again and this time sync /bar directory. The file is + // preserved after reset. + "5a: create /bar/y", + "5b: f = openDir /bar", + "5c: f.sync", + "5d: f.close", + "5e: resetToSynced", + "5f: openDir /bar", + "5g: open /bar/y", + + // Unsynced data in the file is lost on reset. + "5a: f = create /bar/y", + "5b: f.write a", + "5c: f.sync", + "5d: f.write b", + "5e: f.close", + "5f: f = openDir /bar", + "5g: f.sync", + "5h: f.close", + "5i: resetToSynced", + "5j: f = open /bar/y", + "5k: f.read 1 = a", + "5l: f.read 1 fails", + "5m: f.close", + + // reuseForWrite works correctly in strict mode in that unsynced data does not overwrite + // previous contents when a reset happens. + "6a: f = create /z", + "6b: f.write abcdefgh", + "6c: f.sync", + "6d: f.close", + "6e: f = reuseForWrite /z /y", + "6f: f.write x", + "6g: f.sync", + "6h: f.write y", // will be lost + "6i: f.close", + "6j: f = openDir /", + "6k: f.sync", + "6l: f.close", + "6m: resetToSynced", + "6n: f = open /y", + "6o: f.read 8 = xbcdefgh", + "6p: f.close", + + // Ignore syncs. + "7a: f = create /z", + "7b: f.write a", + "7c: f.sync", + "7d: ignoreSyncs", + "7e: f.write b", + "7f: f.sync", + "7g: f.close", + "7h: stopIgnoringSyncs", + "7e: f = openDir /", + "7f: f.sync", + "7g: f.close", + "7h: resetToSynced", + "7i: f = open /z", + "7j: f.read 1 = a", + "7k: f.read 1 fails", + "7l: f.close", + } + runTestCases(t, testCases, fs) +} + +func TestMemFSLock(t *testing.T) { + filesystems := map[string]FS{} + fileLocks := map[string]io.Closer{} + + datadriven.RunTest(t, "testdata/memfs_lock", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "mkfs": + for _, arg := range td.CmdArgs { + filesystems[arg.String()] = NewMem() + } + return "OK" + + // lock fs= handle= path= + case "lock": + var filesystemName string + var path string + var handle string + td.ScanArgs(t, "fs", &filesystemName) + td.ScanArgs(t, "path", &path) + td.ScanArgs(t, "handle", &handle) + fs := filesystems[filesystemName] + if fs == nil { + return fmt.Sprintf("filesystem %q doesn't exist", filesystemName) + } + l, err := fs.Lock(path) + if err != nil { + return err.Error() + } + fileLocks[handle] = l + return "OK" + + // mkdirall fs= path= + case "mkdirall": + var filesystemName string + var path string + td.ScanArgs(t, "fs", &filesystemName) + td.ScanArgs(t, "path", &path) + fs := filesystems[filesystemName] + if fs == nil { + return fmt.Sprintf("filesystem %q doesn't exist", filesystemName) + } + err := fs.MkdirAll(path, 0755) + if err != nil { + return err.Error() + } + return "OK" + + // close handle= + case "close": + var handle string + td.ScanArgs(t, "handle", &handle) + err := fileLocks[handle].Close() + delete(fileLocks, handle) + if err != nil { + return err.Error() + } + return "OK" + default: + return fmt.Sprintf("unrecognized command %q", td.Cmd) + } + }) +} diff --git a/pebble/vfs/syncing_file.go b/pebble/vfs/syncing_file.go new file mode 100644 index 0000000..cf59862 --- /dev/null +++ b/pebble/vfs/syncing_file.go @@ -0,0 +1,215 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package vfs + +import ( + "sync/atomic" + + "github.com/cockroachdb/errors" +) + +// SyncingFileOptions holds the options for a syncingFile. +type SyncingFileOptions struct { + // NoSyncOnClose elides the automatic Sync during Close if it's not possible + // to sync the remainder of the file in a non-blocking way. + NoSyncOnClose bool + BytesPerSync int + PreallocateSize int +} + +type syncingFile struct { + File + // fd can be InvalidFd if the underlying File does not support it. + fd uintptr + noSyncOnClose bool + bytesPerSync int64 + preallocateSize 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 +} + +// NewSyncingFile wraps a writable file and ensures that data is synced +// periodically as it is written. The syncing does not provide persistency +// guarantees for these periodic syncs, but is used to avoid latency spikes if +// the OS automatically decides to write out a large chunk of dirty filesystem +// buffers. The underlying file is fully synced upon close. +func NewSyncingFile(f File, opts SyncingFileOptions) File { + s := &syncingFile{ + File: f, + fd: f.Fd(), + noSyncOnClose: bool(opts.NoSyncOnClose), + bytesPerSync: int64(opts.BytesPerSync), + preallocateSize: int64(opts.PreallocateSize), + } + // Ensure a file that is opened and then closed will be synced, even if no + // data has been written to it. + 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(f.offset.Load()) + + n, err = f.File.Write(p) + if err != nil { + return n, errors.WithStack(err) + } + // The offset is updated atomically so that it can be accessed safely from + // Sync. + f.offset.Add(int64(n)) + if err := f.maybeSync(); err != nil { + return 0, err + } + return n, nil +} + +func (f *syncingFile) preallocate(offset int64) error { + if f.fd == InvalidFd || f.preallocateSize == 0 { + return nil + } + + newPreallocatedBlocks := (offset + f.preallocateSize - 1) / f.preallocateSize + if newPreallocatedBlocks <= f.preallocatedBlocks { + return nil + } + + length := f.preallocateSize * (newPreallocatedBlocks - f.preallocatedBlocks) + offset = f.preallocateSize * f.preallocatedBlocks + f.preallocatedBlocks = newPreallocatedBlocks + return f.Preallocate(offset, length) +} + +func (f *syncingFile) ratchetSyncOffset(offset int64) { + for { + syncOffset := f.syncOffset.Load() + if syncOffset >= offset { + return + } + if f.syncOffset.CompareAndSwap(syncOffset, offset) { + return + } + } +} + +func (f *syncingFile) Sync() error { + // We update syncOffset (atomically) in order to avoid spurious syncs in + // maybeSync. Note that even if syncOffset is larger than the current file + // 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(f.offset.Load()) + return f.SyncData() +} + +func (f *syncingFile) maybeSync() error { + if f.bytesPerSync <= 0 { + return nil + } + + // From the RocksDB source: + // + // We try to avoid sync to the last 1MB of data. For two reasons: + // (1) avoid rewrite the same page that is modified later. + // (2) for older version of OS, write can block while writing out + // the page. + // 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 := f.offset.Load() + if offset <= syncRangeBuffer { + return nil + } + + const syncRangeAlignment = 4 << 10 // 4 KB + syncToOffset := offset - syncRangeBuffer + syncToOffset -= syncToOffset % syncRangeAlignment + syncOffset := f.syncOffset.Load() + if syncToOffset < 0 || (syncToOffset-syncOffset) < f.bytesPerSync { + return nil + } + + if f.fd == InvalidFd { + return errors.WithStack(f.Sync()) + } + + // Note that SyncTo will always be called with an offset < atomic.offset. + // The SyncTo implementation may choose to sync the entire file (i.e. on + // OSes which do not support syncing a portion of the file). + fullSync, err := f.SyncTo(syncToOffset) + if err != nil { + return errors.WithStack(err) + } + if fullSync { + f.ratchetSyncOffset(offset) + } else { + f.ratchetSyncOffset(syncToOffset) + } + return nil +} + +func (f *syncingFile) Close() error { + // Sync any data that has been written but not yet synced unless the file + // has noSyncOnClose option explicitly set. + // + // NB: If the file is capable of non-durability-guarantee SyncTos, and the + // 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 := f.offset.Load(); off > f.syncOffset.Load() { + // There's still remaining dirty data. + + if f.noSyncOnClose { + // If NoSyncOnClose is set, only perform a SyncTo. On linux, SyncTo + // translates to a non-blocking `sync_file_range` call which + // provides no persistence guarantee. Since it's non-blocking, + // there's no latency hit of a blocking sync call, but we still + // ensure we're not allowing significant dirty data to accumulate. + if _, err := f.File.SyncTo(off); err != nil { + return err + } + f.ratchetSyncOffset(off) + } else if err := f.Sync(); err != nil { + return errors.WithStack(err) + } + } + return errors.WithStack(f.File.Close()) +} + +// NewSyncingFS wraps a vfs.FS with one that wraps newly created files with +// vfs.NewSyncingFile. +func NewSyncingFS(fs FS, syncOpts SyncingFileOptions) FS { + return &syncingFS{ + FS: fs, + syncOpts: syncOpts, + } +} + +type syncingFS struct { + FS + syncOpts SyncingFileOptions +} + +var _ FS = (*syncingFS)(nil) + +func (fs *syncingFS) Create(name string) (File, error) { + f, err := fs.FS.Create(name) + if err != nil { + return nil, err + } + return NewSyncingFile(f, fs.syncOpts), nil +} + +func (fs *syncingFS) ReuseForWrite(oldname, newname string) (File, error) { + // TODO(radu): implement this if needed. + panic("unimplemented") +} diff --git a/pebble/vfs/syncing_file_linux_test.go b/pebble/vfs/syncing_file_linux_test.go new file mode 100644 index 0000000..83458a3 --- /dev/null +++ b/pebble/vfs/syncing_file_linux_test.go @@ -0,0 +1,107 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +//go:build linux && !arm +// +build linux,!arm + +package vfs + +import ( + "fmt" + "os" + "syscall" + "testing" + "unsafe" +) + +func TestSyncRangeSmokeTest(t *testing.T) { + testCases := []struct { + err error + expected bool + }{ + {nil, true}, + {syscall.EINVAL, true}, + {syscall.ENOSYS, false}, + } + for i, c := range testCases { + t.Run("", func(t *testing.T) { + ok := syncRangeSmokeTest(uintptr(i), + func(fd int, off int64, n int64, flags int) (err error) { + if i != fd { + t.Fatalf("expected fd %d, but got %d", i, fd) + } + return c.err + }) + if c.expected != ok { + t.Fatalf("expected %t, but got %t: %v", c.expected, ok, c.err) + } + }) + } +} + +func BenchmarkDirectIOWrite(b *testing.B) { + const targetSize = 16 << 20 + const alignment = 4096 + + var wsizes []int + if testing.Verbose() { + wsizes = []int{4 << 10, 8 << 10, 16 << 10, 32 << 10} + } else { + wsizes = []int{4096} + } + + for _, wsize := range wsizes { + b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) { + tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-") + if err != nil { + b.Fatal(err) + } + filename := tmpf.Name() + _ = tmpf.Close() + defer os.Remove(filename) + + var f *os.File + var size int + buf := make([]byte, wsize+alignment) + if a := uintptr(unsafe.Pointer(&buf[0])) & uintptr(alignment-1); a != 0 { + buf = buf[alignment-a:] + } + buf = buf[:wsize] + init := true + + b.SetBytes(int64(len(buf))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if f == nil { + b.StopTimer() + f, err = os.OpenFile(filename, syscall.O_DIRECT|os.O_RDWR, 0666) + if err != nil { + b.Fatal(err) + } + if init { + for size = 0; size < targetSize; size += len(buf) { + if _, err := f.WriteAt(buf, int64(size)); err != nil { + b.Fatal(err) + } + } + } + if err := f.Sync(); err != nil { + b.Fatal(err) + } + size = 0 + b.StartTimer() + } + if _, err := f.WriteAt(buf, int64(size)); err != nil { + b.Fatal(err) + } + size += len(buf) + if size >= targetSize { + _ = f.Close() + f = nil + } + } + b.StopTimer() + }) + } +} diff --git a/pebble/vfs/syncing_file_test.go b/pebble/vfs/syncing_file_test.go new file mode 100644 index 0000000..3a49bdc --- /dev/null +++ b/pebble/vfs/syncing_file_test.go @@ -0,0 +1,292 @@ +// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use +// of this source code is governed by a BSD-style license that can be found in +// the LICENSE file. + +package vfs + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSyncingFile(t *testing.T) { + const mb = 1 << 20 + + tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-") + require.NoError(t, err) + + filename := tmpf.Name() + require.NoError(t, tmpf.Close()) + defer os.Remove(filename) + + f, err := Default.Create(filename) + require.NoError(t, err) + + tf := &mockSyncToFile{File: f, canSyncTo: true} + sf := NewSyncingFile(tf, SyncingFileOptions{BytesPerSync: 8 << 10 /* 8 KB */}) + sf.(*syncingFile).fd = 1 + testCases := []struct { + n int64 + expectedSyncTo int64 + }{ + {mb, -1}, + {mb, mb}, + {4 << 10, mb}, + {4 << 10, mb + 8<<10}, + {8 << 10, mb + 16<<10}, + {16 << 10, mb + 32<<10}, + } + for i, c := range testCases { + _, err := sf.Write(make([]byte, c.n)) + require.NoError(t, err) + + syncTo := sf.(*syncingFile).syncOffset.Load() + if c.expectedSyncTo != syncTo { + t.Fatalf("%d: expected sync to %d, but found %d", i, c.expectedSyncTo, syncTo) + } + } +} + +func TestSyncingFileClose(t *testing.T) { + testCases := []struct { + canSyncTo bool + expected string + }{ + {true, `sync-to(1048576): test [false,] +sync-to(2097152): test [false,] +sync-to(3145728): test [false,] +pre-close: test [offset=4194304 sync-offset=3145728] +sync-data: test [] +close: test [] +`}, + // When SyncTo is not being used, the last sync call ends up syncing all + // of the data causing syncingFile.Close to elide the sync. + {false, `sync-to(1048576): test [true,] +sync-to(3145728): test [true,] +pre-close: test [offset=4194304 sync-offset=4194304] +close: test [] +`}, + } + for _, c := range testCases { + t.Run(fmt.Sprintf("canSyncTo=%t", c.canSyncTo), func(t *testing.T) { + tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-") + require.NoError(t, err) + + filename := tmpf.Name() + require.NoError(t, tmpf.Close()) + defer os.Remove(filename) + + f, err := Default.Create(filename) + require.NoError(t, err) + + var buf bytes.Buffer + tf := &mockSyncToFile{File: f, canSyncTo: c.canSyncTo} + lf := &vfsTestFSFile{tf, "test", &buf} + s := NewSyncingFile(lf, SyncingFileOptions{BytesPerSync: 8 << 10 /* 8 KB */}).(*syncingFile) + + write := func(n int64) { + t.Helper() + _, err := s.Write(make([]byte, n)) + require.NoError(t, err) + } + + const mb = 1 << 20 + write(2 * mb) + write(mb) + write(mb) + + fmt.Fprintf(&buf, "pre-close: %s [offset=%d sync-offset=%d]\n", + lf.name, s.offset.Load(), s.syncOffset.Load()) + require.NoError(t, s.Close()) + + if s := buf.String(); c.expected != s { + t.Fatalf("expected\n%s\nbut found\n%s", c.expected, s) + } + }) + } +} + +type mockSyncToFile struct { + File + canSyncTo bool +} + +func (f *mockSyncToFile) SyncTo(length int64) (fullSync bool, err error) { + if !f.canSyncTo { + if err = f.File.SyncData(); err != nil { + return false, err + } + return true, nil + } + // f.canSyncTo = true + if _, err = f.File.SyncTo(length); err != nil { + return false, err + } + // NB: If the underlying file performed a full sync, lie. + return false, nil +} + +func TestSyncingFileNoSyncOnClose(t *testing.T) { + testCases := []struct { + useSyncTo bool + expectBefore int64 + expectAfter int64 + }{ + {false, 2 << 20, 3<<20 + 128}, + {true, 2 << 20, 3<<20 + 128}, + } + + for _, c := range testCases { + t.Run(fmt.Sprintf("useSyncTo=%v", c.useSyncTo), func(t *testing.T) { + tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-") + require.NoError(t, err) + + filename := tmpf.Name() + require.NoError(t, tmpf.Close()) + defer os.Remove(filename) + + f, err := Default.Create(filename) + require.NoError(t, err) + + tf := &mockSyncToFile{f, c.useSyncTo} + s := NewSyncingFile(tf, SyncingFileOptions{NoSyncOnClose: true, BytesPerSync: 8 << 10}).(*syncingFile) + + write := func(n int64) { + t.Helper() + _, err := s.Write(make([]byte, n)) + require.NoError(t, err) + } + + const mb = 1 << 20 + write(2 * mb) // Sync first 2MB + write(mb) // No sync because syncToOffset = 3M-1M = 2M + write(128) // No sync for the same reason + + syncToBefore := s.syncOffset.Load() + require.NoError(t, s.Close()) + syncToAfter := s.syncOffset.Load() + + // If we're not able to non-blockingly sync using sync-to, + // NoSyncOnClose should elide the sync. + if !c.useSyncTo { + if syncToBefore != c.expectBefore || syncToAfter != c.expectAfter { + t.Fatalf("Expected syncTo before and after closing are %d %d but found %d %d", + c.expectBefore, c.expectAfter, syncToBefore, syncToAfter) + } + } + }) + } +} + +func BenchmarkSyncWrite(b *testing.B) { + const targetSize = 16 << 20 + + var wsizes []int + if testing.Verbose() { + wsizes = []int{64, 512, 1 << 10, 2 << 10, 4 << 10, 8 << 10, 16 << 10, 32 << 10} + } else { + wsizes = []int{64} + } + + run := func(b *testing.B, wsize int, newFile func(string) File) { + tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-") + if err != nil { + b.Fatal(err) + } + filename := tmpf.Name() + _ = tmpf.Close() + defer os.Remove(filename) + + var f File + var size int + buf := make([]byte, wsize) + + b.SetBytes(int64(len(buf))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if f == nil { + b.StopTimer() + f = newFile(filename) + size = 0 + b.StartTimer() + } + if _, err := f.Write(buf); err != nil { + b.Fatal(err) + } + if err := f.Sync(); err != nil { + b.Fatal(err) + } + size += len(buf) + if size >= targetSize { + _ = f.Close() + f = nil + } + } + b.StopTimer() + } + + b.Run("no-prealloc", func(b *testing.B) { + for _, wsize := range wsizes { + b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) { + run(b, wsize, func(filename string) File { + _ = os.Remove(filename) + t, err := os.Create(filename) + if err != nil { + b.Fatal(err) + } + return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 0}) + }) + }) + } + }) + + b.Run("prealloc-4MB", func(b *testing.B) { + for _, wsize := range wsizes { + b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) { + run(b, wsize, func(filename string) File { + _ = os.Remove(filename) + t, err := os.Create(filename) + if err != nil { + b.Fatal(err) + } + return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 4 << 20}) + }) + }) + } + }) + + b.Run("reuse", func(b *testing.B) { + for _, wsize := range wsizes { + b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) { + init := true + run(b, wsize, func(filename string) File { + if init { + init = false + + t, err := os.OpenFile(filename, os.O_RDWR, 0755) + if err != nil { + b.Fatal(err) + } + if _, err := t.Write(make([]byte, targetSize)); err != nil { + b.Fatal(err) + } + if err := t.Sync(); err != nil { + b.Fatal(err) + } + t.Close() + } + + t, err := os.OpenFile(filename, os.O_RDWR, 0755) + if err != nil { + b.Fatal(err) + } + return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 0}) + }) + }) + } + }) +} diff --git a/pebble/vfs/testdata/memfs_lock b/pebble/vfs/testdata/memfs_lock new file mode 100644 index 0000000..0ac2655 --- /dev/null +++ b/pebble/vfs/testdata/memfs_lock @@ -0,0 +1,55 @@ +mkfs A B +---- +OK + +# +# Locking a path with parents that don't exist should error. +# + +lock fs=A path=a/b/c handle=fsApathABC +---- +open a/b/c: file does not exist + +# +# If we create the parents, it should succeed. +# + +mkdirall fs=A path=a/b +---- +OK + +lock fs=A path=a/b/c handle=fsApathABC +---- +OK + +# +# Locking the same path on the same filesystem should fail with EAGAIN. +# + +lock fs=A path=a/b/c handle=bogus +---- +resource temporarily unavailable + +# +# Locking the same path on a DIFFERENT filesystem should succeed. +# + +mkdirall fs=B path=a/b +---- +OK + +lock fs=B path=a/b/c handle=fsBpathABC +---- +OK + +# +# Releasing the lock on fs A should allow us to reacquire it. +# + +close handle=fsApathABC +---- +OK + +lock fs=A path=a/b/c handle=fsApathABC +---- +OK diff --git a/pebble/vfs/testdata/vfs b/pebble/vfs/testdata/vfs new file mode 100644 index 0000000..450377e --- /dev/null +++ b/pebble/vfs/testdata/vfs @@ -0,0 +1,188 @@ +define +link a b +create a +link a b +link a b +link c d +remove b +link-or-copy a b +remove b +---- +link: a -> b [file does not exist] +create: a [] +close: a [] +link: a -> b [] +link: a -> b [file already exists] +link: c -> d [file does not exist] +remove: b [] +link: a -> b [] +remove: b [] + +define linkErr=ErrExist +create a +link a b +link-or-copy a b +---- +create: a [] +close: a [] +link: a -> b [file already exists] +link: a -> b [file already exists] + +define linkErr=ErrNotExist +create a +link a b +link-or-copy a b +---- +create: a [] +close: a [] +link: a -> b [file does not exist] +link: a -> b [file does not exist] + +define linkErr=ErrPermission +create a +link a b +link-or-copy a b +---- +create: a [] +close: a [] +link: a -> b [permission denied] +link: a -> b [permission denied] + +define linkErr=random +create a +link a b +link-or-copy a b +---- +create: a [] +close: a [] +link: a -> b [random] +link: a -> b [random] +open: a [] +create: b [] +sync: b [] +close: b [] +close: a [] + +define +mkdir d +create d/a +mkdir d/b +create d/b/c +---- +mkdir: d [] +create: d/a [] +close: a [] +mkdir: d/b [] +create: d/b/c [] +close: c [] + +# NB: This clone does not specify a destination FS, so the clone target will be +# the same FS. This results in the use of link. + +define +clone d e link +---- +open: d [] +mkdir: e [] +open: d/a [] +link: d/a -> e/a [random] +open: d/a [] +create: e/a [] +sync: a [] +close: a [] +close: d/a [] +close: d/a [] +open: d/b [] +mkdir: e/b [] +open: d/b/c [] +link: d/b/c -> e/b/c [random] +open: d/b/c [] +create: e/b/c [] +sync: c [] +close: c [] +close: d/b/c [] +close: d/b/c [] +close: d/b [] +close: d [] + +define +list e +---- +a +b + +define +list e/b +---- +c + +define +list / +remove e +remove-all e +remove-all e +remove-all e/a/b/c +list / +---- +a +b +d +e +remove: e [file already exists] +remove-all: e [] +remove-all: e [] +remove-all: e/a/b/c [] +a +b +d + +define +reuseForWrite a b +reuseForWrite x y +---- +reuseForWrite: a -> b [] +reuseForWrite: x -> y [file does not exist] + +# NB: This clone target specified a different FS. This results in no use of +# link, despite link being provided. + +define +clone d f mem link +---- +open: d [] +mkdir: f [] +open: d/a [] +create: f/a [] +close: a [] +close: d/a [] +open: d/b [] +mkdir: f/b [] +open: d/b/c [] +create: f/b/c [] +close: c [] +close: d/b/c [] +close: d/b [] +close: d [] + +# NB: This clone does not specify link, so all files are copied. It does specify +# sync, so all files and directories are synced. + +define +clone d g sync +---- +open: d [] +mkdir: g [] +open: d/a [] +create: g/a [] +sync: a [] +close: a [] +close: d/a [] +open: d/b [] +mkdir: g/b [] +open: d/b/c [] +create: g/b/c [] +sync: c [] +close: c [] +close: d/b/c [] +close: d/b [] +close: d [] diff --git a/pebble/vfs/vfs.go b/pebble/vfs/vfs.go new file mode 100644 index 0000000..3cc0b43 --- /dev/null +++ b/pebble/vfs/vfs.go @@ -0,0 +1,419 @@ +// Copyright 2012 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 vfs + +import ( + "io" + "os" + "path/filepath" + "syscall" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" +) + +// File is a readable, writable sequence of bytes. +// +// Typically, it will be an *os.File, but test code may choose to substitute +// memory-backed implementations. +// +// Write-oriented operations (Write, Sync) must be called sequentially: At most +// 1 call to Write or Sync may be executed at any given time. +type File interface { + io.Closer + io.Reader + io.ReaderAt + // Unlike the specification for io.Writer.Write(), the vfs.File.Write() + // 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. + Preallocate(offset, length int64) error + Stat() (os.FileInfo, error) + Sync() error + + // SyncTo requests that a prefix of the file's data be synced to stable + // storage. The caller passes provides a `length`, indicating how many bytes + // to sync from the beginning of the file. SyncTo is a no-op for + // directories, and therefore always returns false. + // + // SyncTo returns a fullSync return value, indicating one of two possible + // outcomes. + // + // If fullSync is false, the first `length` bytes of the file was queued to + // be synced to stable storage. The syncing of the file prefix may happen + // asynchronously. No persistence guarantee is provided. + // + // If fullSync is true, the entirety of the file's contents were + // synchronously synced to stable storage, and a persistence guarantee is + // provided. In this outcome, any modified metadata for the file is not + // guaranteed to be synced unless that metadata is needed in order to allow + // a subsequent data retrieval to be correctly handled. + SyncTo(length int64) (fullSync bool, err error) + + // SyncData requires that all written data be persisted. File metadata is + // not required to be synced. Unsophisticated implementations may call Sync. + SyncData() error + + // Prefetch signals the OS (on supported platforms) to fetch the next length + // bytes in file (as returned by os.File.Fd()) after offset into cache. Any + // subsequent reads in that range will not issue disk IO. + Prefetch(offset int64, length int64) error + + // Fd returns the raw file descriptor when a File is backed by an *os.File. + // It can be used for specific functionality like Prefetch. + // Returns InvalidFd if not supported. + Fd() uintptr +} + +// InvalidFd is a special value returned by File.Fd() when the file is not +// backed by an OS descriptor. +// Note: the special value is consistent with what os.File implementation +// returns on a nil receiver. +const InvalidFd uintptr = ^(uintptr(0)) + +// OpenOption provide an interface to do work on file handles in the Open() +// call. +type OpenOption interface { + // Apply is called on the file handle after it's opened. + Apply(File) +} + +// FS is a namespace for files. +// +// The names are filepath names: they may be / separated or \ separated, +// depending on the underlying operating system. +type FS interface { + // Create creates the named file for reading and writing. If a file + // already exists at the provided name, it's removed first ensuring the + // resulting file descriptor points to a new inode. + Create(name string) (File, error) + + // Link creates newname as a hard link to the oldname file. + Link(oldname, newname string) error + + // 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) + + // Remove removes the named file or directory. + Remove(name string) error + + // Remove removes the named file or directory and any children it + // contains. It removes everything it can but returns the first error it + // encounters. + RemoveAll(name string) error + + // Rename renames a file. It overwrites the file at newname if one exists, + // the same as os.Rename. + Rename(oldname, newname string) error + + // ReuseForWrite attempts to reuse the file with oldname by renaming it to newname and opening + // it for writing without truncation. It is acceptable for the implementation to choose not + // to reuse oldname, and simply create the file with newname -- in this case the implementation + // should delete oldname. If the caller calls this function with an oldname that does not exist, + // the implementation may return an error. + ReuseForWrite(oldname, newname string) (File, error) + + // MkdirAll creates a directory and all necessary parents. The permission + // bits perm have the same semantics as in os.MkdirAll. If the directory + // already exists, MkdirAll does nothing and returns nil. + MkdirAll(dir string, perm os.FileMode) error + + // Lock locks the given file, creating the file if necessary, and + // truncating the file if it already exists. The lock is an exclusive lock + // (a write lock), but locked files should neither be read from nor written + // to. Such files should have zero size and only exist to co-ordinate + // ownership across processes. + // + // A nil Closer is returned if an error occurred. Otherwise, close that + // Closer to release the lock. + // + // On Linux and OSX, a lock has the same semantics as fcntl(2)'s advisory + // locks. In particular, closing any other file descriptor for the same + // file will release the lock prematurely. + // + // Attempting to lock a file that is already locked by the current process + // returns an error and leaves the existing lock untouched. + // + // Lock is not yet implemented on other operating systems, and calling it + // will return an error. + Lock(name string) (io.Closer, error) + + // List returns a listing of the given directory. The names returned are + // relative to dir. + List(dir string) ([]string, error) + + // Stat returns an os.FileInfo describing the named file. + Stat(name string) (os.FileInfo, error) + + // PathBase returns the last element of path. Trailing path separators are + // removed before extracting the last element. If the path is empty, PathBase + // returns ".". If the path consists entirely of separators, PathBase returns a + // single separator. + PathBase(path string) string + + // PathJoin joins any number of path elements into a single path, adding a + // separator if necessary. + PathJoin(elem ...string) string + + // PathDir returns all but the last element of path, typically the path's directory. + PathDir(path string) string + + // GetDiskUsage returns disk space statistics for the filesystem where + // path is any file or directory within that filesystem. + GetDiskUsage(path string) (DiskUsage, error) +} + +// DiskUsage summarizes disk space usage on a filesystem. +type DiskUsage struct { + // Total disk space available to the current process in bytes. + AvailBytes uint64 + // Total disk space in bytes. + TotalBytes uint64 + // Used disk space in bytes. + UsedBytes uint64 +} + +// Default is a FS implementation backed by the underlying operating system's +// file system. +var Default FS = defaultFS{} + +type defaultFS struct{} + +// wrapOSFile takes a standard library OS file and returns a vfs.File. f may be +// nil, in which case wrapOSFile must not panic. In such cases, it's okay if the +// returned vfs.File may panic if used. +func wrapOSFile(f *os.File) File { + // See the implementations in default_{linux,unix,windows}.go. + return wrapOSFileImpl(f) +} + +func (defaultFS) Create(name string) (File, error) { + const openFlags = os.O_RDWR | os.O_CREATE | os.O_EXCL | syscall.O_CLOEXEC + + osFile, err := os.OpenFile(name, openFlags, 0666) + // If the file already exists, remove it and try again. + // + // NB: We choose to remove the file instead of truncating it, despite the + // fact that we can't do so atomically, because it's more resistant to + // misuse when using hard links. + + // We must loop in case another goroutine/thread/process is also + // attempting to create the a file at the same path. + for oserror.IsExist(err) { + if removeErr := os.Remove(name); removeErr != nil && !oserror.IsNotExist(removeErr) { + return wrapOSFile(osFile), errors.WithStack(removeErr) + } + osFile, err = os.OpenFile(name, openFlags, 0666) + } + return wrapOSFile(osFile), errors.WithStack(err) +} + +func (defaultFS) Link(oldname, newname string) error { + return errors.WithStack(os.Link(oldname, newname)) +} + +func (defaultFS) Open(name string, opts ...OpenOption) (File, error) { + osFile, err := os.OpenFile(name, os.O_RDONLY|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, errors.WithStack(err) + } + file := wrapOSFile(osFile) + for _, opt := range opts { + opt.Apply(file) + } + 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)) +} + +func (defaultFS) RemoveAll(name string) error { + return errors.WithStack(os.RemoveAll(name)) +} + +func (defaultFS) Rename(oldname, newname string) error { + return errors.WithStack(os.Rename(oldname, newname)) +} + +func (fs defaultFS) ReuseForWrite(oldname, newname string) (File, error) { + if err := fs.Rename(oldname, newname); err != nil { + return nil, errors.WithStack(err) + } + f, err := os.OpenFile(newname, os.O_RDWR|os.O_CREATE|syscall.O_CLOEXEC, 0666) + return wrapOSFile(f), errors.WithStack(err) +} + +func (defaultFS) MkdirAll(dir string, perm os.FileMode) error { + return errors.WithStack(os.MkdirAll(dir, perm)) +} + +func (defaultFS) List(dir string) ([]string, error) { + f, err := os.Open(dir) + if err != nil { + return nil, err + } + defer f.Close() + dirnames, err := f.Readdirnames(-1) + return dirnames, errors.WithStack(err) +} + +func (defaultFS) Stat(name string) (os.FileInfo, error) { + finfo, err := os.Stat(name) + return finfo, errors.WithStack(err) +} + +func (defaultFS) PathBase(path string) string { + return filepath.Base(path) +} + +func (defaultFS) PathJoin(elem ...string) string { + return filepath.Join(elem...) +} + +func (defaultFS) PathDir(path string) string { + return filepath.Dir(path) +} + +type randomReadsOption struct{} + +// RandomReadsOption is an OpenOption that optimizes opened file handle for +// random reads, by calling fadvise() with POSIX_FADV_RANDOM on Linux systems +// to disable readahead. +var RandomReadsOption OpenOption = &randomReadsOption{} + +// Apply implements the OpenOption interface. +func (randomReadsOption) Apply(f File) { + if fd := f.Fd(); fd != InvalidFd { + _ = fadviseRandom(fd) + } +} + +type sequentialReadsOption struct{} + +// SequentialReadsOption is an OpenOption that optimizes opened file handle for +// sequential reads, by calling fadvise() with POSIX_FADV_SEQUENTIAL on Linux +// systems to enable readahead. +var SequentialReadsOption OpenOption = &sequentialReadsOption{} + +// Apply implements the OpenOption interface. +func (sequentialReadsOption) Apply(f File) { + if fd := f.Fd(); fd != InvalidFd { + _ = fadviseSequential(fd) + } +} + +// Copy copies the contents of oldname to newname. If newname exists, it will +// be overwritten. +func Copy(fs FS, oldname, newname string) error { + return CopyAcrossFS(fs, oldname, fs, newname) +} + +// CopyAcrossFS copies the contents of oldname on srcFS to newname dstFS. If +// newname exists, it will be overwritten. +func CopyAcrossFS(srcFS FS, oldname string, dstFS FS, newname string) error { + src, err := srcFS.Open(oldname, SequentialReadsOption) + if err != nil { + return err + } + defer src.Close() + + dst, err := dstFS.Create(newname) + if err != nil { + return err + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return err + } + return dst.Sync() +} + +// LimitedCopy copies up to maxBytes from oldname to newname. If newname +// exists, it will be overwritten. +func LimitedCopy(fs FS, oldname, newname string, maxBytes int64) error { + src, err := fs.Open(oldname, SequentialReadsOption) + if err != nil { + return err + } + defer src.Close() + + dst, err := fs.Create(newname) + if err != nil { + return err + } + defer dst.Close() + + if _, err := io.Copy(dst, &io.LimitedReader{R: src, N: maxBytes}); err != nil { + return err + } + return dst.Sync() +} + +// LinkOrCopy creates newname as a hard link to the oldname file. If creating +// the hard link fails, LinkOrCopy falls back to copying the file (which may +// also fail if oldname doesn't exist or newname already exists). +func LinkOrCopy(fs FS, oldname, newname string) error { + err := fs.Link(oldname, newname) + if err == nil { + return nil + } + // Permit a handful of errors which we know won't be fixed by copying the + // file. Note that we don't check for the specifics of the error code as it + // isn't easy to do so in a portable manner. On Unix we'd have to check for + // LinkError.Err == syscall.EXDEV. On Windows we'd have to check for + // ERROR_NOT_SAME_DEVICE, ERROR_INVALID_FUNCTION, and + // ERROR_INVALID_PARAMETER. Rather that such OS specific checks, we fall back + // to always trying to copy if hard-linking failed. + if oserror.IsExist(err) || oserror.IsNotExist(err) || oserror.IsPermission(err) { + return err + } + return Copy(fs, oldname, newname) +} + +// Root returns the base FS implementation, unwrapping all nested FSs that +// expose an Unwrap method. +func Root(fs FS) FS { + type unwrapper interface { + Unwrap() FS + } + + for { + u, ok := fs.(unwrapper) + if !ok { + break + } + fs = u.Unwrap() + } + return fs +} + +// ErrUnsupported may be returned a FS when it does not support an operation. +var ErrUnsupported = errors.New("pebble: not supported") diff --git a/pebble/vfs/vfs_test.go b/pebble/vfs/vfs_test.go new file mode 100644 index 0000000..39082fc --- /dev/null +++ b/pebble/vfs/vfs_test.go @@ -0,0 +1,361 @@ +// Copyright 2012 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 vfs + +import ( + "bytes" + "fmt" + "io" + "os" + "runtime" + "sort" + "strings" + "testing" + + "github.com/cockroachdb/datadriven" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "github.com/stretchr/testify/require" +) + +func normalizeError(err error) error { + // It is OS-specific which errors match IsExist, IsNotExist, and + // IsPermission, with OS-specific error messages. We normalize to the + // oserror.Err* errors which have standard error messages across + // platforms. + switch { + case oserror.IsExist(err): + return oserror.ErrExist + case oserror.IsNotExist(err): + return oserror.ErrNotExist + case oserror.IsPermission(err): + return oserror.ErrPermission + } + return err +} + +// vfsTestFS is similar to loggingFS but is more specific to the vfs test. It +// logs more operations and logs return values and errors. +// It also supports injecting an error on Link. +type vfsTestFS struct { + FS + base string + w io.Writer + linkErr error +} + +func (fs vfsTestFS) stripBase(path string) string { + if strings.HasPrefix(path, fs.base+"/") { + return path[len(fs.base)+1:] + } + return path +} + +func (fs vfsTestFS) Create(name string) (File, error) { + f, err := fs.FS.Create(name) + fmt.Fprintf(fs.w, "create: %s [%v]\n", fs.stripBase(name), normalizeError(err)) + return vfsTestFSFile{f, fs.PathBase(name), fs.w}, err +} + +func (fs vfsTestFS) Link(oldname, newname string) error { + err := fs.linkErr + if err == nil { + err = fs.FS.Link(oldname, newname) + } + fmt.Fprintf(fs.w, "link: %s -> %s [%v]\n", + fs.stripBase(oldname), fs.stripBase(newname), normalizeError(err)) + return err +} + +func (fs vfsTestFS) ReuseForWrite(oldname, newname string) (File, error) { + f, err := fs.FS.ReuseForWrite(oldname, newname) + if err == nil { + f = vfsTestFSFile{f, fs.PathBase(newname), fs.w} + } + fmt.Fprintf(fs.w, "reuseForWrite: %s -> %s [%v]\n", + fs.stripBase(oldname), fs.stripBase(newname), normalizeError(err)) + return f, err +} + +func (fs vfsTestFS) MkdirAll(dir string, perm os.FileMode) error { + err := fs.FS.MkdirAll(dir, perm) + fmt.Fprintf(fs.w, "mkdir: %s [%v]\n", fs.stripBase(dir), normalizeError(err)) + return err +} + +func (fs vfsTestFS) Open(name string, opts ...OpenOption) (File, error) { + f, err := fs.FS.Open(name, opts...) + fmt.Fprintf(fs.w, "open: %s [%v]\n", fs.stripBase(name), normalizeError(err)) + return vfsTestFSFile{f, fs.stripBase(name), fs.w}, err +} + +func (fs vfsTestFS) Remove(name string) error { + err := fs.FS.Remove(name) + fmt.Fprintf(fs.w, "remove: %s [%v]\n", fs.stripBase(name), normalizeError(err)) + return err +} + +func (fs vfsTestFS) RemoveAll(name string) error { + err := fs.FS.RemoveAll(name) + fmt.Fprintf(fs.w, "remove-all: %s [%v]\n", fs.stripBase(name), normalizeError(err)) + return err +} + +type vfsTestFSFile struct { + File + name string + w io.Writer +} + +func (f vfsTestFSFile) Close() error { + err := f.File.Close() + fmt.Fprintf(f.w, "close: %s [%v]\n", f.name, err) + return err +} + +func (f vfsTestFSFile) Preallocate(off, n int64) error { + err := f.File.Preallocate(off, n) + fmt.Fprintf(f.w, "preallocate(off=%d,n=%d): %s [%v]\n", off, n, f.name, err) + return err +} + +func (f vfsTestFSFile) Sync() error { + err := f.File.Sync() + fmt.Fprintf(f.w, "sync: %s [%v]\n", f.name, err) + return err +} + +func (f vfsTestFSFile) SyncData() error { + err := f.File.SyncData() + fmt.Fprintf(f.w, "sync-data: %s [%v]\n", f.name, err) + return err +} + +func (f vfsTestFSFile) SyncTo(length int64) (fullSync bool, err error) { + fullSync, err = f.File.SyncTo(length) + fmt.Fprintf(f.w, "sync-to(%d): %s [%t,%v]\n", length, f.name, fullSync, err) + return fullSync, err +} + +func runTestVFS(t *testing.T, baseFS FS, dir string) { + var buf bytes.Buffer + fs := vfsTestFS{FS: baseFS, base: dir, w: &buf} + + datadriven.RunTest(t, "testdata/vfs", func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "define": + buf.Reset() + + for _, arg := range td.CmdArgs { + switch arg.Key { + case "linkErr": + if len(arg.Vals) != 1 { + return fmt.Sprintf("%s: %s expected 1 value", td.Cmd, arg.Key) + } + switch arg.Vals[0] { + case "ErrExist": + fs.linkErr = oserror.ErrExist + case "ErrNotExist": + fs.linkErr = oserror.ErrNotExist + case "ErrPermission": + fs.linkErr = oserror.ErrPermission + default: + fs.linkErr = errors.New(arg.Vals[0]) + } + default: + return fmt.Sprintf("%s: unknown arg: %s", td.Cmd, arg.Key) + } + } + + for _, line := range strings.Split(td.Input, "\n") { + parts := strings.Fields(line) + if len(parts) == 0 { + return " []" + } + + switch parts[0] { + case "clone": + if len(parts) < 3 { + return "clone [disk|mem] [link] [sync]" + } + dstFS := fs + var opts []CloneOption + for _, p := range parts[3:] { + switch p { + case "disk": + dstFS = vfsTestFS{FS: Default, base: dir, w: &buf} + case "mem": + dstFS = vfsTestFS{FS: NewMem(), base: dir, w: &buf} + case "link": + opts = append(opts, CloneTryLink) + case "sync": + opts = append(opts, CloneSync) + default: + return fmt.Sprintf("unrecognized argument %q", p) + } + } + + _, _ = Clone(fs, dstFS, fs.PathJoin(dir, parts[1]), fs.PathJoin(dir, parts[2]), opts...) + + case "create": + if len(parts) != 2 { + return "create " + } + f, _ := fs.Create(fs.PathJoin(dir, parts[1])) + f.Close() + + case "link": + if len(parts) != 3 { + return "link " + } + _ = fs.Link(fs.PathJoin(dir, parts[1]), fs.PathJoin(dir, parts[2])) + + case "link-or-copy": + if len(parts) != 3 { + return "link-or-copy " + } + _ = LinkOrCopy(fs, fs.PathJoin(dir, parts[1]), fs.PathJoin(dir, parts[2])) + + case "reuseForWrite": + if len(parts) != 3 { + return "reuseForWrite " + } + _, _ = fs.ReuseForWrite(fs.PathJoin(dir, parts[1]), fs.PathJoin(dir, parts[2])) + + case "list": + if len(parts) != 2 { + return "list " + } + paths, _ := fs.List(fs.PathJoin(dir, parts[1])) + sort.Strings(paths) + for _, p := range paths { + fmt.Fprintln(&buf, p) + } + + case "mkdir": + if len(parts) != 2 { + return "mkdir " + } + _ = fs.MkdirAll(fs.PathJoin(dir, parts[1]), 0755) + + case "remove": + if len(parts) != 2 { + return "remove " + } + _ = fs.Remove(fs.PathJoin(dir, parts[1])) + + case "remove-all": + if len(parts) != 2 { + return "remove-all " + } + _ = fs.RemoveAll(fs.PathJoin(dir, parts[1])) + } + } + + return buf.String() + + default: + return fmt.Sprintf("unknown command: %s", td.Cmd) + } + }) +} + +func TestVFS(t *testing.T) { + t.Run("mem", func(t *testing.T) { + runTestVFS(t, NewMem(), "") + }) + if runtime.GOOS != "windows" { + t.Run("disk", func(t *testing.T) { + dir, err := os.MkdirTemp("", "test-vfs") + require.NoError(t, err) + defer func() { + _ = os.RemoveAll(dir) + }() + runTestVFS(t, Default, dir) + }) + } +} + +func TestVFSGetDiskUsage(t *testing.T) { + dir, err := os.MkdirTemp("", "test-free-space") + require.NoError(t, err) + defer func() { + _ = os.RemoveAll(dir) + }() + _, err = Default.GetDiskUsage(dir) + require.Nil(t, err) +} + +func TestVFSCreateLinkSemantics(t *testing.T) { + dir, err := os.MkdirTemp("", "test-create-link") + require.NoError(t, err) + defer func() { _ = os.RemoveAll(dir) }() + + for _, fs := range []FS{Default, NewMem()} { + t.Run(fmt.Sprintf("%T", fs), func(t *testing.T) { + writeFile := func(path, contents string) { + path = fs.PathJoin(dir, path) + f, err := fs.Create(path) + require.NoError(t, err) + _, err = f.Write([]byte(contents)) + require.NoError(t, err) + require.NoError(t, f.Close()) + } + readFile := func(path string) string { + path = fs.PathJoin(dir, path) + f, err := fs.Open(path) + require.NoError(t, err) + b, err := io.ReadAll(f) + require.NoError(t, err) + require.NoError(t, f.Close()) + return string(b) + } + require.NoError(t, fs.MkdirAll(dir, 0755)) + + // Write a file 'foo' and create a hardlink at 'bar'. + writeFile("foo", "foo") + require.NoError(t, fs.Link(fs.PathJoin(dir, "foo"), fs.PathJoin(dir, "bar"))) + + // Both files should contain equal contents, because they're backed by + // the same inode. + require.Equal(t, "foo", readFile("foo")) + require.Equal(t, "foo", readFile("bar")) + + // Calling Create on 'bar' must NOT truncate 'foo'. It should create a + // new file at path 'bar' with a new inode. + writeFile("bar", "bar") + + require.Equal(t, "foo", readFile("foo")) + require.Equal(t, "bar", readFile("bar")) + }) + } +} + +// TestVFSRootDirName ensures that opening the root directory on both the +// Default and MemFS works and returns a File which has the name of the +// path separator. +func TestVFSRootDirName(t *testing.T) { + for _, fs := range []FS{Default, NewMem()} { + sep := sep + if fs == Default { + sep = string(os.PathSeparator) + } + rootDir, err := fs.Open(sep) + require.NoError(t, err) + fi, err := rootDir.Stat() + require.NoError(t, err) + require.Equal(t, sep, fi.Name()) + } +} + +// TestOpType is intended to catch operations that have been added without an +// associated string, which could result in a runtime panic. +func TestOpType(t *testing.T) { + for i := 0; i < int(opTypeMax); i++ { + require.NotPanics(t, func() { + _ = OpType(i).String() + }) + } +} diff --git a/pebble/vfs/vfstest/vfstest.go b/pebble/vfs/vfstest/vfstest.go new file mode 100644 index 0000000..7ea3207 --- /dev/null +++ b/pebble/vfs/vfstest/vfstest.go @@ -0,0 +1,32 @@ +// 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 vfstest provides facilities for interacting with or faking +// filesystems during tests and benchmarks. +package vfstest + +import ( + "os" + + "github.com/cockroachdb/pebble/vfs" +) + +// DiscardFile implements vfs.File but discards all written data and reads +// without mutating input buffers. +var DiscardFile vfs.File = (*discardFile)(nil) + +type discardFile struct{} + +func (*discardFile) Close() error { return nil } +func (*discardFile) Read(p []byte) (int, error) { return len(p), nil } +func (*discardFile) ReadAt(p []byte, off int64) (int, error) { return len(p), nil } +func (*discardFile) Write(p []byte) (int, error) { return len(p), nil } +func (*discardFile) WriteAt(p []byte, ofs int64) (int, error) { return len(p), nil } +func (*discardFile) Preallocate(offset, length int64) error { return nil } +func (*discardFile) Stat() (os.FileInfo, error) { return nil, nil } +func (*discardFile) Sync() error { return nil } +func (*discardFile) SyncTo(length int64) (fullSync bool, err error) { return false, nil } +func (*discardFile) SyncData() error { return nil } +func (*discardFile) Prefetch(offset int64, length int64) error { return nil } +func (*discardFile) Fd() uintptr { return 0 }

f|v)jKWOCm|5UH&HX8X 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

Y^(g*z>v_mh>;z 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%0AEOWlyhmg*!TnC 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 z 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ⅆ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ⅆ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 z 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

j)%@`v^lRsxBQggRS$Q46_uB23 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 z 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
U-V3yFCI?;2X)#jPt8oHeFbHv(@it2}K6 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
0WwN-wL0&KSk9_8RJy8WbIwM4r{~_>X_X> z6^obOhI6~#x!=0J@B*9yt7~havHwKJfPdr{_{7b(c%}It0slPwb&Cl2r{Hfj{#Jg5 zvcCuXbMZG3_ssbOc81?-abGlk8-u^m`1dUQeggmQ`Axu|2)U`iU5oqXejV^n!0+Sn z+g|+lfzNSVH2%95|6lnPPVa#13E-BYd?sWDL6^b!E5tqf@plO2UH%yeRNos2#I(Y{ zKMnZzKz0~pm*8&-%ID*65wQ1s2@KS?7jmP4aTL020CpkD_uwx9I18~~mOXUJls`$etp3{B|7WW%zv<+*6O=#^P@p?#+X2Eo6`4-g?L!#&1iA6Z|pgoQV6AfLDV5 zjt5RU{-)vnU6AjF&kPR6UmnUP;rDU4w*>c=0&@w<_u;qk_;(rVtpWCQ{C6_`y_9so z-|qWh2axT9-}~clEo8HCUslIJVCh8>fx|z{>?6L^5Wa=&ajiJWSM4cwUWsoAw|+%6 zZsk3Ex%E15>W88%aX0|K{nZe+b1ZxsE7CPyRp)_sf?SD3x4Q<*@^;GLTK>k? z|2;%K^xX%!MJq6*Q&D&Egmm{eA@1f#(d^^HA<76z?sf>|cbcCfk#P5s5O;H=cs9(n z9H~5>gH+n7EM6kv?mdkm^<=lXqK1Mb*8>BSI#u_RNVwZI39Itdmvu5i)Iv`uC%^-9 zJC!9#B;0L=xSLw9WjYp1BSI9aP)j?N4G^uzZ+Fdgsaj>T$KGfJN=}*tR(INxEVaVj zZ-%&=Yn6j@aw@`-Big{xPIEFP7TxX|OQ~4nv2@@XKFUHBtG-jTqZi%o8ak=WPi6~$ zYvd7SUmS?l$B&ZC<8Ievrm7V_G;#HcMxbOD8R)Jr%#%+@x4ZVHs9Mcu{Tn0Rf$FJm zMI&-Q!`-2H=VeP+Rb?YeiM}=>&|BXX_ndQcs3)MZdpfen5>KFG_a+XbG^4++4ah@s zG~|^4(wGQ;vR>0!Od@!uGA2tOrs6leKEg+DQdxgLeeJenK8f%oBwUM=Qi9SskHm=2 z9p>^hqk7wh2k7#u6`jj^$yjLnoD$B5@o8(j}gjN>;SKdTO;84w8f(LV}}>V(nZT z$bwR4!3tjF&^PB6@!{ZdNK~k3%J`?LR8F8>wfw}3^p^7gUE2%}d{GerZQ0W}W?nB^ zk%Z5jCtwdzB5Uu8XQ|=@QC|32G`q(YWj9RYvtg;j|1hWfHAr)KOWyVBmpa*Arml4# z0j6_cd&#i9{`%peXJLC9OUqQBGRXEMFaCjgUapwHdJ+<@SGrW&Yr>NAxW*6l?EWGR zC9*Kz0gZi|kd*g@eWdDmA4T6K2SA>!*@b<-OV!$Z$2Cvrv}T6BZ|(vipl^;7!N|*FqEEB;rl=Y#aa(E| z7^JTs)tgVT)7`GA5G6pS;@JF9T|D&wLetMbf9@0V?I9TSjD)6Rq3IxKnynuwu0s64 zxt8aJe@$*{i{`6_>e_6=AE@JeQM6wnhJu;;-16CczRJ^1ni#x^=eWLa=6;GHIl8t9 z_4u~HPBKu>77X69^ynqh>%CgG8S8YF%KAfJI(DM8EZ*wS3l0mkN=UeJ2q{5!vf@J4 ztgVW6YC50nt=V~r3vHObI8MY6l5lP4k`f%vX5p^I_y#mS92~)YrOj8nf2BjXM(A2e zKDZu-ww#NPs}0dBvxU{Q!e`HZP_hn~tvw4N52eKjkdHV3^Dn4&Q5}&Kg zNC{%5sY7Q3Gn3TgtY#l42K5noY0LrsC|>9PL|w?P`j`5cLFfNSlkpeohf2zL-b4~V zhL9j)JWdnBfkK#s;ToG+d8O2MjnTDL(AakbCNmN-X&B5VU!NhrS>|b)8jEqQ!vq<+ zs0Z7x^@7#&dEd1H60U_8DbYj};W0^|&HUE``P3s63$6W$I9Ij+?O3GOX4NBFpL>rW z@Mm3FlTP@hx|UQ3bD0b~DFS}6zA>c&?RdWU(FI4Irk-HDt_=k~xWURIy+es?a*RGr zZs+slDVC-7R$KmY@i`qQyg00v=qKh1*IxmNJ|QHC^_5?;@GsS}hS<$n&*!#+P4D`) zg`;>jNfNF_XQd}txqzj)^4133cuyeEc17phN*qtnwNb?J6mVP&j!X5#dFkS8;B)jv zf&aj#*L_32DDky=_~HlzsnL3j-2dR^i@)cy58hT8>G%Ne8=bSbB)|T#`kK5+d?|<| zTmwBRLHsu5*^-eRcfCy-m+RU(Fyia{SwJg+ACA{MPdtk@46yXSsv0-2?rOt{`uZ-S z!&wOl*E33qv$u#9?{pbJZ8%BSwon_+KpU2#4a@Z1<-0CEA3qU{RL=V9XWG#-6-x`` zC+V*u`$HQ}(93E>N8`nMm!s_JyH!Sl;;EFxQl$7 zS9Ldq(`mXEI|>Rat4&)>?K ziKRZ^`x0Q-lfg{X<9JO(PnQFvl^)Nr>Qa{Wp~^~-aXYNc&<}SJoeW8Q6heYnS$!J| zzgaD72zNX{74o*crS+E`iIv%KM`C3bSeXP?rt2Gz#E2<{^J3+=l3Te?U)XWn62jy zmP2L_i-^%!&wc9CD1#TNZD78BYS3^bN%4&CcCEcC!=O};A+yl*PdC0Yl#bC_tZVzJ zrIOK7bI?-r&{7NZ{YdMYU^jtEYjY@N zT!EAqb@=+CMHhmZu85(F^+K64dbYyF>>k&Y@Z;gGp2ca?rTT`WqMup~3D>-~l%OYC z^CuSWn&}I&5TP|T!Ry`j6mb6|iMLdq6$@GOD@&cAmNmq89+3swJMB&_r*vSYuEmwW z{}$XD@UKHuPQ@wS75V}!)RE`l=$ zyK4uUiiOu2pXeH*BAP-F3Qzy=@(-vBS*2@zNZ|@7J0AgKrCxga2p?~rtD6h_2R?Z6 z@02mF5%|7T7{yY!_zHcHoTBh*!qthamFv}vAcJt4aFsr8xX2huB13)R|H2?fb2Hxd z`5vBXtkbn3Vq_5*SqVl~>-$q?@R8fOG9vI>AG%}~F|uCZYrEl!OW=x?dXZeY@N&iI zv8;uyK3`C$DjD&w)z=iS+a%|jhv8?4+K5m=|!B`5Ro?D?^ND8pAyY42uTI|_U_eZZH6=U9X1 z*q|Skvk0Enb17?gzsh4!cP_Y!&^GBw#dCRwvj!3mgplCDsqt{aJDf4x79Y5=`}3sr zHeE{|3tQ~jDxk%@hV2Bfm-Tv|gQBzXYT-B^yZR4`>XWD8#4T) z8A-TOPbooV2`AZAl_Ah(ootQ^TOPR9X%&CJq!wE5(6vlx?HdZ7H=!fwg^pmeK0th$#;`Xf}Zl}jz zJ38n!GzoQJ(52g_Umoxm^T#i5{w`k(*r{vdsL_@ofUH9R*`}wB-h@%yxo1v7)y3_; zyNirsm%!KhAb^y7k9YCkjPP&N`^$4CUJ0-tT$5g72lL0K=Xmv1mj0dkj1TqW^PBsR zHQ%@OvIe8rqHpUzoi8nr#P=a22<`P3S^IRg?A<5_ZA|_}j!}F)zwS9Y8DkHY7RV?@ z!YH=F`F7|VWmeu;6LRJ+XB~b}>$vD)evproOAF@lK7(GoPb`|Pg@kJ{Q%W>#5CTh& z`t3sM7^`*d0JUNzT5%g%ai>1JsDMu#oNGW5__fh@X42OE8eNMT2R=%{$8I>uc5t#s zPg-2cr)(N~3eHIq8|9i#x#X?@b^hQaaS6`5cR1On$Ih}H0oQDb6yRlzn>Gh4;imoh zyN@5HzUu&{)}geP3e($&ShP#;K3K#ePqS;zT0E*YqW+6xDBSF%!v4Mb;l82~UxmbD zAtXrSri@r<9QfDb$HzhAgSs}FXdZ#{msUTGWs*lhHCBeJ^~5ZBGSwX~u>O11%5Hs( z+ZbONzVlV~!x*(NeLYW2i)LPiPiQreya&eTvpMzsf#zO-j5yh(FSa8JEQ! zfYzcGyzrvOm&+60NyIfPAw{^>x3ptzT{E-~h@b)%6udDXf4`Mn=P0I_#=~`1!F6`R zbq?yIm(}r)eVnW72z~$k*jM9-6^XB9!wUBzU)TamJfL^mw~w!5c-hWtI3(@gYST1? zEN+RC7yl8xVo^7qMUliWAtcBWn+kveZPlzf;j}k_308bP(Uync?1%O8S@Hz-{{i<^ z^FF8{?vLx*c;dbnCIAkC`(ygT4fFWm<6OBf12)tiCS<*x;|){{2(leYz`rERu(Dnj0G1=q_}kw(<0DX%Ynrl zfVCXeOXa)lp4M_PzA3CKux~W)N}cXf>g=!6i>uOvwLrr4VzHDUYiY{%1lp3Phn%9E z`?RignE;K4L*qJVd{WjnMx1?k3q)KqMpA@Z-fit!TDQZerAZj?XcUThPXgRf8#Rbv3jaJcln0aqAS~Ru?0Eu(Te}*1q*6 z*3Py57}WF0r_f@I-Z}lm$&@7GT7Z-yOITUEpG2N?nY9q%S~talz?=EhjYG*> zdl=dp(mESjcQY_@h34@_vGXBv@0wm?1+Ld;0#ESd*6~(H^F*U$Y9GlrL^i6LOOdAd z=1h#|RxibP!9?pa1MjyHjGQ&#Ao*&N5) z_A#`*&{`{h1rzr8TZ~ofJVbmq1Qoh90<`VY4Yp-F$mzX%3;2j6R8#{Ma@ab-naJ>K^C8&pPvxnXL z2UWL=#W%C?T_=tv__%t_`g-D{ub~|zKE}O@B}4qpK!iLEK5G9O(8hp|Sfg;5cmmHT zc^PZFUad1IBX#&lFa~A{wAUfw@(?LOPw>VcSo%N}Eogx6p!GENmoDKEoFrVkQl$i; zy?IH{fkHv~kKn`1z2`@Y;Qb7(cR4c8?TFyfh~RyV?nhRD^KT1!nq!nSMrQ+)yzgbdG&n8TU-%so0+=o(wRw z-N1+T0bf2cqyEM|XO!sq6t}<0+Ph}_8u~WQil_xUg^feQdx=;}BDbgyuJ&zStWe)3 z@Jc(5;9n5UFtkIYbq3}&*CVUwW7PE$7Vb5#8G9K^b-h&d)kZ#-fSnF}UQ;k1;ZHSI z^%9HAB=N1PvXmeTZ)}znlbRe&Xso?vzfSEq(9lj$JNCiYsvp`h&B)F=B&LMUs~yuv zp1haZF$*3v2^L-i3-6D9VSq8+`L?lVJKk2o3Yq$Z`k6kHx8p=^EfHF#F)2&-_9Rjp zLWEm)Q+6Tnp8VU-e(ShjiVN=7}k=C zT(=4qlVS8u5EkRv#u4mZR|Xd$HvT(pEZOmA8#BA-h?#ImxDq%iK^EgN6V5HBoO6BY z164Ou%o=8B@zbDn7UESpyt5SEImlS#oU!iB&l{|*Yc4#cscCM9%Y|uKgzJK)xX)bEkl&i z;-geLFx1GMp22q$(B0RDxSKkTR==>j*M=w~B%yB0AKz(1u9ItMlR>m^JdAKKjBuE- z)k!x!Nx6L?i|VGdh2T@(e9$ zIs(cebPQ<-C^<%2|EYXe)42`~1uK0YjW%gDdL+6`;A=DBG}%Uke~7W`(0<Ly<1WHJ9%oARzLyeToWT2zZ1T+|ey6&S+`Bp!*l zCikQW$GiU)*50)-;;?uJ;c)EbF1%63v7W;HNMgRKfs_z%ce8e`#Ipeoh}g)>En`aW z)^}eFB1U5f1tNSCKMMHD@izr_Jsd%#8bjAyqt*!^WWJ5t;v1~U}Sa$x7ElUpkMaG(8VzVqsxDsL|T}WucGla&A zcGulVF{#wh4$njzX2FmP5R*n5xlVWPX?XYj#=>2%T+M0CC+{7@n z$WtX-s%W9E%+S(F;S%)UL$M3K)adPuIlO7Vk+r)_)u6%QdHfKy;COsv?r9v=f>#vp zhs5O}B${eP9zO)!_%=J9ruZ@0(0a^pr5y5V9MXE0 zp~cLBQS5?Ilw(ve4u5$tjwwc8X&j9;Cv7K4Fej?qtZdF#hn%iO)D+>LX>>ar6E5yA z4{`t5j4A}ym_wh%QAC;}wDwH|*VDlD3}gHenbCN2^9XB~sA|yQfUAxpLy{V6vyCHl zA|gEqi5?*&nxdNfpMd-Bn!|hPwaR&hRt&2B$6?LI=vSs2yX0$?jpe#z%rW>9LBO>& z^Z&EJh`?N9+LW)(-0j*Lq1bY&$7<6IZp(M)X>XBLEHJdWWEC?I@uwo<&oRb39k(aH z_t&#~T(3J+Wb+IZY`FNHZ*>U={OW z6$^~x>l~}_tmOgL-j(nKS%s(t8>)>I*-t$Pi7ZuVH7b+IG-71jDg<1+*zCD{7r3FV zCaP=TfHUEIsjcv-2qP!+z=hrplQfN8{e`N+UBB~bpkU{#FtWDQ^QBpma7_b9339*W zbavGhA<%N`o8!XEL)<)mC>hT4x@Cq|G#5HAfO#x{c`P!@yPplw5B!IH_>@{pLx0N6 z!y$U9aeA3F4-$DggorSYCK_%g*Shbw)tktummAtR(t0VTs#jvFdhpv3{&_}Bw&(+C zs`n!uu;N)@a%w0fewA7(l$LHlYWxqQHd=S91Cf|zS! zSCHSY!_3D#SmU&pu#xP2Z25g8!oS$4uMt~#uvwUWPn+CH@xTZ#MoWAr7oT99U(nck*ga(|MqtrMY$}Hl#c}4v1Q@&DuzwDD!F( z(N#s&l;`0WSzFg0$skj7@YWma=li*}k%UVFHGnsjRtvaAZ{7DPS;|K2%m68{5FDO% zTSQ>3v1Z-w=KljrNgMH6bBbG4hIR@S_^RNbYv7<8j98~L_GIP3-`Tycm>$#_&*p85 zZQDk#VFRUyLgbGs%W7xb6w3myRb7`HqGgMr4Ve#G)`FIJ(6Z5}+BNRt^P)vm5VrA~ zODJw_Gqi=Mz_$&wYyvG+M%@+}x9G_me~h-Q-KXj^1{sS(%T{BY}dKZHaRfrUpY!SeS< ztM4Nd-(_fV3!rrs0!cLj$u?t^Ga={MmJgn0AO1(xASj^_{&)Aw)!D zHenkAFR$|Po5=$AU~dYv_8o==ZifZ#GPdE|anYN3*3yQxk5N?!vOq`c-Nv|6VifW) zBw|BIG^I4Rz`5K4ANcXJ?PP(~m|%v|{_e2AEwI2HM)I2Fmz|dd3Vpk8NxP2PvIhH% zs4aJ)E$h&hdyG?wXT!=vzq7QR>N5s)3gR&$w0%bH2-%h-;+iLPbqYXw2Lc&&MJp@wtj7B{iAgdP-3vHY^*Z;|9w<0V2Pu zO8z%JrNDdQ*n1i+G#*2Sk0%L_Oe44gearc zLCJaGh*3F3tTWQx7lyc-e7^$@$qpYMqL843x(`>!m(VogaYIX745M2GquYz#<&cqa zK=dx2CUOP);7=<4+euEM$@>FIvA@=sFh#u2s6pb-AtahGIBp{Ixtr{M_M_Abkhazt zS_(AwCBj4w!!*|8Z~4O!+EJs-`8M~%p3T{reeYUA`9iF#!6SLP>X-*JYPXFS@&93n zWU9(b5gxD~|DLsXy#dj-k`Dl<C*C{Y>qXDG0)R3PB^d~uG~!m@dFJlv zDglbwrh3dn30;dKCaa?LvLd*FFU=(%|x}GvtWzPh|82pkt=U%}Z*G<=`v+81M zbI3G$!88~mMxC*)pB!;`HsPahv39N%{szjqNjObdZ|q$wb88ZDEk>$M=wT8{6Yls+ z`T(LO%G4GTErZd7r$EbTWAfyb!2f`jthBwX)jNr@)910FdRaO*y9=0s3~#+up|YS2Dt z&^k0|7qi$Il6p4iBVjDFfM5<7FB_CZj)f56KG~Fk2)t_+bj+d# zm0J6Upg~WgL8HvV1)DE8uLczrd_3`%G)ix}nc7aGB@MJhn-N;9IiJb&#``f|W$j;A zpE1bJ9a_4Y+b4;R@li;;5kjIVR=Al9xHTJ>^O1GDsl_dUnGXjmQD#IS&fK)MP>ih4 z)%!&Sul)LU528KM)CN)od8i-`6~vqA2TxvfUbI(mYizb;=327G9;P+|75D~%_E^x~ z%`9_PPCaRVq)SkCF={ZMM-R5f?&h9VG8ZP1-&EeEh~T}6W)xcI-uCLh$XI%sT9>8J zdI+@c4y}8bhn)9JylcUvN*`5)AY&1=V7Irqd8#xPNc0UM(Uj8MSQc`sKhyVAQd{;i zwH{E~KN4*jkG4!SE9>T8@;_+HsxbvOP+KOOS~@DwHX{~xHZdWKWIM*3dRW9muL$P9 zlwJLz`jicmY)S;fXGP4Ond|t-nj~HdAwlsl@Dpp{ij_fVg5{PU<@X}3dz;!|YRi1I zWe>DvPqWk+S$nr-IQ!l;8j}f(V}XKv>_0XuVx$E=3K7?6OwD;7ZNfGLUfBCD?V!f& zV`>wrF$>U`iD=ATW>%jxF-SdE&leT+O{}nJ)X?A5PEliaH3M2b2CAb$PYV8>1d4i_ zTb+el??>vw(p`-d)MYpn^)(NV7%J@@A}_1$Dn+=xd*lE-r&z@8{qM(axRTQFR8vb_ z28t4(_4b!={I{7UU;p$po~u~8r793KQjeJ@!+~Ic&EHbCoMCDOL~|B0&|a{_KIX_YnJqVFi9F`Cz<0b|#-U`(QsKY=bI|x7&fM*? zi?hs@1=7STyL>^uo@Hw5iQsY&+#dv|o8!{YhTzAVv3tj*>%s6Iy4Pb(^Ht-nD$d+U--dZZHd_ zmIy7|bXIL2hlFc}(`6QoE6sZwp>$Ni@)A1BVW?1Ai+UYvBuI=i%#mzg6Dzje53$rG zs>0!7713F-l{EVYnS;7c;V-(9glp_CCCK+0TmQv!Z4JFfB=k*xZ*?7+Mvkc^F9*r9 zL2?#I9&E02(qd1NAB$o?T&n8a&<(gq9%kmubW8&xmxT}!p4mjBE#an7^5xTW!ZC0k zZfg0^THAvzHyK@SKm2WfAFno;Sw}>->ly2C4#qlFk)SCDhwp*rz&e2zfW-O`62$if zSFm)~HjL||oe7M-yRhsg_{I)nbJgY{y!A-JH4Ksx94(1OyIk(dNB9)Lu>H*yw7IX0 z$s-rZgSAoXEk^4NL+j<3vv8&uCzKkfakk$L`U+N%iwL%{pVpitCnq0=#0x3|E*ELM z@wkf!xcP~XcHmpDk&soQ@%qC>2AMd3+RRNATdzIMI*(obiK>9yq~MsfRM|hmJh2J~ zD!>mQ;9Aa6b~SkT2;bEpz&@J#+X?cK0#i#{0hTMlat>I|Gvj)m70b<7dXlPgLmt3= z#9=w#oIXnW$m0;{8A3$($XT`8&QYT`(H^-XQ_F_d{?SO4@-TBa+#J$P?vbN8y$Nb9 zvR9!lr^6w9k;ZjOj?zI%jRS?|jy`ga9Nqpz2rPO@p~GR;d18n%nmb&|9s2DqTUSnj zvSUnb3Y0}x9l-2Xz_$@p=i}eG_{#?61?FUDeB@10G;8HL3^u68618H(vAKT4Fp+va z4v~9RCDk6wV+M^kp}@K5Y%UhS|YTu=BQ*jl|v$~EjjAYg&xU^1cB#oH}KE2%dgDThC^$A z3H)&ZUKuJfhsv3JdY8x5A1O&*tIq73+f|(vZ5M?@HRH?EG(IdhwFyM>1du!cS$UaRRxuHN zj(K59yw4?-78N`f^UXqXn8~I#nQZR}Y;P=VZ-QB{NcIKZw(@t@E?sSzAcv8pXp_v* zGeul#4T%9EB!t5>ZhG8dmUD+GfA(|)u`)#{?Jox_WniV;Oy3qSX3Ni&6;Z)g%kKP% zDwt+!n~3(QsGuAbOfeT6T!jra=Rv!up#A$V^rtglri%)+HL%EWh^!OM#O=%ZTkGdz zJED$e^ZxT7g_RkmR!vQN3Qao!O*`4#<#h9&P5VSND{u`ezi7kHWk5slK0?$Ip-nYc zmdS3OL|iYWNDZgJFDWHCud8~Vmm?1vDwl%K3a06NV zY#0wJfcp4&)aTezZPt%>EZ*}mzF_TLFJJ{(yhF<@GbTz}JR}ktwfM$a;TB)XE&j_F zA3B;87T#>W>04-Ob64FM5w^fwUUuJY;T_|xFTeV_)gQC2Tl3|gZ*BWR$@Ja1H=k@B z6BZs8-co!+{0i9XmBnn07Iyh*@xMNkubWBa3h;=q3Nx-b-Sy)23_C|JzdDb9FPMIb z!&i6rpONdo?1rwCdkMMbk8X|F4<@BX{yFBNi|z>T$gVylRPt@U_Lj8xSO1jy?=Ol6 z@4GLNRGL-Iq5mXqv%den)tTRBt-2AH10E4J&z#Wwa(>^x!Y-dJdgHvEp;!@{k<+h2!WdGi0hx$DO?s{xsWa8FK&oOQZ4DTy=T7c-1UC;PuBvyECU8ENBfhX zjBiii%S>%AfiE@p3>M#7C;8u0?C%9OMZ2!|*M>jZKSUuga5>;B7Wa!M@a32}UIXxD z=9*OTWoHhMssf}dnm@q)-8@7YFmO5GCpzz)LE#MG`S{h>{c~*BM>v~pc5xQ9y~B^j zS|zKs1f|f@7qlg2kNz^8wT4Ja2oVv^nur4e@3q%%TRv2pSy9SmR%8Zsf}gC>HWSZTJhW{SLAa7`U)6kH@jv$F|-_;Hyk6YAwK5nq@nqgl!XmYx_g9 zeGRt#Ap8CQ)S6`(FmO5G{|tP_caQ@-ntmOGDa@Jh%!TH}BfZ3)!}Ija?O$%X9Md%G zOs#YsOt2SBa0wc87XBvTuLOTvvCm?;+1;6J@;0Fu_Jga3&Did&dc~c@JfvfitIgGW zpk6v6;D7p14OMKZOwCc+U=J?Vk-% z2s~U4?N@B?*7F0j=>}69zYbrbUvHKbO0P$o{%}eK7%$qC$CL+a!!K|>y(|O>E(iU> zujd>gi`i&uYk=s^uKHVuvbFWi zaXEY^PP=#BaA>^+E4l0OEc38QYdyNSB22ZcG7sj)?4YeL4Ia4~ox(aZska>OdwR@M zKeD#ET6-fNQ)SLvAcxK*VuTQB$^*H_2(2eHKirK{i0znHA&wV;--QTNTg*`(f3jwmtM$i-eNIk1l4jxGVNT5wUl>e*#2-RPP(13lo?Y#FIb6mn1S1~Z z9JFe)vT`i{Xa`BS-VOgRqMP6#|LON$qazP$BnQjUT6@r1d(A<;x`;D6&x_*~72%BV z5185^;&>(S_X5AhJdnGXAAUm(e(r6!qu+<=w6rRT8;GZ9*v`eB`WLZmn6!&x<=`Y74f5mDOOS2CN(~ zcP|-;ehbqn=g7)x?$XD#pZUt_5mQ?ZeBU^*vLCD*G{-w9etSQ~53K!Y^$~*tho}YH zq0Ix6#gP6fNQ?;~(G)2>a0s|5-()W)Q#ppjhDcchQVxKWL*|Kf)#4b`^CIQsls{ZS zBad3_{2=@taQ7*1MELia({^$P%Q67qa*+T0d*BOF z`;@6=klH8B`mr+gg=YCG_H_cAC+!boQpNQ$hb#mJE(g5)Pw#aj+Xnau`gJHqVuxVc z$IO)nCWvDY&&#$ycsMnZls}ElDx`e9xof!OoD^K4YTwFX2GbaiuowgVRo1#%lh$Ek6#LZL zK%&vv29H+c-R4>z5my{2ok}*IU}@W6<668mFa3;n9)Cb3WSe8-@cc*F@oPeq0Rk6t zVUGRQ`gMON3jy{X`n4Jsatam_V@>J1Qhd$gyewqYTX}s+_e4wEN6z2fI<$Q$U*IJD zT(4K`Q*?ifwRua`U6ug?m!tbtz0+=`z>;KXU3USyhm~F;d4arZTh0YXcrV1``D3-= z7xq-aWg#$dIpClD+Nz9tW`GaFufC^JQB?eu*j{ue9akwc#zAg(w6DE_|Jk8~MVk_uoT#A;5d#S8W@HfVCI` z##x(-MPBF?ruq(JsjglwC@*xvR8Q;lAhD@~BwXE^nin<^^h7%re)JEW=;(`Xjp*oO zRdkbF(<8g~nDkb-eu90{L8U{M0Rxvq$5juHA4)zC@J#x3FMK}6iU=fFdEteKb8>jX_i(`#HU(CQ>7h(+YeQ4>lHhEqBi{QYeN(Q z1D8Ymu3wX{BkvqwY2Eg~JJYQhiFte^K_0kSmA~+)Oq&zfpY{hOE3ym_xE$<#Wv#oB z;u)6K1K9onR?G~UWs!2%so|@tLtt)wdljnuHeZ zZ>{SuThy~DpN?alT={j-Fhy{W?cG+!d?y$}q(D_#9i~u2dzps7+d95{b#ud3R7 zqOkHLyDu(886a>uto;4HeJd${8-i4b46z1=mD6an|jmszKXkYSd#lL#4VVA(IoKVyAKy*(ooi_o zggxA(xXfgu zqWRP84OgZi%YcE)0naEndKo2e06&IbeTQKt88DL})_i9%z}rj$Y}6lAeu748j+x|G z#|ve=Y6FogLWl@EIV-b^eD|w5YRh~}%h-$VVWgF`R3vYIIuMk+U3ctE_t3HjyXHl; zVp#?VTn-~UcjrD&ZCPMx!-4J3w?E0(NwXTIHSq2DP4t9@%$ETA;kHNr|ejSKzARFJy zjlyS9v#j(*VzliQywf+cE2~r;I*FB4bOlRea3r0i+FxJ|t?k2;5|Y>wLV~)2%$4kF z*Jf6UR)belXk}Fx#+8TvaZOo+*1mf9VUC5v>+#nQe_i2|c~%c+=YY2lb!BZ`vs{gY z?$OrDC0J0Tp*JK}s`|Uq!6r5ihzE%OxnmXC%UBGL(WF|5H5j4WagFA#H(~$jv?;fj zb*|GNWEn7UIqalA_VcMR0{j4e^~HXHo!ek#4JIVUScL_0HP`boo=s+LUAvltSP_qb zjn&rH&UjN6tdK-hW2`i_{XxL}{L3>Bk)e;rzRt;zY_KYOG_r~@z#=J8K@v%rphO~u=lujUC1&(;KJ#9JWf5@zRe%W zi6&ZFml_bB48q621jkzIoNmFB>}M`uQLgtXe&6p}N^;09vo^)ZrE?N-y;C7Y&NPKr z;|Z;or~dR2F*4cGde(rENmfbh8AkThtBhRE8YbJHW%oZFq6`qY97cYNc;k5*83Q{N zziNHo!)uThwuf7FQ$=>^6;e{GSXkwK;%dYHJt#yWVTo4!Wk%}` zl*iAswA0YoHvpbk22Y%5%}tbH&)ai)1ihFL6!t_d*x_uIoDlmSo`r<##RRo8@Q9*3 zj~8%N$Hxz(&S18s#qEcA%(8|RNYjG}OjjQ`PBbC!D4wegzsQwr%R*q_au|8K?q$Bd zGY4Ky;IpkUn`CA}0IqDir(+=KY1*dfq{!KjaE$aB=bQ%c*ydV8~2npH< zBxutuwyuv@qd&*AN-$%$95a#D0p?rUMBr2yp~97`D)wuw^h$1t|`FqJr;mOfZ4nIV&iEm1Tgy<(S|_9h=W4=UI#) z8L+iQ*6uu+Nx{DFbB(r+IpEyBE9R&b%Q9f#a=A|BVBd4loC~b+ zIitnb`Od33|CpJULE%c``wGyUbJ3g?R$8H)|MI3kp0#z&zy*aXL4Sm{*cvH!TRaPi z>(%FWg{#I}lZUI#-06FC-m->Ta~ZO<1BfU~tQF4GKwDNZR|UVIwjpn5$F2wqQ3eQH zID?R5zcC^?oUkjAr4jaWD+k-&`EU~Q;i~)-ENQLA?-UIDulzxs+{;1w|NW!64twaeSUFJ$e0 zQ`rkLaPb5u z*wapARTK6`Boc>!y}?>GGfH?5RnlY`AaFU@clMikHSq%MK8Nw^Y4EZdyi{3}mJAU43eGVY z3f0Fn_Wbe>3n`3gg*qbxoH83eVPVo5|XaQlBbeA73az zf4YO-OA)nVJGnJ>uzW9tM2bR)aO-(tdC+H@BwoV0%W*k)SI=s72erU1OY46afncY# z#_6KLvMVR*ue896tl4X7#j*?#xE$=C9y~FSu=ijUmaunQmCo)6$h#(7^IX^qd#l*8 z3=p^+?5%mZ4-$4YR_l)ddylo*>55)t3th8f%}+E0u$R~s%T;Vy1_)dM{}yhQdopJ9 zqQUH5q!##9D@6dSMBv(JZR;TdmshA6kiy!z<}Bp|gA=%9JNkE7DI;=uYD*GZRjrgM z36EJ$9=HVBZ3`1WCpKy@|kLD?{V4blgN!BM7Ym1=7Vq;jwkHn_p~=#P-Kx>`#WQz zqY9B_yES&}Zt+>s^NK9Xli0tgH4kAgBp44nXl*+sl8OiR1|=20IgBT;$KzScJJko1 zWq`otFuo^z&OyRHjFkK+TJw;#)Y)Bt7IW?W>FQu3;=auG4pFgX86a>u*bhC`o^LKV z0xKl!!&V8FHh46L{7_XsOTm7HJ?QE*Wf>rFIoR9#{C0x6=A)K29>4ll!cwYXDf_MB zqjLP?-LmPdoonvCAsQ+zi*3(VrF>1LEhJoX_fn!ML*c7wc}Pyh8!`!TpNCgtJPLJ4*+X(W4ktoK1^97th+QQ7H^+Q9)CLR%^vO+uKNFZ3q$3q)k|e zz?*vbrniZblUPY78?UpHddO&qr+!xj-zga3GjOk9KJwuZg}}h&Q1a%+#hC6v zpR`h@NWKZ+5fz|L;rmrK*|p9t%YcE)0bh3NOMcd1y`{}PhF{|mwMy}R;Q?zaU&z~| zY;FEAv|bl`;|@7@j|Osa_i9~3O7a@>X)3R>3=p`S#{1__k4F)9jIC`T>}Y%5Bx!b# zca5$;bzv`hTg8@TfWYNok1slvLfEmkc7U*B>=fsH+}GHmcT{;p!G4|n(Y0GfmH`4+ zz`u=0sgzzD77%uvtz|Goz*zfO(s8~U^r;Ni!nG?o!@*8q?OL;zpQ`oBGC<&Ruz&gI z%F~plb+xsn_*GkkG;J@^G-eH!X_{9A%&2C~CaHAR@JHGCr;B)+MiQ?1`~PBm zDbTuKHUDd3Bi`0lgAIQ-yQgyw1lV+K!&&XHfpGdd+ps{@RF(k(m&3+qwe`;sc7m<# zChT~72;OnvoiF4oRQcTs_8aVv{}-YR5V#!d=O%>n&;9_L;aA^2xZ?@9V`qD?^Xh?j zXl>4-UD0pd3BGFSq*an@EzT}nD$}&K5OKvjDI!936Yec+PkT4+TVf>9*1FWf_PX0E z3&p^d?CUf2sl!i7e@tM{PYaq$mSupzo1Nf8e%-%A} zLPyu??L$I;-nqYlh0A}b4>caJ5qKY4n?~Th?TJ{sI|Ja_7~EE2=S^00 zfeJ3mfPu>auZi1gkgfN%wLZshj0o#vj~OK5?DNxuh75fi>;(4O=d5M1iY?0kfy=?Z zV^@4vQoNt74FR^luN{lSAb4bfZmxC3Aqp#RB4epl!DS&Za5><0J6rc4@cy8j!28)< zF>>Xs5WsO2V2qm9c7ol-L>-pVpS;KCcU-21P5 zaQ}sL?jf-E(64ETw~Mj2AkpsJL+&l0CI4)-5+&nomB{Xk3QDAOradEH zZtx+Ie}oV@D^n2k{<8PSC#bs_Vrv!9*>@Vgh1MSXw%c=89BCg>;&S_FnbUV>){a; zvBov*`0-T3$l$MRv1@WmmH`4+z`v7w#=?&~@-?~PwiZ>PXTQknNb-Zj$|rC`6! zd{?SZFUtUd%fTMp{H+J+@Cslj<5z7n`rNK|z@Ll5bb8ry62xSvXTRH?ec+mQc{_?v zYCDsmG7|aI?L)ER@QO@GxTamC1g+u>!3Qf`Z0!~sNP*UK$se7_jQ#$0ai1A{iGn27 zsn01T2yIv-YvEeHkY&41H^=1|ZQl!@xtOLY@@%bNmz(f;uP<@7Ib36iJ=@tw;prOf z5?J(BRf9`TI=QdQHFE7!i)8LgBHKcU2#0RM)r3_Q+Z$0Nv%L2yKMDvRrnn zf5ptrdKGM|D-ZRAuw<`;8&*JJCs6EfvwdN-znfb7kua0XU<0T=4S|NKUA$HC=>~PBD8#a zjr`1ddq@lmA<-1wJQNAITeC{|%W|VI)QAEjg?0wEjGlRFmj{H0H_-hK%XW`LL>Vw} zIgEr~`^6tAdW=TD3h=NZd&*id9sHIZl-qscU?;HmFJ~>ss!t`$0D%khg*Z1iue$mOuI3>df^@T_6k_fjMQcu)N58wAVFgJl=k zvz()0y)FA<7VcX1Y{(ILBylWzv_0230lqy%Tx)o)EU^hs5qw{2{4$g%8E?^_r6`1O_gLk|R%k`7ND$2Jk7dHw6M^ci_E(+XMdbw?_nq z+6mcmUJ~<#uJ@^w&i6x&#k$&6mW72m0X!m5VxO2P&OM{MU5%(B`B@*=x}Aa~s93&A zKTcGX+1kNaP%*(iS}LuB9@jNTbFf*1m%q!Vy{Ymf%YcE)q2iV`bNKGuiMCb;@USww z|3NWZL7qHLm9KNK6Ih4TptS;71_)dZc2dOBHz|UY!>r?=_(VHqz047yn~T1HuR-tk z*oCe|T3H4NTn_fm3*UKyW!o$GDogIK%kRrZ4V15rzaHpw0#-{EZ!30K}FB}9MFxYO|dK)?l_ znDQqgWvZ=}5GnZ}WjshJw@>uV6`MQHivVw}XI#!>u46sZ?2J6Q$)i0){-QpG6yfIc z!h}Xg<{VE@6rs(slRJx7h)Cj`2j&9q)FU5$P94u2K?=HK9PxyXemFY%F?NFd)}~kD zo%Ija$+enOI-JwsB;Jw+{}em6b1$AYkc2DQlM-HOJUV)w#tW^pTXcGi>|`D$w8>89 z+9}S9Ph<;MsT6lqjPQMyk{F^47`Ple@n8I&Ne&9|l&<)73>;`G9B8^-+;b)#0Gp=H z%YlBcsqRA#w7}M;cSSUxZ|^P;^Kl*0f^xR$j_wKU^T%0>DXQ+W3=p^i{yp3|JE#2c z0X1g@W}fh?uM{>t4K_X7p6GmZ&(j8wtkvKNEz#i9EPBD;(t2dV-XJyku z>wi9U=exxCLZP+47>rK` zeu`4ou1IAisGktEV1u;1Qofh{0whLX3XT0~(R_pcGFuzj1BSiS zKH6PO@{#dQRp}TRkf}rh`}*aeG)0yH0+(ahrI)^+PbRY5*2dAVB`}eBFp-7!=tY~w zSyAU@B2~X-k0Zxf0k(nfn+eCMfa5H&k2~8tJx%2KL>9hJeZrsw+%b_#yS}sII1s4` zAtG#~36rbl?mjeXbO&N(rO?_x4%1u?OL!k= zDsZuLrr2ECU2CtbTLs z*Hdo1o6H>8h4|IC9AvKm*=y{6PR`>^_9d*tWh!++Igdm3I-5CX_P+oT*U;3J^EA-5m8How#hyvH+a1OiF-px z2oq^sX>K9{ZuCRH-A?hlN+_+R!FQIycUIY52Z{LY_Gl6Pk)8$JvzSw@R)Je|SWQ zLK@CXIs#kl3DX9OcR=a(ks)s9SShvP&p#ickgx<9M;A`}hPH6;u(hF37+H6~w-t7` z0SWRZ{H?~{V%XqjJJFfz^|rx8mg>sYuYA%qT5xP|s~y$5NX99Mq^e3v5n+UlZ{g;n z1%Wr|%R3*X0KQW`#c&wmMi^n0y&*Y+2k`SZo4+m{beha&50>`HYp0#W+!Ke?`ju%L zVU{?{+m2humvOLE^Tgp0H&epzwTvTxKD{paX&(lc`)qA76xFuEJWf6w@NGkyxx@6$hE`Ji48A61c$Sa$ICa?c+gfFo4+I2ZM5eILt zJ$GWJFgX&*QSqb*$9t`wwcik;5O}zpCX4+3nU5*%9kjJw#Kb6=Top`ir+qqA#62&Q z8*H$4u7#;9H}IGCoxD}*?60v?5_a*pM-n%wT1knsiF-9XHuY-%Tst!0!=_(;Y?@#+ti?iYgq;eTsY5*W4EZa9-)OuVAtVS7;eD705h1?c8~O? z4LGJ1+vu7`4Pn6h?d^TV+BDt0LuEqVEiJfFR}h+x%=-9BN)3((O?^8M>~?WO6gU01dCAZrt~L}&->-iu@gOd{Pvh%{xr+}Z@*;em$_Q3iYz6X)4reGvl0 zegufac14Mp*F8U07T)m+Kf#J&G9Vkaam*eyR3u~6{-vt&DGj#K8Dp9!LzDpmmtz}0 zUHftw>0WDVsrVH-<7)-5jiYvY<{oS-#6-h+wdl!jv$Ls1j|+U?0kr5owCEAL#+eN8 zY|$5cun%2t_J1uVxSaUmv?#OFcFE;Q61hlya4EuD^z1C8&-SCINb5RVD*`LpWWQp5f7H|0+lGNLO++NPc2 zC3$l{nYDLihe6&cYKhQJ+38(GSMU-fT>E0wuE3)yxpNA*d7Y;mroh@cQaeJ(o@MD|Y;S;L1|k=&{_ zr_$GdcoAD1Y9%u)%yFKGKzw9Un#daHZr6(dD$>R5Ph&#VLP$c>qx(O)^djWFi9%Bt zLjVmJKperLpcpds{VGC>i(KqXE_fSA61(>`wYD*%_;wh_WgG+P7CC9MTu^xlBCd5v zDIzk6O4c@7#cRaix<{rB3!IUN2_Yd08&9XX!5!pYa_6jt{JpNENUgLO9@rx?qo0U* z2`7V+$il;%+k8x2zzfbg@?~ zZu5=lv>#0977^Aja`Gq6{VuEl&fYRi_6EoOZDNIEK;4boXcfc3>K{3cW6}LTtGsL% z_ftJT-4s+-Z~=ez?Okswpb?~ll^R)?$TxhEfJ?DLAwXaW$AdbgdqsFG?0Hi@g+K=^ zE%Ib62O|NOX04QXS{D>ge-SNRF!-jrF=xQ~jaX21J6y+U%AS#1FQ%`xwiw;$*IF;X zRW$FP#clj+tzphLWQUa8gsVZ-lt}Tp))u4jxz^i+Abqa2#ps65wGJ)030H$mugIi} zTgor7VzI1;{95bfw+r!W|No*h)ekGV3D+4Ye{y8X?xQ@5?Mg^$Bi0I?C_85A{Rrvg6`0Nu@CmEbqvqu17RnG%fRAK ziX4)5iYJ65;nGSu+d@a$uwuSBfRdjL0p+lE z=Amb(%LI5Pe)VOdpYMi#J~6V^88&!^-Ip(8-@AsPBgC#oCyYy1(gsAX8zTpTB(hUg zOp2Td%3fhy;9b!6r+UfJiquAd6>S47FbNiz9$7Vf9Pjqe-)2mf%g>JFN^(b$AZ**^x;zluu3Z z^(64&k=lF$&xza=FCzc}v{eD-E6jYvq6DU0|YJy`}v3$exLvV?D%p7fZ;I1Oqk)|$iYq?=V^v7C$V(b zShHd1GvPFL{EN1Y(1u5DS}ErZNW?YRlp?|o8#ikOZ)4A~cN5(sBempm&^;ou3@@b9 zEJ{}}I!a~aiW|5c^0xd4`$b$)K^6i7m!s@;&!%r9y7MEo0RRsh898U`Q2yp;pId?w zhie5m^%SX(vuj*S5V8yqxEyTuTZBQ17a*l0#q%R$VT5OtD^WGS!vV)reaw2_8KMjr zxE%1@Q`bx(@WM!KCV>}3COOBmLf*A~>3M~fkN+QQ=K-Eok;VPQ%o~-p7Z9*~6ewZPizDt6RWWFv^!-dm#XN(uo&Afbf>5_$=c0-=W9JE4T$d+#L^YQF!OlFYo{ z%Nsxap6Bp9dQawe=RNn#IcMh1y|*IzlWGac&FQCGd$c;}&co(Scz?@TlnxAt?)=;LG(JHS&g>Mx4a>7i*QJPdlzth+}vr>Wq(H42FX^TlTe8vV@9^g@9 zf(Z$wqKw9O(4N21Y@esCExv>He7`U{ARwB(I%d$d#2z1HD~UZW*fyVwgiOOP-)z6K zn%#R@NF-D`ARwCE?bN(WC=!C*W(BU~I9$o0xRRrS{q&bbB1f((d4ng+9#qe0#g&w% z$A;D5ApNHPm(b+NFiqs8tidxRuUYaR`;kQ^VCiZFGIB|95q7GJNZ5prYih!8xTiWJ zf6SZ3h0y^4i4B|(Lw@q{x9+8Z33fIe-GhNS5Cd~WaOJ3}^5N#+I51`3>!pnsABve2 zWc|_iz{KDIl+B{==--!S|8AFk7lZcWis%R1hf#t7(Vaii<(C`IqVto3Y&4FtgQ($p z24e%m@yt@+;K4k(7!NY*=e%K^x(o? z%H!~(t$EsyhR1zU5&g{N=9!fe42ZVKqaSYgfZ#Jxz^(**MzG&tm1hWWg$Wp7nEO+n z?1j;R0nzX$k4=sx_^co+Ciu+Y7JTSbj1{!E7AK00o_|^q{l($T#LA|nt) zCIpMr^5%tZe9>S}##dbHb})Z{FG%j{da8>`VBB6^vw|4y+^S&(H^uN#!0g0-; z?M?B*t@%NAnCvhwn6y#a0k2=P-%@h8+75$$;B~FErgT6+v>g(MoLNK{a6yorqYF4c z*s+814!TIzI>0#%M-}pK-qpIlrF39GG<=`){nZ3t7-T7{0ACO+U8fQ{+FO&|*n6w% z;eS^|Kh90#DkT^Y4L|tPbDz-1M*;7MqcO9P7{_DTcPjoB<6l1{$!Wnwdb$vK)Ti^W zC!1%iUM~;`5}J6V)+XK`{09f;Dd96 z!%Mbea;*I9w>!?0oliVL187N*Wx)PXrNN>(a)w6EQP1rCRnqz@lK@nn_|7MJ=pZfsxoEWJ!_OWRIJD_5D6F_wpdy zN#fs(weuzj?toCj2LD83dWn_`B9rN9lloXv<8z>BWiUh+wy0 zjiW1Q12RU@lHlfZ=WtKucZ?#{`I&7VqbRmI$U3b?6k8P>q;niPXiZBx$5LOmRW;YJ>XHRgZ<{q*ghomUQI7;nN(hgr!=eu7Nr9MqS-Ir zI%foRd~J{oqK>Z#_B^pn&M>KS);_918jhZS&j0hM*>R-<1ES&mdoLeFh1NQleGS5I zDI9S*9I-68Aw%66i|jZXdBf)BSwadeX?tu74sO|ewRAPKXc4AGO@1f}t#T2xZfTeH z1X*MwW|(VWkqyCe{TXlk=)arr+V@o_q|Ym&|MFHCB^Z#{fP_!o?j0fcCd@DizA?C1 zfApOIZ<~O-C0!IsO}SGNMh6B&!@sDkE1r?ujClj#QJaEUdGfA0`GS=#AJ*(tUOYTx zim7x!Ks3Al_kaADI=&^yhMz#kHwRasLezbmVD|0a$E&T<6xHLxFiJ2W8oqOE{c#k= zx8i0bjB>l#S#bWIa7mUJ_UzJl1KV2;NVfn-OJmE$5tZ4ZvEkbwl>#41RfYk0WPKi?t;*%@T5Pr^ZV1UKvNc#;z=G6AhM9D(Eu-sZ6|Ixrv_e(&|`UnTgi zAWI|o&S3T)RV1SQ<7WFb!~4Fdh<<877$q1G4KK@_J&vN(?jRe1qcQ7oEw|uWZVUDv ztEPjIZNG*$s@e9~UmGk|Z;p8dEiN3TE*$hkl;R>vO)Bdr7S{I#*(6vbYHzTpSe9*c zxm@!{Ptw-LXY#HOsUwsQ2uN%|vg;f>eIvQaek{g<9kVaEDPOut@yL+5#H?y|$!q*Z zD@G|D5D?A&>ym%`(irRmK~_ra{lU&#>XVmIb1RIL8t9H8Axh~$fM|4=QLXk3mgo(1 z^bktr<=D5sGT1Fmyxdrt)8hjL{K@Aa46t1$vLvL_Kky9KAvIDqq>~OG+UZMYjuRmn659lk0 z;oXeCU=<-s2LwbLXC%^4lQQr#KeOSXhpHgtx%b8B7aaB;i80lJB}Vj1Go^Xxh6k-nE|s z1>yQ*jDTaR{VREM%D53;^ELCjV};BAP&yMwdz)I4$ozKXZ9o@xx4n#;4` zz0ASV9F;HpU5_&XA zp~Q4L$O_3U2VjIQVhCd6n}1|>E%iX1O&Df?D``z zwi5e1Zb5>LbCh=+CGMe=bP7pKcU7~e4i3p4N(Tf)vz;5ibto~lcGzg@cO~vrKSy1g zIwS=Ba5Z|`J^aQ&X1A3N1c*kzUi?$9e??tH=zVnb3>>8#j#3d^dwkCye!HX0VtL)wY?Y{3Gt=O0<$-T+8(&|JC5gk7DfjEMB8J@On*KZyN$zk zfE-A6O7t7VV9y`T_B$k7T!{ZvMBi@REL2J`AlUFL96he`@*c<=F)0o^ilb~0s)Maq zJUfVgi%=yT3U*Ojf+JQ5|K)Xdm>mhJ64dw)oC&6Mmv62cf)>_;Jm&h=c3yjG1e&^l zfi}*L4Y>7<+eT<$6-XxR!nc$}%^}S&E2j8sx{IgG5VNW|K7Q4F@SL7 zbY&PN7!VEbSa*^5yjur{a6c3r}`90SDEc^l5nyLJ~C+Esa855Ae#MNVb9GJ zWz!s1LhKGs`+-a3U2u$y&rSAY)$B_j=DFX7(E$O`>|3@C_$TE|uqV*bvj~M12!*Yk z%FJ<>|MpNArHAFV!8eOx*~wvh$m$)PGQFFLEPIvN!#Lf0T+%POYsEmN+kO5vi0=L4 znYXnj%VaogKUpT-*)@5U$Ul?_tOpXm((F_|CnY5RC>;5Ir+Iy19wR6svsYgJmNqe)S zs);z+n-}v&ABIt`+;gSisi??0ISUu}l`GEF;NRGW+J6qsio14x0v(vmQ=Xf|{p%$&0LK8lKT6 z$LMU%AG4|#r2_(@P2Oza#xi1OJFFCJoa5L)xoj~fv^soUvs3wEjNF0dk5W1yAex=G zY)(5`p#*y+jC5sD+gU zR12L~BPC*K>D8G9?@&DH<*+rdMxdvY+DBO$*0eTXYUb(d<8FSCtYBh&IQm zpSPSPQ|CBrJB^%PPD*>Z`b49{+Gy3Z+SK@52kv`kI(0nPVdtsiIZj8t zL{6Qv_Wt}&!{H+>_=`i#jw>A)5DkCk)2R>9sOjynFjx6?!K zHo~vYGvO7N$i_jxExGw8{%_?)aWo zLGxs%*%XA;E>2t^+u2tt@83rZgsXU+<7Nj!<~OR#fgw)O3Q?sEM+2*HH)kXlp5BNV ziR|#ix`n4H@C|p^Zum&lFsI{snPc!>JG*3vlRiQb8=p%1=i_Eal@17qc9A!Jy8A(L z?tBWL3k53izW>***=3%8&3ht&>98k_gRR3(=JcG4RD<1N8c5%$!PO+~;xh~Ckf}#F zEI%LVywE8fuu@DzC}mr#F894#Rh=yv9a3y79S{&qIK*g7`eoTT#V!Ops>tbcM1G^4 zF4Yre#~W0$pXeEK_eALm^6Nu1d)cld`E(gaqL44fWgOugII0SHT&XE0`jS;EQx%I5MLFE!ap2Lwd3FMec2 z3Dr1YcfwJ&5Q8xrgE7yUlq2uLMjDJm6L|ycqFkO})i}yU0{KqI#qusJwHR#nO0^I* zP7Om_UdFeVjs7YhJsu-_98&G=*nhU$(cK+-z^ZX$ZPo5)=jdsRX zU=qSw%3c$gM7d9{dm5_fF%w&(KD<*}G^km(cf$aL?!Vq;g}M%6%Pl6qSZ-9Or7 zp2)nnl*`Y38b(L=mXwiWM>->?%h!rDz+eA6%wJP7ZqU0T`f?UVNhsOf!2Ijd$=}9f zodNz9HO^_XvJc+=i(T)JgshyXP+`4ta^^n0>q zV_WF5kd;5B0|KJit9HG3F{N^_XVTGjxP1MQ%EvlcYfj2tZND{@OT)~5dyM$v++>H% zrp`}t=9S7MgD=k6NoIC+=NtZ=KmV0^F_jJoi0=HR-CeFG!%T753a|r{os>LPFi!D>pnF2 zPnZTZWExTTNjG`&%|fvOWiEy!S!0fK4C@wR#!SE1x`#SjnnO$+zTq!fJykj|Ale$; z-fi?a?NSDO1zBT|6U(|eu`yFHeoFA)qcMi2IP>&vmWW>e`ZYY+`l%rkRQYr8c5Y|q zf&p?Rjhe)n)=^EwD5_~?PVy$KOiLqYndh)oWS+Use*JI)EMWC#mEkPkR779b*F3XQ zf&tOyIhoi}eB5&ZO4khthx46-^M;9iTokXZ_2>qzs)iA^%-ZYGN(Tf43vwa{l@D&0 zOv4E5v2-*I!)PLg(R63Y(a!SL_U{`;$3M9EPh^=z4x5P12NpW{Ny>5P*IT!Op3uEV z!2Ong_=;&6r2_+^d%yMPqw5L2*kMx%zQ`GWQk9x$|Ek&kX~`BI`)x(^&DOmQr33?_ z;mKQjRMLd9)M1BkG-f8eaW*`0o^w23ZQhLNId4?(hOuTBLgKmh#Kq2x-mo;vD{66Z zm=-lTqA0KA_5AvqTH>YUOC5Had}WCso}|#{E%SJC$#Z=LCfM|oP z8=u{Rd}W!#Qa0jfNBGKg_{su@uikv=?^@lEo$s^xwNsQYRyeHvMs&W+>A7C6s!?ul zX?C!Eb?5Q5PkV2;(g6X{olox{6G!Zo4jV-570%Xel1)F}S}Pw^&EB3Kl8clM2ng1S z#2`w%^TA7LQBAUAV-~?v=D|}+9j;?mL{E7guQw|)e_hDzQd$T5&7IV4YAKqUj0n@D z%8w#>p5#3<>y4+$BCB!39~NP&oDM0f68e_E{D27`tu2CqiEsT`(;TG(0-`Olc=wBa zX(|QwOdMsKk>5rkzs+(csrP_H%5TGxdDeRK{F~&fob+b1Vmfoo_}wAo7n3d zwuF3dt#j_Ayjh7ad)oEv63tHKFRtXzKVqI&>41P}_N2gj;<=~w4%F`0@r`Lbr?vUZlnw}p zw)nRE>mMZc7I-1pfz8gEe##4R#qKfNKd;&1YL)pBVyL~Azh^DM&XZqN33c&p&Wzz|()I?LSc|Z#iS+Sb zHDs=x-(m}0%3Y`k$s#+Plk?=#Ah}tB`Kt@6ErQL@c15UkKtQxbhIW}MKF+q=VI#l} z>~faqCy(iJ9Wdb|G#ow{vs{;#AIG)h4}+QCRHj} z1@<^Ym&i#K8dzKQ%*FHz=NvJqYCVCr-~8i`JC@UBM2C;jW!&qGULjwJK(YUCW@nCS zOXEFn--gsXN_TW~eMoFTvJYOg=v@kfk{uh!Kp0$xFu2j#mT>m6-&L$hOT4kUg$O4H z9hSZyoj>5@&yh)uMxJ%II^C%XgNL3DDX5eV2#D_d*B|u!igv9Xa##;yA9M;19$;eE z+6P@tv-GGYvwpnEJ7IJHKs5Q(Uq{p>lOJ~2Fd`pv=9g!QMRnGn+w5W0?AQ|i(d#B# z>41P}cFf{^;)Cx;uwsCtF}n~;wjq}6bjIn=oJAh{NBFZD=2=1}3_4vNcIK(|*f-Fk zbC?!2g%Xi2rJvlN{N$ZvkYf&82!pVrP7hrL2KiI(n(&3%AQz8PZtgt%#hxGUUe&kiZ%Yrroyu>&CZz6AIv^mL?X&(Lh#h51WJwo*DEAe#Ndxh}cH zPIg%-v6I~7GHDZhHrSpxmR7UzvC-$dndenHARwB(>fqQl^x+h+SL0~RDFmY9SZqCv zf9vsYG5&QzU@CVG>YYJvL<%HtUdQk4WS(`vNNZ=141d_)?zB!RQd=0&Bt1+M5tt%u zVH8^;C9hH4tS`tyDK1+_SGJ8iaj^1sT)izOe4VzCxU#%{7#$E0ZJ{e}%;-<-wl1q6 z>!-N;*UNiow_h9L?G>7hCpn6FjEK}3U;8qyC=QMCj55ucR#J!u!+zAu`Y}b2#97^ zuqOQnO7>hjnu)S<9})p~PUp&J3?pviN&W}Fc#e4zW3QBQkl40v&amDhlha?%4fEF` z7)IDDB}vV9dyQ%Rwu8&4t1+#=z`kENbh6vOn_9mmpS`dH26#j3%|RjMn0Dw?_x#># zrOTp)waw97y@=q?q8yXBEz?uQJHa}-Y!O|%G&f1_g2wf+{OV=v+MTvCywWW=R3DhL=G6E`zU2p4|Fs+GGu%CVoMKSau9^ zu+f-KSAYXb( zvB+8$ou++8JPVQgjoEjl0|KJ^{=mzy)Ww?`0t8$f^+;4WwTpxL>hC!(;-eHLN!eyq*&XGmByKOfoib*?mKiIRp zbJZ5X9W#64p>#k%v_+h)N%d$6qle29j$p!+>CQZ=meG7(*Xnq*FCw>C!tg7ilk?4A zrIcVmH2j6tO~eZdvRpO{aGd0%TI%Be2`Pf^k#KUu^B08_K}rV%M6+)Ro(fXOvt3pK zb|A}Lgca_p&J~!QdrZTT7vlN+SHtMQfN1!~E3W#9e6Xj>762ZV?QUEv^BGkm-Octd zX}0+Exqa(K>E<7)57F#`TKC;U>|QQgK^^bucEl}28aLE8Yy36VZ0LUEm$x_1t8_p> zH2dw>w!KYb6YRZov?s=9I>u%0Q>Jm&X~%|S1>XiG^b*JVe*kIBIJ$-wyO>K5s* zHAVLKXx``|^H+op7sLD{6hfyK}i6oRH0@h2zhTlg9@8h!0 zM`7^ZZoYm+FN~UMw(o2ie2tZ$lnw|;Y(TPm?wtM?V)u1fcVhQ(C!bWq=SRL~gURl0 zuz%t=-xfv(1Vpp9?e7&u7qg$sdePBdxR~8=F?+d5+gIT=0KfNQHYs^rtk(^2S#Rol ze|JfdazymcDkytfecxlP3n(2B5Z(9to;aRBfp(zFcHwAD4>(F!I7+TNR+rxq9pzuw z@$5yWg+kT^r1kOIZ+Gr`6=7(`CeS0vzs$emJJt5^w?>|^Ha;q z%~NE&AYK59szKy=HEsfZ-5H%H(bg<#z{50%R5T#R3z)RUhj0A+C3)WvmmNoc;dQZr zT)f%7pIdog;HAIE`y_v6*|USm)&YF>^>4ac7ls%YA11OX5{h!-j?EqapJVr33?_;a83= z7fEfn%Q^ub80L04tafR@5v*`@g@lXP{xd&d-3nAXARwB}222$X;^ez5n~XBt?b%u0 zjKd>r_KHMZ%}(X_f6jBZnmxl@ zI>(>=uX&b`*sX_TzB_HAoDfrs|AlE$(}*-;_u&b%^2keyP;8Px3f-*9^41Xj=xrwa zscM7Z$t3&MkkSDGi47!+yii_TkU$;|_6j;W7^{H&5MPJ5ty7Q7k9+^d34}Do+uq}y zX~Y-1Y!mn_<^5O;P`r`f?Y!jhB{dDwUHtk|^XI9`-X5fQg}6I$jVJ|+&|pcJ1~rU$ zQP@j+th{|`Uy9kIWPf7@!$AhYK?>Y;`sQI|2kFl5v+j9?ESYEr8R4F7qhmIjOf@^L zng|c6$>C(L3vPYQc@MoFUfEZ$G*|=@ov9!s_H@Ctf~9sk}ZDJe=DNfIOd6! z5)6ojzgD(cyz3kA<#e<^%AQm_Q(okr-n3c_%isD*73nZ1hg^5(1ay9)U z4W}u{K!D>UdwcDY;haWHt_jH2*A}_6*36LHsg!^~H2wP2M?~(N>aqdU^(pRny_%2S z)idD(jGq6(uXsC*4hV>5cj_?Z7^R9CDBg%Y-928QhAP@OHrwy9*lT-)(E$O`?7?{l z1$(B;3R~YE7d6A}tL_xwIWapgH)vfom>$^^l7^J7p!Mw#&E8)3*}l~8SuQIEJ22Dj zfme2ln&B6|)*9#!X*RBAYktSiX1|pV2#99iv-(f&MOed{?Xo#I%KGBP0OR4~6X4?$ z-R*7E!Xdfw1)Z|-HT>E6=9wB?q8}E^Nyf_oq~GAh%kJu}>g52`WI>oF!pYx%kvIG} zjIto(Do8bNmb-8o?jpgrsKqB?T1e%)yrH$Arn+b2eyM!?Ez0%E5HXwZ>XA*&Tes;Y z7RBiGBG%%Odc6qc&JJz(*VmdSf3dar`KR-)7Ox&TEnYoR*d@QN$=?e8*5>UK6(5Z2 z!Z3)R7RwVqFm+xyzT0-fqbC0d_(z(zpDtgWLW~c?Fa*CXZ&hLH!uJ2suT#&M{7UdE zoA;ll_+VJ+OfiVx@eI_0~Xe^%K4 zk8d3o->udDp9g+ki*w5U#8@7NA^4s6z1F$I@}IwbOQG5SV(^Pwq$>YO2P1d^e#RAi zgmsr8#6GhBf9R54$K;Oze@u%U#V5vMKI zK#AKsdGHk!jDKIz5sV+(N%7|*5XX!~|Hrmyt;R3)amWSw-}x$D)F_NjUtih(zb5WK zq4@KNKLPv+EtV_$5aXX=7<7F*cjko$!su*%YQeeH#Gm6b!JiKP^cIEc)lI}W7=}Uo zF4yqF=B6&b%KuN%tDDXX``_|ak!SLE#Ko~4aeRY}4_-QNbpb-8wKKp!Cio@m&b?{!4}yO%?$jp9Cx(?^O#W{EwRKB9EdR0_){is!C%``um#6yg zgOPuM{>OLaUtbeOXZQc<9mRW1{u%Jk#O+diVpuuK=>LyT@exf-{s;ZUjcm<7F6{IA z<*i(kpZq26EPmN(qqGk(EE_8Z+5g9VyvTaaFD!q+r7Z&{za#h^zuc<&PYkQXF!(=Z z@}h@p?Em7MK3HJ#2Z2B6%i^`N|35e6g~eeQ)c>Ck@xs4^(P{gf5cc^vu6ZMqUj%;9 zm;2W#J{S*$VG#enJ$b>AE;P?EpkLo|!_>nMu6!wAbMuySp{!9P4bou5-sfeBDviVfR z&UVM0R*PaNRIU8_l2yc(S!*~-2Lwcyg6;2oDvCD3g1wB2*v?o-8H{z5Vz*15Lp80V z6r}MFtn%T5{$k}+ucN3LT41WXZial0omyDMf@(p=phcEQt1_|{K6WgAKN({I7M5tX zFyAdzJ1rjB8!}sHtc`)UO0?rSn@mG19S{(0j3$3tJBZi|u|tH|3)~@Xy2;7A#Ih+K1+e%2q*k+UNDR5psB}O; zVgo5+UU}7+C&h<-N?n#i?8WZT!<|L(eM3?RJExkBmtTK)z1eT20|KJiA3w4QQ;cBEbiJ)I~IAq~UDFK0Ko9v2t z^=hIobJ--&*-|$VyJo~P%r6zuU;bzsb&{sjw6Y>P?yWFNKp>i4`1HNKDV%^li;fP% z8qE-tOLN_{!PDi35`W{;sT@HY3R145C5|$TphSd|~hVUT!d;A!u!pYoghy9B&J;*OZkG2#99qO}lw2 zb$lf{-VPmK;cnAkJwe~BH~TeCR(Bq#-xbmBLhDCKB^VG5-<|1gqVc)PWrqNdTIsGB zEbmIwIH_&6ZzbDP%))aL_C5fm0|KJimk#;cGZbW3yDXzUf@}_g>{JBVd2U`C`3QH! zxVV-q6~+RlROt4AMBZ#@(Ubrra1Uto>W6MNMN~tZK+k?|I^)jpQEd zvBcgU?y=6DGf??8xxgax5^mC$5dM+KCtA;)D;*dReF-C@}+qAM)VP(aEx z0T;JXo(<)7!H{)9r33_`>5sZ=bE)f_af=@Gz$W(q_C1R6PTjMfXT4g|#R6FpUto52TPg_pnmT_kpgyLySo5iuhZVJ!(^vwHbzh~zxsnPIv^mL zeaEV`hl#xtJ0gg^!(DMuRk3Jqt+4gB*ritWuXI2_H2abr87~ogx68(mO?J6Q)vaB; z1;$=N9;4Z*98Zep{@Lue(g6X{?8cQl{zdFPxF198-R`s*GNZNpHl!$=P|f}}nLl^P zWGfvI5Y1kC<>QxAoZaiPg*Y0s2Hw2^-o3?bQ>x-@WN-WffA&H1EFp1LdSe{hqzP{BuW+>}H)BcT>M zO%s~QvWCnn?eVYYZpx#2?V#*&pa>zb1R-#ERWWFORvbO&S8#QMY8g%9r%Bt^3-6 zlLhm059 zWkq++14 zlu20OV^8d%LZlL#PI|+S&%5396a=nX*JMAKULBU&@#t&9=zxI42HJ}}aAo$DcM-d_ z$0iZG(w(b68i)4Q^7oS#d-ZaYt#m*@G}~XkelX>j1dlBSJF2zUPVai4lE9kXye8q| zGT!`r$m~Wb0fA_GVM^DDL{IYAdZH(KQ@d=CbElhM2&qOsGU&H0;&soPeOF3AAew$r z=>wz5F3BFt=mUC^H!5Flk)z`A9T)+B?uV(x`d&HvX)&C-(xQCc!(As$K;|O{{ohR(}yjp{V|(r*!!DydTLT zDIS|b7HQ)RPL^w=-W?(3@RVwc{C66E?hmH5lnw}pwn)+F`xA-X)?-VFo#LH6B1>(& z{KhWhmsYd!0)*TsldW_>Ks3Ac%(w0$KWXo=-C(nJUMZf%5V^5!Mf5MYl%Zo?9nYIv^mLU6!}y zg#nU%4o6W}#s-R!Votg3`^yv)vA!I}N3}N3A11}L_ZDZ(6Vtqw`0Io)e@*$P<#N8` zY#1HA0ag0Ob*paaNSU^i$6EJA6zS;g9HufYnbDeuM`cupsP??mixybwz<}U=jUq%{ z`uPnv5EU;tRUdDJ|U(Qs1 z-}@mm52XVFqAh;W^z`ef?_gKb(b4F8nim(F;jN!m8S{JU9ohFjm%HNA2;DrE&=2m? z)tjEBe34w@P17Y3bmvj^rSjaeFgh?Gy7RY<`|Qu;i`_lemf+pImHHiyXura2-`4O& z%)h^!8Ab^PM8j(hcs7Y*Nv6kA0gvkL?OY-6=G^x|h}Bbd$HhMNkF0GDN(Tf)vmbo0 z>}QICJv^30>`ZUy0Ciu45@My1q{oOg4<7qh~%VXyUViYAd z!_H3+n}xhJ%k?OV==Jq@!(#I+)uX6m8|;RYR~BDF@pjIX+a}*di&0@(hyo(QAQA~p z;sSU5{X@Ez*$7&La4oaEv`Up2=z7gE>8-m|2dxgg;Po&%Fd(|G`*P#^(CbQjdTan4 z9f{c8303!K6q^U|-)He}6^hPt?xcRdqUh|*zj@3&=gkwub~$~&RTrIYz0GA)#kWk0 z(BknhEhsZZcp_Cisv={|xi)1p(6Uf`^^(8 zB^VHGo;4rd+>u;8*JB*;s2neUwp`1^69^yWneDkYeJaNj2=>y3(g6X{>_OdLtDuhe z_E^ebbUfFac_L9JeCnLFEhn~X^>t0-BRZQWRyr^s8h-8GMxqw)gK-3SRBvxb=`pb; z^U|u2O63O47O!Kn^S#mm0nzM#-}#?!$te90)klKe*PFjc8ilH;%gnxw%2NGK<*i=~ znF}i&5RlkF2a!bo89zYWh3@aMWyJ31WzUfnR_m8Ss<&m;?BtL6bJpz>r2_(@*)!`s zHkEQH*z0gKCKaPU9iuczJynFDFxdWtm&@zmUP|`I5W%oWo!8^}m9f~m@?&HPAbZ!>M`g`X_s^A^j+;w=P_2wBu zg10?``P5~+(Gbzvm!@apfRE0v_yUFa{Q<6?svTaBH%P(PcU_dl{&z?Kzu20>yrXSR)~JYnxs!Qfr33?_&5`-aKblgt3HSjz zT7r?7fogNGcW%;R#O>d{x+XiHGrDPK>ikH|^Zgw!oxR#xG`o&>% zU_kJSZ4tD4m#1~55LJxDnJI9?kzSica&eRbv$X{9WS^>pkjg_6g3~u`2@F2k>)Ty<3v7CcY17d|s>gk&is%nx z!YILjXn4lZKfX?x4Dbm!8k37m)*qQH&+C@{YeL~WJ@|-M&9iird!BSA)1k1)o13jO z8JfHnrb$(xj1*d>o20oV*OL#9^Vn=M$yjeTK35^a9sO)((q~JXh|!hKdnbj_fdSDb zS@P7JK{S8>-$F+Vv4?vw22i1QvaHu7zkdL=`unUEAA>(0GpePCSEYzogAlLsz3qEu zi&FOYN0^-FzVOLF6ELw_3ImmR+x5$iV1s1S2DS34eI~skx_QqqN-!YWK+m7K@kN49 z^jIFjCwNIKRk=xkUMApfNf*}7;Dfe=(SZTc@Uo}-WflWI$zug%ogv80gW!}!aLO^> zAzfaP-(A>=Zs3iqttcV+SvnCOUGg?@xqkXCT3EY7tQCqHs;iD7bmd%q=YJ`KPVv}s zSS4z*w?MD`Qj*LyfB9S5=E$HAEC^ZqRZ2i0c%QtWr=?vk?nF;TR0o|+@pkB)Y_R#O z|1#+g(2DhCrwA+|N3Ug z%Cb@d0@3snGv{SfaGVZjTn5Wb^E!8u>!_(`c_ZsqNvd`iJW!C$pS#-pSxN^4M6<8` z_M(Yn!Jgr8y z1k@rsObZdlYFc2HxbJ@{ETw@o%VXVPn7~Z00=GFt7^CZEE!=e1mcd2s%)c!#&#ZJ{ zK(u9|7Tmm(tUkwMgURZ%z20NxdOoEztK=E1+2Y+Z_PkB$fPiTBq@KwI^l?bA3vd+c zv$26GSf?H5t?wRrotA!F%b^v~t*jqlEYzy889U1>Tv{q@A^yD3{JqBAKw8DM3MK<3 zjr`8J2OlCM&-2&_GV)w6UBBfJcC@%7v}==%QI{?=PpXt)K(rxVO&u;?&$!TI6=dWE z-o*KG0!!{|-HfWJHgdQ6Av=_m4hV>5Z*VUu`AgJAF<_s$>tZT#P ziHX6HcmEiLB9;o$k(oTpDzD#p*xI+Q%pI8LjabrIl(*F4X7h)u7Qd#vl@>quzhxhj z=PmYF!g3g8k(a+q&b;wT4!h8b9$Hms;c@buht2aU9S{(0lto2#E~0t`?EW|!GX`UQ z62|!qZ@I2lB98NSvv^X4d6tlww;t#7z4_-aS}Ibh4{=IgP%cVsM4WPLh4MVUW+mAYbUs z*s@R*gTH?~u9`nYyghL>#!MN!Z$FTf$ zBWXN>JrqY{W?(!n#&}%rmFPF&Mf8vN>hkE9&9l7szL^g#Pz?vz+*L!_+K|%oQCcsjo=UsXF6ces=OUrJD zX!zh)-g$(IhK(LOO-H9=RL;SuEc1qR`V~b(UPbh$R*7y5OXVf88@z*ERM9|x{*2it z_2)DbX&GUJr;DI^Ch22w*K4!K&XOTEdF{F>E919LHMwUED|f}@b9xvh7?9Y2gx~ed z6_=5fw|K1G3cxpeW0tD<69HzJfJ=s{1c__djVD>RjFb)xh=xCZ={+xyYj5>f7r=3n zqjS}ZH^_UflI9u-7Xzo^>mlhtDFK0KdfA8f{gD#cHjkBTMkv|p?OP&m27Fu)lG;m( zs!}`N24feMN(Tf)vt!0?c$ZSm4lG6BXv{){l9dQ0tGy}xRH}(Q^zY=2`k1!6K&sj9 zjh!KHslAUDeZ#bO*59xFYAqq8w!$}nIydYx;ivdPImxQf4isd;{5yvV-C#qRcoB+E2L ze_bogUyC#rY3NDRXDhdqk=O0@*uit~x;@^x(JG)ed?O^F9-N{U958`=@uv`%R7yY~ z_&$dijlDMI?xg{&1qMZued`f6rR-_&m?bV9M5Pmv=H*XD~F$F^#R7NWZJWjg`R< z#J^7_Iq0$W=Mh5=cqRH)F8yrlZos8et7Axa{<(DzM(MzSXp?kgw?0SkLnv1Xe$Xpv zr|gaPv1ZTjmTWO+?T*_4cZE@c0nzZUzI}BajhMqI&H#@( zoA2Ve&zo$e0|KJi6aMtjqvVE1Jl3ZYZg|*B>!+SBB2Tk2Zy()rxZx}3LxP`D0s_(W zqu2Py$Sg-aHXQW85pMxr;#Kw2|73P=xb8aqFq41RCX5aYh=%VuF#RJk%Q26Q0X*uc z*KLq2(t587DbmJhb}Anf)_#gC|@RMh6B& z!~ZpIMbC2*eg;R`LDcjMz1YC`26&LuJEnF&M6Bzxc|&WdwEm`xD17VP531e?Z1Z}S z$hUhJqs3OUXQ~Cw+Q;9*Z@AUe{g9|>g;VS3Y=b@Y-p*}(#ZEqIaa))cL@Rld*O?ne zcRaZkMBCuN3mMbMz^6Snu=O4Ia*3Cwcksf7)_sXPr>T5E_N|D1YnlmHN-!X?fzBcy zbh+w=ztUJa}5&xl#fG z(e&OQ?O#tlPxM*q1oS+?&sZXNYCZEnNY7hW_Z&NiKeD2>(g6X{?4DQNTwD+P^})`@ zQG_MDAs_#yX{dD;Aeue++|(mf z|A1YBqcIadLB#qdmSsH}$GA6t+F0@ChIO5I(Z9^|Tz0qEu|flDeVve%@NeG~Ym7(w zUY@q}hH9HsdR?Y;5^Idq!n)6-S`cl+rMyvX^GwyWoinb~y%z zpKacdWD zQPZpDe%~hXTx$VZ>41R52D*rd{zBl6A~H$`pY;Gckm{G|52@1+-)sKxIvNhEWb?QG z97YEQM8g|IyQ1Pu^I3oDcn809om_pT&RJ{Sw`+DP{|>ruOt#Vi0nzMV;$DA=I^NM| z#nkaMzg&Ns6n(Rvns`XEMZK6^5q(P&6RwnCKs3DLGri^zJRPn=@J@aP?)+B)J~ja@ zbVRT?XH2nRC|Gtjk89wWk1bDiisb6Q0_9M;qy^NmsM3HaR(MkyhM8o&r z$lf47?Ci6lfJbHcyM_-Fn+EZCgPrJx>W-&!Y$&t`kkSDG(d^-8H;b>zcY(>NwPFS+d`AX5>gqFM3{oRe3M73(dA# zfmT*Wcq5kIeb-p=`gSz1CYq{&h}tuG&reL){HYd94$L^#&EL1+d)?sEFb#f9(3Y@u ztln)@qGb83&mb7LhhI^mN|eUSLrRoB%T-l~66M45ArW0E0fA`qvi7+p~-?S@8y+jfBfQ`Xqe>qtemc7FMrH*xeoYUa!4*H z*Ve#8tha;|BuWPaBsS1h|47!}02n*zx+Nsv(H)zuK`w z>41P}c7vksUC0Cb_-ylBuzUMcx2pvLax!ambo2IV5BwxCq-;=1Kp>i4@YOw!(>xpW z<2V{K4+FfTAB!P`w|x5DXVg3oJ+*A70l{R#Is3mU;1w5gwtE5_Oq0 zcb7*SH6WAp^Vx>^s7LzxnUhpFg=fFl!h~5|59;eA&toaK05+-pueAaM(v2D z&RK=Q5e-MrbNN3%H#@F$U_dndjq@v>A^0GlRT6xlKVN^<1nrxe?JEtd#DMiXTi?**<2irS%$HhgRwaP)0{s3YL$G~M;cDo@cVk0=V^JZctwMr z=E&iM_eJ_S$7L$tfEJcBs21en8*k!S)*@C&`qi}Ie$xK$bc43$=~V-v`;2GL4WmQX z#GFYw#{0wSv>_7|qMRhdbj8oFM5;;jv02(5JLy-Xn$|qpT3q|=8@-!EtR$)U6d2%l zT_xA0MxlkZnxj@a_PJ!P0;D` ze6fjs1o9SnSdrg-lBz&HNeZbz%Jtwt1@c8vNCl#lfIzg5o&WFJ`ZPF3VurK`gJXn0 zOHb|K|5mXTwW~Uk_Nj=zzk~U+loAYxhPN!aMSSI<*k@@3AL(~nrz#Kvq?v%rBwdX8 zzP!n~Fgh?G8vgCXu?K17jq=$jz@v)&{hj5)6!u2j_4;2mJC!#MgydYM0|KJiZcgUK zjlmx6vqi)nfD+I2wd9A>Z#Xx#r-pp(}s3nR!vG*GNQWm(Gq2v;MpS z`RZ?K@t-g)MD(a36iItL@WLI*yqE^gMEEw?Y=WPopJ0V=H?+olnua4E_T@)EGJllPfdSEQcUy4+b$k-0|J3n` z{xY?AMw!D}>FclAc*rdDp0&~e0nzOKU%ggA?8!dM2RksyFYTmqE&687@bfhr{qDyf zv4Wz~0RhqME4mI#B%@6A**aoR@mKUwY_#ug{_b^>E%JCjls@ajD8Ybec+*?HpGW(S z058YUn9cCNG4Q`}{;^!;f4|#f(tAFVPR5z;vr}aFY5q#JV1y_5>~wxg8z+?~H{rS0 zn*CQgARxN`9sZtDhsF=s{Yr6k0CHK@2XTQye^!><92qI|jBdu0t%>y36=D@vXC5_v z0ww+&dJvHPpIeylDbh%iXo9VOBrLe{ff8j>C3Pr#8I`bDlyjzug zsk{wz--giv0nygDC*!STa+O&=n+A4ZroZQyswc@EHkg2CG#q}|pFej~7#$c84ZpX2 zt$PSQ+h_9$KFjZbR~?Alg7(((*-Mfw{IEaX==iz`S4uD-8vfapz5Yx-G6yvYS!K4L zH)@o4i%dzUkoiGN-SJdDb#O>_P&yzW`2Mnp;2ZlaZbyk{E*3+HJ;(1o>rT30Q-_4y zQS}V;0sdSAv)f7s0z{)<`Kr|{vdKK3B`<+Z=KB5g7BBkUZI3w?Hm zI=;YPGg4hl^sTSi?=zAuE+(G0yz_=IN-!WA{!V@;@w(kbK1*2&_(DHNmna0NYXV~T zR8MOL@|MHG=)izzc;aJ?aws1!_E|dMQH%W2@v3x-dncrHyHdhMKK?p^->}N0D`&~RCYD{O#eHE~(DmCifk#g@b=8YFVlpOa?(}noWaE`S%dCKnSNOT3<&%z65rOVE z+t)r@W#c(1A%RcnfPmoR?!y6TV-}Aw^H^=gFT4fW#u@f0|KJi(ci!F1F_d(X}>4fYyA%TRt?&3 zH2Yoea$ywF??L?7En##(Ks0-CaK}T$-iQ@kVsG$QwpBJk`&-TSaRz%Z|6AiQIv^mL zUC{oW^)zhn((A-x9gsP;F` zRigzh283xL{M+GKPnf!pl?zQ<@9*69t#0sSm z+jB^z0|KJix1XlQdSxB3V-hbv;TLH2dWTYKa?HJAJm3*gO1V zg{t=WE;gk0*s0BeWh3t|yxxsw&y^Aoh^AkE(`U`eEPHVCI|pXj?HA=Mo8yhNZFh|rrKWmLz)uD@3&xAjwjg2cAzlPU)EQ}Hmh_=T!Z*0t?D)NBO zj^Zeri1EG-<9&-iwMbTx5l3hguWkK;kSbCI*1&#$^&nYAqJb4y&HH#49`2%wl;-Gv z(LI%P$Lf&maLjpJz+K2K>v0kH`u$ZU7SZfIcq{9+LD!$GRdIP9d?m{t*HLBzG_Y{%iT4FB@&P&oS6Tc^zvdTj_Fg?}TV}rxr826Z@FY27?`S)KAH% z+QntBpbggSRE}L-c3G`-KtMG6vbI-h}qZ8M4W7KSLLM_}$jxT#05QJ`LsDt%^DVbEBmd`>{PyFR7e@DbU;8fd%&U!pZd*x*0wj;<^F*2a>EiHz_qJ}i?37# zUnj1xDmPuDiKp>iatl^8R$ttI@x@nP=*vPfb)+e{ZS zA-;qwCmF_%6`1Wm)fR!14CjA46h;RGL|df3>xmVpq1q{fm@lvuVDb8qWsdXiTS1V=)@Bs|z(%>-eQ= zLv_Sac>_l%LQJE57GFXoB&1f+_Obc?A&85821xBSPs-$zrvG(!pH+-WT zSQ{PwABMPueHXY~R61$#ERPJ_A%6M_Ij5$;W>xTaOZ(D!*EJ2v>PiO$M4NZRf$rkd zXdUC(mer{K(&7{Kn{eRMQD;mmY`Ir?2I5CPpS3284h)EfuYMPQ61w8 zrkoeGcJ{3yCBt6LPUU!<(_U3oIv^mL{kP{Ai<{Ny$OOkR7(2yh=+Y8>TWj`h`D4}n zF5s71EAL7N1Vpo6Y94zxO*}H<*;*WBOAwmQAT%Y#7Y~=W!y*NfaVL3gYl%E0G${`W zq{naHB#c1~tW`vF;&I`~BqkocL@a$|N2_8obQh#9GDhe4-TD>R_`%klf(@22R(x(6 zO6h=r#0Dh0a-1XHbJ;zfbw2@ixA?J1DhU+u6-~|d*FRou=t6#jbyHR8fPiTB|5iP7 zi29uw&w7C!)jhsqsq}1YQL@8QJybsW41P}i>y1)?|EYP#89UznH}GLqg*dq*CM1aYp&U;d~0k-{8Bm~ zAez1Wr$L>F-3!T!`rR{ro4%6h+f8QQJk7?HEaJyM4xgM?>_OY6zr(p@w+z3nI84+1G9apW{ZdR?9i%oKtN&x zIU<_0E1dKRRc3wT*>N0Y8xj7J;^P7t@hLrjMfj`Dvn$Q>|JYFMYSZhi(iibs<@kkb zOGITht~T!(6{ZDwc1^)l(zdMG^aB+M{o`4;a`bo>%Jy80$*vfWJ>pwqtFXw#kw(ys zJlone5V8!fdd>R8Pdq7UThQWqv)9HB!mWSeSvjU|`wZ)80BPqq)-QhigjUowYLFYI zfzY+!+13SBu(^4cpIwJXxFqbl>qf4n;&fm<>rswGJ|KP?_P2<7ya=0M?WN5ps^jzs zKDM@b;yO|X21LW}>$dJ;f)9>o{Q(aQitnH&8E9X}Y=66KFHSt7BD(d_FiJ2W8lL{e z(pYlkA@OWD;8BC)v;Plk?*UfTmAwyx?0Zutb<%rpW7O*Dl<>&vU)_`{v8D?-M-htoN+5 z_F8MNUC&Zs%2-ik4&<~{g7agm&D>Pd0D+>gFZ*gCAArh4peF1Ip7nL&dy3e>WxDqh zLF`F&j?GGv1_%^|{qNg0wI`)ydDtpaN~UKq&QWN3@nianzoo$8-&5=*DIsXUKvCe! zi_h`twTWoaN$@OBS-DUO#Z14>-%eroviJ0NDmJhudQu09 z(UzFMJ6hdgL2Rt6F-Kb@4G<`9S2~X!mv|n#iYhb5!_E=*B+nALJB;{^@sM*tY^=*M zD^t<{fugXxyxGXV@{;ReJ?fzF98WKNiG{ac(5W#Udhzo?3!iFVvR>DhqyYj&VSn6t z$fY!No9AK4xXSh;=B0bEn;UU&q$f?Tf(jd{&e~nZ>d(p8X{>^hKJUu)EG)g0hc+;b z3&B9N+2>FD_+nn6?As!$zMw!oSqL394k0fCA#b!NqoDa9wJXie*{45WZbDV_m?}tR zd$M!IvPa@DmY8bWR+|fXLgHr|pLmqaW2%RZp(>o>$x;(ysHRd~n!AlE+<03E+UUA_ zQB)P4y!gJ3ggp%jAFv~)dd3_RX1!;CBL#U_VfV7D$Jnn`>)4V82o#0=Z0{3=ggxEE zW|7>}Jh5sfeX70Dn9=^T!iK9)vu`p6LL?0kC<=Q?;rh=Adj^u=)4-nYDOEcRz;BEy zq#4+iMiV1xfIv~$3qE~kEMd>|uo1wHnBmFkFM42;PCMe?2!)OPQ4<~UPtpK^qOgnq zIsbB6^a$*YsrTWaP*`6!tZ%aCOs=%P|8Mq^vkBXrg}vPHz>I&>>A~t5^EyVec^#|D z!~|C0bvxlFz3ytnOgwo|;Fuk6Shu9rWL=%L=z6#7 zUnc7K+kn5V<2H#;7#l-ikbYuL*>QglK{N3;ZU1U6LL5b>@K165KYG>hzw7w>fxo|Frj(yBW`@8Z`QzTUzcEt(koax?{Mlk1zXtd< z9e1u4@)L#;S0o0;z&2JBemB7tQLyU)@-~hp&F)cLIAe^E@?& zPKd#fFs)*k!&0mP`|R*;vbGrXZ3A z2ox2MmbL%aPQotmu)b4)J=b$$oap9KJ7UC}%iAP5U4@?0gBKt_MLGwLZ*`F+2ho}{Y9aBkn_hi|3~v$o`($_vJzKhq zaSH7GG&?eH2MrH-!BP?8P3ojj$Jab`=SuKJv07acUdHKC#1o z%?Lh{1_%_lD}x(#*7}F4$f$vRl+5ES%%cG2G2gRXoj4QLJlfoBn;%Zjn8J1KFhW-3 ziP<2Hnplj!v}6(XaY5$x!r<;#kVckzSo$<*q}WqkCwgD>*2bQ_^q@xUFCA_!X@Edc z8oBKHjc<{|Ec3AWxEhtv0cZ0-Ba1!tYo)`4e}g~SuNV?I97gCpnl1I5l4k(71;=3B z`Xonl3gHeT;8xf7SxD7b;$a2TA#^VYy%<6-^DIxf5TV_AMSo)_PdN3_!2(tQ;UV zCSv8M*)%2uvHSOPG>wu52o#0AWXOqRvg>soHWgQ+hQLHtz(h(s-KQ%P30tL)*{!zf z$~li+ul4L|#3&0U5Zi*om;*NK`urBc?OG`0r)PX(ZN7&sheBNIJ^fb6A=+tnr4dDz zD}|t6H{HIkmoBQL0RlxSB(r`kU)j9@VU?6!>N&nrY^Uts-=XYn3Y#ypH77SD4G<^_ z`^W9e@1X#-(Zk9JdxK~Dd=awk4vvsr7Q{Z+!V$714G<^_JNCp&(S*Gj{n44g-sGuU zBZ40mKbhlkw|v}WfLH`%=1-Ca2o!~V)y_u#shO?lM-%oIPkF&0?l&Zx5!U`Juz5{K z);c1qBmx7)?Ha|c`>rEDZ@dDVA+~wg3S315bR%tWvzTAqQ6pfDCHQ+SEOMP^t~yPd z;?ntp%q@1ye7&027x5HY1ubDh(QKn*}GDCOZVUnry6+=0Y_G``()HfOrn64uw5I0S36aGYZf_H97}1d394 zR{7^0Xasa8tPt32hi5L%b><_W)9NB!QTn_7M}c$qneb`|8Zb~4c;445>M0DCdDsCG ze5Yqz?Y%sTj(5o2SwSPh9es7=^E8I!=AY^1PvG{3cUaJH@+bI+v8zH`KZR-p5@!cXA*H-zS-mywQGtd zSPf=I6G;OEio)*J;mhSDc%_H+nFGQ1cs8hykwCUpx@?zy!m;^F&8Ulvof?7&3={?a zVn(~034AZKLf}=N>4j1&1TakpxJsb&`j}}KzZZfA3={?a%&hAvL0Yh#nq^>2z?t7_O^M7hscIFY?x}BX194k ze*;I#E#eA}boRt1$xl)f%Znjcnu1ukuqW^ae;as@>|{SC9Z4hmJZIEt4|wZT9sCDF zBO5D1&;Wtrc8%r@!`$0vJw_{(4tQAnT-e}#&*40|1T^xPqq&S%hJpm+;@j-kjUXn8 z06|gc@$V*BB>6!P8whmQ0Z&c2%pqpk8;z`YpeFfj+x>4{a!CUOio!n8;mrz4st$YD zcwCLjMqO5-E~`BmDXPf{tNT~&)<(zE(d4MQJm{G&I}7c=VQi5$nw;~uU_7Z3>b^K? zQYkH;kwUYP2(OzFUdueS6=ME4Y(Tu+zHXbY7@2abfG9nY zwnp|RiNzR9mMkJhHRC4&Z^hzYGN?(gJ!~B`;yUH&GeBwtZ*7d`{!?g#w>PuvBEK+( zxFiu6C^&6_2dOVtKJ+gFuk)}90=GTc<7In80LF;dXPUnA?abfn`j#|cpeXQ1GCuk} zfuHfPvxxvd?U|x>Yl8n8o&T&Rc|P(7V=ady0s}>XKf3?UbP7`S9@f7bDsv_(^EfK= zq$g&YtjzGrf60EuSl{XhQbIq`?2KolJOsEMIErVB@H9vx|0h8d} zppmnlj2Tkhcs%AC5$IKxmRaa_0iQ55)dH%Got*(<=0<0_kg@LGoOdc<>7E}RY*Ugu!X`Sbb< zj1xyLtMDp4$5rRq&>)uQ5QmWuNDdxfn+euJ*O#v@O(%84Sgd<@sH4#{ROy;t*)Z+y zny%;COaG*cDrvw#QR=w5MR$JKQk=z-2t3x>fU_idDh2*0bp9j_yTHD}2w{>22o!}q z^tXh2DZnOJY&5VV;;kMkakzA`BXJn5DiQ(qs-up?K@tIiqR{_*=4ZY>EYV`wgq~of z4-um=SbJtRJK0L|UiO%dj?`Gv0D+>gFI$uH4F&IR7Rw{-L~Ct>I4KbOH_h?lydXAq z2@OB2YfRDrfugW$I#$F}@CNoOT#YJ(zn_G^*CQdk5P$!8xc%^b`coYJYUS^7*7}hu zc!TBs5G*2iUr_w`aguAaeP5}KX@ad_q7hm6S>F+j1#$93UHDfpFuE_KuH4xio$OH%q#hYkbHo}s>nXp z!allLZqz5{o-itP5WlVPpq^H;8cq*yFSpul9?*5^=%Oim>1Qo0ES3fXmIp(y2!lEQ zx%qq&PT<{s%^!PHOi8j>d{1a-kd<8~y7E{|XU41epoXxR&eV{k0RjadYvvyGUG~sJ zggwM!X@ou4Dwk)(Q%o|3bkc&@*tuw)DlTb&KvCGfUTxcx-3+zZSi&A+Wn{|k-?cwE zqWD*9$beS&y z4&d+ToF?%JLB}r#etGBAMMD2WfRPabgXAB2vz=>p;BUk&`~&ReAZ3FGGw7#u&{p6u3XA@#HJ^^c#^ z@ecz3VCTFALjS{naZv~i!cV^4&dUoyQ}V}h{kyl0_(aD)0{kPLcT4#RV{!-#j-O%Y zg{xnI|Hp^t@I{p=7Mo8oKiP_#Cw8l2qp+F(&JVUHrOWN%Mvp_%0D+<+{@S#-RBEfz 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