From d4fe96147c09c30c936863c12177b39eb23d68d8 Mon Sep 17 00:00:00 2001 From: Di Xiao Date: Thu, 12 Sep 2024 04:30:20 +0800 Subject: [PATCH] move over trimmed xet-core (#1) --- .gitignore | 17 + Cargo.lock | 5011 +++++++++++++++++ Cargo.toml | 30 + cache/Cargo.toml | 36 + cache/src/block.rs | 212 + cache/src/disk.rs | 6 + cache/src/disk/cache.rs | 228 + cache/src/disk/size_bound.rs | 388 ++ cache/src/disk/storage.rs | 539 ++ cache/src/error.rs | 48 + cache/src/interface.rs | 105 + cache/src/lib.rs | 71 + cache/src/lru.rs | 75 + cache/src/metrics.rs | 117 + cache/src/util.rs | 84 + cache/src/xorb_cache.rs | 450 ++ cas_client/Cargo.toml | 58 + cas_client/Dockerfile | 37 + cas_client/README.md | 2 + cas_client/src/caching_client.rs | 328 ++ cas_client/src/cas_connection_pool.rs | 454 ++ cas_client/src/client_adapter.rs | 33 + cas_client/src/data_transport.rs | 591 ++ cas_client/src/error.rs | 74 + cas_client/src/grpc.rs | 822 +++ cas_client/src/interface.rs | 92 + cas_client/src/lib.rs | 29 + cas_client/src/local_client.rs | 602 ++ cas_client/src/passthrough_staging_client.rs | 147 + cas_client/src/remote_client.rs | 577 ++ cas_client/src/staging_client.rs | 584 ++ cas_client/src/staging_trait.rs | 58 + cas_client/src/util.rs | 234 + data/Cargo.toml | 157 + data/src/cas_interface.rs | 173 + data/src/chunking.rs | 272 + data/src/clean.rs | 620 ++ data/src/configurations.rs | 137 + data/src/constants.rs | 45 + data/src/data_processing.rs | 367 ++ data/src/errors.rs | 85 + data/src/lib.rs | 20 + data/src/metrics.rs | 15 + data/src/pointer_file.rs | 325 ++ data/src/remote_shard_interface.rs | 466 ++ data/src/repo_salt.rs | 13 + data/src/shard_interface.rs | 55 + data/src/small_file_determination.rs | 20 + error_printer/Cargo.toml | 9 + error_printer/src/lib.rs | 86 + file_utils/Cargo.toml | 23 + file_utils/src/file_metadata.rs | 203 + file_utils/src/lib.rs | 7 + file_utils/src/privilege_context.rs | 379 ++ file_utils/src/safe_file_creator.rs | 286 + mdb_shard/Cargo.toml | 29 + mdb_shard/src/cas_structs.rs | 176 + mdb_shard/src/constants.rs | 9 + mdb_shard/src/error.rs | 55 + mdb_shard/src/file_structs.rs | 163 + mdb_shard/src/lib.rs | 24 + mdb_shard/src/serialization_utils.rs | 356 ++ mdb_shard/src/session_directory.rs | 144 + mdb_shard/src/set_operations.rs | 487 ++ mdb_shard/src/shard_benchmark.rs | 248 + mdb_shard/src/shard_dedup_probe.rs | 16 + mdb_shard/src/shard_file.rs | 2 + mdb_shard/src/shard_file_handle.rs | 248 + mdb_shard/src/shard_file_manager.rs | 977 ++++ mdb_shard/src/shard_file_reconstructor.rs | 15 + mdb_shard/src/shard_format.rs | 1212 ++++ mdb_shard/src/shard_in_memory.rs | 266 + mdb_shard/src/shard_version.rs | 139 + mdb_shard/src/utils.rs | 62 + merkledb/Cargo.toml | 51 + merkledb/benches/rolling_hash_benchmark.rs | 273 + merkledb/src/aggregate_hashes.rs | 55 + merkledb/src/async_chunk_iterator.rs | 509 ++ merkledb/src/bin/testdedupe.rs | 84 + merkledb/src/chunk_iterator.rs | 288 + merkledb/src/constants.rs | 10 + merkledb/src/error.rs | 45 + merkledb/src/internal_methods.rs | 525 ++ merkledb/src/lib.rs | 45 + merkledb/src/merkledb_debug.rs | 318 ++ merkledb/src/merkledb_highlevel_v1.rs | 156 + merkledb/src/merkledb_highlevel_v2.rs | 28 + merkledb/src/merkledb_ingestion_v1.rs | 120 + merkledb/src/merkledb_reconstruction.rs | 177 + merkledb/src/merkledbbase.rs | 157 + merkledb/src/merkledbv1.rs | 15 + merkledb/src/merkledbv2.rs | 9 + merkledb/src/merklememdb.rs | 439 ++ merkledb/src/merklenode.rs | 371 ++ merkledb/src/tests.rs | 569 ++ merklehash/Cargo.toml | 21 + merklehash/src/data_hash.rs | 384 ++ merklehash/src/lib.rs | 49 + parutils/Cargo.toml | 20 + parutils/src/async_iterator.rs | 39 + parutils/src/lib.rs | 7 + parutils/src/parallel_utils.rs | 321 ++ progress_reporting/Cargo.toml | 22 + progress_reporting/src/data_progress.rs | 374 ++ progress_reporting/src/lib.rs | 5 + progress_reporting/src/main.rs | 90 + .../src/writer_with_reporting.rs | 41 + retry_strategy/Cargo.toml | 14 + retry_strategy/src/lib.rs | 128 + shard_client/Cargo.toml | 51 + shard_client/src/error.rs | 26 + shard_client/src/global_dedup_table.rs | 235 + shard_client/src/lib.rs | 86 + shard_client/src/local_shard_client.rs | 138 + shard_client/src/shard_client.rs | 485 ++ utils/Cargo.toml | 49 + utils/README.md | 18 + utils/build.rs | 8 + utils/examples/infra.rs | 54 + utils/proto/alb.proto | 7 + utils/proto/cas.proto | 81 + utils/proto/common.proto | 43 + utils/proto/infra.proto | 22 + utils/proto/shard.proto | 69 + utils/src/compression.rs | 98 + utils/src/consistenthash.rs | 160 + utils/src/constants.rs | 17 + utils/src/errors.rs | 54 + utils/src/gitbaretools.rs | 24 + utils/src/key.rs | 122 + utils/src/lib.rs | 155 + utils/src/output_bytes.rs | 52 + utils/src/safeio.rs | 59 + utils/src/singleflight.rs | 712 +++ utils/src/version.rs | 244 + xet_error/.github/workflows/ci.yml | 105 + xet_error/.gitignore | 3 + xet_error/Cargo.toml | 26 + xet_error/LICENSE-APACHE | 176 + xet_error/LICENSE-MIT | 23 + xet_error/README.md | 227 + xet_error/_git/HEAD | 1 + xet_error/_git/config | 13 + xet_error/_git/description | 1 + xet_error/_git/hooks/applypatch-msg.sample | 15 + xet_error/_git/hooks/commit-msg.sample | 24 + .../_git/hooks/fsmonitor-watchman.sample | 174 + xet_error/_git/hooks/post-update.sample | 8 + xet_error/_git/hooks/pre-applypatch.sample | 14 + xet_error/_git/hooks/pre-commit.sample | 49 + xet_error/_git/hooks/pre-merge-commit.sample | 13 + xet_error/_git/hooks/pre-push.sample | 53 + xet_error/_git/hooks/pre-rebase.sample | 169 + xet_error/_git/hooks/pre-receive.sample | 24 + .../_git/hooks/prepare-commit-msg.sample | 42 + xet_error/_git/hooks/push-to-checkout.sample | 78 + xet_error/_git/hooks/update.sample | 128 + xet_error/_git/index | Bin 0 -> 8877 bytes xet_error/_git/info/exclude | 6 + xet_error/_git/logs/HEAD | 1 + xet_error/_git/logs/refs/heads/master | 1 + xet_error/_git/logs/refs/remotes/origin/HEAD | 1 + ...ea73449d91d43b55b4cc3c27f14c04e0583fd5.idx | Bin 0 -> 67880 bytes ...a73449d91d43b55b4cc3c27f14c04e0583fd5.pack | Bin 0 -> 636849 bytes xet_error/_git/packed-refs | 104 + xet_error/_git/refs/heads/master | 1 + xet_error/_git/refs/remotes/origin/HEAD | 1 + xet_error/build.rs | 97 + xet_error/impl/Cargo.toml | 21 + xet_error/impl/LICENSE-APACHE | 1 + xet_error/impl/LICENSE-MIT | 1 + xet_error/impl/src/ast.rs | 165 + xet_error/impl/src/attr.rs | 210 + xet_error/impl/src/expand.rs | 551 ++ xet_error/impl/src/fmt.rs | 170 + xet_error/impl/src/generics.rs | 83 + xet_error/impl/src/lib.rs | 42 + xet_error/impl/src/prop.rs | 147 + xet_error/impl/src/span.rs | 15 + xet_error/impl/src/valid.rs | 237 + xet_error/rust-toolchain.toml | 2 + xet_error/src/aserror.rs | 50 + xet_error/src/display.rs | 40 + xet_error/src/lib.rs | 272 + xet_error/src/provide.rs | 20 + xet_error/tests/compiletest.rs | 7 + xet_error/tests/test_backtrace.rs | 274 + xet_error/tests/test_deprecated.rs | 10 + xet_error/tests/test_display.rs | 303 + xet_error/tests/test_error.rs | 56 + xet_error/tests/test_expr.rs | 92 + xet_error/tests/test_from.rs | 64 + xet_error/tests/test_generics.rs | 161 + xet_error/tests/test_lints.rs | 18 + xet_error/tests/test_option.rs | 105 + xet_error/tests/test_path.rs | 37 + xet_error/tests/test_source.rs | 65 + xet_error/tests/test_transparent.rs | 78 + xet_error/tests/ui/bad-field-attr.rs | 7 + xet_error/tests/ui/bad-field-attr.stderr | 5 + xet_error/tests/ui/concat-display.rs | 15 + xet_error/tests/ui/concat-display.stderr | 10 + xet_error/tests/ui/duplicate-enum-source.rs | 13 + .../tests/ui/duplicate-enum-source.stderr | 5 + xet_error/tests/ui/duplicate-fmt.rs | 8 + xet_error/tests/ui/duplicate-fmt.stderr | 5 + xet_error/tests/ui/duplicate-struct-source.rs | 11 + .../tests/ui/duplicate-struct-source.stderr | 5 + xet_error/tests/ui/duplicate-transparent.rs | 8 + .../tests/ui/duplicate-transparent.stderr | 5 + .../tests/ui/from-backtrace-backtrace.rs | 15 + .../tests/ui/from-backtrace-backtrace.stderr | 5 + xet_error/tests/ui/from-not-source.rs | 11 + xet_error/tests/ui/from-not-source.stderr | 5 + xet_error/tests/ui/lifetime.rs | 24 + xet_error/tests/ui/lifetime.stderr | 11 + xet_error/tests/ui/missing-fmt.rs | 10 + xet_error/tests/ui/missing-fmt.stderr | 5 + xet_error/tests/ui/no-display.rs | 12 + xet_error/tests/ui/no-display.stderr | 17 + xet_error/tests/ui/source-enum-not-error.rs | 12 + .../tests/ui/source-enum-not-error.stderr | 22 + .../ui/source-enum-unnamed-field-not-error.rs | 12 + ...source-enum-unnamed-field-not-error.stderr | 22 + xet_error/tests/ui/source-struct-not-error.rs | 12 + .../tests/ui/source-struct-not-error.stderr | 21 + .../source-struct-unnamed-field-not-error.rs | 10 + ...urce-struct-unnamed-field-not-error.stderr | 21 + xet_error/tests/ui/transparent-display.rs | 8 + xet_error/tests/ui/transparent-display.stderr | 5 + xet_error/tests/ui/transparent-enum-many.rs | 9 + .../tests/ui/transparent-enum-many.stderr | 6 + .../tests/ui/transparent-enum-not-error.rs | 9 + .../ui/transparent-enum-not-error.stderr | 23 + xet_error/tests/ui/transparent-enum-source.rs | 9 + .../tests/ui/transparent-enum-source.stderr | 5 + ...ransparent-enum-unnamed-field-not-error.rs | 9 + ...parent-enum-unnamed-field-not-error.stderr | 23 + xet_error/tests/ui/transparent-struct-many.rs | 10 + .../tests/ui/transparent-struct-many.stderr | 5 + .../tests/ui/transparent-struct-not-error.rs | 9 + .../ui/transparent-struct-not-error.stderr | 21 + .../tests/ui/transparent-struct-source.rs | 7 + .../tests/ui/transparent-struct-source.stderr | 5 + ...nsparent-struct-unnamed-field-not-error.rs | 7 + ...rent-struct-unnamed-field-not-error.stderr | 21 + xet_error/tests/ui/unexpected-field-fmt.rs | 11 + .../tests/ui/unexpected-field-fmt.stderr | 5 + .../tests/ui/unexpected-struct-source.rs | 7 + .../tests/ui/unexpected-struct-source.stderr | 5 + xet_error/tests/ui/union.rs | 9 + xet_error/tests/ui/union.stderr | 8 + 252 files changed, 34813 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 cache/Cargo.toml create mode 100644 cache/src/block.rs create mode 100644 cache/src/disk.rs create mode 100644 cache/src/disk/cache.rs create mode 100644 cache/src/disk/size_bound.rs create mode 100644 cache/src/disk/storage.rs create mode 100644 cache/src/error.rs create mode 100644 cache/src/interface.rs create mode 100644 cache/src/lib.rs create mode 100644 cache/src/lru.rs create mode 100644 cache/src/metrics.rs create mode 100644 cache/src/util.rs create mode 100644 cache/src/xorb_cache.rs create mode 100644 cas_client/Cargo.toml create mode 100644 cas_client/Dockerfile create mode 100644 cas_client/README.md create mode 100644 cas_client/src/caching_client.rs create mode 100644 cas_client/src/cas_connection_pool.rs create mode 100644 cas_client/src/client_adapter.rs create mode 100644 cas_client/src/data_transport.rs create mode 100644 cas_client/src/error.rs create mode 100644 cas_client/src/grpc.rs create mode 100644 cas_client/src/interface.rs create mode 100644 cas_client/src/lib.rs create mode 100644 cas_client/src/local_client.rs create mode 100644 cas_client/src/passthrough_staging_client.rs create mode 100644 cas_client/src/remote_client.rs create mode 100644 cas_client/src/staging_client.rs create mode 100644 cas_client/src/staging_trait.rs create mode 100644 cas_client/src/util.rs create mode 100644 data/Cargo.toml create mode 100644 data/src/cas_interface.rs create mode 100644 data/src/chunking.rs create mode 100644 data/src/clean.rs create mode 100644 data/src/configurations.rs create mode 100644 data/src/constants.rs create mode 100644 data/src/data_processing.rs create mode 100644 data/src/errors.rs create mode 100644 data/src/lib.rs create mode 100644 data/src/metrics.rs create mode 100644 data/src/pointer_file.rs create mode 100644 data/src/remote_shard_interface.rs create mode 100644 data/src/repo_salt.rs create mode 100644 data/src/shard_interface.rs create mode 100644 data/src/small_file_determination.rs create mode 100644 error_printer/Cargo.toml create mode 100644 error_printer/src/lib.rs create mode 100644 file_utils/Cargo.toml create mode 100644 file_utils/src/file_metadata.rs create mode 100644 file_utils/src/lib.rs create mode 100644 file_utils/src/privilege_context.rs create mode 100644 file_utils/src/safe_file_creator.rs create mode 100644 mdb_shard/Cargo.toml create mode 100644 mdb_shard/src/cas_structs.rs create mode 100644 mdb_shard/src/constants.rs create mode 100644 mdb_shard/src/error.rs create mode 100644 mdb_shard/src/file_structs.rs create mode 100644 mdb_shard/src/lib.rs create mode 100644 mdb_shard/src/serialization_utils.rs create mode 100644 mdb_shard/src/session_directory.rs create mode 100644 mdb_shard/src/set_operations.rs create mode 100644 mdb_shard/src/shard_benchmark.rs create mode 100644 mdb_shard/src/shard_dedup_probe.rs create mode 100644 mdb_shard/src/shard_file.rs create mode 100644 mdb_shard/src/shard_file_handle.rs create mode 100644 mdb_shard/src/shard_file_manager.rs create mode 100644 mdb_shard/src/shard_file_reconstructor.rs create mode 100644 mdb_shard/src/shard_format.rs create mode 100644 mdb_shard/src/shard_in_memory.rs create mode 100644 mdb_shard/src/shard_version.rs create mode 100644 mdb_shard/src/utils.rs create mode 100644 merkledb/Cargo.toml create mode 100644 merkledb/benches/rolling_hash_benchmark.rs create mode 100644 merkledb/src/aggregate_hashes.rs create mode 100644 merkledb/src/async_chunk_iterator.rs create mode 100644 merkledb/src/bin/testdedupe.rs create mode 100644 merkledb/src/chunk_iterator.rs create mode 100644 merkledb/src/constants.rs create mode 100644 merkledb/src/error.rs create mode 100644 merkledb/src/internal_methods.rs create mode 100644 merkledb/src/lib.rs create mode 100644 merkledb/src/merkledb_debug.rs create mode 100644 merkledb/src/merkledb_highlevel_v1.rs create mode 100644 merkledb/src/merkledb_highlevel_v2.rs create mode 100644 merkledb/src/merkledb_ingestion_v1.rs create mode 100644 merkledb/src/merkledb_reconstruction.rs create mode 100644 merkledb/src/merkledbbase.rs create mode 100644 merkledb/src/merkledbv1.rs create mode 100644 merkledb/src/merkledbv2.rs create mode 100644 merkledb/src/merklememdb.rs create mode 100644 merkledb/src/merklenode.rs create mode 100644 merkledb/src/tests.rs create mode 100644 merklehash/Cargo.toml create mode 100644 merklehash/src/data_hash.rs create mode 100644 merklehash/src/lib.rs create mode 100644 parutils/Cargo.toml create mode 100644 parutils/src/async_iterator.rs create mode 100644 parutils/src/lib.rs create mode 100644 parutils/src/parallel_utils.rs create mode 100644 progress_reporting/Cargo.toml create mode 100644 progress_reporting/src/data_progress.rs create mode 100644 progress_reporting/src/lib.rs create mode 100644 progress_reporting/src/main.rs create mode 100644 progress_reporting/src/writer_with_reporting.rs create mode 100644 retry_strategy/Cargo.toml create mode 100644 retry_strategy/src/lib.rs create mode 100644 shard_client/Cargo.toml create mode 100644 shard_client/src/error.rs create mode 100644 shard_client/src/global_dedup_table.rs create mode 100644 shard_client/src/lib.rs create mode 100644 shard_client/src/local_shard_client.rs create mode 100644 shard_client/src/shard_client.rs create mode 100644 utils/Cargo.toml create mode 100644 utils/README.md create mode 100644 utils/build.rs create mode 100644 utils/examples/infra.rs create mode 100644 utils/proto/alb.proto create mode 100644 utils/proto/cas.proto create mode 100644 utils/proto/common.proto create mode 100644 utils/proto/infra.proto create mode 100644 utils/proto/shard.proto create mode 100644 utils/src/compression.rs create mode 100644 utils/src/consistenthash.rs create mode 100644 utils/src/constants.rs create mode 100644 utils/src/errors.rs create mode 100644 utils/src/gitbaretools.rs create mode 100644 utils/src/key.rs create mode 100644 utils/src/lib.rs create mode 100644 utils/src/output_bytes.rs create mode 100644 utils/src/safeio.rs create mode 100644 utils/src/singleflight.rs create mode 100644 utils/src/version.rs create mode 100644 xet_error/.github/workflows/ci.yml create mode 100644 xet_error/.gitignore create mode 100644 xet_error/Cargo.toml create mode 100644 xet_error/LICENSE-APACHE create mode 100644 xet_error/LICENSE-MIT create mode 100644 xet_error/README.md create mode 100644 xet_error/_git/HEAD create mode 100644 xet_error/_git/config create mode 100644 xet_error/_git/description create mode 100755 xet_error/_git/hooks/applypatch-msg.sample create mode 100755 xet_error/_git/hooks/commit-msg.sample create mode 100755 xet_error/_git/hooks/fsmonitor-watchman.sample create mode 100755 xet_error/_git/hooks/post-update.sample create mode 100755 xet_error/_git/hooks/pre-applypatch.sample create mode 100755 xet_error/_git/hooks/pre-commit.sample create mode 100755 xet_error/_git/hooks/pre-merge-commit.sample create mode 100755 xet_error/_git/hooks/pre-push.sample create mode 100755 xet_error/_git/hooks/pre-rebase.sample create mode 100755 xet_error/_git/hooks/pre-receive.sample create mode 100755 xet_error/_git/hooks/prepare-commit-msg.sample create mode 100755 xet_error/_git/hooks/push-to-checkout.sample create mode 100755 xet_error/_git/hooks/update.sample create mode 100644 xet_error/_git/index create mode 100644 xet_error/_git/info/exclude create mode 100644 xet_error/_git/logs/HEAD create mode 100644 xet_error/_git/logs/refs/heads/master create mode 100644 xet_error/_git/logs/refs/remotes/origin/HEAD create mode 100644 xet_error/_git/objects/pack/pack-81ea73449d91d43b55b4cc3c27f14c04e0583fd5.idx create mode 100644 xet_error/_git/objects/pack/pack-81ea73449d91d43b55b4cc3c27f14c04e0583fd5.pack create mode 100644 xet_error/_git/packed-refs create mode 100644 xet_error/_git/refs/heads/master create mode 100644 xet_error/_git/refs/remotes/origin/HEAD create mode 100644 xet_error/build.rs create mode 100644 xet_error/impl/Cargo.toml create mode 120000 xet_error/impl/LICENSE-APACHE create mode 120000 xet_error/impl/LICENSE-MIT create mode 100644 xet_error/impl/src/ast.rs create mode 100644 xet_error/impl/src/attr.rs create mode 100644 xet_error/impl/src/expand.rs create mode 100644 xet_error/impl/src/fmt.rs create mode 100644 xet_error/impl/src/generics.rs create mode 100644 xet_error/impl/src/lib.rs create mode 100644 xet_error/impl/src/prop.rs create mode 100644 xet_error/impl/src/span.rs create mode 100644 xet_error/impl/src/valid.rs create mode 100644 xet_error/rust-toolchain.toml create mode 100644 xet_error/src/aserror.rs create mode 100644 xet_error/src/display.rs create mode 100644 xet_error/src/lib.rs create mode 100644 xet_error/src/provide.rs create mode 100644 xet_error/tests/compiletest.rs create mode 100644 xet_error/tests/test_backtrace.rs create mode 100644 xet_error/tests/test_deprecated.rs create mode 100644 xet_error/tests/test_display.rs create mode 100644 xet_error/tests/test_error.rs create mode 100644 xet_error/tests/test_expr.rs create mode 100644 xet_error/tests/test_from.rs create mode 100644 xet_error/tests/test_generics.rs create mode 100644 xet_error/tests/test_lints.rs create mode 100644 xet_error/tests/test_option.rs create mode 100644 xet_error/tests/test_path.rs create mode 100644 xet_error/tests/test_source.rs create mode 100644 xet_error/tests/test_transparent.rs create mode 100644 xet_error/tests/ui/bad-field-attr.rs create mode 100644 xet_error/tests/ui/bad-field-attr.stderr create mode 100644 xet_error/tests/ui/concat-display.rs create mode 100644 xet_error/tests/ui/concat-display.stderr create mode 100644 xet_error/tests/ui/duplicate-enum-source.rs create mode 100644 xet_error/tests/ui/duplicate-enum-source.stderr create mode 100644 xet_error/tests/ui/duplicate-fmt.rs create mode 100644 xet_error/tests/ui/duplicate-fmt.stderr create mode 100644 xet_error/tests/ui/duplicate-struct-source.rs create mode 100644 xet_error/tests/ui/duplicate-struct-source.stderr create mode 100644 xet_error/tests/ui/duplicate-transparent.rs create mode 100644 xet_error/tests/ui/duplicate-transparent.stderr create mode 100644 xet_error/tests/ui/from-backtrace-backtrace.rs create mode 100644 xet_error/tests/ui/from-backtrace-backtrace.stderr create mode 100644 xet_error/tests/ui/from-not-source.rs create mode 100644 xet_error/tests/ui/from-not-source.stderr create mode 100644 xet_error/tests/ui/lifetime.rs create mode 100644 xet_error/tests/ui/lifetime.stderr create mode 100644 xet_error/tests/ui/missing-fmt.rs create mode 100644 xet_error/tests/ui/missing-fmt.stderr create mode 100644 xet_error/tests/ui/no-display.rs create mode 100644 xet_error/tests/ui/no-display.stderr create mode 100644 xet_error/tests/ui/source-enum-not-error.rs create mode 100644 xet_error/tests/ui/source-enum-not-error.stderr create mode 100644 xet_error/tests/ui/source-enum-unnamed-field-not-error.rs create mode 100644 xet_error/tests/ui/source-enum-unnamed-field-not-error.stderr create mode 100644 xet_error/tests/ui/source-struct-not-error.rs create mode 100644 xet_error/tests/ui/source-struct-not-error.stderr create mode 100644 xet_error/tests/ui/source-struct-unnamed-field-not-error.rs create mode 100644 xet_error/tests/ui/source-struct-unnamed-field-not-error.stderr create mode 100644 xet_error/tests/ui/transparent-display.rs create mode 100644 xet_error/tests/ui/transparent-display.stderr create mode 100644 xet_error/tests/ui/transparent-enum-many.rs create mode 100644 xet_error/tests/ui/transparent-enum-many.stderr create mode 100644 xet_error/tests/ui/transparent-enum-not-error.rs create mode 100644 xet_error/tests/ui/transparent-enum-not-error.stderr create mode 100644 xet_error/tests/ui/transparent-enum-source.rs create mode 100644 xet_error/tests/ui/transparent-enum-source.stderr create mode 100644 xet_error/tests/ui/transparent-enum-unnamed-field-not-error.rs create mode 100644 xet_error/tests/ui/transparent-enum-unnamed-field-not-error.stderr create mode 100644 xet_error/tests/ui/transparent-struct-many.rs create mode 100644 xet_error/tests/ui/transparent-struct-many.stderr create mode 100644 xet_error/tests/ui/transparent-struct-not-error.rs create mode 100644 xet_error/tests/ui/transparent-struct-not-error.stderr create mode 100644 xet_error/tests/ui/transparent-struct-source.rs create mode 100644 xet_error/tests/ui/transparent-struct-source.stderr create mode 100644 xet_error/tests/ui/transparent-struct-unnamed-field-not-error.rs create mode 100644 xet_error/tests/ui/transparent-struct-unnamed-field-not-error.stderr create mode 100644 xet_error/tests/ui/unexpected-field-fmt.rs create mode 100644 xet_error/tests/ui/unexpected-field-fmt.stderr create mode 100644 xet_error/tests/ui/unexpected-struct-source.rs create mode 100644 xet_error/tests/ui/unexpected-struct-source.stderr create mode 100644 xet_error/tests/ui/union.rs create mode 100644 xet_error/tests/ui/union.stderr diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a6f94d6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.idea +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Mac OS Trash +.DS_Store + +# VS Code configs +.vscode diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..62256563 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5011 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anyhow" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" + +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates 3.1.2", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "async-scoped" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7a6a57c8aeb40da1ec037f5d455836852f7a57e69e1b1ad3d8f38ac1d6cadf" +dependencies = [ + "futures", + "pin-project", + "slab", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "async-trait" +version = "0.1.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "binary-heap-plus" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4551d8382e911ecc0d0f0ffb602777988669be09447d536ff4388d1def11296" +dependencies = [ + "compare", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 1.0.0", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata 0.4.7", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "bytestream" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f720842a717d6afaf69fee2dc69b771edc165f12cc3eb1b0e8eeef53a86454" +dependencies = [ + "byteorder", +] + +[[package]] +name = "cache" +version = "0.14.5" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.13.1", + "byteorder", + "chrono", + "lazy_static", + "lru", + "merklehash", + "mockall", + "prometheus", + "rand 0.8.5", + "tempdir", + "tempfile", + "test-context", + "tokio", + "tracing", + "tracing-attributes", + "tracing-futures", + "tracing-subscriber", + "tracing-test", + "utils", + "xet_error", +] + +[[package]] +name = "cas_client" +version = "0.14.5" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bytes", + "cache", + "clap 2.34.0", + "deadpool", + "error_printer", + "futures", + "http 0.2.12", + "http-body-util", + "hyper 1.4.1", + "hyper-rustls", + "hyper-util", + "itertools 0.10.5", + "lazy_static", + "lz4", + "merkledb", + "merklehash", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-jaeger", + "parutils", + "progress_reporting", + "prost", + "rcgen", + "retry_strategy", + "rustls-pemfile 2.1.3", + "serde_json", + "tempfile", + "tokio", + "tokio-native-tls", + "tokio-retry", + "tokio-rustls 0.25.0", + "tokio-stream", + "tonic", + "tower", + "tracing", + "tracing-opentelemetry", + "trait-set", + "utils", + "uuid", + "xet_error", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_derive", + "clap_lex", + "indexmap 1.9.3", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap 0.16.1", +] + +[[package]] +name = "clap_derive" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "compare" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" + +[[package]] +name = "const_format" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" +dependencies = [ + "atty", + "cast", + "clap 2.34.0", + "criterion-plot", + "csv", + "itertools 0.10.5", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot 0.12.3", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "data" +version = "0.14.5" +dependencies = [ + "anyhow", + "assert_cmd", + "async-trait", + "atoi", + "atty", + "base64 0.13.1", + "bincode", + "blake3", + "cas_client", + "chrono", + "clap 3.2.25", + "colored", + "const_format", + "csv-core", + "ctrlc", + "dirs 4.0.0", + "enum_dispatch", + "error_printer", + "fallible-iterator", + "file_utils", + "filetime", + "futures", + "futures-core", + "gearhash", + "git-url-parse", + "git-version", + "git2", + "glob", + "hashers", + "hex", + "http 0.2.12", + "humantime", + "intaglio", + "is_executable", + "itertools 0.10.5", + "lazy_static", + "libc", + "lru", + "lz4", + "mdb_shard", + "merkledb", + "merklehash", + "mockall", + "mockall_double", + "mockstream", + "more-asserts", + "nfsserve", + "normalize-path", + "openssl", + "openssl-probe", + "opentelemetry", + "opentelemetry-jaeger", + "parutils", + "path-absolutize", + "pathdiff", + "pbr", + "predicates 2.1.5", + "prometheus", + "rand 0.8.5", + "rand_chacha", + "regex", + "reqwest", + "retry_strategy", + "ring 0.16.20", + "rstest", + "run_script", + "same-file", + "serde", + "serde_json", + "serde_with", + "serial_test", + "shard_client", + "shellexpand", + "shellish_parse", + "slog", + "slog-async", + "slog-json", + "sorted-vec", + "static_assertions", + "sysinfo", + "tabled", + "tempdir", + "tempfile", + "tokio", + "tokio-test", + "toml 0.5.11", + "tracing", + "tracing-attributes", + "tracing-futures", + "tracing-opentelemetry", + "tracing-subscriber", + "tracing-test", + "url", + "utils", + "utime", + "uuid", + "version-compare", + "walkdir", + "whoami", + "winapi", + "xet_error", +] + +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadqueue" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a2561fd313df162315935989dceb8c99db4ee1933358270a57a3cfb8c957f3" +dependencies = [ + "crossbeam-queue", + "tokio", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error_printer" +version = "0.14.5" +dependencies = [ + "tracing", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "file_utils" +version = "0.14.2" +dependencies = [ + "anyhow", + "colored", + "lazy_static", + "libc", + "rand 0.8.5", + "tempfile", + "tracing", + "whoami", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + +[[package]] +name = "fsio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6fce87c901c64837f745e7fffddeca1de8e054b544ba82c419905d40a0e1be" +dependencies = [ + "dunce", + "rand 0.8.5", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gearhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cf82cf76cd16485e56295a1377c775ce708c9f1a0be6b029076d60a245d213" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" + +[[package]] +name = "git-url-parse" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d7ff03a34ea818a59cf30c0d7aa55354925484fa30bcc4cb96d784ff07578f" +dependencies = [ + "strum", + "thiserror", + "url", +] + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "git2" +version = "0.18.2" +source = "git+https://github.com/xetdata/git2-rs#dfffd3d1eb9e87a9e7e98b24d0a0f54db664cbcc" +dependencies = [ + "bitflags 2.6.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashers" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bca93b15ea5a746f220e56587f71e73c6165eab783df9e26590069953e3c30" +dependencies = [ + "fxhash", +] + +[[package]] +name = "hashring" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bfd649ac5e0f82ae98d547450f1d31af49742be255b5380c61fc8513b9df11" +dependencies = [ + "siphasher", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "heed" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269c7486ed6def5d7b59a427cec3e87b4d4dd4381d01e21c8c9f2d3985688392" +dependencies = [ + "bytemuck", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-rkv-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53a94e5b2fd60417e83ffdfe136c39afacff0d4ac1d8d01cd66928ac610e1a2" + +[[package]] +name = "heed-types" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a6cf0a6952fcedc992602d5cddd1e3fff091fbe87d38636e3ec23a31f32acbd" +dependencies = [ + "bincode", + "bytemuck", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", + "log", + "rustls 0.22.4", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.30", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.30", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "intaglio" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b4f756a47a2dac507018af2d4e47988e93829f34a665da3655b23cc1d21ee47" + +[[package]] +name = "integer-encoding" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" + +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + +[[package]] +name = "is_executable" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba3d8548b8b04dafdf2f4cc6f5e379db766d0a6d9aac233ad4c9a92ea892233" +dependencies = [ + "winapi", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "len-trait" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723558ab8acaa07cb831b424cd164b587ddc1648b34748a30953c404e9a4a65b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libgit2-sys" +version = "0.16.2+1.7.2" +source = "git+https://github.com/xetdata/git2-rs#dfffd3d1eb9e87a9e7e98b24d0a0f54db664cbcc" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.5.3", +] + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lmdb-rkv-sys" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b9ce6b3be08acefa3003c57b7565377432a89ec24476bbe72e11d101f852fe" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "lz4" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958b4caa893816eea05507c20cfe47574a43d9a697138a7872990bba8a0ece68" +dependencies = [ + "libc", + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109de74d5d2353660401699a4174a4ff23fcc649caf553df71933c7fb45ad868" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "mdb_shard" +version = "0.14.5" +dependencies = [ + "anyhow", + "async-scoped", + "async-trait", + "binary-heap-plus", + "clap 3.2.25", + "lazy_static", + "merklehash", + "more-asserts", + "rand 0.8.5", + "regex", + "serde", + "tempdir", + "tempfile", + "tokio", + "tracing", + "uuid", + "xet_error", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "merkledb" +version = "0.14.5" +dependencies = [ + "async-scoped", + "async-trait", + "bincode", + "bitflags 1.3.2", + "blake3", + "clap 3.2.25", + "criterion", + "futures", + "gearhash", + "itertools 0.10.5", + "lazy_static", + "merklehash", + "parutils", + "rand 0.8.5", + "rand_chacha", + "rand_core 0.6.4", + "rayon", + "ron", + "rustc-hash", + "serde", + "structopt", + "tempfile", + "tokio", + "tracing", + "walkdir", + "xet_error", +] + +[[package]] +name = "merklehash" +version = "0.14.5" +dependencies = [ + "blake3", + "generic-array", + "heed", + "rand 0.8.5", + "rand_chacha", + "rand_core 0.6.4", + "safe-transmute", + "serde", + "sha3", + "structopt", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if 1.0.0", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates 2.1.5", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mockall_double" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "mockstream" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bbe0c0c9d254b463b13734bc361d1423289547e052b1e77e5a77292496ba2e" + +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nfsserve" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73615e054238e6bf5e554407b5b23e82fc63616db459057c51b794799eda6fb" +dependencies = [ + "anyhow", + "async-trait", + "byteorder", + "bytestream", + "filetime", + "futures", + "num-derive", + "num-traits", + "smallvec", + "tokio", + "tracing", + "tracing-attributes", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if 1.0.0", + "cfg_aliases", + "libc", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "normalize-path" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c11b7fa387f3c9874e60670ac6d009cefc5ffa8c23437137a9998c0a154e77" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.3.2+3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "js-sys", + "lazy_static", + "percent-encoding", + "pin-project", + "rand 0.8.5", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "opentelemetry-http" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449048140ee61e28f57abe6e9975eedc1f3a29855c7407bd6c12b18578863379" +dependencies = [ + "async-trait", + "bytes", + "http 0.2.12", + "opentelemetry", +] + +[[package]] +name = "opentelemetry-jaeger" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c0b12cd9e3f9b35b52f6e0dac66866c519b26f424f4bbf96e3fe8bfbdc5229" +dependencies = [ + "async-trait", + "lazy_static", + "opentelemetry", + "opentelemetry-semantic-conventions", + "thiserror", + "thrift", + "tokio", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985cc35d832d412224b2cffe2f9194b1b89b6aa5d0bef76d080dce09d90e62bd" +dependencies = [ + "opentelemetry", +] + +[[package]] +name = "ordered-float" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "page_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebde548fbbf1ea81a99b128872779c437752fb99f217c45245e1a61dcd9edcd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "papergrid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae7891b22598926e4398790c8fe6447930c72a67d36d983a49d6ce682ce83290" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.5.3", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "parutils" +version = "0.14.5" +dependencies = [ + "anyhow", + "async-scoped", + "async-trait", + "deadqueue", + "futures", + "len-trait", + "more-asserts", + "tokio", + "tracing", +] + +[[package]] +name = "path-absolutize" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-dedot" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" +dependencies = [ + "once_cell", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbr" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5827dfa0d69b6c92493d6c38e633bbaa5937c153d0d7c28bf12313f8c6d514" +dependencies = [ + "crossbeam-channel", + "libc", + "winapi", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.5.0", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools 0.10.5", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.77", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "progress_reporting" +version = "0.14.5" +dependencies = [ + "atty", + "crossterm", + "more-asserts", + "tokio", + "tracing", + "utils", +] + +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if 1.0.0", + "fnv", + "lazy_static", + "memchr", + "parking_lot 0.12.3", + "protobuf", + "thiserror", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools 0.12.1", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.77", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" +dependencies = [ + "pem", + "ring 0.17.8", + "time", + "yasna", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + +[[package]] +name = "retry_strategy" +version = "0.14.5" +dependencies = [ + "anyhow", + "tokio", + "tokio-retry", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86018df177b1beef6c7c8ef949969c4f7cb9a9344181b92486b23c79995bdaa4" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "rstest" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2288c66aeafe3b2ed227c981f364f9968fa952ef0b30e84ada4486e7ee24d00a" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "run_script" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd85213e37f76b40186ee781cf3a689b05c518c3102c987acf679c573d8e4ef" +dependencies = [ + "fsio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "safe-transmute" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot 0.12.3", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer", + "digest", + "keccak", + "opaque-debug", +] + +[[package]] +name = "shard_client" +version = "0.14.5" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bytes", + "cas_client", + "clap 2.34.0", + "heed", + "http 0.2.12", + "hyper 0.14.30", + "itertools 0.10.5", + "lazy_static", + "mdb_shard", + "merkledb", + "merklehash", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-jaeger", + "prost", + "rand 0.8.5", + "retry_strategy", + "serde_json", + "tempfile", + "tokio", + "tokio-retry", + "tonic", + "tower", + "tracing", + "tracing-opentelemetry", + "utils", + "uuid", + "xet_error", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7e79eddc7b411f9beeaaf2d421de7e7cb3b1ab9eaf1b79704c0e4130cba6b5" +dependencies = [ + "dirs 2.0.2", +] + +[[package]] +name = "shellish_parse" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c29b912ad681a28566f37b936bba1f3580a93b9391c4a0b12cb1c6b4ed79973" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "slog-async" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" +dependencies = [ + "crossbeam-channel", + "slog", + "take_mut", + "thread_local", +] + +[[package]] +name = "slog-json" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1e53f61af1e3c8b852eef0a9dee29008f55d6dd63794f3f12cef786cf0f219" +dependencies = [ + "serde", + "serde_json", + "slog", + "time", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "sorted-vec" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6734caf0b6f51addd5eeacca12fb39b2c6c14e8d4f3ac42f3a78955c0467458" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap 2.34.0", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck 0.3.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.77", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + +[[package]] +name = "sysinfo" +version = "0.26.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c18a6156d1f27a9592ee18c1a846ca8dd5c258b7179fc193ae87c74ebb666f5" +dependencies = [ + "cfg-if 1.0.0", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tabled" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce69a5028cd9576063ec1f48edb2c75339fd835e6094ef3e05b3a079bf594a6" +dependencies = [ + "papergrid", + "tabled_derive", + "unicode-width", +] + +[[package]] +name = "tabled_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "test-context" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b6965c21232186af0092233c18030fe607cfc3960dbabb209325272458eeea" +dependencies = [ + "async-trait", + "futures", + "test-context-macros", +] + +[[package]] +name = "test-context-macros" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506c7664333e246f564949bee4ed39062aa0f11918e6f5a95f553cdad65c274" +dependencies = [ + "quote", + "syn 2.0.77", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "thrift" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b82ca8f46f95b3ce96081fe3dd89160fdea970c254bb72925255d1b62aae692e" +dependencies = [ + "byteorder", + "integer-encoding", + "log", + "ordered-float", + "threadpool", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 1.0.2", + "parking_lot 0.12.3", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap 2.5.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.7", + "bytes", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile 1.0.4", + "tokio", + "tokio-rustls 0.24.1", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f" +dependencies = [ + "once_cell", + "opentelemetry", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log 0.2.0", + "tracing-serde", +] + +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn 2.0.77", +] + +[[package]] +name = "trait-set" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79e2e9c9ab44c6d7c20d5976961b47e8f49ac199154daa514b77cd1ab536625" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "trybuild" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207aa50d36c4be8d8c6ea829478be44a372c6a77669937bb39c698e52f1491e8" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "termcolor", + "toml 0.8.19", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utils" +version = "0.14.5" +dependencies = [ + "anyhow", + "chrono", + "clap 3.2.25", + "futures", + "hashbrown 0.12.3", + "hashring", + "http 0.2.12", + "itertools 0.10.5", + "lazy_static", + "merklehash", + "parking_lot 0.11.2", + "pin-project", + "prost", + "prost-types", + "rand 0.5.6", + "regex", + "serde", + "tempfile", + "tokio", + "tonic", + "tonic-build", + "tracing", + "xet_error", +] + +[[package]] +name = "utime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91baa0c65eabd12fcbdac8cc35ff16159cab95cae96d0222d6d0271db6193cef" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "atomic", + "getrandom", + "rand 0.8.5", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.77", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall 0.5.3", + "wasite", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if 1.0.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "xet-error-impl" +version = "1.0.50" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "xet_error" +version = "0.14.5" +dependencies = [ + "anyhow", + "lazy_static", + "ref-cast", + "rustversion", + "trybuild", + "xet-error-impl", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..34172379 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] +rust-version = "1.79" + +resolver = "2" + +members = [ + "cache", + "cas_client", + "data", + "error_printer", + "file_utils", + "merklehash", + "mdb_shard", + "parutils", + "progress_reporting", + "retry_strategy", + "shard_client", + "utils", + "xet_error", +] + +[profile.release] +opt-level = 3 +lto = true +debug = 1 + +[profile.opt-test] +inherits = "dev" +opt-level = 1 +debug = 1 diff --git a/cache/Cargo.toml b/cache/Cargo.toml new file mode 100644 index 00000000..9bed6179 --- /dev/null +++ b/cache/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "cache" +version = "0.14.5" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +utils = { path = "../utils" } +tokio = { version = "1.36", features = ["full"] } +async-trait = "0.1.9" +base64 = "0.13.0" +lru = "0.12" +xet_error = { path = "../xet_error" } +anyhow = "1" +tracing = "0.1.31" +tracing-attributes = "0.1" +tracing-futures = "0.2" +tracing-test = "0.2.1" +tracing-subscriber = { version = "0.3", features = ["json", "tracing-log"] } +mockall = "0.11" +chrono = "0.4" +tempfile = "3.2.0" +byteorder = "1.4.3" +# metrics +lazy_static = "1.4.0" +prometheus = "0.13.0" + +[dev-dependencies] +tempdir = "0.3.7" +rand = "0.8.5" +test-context = "0.1.3" +merklehash = { path = "../merklehash" } + +[features] +strict = [] diff --git a/cache/src/block.rs b/cache/src/block.rs new file mode 100644 index 00000000..f0147104 --- /dev/null +++ b/cache/src/block.rs @@ -0,0 +1,212 @@ +use std::iter; +use std::ops::Range; + +/// Default size of blocks in bytes. +pub const DEFAULT_BLOCK_SIZE_BYTES: u64 = 16 * 1024 * 1024; // 16MiB + +/// Helper object for converting between absolute offsets and fixed size +/// blocks within a file. +#[derive(Debug)] +pub struct BlockConverter { + /// Size of a block in bytes + block_size: u64, +} + +impl BlockConverter { + pub fn new(block_size: u64) -> BlockConverter { + BlockConverter { block_size } + } + + /// Breaks up the given range into a series of BlockRange corresponding to the + /// blocks contained within the range. + /// + /// ## Implementation note + /// Lifetimes are explicitly indicated here because the returned iterator needs + /// to access a method on the BlockConverter (i.e. [within_block](BlockConverter::within_block)) + /// to dynamically generate the next range. Thus, the `BlockConverter` needs to + /// live as long as the returned iterator. + /// + /// We also add `+ Send` to the Iterator object to indicate to the caller that the + /// provided iterator is safe to be used in async contexts. Without it, the caller + /// will have a problem saving the state of the iterator on calls to await (i.e. a + /// compilation error). + pub fn to_block_ranges<'a>( + &'a self, + range: Range, + ) -> Box + 'a + Send> { + if range.start == range.end { + return Box::new(iter::empty::()); + } + let (idx_start, off_start) = self.abs_to_block(range.start); + let (mut idx_end, off_end) = self.abs_to_block(range.end); + + if off_end == 0 { + // we don't need to include the last block since range.end is exclusive. + idx_end -= 1; + } + + Box::new((idx_start..(idx_end + 1)).map(move |idx| { + let start = if self.within_block(idx, range.start) { + off_start + } else { + 0 + }; + let end = if self.within_block(idx, range.end) { + off_end + } else { + self.block_size + }; + BlockRange::new(idx, start..end, self.block_size) + })) + } + + /// Returns the BlockRange of the entire block located at idx. If the end of the block + /// extends past the "total_size" of the file, then the end of the block_range will be + /// adjusted. + /// If the block index is past the end of the file, then None will be returned. + pub fn get_full_block_range_for( + &self, + idx: u64, + total_size: Option, + ) -> Option { + let r = self.block_to_abs(idx); + let mut end_off = self.block_size; + if let Some(total_size) = total_size { + if r.start >= total_size { + return None; + } + if r.end > total_size { + end_off = total_size - r.start + } + } + Some(BlockRange::new(idx, 0..end_off, self.block_size)) + } + + /// Takes an absolute offset and converts it into block coordinates (block_idx, block_offset), + /// where block_offset is relative to the start of the block. + pub fn abs_to_block(&self, abs_offset: u64) -> (u64, u64) { + (abs_offset / self.block_size, abs_offset % self.block_size) + } + + /// takes the index of some block and returns the absolute offsets of the block as a range. + pub fn block_to_abs(&self, block_index: u64) -> Range { + (block_index * self.block_size)..((block_index + 1) * self.block_size) + } + + pub fn within_block(&self, block_index: u64, abs_off: u64) -> bool { + (block_index * self.block_size) <= abs_off + && abs_off < ((block_index + 1) * self.block_size) + } +} + +impl Default for BlockConverter { + fn default() -> Self { + BlockConverter::new(DEFAULT_BLOCK_SIZE_BYTES) + } +} + +/// A BlockRange corresponds to some range within a particular fixed-size block. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct BlockRange { + idx: u64, + range: Range, + block_size: u64, +} + +impl BlockRange { + pub fn new(idx: u64, range: Range, block_size: u64) -> BlockRange { + assert!(range.end - range.start <= block_size); + BlockRange { + idx, + range, + block_size, + } + } + + /// Getter for the index of the block within an overall file. + pub fn idx(&self) -> u64 { + self.idx + } + + /// Getter for the starting offset within the block. + pub fn start_off(&self) -> u64 { + self.range.start + } + + /// Getter for the ending offset within the block. + pub fn end_off(&self) -> u64 { + self.range.end + } + + /// Getter for the block size. + pub fn block_size(&self) -> u64 { + self.block_size + } + + pub fn to_abs_offsets(&self) -> Range { + let block_start = self.idx * self.block_size; + (block_start + self.range.start)..(block_start + self.range.end) + } +} + +#[cfg(test)] +mod block_convertor_tests { + use super::*; + + const CACHE_SIZE_TEST: u64 = 20; + + fn default_converter() -> BlockConverter { + BlockConverter::new(CACHE_SIZE_TEST) + } + + #[test] + fn test_abs_to_block() { + let c = default_converter(); + assert_eq!(c.abs_to_block(5), (0, 5)); + assert_eq!(c.abs_to_block(0), (0, 0)); + assert_eq!(c.abs_to_block(20), (1, 0)); + assert_eq!(c.abs_to_block(39), (1, 19)); + } + + #[test] + fn test_within_block() { + let c = default_converter(); + assert!(c.within_block(0, 15)); + assert!(c.within_block(0, 0)); + assert!(!c.within_block(0, 20)); + assert!(!c.within_block(0, 43)); + assert!(c.within_block(3, 65)); + assert!(c.within_block(1, 20)); + } + + #[test] + fn test_to_block_ranges() { + let c = default_converter(); + let mut iter = c.to_block_ranges(5..37); + assert_eq!(iter.next().unwrap(), BlockRange::new(0, 5..20, 20)); + assert_eq!(iter.next().unwrap(), BlockRange::new(1, 0..17, 20)); + assert!(iter.next().is_none()); + + let mut iter = c.to_block_ranges(0..20); + assert_eq!(iter.next().unwrap(), BlockRange::new(0, 0..20, 20)); + assert!(iter.next().is_none()); + + let mut iter = c.to_block_ranges(40..80); + assert_eq!(iter.next().unwrap(), BlockRange::new(2, 0..20, 20)); + assert_eq!(iter.next().unwrap(), BlockRange::new(3, 0..20, 20)); + assert!(iter.next().is_none()); + + let mut iter = c.to_block_ranges(25..30); + assert_eq!(iter.next().unwrap(), BlockRange::new(1, 5..10, 20)); + assert!(iter.next().is_none()); + + let mut iter = c.to_block_ranges(25..26); + assert_eq!(iter.next().unwrap(), BlockRange::new(1, 5..6, 20)); + assert!(iter.next().is_none()); + + let mut iter = c.to_block_ranges(0..0); + assert!(iter.next().is_none()); + let mut iter = c.to_block_ranges(24..24); + assert!(iter.next().is_none()); + } +} diff --git a/cache/src/disk.rs b/cache/src/disk.rs new file mode 100644 index 00000000..403ae1b0 --- /dev/null +++ b/cache/src/disk.rs @@ -0,0 +1,6 @@ +// public members +pub mod cache; +mod size_bound; +mod storage; + +pub use crate::disk::cache::DiskCache; diff --git a/cache/src/disk/cache.rs b/cache/src/disk/cache.rs new file mode 100644 index 00000000..51355185 --- /dev/null +++ b/cache/src/disk/cache.rs @@ -0,0 +1,228 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use async_trait::async_trait; +use tracing::{debug, error}; + +use crate::disk::size_bound::{CacheValue, SizeBoundCache}; +use crate::disk::storage::DiskManager; +use crate::interface::{BlockReadRequest, BlockReader}; +use crate::metrics::DISK_EVICTION_AGE; +use crate::CacheError::BlockNotFound; +use crate::{util, CacheError}; + +/// A DiskCache provides a way to cache data using the local disk. +#[derive(Debug, Clone)] +pub struct DiskCache { + disk_manager: Arc, + cache: Arc, +} + +impl DiskCache { + fn new(disk_manager: Arc, cache: SizeBoundCache) -> DiskCache { + DiskCache { + disk_manager, + cache: Arc::new(cache), + } + } + + /// Build a DiskCache from a path on disk and a desired capacity. This will synchronously + /// load any existing files located in the directory into the cache. + pub fn from_config(root_dir: &str, capacity_bytes: u64) -> Result { + let disk_manager = Arc::new(DiskManager::new(PathBuf::from(root_dir.to_string()))); + let cache = SizeBoundCache::new_with_eviction(capacity_bytes, Some(disk_manager.clone())); + let disk_cache = Self::new(disk_manager, cache); + disk_cache.load_cache()?; + Ok(disk_cache) + } + + pub async fn put(&self, request: &BlockReadRequest, val: &[u8]) -> Result { + let key = request_to_key(request); + + let value = CacheValue::new( + val.len() as u64, + request.metadata().version(), + key.clone(), + request.block_size(), + request.block_range().idx(), + ); + let (was_inserted, evicted_blocks) = self.cache.put(key.as_str(), value.clone())?; + if !was_inserted { + return Ok(false); + } + for block in evicted_blocks { + if block.key == key { + debug!("Replacing old block for: {}", key); + } else { + debug!("Evicted: {}", block.key); + DISK_EVICTION_AGE.observe(block.get_elapsed_ms() as f64 / 1000.0) + } + } + + match self.disk_manager.write(&value, val).await { + Ok(_) => Ok(true), + Err(e) => { + self.cache.remove(key.as_str()); + Err(e) + } + } + } + + /// Reads the root dir for the cache, adding the entries it finds into the cache. + fn load_cache(&self) -> Result<(), CacheError> { + let mut err = Ok(()); + let files_to_load_iter = self.disk_manager.init()?; + // TODO: Currently, this just loads files into the LRU cache based off of the + // order that they're read in, which isn't technically correct for LRU. + // We may want to consider loading the files in order of accessed_time + // (as long as the cache FS was mounted with accessed_time metadata). + debug!( + "Successfully initialized cache dir at {:?} , loading existing files into cache", + self.disk_manager.get_root_dir() + ); + let num_files = files_to_load_iter + .map(|val| self.cache.put(val.key.clone().as_str(), val)) + .scan(&mut err, util::until_err) + .filter(|(b, _)| *b) + .count(); + + if let Err(e) = err { + error!("Error loading files from root dir into the cache: {:?}", e); + return Err(e); + } + debug!( + "Loaded {} files from cache dir at {:?} into disk cache", + num_files, + self.disk_manager.get_root_dir() + ); + Ok(()) + } +} + +#[async_trait] +impl BlockReader for DiskCache { + async fn get(&self, request: &BlockReadRequest) -> Result, CacheError> { + let key = request_to_key(request); + let opt_val = self.cache.get(key.as_str()); + match opt_val { + Some(val) => self.disk_manager.read(&val, request.range()).await, + None => Err(BlockNotFound), + } + } +} + +fn request_to_key(request: &BlockReadRequest) -> String { + // Note that this format is used in the Header class to attemp to parse out this metadata. + format!( + "{}.{}.{}", + request.metadata().name(), + request.block_range().idx(), + request.block_size(), + ) +} + +/// Wrapper around some action to take when a block is evicted. +pub trait EvictAction { + /// Evicts the given CacheValue, returning whether or not it was successful. + fn evict(&self, val: &CacheValue) -> Result<(), CacheError>; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block::BlockRange; + use crate::util::test_utils::CacheDirTest; + use crate::FileMetadata; + + fn get_block_req(key: &str, size: u64, block_id: u64) -> BlockReadRequest { + BlockReadRequest::from_block_range( + BlockRange::new(block_id, 0..size, size), + Arc::new(FileMetadata::new(key.to_string(), Some(size), 0)), + ) + } + + fn get_bytes(size: usize) -> Vec { + (0..size).map(|_| rand::random::()).collect::>() + } + + #[tokio::test] + async fn test_put() { + let dir = CacheDirTest::new("cache_put"); + let c = DiskCache::from_config(dir.get_path_str(), 50).unwrap(); + let req = get_block_req("a", 10, 1); + let val = get_bytes(10); + let was_inserted = c.put(&req, val.as_slice()).await.unwrap(); + assert!(was_inserted); + let files = dir.get_entries(); + assert_eq!(files.len(), 1); + assert!(c.cache.get(&request_to_key(&req)).is_some()) + } + + #[tokio::test] + async fn test_put_get() { + let dir = CacheDirTest::new("cache_put_get"); + let c = DiskCache::from_config(dir.get_path_str(), 50).unwrap(); + let req = get_block_req("a", 10, 1); + let val = get_bytes(10); + let was_inserted = c.put(&req, val.as_slice()).await.unwrap(); + assert!(was_inserted); + let data = c.get(&req).await.unwrap(); + assert_eq!(data, val); + } + + #[tokio::test] + async fn test_put_get_multiple() { + let dir = CacheDirTest::new("cache_put_get_multiple"); + let c = DiskCache::from_config(dir.get_path_str(), 50).unwrap(); + let req = get_block_req("a", 10, 1); + let val = get_bytes(10); + let was_inserted = c.put(&req, val.as_slice()).await.unwrap(); + assert!(was_inserted); + let data = c.get(&req).await.unwrap(); + assert_eq!(data, val); + + let data = c.get(&req).await.unwrap(); + assert_eq!(data, val); + } + + #[tokio::test] + async fn test_put_eviction() { + let dir = CacheDirTest::new("cache_put_eviction"); + let c = DiskCache::from_config(dir.get_path_str(), 20).unwrap(); + let req = get_block_req("a", 10, 1); + let val = get_bytes(10); + let mut was_inserted = c.put(&req, val.as_slice()).await.unwrap(); + assert!(was_inserted); + + let req2 = get_block_req("b", 20, 1); + let val2 = get_bytes(20); + was_inserted = c.put(&req2, val2.as_slice()).await.unwrap(); + assert!(was_inserted); + + // "a" should have been evicted + let result = c.get(&req).await; + assert!(result.is_err()); + // contents for "a" should have been removed from disk + let files = dir.get_entries(); + assert_eq!(files.len(), 1); + // "b" should still be fetchable + let vec = c.get(&req2).await.unwrap(); + assert_eq!(vec, val2); + } + + #[tokio::test] + async fn load_existing_cache() { + let dir = CacheDirTest::new("cache_load"); + let dir_str = dir.get_path_str(); + println!("dir: {:?}", dir_str); + let c = DiskCache::from_config(dir.get_path_str(), 20).unwrap(); + let req = get_block_req("a", 10, 1); + let val = get_bytes(10); + c.put(&req, val.as_slice()).await.unwrap(); + drop(c); + + let c2 = DiskCache::from_config(dir.get_path_str(), 20).unwrap(); + let data = c2.get(&req).await.unwrap(); + assert_eq!(val, data); + } +} diff --git a/cache/src/disk/size_bound.rs b/cache/src/disk/size_bound.rs new file mode 100644 index 00000000..0cacb108 --- /dev/null +++ b/cache/src/disk/size_bound.rs @@ -0,0 +1,388 @@ +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; + +use lru::LruCache; +use tracing::error; + +use crate::disk::cache::EvictAction; +use crate::{util, CacheError}; + +/// Defines a cache bound by the size of the elements within. +/// +/// ## Eviction Policy +/// This cache uses an LRU eviction strategy. +/// +/// ## Admission Policy +/// All new blocks are admitted as long as their size doesn't exceed +/// the cache capacity. +/// +/// If attempting to insert a block already present in the cache, then +/// the new block is allowed if it's version is >= the cached version. +/// +/// ## Other Notes +/// This implementation is thread-safe. However, it does so by locking +/// the entire map for any operation. +/// TODO: Check/improve performance. +#[derive(Debug)] +pub struct SizeBoundCache { + cache: Arc>, +} + +/// Internal state of the cache. This is not thread-safe and is intended to be used under the +/// Arc> in the SizeBoundCache. +struct InternalCacheState { + /// How many bytes are allowed in the cache. + capacity: u64, + /// How many bytes are currently used in the cache. + current_usage: u64, + /// The cache implementation. + /// + /// We are using a `RefCell<>` since the `get` function of the + /// LruCache uses `&mut self` (since ordering of the cache changes). + /// We want to abstract away this detail/requirement from the caller + /// and allow calls to `get` without a mutable reference. + values: lru::LruCache, + /// An optional EvictAction that will be called on eviction. + on_evict: Option>, +} + +impl InternalCacheState { + fn get(&mut self, key: &str) -> Option { + self.values.get(key).cloned() + } + + /// Whether or not the key and value should be admitted into the cache. + fn should_admit(&self, key: &str, value: &CacheValue) -> bool { + if !value.key.is_empty() && value.key != key { + error!("BUG: CacheValue.key doesn't match the key it is being inserted under!"); + return false; + } + if value.size > self.capacity { + return false; + } + self.values + .peek(key) + .map_or(true, |old| old.version <= value.version) + } + + /// Removes the indicated key from the cache. Updating the internal state and + /// returning the old value if present. + fn pop(&mut self, key: &str) -> Option { + self.values.pop(key).map(|val| { + self.current_usage -= val.size; + val + }) + } + + /// Pushes the key, value into the cache, updating the internal state. + /// Expects that the key is not present in the cache + fn push(&mut self, key: &str, mut value: CacheValue) { + self.current_usage += value.size; + + // update value with internal state if not present (e.g. from loading on startup) + if value.insertion_time_ms == 0 { + value.insertion_time_ms = util::time_to_epoch_millis(SystemTime::now()); + } + value.key = key.to_string(); + + if self.values.put(key.to_string(), value).is_some() { + panic!("BUG in SizeBoundCache: tried to insert into cache when entry already exists") + } + } + + /// Evicts an element from the cache. + fn evict_next(&mut self) -> Result { + let (_, val) = self + .values + .pop_lru() + .expect("Ran out of items to evict in cache"); + if let Some(evictor) = &self.on_evict { + evictor.evict(&val)?; + } + self.current_usage -= val.size; + Ok(val) + } + + fn space_remaining(&self) -> u64 { + self.capacity - self.current_usage + } +} + +impl Debug for InternalCacheState { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InternalCacheState") + .field("capacity", &self.capacity) + .field("current_usage", &self.current_usage) + .field("num_elements", &self.values.len()) + .finish() + } +} + +#[allow(unused)] +impl SizeBoundCache { + /// Builds a new cache bound to the indicated capacity. + pub fn new(capacity: u64) -> Self { + Self::new_with_eviction(capacity, None) + } + + /// Builds a new cache bound to the indicated capacity. + /// Can optionally accept an EvictAction as an action to run when an object + /// is evicted. + /// + /// ## Implementation note + /// We use an `LruCache::unbounded()` because we don't care about the number + /// of entries in our cache, only the total size of all entries. + pub fn new_with_eviction( + capacity: u64, + on_evict: Option>, + ) -> Self { + SizeBoundCache { + cache: Arc::new(Mutex::new(InternalCacheState { + capacity, + current_usage: 0, + values: LruCache::unbounded(), + on_evict, + })), + } + } + + /// Inserts the given key with the provided value into the cache. Whether the + /// block was inserted into the cache or not If this + /// insertion results in cache eviction, then the evicted entries are returned. + /// + /// If the key already exists in the cache, then the version of the new value + /// is compared with the existing one and if new >= old, then the value will + /// be replaced (and the old one will be returned in the Vec). + /// + /// ## Implementation note + /// We don't need an `&mut self` here since the structure is thread-safe and + /// thus, shared access via this function is ok and doesn't need to be handled + /// by the caller. + pub fn put(&self, key: &str, value: CacheValue) -> Result<(bool, Vec), CacheError> { + let mut evicted = vec![]; + let mut internal = self.cache.lock().unwrap(); + + if !internal.should_admit(key, &value) { + return Ok((false, evicted)); + } + if let Some(old) = internal.pop(key) { + evicted.push(old); + } + while internal.space_remaining() < value.size { + evicted.push(internal.evict_next()?); + } + internal.push(key, value); + Ok((true, evicted)) + } + + /// Retrieves the value corresponding to this key. + /// + /// ## Implementation note + /// Internally, we store + pub fn get(&self, key: &str) -> Option { + let mut internal = self.cache.lock().unwrap(); + internal.get(key) + } + + /// Explicitly removes the indicated key from the cache. Will return the + /// value for the key if it exists. + pub fn remove(&self, key: &str) -> Option { + let mut internal = self.cache.lock().unwrap(); + internal.pop(key) + } +} + +/// Wrapper for info about a particular block stored in the cache. +#[derive(Debug, Default, PartialEq, Eq, Clone)] +pub struct CacheValue { + pub size: u64, + pub version: u128, + pub key: String, + pub block_size: u64, + pub block_idx: u64, + + // updated when the value is inserted into the cache + pub insertion_time_ms: u128, +} + +impl CacheValue { + pub fn new(size: u64, version: u128, key: String, block_size: u64, block_idx: u64) -> Self { + CacheValue { + size, + version, + key, + block_size, + block_idx, + ..Default::default() + } + } + + pub fn get_elapsed_ms(&self) -> u128 { + util::time_to_epoch_millis(SystemTime::now()) - self.insertion_time_ms + } +} + +#[cfg(test)] +mod size_bound_cache_tests { + use super::{CacheValue, SizeBoundCache}; + use crate::disk::cache::EvictAction; + use crate::CacheError; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::{Arc, Mutex}; + + // check that inserting a value for key with the indicated size is successful, returning + // any evicted responses. + fn check_valid_insertion(c: &SizeBoundCache, key: &str, size: u64) -> Vec { + check_valid_insertion_with_version(c, key, size, 1) + } + + fn check_valid_insertion_with_version( + c: &SizeBoundCache, + key: &str, + size: u64, + version: u128, + ) -> Vec { + let (b, v) = c + .put(key, CacheValue::new(size, version, key.to_string(), 0, 0)) + .unwrap(); + assert!(b); + v + } + + #[test] + fn test_insertion_evictions() { + let cache = SizeBoundCache::new(50); + + assert_eq!(check_valid_insertion(&cache, "a", 20).len(), 0); + assert_eq!(check_valid_insertion(&cache, "b", 20).len(), 0); + let evicted = check_valid_insertion(&cache, "c", 20); + assert_eq!(evicted.len(), 1); + assert_eq!(evicted[0].key, "a".to_string()); + } + + #[test] + fn test_multi_evict() { + let cache = SizeBoundCache::new(50); + + assert_eq!(check_valid_insertion(&cache, "a", 20).len(), 0); + assert_eq!(check_valid_insertion(&cache, "b", 20).len(), 0); + assert_eq!(check_valid_insertion(&cache, "c", 40).len(), 2); + } + + #[test] + fn test_lru_eviction() { + let cache = SizeBoundCache::new(50); + assert_eq!(check_valid_insertion(&cache, "a", 20).len(), 0); + assert_eq!(check_valid_insertion(&cache, "b", 20).len(), 0); + assert_eq!(cache.get("a").unwrap().size, 20); + let evicted = check_valid_insertion(&cache, "c", 20); + assert_eq!(evicted.len(), 1); + assert_eq!(evicted[0].key, "b".to_string()); + } + + #[test] + fn test_overwrite() { + let cache = SizeBoundCache::new(50); + assert_eq!(check_valid_insertion(&cache, "a", 20).len(), 0); + assert_eq!(check_valid_insertion(&cache, "b", 20).len(), 0); + let evicted = check_valid_insertion_with_version(&cache, "b", 30, 2); + assert_eq!(evicted.len(), 1); + assert_eq!(evicted[0].key, "b".to_string()); + } + + #[test] + fn test_insertion_denied_too_large() { + let cache = SizeBoundCache::new(10); + let key = "a"; + let (b, v) = cache + .put(key, CacheValue::new(100, 1, key.to_string(), 0, 0)) + .unwrap(); + assert!(!b); + assert_eq!(v.len(), 0); + assert!(cache.get(key).is_none()) + } + + #[test] + fn test_insertion_denied_old_version() { + let cache = SizeBoundCache::new(50); + let key = "a"; + assert_eq!(check_valid_insertion(&cache, key, 20).len(), 0); + let (b, v) = cache + .put(key, CacheValue::new(10, 0, key.to_string(), 0, 0)) + .unwrap(); + assert!(!b); + assert_eq!(v.len(), 0); + let cached_val = cache.get(key).unwrap(); + assert_eq!(cached_val.size, 20); + assert_eq!(cached_val.version, 1); + } + + #[test] + fn test_eviction_callback() { + let e = Arc::new(Evictor::default()); + let cache = SizeBoundCache::new_with_eviction(50, Some(e.clone())); + assert_eq!(check_valid_insertion(&cache, "a", 30).len(), 0); + assert_eq!(check_valid_insertion(&cache, "b", 30).len(), 1); + // Expect that the evictor was called once + let guard = e.evicted_keys.lock().unwrap(); + assert_eq!(guard.len(), 1); + assert_eq!(guard[0].as_str(), "a"); + } + + #[test] + fn test_eviction_callback_multiple() { + let e = Arc::new(Evictor::default()); + let cache = SizeBoundCache::new_with_eviction(50, Some(e.clone())); + assert_eq!(check_valid_insertion(&cache, "a", 20).len(), 0); + assert_eq!(check_valid_insertion(&cache, "b", 30).len(), 0); + assert_eq!(check_valid_insertion(&cache, "c", 40).len(), 2); + // Expect that the evictor was called twice + let guard = e.evicted_keys.lock().unwrap(); + assert_eq!(guard.len(), 2); + assert_eq!(guard[0].as_str(), "a"); + assert_eq!(guard[1].as_str(), "b"); + } + + #[test] + fn test_eviction_callback_error() { + let e = Arc::new(Evictor { + return_err: AtomicBool::new(true), + ..Default::default() + }); + let cache = SizeBoundCache::new_with_eviction(50, Some(e)); + assert_eq!(check_valid_insertion(&cache, "a", 30).len(), 0); + let res = cache.put("b", CacheValue::new(30, 0, "b".to_string(), 0, 0)); + // We expect that there is an error returned (via the EvictAction we gave the cache) and + // that the cache entry was evicted. + assert!(res.is_err()); + assert_eq!(cache.cache.lock().unwrap().values.len(), 0); + } + + /// EvictAction to help test that the SizeBoundCache is calling it. + struct Evictor { + evicted_keys: Arc>>, + return_err: AtomicBool, + } + + impl EvictAction for Evictor { + fn evict(&self, val: &CacheValue) -> Result<(), CacheError> { + if self.return_err.load(Ordering::SeqCst) { + return Err(CacheError::InvariantViolated( + "Invariant violated test error".to_string(), + )); + } + self.evicted_keys.lock().unwrap().push(val.key.clone()); + Ok(()) + } + } + + impl Default for Evictor { + fn default() -> Self { + Evictor { + evicted_keys: Arc::new(Mutex::new(vec![])), + return_err: AtomicBool::new(false), + } + } + } +} diff --git a/cache/src/disk/storage.rs b/cache/src/disk/storage.rs new file mode 100644 index 00000000..0b1d9273 --- /dev/null +++ b/cache/src/disk/storage.rs @@ -0,0 +1,539 @@ +use std::fs; +use std::fs::{remove_file, DirEntry, File}; +use std::io::ErrorKind; +use std::io::Write; +use std::ops::Range; +#[cfg(unix)] +use std::os::unix::fs::{FileExt, MetadataExt}; +#[cfg(windows)] +use std::os::windows::fs::{FileExt, MetadataExt}; +use std::path::{Path, PathBuf}; +use std::str; + +use byteorder::LittleEndian; +use tracing::{debug, info, warn}; + +use crate::disk::cache::EvictAction; +use crate::disk::size_bound::CacheValue; +use crate::metrics::{BLOCKS_STORED, BYTES_STORED, NAME_DISK_CACHE}; +use crate::CacheError::{HeaderError, IOError}; +use crate::{util, CacheError}; + +/// The DiskManager maintains the storage of blocks on disk, including how they're +/// laid out on disk, their format, and how to read/write/delete them. +/// +/// # Disk Cache storage +/// Currently, we store each block as a file on disk under a singular root directory. +/// There are no plans currently to have support for multiple root directories or +/// any tiered storage (e.g. certain blocks are stored on NVMe vs SSD vs HDD). +/// +/// The contents of the file are the block's contents (i.e. no added metadata). +/// +/// The filename contains the following information separated by the `.` character: +/// - a base64 encoded identifier for the block (URL_SAFE config) +/// - the version of the block +/// +/// TODO: Have a more robust format that isn't as easily hackable. +/// TODO: Implement checksums on block data. +#[derive(Debug)] +pub struct DiskManager { + root_dir: PathBuf, +} + +impl DiskManager { + pub fn new(root_dir: PathBuf) -> Self { + DiskManager { root_dir } + } + + pub fn get_root_dir(&self) -> &PathBuf { + &self.root_dir + } + + /// Initializes management of the disk storage system. If things are ok, then + /// an iterator of the cache elements found on disk are returned as an iterator. + /// + /// Initialization involves loading the root directory (or creating it if it + /// doesn't exist) and reading through the root directory to get all of the + /// cache values from it, returning those as an iterator for the caller to use. + pub fn init(&self) -> Result>, CacheError> { + self.initialize_root_dir()?; + + let files = fs::read_dir(self.root_dir.as_path())?; + Ok(Box::new( + files + .scan((), |_, f| f.ok()) + .filter_map(Self::try_load_entry) + .map(|v| { + observe_data_added(v.size); + v + }), + )) + } + + /// Tries to load the indicated directory entry as a cache file. If this entry cannot + /// be loaded as a cache file, then we will log this and will remove the file. + /// + /// Note: we don't throw an error because this isn't a problem to surface further up. + fn try_load_entry(entry: DirEntry) -> Option { + let path = entry.path(); + match to_cache_value(entry) { + Ok(v) => Some(v), + Err(e) => { + info!("Entry in cache dir couldn't be read: {}", e); + remove_file(path.clone()) + .map_err(|e2| info!("Couldn't delete bad cache entry: {:?}, err: {}", path, e2)) + .ok(); + None + } + } + } + + pub async fn write(&self, item: &CacheValue, val: &[u8]) -> Result<(), CacheError> { + let path = self.to_filepath(item); + let size = val.len() as u64; + + let tempfile = tempfile::Builder::new().tempfile_in(&self.root_dir)?; + let mut f = tempfile.as_file(); + let header = Header { + block_size: item.block_size, + block_idx: item.block_idx, + key: item.key.clone(), + }; + header.write_to(&mut f)?; + f.write_all(val) + .map_err(IOError) + .map(|_| observe_data_added(size))?; + + let _ = tempfile.persist(path).map_err(|p| { + warn!("Failed to persist {:?}", p.error); + IOError(p.error) + })?; + + Ok(()) + } + + #[cfg(unix)] + fn read_impl(f: &mut File, buf: &mut [u8], start: u64) -> Result<(), CacheError> { + f.read_exact_at(buf, start)?; + Ok(()) + } + + #[cfg(windows)] + fn read_impl(f: &mut File, buf: &mut Vec, start: u64) -> Result<(), CacheError> { + let num_read = f.seek_read(buf, start)?; + // replicate behavior of unix read_exact_at + if num_read != buf.len() { + return Err(IOError(std::io::Error::new( + ErrorKind::UnexpectedEof, + "failed to fill whole buffer", + ))); + } + Ok(()) + } + + pub async fn read(&self, item: &CacheValue, range: Range) -> Result, CacheError> { + let path = self.to_filepath(item); + let mut f = File::open(path)?; + let mut buf = vec![0u8; (range.end - range.start) as usize]; + let header = Header::read_from(&mut f)?; + let start_off = header.get_header_len() + range.start; + Self::read_impl(&mut f, &mut buf, start_off)?; + Ok(buf) + } + + pub fn remove(&self, item: &CacheValue) -> Result<(), CacheError> { + let path = self.to_filepath(item); + let size = item.size; + remove_file(path) + .map_err(IOError) + .map(|_| observe_data_removed(size)) + } + + /// Checks that self.root_dir points to a directory on the local filesystem, + /// creating it if it doesn't already exist. + fn initialize_root_dir(&self) -> Result<(), CacheError> { + let root = self.root_dir.clone(); + match root.metadata() { + Ok(metadata) => { + if !metadata.is_dir() || metadata.permissions().readonly() { + return Err(CacheError::CacheNotWritableDirectory); + } + metadata + } + Err(e) => { + return match e.kind() { + ErrorKind::NotFound => { + info!("Cache dir doesn't exist. Creating..."); + fs::create_dir_all(root).map_err(IOError) + } + _ => Err(IOError(e)), + } + } + }; + Ok(()) + } + + fn to_filepath(&self, v: &CacheValue) -> PathBuf { + let filename = to_filename(v.key.as_str()); + self.root_dir.join(Path::new(filename.as_str())) + } +} + +fn observe_data_added(size: u64) { + BLOCKS_STORED.with_label_values(&[NAME_DISK_CACHE]).inc(); + BYTES_STORED + .with_label_values(&[NAME_DISK_CACHE]) + .add(size as i64); +} + +fn observe_data_removed(size: u64) { + BLOCKS_STORED.with_label_values(&[NAME_DISK_CACHE]).dec(); + BYTES_STORED + .with_label_values(&[NAME_DISK_CACHE]) + .sub(size as i64); +} + +impl EvictAction for DiskManager { + fn evict(&self, block: &CacheValue) -> Result<(), CacheError> { + self.remove(block) + } +} + +fn to_filename(block_id: &str) -> String { + base64::encode_config(block_id.as_bytes(), base64::URL_SAFE) +} + +/// parses the filename into its "key" and "version" parts. +fn parse_filename(filename: &str) -> Option { + let key = base64::decode_config(filename, base64::URL_SAFE).ok()?; + let block_id = str::from_utf8(key.as_slice()).ok()?; + Some(block_id.to_string()) +} + +#[cfg(unix)] +fn metadata_size(metadata: &fs::Metadata) -> u64 { + metadata.size() +} + +#[cfg(windows)] +fn metadata_size(metadata: &fs::Metadata) -> u64 { + metadata.file_size() +} + +/// Pulls out all the information we need from the directory entry to +/// track it in the cache. +fn to_cache_value(entry: DirEntry) -> Result { + let filename = entry + .file_name() + .into_string() + .map_err(|_| format!("({entry:?}) name isn't convertable to String"))?; + let metadata = entry + .metadata() + .map_err(|e| format!("{filename} metadata couldn't be fetched: {e:?}"))?; + + if !metadata.is_file() { + return Err(format!("{filename} is not a file")); + } + let key = parse_filename(filename.as_str()) + .ok_or_else(|| format!("{filename} doesn't follow naming convention"))?; + + let header = if let Some(h) = Header::attempt_from_key(&key) { + #[cfg(debug_assertions)] + { + let alt_h = + verify_header(entry).map_err(|e| format!("{filename} invalid header: {e:?}"))?; + assert_eq!(alt_h, h); + } + h + } else { + debug!("Warning: Parsing header from filename for {filename} failed; loading from file."); + verify_header(entry).map_err(|e| format!("{filename} invalid header: {e:?}"))? + }; + + let file_size = metadata_size(&metadata) - header.get_header_len(); + + Ok(CacheValue { + size: file_size, + version: 1, + block_size: header.block_size, + block_idx: header.block_idx, + key, + insertion_time_ms: metadata + .modified() + .ok() + .map_or(0, util::time_to_epoch_millis), + }) +} + +fn verify_header(entry: DirEntry) -> Result { + let mut f = File::open(entry.path())?; + Header::read_from(&mut f) +} + +const HEADER_MAGIC: [u8; 8] = [88, 69, 84, 67, 65, 67, 72, 69]; // XETCACHE +const FORMAT_VERSION: u8 = 1u8; +const HEADER_FIXED_SIZE: u64 = 8 + 1 + 8 + 8 + 4; + +/// The header for a cache file consists of the following pieces: +/// u64: HEADER_MAGIC +/// u8: FORMAT_VERSION +/// u64: block size in bytes +/// u64: block index +/// u32: length of key string +/// : the key for the block +/// +/// All numbers are encoded in LittleEndian encoding +#[derive(Debug, Default, PartialEq, Eq)] +struct Header { + block_size: u64, + block_idx: u64, + key: String, +} + +impl Header { + /// Serialize the Header into a byte Vec: + /// [HEADER_MAGIC, FORMAT_VERSION, BLOCK_SIZE, BLOCK_IDX, LEN(NAME), NAME] + fn write_to(&self, file: &mut T) -> Result<(), CacheError> { + file.write_all(&HEADER_MAGIC)?; + file.write_u8(FORMAT_VERSION)?; + file.write_u64::(self.block_size)?; + file.write_u64::(self.block_idx)?; + + let name_bytes = self.key.as_bytes(); + file.write_u32::(name_bytes.len() as u32)?; + file.write_all(name_bytes)?; + Ok(()) + } + + fn read_from(file: &mut T) -> Result { + let mut magic = [0u8; 8]; + file.read_exact(&mut magic)?; + if magic != HEADER_MAGIC { + return Err(HeaderError(format!( + "magic number: {magic:?} doesn't match: {HEADER_MAGIC:?}" + ))); + } + let version = file.read_u8()?; + match version { + 1 => Self::parse_header_v1(file), + _ => Err(HeaderError(format!("version: {version} unsupported"))), + } + } + + fn parse_header_v1(file: &mut T) -> Result { + let block_size = file.read_u64::()?; + let block_idx = file.read_u64::()?; + let name_len = file.read_u32::()? as usize; + let mut name_arr = vec![0; name_len]; + file.read_exact(&mut name_arr)?; + let key = String::from_utf8(name_arr) + .map_err(|e| HeaderError(format!("failed to parse key as UTF8: {e:?}")))?; + Ok(Self { + block_size, + block_idx, + key, + }) + } + + fn get_header_len(&self) -> u64 { + return HEADER_FIXED_SIZE + self.key.as_bytes().len() as u64; + } + + fn attempt_from_key(key: &str) -> Option { + // This assumes the request_to_key function, which dictates the key value and the filename, uses + // the following format: + // format!( + // "{name}.{block_idx}.{block_size}", + // ) + // See the request_to_key function in cache.rs + + let last_dot = key.rfind('.')?; + let second_last_dot = key[..last_dot].rfind('.')?; + + let _name = &key[..second_last_dot]; + let block_idx = key[second_last_dot + 1..last_dot].parse::().ok()?; + let block_size = key[last_dot + 1..].parse::().ok()?; + + Some(Self { + key: key.to_owned(), + block_idx, + block_size, + }) + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use crate::util::test_utils::CacheDirTest; + + use super::*; + + #[test] + fn test_name_conversion() { + let block_id = "file.0"; + let filename = to_filename(block_id); + assert_eq!(filename, "ZmlsZS4w"); + let parsed_block_id = parse_filename(filename.as_str()).unwrap(); + assert_eq!(parsed_block_id, block_id.to_string()); + } + + #[test] + fn test_name_conversion_invalid_format() { + let filename_extra_components = "ZmlsZS4w.1"; + assert!(parse_filename(filename_extra_components).is_none()); + + let filename_id_not_base64 = "a/b/c_txt"; + assert!(parse_filename(filename_id_not_base64).is_none()); + } + + #[tokio::test] + async fn test_manager_write() { + let dir = CacheDirTest::new("write"); + let m = DiskManager::new(dir.get_path().to_path_buf()); + let key = CacheValue::new(5, 0, "abc".to_string(), 1024, 0); + let data: Vec = vec![1, 2, 3, 4, 5]; + m.write(&key, data.as_slice()).await.unwrap(); + let entries = dir.get_entries(); + assert_eq!(entries.len(), 1); + assert_eq!( + entries[0].file_name().into_string().unwrap().as_str(), + "YWJj" + ); + } + + #[tokio::test] + async fn test_manager_write_read() { + let dir = CacheDirTest::new("write_read"); + let m = DiskManager::new(dir.get_path().to_path_buf()); + let key = CacheValue::new(5, 0, "abc".to_string(), 1024, 0); + let data: Vec = vec![1, 2, 3, 4, 5]; + m.write(&key, data.as_slice()).await.unwrap(); + + let data_read = m.read(&key, 0..5).await.unwrap(); + assert_eq!(data, data_read); + } + + #[tokio::test] + async fn test_manager_read_not_found() { + let dir = CacheDirTest::new("read_not_found"); + let m = DiskManager::new(dir.get_path().to_path_buf()); + let key = CacheValue::new(5, 0, "abc".to_string(), 1024, 0); + let res = m.read(&key, 0..5).await; + assert!(res.is_err()); + } + + #[tokio::test] + async fn test_manager_remove() { + let dir = CacheDirTest::new("write_remove"); + let m = DiskManager::new(dir.get_path().to_path_buf()); + let key = CacheValue::new(5, 0, "abc".to_string(), 1024, 0); + let data: Vec = vec![1, 2, 3, 4, 5]; + m.write(&key, data.as_slice()).await.unwrap(); + + m.remove(&key).unwrap(); + + let entries = dir.get_entries(); + assert!(entries.is_empty()); + } + + #[tokio::test] + async fn test_manager_load() { + let dir = CacheDirTest::new("load"); + let m = DiskManager::new(dir.get_path().to_path_buf()); + let mut key = CacheValue::new(5, 0, "abc".to_string(), 1024, 0); + let data: Vec = vec![1, 2, 3, 4, 5]; + m.write(&key, data.as_slice()).await.unwrap(); + key.key = "bcd".to_string(); + m.write(&key, data.as_slice()).await.unwrap(); + + // check that a new DiskManager will load in the 2 cached files. + let m2 = DiskManager::new(dir.get_path().to_path_buf()); + let mut vals: Vec = m2.init().unwrap().collect(); + assert_eq!(vals.len(), 2); + vals.sort_by(|a, b| a.key.cmp(&b.key)); + assert_eq!(vals[0].key.as_str(), "abc"); + assert_eq!(vals[1].key.as_str(), "bcd"); + } + + #[test] + fn test_header_serde() { + let header = Header { + block_size: 16 * 1024 * 1024, + block_idx: 4, + key: "prefix/abcdef20421.4.16000000".to_string(), + }; + let mut buf = Vec::with_capacity(100); + header.write_to(&mut buf).unwrap(); + println!("buf: {:?}", buf); + + let mut file = Cursor::new(buf); + let header_deser = Header::read_from(&mut file).unwrap(); + assert_eq!(header.block_size, header_deser.block_size); + assert_eq!(header.block_idx, header_deser.block_idx); + assert_eq!(header.key, header_deser.key); + } + + #[test] + fn test_deserialize_invalid_magic_num() { + let invalid_magic = vec![ + 78, 65, 74, 64, 61, 63, 68, 65, 1, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 29, + 0, 0, 0, 112, 114, 101, 102, 105, 120, 47, 97, 98, 99, 100, 101, 102, 50, 48, 52, 50, + 49, 46, 52, 46, 49, 54, 48, 48, 48, 48, 48, 48, + ]; + let mut file = Cursor::new(invalid_magic); + let result = Header::read_from(&mut file); + assert!(result.is_err()); + } + + #[test] + fn test_deserialize_unsupported_version() { + let invalid_version = vec![ + 88, 69, 84, 67, 65, 67, 72, 69, 8, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 29, + 0, 0, 0, 112, 114, 101, 102, 105, 120, 47, 97, 98, 99, 100, 101, 102, 50, 48, 52, 50, + 49, 46, 52, 46, 49, 54, 48, 48, 48, 48, 48, 48, + ]; + let mut file = Cursor::new(invalid_version); + let result = Header::read_from(&mut file); + assert!(result.is_err()); + } + + #[test] + fn test_deserialize_eof() { + let invalid_version = vec![ + 88, 69, 84, 67, 65, 67, 72, 69, 1, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 29, + 0, 0, 0, 112, 114, 101, 102, 105, 120, 47, 97, 98, 99, 100, + ]; + let mut file = Cursor::new(invalid_version); + let result = Header::read_from(&mut file); + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_load_delete_invalid_entries() { + let dir = CacheDirTest::new("load_invalid"); + let m = DiskManager::new(dir.get_path().to_path_buf()); + let mut key = CacheValue::new(5, 0, "abc".to_string(), 1024, 0); + let mut data: Vec = vec![1, 2, 3, 4, 5]; + m.write(&key, data.as_slice()).await.unwrap(); + key.key = "bcd".to_string(); + m.write(&key, data.as_slice()).await.unwrap(); + + let invalid_file_path = dir.get_path().to_path_buf().join("YS4xLjEw.1"); + let mut invalid_file = File::create(invalid_file_path.clone()).unwrap(); + invalid_file.write_all(data.as_mut_slice()).unwrap(); + drop(invalid_file); // make sure file has been synced to disk + + // check that a new DiskManager will load in the 2 cached files. + let m2 = DiskManager::new(dir.get_path().to_path_buf()); + let mut vals: Vec = m2.init().unwrap().collect(); + assert_eq!(vals.len(), 2); + vals.sort_by(|a, b| a.key.cmp(&b.key)); + assert_eq!(vals[0].key.as_str(), "abc"); + assert_eq!(vals[1].key.as_str(), "bcd"); + + // check that the invalid file was deleted as part of the load. + assert!(File::open(invalid_file_path).is_err()) + } +} diff --git a/cache/src/error.rs b/cache/src/error.rs new file mode 100644 index 00000000..012a0808 --- /dev/null +++ b/cache/src/error.rs @@ -0,0 +1,48 @@ +use crate::CacheError::OtherTaskError; +use cas::errors::SingleflightError; +use tracing::error; +use xet_error::Error; + +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum CacheError { + #[error(transparent)] + IOError(#[from] std::io::Error), + #[error("Couldn't save all data")] + SaveError, + #[error("Invariant violated in the function: {0}")] + InvariantViolated(String), + #[error("Couldn't find block")] + BlockNotFound, + #[error("disk cache not in a directory")] + CacheNotWritableDirectory, + #[error("input file not formatted properly")] + InvalidFilenameFormat, + #[error("issue joining result back to main")] + JoinError, + + #[error("Invalid range supplied ({0}, {1})")] + InvalidRange(u64, u64), + + #[error(transparent)] + RemoteError(#[from] anyhow::Error), + + #[error("Error with other task reading block: {0}")] + OtherTaskError(String), + + #[error("Error serializing header: {0}")] + HeaderError(String), +} + +// Define our own result type here (this seems to be the standard). +pub type Result = std::result::Result; + +impl From> for CacheError { + fn from(e: SingleflightError) -> Self { + match e { + SingleflightError::InternalError(err) => err, + SingleflightError::WaiterInternalError(err) => OtherTaskError(err), + _ => CacheError::InvariantViolated(format!("BUG: Call to remote: {e:?}")), + } + } +} diff --git a/cache/src/interface.rs b/cache/src/interface.rs new file mode 100644 index 00000000..bc53653f --- /dev/null +++ b/cache/src/interface.rs @@ -0,0 +1,105 @@ +use std::fmt::{Display, Formatter}; +use std::ops::Range; +use std::sync::Arc; + +use async_trait::async_trait; + +use crate::block::BlockRange; +use crate::CacheError; + +/// A Reader provides a way to read blocks from some system. +#[async_trait] +pub trait BlockReader { + /// Reads in data for the specified request. + async fn get(&self, request: &BlockReadRequest) -> Result, CacheError>; +} + +/// A BlockReadRequest encompasses a request for a particular block within a file. +#[derive(Debug, Clone)] +pub struct BlockReadRequest { + /// Block range + block_range: BlockRange, + /// metadata of the file + metadata: Arc, +} + +impl BlockReadRequest { + pub fn from_block_range( + block_range: BlockRange, + metadata: Arc, + ) -> BlockReadRequest { + BlockReadRequest { + block_range, + metadata, + } + } + + /// Starting offset within the block + pub fn start_off(&self) -> u64 { + self.block_range.start_off() + } + + /// Ending offset within the block + pub fn end_off(&self) -> u64 { + self.block_range.end_off() + } + + pub fn range(&self) -> Range { + self.start_off()..self.end_off() + } + + pub fn block_range(&self) -> &BlockRange { + &self.block_range + } + + pub fn metadata(&self) -> &Arc { + &self.metadata + } + + pub fn block_size(&self) -> u64 { + self.block_range.block_size() + } +} + +impl Display for BlockReadRequest { + /// Format string consists of the block being requested (.) + /// followed by the byte range within the block being requested + /// (in Rust Range format, with the start being inclusive and the end being exclusive). + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ {}.{}:{}..{} }}", + self.metadata.name, + self.block_range.idx(), + self.block_range.start_off(), + self.block_range.end_off() + ) + } +} + +/// Relevant metadata for a file in the system. +#[derive(Debug, Clone)] +pub struct FileMetadata { + name: String, + size: Option, + version: u128, // epoch_millis is a u128 +} + +impl FileMetadata { + pub fn new(name: String, size: Option, version: u128) -> Self { + FileMetadata { + name, + size, + version, + } + } + pub fn name(&self) -> &str { + &self.name + } + pub fn size(&self) -> Option { + self.size + } + pub fn version(&self) -> u128 { + self.version + } +} diff --git a/cache/src/lib.rs b/cache/src/lib.rs new file mode 100644 index 00000000..aec5a4bf --- /dev/null +++ b/cache/src/lib.rs @@ -0,0 +1,71 @@ +#![cfg_attr(feature = "strict", deny(warnings))] + +use std::ops::Range; +use std::{fmt::Debug, sync::Arc}; + +use crate::error::Result; +pub use block::BlockConverter; +use cas::key::Key; +use cas::singleflight; +pub use disk::DiskCache; +pub use error::CacheError; +pub use interface::{BlockReadRequest, BlockReader, FileMetadata}; +pub use metrics::set_metrics_service_name; +pub use xorb_cache::XorbCacheImpl; + +mod block; +mod disk; +mod error; +mod interface; +pub mod lru; +mod metrics; +mod util; +mod xorb_cache; + +/// Provides a way for the the cache to read data from the remote source on cache miss. +/// Clients of the cache should adapt their remote store to this API. +#[mockall::automock] +#[async_trait::async_trait] +pub trait Remote: Debug + Sync + Send { + async fn fetch( + &self, + key: &cas::key::Key, + range: Range, + ) -> std::result::Result, anyhow::Error>; +} + +/// A XorbCache is the top level caching service that can be used to read +/// data from a Xorb. +#[async_trait::async_trait] +pub trait XorbCache: Debug + Sync + Send { + async fn fetch_xorb_range( + &self, + key: &Key, + range: Range, + size: Option, + ) -> Result>; + async fn put_cache(&self, key: &Key, contents: &[u8]) -> Result<()>; +} + +pub struct CacheConfig { + pub cache_dir: String, + pub capacity: u64, // size in bytes + pub block_size: u64, // size in bytes +} + +/// Factory method for building the XORB cache. +pub fn from_config( + cfg: CacheConfig, + remote: Arc, +) -> Result> { + let cache = DiskCache::from_config(cfg.cache_dir.as_str(), cfg.capacity)?; + let converter = BlockConverter::new(cfg.block_size); + let request_merger = singleflight::Group::new(); + + Ok(Arc::new(XorbCacheImpl::new( + cache, + remote, + converter, + request_merger, + ))) +} diff --git a/cache/src/lru.rs b/cache/src/lru.rs new file mode 100644 index 00000000..de46d6eb --- /dev/null +++ b/cache/src/lru.rs @@ -0,0 +1,75 @@ +use crate::metrics::{LRU_REQUESTS, STATUS_EXPIRED, STATUS_HIT, STATUS_MISS}; +use chrono::{DateTime, Duration, Utc}; +use lru::LruCache; +use std::{self, hash::Hash}; + +const LRU_CACHE_CAPACITY: usize = 1000; +const LRU_CACHE_TIMEOUT_MINUTES: i64 = 5; + +struct Timestamped { + t: DateTime, + value: Value, +} + +impl Timestamped { + fn new(value: Value) -> Timestamped { + Timestamped { + t: Utc::now(), + value, + } + } +} + +pub struct Lru { + lru: LruCache>, + duration: Duration, + name: String, +} + +impl Default for Lru { + fn default() -> Self { + Self::new(LRU_CACHE_CAPACITY, LRU_CACHE_TIMEOUT_MINUTES, "default") + } +} + +impl Lru { + pub fn new(capacity: usize, duration_minutes: i64, name: &str) -> Self { + Self { + lru: LruCache::new(std::num::NonZero::new(capacity.max(1)).unwrap()), + duration: { + #[allow(deprecated)] // Sometimes the linter seems to hate this.... + Duration::minutes(duration_minutes) + }, + name: name.to_string(), + } + } + + pub fn with_name(mut self, name: &str) -> Self { + self.name = name.to_string(); + self + } + + pub fn fetch(&mut self, key: &K) -> Option { + if let Some(timestamped_v) = self.lru.get(key) { + if Utc::now() - timestamped_v.t < self.duration { + let val = timestamped_v.value.clone(); + self.observe_request(STATUS_HIT); + return Some(val); + } else { + self.observe_request(STATUS_EXPIRED); + self.lru.pop_entry(key); + } + } else { + self.observe_request(STATUS_MISS); + } + None + } + + pub fn put(&mut self, key: &K, val: V) { + self.lru.put(key.clone(), Timestamped::new(val)); + } + + fn observe_request(&self, status: &str) { + LRU_REQUESTS.with_label_values(&[&self.name, status]).inc(); + } +} diff --git a/cache/src/metrics.rs b/cache/src/metrics.rs new file mode 100644 index 00000000..6535f6ac --- /dev/null +++ b/cache/src/metrics.rs @@ -0,0 +1,117 @@ +use std::sync::Mutex; + +use lazy_static::lazy_static; +use prometheus::{ + register_histogram, register_histogram_vec, register_int_counter, register_int_counter_vec, + register_int_gauge_vec, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGaugeVec, +}; + +const DEFAULT_SERVICE: &str = "cas"; +const NAMESPACE: &str = "cache"; + +// Allow the SERVICE to be dynamically configurable +lazy_static! { + static ref SERVICE: Mutex = Mutex::new(DEFAULT_SERVICE.to_string()); +} + +pub const SOURCE_DISK_CACHE: &str = "disk_cache"; +pub const SOURCE_REMOTE: &str = "remote"; +pub const SOURCE_SINGLEFLIGHT: &str = "singleflight"; + +// XorbCache metrics +lazy_static! { + pub static ref BLOCKS_READ: IntCounterVec = register_int_counter_vec!( + prefix_name(NAMESPACE, "block_count").as_str(), + "count of blocks served by the cache by data source", + &["source"] + ) + .unwrap(); + pub static ref DATA_READ: IntCounterVec = register_int_counter_vec!( + prefix_name(NAMESPACE, "data_count").as_str(), + "count of bytes served by the cache by data source", + &["source"] + ) + .unwrap(); + pub static ref READ_ERROR_COUNT: IntCounter = register_int_counter!( + prefix_name(NAMESPACE, "read_error_count").as_str(), + "count of errors reading from the cache" + ) + .unwrap(); + pub static ref WRITE_ERROR_COUNT: IntCounter = register_int_counter!( + prefix_name(NAMESPACE, "write_error_count").as_str(), + "count of errors writing to the cache" + ) + .unwrap(); + pub static ref REQUEST_LATENCY_MS: HistogramVec = register_histogram_vec!( + prefix_name(NAMESPACE, "latency_ms").as_str(), + "latency of cache requests in milliseconds by data source", + &["source"], + vec!(10.0, 25.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0) + ) + .unwrap(); + pub static ref REQUEST_THROUGHPUT: HistogramVec = register_histogram_vec!( + prefix_name(NAMESPACE, "throughput_mbps").as_str(), + "throughput of cache requests in Mbps by data source", + &["source"], + vec!(10.0, 25.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0) + ) + .unwrap(); +} + +pub const NAME_DISK_CACHE: &str = "disk"; + +// BlockCache metrics (e.g. DiskCache) +lazy_static! { + pub static ref BLOCKS_STORED: IntGaugeVec = register_int_gauge_vec!( + prefix_name(NAMESPACE, "blocks_stored").as_str(), + "number of blocks stored in the indicated cache", + &["name"] + ) + .unwrap(); + pub static ref BYTES_STORED: IntGaugeVec = register_int_gauge_vec!( + prefix_name(NAMESPACE, "bytes_stored").as_str(), + "number of bytes stored in the indicated cache", + &["name"] + ) + .unwrap(); + // Note: eviction age is specific to disk since we expect the histogram + // buckets to be specific to the cache type (e.g. if we introduce a memcache, + // it should have lower expected eviction ages). + pub static ref DISK_EVICTION_AGE: Histogram = register_histogram!( + prefix_name(NAMESPACE, "disk_eviction_age").as_str(), + "age of entries evicted by the disk cache in seconds", + vec!(60.0, 300.0, 600.0, 1800.0, 3600.0, 7200.0, 43200.0, 86400.0) + ) + .unwrap(); +} + +pub const STATUS_EXPIRED: &str = "expired"; +pub const STATUS_MISS: &str = "miss"; +pub const STATUS_HIT: &str = "hit"; + +// Lru metrics +lazy_static! { + // status values: [expired, miss, hit] + pub static ref LRU_REQUESTS: IntCounterVec = register_int_counter_vec!( + prefix_name(NAMESPACE, "lru_request_count").as_str(), + "count of requests to an lru cache broken down by cache name and status", + &["name", "status"] + ) + .unwrap(); +} + +fn prefix_name(namespace: &str, name: &str) -> String { + let service = SERVICE + .lock() + .expect("Couldn't get service name for cache metrics"); + format!("{service}_{namespace}_{name}") +} + +/// Sets the name for the "service" field of cache metrics. Needs to be set as +/// part of application startup before cache metrics are initialized. +pub fn set_metrics_service_name(service_name: String) { + let mut contents = SERVICE + .lock() + .expect("FATAL: couldn't lock cache metrics service name"); + *contents = service_name; +} diff --git a/cache/src/util.rs b/cache/src/util.rs new file mode 100644 index 00000000..bc2ced46 --- /dev/null +++ b/cache/src/util.rs @@ -0,0 +1,84 @@ +#![allow(dead_code)] + +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn time_to_epoch_millis(ts: SystemTime) -> u128 { + ts.duration_since(UNIX_EPOCH) + .map_or(0, |dur| dur.as_millis()) +} + +/// Helper fn to be used with `scan()` to early terminate an iterator on +/// the first encounter of an error and be able to store the error in a +/// variable. See tests for an example use case of this. +/// Pulled from 2nd answer of: https://stackoverflow.com/questions/26368288/how-do-i-stop-iteration-and-return-an-error-when-iteratormap-returns-a-result + +pub fn until_err(err: &mut &mut Result<(), E>, item: Result) -> Option { + match item { + Ok(item) => Some(item), + Err(e) => { + **err = Err(e); + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_scan() { + let res: Vec> = vec![Ok(1), Ok(2), Err("err"), Ok(3)]; + + let mut err = Ok(()); + + let valid: Vec = res + // Note: this has to be into_iter() and not iter(), since we need the + // actual values for the until_err to work instead of references. If + // you need to use iter(), you'll need to provide the closure manually. + .into_iter() + .scan(&mut err, until_err) + .collect(); + assert_eq!(err.err().unwrap().to_string(), "err".to_string()); + assert_eq!(valid.len(), 2); + assert_eq!(valid[0], 1); + assert_eq!(valid[1], 2); + } +} + +#[cfg(test)] +pub mod test_utils { + use std::fs::DirEntry; + use std::path::Path; + use std::{fs, str}; + use tempdir::TempDir; + + /// Manages a temporary directory for a test. Will be cleaned up when + /// the struct is dropped. + pub struct CacheDirTest { + dir: TempDir, + } + + impl CacheDirTest { + pub fn new(dir_prefix: &str) -> Self { + CacheDirTest { + dir: TempDir::new(dir_prefix).unwrap(), + } + } + + pub fn get_path(&self) -> &Path { + self.dir.path() + } + + pub fn get_path_str(&self) -> &str { + self.get_path().to_str().unwrap() + } + + pub fn get_entries(&self) -> Vec { + fs::read_dir(self.dir.path()) + .unwrap() + .map(|e| e.unwrap()) + .collect() + } + } +} diff --git a/cache/src/xorb_cache.rs b/cache/src/xorb_cache.rs new file mode 100644 index 00000000..24777883 --- /dev/null +++ b/cache/src/xorb_cache.rs @@ -0,0 +1,450 @@ +use std::cmp::min; +use std::ops::Range; +use std::sync::Arc; +use std::time::SystemTime; + +use tracing::{debug, info, info_span, warn}; +use tracing_futures::Instrument; + +use cas::key::Key; +use cas::singleflight; + +use crate::metrics::{ + BLOCKS_READ, DATA_READ, READ_ERROR_COUNT, REQUEST_LATENCY_MS, REQUEST_THROUGHPUT, + SOURCE_DISK_CACHE, SOURCE_REMOTE, SOURCE_SINGLEFLIGHT, WRITE_ERROR_COUNT, +}; +use crate::{BlockConverter, BlockReadRequest, BlockReader, CacheError, DiskCache, FileMetadata}; +use crate::{Remote, XorbCache}; + +#[derive(Debug)] +pub struct XorbCacheImpl { + cache: DiskCache, + remote: Arc, + block_converter: BlockConverter, + request_merger: singleflight::Group>, CacheError>, +} + +impl XorbCacheImpl { + pub fn new( + cache: DiskCache, + remote: Arc, + block_converter: BlockConverter, + request_merger: singleflight::Group>, CacheError>, + ) -> Self { + XorbCacheImpl { + cache, + remote, + block_converter, + request_merger, + } + } + + async fn read_block_from_cache(&self, request: &BlockReadRequest) -> Option> { + let block_id = request.block_range().idx(); + // check cache for block + let result = self + .cache + .get(request) + .instrument(info_span!("cache_read_block", %block_id)) + .await; + match result { + Ok(data) => { + debug!("Found data for block: {} in cache!", block_id); + return Some(data); + } + Err(CacheError::BlockNotFound) => { + debug!("Cache miss for block: {}", block_id); + } + Err(e) => { + READ_ERROR_COUNT.inc(); + debug!( + "Unexpected issue reading block: {} from cache: {:?}", + block_id, e + ); + } + }; + None + } + + async fn read_block_from_remote( + remote: Arc, + cache: DiskCache, + key: Key, + full_block_request: BlockReadRequest, + ) -> Result>, CacheError> { + let block_id = full_block_request.block_range().idx(); + let block_offsets_abs = full_block_request.block_range().to_abs_offsets(); + let data = remote + .fetch(&key, block_offsets_abs) + .instrument(info_span!("cache_read_remote", %block_id)) + .await + .map_err(CacheError::from)?; + let data = Arc::new(data); + debug!( + "Found data for block: {} in remote!", + full_block_request.block_range().idx() + ); + // cache the block + let res = cache + .put(&full_block_request, &data) + .instrument(info_span!("cache_put_block", %block_id)) + .await; + if res.is_ok() { + debug!( + "Stored block: {} in cache", + full_block_request.block_range().idx() + ); + } else if let Err(e) = res { + WRITE_ERROR_COUNT.inc(); + // This is debug as it doesn't actually cause problems. + info!( + "Failed to store block: {} in cache: err: {:?}", + full_block_request.block_range().idx(), + e + ); + } + Ok(data) + } + + /// Note: assumes that the range has been sanitized (i.e. 0 <= start <= end <= length) + /// TODO: Break this up into more logical pieces, parallelize reads for better performance, + /// instrument with cache hit-rates (BHR/OHR), and stream results to callers. + async fn fetch_range_internal( + &self, + key: &Key, + range: Range, + size: Option, + ) -> Result, CacheError> { + let mut vec: Vec = Vec::with_capacity((range.end - range.start) as usize); + //TODO: do we need versions? + let md = Arc::new(FileMetadata::new(key.to_string(), size, 1)); + + for block_range in self.block_converter.to_block_ranges(range) { + let start = SystemTime::now(); + let request = BlockReadRequest::from_block_range(block_range, md.clone()); + let block_id = request.block_range().idx(); + debug!("Block {} requested", block_id); + // check cache for block + if let Some(mut data) = self.read_block_from_cache(&request).await { + let len = data.len(); + vec.append(&mut data); + observe_read(SOURCE_DISK_CACHE, start, len); + continue; + } + + // load data from remote + // expand read to load entire block so we can cache it and merge the request + // with any concurrent reads trying to fetch the same block. + let full_block = self + .block_converter + .get_full_block_range_for(block_id, md.size()) + .ok_or_else(|| { + CacheError::InvariantViolated("couldn't extend block size".to_string()) + })?; + let full_block_request = BlockReadRequest::from_block_range(full_block, md.clone()); + + let request_key = format!("remote_{full_block_request}"); + let (res, is_owner) = self + .request_merger + .work( + request_key.as_str(), + Self::read_block_from_remote( + self.remote.clone(), + self.cache.clone(), + key.clone(), + full_block_request, + ), + ) + .await; + let data = res?; + let source = if is_owner { + SOURCE_REMOTE + } else { + SOURCE_SINGLEFLIGHT + }; + observe_read(source, start, data.len()); + + // return sub-range of data the caller requested + if (request.start_off() as usize) < data.len() { + let request_end_off = min(request.end_off() as usize, data.len()); + let return_data = &data[(request.start_off() as usize)..request_end_off]; + vec.extend(return_data.iter()); + } + } + debug!("Finished reading data. Read: {} bytes", vec.len()); + Ok(vec) + } + + /// Stores a key into cache, reading the value from the provided Reader + async fn put_cache_internal(&self, key: &Key, contents: &[u8]) -> Result<(), CacheError> { + let len = contents.len() as u64; + let md = Arc::new(FileMetadata::new(key.to_string(), Some(len), 1)); + + for block_range in self.block_converter.to_block_ranges(0..len) { + let block_id = block_range.idx(); + // Since we are breaking up the content into multiple blocks, we need to use + // absolute offsets instead of relative offsets. + let range = block_range.to_abs_offsets(); + let data_slice = &contents[range.start as usize..range.end as usize]; + + let full_block_request = BlockReadRequest::from_block_range(block_range, md.clone()); + // cache the block + let res = self + .cache + .put(&full_block_request, data_slice) + .instrument(info_span!("cache_put_block", %block_id)) + .await; + if res.is_ok() { + debug!( + "Stored block: {} in cache", + full_block_request.block_range().idx() + ); + } else if let Err(e) = res { + WRITE_ERROR_COUNT.inc(); + info!( + "Failed to store block: {} in cache: err: {:?}", + full_block_request.block_range().idx(), + e + ); + } + } + Ok(()) + } +} + +fn observe_read(source: &str, start: SystemTime, size: usize) { + let labels = [source]; + BLOCKS_READ.with_label_values(&labels).inc(); + DATA_READ.with_label_values(&labels).inc_by(size as u64); + match start.elapsed() { + Ok(elapsed) => { + let elapsed_ms = elapsed.as_millis(); + REQUEST_LATENCY_MS + .with_label_values(&labels) + .observe(elapsed_ms as f64); + // 8bit / Byte * 1e3 ms / s * 1 MB / 1e6 Byte + let throughput_mbps = (size * 8) as f64 / (elapsed_ms as f64 * 1000.0); + REQUEST_THROUGHPUT + .with_label_values(&labels) + .observe(throughput_mbps); + } + Err(e) => { + warn!("Stopwatch error occurred: {:?}", e) + } + } +} + +#[async_trait::async_trait] +impl XorbCache for XorbCacheImpl { + #[tracing::instrument(skip(self, key), name = "cache_read")] + async fn fetch_xorb_range( + &self, + key: &Key, + range: Range, + size: Option, + ) -> Result, CacheError> { + if range.end < range.start { + return Err(CacheError::InvalidRange(range.start, range.end)); + } + + let (start_off, end_off) = match size { + Some(size) => (min(range.start, size), min(range.end, size)), + None => (range.start, range.end), + }; + self.fetch_range_internal(key, start_off..end_off, size) + .await + } + async fn put_cache(&self, key: &Key, contents: &[u8]) -> Result<(), CacheError> { + self.put_cache_internal(key, contents).await + } +} + +#[cfg(test)] +mod test { + use std::sync::atomic::{AtomicU32, Ordering}; + use std::time::Duration; + + use rand::rngs::StdRng; + use rand::{RngCore, SeedableRng}; + use test_context::futures::future::join; + use tokio::time::sleep; + + use crate::{util::test_utils::CacheDirTest, MockRemote}; + + use super::*; + + fn new_test_xc( + dir_prefix: &str, + block_size: u64, + capacity: u64, + mock_remote: Arc, + ) -> (CacheDirTest, XorbCacheImpl) { + let test_cache_dir = CacheDirTest::new(dir_prefix); + let disk_cache = DiskCache::from_config(test_cache_dir.get_path_str(), capacity).unwrap(); + let block_converter = BlockConverter::new(block_size); + let request_merger = singleflight::Group::new(); + + ( + test_cache_dir, + XorbCacheImpl::new(disk_cache, mock_remote, block_converter, request_merger), + ) + } + + #[tokio::test] + async fn test_fetch() { + let mut mock_remote = MockRemote::new(); + mock_remote + .expect_fetch() + .times(1) + .returning(|_, _| Ok(vec![13, 21, 7])); + let (_dir, test_xc) = new_test_xc("__tmp_xorb_fetch", 143, 457, Arc::new(mock_remote)); + + let test_key = Key::default(); + + let result = test_xc + .fetch_xorb_range(&test_key, 0..3, Some(10)) + .await + .unwrap(); + + assert_eq!(result.len(), 3); + assert_eq!(result, vec![13, 21, 7]); + + let result_none = test_xc + .fetch_xorb_range(&test_key, 0..3, Some(10)) + .await + .unwrap(); + assert_eq!(result, result_none); + } + + #[tokio::test] + async fn test_put_cache_blocks() { + let block_size: usize = 16; + // insert content of the indicated size into the cache and verify that + // the block(s) are stored correctly. + let test_put_content = |size: usize| async move { + let mut mock_remote = MockRemote::new(); + mock_remote + .expect_fetch() + .times(0) + .returning(|_, _| Err(anyhow::anyhow!("not expected to call remote"))); + let mut rng = StdRng::seed_from_u64(0); + let dir_prefix = format!("__tmp_xorb_put_{size}"); + let (_dir, test_xc) = new_test_xc( + &dir_prefix, + block_size as u64, + u64::MAX, + Arc::new(mock_remote), + ); + let mut data = vec![0u8; size]; + rng.fill_bytes(&mut data[..]); + let test_key = Key::default(); + test_xc.put_cache(&test_key, &data[..]).await.unwrap(); + + let retrieved_data = test_xc + .fetch_xorb_range(&test_key, 0..(size as u64), None) + .await + .unwrap(); + + if data != retrieved_data { + for i in 0..size { + assert_eq!(data[i], retrieved_data[i], "Data at index {i} mismatch:"); + } + } + }; + + // test various sizes of content + test_put_content(0).await; + test_put_content(3).await; + test_put_content(block_size).await; + test_put_content(block_size + 1).await; + test_put_content(block_size * 2).await; + } + + #[tokio::test] + async fn test_range_preconditions() { + let mut mock_remote = MockRemote::new(); + mock_remote.expect_fetch().times(0); + let (_dir, test_xc) = + new_test_xc("__tmp_xorb_preconditions", 143, 457, Arc::new(mock_remote)); + + let test_key = Key::default(); + + let result = test_xc + .fetch_xorb_range(&test_key, 4..4, Some(10)) + .await + .unwrap(); + assert!(result.is_empty()); + let result_none = test_xc + .fetch_xorb_range(&test_key, 4..4, None) + .await + .unwrap(); + assert_eq!(result, result_none); + } + + #[tokio::test] + #[allow(clippy::reversed_empty_ranges)] + async fn test_range_inverted_range() { + let mut mock_remote = MockRemote::new(); + mock_remote.expect_fetch().times(0); + let (_dir, test_xc) = new_test_xc("__tmp_xorb_inverted", 143, 457, Arc::new(mock_remote)); + + let test_key = Key::default(); + + let result = test_xc.fetch_xorb_range(&test_key, 7..3, Some(10)).await; + assert!(result.is_err()); + let result_none = test_xc.fetch_xorb_range(&test_key, 7..3, None).await; + assert!(result_none.is_err()); + } + + /// Mocks remote and keeps the number of times `fetch` is called. + #[derive(Debug, Default)] + struct FetchRecorder { + times_called: Arc, + } + + #[async_trait::async_trait] + impl Remote for FetchRecorder { + async fn fetch( + &self, + _key: &Key, + range: Range, + ) -> std::result::Result, anyhow::Error> { + sleep(Duration::new(0, 500_000_000)).await; + self.times_called.fetch_add(1, Ordering::SeqCst); + let mut vec = Vec::with_capacity((range.end - range.start) as usize); + for i in range { + vec.push(i as u8); + } + Ok(vec) + } + } + + #[tokio::test] + async fn test_read_simultaneous() { + let mock_remote = FetchRecorder::default(); + let times_called = mock_remote.times_called.clone(); + let (dir, test_xc) = + new_test_xc("__tmp_xorb_simultaneous", 143, 457, Arc::new(mock_remote)); + + let test_key = Key::default(); + + let r1 = 0..30; + let r2 = 30..47; + + let f1 = test_xc.fetch_xorb_range(&test_key, r1.clone(), Some(57)); + let f2 = test_xc.fetch_xorb_range(&test_key, r2.clone(), Some(57)); + let (res1, res2) = join(f1, f2).await; + assert_eq!(30, res1.unwrap().len()); + assert_eq!(17, res2.unwrap().len()); + assert_eq!(1, times_called.load(Ordering::SeqCst)); + assert_eq!(1, dir.get_entries().len()); + + let f1 = test_xc.fetch_xorb_range(&test_key, r1, None); + let f2 = test_xc.fetch_xorb_range(&test_key, r2, None); + let (res1, res2) = join(f1, f2).await; + assert_eq!(30, res1.unwrap().len()); + assert_eq!(17, res2.unwrap().len()); + assert_eq!(1, times_called.load(Ordering::SeqCst)); + assert_eq!(1, dir.get_entries().len()); + } +} diff --git a/cas_client/Cargo.toml b/cas_client/Cargo.toml new file mode 100644 index 00000000..3e7196fc --- /dev/null +++ b/cas_client/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "cas_client" +version = "0.14.5" +edition = "2021" + +[features] +strict = [] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +error_printer = {path = "../error_printer"} +utils = {path = "../utils"} +merkledb = {path = "../merkledb"} +merklehash = { path = "../merklehash" } +parutils = {path = "../parutils"} +retry_strategy = {path = "../retry_strategy"} +progress_reporting = {path = "../progress_reporting"} +tonic = {version = "0.10.2", features = ["tls", "tls-roots", "transport"] } +prost = "0.12.3" +tokio = { version = "1.36", features = ["full"] } +tokio-retry = "0.3.0" +tower = {version = "0.4"} +clap = "2.33" +async-trait = "0.1.9" +anyhow = "1" +http = "0.2.5" +xet_error = {path = "../xet_error"} +tempfile = "3" +cache = {path = "../cache"} +deadpool = {version = "0.9.4", features = ["managed", "rt_tokio_1"] } +futures = {version = "0.3", default-features = false, features = ["alloc"]} +tracing = "0.1.31" +bincode = "1.3.3" +uuid = {version = "1", features = ["v4", "fast-rng"]} +lazy_static = "1.4.0" +# trace-propagation +opentelemetry = { version = "0.17", features = ["trace", "rt-tokio"] } +opentelemetry-jaeger = { version = "0.16", features = ["rt-tokio"] } +opentelemetry-http = "0.6.0" +tracing-opentelemetry = "0.17.2" +serde_json = "1.0" +hyper = { version = "1.1.0", features = ["client", "http2"] } +hyper-util = { version = "0.1.2", features = ["http2", "tokio", "client"] } +http-body-util = "0.1.0" +tokio-native-tls = "0.3.1" +bytes = "1" +itertools = "0.10" +tokio-rustls = "0.25.0" +rustls-pemfile = "2.0.0" +hyper-rustls = { version = "0.26.0", features = ["http2"] } +lz4 = "1.24.0" + +[dev-dependencies] +trait-set = "0.3.0" +lazy_static = "1.4.0" +tokio-stream = { version = "0.1", features = ["net"] } +rcgen = "0.12.0" diff --git a/cas_client/Dockerfile b/cas_client/Dockerfile new file mode 100644 index 00000000..a623d92b --- /dev/null +++ b/cas_client/Dockerfile @@ -0,0 +1,37 @@ +FROM rust:1.58 as builder + +RUN USER=root rustup component add rustfmt +RUN USER=root cargo new --bin cas_client + +WORKDIR ./cas_client +ADD ./utils ../utils +COPY ./cas_client/Cargo.toml ./Cargo.toml +RUN cargo build --release +RUN rm src/*.rs + +COPY ./cas_client . +RUN rm ./target/release/deps/cas_client* + +RUN cargo build --release + +FROM debian:buster-slim +ARG APP=/usr/src/app + +RUN apt-get update \ + && apt-get install -y ca-certificates tzdata \ + && rm -rf /var/lib/apt/lists/* + +ENV TZ=Etc/UTC \ + APP_USER=appuser + +RUN groupadd $APP_USER \ + && useradd -g $APP_USER $APP_USER \ + && mkdir -p ${APP} + +COPY --from=builder /cas_client/target/release/cas_client ${APP}/cas_client +RUN mkdir ${APP}/config + +RUN chown -R $APP_USER:$APP_USER ${APP} + +USER $APP_USER +WORKDIR ${APP} diff --git a/cas_client/README.md b/cas_client/README.md new file mode 100644 index 00000000..4d4f6195 --- /dev/null +++ b/cas_client/README.md @@ -0,0 +1,2 @@ +# CAS client +Utilities to wrap around the grpc client, provide load balancing etc. diff --git a/cas_client/src/caching_client.rs b/cas_client/src/caching_client.rs new file mode 100644 index 00000000..2e0e71cb --- /dev/null +++ b/cas_client/src/caching_client.rs @@ -0,0 +1,328 @@ +use crate::error::Result; +use crate::interface::Client; +use crate::{client_adapter::ClientRemoteAdapter, error::CasClientError}; +use async_trait::async_trait; +use cache::{Remote, XorbCache}; +use cas::key::Key; +use error_printer::ErrorPrinter; +use merklehash::MerkleHash; +use std::collections::HashMap; +use std::fmt::Debug; +use std::ops::Range; +use std::path::Path; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{debug, info}; + +pub const DEFAULT_BLOCK_SIZE: u64 = 16 * 1024 * 1024; + +#[derive(Debug)] +pub struct CachingClient { + client: Arc, + cache: Arc, + xorb_lengths: Arc>>, +} + +impl CachingClient { + /// Create a new caching client. + /// client: This is the client object used to satisfy requests + pub fn new( + client: T, + cache_path: &Path, + capacity_bytes: u64, + block_size: u64, + ) -> Result> { + // convert Path to String + let canonical_path = cache_path.canonicalize().map_err(|e| { + CasClientError::ConfigurationError(format!("Error specifying cache path: {e}")) + })?; + + let canonical_string_path = canonical_path.to_str().ok_or_else(|| { + CasClientError::ConfigurationError("Error parsing cache path to UTF-8 path.".to_owned()) + })?; + + let arcclient = Arc::new(client); + let client_remote_arc: Arc = + Arc::new(ClientRemoteAdapter::new(arcclient.clone())); + + let cache = cache::from_config::( + cache::CacheConfig { + cache_dir: canonical_string_path.to_string(), + capacity: capacity_bytes, + block_size, + }, + client_remote_arc, + )?; + + info!( + "Creating CachingClient, path={:?}, byte capacity={}, blocksize={:?}", + cache_path, capacity_bytes, block_size + ); + + Ok(CachingClient { + client: arcclient, + cache, + xorb_lengths: Arc::new(Mutex::new(HashMap::new())), + }) + } +} + +#[async_trait] +impl Client for CachingClient { + async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()> { + // puts write through + debug!( + "CachingClient put to {}/{} of length {} bytes", + prefix, + hash, + data.len() + ); + Ok(self + .client + .put(prefix, hash, data, chunk_boundaries) + .await?) + } + + async fn flush(&self) -> Result<()> { + // forward flush to the underlying client + Ok(self.client.flush().await?) + } + + async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result> { + // get the length, reduce to range read of the entire length. + debug!("CachingClient Get of {}/{}", prefix, hash); + let xorb_size = self + .get_length(prefix, hash) + .await + .debug_error("CachingClient Get: get_length reported error")?; + + debug!("CachingClient Get: get_length call succeeded with value {xorb_size}."); + + self.get_object_range(prefix, hash, vec![(0, xorb_size)]) + .await + .map(|mut v| v.swap_remove(0)) + } + + async fn get_object_range( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>> { + debug!( + "CachingClient GetObjectRange of {}/{}: {:?}", + prefix, hash, ranges + ); + let mut ret: Vec> = Vec::new(); + for (start, end) in ranges { + let prefix_str = prefix.to_string(); + ret.push( + self.cache + .fetch_xorb_range( + &Key { + prefix: prefix_str, + hash: *hash, + }, + Range { start, end }, + None, + ) + .await + .warn_error(format!( + "CachingClient Error on GetObjectRange of {}/{}", + prefix, hash + ))?, + ) + } + Ok(ret) + } + + async fn get_length(&self, prefix: &str, hash: &MerkleHash) -> Result { + debug!("CachingClient GetLength of {}/{}", prefix, hash); + { + // check the xorb length cache + let xorb_lengths = self.xorb_lengths.lock().await; + if let Some(l) = xorb_lengths.get(hash) { + return Ok(*l); + } + // release lock here since get_length may take a while + } + let ret = self.client.get_length(prefix, hash).await; + + if let Ok(l) = ret { + // insert it into the xorb length cache + let mut xorb_lengths = self.xorb_lengths.lock().await; + xorb_lengths.insert(*hash, l); + } + ret + } +} + +#[cfg(test)] +mod tests { + use super::DEFAULT_BLOCK_SIZE; + use crate::*; + use std::fs; + use std::path::Path; + use std::sync::Arc; + use tempfile::TempDir; + + fn path_has_files(path: &Path) -> bool { + fs::read_dir(path).unwrap().count() > 0 + } + + #[tokio::test] + async fn test_basic_read_write() { + let client = Arc::new(LocalClient::default()); + let cachedir = TempDir::new().unwrap(); + assert!(!path_has_files(cachedir.path())); + + let client = CachingClient::new(client, cachedir.path(), 100, DEFAULT_BLOCK_SIZE).unwrap(); + + // the root hash of a single chunk is just the hash of the data + let hello = "hello world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + // write "hello world" + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + + // get length "hello world" + assert_eq!(11, client.get_length("key", &hello_hash).await.unwrap()); + + // read "hello world" + assert_eq!(hello, client.get("key", &hello_hash).await.unwrap()); + + // read range "hello" and "world" + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 11)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + // read range "hello" and "world", with truncation for larger offsets + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 20)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + // empty read + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 6)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + assert!(path_has_files(cachedir.path())); + } + + #[tokio::test] + async fn test_failures() { + let client = Arc::new(LocalClient::default()); + let cachedir = TempDir::new().unwrap(); + assert!(!path_has_files(cachedir.path())); + + let client = CachingClient::new(client, cachedir.path(), 100, DEFAULT_BLOCK_SIZE).unwrap(); + + let hello = "hello world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + // write "hello world" + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + // put the same value a second time. This should be ok. + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + + // put the different value with the same hash + // this should fail + assert_eq!( + CasClientError::HashMismatch, + client + .put( + "key", + &hello_hash, + "hellp world".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + // content shorter than the chunk boundaries should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put( + "key", + &hello_hash, + "hellp wod".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + + // content longer than the chunk boundaries should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put( + "key", + &hello_hash, + "hello world again".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + + // empty writes should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put("key", &hello_hash, vec![], vec![],) + .await + .unwrap_err() + ); + + // compute a hash of something we do not have in the store + let world = "world".as_bytes().to_vec(); + let world_hash = merklehash::compute_data_hash(&world[..]); + + // get length of non-existant object should fail with XORBNotFound + assert_eq!( + CasClientError::XORBNotFound(world_hash), + client.get_length("key", &world_hash).await.unwrap_err() + ); + + // read of non-existant object should fail with XORBNotFound + assert_eq!( + CasClientError::XORBNotFound(world_hash), + client.get("key", &world_hash).await.unwrap_err() + ); + // read range of non-existant object should fail with XORBNotFound + assert!(client + .get_object_range("key", &world_hash, vec![(0, 5)]) + .await + .is_err()); + } +} diff --git a/cas_client/src/cas_connection_pool.rs b/cas_client/src/cas_connection_pool.rs new file mode 100644 index 00000000..0d080fd2 --- /dev/null +++ b/cas_client/src/cas_connection_pool.rs @@ -0,0 +1,454 @@ +use crate::{error::Result, CasClientError}; +use async_trait::async_trait; +use deadpool::{ + managed::{self, Object, PoolConfig, PoolError, Timeouts}, + Runtime, +}; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; +use std::{collections::HashMap, marker::PhantomData}; +use tokio::sync::RwLock; +use tokio_retry::strategy::{jitter, ExponentialBackoff}; +use tokio_retry::RetryIf; +use tracing::{debug, error, info}; +use xet_error::Error; + +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum CasConnectionPoolError { + #[error("Invalid Range Read")] + InvalidRange, + #[error("Locking primitives error")] + LockCannotBeAcquired, + #[error("Connection pool acquisition error: {0}")] + ConnectionPoolAcquisition(String), + #[error("Connection pool creation error: {0}")] + ConnectionPoolCreation(String), +} + +// Magic number for the number of concurrent connections we +// want to support. +const POOL_SIZE: usize = 16; + +const CONNECTION_RETRIES_ON_TIMEOUT: usize = 3; +const CONNECT_TIMEOUT_MS: u64 = 20000; +const CONNECTION_RETRY_BACKOFF_MS: u64 = 10; +const ASYNC_RUNTIME: Runtime = Runtime::Tokio1; + +/// Container for information required to set up and handle +/// CAS connections (both gRPC and H2) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CasConnectionConfig { + // this endpoint contains the scheme info (http/https) + // ideally we'd have the scheme separately. + pub endpoint: String, + pub user_id: String, + pub auth: String, + pub repo_paths: String, + pub git_xet_version: String, + pub root_ca: Option>, +} + +impl CasConnectionConfig { + /// creates a new CasConnectionConfig with given endpoint and user_id + pub fn new( + endpoint: String, + user_id: String, + auth: String, + repo_paths: Vec, + git_xet_version: String, + ) -> CasConnectionConfig { + CasConnectionConfig { + endpoint, + user_id, + auth, + repo_paths: serde_json::to_string(&repo_paths).unwrap_or_else(|_| "[]".to_string()), + git_xet_version, + root_ca: None, + } + } + + pub fn with_root_ca>(mut self, root_ca: T) -> Self { + self.root_ca = Some(Arc::new(root_ca.into())); + self + } +} + +/// to be impl'ed by Connection types (DataTransport, GrpcClient)so that +/// connection pool managers could instantiate them using CasConnectionConfig +#[async_trait] +pub trait FromConnectionConfig: Sized { + async fn new_from_connection_config(config: CasConnectionConfig) -> Result; +} + +#[derive(Debug)] +pub struct PoolManager +where + T: ?Sized, +{ + connection_type: PhantomData, + cas_connection_config: CasConnectionConfig, +} + +#[async_trait] +impl managed::Manager for PoolManager +where + T: FromConnectionConfig + Sync + Send, +{ + type Type = T; + type Error = CasClientError; + + async fn create(&self) -> std::result::Result { + // Currently recreating the GrpcClient itself. In my limited testing, + // this gets slightly better overall performance than cloning the prototype. + Ok(T::new_from_connection_config(self.cas_connection_config.clone()).await?) + } + + async fn recycle(&self, _conn: &mut Self::Type) -> managed::RecycleResult { + Ok(()) + } +} + +// A mapping between IP address and connection pool. Each IP maps to a +// CAS instance, and we keep a fixed pool with the data plane connections. +#[derive(Debug)] +pub struct ConnectionPoolMap +where + T: FromConnectionConfig + Send + Sync, +{ + pool_map: RwLock>>>>, + max_pool_size: usize, +} + +impl ConnectionPoolMap +where + T: FromConnectionConfig + Send + Sync, +{ + pub fn new() -> Self { + ConnectionPoolMap { + pool_map: RwLock::new(HashMap::default()), + max_pool_size: POOL_SIZE, + } + } + + pub fn new_with_pool_size(max_pool_size: usize) -> Self { + ConnectionPoolMap { + pool_map: RwLock::new(HashMap::default()), + max_pool_size, + } + } + + // Creates a connection pool for the given IP address. + async fn create_pool_for_endpoint_impl( + cas_connection_config: CasConnectionConfig, + max_pool_size: usize, + ) -> std::result::Result>, CasConnectionPoolError> { + let endpoint = cas_connection_config.endpoint.clone(); + + let mgr = PoolManager { + connection_type: PhantomData, + cas_connection_config, + }; + + info!("Creating pool for {endpoint}"); + + let pool = managed::Pool::>::builder(mgr) + .config(PoolConfig { + max_size: max_pool_size, + timeouts: Timeouts { + create: Some(Duration::from_millis(CONNECT_TIMEOUT_MS)), + wait: None, + recycle: Some(Duration::from_millis(0)), + }, + }) + .runtime(ASYNC_RUNTIME) + .build() + .map_err(|e| { + error!( + "Error creating connection pool: {:?} server: {}", + e, endpoint + ); + CasConnectionPoolError::ConnectionPoolCreation(format!("{e:?}")) + })?; + + Ok(pool) + } + + // // Gets a connection object for the given endpoint. This will + // // create a connection pool for the endpoint if none exists already. + pub async fn get_connection_for_config( + &self, + cas_connection_config: CasConnectionConfig, + ) -> std::result::Result>, CasConnectionPoolError> { + let strategy = ExponentialBackoff::from_millis(CONNECTION_RETRY_BACKOFF_MS) + .map(jitter) + .take(CONNECTION_RETRIES_ON_TIMEOUT); + + let endpoint = cas_connection_config.endpoint.clone(); + let pool = self.get_pool_for_config(cas_connection_config).await?; + let result = RetryIf::spawn( + strategy, + || async { + debug!("Trying to get connection for endpoint: {}", endpoint); + pool.get().await + }, + is_pool_connection_error_retriable, + ) + .await + .map_err(|e| { + error!( + "Error acquiring connection for {:?} from pool: {:?} after {} retries", + endpoint, e, CONNECTION_RETRIES_ON_TIMEOUT + ); + CasConnectionPoolError::ConnectionPoolCreation(format!("{e:?}")) + })?; + + Ok(result) + } + + // Utility function to get a connection pool for the given endpoint. If none already + // exists, it will create it and insert it into the map. + async fn get_pool_for_config( + &self, + cas_connection_config: CasConnectionConfig, + ) -> std::result::Result>>, CasConnectionPoolError> { + debug!("Using connection pool"); + + // handle the typical case up front where we are connecting to an + // endpoint that already has its pool initialized. Scopes are meant + // to keep the + { + let now = Instant::now(); + debug!("Acquiring connection map read lock"); + let map = self.pool_map.read().await; + debug!( + "Connection map read lock acquired in {} ms", + now.elapsed().as_millis() + ); + + if let Some(pool) = map.get(cas_connection_config.endpoint.as_str()) { + return Ok(pool.clone()); + }; + } + + let endpoint = cas_connection_config.endpoint.clone(); + // If the connection isn't in the map, create it and insert. + // At worst, we'll briefly have multiple pools overwriting the hashmap, but this + // is needed so as not to carry the lock across an await + let new_pool = Arc::new( + Self::create_pool_for_endpoint_impl(cas_connection_config, self.max_pool_size).await?, + ); + { + let now = Instant::now(); + debug!("Acquiring connection map write lock"); + let mut map = self.pool_map.write().await; + + debug!( + "Connection map write lock acquired in {} ms", + now.elapsed().as_millis() + ); + + map.insert(endpoint, new_pool.clone()); + } + Ok(new_pool) + } + + // Utility to see how many connections are avialable for the IP address. + // This currently requires a lock, so it's not a good idea to use this for + // testing if you should get the lock. + pub async fn get_pool_status_for_endpoint( + &self, + ip_address: String, + ) -> Option { + let map = self.pool_map.read().await; + + map.get(&ip_address).map(|s| s.status()) + } +} + +fn is_pool_connection_error_retriable(err: &PoolError) -> bool { + matches!(err, PoolError::Timeout(_)) +} + +impl Default for ConnectionPoolMap +where + T: FromConnectionConfig + Send + Sync, +{ + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use crate::cas_connection_pool::{ConnectionPoolMap, FromConnectionConfig}; + use crate::error::Result; + use async_trait::async_trait; + + use super::CasConnectionConfig; + + const USER_ID: &str = "XET USER"; + const AUTH: &str = "XET SECRET"; + static REPO_PATHS: &str = "/XET/REPO"; + const GIT_XET_VERSION: &str = "0.1.0"; + + #[derive(Debug, Clone)] + struct PoolTestData { + cas_connection_config: CasConnectionConfig, + } + + #[async_trait] + impl FromConnectionConfig for PoolTestData { + async fn new_from_connection_config( + cas_connection_config: CasConnectionConfig, + ) -> Result { + Ok(PoolTestData { + cas_connection_config, + }) + } + } + + #[tokio::test] + async fn test_get_creates_pool() { + let cas_connection_pool = ConnectionPoolMap::::new(); + + let server1 = "foo".to_string(); + let server2 = "bar".to_string(); + + let config1 = CasConnectionConfig::new( + server1.clone(), + USER_ID.to_string(), + AUTH.to_string(), + vec![REPO_PATHS.to_string()], + GIT_XET_VERSION.to_string(), + ); + let config2 = CasConnectionConfig::new( + server2.clone(), + USER_ID.to_string(), + AUTH.to_string(), + vec![REPO_PATHS.to_string()], + GIT_XET_VERSION.to_string(), + ); + + let conn0 = cas_connection_pool + .get_connection_for_config(config1.clone()) + .await + .unwrap(); + assert_eq!(conn0.cas_connection_config, config1.clone()); + let stat = cas_connection_pool + .get_pool_status_for_endpoint(server1.clone()) + .await + .unwrap(); + assert_eq!(stat.size, 1); + assert_eq!(stat.available, 0); + + let conn1 = cas_connection_pool + .get_connection_for_config(config1.clone()) + .await + .unwrap(); + assert_eq!(conn1.cas_connection_config, config1.clone()); + + let stat = cas_connection_pool + .get_pool_status_for_endpoint(server1.clone()) + .await + .unwrap(); + assert_eq!(stat.size, 2); + assert_eq!(stat.available, 0); + + let conn2 = cas_connection_pool + .get_connection_for_config(config1.clone()) + .await + .unwrap(); + assert_eq!(conn2.cas_connection_config, config1.clone()); + + let stat = cas_connection_pool + .get_pool_status_for_endpoint(server1.clone()) + .await + .unwrap(); + assert_eq!(stat.size, 3); + assert_eq!(stat.available, 0); + + let conn3 = cas_connection_pool + .get_connection_for_config(config1.clone()) + .await + .unwrap(); + assert_eq!(conn3.cas_connection_config, config1.clone()); + + let stat = cas_connection_pool + .get_pool_status_for_endpoint(server1.clone()) + .await + .unwrap(); + assert_eq!(stat.size, 4); + assert_eq!(stat.available, 0); + + drop(conn0); + + let stat = cas_connection_pool + .get_pool_status_for_endpoint(server1.clone()) + .await + .unwrap(); + + println!("{}, {}", stat.size, stat.available); + assert_eq!(stat.size, 4); + assert_eq!(stat.available, 1); + + drop(conn1); + + let stat = cas_connection_pool + .get_pool_status_for_endpoint(server1.clone()) + .await + .unwrap(); + + println!("{}, {}", stat.size, stat.available); + assert_eq!(stat.size, 4); + assert_eq!(stat.available, 2); + + // ensure there's no cross pollination between different server strings + let conn0 = cas_connection_pool + .get_connection_for_config(config2.clone()) + .await + .unwrap(); + assert_eq!(conn0.cas_connection_config, config2.clone()); + let stat = cas_connection_pool + .get_pool_status_for_endpoint(server2.clone()) + .await + .unwrap(); + assert_eq!(stat.size, 1); + assert_eq!(stat.available, 0); + + // TODO: Add more tests here + } + + #[tokio::test] + async fn test_repo_name_encoding() { + let data: Vec> = vec![ + vec!["user1/repo-😀".to_string(), "user1/répô_123".to_string()], + vec![ + "user2/👾_repo".to_string(), + "user2/üникод".to_string(), + "user2/foobar!@#".to_string(), + ], + vec!["user3/sømè_repo".to_string(), "user3/你好-世界".to_string()], + vec!["user4/✨🌈repo".to_string()], + vec!["user5/Ω≈ç√repo".to_string()], + vec!["user6/42°_repo".to_string()], + vec![ + "user7/äëïöü_repo".to_string(), + "user7/ĀāĒēĪīŌōŪū".to_string(), + ], + ]; + for inner_vec in data { + let config = CasConnectionConfig::new( + "".to_string(), + "".to_string(), + "".to_string(), + inner_vec.clone(), + "".to_string(), + ); + let vec_of_strings: Vec = serde_json::from_str(config.repo_paths.as_ref()) + .expect("Failed to deserialize JSON"); + assert_eq!(vec_of_strings.clone(), inner_vec.clone()); + } + } +} diff --git a/cas_client/src/client_adapter.rs b/cas_client/src/client_adapter.rs new file mode 100644 index 00000000..e1731e50 --- /dev/null +++ b/cas_client/src/client_adapter.rs @@ -0,0 +1,33 @@ +use crate::interface::Client; +use async_trait::async_trait; +use cache::Remote; +use cas::key::Key; +use std::fmt::Debug; +use std::ops::Range; + +#[derive(Debug)] +pub struct ClientRemoteAdapter { + client: T, +} +impl ClientRemoteAdapter { + pub fn new(client: T) -> ClientRemoteAdapter { + ClientRemoteAdapter { client } + } +} + +#[async_trait] +impl Remote for ClientRemoteAdapter { + /// Fetches the provided range from the backing storage, returning the contents + /// if they are present. + async fn fetch( + &self, + key: &Key, + range: Range, + ) -> std::result::Result, anyhow::Error> { + Ok(self + .client + .get_object_range(&key.prefix, &key.hash, vec![(range.start, range.end)]) + .await + .map(|mut v| v.swap_remove(0))?) + } +} diff --git a/cas_client/src/data_transport.rs b/cas_client/src/data_transport.rs new file mode 100644 index 00000000..edf26b59 --- /dev/null +++ b/cas_client/src/data_transport.rs @@ -0,0 +1,591 @@ +use cas::constants::*; +use std::str::FromStr; +use std::time::Duration; + +use crate::{ + cas_connection_pool::CasConnectionConfig, + grpc::{get_request_id, trace_forwarding}, + remote_client::CAS_PROTOCOL_VERSION, +}; +use anyhow::{anyhow, Result}; +use cas::common::CompressionScheme; +use cas::compression::{ + multiple_accepted_encoding_header_value, CAS_ACCEPT_ENCODING_HEADER, + CAS_CONTENT_ENCODING_HEADER, CAS_INFLATED_SIZE_HEADER, +}; +use error_printer::ErrorPrinter; +use http_body_util::{BodyExt, Full}; +use hyper::body::Bytes; +use hyper::{ + header::RANGE, + header::{HeaderMap, HeaderName, HeaderValue}, + Method, Request, Response, Version, +}; +use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use hyper_util::client::legacy::connect::HttpConnector; +use hyper_util::client::legacy::Client; +use hyper_util::rt::{TokioExecutor, TokioTimer}; +use lazy_static::lazy_static; +use lz4::block::CompressionMode; +use opentelemetry::propagation::{Injector, TextMapPropagator}; +use retry_strategy::RetryStrategy; +use rustls_pemfile::Item; +use tokio_rustls::rustls; +use tokio_rustls::rustls::pki_types::CertificateDer; +use tracing::{debug, error, info_span, warn, Instrument, Span}; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use xet_error::Error; + +use merklehash::MerkleHash; + +const HTTP2_POOL_IDLE_TIMEOUT_SECS: u64 = 30; +const HTTP2_KEEPALIVE_MILLIS: u64 = 500; +const HTTP2_WINDOW_SIZE: u32 = 2147418112; +const NUM_RETRIES: usize = 5; +const BASE_RETRY_DELAY_MS: u64 = 3000; + +lazy_static! { + static ref ACCEPTED_ENCODINGS_HEADER_VALUE: HeaderValue = HeaderValue::from_str( + multiple_accepted_encoding_header_value(vec![ + CompressionScheme::Lz4, + CompressionScheme::None + ]) + .as_str() + ) + .unwrap_or_else(|_| HeaderValue::from_static("")); +} + +pub struct DataTransport { + http2_client: Client, Full>, + retry_strategy: RetryStrategy, + cas_connection_config: CasConnectionConfig, +} + +impl std::fmt::Debug for DataTransport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DataTransport") + .field("authority", &self.authority()) + .finish() + } +} + +/// This struct is used to wrap the error types which we may +/// retry on. Request Errors (which are triggered if there was a +/// header error building the request) are not retryable. +/// Right now this retries every h2 error. Reading these: +/// - https://docs.rs/h2/latest/h2/struct.Error.html, +/// - https://docs.rs/h2/latest/h2/struct.Reason.html +/// unclear if there is any reason not to retry. +#[derive(Error, Debug)] +enum RetryError { + #[error("{0}")] + Hyper(#[from] hyper::Error), + + #[error("{0}")] + HyperLegacy(#[from] hyper_util::client::legacy::Error), + + #[error("Request Error: {0}")] + Request(#[from] anyhow::Error), + + /// Should only be used for non-success errors + #[error("Status Error: {0}")] + Status(hyper::StatusCode), +} +fn is_status_retriable(err: &RetryError) -> bool { + match err { + RetryError::Hyper(_) => true, + RetryError::HyperLegacy(_) => true, + RetryError::Request(_) => false, + RetryError::Status(n) => retry_http_status_code(n), + } +} +fn retry_http_status_code(stat: &hyper::StatusCode) -> bool { + stat.is_server_error() || *stat == hyper::StatusCode::TOO_MANY_REQUESTS +} + +fn is_status_retriable_and_print(err: &RetryError) -> bool { + let ret = is_status_retriable(err); + if ret { + debug!("{}. Retrying...", err); + } + ret +} +fn print_final_retry_error(err: RetryError) -> RetryError { + if is_status_retriable(&err) { + warn!("Many failures {}", err); + } + err +} + +impl DataTransport { + pub fn new( + http2_client: Client, Full>, + retry_strategy: RetryStrategy, + cas_connection_config: CasConnectionConfig, + ) -> Self { + Self { + http2_client, + retry_strategy, + cas_connection_config, + } + } + + /// creates the DataTransport instance for the H2 connection using + /// CasConnectionConfig info, and additional port + pub async fn from_config(cas_connection_config: CasConnectionConfig) -> Result { + debug!( + "Attempting to make HTTP connection with {}", + cas_connection_config.endpoint + ); + let mut builder = Client::builder(TokioExecutor::new()); + builder + .timer(TokioTimer::new()) + .pool_idle_timeout(Duration::from_secs(HTTP2_POOL_IDLE_TIMEOUT_SECS)) + .http2_keep_alive_interval(Duration::from_millis(HTTP2_KEEPALIVE_MILLIS)) + .http2_initial_connection_window_size(HTTP2_WINDOW_SIZE) + .http2_initial_stream_window_size(HTTP2_WINDOW_SIZE) + .http2_only(true); + let root_ca = cas_connection_config + .root_ca + .clone() + .ok_or_else(|| anyhow!("missing server certificate"))?; + let cert = try_from_pem(root_ca.as_bytes())?; + let mut root_store = rustls::RootCertStore::empty(); + root_store.add(cert)?; + let config = rustls::ClientConfig::builder() + // add the CAS certificate to the client's root store + // client does not need to assume identity for authentication + .with_root_certificates(root_store) + .with_no_client_auth(); + + let connector = HttpsConnectorBuilder::new() + .with_tls_config(config) + .https_only() + .enable_http2() + .build(); + let h2_client = builder.build(connector); + let retry_strategy = RetryStrategy::new(NUM_RETRIES, BASE_RETRY_DELAY_MS); + Ok(Self::new(h2_client, retry_strategy, cas_connection_config)) + } + + fn authority(&self) -> &str { + self.cas_connection_config.endpoint.as_str() + } + + fn get_uri(&self, prefix: &str, hash: &MerkleHash) -> String { + let cas_key_string = cas::key::Key { + prefix: prefix.to_string(), + hash: *hash, + } + .to_string(); + if cas_key_string.starts_with('/') { + format!("{}{}", self.authority(), cas_key_string) + } else { + format!("{}/{}", self.authority(), cas_key_string) + } + } + + fn setup_request( + &self, + method: Method, + prefix: &str, + hash: &MerkleHash, + body: Option>, + ) -> Result>> { + let dest = self.get_uri(prefix, hash); + debug!("Calling {} with address: {}", method, dest); + let user_id = self.cas_connection_config.user_id.clone(); + let auth = self.cas_connection_config.auth.clone(); + let request_id = get_request_id(); + let repo_paths = self.cas_connection_config.repo_paths.clone(); + let git_xet_version = self.cas_connection_config.git_xet_version.clone(); + let cas_protocol_version = CAS_PROTOCOL_VERSION.clone(); + + let mut req = Request::builder() + .method(method.clone()) + .header(USER_ID_HEADER, user_id) + .header(AUTH_HEADER, auth) + .header(REQUEST_ID_HEADER, request_id) + .header(REPO_PATHS_HEADER, repo_paths) + .header(GIT_XET_VERSION_HEADER, git_xet_version) + .header(CAS_PROTOCOL_VERSION_HEADER, cas_protocol_version) + .uri(&dest) + .version(Version::HTTP_2); + + if method == Method::GET { + req = req.header( + CAS_ACCEPT_ENCODING_HEADER, + ACCEPTED_ENCODINGS_HEADER_VALUE.clone(), + ); + } + + if trace_forwarding() { + if let Some(headers) = req.headers_mut() { + let mut injector = HeaderInjector(headers); + let propagator = opentelemetry_jaeger::Propagator::new(); + let cur_span = Span::current(); + let ctx = cur_span.context(); + propagator.inject_context(&ctx, &mut injector); + } + } + let bytes = match body { + None => Bytes::new(), + Some(data) => Bytes::from(data), + }; + req.body(Full::new(bytes)).map_err(|e| anyhow!(e)) + } + + // Single get to the H2 server + pub async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result> { + let resp = self + .retry_strategy + .retry( + || async { + let req = self + .setup_request(Method::GET, prefix, hash, None) + .map_err(RetryError::from)?; + + let resp = self + .http2_client + .request(req) + .instrument(info_span!("transport.h2_get")) + .await + .map_err(|e| { + error!("{e}"); + RetryError::from(e) + })?; + + if retry_http_status_code(&resp.status()) { + return Err(RetryError::Status(resp.status())); + } + Ok(resp) + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error)?; + let status = resp.status(); + if status != hyper::StatusCode::OK { + return Err(anyhow!( + "data get status {} received for URL {}", + status, + self.get_uri(prefix, hash) + )); + } + debug!("Received Response from HTTP2 GET: {}", status); + let (encoding, uncompressed_size) = + get_encoding_info(&resp).unwrap_or((CompressionScheme::None, None)); + // Get the body + let bytes = resp + .collect() + .instrument(info_span!("transport.read_body")) + .await? + .to_bytes() + .to_vec(); + let payload_size = bytes.len(); + let bytes = maybe_decode(bytes.as_slice(), encoding, uncompressed_size)?; + debug!( + "GET; encoding: ({}), uncompressed size: ({}), payload ({}) prefix: ({}), hash: ({})", + encoding.as_str_name(), + uncompressed_size.unwrap_or_default(), + payload_size, + prefix, + hash + ); + Ok(bytes) + } + + // Single get range to the H2 server + pub async fn get_range( + &self, + prefix: &str, + hash: &MerkleHash, + range: (u64, u64), + ) -> Result> { + let res = self + .retry_strategy + .retry( + || async { + let mut req = self + .setup_request(Method::GET, prefix, hash, None) + .map_err(RetryError::from)?; + let header_value = + HeaderValue::from_str(&format!("bytes={}-{}", range.0, range.1 - 1)) + .map_err(anyhow::Error::from) + .map_err(RetryError::from)?; + req.headers_mut().insert(RANGE, header_value); + + let resp = self + .http2_client + .request(req) + .instrument(info_span!("transport.h2_get_range")) + .await + .map_err(RetryError::from)?; + + if retry_http_status_code(&resp.status()) { + return Err(RetryError::Status(resp.status())); + } + + let status = resp.status(); + if status != hyper::StatusCode::OK { + return Err(RetryError::Request(anyhow!( + "data get_range status {} received for URL {} with range {:?}", + status, + self.get_uri(prefix, hash), + range + ))); + } + debug!("Received Response from HTTP2 GET range: {}", status); + let (encoding, uncompressed_size) = get_encoding_info(&resp).unwrap_or((CompressionScheme::None, None)); + // Get the body + let bytes: Vec = resp + .collect() + .instrument(info_span!("transport.read_body")) + .await? + .to_bytes() + .to_vec(); + let payload_size = bytes.len(); + let bytes = maybe_decode(bytes.as_slice(), encoding, uncompressed_size)?; + debug!("GET RANGE; encoding: ({}), uncompressed size: ({}), payload ({}) prefix: ({}), hash: ({})", encoding.as_str_name(), uncompressed_size.unwrap_or_default(), payload_size, prefix, hash); + Ok(bytes.to_vec()) + }, + is_status_retriable_and_print, + ) + .await; + + res.map_err(print_final_retry_error) + .map_err(anyhow::Error::from) + } + + // Single put to the H2 server + pub async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: &[u8], + encoding: CompressionScheme, + ) -> Result<()> { + let full_size = data.len(); + let data = maybe_encode(data, encoding)?; + debug!( + "PUT; encoding: ({}), uncompressed size: ({}), payload: ({}), prefix: ({}), hash: ({})", + encoding.as_str_name(), + full_size, + data.len(), + prefix, + hash + ); + let resp = self + .retry_strategy + .retry( + || async { + // compression of data to be done here, for now none. + let mut req = self + .setup_request(Method::POST, prefix, hash, Some(data.clone())) + .map_err(RetryError::from)?; + let headers = req.headers_mut(); + headers.insert(CAS_INFLATED_SIZE_HEADER, HeaderValue::from(full_size)); + headers.insert( + CAS_CONTENT_ENCODING_HEADER, + HeaderValue::from_static(encoding.into()), + ); + + let resp = self + .http2_client + .request(req) + .instrument(info_span!("transport.h2_put")) + .await + .map_err(RetryError::from)?; + + if retry_http_status_code(&resp.status()) { + return Err(RetryError::Status(resp.status())); + } + Ok(resp) + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error)?; + let status = resp.status(); + if status != hyper::StatusCode::OK { + return Err(anyhow!( + "data put status {} received for URL {}", + status, + self.get_uri(prefix, hash), + )); + } + debug!("Received Response from HTTP2 POST: {}", status); + + Ok(()) + } +} + +fn maybe_decode<'a, T: Into<&'a [u8]>>( + bytes: T, + encoding: CompressionScheme, + uncompressed_size: Option, +) -> Result> { + if let CompressionScheme::Lz4 = encoding { + if uncompressed_size.is_none() { + return Err(anyhow!( + "Missing uncompressed size when attempting to decompress LZ4" + )); + } + return lz4::block::decompress(bytes.into(), uncompressed_size).map_err(|e| anyhow!(e)); + } + Ok(bytes.into().to_vec()) +} + +fn get_encoding_info(response: &Response) -> Option<(CompressionScheme, Option)> { + let headers = response.headers(); + let value = headers.get(CAS_CONTENT_ENCODING_HEADER)?; + let as_str = value.to_str().ok()?; + let compression_scheme = CompressionScheme::from_str(as_str).ok()?; + + let value = headers.get(CAS_INFLATED_SIZE_HEADER)?; + let as_str = value.to_str().ok()?; + let uncompressed_size: Option = as_str.parse().ok(); + Some((compression_scheme, uncompressed_size)) +} + +fn maybe_encode<'a, T: Into<&'a [u8]>>(data: T, encoding: CompressionScheme) -> Result> { + if let CompressionScheme::Lz4 = encoding { + lz4::block::compress(data.into(), Some(CompressionMode::DEFAULT), false) + .log_error("LZ4 compression error") + .map_err(|e| anyhow!(e)) + } else { + // None + Ok(data.into().to_vec()) + } +} + +fn try_from_pem(pem: &[u8]) -> Result { + let (item, _) = rustls_pemfile::read_one_from_slice(pem) + .map_err(|e| { + error!("pem error: {e:?}"); + // rustls_pemfile::Error does not impl std::error::Error + anyhow!("rustls_pemfile error {e:?}") + })? + .ok_or_else(|| anyhow!("failed to parse pem"))?; + match item { + Item::X509Certificate(cert) => Ok(cert), + _ => Err(anyhow!("invalid cert format")), + } +} + +pub struct HeaderInjector<'a>(pub &'a mut HeaderMap); + +impl<'a> Injector for HeaderInjector<'a> { + /// Set a key and value in the HeaderMap. Does nothing if the key or value are not valid inputs. + fn set(&mut self, key: &str, value: String) { + if let Ok(key_header) = HeaderName::try_from(key) { + if let Ok(header_value) = HeaderValue::from_str(&value) { + self.0.insert(key_header, header_value); + } + } + } +} + +#[cfg(test)] +mod tests { + use lazy_static::lazy_static; + use std::vec; + + use super::*; + + // cert to use for testing + lazy_static! { + static ref CERT: rcgen::Certificate = rcgen::generate_simple_self_signed(vec![]).unwrap(); + } + + #[tokio::test] + async fn test_from_config() { + let endpoint = "http://localhost:443"; + let config = CasConnectionConfig { + endpoint: endpoint.to_string(), + user_id: "user".to_string(), + auth: "auth".to_string(), + repo_paths: "repo".to_string(), + git_xet_version: "0.1.0".to_string(), + root_ca: None, + } + .with_root_ca(CERT.serialize_pem().unwrap()); + let dt = DataTransport::from_config(config).await.unwrap(); + assert_eq!(dt.authority(), endpoint); + } + + #[tokio::test] + async fn repo_path_header_test() { + let data: Vec> = vec![ + vec!["user1/repo-😀".to_string(), "user1/répô_123".to_string()], + vec![ + "user2/👾_repo".to_string(), + "user2/üникод".to_string(), + "user2/foobar!@#".to_string(), + ], + vec!["user3/sømè_repo".to_string(), "user3/你好-世界".to_string()], + vec!["user4/✨🌈repo".to_string()], + vec!["user5/Ω≈ç√repo".to_string()], + vec!["user6/42°_repo".to_string()], + vec![ + "user7/äëïöü_repo".to_string(), + "user7/ĀāĒēĪīŌōŪū".to_string(), + ], + ]; + for inner_vec in data { + let config = CasConnectionConfig::new( + "".to_string(), + "".to_string(), + "".to_string(), + inner_vec.clone(), + "".to_string(), + ) + .with_root_ca(CERT.serialize_pem().unwrap()); + let client = DataTransport::from_config(config).await.unwrap(); + let hello = "hello".as_bytes().to_vec(); + let hash = merklehash::compute_data_hash(&hello[..]); + let req = client.setup_request(Method::GET, "", &hash, None).unwrap(); + let repo_paths = req.headers().get(REPO_PATHS_HEADER).unwrap(); + let repo_path_str = String::from_utf8(repo_paths.as_bytes().to_vec()).unwrap(); + let vec_of_strings: Vec = + serde_json::from_str(repo_path_str.as_str()).expect("Failed to deserialize JSON"); + assert_eq!(vec_of_strings, inner_vec); + } + } + + #[tokio::test] + async fn string_headers_test() { + let user_id = "XET USER"; + let auth = "XET AUTH"; + let git_xet_version = "0.1.0"; + + let cas_connection_config = CasConnectionConfig::new( + "".to_string(), + user_id.to_string(), + auth.to_string(), + vec![], + git_xet_version.to_string(), + ) + .with_root_ca(CERT.serialize_pem().unwrap()); + let client = DataTransport::from_config(cas_connection_config) + .await + .unwrap(); + let hash = merklehash::compute_data_hash("test".as_bytes()); + let req = client + .setup_request(Method::POST, "default", &hash, None) + .unwrap(); + let headers = req.headers(); + // gets header value assuming all well, panic if not + let get_header_value = |header: &str| headers.get(header).unwrap().to_str().unwrap(); + + // check against values in config + assert_eq!(get_header_value(GIT_XET_VERSION_HEADER), git_xet_version); + assert_eq!(get_header_value(USER_ID_HEADER), user_id); + + // check against global static + assert_eq!( + get_header_value(CAS_PROTOCOL_VERSION_HEADER), + CAS_PROTOCOL_VERSION.as_str() + ); + } +} diff --git a/cas_client/src/error.rs b/cas_client/src/error.rs new file mode 100644 index 00000000..e2fda59d --- /dev/null +++ b/cas_client/src/error.rs @@ -0,0 +1,74 @@ +use cache::CacheError; +use http::uri::InvalidUri; +use merklehash::MerkleHash; +use tonic::metadata::errors::InvalidMetadataValue; +use xet_error::Error; + +use crate::cas_connection_pool::CasConnectionPoolError; + +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum CasClientError { + #[error("Tonic RPC error.")] + TonicError, + + #[error("CAS Cache Error: {0}")] + CacheError(#[from] CacheError), + + #[error("Configuration Error: {0} ")] + ConfigurationError(String), + + #[error("URL Parsing Error.")] + URLError(#[from] InvalidUri), + + #[error("Tonic Trasport Error")] + TonicTransportError(#[from] tonic::transport::Error), + + #[error("Metadata error: {0}")] + MetadataParsingError(#[from] InvalidMetadataValue), + + #[error("CAS Connection Pool Error")] + CasConnectionPoolError(#[from] CasConnectionPoolError), + + #[error("Invalid Range Read")] + InvalidRange, + + #[error("Invalid Arguments")] + InvalidArguments, + + #[error("Hash Mismatch")] + HashMismatch, + + #[error("Other Internal Error: {0}")] + InternalError(anyhow::Error), + + #[error("CAS Hash not found")] + XORBNotFound(MerkleHash), + + #[error("Data transfer timeout")] + DataTransferTimeout, + + #[error("Client connection error {0}")] + Grpc(#[from] anyhow::Error), + + #[error("Batch Error: {0}")] + BatchError(String), + + #[error("Serialization Error: {0}")] + SerializationError(#[from] bincode::Error), + + #[error("Runtime Error (Temp files): {0}")] + RuntimeErrorTempFileError(#[from] tempfile::PersistError), +} + +// Define our own result type here (this seems to be the standard). +pub type Result = std::result::Result; + +impl PartialEq for CasClientError { + fn eq(&self, other: &CasClientError) -> bool { + match (self, other) { + (CasClientError::XORBNotFound(a), CasClientError::XORBNotFound(b)) => a == b, + (e1, e2) => std::mem::discriminant(e1) == std::mem::discriminant(e2), + } + } +} diff --git a/cas_client/src/grpc.rs b/cas_client/src/grpc.rs new file mode 100644 index 00000000..5815bda7 --- /dev/null +++ b/cas_client/src/grpc.rs @@ -0,0 +1,822 @@ +use crate::error::Result; +use std::env::VarError; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use crate::cas_connection_pool::CasConnectionConfig; +use crate::remote_client::CAS_PROTOCOL_VERSION; +use http::Uri; +use opentelemetry::propagation::{Injector, TextMapPropagator}; +use retry_strategy::RetryStrategy; +use tonic::codegen::InterceptedService; +use tonic::metadata::{Ascii, Binary, MetadataKey, MetadataMap, MetadataValue}; +use tonic::service::Interceptor; +use tonic::transport::{Certificate, ClientTlsConfig}; +use tonic::{transport::Channel, Code, Request, Status}; +use tracing::{debug, info, warn, Span}; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use uuid::Uuid; + +use cas::common::CompressionScheme; +use cas::{ + cas::{ + cas_client::CasClient, GetRangeRequest, GetRequest, HeadRequest, PutCompleteRequest, + PutRequest, Range, + }, + common::{EndpointConfig, InitiateRequest, InitiateResponse, Key, Scheme}, + constants::*, +}; +use merklehash::MerkleHash; + +use crate::CasClientError; +pub type CasClientType = CasClient>; + +const DEFAULT_H2_PORT: u16 = 443; +const DEFAULT_PUT_COMPLETE_PORT: u16 = 5000; + +const HTTP2_KEEPALIVE_TIMEOUT_SEC: u64 = 20; +const HTTP2_KEEPALIVE_INTERVAL_SEC: u64 = 1; +const NUM_RETRIES: usize = 5; +const BASE_RETRY_DELAY_MS: u64 = 3000; + +// production ready settings +const INITIATE_CAS_SCHEME: &str = "https"; +const HTTP_CAS_SCHEME: &str = "http"; + +lazy_static::lazy_static! { + static ref DEFAULT_UUID: Uuid = Uuid::new_v4(); + static ref REQUEST_COUNTER: AtomicUsize = AtomicUsize::new(0); + static ref TRACE_FORWARDING: AtomicBool = AtomicBool::new(false); +} + +async fn get_channel(endpoint: &str, root_ca: &Option>) -> Result { + debug!("server name: {}", endpoint); + let mut server_uri: Uri = endpoint + .parse() + .map_err(|e| CasClientError::ConfigurationError(format!("Error parsing endpoint: {e}.")))?; + + // supports an absolute URI (above) or just the host:port (below) + // only used on first endpoint, all other endpoints should come from CAS + // with scheme info already included + // in local/witt modes overridden CAS initial URI should include scheme e.g. + // http://localhost:40000 + if server_uri.scheme().is_none() { + let scheme = if cfg!(test) { + HTTP_CAS_SCHEME + } else { + INITIATE_CAS_SCHEME + }; + server_uri = format!("{scheme}://{endpoint}").parse().unwrap(); + } + + debug!("Connecting to URI: {}", server_uri); + + let mut builder = Channel::builder(server_uri); + if let Some(root_ca) = root_ca { + let tls_config = + ClientTlsConfig::new().ca_certificate(Certificate::from_pem(root_ca.as_str())); + builder = builder.tls_config(tls_config)?; + } + let channel = builder + .keep_alive_timeout(Duration::new(HTTP2_KEEPALIVE_TIMEOUT_SEC, 0)) + .http2_keep_alive_interval(Duration::new(HTTP2_KEEPALIVE_INTERVAL_SEC, 0)) + .timeout(Duration::new(GRPC_TIMEOUT_SEC, 0)) + .connect_timeout(Duration::new(GRPC_TIMEOUT_SEC, 0)) + .connect() + .await?; + Ok(channel) +} + +pub async fn get_client(cas_connection_config: CasConnectionConfig) -> Result { + let timeout_channel = get_channel( + cas_connection_config.endpoint.as_str(), + &cas_connection_config.root_ca, + ) + .await?; + + let client: CasClientType = CasClient::with_interceptor( + timeout_channel, + MetadataHeaderInterceptor::new(cas_connection_config), + ); + Ok(client) +} + +/// Adds common metadata headers to all requests. Currently, this includes +/// authorization and xet-user-id. +/// TODO: at some point, we should re-evaluate how we authenticate/authorize requests to CAS. +#[derive(Debug, Clone)] +pub struct MetadataHeaderInterceptor { + config: CasConnectionConfig, +} + +impl MetadataHeaderInterceptor { + fn new(config: CasConnectionConfig) -> MetadataHeaderInterceptor { + MetadataHeaderInterceptor { config } + } +} + +impl Interceptor for MetadataHeaderInterceptor { + // note original Interceptor trait accepts non-mut request + // but may accept mut request like in this case + fn call(&mut self, mut request: Request<()>) -> std::result::Result, Status> { + request.set_timeout(Duration::new(GRPC_TIMEOUT_SEC, 0)); + let metadata = request.metadata_mut(); + + // pass user_id and repo_paths received from xetconfig + let user_id = get_metadata_ascii_from_str_with_default(&self.config.user_id, DEFAULT_USER); + metadata.insert(USER_ID_HEADER, user_id); + let auth = get_metadata_ascii_from_str_with_default(&self.config.auth, DEFAULT_AUTH); + metadata.insert(AUTH_HEADER, auth); + + let repo_paths = get_repo_paths_metadata_value(&self.config.repo_paths); + metadata.insert_bin(REPO_PATHS_HEADER, repo_paths); + + let git_xet_version = + get_metadata_ascii_from_str_with_default(&self.config.git_xet_version, DEFAULT_VERSION); + metadata.insert(GIT_XET_VERSION_HEADER, git_xet_version); + + let cas_protocol_version: MetadataValue = + MetadataValue::from_static(CAS_PROTOCOL_VERSION.as_str()); + metadata.insert(CAS_PROTOCOL_VERSION_HEADER, cas_protocol_version); + + // propagate tracing context (e.g. trace_id, span_id) to service + if trace_forwarding() { + let mut injector = HeaderInjector(metadata); + let propagator = opentelemetry_jaeger::Propagator::new(); + let cur_span = Span::current(); + let ctx = cur_span.context(); + propagator.inject_context(&ctx, &mut injector); + } + + let request_id = get_request_id(); + metadata.insert( + REQUEST_ID_HEADER, + MetadataValue::from_str(&request_id) + .map_err(|e| Status::internal(format!("Metadata error: {e:?}")))?, + ); + + Ok(request) + } +} + +pub fn set_trace_forwarding(should_enable: bool) { + TRACE_FORWARDING.store(should_enable, Ordering::Relaxed); +} + +pub fn trace_forwarding() -> bool { + TRACE_FORWARDING.load(Ordering::Relaxed) +} + +pub struct HeaderInjector<'a>(pub &'a mut MetadataMap); + +impl<'a> Injector for HeaderInjector<'a> { + /// Set a key and value in the HeaderMap. Does nothing if the key or value are not valid inputs. + fn set(&mut self, key: &str, value: String) { + if let Ok(name) = MetadataKey::from_str(key) { + if let Ok(val) = MetadataValue::from_str(&value) { + self.0.insert(name, val); + } + } + } +} + +fn get_metadata_ascii_from_str_with_default( + value: &str, + default: &'static str, +) -> MetadataValue { + MetadataValue::from_str(value) + .map_err(|_| VarError::NotPresent) + .unwrap_or_else(|_| MetadataValue::from_static(default)) +} + +fn get_repo_paths_metadata_value(repo_paths: &str) -> MetadataValue { + MetadataValue::from_bytes(repo_paths.as_bytes()) +} + +pub fn get_request_id() -> String { + format!( + "{}.{}", + *DEFAULT_UUID, + REQUEST_COUNTER.load(Ordering::Relaxed) + ) +} + +fn inc_request_id() { + REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed); +} + +/// CAS Client that uses GRPC for communication. +/// +/// ## Implementation note +/// The GrpcClient is thread-safe and allows multiplexing requests on the +/// underlying gRPC connection. This is done by cheaply cloning the client: +/// https://docs.rs/tonic/0.1.0/tonic/transport/struct.Channel.html#multiplexing-requests +#[derive(Debug)] +pub struct GrpcClient { + pub endpoint: String, + client: CasClientType, + retry_strategy: RetryStrategy, +} + +impl Clone for GrpcClient { + fn clone(&self) -> Self { + GrpcClient { + endpoint: self.endpoint.clone(), + client: self.client.clone(), + retry_strategy: self.retry_strategy, + } + } +} + +impl GrpcClient { + pub fn new(endpoint: String, client: CasClientType, retry_strategy: RetryStrategy) -> Self { + Self { + endpoint, + client, + retry_strategy, + } + } + + pub async fn from_config(cas_connection_config: CasConnectionConfig) -> Result { + let endpoint = cas_connection_config.endpoint.clone(); + let client: CasClientType = get_client(cas_connection_config).await?; + // Retry policy: Exponential backoff starting at BASE_RETRY_DELAY_MS and retrying NUM_RETRIES times + let retry_strategy = RetryStrategy::new(NUM_RETRIES, BASE_RETRY_DELAY_MS); + Ok(GrpcClient::new(endpoint, client, retry_strategy)) + } +} + +pub fn is_status_retriable(err: &Status) -> bool { + match err.code() { + Code::Ok + | Code::Cancelled + | Code::InvalidArgument + | Code::NotFound + | Code::AlreadyExists + | Code::PermissionDenied + | Code::FailedPrecondition + | Code::OutOfRange + | Code::Unimplemented + | Code::Unauthenticated => false, + Code::Unknown + | Code::DeadlineExceeded + | Code::ResourceExhausted + | Code::Aborted + | Code::Internal + | Code::Unavailable + | Code::DataLoss => true, + } +} + +pub fn is_status_retriable_and_print(err: &Status) -> bool { + let ret = is_status_retriable(err); + if ret { + info!("GRPC Error {}. Retrying...", err); + } + ret +} + +pub fn print_final_retry_error(err: Status) -> Status { + if is_status_retriable(&err) { + warn!("Many failures {}", err); + } + err +} + +impl Drop for GrpcClient { + fn drop(&mut self) { + debug!("GrpcClient: Dropping GRPC Client."); + } +} + +// DTO for initiate rpc response info +pub struct EndpointsInfo { + pub data_plane_endpoint: EndpointConfig, + pub put_complete_endpoint: EndpointConfig, + pub accepted_encodings: Vec, +} + +impl GrpcClient { + #[tracing::instrument(skip_all, name = "cas.client", err, fields(prefix = prefix, hash = hash.hex().as_str(), api = "put", request_id = tracing::field::Empty))] + pub async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()> { + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + debug!( + "GrpcClient Req {}: put to {}/{} of length {} bytes", + get_request_id(), + prefix, + hash, + data.len(), + ); + let request = PutRequest { + key: Some(get_key_for_request(prefix, hash)), + data, + chunk_boundaries, + }; + + let response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + self.client.clone().put(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| { + info!( + "GrpcClient Req {}: Error on Put {}/{} : {:?}", + get_request_id(), + prefix, + hash, + e + ); + CasClientError::Grpc(anyhow::Error::from(e)) + })?; + + debug!( + "GrpcClient Req {}: put to {}/{} complete.", + get_request_id(), + prefix, + hash, + ); + + if !response.into_inner().was_inserted { + debug!( + "GrpcClient Req {}: XORB {}/{} not inserted; already present.", + get_request_id(), + prefix, + hash + ); + } + + Ok(()) + } + + /// on success returns a type of 2 EndpointConfig's + /// first the EndpointConfig for the h2 dataplane endpoint + /// second the EndpointConfig for the grpc endpoint used for put complete rpc + #[tracing::instrument(skip_all, name = "cas.client", err, fields(prefix = prefix, hash = hash.hex().as_str(), api = "initiate", request_id = tracing::field::Empty))] + pub async fn initiate( + &self, + prefix: &str, + hash: &MerkleHash, + payload_size: usize, + ) -> Result { + debug!( + "GrpcClient Req {}. initiate {}/{}, size={payload_size}", + get_request_id(), + prefix, + hash + ); + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + let request = InitiateRequest { + key: Some(get_key_for_request(prefix, hash)), + payload_size: payload_size as u64, + }; + + let response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + self.client.clone().initiate(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| CasClientError::Grpc(anyhow::Error::from(e)))?; + + let InitiateResponse { + data_plane_endpoint, + put_complete_endpoint, + cas_hostname, + accepted_encodings, + } = response.into_inner(); + + let accepted_encodings = accepted_encodings + .into_iter() + .filter_map(|i| CompressionScheme::try_from(i).ok()) + .collect(); + + if data_plane_endpoint.is_none() || put_complete_endpoint.is_none() { + info!("CAS initiate response indicates cas protocol version < v0.2.0, defaulting to v0.1.0 config"); + // default case, relevant for using CAS until prod is synced with v0.2.0 + return Ok(EndpointsInfo { + data_plane_endpoint: EndpointConfig { + host: cas_hostname.clone(), + port: DEFAULT_H2_PORT.into(), + scheme: Scheme::Http.into(), + ..Default::default() + }, + put_complete_endpoint: EndpointConfig { + host: cas_hostname, + port: DEFAULT_PUT_COMPLETE_PORT.into(), + scheme: Scheme::Http.into(), + ..Default::default() + }, + accepted_encodings, + }); + } + debug!( + "GrpcClient Req {}. initiate {}/{}, size={payload_size} complete", + get_request_id(), + prefix, + hash + ); + + Ok(EndpointsInfo { + data_plane_endpoint: data_plane_endpoint.unwrap(), + put_complete_endpoint: put_complete_endpoint.unwrap(), + accepted_encodings, + }) + } + + #[tracing::instrument(skip_all, name = "cas.client", err, fields(prefix = prefix, hash = hash.hex().as_str(), api = "put_complete", request_id = tracing::field::Empty))] + pub async fn put_complete( + &self, + prefix: &str, + hash: &MerkleHash, + chunk_boundaries: &[u64], + ) -> Result<()> { + debug!( + "GrpcClient Req {}. put_complete of {}/{}", + get_request_id(), + prefix, + hash + ); + Span::current().record("request_id", &get_request_id()); + let request = PutCompleteRequest { + key: Some(get_key_for_request(prefix, hash)), + chunk_boundaries: chunk_boundaries.to_owned(), + }; + + let _ = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + self.client.clone().put_complete(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| CasClientError::Grpc(anyhow::Error::from(e)))?; + + debug!( + "GrpcClient Req {}. put_complete of {}/{} complete.", + get_request_id(), + prefix, + hash + ); + Ok(()) + } + + #[tracing::instrument(skip_all, name = "cas.client", err, fields(prefix = prefix, hash = hash.hex().as_str(), api = "get", request_id = tracing::field::Empty))] + pub async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result> { + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + debug!( + "GrpcClient Req {}. Get of {}/{}", + get_request_id(), + prefix, + hash + ); + let request = GetRequest { + key: Some(get_key_for_request(prefix, hash)), + }; + let response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + self.client.clone().get(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| { + info!( + "GrpcClient Req {}. Error on Get {}/{} : {:?}", + get_request_id(), + prefix, + hash, + e + ); + CasClientError::Grpc(anyhow::Error::from(e)) + })?; + + debug!( + "GrpcClient Req {}. Get of {}/{} complete.", + get_request_id(), + prefix, + hash + ); + + Ok(response.into_inner().data) + } + + #[tracing::instrument(skip_all, name = "cas.client", err, fields(prefix = prefix, hash = hash.hex().as_str(), api = "get_range", request_id = tracing::field::Empty))] + pub async fn get_object_range( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>> { + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + debug!( + "GrpcClient Req {}. GetObjectRange of {}/{}", + get_request_id(), + prefix, + hash + ); + // Handle the case where we aren't asked for any real data. + if ranges.len() == 1 && ranges[0].0 == ranges[0].1 { + return Ok(vec![Vec::::new()]); + } + + let range_vec: Vec = ranges + .into_iter() + .map(|(start, end)| Range { start, end }) + .collect(); + let request = GetRangeRequest { + key: Some(get_key_for_request(prefix, hash)), + ranges: range_vec, + }; + let response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + self.client.clone().get_range(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| { + info!( + "GrpcClient Req {}. Error on GetObjectRange of {}/{} : {:?}", + get_request_id(), + prefix, + hash, + e + ); + CasClientError::Grpc(anyhow::Error::from(e)) + })?; + + debug!( + "GrpcClient Req {}. GetObjectRange of {}/{} complete.", + get_request_id(), + prefix, + hash + ); + + Ok(response.into_inner().data) + } + + #[tracing::instrument(skip_all, name = "cas.client", fields(prefix = prefix, hash = hash.hex().as_str(), api = "get_length", request_id = tracing::field::Empty))] + pub async fn get_length(&self, prefix: &str, hash: &MerkleHash) -> Result { + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + debug!( + "GrpcClient Req {}. GetLength of {}/{}", + get_request_id(), + prefix, + hash + ); + let request = HeadRequest { + key: Some(get_key_for_request(prefix, hash)), + }; + let response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + + self.client.clone().head(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| { + debug!( + "GrpcClient Req {}. Error on GetLength of {}/{} : {:?}", + get_request_id(), + prefix, + hash, + e + ); + CasClientError::Grpc(anyhow::Error::from(e)) + })?; + debug!( + "GrpcClient Req {}. GetLength of {}/{} complete.", + get_request_id(), + prefix, + hash + ); + Ok(response.into_inner().size) + } +} + +pub fn get_key_for_request(prefix: &str, hash: &MerkleHash) -> Key { + Key { + prefix: prefix.to_string(), + hash: hash.as_bytes().to_vec(), + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicU32, Ordering}; + use std::sync::Arc; + + use tonic::Response; + + use cas::cas::PutResponse; + + use crate::util::grpc_mock::{MockService, ShutdownHook}; + + use super::*; + + #[tokio::test] + async fn test_put_with_retry() { + let count = Arc::new(AtomicU32::new(0)); + let put_count = count.clone(); + let put_api = move |req: Request| { + assert_eq!(req.into_inner().chunk_boundaries, vec![32, 54, 63]); + if 0 == put_count.fetch_add(1, Ordering::SeqCst) { + return Err(Status::internal("Failed")); + } + Ok(Response::new(PutResponse { was_inserted: true })) + }; + + let (mut hook, client): (ShutdownHook, GrpcClient) = + MockService::default().with_put(put_api).start().await; + + let resp = client + .put("pre1", &MerkleHash::default(), vec![0], vec![32, 54, 63]) + .await; + assert_eq!(2, count.load(Ordering::SeqCst)); + assert!(resp.is_ok()); + hook.async_drop().await; + } + + #[tokio::test] + async fn test_put_exhausted_retries() { + let count = Arc::new(AtomicU32::new(0)); + let put_count = count.clone(); + let put_api = move |req: Request| { + assert_eq!(req.into_inner().chunk_boundaries, vec![31, 54, 63]); + put_count.fetch_add(1, Ordering::SeqCst); + Err(Status::internal("Failed")) + }; + + let (mut hook, client) = MockService::default().with_put(put_api).start().await; + + let resp = client + .put("pre1", &MerkleHash::default(), vec![0], vec![31, 54, 63]) + .await; + assert_eq!(3, count.load(Ordering::SeqCst)); + assert!(resp.is_err()); + hook.async_drop().await + } + + #[tokio::test] + async fn test_put_no_retries() { + let count = Arc::new(AtomicU32::new(0)); + let put_count = count.clone(); + let put_api = move |req: Request| { + assert_eq!(req.into_inner().chunk_boundaries, vec![32, 95, 63]); + put_count.fetch_add(1, Ordering::SeqCst); + Err(Status::internal("Failed")) + }; + let (mut hook, client) = MockService::default() + .with_put(put_api) + .start_with_retry_strategy(RetryStrategy::new(0, 1)) + .await; + + let resp = client + .put("pre1", &MerkleHash::default(), vec![0], vec![32, 95, 63]) + .await; + assert_eq!(1, count.load(Ordering::SeqCst)); + assert!(resp.is_err()); + hook.async_drop().await + } + + #[tokio::test] + async fn test_put_application_error() { + let count = Arc::new(AtomicU32::new(0)); + let put_count = count.clone(); + let put_api = move |req: Request| { + assert_eq!(req.into_inner().chunk_boundaries, vec![32, 56, 63]); + put_count.fetch_add(1, Ordering::SeqCst); + Err(Status::failed_precondition("Failed precondition")) + }; + let (mut hook, client) = MockService::default().with_put(put_api).start().await; + + let resp = client + .put("pre1", &MerkleHash::default(), vec![0], vec![32, 56, 63]) + .await; + assert_eq!(1, count.load(Ordering::SeqCst)); + assert!(resp.is_err()); + hook.async_drop().await + } + + #[test] + fn metadata_header_interceptor_test() { + const XET_VERSION: &str = "0.1.0"; + let cas_connection_cofig: CasConnectionConfig = CasConnectionConfig::new( + "".to_string(), + "xet_user".to_string(), + "xet_auth".to_string(), + vec!["example".to_string()], + XET_VERSION.to_string(), + ); + let mut mh_interceptor = MetadataHeaderInterceptor::new(cas_connection_cofig); + let request = Request::new(()); + + { + // scoped so md reference to request is dropped + let md = request.metadata(); + assert!(md.get(USER_ID_HEADER).is_none()); + assert!(md.get(REQUEST_ID_HEADER).is_none()); + assert!(md.get(REPO_PATHS_HEADER).is_none()); + assert!(md.get(GIT_XET_VERSION_HEADER).is_none()); + assert!(md.get(CAS_PROTOCOL_VERSION_HEADER).is_none()); + } + let request = mh_interceptor.call(request).unwrap(); + + let md = request.metadata(); + let user_id_val = md.get(USER_ID_HEADER).unwrap(); + assert_eq!(user_id_val.to_str().unwrap(), "xet_user"); + let repo_path_val = md.get_bin(REPO_PATHS_HEADER).unwrap(); + assert_eq!(repo_path_val.to_bytes().unwrap().as_ref(), b"[\"example\"]"); + assert!(md.get(REQUEST_ID_HEADER).is_some()); + + assert!(md.get(GIT_XET_VERSION_HEADER).is_some()); + let xet_version = md.get(GIT_XET_VERSION_HEADER).unwrap().to_str().unwrap(); + assert_eq!(xet_version, XET_VERSION); + + // check that global static CAS_PROTOCOL_VERSION is what's set in the header + assert!(md.get(CAS_PROTOCOL_VERSION_HEADER).is_some()); + let cas_protocol_version = md + .get(CAS_PROTOCOL_VERSION_HEADER) + .unwrap() + .to_str() + .unwrap(); + assert_eq!(cas_protocol_version, CAS_PROTOCOL_VERSION.as_str()); + + let data: Vec> = vec![ + vec!["user1/repo-😀".to_string(), "user1/répô_123".to_string()], + vec![ + "user2/👾_repo".to_string(), + "user2/üникод".to_string(), + "user2/foobar!@#".to_string(), + ], + vec!["user3/sømè_repo".to_string(), "user3/你好-世界".to_string()], + vec!["user4/✨🌈repo".to_string()], + vec!["user5/Ω≈ç√repo".to_string()], + vec!["user6/42°_repo".to_string()], + vec![ + "user7/äëïöü_repo".to_string(), + "user7/ĀāĒēĪīŌōŪū".to_string(), + ], + ]; + for inner_vec in data { + let config = CasConnectionConfig::new( + "".to_string(), + "".to_string(), + "".to_string(), + inner_vec.clone(), + "".to_string(), + ); + let mut mh_interceptor = MetadataHeaderInterceptor::new(config); + let request = Request::new(()); + let request = mh_interceptor.call(request).unwrap(); + let md = request.metadata(); + let repo_path_val = md.get_bin(REPO_PATHS_HEADER).unwrap(); + let repo_path_str = + String::from_utf8(repo_path_val.to_bytes().unwrap().to_vec()).unwrap(); + let vec_of_strings: Vec = + serde_json::from_str(repo_path_str.as_str()).expect("Failed to deserialize JSON"); + assert_eq!(vec_of_strings, inner_vec); + } + } +} diff --git a/cas_client/src/interface.rs b/cas_client/src/interface.rs new file mode 100644 index 00000000..b6a328d0 --- /dev/null +++ b/cas_client/src/interface.rs @@ -0,0 +1,92 @@ +use crate::error::Result; +use async_trait::async_trait; +use merklehash::MerkleHash; +use std::sync::Arc; + +/// A Client to the CAS (Content Addressed Storage) service to allow storage and +/// management of XORBs (Xet Object Remote Block). A XORB represents a collection +/// of arbitrary bytes. These bytes are hashed according to a Xet Merkle Hash +/// producing a Merkle Tree. XORBs in the CAS are identified by a combination of +/// a prefix namespacing the XORB and the hash at the root of the Merkle Tree. +#[async_trait] +pub trait Client: core::fmt::Debug { + /// Insert the provided data into the CAS as a XORB indicated by the prefix and hash. + /// The hash will be verified on the server-side according to the chunk boundaries. + /// Chunk Boundaries must be complete; i.e. the last entry in chunk boundary + /// must be the length of data. For instance, if data="helloworld" with 2 chunks + /// ["hello" "world"], chunk_boundaries should be [5, 10]. + /// Empty data and empty chunk boundaries are not accepted. + /// + /// Note that put may background in some implementations and a flush() + /// will be needed. + async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()>; + + /// Clients may do puts in the background. A flush is necessary + /// to enforce completion of all puts. If an error occured during any + /// background put it will be returned here. + async fn flush(&self) -> Result<()>; + + /// Reads all of the contents for the indicated XORB, returning the data or an error + /// if an issue occurred. + async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result>; + + /// Reads the requested ranges for the indicated object. Each range is a tuple of + /// start byte (inclusive) to end byte (exclusive). Will return the contents for + /// the ranges if they exist in the order specified. If there are issues fetching + /// any of the ranges, then an Error will be returned. + async fn get_object_range( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>>; + + /// Gets the length of the XORB or an error if an issue occurred. + async fn get_length(&self, prefix: &str, hash: &MerkleHash) -> Result; +} + +/* + * If T implements Client, Arc also implements Client + */ +#[async_trait] +impl Client for Arc { + async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()> { + (**self).put(prefix, hash, data, chunk_boundaries).await + } + + async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result> { + (**self).get(prefix, hash).await + } + + /// Clients may do puts in the background. A flush is necessary + /// to enforce completion of all puts. If an error occured during any + /// background put it will be returned here.force completion of all puts. + async fn flush(&self) -> Result<()> { + (**self).flush().await + } + + async fn get_object_range( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>> { + (**self).get_object_range(prefix, hash, ranges).await + } + + async fn get_length(&self, prefix: &str, hash: &MerkleHash) -> Result { + (**self).get_length(prefix, hash).await + } +} diff --git a/cas_client/src/lib.rs b/cas_client/src/lib.rs new file mode 100644 index 00000000..cb0579a6 --- /dev/null +++ b/cas_client/src/lib.rs @@ -0,0 +1,29 @@ +#![cfg_attr(feature = "strict", deny(warnings))] +#![allow(dead_code)] + +pub use crate::error::CasClientError; +pub use caching_client::{CachingClient, DEFAULT_BLOCK_SIZE}; +pub use grpc::set_trace_forwarding; +pub use grpc::GrpcClient; +pub use interface::Client; +pub use local_client::LocalClient; +pub use merklehash::MerkleHash; // re-export since this is required for the client API. +pub use passthrough_staging_client::PassthroughStagingClient; +pub use remote_client::RemoteClient; +pub use remote_client::CAS_PROTOCOL_VERSION; +pub use staging_client::{new_staging_client, new_staging_client_with_progressbar, StagingClient}; +pub use staging_trait::{Staging, StagingBypassable}; + +mod caching_client; +mod cas_connection_pool; +mod client_adapter; +mod data_transport; +mod error; +pub mod grpc; +mod interface; +mod local_client; +mod passthrough_staging_client; +mod remote_client; +mod staging_client; +mod staging_trait; +mod util; diff --git a/cas_client/src/local_client.rs b/cas_client/src/local_client.rs new file mode 100644 index 00000000..220713a7 --- /dev/null +++ b/cas_client/src/local_client.rs @@ -0,0 +1,602 @@ +use crate::error::{CasClientError, Result}; +use crate::interface::Client; +use anyhow::anyhow; +use async_trait::async_trait; +use cas::key::Key; +use merkledb::prelude::*; +use merkledb::{Chunk, MerkleMemDB}; +use merklehash::MerkleHash; +use std::fs::{metadata, File}; +use std::io::{BufReader, BufWriter, Read, Seek, Write}; +use std::path::{Path, PathBuf}; +use tempfile::TempDir; +use tracing::{debug, error, info}; + +#[derive(Debug)] +pub struct LocalClient { + // tempdir is created but never used. it is just RAII for directory deletion + // of the temporary directory + #[allow(dead_code)] + tempdir: Option, + pub path: PathBuf, + pub silence_errors: bool, +} +impl Default for LocalClient { + /// Creates a default local client that writes to a temporary directory + /// which gets deleted when LocalClient object is destroyed. + fn default() -> LocalClient { + let tempdir = TempDir::new().unwrap(); + let path = tempdir.path().to_path_buf(); + LocalClient { + tempdir: Some(tempdir), + path, + silence_errors: false, + } + } +} + +fn read_io_to_cas_err(path: &PathBuf, e: std::io::Error) -> CasClientError { + CasClientError::InternalError(anyhow!("Unable to read contents of {:?}. {:?}", path, e)) +} +fn write_io_to_cas_err(path: &PathBuf, e: std::io::Error) -> CasClientError { + CasClientError::InternalError(anyhow!("Unable to write contents of {:?}. {:?}", path, e)) +} + +impl LocalClient { + /// Creates a local client that writes to a particular specified path. + /// Files preexisting in the path may be used to serve queries. + pub fn new(path: &Path, silence_errors: bool) -> LocalClient { + LocalClient { + tempdir: None, + path: path.to_path_buf(), + silence_errors, + } + } + + /// Internal function to get the path for a given hash entry + fn get_path_for_entry(&self, prefix: &str, hash: &MerkleHash) -> PathBuf { + self.path.join(format!("{}.{}", prefix, hash.hex())) + } + + /// File format handling Functions + /// + /// The local disk format for each Xorb is: + /// HEADER + /// - u64: Version Number + /// - u64: data len bytes + /// - u64: chunk boundary in bytes length + /// + /// - all the data + /// - chunk_boundaries as bincode + /// + const HEADER_LEN: u64 = 24; + const HEADER_VERSION: u64 = 0; + + /// Reads a u64 in little endian form from a file + fn read_u64(file: &mut impl Read) -> std::io::Result { + let mut val = [0u8; 8]; + file.read_exact(&mut val)?; + Ok(u64::from_le_bytes(val)) + } + + /// Writes a u64 in little endian form to a file + fn write_u64(file: &mut impl Write, val: u64) -> std::io::Result<()> { + file.write_all(&val.to_le_bytes()) + } + + /// Returns the length and the size of the of the chunk boundary object + fn read_header(file: &mut impl Read) -> std::io::Result<(u64, u64)> { + let version = LocalClient::read_u64(file)?; + if version != LocalClient::HEADER_VERSION { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Invalid File Version", + )); + } + let data_len = LocalClient::read_u64(file)?; + let chunkboundary_len = LocalClient::read_u64(file)?; + Ok((data_len, chunkboundary_len)) + } + + /// Returns the length and the size of the of the chunk boundary object + fn write_header( + file: &mut impl Write, + data_len: u64, + chunkboundary_len: u64, + ) -> std::io::Result<()> { + LocalClient::write_u64(file, LocalClient::HEADER_VERSION)?; + LocalClient::write_u64(file, data_len)?; + LocalClient::write_u64(file, chunkboundary_len)?; + Ok(()) + } + + /// Returns all entries in the local client + pub fn get_all_entries(&self) -> Result> { + let mut ret: Vec<_> = Vec::new(); + + // loop through the directory + self.path + .read_dir() + .map_err(|x| CasClientError::InternalError(x.into()))? + // take only entries which are ok + .filter_map(|x| x.ok()) + // take only entries whose filenames convert into strings + .filter_map(|x| x.file_name().into_string().ok()) + .for_each(|x| { + let mut is_okay = false; + + // try to split the string with the path format [prefix].[hash] + if let Some(pos) = x.rfind('.') { + let prefix = &x[..pos]; + let hash = &x[(pos + 1)..]; + + if let Ok(hash) = MerkleHash::from_hex(hash) { + ret.push(Key { + prefix: prefix.into(), + hash, + }); + is_okay = true; + } + } + if !is_okay { + debug!("File '{x:?}' in staging area not in valid format, ignoring."); + } + }); + Ok(ret) + } + + /// A more complete get() which returns both the chunk boundaries as well + /// as the raw data + pub async fn get_detailed( + &self, + prefix: &str, + hash: &MerkleHash, + ) -> Result<(Vec, Vec)> { + let file_path = self.get_path_for_entry(prefix, hash); + + let file = File::open(&file_path).map_err(|_| { + if !self.silence_errors { + error!("Unable to find file in local CAS {:?}", file_path); + } + CasClientError::XORBNotFound(*hash) + })?; + + let mut reader = BufReader::new(file); + + // read the data length and the chunk boundary length + let (data_len, chunkboundary_len) = + LocalClient::read_header(&mut reader).map_err(|x| read_io_to_cas_err(&file_path, x))?; + + // deserialize the chunk boundary + let mut chunk_boundary_buf = vec![0u8; chunkboundary_len as usize]; + reader + .read_exact(&mut chunk_boundary_buf) + .map_err(|x| read_io_to_cas_err(&file_path, x))?; + let chunk_boundaries: Vec = + bincode::deserialize(&chunk_boundary_buf).map_err(|_| { + CasClientError::InternalError(anyhow!("Invalid deserialization {:?}", file_path)) + })?; + + let mut data = vec![0u8; data_len as usize]; + reader + .read_exact(&mut data) + .map_err(|x| read_io_to_cas_err(&file_path, x))?; + Ok((chunk_boundaries, data)) + } + + /// Deletes an entry + pub fn delete(&self, prefix: &str, hash: &MerkleHash) { + let file_path = self.get_path_for_entry(prefix, hash); + + // unset read-only for Windows to delete + #[cfg(windows)] + { + if let Ok(metadata) = std::fs::metadata(&file_path) { + let mut permissions = metadata.permissions(); + permissions.set_readonly(false); + let _ = std::fs::set_permissions(&file_path, permissions); + } + } + + let _ = std::fs::remove_file(file_path); + } +} + +fn validate_root_hash(data: &[u8], chunk_boundaries: &[u64], hash: &MerkleHash) -> bool { + // at least 1 chunk, and last entry in chunk boundary must match the length + if chunk_boundaries.is_empty() + || chunk_boundaries[chunk_boundaries.len() - 1] as usize != data.len() + { + return false; + } + let mut chunks: Vec = Vec::new(); + let mut left_edge: usize = 0; + for i in chunk_boundaries { + let right_edge = *i as usize; + let hash = merklehash::compute_data_hash(&data[left_edge..right_edge]); + let length = right_edge - left_edge; + chunks.push(Chunk { hash, length }); + left_edge = right_edge; + } + let mut db = MerkleMemDB::default(); + let mut staging = db.start_insertion_staging(); + db.add_file(&mut staging, &chunks); + let ret = db.finalize(staging); + *ret.hash() == *hash +} + +/// The local client stores Xorbs on local disk. +#[async_trait] +impl Client for LocalClient { + async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()> { + let file_path = self.get_path_for_entry(prefix, hash); + + info!("Writing XORB {prefix}/{hash:?} to local path {file_path:?}"); + // no empty writes + if chunk_boundaries.is_empty() || data.is_empty() { + return Err(CasClientError::InvalidArguments); + } + // last boundary must be end of data + if !chunk_boundaries.is_empty() + && chunk_boundaries[chunk_boundaries.len() - 1] as usize != data.len() + { + return Err(CasClientError::InvalidArguments); + } + // validate hash + if !validate_root_hash(&data, &chunk_boundaries, hash) { + return Err(CasClientError::HashMismatch); + } + if let Ok(xorb_size) = self.get_length(prefix, hash).await { + if xorb_size > 0 { + info!("{prefix:?}/{hash:?} already exists in Local CAS; returning."); + return Ok(()); + } + } + if let Ok(metadata) = metadata(&file_path) { + return if metadata.is_file() { + info!("{file_path:?} already exists; returning."); + // if its a file, its ok. we do not overwrite + Ok(()) + } else { + // if its not file we have a problem. + Err(CasClientError::InternalError(anyhow!( + "Attempting to write to {:?}, but {:?} is not a file", + file_path, + file_path + ))) + }; + } + + // we prefix with "[PID]." for now. We should be able to do a cleanup + // in the future. + let tempfile = tempfile::Builder::new() + .prefix(&format!("{}.", std::process::id())) + .suffix(".xorb") + .tempfile_in(&self.path) + .map_err(|e| { + CasClientError::InternalError(anyhow!( + "Unable to create temporary file for staging Xorbs, got {e:?}" + )) + })?; + + let chunk_boundaries_bytes: Vec = bincode::serialize(&chunk_boundaries)?; + + { + let mut writer = BufWriter::new(&tempfile); + LocalClient::write_header( + &mut writer, + data.len() as u64, + chunk_boundaries_bytes.len() as u64, + ) + .map_err(|x| write_io_to_cas_err(&file_path, x))?; + + // write out chunk boundaries then bytes + writer + .write_all(&chunk_boundaries_bytes) + .map_err(|x| write_io_to_cas_err(&file_path, x))?; + + writer + .write_all(&data[..]) + .map_err(|x| write_io_to_cas_err(&file_path, x))?; + + // make sure we flush before persisting + writer + .flush() + .map_err(|x| write_io_to_cas_err(&file_path, x))?; + } + + tempfile.persist(&file_path)?; + + // attempt to set to readonly + // its ok to fail. + if let Ok(metadata) = std::fs::metadata(&file_path) { + let mut permissions = metadata.permissions(); + permissions.set_readonly(true); + let _ = std::fs::set_permissions(&file_path, permissions); + } + + info!("{file_path:?} successfully written."); + + Ok(()) + } + + async fn flush(&self) -> Result<()> { + // this client does not background so no flush is needed + Ok(()) + } + + async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result> { + Ok(self.get_detailed(prefix, hash).await?.1) + } + + async fn get_object_range( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>> { + // Handle the case where we aren't asked for any real data. + if ranges.len() == 1 && ranges[0].0 == ranges[0].1 { + return Ok(vec![Vec::::new()]); + } + let file_path = self.get_path_for_entry(prefix, hash); + + let mut file = File::open(&file_path).map_err(|_| { + if !self.silence_errors { + error!("Unable to find file in local CAS {:?}", file_path); + } + CasClientError::XORBNotFound(*hash) + })?; + + // read the data length and the chunk boundary length + let (data_len, chunkboundary_len) = + LocalClient::read_header(&mut file).map_err(|x| read_io_to_cas_err(&file_path, x))?; + + // calculate where the data starts: + // Its just the header + chunkboundary bytes + let starting_offset = LocalClient::HEADER_LEN + chunkboundary_len; + + let mut ret: Vec> = Vec::new(); + for r in ranges { + let mut start = r.0; + let mut end = r.1; + if start >= data_len { + start = data_len + } + if end > data_len { + end = data_len + } + // end before start, or any position is outside of array + if end < start { + return Err(CasClientError::InvalidRange); + } + let mut data = vec![0u8; (end - start) as usize]; + if end - start > 0 { + file.seek(std::io::SeekFrom::Start(starting_offset + start)) + .map_err(|x| read_io_to_cas_err(&file_path, x))?; + + file.read_exact(&mut data) + .map_err(|x| read_io_to_cas_err(&file_path, x))?; + } + ret.push(data); + } + Ok(ret) + } + + async fn get_length(&self, prefix: &str, hash: &MerkleHash) -> Result { + let file_path = self.get_path_for_entry(prefix, hash); + match File::open(&file_path) { + Ok(mut file) => { + let (len, _) = LocalClient::read_header(&mut file) + .map_err(|x| read_io_to_cas_err(&file_path, x))?; + Ok(len) + } + Err(_) => Err(CasClientError::XORBNotFound(*hash)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cas::key::Key; + use merkledb::detail::hash_node_sequence; + use merkledb::MerkleNode; + + #[tokio::test] + async fn test_basic_read_write() { + let client = LocalClient::default(); + // the root hash of a single chunk is just the hash of the data + let hello = "hello world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + // write "hello world" + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + + // get length "hello world" + assert_eq!(11, client.get_length("key", &hello_hash).await.unwrap()); + + // read "hello world" + assert_eq!(hello, client.get("key", &hello_hash).await.unwrap()); + + // read range "hello" and "world" + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 11)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + // read range "hello" and "world", with truncation for larger offsets + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 20)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + // empty read + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 6)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + } + + #[tokio::test] + async fn test_failures() { + let client = LocalClient::default(); + let hello = "hello world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + // write "hello world" + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + // put the same value a second time. This should be ok. + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + + // we can list all entries + let r = client.get_all_entries().unwrap(); + assert_eq!(r.len(), 1); + assert_eq!( + r, + vec![Key { + prefix: "key".into(), + hash: hello_hash + }] + ); + + // put the different value with the same hash + // this should fail + assert_eq!( + CasClientError::HashMismatch, + client + .put( + "key", + &hello_hash, + "hellp world".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + // content shorter than the chunk boundaries should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put( + "key", + &hello_hash, + "hellp wod".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + + // content longer than the chunk boundaries should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put( + "key", + &hello_hash, + "hello world again".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + + // empty writes should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put("key", &hello_hash, vec![], vec![],) + .await + .unwrap_err() + ); + + // compute a hash of something we do not have in the store + let world = "world".as_bytes().to_vec(); + let world_hash = merklehash::compute_data_hash(&world[..]); + + // get length of non-existant object should fail with XORBNotFound + assert_eq!( + CasClientError::XORBNotFound(world_hash), + client.get_length("key", &world_hash).await.unwrap_err() + ); + + // read of non-existant object should fail with XORBNotFound + assert!(client.get("key", &world_hash).await.is_err()); + // read range of non-existant object should fail with XORBNotFound + assert!(client + .get_object_range("key", &world_hash, vec![(0, 5)]) + .await + .is_err()); + + // we can delete non-existant things + client.delete("key", &world_hash); + + // delete the entry we inserted + client.delete("key", &hello_hash); + let r = client.get_all_entries().unwrap(); + assert_eq!(r.len(), 0); + + // now every read of that key should fail + assert_eq!( + CasClientError::XORBNotFound(hello_hash), + client.get_length("key", &hello_hash).await.unwrap_err() + ); + assert_eq!( + CasClientError::XORBNotFound(hello_hash), + client.get("key", &hello_hash).await.unwrap_err() + ); + } + + #[tokio::test] + async fn test_hashing() { + let client = LocalClient::default(); + // hand construct a tree of 2 chunks + let hello = "hello".as_bytes().to_vec(); + let world = "world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + let world_hash = merklehash::compute_data_hash(&world[..]); + + let hellonode = MerkleNode::new(0, hello_hash, 5, vec![]); + let worldnode = MerkleNode::new(1, world_hash, 5, vec![]); + + let final_hash = hash_node_sequence(&[hellonode, worldnode]); + + // insert should succeed + client + .put( + "key", + &final_hash, + "helloworld".as_bytes().to_vec(), + vec![5, 10], + ) + .await + .unwrap(); + } +} diff --git a/cas_client/src/passthrough_staging_client.rs b/cas_client/src/passthrough_staging_client.rs new file mode 100644 index 00000000..f87a0bdd --- /dev/null +++ b/cas_client/src/passthrough_staging_client.rs @@ -0,0 +1,147 @@ +use futures::stream::FuturesUnordered; +use futures::StreamExt; +use std::fmt::Debug; +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::info; + +use async_trait::async_trait; + +use merklehash::MerkleHash; + +use crate::error::{CasClientError, Result}; +use crate::interface::Client; +use crate::staging_trait::*; + +const PASSTHROUGH_STAGING_MAX_CONCURRENT_UPLOADS: usize = 16; + +type FutureCollectionType = FuturesUnordered> + Send>>>; + +/// The PassthroughStagingClient is a simple wrapper around +/// a Client that provides the trait implementations required for StagingClient +/// All staging operations are no-op. +#[derive(Debug)] +pub struct PassthroughStagingClient { + client: Arc, + put_futures: Mutex, +} + +impl PassthroughStagingClient { + /// Create a new passthrough staging client which wraps any other client. + /// All operations are simply passthrough to the internal client. + /// All staging operations are no-op. + pub fn new(client: Arc) -> PassthroughStagingClient { + PassthroughStagingClient { + client, + put_futures: Mutex::new(FutureCollectionType::new()), + } + } +} + +impl Staging for PassthroughStagingClient {} + +#[async_trait] +impl StagingUpload for PassthroughStagingClient { + /// Upload all staged will upload everything to the remote client. + /// TODO : Caller may need to be wary of a HashMismatch error which will + /// indicate that the local staging environment has been corrupted somehow. + async fn upload_all_staged(&self, _max_concurrent: usize, _retain: bool) -> Result<()> { + Ok(()) + } +} + +#[async_trait] +impl StagingBypassable for PassthroughStagingClient { + async fn put_bypass_stage( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()> { + self.client.put(prefix, hash, data, chunk_boundaries).await + } +} + +#[async_trait] +impl StagingInspect for PassthroughStagingClient { + async fn list_all_staged(&self) -> Result> { + Ok(vec![]) + } + + async fn get_length_staged(&self, _prefix: &str, hash: &MerkleHash) -> Result { + Ok(Err(CasClientError::XORBNotFound(*hash))?) + } + + async fn get_length_remote(&self, prefix: &str, hash: &MerkleHash) -> Result { + let item = self.client.get_length(prefix, hash).await?; + + Ok(item as usize) + } + + fn get_staging_path(&self) -> PathBuf { + PathBuf::default() + } + + fn get_staging_size(&self) -> Result { + Ok(0) + } +} + +#[async_trait] +impl Client for PassthroughStagingClient { + async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()> { + let prefix = prefix.to_string(); + let hash = *hash; + let client = self.client.clone(); + let mut put_futures = self.put_futures.lock().await; + while put_futures.len() >= PASSTHROUGH_STAGING_MAX_CONCURRENT_UPLOADS { + if let Some(Err(e)) = put_futures.next().await { + info!("Error occurred with a background CAS upload."); + // a background upload failed. we returning that error here. + return Err(e); + } + } + put_futures.push(Box::pin(async move { + client.put(&prefix, &hash, data, chunk_boundaries).await + })); + Ok(()) + } + async fn flush(&self) -> Result<()> { + let mut put_futures = self.put_futures.lock().await; + while put_futures.len() > 0 { + if let Some(Err(e)) = put_futures.next().await { + info!("Error occurred with a background CAS upload."); + // a background upload failed. we returning that error here. + return Err(e); + } + } + Ok(()) + } + + async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result> { + self.client.get(prefix, hash).await + } + + async fn get_object_range( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>> { + self.client.get_object_range(prefix, hash, ranges).await + } + + async fn get_length(&self, prefix: &str, hash: &MerkleHash) -> Result { + self.client.get_length(prefix, hash).await + } +} diff --git a/cas_client/src/remote_client.rs b/cas_client/src/remote_client.rs new file mode 100644 index 00000000..26f76400 --- /dev/null +++ b/cas_client/src/remote_client.rs @@ -0,0 +1,577 @@ +use async_trait::async_trait; +use cas::singleflight; +use itertools::Itertools; +use lazy_static::lazy_static; +use tracing::{debug, debug_span, error, info, info_span, Instrument}; + +use merklehash::MerkleHash; + +use cas::common::CompressionScheme; +use std::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::cas_connection_pool::{self, CasConnectionConfig, FromConnectionConfig}; +use crate::data_transport::DataTransport; +use crate::error::{CasClientError, Result}; +use crate::grpc::{EndpointsInfo, GrpcClient}; +use crate::Client; +use retry_strategy::RetryStrategy; + +/// cas protocol version as seen from the client +/// cas protocol determines the parameters and protocols used for +/// cas operations +/// +/// 0.1.0: +/// grpc initiate; port 443; scheme https +/// h2 get + h2 put; port 443; scheme http; host from initiate rpc response +/// grpc put_complete; port 5000; scheme http; host from initiate rpc response +/// 0.2.0: +/// grpc initiate; port 443; scheme https +/// h2 get + h2 put; port, host, and scheme from initiate rpc response +/// grpc put_complete; port, host, and scheme from initiate rpc response +/// defaults to 0.1.0 if initiate does not respond with required info +/// 0.3.0: +/// grpc initiate; port 443; scheme https; widely trusted certificate +/// h2 get + h2 put; port, host and scheme from initiate rpc response; includes custom root certificate authority +/// grpc put_complete; port, host and scheme from initiate rpc response; includes custom root certificate authority +/// defaults to 0.2.0 if initiate does not respond with correct info +const _CAS_PROTOCOL_VERSION: &str = "0.3.0"; + +lazy_static! { + pub static ref CAS_PROTOCOL_VERSION: String = + std::env::var("XET_CAS_PROTOCOL_VERSION").unwrap_or(_CAS_PROTOCOL_VERSION.to_string()); +} + +// Completely arbitrary CAS size for using a single-hit put call. +// This should be tuned after performance testing. +const _MINIMUM_DATA_TRANSPORT_UPLOAD_SIZE: usize = 500; + +const PUT_MAX_RETRIES: usize = 3; +const PUT_RETRY_DELAY_MS: u64 = 1000; + +// We have different pool sizes since the GRPC connections are shorter-lived and +// thus, not as many of them are needed. This helps reduce the impact that connection +// creation can have (which, on MacOS, can be significant (hundreds of ms)). +const H2_TRANSPORT_POOL_SIZE: usize = 16; + +type DataTransportPoolMap = cas_connection_pool::ConnectionPoolMap; + +// Apply an id for instrumentation when new connections are created to help with +// debugging / investigating performance issues related to connection creation. +lazy_static::lazy_static! { + static ref GRPC_CLIENT_ID: AtomicUsize = AtomicUsize::new(0); + static ref H2_CLIENT_ID: AtomicUsize = AtomicUsize::new(0); +} + +#[async_trait] +impl FromConnectionConfig for DataTransport { + async fn new_from_connection_config(config: CasConnectionConfig) -> Result { + let id = H2_CLIENT_ID.fetch_add(1, Ordering::SeqCst); + Ok(DataTransport::from_config(config) + .instrument(info_span!("transport.connect", id)) + .await?) + } +} + +#[async_trait] +impl FromConnectionConfig for GrpcClient { + async fn new_from_connection_config(config: CasConnectionConfig) -> Result { + let id = GRPC_CLIENT_ID.fetch_add(1, Ordering::SeqCst); + Ok(GrpcClient::from_config(config) + .instrument(info_span!("grpc.connect", id)) + .await?) + } +} + +/// CAS Remote client. This negotiates between the control plane (gRPC) +/// and data plane (HTTP) to optimize the uploads and fetches according to +/// the network, file size, and other dynamic qualities. +#[derive(Debug)] +pub struct RemoteClient { + lb_endpoint: String, + user_id: String, + auth: String, + repo_paths: Vec, + grpc_connection_map: Arc>>, + dt_connection_map: DataTransportPoolMap, + length_singleflight: singleflight::Group, + length_cache: Arc>>, + git_xet_version: String, +} + +// DTO's for organization moving around endpoint info +#[derive(Clone)] +struct InitiateResponseEndpointInfo { + endpoint: String, + root_ca: String, +} + +#[derive(Clone)] +struct InitiateResponseEndpoints { + h2: InitiateResponseEndpointInfo, + put_complete: InitiateResponseEndpointInfo, + accepted_encodings: Vec, +} + +impl RemoteClient { + pub fn new( + lb_endpoint: String, + user_id: String, + auth: String, + repo_paths: Vec, + grpc_connection_map: Mutex>, + dt_connection_map: DataTransportPoolMap, + git_xet_version: String, + ) -> Self { + Self { + lb_endpoint, + user_id, + auth, + repo_paths, + grpc_connection_map: Arc::new(grpc_connection_map), + dt_connection_map, + length_singleflight: singleflight::Group::new(), + length_cache: Arc::new(Mutex::new(HashMap::new())), + git_xet_version, + } + } + + pub async fn from_config( + endpoint: &str, + user_id: &str, + auth: &str, + repo_paths: Vec, + git_xet_version: String, + ) -> Self { + // optionally switch between a CAS and a local server running on CAS_GRPC_PORT and + // CAS_HTTP_PORT + Self::new( + endpoint.to_string(), + String::from(user_id), + String::from(auth), + repo_paths, + Mutex::new(HashMap::new()), + cas_connection_pool::ConnectionPoolMap::new_with_pool_size(H2_TRANSPORT_POOL_SIZE), + git_xet_version, + ) + } + + /// utility to generate connection config for an endpoint and other owned information + /// currently only other owned info is `user_id` + fn get_cas_connection_config_for_endpoint(&self, endpoint: String) -> CasConnectionConfig { + CasConnectionConfig::new( + endpoint, + self.user_id.clone(), + self.auth.clone(), + self.repo_paths.clone(), + self.git_xet_version.clone(), + ) + } + + async fn get_grpc_connection_for_config( + &self, + cas_connection_config: CasConnectionConfig, + ) -> Result { + Self::get_grpc_connection_for_config_from_map( + self.grpc_connection_map.clone(), + cas_connection_config, + ) + .await + } + + /// makes an initiate call to the ALB endpoint and returns + /// a tuple of 2 strings, the first being the http direct endpoint + /// and the second is the grpc direct endpoint + async fn initiate_cas_server_query( + &self, + prefix: &str, + hash: &MerkleHash, + len: usize, + ) -> Result { + let cas_connection_config = + self.get_cas_connection_config_for_endpoint(self.lb_endpoint.clone()); + let lb_grpc_client = self + .get_grpc_connection_for_config(cas_connection_config) + .await?; + + let EndpointsInfo { + data_plane_endpoint, + put_complete_endpoint, + accepted_encodings, + } = lb_grpc_client.initiate(prefix, hash, len).await?; + drop(lb_grpc_client); + + debug!("cas initiate response; data plane endpoint: {data_plane_endpoint}; put complete endpoint: {put_complete_endpoint}"); + + Ok(InitiateResponseEndpoints { + h2: InitiateResponseEndpointInfo { + endpoint: data_plane_endpoint.to_string(), + root_ca: data_plane_endpoint.root_ca_certificate, + }, + put_complete: InitiateResponseEndpointInfo { + endpoint: put_complete_endpoint.to_string(), + root_ca: put_complete_endpoint.root_ca_certificate, + }, + accepted_encodings, + }) + } + + async fn put_impl_h2( + &self, + prefix: &str, + hash: &MerkleHash, + data: &[u8], + chunk_boundaries: &[u64], + ) -> Result<()> { + debug!("H2 Put executed with {} {}", prefix, hash); + let InitiateResponseEndpoints { + h2, + put_complete, + accepted_encodings, + } = self + .initiate_cas_server_query(prefix, hash, data.len()) + .instrument(debug_span!("remote_client.initiate")) + .await?; + + let encoding = choose_encoding(accepted_encodings); + + debug!("H2 Put initiate response h2 endpoint: {}, put complete endpoint {}\nh2 cert: {}, put complete cert {}", h2.endpoint, put_complete.endpoint, h2.root_ca, put_complete.root_ca); + + { + // separate scoped to drop transport so that the connection can be reclaimed by the pool + let transport = self + .dt_connection_map + .get_connection_for_config( + self.get_cas_connection_config_for_endpoint(h2.endpoint) + .with_root_ca(h2.root_ca), + ) + .await?; + transport + .put(prefix, hash, data, encoding) + .instrument(debug_span!("remote_client.put_h2")) + .await?; + } + + debug!("Data transport completed"); + + let cas_connection_config = self + .get_cas_connection_config_for_endpoint(put_complete.endpoint) + .with_root_ca(put_complete.root_ca); + let grpc_client = self + .get_grpc_connection_for_config(cas_connection_config) + .await?; + + debug!( + "Received grpc connection from pool: {}", + grpc_client.endpoint + ); + + grpc_client + .put_complete(prefix, hash, chunk_boundaries) + .await + } + + // default implementation, parallel unary + #[allow(dead_code)] + async fn put_impl_unary( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()> { + debug!("Unary Put executed with {} {}", prefix, hash); + + let cas_connection_config = + self.get_cas_connection_config_for_endpoint(self.lb_endpoint.clone()); + let grpc_client = self + .get_grpc_connection_for_config(cas_connection_config) + .await?; + + grpc_client.put(prefix, hash, data, chunk_boundaries).await + } + + // Default implementation, parallel unary + #[allow(dead_code)] + async fn get_impl_unary(&self, prefix: &str, hash: &MerkleHash) -> Result> { + let cas_connection_config = + self.get_cas_connection_config_for_endpoint(self.lb_endpoint.clone()); + let grpc_client = self + .get_grpc_connection_for_config(cas_connection_config) + .await?; + + grpc_client.get(prefix, hash).await + } + + async fn get_impl_h2(&self, prefix: &str, hash: &MerkleHash) -> Result> { + debug!("H2 Get executed with {} {}", prefix, hash); + + let InitiateResponseEndpoints { h2, .. } = self + .initiate_cas_server_query(prefix, hash, 0) + .instrument(debug_span!("remote_client.initiate")) + .await?; + + let cas_connection_config = self + .get_cas_connection_config_for_endpoint(h2.endpoint) + .with_root_ca(h2.root_ca); + let transport = self + .dt_connection_map + .get_connection_for_config(cas_connection_config) + .instrument(debug_span!("remote_client.get_transport_connection")) + .await?; + let data = transport + .get(prefix, hash) + .instrument(debug_span!("remote_client.h2_get")) + .await?; + drop(transport); + + debug!("Data transport completed"); + Ok(data) + } + + // Default implementation, parallel unary + #[allow(dead_code)] + async fn get_object_range_impl_unary( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>> { + debug!("Unary GetRange executed with {} {}", prefix, hash); + + let cas_connection_config = + self.get_cas_connection_config_for_endpoint(self.lb_endpoint.clone()); + let grpc_client = self + .get_grpc_connection_for_config(cas_connection_config) + .await?; + + grpc_client.get_object_range(prefix, hash, ranges).await + } + + async fn get_object_range_impl_h2( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>> { + debug!("H2 GetRange executed with {} {}", prefix, hash); + + let InitiateResponseEndpoints { h2, .. } = self + .initiate_cas_server_query(prefix, hash, 0) + .instrument(debug_span!("remote_client.initiate")) + .await?; + + let cas_connection_config = self + .get_cas_connection_config_for_endpoint(h2.endpoint) + .with_root_ca(h2.root_ca); + let transport = self + .dt_connection_map + .get_connection_for_config(cas_connection_config) + .await?; + + let mut handlers = Vec::new(); + for range in ranges { + handlers.push(transport.get_range(prefix, hash, range)); + } + let results = futures::future::join_all(handlers).await; + let errors: Vec = results + .iter() + .filter_map(|r| r.as_deref().err().map(|s| s.to_string())) + .collect(); + if !errors.is_empty() { + let error_description: String = errors.join("-"); + Err(CasClientError::BatchError(error_description))?; + } + let data = results + .into_iter() + // unwrap is safe since we verified in the above if that no elements have an error + .map(|r| r.unwrap()) + .collect_vec(); + Ok(data) + } +} + +fn choose_encoding(accepted_encodings: Vec) -> CompressionScheme { + if accepted_encodings.is_empty() { + return CompressionScheme::None; + } + if accepted_encodings.contains(&CompressionScheme::Lz4) { + return CompressionScheme::Lz4; + } + CompressionScheme::None +} + +fn cas_client_error_retriable(err: &CasClientError) -> bool { + // we do not retry the logical errors + !matches!( + err, + CasClientError::InvalidRange + | CasClientError::InvalidArguments + | CasClientError::HashMismatch + ) +} + +#[async_trait] +impl Client for RemoteClient { + async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<()> { + // We first check if the block already exists, to avoid an unnecessary upload + if let Ok(xorb_size) = self.get_length(prefix, hash).await { + if xorb_size > 0 { + return Ok(()); + } + } + // We could potentially narrow down the error conditions + // further, but that gets complicated. + // So we just do something pretty coarse-grained + let strategy = RetryStrategy::new(PUT_MAX_RETRIES, PUT_RETRY_DELAY_MS); + let res = strategy + .retry( + || async { + self.put_impl_h2(prefix, hash, &data, &chunk_boundaries) + .await + }, + |e| { + let retry = cas_client_error_retriable(e); + if retry { + info!("Put error {:?}. Retrying...", e); + } + retry + }, + ) + .await; + + if let Err(ref e) = res { + if cas_client_error_retriable(e) { + error!("Too many failures writing {:?}: {:?}.", hash, e); + } + } + res + } + + async fn flush(&self) -> Result<()> { + // this client does not background so no flush is needed + Ok(()) + } + + async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result> { + self.get_impl_h2(prefix, hash).await + } + + async fn get_object_range( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>> { + self.get_object_range_impl_h2(prefix, hash, ranges).await + } + + async fn get_length(&self, prefix: &str, hash: &MerkleHash) -> Result { + let key = format!("{}:{}", prefix, hash.hex()); + + let cache = self.length_cache.clone(); + + // See if it's in the cache first before we try to launch it; this is cheap. + { + let cache = cache.lock().await; + if let Some(v) = cache.get(&key) { + return Ok(*v); + } + } + let cas_connection_config = + self.get_cas_connection_config_for_endpoint(self.lb_endpoint.clone()); + let connection_map = self.grpc_connection_map.clone(); + + let (res, _dedup) = self + .length_singleflight + .work( + &key, + Self::get_length_from_remote( + connection_map, + cas_connection_config, + cache, + prefix.to_string(), + *hash, + ), + ) + .await; + + return match res { + Ok(v) => Ok(v), + Err(singleflight::SingleflightError::InternalError(e)) => Err(e), + Err(e) => Err(CasClientError::InternalError(anyhow::Error::from(e))), + }; + } +} + +// static functions that can be used in spawned tasks +impl RemoteClient { + async fn get_length_from_remote( + connection_map: Arc>>, + cas_connection_config: CasConnectionConfig, + cache: Arc>>, + prefix: String, + hash: MerkleHash, + ) -> Result { + let key = format!("{}:{}", prefix, hash.hex()); + { + let cache = cache.lock().await; + if let Some(v) = cache.get(&key) { + return Ok(*v); + } + } + + let grpc_client = + Self::get_grpc_connection_for_config_from_map(connection_map, cas_connection_config) + .await?; + + debug!("RemoteClient: GetLength of {}/{}", prefix, hash); + + let res = grpc_client.get_length(&prefix, &hash).await?; + + debug!( + "RemoteClient: GetLength of {}/{} request complete", + prefix, hash + ); + + // See if it's in the cache + { + let mut cache = cache.lock().await; + let _ = cache.insert(key.clone(), res); + } + + Ok(res) + } + + async fn get_grpc_connection_for_config_from_map( + grpc_connection_map: Arc>>, + cas_connection_config: CasConnectionConfig, + ) -> Result { + let mut map = grpc_connection_map.lock().await; + if let Some(client) = map.get(&cas_connection_config.endpoint) { + return Ok(client.clone()); + } + // yes the lock is held through to endpoint creation. + // While strictly by locking patterns we should release the + // lock here, create the client, then re-acquire the lock to insert + // into the map, in practice *thousands* of threads could call this + // method simultaneously leading to a "race" where we create + // thousands of connections. + // + // Really we need to "single-flight" connection creation per endpoint. + // Since each RemoteClient really connects to only 1 endpoint, + // just locking the whole method here pretty much does what we need. + let endpoint = cas_connection_config.endpoint.clone(); + let new_client = GrpcClient::new_from_connection_config(cas_connection_config).await?; + map.insert(endpoint, new_client.clone()); + Ok(new_client) + } +} diff --git a/cas_client/src/staging_client.rs b/cas_client/src/staging_client.rs new file mode 100644 index 00000000..1af8de03 --- /dev/null +++ b/cas_client/src/staging_client.rs @@ -0,0 +1,584 @@ +use anyhow::anyhow; +use async_trait::async_trait; +use merklehash::MerkleHash; +use parutils::{tokio_par_for_each, ParallelError}; +use progress_reporting::DataProgressReporter; +use std::fmt::Debug; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{info, info_span, Instrument}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +use crate::error::CasClientError; +use crate::interface::Client; +use crate::local_client::LocalClient; +use crate::staging_trait::*; +use crate::PassthroughStagingClient; + +#[derive(Debug)] +pub struct StagingClient { + client: Arc, + staging_client: LocalClient, + progressbar: bool, +} + +impl StagingClient { + /// Create a new staging client which wraps a remote client. + /// + /// stage_path is the staging directory. + /// + /// Reads will check both the staging environment as well as the + /// the remote client. Puts will write to only staging environment + /// until upload `upload_all_staged()` is called. + /// + /// Staging environment is fully persistent and resilient to restarts. + pub fn new(client: Arc, stage_path: &Path) -> StagingClient { + StagingClient { + client, + staging_client: LocalClient::new(stage_path, true), // silence warnings=true + progressbar: false, + } + } + + /// Create a new staging client which wraps a remote client. + /// + /// stage_path is the staging directory. + /// + /// Reads will check both the staging environment as well as the + /// the remote client. Puts will write to only staging environment + /// until upload `upload_all_staged()` is called. + /// + /// Staging environment is fully persistent and resilient to restarts. + /// This version of the constructor will display a progressbar to stderr + /// when `upload_all_staged()` is called + pub fn new_with_progressbar( + client: Arc, + stage_path: &Path, + ) -> StagingClient { + StagingClient { + client, + staging_client: LocalClient::new(stage_path, true), // silence warnings=true + progressbar: true, + } + } +} + +fn cas_staging_bypass_is_set() -> bool { + // Returns true if XET_CAS_BYPASS_STAGING is set to something besides "0" + std::env::var_os("XET_CAS_BYPASS_STAGING") + .filter(|v| v != "0") + .is_some() +} + +/// Creates a new staging client wraping a staging directory. +/// If a staging directory is provided, it will be used for staging. +/// Otherwise all queries are passed through to the remote directly +/// using the PassthroughStagingClient. +pub fn new_staging_client( + client: T, + stage_path: Option<&Path>, +) -> Arc { + if let (false, Some(path)) = (cas_staging_bypass_is_set(), stage_path) { + Arc::new(StagingClient::new(Arc::new(client), path)) + } else { + Arc::new(PassthroughStagingClient::new(Arc::new(client))) + } +} + +/// Creates a new staging client wraping a staging directory. +/// If a staging directory is provided, it will be used for staging. +/// Otherwise all queries are passed through to the remote directly +/// using the PassthroughStagingClient. +pub fn new_staging_client_with_progressbar( + client: T, + stage_path: Option<&Path>, +) -> Arc { + if let (false, Some(path)) = (cas_staging_bypass_is_set(), stage_path) { + Arc::new(StagingClient::new_with_progressbar(Arc::new(client), path)) + } else { + Arc::new(PassthroughStagingClient::new(Arc::new(client))) + } +} + +impl Staging for StagingClient {} + +#[async_trait] +impl StagingUpload for StagingClient { + /// Upload all staged will upload everything to the remote client. + /// TODO : Caller may need to be wary of a HashMismatch error which will + /// indicate that the local staging environment has been corrupted somehow. + async fn upload_all_staged( + &self, + max_concurrent: usize, + retain: bool, + ) -> Result<(), CasClientError> { + let client = &self.client; + let stage = &self.staging_client; + let entries = stage.get_all_entries()?; + info!( + "XET StagingClient: {} entries to upload to remote.", + entries.len() + ); + + let pb = if self.progressbar && !entries.is_empty() { + let pb = + DataProgressReporter::new("Xet: Uploading data blocks", Some(entries.len()), None); + + pb.register_progress(Some(0), Some(0)); // draw the bar immediately + + Some(Arc::new(Mutex::new(pb))) + } else { + None + }; + let cur_span = info_span!("staging_client.upload_all_staged"); + let ctx = cur_span.context(); + // TODO: This can probably be re-written cleaner with futures::stream + // ex: https://patshaughnessy.net/2020/1/20/downloading-100000-files-using-async-rust + tokio_par_for_each(entries, max_concurrent, |entry, _| { + let pb = pb.clone(); + let span = info_span!("upload_staged_xorb"); + span.set_parent(ctx.clone()); + async move { + // if remote does not have the object + // read the object from staging + // and write the object out to remote + let (cb, val) = stage + .get_detailed(&entry.prefix, &entry.hash) + .instrument(info_span!("read_staged")) + .await?; + let xorb_length = val.len(); + info!( + "Uploading XORB {}/{} of length {}.", + &entry.prefix, + &entry.hash, + val.len() + ); + client.put(&entry.prefix, &entry.hash, val, cb).await?; + + if !retain { + info!( + "Clearing XORB {}/{} from staging area.", + &entry.prefix, &entry.hash, + ); + // Delete it from staging + stage.delete(&entry.prefix, &entry.hash); + } + if let Some(bar) = &pb { + bar.lock() + .await + .register_progress(Some(1), Some(xorb_length)); + } + Ok(()) + } + .instrument(span) + }) + .instrument(cur_span) + .await + .map_err(|e| match e { + ParallelError::JoinError => CasClientError::InternalError(anyhow!("Join Error")), + ParallelError::TaskError(e) => e, + })?; + self.client.flush().await?; + + if let Some(bar) = &pb { + bar.lock().await.finalize(); + } + + Ok(()) + } +} + +#[async_trait] +impl StagingBypassable for StagingClient { + async fn put_bypass_stage( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<(), CasClientError> { + self.client.put(prefix, hash, data, chunk_boundaries).await + } +} + +#[async_trait] +impl StagingInspect for StagingClient { + async fn list_all_staged(&self) -> Result, CasClientError> { + let stage = &self.staging_client; + let items = stage + .get_all_entries()? + .iter() + .map(|item: &cas::key::Key| item.to_string()) + .collect(); + + Ok(items) + } + + async fn get_length_staged( + &self, + prefix: &str, + hash: &MerkleHash, + ) -> Result { + let stage = &self.staging_client; + let item = stage.get_detailed(prefix, hash).await?; + + Ok(item.1.len()) + } + + async fn get_length_remote( + &self, + prefix: &str, + hash: &MerkleHash, + ) -> Result { + let item = self.client.get_length(prefix, hash).await?; + + Ok(item as usize) + } + + fn get_staging_path(&self) -> PathBuf { + self.staging_client.path.clone() + } + + fn get_staging_size(&self) -> Result { + self.staging_client + .path + .read_dir() + .map_err(|x| CasClientError::InternalError(x.into()))? + // take only entries which are ok + .filter_map(|x| x.ok()) + // take only entries whose filenames convert into strings + .filter(|x| { + let mut is_ok = false; + if let Ok(name) = x.file_name().into_string() { + if let Some(pos) = name.rfind('.') { + is_ok = MerkleHash::from_hex(&name[(pos + 1)..]).is_ok() + } + } + + is_ok + }) + .try_fold(0, |acc, file| { + let file = file; + let size = match file.metadata() { + Ok(data) => data.len() as usize, + _ => 0, + }; + Ok(acc + size) + }) + } +} + +#[async_trait] +impl Client for StagingClient { + async fn put( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<(), CasClientError> { + self.staging_client + .put(prefix, hash, data, chunk_boundaries) + .instrument(info_span!("staging_client.put")) + .await + } + + async fn flush(&self) -> Result<(), CasClientError> { + // forward flush to the underlying clients + self.staging_client.flush().await?; + self.client.flush().await + } + + async fn get(&self, prefix: &str, hash: &MerkleHash) -> Result, CasClientError> { + match self + .staging_client + .get(prefix, hash) + .instrument(info_span!("staging_client.get")) + .await + { + Err(CasClientError::XORBNotFound(_)) => self.client.get(prefix, hash).await, + x => x, + } + } + + async fn get_object_range( + &self, + prefix: &str, + hash: &MerkleHash, + ranges: Vec<(u64, u64)>, + ) -> Result>, CasClientError> { + match self + .staging_client + .get_object_range(prefix, hash, ranges.clone()) + .instrument(info_span!("staging_client.get_range")) + .await + { + Err(CasClientError::XORBNotFound(_)) => { + self.client.get_object_range(prefix, hash, ranges).await + } + x => x, + } + } + + async fn get_length(&self, prefix: &str, hash: &MerkleHash) -> Result { + match self + .staging_client + .get_length(prefix, hash) + .instrument(info_span!("staging_client.get_length")) + .await + { + Err(CasClientError::XORBNotFound(_)) => self.client.get_length(prefix, hash).await, + x => x, + } + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + use std::sync::Arc; + + use tempfile::TempDir; + + use crate::staging_client::{StagingClient, StagingUpload}; + use crate::*; + + fn make_staging_client(_client_path: &Path, stage_path: &Path) -> StagingClient { + let client = LocalClient::default(); + StagingClient::new(Arc::new(client), stage_path) + } + + #[tokio::test] + async fn test_general_basic_read_write() { + let localdir = TempDir::new().unwrap(); + let stagedir = TempDir::new().unwrap(); + let client = make_staging_client(localdir.path(), stagedir.path()); + + // the root hash of a single chunk is just the hash of the data + let hello = "hello world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + // write "hello world" + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + + // get length "hello world" + assert_eq!(11, client.get_length("key", &hello_hash).await.unwrap()); + + // read "hello world" + assert_eq!(hello, client.get("key", &hello_hash).await.unwrap()); + + // read range "hello" and "world" + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 11)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + // read range "hello" and "world", with truncation for larger offsets + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 20)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + // empty read + let ranges_to_read: Vec<(u64, u64)> = vec![(0, 5), (6, 6)]; + let expected: Vec> = vec!["hello".as_bytes().to_vec(), "".as_bytes().to_vec()]; + assert_eq!( + expected, + client + .get_object_range("key", &hello_hash, ranges_to_read) + .await + .unwrap() + ); + } + + #[tokio::test] + async fn test_general_failures() { + let localdir = TempDir::new().unwrap(); + let stagedir = TempDir::new().unwrap(); + let client = make_staging_client(localdir.path(), stagedir.path()); + + let hello = "hello world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + // write "hello world" + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + // put the same value a second time. This should be ok. + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + + // put the different value with the same hash + // this should fail + assert_eq!( + CasClientError::HashMismatch, + client + .put( + "key", + &hello_hash, + "hellp world".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + // content shorter than the chunk boundaries should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put( + "key", + &hello_hash, + "hellp wod".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + + // content longer than the chunk boundaries should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put( + "key", + &hello_hash, + "hello world again".as_bytes().to_vec(), + vec![hello.len() as u64], + ) + .await + .unwrap_err() + ); + + // empty writes should fail + assert_eq!( + CasClientError::InvalidArguments, + client + .put("key", &hello_hash, vec![], vec![],) + .await + .unwrap_err() + ); + + // compute a hash of something we do not have in the store + let world = "world".as_bytes().to_vec(); + let world_hash = merklehash::compute_data_hash(&world[..]); + + // get length of non-existant object should fail with XORBNotFound + assert!(client.get_length("key", &world_hash).await.is_err()); + + // read of non-existant object should fail with XORBNotFound + assert!(client.get("key", &world_hash).await.is_err()); + // read range of non-existant object should fail with XORBNotFound + assert_eq!( + CasClientError::XORBNotFound(world_hash), + client + .get_object_range("key", &world_hash, vec![(0, 5)]) + .await + .unwrap_err() + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_staged_read_write() { + let localdir = TempDir::new().unwrap(); + let stagedir = TempDir::new().unwrap(); + let client = make_staging_client(localdir.path(), stagedir.path()); + + // put an object in and make sure it is there + + // the root hash of a single chunk is just the hash of the data + let hello = "hello world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + // write "hello world" + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + + // get length "hello world" + assert_eq!(11, client.get_length("key", &hello_hash).await.unwrap()); + // read "hello world" + assert_eq!(hello, client.get("key", &hello_hash).await.unwrap()); + + // check that the underlying client does not actually have it + assert_eq!( + CasClientError::XORBNotFound(hello_hash), + client.client.get("key", &hello_hash).await.unwrap_err() + ); + + // upload staged + client.upload_all_staged(1, false).await.unwrap(); + + // we can still read it + // get length "hello world" + assert_eq!(11, client.get_length("key", &hello_hash).await.unwrap()); + // read "hello world" + assert_eq!(hello, client.get("key", &hello_hash).await.unwrap()); + + // underlying client has it now + assert_eq!(hello, client.client.get("key", &hello_hash).await.unwrap()); + + // staging client does not + assert_eq!( + CasClientError::XORBNotFound(hello_hash), + client + .staging_client + .get("key", &hello_hash) + .await + .unwrap_err() + ); + } + + #[tokio::test] + async fn test_passthrough() { + let localdir = TempDir::new().unwrap(); + let local = LocalClient::new(localdir.path(), true); + // no staging directory + let client = new_staging_client(local, None); + + // put an object in and make sure it is there + + // the root hash of a single chunk is just the hash of the data + let hello = "hello world".as_bytes().to_vec(); + let hello_hash = merklehash::compute_data_hash(&hello[..]); + // write "hello world" + client + .put("key", &hello_hash, hello.clone(), vec![hello.len() as u64]) + .await + .unwrap(); + client.flush().await.unwrap(); + // get length "hello world" + assert_eq!(11, client.get_length("key", &hello_hash).await.unwrap()); + // read "hello world" + assert_eq!(hello, client.get("key", &hello_hash).await.unwrap()); + + // since there is no stage. get_length_staged should fail. + assert_eq!( + CasClientError::XORBNotFound(hello_hash), + client + .get_length_staged("key", &hello_hash) + .await + .unwrap_err() + ); + + // check that the underlying client has it! + // (this is a passthrough!) + // but we can't get it from the stage object (it is now a Box) + // so we make a new local client at the same directory + let local2 = LocalClient::new(localdir.path(), true); + assert_eq!(hello, local2.get("key", &hello_hash).await.unwrap()); + } +} diff --git a/cas_client/src/staging_trait.rs b/cas_client/src/staging_trait.rs new file mode 100644 index 00000000..728450b9 --- /dev/null +++ b/cas_client/src/staging_trait.rs @@ -0,0 +1,58 @@ +use crate::error::CasClientError; +use crate::interface::Client; +use async_trait::async_trait; +use merklehash::MerkleHash; +use std::path::PathBuf; + +#[async_trait] +pub trait StagingUpload { + async fn upload_all_staged( + &self, + max_concurrent: usize, + retain: bool, + ) -> Result<(), CasClientError>; +} + +#[async_trait] +pub trait StagingBypassable { + async fn put_bypass_stage( + &self, + prefix: &str, + hash: &MerkleHash, + data: Vec, + chunk_boundaries: Vec, + ) -> Result<(), CasClientError>; +} + +#[async_trait] +pub trait StagingInspect { + /// Returns a vector of the XORBs in staging + async fn list_all_staged(&self) -> Result, CasClientError>; + + /// Gets the length of the XORB. This is the same as the + /// get_length method on the Client trait, but it forces the check to + /// come from staging only. + async fn get_length_staged( + &self, + prefix: &str, + hash: &MerkleHash, + ) -> Result; + + /// Gets the length of the XORB. This is the same as the + /// get_length method on the Client trait, but this forces the check to + /// come from the remote CAS server. + async fn get_length_remote( + &self, + prefix: &str, + hash: &MerkleHash, + ) -> Result; + + /// Gets the path to the staging directory. + fn get_staging_path(&self) -> PathBuf; + + /// Gets the sum of the file sizes of the valid XORBS in staging. + fn get_staging_size(&self) -> Result; +} + +#[async_trait] +pub trait Staging: StagingUpload + StagingInspect + Client + StagingBypassable {} diff --git a/cas_client/src/util.rs b/cas_client/src/util.rs new file mode 100644 index 00000000..3a25bc38 --- /dev/null +++ b/cas_client/src/util.rs @@ -0,0 +1,234 @@ +#[cfg(test)] +pub(crate) mod grpc_mock { + use std::sync::atomic::{AtomicU16, Ordering}; + use std::sync::Arc; + use std::time::Duration; + + use cas::infra::infra_utils_server::InfraUtils; + use oneshot::{channel, Receiver}; + use tokio::sync::oneshot; + use tokio::sync::oneshot::Sender; + use tokio::task::JoinHandle; + use tokio::time::sleep; + use tonic::transport::{Error, Server}; + use tonic::{Request, Response, Status}; + + use crate::cas_connection_pool::CasConnectionConfig; + use crate::grpc::get_client; + use crate::grpc::GrpcClient; + use cas::cas::cas_server::{Cas, CasServer}; + use cas::cas::{ + GetRangeRequest, GetRangeResponse, GetRequest, GetResponse, HeadRequest, HeadResponse, + PutCompleteRequest, PutCompleteResponse, PutRequest, PutResponse, + }; + use cas::common::{Empty, InitiateRequest, InitiateResponse}; + use cas::infra::EndpointLoadResponse; + use retry_strategy::RetryStrategy; + + const TEST_PORT_START: u16 = 64400; + + lazy_static::lazy_static! { + static ref CURRENT_PORT: AtomicU16 = AtomicU16::new(TEST_PORT_START); + } + + trait_set::trait_set! { + pub trait PutFn = Fn(Request) -> Result, Status> + 'static; + pub trait InitiateFn = Fn(Request) -> Result, Status> + 'static; + pub trait PutCompleteFn = Fn(Request) -> Result, Status> + 'static; + pub trait GetFn = Fn(Request) -> Result, Status> + 'static; + pub trait GetRangeFn = Fn(Request) -> Result, Status> + 'static; + pub trait HeadFn = Fn(Request) -> Result, Status> + 'static; + } + + /// "Mocks" the grpc service for CAS. This is implemented by allowing the test writer + /// to define the functionality needed for the server and then calling `#start()` to + /// run the server on some port. A GrpcClient will be returned to test with as well + /// as a shutdown hook that can be called to shutdown the mock service. + #[derive(Default)] + pub struct MockService { + put_fn: Option>, + initiate_fn: Option>, + put_complete_fn: Option>, + get_fn: Option>, + get_range_fn: Option>, + head_fn: Option>, + } + + impl MockService { + #[allow(dead_code)] + pub fn with_initiate(self, f: F) -> Self { + Self { + initiate_fn: Some(Arc::new(f)), + ..self + } + } + #[allow(dead_code)] + pub fn with_put_complete(self, f: F) -> Self { + Self { + put_complete_fn: Some(Arc::new(f)), + ..self + } + } + + pub fn with_put(self, f: F) -> Self { + Self { + put_fn: Some(Arc::new(f)), + ..self + } + } + + #[allow(dead_code)] + pub fn with_get(self, f: F) -> Self { + Self { + get_fn: Some(Arc::new(f)), + ..self + } + } + + #[allow(dead_code)] + pub fn with_get_range(self, f: F) -> Self { + Self { + get_range_fn: Some(Arc::new(f)), + ..self + } + } + + #[allow(dead_code)] + pub fn with_head(self, f: F) -> Self { + Self { + head_fn: Some(Arc::new(f)), + ..self + } + } + + pub async fn start(self) -> (ShutdownHook, GrpcClient) { + self.start_with_retry_strategy(RetryStrategy::new(2, 1)) + .await + } + + pub async fn start_with_retry_strategy( + self, + strategy: RetryStrategy, + ) -> (ShutdownHook, GrpcClient) { + // Get next port + let port = CURRENT_PORT.fetch_add(1, Ordering::SeqCst); + let addr = format!("127.0.0.1:{}", port).parse().unwrap(); + + // Start up the server + let (tx, rx) = channel::<()>(); + let handle = tokio::spawn( + Server::builder() + .add_service(CasServer::new(self)) + .serve_with_shutdown(addr, shutdown(rx)), + ); + let shutdown_hook = ShutdownHook::new(tx, handle); + + // Wait for server to start up + sleep(Duration::from_millis(10)).await; + + // Create dedicated client for server + let endpoint = format!("127.0.0.1:{}", port); + let user_id = "xet_user".to_string(); + let auth = "xet_auth".to_string(); + let repo_paths = vec!["example".to_string()]; + let version = "0.1.0".to_string(); + let cas_client = get_client(CasConnectionConfig::new( + endpoint, user_id, auth, repo_paths, version, + )) + .await + .unwrap(); + let client = GrpcClient::new("127.0.0.1".to_string(), cas_client, strategy); + (shutdown_hook, client) + } + } + + // Unsafe hacks so that we can dynamically add in overrides to the mock functionality + // (Fn isn't sync/send). There's probably a better way to do this that isn't so blunt/fragile. + unsafe impl Send for MockService {} + unsafe impl Sync for MockService {} + + #[async_trait::async_trait] + impl InfraUtils for MockService { + async fn endpoint_load( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } + async fn initiate( + &self, + request: Request, + ) -> Result, Status> { + self.initiate_fn.as_ref().unwrap()(request) + } + } + #[async_trait::async_trait] + impl Cas for MockService { + async fn initiate( + &self, + request: Request, + ) -> Result, Status> { + self.initiate_fn.as_ref().unwrap()(request) + } + + async fn put(&self, request: Request) -> Result, Status> { + self.put_fn.as_ref().unwrap()(request) + } + + async fn put_complete( + &self, + request: Request, + ) -> Result, Status> { + self.put_complete_fn.as_ref().unwrap()(request) + } + + async fn get(&self, request: Request) -> Result, Status> { + self.get_fn.as_ref().unwrap()(request) + } + + async fn get_range( + &self, + request: Request, + ) -> Result, Status> { + self.get_range_fn.as_ref().unwrap()(request) + } + + async fn head( + &self, + request: Request, + ) -> Result, Status> { + self.head_fn.as_ref().unwrap()(request) + } + } + + async fn shutdown(rx: Receiver<()>) { + let _ = rx.await; + } + + /// Encapsulates logic to shutdown a running tonic Server. This is done through + /// sending a message on a channel that the server is listening on for shutdown. + /// Once the message has been sent, the spawned task is awaited using its JoinHandle. + /// + /// TODO: implementing `Drop` with async is difficult and the naïve implementation + /// ends up blocking the test completion. There is likely some deadlock somewhere. + pub struct ShutdownHook { + tx: Option>, + join_handle: Option>>, + } + + impl ShutdownHook { + pub fn new(tx: Sender<()>, join_handle: JoinHandle>) -> Self { + Self { + tx: Some(tx), + join_handle: Some(join_handle), + } + } + + pub async fn async_drop(&mut self) { + let tx = self.tx.take(); + let handle = self.join_handle.take(); + let _ = tx.unwrap().send(()); + let _ = handle.unwrap().await; + } + } +} diff --git a/data/Cargo.toml b/data/Cargo.toml new file mode 100644 index 00000000..cac7d86c --- /dev/null +++ b/data/Cargo.toml @@ -0,0 +1,157 @@ +[package] +name = "data" +version = "0.14.5" +edition = "2021" + +[profile.release] +opt-level = 3 +lto = true +debug = 1 + +[lib] +doctest = false + +[[bin]] +name = "example" +path = "src/bin/example.rs" + +[dependencies] +cas_client = { path = "../cas_client" } +merkledb = { path = "../merkledb" } +merklehash = { path = "../merklehash" } +mdb_shard = { path = "../mdb_shard" } +shard_client = { path = "../shard_client" } +utils = { path = "../utils" } +parutils = { path = "../parutils" } +file_utils = { path = "../file_utils" } +retry_strategy = { path = "../retry_strategy" } +error_printer = { path = "../error_printer" } +xet_error = { path = "../xet_error" } +slog = "2.4.1" +slog-async = "2.3.0" +slog-json = "2.2.0" +dirs = "4.0.0" +tokio = { version = "1.36", features = ["full"] } +anyhow = "1" +hex = "0.4.3" +tracing = "0.1.*" +more-asserts = "0.3.*" +futures = "0.3.28" +futures-core = "0.3.28" +pbr = "1.0.4" +async-trait = "0.1.53" +tracing-attributes = "0.1" +tracing-subscriber = { version = "0.3", features = ["tracing-log"] } +clap = { version = "3.1.6", features = ["derive"] } +git2 = { git = "https://github.com/xetdata/git2-rs", default-features = false, features = [ + "https", +] } +base64 = "0.13.0" +fallible-iterator = "0.2.0" +atoi = "1.0.0" +colored = "2.0.0" +pathdiff = "0.2.1" +http = "0.2.8" +same-file = "1.0.6" +tempfile = "3.2.0" +tempdir = "0.3.7" +regex = "1.5.6" +lazy_static = "1.4.0" +is_executable = "1.0.1" +rand = "0.8.4" +version-compare = "0.1.1" +serde = { version = "1.0.142", features = ["derive"] } +serde_json = "1.0.83" +csv-core = "0.1.10" +sorted-vec = "0.8.0" +bincode = "1.3.3" +enum_dispatch = "0.3.8" +lru = "0.12" +intaglio = "1.8.0, <1.9.0" +walkdir = "2" +filetime = "0.2" +ctrlc = "3" +nfsserve = "0.10" +atty = "0.2" +libc = "0.2" +itertools = "0.10.5" +shellish_parse = "2.2" +ring = "0.16.20" +humantime = "2.1.0" +toml = "0.5" +winapi = { version = "0.3", features = [ + "winerror", + "winnt", + "handleapi", + "processthreadsapi", + "securitybaseapi", +] } +normalize-path = "0.1.0" +git-version = "0.3" +const_format = "0.2" +whoami = "1.4.1" +tabled = "0.12.0" +shellexpand = "1.0.0" +blake3 = "1.5.1" +uuid = { version = "1.8.0", features = ["std", "rng", "v6"] } +lz4 = "1.24.0" +git-url-parse = "0.4.4" +path-absolutize = "3.1.1" # Can drop after rust 1.79 +static_assertions = "1.1.0" +gearhash = "0.1.3" +rand_chacha = "0.3.1" + +# tracing +tracing-futures = "0.2" +tracing-test = "0.2.1" +tracing-opentelemetry = "0.17.2" +opentelemetry = { version = "0.17", features = ["trace", "rt-tokio"] } +opentelemetry-jaeger = { version = "0.16", features = ["rt-tokio"] } +openssl-probe = "0.1.5" + +# analytics +url = "2.3" +mockall = "0.11" +mockall_double = "0.3" + +# axe +sysinfo = "0.26.6" +serde_with = "1.6.1" +chrono = { version = "0.4.19", features = ["serde"] } + + +# metrics +prometheus = "0.13.0" +utime = "0.3.1" + +# Other hashers for migration +hashers = "1.0.1" + +# Need to specify this as optional to allow the openssl/vendored option below +openssl = { version = "0.10", features = [], optional = true } +glob = "0.3.1" + +[target.'cfg(not(windows))'.dependencies] +openssl = "0.10" + +# use embedded webpki root certs for MacOS as native certs take a very long time +# to load, which affects startup time significantly +[target.'cfg(macos)'.dependencies] +reqwest = { version = "0.11.4", features = ["json", "webpki-roots"] } + +[target.'cfg(not(macos))'.dependencies] +reqwest = { version = "0.11.4", features = ["json"] } + +[dev-dependencies] +assert_cmd = "2.0" +predicates = "2.1" +rstest = "0.11" +tokio-test = "0.4.2" +mockstream = "0.0.3" +run_script = "0.9.0" +serial_test = "2.0.0" + +[features] +strict = [] +expensive_tests = [] +openssl_vendored = ["openssl/vendored", "git2/vendored-openssl"] diff --git a/data/src/cas_interface.rs b/data/src/cas_interface.rs new file mode 100644 index 00000000..ed9938c5 --- /dev/null +++ b/data/src/cas_interface.rs @@ -0,0 +1,173 @@ +use super::configurations::{Endpoint::*, RepoInfo, StorageConfig}; +use super::errors::Result; +use crate::constants::{MAX_CONCURRENT_DOWNLOADS, XET_VERSION}; +use crate::metrics::FILTER_BYTES_SMUDGED; +use cas_client::{new_staging_client, CachingClient, LocalClient, RemoteClient, Staging}; +use futures::prelude::stream::*; +use merkledb::ObjectRange; +use merklehash::MerkleHash; +use std::env::current_dir; +use std::sync::Arc; +use tracing::{error, info, info_span}; + +// Re-export for external configuration suggestion. +pub use cas_client::DEFAULT_BLOCK_SIZE; + +pub(crate) async fn create_cas_client( + cas_storage_config: &StorageConfig, + maybe_repo_info: &Option, +) -> Result> { + // Local file system based CAS storage. + if let FileSystem(ref path) = cas_storage_config.endpoint { + info!("Using local CAS with path: {:?}.", path); + let path = match path.is_absolute() { + true => path, + false => ¤t_dir()?.join(path), + }; + let client = LocalClient::new(path, false); + return Ok(new_staging_client( + client, + cas_storage_config.staging_directory.as_deref(), + )); + } + + // Now we are using remote server CAS storage. + let Server(ref endpoint) = cas_storage_config.endpoint else { + unreachable!(); + }; + + // Auth info. + let user_id = &cas_storage_config.auth.user_id; + let auth = &cas_storage_config.auth.login_id; + + // Usage tracking. + let repo_paths = maybe_repo_info + .as_ref() + .map(|repo_info| &repo_info.repo_paths) + .cloned() + .unwrap_or_default(); + + // Raw remote client. + let remote_client = Arc::new( + RemoteClient::from_config(endpoint, user_id, auth, repo_paths, XET_VERSION.clone()).await, + ); + + // Try add in caching capability. + let maybe_caching_client = cas_storage_config.cache_config.as_ref().and_then(|cache| { + CachingClient::new( + remote_client.clone(), + &cache.cache_directory, + cache.cache_size, + cache.cache_blocksize, + ) + .map_err(|e| error!("Unable to use caching CAS due to: {:?}", &e)) + .ok() + }); + + // If initiating caching was unsuccessful, fall back to only remote client. + match maybe_caching_client { + Some(caching_client) => { + info!( + "Using caching CAS with endpoint {:?}, caching at {:?}.", + &endpoint, + cas_storage_config + .cache_config + .as_ref() + .unwrap() + .cache_directory + ); + + Ok(new_staging_client( + caching_client, + cas_storage_config.staging_directory.as_deref(), + )) + } + None => { + info!("Using non-caching CAS with endpoint: {:?}.", &endpoint); + Ok(new_staging_client( + remote_client, + cas_storage_config.staging_directory.as_deref(), + )) + } + } +} + +/** Wrapper to consolidate the logic for retrieving from CAS. + */ +async fn get_from_cas( + cas: &Arc, + prefix: String, + hash: MerkleHash, + ranges: (u64, u64), +) -> Result> { + if ranges.0 == ranges.1 { + return Ok(Vec::new()); + } + let mut query_result = cas.get_object_range(&prefix, &hash, vec![ranges]).await?; + Ok(std::mem::take(&mut query_result[0])) +} + +/// Given an Vec describing a series of range of bytes, +/// slice a subrange. This does not check limits and may return shorter +/// results if the slice goes past the end of the range. +pub(crate) fn slice_object_range( + v: &[ObjectRange], + mut start: usize, + mut len: usize, +) -> Vec { + let mut ret: Vec = Vec::new(); + for i in v.iter() { + let ilen = i.end - i.start; + // we have not gotten to the start of the range + if start > 0 && start >= ilen { + // start is still after this range + start -= ilen; + } else { + // either start == 0, or start < packet len. + // Either way, we need some or all of this packet + // and after this packet start must be = 0 + let packet_start = i.start + start; + // the maximum length allowed is how far to end of the packet + // OR the actual slice length requested which ever is shorter. + let max_length_allowed = std::cmp::min(i.end - packet_start, len); + ret.push(ObjectRange { + hash: i.hash, + start: packet_start, + end: packet_start + max_length_allowed, + }); + start = 0; + len -= max_length_allowed; + } + if len == 0 { + break; + } + } + ret +} + +/// Writes a collection of chunks from a Vec to a writer. +pub(crate) async fn data_from_chunks_to_writer( + cas: &Arc, + prefix: String, + chunks: Vec, + writer: &mut impl std::io::Write, +) -> Result<()> { + let mut bytes_smudged: u64 = 0; + let mut strm = iter(chunks.into_iter().map(|objr| { + let prefix = prefix.clone(); + get_from_cas(cas, prefix, objr.hash, (objr.start as u64, objr.end as u64)) + })) + .buffered(*MAX_CONCURRENT_DOWNLOADS); + + while let Some(buf) = strm.next().await { + let buf = buf?; + bytes_smudged += buf.len() as u64; + let s = info_span!("write_chunk"); + let _ = s.enter(); + writer.write_all(&buf)?; + } + + FILTER_BYTES_SMUDGED.inc_by(bytes_smudged); + + Ok(()) +} diff --git a/data/src/chunking.rs b/data/src/chunking.rs new file mode 100644 index 00000000..3c0c6aa6 --- /dev/null +++ b/data/src/chunking.rs @@ -0,0 +1,272 @@ +use super::clean::BufferItem; +use lazy_static::lazy_static; +use merkledb::constants::{ + MAXIMUM_CHUNK_MULTIPLIER, MINIMUM_CHUNK_DIVISOR, N_LOW_VARIANCE_CDC_CHUNKERS, + TARGET_CDC_CHUNK_SIZE, +}; +use merkledb::Chunk; +use merklehash::compute_data_hash; +use rand_chacha::rand_core::RngCore; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaChaRng; +use std::cmp::min; +use std::pin::Pin; +use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; + +pub const HASH_SEED: u64 = 123456; + +struct HasherPointerBox<'a>(*mut gearhash::Hasher<'a>); + +unsafe impl<'a> Send for HasherPointerBox<'a> {} +unsafe impl<'a> Sync for HasherPointerBox<'a> {} + +pub type ChunkYieldType = (Chunk, Vec); + +pub struct LowVarianceChunker { + hash: Vec>, + minimum_chunk: usize, + maximum_chunk: usize, + mask: u64, + // generator state + chunkbuf: Vec, + cur_chunk_len: usize, + // This hasher is referenced *a lot* and there was quite a + // measurable performance gain by making this a raw pointer. + // + // The key problem is that I need a mutable mutable reference to the + // current hasher which is basically an index into hash. + // (Basically cur_hasher = &mut hash[cur_hash_index]) + // + // But because of rust borrow checker rules, this cannot be done + // easily. We can of course just use hash[cur_hash_index] all the time + // but this is in fact a core inner loop and ends up as a perf bottleneck. + cur_hasher: HasherPointerBox<'static>, + cur_hash_index: usize, + data_queue: Receiver>>, + yield_queue: Sender>, +} + +impl LowVarianceChunker { + pub fn run(chunker: Mutex>>) -> JoinHandle<()> { + const MAX_WINDOW_SIZE: usize = 64; + + tokio::spawn(async move { + let mut chunker = chunker.lock().await; + let mut complete = false; + while !complete { + match chunker.data_queue.recv().await { + Some(BufferItem::Value(readbuf)) => { + let read_bytes = readbuf.len(); + if read_bytes > 0 { + let mut cur_pos = 0; + while cur_pos < read_bytes { + // every pass through this loop we either + // 1: create a chunk + // OR + // 2: consume the entire buffer + let chunk_buf_copy_start = cur_pos; + // skip the minimum chunk size + // and noting that the hash has a window size of 64 + // so we should be careful to skip only minimum_chunk - 64 - 1 + if chunker.cur_chunk_len < chunker.minimum_chunk - MAX_WINDOW_SIZE { + let max_advance = min( + chunker.minimum_chunk + - chunker.cur_chunk_len + - MAX_WINDOW_SIZE + - 1, + read_bytes - cur_pos, + ); + cur_pos += max_advance; + chunker.cur_chunk_len += max_advance; + } + let mut consume_len; + let mut create_chunk = false; + // find a chunk boundary after minimum chunk + + // If we have a lot of data, don't read all the way to the end when we'll stop reading + // at the maximum chunk boundary. + let read_end = read_bytes + .min(cur_pos + chunker.maximum_chunk - chunker.cur_chunk_len); + + if let Some(boundary) = unsafe { + (*chunker.cur_hasher.0) + .next_match(&readbuf[cur_pos..read_end], chunker.mask) + } { + consume_len = boundary; + create_chunk = true; + } else { + consume_len = read_end - cur_pos; + } + + // if we hit maximum chunk we must create a chunk + if consume_len + chunker.cur_chunk_len >= chunker.maximum_chunk { + consume_len = chunker.maximum_chunk - chunker.cur_chunk_len; + create_chunk = true; + } + chunker.cur_chunk_len += consume_len; + cur_pos += consume_len; + chunker + .chunkbuf + .extend_from_slice(&readbuf[chunk_buf_copy_start..cur_pos]); + if create_chunk { + // advance the current hash index. + // we actually create a chunk when we run out of hashers + unsafe { (*chunker.cur_hasher.0).set_hash(0) }; + chunker.cur_hash_index += 1; + unsafe { + chunker.cur_hasher = HasherPointerBox( + chunker.hash.as_mut_ptr().add(chunker.cur_hash_index), + ); + } + if chunker.cur_hash_index >= chunker.hash.len() { + let res = ( + Chunk { + length: chunker.chunkbuf.len(), + hash: compute_data_hash(&chunker.chunkbuf[..]), + }, + std::mem::take(&mut chunker.chunkbuf), + ); + // reset chunk buffer state and continue to find the next chunk + chunker + .yield_queue + .send(Some(res)) + .await + .expect("Send chunk to channel error"); + + chunker.chunkbuf.clear(); + chunker.cur_hash_index = 0; + chunker.cur_hasher = + HasherPointerBox(chunker.hash.as_mut_ptr()); + } + chunker.cur_chunk_len = 0; + } + } + } + } + Some(BufferItem::Completed) => { + complete = true; + } + None => (), + } + } + + // main loop complete + if !chunker.chunkbuf.is_empty() { + let res = ( + Chunk { + length: chunker.chunkbuf.len(), + hash: compute_data_hash(&chunker.chunkbuf[..]), + }, + std::mem::take(&mut chunker.chunkbuf), + ); + chunker + .yield_queue + .send(Some(res)) + .await + .expect("Send chunk to channel error"); + } + + // signal finish + chunker + .yield_queue + .send(None) + .await + .expect("Send chunk to channel error"); + }) + } +} + +lazy_static! { + /// The static gearhash seed table. + static ref HASHER_SEED_TABLE: Vec<[u64; 256]> = { + let mut tables: Vec<[u64; 256]> = Vec::new(); + for i in 0..N_LOW_VARIANCE_CDC_CHUNKERS { + let mut rng = ChaChaRng::seed_from_u64(HASH_SEED + i as u64); + let mut bytehash: [u64; 256] = [0; 256]; + #[allow(clippy::needless_range_loop)] + for i in 0..256 { + bytehash[i] = rng.next_u64(); + } + tables.push(bytehash); + } + tables + }; +} + +fn low_variance_chunk_target( + target_chunk_size: usize, + num_hashers: usize, + data: Receiver>>, + yield_queue: Sender>, +) -> Pin> { + // We require the type to be Pinned since we do have a n + // internal pointer. (cur_hasher). + + assert_eq!(target_chunk_size.count_ones(), 1); + assert_eq!(num_hashers.count_ones(), 1); + assert!(target_chunk_size > 1); + assert!(num_hashers < target_chunk_size); + // note the strict lesser than. Combined with count_ones() == 1, + // this limits to 2^31 + assert!(target_chunk_size < u32::MAX as usize); + + let target_per_hash_chunk_size = target_chunk_size / num_hashers; + + let mask = (target_per_hash_chunk_size - 1) as u64; + // we will like to shift the mask left by a bunch since the right + // bits of the gear hash are affected by only a small number of bytes + // really. we just shift it all the way left. + let mask = mask << mask.leading_zeros(); + let minimum_chunk = target_chunk_size / MINIMUM_CHUNK_DIVISOR; + let maximum_chunk = target_chunk_size * MAXIMUM_CHUNK_MULTIPLIER; + + let mut hashers: Vec = Vec::new(); + assert!(num_hashers <= HASHER_SEED_TABLE.len()); + for t in HASHER_SEED_TABLE.chunks(1) { + hashers.push(gearhash::Hasher::new(&t[0])); + if hashers.len() == num_hashers { + break; + } + } + + assert!(maximum_chunk > minimum_chunk); + assert!(!hashers.is_empty()); + let num_hashes = hashers.len(); + let mut res = Box::pin(LowVarianceChunker { + hash: hashers, + minimum_chunk: minimum_chunk / num_hashes, + maximum_chunk: maximum_chunk / num_hashes, + mask, + // generator state init + chunkbuf: Vec::with_capacity(maximum_chunk), + cur_chunk_len: 0, + cur_hasher: HasherPointerBox(std::ptr::null_mut()), + cur_hash_index: 0, + data_queue: data, + yield_queue, + }); + // initialize cur_hasher + unsafe { + let mut_ref: Pin<&mut _> = Pin::as_mut(&mut res); + let mut_ref = Pin::get_unchecked_mut(mut_ref); + mut_ref.cur_hasher = HasherPointerBox(mut_ref.hash.as_mut_ptr()); + } + + res +} + +pub fn chunk_target_default( + data: Receiver>>, + yield_queue: Sender>, +) -> JoinHandle<()> { + let chunker = low_variance_chunk_target( + TARGET_CDC_CHUNK_SIZE, + N_LOW_VARIANCE_CDC_CHUNKERS, + data, + yield_queue, + ); + + LowVarianceChunker::run(Mutex::new(chunker)) +} diff --git a/data/src/clean.rs b/data/src/clean.rs new file mode 100644 index 00000000..90ca681e --- /dev/null +++ b/data/src/clean.rs @@ -0,0 +1,620 @@ +use crate::chunking::{chunk_target_default, ChunkYieldType}; +use crate::configurations::FileQueryPolicy; +use crate::constants::MIN_SPACING_BETWEEN_GLOBAL_DEDUP_QUERIES; +use crate::data_processing::{register_new_cas_block, CASDataAggregator}; +use crate::errors::{ + DataProcessingError::{self, *}, + Result, +}; +use crate::metrics::FILTER_BYTES_CLEANED; +use crate::remote_shard_interface::RemoteShardInterface; +use crate::repo_salt::RepoSalt; +use crate::small_file_determination::{is_file_passthrough, is_possible_start_to_text_file}; +use crate::PointerFile; + +use cas_client::Staging; +use lazy_static::lazy_static; +use mdb_shard::file_structs::{FileDataSequenceEntry, FileDataSequenceHeader, MDBFileInfo}; +use mdb_shard::shard_file_reconstructor::FileReconstructor; +use mdb_shard::{hash_is_global_dedup_eligible, ShardFileManager}; +use merkledb::aggregate_hashes::file_node_hash; +use merkledb::constants::TARGET_CAS_BLOCK_SIZE; +use merklehash::MerkleHash; +use std::collections::HashMap; +use std::mem::take; +use std::ops::DerefMut; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::sync::mpsc::error::TryRecvError; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::sync::Mutex; +use tokio::task::{JoinHandle, JoinSet}; +use tracing::{debug, error, info, warn}; + +// Chunking is the bottleneck, changing batch size doesn't have a big impact. +lazy_static! { + pub static ref DEDUP_CHUNK_BATCH_SIZE: usize = std::env::var("XET_DEDUP_BATCHSIZE") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(1); +} + +pub enum BufferItem { + Value(T), + Completed, +} + +#[derive(Default, Debug)] +struct DedupFileTrackingInfo { + file_hashes: Vec<(MerkleHash, usize)>, + file_info: Vec, + current_cas_file_info_indices: Vec, + file_size: u64, + current_cas_block_hashes: HashMap, + cas_data: CASDataAggregator, +} + +pub struct Cleaner { + // Configurations + small_file_threshold: usize, + enable_global_dedup_queries: bool, + cas_prefix: String, + repo_salt: Option, + + // Utils + shard_manager: Arc, + remote_shards: Arc, + cas: Arc, + + // External Data + global_cas_data: Arc>, + + // Internal workers + chunk_data_queue: Sender>>, + chunking_worker: Mutex>>, + dedup_worker: Mutex>>, + + // Internal Data + tracking_info: Mutex, + small_file_buffer: Mutex>>, + + // Auxiliary info + file_name: Option, +} + +impl Cleaner { + #[allow(clippy::too_many_arguments)] + pub async fn new( + small_file_threshold: usize, + enable_global_dedup_queries: bool, + cas_prefix: String, + repo_salt: Option, + shard_manager: Arc, + remote_shards: Arc, + cas: Arc, + cas_data: Arc>, + buffer_size: usize, + file_name: Option<&Path>, + ) -> Result> { + let (data_p, data_c) = channel::>>(buffer_size); + + let (chunk_p, chunk_c) = channel::>(buffer_size); + + let chunker = chunk_target_default(data_c, chunk_p); + + let cleaner = Arc::new(Cleaner { + small_file_threshold, + enable_global_dedup_queries, + cas_prefix, + repo_salt, + shard_manager, + remote_shards, + cas, + global_cas_data: cas_data, + chunk_data_queue: data_p, + chunking_worker: Mutex::new(Some(chunker)), + dedup_worker: Mutex::new(None), + tracking_info: Mutex::new(Default::default()), + small_file_buffer: Mutex::new(Some(Vec::with_capacity(small_file_threshold))), + file_name: file_name.map(|f| f.to_owned()), + }); + + Self::run(cleaner.clone(), chunk_c).await; + + Ok(cleaner) + } + + pub async fn add_bytes(&self, data: Vec) -> Result<()> { + self.task_is_running().await?; + + if !self.check_passthrough_status(&data).await? { + self.add_data_to_chunking(BufferItem::Value(data)).await? + } + + Ok(()) + } + + pub async fn result(&self) -> Result { + self.finish().await?; + + let mut small_file_buffer = self.small_file_buffer.lock().await; + if let Some(buffer) = small_file_buffer.take() { + return String::from_utf8(buffer).map_err(DataProcessingError::from); + } + + self.to_pointer_file().await + } + + async fn run(cleaner: Arc, mut chunks: Receiver>) { + let cleaner_clone = cleaner.clone(); + let dedup_task = tokio::spawn(async move { + loop { + let mut chunk_vec = Vec::with_capacity(*DEDUP_CHUNK_BATCH_SIZE); + + let mut finished = false; + + for _ in 0..*DEDUP_CHUNK_BATCH_SIZE { + match chunks.try_recv() { + Ok(Some(chunk)) => chunk_vec.push(chunk), + Ok(None) | Err(TryRecvError::Disconnected) => { + finished = true; + break; + } + Err(TryRecvError::Empty) => { + if chunk_vec.is_empty() { + // need to wait a bit to make sure at least one chunk to process + match chunks.recv().await.flatten() { + Some(chunk) => chunk_vec.push(chunk), + None => { + finished = true; + } + } + } + break; + } + } + } + + if !chunk_vec.is_empty() { + let res = cleaner_clone.dedup(&chunk_vec).await; + if res.is_err() { + error!("Clean task error: {res:?}"); + break; + } + } + + if finished { + break; + } + } + }); + + let mut worker = cleaner.dedup_worker.lock().await; + + *worker = Some(dedup_task); + } + + async fn task_is_running(&self) -> Result<()> { + let dedup_worker = self.dedup_worker.lock().await; + + let chunking_worker = self.chunking_worker.lock().await; + + if dedup_worker.is_none() || chunking_worker.is_none() { + return Err(CleanTaskError("no active clean task".to_owned())); + }; + + Ok(()) + } + + async fn add_data_to_chunking(&self, it: BufferItem>) -> Result<()> { + self.chunk_data_queue + .send(it) + .await + .map_err(|e| InternalError(format!("{e}")))?; + + Ok(()) + } + + /// Check passthrough condition of data. + /// Return true if the incoming data is already processed inside, + /// otherwise return false and let the caller to handle the data. + async fn check_passthrough_status(&self, data: &[u8]) -> Result { + let mut small_file_buffer = self.small_file_buffer.lock().await; + + if let Some(mut buffer) = small_file_buffer.take() { + buffer.extend_from_slice(data); + + if !is_possible_start_to_text_file(&buffer) || buffer.len() >= self.small_file_threshold + { + self.add_data_to_chunking(BufferItem::Value(buffer)).await?; + + // not passthrough, but just sent all buffered data + incoming data to chunker + return Ok(true); + } + + *small_file_buffer = Some(buffer); + + // may be passthrough, keep accumulating + return Ok(true); + } + + // not passthrough, already sent all buffered data to chunker + Ok(false) + } + + async fn dedup(&self, chunks: &[ChunkYieldType]) -> Result<()> { + info!("Dedup {} chunks", chunks.len()); + let mut tracking_info = self.tracking_info.lock().await; + + let enable_global_dedup = self.enable_global_dedup_queries; + let salt = self.repo_salt.unwrap_or_default(); + + // Last chunk queried. + let mut last_chunk_index_queried = isize::MIN; + + // All the previous chunk are stored here, use it as the global chunk index start. + let global_chunk_index_start = tracking_info.file_hashes.len(); + + let chunk_hashes = Vec::from_iter(chunks.iter().map(|(c, _)| c.hash)); + + // Now, parallelize the querying of potential new shards on the server end with + // querying for dedup information of the chunks, which are the two most expensive + // parts of the process. Then when we go into the next section, everything is essentially + // a local lookup table so the remaining work should be quite fast. + + // This holds the results of the dedup queries. + let mut deduped_blocks = vec![None; chunks.len()]; + + // Do at most two passes; 1) with global dedup querying possibly enabled, and 2) possibly rerunning + // if the global dedup query came back with a new shard. + + for first_pass in [true, false] { + // Set up a join set for tracking any global dedup queries. + let mut global_dedup_queries = JoinSet::::new(); + + // Now, go through and test all of these for whether or not they can be deduplicated. + let mut local_chunk_index = 0; + while local_chunk_index < chunks.len() { + let global_chunk_index = global_chunk_index_start + local_chunk_index; + + // First check to see if we don't already know what these blocks are from a previous pass. + if let Some((n_deduped, _)) = &deduped_blocks[local_chunk_index] { + local_chunk_index += n_deduped; + } else if let Some((n_deduped, fse)) = self + .shard_manager + .chunk_hash_dedup_query(&chunk_hashes[local_chunk_index..], None) + .await? + { + if !first_pass { + // This means new shards were discovered. + debug!("clean_file ({:?}): {n_deduped} chunks deduped against shard discovered through global dedup.", self.file_name); + } + deduped_blocks[local_chunk_index] = Some((n_deduped, fse)); + local_chunk_index += n_deduped; + + // Now see if we can issue a background query against the global dedup server to see if + // any shards are present that give us more dedup ability. + // + // If we've already queried these against the global dedup, then we can proceed on without + // re-querying anything. Only doing this on the first pass also gaurantees that in the case of errors + // on shard retrieval, we don't get stuck in a loop trying to download and reprocess. + } else { + if enable_global_dedup // Is enabled + && first_pass // Have we seen this on the previous pass? If so, skip. + && (global_chunk_index == 0 // Query all hashes on first iteration. + || hash_is_global_dedup_eligible(&chunk_hashes[local_chunk_index])) + && (global_chunk_index as isize // Limit by enforcing at least 4MB between chunk queries. + >= last_chunk_index_queried + MIN_SPACING_BETWEEN_GLOBAL_DEDUP_QUERIES as isize) + { + // Now, query for a global dedup shard in the background to make sure that all the rest of this can continue. + let remote_shards = self.remote_shards.clone(); + let query_chunk = chunk_hashes[local_chunk_index]; + + let file_name = self.file_name.clone(); + + global_dedup_queries.spawn(async move { + let Ok(query_result) = remote_shards.query_dedup_shard_by_chunk(&query_chunk, &salt).await.map_err(|e| { + debug!("Error encountered attempting to query global dedup table: {e:?}; ignoring."); + e + }) + else { return false; }; + + let Some(shard_hash) = query_result else { + debug!("Queried shard for global dedup with hash {query_chunk:?}; nothing found."); + return false; + }; + + // Okay, we have something, so go ahead and download it in the background. + debug!("global dedup: {file_name:?} deduplicated by shard {shard_hash}; downloading."); + let Ok(_) = remote_shards.download_and_register_shard(&shard_hash).await.map_err(|e| { + warn!("Error encountered attempting to download and register shard {shard_hash} for deduplication : {e:?}; ignoring."); + e + }) + else { return false; }; + + debug!("global dedup: New shard {shard_hash} can be used for deduplication of {file_name:?}; reprocessing file."); + + true + }); + + last_chunk_index_queried = global_chunk_index as isize + } + + local_chunk_index += 1; + } + } + + // Now, see if any of the chunk queries have completed. + let mut has_new_shards = false; + if first_pass { + while let Some(shard_probe_task) = global_dedup_queries.join_next().await { + has_new_shards |= shard_probe_task?; + } + } + + // If we have no new shards, then we're good to go. + if !has_new_shards { + break; + } else { + debug!( + "New shard(s) available for dedup on {:?}; reprocessing chunks.", + self.file_name + ); + } + } + + // Record all the file hashes. + tracking_info + .file_hashes + .extend(chunks.iter().map(|(c, b)| (c.hash, b.len()))); + + // Now, go through and process all the data. + let mut cur_idx = 0; + + while cur_idx < chunks.len() { + let mut n_bytes = 0; + + if let Some((n_deduped, fse)) = deduped_blocks[cur_idx].take() { + // We found one or more chunk hashes present in a cas block somewhere. + + // Update all the metrics. + #[allow(clippy::needless_range_loop)] + for i in cur_idx..(cur_idx + n_deduped) { + n_bytes += chunks[i].1.len(); + } + tracking_info.file_size += n_bytes as u64; + + // Do we modify the previous entry as this is the next logical chunk, or do we + // start a new entry? + if !tracking_info.file_info.is_empty() + && tracking_info.file_info.last().unwrap().cas_hash == fse.cas_hash + && tracking_info.file_info.last().unwrap().chunk_byte_range_end + == fse.chunk_byte_range_start + { + // This block is the contiguous continuation of the last entry + let last_entry = tracking_info.file_info.last_mut().unwrap(); + last_entry.unpacked_segment_bytes += n_bytes as u32; + last_entry.chunk_byte_range_end += n_bytes as u32; + } else { + // This block is new + tracking_info.file_info.push(fse); + } + + cur_idx += n_deduped; + } else { + let (chunk, bytes) = &chunks[cur_idx]; + + n_bytes = chunks[cur_idx].1.len(); + tracking_info.file_size += n_bytes as u64; + + // This is new data. + let add_new_data; + + if let Some(idx) = tracking_info.current_cas_block_hashes.get(&chunk.hash) { + let (_, (data_lb, data_ub)) = tracking_info.cas_data.chunks[*idx]; + + // This chunk will get the CAS hash updated when the local CAS block + // is full and registered. + let file_info_len = tracking_info.file_info.len(); + tracking_info + .current_cas_file_info_indices + .push(file_info_len); + + tracking_info.file_info.push(FileDataSequenceEntry::new( + MerkleHash::default(), + n_bytes, + data_lb, + data_ub, + )); + add_new_data = false; + } else if !tracking_info.file_info.is_empty() + && tracking_info.file_info.last().unwrap().cas_hash == MerkleHash::default() + && tracking_info.file_info.last().unwrap().chunk_byte_range_end as usize + == tracking_info.cas_data.data.len() + { + // This is the next chunk in the CAS block + // we're building, in which case we can just modify the previous entry. + let last_entry = tracking_info.file_info.last_mut().unwrap(); + last_entry.unpacked_segment_bytes += n_bytes as u32; + last_entry.chunk_byte_range_end += n_bytes as u32; + add_new_data = true; + } else { + // This block is unrelated to the previous one. + // This chunk will get the CAS hash updated when the local CAS block + // is full and registered. + let file_info_len = tracking_info.file_info.len(); + tracking_info + .current_cas_file_info_indices + .push(file_info_len); + + let cas_data_len = tracking_info.cas_data.data.len(); + tracking_info.file_info.push(FileDataSequenceEntry::new( + MerkleHash::default(), + n_bytes, + cas_data_len, + cas_data_len + n_bytes, + )); + add_new_data = true; + } + + if add_new_data { + // Add in the chunk and cas information. + let cas_data_chunks_len = tracking_info.cas_data.chunks.len(); + tracking_info + .current_cas_block_hashes + .insert(chunk.hash, cas_data_chunks_len); + + let cas_data_len = tracking_info.cas_data.data.len(); + tracking_info + .cas_data + .chunks + .push((chunk.hash, (cas_data_len, cas_data_len + n_bytes))); + tracking_info.cas_data.data.extend(bytes); + + if tracking_info.cas_data.data.len() > TARGET_CAS_BLOCK_SIZE { + let cas_hash = register_new_cas_block( + &mut tracking_info.cas_data, + &self.shard_manager, + &self.cas, + &self.cas_prefix, + ) + .await?; + + for i in take(&mut tracking_info.current_cas_file_info_indices) { + tracking_info.file_info[i].cas_hash = cas_hash; + } + tracking_info.current_cas_block_hashes.clear(); + } + } + + // Next round. + cur_idx += 1; + } + } + + Ok(()) + } + + async fn finish(&self) -> Result<()> { + self.task_is_running().await?; + + // check if there is remaining data in buffer + let mut small_file_buffer = self.small_file_buffer.lock().await; + if let Some(buffer) = small_file_buffer.take() { + if !is_file_passthrough(&buffer, self.small_file_threshold) { + self.add_data_to_chunking(BufferItem::Value(buffer)).await?; + } else { + // put back for return value + *small_file_buffer = Some(buffer); + } + } + + // signal finish + self.add_data_to_chunking(BufferItem::Completed).await?; + + let mut chunking_worker = self.chunking_worker.lock().await; + if let Some(task) = chunking_worker.take() { + task.await.map_err(|e| InternalError(format!("{e:?}")))?; + } + + let mut dedup_worker = self.dedup_worker.lock().await; + if let Some(task) = dedup_worker.take() { + task.await.map_err(|e| InternalError(format!("{e:?}")))?; + } + + Ok(()) + } + + async fn summarize_dedup_info(&self) -> Result<(MerkleHash, u64)> { + let mut tracking_info = self.tracking_info.lock().await; + + let file_hash = file_node_hash( + &tracking_info.file_hashes, + &self.repo_salt.unwrap_or_default(), + )?; + + let file_size = tracking_info.file_size; + + // Is the file registered already? If so, nothing needs to be added now. + let file_already_registered = match self.remote_shards.file_query_policy { + FileQueryPolicy::LocalFirst | FileQueryPolicy::LocalOnly => self + .shard_manager + .get_file_reconstruction_info(&file_hash) + .await? + .is_some(), + FileQueryPolicy::ServerOnly => false, + }; + + if !file_already_registered { + // Put an accumulated data into the struct-wide cas block for building a future chunk. + let mut cas_data_accumulator = self.global_cas_data.lock().await; + + let shift = cas_data_accumulator.data.len() as u32; + cas_data_accumulator + .data + .append(&mut tracking_info.cas_data.data); + cas_data_accumulator + .chunks + .append(&mut tracking_info.cas_data.chunks); + let new_file_info = MDBFileInfo { + metadata: FileDataSequenceHeader::new(file_hash, tracking_info.file_info.len()), + segments: tracking_info + .file_info + .iter() + .map(|fi| { + // If it's in this new cas chunk, shift everything. + let s = if fi.cas_hash == MerkleHash::default() { + shift + } else { + 0 + }; + + let mut new_fi = fi.clone(); + new_fi.chunk_byte_range_start += s; + new_fi.chunk_byte_range_end += s; + + new_fi + }) + .collect(), + }; + cas_data_accumulator.pending_file_info.push(( + new_file_info, + tracking_info.current_cas_file_info_indices.clone(), + )); + + if cas_data_accumulator.data.len() >= TARGET_CAS_BLOCK_SIZE { + let mut new_cas_data = take(cas_data_accumulator.deref_mut()); + drop(cas_data_accumulator); // Release the lock. + register_new_cas_block( + &mut new_cas_data, + &self.shard_manager, + &self.cas, + &self.cas_prefix, + ) + .await?; + } else { + drop(cas_data_accumulator); + } + } + // we only add to the counters if we see changes + FILTER_BYTES_CLEANED.inc_by(file_size); + + *tracking_info = Default::default(); + + Ok((file_hash, file_size)) + } + + async fn to_pointer_file(&self) -> Result { + let (hash, filesize) = self.summarize_dedup_info().await?; + let pointer_file = PointerFile::init_from_info( + &self + .file_name + .clone() + .map(|f| f.to_str().unwrap_or_default().to_owned()) + .unwrap_or_default(), + &hash.hex(), + filesize, + ); + Ok(pointer_file.to_string()) + } +} diff --git a/data/src/configurations.rs b/data/src/configurations.rs new file mode 100644 index 00000000..51217e8d --- /dev/null +++ b/data/src/configurations.rs @@ -0,0 +1,137 @@ +use crate::errors::Result; +use crate::repo_salt::RepoSalt; +use std::path::PathBuf; +use std::str::FromStr; + +#[derive(Debug)] +pub enum Endpoint { + Server(String), + FileSystem(PathBuf), +} + +#[derive(Debug)] +pub struct Auth { + pub user_id: String, + pub login_id: String, +} + +#[derive(Debug)] +pub struct CacheConfig { + pub cache_directory: PathBuf, + pub cache_size: u64, + pub cache_blocksize: u64, +} + +#[derive(Debug)] +pub struct StorageConfig { + pub endpoint: Endpoint, + pub auth: Auth, + pub prefix: String, + pub cache_config: Option, + pub staging_directory: Option, +} + +#[derive(Debug)] +pub struct DedupConfig { + pub repo_salt: Option, + pub small_file_threshold: usize, + pub global_dedup_policy: GlobalDedupPolicy, +} + +#[derive(Debug)] +pub struct RepoInfo { + pub repo_paths: Vec, +} + +#[derive(PartialEq, Default, Clone, Debug, Copy)] +pub enum FileQueryPolicy { + /// Query local first, then the shard server. + #[default] + LocalFirst, + + /// Only query the server; ignore local shards. + ServerOnly, + + /// Only query local shards. + LocalOnly, +} + +impl FromStr for FileQueryPolicy { + type Err = std::io::Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "local_first" => Ok(FileQueryPolicy::LocalFirst), + "server_only" => Ok(FileQueryPolicy::ServerOnly), + "local_only" => Ok(FileQueryPolicy::LocalOnly), + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid file smudge policy, should be one of local_first, server_only, local_only: {}", s), + )), + } + } +} + +#[derive(PartialEq, Default, Clone, Debug, Copy)] +pub enum GlobalDedupPolicy { + /// Never query for new shards using chunk hashes. + Never, + + /// Only query for new shards when using direct file access methods like `xet cp` + #[default] + OnDirectAccess, + + /// Always query for new shards by chunks (not recommended except for testing) + Always, +} + +impl FromStr for GlobalDedupPolicy { + type Err = std::io::Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "never" => Ok(GlobalDedupPolicy::Never), + "direct_only" => Ok(GlobalDedupPolicy::OnDirectAccess), + "always" => Ok(GlobalDedupPolicy::Always), + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid global dedup query policy, should be one of never, direct_only, always: {}", s), + )), + } + } +} + +#[derive(Debug)] +pub struct TranslatorConfig { + pub file_query_policy: FileQueryPolicy, + pub cas_storage_config: StorageConfig, + pub shard_storage_config: StorageConfig, + pub dedup_config: Option, + pub repo_info: Option, +} + +impl TranslatorConfig { + pub fn validate(&self) -> Result<()> { + if let Endpoint::FileSystem(path) = &self.cas_storage_config.endpoint { + std::fs::create_dir_all(path)?; + } + if let Some(cache) = &self.cas_storage_config.cache_config { + std::fs::create_dir_all(&cache.cache_directory)?; + } + if let Some(path) = &self.cas_storage_config.staging_directory { + std::fs::create_dir_all(path)?; + } + + if let Endpoint::FileSystem(path) = &self.shard_storage_config.endpoint { + std::fs::create_dir_all(path)?; + } + if let Some(cache) = &self.shard_storage_config.cache_config { + std::fs::create_dir_all(&cache.cache_directory)?; + } + if let Some(path) = &self.shard_storage_config.staging_directory { + std::fs::create_dir_all(path)?; + } + + Ok(()) + } +} diff --git a/data/src/constants.rs b/data/src/constants.rs new file mode 100644 index 00000000..08a35272 --- /dev/null +++ b/data/src/constants.rs @@ -0,0 +1,45 @@ +use lazy_static::lazy_static; + +lazy_static! { + // The xet library version. + pub static ref XET_VERSION: String = + std::env::var("XET_VERSION").unwrap_or_else(|_| CURRENT_VERSION.to_string()); + /// The maximum number of simultaneous download streams + pub static ref MAX_CONCURRENT_DOWNLOADS: usize = std::env::var("XET_CONCURRENT_DOWNLOADS").ok().and_then(|s| s.parse().ok()).unwrap_or(8); + /// The maximum number of simultaneous upload streams + pub static ref MAX_CONCURRENT_UPLOADS: usize = std::env::var("XET_CONCURRENT_UPLOADS").ok().and_then(|s| s.parse().ok()).unwrap_or(8); +} + +/// The maximum git filter protocol packet size +pub const GIT_MAX_PACKET_SIZE: usize = 65516; + +/// We put a limit on the pointer file size so that +/// we don't ever try to read a whole giant blob into memory when +/// trying to clean or smudge. +/// See gitxetcore::data::pointer_file for the explanation for this limit. +pub const POINTER_FILE_LIMIT: usize = 150; + +/// If a file has size smaller than this threshold, AND if it "looks-like" +/// text, we interpret this as a text file and passthrough the file, letting +/// git handle it. See `small_file_determination.rs` for details. +/// +/// We set this to be 1 less than a constant multiple of the GIT_MAX_PACKET_SIZE +/// so we can read exactly up to that multiple of packets to determine if it +/// is a small file. +pub const SMALL_FILE_THRESHOLD: usize = 4 * GIT_MAX_PACKET_SIZE - 1; + +// Salt is 256-bit in length. +pub const REPO_SALT_LEN: usize = 32; + +// Approximately 4 MB min spacing between global dedup queries. Calculated by 4MB / TARGET_CHUNK_SIZE +pub const MIN_SPACING_BETWEEN_GLOBAL_DEDUP_QUERIES: usize = 256; + +/// scheme for a local filesystem based CAS server +pub const LOCAL_CAS_SCHEME: &str = "local://"; + +/// The current version +pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Maximum number of entries in the file construction cache +/// which stores File Hash -> reconstruction instructions +pub const FILE_RECONSTRUCTION_CACHE_SIZE: usize = 65536; diff --git a/data/src/data_processing.rs b/data/src/data_processing.rs new file mode 100644 index 00000000..1c7bee0b --- /dev/null +++ b/data/src/data_processing.rs @@ -0,0 +1,367 @@ +use crate::cas_interface::{create_cas_client, data_from_chunks_to_writer, slice_object_range}; +use crate::clean::Cleaner; +use crate::configurations::*; +use crate::constants::MAX_CONCURRENT_UPLOADS; +use crate::errors::*; +use crate::metrics::FILTER_CAS_BYTES_PRODUCED; +use crate::remote_shard_interface::RemoteShardInterface; +use crate::shard_interface::create_shard_manager; +use crate::PointerFile; + +use cas_client::Staging; +use mdb_shard::cas_structs::{CASChunkSequenceEntry, CASChunkSequenceHeader, MDBCASInfo}; +use mdb_shard::file_structs::MDBFileInfo; +use mdb_shard::ShardFileManager; +use merkledb::aggregate_hashes::cas_node_hash; +use merkledb::ObjectRange; +use merklehash::MerkleHash; +use std::mem::take; +use std::ops::DerefMut; +use std::path::Path; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{error, info_span}; +use tracing_futures::Instrument; + +#[derive(Default, Debug)] +pub struct CASDataAggregator { + pub data: Vec, + pub chunks: Vec<(MerkleHash, (usize, usize))>, + // The file info of files that are still being processed. + // As we're building this up, we assume that all files that do not have a size in the header are + // not finished yet and thus cannot be uploaded. + // + // All the cases the default hash for a cas info entry will be filled in with the cas hash for + // an entry once the cas block is finalized and uploaded. These correspond to the indices given + // alongwith the file info. + // This tuple contains the file info (which may be modified) and the divisions in the chunks corresponding + // to this file. + pub pending_file_info: Vec<(MDBFileInfo, Vec)>, +} + +impl CASDataAggregator { + pub fn is_empty(&self) -> bool { + self.data.is_empty() && self.chunks.is_empty() && self.pending_file_info.is_empty() + } +} + +/// Manages the translation of files between the +/// MerkleDB / pointer file format and the materialized version. +/// +/// This class handles the clean and smudge options. +pub struct PointerFileTranslator { + /* ----- Configurations ----- */ + config: TranslatorConfig, + + /* ----- Utils ----- */ + shard_manager: Arc, + remote_shards: Arc, + cas: Arc, + + /* ----- Deduped data shared across files ----- */ + global_cas_data: Arc>, +} + +// Constructors +impl PointerFileTranslator { + pub async fn new(config: TranslatorConfig) -> Result { + let cas_client = create_cas_client(&config.cas_storage_config, &config.repo_info).await?; + + let shard_manager = Arc::new(create_shard_manager(&config.shard_storage_config).await?); + + let remote_shards = { + if let Some(dedup) = &config.dedup_config { + RemoteShardInterface::new( + config.file_query_policy, + &config.shard_storage_config, + Some(shard_manager.clone()), + Some(cas_client.clone()), + dedup.repo_salt, + ) + .await? + } else { + RemoteShardInterface::new_query_only( + config.file_query_policy, + &config.shard_storage_config, + ) + .await? + } + }; + + Ok(Self { + config, + shard_manager, + remote_shards, + cas: cas_client, + global_cas_data: Default::default(), + }) + } +} + +/// Clean operations +impl PointerFileTranslator { + /// Start to clean one file. When cleaning multiple files, each file should + /// be associated with one Cleaner. This allows to launch multiple clean task + /// simultaneously. + /// The caller is responsible for memory usage management, the parameter "buffer_size" + /// indicates the maximum number of Vec in the internal buffer. + pub async fn start_clean( + &self, + buffer_size: usize, + file_name: Option<&Path>, + ) -> Result> { + let Some(ref dedup) = self.config.dedup_config else { + return Err(DataProcessingError::DedupConfigError( + "empty dedup config".to_owned(), + )); + }; + + Cleaner::new( + dedup.small_file_threshold, + matches!(dedup.global_dedup_policy, GlobalDedupPolicy::Always), + self.config.cas_storage_config.prefix.clone(), + dedup.repo_salt, + self.shard_manager.clone(), + self.remote_shards.clone(), + self.cas.clone(), + self.global_cas_data.clone(), + buffer_size, + file_name, + ) + .await + } + + pub async fn finalize_cleaning(&self) -> Result<()> { + // flush accumulated CAS data. + let mut cas_data_accumulator = self.global_cas_data.lock().await; + let mut new_cas_data = take(cas_data_accumulator.deref_mut()); + drop(cas_data_accumulator); // Release the lock. + + if !new_cas_data.is_empty() { + register_new_cas_block( + &mut new_cas_data, + &self.shard_manager, + &self.cas, + &self.config.cas_storage_config.prefix, + ) + .await?; + } + + debug_assert!(new_cas_data.is_empty()); + + self.cas.flush().await?; + + // flush accumulated memory shard. + self.shard_manager.flush().await?; + + self.upload().await?; + + Ok(()) + } + + async fn upload(&self) -> Result<()> { + // First, get all the shards prepared and load them. + let merged_shards_jh = self.remote_shards.merge_shards()?; + + // Make sure that all the uploads and everything are in a good state before proceeding with + // anything changing the remote repository. + // + // Waiting until the CAS uploads finish avoids the following scenario: + // 1. user 1 commit file A and push, but network drops after + // sync_notes_to_remote before uploading cas finishes. + // 2. user 2 tries to git add the same file A, which on filter pulls in + // the new notes, and file A is 100% deduped so no CAS blocks will be created, + // and push. + // + // This results in a bad repo state. + self.upload_cas().await?; + + // Get a list of all the merged shards in order to upload them. + let merged_shards = merged_shards_jh.await??; + + // Now, these need to be sent to the remote. + self.remote_shards + .upload_and_register_shards(merged_shards) + .await?; + + // Finally, we can move all the mdb shards from the session directory, which is used + // by the upload_shard task, to the cache. + self.remote_shards + .move_session_shards_to_local_cache() + .await?; + + Ok(()) + } + + async fn upload_cas(&self) -> Result<()> { + self.cas + .upload_all_staged(*MAX_CONCURRENT_UPLOADS, false) + .await?; + + Ok(()) + } +} + +/// Clean operation helpers +pub(crate) async fn register_new_cas_block( + cas_data: &mut CASDataAggregator, + shard_manager: &Arc, + cas: &Arc, + cas_prefix: &str, +) -> Result { + let cas_hash = cas_node_hash(&cas_data.chunks[..]); + + let raw_bytes_len = cas_data.data.len(); + // We now assume that the server will compress Xorbs using lz4, + // without actually compressing the data client-side. + // The accounting logic will be moved to server-side in the future. + let compressed_bytes_len = lz4::block::compress( + &cas_data.data, + Some(lz4::block::CompressionMode::DEFAULT), + false, + ) + .map(|out| out.len()) + .unwrap_or(raw_bytes_len) + .min(raw_bytes_len); + + let metadata = CASChunkSequenceHeader::new_with_compression( + cas_hash, + cas_data.chunks.len(), + raw_bytes_len, + compressed_bytes_len, + ); + + let mut pos = 0; + let chunks: Vec<_> = cas_data + .chunks + .iter() + .map(|(h, (bytes_lb, bytes_ub))| { + let size = bytes_ub - bytes_lb; + let result = CASChunkSequenceEntry::new(*h, size, pos); + pos += size; + result + }) + .collect(); + + let cas_info = MDBCASInfo { metadata, chunks }; + + let mut chunk_boundaries: Vec = Vec::with_capacity(cas_data.chunks.len()); + let mut running_sum = 0; + + for (_, s) in cas_data.chunks.iter() { + running_sum += s.1 - s.0; + chunk_boundaries.push(running_sum as u64); + } + + if !cas_info.chunks.is_empty() { + shard_manager.add_cas_block(cas_info).await?; + + cas.put( + cas_prefix, + &cas_hash, + take(&mut cas_data.data), + chunk_boundaries, + ) + .await?; + } else { + debug_assert_eq!(cas_hash, MerkleHash::default()); + } + + // Now register any new files as needed. + for (mut fi, chunk_hash_indices) in take(&mut cas_data.pending_file_info) { + for i in chunk_hash_indices { + debug_assert_eq!(fi.segments[i].cas_hash, MerkleHash::default()); + fi.segments[i].cas_hash = cas_hash; + } + + shard_manager.add_file_reconstruction_info(fi).await?; + } + + FILTER_CAS_BYTES_PRODUCED.inc_by(compressed_bytes_len as u64); + + cas_data.data.clear(); + cas_data.chunks.clear(); + cas_data.pending_file_info.clear(); + + Ok(cas_hash) +} + +/// Smudge operations +impl PointerFileTranslator { + pub async fn derive_blocks(&self, hash: &MerkleHash) -> Result> { + if let Some((file_info, _shard_hash)) = self + .remote_shards + .get_file_reconstruction_info(hash) + .await? + { + Ok(file_info + .segments + .into_iter() + .map(|s| ObjectRange { + hash: s.cas_hash, + start: s.chunk_byte_range_start as usize, + end: s.chunk_byte_range_end as usize, + }) + .collect()) + } else { + error!("File Reconstruction info for hash {hash:?} not found."); + Err(DataProcessingError::HashNotFound) + } + } + + pub async fn smudge_file_from_pointer( + &self, + pointer: &PointerFile, + writer: &mut impl std::io::Write, + range: Option<(usize, usize)>, + ) -> Result<()> { + self.smudge_file_from_hash(&pointer.hash()?, writer, range) + .await + } + + pub async fn smudge_file_from_hash( + &self, + file_id: &MerkleHash, + writer: &mut impl std::io::Write, + range: Option<(usize, usize)>, + ) -> Result<()> { + let blocks = self + .derive_blocks(file_id) + .instrument(info_span!("derive_blocks")) + .await?; + + let ranged_blocks = match range { + Some((start, end)) => { + // we expect callers to validate the range, but just in case, check it anyway. + if end < start { + let msg = format!( + "End range value requested ({end}) is less than start range value ({start})" + ); + error!(msg); + return Err(DataProcessingError::ParameterError(msg)); + } + slice_object_range(&blocks, start, end - start) + } + None => blocks, + }; + + self.data_from_chunks_to_writer(ranged_blocks, writer) + .await?; + + Ok(()) + } + + async fn data_from_chunks_to_writer( + &self, + chunks: Vec, + writer: &mut impl std::io::Write, + ) -> Result<()> { + data_from_chunks_to_writer( + &self.cas, + self.config.cas_storage_config.prefix.clone(), + chunks, + writer, + ) + .await + } +} diff --git a/data/src/errors.rs b/data/src/errors.rs new file mode 100644 index 00000000..2888eba3 --- /dev/null +++ b/data/src/errors.rs @@ -0,0 +1,85 @@ +use cas::errors::SingleflightError; +use cas_client::CasClientError; +use mdb_shard::error::MDBShardError; +use merkledb::error::MerkleDBError; +use shard_client::error::ShardClientError; +use std::string::FromUtf8Error; +use std::sync::mpsc::RecvError; +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum DataProcessingError { + #[error("File query policy configuration error: {0}")] + FileQueryPolicyError(String), + + #[error("CAS configuration error: {0}")] + CASConfigError(String), + + #[error("Shard configuration error: {0}")] + ShardConfigError(String), + + #[error("Cache configuration error: {0}")] + CacheConfigError(String), + + #[error("Deduplication configuration error: {0}")] + DedupConfigError(String), + + #[error("Clean task error: {0}")] + CleanTaskError(String), + + #[error("Internal error : {0}")] + InternalError(String), + + #[error("Synchronization error: {0}")] + SyncError(String), + + #[error("Channel error: {0}")] + ChannelRecvError(#[from] RecvError), + + #[error("MerkleDB error: {0}")] + MerkleDBError(#[from] MerkleDBError), + + #[error("MerkleDB Shard error: {0}")] + MDBShardError(#[from] MDBShardError), + + #[error("CAS service error : {0}")] + CasClientError(#[from] CasClientError), + + #[error("Shard service error: {0}")] + ShardClientError(#[from] ShardClientError), + + #[error("Subtask scheduling error: {0}")] + JoinError(#[from] tokio::task::JoinError), + + #[error("Non-small file not cleaned: {0}")] + FileNotCleanedError(#[from] FromUtf8Error), + + #[error("I/O error: {0}")] + IOError(#[from] std::io::Error), + + #[error("Hash not found")] + HashNotFound, + + #[error("Parameter error: {0}")] + ParameterError(String), + + #[error("Unable to parse string as hex hash value")] + HashStringParsingFailure(#[from] merklehash::DataHashHexParseError), + + #[error("Deprecated feature: {0}")] + DeprecatedError(String), +} + +pub type Result = std::result::Result; + +// Specific implementation for this one so that we can extract the internal error when appropriate +impl From> for DataProcessingError { + fn from(value: SingleflightError) -> Self { + let msg = format!("{value:?}"); + xet_error::error_hook(&msg); + match value { + SingleflightError::InternalError(e) => e, + _ => DataProcessingError::InternalError(format!("SingleflightError: {msg}")), + } + } +} diff --git a/data/src/lib.rs b/data/src/lib.rs new file mode 100644 index 00000000..54ad90b5 --- /dev/null +++ b/data/src/lib.rs @@ -0,0 +1,20 @@ +#![allow(dead_code)] + +mod cas_interface; +mod chunking; +mod clean; +pub mod configurations; +mod constants; +mod data_processing; +mod errors; +mod metrics; +mod pointer_file; +mod remote_shard_interface; +mod repo_salt; +mod shard_interface; +mod small_file_determination; + +pub use cas_interface::DEFAULT_BLOCK_SIZE; +pub use constants::SMALL_FILE_THRESHOLD; +pub use data_processing::PointerFileTranslator; +pub use pointer_file::PointerFile; diff --git a/data/src/metrics.rs b/data/src/metrics.rs new file mode 100644 index 00000000..3b6482db --- /dev/null +++ b/data/src/metrics.rs @@ -0,0 +1,15 @@ +use lazy_static::lazy_static; +use prometheus::{register_int_counter, IntCounter}; + +// Some of the common tracking things +lazy_static! { + pub static ref FILTER_CAS_BYTES_PRODUCED: IntCounter = register_int_counter!( + "filter_process_cas_bytes_produced", + "Number of CAS bytes produced during cleaning" + ) + .unwrap(); + pub static ref FILTER_BYTES_CLEANED: IntCounter = + register_int_counter!("filter_process_bytes_cleaned", "Number of bytes cleaned").unwrap(); + pub static ref FILTER_BYTES_SMUDGED: IntCounter = + register_int_counter!("filter_process_bytes_smudged", "Number of bytes smudged").unwrap(); +} diff --git a/data/src/pointer_file.rs b/data/src/pointer_file.rs new file mode 100644 index 00000000..654b80cc --- /dev/null +++ b/data/src/pointer_file.rs @@ -0,0 +1,325 @@ +#![cfg_attr(feature = "strict", deny(warnings))] +use crate::constants::POINTER_FILE_LIMIT; +use merklehash::{DataHashHexParseError, MerkleHash}; +use static_assertions::const_assert; +use std::{collections::BTreeMap, fs, path::Path}; +use toml::Value; +use tracing::{debug, error, warn}; + +const HEADER_PREFIX: &str = "# xet version "; +const CURRENT_VERSION: &str = "0"; + +/// A struct that wraps a Xet pointer file. +/// Xet pointer file format is a TOML file, +/// and the first line must be of the form "# xet version " +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PointerFile { + /// The version string of the pointer file + version_string: String, + + /// The initial path supplied (to a pointer file on disk) + path: String, + + /// Whether the contents represent a valid pointer file. + /// is_valid is true if and only if all of the following are true: + /// * the first line starts with HEADER_PREFIX and then a version string + /// * the whole contents are valid TOML + /// * the TOML contains a top level key "hash" that is a String + /// * the TOML contains a top level key "filesize" that is an Integer + is_valid: bool, + + /// The Merkle hash of the file pointed to by this pointer file + hash: String, + + /// The size of the file pointed to by this pointer file + filesize: u64, +} + +impl PointerFile { + pub fn init_from_string(contents: &str, path: &str) -> PointerFile { + let empty_string = "".to_string(); + + // Start out valid by default. + let mut is_valid = true; + + // Required members: hash and filesize. + // Without these, not considered valid. + let mut hash = empty_string.clone(); + let mut filesize: u64 = 0; + + let lines = contents.lines(); + let first_line: String = lines.take(1).collect(); + if !first_line.starts_with(HEADER_PREFIX) { + // not a valid pointer file - doesn't start with header: + // # xet version + is_valid = false; + return PointerFile { + version_string: empty_string, + path: path.to_string(), + is_valid, + hash, + filesize, + }; + } + + let version_string = first_line[HEADER_PREFIX.len()..].to_string(); + if version_string != CURRENT_VERSION { + warn!("Pointer file version {} encountered. Only version {} is supported. Please upgrade git-xet.", version_string, CURRENT_VERSION); + // not a valid pointer file, doesn't start with header + version string + is_valid = false; + return PointerFile { + version_string, + path: path.to_string(), + is_valid, + hash, + filesize, + }; + } + + // Validated the header -- parse as TOML. + let parsed = match contents.parse::() { + Ok(v) => v, + Err(_) => { + is_valid = false; + Value::String(empty_string) + } + }; + + match parsed.get("hash") { + Some(Value::String(s)) => { + hash = s.to_string(); + } + _ => { + // did not find hash, or + // found a non-string type for hash (unexpected) + is_valid = false; + } + } + + match parsed.get("filesize") { + Some(Value::Integer(i)) => { + if *i < 0 { + // negative int should not be possible for filesize + is_valid = false; + } + filesize = *i as u64; + } + _ => { + // did not find filesize, or + // found a non-int type for filesize (unexpected) + is_valid = false; + } + } + + PointerFile { + version_string, + path: path.to_string(), + is_valid, + hash, + filesize, + } + } + + /// Initialize a pointer file by the contents in the file. + /// This will quickly check the file size before trying to read the + /// entire file. Any I/O failure or file size exceeding a limit means + /// an invalid pointer file. + pub fn init_from_path(path: impl AsRef) -> PointerFile { + let path = path.as_ref().to_str().unwrap(); + let empty_string = "".to_string(); + + let invalid_pointer_file = || PointerFile { + version_string: empty_string.clone(), + path: path.to_owned(), + is_valid: false, + hash: empty_string, + filesize: 0, + }; + + let Ok(file_meta) = fs::metadata(path).map_err(|e| { + debug!("fs:metadata failed: {e:?}"); + e + }) else { + return invalid_pointer_file(); + }; + if file_meta.len() > POINTER_FILE_LIMIT as u64 { + debug!("filesize: {}", file_meta.len()); + return invalid_pointer_file(); + } + let Ok(contents) = fs::read_to_string(path).map_err(|e| { + debug!("fs:read_to_string failed: {e:?}"); + e + }) else { + return invalid_pointer_file(); + }; + + PointerFile::init_from_string(&contents, path) + } + + pub fn init_from_info(path: &str, hash: &str, filesize: u64) -> Self { + Self { + version_string: CURRENT_VERSION.to_string(), + path: path.to_string(), + is_valid: true, + hash: hash.to_string(), + filesize, + } + } + + pub fn is_valid(&self) -> bool { + self.is_valid + } + + pub fn hash_string(&self) -> &String { + &self.hash + } + + pub fn hash(&self) -> std::result::Result { + if self.is_valid { + MerkleHash::from_hex(&self.hash).map_err(|e| { + error!( + "Error parsing hash value in pointer file for {:?}: {e:?}", + self.path + ); + e + }) + } else { + Ok(MerkleHash::default()) + } + } + + pub fn path(&self) -> &str { + &self.path + } + pub fn filesize(&self) -> u64 { + self.filesize + } +} + +pub fn is_xet_pointer_file(data: &[u8]) -> bool { + if data.len() >= POINTER_FILE_LIMIT { + return false; + } + + let Ok(data_str) = std::str::from_utf8(data) else { + return false; + }; + + PointerFile::init_from_string(data_str, "").is_valid() +} + +impl std::fmt::Display for PointerFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !self.is_valid { + warn!("called fmt on invalid PointerFile"); + return write!(f, "# invalid pointer file"); + } + let mut contents = BTreeMap::::new(); + contents.insert("hash".to_string(), Value::String(self.hash.clone())); + assert!(self.filesize <= i64::MAX as u64); + contents.insert("filesize".to_string(), Value::Integer(self.filesize as i64)); + let contents_str = toml::ser::to_string_pretty(&contents).map_err(|e| { + warn!("Error serializing pointer file: {e}:"); + std::fmt::Error + })?; + + assert!(!self.version_string.is_empty()); + write!( + f, + "{}{}\n{}", + HEADER_PREFIX, self.version_string, contents_str + ) + } +} + +// Check pointer file size limit at compile time. +// A valid pointer file looks like below +// +// # xet version 0 +// filesize = +// hash = '<64 digit long string>' +// +// +const_assert!( + POINTER_FILE_LIMIT + >= HEADER_PREFIX.len() + CURRENT_VERSION.len() // header ++ "filesize = ".len() + "9223372036854775807".len() // the largest i64 ++ "hash = ".len() + 64 + 2 // 2 is the single quotes size ++ 2 * 3 // 3 "\n" or "\r\n" on Windows +); + +#[cfg(test)] +mod tests { + const POINTER_FILE_VERSION: &str = "0"; + use super::*; + + #[test] + fn is_valid_pointer_file() { + let empty_string = "".to_string(); + let mut test_contents = "# not a xet file\n42 is a number".to_string(); + let mut test = PointerFile::init_from_string(&test_contents, &empty_string); + assert!(!test.is_valid()); // not valid because it is missing the header prefix + + test_contents = format!("{}{}\n42 is a number", HEADER_PREFIX, POINTER_FILE_VERSION); + test = PointerFile::init_from_string(&test_contents, &empty_string); + assert!(!test.is_valid()); // not valid because it doesn't contain valid TOML + + test_contents = format!("{}{}\nfoo = 'bar'", HEADER_PREFIX, POINTER_FILE_VERSION); + test = PointerFile::init_from_string(&test_contents, &empty_string); + assert!(!test.is_valid()); // not valid because it doesn't contain hash or filesize + + test_contents = format!( + "{}{}\nhash = '12345'\nfilesize = 678", + HEADER_PREFIX, POINTER_FILE_VERSION + ); + test = PointerFile::init_from_string(&test_contents, &empty_string); + assert!(test.is_valid()); // valid + } + + #[test] + fn empty_file() { + let empty_string = "".to_string(); + let test = PointerFile::init_from_string(&empty_string, &empty_string); + assert!(!test.is_valid()); // not valid because empty file + } + + #[test] + fn parses_correctly() { + let empty_string = "".to_string(); + let test_contents = format!( + "{}{}\nhash = '12345'\nfilesize = 678", + HEADER_PREFIX, POINTER_FILE_VERSION + ); + let test = PointerFile::init_from_string(&test_contents, &empty_string); + assert!(test.is_valid()); // valid + assert_eq!(test.filesize(), 678); + assert_eq!(test.hash_string(), "12345"); + assert_eq!(test.version_string, POINTER_FILE_VERSION); + } + + #[test] + fn is_serializable_and_deserializable() { + let empty_string = "".to_string(); + let test_contents = format!( + "{}{}\nhash = '12345'\nfilesize = 678", + HEADER_PREFIX, POINTER_FILE_VERSION + ); + let test = PointerFile::init_from_string(&test_contents, &empty_string); + assert!(test.is_valid()); // valid + + // make sure we can serialize it back out to string + let serialized = test.to_string(); + + // then read it back in, and make sure it's equal to the original + let deserialized = PointerFile::init_from_string(&serialized, &empty_string); + assert_eq!(test, deserialized); + } + + #[test] + fn test_new_version() { + let empty_string = "".to_string(); + let test_contents = format!("{}{}\nhash = '12345'\nfilesize = 678", HEADER_PREFIX, "1.0"); + let test = PointerFile::init_from_string(&test_contents, &empty_string); + assert!(!test.is_valid()); // new version is not valid + } +} diff --git a/data/src/remote_shard_interface.rs b/data/src/remote_shard_interface.rs new file mode 100644 index 00000000..feb86c08 --- /dev/null +++ b/data/src/remote_shard_interface.rs @@ -0,0 +1,466 @@ +use super::configurations::{FileQueryPolicy, StorageConfig}; +use super::errors::{DataProcessingError, Result}; +use super::shard_interface::{create_shard_client, create_shard_manager}; +use crate::constants::{FILE_RECONSTRUCTION_CACHE_SIZE, MAX_CONCURRENT_UPLOADS}; +use crate::repo_salt::RepoSalt; +use cas::singleflight; +use cas_client::Staging; +use file_utils::write_all_safe; +use lru::LruCache; +use mdb_shard::constants::MDB_SHARD_MIN_TARGET_SIZE; +use mdb_shard::session_directory::consolidate_shards_in_directory; +use mdb_shard::{ + error::MDBShardError, file_structs::MDBFileInfo, shard_file_manager::ShardFileManager, + shard_file_reconstructor::FileReconstructor, MDBShardFile, +}; +use merklehash::MerkleHash; +use parutils::tokio_par_for_each; +use shard_client::ShardClientInterface; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::Mutex; +use tokio::task::JoinHandle; +use tracing::{debug, error, info}; + +pub struct RemoteShardInterface { + pub file_query_policy: FileQueryPolicy, + pub shard_prefix: String, + pub shard_cache_directory: Option, + pub shard_session_directory: Option, + + pub repo_salt: Option, + + pub cas: Option>, + pub shard_manager: Option>, + pub shard_client: Option>, + pub reconstruction_cache: + Mutex)>>, + + // A gate on downloading and registering new shards. + pub shard_downloads: Arc>, +} + +impl RemoteShardInterface { + /// Set up a lightweight version of this that can only use operations that query the remote server; + /// anything that tries to download or upload shards will cause a runtime error. + pub async fn new_query_only( + file_query_policy: FileQueryPolicy, + shard_storage_config: &StorageConfig, + ) -> Result> { + Self::new(file_query_policy, shard_storage_config, None, None, None).await + } + + pub async fn new( + file_query_policy: FileQueryPolicy, + shard_storage_config: &StorageConfig, + shard_manager: Option>, + cas: Option>, + repo_salt: Option, + ) -> Result> { + let shard_client = { + if file_query_policy != FileQueryPolicy::LocalOnly { + debug!("data_processing: Setting up file reconstructor to query shard server."); + create_shard_client(shard_storage_config).await.ok() + } else { + None + } + }; + + let shard_manager = + if file_query_policy != FileQueryPolicy::ServerOnly && shard_manager.is_none() { + Some(Arc::new(create_shard_manager(shard_storage_config).await?)) + } else { + shard_manager + }; + + Ok(Arc::new(Self { + file_query_policy, + shard_prefix: shard_storage_config.prefix.clone(), + shard_cache_directory: shard_storage_config + .cache_config + .as_ref() + .map(|cf| cf.cache_directory.clone()), + shard_session_directory: shard_storage_config.staging_directory.clone(), + repo_salt, + shard_manager, + shard_client, + reconstruction_cache: Mutex::new(LruCache::new( + std::num::NonZero::new(FILE_RECONSTRUCTION_CACHE_SIZE).unwrap(), + )), + cas, + shard_downloads: Arc::new(singleflight::Group::new()), + })) + } + + fn cas(&self) -> Result> { + let Some(cas) = self.cas.clone() else { + // Trigger error and backtrace + return Err(DataProcessingError::CASConfigError( + "tried to contact CAS service but cas client was not configured".to_owned(), + ))?; + }; + + Ok(cas) + } + + fn shard_client(&self) -> Result> { + let Some(shard_client) = self.shard_client.clone() else { + // Trigger error and backtrace + return Err(DataProcessingError::FileQueryPolicyError(format!( + "tried to contact Shard service but FileQueryPolicy was set to {:?}", + self.file_query_policy + ))); + }; + + Ok(shard_client) + } + + fn shard_manager(&self) -> Result> { + let Some(shard_manager) = self.shard_manager.clone() else { + // Trigger error and backtrace + return Err(DataProcessingError::FileQueryPolicyError(format!( + "tried to use local Shards but FileQueryPolicy was set to {:?}", + self.file_query_policy + ))); + }; + + Ok(shard_manager) + } + + fn repo_salt(&self) -> Result { + // repo salt is optional for dedup + Ok(self.repo_salt.unwrap_or_default()) + } + + fn shard_cache_directory(&self) -> Result { + let Some(cache_dir) = self.shard_cache_directory.clone() else { + return Err(DataProcessingError::ShardConfigError( + "cache directory not configured".to_owned(), + )); + }; + + Ok(cache_dir) + } + + fn shard_session_directory(&self) -> Result { + let Some(session_dir) = self.shard_session_directory.clone() else { + return Err(DataProcessingError::ShardConfigError( + "staging directory not configured".to_owned(), + )); + }; + + Ok(session_dir) + } + + async fn query_server_for_file_reconstruction_info( + &self, + file_hash: &merklehash::MerkleHash, + ) -> Result)>> { + // In this case, no remote to query + if self.file_query_policy == FileQueryPolicy::LocalOnly { + return Ok(None); + } + + Ok(self + .shard_client()? + .get_file_reconstruction_info(file_hash) + .await?) + } + + async fn get_file_reconstruction_info_impl( + &self, + file_hash: &merklehash::MerkleHash, + ) -> Result)>> { + match self.file_query_policy { + FileQueryPolicy::LocalFirst => { + let local_info = self + .shard_manager + .as_ref() + .ok_or_else(|| { + MDBShardError::SmudgeQueryPolicyError( + "Require ShardFileManager for smudge query policy other than 'server_only'" + .to_owned(), + ) + })? + .get_file_reconstruction_info(file_hash) + .await?; + + if local_info.is_some() { + Ok(local_info) + } else { + Ok(self + .query_server_for_file_reconstruction_info(file_hash) + .await?) + } + } + FileQueryPolicy::ServerOnly => { + self.query_server_for_file_reconstruction_info(file_hash) + .await + } + FileQueryPolicy::LocalOnly => Ok(self + .shard_manager + .as_ref() + .ok_or_else(|| { + MDBShardError::SmudgeQueryPolicyError( + "Require ShardFileManager for smudge query policy other than 'server_only'" + .to_owned(), + ) + })? + .get_file_reconstruction_info(file_hash) + .await?), + } + } + + pub async fn get_file_reconstruction_info( + &self, + file_hash: &merklehash::MerkleHash, + ) -> Result)>> { + { + let mut reader = self.reconstruction_cache.lock().unwrap(); + if let Some(res) = reader.get(file_hash) { + return Ok(Some(res.clone())); + } + } + let response = self.get_file_reconstruction_info_impl(file_hash).await; + match response { + Ok(None) => Ok(None), + Ok(Some(contents)) => { + // we only cache real stuff + self.reconstruction_cache + .lock() + .unwrap() + .put(*file_hash, contents.clone()); + Ok(Some(contents)) + } + Err(e) => Err(e), + } + } + + /// Probes which shards provides dedup information for a chunk. + /// Returns a list of shard hashes with key under 'prefix', + /// Err(_) if an error occured. + async fn get_dedup_shards( + &self, + chunk_hash: &[MerkleHash], + salt: &RepoSalt, + ) -> Result> { + if chunk_hash.is_empty() { + return Ok(vec![]); + } + + if let Some(shard_client) = self.shard_client.as_ref() { + debug!( + "get_dedup_shards: querying for shards with chunk {:?}", + chunk_hash[0] + ); + Ok(shard_client + .get_dedup_shards(&self.shard_prefix, chunk_hash, salt) + .await?) + } else { + Ok(vec![]) + } + } + + /// Convenience wrapper of above for single chunk query + pub async fn query_dedup_shard_by_chunk( + &self, + chunk_hash: &MerkleHash, + salt: &RepoSalt, + ) -> Result> { + Ok(self.get_dedup_shards(&[*chunk_hash], salt).await?.pop()) + } + + fn download_and_register_shard_background( + &self, + shard_hash: &MerkleHash, + ) -> Result>> { + let hex_key = shard_hash.hex(); + + let prefix = self.shard_prefix.to_owned(); + + let shard_hash = shard_hash.to_owned(); + let shard_downloads_sf = self.shard_downloads.clone(); + let shard_manager = self.shard_manager()?; + let cas = self.cas()?; + + let cache_dir = self.shard_cache_directory()?; + + Ok(tokio::spawn(async move { + if shard_manager.shard_is_registered(&shard_hash).await { + info!("download_and_register_shard: Shard {shard_hash:?} is already registered."); + return Ok(()); + } + + shard_downloads_sf + .work(&hex_key, async move { + // Download the shard in question. + let (shard_file, _) = download_shard(&cas, &prefix, &shard_hash, &cache_dir) + .await + .map_err(|e| DataProcessingError::InternalError(format!("{e:?}")))?; + + shard_manager + .register_shards_by_path(&[shard_file], true) + .await?; + + Ok(()) + }) + .await + .0?; + + Ok(()) + })) + } + + pub async fn download_and_register_shard(&self, shard_hash: &MerkleHash) -> Result<()> { + self.download_and_register_shard_background(shard_hash)? + .await? + } + + pub fn merge_shards( + &self, + ) -> Result, MDBShardError>>> { + let session_dir = self.shard_session_directory()?; + + let merged_shards_jh = tokio::spawn(async move { + consolidate_shards_in_directory(&session_dir, MDB_SHARD_MIN_TARGET_SIZE) + }); + + Ok(merged_shards_jh) + } + + pub async fn upload_and_register_shards(&self, shards: Vec) -> Result<()> { + if shards.is_empty() { + return Ok(()); + } + + let salt = self.repo_salt()?; + let cas = self.cas()?; + let cas_ref = &cas; + let shard_client = self.shard_client()?; + let shard_client_ref = &shard_client; + let shard_prefix = self.shard_prefix.clone(); + let shard_prefix_ref = &shard_prefix; + + tokio_par_for_each(shards, *MAX_CONCURRENT_UPLOADS, |si, _| async move { + // For each shard: + // 1. Upload directly to CAS. + // 2. Sync to server. + + debug!( + "Uploading shard {shard_prefix_ref}/{:?} from staging area to CAS.", + &si.shard_hash + ); + let data = std::fs::read(&si.path)?; + let data_len = data.len(); + // Upload the shard. + cas_ref + .put_bypass_stage( + shard_prefix_ref, + &si.shard_hash, + data, + vec![data_len as u64], + ) + .await?; + + debug!( + "Registering shard {shard_prefix_ref}/{:?} with shard server.", + &si.shard_hash + ); + + // That succeeded if we made it here, so now try to sync things. + shard_client_ref + .register_shard_with_salt(shard_prefix_ref, &si.shard_hash, false, &salt) + .await?; + + info!( + "Shard {shard_prefix_ref}/{:?} upload + sync completed successfully.", + &si.shard_hash + ); + + Ok(()) + }) + .await + .map_err(|e| match e { + parutils::ParallelError::JoinError => { + DataProcessingError::InternalError("Join Error".into()) + } + parutils::ParallelError::TaskError(e) => e, + })?; + + cas.flush().await?; + + Ok(()) + } + + pub async fn move_session_shards_to_local_cache(&self) -> Result<()> { + let cache_dir = self.shard_cache_directory()?; + let session_dir = self.shard_session_directory()?; + + let dir_walker = std::fs::read_dir(session_dir)?; + + for file in dir_walker.flatten() { + let file_type = file.file_type()?; + let file_path = file.path(); + if !file_type.is_file() || !is_shard_file(&file_path) { + continue; + } + + std::fs::rename(&file_path, cache_dir.join(file_path.file_name().unwrap()))?; + } + + Ok(()) + } +} + +/// Construct a file name for a MDBShard stored under cache and session dir. +fn local_shard_name(hash: &MerkleHash) -> PathBuf { + PathBuf::from(hash.to_string()).with_extension("mdb") +} + +/// Quickly validate the shard extension +fn is_shard_file(path: &Path) -> bool { + path.extension().and_then(OsStr::to_str) == Some("mdb") +} + +// Download a shard to local cache if not exists. +// Returns the path to the downloaded file and the number of bytes transferred. +// Returns the path to the existing file and 0 (transferred byte) if exists. +async fn download_shard( + cas: &Arc, + prefix: &str, + shard_hash: &MerkleHash, + dest_dir: &Path, +) -> Result<(PathBuf, usize)> { + let shard_name = local_shard_name(shard_hash); + let dest_file = dest_dir.join(&shard_name); + + if dest_file.exists() { + #[cfg(debug_assertions)] + { + MDBShardFile::load_from_file(&dest_file)?.verify_shard_integrity_debug_only(); + } + debug!( + "download_shard: shard file {shard_name:?} already present in local cache, skipping download." + ); + return Ok((dest_file, 0)); + } else { + debug!( + "download_shard: shard file {shard_name:?} does not exist in local cache, downloading from cas." + ); + } + + let bytes: Vec = match cas.get(prefix, shard_hash).await { + Err(e) => { + error!("Error attempting to download shard {prefix}/{shard_hash:?}: {e:?}"); + Err(e)? + } + Ok(data) => data, + }; + + info!("Downloaded shard {prefix}/{shard_hash:?}."); + + write_all_safe(&dest_file, &bytes)?; + + Ok((dest_file, bytes.len())) +} diff --git a/data/src/repo_salt.rs b/data/src/repo_salt.rs new file mode 100644 index 00000000..27b87b38 --- /dev/null +++ b/data/src/repo_salt.rs @@ -0,0 +1,13 @@ +use crate::constants::REPO_SALT_LEN; +use rand::{RngCore, SeedableRng}; +use rand_chacha::ChaCha20Rng; + +pub type RepoSalt = [u8; REPO_SALT_LEN]; + +pub fn generate_repo_salt() -> RepoSalt { + let mut rng = ChaCha20Rng::from_entropy(); + let mut salt = [0u8; REPO_SALT_LEN]; + rng.fill_bytes(&mut salt); + + salt +} diff --git a/data/src/shard_interface.rs b/data/src/shard_interface.rs new file mode 100644 index 00000000..3270633d --- /dev/null +++ b/data/src/shard_interface.rs @@ -0,0 +1,55 @@ +use super::configurations::{Endpoint::*, StorageConfig}; +use super::errors::Result; +use crate::constants::XET_VERSION; +use mdb_shard::ShardFileManager; +use shard_client::{GrpcShardClient, LocalShardClient, ShardClientInterface}; +use std::sync::Arc; +use tracing::{info, warn}; + +pub async fn create_shard_manager( + shard_storage_config: &StorageConfig, +) -> Result { + let shard_session_directory = shard_storage_config + .staging_directory + .as_ref() + .expect("Need shard staging directory to create ShardFileManager"); + let shard_cache_directory = &shard_storage_config + .cache_config + .as_ref() + .expect("Need shard cache directory to create ShardFileManager") + .cache_directory; + + let shard_manager = ShardFileManager::new(shard_session_directory).await?; + + if shard_cache_directory.exists() { + shard_manager + .register_shards_by_path(&[shard_cache_directory], true) + .await?; + } else { + warn!( + "Merkle DB Cache path {:?} does not exist, skipping registration.", + shard_cache_directory + ); + } + + Ok(shard_manager) +} + +pub async fn create_shard_client( + shard_storage_config: &StorageConfig, +) -> Result> { + info!("Shard endpoint = {:?}", shard_storage_config.endpoint); + let client: Arc = match &shard_storage_config.endpoint { + Server(endpoint) => { + let shard_connection_config = shard_client::ShardConnectionConfig { + endpoint: endpoint.clone(), + user_id: shard_storage_config.auth.user_id.clone(), + git_xet_version: XET_VERSION.to_string(), + }; + Arc::new(GrpcShardClient::from_config(shard_connection_config).await?) + } + FileSystem(path) => Arc::new(LocalShardClient::new(path).await?), + }; + + Ok(client) +} diff --git a/data/src/small_file_determination.rs b/data/src/small_file_determination.rs new file mode 100644 index 00000000..f88e2542 --- /dev/null +++ b/data/src/small_file_determination.rs @@ -0,0 +1,20 @@ +/// Implements the small file heuristic to determine if this file +/// should be stored in Git or MerkleDB. +/// The current heuristic is simple: +/// - if it is less than the SMALL_FILE_THRESHOLD +/// AND if it decodes as utf-8, it is a small file +pub fn is_file_passthrough(buf: &[u8], small_file_threshold: usize) -> bool { + buf.len() < small_file_threshold && std::str::from_utf8(buf).is_ok() +} + +pub fn is_possible_start_to_text_file(buf: &[u8]) -> bool { + // In UTF-8 encoding, the maximum length of a character is 4 bytes. This means + // that a valid UTF-8 character can span up to 4 bytes. Therefore, when you have + // a sequence of bytes that could be part of a valid UTF-8 string but is truncated, + // the maximum difference between the end of the buffer and the position indicated + // by e.valid_up_to() from a Utf8Error is 3 bytes. + match std::str::from_utf8(buf) { + Ok(_) => true, + Err(e) => e.valid_up_to() != 0 && e.valid_up_to() >= buf.len().saturating_sub(3), + } +} diff --git a/error_printer/Cargo.toml b/error_printer/Cargo.toml new file mode 100644 index 00000000..8811fa5a --- /dev/null +++ b/error_printer/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "error_printer" +version = "0.14.5" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tracing = "0.1.40" diff --git a/error_printer/src/lib.rs b/error_printer/src/lib.rs new file mode 100644 index 00000000..d7c9a412 --- /dev/null +++ b/error_printer/src/lib.rs @@ -0,0 +1,86 @@ +use std::fmt::{Debug, Display}; +use tracing::{debug, error, info, warn}; + +/// A helper trait to log errors. +/// The logging functions will track the caller's callsite. +/// For a chain of calls A -> B -> C -> ErrorPrinter, the +/// topmost function without #[track_caller] is deemed the callsite. +pub trait ErrorPrinter { + fn log_error(self, message: M) -> Self; + + fn warn_error(self, message: M) -> Self; + + fn debug_error(self, message: M) -> Self; + + fn info_error(self, message: M) -> Self; +} + +impl ErrorPrinter for Result { + /// If self is an Err(e), prints out the given string to tracing::error, + /// appending "error: {e}" to the end of the message. + #[track_caller] + fn log_error(self, message: M) -> Self { + match &self { + Ok(_) => {} + Err(e) => { + let location = std::panic::Location::caller(); + error!( + caller = format!("{}:{}", location.file(), location.line()), + "{}, error: {:?}", message, e + ) + } + } + self + } + + /// If self is an Err(e), prints out the given string to tracing::warn, + /// appending "error: {e}" to the end of the message. + #[track_caller] + fn warn_error(self, message: M) -> Self { + match &self { + Ok(_) => {} + Err(e) => { + let location = std::panic::Location::caller(); + warn!( + caller = format!("{}:{}", location.file(), location.line()), + "{}, error: {:?}", message, e + ) + } + } + self + } + + /// If self is an Err(e), prints out the given string to tracing::debug, + /// appending "error: {e}" to the end of the message. + #[track_caller] + fn debug_error(self, message: M) -> Self { + match &self { + Ok(_) => {} + Err(e) => { + let location = std::panic::Location::caller(); + debug!( + caller = format!("{}:{}", location.file(), location.line()), + "{}, error: {:?}", message, e + ) + } + } + self + } + + /// If self is an Err(e), prints out the given string to tracing::info, + /// appending "error: {e}" to the end of the message. + #[track_caller] + fn info_error(self, message: M) -> Self { + match &self { + Ok(_) => {} + Err(e) => { + let location = std::panic::Location::caller(); + info!( + caller = format!("{}:{}", location.file(), location.line()), + "{}, error: {:?}", message, e + ) + } + } + self + } +} diff --git a/file_utils/Cargo.toml b/file_utils/Cargo.toml new file mode 100644 index 00000000..5a65780c --- /dev/null +++ b/file_utils/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "file_utils" +version = "0.14.2" +edition = "2021" + +[dependencies] +colored = "2.0.0" +tracing = "0.1.*" +libc = "0.2" +lazy_static = "1.4.0" +whoami = "1.4.1" +anyhow = "1" +tempfile = "3.2.0" +rand = "0.8.4" + + +winapi = { version = "0.3", features = [ + "winerror", + "winnt", + "handleapi", + "processthreadsapi", + "securitybaseapi", +] } diff --git a/file_utils/src/file_metadata.rs b/file_utils/src/file_metadata.rs new file mode 100644 index 00000000..afde320c --- /dev/null +++ b/file_utils/src/file_metadata.rs @@ -0,0 +1,203 @@ +use std::{fs::Metadata, path::Path, time::SystemTime}; + +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; + +/// Matches the metadata of a file to another file's metadata +pub fn set_file_metadata>( + path: P, + metadata: &Metadata, + match_owner: bool, +) -> std::io::Result<()> { + let path = path.as_ref(); + + // Set permissions + let permissions = metadata.permissions(); + std::fs::set_permissions(path, permissions.clone())?; + + // Set timestamps + let atime = metadata.accessed()?; + let mtime = metadata.modified()?; + let atime = atime.duration_since(SystemTime::UNIX_EPOCH).unwrap(); + let mtime = mtime.duration_since(SystemTime::UNIX_EPOCH).unwrap(); + let times = [ + libc::timespec { + tv_sec: atime.as_secs() as libc::time_t, + tv_nsec: atime.subsec_nanos() as libc::c_long, + }, + libc::timespec { + tv_sec: mtime.as_secs() as libc::time_t, + tv_nsec: mtime.subsec_nanos() as libc::c_long, + }, + ]; + + #[cfg(unix)] + if let Some(path_s) = path.to_str() { + if match_owner { + // Set ownership + let uid = metadata.uid(); + let gid = metadata.gid(); + unsafe { + libc::chown(path_s.as_bytes().as_ptr() as *const libc::c_char, uid, gid); + } + } + + unsafe { + libc::utimensat( + libc::AT_FDCWD, + path_s.as_bytes().as_ptr() as *const libc::c_char, + times.as_ptr(), + 0, + ); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{self, File}; + use std::os::unix::fs::PermissionsExt; + use std::time::{Duration, SystemTime}; + use tempfile::tempdir; + + #[test] + fn test_set_metadata_permissions() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test_file"); + File::create(&file_path).unwrap(); + + // Set some initial permissions + let mut perms = File::open(&file_path) + .unwrap() + .metadata() + .unwrap() + .permissions(); + perms.set_mode(0o644); + + fs::set_permissions(&file_path, perms.clone()).unwrap(); + + // Create a file with different permissions to copy from + let src_file_path = dir.path().join("src_file"); + let src_file = File::create(&src_file_path).unwrap(); + let mut src_perms = src_file.metadata().unwrap().permissions(); + src_perms.set_mode(0o600); + fs::set_permissions(&src_file_path, src_perms.clone()).unwrap(); + + let src_metadata = src_file.metadata().unwrap(); + + // Apply set_metadata + set_file_metadata(&file_path, &src_metadata, false).unwrap(); + + // Check that permissions have been updated. But we need to re-read some of the things + // as Unix adds an extra bit indicating a regular file. + let updated_metadata = File::open(file_path).unwrap().metadata().unwrap(); + let src_metadata = File::open(src_file_path).unwrap().metadata().unwrap(); + + assert_eq!( + updated_metadata.permissions().mode(), + src_metadata.permissions().mode() + ); + assert_eq!( + updated_metadata.modified().unwrap(), + src_metadata.modified().unwrap() + ); + } + + #[test] + #[cfg(unix)] + fn test_set_metadata_timestamps() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test_file"); + let file = File::create(&file_path).unwrap(); + + // Create a file with specific timestamps to copy from + let src_file_path = dir.path().join("src_file"); + let src_file = File::create(&src_file_path).unwrap(); + + let src_metadata = src_file.metadata().unwrap(); + + let atime = SystemTime::now() - Duration::from_secs(24 * 3600); + let mtime = SystemTime::now() - Duration::from_secs(48 * 3600); + + let times = [ + libc::timespec { + tv_sec: atime + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() as libc::time_t, + tv_nsec: atime + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .subsec_nanos() as libc::c_long, + }, + libc::timespec { + tv_sec: mtime + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() as libc::time_t, + tv_nsec: mtime + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .subsec_nanos() as libc::c_long, + }, + ]; + + unsafe { + libc::utimensat( + libc::AT_FDCWD, + src_file_path.to_str().unwrap().as_bytes().as_ptr() as *const libc::c_char, + times.as_ptr(), + 0, + ); + } + + // Apply set_metadata + set_file_metadata(&file_path, &src_metadata, false).unwrap(); + + // Check that timestamps have been updated + let updated_metadata = file.metadata().unwrap(); + assert_eq!( + updated_metadata.accessed().unwrap(), + src_metadata.accessed().unwrap() + ); + assert_eq!( + updated_metadata.modified().unwrap(), + src_metadata.modified().unwrap() + ); + } + + #[test] + #[cfg(unix)] + fn test_set_metadata_owner() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test_file"); + let file = File::create(&file_path).unwrap(); + + // Create a file with specific ownership to copy from + let src_file_path = dir.path().join("src_file"); + let src_file = File::create(&src_file_path).unwrap(); + + // Set some ownership (only works on Unix systems) + let uid = 1000; + let gid = 1000; + unsafe { + libc::chown( + src_file_path.to_str().unwrap().as_bytes().as_ptr() as *const libc::c_char, + uid, + gid, + ); + } + + let src_metadata = src_file.metadata().unwrap(); + + // Apply set_metadata + set_file_metadata(&file_path, &src_metadata, true).unwrap(); + + // Check that ownership has been updated + let updated_metadata = file.metadata().unwrap(); + assert_eq!(updated_metadata.uid(), src_metadata.uid()); + assert_eq!(updated_metadata.gid(), src_metadata.gid()); + } +} diff --git a/file_utils/src/lib.rs b/file_utils/src/lib.rs new file mode 100644 index 00000000..414edc94 --- /dev/null +++ b/file_utils/src/lib.rs @@ -0,0 +1,7 @@ +mod file_metadata; +mod privilege_context; +mod safe_file_creator; + +pub use privilege_context::{create_dir_all, create_file, PrivilgedExecutionContext}; +pub use safe_file_creator::write_all_safe; +pub use safe_file_creator::SafeFileCreator; diff --git a/file_utils/src/privilege_context.rs b/file_utils/src/privilege_context.rs new file mode 100644 index 00000000..79194e97 --- /dev/null +++ b/file_utils/src/privilege_context.rs @@ -0,0 +1,379 @@ +use lazy_static::lazy_static; +use std::{fs::File, path::Path}; +use tracing::error; + +#[cfg(unix)] +use colored::Colorize; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; + +#[cfg(windows)] +use winapi::um::{ + processthreadsapi::GetCurrentProcess, + processthreadsapi::OpenProcessToken, + securitybaseapi::GetTokenInformation, + winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY}, +}; + +#[cfg(test)] +static mut WARNING_PRINTED: bool = false; + +/// Checks if the program is running under elevated privilege +fn is_elevated_impl() -> bool { + // In a Unix-like environment, when a program is run with sudo, + // the effective user ID (euid) of the process is set to 0. + #[cfg(unix)] + { + unsafe { libc::geteuid() == 0 } + } + + #[cfg(windows)] + { + let mut token: HANDLE = std::ptr::null_mut(); + if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 { + return false; + } + + let mut elevation: TOKEN_ELEVATION = unsafe { std::mem::zeroed() }; + let mut return_length = 0; + let success = unsafe { + GetTokenInformation( + token, + TokenElevation, + &mut elevation as *mut _ as *mut _, + std::mem::size_of::() as u32, + &mut return_length, + ) + }; + + if success == 0 { + false + } else { + elevation.TokenIsElevated != 0 + } + } +} + +lazy_static! { + static ref IS_ELEVATED: bool = is_elevated_impl(); +} + +pub fn is_elevated() -> bool { + *IS_ELEVATED +} + +// Facts: +// Assume there's a standard user A that is not a root user. +// 1. On Unix systems, suppose there is a path 'dir/f' where 'dir' is created by A but 'f' +// created by 'sudo A', A can read, rename or remove 'dir/f'. This implies that it's enough +// to check the permission of 'dir' if we don't directly write into 'dir/f'. This is exactly +// how we interact with the xorb cache: if an eviction is deemed necessary, the replacement +// data is written to a tempfile first and then renamed to the to-be-evicted entry. So even +// if a certain cache file was created by 'sudo A', the eviction by 'A' will succeed. +// 2. On Windows, 'Run as administrator' by logged in user A actually sets %HOMEPATH% to administrator's +// HOME, so by default the xet metadata folders are isolated. If 'run as admin A' explicility configures +// cache or repo path to another location owned by A, ACLs for the created path inherit from the parent +// folder, so A still has full control. + +#[derive(Debug, Clone, Copy)] +pub enum PrivilgedExecutionContext { + Regular, + Elevated, +} + +impl PrivilgedExecutionContext { + pub fn current() -> PrivilgedExecutionContext { + match is_elevated() { + false => PrivilgedExecutionContext::Regular, + true => PrivilgedExecutionContext::Elevated, + } + } + + pub fn is_elevated(&self) -> bool { + match self { + PrivilgedExecutionContext::Regular => false, + PrivilgedExecutionContext::Elevated => true, + } + } + + /// Recursively create a directory and all of its parent components if they are missing for write. + /// If the current process is running with elevated privileges, the entries created + /// will inherit permission from the path parent. + pub fn create_dir_all(&self, path: impl AsRef) -> std::io::Result<()> { + // if path is absolute, cwd is ignored. + let path = std::env::current_dir()?.join(path); + let path = path.as_path(); + + // first find an ancestor of the path that exists. + let mut root = path; + while !root.exists() { + let Some(pparent) = root.parent() else { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Path {root:?} has no parent."), + )); + }; + + root = pparent; + } + + // try recursively create all the directories. + std::fs::create_dir_all(path).map_err(|err| { + if err.kind() == std::io::ErrorKind::PermissionDenied { + permission_warning(root, true); + } + err + })?; + + // with elevated privileges we chown for all entries from path to root. + // Permission inheriting from the parent is the default behavior on Windows, thus + // the below implementation only targets Unix systems. + #[cfg(unix)] + if self.is_elevated() { + let root_meta = std::fs::metadata(root)?; + let mut path = path; + while path != root { + std::os::unix::fs::chown(path, Some(root_meta.uid()), Some(root_meta.gid()))?; + let Some(pparent) = path.parent() else { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Path {path:?} has no parent."), + )); + }; + path = pparent; + } + } + + Ok(()) + } + + /// Open or create a file for write. + /// If the current process is running with elevated privileges, the entries created + /// will inherit permission from the path parent. + pub fn create_file(&self, path: impl AsRef) -> std::io::Result { + // if path is absolute, cwd is ignored. + let path = std::env::current_dir()?.join(path); + let path = path.as_path(); + + let Some(pparent) = path.parent() else { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Path {path:?} has no parent."), + )); + }; + + self.create_dir_all(pparent)?; + + #[allow(unused_variables)] + let parent_meta = std::fs::metadata(pparent)?; + + #[cfg(unix)] + let exist = path.exists(); + + let create = || { + std::fs::OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(path) + .map_err(|err| { + if err.kind() == std::io::ErrorKind::PermissionDenied { + permission_warning(path, false); + } + err + }) + }; + + // Test if the current context has write access to the file. + create()?; + + // exist is only trustable if opening file for R+W succeeded. + // Permission inheriting from the parent is the default behavior on Windows, thus + // the below implementation only targets Unix systems. + #[cfg(unix)] + if !exist && self.is_elevated() { + // changes the ownership. + std::os::unix::fs::chown(path, Some(parent_meta.uid()), Some(parent_meta.gid()))?; + } + + // Now reopen it. + create() + } +} + +pub fn create_dir_all(path: impl AsRef) -> std::io::Result<()> { + PrivilgedExecutionContext::current().create_dir_all(path) +} + +pub fn create_file(path: impl AsRef) -> std::io::Result { + PrivilgedExecutionContext::current().create_file(path) +} + +#[allow(unused_variables)] +fn permission_warning(path: &Path, recursive: bool) { + #[cfg(unix)] + { + let message = format!("The process doesn't have correct read-write permission into path {path:?}, please resets + ownership by 'sudo chown{}{} {path:?}'.", if recursive {" -R "} else {" "}, whoami::username()); + + eprintln!("{}", message.bright_blue()); + } + + #[cfg(windows)] + eprintln!( + "The process doesn't have correct read-write permission into path {path:?}, please resets + permission in the Properties dialog box under the Security tab." + ); + + error!("Permission denied for path {path:?}"); + + #[cfg(test)] + unsafe { + WARNING_PRINTED = true + }; +} + +#[cfg(all(test, unix))] +mod test { + use std::os::unix::fs::MetadataExt; + use std::path::Path; + + use super::{PrivilgedExecutionContext, WARNING_PRINTED}; + + #[test] + #[ignore = "run manually"] + fn test_create_dir_all() -> anyhow::Result<()> { + // Run this test manually, steps: + + // For Unix + // 1. Run the below shell script in an empty dir with standard privileges. + // 2. Set env var 'XET_TEST_PATH' to this path. + // 3. Build the test executable by running 'cargo test -p gitxetcore --lib --no-run'. + // 4. Locate the path to the executable as TEST_EXE + // 5. Run test with a non-root user: 'TEST_EXE config::permission::test::test_create_dir_all --exact --nocapture --include-ignored' + + r#" +sudo mkdir rootdir + "#; + + let test_path = std::env::var("XET_TEST_PATH")?; + std::env::set_current_dir(test_path)?; + let permission = PrivilgedExecutionContext::current(); + + let test = Path::new("rootdir/regdir1/regdir2"); + + assert!(permission.create_dir_all(test).is_err()); + unsafe { assert!(WARNING_PRINTED) }; + + Ok(()) + } + + #[test] + #[ignore = "run manually"] + fn test_create_dir_all_sudo() -> anyhow::Result<()> { + // Run this test manually, steps: + + // For Unix + // 1. Run the below shell script in an empty dir with standard privileges. + // 2. Set env var 'XET_TEST_PATH' to this path. + // 3. Build the test executable by running 'cargo test -p gitxetcore --lib --no-run'. + // 4. Locate the path to the executable as TEST_EXE + // 5. Run test with root user: 'sudo -E TEST_EXE config::permission::test::test_create_dir_all_sudo --exact --nocapture --include-ignored' + + r#" +mkdir regdir + "#; + + let test_path = std::env::var("XET_TEST_PATH")?; + std::env::set_current_dir(test_path)?; + let permission = PrivilgedExecutionContext::current(); + + let test = Path::new("regdir/regdir1/regdir2"); + + permission.create_dir_all(test)?; + + assert!(test.exists()); + + // not owned by root + assert!(std::fs::metadata(test)?.uid() != 0); + + let parent = test.parent().unwrap(); + + // parent not owned by root + assert!(std::fs::metadata(parent)?.uid() != 0); + + Ok(()) + } + + #[test] + #[ignore = "run manually"] + fn test_create_file() -> anyhow::Result<()> { + // Run this test manually, steps: + + // For Unix + // 1. Run the below shell script in an empty dir with standard privileges. + // 2. Set env var 'XET_TEST_PATH' to this path. + // 3. Build the test executable by running 'cargo test -p gitxetcore --lib --no-run'. + // 4. Locate the path to the executable as TEST_EXE + // 5. Run test with a non-root user: 'TEST_EXE config::permission::test::test_create_file --exact --nocapture --include-ignored' + + r#" +sudo mkdir rootdir +sudo touch rootdir/file + "#; + + let test_path = std::env::var("XET_TEST_PATH")?; + std::env::set_current_dir(test_path)?; + let permission = PrivilgedExecutionContext::current(); + + let test1 = Path::new("rootdir/regdir1/regdir2/file"); + + assert!(permission.create_file(test1).is_err()); + unsafe { assert!(WARNING_PRINTED) }; + + unsafe { WARNING_PRINTED = false }; + + let test2 = Path::new("rootdir/file"); + assert!(permission.create_file(test2).is_err()); + unsafe { assert!(WARNING_PRINTED) }; + + Ok(()) + } + + #[test] + #[ignore = "run manually"] + fn test_create_file_sudo() -> anyhow::Result<()> { + // Run this test manually, steps: + + // For Unix + // 1. Run the below shell script in an empty dir with standard privileges. + // 2. Set env var 'XET_TEST_PATH' to this path. + // 3. Build the test executable by running 'cargo test -p gitxetcore --lib --no-run'. + // 4. Locate the path to the executable as TEST_EXE + // 5. Run test with root user: 'sudo -E TEST_EXE config::permission::test::test_create_file_sudo --exact --nocapture --include-ignored' + + r#" +mkdir regdir + "#; + + let test_path = std::env::var("XET_TEST_PATH")?; + std::env::set_current_dir(test_path)?; + + let test = Path::new("regdir/regdir1/regdir2/file"); + + let permission = PrivilgedExecutionContext::current(); + permission.create_file(test)?; + + assert!(test.exists()); + + // not owned by root + assert!(std::fs::metadata(test)?.uid() != 0); + + let parent = test.parent().unwrap(); + + // parent not owned by root + assert!(std::fs::metadata(parent)?.uid() != 0); + + Ok(()) + } +} diff --git a/file_utils/src/safe_file_creator.rs b/file_utils/src/safe_file_creator.rs new file mode 100644 index 00000000..81e1b31b --- /dev/null +++ b/file_utils/src/safe_file_creator.rs @@ -0,0 +1,286 @@ +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use std::fs::{self, File, Metadata}; +use std::io::{self, BufWriter, Write}; +use std::path::{Path, PathBuf}; +use tempfile::NamedTempFile; + +use crate::{create_file, file_metadata::set_file_metadata}; + +pub struct SafeFileCreator { + dest_path: PathBuf, + temp_path: PathBuf, + original_metadata: Option, + writer: Option>, +} + +impl SafeFileCreator { + /// Safely creates a new file at a specific location. Ensures the file is not created with elevated privileges, + /// and a temporary file is created then renamed on close. + pub fn new>(dest_path: P) -> io::Result { + let dest_path = dest_path.as_ref().to_path_buf(); + let temp_path = Self::temp_file_path(&dest_path); + + // This matches the permissions and ownership of the parent directory + let file = create_file(&temp_path)?; + let writer = BufWriter::new(file); + + Ok(SafeFileCreator { + dest_path, + temp_path, + original_metadata: None, + writer: Some(writer), + }) + } + + /// Safely replaces a new file at a specific location. Ensures the file is not created with elevated privileges, + /// and additionally the metadata of the old one will match the new metadata. + pub fn replace_existing>(dest_path: P) -> io::Result { + let mut s = Self::new(&dest_path)?; + s.original_metadata = std::fs::metadata(dest_path).ok(); + Ok(s) + } + + /// Generates a temporary file path in the same directory as the destination file + pub fn temp_file_path>(dest_path: P) -> PathBuf { + let path = dest_path.as_ref(); + let parent = path.parent().unwrap_or_else(|| Path::new(".")); + let file_name = path.file_name().unwrap().to_str().unwrap(); + + let mut rng = thread_rng(); + let random_hash: String = (0..10) + .map(|_| rng.sample(Alphanumeric)) + .map(char::from) + .collect(); + let temp_file_name = format!(".{}.{hash}.tmp", file_name, hash = random_hash); + parent.join(temp_file_name) + } + + /// Closes the writer and replaces the original file with the temporary file + pub fn close(&mut self) -> io::Result<()> { + let Some(mut writer) = self.writer.take() else { + return Ok(()); + }; + + writer.flush()?; + drop(writer); + + // Replace the original file with the new file + fs::rename(&self.temp_path, &self.dest_path)?; + + if let Some(metadata) = self.original_metadata.as_ref() { + set_file_metadata(&self.dest_path, metadata, false)?; + } + let original_permissions = if self.dest_path.exists() { + Some(fs::metadata(&self.dest_path)?.permissions()) + } else { + None + }; + + // Set the original file's permissions to the new file if they exist + if let Some(permissions) = original_permissions { + fs::set_permissions(&self.dest_path, permissions.clone())?; + } + + Ok(()) + } + + fn writer(&mut self) -> io::Result<&mut BufWriter> { + match &mut self.writer { + Some(wr) => Ok(wr), + None => Err(std::io::Error::new( + io::ErrorKind::BrokenPipe, + format!("Writing to {:?} already completed.", &self.dest_path), + )), + } + } +} + +impl Write for SafeFileCreator { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.writer()?.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.writer()?.flush() + } +} + +impl Drop for SafeFileCreator { + fn drop(&mut self) { + if let Err(e) = self.close() { + eprintln!( + "Error: Failed to close writer for {:?}: {}", + &self.dest_path, e + ); + } + } +} + +/// Write all bytes +pub fn write_all_safe(path: &Path, bytes: &[u8]) -> io::Result<()> { + if !path.as_os_str().is_empty() { + let dir = path.parent().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Unable to find parent path from {path:?}"), + ) + })?; + + // Make sure dir exists. + if !dir.exists() { + std::fs::create_dir_all(dir)?; + } + + let mut tempfile = create_temp_file(dir, "")?; + tempfile.write_all(bytes)?; + tempfile.persist(path).map_err(|e| e.error)?; + } + + Ok(()) +} + +pub fn create_temp_file(dir: &Path, suffix: &str) -> io::Result { + let tempfile = tempfile::Builder::new() + .prefix(&format!("{}.", std::process::id())) + .suffix(suffix) + .tempfile_in(dir)?; + + Ok(tempfile) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{self, File}; + use std::io::Read; + use std::os::unix::fs::PermissionsExt; + use tempfile::tempdir; + + #[test] + fn test_safe_file_creator_new() { + let dir = tempdir().unwrap(); + let dest_path = dir.path().join("new_file.txt"); + + let mut safe_file_creator = SafeFileCreator::new(&dest_path).unwrap(); + writeln!(safe_file_creator, "Hello, world!").unwrap(); + safe_file_creator.close().unwrap(); + + // Verify file contents + let mut contents = String::new(); + File::open(&dest_path) + .unwrap() + .read_to_string(&mut contents) + .unwrap(); + assert_eq!(contents.trim(), "Hello, world!"); + + // Verify file permissions + let metadata = fs::metadata(&dest_path).unwrap(); + let permissions = metadata.permissions(); + assert_eq!(permissions.mode() & 0o777, 0o644); // Assuming default creation mode + } + + #[test] + fn test_safe_file_creator_replace_existing() { + let dir = tempdir().unwrap(); + let dest_path = dir.path().join("existing_file.txt"); + + // Create the existing file + { + let mut file = File::create(&dest_path).unwrap(); + file.write_all(b"Old content").unwrap(); + let mut perms = file.metadata().unwrap().permissions(); + perms.set_mode(0o600); + fs::set_permissions(&dest_path, perms).unwrap(); + } + + let mut safe_file_creator = SafeFileCreator::replace_existing(&dest_path).unwrap(); + writeln!(safe_file_creator, "New content").unwrap(); + safe_file_creator.close().unwrap(); + + // Verify file contents + let mut contents = String::new(); + File::open(&dest_path) + .unwrap() + .read_to_string(&mut contents) + .unwrap(); + assert_eq!(contents.trim(), "New content"); + + // Verify file permissions + let metadata = fs::metadata(&dest_path).unwrap(); + let permissions = metadata.permissions(); + assert_eq!(permissions.mode() & 0o777, 0o600); // Original file mode + } + + #[test] + fn test_safe_file_creator_drop() { + let dir = tempdir().unwrap(); + let dest_path = dir.path().join("drop_file.txt"); + + { + let mut safe_file_creator = SafeFileCreator::new(&dest_path).unwrap(); + writeln!(safe_file_creator, "Hello, world!").unwrap(); + // safe_file_creator is dropped here + } + + // Verify file contents + let mut contents = String::new(); + File::open(&dest_path) + .unwrap() + .read_to_string(&mut contents) + .unwrap(); + assert_eq!(contents.trim(), "Hello, world!"); + } + + #[test] + fn test_safe_file_creator_double_close() { + let dir = tempdir().unwrap(); + let dest_path = dir.path().join("double_close_file.txt"); + + let mut safe_file_creator = SafeFileCreator::new(&dest_path).unwrap(); + writeln!(safe_file_creator, "Hello, world!").unwrap(); + safe_file_creator.close().unwrap(); + safe_file_creator.close().unwrap(); // Should be a no-op + + // Verify file contents + let mut contents = String::new(); + File::open(&dest_path) + .unwrap() + .read_to_string(&mut contents) + .unwrap(); + assert_eq!(contents.trim(), "Hello, world!"); + } + + #[test] + #[cfg(unix)] + fn test_safe_file_creator_set_metadata() { + let dir = tempdir().unwrap(); + let dest_path = dir.path().join("metadata_file.txt"); + + // Create the existing file + { + let mut file = File::create(&dest_path).unwrap(); + file.write_all(b"Old content").unwrap(); + let mut perms = file.metadata().unwrap().permissions(); + perms.set_mode(0o600); + fs::set_permissions(&dest_path, perms).unwrap(); + } + + let mut safe_file_creator = SafeFileCreator::replace_existing(&dest_path).unwrap(); + writeln!(safe_file_creator, "New content").unwrap(); + safe_file_creator.close().unwrap(); + + // Verify file contents + let mut contents = String::new(); + File::open(&dest_path) + .unwrap() + .read_to_string(&mut contents) + .unwrap(); + assert_eq!(contents.trim(), "New content"); + + // Verify file permissions + let metadata = fs::metadata(&dest_path).unwrap(); + let permissions = metadata.permissions(); + assert_eq!(permissions.mode() & 0o777, 0o600); // Original file mode + } +} diff --git a/mdb_shard/Cargo.toml b/mdb_shard/Cargo.toml new file mode 100644 index 00000000..70e51e8e --- /dev/null +++ b/mdb_shard/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "mdb_shard" +version = "0.14.5" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +more-asserts = "0.3.*" +tempdir = "0.3.7" +merklehash = { path = "../merklehash" } +serde = {version="1.0.129", features = ["derive"]} +tokio = { version = "1.36", features = ["full"] } +lazy_static = "1.4.0" +regex = "1.5" +tracing = "0.1.*" +uuid = {version = "1.3.2", features = ["v4"]} +async-scoped = {version = "0.7", features = ["use-tokio"]} +binary-heap-plus = "0.5.0" +tempfile = "3.2.0" +clap = { version = "3.1.6", features = ["derive"] } +anyhow = "1" +rand = {version = "0.8.5", features = ["small_rng"]} +xet_error = {path = "../xet_error"} +async-trait = "0.1.9" + +[[bin]] +name = "shard_benchmark" +path = "src/shard_benchmark.rs" diff --git a/mdb_shard/src/cas_structs.rs b/mdb_shard/src/cas_structs.rs new file mode 100644 index 00000000..9b641402 --- /dev/null +++ b/mdb_shard/src/cas_structs.rs @@ -0,0 +1,176 @@ +use crate::serialization_utils::*; +use merklehash::MerkleHash; +use std::fmt::Debug; +use std::io::{Read, Write}; +use std::mem::size_of; + +pub const MDB_DEFAULT_CAS_FLAG: u32 = 0; + +/// Each CAS consists of a CASChunkSequenceHeader following +/// a sequence of CASChunkSequenceEntry. + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CASChunkSequenceHeader { + pub cas_hash: MerkleHash, + pub cas_flags: u32, + pub num_entries: u32, + pub num_bytes_in_cas: u32, + pub num_bytes_on_disk: u32, // the size after CAS block compression +} + +impl CASChunkSequenceHeader { + pub fn new, I2: TryInto + Copy>( + cas_hash: MerkleHash, + num_entries: I1, + num_bytes_in_cas: I2, + ) -> Self + where + >::Error: std::fmt::Debug, + >::Error: std::fmt::Debug, + { + Self { + cas_hash, + cas_flags: MDB_DEFAULT_CAS_FLAG, + num_entries: num_entries.try_into().unwrap(), + num_bytes_in_cas: num_bytes_in_cas.try_into().unwrap(), + num_bytes_on_disk: num_bytes_in_cas.try_into().unwrap(), + } + } + + pub fn new_with_compression, I2: TryInto + Copy>( + cas_hash: MerkleHash, + num_entries: I1, + num_bytes_in_cas: I2, + num_bytes_on_disk: I2, + ) -> Self + where + >::Error: std::fmt::Debug, + >::Error: std::fmt::Debug, + { + Self { + cas_hash, + cas_flags: MDB_DEFAULT_CAS_FLAG, + num_entries: num_entries.try_into().unwrap(), + num_bytes_in_cas: num_bytes_in_cas.try_into().unwrap(), + num_bytes_on_disk: num_bytes_on_disk.try_into().unwrap(), + } + } + + pub fn serialize(&self, writer: &mut W) -> Result { + let mut buf = [0u8; size_of::()]; + { + let mut writer_cur = std::io::Cursor::new(&mut buf[..]); + let writer = &mut writer_cur; + + write_hash(writer, &self.cas_hash)?; + write_u32(writer, self.cas_flags)?; + write_u32(writer, self.num_entries)?; + write_u32(writer, self.num_bytes_in_cas)?; + write_u32(writer, self.num_bytes_on_disk)?; + } + + writer.write_all(&buf[..])?; + + Ok(size_of::()) + } + + pub fn deserialize(reader: &mut R) -> Result { + let mut v = [0u8; size_of::()]; + reader.read_exact(&mut v[..])?; + let mut reader_curs = std::io::Cursor::new(&v); + let reader = &mut reader_curs; + + Ok(Self { + cas_hash: read_hash(reader)?, + cas_flags: read_u32(reader)?, + num_entries: read_u32(reader)?, + num_bytes_in_cas: read_u32(reader)?, + num_bytes_on_disk: read_u32(reader)?, + }) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CASChunkSequenceEntry { + pub chunk_hash: MerkleHash, + pub unpacked_segment_bytes: u32, + pub chunk_byte_range_start: u32, + pub _unused: u64, +} + +impl CASChunkSequenceEntry { + pub fn new, I2: TryInto>( + chunk_hash: MerkleHash, + unpacked_segment_bytes: I1, + chunk_byte_range_start: I2, + ) -> Self + where + >::Error: std::fmt::Debug, + >::Error: std::fmt::Debug, + { + Self { + chunk_hash, + unpacked_segment_bytes: unpacked_segment_bytes.try_into().unwrap(), + chunk_byte_range_start: chunk_byte_range_start.try_into().unwrap(), + #[cfg(test)] + _unused: 216944691646848u64, + #[cfg(not(test))] + _unused: 0, + } + } + + pub fn serialize(&self, writer: &mut W) -> Result { + let mut buf = [0u8; size_of::()]; + { + let mut writer_cur = std::io::Cursor::new(&mut buf[..]); + let writer = &mut writer_cur; + + write_hash(writer, &self.chunk_hash)?; + write_u32(writer, self.chunk_byte_range_start)?; + write_u32(writer, self.unpacked_segment_bytes)?; + write_u64(writer, self._unused)?; + } + + writer.write_all(&buf[..])?; + + Ok(size_of::()) + } + + pub fn deserialize(reader: &mut R) -> Result { + let mut v = [0u8; size_of::()]; + reader.read_exact(&mut v[..])?; + let mut reader_curs = std::io::Cursor::new(&v); + let reader = &mut reader_curs; + + Ok(Self { + chunk_hash: read_hash(reader)?, + chunk_byte_range_start: read_u32(reader)?, + unpacked_segment_bytes: read_u32(reader)?, + _unused: read_u64(reader)?, + }) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct MDBCASInfo { + pub metadata: CASChunkSequenceHeader, + pub chunks: Vec, +} + +impl MDBCASInfo { + pub fn num_bytes(&self) -> u64 { + (size_of::() + + self.chunks.len() * size_of::()) as u64 + } + + pub fn deserialize(reader: &mut R) -> Result { + let metadata = CASChunkSequenceHeader::deserialize(reader)?; + + let mut chunks = Vec::with_capacity(metadata.num_entries as usize); + for _ in 0..metadata.num_entries { + chunks.push(CASChunkSequenceEntry::deserialize(reader)?); + } + + Ok(Self { metadata, chunks }) + } +} diff --git a/mdb_shard/src/constants.rs b/mdb_shard/src/constants.rs new file mode 100644 index 00000000..0e2ab883 --- /dev/null +++ b/mdb_shard/src/constants.rs @@ -0,0 +1,9 @@ +pub const MDB_SHARD_TARGET_SIZE: u64 = 64 * 1024 * 1024; +pub const MDB_SHARD_MIN_TARGET_SIZE: u64 = 48 * 1024 * 1024; + +pub const MDB_SHARD_GLOBAL_DEDUP_CHUNK_MODULUS: u64 = 1024; + +// How the MDB_SHARD_GLOBAL_DEDUP_CHUNK_MODULUS is used. +pub fn hash_is_global_dedup_eligible(h: &merklehash::MerkleHash) -> bool { + (*h) % MDB_SHARD_GLOBAL_DEDUP_CHUNK_MODULUS == 0 +} diff --git a/mdb_shard/src/error.rs b/mdb_shard/src/error.rs new file mode 100644 index 00000000..03e5ed66 --- /dev/null +++ b/mdb_shard/src/error.rs @@ -0,0 +1,55 @@ +use merklehash::MerkleHash; +use std::io; +use xet_error::Error; + +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum MDBShardError { + #[error("File I/O error")] + IOError(#[from] io::Error), + + #[error("Too many collisions when searching for truncated hash : {0}")] + TruncatedHashCollisionError(u64), + + #[error("Shard version error: {0}")] + ShardVersionError(String), + + #[error("Bad file name format: {0}")] + BadFilename(String), + + #[error("Other Internal Error: {0}")] + InternalError(anyhow::Error), + + #[error("Shard not found")] + ShardNotFound(MerkleHash), + + #[error("File not found")] + FileNotFound(MerkleHash), + + #[error("Query failed: {0}")] + QueryFailed(String), + + #[error("Client connection error: {0}")] + GrpcClientError(#[from] anyhow::Error), + + #[error("Smudge query policy Error: {0}")] + SmudgeQueryPolicyError(String), + + #[error("Error: {0}")] + Other(String), +} + +// Define our own result type here (this seems to be the standard). +pub type Result = std::result::Result; + +// For error checking +impl PartialEq for MDBShardError { + fn eq(&self, other: &MDBShardError) -> bool { + match (self, other) { + (MDBShardError::IOError(ref e1), MDBShardError::IOError(ref e2)) => { + e1.kind() == e2.kind() + } + _ => false, + } + } +} diff --git a/mdb_shard/src/file_structs.rs b/mdb_shard/src/file_structs.rs new file mode 100644 index 00000000..fd1e0ad9 --- /dev/null +++ b/mdb_shard/src/file_structs.rs @@ -0,0 +1,163 @@ +use crate::cas_structs::{CASChunkSequenceEntry, CASChunkSequenceHeader}; +use crate::serialization_utils::*; +use merklehash::MerkleHash; +use std::fmt::Debug; +use std::io::{Cursor, Read, Write}; +use std::mem::size_of; + +pub const MDB_DEFAULT_FILE_FLAG: u32 = 0; + +/// Each file consists of a FileDataSequenceHeader following +/// a sequence of FileDataSequenceEntry. + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FileDataSequenceHeader { + pub file_hash: MerkleHash, + pub file_flags: u32, + pub num_entries: u32, + pub _unused: u64, +} + +impl FileDataSequenceHeader { + pub fn new>(file_hash: MerkleHash, num_entries: I) -> Self + where + >::Error: std::fmt::Debug, + { + Self { + file_hash, + file_flags: MDB_DEFAULT_FILE_FLAG, + num_entries: num_entries.try_into().unwrap(), + #[cfg(test)] + _unused: 126846135456846514u64, + #[cfg(not(test))] + _unused: 0, + } + } + + pub fn serialize(&self, writer: &mut W) -> Result { + let mut buf = [0u8; size_of::()]; + { + let mut writer_cur = std::io::Cursor::new(&mut buf[..]); + let writer = &mut writer_cur; + + write_hash(writer, &self.file_hash)?; + write_u32(writer, self.file_flags)?; + write_u32(writer, self.num_entries)?; + write_u64(writer, self._unused)?; + } + + writer.write_all(&buf[..])?; + + Ok(size_of::()) + } + + pub fn deserialize(reader: &mut R) -> Result { + let mut v = [0u8; size_of::()]; + reader.read_exact(&mut v[..])?; + let mut reader_curs = std::io::Cursor::new(&v); + let reader = &mut reader_curs; + + Ok(Self { + file_hash: read_hash(reader)?, + file_flags: read_u32(reader)?, + num_entries: read_u32(reader)?, + _unused: read_u64(reader)?, + }) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct FileDataSequenceEntry { + // maps to one or more CAS chunk(s) + pub cas_hash: MerkleHash, + pub cas_flags: u32, + pub unpacked_segment_bytes: u32, + pub chunk_byte_range_start: u32, + pub chunk_byte_range_end: u32, +} + +impl FileDataSequenceEntry { + pub fn new, I2: TryInto>( + cas_hash: MerkleHash, + unpacked_segment_bytes: I1, + chunk_byte_range_start: I2, + chunk_byte_range_end: I2, + ) -> Self + where + >::Error: std::fmt::Debug, + >::Error: std::fmt::Debug, + { + Self { + cas_hash, + cas_flags: MDB_DEFAULT_FILE_FLAG, + unpacked_segment_bytes: unpacked_segment_bytes.try_into().unwrap(), + chunk_byte_range_start: chunk_byte_range_start.try_into().unwrap(), + chunk_byte_range_end: chunk_byte_range_end.try_into().unwrap(), + } + } + + pub fn from_cas_entries( + metadata: &CASChunkSequenceHeader, + chunks: &[CASChunkSequenceEntry], + chunk_byte_range_end: u32, + ) -> Self { + if chunks.is_empty() { + return Self::default(); + } + + Self { + cas_hash: metadata.cas_hash, + cas_flags: metadata.cas_flags, + unpacked_segment_bytes: chunks.iter().map(|sb| sb.unpacked_segment_bytes).sum(), + chunk_byte_range_start: chunks[0].chunk_byte_range_start, + chunk_byte_range_end, + } + } + + pub fn serialize(&self, writer: &mut W) -> Result { + let mut buf = [0u8; size_of::()]; + { + let mut writer_cur = std::io::Cursor::new(&mut buf[..]); + let writer = &mut writer_cur; + + write_hash(writer, &self.cas_hash)?; + write_u32(writer, self.cas_flags)?; + write_u32(writer, self.unpacked_segment_bytes)?; + write_u32(writer, self.chunk_byte_range_start)?; + write_u32(writer, self.chunk_byte_range_end)?; + } + + writer.write_all(&buf[..])?; + + Ok(size_of::()) + } + + pub fn deserialize(reader: &mut R) -> Result { + let mut v = [0u8; size_of::()]; + reader.read_exact(&mut v[..])?; + + let mut reader_curs = Cursor::new(&v); + let reader = &mut reader_curs; + + Ok(Self { + cas_hash: read_hash(reader)?, + cas_flags: read_u32(reader)?, + unpacked_segment_bytes: read_u32(reader)?, + chunk_byte_range_start: read_u32(reader)?, + chunk_byte_range_end: read_u32(reader)?, + }) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct MDBFileInfo { + pub metadata: FileDataSequenceHeader, + pub segments: Vec, +} + +impl MDBFileInfo { + pub fn num_bytes(&self) -> u64 { + (size_of::() + + self.segments.len() * size_of::()) as u64 + } +} diff --git a/mdb_shard/src/lib.rs b/mdb_shard/src/lib.rs new file mode 100644 index 00000000..3df56868 --- /dev/null +++ b/mdb_shard/src/lib.rs @@ -0,0 +1,24 @@ +pub mod cas_structs; +pub mod constants; +pub mod error; +pub mod file_structs; +pub mod serialization_utils; +pub mod session_directory; +pub mod set_operations; +pub mod shard_dedup_probe; +pub mod shard_file_handle; +pub mod shard_file_manager; +pub mod shard_file_reconstructor; +pub mod shard_format; +pub mod shard_in_memory; +pub mod shard_version; +pub mod utils; + +pub use constants::hash_is_global_dedup_eligible; +pub use constants::MDB_SHARD_TARGET_SIZE; +pub use shard_file_handle::MDBShardFile; +pub use shard_file_manager::ShardFileManager; +pub use shard_format::{MDBShardFileFooter, MDBShardFileHeader, MDBShardInfo}; + +// Temporary to transition dependent code to new location +pub mod shard_file; diff --git a/mdb_shard/src/serialization_utils.rs b/mdb_shard/src/serialization_utils.rs new file mode 100644 index 00000000..32bf6abc --- /dev/null +++ b/mdb_shard/src/serialization_utils.rs @@ -0,0 +1,356 @@ +use merklehash::MerkleHash; +use std::cmp::Ordering; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::mem::{size_of, transmute}; + +pub fn write_hash(writer: &mut W, m: &MerkleHash) -> Result<(), std::io::Error> { + writer.write_all(m.as_bytes()) +} + +pub fn write_u32(writer: &mut W, v: u32) -> Result<(), std::io::Error> { + writer.write_all(&v.to_le_bytes()) +} + +pub fn write_u64(writer: &mut W, v: u64) -> Result<(), std::io::Error> { + writer.write_all(&v.to_le_bytes()) +} + +pub fn write_u32s(writer: &mut W, vs: &[u32]) -> Result<(), std::io::Error> { + for e in vs { + write_u32(writer, *e)?; + } + + Ok(()) +} + +pub fn write_u64s(writer: &mut W, vs: &[u64]) -> Result<(), std::io::Error> { + for e in vs { + write_u64(writer, *e)?; + } + + Ok(()) +} + +pub fn read_hash(reader: &mut R) -> Result { + let mut m = [0u8; 32]; + reader.read_exact(&mut m)?; // Not endian safe. + + Ok(MerkleHash::from(unsafe { + transmute::<[u8; 32], [u64; 4]>(m) + })) +} + +pub fn read_u32(reader: &mut R) -> Result { + let mut buf = [0u8; size_of::()]; + reader.read_exact(&mut buf[..])?; + Ok(u32::from_le_bytes(buf)) +} + +pub fn read_u64(reader: &mut R) -> Result { + let mut buf = [0u8; size_of::()]; + reader.read_exact(&mut buf[..])?; + Ok(u64::from_le_bytes(buf)) +} + +pub fn read_u64s(reader: &mut R, vs: &mut [u64]) -> Result<(), std::io::Error> { + for e in vs.iter_mut() { + *e = read_u64(reader)?; + } + + Ok(()) +} + +/// Performs an interpolation search on a block of sorted, possibly multile +/// u64 hash keys with a simple payload. +/// +/// read_start: The byte offset in the reader that gives the start of the data. +/// num_entries: the number of key, value pairs present. +/// key: the key to search for +/// read_value_function : A function that deserializes the value. +/// +/// result : A mutable slice into which the results get written. If the number +/// of values found equals the length of this buffer at the end, then more values may +/// be present. +/// +/// Returns the number of values found. +/// +/// +pub fn search_on_sorted_u64s< + Value: Default + Copy + std::fmt::Debug, + R: Read + Seek, + ReadValueFunction: Fn(&mut R) -> Result, +>( + reader: &mut R, + read_start: u64, + num_entries: u64, + key: u64, + read_value_function: ReadValueFunction, + result: &mut [Value], +) -> Result { + // + // A few things make this interesting: + // + // 1. We assume an even distribution over keys, allowing us to do interpolation search. + // + // 2. Multiple values may be present. Therefore, it is not enough to find a key; rather, + // we need to be certain we've found all of them. + // + // 2. Seeks are more expensive than forward reads. We assume it's fast to read values sequentially. + // Therefore, once the candidate window is small enough, we just read all the values in the window. + // + // This is the size of the window where doing a sequential read from this point is assumed to be equivalent in speed + // to a seek, then do a read. If the next point is within READ_WINDOW_SIZE entries of the current point, then + // just do a continuous read. + const READ_WINDOW_SIZE: u64 = 256; + const EXPECTED_MAX_NUM_DUPLICATES: u64 = 4; + + let pair_size: u64 = (size_of::() + size_of::()) as u64; + + // Where we'll write the next result. + let mut result_write_idx = 0; + + // Make it bullet proof against corner cases. + if result.is_empty() { + return Ok(0); + } + + let mut write_result = |value: Value| { + // Only record it if there is room. + if result_write_idx < result.len() { + result[result_write_idx] = value; + result_write_idx += 1; + } + }; + + // Now, to avoid reading the ends with a seek, to make the interpolation behave we actually pretend there is 0 entry + // key in the first position and a max valued key in the last position. These will never get read, but they will + // be used to calculate the interpolation. + let mut lo = 0; + let mut lo_key = 0; + let mut hi = num_entries + 1; // Index of last entry, with 2 ghost entries to denote the beginning and end. + let mut hi_key = u64::MAX; + + // Function to query the probe location. + let compute_probe_location = |lo: u64, lo_key: u64, hi: u64, hi_key: u64| { + (lo + ((key - lo_key) as f64 / (hi_key - lo_key) as f64 * (hi - lo) as f64).floor() as u64) + .max(lo + 1) + .min(hi - 1) + }; + + let mut probe_index = compute_probe_location(lo, lo_key, hi, hi_key); + + while lo + READ_WINDOW_SIZE < hi { + // The minus 1 is to handle the shift because of making lo_key == 0 + reader.seek(SeekFrom::Start(read_start + (probe_index - 1) * pair_size))?; + + // First, probe the first entry. + let probe_key = read_u64(reader)?; + + match key.cmp(&probe_key) { + Ordering::Less => { + hi = probe_index; + hi_key = probe_key; + + // Recompute the probe index for the next go. + let candidate_probe_index = compute_probe_location(lo, lo_key, hi, hi_key); + + // Make sure the new probe index is at least READ_WINDOW_SIZE away to make this efficient. + if candidate_probe_index + READ_WINDOW_SIZE > probe_index { + // Safely set this to the current position minus READ_WINDOW_SIZE so the next probe + // likely just reads in all the values between that and the current one if applicable. + let jump_amount = (READ_WINDOW_SIZE).min(probe_index - (lo + 1)); + probe_index -= jump_amount; + } else { + probe_index = candidate_probe_index; + } + } + Ordering::Equal => { + // Read out this value. + write_result(read_value_function(reader)?); + + // Now, read ahead until we've filled all the possible duplicates from this range.. + for _ in (probe_index + 1)..hi { + if read_u64(reader)? != key { + break; + } + write_result(read_value_function(reader)?); + } + + hi = probe_index; + hi_key = probe_key; + + // Since we know we're part of a block of keys, + // and we're assuming that very few keys are actually the same (but need to account + // for all possibilities), then set the probe index to be just a bit before this one. + + let jump_amount = (EXPECTED_MAX_NUM_DUPLICATES).min(probe_index - (lo + 1)); + probe_index -= jump_amount; + } + Ordering::Greater => { + lo = probe_index; + lo_key = probe_key; + + // Repeatedly test this new candidate probe index. + let candidate_probe_index = compute_probe_location(lo, lo_key, hi, hi_key); + + // Jump at least READ_WINDOW_SIZE away + if candidate_probe_index - probe_index <= READ_WINDOW_SIZE { + probe_index = (lo + READ_WINDOW_SIZE).min(hi - 1); + } else { + probe_index = candidate_probe_index; + } + } + }; + } + + // Seek to read everything in the (lo, hi) range. + reader.seek(SeekFrom::Start(read_start + lo * pair_size))?; + + while lo + 1 < hi { + let (probe_key, probe_value) = (read_u64(reader)?, read_value_function(reader)?); + lo += 1; + + match key.cmp(&probe_key) { + Ordering::Less => { + // We're done. + break; + } + Ordering::Equal => { + write_result(probe_value); + } + Ordering::Greater => { + // Keep going + continue; + } + } + } + + Ok(result_write_idx) +} + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, io::Cursor}; + + use super::*; + use rand::prelude::*; + + fn test_interpolation_search( + keys: &[u64], + alt_query_keys: &[u64], + ) -> Result<(), std::io::Error> { + let mut values: Vec<(u64, u64)> = keys + .iter() + .enumerate() + .map(|(i, k)| (*k, 100 + i as u64)) + .collect(); + values.sort_unstable(); + + // First, serialize out the values, and build a + let data_start = 0; + let mut data = vec![0xFFu8; data_start]; // Start off with some. + + let mut all_values = HashMap::>::new(); + + for (k, v) in values.iter() { + all_values.entry(*k).or_default().push(*v); + write_u64(&mut data, *k)?; + write_u64(&mut data, *v)?; + } + + // Now, loop through all the values, running the query function and checking if it works. + let mut dest_values = Vec::::new(); + + for (k, v) in all_values { + dest_values.clear(); + dest_values.resize(v.len() + 1, 0); + + let n_items_found = search_on_sorted_u64s( + &mut Cursor::new(&data), + data_start as u64, + values.len() as u64, + k, + read_u64::>>, + &mut dest_values, + )?; + + // Make sure we found the correct amount. + assert_eq!(n_items_found, v.len()); + + // Clip off the last one, unused. + dest_values.resize(v.len(), 0); + + // Sort it so we can do a proper comparison + dest_values.sort_unstable(); + + assert_eq!(dest_values, v); + } + + // Now test all the other values given that are not in the map. + dest_values.resize(8, 0); + + for k in alt_query_keys { + let n_items_found = search_on_sorted_u64s( + &mut Cursor::new(&data), + data_start as u64, + values.len() as u64, + *k, + read_u64::>>, + &mut dest_values, + )?; + assert_eq!(n_items_found, 0); + } + + Ok(()) + } + + #[test] + fn test_sanity_1() -> Result<(), std::io::Error> { + test_interpolation_search(&[1], &[]) + } + #[test] + fn test_sanity_2() -> Result<(), std::io::Error> { + test_interpolation_search(&[1, 3], &[0, 2, 4, 6, 8]) + } + + #[test] + fn test_empty() -> Result<(), std::io::Error> { + test_interpolation_search(&[], &[1, 2, 4, 6, 8, u64::MAX]) + } + + #[test] + fn test_all_zeros() -> Result<(), std::io::Error> { + test_interpolation_search(&[0; 1], &[u64::MAX, 1, 2, 4, 6, 8]) + } + + #[test] + fn test_all_max() -> Result<(), std::io::Error> { + test_interpolation_search(&vec![u64::MAX; 100], &[0, 1, 2, 4, 6, 8]) + } + + #[test] + fn test_large_random_unique() -> Result<(), std::io::Error> { + let mut v = Vec::::new(); + let mut rng = StdRng::seed_from_u64(0); + + for _ in 0..100 { + v.push(rng.gen()); + } + + test_interpolation_search(&v[..], &[0, u64::MAX]) + } + + #[test] + fn test_large_random_multiples() -> Result<(), std::io::Error> { + let mut v = Vec::::new(); + let mut rng = StdRng::seed_from_u64(0); + + for _ in 0..200 { + let len = rng.gen_range(1..8); + let x: u64 = rng.gen(); + v.resize(v.len() + len, x); + } + + test_interpolation_search(&v[..], &[0, u64::MAX]) + } +} diff --git a/mdb_shard/src/session_directory.rs b/mdb_shard/src/session_directory.rs new file mode 100644 index 00000000..0b6d1233 --- /dev/null +++ b/mdb_shard/src/session_directory.rs @@ -0,0 +1,144 @@ +use crate::error::Result; +use crate::set_operations::shard_set_union; +use crate::shard_file_handle::MDBShardFile; +use merklehash::MerkleHash; +use std::collections::HashSet; +use std::io::Cursor; +use std::io::Read; +use std::mem::swap; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; +use tracing::debug; + +// Merge a collection of shards. +// After calling this, the passed in shards may be invalid -- i.e. may refer to a shard that doesn't exist. +// All shards are either merged into shards in the result directory or moved to that directory (if not there already). +// +// Ordering of staged shards is preserved. + +#[allow(clippy::needless_range_loop)] // The alternative is less readable IMO +pub fn consolidate_shards_in_directory( + session_directory: &Path, + target_max_size: u64, +) -> Result> { + let mut shards: Vec<(SystemTime, _)> = MDBShardFile::load_all(session_directory)? + .into_iter() + .map(|sf| Ok((std::fs::metadata(&sf.path)?.modified()?, sf))) + .collect::>>()?; + + shards.sort_unstable_by_key(|(t, _)| *t); + + // Make not mutable + let shards: Vec<_> = shards.into_iter().map(|(_, s)| s).collect(); + + let mut finished_shards = Vec::::with_capacity(shards.len()); + let mut finished_shard_hashes = HashSet::::with_capacity(shards.len()); + + let mut cur_data = Vec::::with_capacity(target_max_size as usize); + let mut alt_data = Vec::::with_capacity(target_max_size as usize); + let mut out_data = Vec::::with_capacity(target_max_size as usize); + + let mut cur_idx = 0; + + { + while cur_idx < shards.len() { + let cur_sfi: &MDBShardFile = &shards[cur_idx]; + + // Now, see how many we can consolidate. + let mut ub_idx = cur_idx + 1; + let mut current_size = cur_sfi.shard.num_bytes(); + + // Do we have to remove any shards along the way? + let mut shards_to_remove = Vec::<(MerkleHash, PathBuf)>::new(); + + for idx in (cur_idx + 1).. { + if idx == shards.len() + || shards[idx].shard.num_bytes() + current_size >= target_max_size + { + ub_idx = idx; + break; + } + current_size += shards[idx].shard.num_bytes() + } + + if ub_idx == cur_idx + 1 { + // We can't consolidate any here. + + finished_shard_hashes.insert(cur_sfi.shard_hash); + finished_shards.push(cur_sfi.clone()); + } else { + // We have one or more shards to merge, so do this all in memory. + + // Get the current data in a buffer + let mut cur_shard_info = cur_sfi.shard.clone(); + + cur_data.clear(); + std::fs::File::open(&cur_sfi.path)?.read_to_end(&mut cur_data)?; + + // Now, merge in everything in memory + for i in (cur_idx + 1)..ub_idx { + let sfi = &shards[i]; + + alt_data.clear(); + std::fs::File::open(&sfi.path)?.read_to_end(&mut alt_data)?; + + // Now merge the main shard + out_data.clear(); + + // Merge these in to the current shard. + cur_shard_info = shard_set_union( + &cur_shard_info, + &mut Cursor::new(&cur_data), + &sfi.shard, + &mut Cursor::new(&alt_data), + &mut out_data, + )?; + + swap(&mut cur_data, &mut out_data); + } + + // Write out the shard. + let new_sfi = { + MDBShardFile::write_out_from_reader( + session_directory, + &mut Cursor::new(&cur_data), + )? + }; + + debug!( + "Created merged shard {:?} from shards {:?}", + &new_sfi.path, + shards[cur_idx..ub_idx].iter().map(|sfi| &sfi.path) + ); + + finished_shard_hashes.insert(new_sfi.shard_hash); + finished_shards.push(new_sfi); + + // Delete the old ones. + for sfi in shards[cur_idx..ub_idx].iter() { + shards_to_remove.push((sfi.shard_hash, sfi.path.to_path_buf())); + } + } + + for (shard_hash, path) in shards_to_remove.iter() { + if finished_shard_hashes.contains(shard_hash) { + // In rare cases, there could be empty shards or shards with + // duplicate entries and we don't want to delete any shards + // we've already finished + continue; + } + debug!( + "consolidate_shards: Removing {:?}; info merged to {:?}", + &path, + &finished_shards.last().unwrap().shard_hash + ); + + std::fs::remove_file(path)?; + } + + cur_idx = ub_idx; + } + } + + Ok(finished_shards) +} diff --git a/mdb_shard/src/set_operations.rs b/mdb_shard/src/set_operations.rs new file mode 100644 index 00000000..a8389344 --- /dev/null +++ b/mdb_shard/src/set_operations.rs @@ -0,0 +1,487 @@ +use crate::error::Result; +use crate::{ + cas_structs::{CASChunkSequenceEntry, CASChunkSequenceHeader}, + file_structs::{FileDataSequenceEntry, FileDataSequenceHeader}, + serialization_utils::{write_u32, write_u64}, + shard_format::{MDBShardFileFooter, MDBShardFileHeader, MDBShardInfo}, + utils::truncate_hash, +}; +use merklehash::{HashedWrite, MerkleHash}; +use std::{ + env::current_dir, + fs::File, + io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, + mem::size_of, + path::Path, +}; +use uuid::Uuid; + +#[derive(PartialEq, Debug, Copy, Clone)] +enum MDBSetOperation { + Union, + Difference, +} + +enum NextAction { + CopyToOut, + SkipOver, + Nothing, +} + +#[inline] +fn get_next_actions( + h1: Option<&MerkleHash>, + h2: Option<&MerkleHash>, + op: MDBSetOperation, +) -> Option<[NextAction; 2]> { + match (h1, h2) { + (None, None) => None, + (Some(_), None) => { + if op == MDBSetOperation::Union { + Some([NextAction::CopyToOut, NextAction::Nothing]) + } else { + Some([NextAction::SkipOver, NextAction::Nothing]) + } + } + (None, Some(_)) => Some([NextAction::Nothing, NextAction::CopyToOut]), + (Some(ft0), Some(ft1)) => match ft0.cmp(ft1) { + std::cmp::Ordering::Less => { + if op == MDBSetOperation::Union { + Some([NextAction::CopyToOut, NextAction::Nothing]) + } else { + Some([NextAction::SkipOver, NextAction::Nothing]) + } + } + std::cmp::Ordering::Equal => { + if op == MDBSetOperation::Union { + Some([NextAction::CopyToOut, NextAction::SkipOver]) + } else { + Some([NextAction::SkipOver, NextAction::SkipOver]) + } + } + std::cmp::Ordering::Greater => Some([NextAction::Nothing, NextAction::CopyToOut]), + }, + } +} + +fn set_operation( + s: [&MDBShardInfo; 2], + r: [&mut R; 2], + out: &mut W, + op: MDBSetOperation, +) -> Result { + let mut out_offset = 0u64; + + let mut footer = MDBShardFileFooter::default(); + + // Write out the header to the output. + let header = MDBShardFileHeader::default(); + out_offset += header.serialize(out)? as u64; + + /////////////////////////////////// + // File info section. + // Set up the seek for the first section: + r[0].seek(SeekFrom::Start(s[0].metadata.file_info_offset))?; + r[1].seek(SeekFrom::Start(s[1].metadata.file_info_offset))?; + + footer.file_info_offset = out_offset; + + { + // Manually go through the whole file info section and + + // TODO: if we run out of space someday, this should be made into a disk-backed stack. + let mut file_lookup_data = Vec::<(u64, u32)>::new(); + let mut current_index = 0; + + let load_next = |_r: &mut R, _s: &MDBShardInfo| -> Result<_> { + let fdsh = FileDataSequenceHeader::deserialize(_r)?; + if fdsh.file_hash == MerkleHash::default() { + Ok(None) + } else { + Ok(Some(fdsh)) + } + }; + + let mut file_data_header = [load_next(r[0], s[0])?, load_next(r[1], s[1])?]; + + while let Some(action) = get_next_actions( + file_data_header[0].as_ref().map(|h| &h.file_hash), + file_data_header[1].as_ref().map(|h| &h.file_hash), + op, + ) { + for i in [0, 1] { + match action[i] { + NextAction::CopyToOut => { + let fh = file_data_header[i].as_ref().unwrap(); + let n_payload_bytes = + (fh.num_entries as u64) * (size_of::() as u64); + + out_offset += fh.serialize(out)? as u64; + + for _ in 0..fh.num_entries { + let entry = FileDataSequenceEntry::deserialize(r[i])?; + footer.materialized_bytes += entry.unpacked_segment_bytes as u64; + entry.serialize(out)?; + } + + out_offset += n_payload_bytes; + + file_lookup_data.push((truncate_hash(&fh.file_hash), current_index)); + + current_index += 1 + fh.num_entries; + file_data_header[i] = load_next(r[i], s[i])?; + } + NextAction::SkipOver => { + let fh = file_data_header[i].as_ref().unwrap(); + r[i].seek(SeekFrom::Current( + (fh.num_entries as i64) * (size_of::() as i64), + ))?; + file_data_header[i] = load_next(r[i], s[i])?; + } + NextAction::Nothing => {} + }; + } + } + out_offset += FileDataSequenceHeader::default().serialize(out)? as u64; + + footer.file_lookup_offset = out_offset; + footer.file_lookup_num_entry = file_lookup_data.len() as u64; + out_offset += (file_lookup_data.len() * (size_of::() + size_of::())) as u64; + for (h, idx) in file_lookup_data { + write_u64(out, h)?; + write_u32(out, idx)?; + } + } + + { + /////////////////////////////////// + // CAS info section. + // Set up the seek for the first section: + footer.cas_info_offset = out_offset; + + r[0].seek(SeekFrom::Start(s[0].metadata.cas_info_offset))?; + r[1].seek(SeekFrom::Start(s[1].metadata.cas_info_offset))?; + + // Manually go through the whole file info section and + + // TODO: if we run out of space someday, this should be made into a disk-backed stack. + let mut cas_lookup_data = Vec::<(u64, u32)>::new(); + let mut chunk_lookup_data = Vec::<(u64, (u32, u32))>::new(); + + let mut current_index = 0; + + let load_next = |_r: &mut R, _s: &MDBShardInfo| -> Result<_> { + let ccsh = CASChunkSequenceHeader::deserialize(_r)?; + if ccsh.cas_hash == MerkleHash::default() { + Ok(None) + } else { + Ok(Some(ccsh)) + } + }; + + let mut cas_data_header = [load_next(r[0], s[0])?, load_next(r[1], s[1])?]; + + while let Some(action) = get_next_actions( + cas_data_header[0].as_ref().map(|h| &h.cas_hash), + cas_data_header[1].as_ref().map(|h| &h.cas_hash), + op, + ) { + for i in [0, 1] { + match action[i] { + NextAction::CopyToOut => { + let fh = cas_data_header[i].as_ref().unwrap(); + footer.stored_bytes_on_disk += fh.num_bytes_on_disk as u64; + footer.stored_bytes += fh.num_bytes_in_cas as u64; + + out_offset += fh.serialize(out)? as u64; + + for j in 0..fh.num_entries { + let chunk = CASChunkSequenceEntry::deserialize(r[i])?; + + chunk_lookup_data + .push((truncate_hash(&chunk.chunk_hash), (current_index, j))); + out_offset += chunk.serialize(out)? as u64; + } + + cas_lookup_data.push((truncate_hash(&fh.cas_hash), current_index)); + + current_index += 1 + fh.num_entries; + cas_data_header[i] = load_next(r[i], s[i])?; + } + NextAction::SkipOver => { + let fh = cas_data_header[i].as_ref().unwrap(); + r[i].seek(SeekFrom::Current( + (fh.num_entries as i64) * (size_of::() as i64), + ))?; + cas_data_header[i] = load_next(r[i], s[i])?; + } + NextAction::Nothing => {} + }; + } + } + + out_offset += CASChunkSequenceHeader::default().serialize(out)? as u64; + + // Write out the cas and chunk lookup sections. + footer.cas_lookup_offset = out_offset; + footer.cas_lookup_num_entry = cas_lookup_data.len() as u64; + out_offset += (cas_lookup_data.len() * (size_of::() + size_of::())) as u64; + + for (h, idx) in cas_lookup_data { + write_u64(out, h)?; + write_u32(out, idx)?; + } + + // TODO: use radix sort on this? + chunk_lookup_data.sort_unstable_by_key(|t| t.0); + + // Write out the cas and chunk lookup sections. + footer.chunk_lookup_offset = out_offset; + footer.chunk_lookup_num_entry = chunk_lookup_data.len() as u64; + out_offset += (chunk_lookup_data.len() * (size_of::() + 2 * size_of::())) as u64; + + for (h, (i1, i2)) in chunk_lookup_data { + write_u64(out, h)?; + write_u32(out, i1)?; + write_u32(out, i2)?; + } + } + + // Finally, rewrite the footer. + { + footer.footer_offset = out_offset; + footer.serialize(out)?; + } + + Ok(MDBShardInfo { + header, + metadata: footer, + }) +} + +/// Given unions +pub fn shard_set_union( + s1: &MDBShardInfo, + r1: &mut R, + s2: &MDBShardInfo, + r2: &mut R, + out: &mut W, +) -> Result { + set_operation([s1, s2], [r1, r2], out, MDBSetOperation::Union) +} + +pub fn shard_set_difference( + s1: &MDBShardInfo, + r1: &mut R, + s2: &MDBShardInfo, + r2: &mut R, + out: &mut W, +) -> Result { + set_operation([s1, s2], [r1, r2], out, MDBSetOperation::Difference) +} + +fn open_shard_with_bufreader(path: &Path) -> Result<(MDBShardInfo, BufReader)> { + let mut reader = BufReader::new(File::open(path)?); + + let mdb = MDBShardInfo::load_from_file(&mut reader)?; + + Ok((mdb, reader)) +} + +/// Merge two shard files, returning the Merkle hash of the resulting set operation +fn shard_file_op( + f1: &Path, + f2: &Path, + out: &Path, + op: MDBSetOperation, +) -> Result<(MerkleHash, MDBShardInfo)> { + let cur_dir = current_dir()?; + let dir = out.parent().unwrap_or(&cur_dir); + + let uuid = Uuid::new_v4(); + + let temp_file_name = dir.join(format!(".{uuid}.mdb_temp")); + + let mut hashed_write; // Need to access after file is closed. + // Scoped so that file is closed and flushed before name is changed. + + let shard; + { + let temp_file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&temp_file_name)?; + + hashed_write = HashedWrite::new(temp_file); + + let mut buf_write = BufWriter::new(&mut hashed_write); + + // Do the shard op + + let (s1, mut r1) = open_shard_with_bufreader(f1)?; + let (s2, mut r2) = open_shard_with_bufreader(f2)?; + + shard = set_operation([&s1, &s2], [&mut r1, &mut r2], &mut buf_write, op)?; + buf_write.flush()?; + } + // Get the hash + hashed_write.flush()?; + let shard_hash = hashed_write.hash(); + + std::fs::rename(&temp_file_name, out)?; + + Ok((shard_hash, shard)) +} + +/// Performs a set union operation on two shard files, writing the result to a third file and +/// returning the MerkleHash of the resulting shard file. +/// +pub fn shard_file_union(f1: &Path, f2: &Path, out: &Path) -> Result<(MerkleHash, MDBShardInfo)> { + shard_file_op(f1, f2, out, MDBSetOperation::Union) +} + +/// Performs a set difference operation on two shard files, writing the result to a third file and +/// returning the MerkleHash of the resulting shard file. +/// +pub fn shard_file_difference( + f1: &Path, + f2: &Path, + out: &Path, +) -> Result<(MerkleHash, MDBShardInfo)> { + shard_file_op(f1, f2, out, MDBSetOperation::Difference) +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + use crate::error::Result; + use crate::{shard_format::test_routines::*, shard_in_memory::MDBInMemoryShard}; + use merklehash::compute_data_hash; + use tempdir::TempDir; + + fn test_operations( + mem_shard_1: &MDBInMemoryShard, + mem_shard_2: &MDBInMemoryShard, + ) -> Result<()> { + let disk_shard_1 = convert_to_file(mem_shard_1)?; + let disk_shard_2 = convert_to_file(mem_shard_2)?; + + verify_mdb_shards_match(mem_shard_1, Cursor::new(&disk_shard_1))?; + verify_mdb_shards_match(mem_shard_2, Cursor::new(&disk_shard_2))?; + + // Now write these out to disk to verify them + let tmp_dir = TempDir::new("gitxet_shard_set_test")?; + + let shard_path_1 = tmp_dir.path().join("shard_1.mdb"); + let shard_path_2 = tmp_dir.path().join("shard_2.mdb"); + + std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(&shard_path_1)? + .write_all(&disk_shard_1[..])?; + + std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(&shard_path_2)? + .write_all(&disk_shard_2[..])?; + + let mut r1 = Cursor::new(&disk_shard_1); + let s1 = MDBShardInfo::load_from_file(&mut r1)?; + + let mut r2 = Cursor::new(&disk_shard_2); + let s2 = MDBShardInfo::load_from_file(&mut r2)?; + + let mem_union = mem_shard_1.union(mem_shard_2)?; + let mut shard_union = Vec::::new(); + shard_set_union(&s1, &mut r1, &s2, &mut r2, &mut shard_union)?; + verify_mdb_shards_match(&mem_union, Cursor::new(&shard_union))?; + + let disk_union_path = tmp_dir.path().join("shard_union.mdb"); + let (disk_union_hash, _) = + shard_file_union(&shard_path_1, &shard_path_2, &disk_union_path)?; + + let mut disk_union_reader = BufReader::new(File::open(&disk_union_path)?); + verify_mdb_shards_match(&mem_union, &mut disk_union_reader)?; + assert_eq!(disk_union_hash, compute_data_hash(&shard_union[..])); + + let mem_difference = mem_shard_1.difference(mem_shard_2)?; + let mut shard_difference = Vec::::new(); + shard_set_difference(&s1, &mut r1, &s2, &mut r2, &mut shard_difference)?; + verify_mdb_shards_match(&mem_difference, Cursor::new(&shard_difference))?; + + let disk_difference_path = tmp_dir.path().join("shard_difference.mdb"); + let (disk_difference_hash, _) = + shard_file_difference(&shard_path_1, &shard_path_2, &disk_difference_path)?; + + let mut disk_difference_reader = BufReader::new(File::open(&disk_difference_path)?); + verify_mdb_shards_match(&mem_difference, &mut disk_difference_reader)?; + assert_eq!( + disk_difference_hash, + compute_data_hash(&shard_difference[..]) + ); + + Ok(()) + } + + #[test] + fn test_simple() -> Result<()> { + let mem_shard_1 = gen_specific_shard(&[(10, &[(21, 5)])], &[(100, &[(200, (0, 5))])])?; + let mem_shard_2 = gen_specific_shard(&[(11, &[(22, 5)])], &[(101, &[(201, (0, 5))])])?; + + test_operations(&mem_shard_1, &mem_shard_2) + } + + #[test] + fn test_intersecting() -> Result<()> { + let mem_shard_1 = gen_specific_shard(&[(10, &[(21, 5)])], &[(100, &[(200, (0, 5))])])?; + let mem_shard_2 = gen_specific_shard(&[(10, &[(21, 5)])], &[(100, &[(200, (0, 5))])])?; + + test_operations(&mem_shard_1, &mem_shard_2) + } + + #[test] + fn test_intersecting_2() -> Result<()> { + let mem_shard_1 = gen_specific_shard(&[(10, &[(21, 5)])], &[(100, &[(200, (0, 5))])])?; + let mem_shard_2 = gen_specific_shard( + &[(10, &[(21, 5)]), (11, &[(22, 5)])], + &[(100, &[(200, (0, 5))]), (101, &[(201, (0, 5))])], + )?; + + test_operations(&mem_shard_1, &mem_shard_2) + } + + #[test] + fn test_empty() -> Result<()> { + let mem_shard_1 = gen_specific_shard(&[], &[])?; + let mem_shard_2 = gen_specific_shard(&[], &[])?; + + test_operations(&mem_shard_1, &mem_shard_2) + } + #[test] + fn test_empty_2() -> Result<()> { + let mem_shard_1 = gen_random_shard(0, &[0], &[0])?; + let mem_shard_2 = gen_random_shard(1, &[0], &[0])?; + + test_operations(&mem_shard_1, &mem_shard_2) + } + + #[test] + fn test_random() -> Result<()> { + let mem_shard_1 = gen_random_shard(0, &[1, 5, 10, 8], &[4, 3, 5, 9, 4, 6])?; + let mem_shard_2 = gen_random_shard(1, &[3, 5, 9, 8], &[8, 5, 5, 8, 5, 6])?; + + test_operations(&mem_shard_1, &mem_shard_2)?; + + let mem_shard_3 = mem_shard_1.union(&mem_shard_2)?; + + test_operations(&mem_shard_1, &mem_shard_3)?; + test_operations(&mem_shard_2, &mem_shard_3)?; + + Ok(()) + } +} diff --git a/mdb_shard/src/shard_benchmark.rs b/mdb_shard/src/shard_benchmark.rs new file mode 100644 index 00000000..bae7de14 --- /dev/null +++ b/mdb_shard/src/shard_benchmark.rs @@ -0,0 +1,248 @@ +use anyhow::{Ok, Result}; +use clap::{App, Arg}; +use mdb_shard::cas_structs::{CASChunkSequenceEntry, CASChunkSequenceHeader, MDBCASInfo}; +use mdb_shard::shard_file_manager::ShardFileManager; +use mdb_shard::shard_format::test_routines::rng_hash; +use mdb_shard::shard_format::MDBShardInfo; +use mdb_shard::shard_in_memory::MDBInMemoryShard; +use merklehash::MerkleHash; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tempdir::TempDir; +use tokio::time; + +const CAS_BLOCK_SIZE: usize = 512; +const PAR_TASK: usize = 1; + +fn make_shard(size: u64, seed: &mut u64) -> MDBInMemoryShard { + let mut shard = MDBInMemoryShard::default(); + + while shard.shard_file_size() < size { + let mut cas_block = Vec::<_>::new(); + let mut pos = 0u32; + + for _ in 0..CAS_BLOCK_SIZE { + let h = rng_hash(*seed); + + let r = (1000 + (&h as &[u64; 4])[0] % 1000) as u32; + cas_block.push(CASChunkSequenceEntry::new(rng_hash(*seed), r, pos)); + pos += r; + *seed += 1; + } + + shard + .add_cas_block(MDBCASInfo { + metadata: CASChunkSequenceHeader::new(rng_hash(!(*seed)), CAS_BLOCK_SIZE, pos), + chunks: cas_block, + }) + .unwrap(); + } + + shard +} + +async fn run_shard_benchmark( + shard_sizes: Vec<(u64, u64)>, + file_contiguity: usize, + contiguity: usize, + block_hit_proportion: f64, + dir: &Path, +) -> Result<()> { + let mut seed = 0u64; + + eprintln!("Creating shards."); + + for (n_shards, target_size) in shard_sizes { + for i in 0..n_shards { + let shard = make_shard(target_size, &mut seed); + let path = shard.write_to_directory(dir).unwrap(); + + eprintln!( + "-> Target size {target_size:?}: Created shard {:?} / {n_shards:?} with {} CAS blocks and {} chunks", + i + 1, shard.num_cas_entries(), shard.num_cas_entries() * CAS_BLOCK_SIZE + ); + MDBShardInfo::load_from_file(&mut File::open(path)?)?.print_report(); + } + } + eprintln!("Shards created."); + + // Now, spawn tasks to + let counter = Arc::new(AtomicUsize::new(0)); + let mdb = Arc::new(ShardFileManager::new(dir).await?); + + let start_time = Instant::now(); + + // Spawn worker tasks + let mut tasks = Vec::new(); + for t in 0..PAR_TASK { + let top = seed; + let counter_clone = counter.clone(); + let mdb_ref = mdb.clone(); + + tasks.push(tokio::spawn(async move { + let mut rng = StdRng::seed_from_u64(t as u64); + eprintln!("Worker {t:?} running."); + + loop { + let mut hash_val = rng.gen(); + + let mut file_info = Vec::::with_capacity(file_contiguity); + let hit = rng.gen_bool(block_hit_proportion); + + for _ in 0..file_contiguity { + let h_seed = if hit { hash_val % top } else { hash_val }; + hash_val += 1; + file_info.push(rng_hash(h_seed)); + } + + let mut query_loc = 0; + + while query_loc < file_contiguity { + let res = mdb_ref + .chunk_hash_dedup_query( + &file_info[query_loc..(query_loc + contiguity).min(file_info.len())], + None, + ) + .await + .unwrap(); + + query_loc += match res { + Some((i, _)) => i, + None => 1, + }; + } + counter_clone.fetch_add(query_loc, Ordering::Relaxed); + } + })); + } + + // Spawn the printing task + let counter_clone = counter.clone(); + + let print_task = tokio::spawn({ + async move { + loop { + time::sleep(Duration::from_secs(1)).await; + let elapsed_time = start_time.elapsed().as_secs_f64(); + let count = counter_clone.load(Ordering::Relaxed); + println!( + "{count} queries, queries per second: {}", + count as f64 / elapsed_time + ); + } + } + }); + + // Wait for all tasks to complete + for task in tasks { + task.await.unwrap(); + } + print_task.await.unwrap(); + Ok(()) +} + +fn parse_arg(arg: &str) -> (u64, u64) { + let parts: Vec<&str> = arg.split(':').collect(); + if parts.len() != 2 { + panic!("Failed to parse argument: {arg}"); + } + + let size1 = u64::from_str(parts[0]).expect("Failed to parse size1"); + let size2 = u64::from_str(parts[1]).expect("Failed to parse size2"); + + (size1, size2) +} + +#[tokio::main] +async fn main() { + let arg_res = App::new("ShardBenchmark") + .arg( + Arg::new("SIZE") + .multiple_values(true) + .required(true) + .help("Sizes to be parsed"), + ) + .arg( + Arg::new("contiguity") + .long("contiguity") + .takes_value(true) + .default_value("1") + .help("Number of contiguous hashes to call dedup with."), + ) + .arg( + Arg::new("hit_percent") + .long("hit_percent") + .takes_value(true) + .default_value("50") + .help("The percentage of queries to hit a known block."), + ) + .arg( + Arg::new("file_contiguity") + .long("file_contiguity") + .takes_value(true) + .default_value("16") + .help("How many blocks in a file are contiguous in the same hash."), + ) + .arg( + Arg::new("dir") + .long("dir") + .takes_value(true) + .default_value("") + .help("Directory to use"), + ) + .about("A program to run shard benchmarks") + .get_matches(); + + let shard_sizes: Vec<(u64, u64)> = arg_res.values_of("SIZE").unwrap().map(parse_arg).collect(); + + let contiguity: usize = arg_res + .value_of("contiguity") + .unwrap() + .parse() + .expect("Failed to parse contiguity"); + + let file_contiguity: usize = arg_res + .value_of("file_contiguity") + .unwrap() + .parse() + .expect("Failed to parse file_contiguity"); + + let hit_percent: f64 = arg_res + .value_of("hit_percent") + .unwrap() + .parse() + .expect("Failed to parse hit_percent"); + + let temp_dir = TempDir::new("git-xet-shard").expect("Failed to create temp dir"); + + let dir: &str = arg_res.value_of("dir").unwrap(); + + let dir = if dir.is_empty() { + temp_dir.path().to_path_buf() + } else { + PathBuf::from_str(dir).unwrap() + }; + eprintln!("Using dir {dir:?}"); + + let dir = std::fs::canonicalize(dir).unwrap(); + + eprintln!("Using dir {dir:?}"); + + assert!(dir.exists()); + + run_shard_benchmark( + shard_sizes, + contiguity, + file_contiguity, + hit_percent.clamp(0.0, 100.0) / 100.0, + &dir, + ) + .await + .unwrap(); +} diff --git a/mdb_shard/src/shard_dedup_probe.rs b/mdb_shard/src/shard_dedup_probe.rs new file mode 100644 index 00000000..20436ba9 --- /dev/null +++ b/mdb_shard/src/shard_dedup_probe.rs @@ -0,0 +1,16 @@ +use crate::error::Result; +use async_trait::async_trait; +use merklehash::MerkleHash; + +#[async_trait] +pub trait ShardDedupProber { + /// Probes which shards provides dedup information for a chunk. + /// Returns a list of shard hashes with key under 'prefix', + /// Err(_) if an error occured. + async fn get_dedup_shards( + &self, + prefix: &str, + chunk_hash: &[MerkleHash], + salt: &[u8; 32], + ) -> Result>; +} diff --git a/mdb_shard/src/shard_file.rs b/mdb_shard/src/shard_file.rs new file mode 100644 index 00000000..e7077f68 --- /dev/null +++ b/mdb_shard/src/shard_file.rs @@ -0,0 +1,2 @@ +// Temporary to transition dependent code to new location +pub use crate::shard_format::*; diff --git a/mdb_shard/src/shard_file_handle.rs b/mdb_shard/src/shard_file_handle.rs new file mode 100644 index 00000000..3b7e6016 --- /dev/null +++ b/mdb_shard/src/shard_file_handle.rs @@ -0,0 +1,248 @@ +use crate::cas_structs::CASChunkSequenceHeader; +use crate::error::{MDBShardError, Result}; +use crate::file_structs::{FileDataSequenceEntry, MDBFileInfo}; +use crate::utils::{shard_file_name, temp_shard_file_name}; +use crate::{shard_format::MDBShardInfo, utils::parse_shard_filename}; +use merklehash::{compute_data_hash, HashedWrite, MerkleHash}; +use std::io::{BufReader, Read, Seek, Write}; +use std::path::{Path, PathBuf}; +use tracing::{debug, error, warn}; + +/// When a specific implementation of the +/// +#[derive(Debug, Clone, Default)] +pub struct MDBShardFile { + pub shard_hash: MerkleHash, + pub path: PathBuf, + pub shard: MDBShardInfo, +} + +impl MDBShardFile { + pub fn new(shard_hash: MerkleHash, path: PathBuf, shard: MDBShardInfo) -> Result { + let s = Self { + shard_hash, + path, + shard, + }; + + s.verify_shard_integrity_debug_only(); + Ok(s) + } + + pub fn write_out_from_reader( + target_directory: &Path, + reader: &mut R, + ) -> Result { + let mut hashed_write; // Need to access after file is closed. + + let temp_file_name = target_directory.join(temp_shard_file_name()); + + { + // Scoped so that file is closed and flushed before name is changed. + + let out_file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&temp_file_name)?; + + hashed_write = HashedWrite::new(out_file); + + std::io::copy(reader, &mut hashed_write)?; + } + + // Get the hash + hashed_write.flush()?; + let shard_hash = hashed_write.hash(); + + let full_file_name = target_directory.join(shard_file_name(&shard_hash)); + + std::fs::rename(&temp_file_name, &full_file_name)?; + + Self::new( + shard_hash, + full_file_name, + MDBShardInfo::load_from_file(reader)?, + ) + } + + /// Loads the MDBShardFile struct from + /// + pub fn load_from_file(path: &Path) -> Result { + if let Some(shard_hash) = parse_shard_filename(path.to_str().unwrap()) { + let mut f = std::fs::File::open(path)?; + Ok(Self::new( + shard_hash, + std::fs::canonicalize(path)?, + MDBShardInfo::load_from_file(&mut f)?, + )?) + } else { + Err(MDBShardError::BadFilename(format!( + "{path:?} not a valid MerkleDB filename." + ))) + } + } + + pub fn load_all(path: &Path) -> Result> { + let mut shards = Vec::new(); + + if path.is_dir() { + for entry in std::fs::read_dir(path)? { + let entry = entry?; + if let Some(file_name) = entry.file_name().to_str() { + if let Some(h) = parse_shard_filename(file_name) { + shards.push((h, std::fs::canonicalize(entry.path())?)); + } + debug!("Found shard file '{file_name:?}'."); + } + } + } else if let Some(file_name) = path.to_str() { + if let Some(h) = parse_shard_filename(file_name) { + shards.push((h, std::fs::canonicalize(path)?)); + debug!("Registerd shard file '{file_name:?}'."); + } else { + return Err(MDBShardError::BadFilename(format!( + "Filename {file_name} not valid shard file name." + ))); + } + } + + let mut ret = Vec::with_capacity(shards.len()); + + for (shard_hash, path) in shards { + let mut f = std::fs::File::open(&path)?; + + ret.push(MDBShardFile { + shard_hash, + path, + shard: MDBShardInfo::load_from_file(&mut f)?, + }); + } + + #[cfg(debug_assertions)] + { + // In debug mode, verify all shards on loading to catch errors earlier. + for s in ret.iter() { + s.verify_shard_integrity_debug_only(); + } + } + + Ok(ret) + } + + #[inline] + pub fn read_all_cas_blocks(&self) -> Result> { + self.shard.read_all_cas_blocks(&mut self.get_reader()?) + } + + pub fn get_reader(&self) -> Result> { + Ok(BufReader::with_capacity( + 2048, + std::fs::File::open(&self.path)?, + )) + } + + #[inline] + pub fn get_file_reconstruction_info( + &self, + file_hash: &MerkleHash, + ) -> Result> { + self.shard + .get_file_reconstruction_info(&mut self.get_reader()?, file_hash) + } + + #[inline] + pub fn chunk_hash_dedup_query( + &self, + query_hashes: &[MerkleHash], + ) -> Result> { + self.shard + .chunk_hash_dedup_query(&mut self.get_reader()?, query_hashes) + } + + #[inline] + pub fn chunk_hash_dedup_query_direct( + &self, + query_hashes: &[MerkleHash], + cas_block_index: u32, + cas_chunk_offset: u32, + ) -> Result> { + self.shard.chunk_hash_dedup_query_direct( + &mut self.get_reader()?, + query_hashes, + cas_block_index, + cas_chunk_offset, + ) + } + + #[inline] + pub fn read_all_truncated_hashes(&self) -> Result> { + self.shard + .read_all_truncated_hashes(&mut self.get_reader()?) + } + + #[inline] + pub fn read_full_cas_lookup(&self) -> Result> { + self.shard.read_full_cas_lookup(&mut self.get_reader()?) + } + + #[inline] + pub fn verify_shard_integrity_debug_only(&self) { + #[cfg(debug_assertions)] + { + self.verify_shard_integrity(); + } + } + + pub fn verify_shard_integrity(&self) { + debug!("Verifying shard integrity for shard {:?}", &self.path); + + debug!("Header : {:?}", self.shard.header); + debug!("Metadata : {:?}", self.shard.metadata); + + let mut reader = self + .get_reader() + .map_err(|e| { + error!("Error getting reader: {e:?}"); + e + }) + .unwrap(); + + let mut data = Vec::with_capacity(self.shard.num_bytes() as usize); + reader.read_to_end(&mut data).unwrap(); + + // Check the hash + let hash = compute_data_hash(&data[..]); + assert_eq!(hash, self.shard_hash); + + // Check the parsed shard from the filename. + let parsed_shard_hash = parse_shard_filename(&self.path).unwrap(); + assert_eq!(hash, parsed_shard_hash); + + reader.rewind().unwrap(); + + // Check the parsed shard from the filename. + if let Some(parsed_shard_hash) = parse_shard_filename(&self.path) { + if hash != parsed_shard_hash { + error!("Hash parsed from filename does not match the computed hash; hash from filename={parsed_shard_hash:?}, hash of file={hash:?}"); + } + } else { + warn!("Unable to obtain hash from filename."); + } + + // Check the file info sections + reader.rewind().unwrap(); + + let fir = MDBShardInfo::read_file_info_ranges(&mut reader) + .map_err(|e| { + error!("Error reading file info ranges : {e:?}"); + e + }) + .unwrap(); + + debug_assert_eq!(fir.len() as u64, self.shard.metadata.file_lookup_num_entry); + debug!("Integrity test passed for shard {:?}", &self.path); + + // TODO: More parts; but this will at least succeed on the server end. + } +} diff --git a/mdb_shard/src/shard_file_manager.rs b/mdb_shard/src/shard_file_manager.rs new file mode 100644 index 00000000..70247a92 --- /dev/null +++ b/mdb_shard/src/shard_file_manager.rs @@ -0,0 +1,977 @@ +use crate::error::Result; +use crate::shard_file_handle::MDBShardFile; +use crate::shard_file_reconstructor::FileReconstructor; +use crate::utils::truncate_hash; +use async_trait::async_trait; +use merklehash::MerkleHash; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::sync::RwLock; +use tracing::{debug, error, info, trace}; + +use crate::constants::MDB_SHARD_MIN_TARGET_SIZE; +use crate::{cas_structs::*, file_structs::*, shard_in_memory::MDBInMemoryShard}; + +/// A wrapper struct for the in-memory shard to make sure that it gets flushed on teardown. +struct MDBShardFlushGuard { + shard: MDBInMemoryShard, + session_directory: Option, +} + +impl Drop for MDBShardFlushGuard { + fn drop(&mut self) { + if self.shard.is_empty() { + return; + } + + if let Some(sd) = &self.session_directory { + // Check if the flushing directory exists. + if !sd.is_dir() { + error!( + "Error flushing reconstruction data on shutdown: {sd:?} is not a directory or doesn't exist" + ); + return; + } + + self.flush().unwrap_or_else(|e| { + error!("Error flushing reconstruction data on shutdown: {e:?}"); + None + }); + } + } +} + +// Store a maximum of this many indices in memory +const CHUNK_INDEX_TABLE_MAX_DEFAULT_SIZE: usize = 64 * 1024 * 1024; + +#[derive(Default)] +struct ShardFileCollection { + shard_list: Vec<(MDBShardFile, bool)>, + shards: HashMap, + chunk_lookup: HashMap, + num_indexed_shards: usize, + chunk_index_max_size: usize, +} + +impl ShardFileCollection { + fn new() -> Self { + Self { + chunk_index_max_size: std::env::var("XET_MAX_CHUNK_CACHE_SIZE") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(CHUNK_INDEX_TABLE_MAX_DEFAULT_SIZE), + ..Default::default() + } + } +} + +pub struct ShardFileManager { + shard_file_lookup: Arc>, + current_state: Arc>, + target_shard_min_size: u64, +} + +struct ChunkCacheElement { + cas_start_index: u32, + cas_chunk_offset: u16, + shard_index: u16, // This one is last so that the u16 bits can be packed in. +} + +/// Shard file manager to manage all the shards. It is fully thread-safe and async enabled. +/// +/// Usage: +/// +/// // Session directory is where it stores shard and shard state. +/// let mut mng = ShardFileManager::new("") +/// +/// // Add other known shards with register_shards. +/// mng.register_shards(&[other shard, directories, etc.])?; +/// +/// // Run queries, add data, etc. with get_file_reconstruction_info, chunk_hash_dedup_query, +/// add_cas_block, add_file_reconstruction_info. +/// +/// // Finalize by calling process_session_directory +/// let new_shards = mdb.process_session_directory()?; +/// +/// // new_shards is the list of new shards for this session. +/// +impl ShardFileManager { + /// Construct a new shard file manager that uses session_directory as the temporary dumping + pub async fn new(session_directory: &Path) -> Result { + let session_directory = { + if session_directory == PathBuf::default() { + None + } else { + Some(std::fs::canonicalize(session_directory).map_err(|e| { + error!("Error accessing session directory {session_directory:?}: {e:?}"); + e + })?) + } + }; + + let s = Self { + shard_file_lookup: Arc::new(RwLock::new(ShardFileCollection::new())), + current_state: Arc::new(RwLock::new(MDBShardFlushGuard { + shard: MDBInMemoryShard::default(), + session_directory: session_directory.clone(), + })), + target_shard_min_size: MDB_SHARD_MIN_TARGET_SIZE, + }; + + if let Some(sd) = &session_directory { + s.register_shards_by_path(&[sd], false).await?; + } + Ok(s) + } + + // Clear out everything; used mainly for debugging. + pub async fn clear(&self) { + { + let mut sfc_rw = self.shard_file_lookup.write().await; + sfc_rw.shards.clear(); + sfc_rw.chunk_lookup.clear(); + } + + self.current_state.write().await.shard = <_>::default(); + } + + /// Sets the target value of a shard file size. By default, it is given by MDB_SHARD_MIN_TARGET_SIZE + pub fn set_target_shard_min_size(&mut self, s: u64) { + self.target_shard_min_size = s; + } + + /// Registers all the files in a directory with filenames matching the names + /// of an MerkleDB shard. + pub async fn register_shards_by_path>( + &self, + paths: &[P], + shards_are_permanent: bool, + ) -> Result> { + let mut new_shards = Vec::new(); + for p in paths { + new_shards.append(&mut MDBShardFile::load_all(p.as_ref())?); + } + + self.register_shards(&new_shards, shards_are_permanent) + .await?; + Ok(new_shards) + } + + pub async fn register_shards( + &self, + new_shards: &[MDBShardFile], + shards_are_permanent: bool, + ) -> Result<()> { + let mut shards_lg = self.shard_file_lookup.write().await; + + // Go through and register the shards in order of newest to oldest + let mut new_shards: Vec<(&MDBShardFile, SystemTime)> = new_shards + .iter() + .map(|s| { + let modified = std::fs::metadata(&s.path)?.modified()?; + Ok((s, modified)) + }) + .collect::>>()?; + + // Compare in reverse order to sort from newest to oldest + new_shards.sort_by(|(_, t1), (_, t2)| t2.cmp(t1)); + let num_shards = new_shards.len(); + + for (s, _) in new_shards { + debug!( + "register_shards: Registering shard {:?} at {:?}.", + s.shard_hash, s.path + ); + let cur_index = shards_lg.shard_list.len(); + let mut inserted = false; + + shards_lg.shards.entry(s.shard_hash).or_insert_with(|| { + inserted = true; + cur_index + }); + + if inserted { + shards_lg.shard_list.push((s.clone(), shards_are_permanent)); + if cur_index < u16::MAX as usize + && shards_lg.chunk_lookup.len() + s.shard.total_num_chunks() + < shards_lg.chunk_index_max_size + { + for (h, (cas_start_index, cas_chunk_offset)) in s.read_all_truncated_hashes()? { + if cas_chunk_offset > u16::MAX as u32 { + break; + } + let cas_chunk_offset = cas_chunk_offset as u16; + + shards_lg.chunk_lookup.insert( + h, + ChunkCacheElement { + cas_start_index, + cas_chunk_offset, + shard_index: cur_index as u16, + }, + ); + shards_lg.num_indexed_shards = cur_index + 1; + } + } + } + } + + if num_shards != 0 { + info!( + "Registered {} new shards, for {} shards total. Chunk pre-lookup now has {} chunks.", + num_shards, + shards_lg.shard_list.len(), + shards_lg.chunk_lookup.len() + ); + } + + Ok(()) + } + + pub async fn shard_is_registered(&self, shard_hash: &MerkleHash) -> bool { + self.shard_file_lookup + .read() + .await + .shards + .contains_key(shard_hash) + } + + // If the shard with the given hash is present, then it a handle to it is returned. If not, + // returns None. + pub async fn get_shard_handle( + &self, + shard_hash: &MerkleHash, + allow_temporary: bool, + ) -> Option { + let shards_lg = self.shard_file_lookup.read().await; + if let Some(index) = shards_lg.shards.get(shard_hash) { + let (sfi, is_permanent) = &shards_lg.shard_list[*index]; + if !*is_permanent && !allow_temporary { + None + } else { + Some(sfi.clone()) + } + } else { + None + } + } +} + +#[async_trait] +impl FileReconstructor for ShardFileManager { + // Given a file pointer, returns the information needed to reconstruct the file. + // The information is stored in the destination vector dest_results. The function + // returns true if the file hash was found, and false otherwise. + async fn get_file_reconstruction_info( + &self, + file_hash: &MerkleHash, + ) -> Result)>> { + if *file_hash == MerkleHash::default() { + return Ok(Some(( + MDBFileInfo { + metadata: FileDataSequenceHeader::new(MerkleHash::default(), 0), + segments: Vec::default(), + }, + None, + ))); + } + + // First attempt the in-memory version of this. + { + let lg = self.current_state.read().await; + let file_info = lg.shard.get_file_reconstruction_info(file_hash); + if let Some(fi) = file_info { + return Ok(Some((fi, None))); + } + } + + let current_shards = self.shard_file_lookup.read().await; + + for (si, _) in current_shards.shard_list.iter() { + trace!("Querying for hash {file_hash:?} in {:?}.", si.path); + if let Some(fi) = si.get_file_reconstruction_info(file_hash)? { + return Ok(Some((fi, Some(si.shard_hash)))); + } + } + + Ok(None) + } +} + +impl ShardFileManager { + // Performs a query of chunk hashes against known chunk hashes, matching + // as many of the values in query_hashes as possible. It returns the number + // of entries matched from the input hashes, the CAS block hash of the match, + // and the range matched from that block. + pub async fn chunk_hash_dedup_query( + &self, + query_hashes: &[MerkleHash], + origin_tracking: Option<&mut HashMap>, + ) -> Result> { + // First attempt the in-memory version of this. + { + let lg = self.current_state.read().await; + let ret = lg.shard.chunk_hash_dedup_query(query_hashes); + if ret.is_some() { + return Ok(ret); + } + } + + let shard_lg = self.shard_file_lookup.read().await; + + if let Some(cce) = shard_lg.chunk_lookup.get(&truncate_hash(&query_hashes[0])) { + let (si, is_permanent) = &shard_lg.shard_list[cce.shard_index as usize]; + + if let Some((count, fdse)) = si.chunk_hash_dedup_query_direct( + query_hashes, + cce.cas_start_index, + cce.cas_chunk_offset as u32, + )? { + if *is_permanent { + if let Some(tracker) = origin_tracking { + *tracker.entry(si.shard_hash).or_default() += count; + } + } + return Ok(Some((count, fdse))); + } + } + + // Now we skip the indices for the upcoming aspects of stuff. + for (si, is_permanent) in shard_lg.shard_list[shard_lg.num_indexed_shards..].iter() { + trace!("Querying for hash {:?} in {:?}.", &query_hashes[0], si.path); + if let Some((count, fdse)) = si.chunk_hash_dedup_query(query_hashes)? { + if *is_permanent { + if let Some(tracker) = origin_tracking { + *tracker.entry(si.shard_hash).or_default() += count; + } + } + return Ok(Some((count, fdse))); + } + } + + Ok(None) + } + + /// Add CAS info to the in-memory state. + pub async fn add_cas_block(&self, cas_block_contents: MDBCASInfo) -> Result<()> { + let mut lg = self.current_state.write().await; + + if lg.shard.shard_file_size() + cas_block_contents.num_bytes() >= self.target_shard_min_size + { + self.flush_internal(&mut lg).await?; + } + + lg.shard.add_cas_block(cas_block_contents)?; + + Ok(()) + } + + /// Add file reconstruction info to the in-memory state. + pub async fn add_file_reconstruction_info(&self, file_info: MDBFileInfo) -> Result<()> { + let mut lg = self.current_state.write().await; + + if lg.shard.shard_file_size() + file_info.num_bytes() >= self.target_shard_min_size { + self.flush_internal(&mut lg).await?; + } + + lg.shard.add_file_reconstruction_info(file_info)?; + + Ok(()) + } + + async fn flush_internal<'a>( + &self, + mem_shard: &mut tokio::sync::RwLockWriteGuard<'a, MDBShardFlushGuard>, + ) -> Result> { + Ok(if let Some(path) = mem_shard.flush()? { + self.register_shards_by_path(&[&path], false).await?; + Some(path) + } else { + None + }) + } + + /// Flush the current state of the in-memory lookups to a shard in the session directory, + /// returning the hash of the shard and the file written, or None if no file was written. + pub async fn flush(&self) -> Result> { + let mut lg = self.current_state.write().await; + self.flush_internal(&mut lg).await + } +} + +impl MDBShardFlushGuard { + // The flush logic is put here so that this can be done both manually and by drop + pub fn flush(&mut self) -> Result> { + if self.shard.is_empty() { + return Ok(None); + } + + if let Some(sd) = &self.session_directory { + let path = self.shard.write_to_directory(sd)?; + self.shard = MDBInMemoryShard::default(); + + info!("Shard manager flushed new shard to {path:?}."); + + Ok(Some(path)) + } else { + debug!("Shard manager in ephemeral mode; skipping flush to disk."); + Ok(None) + } + } +} + +impl ShardFileManager { + /// Calculate the total materialized bytes (before deduplication) tracked by the manager, + /// including in-memory state and on-disk shards. + pub async fn calculate_total_materialized_bytes(&self) -> Result { + let mut bytes = 0; + { + let lg = self.current_state.read().await; + bytes += lg.shard.materialized_bytes(); + } + + for (si, _) in self.shard_file_lookup.read().await.shard_list.iter() { + bytes += si.shard.materialized_bytes(); + } + + Ok(bytes) + } + + /// Calculate the total stored bytes tracked (after deduplication) tracked by the manager, + /// including in-memory state and on-disk shards. + pub async fn calculate_total_stored_bytes(&self) -> Result { + let mut bytes = 0; + { + let lg = self.current_state.read().await; + bytes += lg.shard.stored_bytes(); + } + + for (si, _) in self.shard_file_lookup.read().await.shard_list.iter() { + bytes += si.shard.stored_bytes(); + } + + Ok(bytes) + } +} + +#[cfg(test)] +mod tests { + + use crate::{ + cas_structs::{CASChunkSequenceEntry, CASChunkSequenceHeader}, + file_structs::FileDataSequenceHeader, + session_directory::consolidate_shards_in_directory, + shard_format::test_routines::{rng_hash, simple_hash}, + }; + + use super::*; + use crate::error::Result; + use more_asserts::assert_lt; + use rand::prelude::*; + use tempdir::TempDir; + + #[allow(clippy::type_complexity)] + pub async fn fill_with_specific_shard( + shard: &mut ShardFileManager, + in_mem_shard: &mut MDBInMemoryShard, + cas_nodes: &[(u64, &[(u64, u32)])], + file_nodes: &[(u64, &[(u64, (u32, u32))])], + ) -> Result<()> { + for (hash, chunks) in cas_nodes { + let mut cas_block = Vec::<_>::new(); + let mut pos = 0; + + for (h, s) in chunks.iter() { + cas_block.push(CASChunkSequenceEntry::new(simple_hash(*h), *s, pos)); + pos += *s; + } + let cas_info = MDBCASInfo { + metadata: CASChunkSequenceHeader::new(simple_hash(*hash), chunks.len(), pos), + chunks: cas_block, + }; + + shard.add_cas_block(cas_info.clone()).await?; + + in_mem_shard.add_cas_block(cas_info)?; + } + + for (file_hash, segments) in file_nodes { + let file_contents: Vec<_> = segments + .iter() + .map(|(h, (lb, ub))| { + FileDataSequenceEntry::new(simple_hash(*h), *ub - *lb, *lb, *ub) + }) + .collect(); + let file_info = MDBFileInfo { + metadata: FileDataSequenceHeader::new(simple_hash(*file_hash), segments.len()), + segments: file_contents, + }; + + shard + .add_file_reconstruction_info(file_info.clone()) + .await?; + + in_mem_shard.add_file_reconstruction_info(file_info)?; + } + + Ok(()) + } + + async fn fill_with_random_shard( + shard: &mut ShardFileManager, + in_mem_shard: &mut MDBInMemoryShard, + seed: u64, + cas_block_sizes: &[usize], + file_chunk_range_sizes: &[usize], + ) -> Result<()> { + // generate the cas content stuff. + let mut rng = StdRng::seed_from_u64(seed); + + for cas_block_size in cas_block_sizes { + let mut chunks = Vec::<_>::new(); + let mut pos = 0u32; + + for _ in 0..*cas_block_size { + chunks.push(CASChunkSequenceEntry::new( + rng_hash(rng.gen()), + rng.gen_range(10000..20000), + pos, + )); + pos += rng.gen_range(10000..20000); + } + let metadata = CASChunkSequenceHeader::new(rng_hash(rng.gen()), *cas_block_size, pos); + let mdb_cas_info = MDBCASInfo { metadata, chunks }; + + shard.add_cas_block(mdb_cas_info.clone()).await?; + in_mem_shard.add_cas_block(mdb_cas_info)?; + } + + for file_block_size in file_chunk_range_sizes { + let file_hash = rng_hash(rng.gen()); + + let segments: Vec<_> = (0..*file_block_size) + .map(|_| { + let lb = rng.gen_range(0..10000); + let ub = lb + rng.gen_range(0..10000); + FileDataSequenceEntry::new(rng_hash(rng.gen()), ub - lb, lb, ub) + }) + .collect(); + + let metadata = FileDataSequenceHeader::new(file_hash, *file_block_size); + + let file_info = MDBFileInfo { metadata, segments }; + + shard + .add_file_reconstruction_info(file_info.clone()) + .await?; + + in_mem_shard.add_file_reconstruction_info(file_info)?; + } + Ok(()) + } + + pub async fn verify_mdb_shards_match( + mdb: &ShardFileManager, + mem_shard: &MDBInMemoryShard, + ) -> Result<()> { + // Now, test that the results on queries from the + for (k, cas_block) in mem_shard.cas_content.iter() { + // Go through and test queries on both the in-memory shard and the + // serialized shard, making sure that they match completely. + + for i in 0..cas_block.chunks.len() { + // Test the dedup query over a few hashes in which all the + // hashes queried are part of the cas_block. + let query_hashes_1: Vec = cas_block.chunks + [i..(i + 3).min(cas_block.chunks.len())] + .iter() + .map(|c| c.chunk_hash) + .collect(); + let n_items_to_read = query_hashes_1.len(); + + // Also test the dedup query over a few hashes in which some of the + // hashes are part of the query, and the last is not. + let mut query_hashes_2 = query_hashes_1.clone(); + query_hashes_2.push(rng_hash(1000000 + i as u64)); + + let lb = cas_block.chunks[i].chunk_byte_range_start; + let ub = if i + 3 >= cas_block.chunks.len() { + cas_block.metadata.num_bytes_in_cas + } else { + cas_block.chunks[i + 3].chunk_byte_range_start + }; + + for query_hashes in [&query_hashes_1, &query_hashes_2] { + let result_m = mem_shard.chunk_hash_dedup_query(query_hashes).unwrap(); + + let result_f = mdb + .chunk_hash_dedup_query(query_hashes, None) + .await? + .unwrap(); + + // Returns a tuple of (num chunks matched, FileDataSequenceEntry) + assert_eq!(result_m.0, n_items_to_read); + assert_eq!(result_f.0, n_items_to_read); + + // Make sure it gives the correct CAS block hash as the second part of the + assert_eq!(result_m.1.cas_hash, *k); + assert_eq!(result_f.1.cas_hash, *k); + + // Make sure the bounds are correct + assert_eq!( + ( + result_m.1.chunk_byte_range_start, + result_m.1.chunk_byte_range_end + ), + (lb, ub) + ); + assert_eq!( + ( + result_f.1.chunk_byte_range_start, + result_f.1.chunk_byte_range_end + ), + (lb, ub) + ); + + // Make sure everything else equal. + assert_eq!(result_m, result_f); + } + } + } + + // Test get file reconstruction info. + // Against some valid hashes, + let mut query_hashes: Vec = + mem_shard.file_content.iter().map(|file| *file.0).collect(); + // and a few (very likely) invalid somes. + for i in 0..3 { + query_hashes.push(rng_hash(1000000 + i as u64)); + } + + for k in query_hashes.iter() { + let result_m = mem_shard.get_file_reconstruction_info(k); + let result_f = mdb.get_file_reconstruction_info(k).await?; + + // Make sure two queries return same results. + assert_eq!(result_m.is_some(), result_f.is_some()); + + // Make sure retriving the expected file. + if result_m.is_some() { + assert_eq!(result_m.unwrap().metadata.file_hash, *k); + assert_eq!(result_f.unwrap().0.metadata.file_hash, *k); + } + } + + // Make sure manager correctly tracking repo size. + assert_eq!( + mdb.calculate_total_materialized_bytes().await?, + mem_shard.materialized_bytes() + ); + assert_eq!( + mdb.calculate_total_stored_bytes().await?, + mem_shard.stored_bytes() + ); + + Ok(()) + } + #[tokio::test] + async fn test_basic_retrieval() -> Result<()> { + let tmp_dir = TempDir::new("gitxet_shard_test_1")?; + let mut mdb_in_mem = MDBInMemoryShard::default(); + + { + let mut mdb = ShardFileManager::new(tmp_dir.path()).await?; + + fill_with_specific_shard( + &mut mdb, + &mut mdb_in_mem, + &[(0, &[(11, 5)])], + &[(100, &[(200, (0, 5))])], + ) + .await?; + + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + + let out_file = mdb.flush().await?.unwrap(); + + // Make sure it still stays consistent after a flush + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + + // Verify that the file is correct + MDBShardFile::load_from_file(&out_file)?.verify_shard_integrity(); + } + { + // Now, make sure that this happens if this directory is opened up + let mut mdb2 = ShardFileManager::new(tmp_dir.path()).await?; + + // Make sure it's all in there this round. + verify_mdb_shards_match(&mdb2, &mdb_in_mem).await?; + + // Now add some more, based on this directory + fill_with_random_shard( + &mut mdb2, + &mut mdb_in_mem, + 0, + &[1, 5, 10, 8], + &[4, 3, 5, 9, 4, 6], + ) + .await?; + + verify_mdb_shards_match(&mdb2, &mdb_in_mem).await?; + + // Now, merge shards in the background. + let merged_shards = + consolidate_shards_in_directory(tmp_dir.path(), MDB_SHARD_MIN_TARGET_SIZE)?; + + assert_eq!(merged_shards.len(), 1); + for si in merged_shards { + assert!(si.path.exists()); + assert!(si.path.to_str().unwrap().contains(&si.shard_hash.hex())) + } + + verify_mdb_shards_match(&mdb2, &mdb_in_mem).await?; + } + + Ok(()) + } + + #[tokio::test] + async fn test_larger_simulated() -> Result<()> { + let tmp_dir = TempDir::new("gitxet_shard_test_2")?; + let mut mdb_in_mem = MDBInMemoryShard::default(); + let mut mdb = ShardFileManager::new(tmp_dir.path()).await?; + + for i in 0..10 { + fill_with_random_shard( + &mut mdb, + &mut mdb_in_mem, + i, + &[1, 5, 10, 8], + &[4, 3, 5, 9, 4, 6], + ) + .await?; + + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + + let out_file = mdb.flush().await?.unwrap(); + + // Make sure it still stays consistent + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + + // Verify that the file is correct + MDBShardFile::load_from_file(&out_file)?.verify_shard_integrity(); + + // Make sure an empty flush doesn't bother anything. + mdb.flush().await?; + + // Now, make sure that this happens if this directory is opened up + let mdb2 = ShardFileManager::new(tmp_dir.path()).await?; + + // Make sure it's all in there this round. + verify_mdb_shards_match(&mdb2, &mdb_in_mem).await?; + } + Ok(()) + } + + #[tokio::test] + async fn test_process_session_management() -> Result<()> { + let tmp_dir = TempDir::new("gitxet_shard_test_3").unwrap(); + let mut mdb_in_mem = MDBInMemoryShard::default(); + + for sesh in 0..3 { + for i in 0..10 { + { + let mut mdb = ShardFileManager::new(tmp_dir.path()).await.unwrap(); + fill_with_random_shard( + &mut mdb, + &mut mdb_in_mem, + 100 * sesh + i, + &[1, 5, 10, 8], + &[4, 3, 5, 9, 4, 6], + ) + .await + .unwrap(); + + verify_mdb_shards_match(&mdb, &mdb_in_mem).await.unwrap(); + + let out_file = mdb.flush().await.unwrap().unwrap(); + + // Make sure it still stays together + verify_mdb_shards_match(&mdb, &mdb_in_mem).await.unwrap(); + + // Verify that the file is correct + MDBShardFile::load_from_file(&out_file)?.verify_shard_integrity(); + + mdb.flush().await.unwrap(); + + verify_mdb_shards_match(&mdb, &mdb_in_mem).await.unwrap(); + } + } + + { + let merged_shards = + consolidate_shards_in_directory(tmp_dir.path(), MDB_SHARD_MIN_TARGET_SIZE) + .unwrap(); + + assert_eq!(merged_shards.len(), 1); + + for si in merged_shards { + assert!(si.path.exists()); + assert!(si.path.to_str().unwrap().contains(&si.shard_hash.hex())) + } + } + + { + // Now, make sure that this happens if this directory is opened up + let mdb2 = ShardFileManager::new(tmp_dir.path()).await.unwrap(); + + verify_mdb_shards_match(&mdb2, &mdb_in_mem).await.unwrap(); + } + } + Ok(()) + } + + #[tokio::test] + async fn test_flush_and_consolidation() -> Result<()> { + let tmp_dir = TempDir::new("gitxet_shard_test_4b")?; + let mut mdb_in_mem = MDBInMemoryShard::default(); + + const T: u64 = 10000; + + { + let mut mdb = ShardFileManager::new(tmp_dir.path()).await?; + mdb.set_target_shard_min_size(T); // Set the targe shard size really low + fill_with_random_shard(&mut mdb, &mut mdb_in_mem, 0, &[16; 16], &[16; 16]).await?; + } + { + let mut mdb = ShardFileManager::new(tmp_dir.path()).await?; + + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + + mdb.set_target_shard_min_size(2 * T); + fill_with_random_shard(&mut mdb, &mut mdb_in_mem, 1, &[25; 25], &[25; 25]).await?; + + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + } + + // Reload and verify + { + let mdb = ShardFileManager::new(tmp_dir.path()).await?; + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + } + + // Merge through the session directory. + { + let rv = consolidate_shards_in_directory(tmp_dir.path(), 8 * T)?; + + let paths = std::fs::read_dir(tmp_dir.path()).unwrap(); + assert_eq!(paths.count(), rv.len()); + + for sfi in rv { + sfi.verify_shard_integrity(); + } + } + + // Reload and verify + { + let mdb = ShardFileManager::new(tmp_dir.path()).await?; + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + } + + Ok(()) + } + + #[tokio::test] + async fn test_size_threshholds() -> Result<()> { + let tmp_dir = TempDir::new("gitxet_shard_test_4")?; + let mut mdb_in_mem = MDBInMemoryShard::default(); + + const T: u64 = 4096; + + for i in 0..5 { + let mut mdb = ShardFileManager::new(tmp_dir.path()).await?; + mdb.set_target_shard_min_size(T); // Set the targe shard size really low + fill_with_random_shard(&mut mdb, &mut mdb_in_mem, i, &[5; 25], &[5; 25]).await?; + + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + + let out_file = mdb.flush().await?.unwrap(); + + // Verify that the file is correct + MDBShardFile::load_from_file(&out_file)?.verify_shard_integrity(); + + // Make sure it still stays together + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + + assert!(mdb.flush().await?.is_none()); + } + + // Now, do a new shard that has less + let mut last_num_files = None; + let mut target_size = T; + loop { + // Now, make sure that this happens if this directory is opened up + let mut mdb2 = ShardFileManager::new(tmp_dir.path()).await?; + mdb2.set_target_shard_min_size(target_size); + + // Make sure it's all in there this round. + verify_mdb_shards_match(&mdb2, &mdb_in_mem).await?; + + let merged_shards = consolidate_shards_in_directory(tmp_dir.path(), target_size)?; + + for si in merged_shards.iter() { + assert!(si.path.exists()); + assert!(si.path.to_str().unwrap().contains(&si.shard_hash.hex())) + } + + let n_merged_shards = merged_shards.len(); + + if n_merged_shards == 1 { + break; + } + + if let Some(n) = last_num_files { + assert_lt!(n_merged_shards, n); + } + + last_num_files = Some(n_merged_shards); + + // So the shards will all be consolidated in the next round. + target_size *= 2; + } + Ok(()) + } + + #[tokio::test] + async fn test_teardown() -> Result<()> { + let tmp_dir = TempDir::new("gitxet_shard_test_1")?; + let mut mdb_in_mem = MDBInMemoryShard::default(); + + { + let mut mdb = ShardFileManager::new(tmp_dir.path()).await?; + + fill_with_specific_shard( + &mut mdb, + &mut mdb_in_mem, + &[(0, &[(11, 5)])], + &[(100, &[(200, (0, 5))])], + ) + .await?; + + verify_mdb_shards_match(&mdb, &mdb_in_mem).await?; + // Note, no flush + } + + { + // Now, make sure that this happens if this directory is opened up + let mdb2 = ShardFileManager::new(tmp_dir.path()).await?; + + // Make sure it's all in there this round. + verify_mdb_shards_match(&mdb2, &mdb_in_mem).await?; + } + + Ok(()) + } +} diff --git a/mdb_shard/src/shard_file_reconstructor.rs b/mdb_shard/src/shard_file_reconstructor.rs new file mode 100644 index 00000000..cc4328a3 --- /dev/null +++ b/mdb_shard/src/shard_file_reconstructor.rs @@ -0,0 +1,15 @@ +use async_trait::async_trait; +use merklehash::MerkleHash; + +use crate::{error::MDBShardError, file_structs::MDBFileInfo}; + +#[async_trait] +pub trait FileReconstructor { + /// Returns a pair of (file reconstruction information, maybe shard ID) + /// Err(_) if an error occured + /// Ok(None) if the file is not found. + async fn get_file_reconstruction_info( + &self, + file_hash: &MerkleHash, + ) -> Result)>, MDBShardError>; +} diff --git a/mdb_shard/src/shard_format.rs b/mdb_shard/src/shard_format.rs new file mode 100644 index 00000000..4da188b9 --- /dev/null +++ b/mdb_shard/src/shard_format.rs @@ -0,0 +1,1212 @@ +use crate::constants::*; +use crate::error::{MDBShardError, Result}; +use crate::serialization_utils::*; +use merklehash::MerkleHash; + +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::mem::size_of; +use std::sync::Arc; +use tracing::{debug, error}; + +use crate::cas_structs::*; +use crate::file_structs::*; +use crate::shard_in_memory::MDBInMemoryShard; +use crate::shard_version; +use crate::utils::truncate_hash; + +// Same size for FileDataSequenceHeader and FileDataSequenceEntry +const MDB_FILE_INFO_ENTRY_SIZE: u64 = (size_of::<[u64; 4]>() + 4 * size_of::()) as u64; +// Same size for CASChunkSequenceHeader and CASChunkSequenceEntry +const MDB_CAS_INFO_ENTRY_SIZE: u64 = (size_of::<[u64; 4]>() + 4 * size_of::()) as u64; +const MDB_SHARD_FOOTER_SIZE: i64 = size_of::() as i64; + +// At the start of each shard file, insert "MerkleDB Shard" plus a magic-number sequence of random bytes to ensure +// that we are able to quickly identify a CAS file as a shard file. +const MDB_SHARD_HEADER_TAG: [u8; 32] = [ + b'M', b'e', b'r', b'k', b'l', b'e', b'D', b'B', b' ', b'S', b'h', b'a', b'r', b'd', 0, 85, 105, + 103, 69, 106, 123, 129, 87, 131, 165, 189, 217, 92, 205, 209, 74, 169, +]; + +#[derive(Clone, Debug)] +pub struct MDBShardFileHeader { + // Header to be determined? "XetHub MDB Shard File Version 1" + pub tag: [u8; 32], + pub version: u64, + pub footer_size: u64, +} + +impl Default for MDBShardFileHeader { + fn default() -> Self { + Self { + tag: MDB_SHARD_HEADER_TAG, + version: shard_version::MDB_SHARD_HEADER_VERSION, + footer_size: MDB_SHARD_FOOTER_SIZE as u64, + } + } +} + +impl MDBShardFileHeader { + pub fn serialize(&self, writer: &mut W) -> Result { + writer.write_all(&MDB_SHARD_HEADER_TAG)?; + write_u64(writer, self.version)?; + write_u64(writer, self.footer_size)?; + + Ok(size_of::()) + } + + pub fn deserialize(reader: &mut R) -> Result { + let mut tag = [0u8; 32]; + reader.read_exact(&mut tag)?; + + if tag != MDB_SHARD_HEADER_TAG { + return Err(MDBShardError::ShardVersionError( + "File does not appear to be a valid Merkle DB Shard file (Wrong Magic Number)." + .to_owned(), + )); + } + + Ok(Self { + tag, + version: read_u64(reader)?, + footer_size: read_u64(reader)?, + }) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct MDBShardFileFooter { + pub version: u64, + pub file_info_offset: u64, + pub file_lookup_offset: u64, + pub file_lookup_num_entry: u64, + pub cas_info_offset: u64, + pub cas_lookup_offset: u64, + pub cas_lookup_num_entry: u64, + pub chunk_lookup_offset: u64, + pub chunk_lookup_num_entry: u64, + + // More locations to stick in here if needed. + _buffer: [u64; 7], + pub stored_bytes_on_disk: u64, + pub materialized_bytes: u64, + pub stored_bytes: u64, + pub footer_offset: u64, // Always last. +} + +impl Default for MDBShardFileFooter { + fn default() -> Self { + Self { + version: shard_version::MDB_SHARD_FOOTER_VERSION, + file_info_offset: 0, + file_lookup_offset: 0, + file_lookup_num_entry: 0, + cas_info_offset: 0, + cas_lookup_offset: 0, + cas_lookup_num_entry: 0, + chunk_lookup_offset: 0, + chunk_lookup_num_entry: 0, + _buffer: [0u64; 7], + stored_bytes_on_disk: 0, + materialized_bytes: 0, + stored_bytes: 0, + footer_offset: 0, + } + } +} + +impl MDBShardFileFooter { + pub fn serialize(&self, writer: &mut W) -> Result { + write_u64(writer, self.version)?; + write_u64(writer, self.file_info_offset)?; + write_u64(writer, self.file_lookup_offset)?; + write_u64(writer, self.file_lookup_num_entry)?; + write_u64(writer, self.cas_info_offset)?; + write_u64(writer, self.cas_lookup_offset)?; + write_u64(writer, self.cas_lookup_num_entry)?; + write_u64(writer, self.chunk_lookup_offset)?; + write_u64(writer, self.chunk_lookup_num_entry)?; + write_u64s(writer, &self._buffer)?; + write_u64(writer, self.stored_bytes_on_disk)?; + write_u64(writer, self.materialized_bytes)?; + write_u64(writer, self.stored_bytes)?; + write_u64(writer, self.footer_offset)?; + + Ok(size_of::()) + } + + pub fn deserialize(reader: &mut R) -> Result { + let mut obj = Self { + version: read_u64(reader)?, + file_info_offset: read_u64(reader)?, + file_lookup_offset: read_u64(reader)?, + file_lookup_num_entry: read_u64(reader)?, + cas_info_offset: read_u64(reader)?, + cas_lookup_offset: read_u64(reader)?, + cas_lookup_num_entry: read_u64(reader)?, + chunk_lookup_offset: read_u64(reader)?, + chunk_lookup_num_entry: read_u64(reader)?, + ..Default::default() + }; + read_u64s(reader, &mut obj._buffer)?; + obj.stored_bytes_on_disk = read_u64(reader)?; + obj.materialized_bytes = read_u64(reader)?; + obj.stored_bytes = read_u64(reader)?; + obj.footer_offset = read_u64(reader)?; + + Ok(obj) + } +} + +/// File info. This is a list of the file hash content to be downloaded. +/// +/// Each file consists of a FileDataSequenceHeader following +/// a sequence of FileDataSequenceEntry. +/// +/// [ +/// FileDataSequenceHeader, // u32 index in File lookup directs here. +/// [ +/// FileDataSequenceEntry, +/// ], +/// ], // Repeats per file. +/// +/// ---------------------------------------------------------------------------- +/// +/// File info lookup. This is a lookup of a truncated file hash to the +/// location index in the File info section. +/// +/// Sorted Vec<(u64, u32)> on the u64. +/// +/// The first entry is the u64 truncated file hash, and the next entry is the +/// index in the file info section of the element that starts the file reconstruction section. +/// +/// ---------------------------------------------------------------------------- +/// +/// CAS info. This is a list of chunks in order of appearance in the CAS chunks. +/// +/// Each CAS consists of a CASChunkSequenceHeader following +/// a sequence of CASChunkSequenceEntry. +/// +/// [ +/// CASChunkSequenceHeader, // u32 index in CAS lookup directs here. +/// [ +/// CASChunkSequenceEntry, // (u32, u32) index in Chunk lookup directs here. +/// ], +/// ], // Repeats per CAS. +/// +/// ---------------------------------------------------------------------------- +/// +/// CAS info lookup. This is a lookup of a truncated CAS hash to the +/// location index in the CAS info section. +/// +/// Sorted Vec<(u64, u32)> on the u64. +/// +/// The first entry is the u64 truncated CAS block hash, and the next entry is the +/// index in the cas info section of the element that starts the cas entry section. +/// +/// ---------------------------------------------------------------------------- +/// +/// Chunk info lookup. This is a lookup of a truncated CAS chunk hash to the +/// location in the CAS info section. +/// +/// Sorted Vec<(u64, (u32, u32))> on the u64. +/// +/// The first entry is the u64 truncated CAS chunk in a CAS block, the first u32 is the index +/// in the CAS info section that is the start of the CAS block, and the subsequent u32 gives +/// the offset index of the chunk in that CAS block. + +#[derive(Clone, Default, Debug)] +pub struct MDBShardInfo { + pub header: MDBShardFileHeader, + pub metadata: MDBShardFileFooter, +} + +impl MDBShardInfo { + pub fn load_from_file(reader: &mut R) -> Result { + let mut obj = Self::default(); + + // Move cursor to beginning of shard file. + reader.rewind()?; + obj.header = MDBShardFileHeader::deserialize(reader)?; + + // Move cursor to end of shard file minus footer size. + reader.seek(SeekFrom::End(-MDB_SHARD_FOOTER_SIZE))?; + obj.metadata = MDBShardFileFooter::deserialize(reader)?; + + Ok(obj) + } + + pub fn serialize_from(writer: &mut W, mdb: &MDBInMemoryShard) -> Result { + let mut shard = MDBShardInfo::default(); + + let mut bytes_pos: usize = 0; + + // Write shard header. + bytes_pos += shard.header.serialize(writer)?; + + // Write file info. + shard.metadata.file_info_offset = bytes_pos as u64; + let ((file_lookup_keys, file_lookup_vals), bytes_written) = + Self::convert_and_save_file_info(writer, &mdb.file_content)?; + bytes_pos += bytes_written; + + // Write file info lookup table. + shard.metadata.file_lookup_offset = bytes_pos as u64; + shard.metadata.file_lookup_num_entry = file_lookup_keys.len() as u64; + for (&e1, &e2) in file_lookup_keys.iter().zip(file_lookup_vals.iter()) { + write_u64(writer, e1)?; + write_u32(writer, e2)?; + } + bytes_pos += + size_of::() * file_lookup_keys.len() + size_of::() * file_lookup_vals.len(); + + // Release memory. + drop(file_lookup_keys); + drop(file_lookup_vals); + + // Write CAS info. + shard.metadata.cas_info_offset = bytes_pos as u64; + let ( + (cas_lookup_keys, cas_lookup_vals), + (chunk_lookup_keys, chunk_lookup_vals), + bytes_written, + ) = Self::convert_and_save_cas_info(writer, &mdb.cas_content)?; + bytes_pos += bytes_written; + + // Write cas info lookup table. + shard.metadata.cas_lookup_offset = bytes_pos as u64; + shard.metadata.cas_lookup_num_entry = cas_lookup_keys.len() as u64; + for (&e1, &e2) in cas_lookup_keys.iter().zip(cas_lookup_vals.iter()) { + write_u64(writer, e1)?; + write_u32(writer, e2)?; + } + bytes_pos += + size_of::() * cas_lookup_keys.len() + size_of::() * cas_lookup_vals.len(); + + // Write chunk lookup table. + shard.metadata.chunk_lookup_offset = bytes_pos as u64; + shard.metadata.chunk_lookup_num_entry = chunk_lookup_keys.len() as u64; + for (&e1, &e2) in chunk_lookup_keys.iter().zip(chunk_lookup_vals.iter()) { + write_u64(writer, e1)?; + write_u32(writer, e2.0)?; + write_u32(writer, e2.1)?; + } + bytes_pos += + size_of::() * chunk_lookup_keys.len() + size_of::() * chunk_lookup_vals.len(); + + // Update repo size information. + shard.metadata.stored_bytes_on_disk = mdb.stored_bytes_on_disk(); + shard.metadata.materialized_bytes = mdb.materialized_bytes(); + shard.metadata.stored_bytes = mdb.stored_bytes(); + + // Update footer offset. + shard.metadata.footer_offset = bytes_pos as u64; + + // Write shard footer. + shard.metadata.serialize(writer)?; + + Ok(shard) + } + + #[allow(clippy::type_complexity)] + fn convert_and_save_file_info( + writer: &mut W, + file_content: &BTreeMap, + ) -> Result<( + (Vec, Vec), // File Lookup Info + usize, // Bytes used for File Content Info + )> { + // File info lookup table. + let mut file_lookup_keys = Vec::::with_capacity(file_content.len()); + let mut file_lookup_vals = Vec::::with_capacity(file_content.len()); + + let mut index: u32 = 0; + let mut bytes_written = 0; + + for (file_hash, content) in file_content { + file_lookup_keys.push(truncate_hash(file_hash)); + file_lookup_vals.push(index); + + bytes_written += content.metadata.serialize(writer)?; + + for file_segment in content.segments.iter() { + bytes_written += file_segment.serialize(writer)?; + } + + index += 1 + content.metadata.num_entries; + } + + // Serialize a single block of 00 bytes as a guard for sequential reading. + bytes_written += FileDataSequenceHeader::default().serialize(writer)?; + + // No need to sort because BTreeMap is ordered and we truncate by the first 8 bytes. + Ok(((file_lookup_keys, file_lookup_vals), bytes_written)) + } + + #[allow(clippy::type_complexity)] + fn convert_and_save_cas_info( + writer: &mut W, + cas_content: &BTreeMap>, + ) -> Result<( + (Vec, Vec), // CAS Lookup Info + (Vec, Vec<(u32, u32)>), // Chunk Lookup Info + usize, // Bytes used for CAS Content Info + )> { + // CAS info lookup table. + let mut cas_lookup_keys = Vec::::with_capacity(cas_content.len()); + let mut cas_lookup_vals = Vec::::with_capacity(cas_content.len()); + + // Chunk lookup table. + let mut chunk_lookup_keys = Vec::::with_capacity(cas_content.len()); // may grow + let mut chunk_lookup_vals = Vec::<(u32, u32)>::with_capacity(cas_content.len()); // may grow + + let mut index: u32 = 0; + let mut bytes_written = 0; + + for (cas_hash, content) in cas_content { + cas_lookup_keys.push(truncate_hash(cas_hash)); + cas_lookup_vals.push(index); + + bytes_written += content.metadata.serialize(writer)?; + + for (i, chunk) in content.chunks.iter().enumerate() { + bytes_written += chunk.serialize(writer)?; + + chunk_lookup_keys.push(truncate_hash(&chunk.chunk_hash)); + chunk_lookup_vals.push((index, i as u32)); + } + + index += 1 + content.chunks.len() as u32; + } + + // Serialize a single block of 00 bytes as a guard for sequential reading. + bytes_written += CASChunkSequenceHeader::default().serialize(writer)?; + + // No need to sort cas_lookup_ because BTreeMap is ordered and we truncate by the first 8 bytes. + + // Sort chunk lookup table by key. + let mut chunk_lookup_combined = chunk_lookup_keys + .iter() + .zip(chunk_lookup_vals.iter()) + .collect::>(); + + chunk_lookup_combined.sort_unstable_by_key(|&(k, _)| k); + + Ok(( + (cas_lookup_keys, cas_lookup_vals), + ( + chunk_lookup_combined.iter().map(|&(k, _)| *k).collect(), + chunk_lookup_combined.iter().map(|&(_, v)| *v).collect(), + ), + bytes_written, + )) + } + + pub fn get_file_info_index_by_hash( + &self, + reader: &mut R, + file_hash: &MerkleHash, + dest_indices: &mut [u32; 8], + ) -> Result { + let num_indices = search_on_sorted_u64s( + reader, + self.metadata.file_lookup_offset, + self.metadata.file_lookup_num_entry, + truncate_hash(file_hash), + read_u32::, + dest_indices, + )?; + + // Assume no more than 8 collisions. + if num_indices < dest_indices.len() { + Ok(num_indices) + } else { + Err(MDBShardError::TruncatedHashCollisionError(truncate_hash( + file_hash, + ))) + } + } + + pub fn get_cas_info_index_by_hash( + &self, + reader: &mut R, + cas_hash: &MerkleHash, + dest_indices: &mut [u32; 8], + ) -> Result { + let num_indices = search_on_sorted_u64s( + reader, + self.metadata.cas_lookup_offset, + self.metadata.cas_lookup_num_entry, + truncate_hash(cas_hash), + read_u32::, + dest_indices, + )?; + + // Assume no more than 8 collisions. + if num_indices < dest_indices.len() { + Ok(num_indices) + } else { + Err(MDBShardError::TruncatedHashCollisionError(truncate_hash( + cas_hash, + ))) + } + } + + pub fn get_cas_info_index_by_chunk( + &self, + reader: &mut R, + chunk_hash: &MerkleHash, + dest_indices: &mut [(u32, u32); 8], + ) -> Result { + let num_indices = search_on_sorted_u64s( + reader, + self.metadata.chunk_lookup_offset, + self.metadata.chunk_lookup_num_entry, + truncate_hash(chunk_hash), + |reader| (Ok((read_u32(reader)?, read_u32(reader)?))), + dest_indices, + )?; + + // Chunk lookup hashes are Ok to have (many) collisions, + // we will use a subset of collisions to do dedup. + if num_indices == dest_indices.len() { + debug!( + "Found {:?} or more collisions when searching for truncated hash {:?}", + dest_indices.len(), + truncate_hash(chunk_hash) + ); + } + + Ok(num_indices) + } + + /// Reads the file info from a specific index. Note that this is the position + pub fn read_file_info( + &self, + reader: &mut R, + file_entry_index: u32, + ) -> Result { + reader.seek(SeekFrom::Start( + self.metadata.file_info_offset + MDB_FILE_INFO_ENTRY_SIZE * (file_entry_index as u64), + ))?; + + let file_header = FileDataSequenceHeader::deserialize(reader)?; + + let num_entries = file_header.num_entries; + + let mut mdb_file = MDBFileInfo { + metadata: file_header, + ..Default::default() + }; + + for _ in 0..num_entries { + mdb_file + .segments + .push(FileDataSequenceEntry::deserialize(reader)?); + } + + Ok(mdb_file) + } + + pub fn read_all_file_info_sections( + &self, + reader: &mut R, + ) -> Result> { + let mut ret = Vec::::with_capacity(self.num_file_entries()); + + reader.seek(SeekFrom::Start(self.metadata.file_info_offset))?; + + for _ in 0..self.num_file_entries() { + let file_header = FileDataSequenceHeader::deserialize(reader)?; + + let num_entries = file_header.num_entries; + + let mut mdb_file = MDBFileInfo { + metadata: file_header, + ..Default::default() + }; + + for _ in 0..num_entries { + mdb_file + .segments + .push(FileDataSequenceEntry::deserialize(reader)?); + } + + ret.push(mdb_file); + } + + Ok(ret) + } + + pub fn read_all_cas_blocks( + &self, + reader: &mut R, + ) -> Result> { + // Reads all the cas blocks, returning a list of the cas info and the + // starting position of that cas block. + + let mut cas_blocks = Vec::<(CASChunkSequenceHeader, u64)>::with_capacity( + self.metadata.cas_lookup_num_entry as usize, + ); + + reader.seek(SeekFrom::Start(self.metadata.cas_info_offset))?; + + for _ in 0..self.metadata.cas_lookup_num_entry { + let pos = reader.stream_position()?; + let cas_block = CASChunkSequenceHeader::deserialize(reader)?; + let n = cas_block.num_entries; + cas_blocks.push((cas_block, pos)); + + reader.seek(SeekFrom::Current( + (size_of::() as i64) * (n as i64), + ))?; + } + Ok(cas_blocks) + } + + /// Returns a vector holding all the chunk hashes along with their (cas idx, entry idx) locations + pub fn read_all_cas_blocks_full( + &self, + reader: &mut R, + ) -> Result> { + let mut ret = Vec::with_capacity(self.num_cas_entries()); + + reader.seek(SeekFrom::Start(self.metadata.cas_info_offset))?; + + for _ in 0..(self.num_cas_entries() as u32) { + ret.push(MDBCASInfo::deserialize(reader)?); + } + + Ok(ret) + } + + pub fn read_full_cas_lookup(&self, reader: &mut R) -> Result> { + // Reads all the cas blocks, returning a list of the cas info and the + // starting position of that cas block. + + let mut cas_lookup: Vec<(u64, u32)> = + Vec::with_capacity(self.metadata.cas_lookup_num_entry as usize); + + reader.seek(SeekFrom::Start(self.metadata.cas_lookup_offset))?; + + for _ in 0..self.metadata.cas_lookup_num_entry { + let trunc_cas_hash: u64 = read_u64(reader)?; + let idx: u32 = read_u32(reader)?; + cas_lookup.push((trunc_cas_hash, idx)); + } + + Ok(cas_lookup) + } + + // Given a file pointer, returns the information needed to reconstruct the file. + // The information is stored in the destination vector dest_results. The function + // returns true if the file hash was found, and false otherwise. + pub fn get_file_reconstruction_info( + &self, + reader: &mut R, + file_hash: &MerkleHash, + ) -> Result> { + // Search in file info lookup table. + let mut dest_indices = [0u32; 8]; + let num_indices = self.get_file_info_index_by_hash(reader, file_hash, &mut dest_indices)?; + + // Check each file info if the file hash matches. + for &file_entry_index in dest_indices.iter().take(num_indices) { + let mdb_file = self.read_file_info(reader, file_entry_index)?; + if mdb_file.metadata.file_hash == *file_hash { + return Ok(Some(mdb_file)); + } + } + + Ok(None) + } + + // Performs a query of block hashes against a known block hash, matching + // as many of the values in query_hashes as possible. It returns the number + // of entries matched from the input hashes, the CAS block hash of the match, + // and the range matched from that block. + // In this case, a location hint is given to the function. It will only return a + // match from that point + pub fn chunk_hash_dedup_query_direct( + &self, + reader: &mut R, + query_hashes: &[MerkleHash], + cas_entry_index: u32, + cas_chunk_offset: u32, + ) -> Result> { + if query_hashes.is_empty() { + return Ok(None); + } + + reader.seek(SeekFrom::Start( + self.metadata.cas_info_offset + MDB_CAS_INFO_ENTRY_SIZE * (cas_entry_index as u64), + ))?; + + let cas_header = CASChunkSequenceHeader::deserialize(reader)?; + + if cas_chunk_offset != 0 { + // Jump forward to the chunk at chunk_offset. + reader.seek(SeekFrom::Current( + MDB_CAS_INFO_ENTRY_SIZE as i64 * cas_chunk_offset as i64, + ))?; + } + + // Now, read in data while the query hashes match. + let first_chunk = CASChunkSequenceEntry::deserialize(reader)?; + if first_chunk.chunk_hash != query_hashes[0] { + return Ok(None); + } + + let mut n_bytes = first_chunk.unpacked_segment_bytes; + let chunk_byte_range_start = first_chunk.chunk_byte_range_start; + + // Read everything else until the CAS block end. + let mut end_idx = 0; + let mut chunk_byte_range_end = chunk_byte_range_start; + for i in 1.. { + if cas_chunk_offset as usize + i == cas_header.num_entries as usize { + end_idx = i; + chunk_byte_range_end = cas_header.num_bytes_in_cas; + break; + } + + let chunk = CASChunkSequenceEntry::deserialize(reader)?; + + if i == query_hashes.len() || chunk.chunk_hash != query_hashes[i] { + end_idx = i; + chunk_byte_range_end = chunk.chunk_byte_range_start; + break; + } + + n_bytes += chunk.unpacked_segment_bytes; + } + + Ok(Some(( + end_idx, + FileDataSequenceEntry { + cas_hash: cas_header.cas_hash, + cas_flags: cas_header.cas_flags, + unpacked_segment_bytes: n_bytes, + chunk_byte_range_start, + chunk_byte_range_end, + }, + ))) + } + + // Performs a query of block hashes against a known block hash, matching + // as many of the values in query_hashes as possible. It returns the number + // of entries matched from the input hashes, the CAS block hash of the match, + // and the range matched from that block. + pub fn chunk_hash_dedup_query( + &self, + reader: &mut R, + query_hashes: &[MerkleHash], + ) -> Result> { + if query_hashes.is_empty() { + return Ok(None); + } + + // Lookup CAS block from chunk lookup. + let mut dest_indices = [(0u32, 0u32); 8]; + let num_indices = + self.get_cas_info_index_by_chunk(reader, &query_hashes[0], &mut dest_indices)?; + + // Sequentially match chunks in that block. + for &(cas_index, chunk_offset) in dest_indices.iter().take(num_indices) { + if let Some(cas) = + self.chunk_hash_dedup_query_direct(reader, query_hashes, cas_index, chunk_offset)? + { + return Ok(Some(cas)); + } + } + Ok(None) + } + + pub fn read_all_truncated_hashes( + &self, + reader: &mut R, + ) -> Result> { + reader.seek(SeekFrom::Start(self.metadata.chunk_lookup_offset))?; + + let mut ret = Vec::with_capacity(self.metadata.chunk_lookup_num_entry as usize); + for _ in 0..self.metadata.chunk_lookup_num_entry { + ret.push((read_u64(reader)?, (read_u32(reader)?, read_u32(reader)?))); + } + + Ok(ret) + } + + pub fn num_cas_entries(&self) -> usize { + self.metadata.cas_lookup_num_entry as usize + } + + pub fn num_file_entries(&self) -> usize { + self.metadata.file_lookup_num_entry as usize + } + + pub fn total_num_chunks(&self) -> usize { + self.metadata.chunk_lookup_num_entry as usize + } + + /// Returns the number of bytes in the shard + pub fn num_bytes(&self) -> u64 { + self.metadata.footer_offset + size_of::() as u64 + } + + pub fn stored_bytes_on_disk(&self) -> u64 { + self.metadata.stored_bytes_on_disk + } + + pub fn materialized_bytes(&self) -> u64 { + self.metadata.materialized_bytes + } + + pub fn stored_bytes(&self) -> u64 { + self.metadata.stored_bytes + } + + /// returns the number of bytes that is fixed and not part of any content; i.e. would be part of an empty shard. + pub fn non_content_byte_size() -> u64 { + (size_of::() + size_of::()) as u64 // Header and footer + + size_of::() as u64 // Guard block for scanning. + + size_of::() as u64 // Guard block for scanning. + } + + pub fn print_report(&self) { + eprintln!( + "Byte size of file info: {}", + self.metadata.file_lookup_offset - self.metadata.file_info_offset + ); + eprintln!( + "Byte size of file lookup: {}", + self.metadata.cas_info_offset - self.metadata.file_lookup_offset + ); + eprintln!( + "Byte size of cas info: {}", + self.metadata.cas_lookup_offset - self.metadata.cas_info_offset + ); + eprintln!( + "Byte size of cas lookup: {}", + self.metadata.chunk_lookup_offset - self.metadata.cas_lookup_offset + ); + eprintln!( + "Byte size of chunk lookup: {}", + self.metadata.footer_offset - self.metadata.chunk_lookup_offset + ); + } + + pub fn read_file_info_ranges( + reader: &mut R, + ) -> Result> { + let mut ret = Vec::new(); + + let _shard_header = MDBShardFileHeader::deserialize(reader)?; + + loop { + let header = FileDataSequenceHeader::deserialize(reader)?; + + if header.file_hash == MerkleHash::default() { + break; + } + + let byte_start = reader.stream_position()?; + + reader.seek(SeekFrom::Current( + (header.num_entries as i64) * (size_of::() as i64), + ))?; + let byte_end = reader.stream_position()?; + + ret.push((header.file_hash, (byte_start, byte_end))); + } + + Ok(ret) + } + + /// Returns a list of chunk hashes for the global dedup service. + /// The chunk hashes are either multiple of 'hash_filter_modulues', + /// or the hash of the first chunk of a file present in the shard. + pub fn read_cas_chunks_for_global_dedup( + reader: &mut R, + ) -> Result> { + let mut ret = Vec::new(); + + // First, go through and get all of the cas chunks. This allows us to form the lookup for the CAS block + // hashes later. + let shard = MDBShardInfo::load_from_file(reader)?; + + let cas_chunks = shard.read_all_cas_blocks_full(reader)?; + let mut cas_block_lookup = HashMap::::with_capacity(cas_chunks.len()); + + for (i, cas_info) in cas_chunks.iter().enumerate() { + cas_block_lookup.insert(cas_info.metadata.cas_hash, i); + for chunk in cas_info.chunks.iter() { + if hash_is_global_dedup_eligible(&chunk.chunk_hash) { + ret.push(chunk.chunk_hash); + } + } + } + + // Now, go through all the files present, collecting the first chunks of the files. + // TODO: break this out into a utility if needed. + let files = shard.read_all_file_info_sections(reader)?; + + for fi in files { + let Some(entry) = fi.segments.first() else { + continue; + }; + + let Some(cas_block_index) = cas_block_lookup.get(&entry.cas_hash) else { + continue; + }; + + // Scan the cas entries to get the proper index + let Some(first_chunk_hash) = ('a: { + for e in cas_chunks[*cas_block_index].chunks.iter() { + if e.chunk_byte_range_start == entry.chunk_byte_range_start { + break 'a Some(e.chunk_hash); + } + } + error!("Error: Shard file start in CAS is not on chunk boundary."); + break 'a None; + }) else { + continue; + }; + + ret.push(first_chunk_hash); + } + + Ok(ret) + } +} + +pub mod test_routines { + use std::io::{Cursor, Read, Seek}; + use std::mem::size_of; + + use crate::cas_structs::{CASChunkSequenceEntry, CASChunkSequenceHeader, MDBCASInfo}; + use crate::error::Result; + use crate::file_structs::{FileDataSequenceEntry, FileDataSequenceHeader, MDBFileInfo}; + use crate::shard_format::MDBShardInfo; + use crate::shard_in_memory::MDBInMemoryShard; + use merklehash::MerkleHash; + use rand::rngs::{SmallRng, StdRng}; + use rand::{Rng, SeedableRng}; + + pub fn simple_hash(n: u64) -> MerkleHash { + MerkleHash::from([n, 1, 0, 0]) + } + pub fn rng_hash(seed: u64) -> MerkleHash { + let mut rng = SmallRng::seed_from_u64(seed); + MerkleHash::from([rng.gen(), rng.gen(), rng.gen(), rng.gen()]) + } + + pub fn convert_to_file(shard: &MDBInMemoryShard) -> Result> { + let mut buffer = Vec::::new(); + + MDBShardInfo::serialize_from(&mut buffer, shard)?; + + Ok(buffer) + } + + #[allow(clippy::type_complexity)] + pub fn gen_specific_shard( + cas_nodes: &[(u64, &[(u64, u32)])], + file_nodes: &[(u64, &[(u64, (u32, u32))])], + ) -> Result { + let mut shard = MDBInMemoryShard::default(); + + for (hash, chunks) in cas_nodes { + let mut cas_block = Vec::<_>::new(); + let mut pos = 0u32; + + for (h, s) in chunks.iter() { + cas_block.push(CASChunkSequenceEntry::new(simple_hash(*h), pos, *s)); + pos += s + } + + shard.add_cas_block(MDBCASInfo { + metadata: CASChunkSequenceHeader::new(simple_hash(*hash), chunks.len(), pos), + chunks: cas_block, + })?; + } + + for (file_hash, segments) in file_nodes { + let file_contents: Vec<_> = segments + .iter() + .map(|(h, (lb, ub))| { + FileDataSequenceEntry::new(simple_hash(*h), *ub - *lb, *lb, *ub) + }) + .collect(); + + shard.add_file_reconstruction_info(MDBFileInfo { + metadata: FileDataSequenceHeader::new(simple_hash(*file_hash), segments.len()), + segments: file_contents, + })?; + } + + Ok(shard) + } + + pub fn gen_random_shard( + seed: u64, + cas_block_sizes: &[usize], + file_chunk_range_sizes: &[usize], + ) -> Result { + // generate the cas content stuff. + let mut shard = MDBInMemoryShard::default(); + let mut rng = StdRng::seed_from_u64(seed); + + for cas_block_size in cas_block_sizes { + let mut cas_block = Vec::<_>::new(); + let mut pos = 0u32; + + for _ in 0..*cas_block_size { + cas_block.push(CASChunkSequenceEntry::new( + rng_hash(rng.gen()), + rng.gen_range(10000..20000), + pos, + )); + pos += rng.gen_range(10000..20000); + } + + shard.add_cas_block(MDBCASInfo { + metadata: CASChunkSequenceHeader::new(rng_hash(rng.gen()), *cas_block_size, pos), + chunks: cas_block, + })?; + } + + for file_block_size in file_chunk_range_sizes { + let file_hash = rng_hash(rng.gen()); + + let file_contents: Vec<_> = (0..*file_block_size) + .map(|_| { + let lb = rng.gen_range(0..10000); + let ub = lb + rng.gen_range(0..10000); + FileDataSequenceEntry::new(rng_hash(rng.gen()), ub - lb, lb, ub) + }) + .collect(); + + shard.add_file_reconstruction_info(MDBFileInfo { + metadata: FileDataSequenceHeader::new(file_hash, *file_block_size), + segments: file_contents, + })?; + } + + Ok(shard) + } + + pub fn verify_mdb_shard(shard: &MDBInMemoryShard) -> Result<()> { + let buffer = convert_to_file(shard)?; + + verify_mdb_shards_match(shard, Cursor::new(&buffer)) + } + + pub fn verify_mdb_shards_match( + mem_shard: &MDBInMemoryShard, + shard_info: R, + ) -> Result<()> { + let mut cursor = shard_info; + // Now, test that the results on queries from the + let shard_file = MDBShardInfo::load_from_file(&mut cursor)?; + + assert_eq!(mem_shard.shard_file_size(), shard_file.num_bytes()); + assert_eq!( + mem_shard.materialized_bytes(), + shard_file.materialized_bytes() + ); + assert_eq!(mem_shard.stored_bytes(), shard_file.stored_bytes()); + + for (k, cas_block) in mem_shard.cas_content.iter() { + // Go through and test queries on both the in-memory shard and the + // serialized shard, making sure that they match completely. + + for i in 0..cas_block.chunks.len() { + // Test the dedup query over a few hashes in which all the + // hashes queried are part of the cas_block. + let query_hashes_1: Vec = cas_block.chunks + [i..(i + 3).min(cas_block.chunks.len())] + .iter() + .map(|c| c.chunk_hash) + .collect(); + let n_items_to_read = query_hashes_1.len(); + + // Also test the dedup query over a few hashes in which some of the + // hashes are part of the query, and the last is not. + let mut query_hashes_2 = query_hashes_1.clone(); + query_hashes_2.push(rng_hash(1000000 + i as u64)); + + let lb = cas_block.chunks[i].chunk_byte_range_start; + let ub = if i + 3 >= cas_block.chunks.len() { + cas_block.metadata.num_bytes_in_cas + } else { + cas_block.chunks[i + 3].chunk_byte_range_start + }; + + for query_hashes in [&query_hashes_1, &query_hashes_2] { + let result_m = mem_shard.chunk_hash_dedup_query(query_hashes).unwrap(); + + let result_f = shard_file + .chunk_hash_dedup_query(&mut cursor, query_hashes)? + .unwrap(); + + // Returns a tuple of (num chunks matched, FileDataSequenceEntry) + assert_eq!(result_m.0, n_items_to_read); + assert_eq!(result_f.0, n_items_to_read); + + // Make sure it gives the correct CAS block hash as the second part of the + assert_eq!(result_m.1.cas_hash, *k); + assert_eq!(result_f.1.cas_hash, *k); + + // Make sure the bounds are correct + assert_eq!( + ( + result_m.1.chunk_byte_range_start, + result_m.1.chunk_byte_range_end + ), + (lb, ub) + ); + assert_eq!( + ( + result_f.1.chunk_byte_range_start, + result_f.1.chunk_byte_range_end + ), + (lb, ub) + ); + + // Make sure everything else equal. + assert_eq!(result_m, result_f); + } + } + } + + // Test get file reconstruction info. + // Against some valid hashes, + let mut query_hashes: Vec = + mem_shard.file_content.iter().map(|file| *file.0).collect(); + // and a few (very likely) invalid somes. + for i in 0..3 { + query_hashes.push(rng_hash(1000000 + i as u64)); + } + + for k in query_hashes.iter() { + let result_m = mem_shard.get_file_reconstruction_info(k); + let result_f = shard_file.get_file_reconstruction_info(&mut cursor, k)?; + + // Make sure two queries return same results. + assert_eq!(result_m, result_f); + + // Make sure retriving the expected file. + if result_m.is_some() { + assert_eq!(result_m.unwrap().metadata.file_hash, *k); + assert_eq!(result_f.unwrap().metadata.file_hash, *k); + } + } + + // Make sure the cas blocks and chunks are correct. + let cas_blocks_full = shard_file.read_all_cas_blocks_full(&mut cursor)?; + + // Make sure the cas blocks are correct + let cas_blocks = shard_file.read_all_cas_blocks(&mut cursor)?; + + for (i, (cas_block, pos)) in cas_blocks.into_iter().enumerate() { + let cas = mem_shard.cas_content.get(&cas_block.cas_hash).unwrap(); + + assert_eq!(cas_block.num_entries, cas.chunks.len() as u32); + + cursor.seek(std::io::SeekFrom::Start(pos))?; + + let read_cas = MDBCASInfo::deserialize(&mut cursor)?; + assert_eq!(read_cas.metadata, cas_block); + + assert_eq!(&read_cas, cas.as_ref()); + assert_eq!(&cas_blocks_full[i], cas.as_ref()); + } + + // Make sure the file info section is good + { + cursor.seek(std::io::SeekFrom::Start(0))?; + let file_info = MDBShardInfo::read_file_info_ranges(&mut cursor)?; + + assert_eq!(file_info.len(), mem_shard.file_content.len()); + + for (file_hash, (byte_start, byte_end)) in file_info { + cursor.seek(std::io::SeekFrom::Start(byte_start))?; + + let num_entries = + (byte_end - byte_start) / (size_of::() as u64); + + // No leftovers + assert_eq!( + num_entries * (size_of::() as u64), + (byte_end - byte_start) + ); + + let true_fie = mem_shard.file_content.get(&file_hash).unwrap(); + + assert_eq!(num_entries, true_fie.segments.len() as u64); + + for i in 0..num_entries { + let pos = byte_start + i * (size_of::() as u64); + + cursor.seek(std::io::SeekFrom::Start(pos))?; + + let fie = FileDataSequenceEntry::deserialize(&mut cursor)?; + + assert_eq!(true_fie.segments[i as usize], fie); + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::error::Result; + + use super::test_routines::*; + + #[test] + fn test_simple() -> Result<()> { + let shard = gen_random_shard(0, &[1, 1], &[1])?; + + verify_mdb_shard(&shard)?; + + Ok(()) + } + + #[test] + fn test_specific() -> Result<()> { + let mem_shard_1 = gen_specific_shard(&[(0, &[(11, 5)])], &[(100, &[(200, (0, 5))])])?; + verify_mdb_shard(&mem_shard_1) + } + + #[test] + fn test_multiple() -> Result<()> { + let shard = gen_random_shard(0, &[1, 5, 10, 8], &[4, 3, 5, 9, 4, 6])?; + + verify_mdb_shard(&shard)?; + + Ok(()) + } + + #[test] + fn test_corner_cases_empty() -> Result<()> { + let shard = gen_random_shard(0, &[0], &[0])?; + + verify_mdb_shard(&shard)?; + + Ok(()) + } + + #[test] + fn test_corner_cases_empty_entries() -> Result<()> { + let shard = gen_random_shard(0, &[5, 6, 0, 10, 0], &[3, 4, 5, 0, 4, 0])?; + + verify_mdb_shard(&shard)?; + + Ok(()) + } +} diff --git a/mdb_shard/src/shard_in_memory.rs b/mdb_shard/src/shard_in_memory.rs new file mode 100644 index 00000000..85a0e15b --- /dev/null +++ b/mdb_shard/src/shard_in_memory.rs @@ -0,0 +1,266 @@ +// The shard structure for the in memory querying + +use merklehash::{HashedWrite, MerkleHash}; +use std::{ + collections::{BTreeMap, HashMap}, + io::{BufWriter, Write}, + mem::size_of, + path::{Path, PathBuf}, + sync::Arc, +}; +use tracing::debug; + +use crate::{ + cas_structs::*, + error::Result, + file_structs::*, + shard_format::MDBShardInfo, + utils::{shard_file_name, temp_shard_file_name}, +}; + +#[allow(clippy::type_complexity)] +#[derive(Clone, Default, Debug)] +pub struct MDBInMemoryShard { + pub cas_content: BTreeMap>, + pub file_content: BTreeMap, + pub chunk_hash_lookup: HashMap, u64)>, + current_shard_file_size: u64, +} + +impl MDBInMemoryShard { + pub fn add_cas_block(&mut self, cas_block_contents: MDBCASInfo) -> Result<()> { + let dest_content_v = Arc::new(cas_block_contents); + self.cas_content + .insert(dest_content_v.metadata.cas_hash, dest_content_v.clone()); + + for (i, chunk) in dest_content_v.chunks.iter().enumerate() { + self.chunk_hash_lookup + .insert(chunk.chunk_hash, (dest_content_v.clone(), i as u64)); + self.current_shard_file_size += (size_of::() + 2 * size_of::()) as u64; + } + self.current_shard_file_size += dest_content_v.num_bytes(); + self.current_shard_file_size += (size_of::() + size_of::()) as u64; + + Ok(()) + } + + pub fn add_file_reconstruction_info(&mut self, file_info: MDBFileInfo) -> Result<()> { + self.current_shard_file_size += file_info.num_bytes(); + self.current_shard_file_size += (size_of::() + size_of::()) as u64; + + self.file_content + .insert(file_info.metadata.file_hash, file_info); + + Ok(()) + } + + pub fn union(&self, other: &Self) -> Result { + let mut cas_content = self.cas_content.clone(); + other.cas_content.iter().for_each(|(k, v)| { + cas_content.insert(*k, v.clone()); + }); + + let mut file_content = self.file_content.clone(); + other.file_content.iter().for_each(|(k, v)| { + file_content.insert(*k, v.clone()); + }); + + let mut chunk_hash_lookup = self.chunk_hash_lookup.clone(); + other.chunk_hash_lookup.iter().for_each(|(k, v)| { + chunk_hash_lookup.insert(*k, v.clone()); + }); + + let mut s = Self { + cas_content, + file_content, + current_shard_file_size: 0, + chunk_hash_lookup, + }; + + s.recalculate_shard_size(); + Ok(s) + } + + pub fn recalculate_shard_size(&mut self) { + // Calculate the size + let mut num_bytes = 0u64; + for (_, cas_block_contents) in self.cas_content.iter() { + num_bytes += cas_block_contents.num_bytes(); + + // The cas lookup table + num_bytes += (size_of::() + size_of::()) as u64; + } + + for (_, file_info) in self.file_content.iter() { + num_bytes += file_info.num_bytes(); + num_bytes += (size_of::() + size_of::()) as u64; + } + + num_bytes += + ((size_of::() + 2 * size_of::()) * self.chunk_hash_lookup.len()) as u64; + + self.current_shard_file_size = num_bytes; + } + + pub fn difference(&self, other: &Self) -> Result { + let mut s = Self { + cas_content: other + .cas_content + .iter() + .filter(|(k, _)| !self.cas_content.contains_key(k)) + .map(|(k, v)| (*k, v.clone())) + .collect(), + file_content: other + .file_content + .iter() + .filter(|(k, _)| !self.file_content.contains_key(k)) + .map(|(k, v)| (*k, v.clone())) + .collect(), + chunk_hash_lookup: other + .chunk_hash_lookup + .iter() + .filter(|(k, _)| !self.chunk_hash_lookup.contains_key(k)) + .map(|(k, v)| (*k, v.clone())) + .collect(), + current_shard_file_size: 0, + }; + s.recalculate_shard_size(); + Ok(s) + } + + /// Given a file pointer, returns the information needed to reconstruct the file. + /// Returns the file info if the file hash was found, and None otherwise. + pub fn get_file_reconstruction_info(&self, file_hash: &MerkleHash) -> Option { + if let Some(mdb_file) = self.file_content.get(file_hash) { + return Some(mdb_file.clone()); + } + + None + } + + pub fn chunk_hash_dedup_query( + &self, + query_hashes: &[MerkleHash], + ) -> Option<(usize, FileDataSequenceEntry)> { + if query_hashes.is_empty() { + return None; + } + + let (chunk_ref, offset) = match self.chunk_hash_lookup.get(&query_hashes[0]) { + Some(s) => s, + None => return None, + }; + + let offset = *offset as usize; + + let end_byte_offset; + let mut query_idx = 0; + + loop { + if offset + query_idx >= chunk_ref.chunks.len() { + end_byte_offset = chunk_ref.metadata.num_bytes_in_cas; + break; + } + if query_idx >= query_hashes.len() + || chunk_ref.chunks[offset + query_idx].chunk_hash != query_hashes[query_idx] + { + end_byte_offset = chunk_ref.chunks[offset + query_idx].chunk_byte_range_start; + break; + } + query_idx += 1; + } + + Some(( + query_idx, + FileDataSequenceEntry::from_cas_entries( + &chunk_ref.metadata, + &chunk_ref.chunks[offset..(offset + query_idx)], + end_byte_offset, + ), + )) + } + + pub fn num_cas_entries(&self) -> usize { + self.cas_content.len() + } + + pub fn num_file_entries(&self) -> usize { + self.file_content.len() + } + + pub fn stored_bytes_on_disk(&self) -> u64 { + self.cas_content.iter().fold(0u64, |acc, (_, cas)| { + acc + cas.metadata.num_bytes_on_disk as u64 + }) + } + + pub fn materialized_bytes(&self) -> u64 { + self.file_content.iter().fold(0u64, |acc, (_, file)| { + acc + file + .segments + .iter() + .fold(0u64, |acc, entry| acc + entry.unpacked_segment_bytes as u64) + }) + } + + pub fn stored_bytes(&self) -> u64 { + self.cas_content.iter().fold(0u64, |acc, (_, cas)| { + acc + cas.metadata.num_bytes_in_cas as u64 + }) + } + + pub fn is_empty(&self) -> bool { + self.cas_content.is_empty() && self.file_content.is_empty() + } + + /// Returns the number of bytes required + pub fn shard_file_size(&self) -> u64 { + self.current_shard_file_size + MDBShardInfo::non_content_byte_size() + } + + /// Writes the shard out to a file. + pub fn write_to_temp_shard_file(&self, temp_file_name: &Path) -> Result { + let mut hashed_write; // Need to access after file is closed. + + { + // Scoped so that file is closed and flushed before name is changed. + + let out_file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(temp_file_name)?; + + hashed_write = HashedWrite::new(out_file); + + let mut buf_write = BufWriter::new(&mut hashed_write); + + // Ask for write access, as we'll flush this at the end + MDBShardInfo::serialize_from(&mut buf_write, self)?; + + debug!("Writing out in-memory shard to {temp_file_name:?}."); + + buf_write.flush()?; + } + + // Get the hash + hashed_write.flush()?; + let shard_hash = hashed_write.hash(); + + Ok(shard_hash) + } + pub fn write_to_directory(&self, directory: &Path) -> Result { + // First, create a temporary shard structure in that directory. + let temp_file_name = directory.join(temp_shard_file_name()); + + let shard_hash = self.write_to_temp_shard_file(&temp_file_name)?; + + let full_file_name = directory.join(shard_file_name(&shard_hash)); + + std::fs::rename(&temp_file_name, &full_file_name)?; + + debug!("Wrote out in-memory shard to {full_file_name:?}."); + + Ok(full_file_name) + } +} diff --git a/mdb_shard/src/shard_version.rs b/mdb_shard/src/shard_version.rs new file mode 100644 index 00000000..9e09b048 --- /dev/null +++ b/mdb_shard/src/shard_version.rs @@ -0,0 +1,139 @@ +use std::{fmt, path::Path, str::FromStr}; + +use crate::error::{MDBShardError, Result}; + +pub const MDB_SHARD_VERSION: u64 = 2; +pub const MDB_SHARD_HEADER_VERSION: u64 = MDB_SHARD_VERSION; +pub const MDB_SHARD_FOOTER_VERSION: u64 = MDB_SHARD_VERSION; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug, Default)] +pub enum ShardVersion { + /// In a git repo that does not have merkledb v1 or v2 elements. + Uninitialized = 0, + + // Use MerkleMemDB + V1 = 1, + + // Use MDBShardInfo + #[default] + V2, + // Future versions can be added to this enum +} + +impl TryFrom for ShardVersion { + type Error = MDBShardError; + + fn try_from(value: u64) -> std::result::Result { + match value { + 1 => Ok(Self::V1), + 2 => Ok(Self::V2), + _ => Err(MDBShardError::ShardVersionError(format!( + "{} is not a valid version", + value + ))), + } + } +} + +impl FromStr for ShardVersion { + type Err = MDBShardError; + + fn from_str(s: &str) -> std::result::Result { + let v = s.parse::().map_err(|_| { + MDBShardError::ShardVersionError(format!("{} is not a valid version", s)) + })?; + ShardVersion::try_from(v) + } +} + +impl fmt::Display for ShardVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let v = *self as u64; + write!(f, "{v}") + } +} + +impl ShardVersion { + pub fn try_from_file(path: impl AsRef) -> Result { + std::fs::read_to_string(path)?.parse::() + } + + pub fn get_value(&self) -> u64 { + *self as u64 + } + + pub fn get_lower(&self) -> Option { + ShardVersion::try_from(*self as u64 - 1).ok() + } + + pub fn need_salt(&self) -> bool { + match self { + ShardVersion::Uninitialized => false, + ShardVersion::V1 => false, + ShardVersion::V2 => true, + } + } + + pub fn get_max() -> ShardVersion { + Self::try_from(MDB_SHARD_VERSION).unwrap() + } +} + +#[cfg(test)] +mod tests { + use crate::shard_version::{ShardVersion, MDB_SHARD_VERSION}; + + use crate::error::*; + + use std::str::FromStr; + use tempfile::TempDir; + + #[test] + fn test_from_u64() -> Result<()> { + assert_eq!(ShardVersion::try_from(1)?, ShardVersion::V1); + assert_eq!(ShardVersion::try_from(2)?, ShardVersion::V2); + assert!(ShardVersion::try_from(0).is_err()); + + Ok(()) + } + + #[test] + fn test_from_string() -> Result<()> { + assert_eq!(ShardVersion::from_str("1")?, ShardVersion::V1); + assert_eq!(ShardVersion::from_str("2")?, ShardVersion::V2); + assert!(ShardVersion::from_str("0").is_err()); + assert!(ShardVersion::from_str("text").is_err()); + + Ok(()) + } + + #[test] + fn test_from_file() -> Result<()> { + let tmp_dir = TempDir::new()?; + let v = ShardVersion::V1; + let file_name = tmp_dir.path().join("version.lock"); + + std::fs::write(&file_name, v.to_string())?; + + let r = ShardVersion::try_from_file(&file_name)?; + + assert_eq!(v, r); + + Ok(()) + } + + #[test] + fn test_get_lower() -> Result<()> { + assert_eq!(ShardVersion::V2.get_lower(), Some(ShardVersion::V1)); + assert_eq!(ShardVersion::V1.get_lower(), None); + + Ok(()) + } + + #[test] + fn test_get_max() -> Result<()> { + assert_eq!(ShardVersion::get_max().get_value(), MDB_SHARD_VERSION); + + Ok(()) + } +} diff --git a/mdb_shard/src/utils.rs b/mdb_shard/src/utils.rs new file mode 100644 index 00000000..672279d6 --- /dev/null +++ b/mdb_shard/src/utils.rs @@ -0,0 +1,62 @@ +use lazy_static::lazy_static; +use merklehash::MerkleHash; +use regex::Regex; +use std::{ffi::OsStr, ops::Deref, path::Path}; +use uuid::Uuid; + +lazy_static! { + static ref MERKLE_DB_FILE_PATTERN: Regex = + Regex::new(r"^(?P[0-9a-fA-F]{64})\.mdb$").unwrap(); +} + +/// Parses a shard filename. If the filename matches the shard filename pattern, +/// then Some(hash) is returned, where hash is the CAS hash of the merkledb file. +/// If the filename does not match, None is returned. +#[inline] +pub fn parse_shard_filename>(path: P) -> Option { + let path: &Path = path.as_ref(); + let filename = path.file_name()?; + + let filename = filename.to_str().unwrap_or_default(); + + MERKLE_DB_FILE_PATTERN + .captures(filename) + .map(|capture| MerkleHash::from_hex(capture.name("hash").unwrap().as_str()).unwrap()) +} + +#[inline] +pub fn truncate_hash(hash: &MerkleHash) -> u64 { + hash.deref()[0] +} + +pub fn shard_file_name(hash: &MerkleHash) -> String { + format!("{}.mdb", hash.hex()) +} + +pub fn temp_shard_file_name() -> String { + let uuid = Uuid::new_v4(); + format!(".{uuid}.mdb_temp") +} + +pub fn is_temp_shard_file(p: &Path) -> bool { + p.file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_str() + .unwrap_or("") + .ends_with("mdb_temp") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shard_format::test_routines::rng_hash; + + #[test] + fn test_regex() { + let mh = rng_hash(0); + + assert!(parse_shard_filename(format!("/Users/me/temp/{}.mdb", mh.hex())).is_some()); + assert!(parse_shard_filename(format!("{}.mdb", mh.hex())).is_some()); + assert!(parse_shard_filename(format!("other_{}.mdb", mh.hex())).is_none()); + } +} diff --git a/merkledb/Cargo.toml b/merkledb/Cargo.toml new file mode 100644 index 00000000..508d3ec2 --- /dev/null +++ b/merkledb/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "merkledb" +version = "0.14.5" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +doctest = false + +[dependencies] +merklehash = { path = "../merklehash"} +rand = "0.8.4" +rand_core = "0.6.3" +rand_chacha = "0.3.1" +serde = {version="1.0.129", features = ["derive"]} +structopt = "0.3.22" +ron = "0.6.4" +gearhash = "0.1.3" +walkdir = "2.3.2" +rayon = "1.5.1" +bincode = "1.3.3" +rustc-hash = "1.1.0" +tempfile = "3.2.0" +bitflags = "1.3.2" +itertools = "0.10.1" +xet_error = {path = "../xet_error"} +tracing = "0.1.31" +async-trait = "0.1.9" +tokio = { version = "1.36", features = ["full"] } +futures = "0.3" +lazy_static = "1.4.0" +clap = { version = "3.1.6", features = ["derive"] } +blake3 = "1.5.1" +parutils = { path = "../parutils"} + +[dev-dependencies] +criterion = { version = "0.3.5", features = ["html_reports"] } +lazy_static = "1.4.0" +async-scoped = {version = "0.7", features = ["use-tokio"]} + +[[bench]] +name = "rolling_hash_benchmark" +harness = false + +[[bin]] +name = "testdedupe" +path = "src/bin/testdedupe.rs" + +[features] +strict = [] diff --git a/merkledb/benches/rolling_hash_benchmark.rs b/merkledb/benches/rolling_hash_benchmark.rs new file mode 100644 index 00000000..752f1180 --- /dev/null +++ b/merkledb/benches/rolling_hash_benchmark.rs @@ -0,0 +1,273 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use lazy_static::lazy_static; +use rand_chacha::rand_core::RngCore; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaChaRng; +use std::{collections::VecDeque, fmt}; + +pub trait RollingHash { + fn clone(&self) -> Box; + + fn current_hash(&self) -> u32; + fn reset(&mut self); + fn accumulate_byte(&mut self, b: u8) -> u32; + fn at_chunk_boundary(&self) -> bool; + + fn accumulate(&mut self, buf: &[u8]) -> u32 { + for &c in buf.iter() { + self.accumulate_byte(c); + } + self.current_hash() + } + + fn scan_to_chunk_boundary(&mut self, buf: &[u8]) -> (u32, usize) { + for (u, &c) in buf.iter().enumerate() { + self.accumulate_byte(c); + if self.at_chunk_boundary() { + return (self.current_hash(), u); + } + } + (self.current_hash(), buf.len()) + } +} + +#[derive(Debug)] +pub struct SumHash { + mask: u32, + bytehash: [u32; 256], + history: VecDeque, + windowsize: usize, + current_hash: u32, +} + +impl SumHash { + /// if seed is 0, each byte is added without a byte hash. i.e. + /// the hash of a window is exactly the sum of all the bytes. + /// Otherwise, a RNG is used to generate a fixed random value for each + /// byte. + pub fn init(mask: u32, windowsize: usize, seed: u64) -> SumHash { + let mut bytehash: [u32; 256] = [0; 256]; + if seed > 0 { + let mut rng = ChaChaRng::seed_from_u64(seed); + for i in 0..256 { + bytehash[i] = rng.next_u32(); + } + } else { + for i in 0..256 { + bytehash[i] = i as u32; + } + } + assert!(windowsize > 0); + SumHash { + mask, + bytehash, + history: VecDeque::new(), + windowsize, + current_hash: 0, + } + } +} + +impl RollingHash for SumHash { + fn clone(&self) -> Box { + Box::new(SumHash { + mask: self.mask, + bytehash: self.bytehash, + history: self.history.clone(), + windowsize: self.windowsize, + current_hash: self.current_hash, + }) + } + fn current_hash(&self) -> u32 { + self.current_hash + } + fn reset(&mut self) { + self.current_hash = 0_u32; + self.history.clear(); + } + fn at_chunk_boundary(&self) -> bool { + self.current_hash & self.mask == 0 + } + fn accumulate_byte(&mut self, b: u8) -> u32 { + self.history.push_back(b); + self.current_hash = self.current_hash.wrapping_add(self.bytehash[b as usize]); + if self.history.len() > self.windowsize { + let first = *(self.history.front().unwrap()); + self.current_hash = self + .current_hash + .wrapping_sub(self.bytehash[first as usize]); + self.history.pop_front(); + } + self.current_hash + } +} + +impl fmt::Display for SumHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "mask: {}, windowsize {}", self.mask, self.windowsize) + } +} + +#[derive(Debug)] +pub struct BuzHash { + mask: u32, + bytehash_right: [u32; 256], // bytehash to use when adding a byte to the right + bytehash_left: [u32; 256], // bytehash to use when removing a byte from the left + history: VecDeque, + windowsize: usize, + current_hash: u32, +} + +impl BuzHash { + pub fn init(mask: u32, windowsize: usize, seed: u64) -> BuzHash { + let mut bytehash_right: [u32; 256] = [0; 256]; + let mut bytehash_left: [u32; 256] = [0; 256]; + let mut rng = ChaChaRng::seed_from_u64(seed); + for i in 0..256 { + bytehash_right[i] = rng.next_u32(); + bytehash_left[i] = bytehash_right[i].rotate_left(windowsize as u32); + } + assert!(windowsize > 0); + BuzHash { + mask, + bytehash_right, + bytehash_left, + history: VecDeque::new(), + windowsize, + current_hash: 0, + } + } +} + +impl RollingHash for BuzHash { + fn clone(&self) -> Box { + Box::new(BuzHash { + mask: self.mask, + bytehash_right: self.bytehash_right, + bytehash_left: self.bytehash_left, + history: self.history.clone(), + windowsize: self.windowsize, + current_hash: self.current_hash, + }) + } + fn current_hash(&self) -> u32 { + self.current_hash + } + fn reset(&mut self) { + self.current_hash = 0_u32; + self.history.clear(); + } + fn at_chunk_boundary(&self) -> bool { + self.current_hash & self.mask == 0 + } + fn accumulate_byte(&mut self, b: u8) -> u32 { + self.history.push_back(b); + self.current_hash = self.current_hash.rotate_left(1) ^ self.bytehash_right[b as usize]; + if self.history.len() > self.windowsize { + let first = *(self.history.front().unwrap()); + self.current_hash ^= self.bytehash_left[first as usize]; + self.history.pop_front(); + } + self.current_hash + } +} + +impl fmt::Display for BuzHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "mask: {}, windowsize {}", self.mask, self.windowsize) + } +} + +#[derive(Debug)] +pub struct GearHash { + mask: u32, + bytehash: [u32; 256], + current_hash: u32, +} + +impl GearHash { + pub fn init(mask: u32, seed: u64) -> GearHash { + let mut rng = ChaChaRng::seed_from_u64(seed); + let mut bytehash: [u32; 256] = [0; 256]; + for i in 0..256 { + bytehash[i] = rng.next_u32(); + } + GearHash { + mask, + bytehash, + current_hash: 0, + } + } +} + +impl RollingHash for GearHash { + fn clone(&self) -> Box { + Box::new(GearHash { + mask: self.mask, + bytehash: self.bytehash, + current_hash: self.current_hash, + }) + } + #[inline(always)] + fn current_hash(&self) -> u32 { + self.current_hash + } + fn reset(&mut self) { + self.current_hash = 0_u32; + } + #[inline(always)] + fn accumulate_byte(&mut self, b: u8) -> u32 { + self.current_hash = self + .current_hash + .wrapping_shl(1) + .wrapping_add(self.bytehash[b as usize]); + self.current_hash + } + #[inline(always)] + fn at_chunk_boundary(&self) -> bool { + self.current_hash & self.mask == 0 + } +} + +impl fmt::Display for GearHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "mask: {}, windowsize 32", self.mask) + } +} + +const BENCH_INPUT_SEED: u64 = 0xa383d96f7becd17e; +const BUF_SIZE: usize = 128; // * 1024 + +lazy_static! { + static ref BENCH_INPUT_BUF: [u8; BUF_SIZE] = { + use rand::{RngCore, SeedableRng}; + let mut bytes = [0u8; BUF_SIZE]; + rand::rngs::StdRng::seed_from_u64(BENCH_INPUT_SEED).fill_bytes(&mut bytes); + bytes + }; +} + +fn run_rolling_hash(rolling_hash: &mut impl RollingHash) { + let len = 0usize; + loop { + let (_, len) = black_box(rolling_hash.scan_to_chunk_boundary(&BENCH_INPUT_BUF[len..])); + if len >= BUF_SIZE { + break; + } + } +} + +fn bench_rolling_hashes(c: &mut Criterion) { + let mut sh = SumHash::init(0xFF, 64, 1234); + let mut bh = BuzHash::init(0xFF, 128, 1234); + let mut gh = GearHash::init(0xFF, 1234); + + let mut group = c.benchmark_group("Rolling Hashes"); + group.bench_function("SumHash", |b| b.iter(|| run_rolling_hash(&mut sh))); + group.bench_function("BuzzHash", |b| b.iter(|| run_rolling_hash(&mut bh))); + group.bench_function("GearHash", |b| b.iter(|| run_rolling_hash(&mut gh))); + group.finish(); +} + +criterion_group!(benches, bench_rolling_hashes); +criterion_main!(benches); diff --git a/merkledb/src/aggregate_hashes.rs b/merkledb/src/aggregate_hashes.rs new file mode 100644 index 00000000..38bfb5c8 --- /dev/null +++ b/merkledb/src/aggregate_hashes.rs @@ -0,0 +1,55 @@ +use merklehash::MerkleHash; + +use blake3; + +use crate::error::{MerkleDBError, Result}; +use crate::merkledb_highlevel_v2::MerkleDBHighLevelMethodsV2; +use crate::MerkleNode; +use crate::{merkledbbase::MerkleDBBase, MerkleMemDB}; + +// Given a list of hashes and sizes, compute the aggregate hash for a cas node. +pub fn cas_node_hash(chunks: &[(MerkleHash, (usize, usize))]) -> MerkleHash { + // Create an ephemeral MDB. + if chunks.is_empty() { + return MerkleHash::default(); + } + + let mut mdb = MerkleMemDB::default(); + + let nodes: Vec = chunks + .iter() + .map(|(h, (lb, ub))| mdb.maybe_add_node(h, ub - lb, Vec::default()).0) + .collect(); + + let m = mdb.merge_to_cas(&nodes[..]); + + *m.hash() +} + +// Given a list of hashes and sizes, compute the aggregate hash for a file node. +pub fn file_node_hash(chunks: &[(MerkleHash, usize)], salt: &[u8; 32]) -> Result { + // Create an ephemeral MDB. + if chunks.is_empty() { + return Ok(MerkleHash::default()); + } + + let mut mdb = MerkleMemDB::default(); + + let nodes: Vec = chunks + .iter() + .map(|(h, size)| mdb.maybe_add_node(h, *size, Vec::default()).0) + .collect(); + + let m = mdb.merge_to_file(&nodes[..]); + + with_salt(m.hash(), salt) +} + +pub fn with_salt(hash: &MerkleHash, salt: &[u8; 32]) -> Result { + let salted_hash = blake3::keyed_hash(salt, hash.as_bytes()); + + let salted_hash = MerkleHash::try_from(salted_hash.as_bytes().as_slice()) + .map_err(|_| MerkleDBError::Other("fail to salt a MerkleHash".to_owned()))?; + + Ok(salted_hash) +} diff --git a/merkledb/src/async_chunk_iterator.rs b/merkledb/src/async_chunk_iterator.rs new file mode 100644 index 00000000..ad20c224 --- /dev/null +++ b/merkledb/src/async_chunk_iterator.rs @@ -0,0 +1,509 @@ +use super::constants::*; +use crate::chunk_iterator::HASH_SEED; +use crate::Chunk; +use async_trait::async_trait; +use lazy_static::lazy_static; +use merklehash::*; +use parutils::AsyncIterator; +use rand_chacha::rand_core::RngCore; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaChaRng; +use std::cmp::min; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::pin::Pin; + +type ChunkYieldType = (Chunk, Vec); + +/// Chunk Generator given an input stream. Do not use directly. +/// Use `async_chunk_target`. +pub struct AsyncChunker, E: Send + Sync + 'static> +where + T::Item: AsRef<[u8]>, +{ + iter: T, + hash: gearhash::Hasher<'static>, + minimum_chunk: usize, + maximum_chunk: usize, + mask: u64, + // generator state + chunkbuf: Vec, + cur_chunk_len: usize, + yield_queue: VecDeque, + complete_after_queue: bool, + _e: PhantomData, +} + +#[async_trait] +impl, E: Send + Sync + 'static> AsyncIterator for AsyncChunker +where + T::Item: AsRef<[u8]>, +{ + type Item = (Chunk, Vec); + + /// Returns GenType::Yielded((Chunk, Vec)) when there is a chunk. + /// call again for the next chunk. + /// returns GenType::Complete(io::Result<()>) on completion. + /// + /// If any errors are encountered, Complete(Err(io::Err)) will be + /// returned. + /// + /// ```ignore + /// loop { + /// match generator.next().await { + /// GenType::Yielded((chunk, bytes)) => { + /// chunks.push(chunk); + /// } + /// GenType::Complete(Err(e)) => { + /// // error condition + /// break; + /// } + /// GenType::Complete(Ok(())) => { + /// // generator done + /// break; + /// } + /// } + /// } + /// ``` + /// + /// Note that the std::ops::Generator trait calls this resume(). + /// We can implement the Generator trait in the future when it stabilizes. + async fn next(&mut self) -> Result, E> { + const MAX_WINDOW_SIZE: usize = 64; + if let Some(res) = self.yield_queue.pop_front() { + return Ok(Some(res)); + } + while !self.complete_after_queue && self.yield_queue.is_empty() { + match self.iter.next().await? { + Some(readbuf) => { + let readbuf = readbuf.as_ref(); + let read_bytes = readbuf.len(); + // 0 byte read is assumed EOF + if read_bytes > 0 { + let mut cur_pos = 0; + while cur_pos < read_bytes { + // every pass through this loop we either + // 1: create a chunk + // OR + // 2: consume the entire buffer + let chunk_buf_copy_start = cur_pos; + // skip the minimum chunk size + // and noting that the hash has a window size of 64 + // so we should be careful to skip only minimum_chunk - 64 - 1 + if self.cur_chunk_len < self.minimum_chunk - MAX_WINDOW_SIZE { + let max_advance = min( + self.minimum_chunk - self.cur_chunk_len - MAX_WINDOW_SIZE - 1, + read_bytes - cur_pos, + ); + cur_pos += max_advance; + self.cur_chunk_len += max_advance; + } + let mut consume_len; + let mut create_chunk = false; + // find a chunk boundary after minimum chunk + if let Some(boundary) = self + .hash + .next_match(&readbuf[cur_pos..read_bytes], self.mask) + { + consume_len = boundary; + create_chunk = true; + } else { + consume_len = read_bytes - cur_pos; + } + + // if we hit maximum chunk we must create a chunk + if consume_len + self.cur_chunk_len >= self.maximum_chunk { + consume_len = self.maximum_chunk - self.cur_chunk_len; + create_chunk = true; + } + self.cur_chunk_len += consume_len; + cur_pos += consume_len; + self.chunkbuf + .extend_from_slice(&readbuf[chunk_buf_copy_start..cur_pos]); + if create_chunk { + let res = ( + Chunk { + length: self.chunkbuf.len(), + hash: compute_data_hash(&self.chunkbuf[..]), + }, + std::mem::take(&mut self.chunkbuf), + ); + self.yield_queue.push_back(res); + + // reset chunk buffer state and continue to find the next chunk + self.chunkbuf.clear(); + self.hash.set_hash(0); + self.cur_chunk_len = 0; + } + } + } + } + None => { + self.complete_after_queue = true; + } + } + } + if let Some(res) = self.yield_queue.pop_front() { + return Ok(Some(res)); + } + // main loop complete + self.complete_after_queue = true; + if !self.chunkbuf.is_empty() { + let res = ( + Chunk { + length: self.chunkbuf.len(), + hash: compute_data_hash(&self.chunkbuf[..]), + }, + std::mem::take(&mut self.chunkbuf), + ); + return Ok(Some(res)); + } + Ok(None) + } +} + +// A version of chunk iter where a default hasher is used and parameters +// automatically determined given a target chunk size in bytes. +// target_chunk_size should be a power of 2, and no larger than 2^31 +// Gearhash is the default since it has good perf tradeoffs +pub fn async_chunk_target, E: Send + Sync + 'static>( + iter: T, + target_chunk_size: usize, +) -> AsyncChunker +where + T::Item: AsRef<[u8]>, +{ + assert_eq!(target_chunk_size.count_ones(), 1); + assert!(target_chunk_size > 1); + // note the strict lesser than. Combined with count_ones() == 1, + // this limits to 2^31 + assert!(target_chunk_size < u32::MAX as usize); + + let mask = (target_chunk_size - 1) as u64; + // we will like to shift the mask left by a bunch since the right + // bits of the gear hash are affected by only a small number of bytes + // really. we just shift it all the way left. + let mask = mask << mask.leading_zeros(); + let minimum_chunk = target_chunk_size / MINIMUM_CHUNK_DIVISOR; + let maximum_chunk = target_chunk_size * MAXIMUM_CHUNK_MULTIPLIER; + + assert!(maximum_chunk > minimum_chunk); + let hash = gearhash::Hasher::default(); + AsyncChunker { + iter, + hash, + minimum_chunk, + maximum_chunk, + mask, + // generator state init + chunkbuf: Vec::with_capacity(maximum_chunk), + cur_chunk_len: 0, + yield_queue: VecDeque::new(), + complete_after_queue: false, + _e: Default::default(), + } +} + +struct HasherPointerBox<'a>(*mut gearhash::Hasher<'a>); + +unsafe impl<'a> Send for HasherPointerBox<'a> {} +unsafe impl<'a> Sync for HasherPointerBox<'a> {} + +/// low Variance Chunk Generator given an input stream. Do not use directly. +/// Use `async_chunk_target_default` or `async_low_variance_chunk_target`. +pub struct AsyncLowVarianceChunker, E: Send + Sync + 'static> +where + T::Item: AsRef<[u8]>, +{ + iter: T, + hash: Vec>, + minimum_chunk: usize, + maximum_chunk: usize, + mask: u64, + // generator state + chunkbuf: Vec, + cur_chunk_len: usize, + // This hasher is referenced *a lot* and there was quite a + // measurable performance gain by making this a raw pointer. + // + // The key problem is that I need a mutable mutable reference to the + // current hasher which is basically an index into hash. + // (Basically cur_hasher = &mut hash[cur_hash_index]) + // + // But because of rust borrow checker rules, this cannot be done + // easily. We can of course just use hash[cur_hash_index] all the time + // but this is in fact a core inner loop and ends up as a perf bottleneck. + cur_hasher: HasherPointerBox<'static>, + cur_hash_index: usize, + yield_queue: VecDeque, + complete_after_queue: bool, + _e: PhantomData, +} + +#[async_trait] +impl, E: Send + Sync + 'static> AsyncIterator + for AsyncLowVarianceChunker +where + T::Item: AsRef<[u8]>, +{ + type Item = ChunkYieldType; + + /// Returns GenType::Yielded((Chunk, Vec)) when there is a chunk. + /// call again for the next chunk. + /// returns GenType::Complete(io::Result<()>) on completion. + /// + /// If any errors are encountered, Complete(Err(io::Err)) will be + /// returned. + /// + /// ```ignore + /// loop { + /// match generator.next().await { + /// GenType::Yielded((chunk, bytes)) => { + /// chunks.push(chunk); + /// } + /// GenType::Complete(Err(e)) => { + /// // error condition + /// break; + /// } + /// GenType::Complete(Ok(())) => { + /// // generator done + /// break; + /// } + /// } + /// } + /// ``` + /// + /// Note that the std::ops::Generator trait calls this resume(). + /// We can implement the Generator trait in the future when it stabilizes. + async fn next(&mut self) -> Result, E> { + const MAX_WINDOW_SIZE: usize = 64; + + if let Some(res) = self.yield_queue.pop_front() { + return Ok(Some(res)); + } + while !self.complete_after_queue && self.yield_queue.is_empty() { + match self.iter.next().await? { + Some(readbuf) => { + let readbuf: &[u8] = readbuf.as_ref(); + let read_bytes = readbuf.len(); + if read_bytes > 0 { + let mut cur_pos = 0; + while cur_pos < read_bytes { + // every pass through this loop we either + // 1: create a chunk + // OR + // 2: consume the entire buffer + let chunk_buf_copy_start = cur_pos; + // skip the minimum chunk size + // and noting that the hash has a window size of 64 + // so we should be careful to skip only minimum_chunk - 64 - 1 + if self.cur_chunk_len < self.minimum_chunk - MAX_WINDOW_SIZE { + let max_advance = min( + self.minimum_chunk - self.cur_chunk_len - MAX_WINDOW_SIZE - 1, + read_bytes - cur_pos, + ); + cur_pos += max_advance; + self.cur_chunk_len += max_advance; + } + let mut consume_len; + let mut create_chunk = false; + // find a chunk boundary after minimum chunk + + // If we have a lot of data, don't read all the way to the end when we'll stop reading + // at the maximum chunk boundary. + let read_end = + read_bytes.min(cur_pos + self.maximum_chunk - self.cur_chunk_len); + + if let Some(boundary) = unsafe { + (*self.cur_hasher.0) + .next_match(&readbuf[cur_pos..read_end], self.mask) + } { + consume_len = boundary; + create_chunk = true; + } else { + consume_len = read_end - cur_pos; + } + + // if we hit maximum chunk we must create a chunk + if consume_len + self.cur_chunk_len >= self.maximum_chunk { + consume_len = self.maximum_chunk - self.cur_chunk_len; + create_chunk = true; + } + self.cur_chunk_len += consume_len; + cur_pos += consume_len; + self.chunkbuf + .extend_from_slice(&readbuf[chunk_buf_copy_start..cur_pos]); + if create_chunk { + // advance the current hash index. + // we actually create a chunk when we run out of hashers + unsafe { (*self.cur_hasher.0).set_hash(0) }; + self.cur_hash_index += 1; + unsafe { + self.cur_hasher = HasherPointerBox( + self.hash.as_mut_ptr().add(self.cur_hash_index), + ); + } + if self.cur_hash_index >= self.hash.len() { + let res = ( + Chunk { + length: self.chunkbuf.len(), + hash: compute_data_hash(&self.chunkbuf[..]), + }, + std::mem::take(&mut self.chunkbuf), + ); + // reset chunk buffer state and continue to find the next chunk + self.yield_queue.push_back(res); + + self.chunkbuf.clear(); + self.cur_hash_index = 0; + self.cur_hasher = HasherPointerBox(self.hash.as_mut_ptr()); + } + self.cur_chunk_len = 0; + } + } + } + } + None => { + self.complete_after_queue = true; + } + } + } + if let Some(res) = self.yield_queue.pop_front() { + return Ok(Some(res)); + } + // main loop complete + if !self.chunkbuf.is_empty() { + let res = ( + Chunk { + length: self.chunkbuf.len(), + hash: compute_data_hash(&self.chunkbuf[..]), + }, + std::mem::take(&mut self.chunkbuf), + ); + return Ok(Some(res)); + } + Ok(None) + } +} + +lazy_static! { + /// The static gearhash seed table. + static ref HASHER_SEED_TABLE: Vec<[u64; 256]> = { + let mut tables: Vec<[u64; 256]> = Vec::new(); + for i in 0..N_LOW_VARIANCE_CDC_CHUNKERS { + let mut rng = ChaChaRng::seed_from_u64(HASH_SEED + i as u64); + let mut bytehash: [u64; 256] = [0; 256]; + #[allow(clippy::needless_range_loop)] + for i in 0..256 { + bytehash[i] = rng.next_u64(); + } + tables.push(bytehash); + } + tables + }; +} + +// Annoying that we have to explicitly declare this, but that is the cost of using async_trait +#[async_trait] +impl> AsyncIterator + for Pin>> +where + T::Item: AsRef<[u8]>, +{ + type Item = ChunkYieldType; + + async fn next(&mut self) -> Result, E> { + unsafe { + let mut_ref: Pin<&mut _> = Pin::as_mut(self); + let mut_ref = Pin::get_unchecked_mut(mut_ref); + mut_ref.next().await + } + } +} + +/// A version of low_variance_chunk_iter where a default hasher is used and parameters +/// automatically determined given a target chunk size in bytes. +/// target_chunk_size should be a power of 2, and no larger than 2^31 +/// num_hashers must be a power of 2 and smaller than target_chunk_size. +/// Gearhash is the default since it has good perf tradeoffs. +/// +/// num_hashers cannot be larger than N_LOW_VARIANCE_CDC_CHUNKERS +/// +/// Returns a Generator. See `AsyncLowVarianceChunker` +#[allow(clippy::needless_lifetimes, dead_code)] +pub fn async_low_variance_chunk_target + 'static, E: Send + Sync + 'static>( + iter: T, + target_chunk_size: usize, + num_hashers: usize, +) -> Pin>> +where + T::Item: AsRef<[u8]>, +{ + // We require the type to be Pinned since we do have a n + // internal pointer. (cur_hasher). + + assert_eq!(target_chunk_size.count_ones(), 1); + assert_eq!(num_hashers.count_ones(), 1); + assert!(target_chunk_size > 1); + assert!(num_hashers < target_chunk_size); + // note the strict lesser than. Combined with count_ones() == 1, + // this limits to 2^31 + assert!(target_chunk_size < u32::MAX as usize); + + let target_per_hash_chunk_size = target_chunk_size / num_hashers; + + let mask = (target_per_hash_chunk_size - 1) as u64; + // we will like to shift the mask left by a bunch since the right + // bits of the gear hash are affected by only a small number of bytes + // really. we just shift it all the way left. + let mask = mask << mask.leading_zeros(); + let minimum_chunk = target_chunk_size / MINIMUM_CHUNK_DIVISOR; + let maximum_chunk = target_chunk_size * MAXIMUM_CHUNK_MULTIPLIER; + + let mut hashers: Vec = Vec::new(); + assert!(num_hashers <= HASHER_SEED_TABLE.len()); + for t in HASHER_SEED_TABLE.chunks(1) { + hashers.push(gearhash::Hasher::new(&t[0])); + if hashers.len() == num_hashers { + break; + } + } + + assert!(maximum_chunk > minimum_chunk); + assert!(!hashers.is_empty()); + let num_hashes = hashers.len(); + let mut res = Box::pin(AsyncLowVarianceChunker { + iter, + hash: hashers, + minimum_chunk: minimum_chunk / num_hashes, + maximum_chunk: maximum_chunk / num_hashes, + mask, + // generator state init + chunkbuf: Vec::with_capacity(maximum_chunk), + cur_chunk_len: 0, + cur_hasher: HasherPointerBox(std::ptr::null_mut()), + cur_hash_index: 0, + yield_queue: VecDeque::new(), + complete_after_queue: false, + _e: Default::default(), + }); + // initialize cur_hasher + unsafe { + let mut_ref: Pin<&mut _> = Pin::as_mut(&mut res); + let mut_ref = Pin::get_unchecked_mut(mut_ref); + mut_ref.cur_hasher = HasherPointerBox(mut_ref.hash.as_mut_ptr()); + } + + res +} + +/// Chunks an input stream with the default low variance configuration. +/// Returns a Generator. See `AsyncLowVarianceChunker` +pub fn async_chunk_target_default + 'static, E: Send + Sync + 'static>( + iter: T, +) -> Pin>> +where + T::Item: AsRef<[u8]>, +{ + async_low_variance_chunk_target(iter, TARGET_CDC_CHUNK_SIZE, N_LOW_VARIANCE_CDC_CHUNKERS) +} diff --git a/merkledb/src/bin/testdedupe.rs b/merkledb/src/bin/testdedupe.rs new file mode 100644 index 00000000..f0e7d75f --- /dev/null +++ b/merkledb/src/bin/testdedupe.rs @@ -0,0 +1,84 @@ +// Copyright (c) 2020 Nathan Fiedler +// +use clap::{App, Arg}; +use merkledb::{chunk_target, low_variance_chunk_target}; +use merklehash::*; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap, HashSet}; +use std::fs::File; +use std::str::FromStr; + +fn main() { + fn is_integer(v: &str) -> Result<(), String> { + if u64::from_str(v).is_ok() { + return Ok(()); + } + Err(String::from( + "The size must be a valid unsigned 64-bit integer.", + )) + } + let matches = App::new("Example of using fastcdc crate.") + .about("Splits a (large) file and computes checksums.") + .arg( + Arg::new("size") + .short('s') + .long("size") + .value_name("SIZE") + .help("The desired average size of the chunks.") + .takes_value(true) + .validator(is_integer), + ) + .arg( + Arg::new("lowvariance") + .short('l') + .long("lowvariance") + .help("If the low variance chunker is used"), + ) + .arg( + Arg::new("INPUT") + .help("Sets the input file to use") + .required(true) + .index(1), + ) + .get_matches(); + let size = matches.value_of("size").unwrap_or("131072"); + let lv: bool = if matches.occurrences_of("lowvariance") >= 1 { + eprintln!("Using the low variance chunker"); + true + } else { + eprintln!("Using the regular chunker"); + false + }; + let avg_size = u64::from_str(size).unwrap() as usize; + let filename = matches.value_of("INPUT").unwrap(); + let mut file = File::open(filename).expect("cannot open file!"); + let chunks = if lv { + low_variance_chunk_target(&mut file, avg_size, 8) + } else { + chunk_target(&mut file, avg_size) + }; + let mut h: HashSet = HashSet::new(); + let mut dist: BTreeMap = BTreeMap::new(); + let mut len: usize = 0; + let mut ulen: usize = 0; + let total_chunks = chunks.len(); + for entry in chunks { + let digest = entry.hash; + if !h.contains(&digest) { + len += entry.length; + h.insert(digest); + if let Entry::Vacant(e) = dist.entry(entry.length) { + e.insert(1); + } else { + *dist.get_mut(&entry.length).unwrap() += 1; + } + } + ulen += entry.length; + } + println!("{} / {} = {}", len, ulen, len as f64 / ulen as f64); + println!("{} unique chunks", h.len()); + println!("{total_chunks} total chunks"); + for (k, v) in dist.iter() { + println!("{k}, {v}"); + } +} diff --git a/merkledb/src/chunk_iterator.rs b/merkledb/src/chunk_iterator.rs new file mode 100644 index 00000000..78389a68 --- /dev/null +++ b/merkledb/src/chunk_iterator.rs @@ -0,0 +1,288 @@ +use super::constants::*; +use merklehash::*; +use rand_chacha::rand_core::RngCore; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaChaRng; +use std::cmp::min; +use std::io; +use std::io::Read; + +#[derive(Debug, Clone)] +pub struct Chunk { + pub hash: DataHash, + pub length: usize, +} + +pub const READ_BUF_SIZE: usize = 65536; +pub const HASH_SEED: u64 = 123456; + +pub struct Chunker<'a, T: Read> { + iter: &'a mut T, + hash: gearhash::Hasher<'a>, + minimum_chunk: usize, + maximum_chunk: usize, + mask: u64, +} + +fn fill_buf(reader: &mut impl Read, buf: &mut [u8]) -> io::Result { + reader.read(buf) +} + +impl<'a, T: Read> Chunker<'a, T> { + fn gen(&mut self) -> Vec { + let mut ret: Vec = Vec::with_capacity(1024); + let mut chunkbuf: Vec = Vec::with_capacity(self.maximum_chunk); + let mut cur_chunk_len: usize = 0; + let mut readbuf: [u8; READ_BUF_SIZE] = [0; READ_BUF_SIZE]; + const MAX_WINDOW_SIZE: usize = 64; + while let Ok(read_bytes) = fill_buf(&mut self.iter, &mut readbuf) { + if read_bytes == 0 { + break; + } + let mut cur_pos = 0; + while cur_pos < read_bytes { + // every pass through this loop we either + // 1: create a chunk + // OR + // 2: consume the entire buffer + let chunk_buf_copy_start = cur_pos; + // skip the minimum chunk size + // and noting that the hash has a window size of 64 + // so we should be careful to skip only minimum_chunk - 64 - 1 + if cur_chunk_len < self.minimum_chunk - MAX_WINDOW_SIZE { + let max_advance = min( + self.minimum_chunk - cur_chunk_len - MAX_WINDOW_SIZE - 1, + read_bytes - cur_pos, + ); + cur_pos += max_advance; + cur_chunk_len += max_advance; + } + let mut consume_len; + let mut create_chunk = false; + // find a chunk boundary after minimum chunk + if let Some(boundary) = self + .hash + .next_match(&readbuf[cur_pos..read_bytes], self.mask) + { + consume_len = boundary; + create_chunk = true; + } else { + consume_len = read_bytes - cur_pos; + } + + // if we hit maximum chunk we must create a chunk + if consume_len + cur_chunk_len >= self.maximum_chunk { + consume_len = self.maximum_chunk - cur_chunk_len; + create_chunk = true; + } + cur_chunk_len += consume_len; + cur_pos += consume_len; + chunkbuf.extend_from_slice(&readbuf[chunk_buf_copy_start..cur_pos]); + if create_chunk { + ret.push(Chunk { + length: chunkbuf.len(), + hash: compute_data_hash(&chunkbuf[..]), + }); + + // reset chunk buffer state and continue to find the next chunk + chunkbuf.clear(); + self.hash.set_hash(0); + cur_chunk_len = 0; + } + } + } + if !chunkbuf.is_empty() { + ret.push(Chunk { + length: chunkbuf.len(), + hash: compute_data_hash(&chunkbuf[..]), + }); + } + ret + } +} + +// A version of chunk iter where a default hasher is used and parameters +// automatically determined given a target chunk size in bytes. +// target_chunk_size should be a power of 2, and no larger than 2^31 +// Gearhash is the default since it has good perf tradeoffs +pub fn chunk_target(iter: &mut T, target_chunk_size: usize) -> Vec { + assert_eq!(target_chunk_size.count_ones(), 1); + assert!(target_chunk_size > 1); + // note the strict lesser than. Combined with count_ones() == 1, + // this limits to 2^31 + assert!(target_chunk_size < u32::MAX as usize); + + let mask = (target_chunk_size - 1) as u64; + // we will like to shift the mask left by a bunch since the right + // bits of the gear hash are affected by only a small number of bytes + // really. we just shift it all the way left. + let mask = mask << mask.leading_zeros(); + let minimum_chunk = target_chunk_size / MINIMUM_CHUNK_DIVISOR; + let maximum_chunk = target_chunk_size * MAXIMUM_CHUNK_MULTIPLIER; + + assert!(maximum_chunk > minimum_chunk); + let hash = gearhash::Hasher::default(); + Chunker { + iter, + hash, + minimum_chunk, + maximum_chunk, + mask, + } + .gen() +} + +pub struct LowVarianceChunker<'a, T: Read> { + iter: &'a mut T, + hash: Vec>, + minimum_chunk: usize, + maximum_chunk: usize, + mask: u64, +} + +impl<'a, T: Read> LowVarianceChunker<'a, T> { + fn gen(&mut self) -> Vec { + let mut ret: Vec = Vec::with_capacity(1024); + let mut chunkbuf: Vec = Vec::with_capacity(self.maximum_chunk); + let mut cur_chunk_len: usize = 0; + let mut readbuf: [u8; READ_BUF_SIZE] = [0; READ_BUF_SIZE]; + const MAX_WINDOW_SIZE: usize = 64; + let mut cur_hasher = self.hash.as_mut_ptr(); + let mut cur_hash_index: usize = 0; + while let Ok(read_bytes) = fill_buf(&mut self.iter, &mut readbuf) { + if read_bytes == 0 { + break; + } + let mut cur_pos = 0; + while cur_pos < read_bytes { + // every pass through this loop we either + // 1: create a chunk + // OR + // 2: consume the entire buffer + let chunk_buf_copy_start = cur_pos; + // skip the minimum chunk size + // and noting that the hash has a window size of 64 + // so we should be careful to skip only minimum_chunk - 64 - 1 + if cur_chunk_len < self.minimum_chunk - MAX_WINDOW_SIZE { + let max_advance = min( + self.minimum_chunk - cur_chunk_len - MAX_WINDOW_SIZE - 1, + read_bytes - cur_pos, + ); + cur_pos += max_advance; + cur_chunk_len += max_advance; + } + let mut consume_len; + let mut create_chunk = false; + // find a chunk boundary after minimum chunk + if let Some(boundary) = + unsafe { (*cur_hasher).next_match(&readbuf[cur_pos..read_bytes], self.mask) } + { + consume_len = boundary; + create_chunk = true; + } else { + consume_len = read_bytes - cur_pos; + } + + // if we hit maximum chunk we must create a chunk + if consume_len + cur_chunk_len >= self.maximum_chunk { + consume_len = self.maximum_chunk - cur_chunk_len; + create_chunk = true; + } + cur_chunk_len += consume_len; + cur_pos += consume_len; + chunkbuf.extend_from_slice(&readbuf[chunk_buf_copy_start..cur_pos]); + if create_chunk { + // advance the current hash index. + // we actually create a chunk when we run out of hashers + unsafe { (*cur_hasher).set_hash(0) }; + cur_hash_index += 1; + unsafe { + cur_hasher = self.hash.as_mut_ptr().add(cur_hash_index); + } + if cur_hash_index >= self.hash.len() { + ret.push(Chunk { + length: chunkbuf.len(), + hash: compute_data_hash(&chunkbuf[..]), + }); + + // reset chunk buffer state and continue to find the next chunk + chunkbuf.clear(); + cur_hash_index = 0; + cur_hasher = self.hash.as_mut_ptr(); + } + cur_chunk_len = 0; + } + } + } + if !chunkbuf.is_empty() { + ret.push(Chunk { + hash: compute_data_hash(&chunkbuf[..]), + length: chunkbuf.len(), + }); + } + ret + } +} + +// A version of low_variance_chunk_iter where a default hasher is used and parameters +// automatically determined given a target chunk size in bytes. +// target_chunk_size should be a power of 2, and no larger than 2^31 +// num_hashers must be a power of 2 and smaller than target_chunk_size. +// Gearhash is the default since it has good perf tradeoffs +#[allow(clippy::needless_lifetimes)] +pub fn low_variance_chunk_target<'a, T: Read>( + iter: &'a mut T, + target_chunk_size: usize, + num_hashers: usize, +) -> Vec { + assert_eq!(target_chunk_size.count_ones(), 1); + assert_eq!(num_hashers.count_ones(), 1); + assert!(target_chunk_size > 1); + assert!(num_hashers < target_chunk_size); + // note the strict lesser than. Combined with count_ones() == 1, + // this limits to 2^31 + assert!(target_chunk_size < u32::MAX as usize); + + let target_per_hash_chunk_size = target_chunk_size / num_hashers; + + let mask = (target_per_hash_chunk_size - 1) as u64; + // we will like to shift the mask left by a bunch since the right + // bits of the gear hash are affected by only a small number of bytes + // really. we just shift it all the way left. + let mask = mask << mask.leading_zeros(); + let minimum_chunk = target_chunk_size / MINIMUM_CHUNK_DIVISOR; + let maximum_chunk = target_chunk_size * MAXIMUM_CHUNK_MULTIPLIER; + + let mut hashers: Vec = Vec::new(); + let mut tables: Vec<[u64; 256]> = Vec::new(); + + for i in 0..num_hashers { + let mut rng = ChaChaRng::seed_from_u64(HASH_SEED + i as u64); + let mut bytehash: [u64; 256] = [0; 256]; + #[allow(clippy::needless_range_loop)] + for i in 0..256 { + bytehash[i] = rng.next_u64(); + } + tables.push(bytehash); + } + for t in tables.chunks(1) { + hashers.push(gearhash::Hasher::new(&t[0])); + } + + assert!(maximum_chunk > minimum_chunk); + assert!(!hashers.is_empty()); + let num_hashes = hashers.len(); + let mut chunker = LowVarianceChunker { + iter, + hash: hashers, + minimum_chunk: minimum_chunk / num_hashes, + maximum_chunk: maximum_chunk / num_hashes, + mask, + }; + + chunker.gen() +} + +pub fn chunk_target_default(iter: &mut T) -> Vec { + low_variance_chunk_target(iter, TARGET_CDC_CHUNK_SIZE, N_LOW_VARIANCE_CDC_CHUNKERS) +} diff --git a/merkledb/src/constants.rs b/merkledb/src/constants.rs new file mode 100644 index 00000000..e2e744ea --- /dev/null +++ b/merkledb/src/constants.rs @@ -0,0 +1,10 @@ +pub const MEAN_TREE_BRANCHING_FACTOR: u64 = 4; +pub const TARGET_CAS_BLOCK_SIZE: usize = 15 * 1024 * 1024; +pub const IDEAL_CAS_BLOCK_SIZE: usize = 16 * 1024 * 1024; +pub const TARGET_CDC_CHUNK_SIZE: usize = 16384; +pub const N_LOW_VARIANCE_CDC_CHUNKERS: usize = 8; + +/// TARGET_CDC_CHUNK_SIZE / MINIMUM_CHUNK_DIVISOR is the smallest chunk size +pub const MINIMUM_CHUNK_DIVISOR: usize = 4; +/// TARGET_CDC_CHUNK_SIZE * MAXIMUM_CHUNK_MULTIPLIER is the largest chunk size +pub const MAXIMUM_CHUNK_MULTIPLIER: usize = 8; diff --git a/merkledb/src/error.rs b/merkledb/src/error.rs new file mode 100644 index 00000000..08b6a146 --- /dev/null +++ b/merkledb/src/error.rs @@ -0,0 +1,45 @@ +use std::io; +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum MerkleDBError { + #[error("File I/O error")] + IOError(#[from] io::Error), + + #[error("Graph invariant broken : {0}")] + GraphInvariantError(String), + + #[error("Serialization/Deserialization Error : {0}")] + BinCodeError(#[from] bincode::Error), + + #[error("Bad file name format: {0}")] + BadFilename(String), + + #[error("Error: {0}")] + Other(String), +} + +// Define our own result type here (this seems to be the standard). +pub type Result = std::result::Result; + +// For error checking +impl PartialEq for MerkleDBError { + fn eq(&self, other: &MerkleDBError) -> bool { + match (self, other) { + (MerkleDBError::IOError(ref e1), MerkleDBError::IOError(ref e2)) => { + e1.kind() == e2.kind() + } + (MerkleDBError::GraphInvariantError(s1), MerkleDBError::GraphInvariantError(s2)) => { + s1 == s2 + } + (MerkleDBError::BinCodeError(_), MerkleDBError::BinCodeError(_)) => { + // TODO: expand this. Currently Encode/decode errors in bincode implement PartialEq, + // but the general error class has an Other enumeration that does not and thus it doesn't + // implement it. For now, just leave this as true since we're implementing this for + // testing purposes. + true + } + _ => false, + } + } +} diff --git a/merkledb/src/internal_methods.rs b/merkledb/src/internal_methods.rs new file mode 100644 index 00000000..e6ddd5b8 --- /dev/null +++ b/merkledb/src/internal_methods.rs @@ -0,0 +1,525 @@ +use super::constants::*; +use super::merklenode::*; +use crate::error::*; +use crate::merkledbbase::MerkleDBBase; +use std::collections::{HashMap, HashSet}; +/**************************************************************************/ +/* */ +/* Internal Algorithms */ +/* */ +/**************************************************************************/ + +/** + * Inserts a leaf node described by just a hash and a length into a database, + * returning an existing node if one already exists. + */ +pub fn node_from_hash( + db: &mut (impl MerkleDBBase + ?Sized), + hash: &MerkleHash, + len: usize, +) -> MerkleNode { + db.add_node(hash, len, Vec::new()) +} + +/** + * Inserts an interior node described by just a hash and a length into a database, + * returning an existing node if one already exists. + */ +pub fn node_from_children( + db: &mut (impl MerkleDBBase + ?Sized), + children: &[MerkleNode], + len: usize, +) -> MerkleNode { + let hash = hash_node_sequence(children); + let children_id: Vec<_> = children.iter().map(|x| (x.id(), x.len())).collect(); + db.add_node(&hash, len, children_id) +} + +/** + * Builds one level of hashes above "nodes". + * Returns a pair of arrays (parent_of_node, parents) + * + * parents is the level set above nodes. + * parent_of_node is the same length as nodes, and parent_of_node[i] is + * the id of the parent of node[i]. + * + * For instance, if given a list of nodes [n1,n2,n3,n4], with IDs [1,2,3,4] + * this function may return + * + * ```ignore + * parent_of_node = [5, 5, 6, 6] + * parents = [n5, n6] + * ``` + * + * This means that 2 parent nodes n5, n6 were created. Where n5 has 2 children + * [n1,n2], and n6 has two children [n3,n4]. + */ +pub fn merge_one_level( + db: &mut (impl MerkleDBBase + ?Sized), + nodes: &[MerkleNode], +) -> (Vec, Vec) { + /* + * We basically loop through the set of nodes tracking a window between + * cur_children_start_idx and idx (current index). + * [. . . . . . . . . . . ] + * ^ ^ + * | | + * start_idx | + * | + * idx + * + * When the current node at idx satisfies the cut condition: + * - the hash % MEAN_TREE_BRANCHING_FACTOR == 0: assuming a random + * hash distribution, this implies on average, the number of children + * is MEAN_TREE_BRANCHING_FACTOR, + * - OR this is the last node in the list. + * - subject to each parent must have at least 2 children, and at most + * MEAN_TREE_BRANCHING_FACTOR * 2 children: This ensures that + * the graph always has at most 1/2 the number of parents as children. + * and we don't have too wide branches. + * + * We build a parent, update the indices, and shift start_idx to the + * next index (idx + 1) to set up the window for the next parent. + */ + let total_children = nodes.len(); + // return value. the set of parents created + let mut parents: Vec = Vec::new(); + // return value. the parent of each input node + let mut parent_of_node: Vec = vec![0; nodes.len()]; + + // the start of the window for the next parent + let mut cur_children_start_idx = 0; + // The total length of the string represented by the next parent. + // (we are tracking this because it is needed metadata for the node + // creation) + let mut cur_children_total_len: usize = 0; + + for (idx, node) in nodes.iter().enumerate() { + cur_children_total_len += node.len(); + let test_hash = node.hash(); + // minumum number of children is 2 + // maximum number of children is 2* mean_branching_factor + // and we test for a cut by + // modding last 64 bits of the hash with the mean branching factor + let num_children_so_far = idx - cur_children_start_idx; + // sorry. I like the extra parens here. Its hard to remember which has + // precedence && or || + #[allow(unused_parens)] + if (num_children_so_far >= 2 && test_hash[3] % MEAN_TREE_BRANCHING_FACTOR == 0) + || num_children_so_far >= 2 * (MEAN_TREE_BRANCHING_FACTOR as usize) + || idx + 1 == total_children + { + // cut a parent node here + let parent_node = node_from_children( + db, + &nodes[cur_children_start_idx..=idx], + cur_children_total_len, + ); + let parent_id = parent_node.id(); + parents.push(parent_node); + #[allow(clippy::needless_range_loop)] + for ch_index in cur_children_start_idx..=idx { + parent_of_node[ch_index] = parent_id; + } + // set up for the next window + cur_children_total_len = 0; + cur_children_start_idx = idx + 1; + } + } + (parent_of_node, parents) +} + +/** + * A simple helper function that loops over nodes and parent_of_node + * (as produced by merge_one_level) and assigns the parent to each node. + */ +fn assign_node_parents( + db: &mut (impl MerkleDBBase + ?Sized), + nodes: &mut [MerkleNode], + parent_of_node: &[u64], + parent_type: NodeDataType, +) { + for (node, parent_id) in nodes.iter().zip(parent_of_node.iter()) { + let mut attr = db.node_attributes(node.id()).unwrap_or_default(); + if attr.parent(parent_type) == ID_UNASSIGNED { + attr.set_parent(parent_type, *parent_id); + db.set_node_attributes(node.id(), &attr); + } + } +} + +/** + * This is the return type of `find_descendent_reconstructor`. + * It is used to describe how the root relates to the descendents found + * and vice versa. + * - For the root, how to put together the descendent nodes to construct the + * value at the root. + * - and for each descendent, what subrange of the root does it correspond to. + */ +#[derive(Default)] +pub struct RootConstructionDescription { + pub root_id: MerkleNodeId, + /** + * A collection of descendent ranges that when put together will make up the root node + */ + pub descendent_ranges_for_root: Vec, + + /** + * For each descendent range, the corresponding range in the root node. + */ + pub root_ranges_in_descendent: Vec<(MerkleNodeId, ObjectRangeById)>, +} + +/** + * Main recursive implementation for find_descendent_reconstructor + */ +pub fn find_descendent_reconstructor_impl<'a>( + db: &(impl MerkleDBBase + ?Sized), + node: &MerkleNode, + root_id: MerkleNodeId, + mut root_start_byte: usize, + visited: &'a mut HashMap>, + condition: &impl Fn(&MerkleNode, &MerkleNodeAttributes) -> bool, + ret_root_ranges_in_descendent: &mut Vec<(MerkleNodeId, ObjectRangeById)>, +) -> Result<&'a mut Vec> { + if visited.contains_key(&node.id()) { + return Ok(visited.get_mut(&node.id()).unwrap()); + } + + let attr = db.node_attributes(node.id()).unwrap(); + if condition(node, &attr) { + ret_root_ranges_in_descendent.push(( + node.id(), + ObjectRangeById { + id: root_id, + start: root_start_byte, + end: root_start_byte + node.len(), + }, + )); + let myrange = ObjectRangeById { + id: node.id(), + start: 0, + end: node.len(), + }; + visited.insert(node.id(), vec![myrange]); + } else { + // if this is a leaf, we are in trouble. That means we that there is + // a path in which the user specified condition is never achieved. + // TODO: this will generally indicate a graph inconsistency and we may + // need a way to diagnose this. + if node.children().is_empty() { + return Err(MerkleDBError::GraphInvariantError( + "Reached a leaf while searching for descendents".into(), + )); + } + let mut concat_desc_range: Vec = Vec::new(); + for ch in node.children().iter() { + let chnode = db.find_node_by_id(ch.0).unwrap(); + let desc_range = find_descendent_reconstructor_impl( + db, + &chnode, + root_id, + root_start_byte, + visited, + condition, + ret_root_ranges_in_descendent, + )?; + concat_desc_range.extend(desc_range.iter().cloned()); + root_start_byte += chnode.len(); + } + visited.insert(node.id(), concat_desc_range); + } + Ok(visited.get_mut(&node.id()).unwrap()) +} +/** + * Looks for all descendents of a given node matching a condition passed as + * a function (condition) and returns a description of the forward-backward + * relationship between the descendents and the root and vice versa. + * + * root: The root node to start walking from + * condition: a user defined function which should return true if this node + * should be returned. The descendents of a "true" node will not + * be traversed. + * + * Returns an instance of RootConstructionDescription. + * + * Note that the condition must be satisfiable at some node on every path from + * the root to the leaf. That is to say on a recursive DFS walk of the graph + * which backtracks everytime condition=True, I should not ever reach a leaf + * where condition(leaf) evaluates to False. Right now on such a failure, + * we trigger an assertion failure. TODO will be to catch the failure and provide + * remedies; generally in such a situation, it means that we have a graph + * inconsistency. + */ +pub fn find_descendent_reconstructor( + db: &(impl MerkleDBBase + ?Sized), + root: &MerkleNode, + condition: impl Fn(&MerkleNode, &MerkleNodeAttributes) -> bool, +) -> Result { + let root_id = root.id(); + let mut visited: HashMap> = HashMap::new(); + let mut ret = RootConstructionDescription { + root_id, + ..Default::default() + }; + + ret.descendent_ranges_for_root + .clone_from(find_descendent_reconstructor_impl( + db, + root, + root_id, + 0, + &mut visited, + &condition, + &mut ret.root_ranges_in_descendent, + )?); + Ok(ret) +} + +/** + * Find a way to reconstruct the given root nodes using ranges of nodes + * of type dest_tag. + * + * As an example, lets consider the case where root node is a FILE + * and dest_tag is CAS. In this case, this function will return the sequence of + * CAS nodes that have to be read to reconstruct the root FILE. + * + * Returns None if unable to solve. + */ +pub fn find_reconstruction( + db: &(impl MerkleDBBase + ?Sized), + root: &[MerkleNode], + dest_tag: NodeDataType, +) -> Result)>> { + // to make this more concrete in the comments we will assume + // src_tag == CAS and dest_tag = FILE + + // these are nodes with both CAS and file parents. + // These will our "bridge" between CAS and FILE nodes. + // Call this N. + // nodes_between_src_and_dest is now a list of nodes in N, and for each node + // it's range in root. + let root_reconstructors: Vec> = root + .iter() + .map(|x| find_descendent_reconstructor(db, x, |_, attr| attr.has_data(dest_tag))) + .collect(); + + // make sure all succeeded + if !root_reconstructors.iter().all(|x| x.is_ok()) { + return Err(MerkleDBError::GraphInvariantError( + "Reconstruction infeasible".into(), + )); + } + let root_reconstructors: Vec = + root_reconstructors.into_iter().flatten().collect(); + + let mut nodes_between_src_and_dest: HashSet = Default::default(); + for aroot in &root_reconstructors { + for desc in &aroot.root_ranges_in_descendent { + nodes_between_src_and_dest.insert(desc.0); + } + } + // The basic strategy here is conceptually as follows + // Since N has both CAS and FILE parents then the graph conceptually looks + // like this + // + // CAS FILE + // \ / + // \ / + // v v + // N + // + // (where FILE, CAS and N are all collection of nodes) + // So to reconstruct say CAS from FILE, we + // 1. Find how to construct CAS with nodes in N + // 2. Find how to construct N from FILE + // 3. combine (1) and (2) to get from CAS->FILE + + let src_to_n: Vec<(MerkleNodeId, Vec)> = root_reconstructors + .into_iter() + .map(|x| (x.root_id, x.descendent_ranges_for_root)) + .collect(); + + // This gives the relationship between N->File + let n_to_dest: HashMap = nodes_between_src_and_dest + .iter() + .map(|x| db.find_node_by_id(*x).unwrap()) + .map(|x| { + ( + x.id(), + find_ancestor_reconstructor(db, &x, dest_tag).unwrap(), + ) + }) + .collect(); + + // each entry n_to_dest[N] = [F, start, end] + // tells me that I can construct the value at N with F[start, end] + + // we want to get cas to file, and so how that works is tht + // + // for each n_1[start_1,end_1], we map n_1 through n_to_dest + // (say n_to_dest[n_1] = [f_k, start_k, end_k] + // + // substituting, this gives us + // + // f_k[start_k, end_k][start_1, end_1] + // + // Then to combine this into a single range, we just note that + // length = end_1 - start_1, and so the combined range is just + // f_k[start_k + start_1, length] + // + Ok(src_to_n + .iter() + .map(|(id, ranges)| { + let new_ranges: Vec = ranges + .iter() + .map(|range| { + let length = range.end - range.start; + let frange = &n_to_dest[&range.id]; + // sanity check + assert!(length <= frange.end - frange.start); + let new_start = range.start + frange.start; + ObjectRange { + hash: *db.find_node_by_id(frange.id).unwrap().hash(), + start: new_start, + end: new_start + length, + } + }) + .collect(); + ( + *db.find_node_by_id(*id).unwrap().hash(), + simplify_ranges(&new_ranges[..]), + ) + }) + .collect()) +} + +/** + * Find the subrange of a parent of type reconstructor_type that can + * reconstruct the value of this node. + * + * This function simply walks up the parent attribute for the target type + * until it finds a node taged with the target typed. A bit of arithmetic + * is needed to figure out the original node's range inside the parent's range + * every iteration. + */ +pub fn find_ancestor_reconstructor( + db: &(impl MerkleDBBase + ?Sized), + node: &MerkleNode, + reconstructor_type: NodeDataType, +) -> Result { + let mut cur = node.clone(); + // the subrange of 'node' inside of 'cur' + let mut range_start: usize = 0; + loop { + let attr = db.node_attributes(cur.id()).unwrap(); + assert!(attr.has_data(reconstructor_type)); + if attr.is_type(reconstructor_type) { + return Ok(ObjectRangeById { + id: cur.id(), + start: range_start, + end: range_start + node.len(), + }); + } + // we ascend + let parent = attr.parent(reconstructor_type); + if parent != ID_UNASSIGNED { + let parent = db.find_node_by_id(parent).unwrap(); + let mut running_sum: usize = 0; + // range_start is node's position relative to cur right now. + // + // we need to shift it to be relative to the parent + // so we find where cur's position is in the list of + // siblings and shift the range start. + for (sibling, sibling_len) in parent.children().iter() { + if *sibling == cur.id() { + range_start += running_sum; + break; + } + running_sum += sibling_len; + } + cur = parent; + // + } else { + // something wrong. we should have hit + // a node of the reconstructor type, but such a node + // was not found + return Err(MerkleDBError::GraphInvariantError( + "Parent invariant violated. Did not reach a parent of appropriate type.".into(), + )); + } + } +} +/** + * Performs a level-wise merge of a collection of nodes, handling the + * CAS assignment at the same time. + * + * The basic mechanism is that when a file is added, it produces a collection + * of chunks (nodes). That is filtered to the subset which does not have data + * since we may have chunks which were stored before (nodes_without_cas_entry). + * + * nodes are level-wise merged to a single root node which is a FILE + * and parents are assigned accordingly for all the nodes in between. + * + * nodes_without_cas_entry are similarly level-wise merged but with a heuristic + * along the way where a CAS node may be constructed if the node size is + * which is sometimes. + * + */ +pub fn merge( + db: &mut (impl MerkleDBBase + ?Sized), + mut nodes: Vec, + root_is_file: bool, + root_is_cas: bool, +) -> MerkleNode { + // merge nodes to get the file representation + assert!(!nodes.is_empty()); + while nodes.len() > 1 { + let (parent_of_node, mut parents) = merge_one_level(db, &nodes); + if root_is_file { + assign_node_parents(db, &mut nodes, &parent_of_node, NodeDataType::FILE); + } + if root_is_cas { + assign_node_parents(db, &mut nodes, &parent_of_node, NodeDataType::CAS); + } + nodes = std::mem::take(&mut parents); + } + let r = &nodes[0]; + if root_is_file { + let mut attr = db.node_attributes(r.id()).unwrap_or_default(); + attr.set_file(); + db.set_node_attributes(r.id(), &attr); + } + if root_is_cas { + let mut attr = db.node_attributes(r.id()).unwrap_or_default(); + attr.set_cas(); + db.set_node_attributes(r.id(), &attr); + } + nodes[0].clone() +} + +/// Given a list of nodes with a particular node attribute +/// set the corresponding parent attribute in each descendent. +pub fn assign_all_parents( + db: &mut (impl MerkleDBBase + ?Sized), + mut roots: Vec, + root_type: NodeDataType, +) { + // we run a BFS against roots + let mut next_roots: HashSet = HashSet::new(); + while !roots.is_empty() { + for id in roots { + if let Some(node) = db.find_node_by_id(id) { + for (chid, _) in node.children() { + if let Some(mut attr) = db.node_attributes(*chid) { + attr.set_parent(root_type, id); + db.set_node_attributes(*chid, &attr); + next_roots.insert(*chid); + } + } + } + } + roots = next_roots.drain().collect(); + next_roots.clear(); + } +} diff --git a/merkledb/src/lib.rs b/merkledb/src/lib.rs new file mode 100644 index 00000000..d54ea2a8 --- /dev/null +++ b/merkledb/src/lib.rs @@ -0,0 +1,45 @@ +#![cfg_attr(feature = "strict", deny(warnings))] +#![allow(dead_code)] + +pub mod aggregate_hashes; +mod async_chunk_iterator; +mod chunk_iterator; +pub mod constants; +pub mod error; +mod internal_methods; +mod merkledb_debug; +mod merkledb_highlevel_v1; +mod merkledb_highlevel_v2; +mod merkledb_ingestion_v1; +mod merkledb_reconstruction; +mod merkledbbase; +mod merkledbv1; +mod merkledbv2; +mod merklememdb; +mod merklenode; +mod tests; + +pub use crate::merkledb_highlevel_v1::InsertionStaging; +pub use chunk_iterator::{chunk_target, low_variance_chunk_target, Chunk}; +pub use merkledbv1::MerkleDBV1; +pub use merkledbv2::MerkleDBV2; +pub use merklememdb::MerkleMemDB; +pub use merklenode::{MerkleNode, MerkleNodeAttributes, MerkleNodeId, NodeDataType, ObjectRange}; +pub mod prelude { + pub use crate::merkledb_debug::MerkleDBDebugMethods; + pub use crate::merkledb_highlevel_v1::MerkleDBHighLevelMethodsV1; + pub use crate::merkledb_ingestion_v1::MerkleDBIngestionMethodsV1; + pub use crate::merkledb_reconstruction::MerkleDBReconstruction; + pub use crate::merkledbbase::MerkleDBBase; + pub use crate::merkledbv1::MerkleDBV1; +} + +pub mod prelude_v2 { + pub use crate::merkledb_debug::MerkleDBDebugMethods; + pub use crate::merkledb_highlevel_v2::MerkleDBHighLevelMethodsV2; + pub use crate::merkledb_reconstruction::MerkleDBReconstruction; + pub use crate::merkledbbase::MerkleDBBase; +} +pub mod detail { + pub use crate::merklenode::hash_node_sequence; +} diff --git a/merkledb/src/merkledb_debug.rs b/merkledb/src/merkledb_debug.rs new file mode 100644 index 00000000..b5de408a --- /dev/null +++ b/merkledb/src/merkledb_debug.rs @@ -0,0 +1,318 @@ +use crate::chunk_iterator::Chunk; +use crate::internal_methods::*; +use crate::merkledb_highlevel_v1::*; +use crate::merkledb_reconstruction::MerkleDBReconstruction; +use crate::merkledbbase::MerkleDBBase; +use crate::merklememdb::MerkleMemDB; +use crate::merklenode::*; +use std::collections::HashSet; + +pub trait MerkleDBDebugMethods: MerkleDBBase + MerkleDBReconstruction { + fn print_node_details(&self, node: &MerkleNode) -> String { + let attr = self.node_attributes(node.id()).unwrap_or_default(); + let mut ret = String::new(); + if attr.has_cas_data() { + let cas_report = + if let Ok(r) = find_ancestor_reconstructor(self, node, NodeDataType::CAS) { + format!( + "\tSubstring [{}, {}) of CAS entry {}\n", + r.start, + r.end, + self.find_node_by_id(r.id) + .map_or("?".to_string(), |n| n.hash().to_string()) + ) + } else { + "\tHas CAS data but unable to derive origin CAS node\n".to_string() + }; + ret.push_str(&cas_report); + } + if attr.has_file_data() { + let cas_report = + if let Ok(r) = find_ancestor_reconstructor(self, node, NodeDataType::FILE) { + format!( + "\tSubstring [{}, {}) of FILE entry {}\n", + r.start, + r.end, + self.find_node_by_id(r.id) + .map_or("?".to_string(), |n| n.hash().to_string()) + ) + } else { + "\tHas FILE data but unable to derive origin FILE node\n".to_string() + }; + ret.push_str(&cas_report); + } + ret.push('\n'); + if attr.has_file_data() { + let recon_report = if let Ok(res) = self.reconstruct_from_cas(&[node.clone()]) { + format!("CAS Reconstruction: {res:?}") + } else { + "\tUnable to reconstruct from CAS\n".to_string() + }; + ret.push_str(&recon_report); + } + + ret + } + /** + * Checks that Hash->Id and Id->Hash match up + * + * Returns true if the invariants pass, and false otherwise. + */ + fn check_hash_invertibility_invariant(&self) -> bool { + let mut ret = true; + for i in 0..self.get_sequence_number() { + // there is no requirement that IDs be sequential just monotonic + if let Some(node) = self.find_node_by_id(i as MerkleNodeId) { + if let Some(hashtoid) = self.hash_to_id(node.hash()) { + if hashtoid != (i as MerkleNodeId) { + eprintln!( + "Node {node:?} hash_to_id resolves to {hashtoid:?} which should be {i:?}" + ); + ret = false; + } + } else { + eprintln!("Node {node:?} hash_to_id resolves to None which should be {i:?}"); + } + } + } + ret + } + /** + * Checks that children and parent of every node exists. + * + * Returns true if the invariants pass, and false otherwise. + */ + fn check_reachability_invariant(&self) -> bool { + let mut id_exists: HashSet = HashSet::new(); + for i in 0..self.get_sequence_number() { + // there is no requirement that IDs be sequential just monotonic + if self.find_node_by_id(i as MerkleNodeId).is_some() { + id_exists.insert(i); + } + } + let mut ret = true; + for i in 0..self.get_sequence_number() { + if let Some(node) = self.find_node_by_id(i as MerkleNodeId) { + let mut chlensum: usize = 0; + for (ch, len) in node.children().iter() { + if !id_exists.contains(ch) { + eprintln!("Child {ch:?} of Node {node:?} does not exist"); + ret = false; + } + chlensum += len; + } + if !node.children().is_empty() && node.len() != chlensum { + eprintln!("Sum of children length do not sum to node length. {node:?}"); + } + if let Some(attr) = self.node_attributes(i as MerkleNodeId) { + let cas_parent = attr.cas_parent(); + let file_parent = attr.file_parent(); + if cas_parent != 0 && !id_exists.contains(&cas_parent) { + eprintln!( + "CAS Parent in attribute {attr:?} of Node {node:?} does not exist" + ); + ret = false; + } + if file_parent != 0 && !id_exists.contains(&file_parent) { + eprintln!( + "FILE Parent in attribute {attr:?} of Node {node:?} does not exist" + ); + ret = false; + } + } + } + } + ret + } + /** + * Check that File and Cas parents lead to a File or Cas node + */ + fn check_parent_invariant(&self) -> bool { + let mut ret = true; + for i in 0..self.get_sequence_number() { + if let Some(node) = self.find_node_by_id(i as MerkleNodeId) { + if let Some(attr) = self.node_attributes(i as MerkleNodeId) { + if attr.has_cas_data() { + let f = find_ancestor_reconstructor(self, &node, NodeDataType::CAS); + if f.is_err() { + eprintln!( + "Parent invariant broken on node {node:?}. Unable to track to CAS root" + ); + } + ret &= f.is_ok(); + } else if attr.has_file_data() { + let f = find_ancestor_reconstructor(self, &node, NodeDataType::FILE); + ret &= f.is_ok(); + if f.is_err() { + eprintln!( + "Parent invariant broken on node {node:?}. Unable to track to File root" + ); + } + }; + } + } + } + ret + } + + /// if A has B as a parent, then B must have A as a child + fn check_parent_invariant_basic(&self) -> bool { + let mut ret = true; + for i in 0..self.get_sequence_number() { + if let Some(node) = self.find_node_by_id(i as MerkleNodeId) { + let nodeattr = self.node_attributes(node.id()).unwrap(); + let cas_parent = nodeattr.cas_parent(); + if cas_parent > 0 { + if let Some(parent) = self.find_node_by_id(cas_parent) { + if !parent.children().iter().any(|x| x.0 == node.id()) { + eprintln!( + "CAS Parent node of {node:?} has no children. Parent: {parent:?}" + ); + ret = false; + } + } else { + eprintln!("CAS Parent node of {node:?} not found, Attr: {nodeattr:?}"); + ret = false; + } + } + let file_parent = nodeattr.file_parent(); + if file_parent > 0 { + if let Some(parent) = self.find_node_by_id(file_parent) { + if !parent.children().iter().any(|x| x.0 == node.id()) { + eprintln!( + "FILE Parent node of {node:?} has no children. Parent: {parent:?}" + ); + ret = false; + } + } else { + eprintln!("FILE Parent node of {node:?} not found, Attr: {nodeattr:?}"); + ret = false; + } + } + } + } + ret + } + + fn every_root_is_cas_and_file_reachable_invariant(&self) -> bool { + let mut ret = true; + for i in 0..self.get_sequence_number() { + if let Some(node) = self.find_node_by_id(i as MerkleNodeId) { + let nodeattr = self.node_attributes(node.id()).unwrap(); + // we only check roots. i.e. nodes with no parents. + if nodeattr.cas_parent() > 0 || nodeattr.file_parent() > 0 { + continue; + } + // every node must be cas to file or file to cas solvable + let cas_to_file = self.reconstruct_from_file(&[node.clone()]).unwrap(); + let file_to_cas = self.reconstruct_from_cas(&[node.clone()]).unwrap(); + if cas_to_file.is_empty() && file_to_cas.is_empty() { + let attr = self.node_attributes(node.id()).unwrap(); + eprintln!( + "CAS File relation invariant broken for node {node:?}\n\ + \tattr: {attr:?}\n\ + \tcas_to_file: {cas_to_file:?}\n\ + \tfile_to_cas: {file_to_cas:?}\n" + ); + ret = false; + } + // check that the lengths match up for each cas_to_file, + // file_to_cas entry + // + for (h, range) in cas_to_file { + let total_len: usize = range.iter().map(|x| x.end - x.start).sum(); + let n = self.find_node(&h).unwrap(); + if n.len() != total_len { + let attr = self.node_attributes(n.id()).unwrap(); + eprintln!( + "When querying {node:?}, attr {nodeattr:?},\n\ + \tCAS To file length mismatch for node {n:?}. attr: {attr:?}, \n\ + \tranges acquired is {range:?}\n" + ); + ret = false; + } + } + for (h, range) in file_to_cas { + let total_len: usize = range.iter().map(|x| x.end - x.start).sum(); + let n = self.find_node(&h).unwrap(); + if n.len() != total_len { + let attr = self.node_attributes(n.id()).unwrap(); + eprintln!( + "When querying {node:?}, attr {nodeattr:?}, \n\ + \tFile To CAS length mismatch for node {n:?}. \n\ + \tattr: {attr:?}, ranges acquired is {range:?}\n" + ); + ret = false; + } + } + } + } + ret + } + + /// compare the given hash with what I get with a fresh insertion + /// of hunks into a MerkleDB + fn fresh_hash_of_chunks(&self, chunks: &[Chunk]) -> MerkleHash { + let mut db = MerkleMemDB::default(); + let mut staging = db.start_insertion_staging(); + db.add_file(&mut staging, chunks); + let ret = db.finalize(staging); + *ret.hash() + } + + /// compare the CAS hash in the current database, with the hash + /// computed by hashing the set of chunks making up the CAS. + fn validate_db_cas_node(&self, cashash: &MerkleHash) -> bool { + let cas_node = self.find_node(cashash).unwrap(); + let cas_leaves = self.find_all_leaves(&cas_node).unwrap(); + let chunks = cas_leaves + .iter() + .map(|x| Chunk { + hash: *x.hash(), + length: x.len(), + }) + .collect::>(); + let newhash = self.fresh_hash_of_chunks(&chunks); + if newhash != *cashash { + eprintln!( + "CAS Hash Validation Mismatch \n\ + \tExpecting {cashash:?}, got {newhash:?} \n\ + \tChunks are {chunks:?}\n" + ); + } + newhash == *cashash + } + + fn validate_every_cas_node_hash(&self) -> bool { + let mut ret = true; + let mut cashashes: Vec = Vec::new(); + for i in 0..self.get_sequence_number() { + if let Some(node) = self.find_node_by_id(i as MerkleNodeId) { + if let Some(attr) = self.node_attributes(i as MerkleNodeId) { + if attr.is_cas() { + cashashes.push(*node.hash()); + } + } + } + } + for hash in cashashes { + ret &= self.validate_db_cas_node(&hash); + } + ret + } + fn all_invariant_checks(&self) -> bool { + self.db_invariant_checks() + && self.check_reachability_invariant() + && self.check_hash_invertibility_invariant() + && self.every_root_is_cas_and_file_reachable_invariant() + && self.validate_every_cas_node_hash() + && self.check_parent_invariant_basic() + } + + fn only_file_invariant_checks(&self) -> bool { + self.db_invariant_checks() + && self.check_reachability_invariant() + && self.check_hash_invertibility_invariant() + && self.check_parent_invariant_basic() + } +} diff --git a/merkledb/src/merkledb_highlevel_v1.rs b/merkledb/src/merkledb_highlevel_v1.rs new file mode 100644 index 00000000..a2f11eec --- /dev/null +++ b/merkledb/src/merkledb_highlevel_v1.rs @@ -0,0 +1,156 @@ +use crate::chunk_iterator::*; +use crate::constants::*; +use crate::internal_methods::*; +use crate::merkledbbase::*; +use crate::merklenode::*; +use merklehash::MerkleHash; +use std::collections::HashSet; + +/** + * An opaque struct used to stage a batch collection of inserts (like a directory) + */ +pub struct InsertionStaging { + /// all the file roots inserted so far + file_roots: Vec, + /// all the leaves without CAS entries + nodes_without_cas_entry: Vec, + /// sum of length of all the nodes_without_cas_entry + nodes_without_cas_entry_length: usize, + /// hashset of all the unique ids in nodes_without_cas_entry + ids_in_nodes_without_cas_entry: HashSet, + /// all new cas roots created + cas_roots: Vec, + /// We will aim to create CAS nodes of this size + cas_block_size: usize, +} +impl InsertionStaging { + /// Merge with another Insertion Staging struct + pub fn combine(&mut self, mut other: InsertionStaging) { + self.file_roots.append(&mut other.file_roots); + self.nodes_without_cas_entry + .append(&mut other.nodes_without_cas_entry); + self.nodes_without_cas_entry_length += other.nodes_without_cas_entry_length; + } + + /// Modifies the target CAS Block size. + pub fn set_target_cas_block_size(&mut self, cas_block_size: usize) { + self.cas_block_size = cas_block_size; + } + /// constructs all the required cas nodes by grouping them into groups + /// of up to the cas_block_size (default TARGET_CAS_BLOCK_SIZE) + fn build_cas_nodes(&mut self, db: &mut (impl MerkleDBBase + ?Sized), flush: bool) { + if self.nodes_without_cas_entry_length >= self.cas_block_size { + let mut running_sum: usize = 0; + let mut start = 0; + for i in 0..self.nodes_without_cas_entry.len() { + running_sum += self.nodes_without_cas_entry[i].len(); + if running_sum >= self.cas_block_size { + let end = i + 1; + let cas_root = merge( + db, + self.nodes_without_cas_entry[start..end].to_vec(), + false, + true, + ); + self.cas_roots.push(cas_root); + + for j in start..end { + self.ids_in_nodes_without_cas_entry + .remove(&self.nodes_without_cas_entry[j].id()); + } + self.nodes_without_cas_entry_length -= running_sum; + start = end; + running_sum = 0; + } + } + self.nodes_without_cas_entry = self.nodes_without_cas_entry[start..].to_vec(); + } + if flush && !self.nodes_without_cas_entry.is_empty() { + let cas_root = merge( + db, + std::mem::take(&mut self.nodes_without_cas_entry), + false, + true, + ); + self.cas_roots.push(cas_root); + } + } +} + +pub trait MerkleDBHighLevelMethodsV1: MerkleDBBase { + /** creates a new insertion staging object. The basic usage is + * - start_insertion_staging + * - add_file + * - add_file + * - add_file + * - finalize + */ + fn start_insertion_staging(&self) -> InsertionStaging { + InsertionStaging { + file_roots: Vec::new(), + nodes_without_cas_entry: Vec::new(), + nodes_without_cas_entry_length: 0, + ids_in_nodes_without_cas_entry: HashSet::new(), + cas_roots: Vec::new(), + cas_block_size: TARGET_CAS_BLOCK_SIZE, + } + } + + /// Adds a file + fn add_file(&mut self, staging: &mut InsertionStaging, chunk: &[Chunk]) -> MerkleHash { + if chunk.is_empty() { + return MerkleHash::default(); + } + let ch: Vec = chunk + .iter() + .map(|i| node_from_hash(self, &i.hash, i.length)) + .collect(); + + // we filter nodes_without_cas_entry to be unique + // hashset return true if the set does not have it already present + // and false otherwise. So this is a cute compact stable way to + // select only unique entries. + let mut nodes_without_cas_entry: Vec = ch + .iter() + .filter(|x| { + !self + .node_attributes(x.id()) + .unwrap_or_default() + .has_cas_data() + }) + .filter(|x| staging.ids_in_nodes_without_cas_entry.insert(x.id())) + .cloned() + .collect(); + let file_root = merge(self, ch, true, false); + staging.nodes_without_cas_entry_length += nodes_without_cas_entry + .iter() + .map(|x| x.len()) + .sum::(); + staging + .nodes_without_cas_entry + .append(&mut nodes_without_cas_entry); + + staging.build_cas_nodes(self, false); + let hash = *file_root.hash(); + staging.file_roots.push(file_root); + hash + } + + /// Finalizes the insertion. + fn finalize(&mut self, mut staging: InsertionStaging) -> MerkleNode { + if staging.file_roots.is_empty() { + return MerkleNode::default(); + } + // sort by length then hash + let comparator = |a: &MerkleNode, b: &MerkleNode| { + if a.len() != b.len() { + a.len().partial_cmp(&b.len()).unwrap() + } else { + a.hash().partial_cmp(b.hash()).unwrap() + } + }; + staging.file_roots.sort_unstable_by(comparator); + staging.build_cas_nodes(self, true); + merge(self, staging.file_roots, false, false) + } +} diff --git a/merkledb/src/merkledb_highlevel_v2.rs b/merkledb/src/merkledb_highlevel_v2.rs new file mode 100644 index 00000000..a1521730 --- /dev/null +++ b/merkledb/src/merkledb_highlevel_v2.rs @@ -0,0 +1,28 @@ +use crate::chunk_iterator::*; +use crate::internal_methods::*; +use crate::merkledbbase::*; +use crate::merklenode::*; + +pub trait MerkleDBHighLevelMethodsV2: MerkleDBBase { + /// Adds a chunk to the database + /// Returns (node, new_node) + /// new_node = true if this is a new chunk that has never been seen + fn add_chunk(&mut self, chunk: &Chunk) -> (MerkleNode, bool) { + self.maybe_add_node(&chunk.hash, chunk.length, Vec::new()) + } + + /// Merges a collection of chunks to form a file node + fn merge_to_file(&mut self, nodes: &[MerkleNode]) -> MerkleNode { + merge(self, nodes.to_owned(), true, false) + } + + /// Merges a collection of chunks to form a CAS node + fn merge_to_cas(&mut self, nodes: &[MerkleNode]) -> MerkleNode { + merge(self, nodes.to_owned(), false, true) + } + + /// Merges a collection of chunks where the root is untagged + fn merge(&mut self, nodes: &[MerkleNode]) -> MerkleNode { + merge(self, nodes.to_owned(), false, false) + } +} diff --git a/merkledb/src/merkledb_ingestion_v1.rs b/merkledb/src/merkledb_ingestion_v1.rs new file mode 100644 index 00000000..8408341e --- /dev/null +++ b/merkledb/src/merkledb_ingestion_v1.rs @@ -0,0 +1,120 @@ +use crate::chunk_iterator::*; +use crate::constants::*; +use crate::error::Result; +use crate::merkledb_highlevel_v1::*; +use crate::merklenode::*; +use rayon::prelude::*; +use std::fs::File; +use std::io::Write; +use std::io::{BufReader, BufWriter}; +use std::path::PathBuf; +use std::sync::mpsc::sync_channel; +use std::thread; +use std::time::{Instant, SystemTime}; +use tracing::{debug, info}; +use walkdir::*; + +pub trait MerkleDBIngestionMethodsV1: MerkleDBHighLevelMethodsV1 { + /** + * Ingests a single file into the MerkleTree returning a MerkleNode + * on success. + */ + fn ingest_file(&mut self, input: PathBuf) -> Result { + debug!("Ingesting file {:?}", input); + let input_file = File::open(&input)?; + let mut buf_reader = BufReader::new(input_file); + let chunks = low_variance_chunk_target( + &mut buf_reader, + TARGET_CDC_CHUNK_SIZE, + N_LOW_VARIANCE_CDC_CHUNKERS, + ); + let mut staging = self.start_insertion_staging(); + self.add_file(&mut staging, &chunks); + let ret = self.finalize(staging); + self.flush()?; + Ok(ret) + } + + /** + * Ingests an entire directory into the MerkleTree returning a MerkleNode + * on success. + */ + fn ingest_dir( + &mut self, + input: PathBuf, + after_file_date: Option, + metadata_output_file: &mut BufWriter, + ) -> Option { + let now = Instant::now(); + debug!("Ingesting dir {:?}", input); + let mut staging = self.start_insertion_staging(); + // Walkdir usage copied from https://docs.rs/walkdir/2.3.2/walkdir/ + // TODO: magic constant here. Probably change to something like k * nCPUs + let (tx, rx) = sync_channel::<(Vec, PathBuf)>(64); + thread::spawn(move || { + WalkDir::new(&input) + .follow_links(false) // do not follow symlinks + .into_iter() + .filter_entry(|e| !e.path_is_symlink()) // do not read symlinks + .filter_map(|e| e.ok()) + .par_bridge() // we use par_bridge() instead of + // collecting then par_iter since when + // there are millions of files, collecting + // will be terrifyingly memory intensive. + .filter(|p| p.file_type().is_file()) + .filter(|p| { + if let Some(after) = after_file_date { + if let Ok(Ok(modified)) = p.metadata().map(|m| m.modified()) { + modified >= after + } else { + true + } + } else { + true + } + }) + .map(|entry| { + let path = entry.into_path(); + + let input_file = match File::open(&path) { + Err(why) => panic!("Cannot open {input:?}: {why}"), + Ok(file) => file, + }; + let mut buf_reader = BufReader::new(input_file); + let chunks = low_variance_chunk_target( + &mut buf_reader, + TARGET_CDC_CHUNK_SIZE, + N_LOW_VARIANCE_CDC_CHUNKERS, + ); + (chunks, path) + }) + .for_each(|x| tx.send(x).unwrap()); + }); + while let Ok((chunks, path)) = rx.recv() { + if !chunks.is_empty() { + let hash = self.add_file(&mut staging, &chunks); + writeln!(metadata_output_file, "{hash:x} {path:?}").unwrap(); + } else { + writeln!( + metadata_output_file, + "{:x} {:?}", + MerkleHash::default(), + path + ) + .unwrap(); + } + } + let dirroot = self.finalize(staging); + self.flush().unwrap(); + + let chunk_time = now.elapsed().as_secs_f64(); + info!("Completed chunking in {:.2?}", now.elapsed()); + + let total_length = dirroot.len() as f64; + info!( + "Chunking speed: {} MB/s", + total_length / 1024.0 / 1024.0 / chunk_time + ); + Some(dirroot) + } +} diff --git a/merkledb/src/merkledb_reconstruction.rs b/merkledb/src/merkledb_reconstruction.rs new file mode 100644 index 00000000..3c19aecc --- /dev/null +++ b/merkledb/src/merkledb_reconstruction.rs @@ -0,0 +1,177 @@ +use crate::error::*; +use crate::internal_methods::*; +use crate::merkledbbase::MerkleDBBase; +use crate::merklenode::*; +use merklehash::MerkleHash; +/// +/// Describes methods for performing FILE2CAS or CAS2FILE reconstructions from a MerkleDB +/// +pub trait MerkleDBReconstruction: MerkleDBBase { + /** + * Look for all CAS's which are dependent exclusively on descendents of + * this node, and returns the concatenation of FILE ranges needed to + * reconstruct each CAS node. Only traverses nodes + * which were inserted after after_sequence. + * + * Details: + * There are situations where + * the result may be incomplete and hence is misleading. + * Say I have the following graph: + * + * F1 F2 [CAS1] + * \ \ / / + * \ \ / / + * \ X / + * \ / \ / + * N M + * + * If I query file_to_cas(F1), I will get the correct range information + * (how to construct F1). + * + * But if I query cas_to_file(F1) this gets a bit strange: + * The initial search will find all all the CAS nodes which are dependent + * on F1 and will find CAS1. However since F1 is queried, it will only find + * the F1 relation and the resultant range will be incomplete since CAS1 + * is really produced by blocks from F1 and F2. This will then be filtered + * out and so cas_to_file(F1) will not return CAS1. + * + * Really if you want to find all CASes needed for a given file, + * use find_all_file_to_cas(). + */ + fn reconstruct_from_file( + &self, + root: &[MerkleNode], + ) -> Result)>> { + let ret = find_reconstruction(self, root, NodeDataType::FILE)?; + Ok(ret + .into_iter() + .filter(|(h, range)| { + let n = self.find_node(h).unwrap(); + let total_len: usize = range.iter().map(|x| x.end - x.start).sum(); + n.len() == total_len + }) + .collect()) + } + + /** + * Look for all FILE which are only dependent exclusively on + * descendents of this node + * and returns the concatenation of CAS ranges + * needed to reconstruct each FILE node. + * Only traverses nodes which were inserted after after_sequence. + */ + fn reconstruct_from_cas( + &self, + root: &[MerkleNode], + ) -> Result)>> { + let ret = find_reconstruction(self, root, NodeDataType::CAS)?; + // we need to filter this + Ok(ret + .into_iter() + .filter(|(h, range)| { + let n = self.find_node(h).unwrap(); + let total_len: usize = range.iter().map(|x| x.end - x.start).sum(); + n.len() == total_len + }) + .collect()) + } + + fn find_all_leaves(&self, root: &MerkleNode) -> Result> { + Ok( + find_descendent_reconstructor(self, root, |n, _| n.children().is_empty())? + .root_ranges_in_descendent + .iter() + .map(|x| self.find_node_by_id(x.0).unwrap()) + .collect(), + ) + } + + /** + * From a given set of cas roots, return a list of data hashes and ranges. + * + * Each ObjectRange in the return value corresponds to the hash of a file + * node and the of bytes in that file that equals one chunk. * + */ + fn find_cas_node_file_data(&self, cas_root: &MerkleNode) -> Result> { + // First, we need to get all the base nodes for each of the leaves here. The return + // value of find_reconstruction has the ordered list of base data to be used. + let file_node_ranges = find_reconstruction(self, &[cas_root.clone()], NodeDataType::FILE)?; + + // To get all the proper chunk boundaries, we need to use all the leaves + // to determine the ranges of these nodes. + + let leaves: Vec = + find_descendent_reconstructor(self, cas_root, |n, _| n.children().is_empty())? + .root_ranges_in_descendent + .iter() + .map(|x| self.find_node_by_id(x.0).unwrap()) + .collect(); + + let mut ret: Vec = Vec::new(); + + assert!(!file_node_ranges.is_empty()); + + let mut fnr_idx: usize = 0; + let mut fnr_range_idx: usize = 0; + + let mut in_node_start_idx: usize = 0; + + for i in 0..leaves.len() { + assert_ne!(fnr_idx, file_node_ranges.len()); + assert_ne!(fnr_range_idx, file_node_ranges[fnr_idx].1.len()); + + let in_node_end_idx = in_node_start_idx + leaves[i].len(); + let cur_src_range = &(file_node_ranges[fnr_idx].1[fnr_range_idx]); + + ret.push(ObjectRange { + hash: file_node_ranges[fnr_idx].0, + start: cur_src_range.start + in_node_start_idx, + end: cur_src_range.start + in_node_end_idx, + }); + + // Advance the pointer in the subnodes + if in_node_end_idx == cur_src_range.end - cur_src_range.start { + fnr_range_idx += 1; + in_node_start_idx = 0; + + if fnr_range_idx == file_node_ranges[fnr_idx].1.len() { + fnr_idx += 1; + fnr_range_idx = 0; + + if fnr_idx == file_node_ranges.len() { + // If this is true, then we should exactly be on the last loop. + assert_eq!(i, leaves.len() - 1); + } + } + } else { + // The boundaries should match up exactly + assert!(in_node_end_idx < cur_src_range.end - cur_src_range.start); + } + } + + assert_eq!(leaves.len(), ret.len()); + Ok(ret) + } + + fn find_all_descendents_of_type( + &self, + root: MerkleNode, + nodetype: NodeDataType, + ) -> Result> { + if self + .node_attributes(root.id()) + .unwrap_or_default() + .is_type(nodetype) + { + Ok(vec![root]) + } else { + Ok( + find_descendent_reconstructor(self, &root, |_, attr| attr.is_type(nodetype))? + .root_ranges_in_descendent + .iter() + .map(|x| self.find_node_by_id(x.0).unwrap()) + .collect(), + ) + } + } +} diff --git a/merkledb/src/merkledbbase.rs b/merkledb/src/merkledbbase.rs new file mode 100644 index 00000000..27f35d73 --- /dev/null +++ b/merkledb/src/merkledbbase.rs @@ -0,0 +1,157 @@ +use super::error::*; +use super::merklenode::*; + +/** +Basic Concepts +============== +The Merkle Tree (DAG) is a general DAG (Directed Acyclic Graph) of hashes and +how they relate to each other. Basically, each node represents a +a "byte string" and the DAG describes known substring relationships with each other. + +For instance, take an arbitrary node in the graph, say a root node with hash +"AAA111". What this really means is that there is some byte +string that hashes to AAA111. (For a very particular hash function which we +will describe later). + +Furthermore, if the node AAA111 has 2 children "BBB222" and "CCC333", then +the concatenation of the byte strings which hash to "BBBB222" and "CCC333" +respectively *MUST* result in the byte string that hashed to "AAA111". + +Formally, the following relationship is the primary invariant: + + +For a node $n$ with children $\Gamma(n)$, and a string $S_n$ s.t. $hash(S_n) = n$. +and $\forall c\in\Gamma(n) hash(S_c) = c$, then + + $S_n = \bigdot_c\in\Gamma(n) S_c$ + +where \bigdot represents concatenation. Note that the set of children \Gamma(n) +is ordered. + + +This relationship means that if we see a new node "FFF666" with children +["CCC333", and "ABABAB"], we know that the end of the string for "AAA111" must +matches the start of the string for "FFF666". + +Note that the substring relationship only sufficient but not necessary. That is +if we have two strings with overlapping substrings, it is not guaranteed that +their descendent hashes will overlap at all as this is dependent on the tree +construction procedure. + +Construction +------------ +The key behind how the MerkleDAG works is that the hash function is exactly +the same as the tree construction procedure. That is Parent nodes are constructed +by hashing the hashes of its children. Thus as long as the children merging +algorithm and the leaf creation algorithm is deterministic, the same hash value +and most importantly, the entire tree structure will be deterministic for the +same inputs. + +Metadata +-------- +The MerkleDAG alone only provides information about how strings relate to each +other, however, in practice we need a lot more metadata to accomplish a storage +system. + +1: We need to know which nodes correspond to actual files and how to +reconstruct them. +2. We need to know what to actually store. +3. Given a new file, we need to know how it relates with existing nodes and what +new nodes need to stored. + +To make consistent some terminology. + +Content Addressed Storage (CAS): A repository of hash->bytes + +Chunks/Leaves: These are leaf nodes in the MerkleDAG and are produced when the Content +Defined Chunking procedure is run on a file. + +Root: This are nodes in the MerkleDAG with no parent. + +File Root: This are nodes in the MerkleDAG which correspond to actual user files. +*/ + +/** + * This defines the basic minimal database interface for a MerkleDB. + * + * The MerkleDB stores two classes of information: + * - MerkleNodes + * - MerkleNodeAttributes + * + * MerkleNodes are entirely immutable, and once created, cannot be modified. + * On creation the MerkleNode is assigned a monotonic 64-bit ID. + */ +pub trait MerkleDBBase { + /// + /// Inserts a new node into the MerkleDB if a node with the hash does not + /// already exists; returns the existing node from the DB otherwise. + /// + /// This implicitly creates an empty attribute for the node as well. + /// That is, node_attribute(node.id) for a newly inserted node must succeed. + /// + /// Returns (node, new_node) + /// new_node = true if this is a new node. and false if the node + /// already exists. + /// + /// Note that changes may be commited until a flush() is issued. + /// + fn maybe_add_node( + &mut self, + hash: &MerkleHash, + len: usize, + children: Vec<(MerkleNodeId, usize)>, + ) -> (MerkleNode, bool); + + /// + /// Inserts a new node into the MerkleDB if a node with the hash does not + /// already exists; returns the existing node from the DB otherwise. + /// + /// This implicitly creates an empty attribute for the node as well. + /// That is, node_attribute(node.id) for a newly inserted node must succeed. + /// + /// Note that changes may be commited until a flush() is issued. + /// + fn add_node( + &mut self, + hash: &MerkleHash, + len: usize, + children: Vec<(MerkleNodeId, usize)>, + ) -> MerkleNode { + self.maybe_add_node(hash, len, children).0 + } + + /// Finds a node by its ID + fn find_node_by_id(&self, h: MerkleNodeId) -> Option; + + /// Converts a hash to an ID + fn hash_to_id(&self, h: &MerkleHash) -> Option; + + /// Finds a node by its hash + fn find_node(&self, h: &MerkleHash) -> Option; + + /// Finds the node attributes for a given node ID + fn node_attributes(&self, h: MerkleNodeId) -> Option; + + /// Updates the node attributes for a given node ID + /// Note that changes may be commited until a flush() is issued. + fn set_node_attributes(&mut self, h: MerkleNodeId, attr: &MerkleNodeAttributes) -> Option<()>; + + /// Flushes all changes. + fn flush(&mut self) -> Result<()>; + + /** Returns a monotonic ID which can be used to identify the current + * time state of the database. This is essentially the last node ID + * inserted. + */ + fn get_sequence_number(&self) -> MerkleNodeId; + + /** + * Database specific invariant checks which cannot be checked by graph + * traversal. + * Returns true if the invariants pass, and false otherwise. + */ + fn db_invariant_checks(&self) -> bool; + + /// Sets if we autosync on drop. Defaults to true + fn autosync_on_drop(&mut self, autosync: bool); +} diff --git a/merkledb/src/merkledbv1.rs b/merkledb/src/merkledbv1.rs new file mode 100644 index 00000000..b74f5e84 --- /dev/null +++ b/merkledb/src/merkledbv1.rs @@ -0,0 +1,15 @@ +use crate::merklememdb::MerkleMemDB; + +use crate::merkledb_debug::*; +use crate::merkledb_highlevel_v1::*; +use crate::merkledb_ingestion_v1::*; +use crate::merkledbbase::MerkleDBBase; + +pub trait MerkleDBV1: + MerkleDBBase + MerkleDBHighLevelMethodsV1 + MerkleDBIngestionMethodsV1 + MerkleDBDebugMethods +{ +} + +impl MerkleDBHighLevelMethodsV1 for MerkleMemDB {} +impl MerkleDBIngestionMethodsV1 for MerkleMemDB {} +impl MerkleDBV1 for MerkleMemDB {} diff --git a/merkledb/src/merkledbv2.rs b/merkledb/src/merkledbv2.rs new file mode 100644 index 00000000..de6d6f88 --- /dev/null +++ b/merkledb/src/merkledbv2.rs @@ -0,0 +1,9 @@ +use crate::merklememdb::MerkleMemDB; + +use crate::merkledb_debug::*; +use crate::merkledb_highlevel_v2::*; +use crate::merkledbbase::MerkleDBBase; + +pub trait MerkleDBV2: MerkleDBBase + MerkleDBHighLevelMethodsV2 + MerkleDBDebugMethods {} + +impl MerkleDBHighLevelMethodsV2 for MerkleMemDB {} diff --git a/merkledb/src/merklememdb.rs b/merkledb/src/merklememdb.rs new file mode 100644 index 00000000..4cdb9a9b --- /dev/null +++ b/merkledb/src/merklememdb.rs @@ -0,0 +1,439 @@ +use crate::error::*; +use crate::merklenode::*; + +use crate::internal_methods::assign_all_parents; +use crate::merkledb_debug::*; +use crate::merkledb_reconstruction::*; +use crate::merkledbbase::*; + +use bincode::Options; +use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::fs::File; +use std::io::{BufReader, BufWriter, Read, Write}; +use std::path::{Path, PathBuf}; +use tracing::{debug, error}; + +/** + * Since we want the graph to use small node ID values for connectivity, + * The database is stored as two parts a NodeDB: which is ID->Node + * and a hashdb which is Hash->ID + */ + +#[derive(Serialize, Deserialize, Clone)] +pub struct MerkleMemDB { + /// Stores NodeId->MerkleNode + nodedb: Vec, + /// Stores NodeId->MerkleNodeAttributes + attributedb: Vec, + /// stores NodeHash->NodeId + #[serde(skip)] + hashdb: FxHashMap, + #[serde(skip)] + path: PathBuf, + #[serde(skip)] + changed: bool, + /// if set (defaults to true) will sync the DB on drop + #[serde(skip)] + autosync: bool, +} + +impl Default for MerkleMemDB { + /** + * Create an empty in-memory only DB + */ + fn default() -> MerkleMemDB { + /* + * We create a node 0 containing the hash of all 0s used to denote + * the empty string. This is both a CAS and a FILE node for simplicity, + * to allow for both the empty string FILE and the empty string CAS + * case without requiring much explicit client-side handling. + */ + let mut node_0_attributes = MerkleNodeAttributes::default(); + node_0_attributes.set_file(); + node_0_attributes.set_cas(); + let mut ret = MerkleMemDB { + nodedb: vec![MerkleNode::default(); 1], // first real node starts at index 1 + attributedb: vec![node_0_attributes; 1], // first real node starts at index 1 + hashdb: FxHashMap::default(), + path: PathBuf::default(), + changed: false, + autosync: true, + }; + // for the 0 node. + ret.hashdb.insert(MerkleHash::default(), 0); + ret + } +} + +impl MerkleMemDB { + /** + * Opens the database at a given path, creating it if it does not already + * exist. Two databases are created. one at [PATH]/node to store the + * node DB, and another at [PATH]/hash to store the hash DB. + */ + pub fn open>(path: T) -> Result { + let f = File::open(path.as_ref()); + debug!("Opening DB {:?}", path.as_ref()); + #[allow(clippy::field_reassign_with_default)] + if let Ok(f) = f { + let buf = BufReader::with_capacity(1024 * 1024, f); // 1MB + let mut memdb = MerkleMemDB::open_reader(buf)?; + memdb.path = path.as_ref().into(); + Ok(memdb) + } else { + let mut memdb = MerkleMemDB::default(); + memdb.path = path.as_ref().into(); + memdb.changed = true; + Ok(memdb) + } + } + pub fn open_reader(r: T) -> Result { + debug!("Opening DB from reader"); + #[allow(clippy::field_reassign_with_default)] + let options = bincode::DefaultOptions::new().with_fixint_encoding(); + let mut memdb: MerkleMemDB = options.deserialize_from(r)?; + memdb.rebuild_hash(); + memdb.path = PathBuf::default(); + Ok(memdb) + } + + pub fn write_into(&mut self, writer: T) { + let options = bincode::DefaultOptions::new().with_fixint_encoding(); + options.serialize_into(writer, &self).unwrap(); + } + pub fn rebuild_hash(&mut self) { + for n in self.nodedb.iter() { + self.hashdb.insert(*n.hash(), n.id()); + } + } + pub fn assign_from(&mut self, other: &MerkleMemDB) { + self.nodedb.clone_from(&other.nodedb); + self.attributedb.clone_from(&other.attributedb); + self.hashdb.clone_from(&other.hashdb); + } + + /// Merges the self with another db + /// union_with can be called repeatedly, but union_finalize has + /// to be called at the end + pub fn union_with(&mut self, other: &MerkleMemDB) { + let mut old_id_to_new_id: FxHashMap = FxHashMap::default(); + let mut cur_insert_id = self.nodedb.len() as MerkleNodeId; + // look at all the new keys and determine the IDs we are assigning them to + for n in &other.nodedb { + if !self.hashdb.contains_key(n.hash()) { + old_id_to_new_id.insert(n.id(), cur_insert_id); + cur_insert_id += 1; + } else { + let mynode = self.find_node(n.hash()).unwrap(); + old_id_to_new_id.insert(n.id(), mynode.id()); + } + } + for n in &other.nodedb { + if !self.hashdb.contains_key(n.hash()) { + // convert children to hash + // then to local ID. + let ch = n + .children() + .iter() + .map(|x| (*old_id_to_new_id.get(&(x.0 as MerkleNodeId)).unwrap(), x.1)) + .collect(); + self.add_node(n.hash(), n.len(), ch); + } else { + // It is possible that I have a partial tree. + // i.e. I do not think a node has children, + // when it actually does and so I do actually need to update + // my list of children. + let id = *old_id_to_new_id.get(&n.id()).unwrap(); + if !n.children().is_empty() && self.nodedb[id as usize].children().is_empty() { + let ch = n + .children() + .iter() + .map(|x| (*old_id_to_new_id.get(&(x.0 as MerkleNodeId)).unwrap(), x.1)) + .collect(); + self.nodedb[id as usize].set_children(ch); + } + } + } + + for n in &other.nodedb { + let newid = *old_id_to_new_id.get(&n.id()).unwrap(); + let attr = other.attributedb[n.id() as usize]; + let selfattr = &mut self.attributedb[newid as usize]; + selfattr.merge_attributes(&attr); + } + } + /// This function must be called after merges + /// We recompute all the parent relationships + pub fn union_finalize(&mut self) -> Result<()> { + if self.nodedb.len() != self.attributedb.len() { + return Err(MerkleDBError::GraphInvariantError( + "NodeDB and AttributeDB length mismatch".into(), + )); + } + for nodetype in [NodeDataType::CAS, NodeDataType::FILE] { + let nodelist: Vec = self + .nodedb + .iter() + .filter_map(|x| { + if self.attributedb[x.id() as usize].is_type(nodetype) { + Some(x.id()) + } else { + None + } + }) + .collect(); + assign_all_parents(self, nodelist, nodetype); + } + + Ok(()) + } + + /// Computes the set difference between a and b and add it to self. + /// effectively self = a - b + pub fn difference(&mut self, a: &MerkleMemDB, b: &MerkleMemDB) { + let mut a_id_to_new_id: FxHashMap = FxHashMap::default(); + // we loop through a looking for it in b + for n in &a.nodedb { + let a_attr = a.attributedb[n.id() as usize]; + // if b does not contain this node + // OR b's node and a's node have different attributes, + // Then I need to add. + if !b.hashdb.contains_key(n.hash()) + || !b.attributedb[b.hashdb[n.hash()] as usize].type_equal(&a_attr) + { + // b does not contain this node. so this is part + // of the set difference. All the nodes in a - b + // must be *complete* (i.e. have children information) + + // look for it in self + if let Some(selfid) = self.hashdb.get(n.hash()) { + // self already has it, so skip. Cache the lookup + a_id_to_new_id.insert(n.id(), *selfid); + } else { + // ok we need to do an insert. + let newn = self.add_node(n.hash(), n.len(), Vec::new()); + a_id_to_new_id.insert(n.id(), newn.id()); + } + // loop through n's children and make sure they exist, + // inserting an empty children list if they do not. + for ch in n.children() { + let chnode = a.find_node_by_id(ch.0).unwrap(); + let newnode = self.add_node(chnode.hash(), chnode.len(), Vec::new()); + a_id_to_new_id.insert(ch.0, newnode.id()); + } + } + } + // loop through one more time updating the children information + // and also updating the parent information + for n in &a.nodedb { + if !b.hashdb.contains_key(n.hash()) { + // this is an all new node. we have to create all the children + // information + let id = *a_id_to_new_id.get(&n.id()).unwrap(); + if !n.children().is_empty() && self.nodedb[id as usize].children().is_empty() { + let newch = n + .children() + .iter() + .map(|x| (*a_id_to_new_id.get(&(x.0 as MerkleNodeId)).unwrap(), x.1)) + .collect(); + self.nodedb[id as usize].set_children(newch); + } + } + // we carry all the attribute data always + if let Some(id) = a_id_to_new_id.get(&n.id()) { + let attr = a.attributedb[n.id() as usize]; + let selfattr = &mut self.attributedb[*id as usize]; + if attr.is_file() { + selfattr.set_file(); + } + if attr.is_cas() { + selfattr.set_cas(); + } + } + } + } + pub fn print_node(&self, n: &MerkleNode) -> String { + // make a list of children hashes , substituting a "?" for every + // children that cannot be found + let chlist: Vec<_> = n + .children() + .iter() + .map(|id| { + self.find_node_by_id(id.0) + .map_or("?".to_string(), |node| node.hash().to_string()) + }) + .collect(); + + // print the attribute string substituting a "?" if the attribute cannot + // be found + let attr_string = self + .attributedb + .get(n.id() as usize) + .map_or("?".to_string(), |attr| format!("{attr:?}")); + format!( + "{}: len:{} children:{:?} attr:{:?}", + n.hash(), + n.len(), + chlist, + attr_string + ) + } + pub fn get_path(&self) -> &Path { + &self.path + } + + pub fn node_iterator(&self) -> impl Iterator { + self.nodedb.iter() + } + + pub fn attr_iterator(&self) -> impl Iterator { + self.attributedb.iter() + } + + pub fn is_empty(&self) -> bool { + // the default db has first real node starts at index 1 + self.nodedb.len() == 1 + } +} + +impl Drop for MerkleMemDB { + fn drop(&mut self) { + if self.autosync { + self.flush().unwrap(); + } + } +} +impl Debug for MerkleMemDB { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + for n in &self.nodedb { + writeln!(f, "{}", self.print_node(n))?; + } + Ok(()) + } +} + +// Direct database interaction routines +impl MerkleDBBase for MerkleMemDB { + /** + * Creates a node with the given specification, + * returning the MerkleNode object. + * + * This should be used anytime + * a new MerkleNode object is needed since the MerkleNode also stores its + * current ID, and that cannot be derived without actually performing + * a database insertion (and incrementing next_node_id). + * The hashdb index also needs to be updated as well. + */ + fn maybe_add_node( + &mut self, + hash: &MerkleHash, + len: usize, + children: Vec<(MerkleNodeId, usize)>, + ) -> (MerkleNode, bool) { + if let Some(node) = self.find_node(hash) { + (node, false) + } else { + self.changed = true; + let id = self.nodedb.len() as MerkleNodeId; + let node = MerkleNode::new(id, *hash, len, children); + self.hashdb.insert(*hash, id); + self.nodedb.push(node.clone()); + self.attributedb.push(MerkleNodeAttributes::default()); + (node, true) + } + } + + /** + * Find a node by ID, returning None if such a node does not exist. + * Panics if the DB cannot be read. + */ + fn find_node_by_id(&self, h: MerkleNodeId) -> Option { + if (h as usize) < self.nodedb.len() { + let ret = &self.nodedb[h as usize]; + Some(ret.clone()) + } else { + None + } + } + /** + * Converts a Hash to an ID returning None if such a hash does not exist. + * Panics if the DB cannot be read. + */ + fn hash_to_id(&self, h: &MerkleHash) -> Option { + self.hashdb.get(h).copied() + } + + /** + * Find a node by Hash, returning None if such a node does not exist. + * Panics if the DB cannot be read. + */ + fn find_node(&self, h: &MerkleHash) -> Option { + self.hash_to_id(h).and_then(|x| self.find_node_by_id(x)) + } + fn node_attributes(&self, h: MerkleNodeId) -> Option { + self.attributedb.get(h as usize).cloned() + } + + fn set_node_attributes(&mut self, h: MerkleNodeId, attr: &MerkleNodeAttributes) -> Option<()> { + let index = h as usize; + if index < self.attributedb.len() { + self.changed = true; + self.attributedb[index] = *attr; + Some(()) + } else { + None + } + } + fn flush(&mut self) -> Result<()> { + if self.changed && !self.path.as_os_str().is_empty() { + use std::io::{Error, ErrorKind}; + let dbpath = self.path.parent().ok_or_else(|| { + Error::new( + ErrorKind::InvalidInput, + format!( + "Unable to find MerkleDB output parent path from {:?}", + self.path + ), + ) + })?; + // we prefix with "[PID]." for now. We should be able to do a cleanup + // in the future. + let tempfile = tempfile::Builder::new() + .prefix(&format!("{}.", std::process::id())) + .suffix(".db") + .tempfile_in(dbpath)?; + debug!("Flushing DB to {:?} via {:?}", self.path, tempfile.path()); + let f = BufWriter::new(&tempfile); + self.write_into(f); + self.changed = false; + // e is a PersistError. e.error is an IoError + tempfile.persist(&self.path).map_err(|e| e.error)?; + } + Ok(()) + } + fn get_sequence_number(&self) -> MerkleNodeId { + self.nodedb.len() as MerkleNodeId + } + + fn db_invariant_checks(&self) -> bool { + let ret = self.hashdb.len() == self.attributedb.len() + && self.attributedb.len() == self.nodedb.len(); + if !ret { + error!( + "DB length mismatch: HashDB len {} , attributeDB len {} nodedb len {}", + self.hashdb.len(), + self.attributedb.len(), + self.nodedb.len() + ); + } + ret + } + fn autosync_on_drop(&mut self, autosync: bool) { + self.autosync = autosync; + } +} + +impl MerkleDBReconstruction for MerkleMemDB {} +impl MerkleDBDebugMethods for MerkleMemDB {} diff --git a/merkledb/src/merklenode.rs b/merkledb/src/merklenode.rs new file mode 100644 index 00000000..799fb91d --- /dev/null +++ b/merkledb/src/merklenode.rs @@ -0,0 +1,371 @@ +use bitflags::bitflags; +use merklehash::*; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::convert::TryInto; +use std::fmt::Write; + +pub use merklehash::MerkleHash; +/// A more compact representation of nodes in the MerkleTree. +/// The ID is a counter that begins at 1. 0 is not a valid ID. +pub type MerkleNodeId = u64; + +/// Node parents can be unassigned in which case 0 is used to identify that case. +/// 0 is not a valid node ID. +pub const ID_UNASSIGNED: MerkleNodeId = 0; + +/** + * Nodes can have one or more sources of data. + * Either from a FILE, CAS or both. This enumeration is used for several + * NodeAttribute accessors. + */ +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum NodeDataType { + CAS = 0, + FILE = 1, +} +bitflags! { + + /** + * Nodes can have one or more sources of data. + * Either from a FILE, CAS or both. DataTypeBitfield provides + * a bitfield type for use internally. + */ + #[derive(Serialize, Deserialize, Default)] + struct DataTypeBitfield: u8 { + /// This node contains a Cas entry + const CAS = 0x1_u8; + /// This node contains a File entry + const FILE = 0x2_u8; + } +} + +/**************************************************************************/ +/* */ +/* MerkleNode */ +/* */ +/**************************************************************************/ + +/** + * Represents an immutable node in a MerkleDB. This node once constructed + * and stored is completely immutable. + */ +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MerkleNode { + /// The Id of the current node. + id: MerkleNodeId, + /// The Merkle Hash of the current node. We don't serialize this + hash: MerkleHash, + /// The length of the bytes stored here + len: usize, + /// The IDs of the children + children: Vec<(MerkleNodeId, usize)>, +} + +impl Default for MerkleNode { + fn default() -> MerkleNode { + MerkleNode { + id: ID_UNASSIGNED, + hash: MerkleHash::default(), + len: 0, + children: Vec::new(), + } + } +} + +impl MerkleNode { + /// Gets the id of this node + pub fn id(&self) -> MerkleNodeId { + self.id + } + /// Gets the hash of this node + pub fn hash(&self) -> &MerkleHash { + &self.hash + } + /// Gets the length of data this node represents + pub fn len(&self) -> usize { + self.len + } + /// Whether the node contains empty data + pub fn is_empty(&self) -> bool { + self.len == 0 + } + /// Gets the list of IDs of the children and their lengths + pub fn children(&self) -> &Vec<(MerkleNodeId, usize)> { + &self.children + } + /// Updates the list of children. + pub fn set_children(&mut self, ch: Vec<(MerkleNodeId, usize)>) { + self.children = ch; + } + /// Constructs a new node + pub fn new( + id: MerkleNodeId, + hash: MerkleHash, + len: usize, + children: Vec<(MerkleNodeId, usize)>, + ) -> MerkleNode { + MerkleNode { + id, + hash, + len, + children, + } + } +} + +thread_local! { +static HASH_NODE_SEQUENCE_BUFFER: RefCell = + RefCell::new(String::with_capacity(1024)); +} + +/** + * Hashing method used to derive a parent node from child node hashes. + * + * We just print out the string + * + * ```ignore + * [child hash 1] : [child len 1] + * [child hash 2] : [child len 2] + * [child hash 3] : [child len 2] + * ``` + * + * With a "\n" after every line. And hash that. + * + * For performance, this function reuses an internal thread-local buffer. + */ +pub fn hash_node_sequence(hash: &[MerkleNode]) -> MerkleHash { + HASH_NODE_SEQUENCE_BUFFER.with(|buffer| { + let mut buf = buffer.borrow_mut(); + buf.clear(); + for node in hash.iter() { + writeln!(buf, "{:x} : {}", node.hash(), node.len()).unwrap(); + } + compute_internal_node_hash(buf.as_bytes()) + }) +} + +/**************************************************************************/ +/* */ +/* MerkleNodeAttributes */ +/* */ +/**************************************************************************/ + +/** + * Each node may have additional attributes to identify where we can go + * to find the value of a node. For instance, the node may by itself reference + * an entry in CAS storage, or a File. This can be checked by inspecting the + * attributes bitfield. Alternatively, it may be a substring / descendent of + * a File, and the originating File can be found by following the parent[File] + * link. + */ +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] +pub struct MerkleNodeAttributes { + /** + * If parent[CAS] is set, the value of this node can be found by + * traversing to parent[CAS]. parent[CAS] must have this node as a child. + */ + parent: [MerkleNodeId; 2], + /** + * If attributes[CAS] is set, then the value of this node can be found + * by querying CAS storage. Similarly, if attributes[FILE] is set + * then there exists a file whose contents are exactly this node. + */ + attributes: DataTypeBitfield, +} + +impl MerkleNodeAttributes { + /// Returns the parent of this node in the CAS graph. + pub fn cas_parent(&self) -> MerkleNodeId { + self.parent[NodeDataType::CAS as usize] + } + /// Returns the parent of this node in the FILE graph. + pub fn file_parent(&self) -> MerkleNodeId { + self.parent[NodeDataType::FILE as usize] + } + /** Sets parent of this node in the CAS graph. + * Unchecked Invariant: parent must have this node as a child. + */ + pub fn set_cas_parent(&mut self, parent: MerkleNodeId) { + self.parent[NodeDataType::CAS as usize] = parent; + } + /** Sets parent of this node in the FILE graph. + * Unchecked Invariant: parent must have this node as a child. + */ + pub fn set_file_parent(&mut self, parent: MerkleNodeId) { + self.parent[NodeDataType::FILE as usize] = parent; + } + /** Sets parent of this node in either FILE or CAS graph. + * Unchecked Invariant: parent must have this node as a child. + */ + pub fn set_parent(&mut self, parent_type: NodeDataType, parent: MerkleNodeId) { + self.parent[parent_type as usize] = parent; + } + /** + * Returns the parent of this node in either graph. + */ + pub fn parent(&self, parent_type: NodeDataType) -> MerkleNodeId { + self.parent[parent_type as usize] + } + /// Whether this node is a root of a file + pub fn is_file(&self) -> bool { + self.attributes.contains(DataTypeBitfield::FILE) + } + /// Whether this node points to a complete entry in Content Addressed Storage + pub fn is_cas(&self) -> bool { + self.attributes.contains(DataTypeBitfield::CAS) + } + /// Whether this node points to a complete entry in either FILE or CAS + pub fn is_type(&self, data_type: NodeDataType) -> bool { + if data_type == NodeDataType::CAS { + self.is_cas() + } else { + self.is_file() + } + } + + /// Whether this node is a root of a file + pub fn set_file(&mut self) { + self.attributes.set(DataTypeBitfield::FILE, true); + } + /// Whether this node points to a complete entry in Content Addressed Storage + pub fn set_cas(&mut self) { + self.attributes.set(DataTypeBitfield::CAS, true); + } + + /// Returns true if both self and other have the same attributes (FILE or CAS) + pub fn type_equal(&self, other: &MerkleNodeAttributes) -> bool { + self.attributes == other.attributes + } + /** Whether this node is a substring of a CAS entry. + * This is a simple check: either this node is a CAS entry, or + * it has a CAS parent. + */ + pub fn has_cas_data(&self) -> bool { + self.attributes.contains(DataTypeBitfield::CAS) + || self.parent[NodeDataType::CAS as usize] != ID_UNASSIGNED + } + /** Whether this node is a substring of a FILE entry. + * This is a simple check: either this node is a FILE entry, or + * it has a CAS parent. + */ + pub fn has_file_data(&self) -> bool { + self.attributes.contains(DataTypeBitfield::FILE) + || self.parent[NodeDataType::FILE as usize] != ID_UNASSIGNED + } + /** + * Checks if this is a substring of either a CAS or FILE entry. + */ + pub fn has_data(&self, data_type: NodeDataType) -> bool { + if data_type == NodeDataType::CAS { + self.has_cas_data() + } else { + self.has_file_data() + } + } + pub fn merge_attributes(&mut self, other: &MerkleNodeAttributes) { + self.attributes |= other.attributes; + } +} +/**************************************************************************/ +/* */ +/* ObjectRange */ +/* */ +/**************************************************************************/ + +/** + * Describes a range of bytes in an object. + */ +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ObjectRange { + pub hash: MerkleHash, + pub start: usize, + pub end: usize, +} + +/** + * Describes a range of bytes in an object. + */ +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ObjectRangeById { + pub id: MerkleNodeId, + pub start: usize, + pub end: usize, +} + +/** + * Given a collection of ranges, simplify the ranges by collapsing + * consesutive ranges into one. For instance: + * + * ```ignore + * [a, 0, 10], [a,10,20], [b,0, 15] + * ``` + * + * will be collapsed into + * + * ```ignore + * [a, 0, 20], [b,0, 15] + * ``` + */ +pub fn simplify_ranges(ranges: &[ObjectRange]) -> Vec { + let mut ret: Vec = Vec::with_capacity(ranges.len()); + for range in ranges { + if !ret.is_empty() { + let last = ret.last_mut().unwrap(); + if last.hash == range.hash && last.end == range.start { + last.end = range.end; + continue; + } + } + ret.push(range.clone()); + } + ret +} + +/**************************************************************************/ +/* */ +/* Conversion to and from bytes for the RocksDB storage */ +/* */ +/**************************************************************************/ + +pub trait RocksDBConversion { + // TODO: can we do better than Vec here? This incurs a small heap alloc + // especially for NodeId and MerkleHash that should be unnecessary + // Perhaps we can template around it? + fn to_db_bytes(&self) -> Vec; + fn from_db_bytes(bytes: &[u8]) -> T; +} +/** + * Conversion routines to and from bytes + */ +impl RocksDBConversion for MerkleNode { + fn to_db_bytes(&self) -> Vec { + bincode::serialize(self).unwrap() + } + fn from_db_bytes(bytes: &[u8]) -> MerkleNode { + bincode::deserialize(bytes).unwrap() + } +} + +/** + * Conversion routines to and from bytes for MerkleNodeId. + * We use Big Endian here so the lexicographic sort by RocksDB + * gives the nodes in the right order + */ +impl RocksDBConversion for MerkleNodeId { + fn to_db_bytes(&self) -> Vec { + self.to_be_bytes().to_vec() + } + fn from_db_bytes(bytes: &[u8]) -> MerkleNodeId { + MerkleNodeId::from_be_bytes(bytes.try_into().unwrap()) + } +} + +impl RocksDBConversion for MerkleHash { + fn to_db_bytes(&self) -> Vec { + self.as_bytes().to_vec() + } + fn from_db_bytes(bytes: &[u8]) -> MerkleHash { + MerkleHash::try_from(bytes).unwrap() + } +} diff --git a/merkledb/src/tests.rs b/merkledb/src/tests.rs new file mode 100644 index 00000000..9c24774e --- /dev/null +++ b/merkledb/src/tests.rs @@ -0,0 +1,569 @@ +#[cfg(test)] +mod component_tests { + use std::collections::HashSet; + use std::io; + use std::io::Cursor; + + use merklehash::{compute_data_hash, MerkleHash}; + use parutils::AsyncIterator; + + use crate::async_chunk_iterator::async_low_variance_chunk_target; + use crate::chunk_iterator::*; + use crate::constants::*; + use crate::prelude::*; + use crate::prelude_v2::*; + use crate::MerkleMemDB; + + #[test] + fn simple_test() { + let mut mdb = MerkleMemDB::default(); + let h1 = compute_data_hash("hello world".as_bytes()); + let h2 = compute_data_hash("pikachu".as_bytes()); + let chunks = vec![ + Chunk { + hash: h1, + length: 11, + }, + Chunk { + hash: h2, + length: 7, + }, + ]; + + let mut staging = mdb.start_insertion_staging(); + mdb.add_file(&mut staging, &chunks); + let res = mdb.finalize(staging); + let nodes = mdb.find_all_leaves(&res).unwrap(); + assert_eq!(nodes.len(), 2); + assert_eq!(*nodes[0].hash(), chunks[0].hash); + assert_eq!(*nodes[1].hash(), chunks[1].hash); + assert_eq!(nodes[0].len(), chunks[0].length); + assert_eq!(nodes[1].len(), chunks[1].length); + assert_ne!(nodes[0].id(), nodes[1].id()); + + // we should get back original ids if we insert again + let mut staging = mdb.start_insertion_staging(); + mdb.add_file(&mut staging, &chunks); + let res = mdb.finalize(staging); + let nodes2 = mdb.find_all_leaves(&res).unwrap(); + assert_eq!(nodes2[0].id(), nodes[0].id()); + assert_eq!(nodes2[1].id(), nodes[1].id()); + + // search by hash() + let nodes3 = [mdb.find_node(&h1).unwrap(), mdb.find_node(&h2).unwrap()]; + assert_eq!(nodes3[0].id(), nodes[0].id()); + assert_eq!(nodes3[1].id(), nodes[1].id()); + + // search by id + let nodes4 = [ + mdb.find_node_by_id(nodes[0].id()).unwrap(), + mdb.find_node_by_id(nodes[1].id()).unwrap(), + ]; + assert_eq!(nodes4[0].id(), nodes[0].id()); + assert_eq!(nodes4[1].id(), nodes[1].id()); + assert!(mdb.all_invariant_checks()); + } + + #[test] + fn duplicate_file() { + let mut mdb = MerkleMemDB::default(); + let h1 = compute_data_hash("hello world".as_bytes()); + let h2 = compute_data_hash("pikachu".as_bytes()); + let chunks = vec![ + Chunk { + hash: h1, + length: 11, + }, + Chunk { + hash: h2, + length: 7, + }, + ]; + + let mut staging = mdb.start_insertion_staging(); + mdb.add_file(&mut staging, &chunks); + // add the same file twice + mdb.add_file(&mut staging, &chunks); + let res = mdb.finalize(staging); + // there should still be only 2 nodes + let nodes = mdb.find_all_leaves(&res).unwrap(); + assert_eq!(nodes.len(), 2); + assert!(mdb.all_invariant_checks()); + } + fn generate_random_string(seed: u64, len: usize) -> Vec { + use rand::{RngCore, SeedableRng}; + let mut bytes: Vec = vec![0; len]; + bytes.resize(len, 0_u8); + rand::rngs::StdRng::seed_from_u64(seed).fill_bytes(&mut bytes); + bytes + } + + fn generate_uniform_string(len: usize) -> Vec { + let mut bytes: Vec = vec![0; len]; + bytes.resize(len, 0_u8); + bytes + } + + const PER_SEED_INPUT_SIZE: usize = 128 * 1024; + + fn generate_random_chunks(seed: u64) -> Vec { + let input = generate_random_string(seed, PER_SEED_INPUT_SIZE); + let mut reader = Cursor::new(&input[..]); + low_variance_chunk_target( + &mut reader, + TARGET_CDC_CHUNK_SIZE, + N_LOW_VARIANCE_CDC_CHUNKERS, + ) + } + + #[test] + fn cas_consistency_basic() { + // make a bunch of random chunks + // 128k of chunks with seed=0 + // 128k of chunks with seed=1 + // 128k of chunks with seed=2 + // 128k of chunks with seed=3 + // and repeat a bunch of times + let mut chunks1234: Vec = Vec::new(); + + for seed in 0..16_u64 { + let mut ch = generate_random_chunks(seed % 4); + chunks1234.append(&mut ch); + } + + // insert this into the MerkleDB + + let mut mdb = MerkleMemDB::default(); + let mut staging = mdb.start_insertion_staging(); + mdb.add_file(&mut staging, &chunks1234); + let file_root = mdb.finalize(staging); + assert!(mdb.all_invariant_checks()); + + // check the CAS's produced + // there should be 1 unique CAS node + let file_recon = mdb.reconstruct_from_cas(&[file_root.clone()]).unwrap(); + assert_eq!(file_recon[0].0, *file_root.hash()); + let cas_ranges = file_recon[0].1.clone(); + // and the CAS should be chunks of seed 0..3 concatenated 4 times + assert_eq!(cas_ranges.len(), 4); + + let unique_cas_hashes = cas_ranges + .iter() + .map(|x| x.hash) // get the hash + .collect::>() // collect into a hashset + .into_iter() // iterate through the hashset again consuming + .collect::>(); // collect into a vec + + assert_eq!(unique_cas_hashes.len(), 1); + let cas_node_hash = unique_cas_hashes[0]; + + // check that the cas node contains the string for seed=0,1,2,3 + let cas_node = mdb.find_node(&cas_node_hash).unwrap(); + assert_eq!(cas_node.len(), 4 * PER_SEED_INPUT_SIZE); // strings for seed=0,1,2,3 + + // ok. validate we that the cas node comprises of exactly the chunks for + // seed=0,1,2,3 + let cas_leaves = mdb.find_all_leaves(&cas_node).unwrap(); + let mut expected_leaves: Vec = Vec::new(); + for seed in 0..4_u64 { + expected_leaves.append(&mut generate_random_chunks(seed)); + } + assert_eq!(expected_leaves.len(), cas_leaves.len()); + for i in 0..expected_leaves.len() { + assert_eq!(expected_leaves[i].hash, *cas_leaves[i].hash()); + } + } + + #[test] + fn cas_consistency_multifile() { + let mut mdb = MerkleMemDB::default(); + let mut staging = mdb.start_insertion_staging(); + const CAS_BLOCK_SIZE: usize = 300 * 1024; + staging.set_target_cas_block_size(CAS_BLOCK_SIZE); + // make 10 files + for file in 0..10_u64 { + // each file is 2 random sequences of 128KB worth of chunks. + // There are 13 such unique sequences + let mut ch = generate_random_chunks((file * 137) % 13); + ch.append(&mut generate_random_chunks((file * 67) % 13)); + mdb.add_file(&mut staging, &ch); + } + // add one more unique file + mdb.add_file(&mut staging, &generate_random_chunks(456)); + let file_root = mdb.finalize(staging); + assert!(mdb.all_invariant_checks()); + + // check the CAS's produced are not excessively large. Just + // a bit bigger than the target CAS block size. + const PEAK_CAS_BLOCK_SIZE: usize = CAS_BLOCK_SIZE + 50 * 1024; + // + // there should be 1 unique CAS node + let file_recon = mdb.reconstruct_from_cas(&[file_root]).unwrap(); + let cas_ranges = file_recon[0].1.clone(); + let unique_cas_hashes = cas_ranges + .iter() + .map(|x| x.hash) // get the hash + .collect::>() // collect into a hashset + .into_iter() // iterate through the hashset again consuming + .collect::>(); // collect into a vec + + for hash in unique_cas_hashes { + let node = mdb.find_node(&hash).unwrap(); + assert!(node.len() < PEAK_CAS_BLOCK_SIZE); + } + } + + #[test] + fn test_db_union() { + let mut mdb = MerkleMemDB::default(); + let f1hash: MerkleHash; + let h1 = compute_data_hash("hello world".as_bytes()); + let h2 = compute_data_hash("pikachu".as_bytes()); + { + let chunks = vec![ + Chunk { + hash: h1, + length: 11, + }, + Chunk { + hash: h2, + length: 7, + }, + ]; + + let mut staging = mdb.start_insertion_staging(); + mdb.add_file(&mut staging, &chunks); + f1hash = *mdb.finalize(staging).hash(); + } + + let h3 = compute_data_hash("poo".as_bytes()); + let mut mdb2 = MerkleMemDB::default(); + let f2hash: MerkleHash; + { + let chunks = vec![ + Chunk { + hash: h1, + length: 11, + }, + Chunk { + hash: h3, + length: 3, + }, + ]; + + let mut staging = mdb2.start_insertion_staging(); + mdb2.add_file(&mut staging, &chunks); + f2hash = *mdb2.finalize(staging).hash(); + } + + mdb.union_with(&mdb2); + mdb.union_finalize().unwrap(); + // file 1 node + { + let nodes = mdb + .find_all_leaves(&mdb.find_node(&f1hash).unwrap()) + .unwrap(); + assert_eq!(nodes.len(), 2); + assert_eq!(*nodes[0].hash(), h1); + assert_eq!(*nodes[1].hash(), h2); + } + // file 2 nodes + { + let nodes = mdb + .find_all_leaves(&mdb.find_node(&f2hash).unwrap()) + .unwrap(); + assert_eq!(nodes.len(), 2); + assert_eq!(*nodes[0].hash(), h1); + assert_eq!(*nodes[1].hash(), h3); + } + assert!(mdb.all_invariant_checks()); + } + + #[test] + fn test_db_difference() { + // create db1 which is "hello world" "pika" + let mut mdb = MerkleMemDB::default(); + let h1 = compute_data_hash("hello world".as_bytes()); + let h2 = compute_data_hash("pikachu".as_bytes()); + let chunks = [ + Chunk { + hash: h1, + length: 11, + }, + Chunk { + hash: h2, + length: 7, + }, + ]; + + let nodes: Vec<_> = chunks.iter().map(|x| mdb.add_chunk(x).0).collect(); + let f1hash = *mdb.merge_to_file(&nodes).hash(); + + // create db1 which is "hello world" "pika" "poo" + // but "hello world" and "pika" are merged first + let h3 = compute_data_hash("poo".as_bytes()); + let mut mdb2 = MerkleMemDB::default(); + let chunks = [ + Chunk { + hash: h1, + length: 11, + }, + Chunk { + hash: h2, + length: 7, + }, + ]; + + let nodes: Vec<_> = chunks.iter().map(|x| mdb2.add_chunk(x).0).collect(); + let f1node = mdb2.merge_to_file(&nodes); + let append_poo: Vec<_> = vec![ + f1node, + mdb2.add_chunk(&Chunk { + hash: h3, + length: 3, + }) + .0, + ]; + let f2node = mdb2.merge_to_file(&append_poo); + let f2hash = *f2node.hash(); + mdb2.only_file_invariant_checks(); + + let mut diff = MerkleMemDB::default(); + diff.difference(&mdb2, &mdb); + eprintln!("Old: {:?}\n", mdb); + eprintln!("New: {:?}\n", mdb2); + eprintln!("Diff: {:?}\n", diff); + + // diff should contain h3, f2hash and f1hash + // (seq number is 4 cos id 0 is reserved) + assert_eq!(diff.get_sequence_number(), 4); + + // check that these two nodes are there + diff.find_node(&h3).unwrap(); + diff.find_node(&f2hash).unwrap(); + diff.find_node(&f1hash).unwrap(); + + mdb.union_with(&diff); + mdb.union_finalize().unwrap(); + + eprintln!("Union: {:?}\n", mdb); + + mdb.only_file_invariant_checks(); + assert_eq!(mdb.get_sequence_number(), mdb2.get_sequence_number()); + } + + #[test] + fn test_randomized_union_diff() { + // In this test, we basically make a chain of diffs + // start from an empty MDB. add a bunch of things to it, take a diff + // and repeat. + // + // We should be able to merge the diffs in an arbitrary order + // and obtain back the same MDB + use crate::MerkleNodeId; + use rand::seq::SliceRandom; + use rand::{RngCore, SeedableRng}; + let mut rng = rand::rngs::StdRng::seed_from_u64(12345); + + let mut mdb = MerkleMemDB::default(); + // the collection of all the diffs + let mut diffs: Vec = Vec::new(); + for _iters in 0..100 { + // we keep the previous db so we can compute a idff + let prevmdb = mdb.clone(); + // in each iteration we add a "file" and a "cas" with a different + // set of chunks in each. + // We limit to a set of no more than 1000 unique chunks to get + // a decent amount of intersection in the tree structures + let filechunks: Vec = (0..(1 + (rng.next_u64() % 10))) + .map(|_| { + let h = compute_data_hash(&generate_random_string(rng.next_u64() % 1000, 100)); + Chunk { + hash: h, + length: 100, + } + }) + .collect(); + let filenodes: Vec<_> = filechunks.iter().map(|x| mdb.add_chunk(x).0).collect(); + mdb.merge_to_file(&filenodes); + + let caschunks: Vec = (0..(1 + (rng.next_u64() % 10))) + .map(|_| { + let h = compute_data_hash(&generate_random_string(rng.next_u64() % 1000, 100)); + Chunk { + hash: h, + length: 100, + } + }) + .collect(); + let casnodes: Vec<_> = caschunks.iter().map(|x| mdb.add_chunk(x).0).collect(); + mdb.merge_to_cas(&casnodes); + let mut diff = MerkleMemDB::default(); + diff.difference(&mdb, &prevmdb); + diffs.push(diff); + } + + // now we have collected a 100 diffs. we will union them arbitrarily + diffs.shuffle(&mut rng); + let mut unionmdb = MerkleMemDB::default(); + for d in diffs { + unionmdb.union_with(&d); + } + unionmdb.union_finalize().unwrap(); + // do all checks + mdb.only_file_invariant_checks(); + unionmdb.only_file_invariant_checks(); + // now, mdb and unionmdb should be identical. + // apart form parent attributes which may differ. + for i in 0..mdb.get_sequence_number() { + let mdbnode = mdb.find_node_by_id(i as MerkleNodeId).unwrap(); + let unionnode = unionmdb.find_node(mdbnode.hash()).unwrap(); + // check that they have the same length and children count + assert_eq!(mdbnode.len(), unionnode.len()); + assert_eq!(mdbnode.children().len(), unionnode.children().len()); + + // check that is_cas and is_file attributes match + let mdbnodeattr = mdb.node_attributes(mdbnode.id()).unwrap(); + let unionnodeattr = unionmdb.node_attributes(unionnode.id()).unwrap(); + assert_eq!(mdbnodeattr.is_cas(), unionnodeattr.is_cas()); + assert_eq!(mdbnodeattr.is_file(), unionnodeattr.is_file()); + assert_eq!(mdbnodeattr.has_cas_data(), unionnodeattr.has_cas_data()); + assert_eq!(mdbnodeattr.has_file_data(), unionnodeattr.has_file_data()); + + // loop through each child validating they are the same + for (chidx, chid) in mdbnode.children().iter().enumerate() { + let unionchid = unionnode.children()[chidx]; + assert_eq!(chid.1, unionchid.1); + let chnode = mdb.find_node_by_id(chid.0).unwrap(); + let unionchnode = unionmdb.find_node_by_id(unionchid.0).unwrap(); + assert_eq!(chnode.hash(), unionchnode.hash()); + } + + // we just assert that they can both reconstruct (or not) + // The exact reconstruction is not important. + assert_eq!( + mdb.reconstruct_from_file(&[mdbnode.clone()]).is_ok(), + unionmdb.reconstruct_from_file(&[unionnode.clone()]).is_ok() + ); + assert_eq!( + mdb.reconstruct_from_cas(&[mdbnode.clone()]).is_ok(), + unionmdb.reconstruct_from_cas(&[unionnode.clone()]).is_ok() + ); + } + } + + struct AsyncVec { + pub items: std::collections::VecDeque>, + } + #[async_trait::async_trait] + impl AsyncIterator for AsyncVec { + type Item = Vec; + + async fn next(&mut self) -> std::io::Result>> { + if self.items.is_empty() { + return Ok(None); + } + Ok(Some(self.items.pop_front().unwrap())) + } + } + + #[tokio::test] + async fn test_async_chunker() { + let seed = 12345; + let input = generate_random_string(seed, PER_SEED_INPUT_SIZE); + let mut reader = Cursor::new(&input[..]); + let chunks = low_variance_chunk_target( + &mut reader, + TARGET_CDC_CHUNK_SIZE, + N_LOW_VARIANCE_CDC_CHUNKERS, + ); + let mut v: std::collections::VecDeque> = std::collections::VecDeque::new(); + let mut start: usize = 0; + + use rand::{Rng, SeedableRng}; + let mut bytes: Vec = vec![0; PER_SEED_INPUT_SIZE]; + bytes.resize(PER_SEED_INPUT_SIZE, 0_u8); + let mut rng = rand::rngs::StdRng::seed_from_u64(seed); + + while start < input.len() { + let tlen: u16 = rng.gen::() % 4096; + let mut end = start + tlen as usize; + if end > input.len() { + end = input.len(); + } + v.push_back(input[start..end].into()); + start = end; + } + let reader2 = AsyncVec { items: v }; + + let mut async_chunks: Vec = Vec::new(); + + let mut generator = async_low_variance_chunk_target( + reader2, + TARGET_CDC_CHUNK_SIZE, + N_LOW_VARIANCE_CDC_CHUNKERS, + ); + while let Some(a) = generator.next().await.unwrap() { + async_chunks.push(a.0); + } + eprintln!("aaa {:?}", async_chunks); + eprintln!("ccc {:?}", chunks); + assert_eq!(chunks.len(), async_chunks.len()); + for i in 0..chunks.len() { + assert_eq!(chunks[i].length, async_chunks[i].length); + assert_eq!(chunks[i].hash, async_chunks[i].hash); + } + } + + #[tokio::test] + async fn test_async_chunker_uniform_string() { + // this test uses a uniform byte string + // and a small iterator input size which + // forces the chunker to possibly have to read multiple times + // before generating a single chunk + let seed = 12345; + let input = generate_uniform_string(PER_SEED_INPUT_SIZE); + let mut reader = Cursor::new(&input[..]); + let chunks = low_variance_chunk_target( + &mut reader, + TARGET_CDC_CHUNK_SIZE, + N_LOW_VARIANCE_CDC_CHUNKERS, + ); + let mut v: std::collections::VecDeque> = std::collections::VecDeque::new(); + let mut start: usize = 0; + + use rand::{Rng, SeedableRng}; + let mut bytes: Vec = vec![0; PER_SEED_INPUT_SIZE]; + bytes.resize(PER_SEED_INPUT_SIZE, 0_u8); + let mut rng = rand::rngs::StdRng::seed_from_u64(seed); + + while start < input.len() { + let tlen: u16 = rng.gen::() % 4096; + let mut end = start + tlen as usize; + if end > input.len() { + end = input.len(); + } + v.push_back(input[start..end].into()); + start = end; + } + let reader2 = AsyncVec { items: v }; + + let mut async_chunks: Vec = Vec::new(); + + let mut generator = async_low_variance_chunk_target( + reader2, + TARGET_CDC_CHUNK_SIZE, + N_LOW_VARIANCE_CDC_CHUNKERS, + ); + while let Some(a) = generator.next().await.unwrap() { + async_chunks.push(a.0); + } + eprintln!("aaa {:?}", async_chunks); + eprintln!("ccc {:?}", chunks); + assert_eq!(chunks.len(), async_chunks.len()); + for i in 0..chunks.len() { + assert_eq!(chunks[i].length, async_chunks[i].length); + assert_eq!(chunks[i].hash, async_chunks[i].hash); + } + } +} diff --git a/merklehash/Cargo.toml b/merklehash/Cargo.toml new file mode 100644 index 00000000..a21f1104 --- /dev/null +++ b/merklehash/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "merklehash" +version = "0.14.5" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.4" +rand_core = "0.6.3" +rand_chacha = "0.3.1" +structopt = "0.3.22" +sha3 = "0.9.1" +blake3 = "1.5.1" +generic-array = "0.14.4" +safe-transmute = "0.11.2" +serde = { version = "1.0.129", features = ["derive"] } +heed = "0.11" + +[features] +strict = [] diff --git a/merklehash/src/data_hash.rs b/merklehash/src/data_hash.rs new file mode 100644 index 00000000..15bef9fe --- /dev/null +++ b/merklehash/src/data_hash.rs @@ -0,0 +1,384 @@ +use safe_transmute::transmute_to_bytes; +use serde::{Deserialize, Serialize}; +use std::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; +use std::error::Error; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::io::Write; +use std::mem::transmute_copy; +use std::num::ParseIntError; +use std::ops::{Deref, DerefMut}; +use std::str; + +/**************************************************************************/ +/* */ +/* DataHash */ +/* */ +/**************************************************************************/ + +/// The DataHash is a transparent 256-bit value stores as `[u64; 4]`. +/// +/// [compute_data_hash] and [compute_internal_node_hash] are the two main +/// ways in which a hash on data can be computed. +/// +/// Many convenient trait implementations are provided for printing, comparing, +/// and parsing. +/// +/// ```ignore +/// let string = "hello world"; +/// let hash = compute_data_hash(slice.as_bytes()); +/// println!("Hello Hash {}", hash); +/// ``` +#[derive(Clone, Copy, Serialize, Deserialize)] +pub struct DataHash([u64; 4]); + +impl Deref for DataHash { + type Target = [u64; 4]; + #[inline(always)] + fn deref(&self) -> &[u64; 4] { + &self.0 + } +} + +impl DerefMut for DataHash { + #[inline(always)] + fn deref_mut(&mut self) -> &mut [u64; 4] { + &mut (self.0) + } +} + +impl From<[u64; 4]> for DataHash { + fn from(value: [u64; 4]) -> Self { + DataHash(value) + } +} + +impl From<[u8; 32]> for DataHash { + fn from(value: [u8; 32]) -> Self { + unsafe { Self(transmute_copy::<[u8; 32], [u64; 4]>(&value)) } + } +} + +impl From<&[u8; 32]> for DataHash { + fn from(value: &[u8; 32]) -> Self { + unsafe { Self(transmute_copy::<[u8; 32], [u64; 4]>(value)) } + } +} + +impl AsRef<[u8]> for DataHash { + fn as_ref(&self) -> &[u8] { + transmute_to_bytes(self.deref()) + } +} + +impl Default for DataHash { + /// The default constructor returns a DataHash of 0s + fn default() -> DataHash { + DataHash([0; 4]) + } +} + +impl PartialEq for DataHash { + fn eq(&self, other: &Self) -> bool { + self[0] == other[0] && self[1] == other[1] && self[2] == other[2] && self[3] == other[3] + } +} + +impl Eq for DataHash {} + +impl Ord for DataHash { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } +} + +impl PartialOrd for DataHash { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl core::ops::Rem for DataHash { + type Output = u64; + + fn rem(self, rhs: u64) -> Self::Output { + self[3] % rhs + } +} + +unsafe impl heed::bytemuck::Zeroable for DataHash { + fn zeroed() -> Self { + DataHash([0; 4]) + } +} +unsafe impl heed::bytemuck::Pod for DataHash {} + +/// The error type that is returned if [DataHash::from_hex] fails. +#[derive(Debug, Clone)] +pub struct DataHashHexParseError; + +impl Error for DataHashHexParseError {} + +impl fmt::Display for DataHashHexParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid hex input for DataHash") + } +} + +impl From for DataHashHexParseError { + fn from(_err: ParseIntError) -> Self { + DataHashHexParseError {} + } +} + +impl DataHash { + /// Returns the hexadecimal printout of the hash. + pub fn hex(&self) -> String { + format!( + "{:016x}{:016x}{:016x}{:016x}", + self.0[0], self.0[1], self.0[2], self.0[3] + ) + } + + /// Parses a hexadecimal string as a DataHash, returning + /// Err(DataHashHexParseError) on failure. + pub fn from_hex(h: &str) -> Result { + if h.len() != 64 { + return Err(DataHashHexParseError {}); + } + let mut ret: DataHash = Default::default(); + + let good = h.as_bytes().iter().all(|c| c.is_ascii_hexdigit()); + if !good { + return Err(DataHashHexParseError {}); + } + ret.0[0] = u64::from_str_radix(&h[..16], 16)?; + ret.0[1] = u64::from_str_radix(&h[16..32], 16)?; + ret.0[2] = u64::from_str_radix(&h[32..48], 16)?; + ret.0[3] = u64::from_str_radix(&h[48..64], 16)?; + Ok(ret) + } + + /// Returns the datahash as a raw byte slice. + pub fn as_bytes(&self) -> &[u8] { + transmute_to_bytes(&self.0[..]) + } + + pub fn from_slice(value: &[u8]) -> Result { + if value.len() != 32 { + return Err(DataHashBytesParseError); + } + let mut hash: DataHash = DataHash::default(); + unsafe { + let src = value.as_ptr(); + let dst = hash.0.as_mut_ptr() as *mut u8; + std::ptr::copy_nonoverlapping(src, dst, 32); + } + Ok(hash) + } +} + +/// The error type that is returned if TryFrom<&[u8]> fails. +#[derive(Debug, Clone)] +pub struct DataHashBytesParseError; + +impl Error for DataHashBytesParseError {} + +impl fmt::Display for DataHashBytesParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid bytes input for DataHash") + } +} + +impl TryFrom<&[u8]> for DataHash { + type Error = DataHashBytesParseError; + + fn try_from(value: &[u8]) -> Result { + Self::from_slice(value) + } +} + +impl From for Vec { + fn from(val: DataHash) -> Self { + val.as_bytes().into() + } +} + +impl From<&DataHash> for Vec { + fn from(val: &DataHash) -> Self { + val.as_bytes().into() + } +} + +// this is already a nice hash function. We just give the last 64-bits +// for use in hashtables etc. +impl Hash for DataHash { + fn hash(&self, state: &mut H) { + state.write_u64(self.0[0]); + } +} +// as generated from random.org +/// The hash key used for [compute_data_hash] +const DATA_KEY: [u8; 32] = [ + 102, 151, 245, 119, 91, 149, 80, 222, 49, 53, 203, 172, 165, 151, 24, 28, 157, 228, 33, 16, + 155, 235, 43, 88, 180, 208, 176, 75, 147, 173, 242, 41, +]; + +/// The hash key used for [compute_internal_node_hash] +const INTERNAL_NODE_HASH: [u8; 32] = [ + 1, 126, 197, 199, 165, 71, 41, 150, 253, 148, 102, 102, 180, 138, 2, 230, 93, 221, 83, 111, 55, + 199, 109, 210, 248, 99, 82, 230, 74, 83, 113, 63, +]; + +/// Hash function used to compute a leaf hash of the MerkleTree +/// from any user-provided sequence of bytes. You should be using +/// [compute_internal_node_hash] if this hash is to be used for interior +/// nodes. +/// +/// Example: +/// ```ignore +/// let string = "hello world"; +/// let hash = compute_data_hash(slice.as_bytes()); +/// println!("Hello Hash {}", hash); +/// ``` +pub fn compute_data_hash(slice: &[u8]) -> DataHash { + let digest = blake3::keyed_hash(&DATA_KEY, slice); + DataHash::from(digest.as_bytes()) +} + +/// Hash function used to compute the hash of an interior node. +/// +/// Note that this method also accepts a slice +/// of `&[u8]` and it is up to the caller to format the string appropriately. +/// For instance: the string could be simply the child hashes printed out +/// consecutively. +/// +/// The reason why this method does not simply take an array of Hashes, and +/// instead require the caller to format the input as a string is to allow the +/// user to add additional information to the string being hashed (beyond just +/// the hashes itself). i.e. the string being hashed could be a concatenation +/// of "hashes of children + children metadata". +/// +/// Example: +/// ```ignore +/// let mut buf = String::with_capacity(1024); +/// for node in nodes.iter() { +/// writeln!(buf, "{:x} : {}", node.hash(), node.len()).unwrap(); +/// } +/// compute_internal_node_hash(buf.as_bytes()) +/// ``` +pub fn compute_internal_node_hash(slice: &[u8]) -> DataHash { + let digest = blake3::keyed_hash(&INTERNAL_NODE_HASH, slice); + DataHash::from(digest.as_bytes()) +} + +impl fmt::LowerHex for DataHash { + /// Allow the DataHash to be printed with + /// `println!("{:x}", hash)` + /// This prints the hexadecimal representation of the Hash. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{}", self.hex()) + } +} + +impl fmt::Display for DataHash { + /// Allow the DataHash to be printed with + /// `println!("{}", hash)` + /// This prints the hexadecimal representation of the Hash. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{}", self.hex()) + } +} +impl fmt::Debug for DataHash { + /// Allow the DataHash to be printed with + /// `println!("{:?}", hash)` + /// This prints the hexadecimal representation of the Hash. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{}", self.hex()) + } +} + +/// Wrapper around a Write trait that allows computation of the hash at the end. +/// +/// It is recommended to wrap this in a BufWriter object: i.e. +/// +/// let out_file = std::fs::OpenOptions::new().create(true).write(true).open("temp.fs")?; +/// +/// let hashed_write = HashedWrite::new(out_file); +/// +/// { +/// let buf_writer = BufWriter::new(&mut hashed_write); +/// +/// // Do the writing against buf_writer +/// +/// +/// } +/// +/// let h = hashed_write.hash(); +/// +pub struct HashedWrite { + hasher: blake3::Hasher, + writer: W, +} + +impl HashedWrite { + pub fn new(writer: W) -> Self { + Self { + hasher: blake3::Hasher::new_keyed(&DATA_KEY), + writer, + } + } + + pub fn hash(&self) -> DataHash { + let digest = self.hasher.finalize(); + DataHash::from(digest.as_bytes()) + } +} + +impl Write for HashedWrite { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.hasher.update(buf); + self.writer.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } +} + +#[cfg(test)] +mod tests { + use rand::prelude::*; + use std::io::Write; + + use crate::{compute_data_hash, DataHash, HashedWrite}; + + #[test] + fn test_try_from_bytes() { + let hash_bytes_proper = [1u8; 32].to_vec(); + assert!(DataHash::try_from(hash_bytes_proper.as_slice()).is_ok()); + + let hash_bytes_improper = [1u8; 31]; + assert!(DataHash::try_from(hash_bytes_improper.as_slice()).is_err()); + } + + #[test] + fn test_hashed_write() -> std::io::Result<()> { + let mut written_data = Vec::::with_capacity(300); + let mut raw_data = vec![0u8; 300]; + + let mut rng = StdRng::seed_from_u64(0); + rng.fill_bytes(&mut raw_data[..]); + + let mut hashed_write = HashedWrite::new(&mut written_data); + + for i in 0..30 { + hashed_write.write_all(&raw_data[(10 * i)..(10 * (i + 1))])?; + } + + assert_eq!(hashed_write.hash(), compute_data_hash(&raw_data[..])); + assert_eq!(written_data, raw_data); + + Ok(()) + } +} diff --git a/merklehash/src/lib.rs b/merklehash/src/lib.rs new file mode 100644 index 00000000..ebe48f40 --- /dev/null +++ b/merklehash/src/lib.rs @@ -0,0 +1,49 @@ +//! The merklehash module provides common and convenient operations +//! around the [DataHash] (aliased to [MerkleHash]). +//! +//! The [MerkleHash] is internally a 256-bit value stored as 4 u64 and is +//! a node in a MerkleTree. A MerkleTree is a hierarchical datastructure +//! where the leaves are hashes of data (for instance, blocks in a file). +//! Then the hash of each non-leaf node is derived from the hashes of its child +//! nodes. +//! +//! A default constructor is provided to make the hash of 0s. +//! ```ignore +//! // creates a default hash value of all 0s +//! let hash = MerkleHash::default(); +//! ``` +//! +//! Two hash functions are provided to compute a MerkleHash from a slice of +//! bytes. The first is [compute_data_hash] which should be used when computing +//! a hash from any user-provided sequence of bytes (i.e. the leaf nodes) +//! ```ignore +//! // compute from a byte slice of &[u8] +//! let string = "hello world"; +//! let hash = compute_data_hash(slice.as_bytes()); +//! ``` +//! +//! The second is [compute_internal_node_hash] should be used when computing +//! the hash of interior nodes. Note that this method also just accepts a slice +//! of `&[u8]` and it is up to the caller to format the string appropriately. +//! For instance: the string could be simply the child hashes printed out +//! consecutively. +//! +//! The reason why this method does not simply take an array of Hashes, and +//! instead require the caller to format the input as a string is to allow the +//! user to add additional information to the string being hashed (beyond just +//! the hashes itself). i.e. the string being hashed could be a concatenation +//! of "hashes of children + children metadata". +//! ```ignore +//! let hash = compute_internal_node_hash(slice.as_bytes()); +//! ``` +//! +//! The two hash functions [compute_data_hash] and [compute_internal_node_hash] +//! are keyed differently such the same inputs will produce different outputs. +//! And in particular, it should be difficult to find a collision where +//! a `compute_data_hash(a) == compute_internal_node_hash(b)` + +#![cfg_attr(feature = "strict", deny(warnings))] + +pub mod data_hash; +pub use data_hash::*; +pub type MerkleHash = DataHash; diff --git a/parutils/Cargo.toml b/parutils/Cargo.toml new file mode 100644 index 00000000..f446d279 --- /dev/null +++ b/parutils/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "parutils" +version = "0.14.5" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.36", features = ["full"] } +anyhow = "1" +async-scoped = {version = "0.7", features = ["use-tokio"]} +futures = "0.3.28" +async-trait = "0.1.53" +deadqueue = "0.2.4" +tracing = "0.1.31" +more-asserts = "0.3.*" +len-trait = "0.6.1" + +[features] +strict = [] diff --git a/parutils/src/async_iterator.rs b/parutils/src/async_iterator.rs new file mode 100644 index 00000000..39550207 --- /dev/null +++ b/parutils/src/async_iterator.rs @@ -0,0 +1,39 @@ +use async_trait::async_trait; + +#[async_trait] +pub trait AsyncIterator: Send + Sync { + type Item: Send + Sync; + + /// The traditional next method for iterators, with a Result and Error + /// type. Returns None when everything is done. + async fn next(&mut self) -> Result, E>; +} + +#[async_trait] +pub trait BatchedAsyncIterator: AsyncIterator { + /// Return a block of items. If the stream is done, then an empty vector is returned; + /// otherwise, at least one item is returned. + /// + /// If given, max_num dictates the maximum number of items to return. If None, then all + /// available items are returned. + /// + async fn next_batch(&mut self, max_num: Option) -> Result, E>; + + /// Returns the number of items remaining in the stream + /// if known, and None otherwise. Returns Some(0) if + /// there are no items remaining. + fn items_remaining(&self) -> Option; +} + +#[async_trait] +impl AsyncIterator for Vec { + type Item = Vec; + + async fn next(&mut self) -> Result, E> { + if self.is_empty() { + Ok(None) + } else { + Ok(Some(std::mem::take(self))) + } + } +} diff --git a/parutils/src/lib.rs b/parutils/src/lib.rs new file mode 100644 index 00000000..2074c780 --- /dev/null +++ b/parutils/src/lib.rs @@ -0,0 +1,7 @@ +#![cfg_attr(feature = "strict", deny(warnings))] + +mod parallel_utils; +pub use parallel_utils::*; + +mod async_iterator; +pub use async_iterator::*; diff --git a/parutils/src/parallel_utils.rs b/parutils/src/parallel_utils.rs new file mode 100644 index 00000000..427a1aee --- /dev/null +++ b/parutils/src/parallel_utils.rs @@ -0,0 +1,321 @@ +use futures::prelude::stream::*; +use std::mem::take; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Debug)] +pub enum ParallelError { + JoinError, + TaskError(E), +} + +/// Call an async closure in parallel within the tokio runtime, with one call for each index in 0..n_tasks. +/// +/// Usage: +/// +/// ```ignore +/// let v = vec![...]; +/// let v_ref = &v_ref; // Move is used, so capture non-move context with references. +/// +/// run_tokio_parallel(v.len(), |idx| async move { +/// // Don't actually do this +/// let x = &v_ref[i]; +/// // do something +/// }).await?; +/// ``` +/// +/// +/// Note: Use Arc> around writable things. +/// +pub async fn run_tokio_parallel( + n_tasks: usize, + max_concurrent: usize, + f: F, +) -> Result<(), ParallelError> +where + F: Send + Sync + Fn(usize) -> R, + R: futures::Future> + Send, + E: Send + Sync + 'static, +{ + let proc_queue = Arc::new(AtomicUsize::new(0usize)); + + let (_, outputs) = async_scoped::TokioScope::scope_and_block(|scope| { + for _ in 0..max_concurrent { + let proc_queue = proc_queue.clone(); + let f = &f; + + scope.spawn(async move { + loop { + let idx = proc_queue.fetch_add(1, Ordering::Relaxed); + + if idx >= n_tasks { + return Result::<(), E>::Ok(()); + } + + f(idx).await?; + } + }); + } + }); + + for o in outputs { + match o { + // this is a tokio join error + Err(_) => Err(ParallelError::::JoinError)?, + Ok(Err(e)) => Err(ParallelError::::TaskError(e))?, + _ => (), + } + } + + Ok(()) +} + +/// Call an async closure in parallel within the tokio runtime, with one call for each index in 0..n_tasks. +/// +/// Usage: +/// ```ignore +/// let v_in : Vec = vec![...]; +/// +/// let v_out : Vec = +/// tokio_par_for_each(v_in, |(item : InputType, idx : usize)| async move { +/// +/// // do something to item +/// let out : OutputType = ... +/// return Ok(out) +/// }).await?; +/// ``` +/// +pub async fn tokio_par_for_each( + input: Vec, + max_concurrent: usize, + f: F, +) -> Result, ParallelError> +where + F: Send + Sync + Fn(I, usize) -> R, + I: Send + Default, + R: futures::Future> + Send, + Q: Default + Send, + E: Send + Sync + 'static, +{ + let mut _output: Vec = Vec::with_capacity(input.len()); + for _ in 0..input.len() { + _output.push(Q::default()); + } + let n_tasks = input.len(); + let proc_queue = Arc::new(Mutex::<(usize, Vec)>::new((0usize, input))); + let proc_result = Arc::new(Mutex::new(_output)); + + let (_, outputs) = async_scoped::TokioScope::scope_and_block(|scope| { + for _ in 0..max_concurrent { + let proc_queue = proc_queue.clone(); + let proc_result = proc_result.clone(); + let f = &f; + + scope.spawn(async move { + loop { + let idx; + let task: I; + + { + let mut obj = proc_queue.lock().await; + let stats = &mut obj; + idx = stats.0; + if idx >= n_tasks { + return Result::<(), E>::Ok(()); + } else { + task = take(&mut stats.1[idx]); + stats.0 += 1; + } + } + + let result = f(task, idx).await?; + + { + let mut obj = proc_result.lock().await; + obj[idx] = result; + } + } + }); + } + }); + + // TODO: duplicate with line 62-69 + for o in outputs { + match o { + // this is a tokio join error + Err(_) => Err(ParallelError::::JoinError)?, + Ok(Err(e)) => Err(ParallelError::::TaskError(e))?, + _ => (), + } + } + + let mut obj = proc_result.lock().await; + Ok(take(&mut obj)) +} + +/// Call an async closure in parallel within the tokio runtime, with one call for each index in 0..n_tasks. +/// Return immediately when the closure returns an Ok() value. Return None when no closure call +/// returns an Ok(). +/// +/// Usage: +/// ```ignore +/// let v_in : Vec = vec![...]; +/// +/// let v_out : Option = +/// tokio_par_for_any_ok(v_in, |(item : InputType, idx : usize)| async move { +/// +/// // do something to item +/// let out : Result = ... +/// return out +/// }).await; +/// ``` +/// +pub async fn tokio_par_for_any_ok( + input: Vec, + max_concurrent: usize, + f: F, +) -> Option +where + F: Send + Sync + Fn(I, usize) -> R, + I: Send, + R: futures::Future> + Send, + Q: Send + Default, + E: Send + Sync + 'static, +{ + let mut strm = iter( + input + .into_iter() + .enumerate() + .map(|(idx, objr)| f(objr, idx)), + ) + .buffer_unordered(max_concurrent); + + while let Some(maybe_out) = strm.next().await { + if let Ok(out) = maybe_out { + return Some(out); + } + } + + None +} + +/// Allow an async function to be run from a non-async routine. +/// Note that this blocks the current thread and requires multi-threaded +/// tokio runtime. Use carefully!!! +/// Also known to have bad interactions with futures_streams; don't +/// use in those paths. +pub fn block_on_async_function(f: F) -> Result> +where + F: Send + Sync + Fn() -> R, + R: futures::Future> + Send, + E: Send + Sync + 'static, + V: Send + Sync + 'static, +{ + let (_, mut outputs) = async_scoped::TokioScope::scope_and_block(|scope| { + let f = &f; + + scope.spawn(async move { + let ret: V = f().await?; + Result::::Ok(ret) + }); + }); + + if outputs.len() != 1 { + Err(ParallelError::::JoinError) + } else { + match outputs.pop().unwrap() { + // this is a tokio join error + Err(_) => Err(ParallelError::::JoinError), + Ok(Err(e)) => Err(ParallelError::::TaskError(e)), + Ok(Ok(v)) => Ok(v), + } + } +} + +#[cfg(test)] +mod parallel_tests { + + use more_asserts::{assert_ge, assert_le}; + + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::time::{Duration, Instant}; + + #[tokio::test(flavor = "multi_thread")] + async fn test_parallel() { + let atomic_ctr = AtomicUsize::new(0); + run_tokio_parallel(400, 4, |_| async { + atomic_ctr.fetch_add(1, Ordering::Relaxed); + Result::<_, anyhow::Error>::Ok(()) + }) + .await + .unwrap(); + + assert_eq!(atomic_ctr.load(Ordering::SeqCst), 400); + } + #[tokio::test(flavor = "multi_thread")] + async fn test_simple_parallel() -> Result<(), Box> { + let data: Vec = (0..400).map(|i| format!("Number = {}", &i)).collect(); + + let data_ref: Vec = data + .iter() + .enumerate() + .map(|(i, s)| format!("{}{}{}", &s, ":", &i)) + .collect(); + + // let data_test: Vec = + let r = tokio_par_for_each(data, 4, |s, i| async move { + Result::<_, anyhow::Error>::Ok(format!("{}{}{}", &s, ":", &i)) + }) + .await + .unwrap(); + + assert_eq!(data_ref.len(), r.len()); + for i in 0..data_ref.len() { + assert_eq!(data_ref[i], r[i]); + } + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_parallel_any_ok() { + let fun = |sec: u32, idx: usize| async move { + std::thread::sleep(Duration::from_secs(5_u64.pow(sec))); + if idx == 0 { + Ok(sec) + } else { + Err(anyhow::anyhow!("sleeping too long")) + } + }; + + let input = vec![0, 1, 2]; + // sleeps respectively 1 sec, 5 sec, 25 sec. + let t_start = Instant::now(); + let output = tokio_par_for_any_ok(input.clone(), 3, fun).await; + let elapsed = t_start.elapsed(); + assert_eq!(output, Some(0)); + assert_ge!(elapsed, Duration::from_secs(1)); + assert_le!(elapsed, Duration::from_secs(5)); + + let input = vec![1, 0, 2]; + // sleeps respectively 5 sec, 1 sec, 25 sec. + let t_start = Instant::now(); + let output = tokio_par_for_any_ok(input.clone(), 3, fun).await; + let elapsed = t_start.elapsed(); + assert_eq!(output, Some(1)); + assert_ge!(elapsed, Duration::from_secs(5)); + assert_le!(elapsed, Duration::from_secs(25)); + } + + async fn value() -> Result { + Ok(42) + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_single_async() { + let v = block_on_async_function(|| async move { value().await }).unwrap(); + assert_eq!(v, 42); + } +} diff --git a/progress_reporting/Cargo.toml b/progress_reporting/Cargo.toml new file mode 100644 index 00000000..106bfe47 --- /dev/null +++ b/progress_reporting/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "progress_reporting" +version = "0.14.5" +edition = "2021" + +[lib] +path = "src/lib.rs" + +[[bin]] +name = "test_progress_reporting" +path = "src/main.rs" + + +[dependencies] +crossterm = "0.25.0" +tokio = { version = "1.36", features = ["full"] } +utils = {path = "../utils"} +tracing = "0.1.*" +more-asserts = "0.3.*" +atty = "0.2" + + diff --git a/progress_reporting/src/data_progress.rs b/progress_reporting/src/data_progress.rs new file mode 100644 index 00000000..2f8e9f72 --- /dev/null +++ b/progress_reporting/src/data_progress.rs @@ -0,0 +1,374 @@ +use cas::output_bytes; +use crossterm::{cursor, QueueableCommand}; +use std::io::{stderr, Write}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +const MAX_PRINT_INTERVAL_MS: u64 = 250; + +#[derive(Debug, Default)] +struct DPRPrintInfo { + last_write_length: usize, + buffer: Vec, +} + +#[derive(Debug)] +pub struct DataProgressReporter { + is_active: AtomicBool, + disable: bool, + total_count: AtomicUsize, + total_bytes: AtomicUsize, + current_count: AtomicUsize, + current_bytes: AtomicUsize, + message: String, + time_at_start: Instant, + last_print_time: AtomicU64, // In miliseconds since time_at_start + + // Also used as a lock around the printing. + print_info: Mutex, +} + +impl DataProgressReporter { + pub fn new( + message: &str, + total_unit_count: Option, + total_byte_count: Option, + ) -> Arc { + Arc::new(Self { + is_active: AtomicBool::new(true), + disable: atty::isnt(atty::Stream::Stderr), + total_count: AtomicUsize::new(total_unit_count.unwrap_or(0)), + total_bytes: AtomicUsize::new(total_byte_count.unwrap_or(0)), + current_count: AtomicUsize::new(0), + current_bytes: AtomicUsize::new(0), + message: message.to_owned(), + time_at_start: Instant::now(), + last_print_time: AtomicU64::new(0), + print_info: Mutex::new(<_>::default()), + }) + } + + pub fn new_inactive( + message: &str, + total_unit_count: Option, + total_byte_count: Option, + ) -> Arc { + let s = Self::new(message, total_unit_count, total_byte_count); + s.is_active.store(false, Ordering::Relaxed); + s + } + + pub fn set_active(&self, active_flag: bool) { + self.is_active.store(active_flag, Ordering::Relaxed); + } + + /// Adds progress into the register, printing the result. + /// + /// There are two quantities here, unit_amount and bytes. If total_unit_count was given + /// initially, then unit_amount should be specified to indicate how many new units were completed. + /// Note that if no total count was specified, then this should be None. + /// + /// bytes indicates the number of bytes completed along with these units. + /// + /// + /// Example 1: + /// + /// let mut pb = DataProgressReporter::new("Testing progress bar", Some(10)); + /// pb.register_progress(Some(1), 25 * 1024); + /// pb.register_progress(Some(8), 50 * 1024); + /// pb.finalize(); + /// + /// + /// This will update the progress display line to, in order, + /// + /// Testing progress bar: (1/10), 25 KiB | 25 KiB/s. + /// Testing progress bar: (9/10), 75 KiB | 75 KiB/s. + /// Testing progress bar: (10/10), 75 KiB | 75 KiB/s, done. + /// + /// + /// Example 2: + /// + /// + /// let mut pb = DataProgressReporter::new("Testing progress bar, bytes only", None); + /// pb.register_progress(None, 25 * 1024); + /// pb.register_progress(None, 50 * 1024); + /// pb.finalize(); + /// + /// This will update the progress display line to, in order, + /// + /// Testing progress bar, bytes only: 25 KiB | 25 KiB/s. + /// Testing progress bar, bytes only: 75 KiB | 75 KiB/s. + /// Testing progress bar, bytes only: 75 KiB | 75 KiB/s, done. + /// + pub fn register_progress(&self, unit_amount: Option, bytes: Option) { + if let Some(c) = unit_amount { + self.current_count.fetch_add(c, Ordering::Relaxed); + } + + if let Some(b) = bytes { + self.current_bytes.fetch_add(b, Ordering::Relaxed); + } + + let _ = self.print(false); + } + + /// Sometimes, when we're scanning things, this can be a moving target + pub fn update_target(&self, unit_delta_amount: Option, byte_delta: Option) { + if let Some(c) = unit_delta_amount { + self.total_count.fetch_add(c, Ordering::Relaxed); + } + + if let Some(b) = byte_delta { + self.total_bytes.fetch_add(b, Ordering::Relaxed); + } + } + + pub fn set_progress(&self, unit_amount: Option, bytes: Option) { + if let Some(c) = unit_amount { + self.current_count.store(c, Ordering::Relaxed); + } + + if let Some(b) = bytes { + self.current_bytes.store(b, Ordering::Relaxed); + } + + let _ = self.print(false); + } + + /// Call when done with all progress. + pub fn finalize(&self) { + let _ = self.print(true); + } + + /// Does the actual printing + fn print(&self, is_final: bool) -> std::result::Result<(), std::io::Error> { + // Put a minimum width for the line we print. This is based on git's messages, + // which can be longer than ours, causing weird inteleaving when the user does + // stuff to the input (hits a new line, etc.) So, print a minimum buffer of spaces. + const PRINT_LINE_MIN_WIDTH: usize = 74; + + if self.disable || !self.is_active.load(Ordering::Relaxed) { + return Ok(()); + } + + let elapsed_time = Instant::now().duration_since(self.time_at_start); + + let elapsed_millis = elapsed_time.as_millis().min(u64::MAX as u128) as u64; + + let last_print_time = self.last_print_time.load(Ordering::Relaxed); + + if !is_final + && last_print_time != 0 + && elapsed_millis < last_print_time + MAX_PRINT_INTERVAL_MS + { + return Ok(()); + } + + // Acquire the print lock + let Ok(mut lg_print_info) = self.print_info.lock() else { + return Ok(()); + }; + + // get the last print time + let last_print_time = self.last_print_time.load(Ordering::Relaxed); + + // Is this condition still valid? Could have been updated while waiting for the lock. + if !is_final + && last_print_time != 0 + && elapsed_millis < last_print_time + MAX_PRINT_INTERVAL_MS + { + return Ok(()); + } + + const WHITESPACE: &str = " "; + let current_bytes = self.current_bytes.load(Ordering::Relaxed); + let current_count = self.current_count.load(Ordering::Relaxed); + + // Now, get the info. + let byte_rate = (1000 * current_bytes) / (usize::max(1000, elapsed_millis as usize)); + + let mut write_str = match ( + self.total_count.load(Ordering::Relaxed), + self.total_bytes.load(Ordering::Relaxed), + ) { + (0 | 1, 0) => { + match (current_count, current_bytes) { + (0, 0) => { + // Things are still pending, so just print ... to indicate this. + + // No information yet. + // EX: Uploading: ... + format!("{}: ...", self.message) + } + (0, b) => { + // Just the number of bytes transferred so far. + // EX: Uploading: 2.5MB | 1MB/s. + format!( + "{}: {} | {}/s{}", + self.message, + &output_bytes(b), + &output_bytes(byte_rate), + if is_final { ", done." } else { "." } + ) + } + (c, 0) => { + // Just the number of units completed, no info on total. + // EX: Scanning Directories: 23 completed. + format!( + "{}: {} completed{}", + self.message, + c, + if is_final { ", done." } else { "." } + ) + } + (c, b) => { + // Number of units completed and number of bytes, no info on totals: + // EX: Downloading: 45/??, 210 MB | 32MB/s. + format!( + "{}: {} / {}, {} | {}/s{}", + self.message, + c, + if is_final { + format!("{c}") + } else { + "??".to_owned() + }, + &output_bytes(b), + &output_bytes(byte_rate), + if is_final { ", done." } else { "." } + ) + } + } + } + (0 | 1, total_bytes) => { + // Number of bytes completed and total bytes. + // EX: Downloading: (210 MB / 1.2 GB) | 32MB/s. + + format!( + "{}: ({} / {}) | {}/s{}", + self.message, + &output_bytes(if is_final { total_bytes } else { current_bytes }), + &output_bytes(total_bytes), + &output_bytes(byte_rate), + if is_final { ", done." } else { "." } + ) + } + (total_count, 0) => { + if current_bytes != 0 { + // Number of units completed and bytes completed, no total byte information. + // EX: Downloading: (750 / 1001), 453MB | 23MB/s. + format!( + "{}: ({} / {}), {} | {}/s{}", + self.message, + if is_final { total_count } else { current_count }, + total_count, + &output_bytes(current_bytes), + &output_bytes(byte_rate), + if is_final { ", done." } else { "." } + ) + } else { + // Number of units completed and total count, no byte information. + // EX: Scanning: (750 / 1001). + format!( + "{}: ({} / {}){}", + self.message, + if is_final { total_count } else { current_count }, + total_count, + if is_final { ", done." } else { "." } + ) + } + } + (total_count, total_bytes) => { + // Number of units completed and bytes completed, total byte information (but total not used here.. + // EX: Downloading: (750 / 1001), 453MB | 23MB/s. + + format!( + "{}: ({} / {}), {} | {}/s{}", + self.message, + if is_final { total_count } else { current_count }, + total_count, + &output_bytes(if is_final { total_bytes } else { current_bytes }), + &output_bytes(byte_rate), + if is_final { ", done." } else { "." } + ) + } + }; + + let write_str_pad_len = lg_print_info.last_write_length.max(PRINT_LINE_MIN_WIDTH); + + if write_str.len() < write_str_pad_len { + let mut len_to_write = write_str_pad_len - write_str.len(); + + loop { + write_str.push_str(&WHITESPACE[..usize::min(len_to_write, WHITESPACE.len())]); + if len_to_write > WHITESPACE.len() { + len_to_write -= WHITESPACE.len(); + continue; + } else { + break; + } + } + } + + // Annoyingly, on windows prior to Windows 11, cursor movement is done as soon as the buffer + + lg_print_info.last_write_length = write_str.len(); + + let use_buffer = { + #[cfg(windows)] + { + crossterm::ansi_support::supports_ansi() + } + #[cfg(not(windows))] + { + true + } + }; + + if use_buffer { + lg_print_info.buffer.clear(); + } + + let mut stderr = stderr(); + + // We haven't printed anything yet; it's all in the write_str + if last_print_time == 0 { + // Move the cursor to the next line. We can't seem to use cursor::MoveToNextLine + // as it doesn't seem to add anything to the terminal buffer. Doing a newline like + // this seems to work fine. + if use_buffer { + lg_print_info.buffer.write_all("\n".as_bytes())?; + } else { + stderr.write_all("\n".as_bytes())?; + } + } + if use_buffer { + lg_print_info.buffer.queue(cursor::SavePosition)?; + lg_print_info.buffer.queue(cursor::MoveToPreviousLine(1))?; + + lg_print_info.buffer.queue(cursor::MoveToColumn(0)).unwrap(); + + lg_print_info.buffer.write_all(write_str.as_bytes())?; + + lg_print_info.buffer.queue(cursor::RestorePosition)?; + + // Finally, flush it out. + stderr.write_all(&lg_print_info.buffer)?; + } else { + stderr.queue(cursor::SavePosition)?; + stderr.queue(cursor::MoveToPreviousLine(1))?; + + stderr.queue(cursor::MoveToColumn(0)).unwrap(); + + stderr.write_all(write_str.as_bytes())?; + + stderr.queue(cursor::RestorePosition)?; + } + + self.last_print_time + .store(elapsed_millis + 1, Ordering::Relaxed); + + Ok(()) + } +} diff --git a/progress_reporting/src/lib.rs b/progress_reporting/src/lib.rs new file mode 100644 index 00000000..40af29cf --- /dev/null +++ b/progress_reporting/src/lib.rs @@ -0,0 +1,5 @@ +mod data_progress; +mod writer_with_reporting; + +pub use data_progress::DataProgressReporter; +pub use writer_with_reporting::ReportedWriter; diff --git a/progress_reporting/src/main.rs b/progress_reporting/src/main.rs new file mode 100644 index 00000000..e642b665 --- /dev/null +++ b/progress_reporting/src/main.rs @@ -0,0 +1,90 @@ +use core::time; +use progress_reporting::DataProgressReporter; +use std::thread::sleep; + +fn main() { + let pb = DataProgressReporter::new("Testing progress bar (no totals, no bytes)", None, None); + + pb.register_progress(Some(1), None); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(2), None); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(1), None); + sleep(time::Duration::from_millis(500)); + pb.finalize(); + + let pb = DataProgressReporter::new( + "Testing progress bar (total count, no bytes)", + Some(5), + None, + ); + pb.register_progress(Some(1), None); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(2), None); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(1), None); + sleep(time::Duration::from_millis(500)); + pb.finalize(); + + let pb = DataProgressReporter::new("Testing progress bar (no totals, only bytes)", None, None); + + pb.register_progress(None, Some(5000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(None, Some(6000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(None, Some(4000)); + sleep(time::Duration::from_millis(500)); + pb.finalize(); + + let pb = DataProgressReporter::new( + "Testing progress bar (only bytes + total)", + None, + Some(20000), + ); + + pb.register_progress(None, Some(5000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(None, Some(6000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(None, Some(4000)); + sleep(time::Duration::from_millis(500)); + pb.finalize(); + + let pb = DataProgressReporter::new( + "Testing progress bar (no totals, both count + bytes)", + None, + None, + ); + + pb.register_progress(Some(5), Some(5000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(10), Some(6000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(12), Some(4000)); + sleep(time::Duration::from_millis(500)); + pb.finalize(); + + let pb = DataProgressReporter::new("Testing progress bar (Total count)", Some(30), None); + + pb.register_progress(Some(5), Some(5000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(10), Some(6000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(12), Some(4000)); + sleep(time::Duration::from_millis(500)); + pb.finalize(); + + let pb = DataProgressReporter::new( + "Testing progress bar (Total count + total bytes)", + Some(30), + Some(20000), + ); + + pb.register_progress(Some(5), Some(5000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(10), Some(6000)); + sleep(time::Duration::from_millis(500)); + pb.register_progress(Some(12), Some(4000)); + sleep(time::Duration::from_millis(500)); + pb.finalize(); +} diff --git a/progress_reporting/src/writer_with_reporting.rs b/progress_reporting/src/writer_with_reporting.rs new file mode 100644 index 00000000..e9819e6e --- /dev/null +++ b/progress_reporting/src/writer_with_reporting.rs @@ -0,0 +1,41 @@ +use crate::DataProgressReporter; +use std::io::Write; +use std::sync::Arc; + +pub struct ReportedWriter { + writer: W, + progress_reporter: Option>, +} + +impl ReportedWriter { + pub fn new(writer: W, progress_reporter: &Option>) -> Self { + Self { + writer, + progress_reporter: progress_reporter.as_ref().cloned(), + } + } + + fn report(&self, s: usize) { + if let Some(pr) = self.progress_reporter.as_ref() { + pr.register_progress(None, Some(s)); + } + } +} + +impl Write for ReportedWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let res = self.writer.write(buf)?; + self.report(res); + Ok(res) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } + + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + self.writer.write_all(buf)?; + self.report(buf.len()); + Ok(()) + } +} diff --git a/retry_strategy/Cargo.toml b/retry_strategy/Cargo.toml new file mode 100644 index 00000000..c43bde06 --- /dev/null +++ b/retry_strategy/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "retry_strategy" +version = "0.14.5" +edition = "2021" + +[features] +strict = [] + +[dependencies] +tokio-retry = "0.3.0" + +[dev-dependencies] +anyhow = "1" +tokio = { version = "1.36", features = ["full"] } diff --git a/retry_strategy/src/lib.rs b/retry_strategy/src/lib.rs new file mode 100644 index 00000000..cf5abc59 --- /dev/null +++ b/retry_strategy/src/lib.rs @@ -0,0 +1,128 @@ +use tokio_retry::strategy::{jitter, FibonacciBackoff}; +use tokio_retry::{Action, RetryIf}; + +#[derive(Debug, Clone, Copy)] +pub struct RetryStrategy { + num_retries: usize, + base_backoff_ms: u64, +} + +impl RetryStrategy { + pub fn new(num_retries: usize, base_backoff_ms: u64) -> Self { + Self { + num_retries, + base_backoff_ms, + } + } + + pub async fn retry FnMut(&'r E) -> bool>( + &self, + action: A, + retryable: C, + ) -> Result + where + A: Action, + { + let strategy = FibonacciBackoff::from_millis(self.base_backoff_ms) + .map(jitter) + .take(self.num_retries); + RetryIf::spawn(strategy, action, retryable).await + } +} +#[cfg(test)] +mod tests { + use super::*; + use anyhow::anyhow; + use std::sync::atomic::{AtomicU32, Ordering}; + #[tokio::test] + async fn test_retry_failures() { + let error_count = AtomicU32::new(0); + let retry_count = AtomicU32::new(0); + let strategy = RetryStrategy::new(3, 1); // 1ms + let e = strategy + .retry( + || async { + retry_count.fetch_add(1, Ordering::Relaxed); + Err::<(), anyhow::Error>(anyhow!("moof")) + }, + |_| { + error_count.fetch_add(1, Ordering::Relaxed); + true + }, + ) + .await; + assert!(e.is_err()); + // retries 3 times so 4 attempts + assert_eq!(retry_count.load(Ordering::Relaxed), 4); + assert!(error_count.load(Ordering::Relaxed) >= 3); + } + + #[tokio::test] + async fn test_retry_success() { + let error_count = AtomicU32::new(0); + let retry_count = AtomicU32::new(0); + let strategy = RetryStrategy::new(3, 1); // 1ms + let e = strategy + .retry( + || async { + retry_count.fetch_add(1, Ordering::Relaxed); + Ok::<(), anyhow::Error>(()) + }, + |_| { + error_count.fetch_add(1, Ordering::Relaxed); + true + }, + ) + .await; + assert!(e.is_ok()); + assert_eq!(retry_count.load(Ordering::Relaxed), 1); + assert_eq!(error_count.load(Ordering::Relaxed), 0); + } + + #[tokio::test] + async fn test_retry_eventual_success() { + let error_count = AtomicU32::new(0); + let retry_count = AtomicU32::new(0); + let strategy = RetryStrategy::new(3, 1); // 1ms + let e = strategy + .retry( + || async { + if retry_count.fetch_add(1, Ordering::Relaxed) == 2 { + Ok(()) + } else { + Err(anyhow!("moof")) + } + }, + |_| { + error_count.fetch_add(1, Ordering::Relaxed); + true + }, + ) + .await; + assert!(e.is_ok()); + assert_eq!(retry_count.load(Ordering::Relaxed), 3); + assert!(error_count.load(Ordering::Relaxed) >= 2); + } + + #[tokio::test] + async fn test_do_not_retry() { + let error_count = AtomicU32::new(0); + let retry_count = AtomicU32::new(0); + let strategy = RetryStrategy::new(3, 1); // 1ms + let e = strategy + .retry( + || async { + retry_count.fetch_add(1, Ordering::Relaxed); + Err::<(), anyhow::Error>(anyhow!("moof")) + }, + |_| { + error_count.fetch_add(1, Ordering::Relaxed); + false + }, + ) + .await; + assert!(e.is_err()); + assert_eq!(retry_count.load(Ordering::Relaxed), 1); + assert!(error_count.load(Ordering::Relaxed) == 1); + } +} diff --git a/shard_client/Cargo.toml b/shard_client/Cargo.toml new file mode 100644 index 00000000..a1812e04 --- /dev/null +++ b/shard_client/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "shard_client" +version = "0.14.5" +edition = "2021" + +[features] +strict = [] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +utils = {path = "../utils"} +merklehash = {path = "../merklehash"} +retry_strategy = {path = "../retry_strategy"} +cas_client = {path = "../cas_client"} +xet_error = {path = "../xet_error"} +mdb_shard = {path = "../mdb_shard"} +merkledb = {path = "../merkledb"} +tonic = {version = "0.10.2", features = ["tls", "tls-roots", "transport"] } +prost = "0.12.3" +tokio = { version = "1.36", features = ["full"] } +tokio-retry = "0.3.0" +tower = {version = "0.4"} +clap = "2.33" +async-trait = "0.1.9" +anyhow = "1" +http = "0.2.5" +serde_json = "1.0" +tempfile = "3" +tracing = "0.1.31" +bincode = "1.3.3" +uuid = {version = "1", features = ["v4", "fast-rng"]} +lazy_static = "1.4.0" +heed = "0.11" + +# trace-propagation +opentelemetry = { version = "0.17", features = ["trace", "rt-tokio"] } +opentelemetry-jaeger = { version = "0.16", features = ["rt-tokio"] } +opentelemetry-http = "0.6.0" +tracing-opentelemetry = "0.17.2" + + + +# HTTP2 GET AND POST support +hyper = "0.14.18" +bytes = "1" +itertools = "0.10" + +[dev-dependencies] +rand = "0.8.5" + diff --git a/shard_client/src/error.rs b/shard_client/src/error.rs new file mode 100644 index 00000000..9c134d77 --- /dev/null +++ b/shard_client/src/error.rs @@ -0,0 +1,26 @@ +use xet_error::Error; + +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum ShardClientError { + #[error("File I/O error")] + IOError(#[from] std::io::Error), + + #[error("LMDB Error: {0}")] + ShardDedupDBError(String), + + #[error("Data Parsing Error")] + DataParsingError(String), + + #[error("Error : {0}")] + Other(String), + + #[error("MerkleDB Shard Error : {0}")] + MDBShardError(#[from] mdb_shard::error::MDBShardError), + + #[error("Client connection error: {0}")] + GrpcClientError(#[from] anyhow::Error), +} + +// Define our own result type here (this seems to be the standard). +pub type Result = std::result::Result; diff --git a/shard_client/src/global_dedup_table.rs b/shard_client/src/global_dedup_table.rs new file mode 100644 index 00000000..875e6bcb --- /dev/null +++ b/shard_client/src/global_dedup_table.rs @@ -0,0 +1,235 @@ +use crate::error::{Result, ShardClientError}; +use heed::types::*; +use heed::EnvOpenOptions; +use itertools::Itertools; +use merkledb::aggregate_hashes::with_salt; +use merklehash::MerkleHash; +use std::collections::HashMap; +use std::{path::Path, sync::Arc}; +use tokio::sync::RwLock; +use tracing::{info, warn}; + +type DB = heed::Database, OwnedType>; + +pub struct DiskBasedGlobalDedupTable { + env: heed::Env, + table: RwLock>>, // map of chunk_hash -> shard_hash +} + +// Annoyingly, heed::Error is not Send/Sync, so convert to string. +fn map_db_error(e: heed::Error) -> ShardClientError { + let msg = format!("Global shard dedup database error: {e:?}"); + warn!("{msg}"); + ShardClientError::ShardDedupDBError(msg) +} + +impl DiskBasedGlobalDedupTable { + pub fn open_or_create(path: impl AsRef) -> Result { + let db_path = path.as_ref().join("global_shard_dedup.db"); + info!("Using {db_path:?} as path to global shard dedup database."); + + std::fs::create_dir_all(&db_path)?; + let env = EnvOpenOptions::new() + .max_dbs(32) + .max_readers(32) + .open(&db_path) + .map_err(map_db_error)?; + + Ok(Self { + env, + table: RwLock::new(HashMap::new()), + }) + } + + async fn get_db(&self, prefix: &str) -> Result> { + if let Some(db) = self.table.read().await.get(prefix).cloned() { + return Ok(db); + } + + let mut write_lock = self.table.write().await; + + match write_lock.entry(prefix.to_owned()) { + std::collections::hash_map::Entry::Occupied(db) => Ok(db.get().clone()), + std::collections::hash_map::Entry::Vacant(entry_ref) => { + let db = Arc::new( + self.env + .create_database(Some(prefix)) + .map_err(map_db_error)?, + ); + entry_ref.insert(db.clone()); + Ok(db) + } + } + } + + pub async fn batch_add( + &self, + chunk_hashes: &[MerkleHash], + shard_hash: &MerkleHash, + prefix: &str, + salt: &[u8; 32], + ) -> Result<()> { + let db = self.get_db(prefix).await?; + + let mut write_txn = self.env.write_txn().map_err(map_db_error)?; + + chunk_hashes.iter().for_each(|chunk| { + let maybe_salted_chunk_hash = with_salt(chunk, salt).ok(); + if let Some(salted_chunk_hash) = maybe_salted_chunk_hash { + let _ = db + .put(&mut write_txn, &salted_chunk_hash, shard_hash) + .map_err(map_db_error); // Prints warning for error, otherwise ignores. + } + }); + write_txn.commit().map_err(map_db_error)?; + + Ok(()) + } + + pub async fn query(&self, salted_chunk_hash: &[MerkleHash], prefix: &str) -> Vec { + let Ok(db) = self.get_db(prefix).await else { + return vec![]; + }; + + let Ok(read_txn) = self.env.read_txn().map_err(|e| { + warn!("Error starting read transaction for prefix {prefix}: {e:?}"); + e + }) else { + return vec![]; + }; + + salted_chunk_hash + .iter() + .filter_map(|chunk| db.get(&read_txn, chunk).unwrap_or(None)) + .collect_vec() + } +} + +#[cfg(test)] +mod tests { + use super::DiskBasedGlobalDedupTable; + use itertools::Itertools; + use mdb_shard::shard_format::test_routines::rng_hash; + use merkledb::aggregate_hashes::with_salt; + use rand::{thread_rng, Rng}; + use std::sync::Arc; + use tempfile::TempDir; + + #[tokio::test] + async fn test_basic_insert_retrieval() -> anyhow::Result<()> { + let tempdir = TempDir::new()?; + + let db_file = tempdir.path().join("db"); + + let db = DiskBasedGlobalDedupTable::open_or_create(&db_file)?; + + let mut rng = thread_rng(); + + let prefix = "default"; + let chunk_hash = rng_hash(rng.gen()); + let shard_hash = rng_hash(rng.gen()); + let salt: [u8; 32] = rng.gen(); + + db.batch_add(&[chunk_hash], &shard_hash, "default", &salt) + .await?; + + let query_shard = db.query(&[with_salt(&chunk_hash, &salt)?], prefix).await; + + assert_eq!(query_shard.len(), 1); + assert_eq!(query_shard.first(), Some(&shard_hash)); + + Ok(()) + } + + #[tokio::test] + async fn test_multithread_insert_retrieval() -> anyhow::Result<()> { + let tempdir = TempDir::new()?; + + let db_file = tempdir.path().join("db"); + + let db = Arc::new(DiskBasedGlobalDedupTable::open_or_create(&db_file)?); + + let mut rng = thread_rng(); + let prefix = "default"; + let chunk_hashes = (0..10).map(|_| rng_hash(rng.gen())).collect_vec(); + let shard_hashes = (0..10).map(|_| rng_hash(rng.gen())).collect_vec(); + let salt: [u8; 32] = rng.gen(); + + // insert to the db concurrently + let handles = (0..10) + .map(|i| { + let chunk_hash = chunk_hashes[i]; + let shard_hash = shard_hashes[i]; + let db = db.clone(); + + tokio::spawn(async move { + db.batch_add(&[chunk_hash], &shard_hash, prefix, &salt) + .await + }) + }) + .collect_vec(); + + for h in handles { + let _ = h.await?; + } + + // now examine that inserts succeeded + for i in 0..10 { + let chunk_hash = chunk_hashes[i]; + let shard_hash = shard_hashes[i]; + let query_shard = db.query(&[with_salt(&chunk_hash, &salt)?], prefix).await; + + assert_eq!(query_shard.len(), 1); + assert_eq!(query_shard.first(), Some(&shard_hash)); + } + + Ok(()) + } + + #[tokio::test] + async fn test_multi_db_instance_insert_retrieval() -> anyhow::Result<()> { + let tempdir = TempDir::new()?; + + let db_file = tempdir.path().join("db"); + + let mut rng = thread_rng(); + let prefix = "default"; + let chunk_hashes = (0..1000).map(|_| rng_hash(rng.gen())).collect_vec(); + let shard_hashes = (0..10).map(|_| rng_hash(rng.gen())).collect_vec(); + let salt: [u8; 32] = rng.gen(); + + // insert to the db concurrently + let handles = (0..10) + .map(|i| { + let chunk_hashes = chunk_hashes[i * 100..(i + 1) * 100].to_vec(); + let shard_hash = shard_hashes[i]; + let db_file = db_file.clone(); + + tokio::spawn(async move { + let db = DiskBasedGlobalDedupTable::open_or_create(&db_file).unwrap(); + db.batch_add(&chunk_hashes, &shard_hash, prefix, &salt) + .await + }) + }) + .collect_vec(); + + for h in handles { + let _ = h.await?; + } + + // now examine that inserts succeeded + let db = DiskBasedGlobalDedupTable::open_or_create(&db_file)?; + for i in 0..10 { + let shard_hash = shard_hashes[i]; + + for chunk_hash in &chunk_hashes[i * 100..(i + 1) * 100] { + let query_shard = db.query(&[with_salt(chunk_hash, &salt)?], prefix).await; + + assert_eq!(query_shard.len(), 1); + assert_eq!(query_shard.first(), Some(&shard_hash)); + } + } + + Ok(()) + } +} diff --git a/shard_client/src/lib.rs b/shard_client/src/lib.rs new file mode 100644 index 00000000..c09d0f4c --- /dev/null +++ b/shard_client/src/lib.rs @@ -0,0 +1,86 @@ +#![allow( + unknown_lints, + renamed_and_removed_lints, + clippy::blocks_in_conditions, + clippy::blocks_in_if_conditions +)] +use crate::error::Result; +use async_trait::async_trait; +pub use local_shard_client::LocalShardClient; +use mdb_shard::shard_dedup_probe::ShardDedupProber; +use mdb_shard::shard_file_reconstructor::FileReconstructor; +use merklehash::MerkleHash; +pub use shard_client::GrpcShardClient; +use tracing::info; +// we reexport FileDataSequenceEntry +pub use mdb_shard::file_structs::FileDataSequenceEntry; + +pub mod error; +mod global_dedup_table; +mod local_shard_client; +mod shard_client; + +/// Container for information required to set up and handle +/// Shard connections +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ShardConnectionConfig { + pub endpoint: String, + pub user_id: String, + pub git_xet_version: String, +} + +impl ShardConnectionConfig { + /// creates a new ShardConnectionConfig with given endpoint and user_id + pub fn new(endpoint: String, user_id: String, git_xet_version: String) -> Self { + ShardConnectionConfig { + endpoint, + user_id, + git_xet_version, + } + } +} + +pub trait ShardClientInterface: + RegistrationClient + FileReconstructor + ShardDedupProber + Send + Sync +{ +} + +/// A Client to the Shard service. The shard service +/// provides for +/// 1. the ingestion of Shard information from CAS to the Shard service +/// 2. querying of file->reconstruction information +#[async_trait] +pub trait RegistrationClient { + /// Requests the service to add a shard file currently stored in CAS under the prefix/hash + async fn register_shard_v1(&self, prefix: &str, hash: &MerkleHash, force: bool) -> Result<()>; + + /// Requests the service to add a shard file currently stored in CAS under the prefix/hash, + /// and add chunk->shard information to the global dedup service. + async fn register_shard_with_salt( + &self, + prefix: &str, + hash: &MerkleHash, + force: bool, + salt: &[u8; 32], + ) -> Result<()>; + + async fn register_shard( + &self, + prefix: &str, + hash: &MerkleHash, + force: bool, + salt: &[u8; 32], + ) -> Result<()> { + // Attempts to register a shard using the salted version; if that fails, + // then reverts to the unsalted v1 version. + if let Err(e) = self + .register_shard_with_salt(prefix, hash, force, salt) + .await + { + info!("register_shard: register_shard_with_salt had error {e:?}; reverting to register_shard_v1."); + self.register_shard_v1(prefix, hash, force).await + } else { + Ok(()) + } + } +} diff --git a/shard_client/src/local_shard_client.rs b/shard_client/src/local_shard_client.rs new file mode 100644 index 00000000..14cda627 --- /dev/null +++ b/shard_client/src/local_shard_client.rs @@ -0,0 +1,138 @@ +use async_trait::async_trait; +use cas_client::{Client, LocalClient}; +use itertools::Itertools; +use mdb_shard::error::MDBShardError; +use mdb_shard::file_structs::MDBFileInfo; +use mdb_shard::shard_dedup_probe::ShardDedupProber; +use mdb_shard::{shard_file_reconstructor::FileReconstructor, ShardFileManager}; +use mdb_shard::{MDBShardFile, MDBShardInfo}; +use merkledb::aggregate_hashes::with_salt; +use merklehash::MerkleHash; +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +use crate::error::ShardClientError; +use crate::{ + error::Result, global_dedup_table::DiskBasedGlobalDedupTable, RegistrationClient, + ShardClientInterface, +}; + +/// This creates a persistent local shard client that simulates the shard server. It +/// Is intended to use for testing interactions between local repos that would normally +/// require the use of the remote shard server. +pub struct LocalShardClient { + shard_manager: ShardFileManager, + cas: LocalClient, + shard_directory: PathBuf, + global_dedup: DiskBasedGlobalDedupTable, +} + +impl LocalShardClient { + pub async fn new(cas_directory: &Path) -> Result { + let shard_directory = cas_directory.join("shards"); + if !shard_directory.exists() { + std::fs::create_dir_all(&shard_directory).map_err(|e| { + ShardClientError::Other(format!( + "Error creating local shard directory {shard_directory:?}: {e:?}." + )) + })?; + } + + let shard_manager = ShardFileManager::new(&shard_directory).await?; + shard_manager + .register_shards_by_path(&[&shard_directory], true) + .await?; + + let cas = LocalClient::new(cas_directory, false); + + let global_dedup = DiskBasedGlobalDedupTable::open_or_create( + cas_directory.join("ddb").join("chunk2shard.db"), + )?; + + Ok(LocalShardClient { + shard_manager, + cas, + shard_directory, + global_dedup, + }) + } +} + +#[async_trait] +impl FileReconstructor for LocalShardClient { + /// Query the shard server for the file reconstruction info. + /// Returns the FileInfo for reconstructing the file and the shard ID that + /// defines the file info. + async fn get_file_reconstruction_info( + &self, + file_hash: &MerkleHash, + ) -> mdb_shard::error::Result)>> { + self.shard_manager + .get_file_reconstruction_info(file_hash) + .await + } +} + +#[async_trait] +impl RegistrationClient for LocalShardClient { + async fn register_shard_v1(&self, prefix: &str, hash: &MerkleHash, _force: bool) -> Result<()> { + // Dump the shard from the CAS to the shard directory. Go through the local client to unpack this. + + let shard_data = self.cas.get(prefix, hash).await.map_err(|e| { + ShardClientError::Other(format!( + "Error retrieving shard content from cas for local registration: {e:?}." + )) + })?; + + let shard = MDBShardFile::write_out_from_reader( + &self.shard_directory, + &mut Cursor::new(shard_data), + )?; + + self.shard_manager.register_shards(&[shard], true).await?; + + Ok(()) + } + + async fn register_shard_with_salt( + &self, + prefix: &str, + hash: &MerkleHash, + force: bool, + salt: &[u8; 32], + ) -> Result<()> { + self.register_shard_v1(prefix, hash, force).await?; + + let Some(shard) = self.shard_manager.get_shard_handle(hash, false).await else { + return Err(MDBShardError::ShardNotFound(*hash).into()); + }; + + let mut shard_reader = shard.get_reader()?; + + let chunk_hashes = MDBShardInfo::read_cas_chunks_for_global_dedup(&mut shard_reader)?; + + self.global_dedup + .batch_add(&chunk_hashes, hash, prefix, salt) + .await?; + + Ok(()) + } +} + +#[async_trait] +impl ShardDedupProber for LocalShardClient { + async fn get_dedup_shards( + &self, + prefix: &str, + chunk_hash: &[MerkleHash], + salt: &[u8; 32], + ) -> mdb_shard::error::Result> { + let salted_chunk_hash = chunk_hash + .iter() + .filter_map(|chunk| with_salt(chunk, salt).ok()) + .collect_vec(); + Ok(self.global_dedup.query(&salted_chunk_hash, prefix).await) + } +} + +impl ShardClientInterface for LocalShardClient {} diff --git a/shard_client/src/shard_client.rs b/shard_client/src/shard_client.rs new file mode 100644 index 00000000..44213866 --- /dev/null +++ b/shard_client/src/shard_client.rs @@ -0,0 +1,485 @@ +use async_trait::async_trait; +use http::Uri; +use itertools::Itertools; +use mdb_shard::error::MDBShardError; +use mdb_shard::file_structs::{FileDataSequenceEntry, FileDataSequenceHeader, MDBFileInfo}; +use mdb_shard::shard_dedup_probe::ShardDedupProber; +use mdb_shard::shard_file_reconstructor::FileReconstructor; +use merkledb::aggregate_hashes::with_salt; +use opentelemetry::propagation::{Injector, TextMapPropagator}; +use retry_strategy::RetryStrategy; +use std::env::VarError; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::time::Duration; +use tonic::codegen::InterceptedService; +use tonic::metadata::{Ascii, MetadataKey, MetadataMap, MetadataValue}; +use tonic::service::Interceptor; +use tonic::Response; +use tonic::{transport::Channel, Request, Status}; +use tracing::{debug, info, warn, Span}; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use uuid::Uuid; + +use cas::{ + constants::*, + shard::{ + shard_client::ShardClient, QueryChunkRequest, QueryChunkResponse, QueryFileRequest, + QueryFileResponse, SyncShardRequest, SyncShardResponse, SyncShardWithSaltRequest, + }, +}; +use cas_client::grpc::{ + get_key_for_request, is_status_retriable_and_print, print_final_retry_error, +}; +use merklehash::MerkleHash; + +use crate::{ + error::{Result, ShardClientError}, + RegistrationClient, ShardClientInterface, ShardConnectionConfig, +}; +pub type ShardClientType = ShardClient>; + +const DEFAULT_VERSION: &str = "0.0.0"; + +const HTTP2_KEEPALIVE_TIMEOUT_SEC: u64 = 20; +const HTTP2_KEEPALIVE_INTERVAL_SEC: u64 = 1; +const NUM_RETRIES: usize = 5; +const BASE_RETRY_DELAY_MS: u64 = 3000; + +// production ready settings +const INITIATE_CAS_SCHEME: &str = "https"; +const HTTP_CAS_SCHEME: &str = "http"; + +// up from default 4MB which is not enough for reconstructing _really_ large files +const GRPC_MESSAGE_LIMIT: usize = 256 * 1024 * 1024; + +lazy_static::lazy_static! { + static ref DEFAULT_UUID: Uuid = Uuid::new_v4(); + static ref REQUEST_COUNTER: AtomicUsize = AtomicUsize::new(0); + static ref TRACE_FORWARDING: AtomicBool = AtomicBool::new(false); +} + +async fn get_channel(endpoint: &str) -> anyhow::Result { + debug!("shard client get_channel: server name: {}", endpoint); + let mut server_uri: Uri = endpoint.parse()?; + + // supports an absolute URI (above) or just the host:port (below) + // only used on first endpoint, all other endpoints should come from CAS + // with scheme info already included + // in local/witt modes overriden CAS initial URI should include scheme e.g. + // http://localhost:40000 + if server_uri.scheme().is_none() { + let scheme = if cfg!(test) { + HTTP_CAS_SCHEME + } else { + INITIATE_CAS_SCHEME + }; + server_uri = format!("{scheme}://{endpoint}").parse().unwrap(); + } + + debug!("Server URI: {}", server_uri); + + let channel = Channel::builder(server_uri) + .keep_alive_timeout(Duration::new(HTTP2_KEEPALIVE_TIMEOUT_SEC, 0)) + .http2_keep_alive_interval(Duration::new(HTTP2_KEEPALIVE_INTERVAL_SEC, 0)) + .timeout(Duration::new(GRPC_TIMEOUT_SEC, 0)) + .connect_timeout(Duration::new(GRPC_TIMEOUT_SEC, 0)) + .connect() + .await?; + Ok(channel) +} + +pub async fn get_client(shard_connection_config: ShardConnectionConfig) -> Result { + let endpoint = shard_connection_config.endpoint.as_str(); + + if endpoint.starts_with("local://") { + return Err(ShardClientError::Other( + "Cannot connect to shard client using local:// CAS config.".to_owned(), + )); + } + + let timeout_channel = get_channel(endpoint).await?; + + let client: ShardClientType = ShardClient::with_interceptor( + timeout_channel, + MetadataHeaderInterceptor::new(shard_connection_config), + ) + .max_decoding_message_size(GRPC_MESSAGE_LIMIT) + .max_encoding_message_size(GRPC_MESSAGE_LIMIT); + Ok(client) +} + +/// Adds common metadata headers to all requests. Currently, this includes +/// authorization and xet-user-id. +/// TODO: at some point, we should re-evaluate how we authenticate/authorize requests to CAS. +#[derive(Debug, Clone)] +pub struct MetadataHeaderInterceptor { + config: ShardConnectionConfig, +} + +impl MetadataHeaderInterceptor { + fn new(config: ShardConnectionConfig) -> MetadataHeaderInterceptor { + MetadataHeaderInterceptor { config } + } +} + +impl Interceptor for MetadataHeaderInterceptor { + // note original Interceptor trait accepts non-mut request + // but may accept mut request like in this case + fn call(&mut self, mut request: Request<()>) -> std::result::Result, Status> { + request.set_timeout(Duration::new(GRPC_TIMEOUT_SEC, 0)); + let metadata = request.metadata_mut(); + + let token = MetadataValue::from_static("Bearer some-secret-token"); + metadata.insert(AUTHORIZATION_HEADER, token); + + // pass user_id and repo_paths received from xetconfig + let user_id = get_metadata_ascii_from_str_with_default(&self.config.user_id, DEFAULT_USER); + metadata.insert(USER_ID_HEADER, user_id); + + let git_xet_version = + get_metadata_ascii_from_str_with_default(&self.config.git_xet_version, DEFAULT_VERSION); + metadata.insert(GIT_XET_VERSION_HEADER, git_xet_version); + + let cas_protocol_version: MetadataValue = + MetadataValue::from_static(&cas_client::CAS_PROTOCOL_VERSION); + metadata.insert(CAS_PROTOCOL_VERSION_HEADER, cas_protocol_version); + + // propagate tracing context (e.g. trace_id, span_id) to service + if trace_forwarding() { + let mut injector = HeaderInjector(metadata); + let propagator = opentelemetry_jaeger::Propagator::new(); + let cur_span = Span::current(); + let ctx = cur_span.context(); + propagator.inject_context(&ctx, &mut injector); + } + + let request_id = get_request_id(); + metadata.insert( + REQUEST_ID_HEADER, + MetadataValue::from_str(&request_id).unwrap(), + ); + + Ok(request) + } +} + +pub fn _set_trace_forwarding(should_enable: bool) { + TRACE_FORWARDING.store(should_enable, Ordering::Relaxed); +} + +pub fn trace_forwarding() -> bool { + TRACE_FORWARDING.load(Ordering::Relaxed) +} + +pub struct HeaderInjector<'a>(pub &'a mut MetadataMap); + +impl<'a> Injector for HeaderInjector<'a> { + /// Set a key and value in the HeaderMap. Does nothing if the key or value are not valid inputs. + fn set(&mut self, key: &str, value: String) { + if let Ok(name) = MetadataKey::from_str(key) { + if let Ok(val) = MetadataValue::from_str(&value) { + self.0.insert(name, val); + } + } + } +} + +fn get_metadata_ascii_from_str_with_default( + value: &str, + default: &'static str, +) -> MetadataValue { + MetadataValue::from_str(value) + .map_err(|_| VarError::NotPresent) + .unwrap_or_else(|_| MetadataValue::from_static(default)) +} + +pub fn get_request_id() -> String { + format!( + "{}.{}", + *DEFAULT_UUID, + REQUEST_COUNTER.load(Ordering::Relaxed) + ) +} + +fn inc_request_id() { + REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed); +} + +/// CAS Client that uses GRPC for communication. +/// +/// ## Implementation note +/// The GrpcClient is thread-safe and allows multiplexing requests on the +/// underlying gRPC connection. This is done by cheaply cloning the client: +/// https://docs.rs/tonic/0.1.0/tonic/transport/struct.Channel.html#multiplexing-requests +#[derive(Debug)] +pub struct GrpcShardClient { + pub endpoint: String, + client: ShardClientType, + retry_strategy: RetryStrategy, +} + +impl Clone for GrpcShardClient { + fn clone(&self) -> Self { + Self { + endpoint: self.endpoint.clone(), + client: self.client.clone(), + retry_strategy: self.retry_strategy, + } + } +} + +impl GrpcShardClient { + pub fn new(endpoint: String, client: ShardClientType, retry_strategy: RetryStrategy) -> Self { + Self { + endpoint, + client, + retry_strategy, + } + } + + pub async fn from_config( + shard_connection_config: ShardConnectionConfig, + ) -> Result { + debug!("Creating GrpcShardClient from config: {shard_connection_config:?}"); + let endpoint = shard_connection_config.endpoint.clone(); + let client: ShardClientType = get_client(shard_connection_config).await?; + // Retry policy: Exponential backoff starting at BASE_RETRY_DELAY_MS and retrying NUM_RETRIES times + let retry_strategy = RetryStrategy::new(NUM_RETRIES, BASE_RETRY_DELAY_MS); + Ok(GrpcShardClient::new(endpoint, client, retry_strategy)) + } +} + +#[async_trait] +impl RegistrationClient for GrpcShardClient { + #[tracing::instrument(skip_all, name = "shard.client", err, fields(prefix = prefix, hash = hash.hex().as_str(), api = "register_shard", request_id = tracing::field::Empty))] + async fn register_shard_v1(&self, prefix: &str, hash: &MerkleHash, force: bool) -> Result<()> { + info!("Registering shard {prefix}/{hash:?}"); + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + debug!( + "GrpcShardClient Req {}: register {}/{} as shard", + get_request_id(), + prefix, + hash, + ); + let request = SyncShardRequest { + key: Some(get_key_for_request(prefix, hash)), + force_sync: force, + }; + + let response: Response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + debug!("GrpcShardClient register_shard: Attemping call to sync_shard, req = {req:?})"); + self.client.clone().sync_shard(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| { + warn!( + "GrpcShardClient Req {}: Error on shard register {}/{:?} : {:?}", + get_request_id(), + prefix, + hash, + e + ); + ShardClientError::GrpcClientError(anyhow::Error::from(e)) + })?; + + // It appears that both exists and sync_performed achieve the correct results. + if response.into_inner().response == 0 + /*SyncShardResponseType::Exists */ + { + info!("Shard {prefix:?}/{hash:?} already synced; skipping."); + } else { + info!("Shard {prefix:?}/{hash:?} synced."); + } + Ok(()) + } + + #[tracing::instrument(skip_all, name = "shard.client", err, fields(prefix = prefix, hash = format!("{hash}"), salt = format!("{salt:x?}"), api = "register_shard_with_salt", request_id = tracing::field::Empty))] + async fn register_shard_with_salt( + &self, + prefix: &str, + hash: &MerkleHash, + force: bool, + salt: &[u8; 32], + ) -> Result<()> { + info!("Registering shard {prefix}/{hash} (w/ salt)"); + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + debug!( + "GrpcShardClient Req {}: register {prefix}/{hash} as shard", + get_request_id(), + ); + let request = SyncShardWithSaltRequest { + ssr: Some(SyncShardRequest { + key: Some(get_key_for_request(prefix, hash)), + force_sync: force, + }), + salt: salt.to_vec(), + }; + + let response: Response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + debug!("GrpcShardClient register_shard_with_salt: Attemping call to sync_shard_with_salt, req = {req:?})"); + self.client.clone().sync_shard_with_salt(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| { + warn!( + "GrpcShardClient Req {}: Error on shard register {prefix}/{hash} with salt {salt:x?} : {e:?}", + get_request_id(), + ); + ShardClientError::GrpcClientError(anyhow::Error::from(e)) + })?; + + // It appears that both exists and sync_performed achieve the correct results. + if response.into_inner().response == 0 + /*SyncShardResponseType::Exists */ + { + info!("Shard {prefix}/{hash} already synced; skipping."); + } else { + info!("Shard {prefix}/{hash} synced with global dedup."); + } + Ok(()) + } +} + +#[async_trait] +impl FileReconstructor for GrpcShardClient { + /// Query the shard server for the file reconstruction info. + /// Returns the FileInfo for reconstructing the file and the shard ID that + /// defines the file info. + /// + /// TODO: record the shards that are + #[tracing::instrument(skip_all, name = "shard.client", err, fields(file_hash = file_hash.hex().as_str(), api = "get_file_reconstruction_info", request_id = tracing::field::Empty))] + async fn get_file_reconstruction_info( + &self, + file_hash: &MerkleHash, + ) -> mdb_shard::error::Result)>> { + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + debug!( + "GrpcShardClient Req {}. get_file_reconstruction_info for fileid of {}", + get_request_id(), + file_hash + ); + let request = QueryFileRequest { + file_id: file_hash.into(), + }; + let response: Response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + self.client.clone().query_file(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| { + warn!( + "GrpcShardClient Req {}. Error on get_reconstruction {} : {:?}", + get_request_id(), + file_hash, + e + ); + MDBShardError::GrpcClientError(anyhow::Error::from(e)) + })?; + + let response_info = response.into_inner(); + + Ok(Some(( + MDBFileInfo { + metadata: FileDataSequenceHeader::new( + *file_hash, + response_info.reconstruction.len(), + ), + segments: response_info + .reconstruction + .into_iter() + .map(|ce| { + FileDataSequenceEntry::new( + ce.cas_id[..].try_into().unwrap(), + ce.unpacked_length, + ce.range.as_ref().unwrap().start, + ce.range.as_ref().unwrap().end, + ) + }) + .collect(), + }, + response_info + .shard_id + .and_then(|k| MerkleHash::try_from(&k.hash[..]).ok()), + ))) + } +} + +#[async_trait] +impl ShardDedupProber for GrpcShardClient { + #[tracing::instrument(skip_all, name = "shard.client", err, fields(prefix = prefix, chunk_hash = format!("{chunk_hash:?}"), api = "register_shard_with_salt", request_id = tracing::field::Empty))] + async fn get_dedup_shards( + &self, + prefix: &str, + chunk_hash: &[MerkleHash], + salt: &[u8; 32], + ) -> mdb_shard::error::Result> { + inc_request_id(); + Span::current().record("request_id", &get_request_id()); + debug!( + "GrpcShardClient Req {}. get_dedup_shards for chunk hashes {prefix} / {chunk_hash:?}", + get_request_id(), + ); + let request = QueryChunkRequest { + prefix: prefix.into(), + chunk: chunk_hash + .iter() + .filter_map(|chunk| with_salt(chunk, salt).ok()) + .map(|salted_chunk| salted_chunk.as_bytes().to_vec()) + .collect_vec(), + }; + let response: Response = self + .retry_strategy + .retry( + || async { + let req = Request::new(request.clone()); + self.client.clone().query_chunk(req).await + }, + is_status_retriable_and_print, + ) + .await + .map_err(print_final_retry_error) + .map_err(|e| { + warn!( + "GrpcShardClient Req {}. Error on get_dedup_shards {prefix} / {chunk_hash:?} : {e:?}", + get_request_id(), + ); + MDBShardError::GrpcClientError(anyhow::Error::from(e)) + })?; + + let response_info = response.into_inner(); + + Ok(response_info + .shard + .iter() + .flat_map(|hash| MerkleHash::try_from(&hash[..])) + .collect_vec()) + } +} + +impl ShardClientInterface for GrpcShardClient {} + +// TODO: copy tests from grpc diff --git a/utils/Cargo.toml b/utils/Cargo.toml new file mode 100644 index 00000000..9542bd03 --- /dev/null +++ b/utils/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "utils" +version = "0.14.5" +edition = "2021" + +[lib] +name = "cas" +path = "src/lib.rs" + +[dependencies] +tonic = {version = "0.10.2", features = ["tls", "tls-roots", "transport"] } +prost = "0.12.3" +prost-types = "0.12.3" +serde = {version = "1.0", features = ["derive"] } +merklehash = { path = "../merklehash"} +xet_error = {path = "../xet_error"} +futures = "0.3.28" +tempfile = "3.9.0" + +# singleflight +tokio = {version = "1", features = ["sync"] } +hashbrown = "0.12.0" +parking_lot = "0.11" +anyhow = "1" +pin-project = "1.0.12" + +# consistenthash +hashring = "0.3.0" +tracing = "0.1.31" +chrono = "0.4" +lazy_static = "1.4.0" +regex = "1.7.3" + +[build-dependencies] +tonic-build = {version= "0.10.2", features=["transport"]} + +[dev-dependencies] +tokio = { version = "1.36", features = ["full"] } +futures = "0.3.21" +clap = { version = "3.1.6", features = ["derive"] } +http = "0.2.5" +rand = "0.5" +itertools = "0.10" + +[[example]] +name = "infra" + +[features] +strict = [] diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 00000000..409997ce --- /dev/null +++ b/utils/README.md @@ -0,0 +1,18 @@ +# Proto +Directory where gproto files will be created + +# Operational helpers +- Logs, metrics and traces +- Configuration +- Access to AWS services (e.g. S3) + +# Examples +## Identify which cas_server owns a particular key +``` +cargo run --example infra -- --server-name cas-lb.xetbeta.com:5000 --key bar +Host: 35.89.208.89 +Load Stats: SystemStatus { timestamp: "2022-07-06T19:15:00Z", cpu_utilization: 0.3416666833712037 } +Host: 54.245.178.249 +Load Stats: SystemStatus { timestamp: "2022-07-06T19:15:00Z", cpu_utilization: 0.2943333333333333 } +Key bar gets hashed to server "54.245.178.249" +``` diff --git a/utils/build.rs b/utils/build.rs new file mode 100644 index 00000000..2a073e56 --- /dev/null +++ b/utils/build.rs @@ -0,0 +1,8 @@ +fn main() -> Result<(), Box> { + tonic_build::configure().compile(&["proto/common.proto"], &["proto"])?; + tonic_build::configure().compile(&["proto/cas.proto"], &["proto"])?; + tonic_build::configure().compile(&["proto/infra.proto"], &["proto"])?; + tonic_build::configure().compile(&["proto/alb.proto"], &["proto"])?; + tonic_build::configure().compile(&["proto/shard.proto"], &["proto"])?; + Ok(()) +} diff --git a/utils/examples/infra.rs b/utils/examples/infra.rs new file mode 100644 index 00000000..d44515f3 --- /dev/null +++ b/utils/examples/infra.rs @@ -0,0 +1,54 @@ +use cas::common::Empty; +use cas::consistenthash::ConsistentHash; +use cas::infra::infra_utils_client::InfraUtilsClient; +use clap::Parser; +use http::Uri; +use tonic::transport::Channel; + +pub type InfraUtilsClientType = InfraUtilsClient; +pub async fn get_infra_client(server_name: &str) -> anyhow::Result { + let mut server_uri: Uri = server_name.parse()?; + + // supports an absolute URI (above) or just the host:port (below) + if server_uri.scheme().is_none() { + server_uri = format!("https://{}", server_name).parse().unwrap(); + } + + let channel = Channel::builder(server_uri).connect().await?; + Ok(InfraUtilsClient::new(channel)) +} + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(short, long, value_parser)] + server_name: String, + + #[clap(short, long, value_parser)] + key: Option, +} +#[tokio::main] +async fn main() { + let args = Args::parse(); + let mut client = get_infra_client(&args.server_name).await.unwrap(); + let request = Empty {}; + let response = client.endpoint_load(request).await.unwrap(); + if let Some(key) = args.key { + let ch = ConsistentHash::new(response.into_inner().responses).unwrap(); + + println!( + "Key {} gets hashed to server {:?}", + &key, + ch.server(&key).unwrap() + ); + return; + } + // when no key is specified, print out the entire response + for load_status in response.into_inner().responses.into_iter() { + if let Some(load_stat) = load_status.status { + println!("Host: {}", &load_status.address); + println!("Load Stats: {:?}", load_stat); + } + } +} diff --git a/utils/proto/alb.proto b/utils/proto/alb.proto new file mode 100644 index 00000000..87fd8662 --- /dev/null +++ b/utils/proto/alb.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; +package AWS; +import public "common.proto"; + +service ALB { + rpc healthcheck(common.Empty) returns (common.Empty); +} diff --git a/utils/proto/cas.proto b/utils/proto/cas.proto new file mode 100644 index 00000000..e6a7daee --- /dev/null +++ b/utils/proto/cas.proto @@ -0,0 +1,81 @@ +/* + * https://www.notion.so/cantorsystems/GlodHub-Client-Architecture-0992cff0f95e4203bf7763e9951f1fe8 + */ +syntax = "proto3"; +package cas; +import public "common.proto"; + +// The CAS (Content Addressed Storage) service. +service Cas { + // Initiates uploads of an object. + rpc Initiate(common.InitiateRequest) returns (common.InitiateResponse); + + // Uploads the provided bytes to an object. + // This will also verify the hash is correct. + rpc Put(PutRequest) returns (PutResponse); + + // Completes uploads of an object. This verifies that hash is correct. + rpc PutComplete(PutCompleteRequest) returns (PutCompleteResponse); + + + // Downloads all bytes for the indicated object. + rpc Get(GetRequest) returns (GetResponse); + + + // Downloads a set of ranges within an object. + rpc GetRange(GetRangeRequest) returns (GetRangeResponse); + + // Retrieve metadata about a particular object. + rpc Head(HeadRequest) returns (HeadResponse); +} + + + +message PutRequest { + common.Key key = 1; + bytes data = 2; + repeated uint64 chunk_boundaries = 3; +} + +message PutResponse { + bool was_inserted = 1; +} + + +message PutCompleteRequest { + common.Key key = 1; + repeated uint64 chunk_boundaries = 2; +} + +message PutCompleteResponse { + bool was_inserted = 1; +} + +message GetRequest { + common.Key key = 1; +} + +message GetResponse { + bytes data = 1; +} +message GetRangeRequest { + common.Key key = 1; + repeated Range ranges = 2; +} + +message GetRangeResponse { + repeated bytes data = 1; +} + +message HeadRequest { + common.Key key = 1; +} + +message HeadResponse { + uint64 size = 1; +} + +message Range { + uint64 start = 1; + uint64 end = 2; +} diff --git a/utils/proto/common.proto b/utils/proto/common.proto new file mode 100644 index 00000000..1d438d85 --- /dev/null +++ b/utils/proto/common.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; +package common; +option go_package = "proto/"; +import "google/protobuf/timestamp.proto"; +message Key { + string prefix = 1; + bytes hash = 2; +} + +message InitiateRequest { + common.Key key = 1; + uint64 payload_size = 2; +} + +enum CompressionScheme { + NONE = 0; + LZ4 = 1; +} + +enum Scheme { + HTTP = 0; + HTTPS = 1; +} + +message EndpointConfig { + string host = 1; + int32 port = 2; + Scheme scheme = 3; + string root_ca_certificate = 4; +} + +message InitiateResponse { + // filled but deprecated in v0.2.0 clients should use host in endpoint config + string cas_hostname = 1; + + EndpointConfig data_plane_endpoint = 2; + EndpointConfig put_complete_endpoint = 3; + + repeated CompressionScheme accepted_encodings = 4; +} + +message Empty { +} diff --git a/utils/proto/infra.proto b/utils/proto/infra.proto new file mode 100644 index 00000000..02fad5bb --- /dev/null +++ b/utils/proto/infra.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; +package infra; +import public "common.proto"; + +service InfraUtils { + rpc EndpointLoad(common.Empty) returns (EndpointLoadResponse); + // Initiates uploads of an object. + rpc Initiate(common.InitiateRequest) returns (common.InitiateResponse); +} + +message SystemStatus { + string timestamp = 1; + double cpu_utilization = 2; +} +message LoadStatus { + string address = 1; + SystemStatus status = 2; +} + +message EndpointLoadResponse { + repeated LoadStatus responses = 1; +} diff --git a/utils/proto/shard.proto b/utils/proto/shard.proto new file mode 100644 index 00000000..644533fc --- /dev/null +++ b/utils/proto/shard.proto @@ -0,0 +1,69 @@ +/* + * https://www.notion.so/xethub/MerkleDBv2-Xet-CLI-Architecture-62c3177c92834864883bd3fa442feadc + * https://www.notion.so/xethub/MerkleDBv2-The-Final-Stage-cc654b5266294d399503c3431131fafa + */ +syntax = "proto3"; +package shard; +import public "common.proto"; + +// The Shard service. +service Shard { + // Queries for file->shard information. + rpc QueryFile(QueryFileRequest) returns (QueryFileResponse); + + // Synchronizes a shard from CAS to the Shard Service for querying + rpc SyncShard(SyncShardRequest) returns (SyncShardResponse); + + // Queries for chunk->shard information. + rpc QueryChunk(QueryChunkRequest) returns (QueryChunkResponse); + + // SyncShard + synchronizes chunk->shard information to the Shard Service + rpc SyncShardWithSalt(SyncShardWithSaltRequest) returns (SyncShardResponse); +} +message QueryFileRequest { + bytes file_id = 1; +} + +message Range { + uint64 start = 1; + uint64 end = 2; +} + +message CASReconstructionTerm { + bytes cas_id = 1; + uint64 unpacked_length = 2; + Range range = 3; +} + +message QueryFileResponse { + repeated CASReconstructionTerm reconstruction = 1; + common.Key shard_id = 2; +} + +message SyncShardRequest { + common.Key key = 1; + bool force_sync = 2; +} + +enum SyncShardResponseType { + Exists = 0; + SyncPerformed = 1; +} + +message SyncShardResponse { + SyncShardResponseType response = 1; +} + +message QueryChunkRequest { + string prefix = 1; + repeated bytes chunk = 2; +} + +message QueryChunkResponse { + repeated bytes shard = 1; +} + +message SyncShardWithSaltRequest { + SyncShardRequest ssr = 1; + bytes salt = 2; +} diff --git a/utils/src/compression.rs b/utils/src/compression.rs new file mode 100644 index 00000000..40f7fbe7 --- /dev/null +++ b/utils/src/compression.rs @@ -0,0 +1,98 @@ +use crate::common::CompressionScheme; +use anyhow::anyhow; +use std::str::FromStr; + +pub const CAS_CONTENT_ENCODING_HEADER: &str = "xet-cas-content-encoding"; +pub const CAS_ACCEPT_ENCODING_HEADER: &str = "xet-cas-content-encoding"; +pub const CAS_INFLATED_SIZE_HEADER: &str = "xet-cas-inflated-size"; + +// officially speaking, string representations of the CompressionScheme enum values +// are dictated by prost for generating `as_str_name` and `from_str_name`, and since +// we cannot guarantee no one will use them, we will accept them as the string +// representations instead of CompressionScheme. +// These functions follow protobuf style, so the output strings are uppercase snake case, +// however we will still try to convert lower case/mixed case to CompressionScheme +// as well as count the empty string as CompressionScheme::None. +// we will also attempt to avoid as_str_name/from_str_name in favor of more rusty +// trait usage (From/FromStr) +impl From<&CompressionScheme> for &'static str { + fn from(value: &CompressionScheme) -> Self { + value.as_str_name() + } +} + +impl From for &'static str { + fn from(value: CompressionScheme) -> Self { + From::from(&value) + } +} + +impl FromStr for CompressionScheme { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Ok(CompressionScheme::None); + } + Self::from_str_name(s.to_uppercase().as_str()) + .ok_or_else(|| anyhow!("could not convert &str to CompressionScheme")) + } +} + +// in the header value, we will consider +pub fn multiple_accepted_encoding_header_value(list: Vec) -> String { + let as_strs: Vec<&str> = list.iter().map(Into::into).collect(); + as_strs.join(";").to_string() +} + +#[cfg(test)] +mod tests { + use crate::compression::{multiple_accepted_encoding_header_value, CompressionScheme}; + use std::str::FromStr; + + #[test] + fn test_from_str() { + assert_eq!( + CompressionScheme::from_str("LZ4").unwrap(), + CompressionScheme::Lz4 + ); + assert_eq!( + CompressionScheme::from_str("NONE").unwrap(), + CompressionScheme::None + ); + assert_eq!( + CompressionScheme::from_str("NoNE").unwrap(), + CompressionScheme::None + ); + assert_eq!( + CompressionScheme::from_str("none").unwrap(), + CompressionScheme::None + ); + assert_eq!( + CompressionScheme::from_str("").unwrap(), + CompressionScheme::None + ); + assert!(CompressionScheme::from_str("not-scheme").is_err()); + } + + #[test] + fn test_to_str() { + assert_eq!(Into::<&str>::into(CompressionScheme::Lz4), "LZ4"); + assert_eq!(Into::<&str>::into(CompressionScheme::None), "NONE"); + } + + #[test] + fn test_multiple_accepted_encoding_header_value() { + let multi = vec![CompressionScheme::Lz4, CompressionScheme::None]; + assert_eq!( + multiple_accepted_encoding_header_value(multi), + "LZ4;NONE".to_string() + ); + + let singular = vec![CompressionScheme::Lz4]; + assert_eq!( + multiple_accepted_encoding_header_value(singular), + "LZ4".to_string() + ); + } +} diff --git a/utils/src/consistenthash.rs b/utils/src/consistenthash.rs new file mode 100644 index 00000000..843bda0f --- /dev/null +++ b/utils/src/consistenthash.rs @@ -0,0 +1,160 @@ +use crate::infra::LoadStatus; +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Utc}; +use hashring::HashRing; +use tracing::debug; + +#[derive(Debug, Clone, Hash, PartialEq)] +struct VNode { + addr: String, +} + +impl VNode { + fn new(ip: &str) -> Self { + VNode { + addr: ip.to_string(), + } + } +} + +pub struct ConsistentHash { + ring: HashRing, + pub ts: DateTime, +} + +impl ConsistentHash { + pub fn new(load_status_vec: Vec) -> Result { + let mut ring: HashRing = HashRing::new(); + let mut oldest_ts = Utc::now(); + let mut valid_hosts = 0; + for load_status in &load_status_vec { + if let Some(load_stat) = &load_status.status { + debug!("Host: {}", &load_status.address); + debug!("Load Stats: {:?}", load_stat); + let ts = DateTime::parse_from_rfc3339(&load_stat.timestamp)?.with_timezone(&Utc); + if oldest_ts > ts { + oldest_ts = ts; + } + ring.add(VNode::new(&load_status.address)); + valid_hosts += 1; + } + } + if valid_hosts == 0 { + return Err(anyhow!( + "Unable to create ConsistentHash with empty host set {:?}", + load_status_vec + )); + } + Ok(Self { + ring, + ts: oldest_ts, + }) + } + + pub fn server(&self, key: &str) -> Option { + self.ring.get(&key).map(|val| val.addr.to_string()) + } +} +#[cfg(test)] +mod tests { + use crate::consistenthash::ConsistentHash; + use crate::infra::{LoadStatus, SystemStatus}; + use chrono::{FixedOffset, TimeZone}; + use itertools::Itertools; + use rand::SeedableRng; + use rand::{distributions::Alphanumeric, Rng}; + use std::collections::HashMap; + #[test] + fn test_empty_errors() { + let res = ConsistentHash::new(vec![]); + assert!(res.is_err()); + + let infra_input = vec![LoadStatus { + address: "localhost".to_string(), + status: None, + }]; + let res = ConsistentHash::new(infra_input); + assert!(res.is_err()); + } + + #[test] + fn test_time_parsing() { + let infra_input = vec![LoadStatus { + address: "localhost".to_string(), + status: Some(SystemStatus { + timestamp: "junk".to_string(), + cpu_utilization: 1.0, + }), + }]; + let res = ConsistentHash::new(infra_input); + assert!(res.is_err()); + + let infra_input = vec![ + LoadStatus { + address: "1.1.1.1".to_string(), + status: Some(SystemStatus { + timestamp: "2022-07-06T19:15:00Z".to_string(), + cpu_utilization: 1.0, + }), + }, + LoadStatus { + address: "2.1.1.1".to_string(), + status: Some(SystemStatus { + timestamp: "2022-07-07T19:15:00Z".to_string(), + cpu_utilization: 1.0, + }), + }, + ]; + let res = ConsistentHash::new(infra_input); + assert!(res.is_ok()); + let res = res.unwrap(); + assert_eq!( + res.ts, + FixedOffset::east_opt(0) + .unwrap() + .with_ymd_and_hms(2022, 7, 6, 19, 15, 0) + .unwrap() + ); + } + #[test] + fn test_distribution() { + let infra_input = vec![ + LoadStatus { + address: "35.89.208.89".to_string(), + status: Some(SystemStatus { + timestamp: "2022-07-06T19:15:00Z".to_string(), + cpu_utilization: 1.0, + }), + }, + LoadStatus { + address: "54.245.178.249".to_string(), + status: Some(SystemStatus { + timestamp: "2022-07-07T19:15:00Z".to_string(), + cpu_utilization: 1.0, + }), + }, + ]; + let ch = ConsistentHash::new(infra_input).unwrap(); + let mut rng = rand::rngs::SmallRng::from_seed([ + 40, 219, 206, 212, 254, 181, 162, 148, 15, 114, 37, 56, 217, 149, 76, 254, + ]); + let server_counts: HashMap = (0..20) + .map(|_| { + let key: String = rng + .sample_iter(&Alphanumeric) + .take(7) + .map(char::from) + .collect(); + ch.server(&key).unwrap() + }) + .counts(); + // wide range for testing, I am seeing typical outputs like + // Server is 35.89.208.89 and count is 11 + // Server is 54.245.178.249 and count is 9 + let expected_range = 3..18; + for (server, count) in server_counts.iter() { + println!("Server is {} and count is {}", server, count); + assert!(expected_range.contains(count)); + } + } +} diff --git a/utils/src/constants.rs b/utils/src/constants.rs new file mode 100644 index 00000000..4e255ec7 --- /dev/null +++ b/utils/src/constants.rs @@ -0,0 +1,17 @@ +// This file holds constants used by the server and client. +pub const AUTHORIZATION_HEADER: &str = "authorization"; +pub const USER_ID_HEADER: &str = "xet-user-id"; +pub const AUTH_HEADER: &str = "xet-auth"; +pub const REPO_PATHS_HEADER: &str = "xet-repo-paths-bin"; +pub const REQUEST_ID_HEADER: &str = "xet-request-id"; +pub const GIT_XET_VERSION_HEADER: &str = "xet-version"; +pub const TRACE_ID_HEADER: &str = "uber-trace-id"; +pub const CONTENT_TYPE_HEADER: &str = "Content-Type"; +pub const JSON_CONTENT_TYPE: &str = "application/json"; +pub const CAS_PROTOCOL_VERSION_HEADER: &str = "xet-cas-protocol-version"; +pub const CLIENT_IP_HEADER: &str = "x-forwarded-for"; +pub const UNKNOWN_IP: &str = "0.0.0.0"; +pub const DEFAULT_USER: &str = "anonymous"; +pub const DEFAULT_AUTH: &str = "unknown"; +pub const DEFAULT_VERSION: &str = "0.0.0"; +pub const GRPC_TIMEOUT_SEC: u64 = 60; diff --git a/utils/src/errors.rs b/utils/src/errors.rs new file mode 100644 index 00000000..d27be68a --- /dev/null +++ b/utils/src/errors.rs @@ -0,0 +1,54 @@ +use xet_error::Error; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum KeyError { + #[error("Key parsing failure: {0}")] + UnparsableKey(String), +} + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum SingleflightError +where + E: Send + std::fmt::Debug + Sync, +{ + #[error("BUG: singleflight waiter was notified before result was updated")] + NoResult, + + #[error("BUG: call was removed before singleflight owner could update it")] + CallMissing, + + #[error("BUG: call didn't create a Notifier for the initial task")] + NoNotifierCreated, + + #[error(transparent)] + InternalError(#[from] E), + + #[error("Real call failed: {0}")] + WaiterInternalError(String), + + #[error("JoinError inside singleflight owner task: {0}")] + JoinError(String), + + #[error("Owner task panicked")] + OwnerPanicked, +} + +impl Clone for SingleflightError { + fn clone(&self) -> Self { + match self { + SingleflightError::NoResult => SingleflightError::NoResult, + SingleflightError::CallMissing => SingleflightError::CallMissing, + SingleflightError::NoNotifierCreated => SingleflightError::NoNotifierCreated, + SingleflightError::InternalError(e) => { + SingleflightError::WaiterInternalError(format!("{e:?}")) + } + SingleflightError::WaiterInternalError(s) => { + SingleflightError::WaiterInternalError(s.clone()) + } + SingleflightError::JoinError(e) => SingleflightError::JoinError(e.clone()), + SingleflightError::OwnerPanicked => SingleflightError::OwnerPanicked, + } + } +} diff --git a/utils/src/gitbaretools.rs b/utils/src/gitbaretools.rs new file mode 100644 index 00000000..21ed4a92 --- /dev/null +++ b/utils/src/gitbaretools.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Action { + pub action: String, + pub file_path: String, + #[serde(default)] + pub previous_path: String, + #[serde(default)] + pub execute_filemode: bool, + #[serde(default)] + pub content: String, +} + +/// JSON descriptions of the manifest +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JSONCommand { + pub author_name: String, + pub author_email: String, + pub branch: String, + pub commit_message: String, + #[serde(default)] + pub create_ref: bool, + pub actions: Vec, +} diff --git a/utils/src/key.rs b/utils/src/key.rs new file mode 100644 index 00000000..95126167 --- /dev/null +++ b/utils/src/key.rs @@ -0,0 +1,122 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +use merklehash::MerkleHash; + +use crate::errors::KeyError; + +/// A Key indicates a prefixed merkle hash for some data stored in the CAS DB. +#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Ord, PartialOrd, Eq, Hash, Clone)] +pub struct Key { + pub prefix: String, + pub hash: MerkleHash, +} + +impl TryFrom<&crate::common::Key> for Key { + type Error = KeyError; + + fn try_from(proto_key: &crate::common::Key) -> Result { + let hash = MerkleHash::try_from(proto_key.hash.as_slice()) + .map_err(|e| KeyError::UnparsableKey(format!("{e:?}")))?; + Ok(Key { + prefix: proto_key.prefix.clone(), + hash, + }) + } +} + +impl Display for Key { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{:x}", self.prefix, self.hash) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_proto() { + let proto = crate::common::Key { + prefix: "abc".to_string(), + hash: [1u8; 32].to_vec(), + }; + let key = Key::try_from(&proto).unwrap(); + assert_eq!(proto.prefix, key.prefix); + assert_eq!(&proto.hash, key.hash.as_bytes()); + } + + #[test] + fn test_from_invalid_proto() { + let proto = crate::common::Key { + prefix: "".to_string(), + hash: vec![1, 2, 3], + }; + let res = Key::try_from(&proto); + assert!(res.is_err()); + assert!(matches!(res, Err(KeyError::UnparsableKey(..)))) + } + + #[test] + fn test_display() { + let proto = crate::common::Key { + prefix: "abc".to_string(), + hash: [1u8; 32].to_vec(), + }; + let key = Key::try_from(&proto).unwrap(); + assert_eq!( + "abc/0101010101010101010101010101010101010101010101010101010101010101", + key.to_string() + ); + } + + #[test] + fn test_equality() { + let proto1 = crate::common::Key { + prefix: "abc".to_string(), + hash: [1u8; 32].to_vec(), + }; + let key1 = Key::try_from(&proto1).unwrap(); + assert_eq!(key1, key1); + + let proto2 = crate::common::Key { + prefix: "abc".to_string(), + hash: [1u8; 32].to_vec(), + }; + let key2 = Key::try_from(&proto2).unwrap(); + assert_eq!(key2, key1); + assert_eq!(key1, key2); // symmetry + + let proto3 = crate::common::Key { + prefix: "abc".to_string(), + hash: [1u8; 32].to_vec(), + }; + let key3 = Key::try_from(&proto3).unwrap(); + assert_eq!(key1, key3); + assert_eq!(key2, key3); // transitive + } + + #[test] + fn test_inequality() { + let proto1 = crate::common::Key { + prefix: "abc".to_string(), + hash: [1u8; 32].to_vec(), + }; + let key1 = Key::try_from(&proto1).unwrap(); + + let proto2 = crate::common::Key { + prefix: "def".to_string(), + hash: [1u8; 32].to_vec(), + }; + let key2 = Key::try_from(&proto2).unwrap(); + assert_ne!(key2, key1); + + let proto3 = crate::common::Key { + prefix: "abc".to_string(), + hash: [2u8; 32].to_vec(), + }; + let key3 = Key::try_from(&proto3).unwrap(); + assert_ne!(key1, key3); + assert_ne!(key2, key3); + } +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs new file mode 100644 index 00000000..f6f1c6b2 --- /dev/null +++ b/utils/src/lib.rs @@ -0,0 +1,155 @@ +#![cfg_attr(feature = "strict", deny(warnings))] + +use std::ops::Range; + +use tonic::Status; + +// The auto code generated by tonic has clippy warnings. Disabling until those are +// resolved in tonic/prost. +pub mod common { + #![allow(clippy::derive_partial_eq_without_eq)] + tonic::include_proto!("common"); +} + +pub mod cas { + #![allow(clippy::derive_partial_eq_without_eq)] + tonic::include_proto!("cas"); +} + +pub mod infra { + #![allow(clippy::derive_partial_eq_without_eq)] + tonic::include_proto!("infra"); +} + +pub mod alb { + #![allow(clippy::derive_partial_eq_without_eq)] + tonic::include_proto!("aws"); +} + +pub mod shard { + #![allow(clippy::derive_partial_eq_without_eq)] + tonic::include_proto!("shard"); +} + +pub mod compression; +pub mod consistenthash; +pub mod constants; +pub mod errors; +pub mod gitbaretools; +pub mod key; +pub mod safeio; +pub mod singleflight; +pub mod version; + +mod output_bytes; + +use crate::common::{CompressionScheme, InitiateResponse}; +pub use output_bytes::output_bytes; + +impl TryFrom for Range { + type Error = Status; + + fn try_from(range_proto: cas::Range) -> Result { + if range_proto.start > range_proto.end { + return Err(Status::failed_precondition(format!( + "Range: {range_proto:?} has an end smaller than the start" + ))); + } + Ok(range_proto.start..range_proto.end) + } +} + +impl TryFrom<&cas::Range> for Range { + type Error = Status; + + fn try_from(range_proto: &cas::Range) -> Result { + if range_proto.start > range_proto.end { + return Err(Status::failed_precondition(format!( + "Range: {range_proto:?} has an end smaller than the start" + ))); + } + Ok(range_proto.start..range_proto.end) + } +} + +impl std::fmt::Display for common::Scheme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let as_str = match self { + common::Scheme::Http => "http", + common::Scheme::Https => "https", + }; + write!(f, "{as_str}") + } +} + +impl std::fmt::Display for common::EndpointConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let common::EndpointConfig { + host, port, scheme, .. + } = self; + let scheme_parsed = common::Scheme::try_from(*scheme).unwrap_or_default(); + write!(f, "{scheme_parsed}://{host}:{port}") + } +} + +impl InitiateResponse { + pub fn get_accepted_encodings_parsed(&self) -> Vec { + self.accepted_encodings + .iter() + .filter_map(|i| CompressionScheme::try_from(*i).ok()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use crate::common::EndpointConfig; + + use super::*; + + #[test] + fn test_range_conversion() { + let r = cas::Range { start: 0, end: 10 }; + let range = Range::try_from(r).unwrap(); + assert_eq!(range.start, 0); + assert_eq!(range.end, 10); + } + + #[test] + fn test_range_conversion_zero_len() { + let r = cas::Range { start: 10, end: 10 }; + let range = Range::try_from(r).unwrap(); + assert_eq!(range.start, 10); + assert_eq!(range.end, 10); + } + + #[test] + fn test_range_conversion_failed() { + let r = cas::Range { start: 20, end: 10 }; + let res = Range::try_from(r); + assert!(res.is_err()); + } + + #[test] + fn test_endpoint_config_to_endpoint_string() { + let host = "xetxet"; + let port = 443; + let e1 = EndpointConfig { + host: host.to_string(), + port, + scheme: common::Scheme::Http.into(), + root_ca_certificate: String::new(), + }; + + assert_eq!(e1.to_string(), format!("http://{host}:{port}")); + + let e2 = EndpointConfig { + host: host.to_string(), + port, + scheme: common::Scheme::Https.into(), + root_ca_certificate: String::from("abcd"), + }; + + assert_eq!(e2.to_string(), format!("https://{host}:{port}")); + } +} diff --git a/utils/src/output_bytes.rs b/utils/src/output_bytes.rs new file mode 100644 index 00000000..322971a8 --- /dev/null +++ b/utils/src/output_bytes.rs @@ -0,0 +1,52 @@ +/// Convert a usize into an output string, chooosing the nearest byte prefix. +/// +/// # Arguments +/// * `v` - the size in bytes +pub fn output_bytes(v: usize) -> String { + let map = vec![ + (1_099_511_627_776, "TiB"), + (1_073_741_824, "GiB"), + (1_048_576, "MiB"), + (1024, "KiB"), + ]; + + if v == 0 { + return "0 bytes".to_string(); + } + + for (div, s) in map { + let curr = v as f64 / div as f64; + if v / div > 0 { + return if v % div == 0 { + format!("{} {}", v / div, s) + } else { + format!("{curr:.2} {s}") + }; + } + } + + format!("{v} bytes") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_size_conversion() { + assert_eq!("500 bytes", output_bytes(500)); + assert_eq!("999 bytes", output_bytes(999)); + assert_eq!("1 KiB", output_bytes(1024)); + assert_eq!("1.00 KiB", output_bytes(1025)); + assert_eq!("999.99 KiB", output_bytes(1_023_989)); + assert_eq!("1 MiB", output_bytes(1_048_576)); + assert_eq!("1.00 MiB", output_bytes(1048577)); + assert_eq!("999.99 MiB", output_bytes(1_048_565_514)); + assert_eq!("1 GiB", output_bytes(1_073_741_824)); + assert_eq!("1.00 GiB", output_bytes(1_073_741_825)); + assert_eq!("999.99 GiB", output_bytes(1_073_731_086_581)); + assert_eq!("1 TiB", output_bytes(1_099_511_627_776)); + assert_eq!("1.00 TiB", output_bytes(1_099_511_627_777)); + assert_eq!("1234.57 TiB", output_bytes(1_357_424_070_303_416)); + } +} diff --git a/utils/src/safeio.rs b/utils/src/safeio.rs new file mode 100644 index 00000000..8e2163c9 --- /dev/null +++ b/utils/src/safeio.rs @@ -0,0 +1,59 @@ +use std::{ + io::{self, Write}, + path::Path, +}; +use tempfile::NamedTempFile; + +/// Write all bytes +pub fn write_all_file_safe(path: &Path, bytes: &[u8]) -> io::Result<()> { + if !path.as_os_str().is_empty() { + let dir = path.parent().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Unable to find parent path from {path:?}"), + ) + })?; + + // Make sure dir exists. + if !dir.exists() { + std::fs::create_dir_all(dir)?; + } + + let mut tempfile = create_temp_file(dir, "")?; + tempfile.write_all(bytes)?; + tempfile.persist(path).map_err(|e| e.error)?; + } + + Ok(()) +} + +pub fn create_temp_file(dir: &Path, suffix: &str) -> io::Result { + let tempfile = tempfile::Builder::new() + .prefix(&format!("{}.", std::process::id())) + .suffix(suffix) + .tempfile_in(dir)?; + + Ok(tempfile) +} + +#[cfg(test)] +mod test { + use anyhow::Result; + use std::fs; + use tempfile::TempDir; + + use super::write_all_file_safe; + + #[test] + fn test_small_file_write() -> Result<()> { + let tmp_dir = TempDir::new()?; + let bytes = vec![1u8; 1000]; + let file_name = tmp_dir.path().join("data"); + + write_all_file_safe(&file_name, &bytes)?; + + assert_eq!(fs::read(file_name)?, bytes); + + Ok(()) + } +} diff --git a/utils/src/singleflight.rs b/utils/src/singleflight.rs new file mode 100644 index 00000000..982c8cc9 --- /dev/null +++ b/utils/src/singleflight.rs @@ -0,0 +1,712 @@ +//! A singleflight implementation for tokio. +//! +//! Inspired by [async_singleflight](https://crates.io/crates/async_singleflight). +//! +//! # Examples +//! +//! ```no_run +//! use futures::future::join_all; +//! use std::sync::Arc; +//! use std::time::Duration; +//! +//! use cas::singleflight::Group; +//! +//! const RES: usize = 7; +//! +//! async fn expensive_fn() -> Result { +//! tokio::time::sleep(Duration::new(1, 500)).await; +//! Ok(RES) +//! } +//! +//! #[tokio::main] +//! async fn main() { +//! let g = Arc::new(Group::<_, ()>::new()); +//! let mut handlers = Vec::new(); +//! for _ in 0..10 { +//! let g = g.clone(); +//! handlers.push(tokio::spawn(async move { +//! let res = g.work("key", expensive_fn()).await.0; +//! let r = res.unwrap(); +//! println!("{}", r); +//! })); +//! } +//! +//! join_all(handlers).await; +//! } +//! ``` +//! + +use futures::future::Either; +use std::fmt::Debug; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, AtomicU16, Ordering}; +use std::sync::Arc; +use std::task::{ready, Context, Poll}; +use std::{future::Future, marker::PhantomData}; + +use hashbrown::HashMap; +use parking_lot::RwLock; +use pin_project::{pin_project, pinned_drop}; +use tokio::sync::{Mutex, Notify}; +use tracing::debug; + +pub use crate::errors::SingleflightError; + +type SingleflightResult = Result>; +type CallMap = HashMap>>; + +// Marker Traits to help make the code a bit cleaner. + +/// ResultType indicates the success type of a singleflight [Group]. +/// Since the actual processing might occur on a separate thread, +/// we need to type to be [Send] + [Sync]. It also needs to be [Clone] +/// so that we can clone the response across many tasks +pub trait ResultType: Send + Clone + Sync + Debug {} +impl ResultType for T {} + +/// Indicates the Error type of a singleflight [Group]. +/// The response might have been generated on a separate +/// thread, thus, we need this type to be [Send] + [Sync]. +pub trait ResultError: Send + Debug + Sync {} +impl ResultError for E {} + +/// Futures provided to a singleflight Group must produce a [Result] +/// for some T, E. This future must also be [Send] +/// as it could be spawned as a tokio task. +pub trait TaskFuture: Future> + Send {} +impl> + Send> TaskFuture for F {} + +/// Call represents the (eventual) results of running some Future. +/// +/// It consists of a condition variable that can be waited upon until the +/// owner task [completes](Call::complete) it. +/// +/// Tasks can get the Call's result using [get_future](Call::get_future) +/// to get a Future to await. Or they can call [get](Call::get) +/// to try and get the result synchronously if the Call is already complete. +#[derive(Debug, Clone)] +struct Call +where + T: ResultType, + E: ResultError, +{ + // The condition variable + nt: Arc, + + // The result of the operation. Kept under a RWLock that is expected + // to be write-once, read-many. + // We use a lock instead of an AtomicPtr since updating the result and + // notifying the waiters needs to be atomic to avoid tasks missing the + // notification or to avoid tasks reading an empty value. + // + // Also important to note is that this lock is synchronous as we need + // to be able to store the value in the [OwnerTask::drop] function if + // the underlying future panics. Thus, complete() must be synchronous. + // This is ok since we are never holding the mutex across an await + // boundary (all functions are synchronous), and the critical section + // is fast. + res: Arc>>>, + + // Number of tasks that were waiting + num_waiters: Arc, +} + +impl Call +where + T: ResultType, + E: ResultError, +{ + fn new() -> Self { + Self { + nt: Arc::new(Notify::new()), + res: Arc::new(RwLock::new(None)), + num_waiters: Arc::new(AtomicU16::new(0)), + } + } + + /// Completes the Call. This involves storing the provided result into the Call + /// and notifying all waiters that there is a value. + fn complete(&self, res: SingleflightResult) { + // write-lock + let mut val = self.res.write(); + *val = Some(res); + self.nt.notify_waiters(); + let num_waiters = self.num_waiters.load(Ordering::SeqCst); + debug!("Completed Call with: {} waiters", num_waiters); + } + + /// Gets a Future that can be awaited to get the singleflight results, whenever that + /// might occur. + fn get_future(&self) -> impl Future> + '_ { + // read-lock + let res = self.res.read(); + if let Some(result) = res.clone() { + // we already have the result, provide it back to the caller. + debug!("Call already completed"); + Either::Left(async move { result }) + } else { + // no result yet, we are a waiter task. + self.num_waiters.fetch_add(1, Ordering::SeqCst); + debug!("Adding to Call's Notify"); + + // Note that the `notified()` needs to be performed outside of the async + // block since we need to register our waiting within this read-lock + // or else, we might miss the owner task's notification. + let notified = self.nt.notified(); + Either::Right(async move { + notified.await; + self.get() + }) + } + } + + /// Gets the result for the Call if set. + /// If not set, then [SingleflightError::NoResult] is returned + fn get(&self) -> SingleflightResult { + let res = self.res.read(); + res.clone().unwrap_or(Err(SingleflightError::NoResult)) + } +} + +/// Group represents a class of work and creates a space in which units of work +/// can be executed with duplicate suppression. +#[derive(Default, Debug)] +pub struct Group +where + T: ResultType + 'static, + E: ResultError, +{ + call_map: Arc>>, + _marker: PhantomData, +} + +impl Group +where + T: ResultType + 'static, + E: ResultError, +{ + /// Create a new Group to do work with. + pub fn new() -> Group { + Group { + call_map: Arc::new(Mutex::new(HashMap::new())), + _marker: PhantomData, + } + } + + /// Execute and return the value for a given function, making sure that only one + /// operation is in-flight at a given moment. If a duplicate call comes in, that caller will + /// wait until the original call completes and return the same value. + /// The second return value indicates whether the call is the owner. + /// + /// On error, the owner will receive the original error returned from the function + /// as a SingleflightError::InternalError, all waiters will receive a copy of the + /// error message wrapped in a: SingleflightError::WaiterInternalError. + /// This is due to the fact that most error types don't implement Clone (e.g. anyhow::Error) + /// and thus we can't clone the original error for all of the waiters. + pub async fn work( + &self, + key: &str, + fut: impl TaskFuture + 'static, + ) -> (Result>, bool) { + // Get the call to use and a handle for retrieving the results + let (call, created) = self.get_call_or_create(key).await; + let results_future = call.get_future(); + + if created { + // spawn the owner task and wait + let owner_task = OwnerTask::new(fut, call.clone()); + let owner_handle = tokio::spawn(owner_task); + + // wait for the owner task and results to come back + let (handle_result, future_result) = tokio::join!(owner_handle, results_future); + let result = handle_result + .map_err(|e| SingleflightError::JoinError(e.to_string())) + .and(future_result); + + // since we created the call, remove it from the map + if let Err(e) = self.remove_call(key).await { + return (Err(e), true); + } + (result, true) + } else { + (results_future.await, false) + } + } + + /// Gets the [Call] to use from the call_map or else inserts a new Call + /// into the map. + /// + /// Returns the [Call] that should be used and whether it was created or + /// not. + async fn get_call_or_create(&self, key: &str) -> (Arc>, bool) { + let mut m = self.call_map.lock().await; + if let Some(c) = m.get(key).cloned() { + (c, false) + } else { + let c = Arc::new(Call::new()); + let our_call = c.clone(); + m.insert(key.to_owned(), c); + (our_call, true) + } + } + + /// Removes the [Call] associated with the Key. If there is no such [Call], + /// then an error is returned. + async fn remove_call(&self, key: &str) -> SingleflightResult<(), E> { + let mut m = self.call_map.lock().await; + m.remove(key).ok_or(SingleflightError::CallMissing)?; + Ok(()) + } +} + +/// Defines a task to own the poll'ing of the Future and ensure that the +/// call is updated (i.e. result stored and waiters notified) when the +/// Future completes (even if the future panics). +/// +/// We can guarantee that the [Call] gets notified even during a Panic +/// since tokio tasks will catch panics and call the `drop()` function. +/// +/// For more info, see: https://github.com/tokio-rs/tokio/blob/4eed411519783ef6f58cbf74f886f91142b5cfa6/tokio/src/runtime/task/harness.rs#L453-L459 +/// and the discussion on: https://users.rust-lang.org/t/how-panic-calls-drop-functions/53663/8 +/// +/// Pin'ed since it is a Future implementation. +#[pin_project(PinnedDrop)] +#[must_use = "futures do nothing unless you `.await` or poll them"] +struct OwnerTask +where + T: ResultType, + E: ResultError, + F: TaskFuture, +{ + #[pin] + fut: F, + got_response: AtomicBool, + call: Arc>, +} + +impl OwnerTask +where + T: ResultType, + E: ResultError, + F: TaskFuture, +{ + fn new(fut: F, call: Arc>) -> Self { + Self { + fut, + got_response: AtomicBool::new(false), + call, + } + } +} + +impl Future for OwnerTask +where + T: ResultType, + E: ResultError, + F: TaskFuture, +{ + type Output = Result>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + let res: Result = ready!(this.fut.poll(cx)); + let res = res.map_err(|e| SingleflightError::InternalError(e)); + // we have a result, so store it into our call and notify all waiters. + let call = this.call; + this.got_response.store(true, Ordering::SeqCst); + call.complete(res.clone()); + Poll::Ready(res) + } +} + +#[pinned_drop] +impl PinnedDrop for OwnerTask +where + T: ResultType, + E: ResultError, + F: TaskFuture, +{ + fn drop(self: Pin<&mut Self>) { + // If we don't have a result stored in the call, then we panicked and + // should store an error, notifying all waiters of the panic. + let this = self.project(); + if !this.got_response.load(Ordering::SeqCst) { + let call = this.call; + call.complete(Err(SingleflightError::OwnerPanicked)) + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicU32, Ordering}; + use std::sync::Arc; + use std::time::Duration; + + use futures::future::join_all; + use futures::stream::iter; + use futures::StreamExt; + use hashbrown::HashMap; + use tokio::sync::mpsc::error::SendError; + use tokio::sync::mpsc::{channel, Sender}; + use tokio::sync::{Mutex, Notify}; + use tokio::task::JoinHandle; + use tokio::time::timeout; + + use crate::errors::SingleflightError; + use crate::singleflight::{Call, OwnerTask}; + + use super::Group; + + /// A period of time for waiters to wait for a notification from the owner + /// task. This is expected to be sufficient time for the test futures to + /// complete. Thus, if we hit this timeout, then likely, there is something + /// wrong with the [Call] notifications. + const WAITER_TIMEOUT: Duration = Duration::from_millis(100); + + const RES: usize = 7; + + async fn return_res() -> Result { + Ok(RES) + } + + async fn expensive_fn(x: Arc, resp: usize) -> Result { + tokio::time::sleep(Duration::new(1, 0)).await; + x.fetch_add(1, Ordering::SeqCst); + Ok(resp) + } + + #[tokio::test] + async fn test_simple() { + let g = Group::new(); + let res = g.work("key", return_res()).await.0; + let r = res.unwrap(); + assert_eq!(r, RES); + } + + #[tokio::test] + async fn test_multiple_threads() { + let times_called = Arc::new(AtomicU32::new(0)); + + let g: Arc> = Arc::new(Group::new()); + let mut handlers = Vec::new(); + for _ in 0..10 { + let g = g.clone(); + let counter = times_called.clone(); + handlers.push(tokio::spawn(async move { + let tup = g.work("key", expensive_fn(counter, RES)).await; + let res = tup.0; + let fn_response = res.unwrap(); + (fn_response, tup.1) + })); + } + + let num_callers = join_all(handlers) + .await + .into_iter() + .map(|r| r.unwrap()) + .filter(|(val, is_caller)| { + assert_eq!(*val, RES); + *is_caller + }) + .count(); + assert_eq!(1, num_callers); + assert_eq!(1, times_called.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn test_error() { + let times_called = Arc::new(AtomicU32::new(0)); + + async fn expensive_error_fn(x: Arc) -> Result { + tokio::time::sleep(Duration::new(1, 500)).await; + x.fetch_add(1, Ordering::SeqCst); + Err("Error") + } + + let g: Arc> = Arc::new(Group::new()); + let mut handlers = Vec::new(); + for _ in 0..10 { + let g = g.clone(); + let counter = times_called.clone(); + handlers.push(tokio::spawn(async move { + let tup = g.work("key", expensive_error_fn(counter)).await; + let res = tup.0; + assert!(res.is_err()); + tup.1 + })); + } + + let num_callers = join_all(handlers) + .await + .into_iter() + .map(|r| r.unwrap()) + .filter(|b| *b) + .count(); + assert_eq!(1, num_callers); + assert_eq!(1, times_called.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn test_multiple_keys() { + let times_called_x = Arc::new(AtomicU32::new(0)); + let times_called_y = Arc::new(AtomicU32::new(0)); + + let mut handlers1 = call_success_n_times(5, "key", times_called_x.clone(), 7); + let mut handlers2 = call_success_n_times(5, "key2", times_called_y.clone(), 13); + handlers1.append(&mut handlers2); + let count_x = AtomicU32::new(0); + let count_y = AtomicU32::new(0); + + let num_callers = join_all(handlers1) + .await + .into_iter() + .map(|r| r.unwrap()) + .filter(|(val, is_caller)| { + if *val == 7 { + count_x.fetch_add(1, Ordering::SeqCst); + } else if *val == 13 { + count_y.fetch_add(1, Ordering::SeqCst); + } else { + panic!("joined a number not expected: {}", *val); + } + *is_caller + }) + .count(); + assert_eq!(2, num_callers); + assert_eq!(5, count_x.load(Ordering::SeqCst)); + assert_eq!(5, count_y.load(Ordering::SeqCst)); + assert_eq!(1, times_called_x.load(Ordering::SeqCst)); + assert_eq!(1, times_called_y.load(Ordering::SeqCst)); + } + + fn call_success_n_times( + times: usize, + key: &str, + c: Arc, + val: usize, + ) -> Vec> { + let g: Arc> = Arc::new(Group::new()); + let mut handlers = Vec::new(); + for _ in 0..times { + let g = g.clone(); + let counter = c.clone(); + let k = key.to_owned(); + handlers.push(tokio::spawn(async move { + let tup = g.work(k.as_str(), expensive_fn(counter, val)).await; + let res = tup.0; + let fn_response = res.unwrap(); + (fn_response, tup.1) + })); + } + handlers + } + + #[tokio::test] + async fn test_owner_task_future_impl() { + const VAL: i32 = 10; + let future = async { Ok::(VAL) }; + let call = Arc::new(Call::new()); + let owner_task = OwnerTask::new(future, call.clone()); + let result = tokio::spawn(owner_task).await; + assert_eq!(VAL, result.unwrap().unwrap()); + assert_eq!(VAL, call.get().unwrap()); + } + + #[tokio::test] + async fn test_owner_task_future_notify() { + const VAL: i32 = 10; + let future = async { Ok::(VAL) }; + let call = Arc::new(Call::new()); + let call_waiter = call.clone(); + let waiter_task = async move { + let waiter_future = call_waiter.get_future(); + assert_eq!(VAL, waiter_future.await.unwrap()); + }; + let waiter_handle = tokio::spawn(waiter_task); + let owner_task = OwnerTask::new(future, call.clone()); + let result = tokio::spawn(owner_task).await; + timeout(WAITER_TIMEOUT, waiter_handle) + .await + .unwrap() + .unwrap(); + assert_eq!(VAL, result.unwrap().unwrap()); + assert_eq!(VAL, call.get().unwrap()); + assert_eq!(1, call.num_waiters.load(Ordering::SeqCst)) // we should have had 1 waiter + } + + #[tokio::test] + async fn test_owner_task_future_panic() { + let future = async { panic!("failing task") }; + let call = Arc::new(Call::::new()); + let call_waiter = call.clone(); + let waiter_task = async move { + let waiter_future = call_waiter.get_future(); + let result = waiter_future.await; + assert!(matches!(result, Err(SingleflightError::OwnerPanicked))); + }; + let waiter_handle = tokio::spawn(waiter_task); + + let owner_task = OwnerTask::new(future, call.clone()); + let result = tokio::spawn(owner_task).await; + assert!(result.is_err()); + timeout(WAITER_TIMEOUT, waiter_handle) + .await + .unwrap() + .unwrap(); + assert_eq!(1, call.num_waiters.load(Ordering::SeqCst)) // we should have had 1 waiter + } + + #[tokio::test] + async fn test_deadlock() { + /* + Each spawned tokio task is expected to send some ints to the main task via a bounded buffer. + The ints are fetched using a futures::Buffered stream over some future. These futures will + call into singleflight to fetch an int. + + To setup the deadlock, we have 3 tasks: main, t1, and t2 with the following dependency: + main is waiting to read from t1, t1 is a waiter on some element that t2 is working on, + t2 is blocked writing to the buffer (i.e. waiting for main to read). + + to accomplish this, we spawn t1, t2. Each will start up their sub-tasks (3 at a time). + However, there is a dependency where task2[2] runs for some int x and task1[4] needs + that value, thus triggering a dependency within singleflight. + */ + let group: Arc> = Arc::new(Group::new()); + // communication channels + let (send1, mut recv1) = channel::(1); + let (send2, mut recv2) = channel::(1); + // Items to return on the channels from the tasks. + let vals1: Vec = vec![1, 2, 3, 4, SHARED_ITEM]; + let vals2: Vec = vec![6, 7, SHARED_ITEM, 8, 9]; + + // waiters allows us to define the order that sub-tasks run in the underlying tasks. + // We need this for 2 reasons: + // 1. SHARED_ITEM sub-task in t2 needs to block until we can ensure that it has a waiter + // 2. vals2[1] needs to block to ensure that t2's SHARED_ITEM starts. + let waiters: Arc>>> = Arc::new(Mutex::new(HashMap::new())); + { + let mut guard = waiters.lock().await; + guard.insert(vals2[1], Arc::new(Notify::new())); + guard.insert(SHARED_ITEM, Arc::new(Notify::new())); + } + + // spawn tasks + let t1 = tokio::spawn(run_task( + 1, + group.clone(), + waiters.clone(), + send1, + false, + vals1.clone(), + )); + let t2 = tokio::spawn(run_task( + 2, + group.clone(), + waiters.clone(), + send2, + true, + vals2.clone(), + )); + + // try to receive all the values from task1 without getting stuck. + for (i, expected_val) in vals1.into_iter().enumerate() { + if i == 3 { + // resume vals2[1] to allow task2 to get "stuck" waiting on send2.send() + println!("[main] notifying val: {}", vals2[1]); + let guard = waiters.lock().await; + guard.get(&vals2[1]).unwrap().notify_one(); + println!("[main] notified val: {}", vals2[1]) + } + if i == 4 { + // resume task2's SHARED_ITEM sub-task since we now have a waiter (i.e. vals1[4]). + println!("[main] notifying val: {}", SHARED_ITEM); + let guard = waiters.lock().await; + guard.get(&SHARED_ITEM).unwrap().notify_one(); + println!("[main] notified val: {}", SHARED_ITEM); + } + println!("[main] getting t1[{}]", i); + let res = timeout(WAITER_TIMEOUT, recv1.recv()).await.map_err(|_| { + format!( + "Timed out on task1 waiting for val: {}. Likely deadlock.", + expected_val + ) + }); + let val = res.unwrap().unwrap(); + println!("[main] got val: {} from t1[{}]", val, i); + assert_eq!(expected_val, val); + } + + // try to receive all the values from task2 without getting stuck. + for expected_val in vals2 { + let res = timeout(WAITER_TIMEOUT, recv2.recv()).await.map_err(|_| { + format!( + "Timed out on task2 waiting for val: {}. Likely deadlock.", + expected_val + ) + }); + let val = res.unwrap().unwrap(); + assert_eq!(expected_val, val); + } + + // make sure t1,t2 completed successfully. + t1.await.unwrap().unwrap(); + t2.await.unwrap().unwrap(); + } + + const SHARED_ITEM: usize = 5; + + async fn run_task( + id: i32, + g: Arc>, + waiters: Arc>>>, + send_chan: Sender, + should_own: bool, + vals: Vec, + ) -> Result<(), SendError> { + // create a buffered stream that will run at most 3 sub-tasks concurrently. + let mut strm = iter(vals.into_iter().map(|v| { + let g = g.clone(); + let waiters = waiters.clone(); + // get the sub-task for the given item. + async move { + println!("[task: {}] running task for: {}", id, v); + let (res, is_owner) = g.work(format!("{}", v).as_str(), run_fut(v, waiters)).await; + println!( + "[task: {}] completed task for: {}, is_owner: {}", + id, v, is_owner + ); + if v == SHARED_ITEM { + assert_eq!(should_own, is_owner); + } + res.unwrap() + } + })) + .buffered(3); + + while let Some(val) = strm.next().await { + println!("[task: {}] sending next element: {}", id, val); + send_chan.send(val).await?; + println!("[task: {}] sent next element: {}", id, val); + } + println!("[task: {}] done executing", id); + Ok(()) + } + + async fn run_fut( + v: usize, + waiters: Arc>>>, + ) -> Result { + let waiter = { + let x = waiters.lock().await; + x.get(&v).cloned() + }; + // wait for the main task to tell us to proceed. + if let Some(waiter) = waiter { + println!("val: {}, waiting for signal", v); + waiter.notified().await; + println!("val: {}, woke up from signal", v); + } + Ok(v) + } +} diff --git a/utils/src/version.rs b/utils/src/version.rs new file mode 100644 index 00000000..54a59d15 --- /dev/null +++ b/utils/src/version.rs @@ -0,0 +1,244 @@ +use std::str::FromStr; + +use lazy_static::lazy_static; +use regex::{Captures, Regex}; +use xet_error::Error; + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +pub struct Version { + pub major: u32, + pub minor: u32, + pub patch: u32, +} + +impl Version { + pub const fn new(major: u32, minor: u32, patch: u32) -> Self { + Self { + major, + minor, + patch, + } + } +} + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum VersionError { + #[error("Could not parse {0} as version, expecting standard semantic version e.g. \"0.8.2\"")] + ParseFailed(String), +} + +const MAJOR_KEY: &str = "major"; +const MINOR_KEY: &str = "minor"; +const PATCH_KEY: &str = "patch"; + +lazy_static! { + // matching semantic versions with optional `v` at the start and optional suffix beginning with `-` + // patch version is expected but optional + // valid forms: + // 1.0.0 + // v0.9.2 + // 0.123.0-anything + // v0.0.0-multi-dash + // 0.1 + // v0.1-test + // invalid forms: + // x.0.0 + // 0.0.0- + // x7.9.0 + static ref VERSION_REGEX: Regex = Regex::new( + format!(r"^v?(?P<{MAJOR_KEY}>\d+)\.(?P<{MINOR_KEY}>\d+)(\.(?P<{PATCH_KEY}>\d+))?(-.+)?$").as_str() + ) + .unwrap(); +} + +/// util to convert regex capture group to parsable +/// captures must have named regex group and specifically have +/// the specific name given +#[inline] +fn parse_from_capture_by_key<'a, T>( + captures: &Captures<'a>, + name: &'a str, +) -> Result +where + T: FromStr, +{ + let capture = if let Some(major) = captures.name(name) { + major + } else { + return Err(VersionError::ParseFailed(format!("invalid {name}"))); + }; + + capture + .as_str() + .parse::() + .map_err(|_| VersionError::ParseFailed(format!("invalid {name}"))) +} + +/// enable version string to Version struct conversion +impl TryFrom<&str> for Version { + type Error = VersionError; + fn try_from(value: &str) -> Result { + let captures = match VERSION_REGEX.captures(value) { + Some(captures) => captures, + None => return Err(VersionError::ParseFailed(value.to_string())), + }; + + let major = parse_from_capture_by_key::(&captures, MAJOR_KEY)?; + let minor = parse_from_capture_by_key::(&captures, MINOR_KEY)?; + // allow patch to not be included + let patch = parse_from_capture_by_key::(&captures, PATCH_KEY).unwrap_or(0); + + Ok(Version { + major, + minor, + patch, + }) + } +} + +/// enable version String to Version struct conversion +impl TryFrom for Version { + type Error = VersionError; + fn try_from(value: String) -> Result { + Version::try_from(value.as_str()) + } +} + +/// enable comparing versions +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + match self.major.partial_cmp(&other.major) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.minor.partial_cmp(&other.minor) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + self.patch.partial_cmp(&other.patch) + } +} + +/// allow pretty print +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Version { + major, + minor, + patch, + } = self; + write!(f, "{major}.{minor}.{patch}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_version_to_string() { + let version = Version::new(1, 2, 3); + assert_eq!(version.to_string(), "1.2.3".to_string()) + } + + #[test] + fn test_version_try_from_str() { + let expected = Version::new(1, 2, 3); + + let standard_version_str = "1.2.3"; + assert_eq!(Version::try_from(standard_version_str).unwrap(), expected); + } + + #[test] + fn test_version_try_from_str_special_cases() { + let expected = Version::new(1, 2, 0); + + let no_patch = "1.2"; + assert_eq!(Version::try_from(no_patch).unwrap(), expected); + + let with_v = "v1.2.0"; + assert_eq!(Version::try_from(with_v).unwrap(), expected); + + let with_suffix = "v1.2.0-abc"; + assert_eq!(Version::try_from(with_suffix).unwrap(), expected); + + let with_v = "v1.2.0-asdasd-adsda"; + assert_eq!(Version::try_from(with_v).unwrap(), expected); + } + + #[test] + fn test_version_try_from_str_failure() { + let bad_all_over = "bad string"; + assert!(Version::try_from(bad_all_over).is_err()); + + let bad_major = "a.2.3"; + assert!(Version::try_from(bad_major).is_err()); + + let bad_minor = "1.a.3"; + assert!(Version::try_from(bad_minor).is_err()); + + let bad_patch = "1.2.a"; + assert!(Version::try_from(bad_patch).is_err()); + + let bad_suffix = "1.2.3aaa"; + assert!(Version::try_from(bad_suffix).is_err()); + + let bad_prefix = "p1.2."; + assert!(Version::try_from(bad_prefix).is_err()); + } + + #[test] + fn test_version_default() { + assert_eq!(Version::default(), Version::new(0, 0, 0)); + } + + #[test] + fn test_version_comparison() { + let zeros = Version::default(); + let ones = Version::new(1, 1, 1); + let ninties = Version::new(90, 90, 90); + + // 0.0.0 < 1.1.1 && 1.1.1 < 90.90.90 && 0.0.0 < 90.90.90 + assert!(zeros < ones); + assert!(ones < ninties); + assert!(zeros < ninties); + + // 0.0.0 < 0.1.1 && 0.1.1 < 1.1.1 + let zero_one_one = Version::new(0, 1, 1); + assert!(zeros < zero_one_one); + assert!(zero_one_one < ones); + + // 0.1.0 < 0.1.1 + let zero_one_zero = Version::new(0, 1, 0); + assert!(zero_one_zero < zero_one_one); + + // 1.0.0 > 0.1.0 && 1.0.0 > 0.1.1 + let one_zero_zero = Version::new(1, 0, 0); + assert!(one_zero_zero > zero_one_zero); + assert!(one_zero_zero > zero_one_one); + } + + #[test] + fn test_version_range() { + let zeros = Version::new(0, 0, 0); + let o_nine_o = Version::new(0, 9, 0); + let ones = Version::new(1, 1, 1); + let ninties = Version::new(90, 90, 90); + let ninety_ninety_ninety_one = Version::new(90, 90, 90); + + let range = o_nine_o..ninties; + assert!(!range.contains(&zeros)); + assert!(range.contains(&o_nine_o)); + assert!(range.contains(&ones)); + assert!(!range.contains(&ninties)); + assert!(!range.contains(&ninety_ninety_ninety_one)); + + let range_inclusive = o_nine_o..=ninties; + assert!(!range.contains(&zeros)); + assert!(range_inclusive.contains(&o_nine_o)); + assert!(range_inclusive.contains(&ones)); + assert!(range_inclusive.contains(&ninties)); + assert!(!range.contains(&ninety_ninety_ninety_one)); + } +} diff --git a/xet_error/.github/workflows/ci.yml b/xet_error/.github/workflows/ci.yml new file mode 100644 index 00000000..c67e7fca --- /dev/null +++ b/xet_error/.github/workflows/ci.yml @@ -0,0 +1,105 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + schedule: [cron: "40 1 * * *"] + +permissions: + contents: read + +env: + RUSTFLAGS: -Dwarnings + +jobs: + pre_ci: + uses: dtolnay/.github/.github/workflows/pre_ci.yml@master + + test: + name: Rust ${{matrix.rust}} + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: [nightly, beta, stable, 1.56.0] + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{matrix.rust}} + components: rust-src + - name: Enable type layout randomization + run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zrandomize-layout >> $GITHUB_ENV + if: matrix.rust == 'nightly' + - name: Enable nightly-only tests + run: echo RUSTFLAGS=${RUSTFLAGS}\ --cfg=xet_error_nightly_testing >> $GITHUB_ENV + if: matrix.rust == 'nightly' + - run: cargo test --all + + minimal: + name: Minimal versions + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - run: cargo generate-lockfile -Z minimal-versions + - run: cargo check --locked + + doc: + name: Documentation + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + RUSTDOCFLAGS: -Dwarnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + - uses: dtolnay/install@cargo-docs-rs + - run: cargo docs-rs + + clippy: + name: Clippy + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + with: + components: clippy, rust-src + - run: cargo clippy --tests --workspace -- -Dclippy::all -Dclippy::pedantic + + miri: + name: Miri + needs: pre_ci + if: needs.pre_ci.outputs.continue + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@miri + - run: cargo miri setup + - run: cargo miri test + env: + MIRIFLAGS: -Zmiri-strict-provenance + + outdated: + name: Outdated + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/install@cargo-outdated + - run: cargo outdated --workspace --exit-code 1 diff --git a/xet_error/.gitignore b/xet_error/.gitignore new file mode 100644 index 00000000..69369904 --- /dev/null +++ b/xet_error/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/xet_error/Cargo.toml b/xet_error/Cargo.toml new file mode 100644 index 00000000..c9d37024 --- /dev/null +++ b/xet_error/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "xet_error" +version = "0.14.5" +authors = ["David Tolnay "] +categories = ["rust-patterns"] +description = "derive(Error)" +documentation = "https://docs.rs/thiserror" +edition = "2021" +keywords = ["error", "error-handling", "derive"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/dtolnay/thiserror" +rust-version = "1.56" + +[dependencies] +xet-error-impl = { version = "=1.0.50", path = "impl" } +lazy_static = "1.4.0" + +[dev-dependencies] +anyhow = "1.0.73" +ref-cast = "1.0.18" +rustversion = "1.0.13" +trybuild = { version = "1.0.81", features = ["diff"] } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = ["--generate-link-to-definition"] diff --git a/xet_error/LICENSE-APACHE b/xet_error/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/xet_error/LICENSE-APACHE @@ -0,0 +1,176 @@ + 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 diff --git a/xet_error/LICENSE-MIT b/xet_error/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/xet_error/LICENSE-MIT @@ -0,0 +1,23 @@ +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/xet_error/README.md b/xet_error/README.md new file mode 100644 index 00000000..de95d042 --- /dev/null +++ b/xet_error/README.md @@ -0,0 +1,227 @@ +Xet_error +========= + +This is a local source drop of the thiserror package, with hooks installed for error reporting and tracing. + +derive(Error) +============= + +[github](https://github.com/dtolnay/thiserror) +[crates.io](https://crates.io/crates/thiserror) +[docs.rs](https://docs.rs/thiserror) +[build status](https://github.com/dtolnay/thiserror/actions?query=branch%3Amaster) + +This library provides a convenient derive macro for the standard library's +[`std::error::Error`] trait. + +[`std::error::Error`]: https://doc.rust-lang.org/std/error/trait.Error.html + +```toml +[dependencies] +thiserror = "1.0" +``` + +*Compiler support: requires rustc 1.56+* + +
+ +## Example + +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DataStoreError { + #[error("data store disconnected")] + Disconnect(#[from] io::Error), + #[error("the data for key `{0}` is not available")] + Redaction(String), + #[error("invalid header (expected {expected:?}, found {found:?})")] + InvalidHeader { + expected: String, + found: String, + }, + #[error("unknown data store error")] + Unknown, +} +``` + +
+ +## Details + +- Thiserror deliberately does not appear in your public API. You get the same + thing as if you had written an implementation of `std::error::Error` by hand, + and switching from handwritten impls to thiserror or vice versa is not a + breaking change. + +- Errors may be enums, structs with named fields, tuple structs, or unit + structs. + +- A `Display` impl is generated for your error if you provide `#[error("...")]` + messages on the struct or each variant of your enum, as shown above in the + example. + + The messages support a shorthand for interpolating fields from the error. + + - `#[error("{var}")]` ⟶ `write!("{}", self.var)` + - `#[error("{0}")]` ⟶ `write!("{}", self.0)` + - `#[error("{var:?}")]` ⟶ `write!("{:?}", self.var)` + - `#[error("{0:?}")]` ⟶ `write!("{:?}", self.0)` + + These shorthands can be used together with any additional format args, which + may be arbitrary expressions. For example: + + ```rust + #[derive(Error, Debug)] + pub enum Error { + #[error("invalid rdo_lookahead_frames {0} (expected < {})", i32::MAX)] + InvalidLookahead(u32), + } + ``` + + If one of the additional expression arguments needs to refer to a field of the + struct or enum, then refer to named fields as `.var` and tuple fields as `.0`. + + ```rust + #[derive(Error, Debug)] + pub enum Error { + #[error("first letter must be lowercase but was {:?}", first_char(.0))] + WrongCase(String), + #[error("invalid index {idx}, expected at least {} and at most {}", .limits.lo, .limits.hi)] + OutOfBounds { idx: usize, limits: Limits }, + } + ``` + +- A `From` impl is generated for each variant containing a `#[from]` attribute. + + Note that the variant must not contain any other fields beyond the source + error and possibly a backtrace. A backtrace is captured from within the `From` + impl if there is a field for it. + + ```rust + #[derive(Error, Debug)] + pub enum MyError { + Io { + #[from] + source: io::Error, + backtrace: Backtrace, + }, + } + ``` + +- The Error trait's `source()` method is implemented to return whichever field + has a `#[source]` attribute or is named `source`, if any. This is for + identifying the underlying lower level error that caused your error. + + The `#[from]` attribute always implies that the same field is `#[source]`, so + you don't ever need to specify both attributes. + + Any error type that implements `std::error::Error` or dereferences to `dyn + std::error::Error` will work as a source. + + ```rust + #[derive(Error, Debug)] + pub struct MyError { + msg: String, + #[source] // optional if field name is `source` + source: anyhow::Error, + } + ``` + +- The Error trait's `provide()` method is implemented to provide whichever field + has a type named `Backtrace`, if any, as a `std::backtrace::Backtrace`. + + ```rust + use std::backtrace::Backtrace; + + #[derive(Error, Debug)] + pub struct MyError { + msg: String, + backtrace: Backtrace, // automatically detected + } + ``` + +- If a field is both a source (named `source`, or has `#[source]` or `#[from]` + attribute) *and* is marked `#[backtrace]`, then the Error trait's `provide()` + method is forwarded to the source's `provide` so that both layers of the error + share the same backtrace. + + ```rust + #[derive(Error, Debug)] + pub enum MyError { + Io { + #[backtrace] + source: io::Error, + }, + } + ``` + +- Errors may use `error(transparent)` to forward the source and Display methods + straight through to an underlying error without adding an additional message. + This would be appropriate for enums that need an "anything else" variant. + + ```rust + #[derive(Error, Debug)] + pub enum MyError { + ... + + #[error(transparent)] + Other(#[from] anyhow::Error), // source and Display delegate to anyhow::Error + } + ``` + + Another use case is hiding implementation details of an error representation + behind an opaque error type, so that the representation is able to evolve + without breaking the crate's public API. + + ```rust + // PublicError is public, but opaque and easy to keep compatible. + #[derive(Error, Debug)] + #[error(transparent)] + pub struct PublicError(#[from] ErrorRepr); + + impl PublicError { + // Accessors for anything we do want to expose publicly. + } + + // Private and free to change across minor version of the crate. + #[derive(Error, Debug)] + enum ErrorRepr { + ... + } + ``` + +- See also the [`anyhow`] library for a convenient single error type to use in + application code. + + [`anyhow`]: https://github.com/dtolnay/anyhow + +
+ +## Comparison to anyhow + +Use thiserror if you care about designing your own dedicated error type(s) so +that the caller receives exactly the information that you choose in the event of +failure. This most often applies to library-like code. Use [Anyhow] if you don't +care what error type your functions return, you just want it to be easy. This is +common in application-like code. + +[Anyhow]: https://github.com/dtolnay/anyhow + +
+ +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + diff --git a/xet_error/_git/HEAD b/xet_error/_git/HEAD new file mode 100644 index 00000000..cb089cd8 --- /dev/null +++ b/xet_error/_git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/xet_error/_git/config b/xet_error/_git/config new file mode 100644 index 00000000..7d076b13 --- /dev/null +++ b/xet_error/_git/config @@ -0,0 +1,13 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[remote "origin"] + url = git@github.com:dtolnay/thiserror.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "master"] + remote = origin + merge = refs/heads/master diff --git a/xet_error/_git/description b/xet_error/_git/description new file mode 100644 index 00000000..498b267a --- /dev/null +++ b/xet_error/_git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/xet_error/_git/hooks/applypatch-msg.sample b/xet_error/_git/hooks/applypatch-msg.sample new file mode 100755 index 00000000..a5d7b84a --- /dev/null +++ b/xet_error/_git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/xet_error/_git/hooks/commit-msg.sample b/xet_error/_git/hooks/commit-msg.sample new file mode 100755 index 00000000..b58d1184 --- /dev/null +++ b/xet_error/_git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/xet_error/_git/hooks/fsmonitor-watchman.sample b/xet_error/_git/hooks/fsmonitor-watchman.sample new file mode 100755 index 00000000..23e856f5 --- /dev/null +++ b/xet_error/_git/hooks/fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/xet_error/_git/hooks/post-update.sample b/xet_error/_git/hooks/post-update.sample new file mode 100755 index 00000000..ec17ec19 --- /dev/null +++ b/xet_error/_git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/xet_error/_git/hooks/pre-applypatch.sample b/xet_error/_git/hooks/pre-applypatch.sample new file mode 100755 index 00000000..4142082b --- /dev/null +++ b/xet_error/_git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/xet_error/_git/hooks/pre-commit.sample b/xet_error/_git/hooks/pre-commit.sample new file mode 100755 index 00000000..e144712c --- /dev/null +++ b/xet_error/_git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/xet_error/_git/hooks/pre-merge-commit.sample b/xet_error/_git/hooks/pre-merge-commit.sample new file mode 100755 index 00000000..399eab19 --- /dev/null +++ b/xet_error/_git/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/xet_error/_git/hooks/pre-push.sample b/xet_error/_git/hooks/pre-push.sample new file mode 100755 index 00000000..4ce688d3 --- /dev/null +++ b/xet_error/_git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/xet_error/_git/hooks/pre-rebase.sample b/xet_error/_git/hooks/pre-rebase.sample new file mode 100755 index 00000000..6cbef5c3 --- /dev/null +++ b/xet_error/_git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/xet_error/_git/hooks/pre-receive.sample b/xet_error/_git/hooks/pre-receive.sample new file mode 100755 index 00000000..a1fd29ec --- /dev/null +++ b/xet_error/_git/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/xet_error/_git/hooks/prepare-commit-msg.sample b/xet_error/_git/hooks/prepare-commit-msg.sample new file mode 100755 index 00000000..10fa14c5 --- /dev/null +++ b/xet_error/_git/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/xet_error/_git/hooks/push-to-checkout.sample b/xet_error/_git/hooks/push-to-checkout.sample new file mode 100755 index 00000000..af5a0c00 --- /dev/null +++ b/xet_error/_git/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/xet_error/_git/index b/xet_error/_git/index new file mode 100644 index 0000000000000000000000000000000000000000..7c89b61ee84d2ac12b15477524458b21e03464a6 GIT binary patch literal 8877 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�(38tecJ4i4GEsg|v5%PU zqoh;3&iB)~af1%d`!q)Q)wLk+L1+C7%DML~A6ZU_|MUp)9uO+_kuiC01qhmVq zOyBeF*6;a){g>~B#6!SBejRZ>->Dq(Yf>frpklu;x}TEg9y{YWJKJ+dUCsw(^))CA z_+7Em4ov!wezGia-I}fHkv~-I8&3BH>&AJt#+ST3Q|SKnPnE0A!DfGqJo}n5=P9Qy zeNZ6o^J8_SVt#}iKiji*>9L4`O4t4G2x=Ox-fQr=^+=b}7rpT*S>QZgoO_FWpkkj$ zIiIwr`P1frBNH;Gs5|Oq`0jmScDr*ZWnNHy(9M^}OsAOpK{j{7Bh+QjXscjGC{{3ac;c5 z9*%sVVxKi~KKpML{}Aq%aB+e8)|C-e=iVMOP%2BH%!hxhA2Bd`Zu*G-&fA?->=PsB zvpVsTsVev5>s^|z`DUz>F{L|4spkT#Z?E5XMnunf-jYxB2UP4EOZQbuwl%zz)4bYw z_!W_#q_)a)e&CPezJMwJ-IuH)%Cq$n@%|ku_FYT&1zU!-&Guh-tvwWauBp^0uy?z*FSt@Nx>yVK7U)hCl&i8(EY%gKinsb zNv(9usy_aH`2&_g+^Z+QdwgR~VL!YBq+jNdKAn#6wF&(G7b^ByFXvNYu5rU+cwf&P z^S_^-V!jH{ylS=~5{&&`Ui6)Y_+4UKQn62>oDc6r=g0HaPv2x8oVh=@v7^x~Ysi-> zVEsLx@9#9^_aRz?RP38X_f^vE9FprU@hEOBIQ)K6;ft@lTN|2!z_kC5FSUxej@`xc zkUv!HpG@}$nbD3rx9I6E*xxPQs$A8yt}}0Eo;T2!`Ahiln>jWMz75B()5r%8#QkLQ ziB!x_k>lSuKYet=*p$q%D=n2Wo|lSCo||_WP@ac->cu~4zk200&IWpKvq>Z!|LR31=1es6@HPNlv=HW*B) z3x0Vl<&9J@MfYZ@VqUr&Z}*JL_eTy~7X75%p+Q}j`PJmZv)NhX_Y?5>Ov0D)$ROeV z^jzd5{kej-V5)__mCE5QacsRXwWj2tBd&aJoBTU@UH0%hC}5>;uCmelae;0^%7_*x zzK_o@d#kUh{nB6hJs+@`sBfVnDfw~Lf{+I59DQzmchDQlmVi4onej8;7tS7^dF0BfQdu};m7bClT+2FdQvbOZc>Jko^BsI0aFp57! z;?jC@aaEJGeUZt^%OmgN9|i#y@UtTgByX}FXF0@gnoz1Tt#0ta+JN)29} 1701979145 -0800 clone: from github.com:dtolnay/thiserror.git diff --git a/xet_error/_git/logs/refs/heads/master b/xet_error/_git/logs/refs/heads/master new file mode 100644 index 00000000..a4c80ab5 --- /dev/null +++ b/xet_error/_git/logs/refs/heads/master @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 e9ea67c7e251764c3c2d839b6c06d9f35b154647 Hoyt Koepke 1701979145 -0800 clone: from github.com:dtolnay/thiserror.git diff --git a/xet_error/_git/logs/refs/remotes/origin/HEAD b/xet_error/_git/logs/refs/remotes/origin/HEAD new file mode 100644 index 00000000..a4c80ab5 --- /dev/null +++ b/xet_error/_git/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 e9ea67c7e251764c3c2d839b6c06d9f35b154647 Hoyt Koepke 1701979145 -0800 clone: from github.com:dtolnay/thiserror.git diff --git a/xet_error/_git/objects/pack/pack-81ea73449d91d43b55b4cc3c27f14c04e0583fd5.idx b/xet_error/_git/objects/pack/pack-81ea73449d91d43b55b4cc3c27f14c04e0583fd5.idx new file mode 100644 index 0000000000000000000000000000000000000000..3b608a3acd3377d0effaed92255de57eab304f81 GIT binary patch literal 67880 zcmWLCLwFtw7zN=nZR^hB-Ta&Lob%1%U(9^FE17|S zfPeuI0oVW%00V#V6n2y_Gk`hk%F*Z}+hNq{;)3jlNjvjYH~z<>;}1VB0<4^ReZ0(1j_PGF0G z6#$R}2IPPNy}*DRuul*Wa3}y801rS3UEN~zT zyafPcfdieurvZNf8vvjW_#@y81Ox&GfDAwf5CVV=A?N`-06~BRKmh>U7XrwFa0K`P zA^^z%;9d|V0ANE1U_*#rz$joA0OUac-5?GDKraYjKS*FdNZ?+OSO8K09e@SE2LN`2 z1hOE3ZjgomAP>?R5D16?WB`CXNFWcg763dSkUapPALKXy=m@zC*aKVuUO+&A89|_c zj!;MdQ~+>0C^7&O0Jt5L7(fO9+z(0{U;?lNI01l7p#lJrfK&jmD-^IR6p#%CbcN~! z0DYl;0p>nq45D!09F7mKnkD+0D3}O0~`QefM7s00LX&| zxTNEE1hf&Gfw%+t3`htUHNg?CULcXm=! zqYka#5zlUK_+o(|hMFsif5Kh0dFJ_iZP50f#BR|*96(mj!<`IB14<9flk3pEaPx9O z9R4t*dvYTOqqJ!+27Vq=@vMe}#5AFOUUB2-ZBd^ncwLFu(8bGvlMzdI7M;{l)7EG<(XLPzc*qGyc(t(@gnInQ0Xv8Suuxx zcU5Ucb1yWD%US3UP!*N@v%hu*IlP?HhJbR+4kqyKKzcAB{OTa#H6P46ZCjE3vmC zxGJu+M}4Nn5xd@@0iU)q28^V1=L7>9Eg{R%SMP&MYV2XR%kl z0xhZGWi+q6A2{b{8DZ{>?6EO9_n^Z{OlzSV3%I=F9EvdnC@5xR5jQvmcbQiS6S!{g zKUM7`y}+af10zSG5{u_RF>qhn$lVx>Y7vX0=*e`9(+P8&5Ae7I4USmaR&Hf{QWo-_ zMPhCW1n{PWXGhBwED)_|@y(}we~)#vI`C$5s@_Czdwy^2DX2#It7WeqN$~FUMq{rk zD5xx$xR8xYx?ZH99q>ak5h9%Q>CUZp_L?&a`Mal|Y9M$+_L0FrGX{we z6G8P#tREdmyAV^SPbVPV`B*hKnyih1mt0(jfd( zFOZ0i-Ywee8v8cN2|X&62Oxqgvji=2mmQFB8i}0tKjP)D@FCJ2I;n#vscm#=?%Vsa ze~+0J#zJJARe_Cjs9)OHyy0<@tefd|gFr8Q0K7md%3iQXxA3!w?X`OG@1Qn zl{{I>VE%{Jnt2E_2NV)JDNc4b-!y0JrgU|(?M%hSas(2)8s+Rw>)*XVOa2~HVEgvV zNe(1!o!y^%kMLko-RU>#ENTY(69Y&FCut1fM_q}s7<;}CLTb=BjYUXLA6oKXU_vFg zp2$>W$5ORJQMQmNlzr2JHvcRH84>1~nWdP6XAvNC-Fp*hMOxYhXKdpZsQ5jrip(L) z=zfKq5(dNgs9SB|hgLbg_&Y%Ezv34M(x6D5WtXsiZY`@;ouxzG6uo^$Wh6+d~!A-ezmx zHEmDNK??3ou2gcL=0R<_tl{XTUfa94Ht(mI%-0`#P(YnRpiHOe(CnxR&S|qoE9rPD zeCujfu9r;9e^$b1c=&I9!`h&Xx zLMyZ|BDH5epaJ@+A1@3Ns&yL?ts9Gm5EOB7)e#1vPW|r#m6^0=ADtrklA_(xD;kU< z$v~LNKYfyB^A794mzg$)2yYn8%Vn?DDd=sM$7KUKtqIe^MP(Qr5@j4YJyipUExikL z{F?p!<}MiPS!C0k0LR&yA|yxiUr{wr>$ot^owY8^0{eWS;{B}ip$_R?Spa{3tD9riF?OAZc+XKe!@^Gab=WnDLP z>Yp(GeJA>4_1|S}1r>7$Ur5b|*GR%_EBJOsvd7c3P&Y8dA}*KR#^l1>V=e1^$=omN z9n<4Ow%?YErs%=qA7N%}7`98h-0E77A?DfIs7=5o%u$V&Yhi)A%TVYv4pM|xJ*~ht zz?z1W)x~kOyo0NPRB%NFjs(K?p>Lfg*m@<|1yH8wY{fEFQ%=GD+i4;gqiPa@L00A~ zf+LnwAu)x$ld4~P`u*sp_NozA%z9*!mOKvotd&qT9!=T0m`jGdYV{sP_FxSA=I+?W zf}FA1T^U*6&xR{3vK#5%x(r3K{ZVcTG?0@`WSz$BGx4S z)n^7*B(O30?8DZ8#oNbCw0|%Q<8cc&CUKH!Lp95@uqX@tkrRumZcF+la&F!H{_JpChFN{oy%bG zmC@5!X+;kHR^E6h=1(SQ5tcdkKs!P+NcJ8AjFrxHs|q=bO-r#bu8>dmWg8;`k8$HV zvbszxIZ@nVF`+5Q+3_xdaKQs8+AWo}MI(j^euGNsN&Gv4Mk^M^h^n@TEB{E0Fl*bt zAvGlgtt%)0+IKX*VX0a!5TYtA1sYTYoi}o)`f>fLm97NxxivyEd;(_#LymW}qO=sf z#mlSngFl+uL?SN;0dF8@ieAjA+^f1re@YvNlHOMkQeG%u-*i?s0!QL6JlLmswb)4! zs=1*1WlYROv%Q7FzKdZI#3IrVIuB4x%-#oge?8#^=}u9D%`8D8bd7!Nje0%A;wg2| z_;b;liQ!Bj9QKsS%$&qmd3p){Cn6GVBQvRlaQ?I>ThiCUXw`4ORwFr+f?^(qh{+Rt z_iSF#DAoB8GN`qtfga0=NPa%{zNKB3G{R+jxN3Q9DUyT@6&+ zkiqgKqF$-P2aakw;uPxP!*aGUBxN}mVj@RlREq}@YDYl<-^cx~F?a_p60L@4Rs>k0 zlikx&)6{1icf3CR~lhHH&q>NY4ClEmg7l(c$JqMEQo-IXKc)4tEKRTDkk=}_%abCr>Eg>y_j(4IqaB?ZGAj7?> zP%tJ4wgf}xt!gE@x)OCnAxkCV-meb~s21OIxOuq?+`tofAWMzNnWC1=-9aPT9f-{K zNLxm2AS-#gZRnk+Z%xSiOO|3ee2OF!B75Cj`Kf0-5?fC}*%yyDknyX5BS#w~z8A=f zix2vBtmyMme2wG$L(VtcKscJ`GKw`y)Z3k3Zf~1rMJ^+@Osgs4k9`IWh^(91#R)oR zN3QT`?h4dLmk2ykXH8`M7O)E-K;HG=Rf@Yb>Y-ggYV5zfo%`};M!scLE^m+}Fh_X9 zNz;KWu@~ZRK*3X`_`P=-5M)n7Ge@s)USBHcj6!G!sa|kP`DX~#gQk1QI1@RR2!$D5 zkkX#Q%w#1md7qNmw&=323q_mxu;T+a`#GvYK!XUGlC8s)Z zU{Zf?p?+vM@L)4N_`1`JCFv*BLuS(a!bU}kU?MNe2*BA@D!xy-L`P6krA4KO5T;c-`Y+GG zRK=vfre{@Tg$h+@s)Q0W+$Z`O55kzCurtK9p98gkf@dbTEFz-33_4b#p6Tkhc@}DU z%;k1NJe=@cecLZ=+qI@W+y~Tc8HwkzUr7H^_rxINnb^Mq5RB%btJ2BsFiZNr<#!Pnn^=_xd-DR%-gZI>&*4v*QD$N2|7Z9$e-)L zr;2Kd5f7pYDKuIjvF}GCw#vZt{0c&)f%4SRINpeU4WdM&FsNkssw5SyzCmCD+c;Ep zCc;Fci(D_7)}2UIN=`9tO`FlAnHxb9RunDg$SI;0LE>57mwu2(swPBJ*KR0V%+fet zL_xybuSssCl|e@HPH|{7*CcDL%Goi1W)_WPM4dy+8h1jCtj>ti<7k2qk;Dri%?v`T z>oXF}T}lhRA)Is0e$9H6C%!-%&YPfCYQk!R;E%hCXKcBfz2!znY=VasMC|B$43zIs z_yJ;Sqc4Uo5ebSUdxiWEGdYqvSri-6aAt@unNbNPQK$P(Ho=yd-wMT&Up>`?kj55cJQBNAug^;5Qw}2x4OURC!F+FpsKNq z{p-g?LQVx|eptyENKG7ce#(L7nivT-2@^b1I)>l-MV=yzUoKP=JdNX%lNwecEHlnkiVozPr4dvli9W$3G-VQ|rHh2FG z%J3%13>jgj1fO3=T+0axw)y7SLHaPHeI5h!s^x;LFzO-_b{c!p+CM>-f_xq>bd^F| zz4&ct?4}7YyE-OMydTJ07jZ=)ox3z-*kg1?hCff-e#qW=+1?`{>+5!HVqZ!ftm~c= z=++~K>H4uGMQne3U_W$oS>~t)xbS&84^z6w$%~}1;h-1%JQN}-Gu^BLBa$wm#MaHw z!NIfKVNd|uNMNk$Bic5Xz-w0G$6C_q70 zY`CV()5y#%!;$*C6QL2S^0m9thepfTjph~Sgku4_M(20H;YeKu9hmm7=&$3%5{}Q= z4BmWcZKRR-XDzzyj+1Ji#{5Wu;oe&KM;GEf$S2?d!;RyTUg zam59dBHUHxgN5+!Pw0;p8z2)6MZqrXmaWHI~KQCziGS+32Q2(i1{}he%7y-Xf)hf2cn*6xmh{=zi)8t zWa*RJ<>k23`d7gJULkAHwbF0U2v0&p0eRd%qIEV+XOYh7CZVfMcNmcLu4s55@Uj7M zv^Q=d*dko8Ao05%X z>Iv}ptdF_-R0tQV;hD+P@Kx-OAxiKJ`kcQ+$R2oF+pNHf!9n*`M)L8JJ6$Z&&hTp5 z`-{ao`N?Qxa=!7J_o7foZ?aikF@Noq9W4|es9r zxkYRzUiyVEEGEnm=gC_pG{Bfa3}gPgeK-nV@x96`X%3yBl+-*Pb$vNfCDRDsaH2Dj z+)tlhAcW2z&LU?18H*4LwL3pGJj)43O5ZZSFxKK4Kn0ZuvMlv?(Ll$lYQrQjd$AtsIfC zWQIOCz75oxRm(vFUIiBTc&``UcD}-JsUWkXyLWj4TYgyR3w5=_924h?t&EjVT+=~< zKiSRfdzof<5GH}kH)3`EpI21`dsny779^FbHg*u7{LqBhi<27!uk6sdr?WESL3>ql zKdl(crv5u2gy;R)=;|0^jIsbDhLIX+=x~EWNZQ==xQVftvo>fSyZlDPLojP>z&5>&Gpx=XtA%Cj|;b z->Zf*VbLDHWt%iH=!oo`$WdDRUG|(j;YF(@URVF!*;E(aHC#++#vg1O!Y^ahs?x79 z_Myu_iput+y*X$JWpUm+l)_Q8<*!>8u|L8V) ziL?{KJ465Fzx~D;)Y@phmUY|DBQifSj(<7FrGP1RpD8KEj?gGt0?qqSY8!&30gdv7}3~|;k{?l%#}s=?lVLB zsl4dtbfOgxln@l3Y11g6UB884PscM?HbVq2iEF;3J5r<`NXUW zBE$#@RGvgCWP2Fr3}`zY)>5G(P{cgW8js}Vct;RA1`Lvk^OjtGd&K(v&P4NBeo!(u z8tS%5$zlh8BrG0kqpx%u1g3ZXp1E4?xxFxx4|u3Q;9p1 zBR)Ox^d}T^hdH$=g*PJ5zllfRZ$0cKh|jf+NMvOUOBGvQREVe2@*V^V|7?-MwH@7P znWqeGb(4@Vw)}+CZLaxY*YsVD+ChyxKSZMPUL zBZE5BrL{zy^o3+!Oi7X`CZs%o*1Dv;V0yVSK21s^c21I1rolm;yahFsnY)E}Cz`X; z15T2a)xo{=@4q)s6ULsma0r!cfn}1$);EEsTb6+Ks_Vt|Ahp-R#Y2)VpY$SLE*b3b zb(3q2cS9O*%_owt=y;Y|@wX^n?JaPKhxWbl@;OqBe0}@gXZIZ9DU_f2#c)2??jNMM z@p*9VkR&P7O`QW=vS?Ye@$;nADId%&Gew-ef0U@GS9smhQ2vk>3Vy@*(uIUqh#PQX zbhO+2r-49Px<)=ofG5XT?wdB=W<^xGsX;?p-jcw@T661!S4EUlWi5n@|3XJvA1)-T z7;5JmrP`OR^ac99LjO+MzSc)s#J-v}y|Q<7zMVSQ&UQQ8LT11v9{&oVHQo}@TP|6?ZI$SUm z_sc)O8ZF4lbl1AnUDS2@LN*1xqanBYp6^k}0-v`%v5&#!74ly2N#76L<=IfkF3Kp# zO%Ttw#5BE*p;lLTad7p?A)j?TOQFqB_J!+l!m?cJKz8fNaptfY7Qkhg(rz}j=YUV|9goeXTeuiq{<%G za81`7Q7)HBvD{`Pcb6|elR+1YOrRB1jLW$(2<1j0_o`n7Kgt~#{BOUVr)^TlKK)-l zxxeUv-u=Ks4m!jT(&*zY(h@%=dBtm9NTMjaIg~k{;tz*X8wnR%@=e7l5SVb+QqZ4$ z`Rh)SoQp7GNwIVW(-YV9P$+JMEn%>ghs?QOv@dILE~H1zQW*MwVx$NMJN|je7jIU~ z&}zwQrf~WBJzsPC-NXLfaq3t<@T?PuNfDBQfcelvYR@n+IqVM2k;yv>Mv>~KXSKaa z%3Z}0$JyU6RTn)fO_9O1H%XFzPi0x#S}I)jSBY(Rmm<4I1G~_Qw)B@xD##Plq2o-Z*n=I-0TNCY16_ zi#i-m`)fhhmC+r^>R2~9_LOE!vG3UWivvpuTIBtSss4HP$&?nJ$2gS3z0!~k)C&2L z8+{B77L-G#I%B_R&NgH~r2QY;PQkKMTB<%>$2KWzw$w=8botam+@m(R@b0h!9y zFYlku-+={cXH-NBNT_E^TrjHgMl@{bPD8)Y9*3|@9(>(rLuIPQdJJ z4BQ*f?s}ry2&YZ5T16^h4o)K8^FrZ1Vn(3);w3Ys3Vg*GRG%JKzM~}?*JYrFDV6;o z2vFjrD#+jeARS$+t97APkqf>m5iHrb^{`SI~Vfcfqc7tL>4yjAm= z97Ih`_OdtH9q*aUSh@gLnHHBfhLu1qJZ$l;t^Xesn)z=QSk)gVe@!x|v@hNtwlimj^O35o|5-}2414BW*nR_ItlQGylgKPk$sYterd;$9Y9i zp9bD$G}6%J#u~80i5OmJ1)-YJ;AR~g+pQuvw-%tvDo}N@AMvx$5Wk{3q6$>yTW8m2 zh_6qd^4v($80NAfqVh8KSItjbKqi4t1%Uphv6^M2Yb}mxK>D{7j7lwlj0rbL6VJw* zB?IMhs&uA0Srxb9%8~(2lj1GfC73WOeTB%3*r$%)$|E7Lj-EkByyL>2+Q zX%=on6xd|5?nsFl?T>aV#yJ$7rt#u?F_NBfIIXNCTETD>>57$K-^k1~#VFMx3yxhv zv@$YjGx4qD+&6{BUc3EWYKogBw582ZiB0T6F*h>m{CUUNAHqs6v{&lJa6_a&WpI^)?9Q93ewiT;?D;S8=RZ`f8&L37&v3AsJdKx21bU{|q{odT`7WRo` z3K?g5Y4CC@y^A@co3$17Ec;IzE(&h@8NMFWzQ`hB<7`YdhYH^Q>ykeNmK^~Xb);57`xF5dcEeLJ6pyLr^2b8qZykMp|#T|diTu} zb9gZP7x4GQT$&2a)*Uz#`mot%#!Zc(-crU3M!nN8w9b41`fL)bz2-b0{xr>NSr8=& zQU*dR`W#Xf1RRL8oevZ8bp$_`pdkH8`u1eRkLRC1;Qp)QQTySQt7NwNM87pS;6)~# zETkn>ZTm;o@2BE9DE)Io`K4Wj|M@MK728ZbHe!^1|HvqB)PR(; zCj-Vf`fjvJ==Eb3??5(8z;s+$5d*o)(-Skv+u4nICOumn-TtB&7z3jooV>?Cvwe}) z;-=(rUNyc^0E5M!`Zu-LV#ZH-exIiH!^{H*M~2)PHL2elB`*Wav;VlT5K|GH?HEcu z%dI-68ZTPT7&*g5ho{22-5Dw!Hy!J%Rhdw;Id#MOupkOmn;6ccc{D?X{K|9VvHb5w zAFFV?2N`vI$~AMO*m5O_oBQv+sApQ2Mj3VTF|q$3DW?nL{_2o1Vb*O>&ty!e9ChCo zz_@;aav#)pR^QweUSTYHh=;9QdS>h_i1o?yA!B;nO z^Ae?Sj2XMTtYZ8csgXa&^H&d@PO5?wjiJcP-DSe%#f)7Wku?wMhIFHD|6ps$lw)E* zGZSUbB}5N)lr0yV+IzxA7G#p}92M4pKjkZLhguh&H5D_1G-Hw-d!cQt|-oWc2x>Lt)?l>QfU!WdSQB&aH-3Qe%9iB)mBg@;}INS zL}!NOH|qQ4$kzZ)ALbPmawkq=XTxkD3*{5(NcZb0usP~P{n*>n?{9fd{wgwoFQXFCKFP1yVgnibAs9afQ>i45<}Bv< z_-SP@)-ezmq3B=#b;K@%iE1z}=Pl8J1e`q$GNCw#Om<{MAHlMqO|jxas~536?RA)( zS)z|KC-|^XJW$KyYQ8GRp9&z)K;1YQW!1ANueX|3XCl|TH@&V`OWSK9Wbm=*X*Cj1 zi#ZFoDKS&H$Rs_aIK#2%9aQ^-%UpZ-#piF**$dgC6oayON~cZppetCXEZgC7CMd>g zDKN7nTomGJuJE|m=w+@Zs7cWmhN7}Ge#FqoSXtg`&ez|#tb?#k@Mf_zYZ6yOBr`--rFJN4bNZ6^Fg8(x$z!-&eS`|4zc zX`Oo>xz=D*LN_HDnCDZhT6APZ(hw_|Ont*&`ZYeHU`?Vn8o$XZf2#56eEG9IO+b0p z?_%^n^}=7Q3P|Ku^Q(WnehRI_wjx&3Xi(d*rh3D2!zjUlUwvLd4!EzPV^d?Z*7)!Q zQ6IDH3q;{k?yBw^iUzZ?u9E!=cPvcF)=?xrfs5#tCfrB^`f5wrB-tj_b7$r;EZX-m#SK~D4MEL)RBk=Nb5mm*v^I< zT_XG;gejh0R*kQ~m7Tg8*unC!RD@gP3B$a ze_(6fvST^Gj`tnEucAB=bwTkCm;Dcl3nSN&t0s$ z=ttqC|HtWku}Lo%CVQ0=sS4UmL_034)-$&NH&;&{Gy743HXQ#gFFxNs`dc&@LWM>9 zKKuQdbAGA?=f1DFrK|4P)7A7REr+njHQu}N=CfVPr%l`jI~rN%F^4R@lyllyA%7aZ z`oe!kdAW-S845iTof|)793le zFn+D=4Ni-?A#mQ15CLY+92}2ncxyK~dCRov(o{zsBWElqQk)?A$K2a}Wvb@11fvJK z(;0!4ES#w3V6Lg@T#&OpAkyvgoNj)$r<`az;E~80Ixb-vV8(RsUL?mTe>u@R30A-# zv&$}+BKx-TWt=^=t~hnZ{sz#{%)>S?)!98myqq8M;&2*C|D##3ipx)W@=lbrqHhq8 zN9J5I4&+6z+9^rv2v68K?5$78?dL)(R8^V!N+DB5HS#U~H~s^PjD19c+|P?N z$Fm5rY;vyYFP}&PS?_y_$F6y-_eHiB)>dxl;Dv>0NDUIa(Ab2&ZBS#vziQkVP0COb zC(@v!6vJ80CZlWsmvhxXj!bN^394dTCI7&59tnaH-gAWlXeZ6023yNiEraxdSfUvii$1`?jY%DXS6h6%szE*B%1jW~?eVBQOm-lUOe=N>$&tW%xc&)r6+&5?~el4Zbnsn3-su`HCfyb899pSb%KOJU4;8FlecQ~4X} z&mFTPeqYV`<$cKWvd6=`$&+0EFx*VP{SeFiFYEP=m!F3vaJ*{>25g5qxDi@7{h8&E zR~C4!33~uSrEH}sw-^CGsDYf7SBw6hfzHtUYX6L&r`K4M+x4h`*X)Zz@h7!#)+h=qt|<*J^gffw8Sc^;iTZw7XZC6#xZ4@QSC^lEavXXnr}Z_PQ# zH#MBh{)qZ=YaGfc$KrT7Z&!L#mo_;CuEpLf;h@5}nzZx)Z|~@)v85)=U03uu`W-oa z8?=EfZ{NHD8hgRk9_jbLjekO^&d0_Cy!|sXJJjeDNOPBWtwZw*NKpE>LWJAA?OY`P9!+9?!mN)i01MSuMY3;0UdD1FJo(YtyeKm(?gyUzlju=!GK4i4AZKFMW-5493is08MP7E6xvHCKL z+k>@SeyBPPeB?L4HSRE&?PjY}tjG8Zk9M_5I?eBm_iOioLBz68SUx8fJWTD?oSHwB z$o)|t>NW>r+GJVCj?F+WR)D{xcwO(d)?-s+I|GfbhoAv3wT-{j<69^*5!Qak+s(Sj zU5o;2%YeW7A{&c~B9frLSUW_xc$_OfVwAr%2S03_&B;dCpu7WzlZh20k&geDuJ)$3 zs#Z`{zgfq?9Nfe@EhGQbQbqvT5IbS7g*H+fz2(|-=so|x+JfTb6}WQGC)F7m5)EIr zMS1@7%LkDM+TRoRoanygTLh=i0r~uQ8zE;R27xr3>hGZb$8-6LuS-AJP`VV9Sur-z zO>x7-QQVSfWz~Xt7D_i3oG{pL7n7><(>yA&1XrLTmQYR{yKc0+FQtv zI@S!`GNNFJ-wy=xpxz191Ycf18U#`)15T3_s+1HiKM@^2;Qws>Xtow>+b&Dm-R6y% zBSiNiSkJ@yv1}OOXPkEiE3WYx;PRz0Y!a*WW6iqqEa2nx4ZeZE;sYn==)tz>$3?uf zj~Z?4EXWwqYZW1ciHeWA09Ixl*eX`6iGzGg!Bxpj*-{Lu0QqRqVSo(on477`>Y}=w z6&c-^0E-PW8bl1ce+O1X%gxPS-Pf>T0kOq`h1#VJmGwl;D+bw-vRAe<0nJi{)4`b+ zoJFohFMQS~tM+pp0oxVaVBg-4@(+t**76%RaW#hqfdH3pT(mX9SvAo9m9O4xMzV%Z zfvN|LkZD*5?cRd_s7xgz(Rm5NneQ)G2vW+!P|TUFT)1#S{m}89l{b+y6% z%Rq0JXb!0juOY|hXVh6n6O{ff{4@~|d6Nw0W$rOqFthvmC}^Y#E)KOasAb8NWY_sm zejv#wM$jgwjNcN^sm8ZXVb1{V@|9>YUNB&*lTo$etlL7MmLeH_&Rz4qP%uycVVa~7 zTjzwBT$de9&p`NIT`)#3JU=JuRU4&Ko@T-n8-l+=QZUY*9z)b;8*7lIY}K?*WkpKL zPOw-yFJiX9pRtC<$Z6rakcW_pg!wQ^8-$1cpa0PGW;)0A5n!=MrEsOYEYjnHpJ%mtQ z^efD<)`_=SkfZ&wr(cFIIE8{5^fux@G7lDTsleKsX1XNk3WVAhY*j!u6=-O5aHZw% zm_kF}UWCcl1VW?~)5Mo(VqIziroCJm3qBlnW+d;U?<^L4<&n()njp*gCse~%AJUH z=G5)x;GYM?<`J<+$icz3bte(Ks_>M6LfHL`uG9;%0OQws<$aM{9OW0Uy+-VBM^UTO zOQaWG-x84`O3-6leO8nG0pSaLyq$)Xg06N&A;!Mf z%dFBW$Of1P6vI3}4SJEG1*db{Aqt55u0yUOiJMv0taxJp0Zn57^0%0 zv5)%l=2+VyU|Ztb{C%epH*TUDBL-1Abo{&yZaf+Wo`Xv0Q)^_2 z?V=@2`Uj%z+;ukz`w<&Y=t&g&)JiYKA=08fDF3)$PvK1r72~T2+}QuNxz>pe(HR=~ zZf078cg8F~RlPy1Qj&@eomj|3ehy((*>ha{%U9r&`X~?`2FJRmHgwsDS}lAc-9>}} zb)^)YT}jZY(L$4+uKK|j3A*QsVcQ_O=%@`-3S!HzS&A@J zbC~qsdohMwapM-ED-4Y73uT%HAyW?Yf4sBRX+FF}_x+2_45RV_1XyJ480*BTB0lCs zk5Gr0tti1*HSJ3N2$_T#Y0BwuffaU0m>fN|mZHo?dg zVmW)Y#EXN8iTCYnGH#9WaR^m7Wsb1%qk(ydDIWfosPZb8aTyNQ4UiBi3fnCXeo%=8l5#CE8QD%PBnOUEp#STK=RFGxD3 zo(_#t$>C4O_D=C>u|yD>%8-_NH-~n5=G4ygB*rK@u`X;;+V52Fzhr)GdR1NK=c|cs zVrv@u*5i_or~eT*VQWFLC6QrFi`|!0nB2a3l<=|_>BhL6`0Bh&{!{1*&(+<#JH7 zPSJZ2#BVk{l^CLZ$KL|aS(7FXdwXWp3O2UPlY?-$vbKd%Ur5jC7 zEv|EuM8eM}IY1%(eSJ<4NAl~|SFaMkK_VPYuoO-wO!~Jutu{8qF}1e8p+vY{3h4NB z+7{e^8k4lc2up8cvqZKo7q;tQp!OfA>7Q@&+>|dXsFL8BC-;}kmYCeMCaK7+j5Tha zlal1G zC40o=oe!KO(UP*lnJ|$%ip`~!YP>U8gMRte?2?L1K8a_r5sg0>ux2zO9cgi%-6f+6 zrLMEjHmyr}B4#*wW;`!XDkZmEg-9F?9nP>o1{`@!%$0any zsSqB{=#!$|=50z;qq-Uo9NyOWO>7?kqfK(nWx?ln!tJ7??aZLZ&3sFX_GPsPp0 z$%bkkHh4Oau{a#cWs&-s+84KzFr@E8@5jX*EO^dEsUccE~UE*?LG zFKwAT<6LEksYGcyo*Mb@RN|U1e7`HY!Rj1+o;+#gu@~fSS_F*qdu0_GWdE``g?ZnZgybX& zHTXyxQbXyKFbqE8wr*I+Hx#TYG$Q2yha%~WrNjZJ0|-}8gglLx7vqpusTJv(EBog< zvy1a}iDvruJ<4Dc{x<3E$B|a%nrEr|6A>?>{X%i>1Uc#6?T0dkKL;yoZjI%2CGrQ_ zg~HNLS&aggN4dMHf{jv2*ajmLl$|mNnT*!v-8mEv2yJv|&0|Tf8r3q0Ln!q03&i9e z`qf@-Ca~@^u@5rH2=!Q=QCZS?>?jL0&i9mqbt5ts(6RXnS@B)YFv0{Y_urTvqA@Z# z&0c5F(gMk}4|jVRLSWA@|4L+r4lI3Wxr)gZnTwo$vxcAXhosAlVbU-5@4g0J1?XkZ zHDhotRo%<{)w_844`7mn9nA3O^lEQ8AgM8 zoBW61!Sg}vp`j#1S*tIL1>w%`@npK4A1x8Cjuan_{<~dP2tsMLQg6VM}kpSndcKD@)WQ7u_xni1eTB?(GN0Nwf@>{k&cz> zT}{>$v2)DFr~wRF<8sQ>ZY!3H>XiXh&Ti8cZ$=~8jE-AO$+6|SUCeGBi+r+$|9a(Q zE5JcSHw!%<&)uy(FFqQ|_;=uBD~m@at}Vyk(IV$#$ji!AHJN8+TlOA6jz4=LkG}oV zRoW)S^LOK9yYIasHP+$4m>&4=QhjuEGVM6!pi1}5ywQpUlnF7akk;O|627zLNNePe z+f=t(dtL1aon#-q1SaL>_zMjVb3rE48zpG$4AA`+xN!#ML}oA&xOzl1Ahmf+nJAWW zlwhXiv?~U|jNC#t9eIOfE=_@(pw26eejhdZc zdCpJT>den`^#;V`oZ<{_9uOlyap#>w5D6)a%bVnT!EL`rODijyjcq63$rf^sthtDa|*nd zOTiVXyrYTR{>nMx;tIW4xuJ4Xrub5*{MH5LVZ&Cxn_n44klfpF;*3PA0#8^6_x-ys z(~+BVD0KO)Qp*+3LkhIm&YcFUV>%^ z1YKdJZ?E4viroTK^`WVz2X(pEx*C!x)M0tfii7Z?>`X6jn6 za^tpvicgk1SEIZ9y*#dvzo4FJdJj2-mEgPkOVx)zx^%k#AtQCtsM{XPE1`U{HG!Hv zXrT{}bN18!U0XgeQ&JSQHZuUn%Gp`EVo2#d6e>FAQc89I=_YkQ`1=+S=W1UmTKLea zR4L2tq>kPWLKO8tC$_EbrxSu-jaBGB=YE<)XzA`&8-K%#;pyma6 z;%nYrGALY8lQIwSrH@Zz$09Bthg6dT4VnK*oH9>_&_X)>mPJZgFG(Pp5si&zlCngw z$FBmPo}YEDd648d^qk*<*2>Q0F0Dp?EBH@o53GWS?j6IfK9oKF42GaRyQ;l}O)mrA z6f013J}CQOV8s3%%jRwnJ@;XmE>L3L&{Gc6)fe+k5&PV!UYKcKD3qTzq*g93pqRO7 z-Z91II=97!CQ^0LQc*#3ehxlBN*Z01HJcDFoxWaM1yP~ayx!Y;H4}BV&7h+FowzzS z@S?(r(|KT4PXZ(3YWMOD&;B{m@1kP(#OhaIo#s}ql-VCEzK@$Lnx|6yZIKp*tdiYC z-R|JBd8qz=#HrFK+^m(u&G}lY-PIGg7k7Fg7a*-=A&HR@sBW-P zy&!6>y=7!mxy9~0=c8de{-VnUHwJ3G%!AqZay=lxz zv?7(@rBnTKBSz|iXx#rm($lglNA^Ko8IFF`_GhR|>bZNU1fsYZ*%Yv@;1R0e*PN@H z-M4G1p6aZ~_XU_4;P%pfYuKpQe<%H(uJtvI{Y}QC5sfxJt*@uviSUK;GvtUP=e$8I zZHyspX6i$IRlCI-5q#2o-s#`=_8))&@;Dv8zH6CKh;kf z{s%Ka%)hG%zd=74pke|{i}9E46ICQ;YPu2!}X zj8^dLw8`BDXk{&4AI!>VvsT4-eb~gnPlp^nW?fXVVBP(7gjU}PnrR2Nuz;lCYkXsW zD4Q?gs8;{`*F96zF#?H%<)AQ7GDQgf-&aI=?9Ay2BE_}lP?+7Hy1i*g1y@Bm5ke}} z3K`glOs0q63Eff9uUBSPf7zube?zV{S!fP?dK`Ij^H*@BB35s}X59^6uncCxs99U0 zMpvNx7AB;CFgsg1CpTAD>kUr>iC45ySdGES^b_pRJ6@?#&LjZocvrOPA&+ZBQJu9I zDH|$dl&0dshgZn2nCW_RD}_JH%`ASViU)za6j%m8)e9+7i|_1Bnltf6-|Of!U06WZ z(58v`$k~3P{t>O3ckhv+e^^x#h8}w&5-2=Ozjo4GCBwyL_gHLPFo8`qHB~oCY2Y>< zB|d%*Iaq&JMMV_{Vi{=FmsfnNV#V+}l~{)X|A3~C!|-Lfngve8N8vFH`&gG)DMvSx z)L|p+f0pJWZGT9ZyjZby?4W_9;{VJwIXE#oj@dDIN?6ezMaPkzI8-6DZ$?N;m*;F-fT1WvMW z8&d_l0=D)7DOqWnNnsOj&(BOY7EYm&$jB(cQ<-m&=^+Em@Rs zTR41JVhpI3eCEbQFDUZ>ty$N|HPZqpwvTW}Ia1+?PV|Il$XWxirE8;YRHI5hzb@To zm!=Z`BU%OXZga=fcuK8Jmv>{}0WTq=f8zh|WWNM2^QxFjxIx?49noG9*( z9zG|XCh(=Cua*7&aa%&AoTiXjB{Z6**diiDWSH(;5?gy@<)@O)5P`oak&_AoXyZ5f z_*;oWXqL?{z*r-deW31HeRheg%3GN|p>D9`BIfZE@nI^AUaZ4S*<1VC>*FlGfG^5L zi)GU*UfI-?V_Xo3i(C0+rT2xVpDcMA zHhhp)8C+j9Dqdh7OZ!R4z?I8a$?;CG$XsIe?L>5ZMbMGtOvf29m52?wWn7S4BX`Kc z@{R!!jj9m-$M(jMtX#IkA!?H2(FIm(If7N=@}IA)wp_)9T+k)50WKc$ebg%%>r}xp z(_Ia^akuJiD2gy!6qscn0YIAt>s>0!FTl2stp21wA)H>i)F+kQD_t#5N9*c~;o#Z1 zD(NJ3uwNiid0le*9Osp3Tvc9qY=+4=Hetugwq5uiRA-^ZjJBrJEUz7we4@Ne30?^- z&?s6-VpT-NP#8K%?_i+>C|(lABz0j!bR}KH@FHm8`*ol!yk0WAq>zeIJ=9!EEnPFA zD>$9yJYGf7d8|TycV?IB| z3J&u$QvO8m{Xg|S2VS5(*V+n>-SBXi6a0>Cn~{Tsyk4Wl98=PF1CM~hX8>MZfu;=N z0baZNfZ<#yECNmsZAhSMt}`@bCtlc~{05*ZUVi$A!XLGHk$a2u1zzpsYii$j9xVdp z^Og_SSLws}R$li>@6L0&4KH{^nV!)w_}ubC)n5{*JSFUAgafREtK{8I7%(ej`(HNN z$+Dz>%d_`YWb=_oB3o5ZqF+FWoRG=T>rAEa&pge8Cv~n5K3`>mr#-y12CQEmmkf>p zA(z@yKwowngO#ILe_}>7{xIK(G-O|0I$wSp;SB4ZW+d}P+uhksp1w?lXJ3Z;0XYKk zPoDc6vzCz$n1WM8#(K~`$I*6jnZh^2Vuwet=l!F4A>F~MRQf2KzF4A+LO<@jNj(K*mif}c5<$c!G@O}0b zaA6&!(@1a(_+evrv8gvseV2QU5E8m{{7A!KB4K6qg_dDZ59E4zYsz!F#ZXU><6&nV zLc4u}P%cV&0QEHBZ`P$J=i zg<<~Qx=3Pqul3XYl5FGcUkR_S9%3p7ON~jOW3a-fp3s+^JgM`o&SFwF7zntCcwbnR zsYgW?2gHG-;9^}gpZhE4gPMXnaDPnvl ziiW7)=lEgM{sP*@Y(&>|5n`Gh65zs@9Z1#eqOz~i#&f##)MDu7Yxap7^~$BdO)qMK zs`tp2tzzB4Z00F8tOMbWN7ay|PN6pcaOa)Q$?qjH`8oBp7FBujKKmCf`%-uY1C1bO7 zZq?b~}yL`4bW$B48gk#mT?zW3J@&yYzAqI6wPL7B{hhzt2r!S({ z-2DKr`pu%Yb|e$!GGr38eI)THmZn(kvSmrd z9%L?Slw=PE&MR%`6wn7{l6&4*@?FlDtlAeQepH?T%2ru*?kStYU9^JTc>e#mJv zI?Ny3C~LdUyc%-*3}x4CI2HvVTkI=A3R6Xw?l)-;%4Ol5n~i7q7i;lu53NWuzeYl9 zk!CjP|Enz_6~i$^gm?Z}oOrqrux34jR*B)z*bpfu*QGwHX?sqJJ!WG#>Z*}T^o9er z;&t`yITtYyIc8>9OzEd$^MU96YMvPP753AvnP#8V`aqrMQAE4~OzG=g8YOqjWM!M+>~*z97u}1NkR;yynnr%HD=k`)so>clwx<S zSWNmg*k<15G{A?ib~M=NmkmufOcG}L1ZMyj;ZUfA;aox?;AVeym3Dqaoo5Xhs^P=x z&U6X)`he6sIHRFtwP!AsCA-4qXe>AlHkVH{ach<}k!Lm3urs0Z3!XQ#U28nCPsil& z?q^4!TP;`-7C<&-FFgts*etio6lZVWUYH|FPn77&fb|G0Yn3N|PG^kQta}f#NG+48 z&dj)iN}jXEMrXzoKx`h50Kh+uyRc$iaQ^(vAZO@MWZRjQ?fWoR4|aA3@%bgYNoXpr zy^RE>oyxk~h%=e(81l0ifoLoCT{hGMJmiqJ@G~;pCVpvlyJ$R*w$X<3Gr|Fk z&wEL3g{EM^cS)2P2x!5LZqI$1&!>2d^#*t-kYdm};b`Fc-WQK@l#*R_MFWZ#jGy~v zj%fuih}az3&Mi6*i+KolcxJKDY-uoQEBFexVd5-c!VEtSrPc3*muWVc1S=Yh&uMlx z)6F!p0Q0CPi)l$KcY^_7edzJ(zgV4@H4U)bGHFr14&7|_**u4!g+1`9lb6M34vDPidj(;vuwQfX>95zet0^5Y-K`@@{m{Q%1_ooSDZPKp!8 zi2X5if9z`k9og)5&S}0^r=;GFyGufGY6CN1$uKC0nq>LvrSm?9OS2qP#%Pnu2x;FLR~p%BS%PWjE#K1TqB_@0fNBg`0@SFhF4hj` z(gaueq2tsO9BLXLIl1LI>z%^7DlV{11qFpLgKATXki(8#hG4{V^-pCDl+)>xo@!a% z^X&w9E9$!sou)-JU?X>GgleefYV{GNiiZ2tP(~qQ+v1|{ylS)rf&OdY8FfuM+(+XV znv+G||7z3o(r=cJ2&C1#BX>>;a{d@_MQYu(+Z|{oLVx^=dMGqq%p6x}yK3ZruT76Mx`{fzRyfIP#_;m|vL=HWHEW;CSSrMIFDZ7& zBXxISRqyps32UN8sBDy?vMYtmU!##3q!;cZzH6fw5+k%8(y+D4+#NXZDZAtUoNK8W zw~@`C-bEJ^D^Mo&uC^QNziY0u0JldhASHO6u2%Q?UAjO5ZEOCgKy(LW4}bIc!0$Z? z&(UTd_-q8de6wm3TvG>nb2jF{4-i>L25chS#jSVbdCmOXrA9Zk>;wmwlWZ}c!|J?S zN#;WSo=8Ul)p8JGz-&%Fs9xqV&5ySLzl?qN1t8xhKx|qdS;2aq#z0$Y+Q$^&c1)H> z$ZTF?TS}a3=Pb-yCTX(ydK^0>LTrZ3ri!W`%<+jG*e9vFQDT|@lWdluz5lxa!%gKG zfr6_Iwod^|acr5LE?T=%K#B<02*xUTiW$XmW^A8UrMT|_v*3>@h+At!pdyQtt8Fsn z<9VTWPTKX@@3?=g3C5J{t8I#sOc8R>Yy+Ao#8QR_ba?TfvTcq9N5D14!R({GKiCBs z2aNP38f~`BVKD&TKyAs!;|M)joAI7lCSl*vbsO)Uh;7~UM3Wq| zoMwz5`zKy3i)EsN2W|Rqz-T2z5{|YgKAE?Lks2o;^KLz?ntkU9u(~Ino;1hc#;+I# z({7-G1?$$Z$8`y_kDI&iNpJZpi*C7Zq-;fF?U%>TX=j%qFo%GlfNsF(c!(=%W03e= zvGEb(;7KXJ5N{QP9l)EZ8ocslvp9F88FY_@Q*S&2?0VDi5s!oICx|F^3Vh`lK5slg z<7-lpw{Ds@*rrn@xGMU{F>goTC+kkbtLpjQbx}GnYBEZ2Ja1q)g6l1Pz$d*!mj<2{ z^;5egfQP0%4M=A7r40HgDnW5?4~K1c-2w$Vb13BR`g% zPH+^qo*GHx>0P+J*CN2hNK8hnnQ%ZfrFln(4381Q&cxnn7$@RqL2&N+Ve}?LUzz)b z2}>Ek%mx}?1#t_Iq|Sh?S877OwQ6k8?Ngv$A8~BE zp$ZL;M&I6hYL7wGgVk&AByn;Tc)N6Ts1vDG6pFZHb!F~CcX5!v+;KAB1~EhO-fIws zYlrB_OmU`HzSuvk{hSrdJZw1o%r8CEDRHq~#)vy36@x_dZZ%W4`aY)J<#EUB><-i2 zi63~s<;|U(I$>+8cyZ4i?;FK?{>5Hv{|&D@#A}^3>~ZCik$b!?)*8VQ=UDMDoz-s! z_i^wIN0A@!57gd(_OLXIu*wM)dU6*oxDqYfmU2eW#han6AmNe{TgEsEJ2>MN)N)dd36~mm9r0(zwG^jb)7^4KKyq8d z$w&*C2S1fjGlkc#w_F?jxN?zMTN!f}DRVM@9jWd6F7XV;s<9&{sC}{BCUbj*6SSw;%#yjXok66)>%&*H*>j(; z9H19|gff20G{%}B61vDV4|Ab&BwzvELJDmad}^_ie;*oiiF6U_^rwj`QMkG#{y#YC z!qb$bxO5ZeH&T8B6yq15gN2D9dsfAug3Dm&k+8UybB?0EXEW8=X78wIVYWI zai1+`G0Zj~orYD<7<9dw3p#s1{TNHAtAd|@Qb-&E3Ut5cUb0)M)0+zAwQ$xEgzr3|Sak{(bJ`>qgqK})l#FG*`8cKYGj$AU>pqh;OPmDyllXYgIn<}Nc*$(lLL|ed=!k3!_8g+7x z6jCcK zhjvb1xydG~5mi6>{#{q9>(V3q7}rBshjwzY z?9J7hijMeJ@b63o|8ICYkg(It9Gc-NUCB^ISD=AF$#yEhHn@{)vKaXk+(RRgFfino}4NgK3weX^sYjf(|5OR-A$TuuC+QvFEdD% z_cj)ZE_cK}qB@*r4L1mEDH(LoYfoiG4R|rn>_V1T_T4XTFg|As$=4Ag4R}bkZTnYsQc&w0O1tp-w7Rj#T^b^fn`BCiAeD z`FP4?-(lj$ErRvV?!t-75etIU?s(w;?U4N(?GtSEC-lnj42jsDe|Z0d<~HrUeNEEt z;FdoUM6Foc-gz3B>yzM*5gXBMe#bnoQ1>EVF?lF#_0GAi&7e55BpX;mw`0dMGt3eR-QbGsNQp(f}jsN3|rrESK;)#d({xI}kEYa?fBw@g}rqb5@h_ z@p+r~2$iHD!Pb!05Fu=#E~nOek9oI1RYe|)%;>JvsH+hL?s_Z_ z@8PR-gTW7hXV<5z(s}F3vU)Q=+E0$ zRi>Zg-g3Wc6@BndtgU;7QR@d|LYe*GAthU?uzO;oe@_~N-Bj&zT>RuY+r71B9eZa`%g=o`)Rf)c-vI{6a||vyVS9)Y z1`rKF>kOmii$jM-tjKD+ID4qO7oPc`c?HVPtc^Kx>W+o_gnPq;a4k}s!a5*0qQt|b zwKtNFihIZoQ}YQUBsTOrAWDg;)~hc7V0+9q*P|?)CjlTFJBj0(q^_t0Gkeo#0GNtT zjGq1AkK7oKgz%dCw0uFwa2DnM!0Q(hDe7_)21w^STYN*Elph^WrTTQ01k>b%_okhu zTYPAv=iIg>?DcYs3pbW0@O{GMynKnMz1+E2qQhQBpanJDt8@$r|*is&wQQA>%6gIuuXV!yN(e^VNXZ7J$%Dd z-|8@|Jhb4OaRquNN<6E9CVb<8z#u45jrMMNyw(v9&Y2AuJ$&qOWFWBczrM(*CzV}+ z3lEqCt9=53Wil-Vrxt-YFk4PV_VbbkS2&+hpvvdwrV4oyacb4Amq!4F}vXUE9}M z&V90LktBGM<~`ZAsKV?RB}6gq)qTeymNnS6jrCK!ehxZEPl=K--!)M8@uHkR`bcA zN2Vb&Fn-n~<$!|= zm+yJ+yMTTBmUj!vTMZlnT#3+G`6F#^V}OEiR9WiD%%60!T?6Wozw)ezL4cF(*W@Ou zwQeRnEr3dn!sAGDgMg!#i4#9+eQpcA&lP%CP{^RC+JL574$gh_ru$lNDpLMp21iR> z4uH1ras-v5g~D3%gS}c+l9{9{D}c=WmpYD`nfrfd5`(83W2c}d#epC7Iar?tG<{dl z%3nnb9F@{k6J-Oo6oS`l*r`v3&>K3SSF| zGu1nh1pIH4I{3W6dFq;U1I>RNYpA;p~v>O)5OR)R%&kYR}FjsTUR?ta2jk(9Q~ zjDl&n8#Zo78B_N(OyBG)>A@+#CW3pxBhY{xc4SEut&C_ne+QW1=z@?bXU!ogo71oJ zwQGTEr+TkxoE370ii(1OG6p}|;F%!&{ss%3iNdbV5y2xY$ADuah|PbjT2+7Fl2{P34PK<<0xe}k4J@8iO_ zx0eJ6T%D050+BLh#)G96n$tKgn#}QJ>S`%RjsNbI5`)9{bcEP82#u?mBe~o^cce0r zAA{RwTGd$M12wEt89l@6tJq9a%7hRp%3bh~32*esFC8l=W%u~InS?`*-7d3L8qu33 zMugOhQaF0Mri5Ge#=OLPZzO2t6TMJ|GTFPgD}-T{*J8J6!3B%-WXZ7KFa;JggAs&kc93#ceeuK?*D^Eri#}7?RMO^>qaF=+QMc zA65OTl!WZ97?_o{#RQ_p8nusy2OS9x!i4pyF-c57n*sVtWgNw&5}A|n0)+M-(uAlH z!HfU+WK!}M!=N6-`h`(NK>?UtA!QP6VL0fk|7_Mac!hag{L(hM8|3xp+FJEMtEs8# zIE9~3g@bQdy#o}+8^Hs55|1jN)>@`jl_C965oN|-*kY9NnM#RB1b zkcPk|ULl%M=>NQUAxVr{L6aNY_6N*KH)=fONbd=_%p2WHd3dqIk&kWAl2L$ z_J|*>WIzR}Ab1+{@ACyglySAT{fIBhP%l2)uX4ObHxLkP?)i>7IEXy20+7e)?O^1X zFP6L|+QUV}c!*+Ro6N|Gz22`ThKo1>ZGa!W28e5}&H)~xmlZ&X^&@oz22YcSlZbl1 zBd7t=bY$ab2%TcapS(QwZHRoIv;5Yr)+~f_lA%4fD1u*DH;9DWJ(5=dz_LN-%KdiY zU9_rBqKOF?{_lv`N)1O`H^D~KHA|yUPl*@11w%Zc z!0m5I(Y3YnnnLjd>WL?@-V9i_@XhY8M(O)woVJkbSBZm*^%-TaSVw$`@6jxRtZ`rHISkE<#lBWJ4$oAd4~VZ_q&^LoX1F5d{C^z%azC`HNG` z4A}yV^i-eDJLQcE{ z4=2Bm&man(t*P^ZKa0&!o7ehP((Ar!?!N#VgF`E`&5QA#8INkeJaKRGO}GPH0on75 zVvI6HeJuL%J&YY<2r@wFdSVfwK#W_YIN*L4ut%I3pl9{J%eP|Y*Nj{vOtZd+ng!}G zd`z{y32uVgL5z3o8xyS;fgqS{H+h-{4lSAAwTz#Uc*xRv?z+c1zp1;`9WNHoq>QkC5+Ih^ItB{T|ex(#bpKoSdheU{*5c5kZ{PQGc&_@*NQ@u;zO|{h>c4LW<=;; zfX3{IG7}mq+IMzPh>cMSk%C%M5~xk)Js~+gCX-~9YmKm%y2iZ{^FcPip#gc!+f)!s z6pg$nf}1AyoC>`~Bnthdo>d6tRE@$Y%U=ALmlr4pQ688*srwfXXpRbTBf-ZcgBEMV zjc8YX?nkU7VvZ3?s~{XB8%zlRo(wqMwqUXdqmD1X&yqmIn7&mB2}py1%(R2R?~XBb z%aU@DNQW+><+a5{SufMZ&Ylr}g;EHp^-j1RXy09=hc_VYkohHE5tPj{%W{$kGJ+MP= z3-Pe9Hl0N!ElzAH&5q=Dkm`XP)AJ&iX$rRw{u=J@fsYa@4>prclxc~gfXrZ0<>wG0 zYL7u1_1KW*6FWbVEYlNcS`gYokB?j4s2zoh8}LPLTwIZ()yNN#36El%u8N5*Ug1u| zULq^u1GW-|HjitGZR_)A@tNdHWfMFB?0FQ-*7$5`KJa+^lNT7?HWWG$KhmWQsF_wa!HyZd|AuHYV zptRbfZI8NN?><89fU}(d7+vUs9Hzt0V2|4peCWtXldj?uS1Nf(=aPxl9FOoDQg5B` z;8@_vU@8UCt-$9|g^&UbNCYGs@KL5wP1sQZax!(;UXTv~iHMOi@z-Nz+?Eg{_+Cpo z8IV?h?%?N(2`x(A<+G<_9;kSE;*eVXbWIKOI7 zCXxatX|fI*Dsf7}HMdtw@jr?>R+0iU?(8j?V3(IF8E>t9uMJts;gSm1GL^3MDNx_T zhT!6lnGmr2wvsjI_7)IY)w^{iMfdNN-+#4?ppr%~n%?L^RmRATiZaVZAj8BzHIhs- zyT_v9+FTXPB^JEm_h8$8LDAR1*?*KJ(7;y+Yw9P!HaHDLEwB; zhXA`EHK2($tGHZ)aY#FHC8(Ruyw1nqKCt<_6o^#Yi4K@gsI;gypvu&n{yZ?eqAGn$>+oPmOqXgP?K*yV@Jz0QetoOshH^mLk|%` zxs!#&3^7JyTC$z5xT&>ZvB6z7dy|XK$oXT#q;WX`s4isi%sNo7S(Eb{eGa>B31G;v7J96SD5r^_50nc} ziLRpPoM*BY3()wu`&9WV1(YkD^KT!AP*@SgrfSpjg9hPZkd!`C*&RR1BrIezm+^aJ5l-=|Kl)S;jA-ZBH1w?WR+Peb8v9Icekn4Zct5`^Odb+2$g+o z@DvTYh+!^$7&%#fJ#e}1B9(tu%TdaLH|o2ncnZ578H!Ep@|BYPMbAdWeM*eoOjbZ8 z*dq2mbCsNHDl*8JkiEin7wt9$4{q~q2$i9aZL`>FSP*l~H)f{xgqoR!U(WY*Dp*;=O1T~ zEtS`6J5xQ*h5$|ruLkjAoPM=Wt(Et}s}B#7!^nQw(J^be7=fvFUwOFQ4vX)z5Z!;TF)n>F{CHem`8hWh^5|(oG z{(Fmw2Ozn`45fcctw)OK7nZY`EEk08)}Cn2bzB4SvlQQsDVDg+h^9To$I$0T$ihwN zHLqphESAw56ZS9*yR0tENAM44h{@6TchLm zb(a%h7vHOLiRu$Dz;`r-dW&)YPnRb$dR6{y0V1{^m23XrN`;Zj)R#bGW0LUi%+8Yl z1Y(95ici|CPM1QCtR&^pA)I`)aR47n84@4I6d+frOq^51m^anG!)|ahH==XG<}PD(p2*`UuO3U82{L1eenFrH?#V z8ATLpH6msP45x=AJD2XXTZ@W|BXhS*ENkG@`V`h#ZJ09xQ-INlDmh_pG^FzQVE|{d zZkStHr!V@|LmMpF5R7{zu%wY?d6-?K$JxYSVxA_Cz-gosP(sp1N0@F9VR9y70+r3G zZn~^XIe*h=x|oeI)gx_zCaO_ro?+qs?nc)K-O`Vt*%B>cp8_ruXIhkX} zFYq#?hM7kPUqp~ymE)MI!kJ_JZNL8;kMP44-|dw{Y*NaK#F=x7ztsLG_3HABzGF=H zZZxVBc$s@1u!M68bV-c|*h3?L<2p{r5}AyBbRxwAbSLz-b01Ze`^qH^HJQ#Y-rjnq zwU0kXDZ$?dzu^LOyKhViJl+!v|;)9b}Zxi z{F*g?2{J^C{emjg_vs*;bWoKCOqz2YRAc4>S9#NkA%Teq3RP^wiJF#9DC!Ic&2-^5 zlfly~?!A}Go|?r1tzEC#X4el&)6xIjJ!`t)b(+jdcHZ&yPifmg?_<4@n(FhS&6@Ql zTB%UK^|c~RB~z0#G0{*Q7v2@8W19EI0NRsPsWUlU)diZo z-=#4H1)C*YG%)Df=UV-c!T##IZCE;JAe%5;kC>wA#FXKe(JT;1IbRKSiJMd`?~l7N z;Cx;V#m&lSEdmS(h?{e%FBx{%65=|G%3AM#Y_;)b_2JCr)I(p;SFR%=X)S1ht6Mir{RAmz&`95+2AtoK5--RNStfy6MXVp_}Ct zG^Us8vNJ*RVMTw}Xgl1XL!1#cx393a1y1KgjR~J69rb&rRGefgYY?}3qURH7*D6|l zpH-;Uo}A_yh)4~a`eP*2ZaB+zHzG~iJe=mLJfI^Ss!n7KzY2s@*AL`0kDUVf&1oLq1H^h@t^h>F;6?N>!Qs&z)U}-YI>o4}@9@z00q6E6)yV z^PO#LRZUY>Z~cJl`P;ZdDd98ww4JTg_7I#jz^4!(HKw#x;`i7LdY!wHrH?rNo&U~t zqrf3!6j_CpCY{KOCkYfh6`{{0ucO!9f9xnj%$?w4!*#M?9iAZ~ zxQ6&c;|CuSIiS#wHK`WKIG&pO|0G@-9HG9TkYXm>x1XwPHlDk4B!%Zkl(W0TeNC3l3Pw3VT``cb?lo*?**PCz%{#Xnm6xfHUWHd!HR!Q|lfH zKVLt02nhT~7>@;;I-gwxcbG!*;GDeBEzf6`&EsENqP2KsX4@ z#(87jYKzvfN1z!0@P>u_5Aj7eb1s838Z4Kb;GiUfF{MPs5OC|p1I0<$pHNv2*`O&L zMG&ZnheQc|9*D{{Jk|@1MW8RG;XQ;qsJz#A%eewNMN}|)#-K2bg#&IGVE2hk@)it! z)tTaZL7+`bS*g^RkIcD+nzo1!L_1P@0-%S(X!6a1m`_2WDtfTKD?CH;Q=pn<$Sy5| z|De3)<>mT`SMEFm)gU5)u7~Iw0|o43{5BdgN`GX1g5Nl zO`!`PG$u$r+(bETuIs)*8W54w3!xJ*_xqbrFQjI2(8WmTI1^5_RiP;n4$g*b$1o_ z!_Xux(8M_cY~axd3!(<$Cj!LN-K#}m|4#Er&G+yDfTAOWaIN%F*3ufz_G3{Cz8}mF z)S@l)b3V#}TmPSJEH-q4v08hbn4&)>fRjF@BlXQ#OY&OgS!A)`7dQ3G{oNvS~w%cmm4G)`l~ z2BS(nbm^IlpfKm&;TjgG)c}Q;M59XZ)~2ptBY0es@=S-M{s~sr5~E?bo+ZIrK!#*v zk=Q_!?i-!;Bco^bOI3+8|16(RIiSK*L0mTtI-__29D%<^mI%oLUb^rJ1KsIG`=fhW zu^)UBJsZm;yl3C$z&Ey z0b!7N)TD-46kWX~j9voO0T{3_#sgNMtfZfOU_IaW?7#mT@Ga^P2XbN;Q>3&p{oW56 zsEa>%O1!G6&TL3TD5T9w@t?L_#9@Erd$-~*)%l+(o}|^TeOU+Gwe?i~G1K`t!$-w@ z#H8czwxw6u|NC4NJ&#cVOFVq98l>i!R}XSWe8^{Bl?D4B&WM%%w51P9lHZ_`l!Hyh zqii*L$PWj|kfk7bj2GI7(B7vQkH>@laquc06Qx0yWasFo7d(7qT^9LktAe9X#id5L z15_EtnQySC0Nwr{_A2X?w54t7Cv(24zMhoBE?sTVoK)_a$EAPtP-%SMUogJnMpYRz zBG(|^A*H>BjUSbqy+pZ6TO(F<(jpDc`=!lIZwIfUW`a;E)Wa#b!W`Jv}u zQB0`y@{}Q$c&9~J;Xl=wGvF?i+q~wtBW({elBbEjLK&_%Ym)Rh$%34#XCgO%7N^o6 z*O6bih(S81myZ2#_W*0!ov0Lm;nV(^0$oR&WzvO8!LatN$fz)fkl?}$h;d@*u{c zsi`BARxLxUxEi=t2&QU7tYlV40jW}CTqxT#i4udGMhGL!W%`-M!Kq+qlbZ-wZs_zf zqTl35mU~*Zim78%D%`tkD^R>cgfs0aU>9~hc&TmWn>};zD8Pru^~a*Bwwk!ioT-mv zG9!%W$Vy|*V9%-o7{VH*?y0Ny2^MoNq1;g+YfhO!z8M`QvZ<~5T9CRo6rv$4L;E>3c?5Y#yMALp{EIZC>+{J#ne*~dQ zd#V?PAk5d3%gW>ymTVcM`}CrJ8LDw+UDi2QljSk`e0^#Xhb;5bCaQf)HU(^{jxOV} z#7OzAM|#&!$EuJKLqVEfJ&PxABNb&ntIV}q`Kpk_&qMF%7Vgy5+^V@Q z+d})U8yE+TmG{~;GyCmrFskNNEeRd{;unpkLsIo*SaKP&hpPBQoy#S&8YIJzR>UB$ z$&?o7u&Vsw> zGO?sKm0ev&0;`a=IK5w?dh?;Ii}I`)Fih}uAgh)UKi!|INS3W39Df{?RgDe7N~@T2 z*wMk#T_XP7XW_Q{O`tRD*sG)9@oqMAD#0XUm5CUeH_3R-4y&met$NnhB*;=1zIvdF z0VR7%PpjUga}~pROLPJill=le?ofQY*{oUdMoTxavkQ)rOZSBBSO5ab8%7=untX=DFSWK7&KUe*#&%vnypvg z-KPd|<5AiJMIVyHYGLSVovm};mm3Ui0@{S;)r!XnRbewcHLZ}eG`W7)GsQY-c22_2 z8*K{GYpvCR61soyW!+y~P=o6{9d|Yx-mTUX4GqIYF()&*+MBZSYO>p9V6F&T2xFFk z=LyMm3Er0+>?I{Afvy+8FGiQpJAoyTmjk~o)D11Ex~^Mv!TuU-3!<)%7_7e?1CE&hxJR4E0oYH4tE^ z!o5|#Qq%*k%t^0XS+^3CL6Uvbrdl32KRt^jmaMOe4)Z@>k^ujmpkbir*$>?H+)S^O zf@L3e59EnxuuX1TJEzd5_&~4zkQ+JeWbM&Zz#$dL$+~4tp&76#;hmOOXygU5YbLZ+ zQjZLr3%;<0NiQ4E7Q>4)^4Z^I(oHfH%GI!k`tldA{enoL0i41OvKa9No;I+T0P1l> zN1@qCyPZ9}*0@vPY!0xt=@(Av$OEIq#P-Uh3v6v~Ec~#>ruyY@cNf`srA67&txt)l zw)e2qVUOf`KBkScKqPd_wn|rW9f`2>Z}y&baACouJl87W9&Y>JP=uuH$orSVTKU?!5;$13W+JcKV__n+RswT2#W)Q$L zrXPrxBZg#?pNg1m3K6n+f(`reV7_Z6YBfD=v}?l2NHDUpJZ*22jpvn({Ll=KN{JQP zU>35oy5ZY0T-7Ui#5mIlhv3nek+ib5IiUbE&9hQMxj7O@yE9K5+mo{9n0clqW))<| zL|$JWj{crFRhhE;kn9R+t>d&HMKegd4Q$%PQ9rXd{%XOP{y>G|+G2bLtm?DDIlLTIyfiV|Q7Q;iFWs|!!)3N5G9-{7vyFsA zbY!cLDz>wLFwTY|Ykq+Ete>;dJV6;W z4#u$rR6tC3F_-sWY4Ee&uXWJ20__`|T^>xuQ{6W>Ur)2Xx>fFasz{GPiVCF`@hxV?3=1WrJ>;U;7*zq{?oM} z@o8EtO5k%-rcp~XHD0we+WtEQVXZDj$(GWy`E;!vI-Ip=XzbB^!O4S1B@>$z z%AV&Je|oiS@O_JHf0@n)aWX+oXhI;n6!*1uKEU8U&q=~|S=G$GE#wHorQfxD8c3hV zm48eE!OYw6`R{0IGy%2G0P!bQm)NHZ1^r@)qlv{&=4ehSZ6Q8aPcuqWaetPosui{-WGd6Dtx>os z8xL&I-s)W+NZz(5qmDnAMkpwEp}hwouI|n2o}{)vx5*EU(-CzgWYu`&Qt6{c&qTyFIZY&dpIho*f-feneK5({%oc#}=j%t66tvcRYi}Ob8IHb0tz`(a} z`kTj^61x>#QPZ|TyGgdk;S;(_Dg%o{)U5-BTpNb0vIDm2${&5B1_!K|M35Y?fYOv= z!ezG|)fnfJhl9$N+QHF>wUz~LXCk*Ad`{ela(E5EF6La702$p(bvCy{*!1pKYOs(l zV3;bMDNaCZ+bE@w>A7kkh9qhM{ z;fWg>XsmY81V|ySpf`n;kq@`0zVQ5~Ef9lvG@T*9HS<;LKqa@R>44XR1x71Z90?cw zzuoa&_ba#d-LpB#>Q`MHL(3Lu1(LB6UOujTWiGeQMY%TaE zqu3|5CYEOhQp~u5g@5Hnpr&<$=nK6#e%a7l95>W&3(sH{K z^ssuM)HXV$gD>^1b=qt!If}bUyFaK8!6Hz>nsuyPT=wKlCIh=tW@;*yAWcvY%FQuu z`A)$^d`!D;KhU2hj@*BD4X*DiT-xlbxHr3@0X;u8pd!!D;1S|J4J};f2X(u|rh%0J z=Efm8)F{V@rGG`EJBYkakfetiVz$g#eYv=uHtThIajLwa0C3Jq=7(G2tq{eJ9hG03 z8XCN#Uq!^yc|Gt~qR=#CPJNHC=~ukA3(>V?kSF;P1vRIqrPZELxy8K;*9zxu$Eg&K z`>{#x8cO4f#JIghoQ(6uI~KQkjZlc^`VNu(@r}J%ZNUPcOlBrbD!L@@@o#ccdSZU)HA+f z-`Hr&L@^@7lj9L*GKIK>cwg(r24aj81I3*HXVcB;P5D zmML*F1C!>wbh_p+_#wYMx>8L8R3w%b{O z8?L{JKw&1q&0YC=oi{F&z?zyigPFgSdMQPoLI#*4!g;!v5xW_OlqtWvI(G_wCFbe< zmfaAVF&Tt;f&9P6_@uE5lyEW|WFO2ER%O1~FVw&1wW?L|NTKWUSJ0RSbiy{sL?plo zE$JNQ;$+~wk{i7htOVIRK@Y$-x3HZlY0gl6oA^pC4a($7v&_I=p49BW)PQ9{xcbuT zrM&1Hwcx;M7&>#rW*X;UMKU~qX+GVH8g#&&+#*3yYm8V~xbGiuFkf~rEegP;Z|}Y= zc=r$zFQukoPu5OyMoz%KH#L5N7SO97tX?j6b|J}UV;8{3e6iw*G@HXm4F1u0KKP-| ztM0(l)nw1J&f!(EZ8q4rVrSFl_maTlfNp7t*p^@s5e}pG^LIf#gJ_uIRxdec#0`;E1moMf1>Qy1i+mJmJA& zFmr`!&6ZfPI?7k3J~g|0$z#E3+V%6y7|SgR$rIiDcz}j+z@5y2WqA>_ zkQtUzyavIWvj61hbR)^y_OKDM?%4?RVh6q)Z|Wxk0f+G9E)SRadE->STIA} z5))0+F!Y)_|Do&`=Z(Sxd^J?v6>O4;zubvtUex%wwBf>0j{1r|3;m9`t&R-MausPL zKwQFFaR?JRs*7Up!ZAReK5I@4;|9WYoainL)mr|<#_GguNOst_j7!3XRzk2jv7jMc z0>JJjKw?(xudu?8kErpdCAI+M6=SHS8#tCN1X;qnrD$ERMrfFK{6Tm+R4?pw`nbZv z0@>g)lpXisG*G$jYkwIbhH1jZd!rmeV;6WtN&15hQS1d`Z}7tS4f_<>6b{zO-QW8? zCt`U*;TOXMIv9|Pxw?-Z*JVU#-&|Q67@@-rgm_0MKkK)NOU2ITMWPepGZYzA-7na!snP1Fhj&$eog4iN;QD+3;fO5o~(++rs z(X1NJJRB*kx%M+F}v}{KMN+tiJHVp)7*D12~m>8R5j&!m%}tOdX)d zDB68^rsQ~f7f{6`fvU(RmUm4J?k;y(^iQDWiBiQW8py7LTDe+;xV^nvA!VnGlkvrf z6AO>mLcGSaNSpz-{ptG&aaP67a!~pyQZfYG*?lfhU4r16C3wZo-d=|!xt-<^ccmMW zS~bRnR}#h0%3Jy!PxP=a7#1|p`PS42TRz3wxj)w2SJnTR;}Tvxdl$;=av{b52}Q@C zhanjSNV}SS-xsB~sKCY$*>Y!GT0hVKYqw`-egueqoJz(RI2`=AJ5B`CDx;%vz*)xc zqS3}n^FtF(pCE+G6_KxS@!_{Ht>wmEHUlr_;5;if0HAihq|mZqa=gY~f|al2uY#58 z3H&logEk8O&bG#qAy6r+&~@jo^udW>@#%p_`SZrI6>ibwS|g}!7OVJmP^U(`hBd~x z{P4sw!WQs+LV~QSEmrk;fk?*O#+16ETbX=VWV{b<<`}-rvw_DOg)DN=*Z6Gm$WNoe zCt`Wc1dqpq6CY7h^d4Zu1ldLua0&9`fpf>30zBaijFkW(a~^P}K7vZaqJ+nsSC!JA zmS_oB&*2C;rqDI1z4^zVT$|<4#tt6Zc<R=JcIc2ivIk#!HG0?jmN8lZtKYSt|hDriTq9% zE$HewEYAc}6N1V#l+S2cHl^)7k4?LOqgtkXr|VL`n@RjK+zF#Q`6YC~pKm-80JFSWZ|wZqufxOp~E78T;7UP&~@v+=t2Z zaB&)I7K!oC5QW}9jdaT6043|ihM^)EG^FXyH6MySpRihXgS|iInXNVuVQ&<{N z-$3$wt0w(~Qhz1vXK*EQ1%-AXTMGJ?#x26tkG!cQ=jad^|d?Xn>Qy@kxXD!5|J|Cth`IIO9~ zO<6_a15wQ6jJPjUu#{_(*UH7oC?ri#D7noNpz~B&i<|S#*Nu*?>$wx`+KbH|`s96X z`D<9fzE{QMY-1Yn2rJDiowKThb3CyPE3?zqB^u6?SK7@zAhAPlf9!&CF-bN*;Y-h^ zih0dU64Ej{3FkxGRbg}uhxR0&_8rY>d@LDUJtB3(cdGfx!fhw0vdqnDlSb7!UgyoJ z1hdi`9~#Xd0h`T;gMJ4LCFZSYgg0(~5pm;iXR=B+WBI)7 zJ7Lc|c@dD6Jriwydo##3`F|-}?d8&J>T_mm;a>}5mYUo}(v(|)ODTzJpwCP>|Z?ZPVk+CR}( z6uE##%%RW>6^r9Tu=&Y5h^yIbtb05|oBhxnx?2dU%1|5RbAi{@Jq!lOsO-=+uVwtn z7rb|!PUEuR4Y0;)O;yl5+pXZZlExV+%l!;x}#<8wHfi-(zK zN(`-mha#5*RSVGdA;Jhon~>v~mmq%xWQ#SVEXC0awMlBrQu`rkS070Q6(~`pX?oEd zajy|_pW8?=%?PsfMP796c@fbaOm1TqftHXlR_S1Oz*g2^6Ccqux2oEWoM|>WR2+n| zRu?s^A#%|+dWS_wQ+q2fgWjzm-c?Cm@sQDlTkKrKWg38m5W#0nZ1HONdydhUM0dK3 z8D&Yx$db(~j=HL^AQ92pdEg^tdm-USpW0Y(u#z_68)?!U9G2i9CvKY#*1i+apt`;i zqHxk)trZ+)zcM}^RPacy zeC;ulcX`tHVesnpox2j88wY8^sWnlzBumo>khJ$wCi`n$#?qISLRMD7^S;v^mb;=y zo+S3AK}a{qJcOJT=qo59%#*Z$MgC+pqT?*STY zqx@0OR8!r}voO=|28>ViB!)np*WVOUgcK=kkQm2&_=qK8N4CkWe$`+1dnF}%YW1;Y_IE6alkM$Jp06EetOFb>r;&!z8%`26p++WEGUc3Lz>mI^j_FwA%g1R z%Tv^xTJaB$utb7b>-|6vGPdoE6LQp@;+tuPC4cE17Z)N7Q2#;-v&Gc4w~`#Bn$A{+F{ooQeIcYlOy@04+4axS61s&A#ma?KUEnsYiw}WCI(vU8+3A@!D z>+jw0JyECG#8>!eyC;e|77VT{&t>Peao$%Etg3t)Ta7DAjbpW%@ zEgkuhC*svFz3|xxWK5(GZ28!la@VQ)=%Cd*YN$4gv?jg(T~KjGIr1x>fA!nKeH<&daj{hESz=y}&g0d035NxrXt22{Zj#3TlW z3dz@5yCi~Rpd>&N(H{%OPBWIFBoEhSO;mM%!W80{i4n94V>SMekwDkfUnx?@kQKCi z3!}H{!f7CI;1bv1iP@H$F|>Is=fVYw_GD)nDC5{0bY{G8{r*9N<{f3heL|2=R4&*X zzfFG3e=a3+&Ww7r++rERUTfGyMqoyT&5J4z!j{%*NQ$w54BFUUoY^(6znN>^mt)_? zLUhki36$8iUGtKQDDNe;Xk^g0ULCf~lZe>QD-!m-IfHDONmm&l6z;BxDI(a^M4ha% zi>SfQbbJPfGh^43T>{wEUS?JLF{llQQrE;3Z}G@Ez!cdRU1HLb47iv)S7lh13x_Cz zDZSY@0L}m@$l5C@B32CfCqCiChx^$=n2IsHEOBj!vxTV~|* z1Ftt&WKj3Di{sgQdU7cuNo$z1mWjyOrbsgqs&?6l*>-j>Iu^3J7m%`8W4g4>jP}`& zygjJ9<7_)I_y2cP|HMhu_>S4j(M%xQK!)0nHC-{L@^G|^WNF#rf3iuU?=mfV1@A6{ zg2sx;58m1B54uU#tUrKFLD1Yk-}Lafk?<@IrXfxU*RQZYe_}($LVJaWJQsBlK z-|pHam4M^Swvm5fwfJ6QdVm@&6S>+gI3msw9JFQSz?Lk;%PJ4PyXM+S1rc9fag``} zBbxw_u3dXY&T!goB436(K7S=q_5kBtgoyPZRCn5L=otx4(LBq1n8}UQRaUogm$%w} zY=C^QY+8ywV5#8**0Mtsx^>!+a<+b&VGXF@7pM9U$;tQyyOi4EC|iRJiOy<$V{2D7 z_4lgxK*8Ga9}9H2K|n!LS2iPZ&cF{T0*Tu3EgO44m<}dD`C$vo{8!W*-tOB5#fN(K zd^$6r9U@COb;AAhlyKV~I)!GX4iapkii+rvBEj#RrjgqX zfUY*2`3%otwJh7c4}olaIsYPt@p%S#7QY$16Oh~6S}!Zj5TG1U_VcIk$fT?NB_G@D z)}Jd`tU3+^oQ{(=@_3eh>pa`+-b4i1pS2X(U=C^vuvwY!pFrFaGdh7c4n;c$B!B^w zPX~5BOxD~iQnh89ZigKgx(D!23(j8IpQPM8Txy3`59?v}tRdP<#jRMq9wFRF7{@Uf z*3Zv$61cR0jDT_tMC6P*4x~JG@AW*cx=oq=MGBNGG}WoBOlzhY@neUfWb%O zmIgfBC(GdJudUq3;yTTLfMv*cxP38jIoM=?uXNn}l9ihW@Im-SP-jdV{E=}bX5ZZl zIO$#Zt${zvn5fwPMR{KC3E15ike6iESTQqjXanYTsQ~4E(g)p)r;Tx&ni7%bCH=cV zab>}|EPc^{7j#N0h5t)Xp+J@dlNlI*ll{Plnp6MWw43faQ z$^_n62#4a;OEr@R$B9?}(@Vvppqk!i8R;V&NA2T=^Q!rfZT!C8>lEIH>>&t)|DfBl z?A0_}rm}}SFALt8Y=%ABOxTmlN=&WzS6WmX){)+~pphXv84w`zf8ccp7fIqUE5qK+ z^5B3D-T&f|4MOY~E@>0TMFZaD2Bmdf*#>yykOV+2n*$mzp?}`y8Vg>c)~(gh4pbqQ z=-q(t)Ii?*ug0{za$u*aFJr6aES#7Ojeg%A8LN&<9e~35VTTlE-HLiXBUj%zp(?!} zX6Sv$3ui=KNh-Y?SPtJ)d`^rzP;nY|HtUH1U0ggh6J_6eB08d`jWp4BPNM>1CcMJCbu(n6VN2uSugs^;;RO8Nc z|COehv1)R%QY_!a1D`l~L8|U1=&Xv0BX}TXwOQZM%MuyPMGkT*k3(o_=+)hh;5gsj zT*ar|K`w^3#r8P}{7M@#rBvV%gpVXu_yTII{Gva$qI0@0&CB3oi|Fl*x;fDvfU^;2 zwqfn2q$}XGDNsICwMR@82^#YnY>y-E7);>Gc9NE1IoDI&F*p*qE{o{YXG?q~S4I~(DX z=>|Ty;hw73Jk#N@a1Q#D7Khe>RATcuvhcF*EWzR1F57!+biIFGFfzkOcn{Bdyo%yk zmf?PiyQh`~suOP9Q=2C1eSG47eVWnD7h)P`hSuT()>v~#4J+b?|LHCD?No!fp+@FX zn0K*ZHKpRGI!6GoES5MBLm!i1qvtQr_ZQ-=5w!v&Y>e!|^dNx!d50xHpF;_-X=$+SOqdIbu`>N!~^4lHP1h7 z9%W|kve@JCQkXnSMZ|vUg3Y3alT6@ATR!9= z6#{U;(&867W2R3roc$b{|C8h-;9G@UwCsRy7rv^XR6z_^R`=vPS`_XS`zidf!u51N zd^jXft*hj8fKt4*bpOc&o`G2-e>1ZnooeKUM+NyTqgO9tz`#)hM{}HlL5}2{5c0Yy zJQ0Mnk))wlM(0XF>L29N@rbq_40X9JrcYvHWaczXKVRhx_%Ac!(og5U=@?hNw{Pi* zWpw2k9(JumVyxnk4?Bv92iPS!Mv~=`x3ZzawzX9NaQdlvvr%ixbm8Tqj+`&1%!3XW zYLssiPUnlGgx=-PfMibQ-FNDWamuw$sI7Q{sl?Ef27dG+v5`qARm8YfSDNyC^ z*cJiAnMs1_TsrKSx`H!!;+f`8*iSDSY*@l0P3xi@h<6pIR|Y`bcfK|PQY3nPxJQpH_*yjd$v<<}x(J<#_!GTx4a8o3j1xzF|%^u>AqXy@8Y7-Z|e|C6@NPp677`Zp+HPYvS zReE!teEw3m(79H1*)$&#a|h>yV2Gp294zh`JP5e_N>>1!wz=n$(BL`%I@F8&|5>g7 zS&=#XhDPVhMSu@?`$MDrGX{wPeu(I*v6|=fNCP20bq}Np02)?8KT;aIR#fN?|CZU} zSL%k%%-shahYht^!tUr9;OT$#qFG4z-3nR!^5FOLpit;9^J3fbcd?sgU{BpKWYjLe zBZ%lw^*HHCX%=1!c;VVQ)O9S`KJw^YNc>-W@@+9`?Jv@*&?ct_K0fGd0!-g)H#(0} z?{91Hp`n6hEl=oeGo+0DfUJpiWkzy|BOQJOq+sZahs7x)*4B@i3!3w*t!_9)&Zy{; znc{ER#uX}c*piH3YrdC{nziWg77*9~Ac(6oFTP^{R-7g}<{{|#NtsNOvixk^YMQlH zs#^n8dUfds>2-+)^V+w#8CXmb3NVXFbR_931MsosSp^5HRSNM_kE+*wYH#T@hW5xD z!=pJFg>vrUEupCjkwEENO4_SCw=K9>ITb<3y7Jmiq$BBW4OiFYg4gwAca_>b@8@FC zE_CU5D9&qg#uHByAgQl?96aPPp)2WsxS|nYngdvfPxH(f*9FV}io)rcm>&B9hc0N1 zuf0gQAwr*J8|3NWLSxrK*H=yTM+O0l(D0Q*2+!&2XUF1Ec1%1igPUvy+4EZ!Mn~!Q zse$mO@i;~IVtt8$evV_Ua#-pHP+eRtUQ}v7!`&ex#YZ)N_Og5eO<~whnjSOn8as#qb(y@h!W~;^O#wA-Z@uBoH-OCAbxX6 zi3jSdBmO}Vpn!@wBXX%8+9(X`=iKVa6?3>XaM%QuXh$f}e)Pty7tre6KTxPIu$;NX zYc}+RZgUs?xC842Cn(goS}6MIK%dyYvPIQ%pit`^LEjc1=WD0x0om37)o>rgw07$+ zAJOoE*9;|!_WSqj>=3nr*_7)rF(j{skTb9x>M`aNSU8v^My%^!@g>tfKT#ArSZ$D_ zh251Yhu&GF;hf&Gp?28aD}^J^vLWlVSxW2rg8W}dLav7@w<&P{uk^` zLp-IszM*>2Rd-n?nB{ghGO6r%xJ}3%00zS>wV4T?jY)ejM}6#r6zSNgM|8JWz;n#q z4V<1W`C06m$81ue|H~dkC1uo-Lxr|s6-Ml?agCF0;%MZtj)Tfqqp5{kaz^a6m9vQ~ zwNVU=ag3mYA{>&%n_KL}a3z=PI>9`rv4C8?Sk8)>&U5V4ZeDi|Hv4+x(AVS30WwO- zUmEP-y6nN>%dZg_LjRloL>%}kr6=s?`5KTrJ>^o!@3!~sCqlgO6KL%tO+mH%R)G&_ z50YQ=g%yN)YbEVDY#?;Lm=FEuePg|NLVFD>O!Dnghuo5K2+bzr#k$chUIH~ADLw6z z*6MtZv!^XrGc|1D6z?dJ^`PyP!>`ypc9A+kDypfB-jjF;WT5S%VgegD}e3kgeGv7rQqvcz(jyHy*@k5dB}Gy=2-D0IYf$bZV;8wcI=@lKp8bqnIdttO z$o%dqN7JuuIK#}>rZF;E`wTnV&t>jXK^UTfUxTh+7{zJ1Gh%%{$b=!`JeBOuQ9iVjJygLQ0(lGR4BX7c5d&a0)f%G z_+;Zd+OB1@iGkRx8wT&GuM$s@z5g&Z;r^io9lBttex>iu;9+pvsh>g(YiC6;fqg*e z@IUX<=2?%9c!AJt||ozWeNoc17@4eM$le$p=ag zvgFJT4zr5L?~CwYP1V-oEe%*4eHM#|9klP~Nq6vX z;+f$Fzmeb9>4%ZSBb25z^=a^O#wPEsC90Yqz9bN`llFaN<)`qMaqGHUKL$^-v*=7_ z0q#&`+zs%vzu7zg*da2!Lz66t99SK?3zG21oV_XdCe|W+rC_KA$gno71Dx>D0~8l+ zr{AmtgNlne(yZ1}x6SbBFmYYam>>Fr;$`v3?~_e@l!);21E;#l=3klyWE3A*+M9Ut z87J`kmXUB}A-X|S1jXCtTekAc1b*=)kmBwQW|=P@HZ1+=?k~L*X{Yfknu`DC0R&{B zq-=31_sS?dK=Sc3st!I@O0?ILktam7v(r`pJ#g_?AGy>aY0^{euR8C^!kIPpu(k1K zGh#V^y^JOxi|c+ccR{3`*3+Z7~%+6nFj|~Kl>VNWQ zlaYW{ybdfp0PYTbJe^$M8{hJd?!DsvQN3uo05kRE1nToE+FtUy2(E^6TCB|G7)(9P zO?Iw8oqF=!$$-y0_L?ppI+?3b;me_5AZzmX5pOG)>JQfshJ8P7&{_uMwHNaaL0nDv z$KjCL41{mUo^!bcT4nPRVW^D}O4f}0t36oQ?^{kQ4H@$m4~#1q;xftWjwi-_L0_ANkUBHiu_g`2b=GCz=B3R5ViB1 z(Z|oBQ;;A7Y?mVzTJklY+4u9FT$+ep0UivI0N2H&sMW6;9{}^G(s@$etm<#Ri=dFo z&_{Cbdsg$aj5>;sa$J9_XnmiijSo!q4d3(J5P4cLP0%Vb7hGgTvxKMA-{ABK5Ggwz zC6$N_FOo?);G1c5Z)fxrA&Rb5>p#V+3@ufmQPtA5{3P@xN#O4@N07opVLkMEGE2B2 zaS!xB14~8%_|=c8_Z}Jkx0uuc{wDNlYLA|leq@mAJH_;kRY&6QqhW&FXuOA{cs5_s`?v5`w*ifmW@j<$+Y&Cq!~9J0bp+AnXoyg zEf?&X+jaJvi8=ws`cM0@$G5LBAOnoxwiou?UR2`1U_*$kL)w|^>Rz06-puyq2Qf@w ztD)x6wf+@(^9~9h!BqCkqg)N*Kr`sX2mnwg)fD$Bua(hnzQFDl ze%F#Q#{U{kh{yLaJ-9Un4dogMdBB$0yV;qy0O|Kq=Ti?}G}?X|rv59@=a-8fq?7ko zph%l6-Tt9kxNx+e6`bL!an1K(@G-a!C+g0yL?adc&@r(cMB?|3=i`|#TH3}M$uZ~^ zcWalE8<+Q*h4(AqphiTqlC1H%f0sB69#8kDH({>#Yh!Yels&!RVbH!1Obqw1jX)gq zH4x`9p`l-vOWS~yIfeJJf0{Nnh9Vp{uX&0~RM;o5V=4Etr+j8k6NG0#H$K!!{rM)0 z(SG;jikY*=F3K*h4`yO*3uTgji}d&bq#@*HgXfSNySv!O-HS`F-OKn4qtO*6K6!g1 z4ganmO`vAtq=EPiec%|ZSq&JMxwxuz;x#Xc(l7WUU&Q^Ox$Aa~Lw2O{o1>o-u_X9D zv}J}E;#QFHiVrC{_A{+_U}xni{O zNb5H9NK5!;`kHYM5Rk5SMe3Xak>9jh-b`^x{~<#XzXQ; zctuAAwAV(Xc-ne(wlnz;=TjBMThk~d1=r-G&7sRyz?k_Y!cpw58%)j!LN3+!r|W5V zZ~6H{fiJNj2tV5uhGIY-ZJ{MFME3b>h=Z1vc~0B&`i?=>G#q4(B2xKsSa%!~kMGAe zN}G^3U%qQ-NVxfUseg%s^I2CDdw*u@Hr#@b3;FqlR>1U`(BgIbXp0m{_LKOVUIO_BI^9R)G=yS_lrh$;Ky!84ppCYO>XP;yP$Hj&?bmpn`rvEG}nngcD#*3fuByz(E`I| zBU<{vr>YHg07$K*KM_VgDtara6?dRB+a-M^chWwZP`)kG0Zy-aOa z8G#6iHnPZ>z>xf6B0Lp*QS5R&hVdwLNO=6Y2}4ps{GANn_z8Rn z@FQsOC)xc7?o@7Rnmf(p?ph7}%?_=wxSjnrZztZB77k)n4v4Dm4qojTli>X}#2JVF zmgFoFix$$J99-nOf(HFM;R6yOJ;P1Cf)^tC8CvSAWvl%{ymu0{6v&kxBRZpeWXDQ} znhgC+vH1~KGCRS(_0mg6*TjWwjllh6s0v~Fs1~6brnEB<1x%{}iaPzWX0IuKs8b!S z2w7oU3XTgR+BW^r@X$P+-FIA&LP4`*5S5`x*7^PHqR)wHDaINdLLC1~4wfjIs5<@# ztH!>)a?61bA-0!2gu8i>6{-FVR?!U=vJM~|VbmkqtiQxFM|pZ9Sd)jmZ8`SbBD`v5lX`RZ_HU1q;C>BtZUnIzUb}8LCd3BCud0)Umc{ z9GU+6o?E)Cm5NKK0&5ZuyfUri8!P_&8WviX)d#&hW4}K-V`hFXNbmm&3RqbVoD#vJ z{(Vi$9hTs3x+eb)9DCpSf=!)FE#48206(`JlXL$zP1L($02|25m2+I|t0PtDGid*E zl+f;r5JbkEU{Ah3el8^hwH*I<C0<7lvN`{j4m5g_Q^tfn86ai16(WY>9)ACp;KWrn zz3l|KlATM$=P!{m1W5mv#gH_PxeVwT*)%#4;PwP3aPj}d673-|7g%BM97s;SWxItb z*QNi+lmPBF?;ppeRoEGMyN0 zZ`ka&h}RhCkl6O0g3!O?1xzX>>XE3<|bxUe-GOtGZqC52b+VnxL}U zfOqy|sUZ&cr39VJG3-4!Idjq!tqqrS5E|wsEpK1eXQKw@&rw<-_FVk282>^hMCE0c z1OnO$gB6^9MITiaJXy4*u8jIkDAG{K9*7oEEyVXE-bQAZq*Mcfkr51`tYzMwHpPFw zg&ATb9ZjrV!(5)9aLD8bE!v`{qF?po{YxXkkAElAbg0}x%I=1J|1gs~9p$}W!e&Jj zKZ|QCI0+ikB+Z%05>y4zw4P}Ok(VU z;DT11xzp6HV`MIQ^r03ZHL#RZw4i=!IzGJXnf9X_+8{plVrkt!9geXV`H4kBu@y=? z+`tXpLc;RT8dTYAajb8Jr&>sN9LV8TcA;fDx%f8uOu$A9xJ&2s8LGtgK&g!3ZI<^A zKmY`*QGWTj&V#-3oXC2Ss$E&*lG7 zno&?Guc#n&!L@r{-{?`|B{?tl=5#tD4item@y3$@ce+y#gL=$ui<5F{+QT0EI)sUY zisgjIQ@8R`@;0CA*#i(WvXBja0jaqfg9478*vmc*1+l!0ev5*Bsi{&Q1V>x|yf?Ll zLNi20sqlt0vQBz84pCcV4lb`5wk6lMQh`U;YuMKnU3olJYZqQ;+UFcf#ze?a5*n0< zQbLg^ktwMZCAY{dQbH7oP*GG=q@;voXeMJM4T=<{K{HY!eb4uYU-xqC{qD7%X{{r9 zX6jas<78?lxYlf_QaSZ?wZ+HaSzhbROe0)At~ZhT&(Js~Z(Xd-wvFmHjs#z+F}E3s z_21Ij_&i45cih3lla}|+=B$)uSy$`Lu~T~d)NURyG0MYr$U4W|68zBEw!t~h?)&UG>6M{7m*{`Ht> zCwk{+(V@5YjIYVsom<`0XGlzNsXQ}&s}g6$L7OV?g%Og)Lp$m?OD<14>!I{&dy`+^ zlOnQYG25S5LBzNdH=3lx7eZj$Y(&TPT^a)A`@g}a`Lip|Ar#^o^~?A zc$te;_Q~ZFM9-9E-DsOyJT`d4^=slY|CTp!mJfd3&w8}u4sWRIH0RK!fu-LA{Pt>} z<^HZ;l{3XT@ZdY~w<-%hG(y~WrdvHxUXnezPqxF_h)%|?x*JtKN7Qazmy!6 zJXqrBl2KN1{B^>Ce2*meA(8I>r^{b{b~ZV6bH>uhKk3$93y)R#rN>4@X8oz#yV_aj z(c!tPrNo;Gw$`V)<5_kiNy(6kG)rVFA!|2M|_u)cHNAItM%vqj#2Dms{A_b*0L<9-GImGS9BKP#-j;{&f9t`g5LLlx~3N{mXmi+J#&e z&b#vBYv7952{(3HM7pszJr-G{rJ}68q0yaJY^ImLMrhlt=Gr?iaPL#On#cNXJDb+d z3CghU7TtBYd4m2*e%zr>fwQq>@0#DP@!C2uF15o8+m)>UnQ5O9-u+zn=YIbZzjOPQ zY|b3pwXrE>%A5bXQ#Ibq9%|<7-YezFM_sjdk8PbRW4bH-$ofN9e+d#yHg!sEb2@OU zWQXT}J64UYjy(1Pzkq-C_fE}`*=yGHKZ|whw|i7j{qu*IRlqUH_}Ty6nRGU`;G$Af z>XmJEU;X2TGv+;zxi|ZYYW$Jq$=`p3D`g~ST@ee6IQQ+Gmq>`%7sJ~eyWi8 z;mAO$!Rn$L8oTWqwuVQoZH+tctkC=KU%=P`t$Bab);T@%eAPNn?$Z5@YMwXb1A_Z* zz1h@n`YnF>;mc{JBmIe`D#dz1o05d43DY&cy?gp{YNXAz^2wZ~&(yghN|hEnhW*N} z6d3%DzWOl7BgAu#Lg+W$^b>;i`KKf{C!CR($!YFittdSI^Sil@xl~0iJ2z(NbyVc-0sG@0%K9a0KFpQt zr3cqs8>9yZw#}-vADP}2;i#Rm;hXdB-MgRS2f#J@97KapxXsRZp{cfjykg<=hCi-=#$F=UxayJ*{rXzkwV3b~#?y zq_^pB!UxWes;W&+34Sh3Nxytes>f7+eR+ALe{t9AUA;vcj7}AnZtj`KtR9ltQ+qqu zZ+GSX5a;rsw{0uG;b$La#GO8SJZ$+}`Hcgr;+}VQ@pxw+f1WeLRDaT=f#fjP$AOy+ z<;%*Jr*ti7@+HMR+@Ce=Ld?|Ckc#JN>t4)L z7uVi(=F-q5B9L%nr#W^t7QkrjI~;u zWYhG^R3WA(VD;DrKa1aCqY-1qtuWJEx4xu0Gesjap|3VI?Wyn+`*!Vyk-7$@6aD{9 zSe?4!MM-lEQ=F*S@^#4msk$7mDq_*^$&xP)rG;FyjbrsK+#-^3Om*dopWhC<>|JCs z#eBWxpWo@c2S>##WLoPbM0aT_m$p9F{ahLFSn0D!&C(MOn-xEgJ>O&gBQmo3?vYwy zir9)9rAFo{653wz<8|n5R%^PqhsMtL+Bqp24{dbs8r<@I&F__;GUXjpl-6V*wUzlQ zRpKILEpx4?Z}Yf&miC8jGY-{9g?!XBR=Sq;>k~Jg zvE96WicZ&ODU}%l>BOl4U9Y#5xyhV+*K+tV=f27(9p|{fSPW*Zw#?y6EXX2l0lGm~cnK?jsSIny<8LKP{a7V9W6J%4sdBD|v!1 zXIuXJHM;hGQD1c0!on}DeL;PjSVsf&n=c-HF;wrf+VoP_^39D(z7`WoceMXg8ZOi` zz3UZt;9?2ODv@oMxiBFAD4a1Hr-KE zYwxKvCFN6c{>y+DM=g3g49~V(+4gRXophBMUV2pQ%-^)$x2HcPJ-xD4`DM4-kn{N` z4irWHk#mFSa ziJf2d#ZPtPX}ibEI59fqqTbW`Z+s8*%s#NG^!(r>r7hO->pa~(EtIc*iLsHN*A;QL z!>(t)*{1$}iR{eNW7Yd!WS?A|x_Y|q)s2g;5s zGapBezwbBjI`2Z*Dl^V~DHWgcCBfG34@7G|RQ|Ujjgfz}Db!&!@!W~;<0g&W8Fqg} z+sMRHtH;TEdRnK|^IW@`7aemgkNJtM3wB&0eE)aKq1{T)t{s}%(>J!W-=>l4tG+H_ zvG&b)5y|a_HOr>&_7uI{_4f2B{rVTVj&CL&^1h^fP0X_*e)LhA;oQ>udX2P z^B4N=9N*c-d9AS9RsHedTL#59W=ejo`B3JvVmP#CP%SplwA1}&%jCYY^Ui^bLOL9! z{p#%$uJ^WyxEN_}Ijn5mnLWX4UCP4}lN|>eq8nX*8$WWM@G|iVzfNb$dE?%9@*$Sv z#D)e-?qS0RgV^HTmEJbc0F0UuqeO#r}_hJqtmw@o>()|*|B=+ zj`Vd~N);PcT5u*GX8&;#Np08)(@(C%u|_S z7OM2s-?D*sasJi&a=oEBVu_A(<94oldrQJNwM|F#^=6r_ereS|2Eq?t@v;Y_Z^fXCpvlU=@QTUt;FeP9n|tJ!59Z9?7P_pZP8j@9HM~#W(klAL z{RV|h75o+Jao%eT)N~`KsJE;zpOBhTYbBA_e0S;?lL5({vVrU#k%qL1KDsqECq*V` z$wq#-9Itd-M16I9;XaAT?DMoWV8z&~NxM@&Q*U2%(>%qvWK%m zj{SIO*=-*lC~q^ll66QmplFZJv6{!>%8aDq#HbDn?kdJP+!%Hc#eJ>xH$CqmL@mHv$NYvnBM_U;|MY2Lql@ATWV zR!N^a`>#z)rCi)lM}L>G(%Th-O_ixj)JF8h9_(+hjQRE0$(|OaR6Cx{WZ7yofBNKg zez!&8g9U?eUHIW?S=ZE}9r=fAw!T{(d#$Aa?5B+wCvx z-wmuNex9zVI(v8l_wc)jNQ;Vt2aD<-T>f%bCS26LT=ltuoOvE^B(ErQbl2D`o;|w1 zFIO+E&YbeHt^R0lx4YO-$BKJm4pY!fYO;1}ac}>3HDo_mFi=MANqX}RsyzFcoGUHnj32VGW*U6X79Q5I77nOY zIE{W8+32gau(H`*-6kPS&)Z<$oo{^!{U3`%_XM6<`B_o5K(Tk*8?%*V!}+KBTo&r* zeC3JO`Mey`yZ^-*?1!@Yn%nca0S^Phbbq?G6xRt{!P>Jg1yt$53I3v+IFa*M9#>TZv_E?;96 zy^H3m>1YJcT6Lau?TsT+jkb5_%&lk-in=>gRd+{kMsnHvmbR8nFFRWbrVZVh>vl?K z{n_=2ss|&MoUfcEJ60m>w57rF3!Srrrnh&$jqrN>&|hky;pX+W^R7P)tTYWRQ5*@L zz!vxGOu1UG9T%F^^mEe2U57-ROnbIGy4yF&K@2YrSkhi)mx8M9GmSJ|YiEM_lV8!NwgY1xsPw{qsk z1l9ctzf!K(oReF({m_yYeU*@@8@_M1er7D;v01nEX`-A~$vY>R@lk8!YulxyO9x}$ zM=fHzy^QmnIlnwq$z-%~#P7hX(?^B1M`ae>8rI$K&C8YY%53$TKGLS?C}T9}CKdH= z-FNl{U2m`XDcSlL9_w^U`jh44Kc@=y^OX&&uYCR!ck%km%I(o5t;0(ech_<6XI=K5 z6O|oNW?uT{?YNhx&m~nmij6L)*O%XDADk*CC)Zz@D6i(Yb7}ZZagjv>^Ht2HO&foU zmp3W6U26GTb$y00vta6fGmWMewSJCdp7y4W=AmEf?IJ2&oK^O!c*?0*zIYmF=vfkL zy1Q%j9TmHy5s?P23B_TrVv@5){&hEgP%j?H$`RRbbN$Fb*7m<94;DG>JemIO_^Zq1 zr#3c9^(F-LetDDfykF0xuU^(;ou=ob4_PSIdl{ z5BxVRH5#h#RPLpnGqN79`FUN=za>$ZbZmmcVb|`RFVqt7VM1!bjHjR)?IS^ip~gY5(dZDUssY6Y`@mUc~RTjks+fTA+xa6=C*92u4>l8%ja1J zHZh8YmFqUYnAthg!QRwiMx^4EvKkYQB@3Plk4w+`XQLcy>0Z-N`?uqz3<_q_IgdV8Bbh~ zB?O4K?r;+`YJ6dz^>>V%nsvV7!a{FVotJ?zPrm2R*}i?r#5L-=JG~?>K8RQOJ$%T~ z%ggCb*rL)&$9nedQ2SkC@LKO)VVTY0bcJ*+oy#d{;&#R>f5}-@tXojSx%seLICH_N zjqisZ1nRdQHua8Lney$A?S?w7Nr~3$-y9o}URzprNN#mS*ci**%IbX!-aWqGZm>G! z<*Hh-aZ4f$i=#hjMZMp*{Zhe>KK)Hfk)sPuH@?m*eZBT$RYuy2ZsvNXl6F)}NpDwr z&X~-GZAHoyVSKVb*~`>jx%_eD%j0eRW>@QTH5WISlm&KpPX3YV7a!p`e%<8lIj2;( zhSxm<%+HnDANMc&eO71fh;@l}DHUC;%F~-LOC|95l^){pgtO zy`}NZia$~|t?pZuxR%>=C++I_d_UT6SX-rUm5tdS@`C@vYsF&|*(W9Y`Pu=#(+^BXb|M$I}QHk)8yQq>j z_s+{MudyLBnk3Kox;Au(ygi=mAoEp`6~*@KQ2%XI7$|Kyzct|3f<;NEMW?v;c`LtR z&2-kbOYz)0Q?XY5O-hhjpGnS5$-T3mtu)&?I%jFzC-uha%9G2sY&#lu*hjJZ`AW}Z zTWhO5XWMv#PoCWUWgRoK**A?n^kKm@hZ*A*UH({bbGnK0O?%gmd-z5TIjeUxpYQNU zA3NOmOm~*Ih|kc2Nq1K=UndP+$=M$BDy_%p%{V0qzrQDK^0V(=J36zm!Qb+OW5==XVbHn&&ld9~`UkU8#SnN!yx^E%|ScDJmX(CRuvWz`kzr_Aps{6L(JK5*NhhOf4`gB2I z$zYGve->Nr9lV-h^IX2fEKM~wvEM~z+aNvZf z!u6UNQh$Fsy%!`@eY&+Q&M^H?`EY;pU;9_89&_iNv$yGS+i+#Z+BkzxVIR(ke!sX* zhQ41ox;bO~=HoRP_RsH?YMAC6@v)FyuA0hz(JKj%OB9kTjvhCCc*6NS?-}8T^Ymm>?~Q%qtz~*jyozn$Z_%8%D&yx@FVolq;r??E2<67O&ma7}zWi(T zx;x9Z-t{Vv{ZJQZHphM6&jai)Ip_N;n&Rs3XmIqWpZ8^j&i8$^`1!sw<#zqcmiCmE z+KMz@4J+bK>?}`N-QMP#erWL{hvOG!toc){by@jA)r!bsSE2s6hY5X5Q{vtOT(;Fm z+4Z)e1-(_bgMRr`RX5%?I3n`qNbj)L@zA{3M`7NZjz&z3&G278Hkf-)I8c&bu~|2B zWoY<~bs3xGJ!Ll@Y%}%c;tUX)WsTj|zXp8f!ZNGKk{NE3oYrooz z@!9w$vDKlTEOr%VaI9BtJa6czu{I#`^p3){1)m(cuPj^kSY(0in;$y1Ykc;uD3DrV zZ|mc2lrA+PO=$D&Np8=Ktni&0@m(fwiYi>P@;rWxs;Kkch?&2#Yn8as{-X7B*l<$4p51^I3cUtJpO$)q09=%JS3Bt*uhrhu{9F5h|kE;l65T|KD2W z{p)jw3^?P%o91pPlYaH;`PLl|ix#ga-#B{h!)ks{fJAE9yva+=UEH3Rw;$^7-+gr_*Xu#Fd|9j~?lN?z?m7pkT>@{mX*> zG2-`467?f4yj7f{a-ybZiQnziI_vnMFTvp@K`LyexG>T*XzF%(5b&qj@}SO3Khqhy zKXf#`Ht#msaqHCvw?ZaqdHlrb^UGCubAt92<;R{b+kIM~(-rXY-t*O6XZLrGEp#>? zJa0Tjru>fN+ftW@f4$#xl;*3=i`^f5eq@oDk(o*9)Y7rfhunAV7B_d#Jg)9LT(zlH zJh)n`^ZdHBOM{;NuTG9}JGSO0yDZO2dwc3XnU~?_QPNHd$0{>-GfO8fH~%3aQ=1*X zbk*aoxD`&9kMC_inS1QJSM)V^zgx2;Eu)=Jy-1vrx3~MLUCPzV_(dz z?nziBE~~n*Mfus#7}L8V;_vTN%Q$}D5?6D5MAJI;%1<4!r}>tnOQ&uR>^fYKy4rll zNY|quIh7@qoBj;e{R_QP*SSk&(d9quq%>3$lEV^q1n#fBzjd41{R*!WJ;%PuX4)!G z2)Usi-S$l8#-O8q^f5=R9_dZG6O5P@W{Um`|D1eO`f}}>7ZV>8|6AbrU|U4j*L{9V ztRH)d<>|Xmy&9*bZ)#CL^=Mc1MfcbI>@}HRj)b4joKap;W1;J#?vr3)nUOr_=VvJm zn=0#uIk(?$Q|1J>`%xHkPyFv{LrKYhIa&XZ=NX z#iZyOO{+$;WG29O^r~;f%$O+~3xt{+hX!@|jz@h>PT%$ycSUJ(rN&7Q_nhOE#@+FC zx?=I%@o9M2!f_ABIP_h1lbXHLZ^K*Pe_3~`Hg7A|6MI#vnSN3EjIwi3;)xuaW((aL zvO`@8+q!b!Tumrg{yJ#RQElC=DM!xhlybMk4+o^Co9Fes*&R1HTUCQgMt}aQXiQ+9 zY7fQ9Ob+|@xND1I*h+PZ$Qk!gzs(^>wbaiidGUUeL!$r5y8St*-=}t9O4hgIiF>|x z99d+s`MQ!%&aj%U$>bTIuDuRVs3<(Nc=g5zQJX9Ot*XlWb)&z#z?X4MW$izg|6pJ8 zDX9x9b{yWI>G9Hb)%qiw@bB5*jyoECqPI2T1hIHI#6(q+VB=h3mZ}p|DoebSLgF=A z5qrV};_Z7tLep2oi?t>uw2y>SW|LrL8VS?tiBTCtf;)vIEEz{k*#qLUv+?>r#AK@y z%QA?Vr~im|%9oh4TZ!2{o0!M>Bq;D8#(g3Qe6A5sJebI*f`k#$#5gGu>-ZdEPh3j^ zZ!_Z3MPl~eBEeJ+@!p;#uJ|3|hO$Vovxb<21;i_wOw2TVgq$WO;4=x-HHpdTA%1uV zag=8ghsTg0_$LYe3n4+w4H86T6BFl5eCy*Rv@jyxr6LlTHWR0JJCVUQ;nzU(PtE@zS;If-~r zdWngMBEixE+`E&wmxqZLokW6YRT3UEB-*u?nA5+B>CYv$u`CI;7?CgrwK~fq61*c` zP5}wUrxG(Oj+k42NSJbx1oPGtBP&PDqlc(>5$ZpYco_!7Pw^&h)DzV495FW2hf8ZB3|n= z;@z7|ywe=?Zy5=%<`VbE8e&$>BSA_#3I0VBQ+RU^Unh5b; zK1GjRNw9o5@k&we%@#ygCK0!On8=PHUZN8*=fa7tdl`6IOZ@pKfCUTmE}R61uj3g~ ziFbVs3AUvW^DTgw`{?(I&qR}PZq3u^O9rtvg#l;FNT5%|+wVjCJAuUe`vYgN0j5ky zxF8LEK+P4(h<|k~u}+tgU^|C|Yc7z$E`%suhnS7s#7qEoTT@BsW`O6ILQLcq)JBbj zCcjA-97V#DGQ>N8XIFbkj9vo?`2zGtfw)~aiPr`^Hs1r@>`9PukeIM5B#@p&jM5|G zRjCs1&JGg1-9ntuUgFO@PE0+pp&v!e&V2M|195YtNf`K(c-lS0sSYDvq%!M1op7EM^oxVhVUr6xlIdOH4kU)t?yf8yzj_xM@nHR(|yG=rgpF}}QBnV`a zkPUpQsS@Wj8#vb{UXKPb;fIJLeVO>$X~Zj7NdgHU5*qv^f$b=9YHLX_4SWxlCOS7r zjN&cgE8?v6UL@F|PrQT*61eyi6MKx9_2Y^8l!5+UB7tfbv6qM9^GivXJPp_wAi-oS zoZ%0#GYf%J)S1Q*&wBjh#J zP6dYZi0S-JjF$+}0Tbd#c#@#*9eBSTJ^n@PPtwGDRzU2#H6*wT-n{i8Y93F#-MFr; z5^>&ZK|>Z1sh1I#Uqa05PbAEVClbpd_Iu#=OB*qlJaGm!5`^WTmz6||-Vpm=F)`Qe zNob`+O!Io;-9_JQ-jl$719W{Mu~q|@KV}hApiI0Yz;P`8WQdjA;uyXnt{57K;LgSp)X@m zD|{_tFY&sxNnioZi1jAHf?}fYF~mI;Nc?{rh-JYgf!PgWjsuS&BE)a|MvTi666)^< z226+*@*FsVU!-m(?tFb>Zxkh#zdULSTy?aNU=gqza|pOO1Fjw*Ry6um4@}*)BHqut z(3RQ5mPTDTsHIvR^ckE#o=&W~b3_YJ)8IrB_ybpl{lq;QM?$G*#AKGE?-SvF&>&VL zF$Z1{vkv{~XOU1(nOF-uh)KRi+`ngtX*dlHnNF-P>(JK;!1YAn<~H#z!0YyWAfZA8 z2_|a7$4ZEk9!}gD*2M0!C*Ix%B$(ww%nSz-mcmbWqQ@2*x!Wm~ACdOHm_+sG7dg#bK zRT6k@L;pVFI(*{9-6bYIi-fVLd2ucY&bSfB&5Q)T7U0bVXtN5@@GD|A4UkZSO@hVm ziJ2rrg3&<|d_fOA@r>Iik)ZD%ejPRV)Qi^y;yJgyC*HIx$OoOopO-;A;u9~kl9;q; zVoi0#*^w_=z7S{4ZDO9BA&x#g&j6T_s)jy+pWbiauN~m5BynFk0n@-u2(%&>SPNZD zLi$C5F)1WS9YX@02I#gSYI2=ejyH+0C(W+eQ4m- zdqiy=M5dC&yAnXc;|0X%+7b7U0}0F%z-KNoc>yH&=?Ki4l0YSwg!@hs6MY9?eT({3+lL z{A()C>Q20W(C3|2s2}t%L5g^{qlqFriC2O;{T09m&`*O563BrAOMv6a(ZoC0MuG@n zZ^|y>>VmgdyTNf}IX4>;d`~1^HD3Q?1MyGJCw>69TPQ&SN4$RWC^1R@5k1<9zTAN4 z;&qPjiWAU&vvQmn*B=KQOuUM$v;jHdJF!-Y6K5$hLKyzPS((^v;E9qQF@6H#y5qW$ zN1$23#9R7|r1m)nbmNc<^n(@4kku!{lrCdwAtNV%f7&R96C&3A4V%~$FTIawe)MZ-( zu_FzMIc!5*{Q>k3{wq~X!pPmwj`_q1E`dhL5^u|K;-!K+hWEjzv%r@-{2P5-*$)2# zR?ct2*V2h|3>bL47nuTjuy+b@gFF#zM1tw7pb@sMqlJE%fq2wYwLlXROH!zb-?9+dU8CwWXKS;di^N{P}h|?$z{3G)!1rdo?5Nj5^ zq+ut#@DXuEImAvfN6rU+{cx5E;OC5;BnUv?w8MZGJi8q{;rB1-O9);KpnOqS2v-T$h9HJ&Z+P^ zCoAH(A_ttXCV>omX%gz+e+j)rPm6A&54)hlsLL${66W4S@1b*zK{(55^x6uZu0TAz zTgR+FO03Zi;w9kgS_}#HAj8!iB3?Xty9#xYhaQ~?A>r~5#B@VdLyy^oWLw~7{0Iv zvx^TgMLER%3f+*_1%I!S;M{8B|6PwP@tWA{s=@z3qQ*3MXfm`?omh?V3@d2!qyl)= zLgX57*%WnsgIXMV3Ek!r>yERM`Fd^BJR`#;(N!C zz<3YwqNGT;{WG-m0cHZ!&r_3lmeWZXhWxqe5ayyx=$tul20Vw!6W=Z!`42hvh8kuO z5j^u_V*JOEaC#Q;Jr2OLQLjtjOQkmS7CrsvNgOrktzrjs7H7SNJm0p57!Ax$zNc`F z7-Sdd&u$^nM$9ztR7fb#Ag7=<+4s?-yQpC$I1Udz1+FZ)j|_hccq~L6kU2ZVpkc@Y zy$!^Bumky=Ma+$x#7sw)_ZkNuM#eI?AbtYs6NMH@$dGv*Nrp3g}SqCn@ zCP7d+bi^511N9sa?5nwxU~eLk`~qTnyNFvxBoK8XuA(aLxe_z7A7*;^Kmu^=vJBal zK^BFt1Vxcxg(3PFfb4>^-l@V&jr+~gAYL|d&Kl&$dFM#DY!~|ae_hy2JcAM9jt4&q z?g7WhM_uT@Jn~SI95GW;N$3|sTr=FmAN4t(OZ@+0Nf-?+arY!T%7$mYB=*QAWabsb zd{-u>8oa-UIyk?@3>FR#yhpqUaD1;nvE9Ml&HczW;0|jD_4x}AL;i47BhF3u?%v&~ z;bvevgg8#Ch^;S)*-!waA_62yOyMNCr;35!OcN8rsv^vdWV@oX+5YhHwAfM*{Z zN$9+WSOy+AyA3gSv4a@A2%lIDUDQV|#*QSJ2OXRbY~+w&HgvK?pIHB&r!;^UIWy=- zJn`2a0LB+!PRk&!H1x&38Rrs*-pbbz;2Hk!w}V@Wf7BbeN3Jk~ zw^%(Tf$I@s-BTp?mg~eI_fsCeCa+JzbORDj8joj2R?tJ{Q?e(1>_TFH!}TkBiN#(I zTon?n#IE9e0vc2+lK@l_?q7bVhONcqcq?K`qt_NAJ9P0?_-Vy|F-*skHC)+ z_#^?}cw0)8{}n#wN&?-7$Xj;UQ~DuWHlhzfc+C>x2Ek(zk&C!^rg}M|MASy-5AhmQ zkXv>W@82n8yQ9$g4ak$uxCT6@@+R^D`e%;%8gB#dp_%9JBP$_4Ea-rSVQ07q&-DzM zOL-J}51)*HE^RHxj5C=86TTsz6#~o11NC^`tf|B+_>5eO9mq9cCKvfxX%2Q#$ji6V ziU0H~@jc3Qb7?hY!ESHQJzmQX~|FyKeWa#b0b{0bx2frjS_btKM4&Ehp0M2w0*eb~*P9V7YbddP+C&8IV#4Tr}!)bHZFk2R>TT(1orSfHgTu{ zd_%5;czH?0BmxtS74TX-d*()D6gA{K_+6zs?js6}?Ssw@VyEnZ3By~Ixf4=Zv}D`?;GX~gbYgIy3jQ)wD_8;d>7U*cicz~y=& z$Dsd|JtQ`1J`)|7yI$QCv0&~GJVdRt`g^~eC&Z{I;*j#m-O z`Z}@SB47Ce6C3B@T&N4j4A)M>o=gvU-U!&2#n-Q(Prs25@Z8?e+~zmXL(J)zEtu5H zm_30p%_ibjTOeB^>%G`deDf3VIB>Ok9QJE_z+vRsL!-ofo(avy`7gmI!~frR8{%K| zAzlo;!~xuWJcXFE)tEa&FgF1QHSi*pDVWod{rS~bivlH{gx8QNR7D`^*)@NeTsrtB5JY{xXDxJy{Ge>IQ#8<~)IW z9>FueZ6UVUC<&GFkwJ`!`wN+8>2v7*EBI17F*m`Z$(h8fw*qGquy+Aw1K*NB5;;^I znlp6<=9Ss#S1j=hO0h@mz|IY?%eaGmJajKP5cPkJoC8b-RYG&2caNZdc7KU~uM<3l z9tME-YV)z9!T0>dexv|A*dI(BodjY9Vjr&X1>Vy|{2CczJh{*`8`Nn6u{Abhp8rF_ zJx8IDPoYnV#Cx^`_iBN@y@r>J#U3#T^E&uBuLha|tULfl)P5k}tp|URX$`uGBOeK$ zoJ-uvqQrH9-h1Pk>X;G6!XNyxgRY!M?EAR4k{RY6aBPkXc$h}){7UR5FfW(j*=C+3 zK`F3hmV!)`N}Omuv2v~9cbG$#>?W2xcBIpqiJK`PdID^QEQOw10t+(4yN?<0FEq#( zo)HCZ+=Q+KUct@zWJm@)kV1)hEBh+jfa>xYa&^T9k z)HuvWlE_ZL*c-fG;pjrNz8*f^hkbfA@f_i$Pd*TP0r0vQ965*DH7OBOQj0z#i)TY8 zio@aeJ(v%|h@A+$adoiUfNs2c27Utv2cZR%UZ7{9z^)~H6+4JL0WcOpf`z#6WBC6X z+^en{+=QQ6i9##dFk49yOA1+5{vEgge_j5Lgu8!1JNA-rvm^0&;KwC4(Zr3!nS=Kt zuJ>V{g66m_$9rvfrl*UD(}tZy=wa*&HxRD_^$o(@wsQqAfqVN%z?02@0el|sJu$&Q zh}Ek>tja)QH()3B7(DrgU9I~v+`k5M5O8JzPnAQ)aP~#^!Jlrx^o4iCeYzICjznH> z#rt+sp`DJz{tsu?D1tYf1a3!2aMl1?dKDa8K*DwA*n`;+6dVqVQmM2?3($zI2d zJ4~#HM&O@3G1|T)JoFLo3lQ;bumji$U5W_C`%b{_6nuPm4qus1Z1y1bDabEAJBeNB zhtEY2r?Z@xwYTAunDf%dK-+|64#j_VK(w~h6J(eY|%?g;0hjm4g9R z#0-yJ^Z3Y&F#Ltx?ZvGAA-(SShlq0?a>S>AIhD=6X zFeAF+_2Z%K&hxNCxr6H?dyS7Gt{e2^@I%aBD~Wm6hkGNJFXadyNB$NF$V8{_akefC)2@)<&&XrBk+p^^!5M= zXXA4_!|jSBYo4IeuKI zw#P8AI~VWokB1-Oj5{8~_aMcQ)HUP)&z!N<2B68I0&A_b~v2Hyj-VvP9 z_c^gel8`B&OXHyliUEwKOVg?CPZ z_p(X2Wq{aIREX_+kw^i$ZGsGN0~&4llLXbMcN6wp-8)cIe`M89#Lf}H_hC;FU4~gI zo~R7Eb6H42&UX0XCHR~PegtAAG!%b&qMn&vnD^|UBj3@BPUy%qVog^d{=(zLRysqh zVjs+P;>3Rp|G5fGz&H4ra?o68K=Bb^6g5iACZ={OG6S+mEO@Jz0NveB+;qIhDN;yW zhb-cGL2Eb7C$16p%ySHooq^-jkIxMvae0BOJ%Jo7!gwGKMMxraQu1-q*p%mh7{ zrLc36_d>3Lc0BCG>JVo-d~NDPoKuM1bSJVpdOYPkG5@lO z|7r*EJM@sj=VBiS++Ll7_Z|!3XV5FJDq!!ci4)y3 zM3!y@KA{Kob(kOKA^TbpEj1&)1bnh2mS_*=`UlXPdM?pH;NTnf&#eRSIY;a(kQa?~ zk$Y`1bDkhhA#_d$J(q>*$1N34H&I@l4bi=PN z;O7%*V}mkE@S3tXtR}z^DAr6KA(I{9!us>MvwLcX$b&!`TDphaWD0=hPqzJV17z z{gs%#z-nC!ku$DsQia`$0bZ96KS55@I*dIbcEL*6Y~YHOAa) zuHTwlUO-3+2nYxS2nbp+f!RIFK129FPJn}7g z22k-%Fi=l#ALEh)6-PClk`A^O4>3ydc*(lTieQD1T11hE(gv_`m;ofBTLTgnQ9&tA zO{ZTC%JYfJ$q^Kml%#M&(!~kkiXxH^Sf^COXc2F@+{93p>chd~hE#o6Wzs|WXa(x? zSkfjEeHN8UMDYQ~GLWXmG>Wwr;~5*hPUCz-ag{_Oue28B*ZhL*G!jfHNbV@C3TOn4 zRO}`AwXnQ|hBw-5_%;(_6+5p;-z6zc8hXY%r~A?quG~l~!0KGhX4cJlk7MA!{H5&3 ziVf0gLhG2+t|U}X4_R3-HGP#okX@&S7{uqfLLIov&P@yGpSeKEDB0ZBus9NciW#@* zcX!doT@AbNlf6@_ZfA2tk=>R$+%*p7rM3Clojv0F@nI`0xvQqMv>e`SZP7s}v}TL0 z($Tp9ae*Q0rmvP;=qbu@pRn=1`He<|>q01&dy;SVba@TA!@yIYT+c-nnuAQ?lB3dMuKB@xa+yY|gb1sDq zg0z&6eU67=ttnlEr>;fiN=RY15W1i9BZ+y3$EcCSkxtFjnh$1_47<)y>SD zepW?5+CP82d(5T%)*O)a5=w_0*E1B%HL3}_c-z|Z6-H2{U1)_m$r2?@oDF6tE4CI| z+Q^d;!t|-#Vjsci%9qJ$>dW53w$<|J^(d>*Uhmc`8N@uPTt0!r2`07_MnC74Bl-|q z*nf#qEbO7g}H*S znam?776Rk%=3l`(xX!;9w#-#1hDg=X&TPedWj4jPQtZkE4~6b>)t5|t{s!1xA5UZS z!3;)WeZXK)+ub+j_}Ign>tFaBdFAlkc8Y!9ewphEvS_?ZvGM&oF&@)@=)6M*&+)@@ zn}Af>wyE#yEwSe=RQtLDlLwb)Nt`mpGN5QobgeeL6p3K>j4b z5+lA^*Ve8^)qa*3QA?E{;45(4&ULkf9_r;{tp?FOp@{Ih8Kk5gl04MLv8so%PM!(0 z{AF7{&f;>6%9YZ0{!tlF8XV?RT$7969+TmvpbcVkkcCv?oMdbp}3q6GghYF)*Np0HT~&!f*BX`mu(D#}&I1bRFq;zN9S0xvh#iG26d zSzzzjRn{~pB;USgxi@9ZQv|0))DXw2)wz^Smv!2q9t$OYKNoGCuM-I`MPNNb__OfVycLHR(4|BT!AW*s8i0K zDMD$TKP*TwHytR5FA2Z3AiwLXfV}{Xru*C9k;BQqTyJO2kEzbtCR8xBC8v0G90#R5 zD#qcvlwh-v#v197l$rI0+fN_>rj3pA1CC8gYRl157u}s!UIT8|; z9Ej$39gp&LaBwpgAj<-nCCY5Cda{6WV<@D!c9C`j>=nG!)(>5oYi+I8?h=gnM2!$p zH&jT*%IK9Q@Bn>^Wh{${+&3J+;^1upN1pfNA*$trvx$vYA!rr|8~Pf&@ z4LRCB>s&05FSZt`9V@RzWW8Tl^6NJ&6`LIX;3x)ubMCx>^r-DPg6fvtlR`tA&$NS5 zsF7h2F!a-^X6Kh)K8`rr%)B*1KomaNQLmoJ4-bS<^{L-PV zRbG>N-5C_Qc7~4Fn&Pn+6~}L79?Cge_(og1$6K771<81>4)d#`cm}i4ho9r!uN7-7 zsJJ23iO9Y4=N`1e1FH1^g@JTC{A~uzQ)5v_dzwd)G7qnqt&*ja`UP^_py99gkVd@I z`Ke_-c&Rj~V_I6e&U`JS$F07dSI1#R&ZS+$%#j->{iPG5TL=^k2fDS5UuVYE*YVx@6Qf6ycrn!x+4y07<=S3TL!DeB<{e> zEDQ|Hj0)mY)3l?L60{PubhFe{rlm9*#Z1%bAwB2(hXaTsZW@=eEE#rC3ebuuxk+hR zI*LCR7)87yvYn&1ikXrW=3DvUy+vwnni_zRYF2`#d{Rc1dO`+>B7S*7N>kXQct^x- zH!G{vv&_4Dof3hFi>ZTvtRboR>7>)!^OXwiIwueRpyKspW~I{<{nc-38Ct4wdJxKw zruhTU5}(x^-IC!~Uo(0pd|8!PK?QXMAXPI*`sf1B`z4px)B>C~Ds>IZL|Ezg#Uk{> z(h*mo#ZlhEI4(V#z@#;w#kg;FhxJPU%seDjRY&|O1GZg#f z9#jTqhCpC2Md@&3nNs&9)~wFWh{u%aB+hQ@^J&jqK)TSUOV_}GQtny6gg`sr!(hW; z@j_@~NDEqHu4CyRr@`3xhFHb?eaW45YS+L!ya<&ULjT!9#Skl0kwTt7B0I!99t4ehMV_+`G7de~R%kqK5~H#gFNfN-CiWLpeWldr}}-kenS}uO3TdzAHi4x)CIv zQ%u&>B8*6%ptz9g#{tnDsV@PxTcaKCtE%m=oy@I;!`GpRw<82Nsvd`Vs@C0?I`g9A z8_Owma-4Y2wW2oIu|=SFO3TB)2=&Agsk&Nc{tYKK(Bn{7fCW<$m$xH3;F>an^v~)M zs#`-!vP1dSsu$=Sm7EzJv()C*B4PG|bk@GQrfJ7=s7UGB5Gg6QT-&t4^kkk(2c@84 zZJ#Q6D|u(RCaO1V-3EE`SC43>wH!3-)%<1mS=>r}lUQDMF~v|W#_Jq@|2P|ERmD!P z-^QKMV0&DE8RbMuIeQ?d1+5cZ3VPj^*$HQ3(j47VVOw$+=HWF%%3=_Si->RE-;bBU zXHpzOV2BS<(eMMh4bx1jzZ9sO9AzmIOS~yySI2h|6D>~bIZ?VN3ft!WF1X~$GL_sn zx)<=E?kDInB{2uMh+dn{nf+T*$X9oo`=Qj_6%PYD5tav9Dr9#74$Of+&HRfrvDB&pA@%7s3Be%Z9U zY>;}``{SjR18F0or6jU7-I0tHFkZ4`Uys;%rVOg9&47tKsgg!5+z}nO-0J?N6Eq0Z zeBP8vhlRgS#`yxXoRFL7ICs1jgGz#uI9`EmYg^Ki?5gd^8N>cK<81LGR>JH0y|f2m zq1GHV&n{OtjagEv=){NB&zt$n*E&y!4hfqv<9NRtsoRr$4R?+`NjWZZr||F}dvJnY zYXffKfFFZC`W#pEjwn=~i=f@J{%5DOm!*-MUZx(K1{j>cA7recp_4TBKc`pa7gent zB$cg(ibC-J+u1+8IK#XsKgZfRx!4_Npym9%t^2qk5KXn-^eY&Ynm#c+*ftgKQ z%sHPaU4OWA1|mH+f&KPCa8rK`BNd&$sn+rg9t0AqDzPdwr0a7Up&I7TDt4ZBHFzUy z*2I0x?!CB|ab0Sf2tL!OP6{&3KDx`ME73UbD8!+{ifa-yH1? z6F}Gp*^)Dg!)hIi1slJB6H(_ES_eqFXTA4+1b74@7o3XjcSQsJssalrg#JIZkw*4E zN*qsJMMN?Lmr9P&(2Yt|f|c#9+q>liN_XFv=&7FZ zA=F4d!n`q~ms625Ll+9(gq>}dWqC+WII4AE`l)j5YWkWoDT@dw_-dF+Qi(5yP-oVZ zz&;Urbw9{`cA9b(&EQ_!0O1hxqFn*|Be-W}_OpjL2quMpB#aOwBl6XKX6Qe8&lX#l z=D*Q~qTVk6O>w}>_VKu9*(8ok6^H``Uvr0CoBKK@n5wP|8!@cvx6aOhW$MF0?3d)$ zW~`^2$-fnrYsK7zMjrCtp_58xjn1GcDYMWZ0mMB-)IjoA1@p@SQ4M|*S+5eF$VdBZ z)2$>dL!L!_7`AQKiB1P))Yy2058IAD4?H`*qJCAqcV2e;CVz3rcM2yGjot0-;!?(w zgo2j}y+(f__`K6o=)mMJUQ|8%CZq0FhfNj8f|^`x-O#$|0DALNLOUG%KhO`+LTl}ixzN;8ioUQsw8 z$~|hB5J`s=m0aD8!8dT}YTBV>e_u}qO@}Kvd}w<}=ad%riVO5*MF&!!C=|>0mg}ag zBEH+ZYdXEef3AzF7%{DJ#ha%eUoCyz_+3q)205~30XndVs5 zj2H&;N_v;^X~kbSd}yKB3`d=t)?pjbWAXoPOnWP7$Pp#q zoqddnjxKTJsu;C4VOf*Rpc&fd1pZFKrO-Efwhck#6fOlS*?^V;j^Uo~c#je}g*XzB zLvBHHFj|^nmS()*rl9Zd7J+L=u=Zf75JVCHo5fxdRW-eh)vF{9pj?N%BNofO(RySQick_|0J?>XBC2!(oF>V|fycB$eZ#`_d9YVZ-G~2)FNi>gr^TT|F)(mIB zP5uv$P?T}X=SSz9$R1Z8j7Se5=wFYo%pb3HpAt2l z&6;*yHnSuU@L`EShkVQ6(hM+kJTb6|I%x(JQW#bW%5Wqqmf+b)P?f)BU?#Kz`K8qD zl}7nP@#Fc00$55UBP5Y1I-e6G!fHmPhUtPjkqP4cVwAFjM$UZnD22my!N`&E{G2(9 zaMGisa=p!TRF2@&(BIm=MWka*Y$z&wR~ByFdvL#%|GYuH5JLrDpj!$$$ezZ|ouXV&WPb|BK<*H7dt z=S9`-PZJ49kGZT8G2X4udo|f#mHCT&?)VNHBjxzDh-Aj@}LJI20>G9N^q& zvy^Rg6V33iEU)%!iMjk_?&&iZs+=nCrjw2V#o4&fo z_aYH*i_S)nMvKFmz6fWqa{UD^J|mD@Je7i)#cIOW1gIAhnUwl0>iVwoYc z>jR8YnM3(1mhEf^f693%a-MPRtZ$&tRyBzrjrR^!DD9zjR zfueJ`)*Nr(si5#QbOLKcVzUIlntneyBuDVq1NIDw>E>_Yaj&wmB-RST{i$5P||WM55?`Gep}5hwkpq)oWvh z3X$sOgreQhc&@W|~0Q+s^0i}^si1l0MAAbo88hhX*WeyQGLjO;7O>%>(h09Jv z1DN3nghz`dZH`@jxZ4 z=X3&KCABl2XYei|wh^a~oVWT(X?-e4!;IG10V3!LQgz(@ZaKcnr^AR;b_#i6z=d4w z7V$b>I+uchLG%mmIyIxwNiFblE^F(vLj$mb8*p7Nf4>xs-b8&#u4q@3a)OPk%4fBDna*|Qmu?!7QyMNb(6 zk2|~%+&>vCL~^yM$tRywh-#)%oJ}vS&1;G7vM&zy;6wnt9Q%K}8G7SgJj5gN@${8x z?~dy#mv4R3;_gidvyH2Ag@;^Wl>R+>kG`&^m7#Kond+NLbKmkp@MgT)U?s<70>rqk zGLyk?HtNE0M>EN@*WIpd#^34@@KARqg7B6D>0~?x-v<3z);uQ0pyepQbeaZI zAcP%7hXuRnLJ6v(eK;dGJA>V-?nLIsip7&!9-YMyE$XLuATsh{Wv^6<2@o$ebF8}D zOh~mdSGYr-9`WbZGG}K-DcGmm_M32m?XeJUm1e^3>YXVe2tHPIk=^S564-7q?Ou11 zHIl6qa0dC-{^iQUxzR{vGz%H(n{sho(2YZfz`2UTHlN5<7H`(%vedsDGw3 zNtjn39X8%}_s$QpFGsSKByv!T1IWRBcwg)tbNROA=_Y+MLBNK><>UP$a8^;scYQa4 z(D?#6V|ko^z8+Zc%g6v7W31J?1kua?vE2U)C;lInTOPCa^>=2?zw7P|i%PM_)~61W3#bQ7zPB7W#E~EuaT$7*pCxWzNgjP)&d|)c-`D z1N$TYc(8HyWCy{5QKPPAd;vRarDNg45LfE)klY$}dn}+_XE@SIqh^7kl@!Rfz~X z)Lp}8rk$ea{foZjz_;u`vM2U>KQ`6t=NApIgjootjkb{4guDRm7nJzSFPp`;a0|$% z&3GBDP=Q*zV880I+_Q?}Ct&&+euf;ck-&LX>L31%3&~z}s}bUbN^pgUmxEA}9L>pl(?)d!Pg>I*t&X{f$xvI*x{9k->0mgC&7VpBn^(9V3lL2Njf%@CTGQ z)CQ`6aYOB61>28hG{|rU{VMFpW6jb`iIK<}u3AiT!T5`wiTv9GLrUPuqgmQNa*~@K zW}k3u0E_#&${RnE($r}Bo&Us$ny~&eU9bh19I^cXKc7aSEx!2j7%r>NoCODsaq!=L z;&ju8A$FWWzjQZ9qAu;7LKmj!LrWN}l4vgaJ@`6?y>6p(*q3}4UCXZQLns`yG7M^i zon9-sbX3_OqZeVBMS@yjM*Qpo&83VStr2zeopv08@_AtSYIonO`bjZAF6-hbwK8d9 z3QSkjU#jaX8w3`uPrpPXA*14nOK(ev(xDc|Za$at!?N?p+1ND#PPPTgyXF)>I8PGG z^3944u-L8M>wH%2K7fBkWOry=>1TW5MtE0{Pu4p?znM8%Wl270ey6;_HV8D-qd;S? z5;yj^9-!^O>*kEme;wsxtL$EuZaFZQN;qyj0X{@(A&3p>qLii-7zLX0eVYAym9SFp zGN4&wALM74k;O&Us2eV(!N|!$%fnk&SSW6k|8GIq2i;T#urjtq!--37y$=L5k`0S- z0$L{VL*LkeM*f$+2|Yzc`6{ zL0AbXc@j&3b8)!HL4_(wHKubJFBF@o=>N6N+Q8Y zz``a_vuaJkRM+pB* zlO?5RWhSHn60d&o#r9HV+%g!mUf8F5fsOab$;<7 z{yK~c59@Z-faO?iqoEO|61)1JOijuI7OaVtFXT?X!M`swvsTiasfi%cI zny>q6r=Gi7L~2UToH=^ZytZt>H3X$Px?Alj*>|keom4Hfm6?yv0fV4wI&eM~ZKS`r zWY>$`Mz3LF<6%0lZ&+_SMv_qV=nYh0S-I4;tgo91D=$x{axzWJEaQ$8aX%n;JgBr@y&j+PV4RC zRQ0dY?#BEN?aji$1M7b(V`Wen6j1N@MLk<-H_9t(zWGxoyU`Q@TC$(PjRH1sT}Ry_ ztSUSKPS26+X5{63TB{{yp9yQ)-k@QP9!Sjc`7^l?RHZmm4u5v?8-ubT)M1Z zzJ4wW$J7Qvpoe5tDw`qSpLv{)O!JCTOiq2!v<*EYHBv9?BYFb~Z)~g4^=#Wo*+Oa% ztaV(1?DXdQ_$@i;AIM+BM925o{e=9z4<7XR{=gp#V#?1n5q*s5%bjaO5Eu=GzdyM8A=Hn@rmVH@=1Cr=^(DR_}Txe z${Oz1OorU*{BDkUG2NWLP_)PM{)BJ5_T0!UR(CIR6?JEAxwDo^Z8yM|lhPA%*hz^O z@yy(sw9i*`7{+aUqN$a|d@ZHHTduAhp}{@b){}ASyUo+YXm^ z9`j}<@Jx8zVN{?5{k6_xc^$t3^t)$=^D4c_PsH@uiktJ_Q}9OgigMqCH`AfuSJV3l z{bn48n-)eCo&9NAW*Q`&KOt?4quI(oW`JO?hOcA)-tpUU&%hjP{W_B8Ki4YlcWTRi zI;X!Pk(_kV6U3863vF}ut05E-wA?+1DrgPbnxlDzf3_QJCQ3Y&)FUDdaFsD>;N0qy z$(so7*wQrB9Xz(zWg;iNwCYwMW7yNKNqF0ez_Q+GiTqZ-mM)wC(u{iEHqa-5Yi>IZ zLb_e+8_Mr`K5{WAhElK3H}1NvIT)=~6|pb3|+tAfvH0a8^I;xcp+Au7`|{dvP4S7;s@-$gx=#r4|0b9X+g z8z!?ajr=xib_I@l4OSVZd5rf5W?qu6Jj^e7;brgzZ?D{d`LSb%K>c!X9)Cx+0JCSC4-%JJw6aEfpya)6><)tKJft zx@VOj#Fepur2!cxZU^mfcFKT(>DXs*W7O}8Z|{pgut2bH?iy#{5x76)K@+IxyBB#2 zsdgVUQ3Q+&%{3sFKo>FcXTUlKcXl5zJFJS5oHz=GpIG;E(*@4roHy%R&{?lXZ9+{@ zjxjtyJ^@@Ctu0vpqniq$T=Ij$@mr>}c;$KXxWOJ=9`r1hVrLh`!ab{l++I#U2rnvp zEgJ*y^^oZu^?>_!NSF+p#y5YwYCV`g00Ojw07ubI?Vc5)AR{&K14@q7;8Z?a=?rEBQ1t%NyLu3qNp^Et-5|>X3q<6Xw!AAf~%S{ZaNX|t)?=VcOc8X z6RVG&brHA%O?9!+*&?Ic$Rmct9F_Yxe0V=%uw1g(eIhp*VoHK#<7T!kyEa-pip%qx3lF9$0-%O{9mX_+Jy+= zCkP7E9>Ez@fRcOG`cQkIi{R`aibwB~nVUvU1K*sHg51ddQt1b{n>byrU>dt>&VYBdLS%H9aH)+@%R^&HmR`ZAV{&d%!-Z+}^kPJaf zAD0)6#uAKoU?>scIK0m?^Uyf3Ae|_rF>?<{_6RjY*VucZA%DXfa0TN|tks?}mB^rG zP%2D|`awItX^**uVC5x>gppQ)!%ONe7*>Yll7MR}*4=^qjQ8E{)n*4vzoa{kCgKXos7` zqD7g#l*JY$@e<>44dSP#YATIPEE;oXxsJrdXgG)#B`XT7GrOLa(Du6P;rsYO&&fuY z5H1ozJbPrCUcrF-+PpP`Q*Npht3tq)^BP1p-|HrU4%LFZayKbI_p6v!@|uUxu;ZC@ zlsJCBQ0ycaGrD$22};)0g#~we3wu+@x^L{5pkSbni4`3m85jdhVro|`&p~2La7|b= zifOs36n(ztfRLX)mqdldCOoD>B#G2xSviHl9;K0sA^s(gLBkYR#C5gr??dBy1kZq| ze`TT8dS~^?>Sekypk)}Y156NbcL0}~Os(U(t~PM$I52aO5>CS3CM<3Pbf7YI51BG& zm!g}xbmV0)gQhS`|KP|{RVHT7GP%UEF^$%=0XGf@Q1*2Mg;~Q%=#}qZYXjIRa_fU! z)4%EUj-$sECAM?xEwbFt1##8(ca8{fT`dZGbn!`1dlOp#dD}Fx2|FBXCvr6@G5;RB z2_Cj=+!oyjYIdYCc~j5Q{$@BCU4^qhj=tt|{Hs4{P^tUq;CJBm#3<0cWi23i z3$xocR0~jllb#TaYAn$d@ z#hn9eKJK5Xd)!lHS$Q$0rK#e;&B-F#V|rABa8!%&#NJh-UNBy)wUUkZYAT)zeffS5 zH>iE1Q(RGbw{C*MseKfFLQ`gTyKdG>)FK{!c@JRd*sMO4^A+21+A>-VJS$_UdgT=Z z^y>cN@%VG18zU_01UBT;-PM3Xx#DUZ!djjnyiz+zOKu3O1!O9GAveRs*6i%^r_DnCQ5RwGdTk;(KvgtSPMkm(-a_xrmiI*~l-sM6VnZ5EG z!!!%AySpRSifzs+uN!R-MtwbrY>JERO7(&$%hhuQr2OB9maLfgu0#I9t1l_;ls_%t z63Hvsq~^Rke(x00dq!@;yOnjE z?A*WitEJeoow_I-&trRs@HY(}&qvBRia{F&;afm)i$w>AgN^TtvB67|E(S8ehMGMe z8)OwajYtdzqIpGN-FEOz;oC)9L?h=aK#;t|XvNsByFn{9%nBmw&wa00B1>kC0#oum z()ANKFH(7Zc4Ripy(rrMSMM?AZMYdp1?x~v;FtG4TNu^9}$ z@PYw`zX7OV;UodCxb5qXBmW(Ak*KKDPr>s!0RZE7s(2LM0e$B-Kmr&I#|99WeU25zizRO$z)-u0AmL^+W4J zgGEBa&qU`Qaz|K0fgKr!`=w8kM#{%rX^Q!aZ!4d^xE_e_ix*I9jfhu8%)Xvd0+DC; zafJ4d-=lszquer65CE8C_^4-k1!P9t3rFE!%vfL&%p^-kI#q`J))MOb!z5ft=)ky1SHJzyv7t!GrtSTK)K{zzh zQo>$^^Y47j#EY2Jr`Fl+!0TkK1DN>S!2u&7LXWt^P8h@VY32mMb+ z1M*MYH4gmodM(EYE`SPlzuG5XVdGSHe|~ zr2zh5BwWU4P%X) zA;FZQZVRPo;#a=Sk90&4gzm2tQ}t$8?g>z{p9(dx$mZ(=Bma+ml~6{7l}AmffdQ_E zLPqzSK_Rkf^fv;&+j4=k+1|{b#UmQr=<+iUm$y`<9j+fGkhe?M17`Z9#7Pq4z=xi1 z2rlX1f3w2bDrKr1FT%Jl!l)~$EkB-JT6CsUFo1ipvvo-K!T4z{Kjrs;JBlrBDK;4C z=N?4k5;kG$7SR-KGkA`4rLIFhH9Y2v>EWQA9O2CApgpjyQW#yUU^)AHTO?km4RKk4 zuLW;7EglJ}dAvcpMb66-60C81ixg~LQ1-f((Dq0U*$;q)7ZRTwCbBx z4%_Qu+saoQ|8b}7boOSyl62H;{h2Zn=o0D#%m=|~cr#4M zXNg_8CEzZywX`EVtS1O_a)+1LF3}``bht~}OzJR(@5VnYB1VbVcPQNa4YL?28t8GAU&WLaJ>cb+Iz;;iezdMz}qx+(1qu2Z< z+Cy<8wRH;mIBf>!cCQzg7dwrNMvKK2pP2ICPNL^BtEy-8oSaZ;9#wauS<5XByH{2^ zpAYC`6Yns7nMGXCTRSt1|NkLLv=@1x^$MVK?7oC2ggL+0BO&#WZiP}&_%9Nc0HcV0 zpl0aVby&5Kztu4{jlr8?wr05t0R*SaUGd*ZB47fb-R~seB;a^WG}PT!o7RJzrhoOhp z^%9E)&~qQ+K>mLo{@*n5Ld7J&V8^4~D&jaDlZSeI{F07?v)R})=K&IEz7qcQ|20Q2jK{h`{6D(kkWj7UD)HCW}6$?{pv`G*EY${~u9BruBf z+-T*ax114W%evNfbMlYd>2+?2jbPBwz?Sc|x9-(uQ_=ABCJ17mG!10aK;>(4b86=S ziQAT^pxI_io6(Kg@Gyte1j5R9hD1@C1wqsOc|6t^m%Vh{CEqdUG!=!^$g{ z#JeLnU~NzfNj4X`tpXJTr%|Py3+9Z)VCuE1QP66|B8lF!+i^sEEd)~wdBvfU~4Ou z6b*&)D}q2*xKH&uJG^<=ziupcqcS=LAx<9kD+GM4e0%}4PukPOJsRURF8$(lbAJADaG7Y$@o-1byG85%3pMevYl^6N(EZA`qG z!14k1eQcwLqa(>mq_DA3$|RBPrkgDv&Sw@*^~y5faqc3HH%tqQobAKwL!g2c4IT{h z7~t%exsU5%_mPd*-As~TsvJ=Vd!pj~sHtWFmGr7+Uq-}UAl@nvxm;C<`+tmV7|(&M zt_l9iZk6{2(~?}6@&PsF0zj_7Lg3u9yy#m6%!42r3Ia1S1T_t19m)gi12fHnYbpdq zB_ssulDmC9O(jGv5_BDXt9=zL93pIG4RtxwigGoywKFs;?aa-sEeu;Vf?nCFfv%OX z0oDsY$@TZ%&fNb5Fn^RsAPPjF28RFN)_n}2LiA@q>@9L##a(QgaF_2Nlg=1qW9_=x z8iR{CssHilpqr$lTBdTEBuTY7hs`PIAN)%&ghc8LT~I}p1^gbQM%Tl67cG!oMHv>{ zV3ehlxCU7JQ0g}_G)mr%F+yZ{Bn*Xad@-_gyl|u7U%EmHEEa^8c2HcFS=g23I84pn z>(pJfd$U*JhuebETt6UPRTYgzb`_8w2bd8?y|=;v{;z4<)g&I;K0Ciu15H^dB5IH_ zqmZ|+__QAOpKjUc*yyCaj_z?HCBsy(qN?_*_7xnvO{^2W8jp2h(~&*ICh%iba4s*qTh9h>qHUN5F^;m)*t=Hi)E(olk8ZCn=#&M)u2e?QMs`n# zfw^nzaUQ#7Kjd?Mb zXjz{Pnh&4N&P4Qe&vR+5@R4>i&OaLjFlAf-yg5|8r@1O)6jGF8Owr@I_oLowsGi)- zlm70-z1`jZ@L=z)E4D?IZa-t(2JrPF15%Ofg!(cRR4%$@9pb})s~u|49dwDc=%|4S ziN|T%a2Xo8`*o=8M11_}smmmX2ngI(ur9arWLAt@AixWLduFn1_coj@HFvwp#+-dL z@00Oe+Lo(Kb=I1QP>zkYMjZ^=koA=~yqc7>mVyJJyx)P6BOw#S%5!4tc6ogb^qU6D zdDV{iX=pOn`QgmWy8LqDNFu5OMj(ey#nf`wP2+Tfj1TU1$^CPvmj@YBO&3fP&;AVp z>UOd5fLUz#u+Gb$$W?TBlID$BIM&u+$d^Q5K8H%ebiXXd0e~O&0Lg)Xyd2PX&^xfm z5j|6jJly{%rn*2B=s$|Ntn!_d7vEG!gI~eIW!S-&mxh2dW%2gW<^1V%Hx?KFPTffh zr2vx^I|Ultp$&0o7INekgCyTGh=It#P{O0e8cU2C`EVQ8K--gy9MKqqUuB{S=P-Kz zoUTb2=FT3ZoIsJLL&A<3x>t4umES88>^;v=>kP;>fAjgGS!=cIo~!-@jmHbdBhknJ zb>_Q=yd?(=IohO% z6cx}%4iF93&cmWVQjD8f4CGtw6X@9ssFLFtdb6yKe@=}}9;t?_MYmX)x4mi|zT|z9 z{Hlc1T(Ec0#db4~ZX%REazR5gYb!dmP+**SdD5Pd9Il?rUOH@RbcwZIKIvpf^t`&t zD%~#gT|mpeY+rDO7V~-Pu&-H(U(l-y2CjbIt2GxlKGFW#HK*!Xb|k1~^7b2yqK4sO zvWO|o{y zd7JJNuSw*dXmf6&z)^+C>r@+V;r;<(%Wwr|J#?vDz@7&p*SBsa+1mk+Rqr`wp|`pt-Ej3H#-p-FiNvJi077w1VSF3&94Mtm_0d{!XSP7TQI`h0&`?9=Lxa* z!TnO?r#*}q>1e`j!vQhV@JlC(_cAf&qmQSbh!atJ_IM#uUmmmWj38oP*E%1XzB7Tx zLY56;p~1PinX!@K=E11=eW50yy*SYXTJys6ZGFW$#HQ2)pUkyQzMSMAR>Z!o8jQnd zNPu4O5MHfchJwvuPh_91M&ghW#3=nh^ zsmNiih`&jMhd<%>;<3OIe7ly<9gjxwUAn3fwwab$M>&cEr0K*-^-@d*qL{XCv=wfX zNhZ997Liy32agtba?c%rNvT;C6Q$r2qiFP?ke0%C!Rb;Rb12y*FjioQbc^qf?^pr+ z6KdK4^9ymWxF4k{+lSMxqYmF)S)qJD#NsFkq}X*kxM;r%eP@B`r9S<-FS|%v!MDGr zbotbIp%8zER|VB%B@}#RD*1!3*YkRbtX5PhVyiF5wKA(e3Yn>NPiL*IqAQ`?Q0i#Y z3SsYQwGoNf*vnX z6yOsIHXN>C0RPtz_gU-9-XL(DU#++$Iu{izV)Oik67~C2I7H@pTqEUN3=@*N@u*1! zOT~#z^+6k^@iKZ}+VaU$hDr|oI^#Qk(e@K&K^3%4!6*tTukwrzH7+v?c1ZQC8& zwr!)6N#A?tP0dd@b!ye#d+D3bK8BQVjy*G*(30bR4e_{}8erwGQDej6W+btL8f<@r zkjrg#^VjR+e$rGMq!0{_$G8>}5?N07(WUYx70cLUUuf420@Wno`{S# zZK!^dZn-d6iiLf1Oo~O?4p~Ysy{|wAgKT}Su8Q8&O!?hBeVLlH#v zYtwFFa+3=HDcEm6t>A?`1=PI&Tnp9gSWevQDu0BP`y)@*`Z7d{Dg%Y1ozKx|7!x9Y zsb{Gh+TG1p#VBHcyp{sS`88EBW|r*YHG53SIRzU4kFs%brC`GbI%;o`?|2G}4mWhA z+9T%4{tDx&g`Fvv=TU@x?}P=`pp>PGSNfMOz*A%TI=NC;um}K~yMP)Sn=9*MbxZIw z#hP5YK{LI`_3rx1M*MeJlK9HthV4P5(3@mHE}$m(Ilh^*{s+BLnbFn?XF1>luCF-E z6MDXYV#WX9()igrpiG$A#=h5M?gY>Dxp06n-d34r$b~HpDps! zyc5P5Kv+^iB!n4=v;m%yNR?fWh+zs^rTAbP@x8!)4T5*)$z0|<=A4Op6m-fF6s7lU z8iq_pi8uriooPw$>HD;q@JH62PDJxFB?J5O46}}aT=y^9FPe=e%btdx>7vj@pokFB z1mG(GD72s1vcz-pebRb4QBKt#+bci!QUV_613=_-GgkUq_I(UOAb^^IFPehdoK&xP z_FnIa(ezG;oJAF$y}`XW#OFpjH^p+u##6aTQfY$wH34~hMT4JCq6imR>LUABippxG z1ecwU_g#7)S1ZzRI-2r2f^L>-1;5jx;;gKj;o^1<65#`#$}0;u7;EOB4qkRCA&1#J z6-JtCbbcy9_72eHiBCzy+Ivu&dWUH01|utH?@Z30I(gdlwFbHf6swgNPPTw=8`{Jx zFfpqpQohj0wGuBpg)L&B{nSGb(^7oo3J%2#jPI#aZ}i>6s6`(fjm2`-@2 zEc3$@NV%QA+3KdA6o#w6N}9IlHXtig^eW^AE$04ubGtaHS$V&)LRUz7e|(0n1OK!D z16B>2(^lKb#d4)E7F~?7kmqKvG}&6WlLe%Xi?YVyT`Y6+rS)YP#DUYe;j`qq)4$2q z?dH;8ObW&d60;E-&5fI?wo|Q3BZVG11z;G$Nbum}}>4;MG#BUxqhFdqt0Vrb)s3~-#NuTXQa#t8aZn<3evs{> zyc`TREMSA}H)j~h^>Q8OMh>7yrf?aHmIOeoxERCxZZm84Hu9j=YoRVVhbH%~a?_D* z7EuG5%U5(jU&)zDq0dIlt%WZu#z98Ad*^Y$C(vFEiBC|68pY!OB1Up1QvyKb<};$F z(3^!@V$6KC-<|!}CHCwV1_O6XfIfe^Nc)tC}uG5<`+TdGKAx~bKqV1*=-sxlEZQ9PooUy3p&gcBqQB`WtB zcvk?Ehq?p^O*3SgeQqp&sr*h{lz4g@T7J&qgen2Zdn)iAK~SM{^6E&7Ki_7VZf_cg zyBoc8v?Ciqg-#&oGiz3Y?k&C!|AiL>CCg`=;XWQR`u8rMMy zn;=eke=!jTToGh0*Ow$RpPTT`ZSgtN&Sjj6n1(a=v2FF4V@YgXr+}>WMCpl5JUztT z3gyjIhX&n_Fxr#;>)(tf20UXk7n8+fk{0NEPyNm%ym5uy(N`Ga*-^b0H_@Xt299LM z*p7)3cMcG)4f>{c#Eo9<-I23!4Hl$AVZ^au3v6eUBh9Pkmin8NnS1%olV_gdmtEw{ z28u>u&uQFzIU(mvEQ80;TB+v-eW3)qjr$#*%<-;3N&YHTsX6`+5nv{c6k92R;)3A! zMQE3m)-~Try5Vqr9R)G9211Alts=^e)5}+1xE@A0uvzS%zQsts+HHeN$z|y}v)?%{(D)4#1v-fxUib&#OYn_d5W{8Lbn zeTPg=40ehBxvzSTZ+ouB_uOKAW049=*2MJwUq&$8A;v!_7R^u%#s_c&J5SM4{1s1x3kPABvIboFbasook zz7CUx7m%nMI+&}ln%ja)Z9hxKON9ql>$3gE^V%}2cR=uhzo6Gos$Bj~MJ9;6nuQi| zm_^9$2D*M;OD*{DoqE)Ta%C-YtPQT3(Z-j@Uv4W0errrg1$!9@W|NEnKwY?wI;lqG zA{joDO$z~)n=`evxF@I%^Lgmp6iMt05fk9PX>nmz&AO=;P24i5!3GjhrY$wztEs#Q z^(~a2&yVPkR5Lz0-5iC+T$p+GJ+F;%NsykJU8H$D@+@@}wms_%21xX~!%)9FvPq|; zT2?6~ZzYI9kaQ?5!E;>(;GVXAxsZ&a7aj3fPPQ6GV!AlI-4gI8f)U2{LNlSG+imHQb_8U zK2rsHqt~!F`0`%GV%B@J^@eq(+h_zH&@ZKib?81@=1bc5@t8k6_*__20>#hVZZJkE z9n`VaBOX3CJbL!aCp`Osjg|g6&L}@~C)M_4ely|Q7dvm)fDBdC=W({c&?t|4=Rhu= zcPZx4;gZN<{_lV;%<>Bcp%PQaHUWA$1{LpCY}#87nbi2U*)Jls6|W5(qoJZVnjSFi z-Fa-ko~;ZebOpa|c$i)$iIdq2%PKFK=3BkU9o%u<2y#SR1)8;8&6Bo zV*>4t;*gnvVn2~18_B4JM);Mmt%_!x@=Fh50o#-c=LpmtbU^*s@Hev|NVkJF4mQ4FX&V@>PAKU!BohIJmNboaY4eQNru*{HGWd#ro`YX}O&i3Se(+eM8s zEQYo`F`)PrR$nhQ*7UXR6k?TJ0qr~r8fVrZk>}rq{#-CFzvwFp%Cw5(Tx>85d^DZb z8@S?Scx@tJqdNx=VyUy%t%|p3IciX9rKCV)ViD*4tO^ycn*}~w;E@q!2g%5DtU&U15 zMrD>~1&~yDGj_qOMZ?193NZ&G1B;X1c)CY!WDlgR z8zpsrPua}c=JI-nwq^ckwkhWkYj1N(KP=E+j&VX(9wISS^S8WIsL}Y@Kt%2xZnAhL zF3Hbnr@*HAG&DGFb#k=^pg?2}+P>)hIJVm*C*{p0JlR@L+v?tQT!-{pFY;mbtM@1G z+5-1uGD%kk=ug5rgxd=Reu3Aah~_xa5js!CnanW;p3>lYa+-13b=Z)thr(9nY4%n! z-96~Izkn>?03)iCLs8UfCQBA?{Z^|4;$ioAWC~?zyf^DBkt_a?ckG%UtIuS1<+i_* z>dXMlQl^g1Gi5|kfOqt1D4i(A~ z25A~3g0xkrLXu$N{`7o_8eq@ODn-{9>*?xZfA)a;r8NuTgdi+9*gh)A6~Ho*51e~U z3GE;yOaa9y>@1nvE2bb26Mz#9%_T~tTLeiJ0Zfd?5vBZFB#eVloM;Alj79ARuWU@m zjKH0}))@H5e`R;4zDDNq5U_KVySu_X+6Y7Jj~^Er_gZ4Z8}s(vPTT2NCVhLbGMZt~ zv1^?bV24$M@;m1_6WJf&X=u~rr#4Dy{84M6>ph=&Ty98{Xr%O0q~zu{v0K@~_+05? z(yB#5>aFYZ=ZNWED}ff>q71Jc&*n!bF=ISdH-l7@x+o#<%5uz5kle7^EL_KlP-QDI z-{5iGPYwi-ZaUGVNrUN(HVu{<_4TPomGWUy_Ygp4b&#-dLjQo~X#{ z+_2L|3JCSVTRwmiS8Sqwa$DgMbjWm0YBPS}wAhXfWtD9re8ZEdkLuE?r!V|na+?Rw z$Z41epo)_V%UsSA9Ri;y*4{S8?!_!l6-IM^@6Uiijmr`1qpNF+l6^P^7jVkz> zbEZnw=K9CbyF1fu=)Ov}@q9gX=HN5Naop+CPBPu!93zdXlN*wK&+#UggcnC_V_0Eg zEJe6dbFzA3XkjFCe(M`@C+3m!*_6E5i9sVSu`j(7x>z=dNspm{9 zRb12ywmEaXqdETIrbOOarN0R9kl;wVS~mGT<`iL)O$j#v^5I|0%K8bL_}h2@im zIY&rH>_k-P5GLCU5=r6RLQ`rMaxh`U@q#>$WQGd7wC5}V^>Z@yW^boKw&IxjV#5!> zSTh2_JSS)uydM$dIp3z4=CrmMX6So4MVM2a^!3h9vck5lZ^g%+#aW#vIN*JX(Ldn) zmvsV#lq2P_R&mA8zmf15sP!Pyr8T~q=X*?p&KV9NYmt*=$)`wt+G8IPz|vmXb1J}z z@)`hrX>yelzB)0IvT|byek);LuU6*~vVFb6%8GqGK*B}aZlk_;PGBHNELHxT&cKzY z-jZu7y0fm7!8mJOMWfTb%6hVK9RGDcZLTS^+D-ClNi>le!E(Ne48}$aHxf1gtIm*` zC`}Q%p5*~-)_IccD^s)?+i*a3X|%BNH|=d))AtgRoNyz1G57-;tjM8uwP6;|NGR?t zfX;xK>EmTB1-+A2?5|7ocC$my$7XfwS~K(@=^>*VuIr|4RS zF{5Q&*?7jubVJ-s_&lz-#Z}-#f>BOuGUMWevL#Rf3~eAX1Hp4)bWzDeCYn@-Z3?j# zU8+eoggKnkQktSy$>7lP+xRA8wG7l3n2!m@j~)2+1|4j&0&UJWYPNeYQ#DN4eFM9_ zCs(7XQgz&!I9AnW*mEMBN^u$2@1wNtO4FmayU|L(Z`;$dWmN`rGj8pquv9lP{ksAMW@lKpRbF+WmKc1znY%8wpMYuddz}>v>~_Gw4C! zq)L?LTVU^v?J>osH{alZp=?FQBlS}gv<|x`GO%C{WcIQL`<#<2tA3Y;c(Un=k4h8v zxigpOP~+|*shQc! zE@Wvw*F+?8@9VMs84RjvdYDf`n3o5roizpBBl+-tpv636F#umgeUzZT;QbJa-2a_2 zoQ*%k@;|2RaFH+0-=J*W#e-w;fKXr`A<(*wgoMta@P{jN`Qgf9I(lm0xK8V5oD}ka z1d~b?1k%IY1JE@I_n6~De4ZA@#wl8Ec+r zE+26mf0ue)*a~|9^M9pOorS>4y+gW(*c$QMH}7Ntp-{{WYY%Iv*>Z3vWgMr(4(NF> zJBS^wPRepw0E@RVNgCtW+vqk;{~5pX`1QwtO+PK!6?<5N zZhPYPKDHmkFtiW{H!t-^q!nB=7!!2)oi(}=w_U0D<37vcyn}j+#Se*(g+ndy5ZbcQ z>f97vF30IvZ0SOH#D4uc94yhhR$=GT^7I1~_707xah&#Q8C%>@A(1c9c5;dpQ-A%m zWdi%}gJf;yQ}OaB{l`(@Ss+m9!`U8>wR5j=p zb*XBl^vR_*c&8;8w`pyX;={qp zziBYOhN)>lrnm|9XX;~PhSPkwT(EEIX`}kCefR^CnA&Q~+3R$%Ux$)X9O^UrD6gt@ zQR06eFsIbMFBp?D)j2U8>pp(*bL6~3AC)L8{ueJ})Bo^t!y~#g_h}shWPu)H(EcvaPUXZ2uo*<~tHFR=2vGs`AB-Q9N*UTF1jyzlLgr= zu)iiS<8s~BWH{*_Q0cT3JLZqsX$0uzOznUjZvFLSJy(c^q=D~ z{i>F23QGtHGdXF00d_4ou?P2u6N__y3cCUm#5ifAZY=QI z;7dmTK*K@ld9vgoD4B37mU80g!Dz;5)=k7U@o9{2aN3Zvx;M+nm%P*MX1S&YM&hPx zWfxLQ@=h$!f7uqDXpUY{Q!Yf2Pb-nz#?XVBq!9@XC>h9M)v$_<+R-YXk-4uv!*qRo zt0vB5{W$<>|BLhxa8d7l0VgnpqPw}LZYv7TI<~C5knD(w^muXt>klfhvnUs7qU^cy zLdo4(y2#4alh6AP9X9xKeYuTh7g>>oo48jOPhW%dE{nYir%NtM9S!4u1zXHV>$%54 zEFoiHa0i-r)&Qrs=i{ofZ0+)W;IK%UJG^o1cIN7oP)Q*!H|s4-AE^rRVQpXTpgnlwL&54jG@fBY9HQpx>lyd$%?5)&x*!gu>s07jJxQGjCv}?fdD++v7m##70?BU7bpT@sf;VjimSzbaY z2{3a@L(Ir1MS`Ks5`ic>1aZ$oF6KZ~($zPINMEc09$J(RP%s~cjG6FPg(AanmBSQ+ z$%J&Yk0n?okb)3#5F_XzsTG*xPjOhljvB0YQ10F1#%KCcU0IKU_BS9!PNIgSu?5^G z0cgS?UlUyk<&QnLYGRn^XS6H}F2Q2AbwJmI*iL*+Df>CW{K{*?_z+Bimm(X{%CFE< zv}|fXHh_W8Pfv4S+{U?<6bZ;5YhHroTW%hWU+Lwg(;w{+8Ahs6oDt0h{HiXk?ZC3k5BZ{uT8#$kb}O)}}< z)FVrVxkO zOCB(Kb$R!X1B6?# zX)mS?I-KF?c)*VrSEIKvc>dI(czGrPp6{W2(K!lZ$KJtzfYnBZx|vApal88FnND;o zdbBpon~!R)nn+&u1S0fiT-Nrj!-zm!)2RTwKgGNFw!|K{c<;Ref&W- z^-pOv#=a?h!L)9=(jx(ws{;+Xap?4sgW6&yirR?!Iesi@66KeRrC4v{aN?79@0{n( z4S0mZ411Q>kcK9i7JK?8-S@ z`R5xtm@nkysnNH$Saa<66UL-;1xO5JUe73f$nY1?=8l^9fAFF_@q-tH%?H%W>UNwQ zFiPZ~%#|LPQ5|C^Fpl0aEIN!n{*1^!JA1R#xssd95)aXD<)XMbiU|O}-zbNYIRQ8W zb(B*HGLu6zlu&t3?m)CI=&)}+&`P(j-(Ls$?1P1)u~`<%5<2I_kiDkQ<#-1-DJSgC_*HA z5Z*hIBCq4Zx&Tjy=)kC3&F{c9^MnU5>+q=a&eJ~DUs9~upOmvW$EN<88#^F$8 z+GFe&Ew&cWGT@}UY!RoaiGDttN^`jZbzxhjve&dXJLUn-NZR3!s*AFoY*&h~T!lEB z52|C@GW@yq_EScs=j_2kJ6Ul_Dg;h=3GikyTd(HAzf5J#jG|=ub81a3ut@&F2HM~m zztL(vkBf~R*DXd$e1E)jdnL~(EX ztyEIeCSM|~;oDlMp~zt=r`Et$XQ_SS{AU8s$V=7)`!-~o-6Ie9Rs4de_hTy5k^4@@ zm=tCWME)~Ie=C}}y5q>I8u;G&(rrE1kSoTHRi0yLZ+CL)o{Q_MsrTJtB0o`TeZ>PQ1Iya*!BBR+>u z+@D#Jf?*0VAwG*ROE|d{!X$mNlBm{DG{B=mDvfZ#{l>G7&?z~n&MLh#2<*m?K za>?67_k(+vR51xLNfoc|ha!86Y+2%%|2{~mbu5#*-Pl=ZO}PqebO8&nY1eYUYyD@s zRPcCi{n0HIE!#D1T76YB1Mj9tIp>f=xvbZ9I-8_@nOH&k+6A^C)a4~-Z~okBnXm5lkUHI${X9D6p*Ox={(}C3^{3 zKEnvvUgu#qB9ov-Ya>(|cwf$>S))8xnpoCDOzE-oe7g#+p=K0-_fS=8;LhaTHf$DK zVpeO=OxF90Y5_RD5rW`D3+=;vkzae$EJ00p1&l{~gkfKj?aGNs=pEedJC}_db!P&&t*HS;3cdTqx~@Kdk{wzG#CXD=|4waObW5s+&s`Xu z6_3_rP5L{+Xk-HpPVwU}84a6GDGm~GT_6&3-*d%Z2XK!j?{g6(+b>f;gz{W^bq=ff zK_YCY5jUKk>%ic|t&OmbT$tChO?tuyd1mdO0LTynpA7{j#cuCzDnvRz;O2&`{eR)| z|Ec`iZyFh@Qewauz^mnc)S$K>|U7&l7TZ#QTT^O(7@ah>XKfnZ*K_1VzNW6C!v7p^y-W zc#3`!z3X_^iS2^~))!_C{x|NQs$bKYMOD4?EpO0uM8?J`Cm@*KMkFEmzVjtGKZ)KJ zk5qUQxSqd1iFzCGtSM#)>jlMuNW0JdpLpTfT4hN^Moy)>I#!52pKDgnyjzy}3oxg( zf|o-ucC?{aDI3^RstY@dT8OyYLT=4lWe7{G9d(397sMlp z^a*h*h>a!k*yQs-m4`Cv7>awf#*vGAJl65Cb=$s*Q-$d)8JeM7>Pu4(WWvz_Y4JUw zMH&8na{Xu`^t_AP3jbav5HO1To{BmNXG-ZWbUuhwt`;xZyN-~W%+pwOFZh*K*8Tj< zOu1ncsZTL*thW#vALmEML&HW zg-<$I>AYX&zCQtAP{+#j*cFQOYszf=tWob+YG|EL@_bEHo=GB?k*x zNB&03p6iXkTRzaor|SkOOb;dNuqR(9#ws%DOW#g8#M0LO`ZXpWpHAar_fDL_7gfp6 zdy~8#X#ATQEag1b?#$PUlz}_XEGxv&RH=3QRgEiFffxeX%PJ6~L9I;p4D5$5#}Q1h zd_0bdj>Hx=&;l`in7wpI!ZkRSP8~2g6U+j1sWaTb2l4iZdL+%?Xml6TiY&O{ddu>os!M{s$dT&=uXVGU_TRu^cjwY;( z@6`JQpf#C~bHh%h>N3+;(~1r-&rUp}T4X=%jb6w*#s?D9noGw=Evgu0F!TeNsMDVN z!*CS#)V4x6bgHWI=byh8IM)yls(Fx?ndTQ)MsRtG3tQhbKG3+||FRbIQ`{PGAgk4A^!3!^5kW9a}P{ z4VaUxy?oQj4HHz&T<=9nxaHI7AvC}cP9XOh@r*>LDUz7`h-8Puk>0LG%ydC%LBB;8 zfnhb-0a1^L$ipGZi6S3l5J2h3!vr8m6^&KH7*Kw}X+ljYufg|;)JFI#?>L{rei1Ch z{zTvvm%O*$pXC8abAqWx91%VZO;U>anyO1EKlnR`3Nb2yKHtrpCmew5+@5z&xdL1x zh%f$WmP`G*F0u>JLd32nnu;#1oJJ-}fsczb-dHaV6nYrmE;iN8Inr2GVv6`*M<{*j z^vtXw;jF{ma!?jh;(m|Iq;3{nZr*%#59QNoWB4={`kUUEsb9ci-u6wXrUMix1gd&k zo-3r!q{0$CV0OHQ5!`0QknH!zV_vvzN?yjkPTny`m1DQMyD;I!y6k!3#!3R0vCiE- zvU-irCreKRS03OwLq=WEZ)4`7Ot=%3!x2lEvC1t3`K)N9ulGHQvzK%fQk^Pad6 za>)<0stQAYyal+FXlv)uSfe&PbClk=!y8`S&3B@Id{nfW2ei&>?D7GTYjARHb7-W# zBL;TzlQpeJsc;XD;x~E&X^`)esPQ{O*Stno+?X!``&Q&gc}Enw6mfWwB&r;&a2)Y2 zJ0@ET4!u1z=u4$-Z<_BU=b+ZxMEOtN2T zTrtDy*&W(AN@rquRjL=aSIp7Svb#weZIN31IB1>1D>a1xw9uh5^uJRL8F08+1eV>J(rhPAAP>pB6L}U8vU!s>P_yO|~ zD9inCA^X2TZuJ8*^Je9=Bq#!kA_4-Us}2}QJ4H<14?mWr;;LD!uB)%=R+|y?+duG7 zG#oOCkVI551%w&`jfadgfe{gP@Cb=sAf~;P_>8g-5NdfG*}MVTZs;T3YLvf zk+wFhZ6I_GhRz&HDR&({$e*gv+v@V;1a5F&S!SlLwL2HpC?iy6(~*+kdwvEs`N)yo z2)=T==Y+WIm?PUbR>*lu%NpT0Kubg}*Rr?~_8<|;gs&K>E z-9v&r_q3cqbHZ`eA~+NCUO?Ylwd2_A|7 zjoEaBs=pomN!!j4BITa?*)yHvF01FLavV?2ijUV<`*>>auf;^@c~7d7&!6RWR4F|H+&taPQ?W? zyvSuMDn%l@(1mw`lO0%l%P!-y?o6e z0-@gswzm#U+7!$L#S=W8VhnC0>OsBgf9K|1GOp$rhg8dQ(J}D`+>PNAmYxCS3-7O} zj-@*r1Vysb0kKAS3%6n(Rrhl63#mN6Xzz{rrP;_~&l_S#k~IDh5|C-C6hBw=`v>?Ym{d$#c+B~qm+y2ULr@Ni{Htegl z-SSEE<1^y8_#wy+5>txgfo9P^{~$e}dsirO{ueH?3sxFGt{C5;^vVxM@5+wWN=QU{ zBqF3qdpJZ9@1c>}o5!nj%~;P^SKi8J4 zh-Qo>OdX~XYKul@5E*b{hiJs(5(&knoBWxxq?z{HP#iUmJs3@l8G}WbqF-9TweIN< z+nS!3HT&gRzVyCmGJbZlhL$&Afq*^C@DmWc&pgo#GQK9t5ybD@jeS)Z6F}cj-U{0^ z@m(ysTiJs$oxsMge=>iuneC4)rc+4pLuqB^$Y|!0jY=#WOM8`QfsNyiFj?$a&!QPBDApQS;?q@?W7fn$z-^A3It`}%gPwQRPCsgrG=kR8qnyi;%ZrDrqLYo;^x zsk}+ZKovqDv$`!g$n)~gmh*2gYa=lUoi#p@E4ntS+u?gTS@z9{!r{|vOzSNd>zFbh8WC?p9%=OjdC&p z`w1{L`;opAp~OL|L}j*<>|=CG>7a%Rg^ZxOy!6c;bJm`>mJXFir)#mZ&o8(x?Oq3k!l@pKf@G)bYtaFmW#E-IEj>^pFRjf#Pu)oHoj}pHEw$1+kL*CZZ!JQV zJ%&R!v6aKtm&~^Iz19t{@gg{rsZN*iFAOQ2q_-DajpreSt!bKgtsz&wJ(!Vi+>>|C z2WnHW=P%UMWT<%68P)c)>VoyTLdV@0!P9we+}qtGeyTOJ^k_4sb0d=HvKGM_;@6_{ zZrSMx%1FnX=Z@#QAa0plO_z?Jzn_z{_$1qEGST3V9P_dlUPw^EBmpAvPakVSh^Vae zwkx!ayMlhxED#LPL>K%aCl}+w9(#cqvp z_ejLBUVySS)$so!W~`=C4MpU{dnmoo?Tq{9HK6MLHbA33Q&)eZzNV7U+L@_jrU?pG zDq4#h!E(z>L?8$w-;gnJ&tzFW z3o^0b%hi24$1XA$+Jv)j;R7q~lpFTs>AE9J0ylY0)>Fxt8>D-H5ytlk4hd%qBH=+Z zJ~wZ^x5y^Mqp}T`%}k!~hrvP)M@P*z^RFe|Y3vAtQ`>(2<1C{+PBqyCo6<^-%m&72 zYQ>)bbom4Vb{Tsl0n8ioI-!*wydhU91_bCrhkrObhc{6u_B~En;3(hE z!pbhl=(h?Na|kgLEZb!v zal=|JT7hrU51zAaujPxAt@h3oDs1Sxbx0yQ&*!Rd~zi>rO21 z>4tp8J4JRs%BkE{Q9H$_Z-@r$DK_h%0$*hC;YlLeZr887s0E4SkG!A@BXM-7zM|Ac z=DLn=cD;OtxvI+l-NAUBlKFDW_iM@5Z~E4PaTP`l1O{3}e-m|D-GPu-x%>}clExZ- zd}RL#T88@;q@m7TF(jrTHX?AFCIj>%2MOrgwc~Dk9O;;3VSBi;pO~ovrY)#uoAHd? zxhM)lp<#*&r5r;;OmrQ(rv!+nl-DzQsl{R@@l0}Nm^=g$pEuN)k1C@=OQ%p0sLvn9 z=UIi2QPXH2Z`^`7wiF=!7JeCZy=~0$qxWO;3F)XcJpBfU3q}#Y#X0PU|5x-c?8P#( z^xbYVY$^o%diN3*QNi7(s*7-1LHs+u_VXB{58(c>l>(w*5#k7vG9vbxT*(uEo(WmG zk?GDmY+A-~wDw){GKYLYYoh!OnH8*9zjyN}yb_XGl+|cHptHP^E&K{))ngW9 zaWqy@57ZV-_wXuE<_{q?Ch&G$aC>u22Kr|AMTnb85_t@XSAA|BdPsR{jH089+4*d? zomkYwXhi*0&mpi)H6im@oKCfl=sKXDo>$0dBPGN*rthBo9T{DrwW3%-d7 z#I>xME=_KND-hrzm)s{!<|{7w$Tzxm^1Hc`?c=UlU)W1Y+H07X>rY6uGW$IIuKS?= zdnV3E5C=s{Y`ITz2n-o~%X%nn*se=Tm9S4p~5y_KrH&W{!A2nd>0y zix}X503ZAmHkFKqU9ItLs%wvLX!?Wbl16@jus+HS{~a)rwLcon&vSE7&tEys98jC`y3Rq?dN!+p}u^iV5XC zPk{YSm+wvGuVf22{qd6V+eeU#CKyQM4EV)~|5Gl57gRp+?VhT{H1_zv@HCle;Q6u5 zc*m^_)>wzJ6=a&II$jRx`V?GQDG*6_jx2o{)wOaZ)pW}qfO$|Vjv@18pxq?@~yG*I5a zaXQ+XTJ%CraKv(0YeN^peOtcW2VXI_V@1rIDWi)%a> zny$y^>T9DI3G@S#oJDpjBWfXgY-K3+rB1{0>(%CLxL0b{^@2i@D>B9(m?4=LdgxiuRbhq*R$Gpu^_oiJ!Zxa_vuF4ej1@r})upyA-5U&o zPVRmDyWADx5i6$&O1^Cczmqz!;S}^l#c%uGlZ6v9E7_;w>c2PE1aoF8PWA0y&;6qd zNoM}(x^$XZqoDNM>E=E7y+FO|EYnGt`Zsv5*BC!!l|Orq=Rfbwfl?*Z_m$J%N1Fbm zlUX0t-T!YV`$;GNYrmN5WMQT~5(UJz?)#OAd6NdtylMk>@NX&m$C6Ruo>fV)?iB!} zhcW4icu(gcK?Z^#(wmqF3Wtk^#|fv%D`cdGxs$YEko7fMj3-%ylW-l)vk0=Nn<~gi zB|+}|yeC0pLa2-|#ND@Gzqa}fX%6U!15X+N+?xkN zcqjW|ni27RpcZ?+q_Vrhuecaw8#J*8r1SUJM9xhae-;3UdCj|cyorhWSo?BW#X=KS z*6TsU6E+|1%8iH&rKYQIv4T%C zvY9+(4gMyse`xg=s5Y0^4xl%?LhZG1n)->w0eT68*OMw|JGgaKL;_c=G%2?SJw!5{ z#Cx(ZA4?2_pBotV5nXZBk=-hqvpA=^JpWvFZpf<$H3F^-FGeBygt!b> z!Fcg1p?6~5nIy=H;$Itln-366LMf^v?zZ_lk3LKi%KPKX{-lyn zK&4mcJ;?`)+~DUh@^04U&WnxGOq9BLZF&w&D(fY&XEiZvWHYb_D=e(Vb=Kv9Q5Aau zR!F(b7Siis?p`#2TXBQVLtXR+10#b6w}YRiUOL590iqK0mqvR2>cy+a_%XtiQ~5+M zh4++ES^&Tz4&{+@OgT}+8I%Z;Y+|CRz%i!C zqMG15Y^IWvG73<-&k7t8>fnF;*033{pd*!50h)l=?RJ@8PBF zpt>8qTLAutx0idfr{69w$xk!<0R8(Ua`!N@0r(uD2czsh%MUuAxwfn-HyuDf!$p!8 zlg(b94k9X%Hbh`;#eT#u6LX;KhOt}7pB`=%&@hPC^rckTB7P()gH6k{A7=}Jg4+?mF z&nh$KZa}b)bQdRW;uLh7{lQi5iq7}W8oDwyV=czagj>a7K4*3fuHjaxX}JvMk|~09 zZhM(P2mn>{QAH*F2v0saz6kIvZ{_goc*TTG%mjE_?rFV=~Nu|YO{ zVSsL_p+iKhLAP(U(>H-I4O$t^$5}pfy5I5{ILF32X(nxBhI;tb#+tr+O!uaeP-x!z z3N0v!#h0|W8jjJ7zBg@UF=vIu-a{q9U4y42USmX4FT-u%MTK_TJw;apPVuI*5Pi7n zha$@C^R%OtBxljpnEAYIrS>#D4vxu8Evybil1D`JDa$X1CwQ2O-;B*X6gLnYCD?_% zC{2*D!MDM|mVEPjN!(5^>A>&F`Q<|BbtAjYyb;oFQ5i4Z>%*7Aw(4ZuTV|D7=mkek~8Rb$qDU zdHxPXUcX1Yad#Dr`gvz|%FL{A0owHz!|U@Z64E3sCFa5K8eKK4^W%ko@Y<6Nwc_{u z*R+IKayoI)c(Y;-+|I_njJYg#_SLBJ-cPpAwCzi%uND**>k!{eMf;832%+~B>Xi=Q zF4!*~S-`C(=31dewOK@+0$OKmqVO=Q_Ig?tFP&8;p;G zH`Y(Z7u8k86nj>2%5Zhr$u1e-BMQ)|zY#7G@8!u{ij3M)R*0I$isnYE-4Wa~?_7rz zPEmJl#Z9#^{^osrH_6%wPjc7!ftdRF(}b1dsT?lh~eUW)O}3DxJ4N4oOw z%}FW^BmK#!#HL9o#D&F<-0v!G3nHAKstqURx|b_%63(mV(8yloya~M4j5`_o^VcD9 zF4T$RgOa9uul!f>Spt8e-&Mvcy-)APNw)R8Uy-{Ig3fJ&7ow`~f5!&iJjCleP=5l# zmR^S0fClyXt#3fsGD|Tm;Ro=WsP8=LJ%+u6vfqC-^Z($;P9!HH7e-{*AaIzyRt19l zWkz5_Yhxb^t{SAOrNld$%PN`SF+mm4AAcPj(F)s))Cj{BnjEJ)bfM!g>dgyK!pnc^; z_onbiLL@)OHd+4xVxd=NnO^9E@+%YpDCXa-vduikZ39s6H?OxUyRE}K{hrwc+mny_ z_wWyafCno(Z>U!(VCHHS)z&H&vGp7FnDG)WeS-^b`Wf zxn(7U<4NYFqvApBj}>yDW><3NWc9Zt+HLtp2CDv06-w&QD;Pmquax=sPPocobLB_L zF>{BDXIJoK*RK%AtR{cBF*xzk=;;O6kqY zs_1^n4oFxbNkX-ib9P#BjCgVBajUS2sTf)+U2?v{JKQ?RPcL zL=HYwM9uXKk~o3SU+zr>Jt4BRg}LfsO;ny>uIKx7=_pLPh}idjhCZ-=Z@ZO>f}>$+ z?~E6+o$HGF`Lv)7Im_&_#!I1E2OwQh$>cjZIDG4Z1B)ydX!^AI1y_1)>%Dh|%H z;?b$=rpqOvs-v)RwhGyxsPk%ugtsK?4t$yj^X_in-CU{yK5H%OLG?W^$@=i13RaX` zGS(tztPfxN-mJ1l7^wY}Mr~qq-hy~6ru0$^@<|MvY~7|Cqbw;Zk*w5Z3Vx*7+M8;c{`OyzK~gJpB_ zIC-Jf&3ROmy|0I&O8QqnGF6O5JAz;i?kJ1JY8+b1^)#$eE7ZLWB15pV(G>YtmgE=|I7uFC zKWs7@IyIQ!xLTUoL{&iDUYimk2hi^;-rcZpmBwPK@yk08Go1IrEdNaYOFnD--N^jE z!C*Ydn(#y^WWDnsvMBmZ_9s{Xz3{HomXP;;|5N52_5>RXD>E2hlVAIN0wx(Z{cki` zi~3n?@vCGR%d~WXDRGgRkKlpG|w%GLcn6~!b+qscSBY^%W$F6n3~q ze!4pWY;F^CCuQVCM2n>)u|zDiYhv}+pW%lK=Vrl;?k~Hu zgF&MVa?WLxy|5cwSjtgIpFa9zLpm3aigE+2|3B`Z{ja9m3Mw z9wwnfYTKg8UjAZ4x}7@fYmL5xU8O70n^(s`cuUG?Ilz|R^IXs8d2#U4K4#R=-eKT+ zX_G8DS8yVHOcLyX@(N5OZ}+ihTLDp=!zAw!E&HZ@cXj zlKD2qNq#{-=&M{E5e4AtcU}VA`1cPXXY|s*cF%7G2>*aw|5lRvpBK#kJopRUr}M=p zB8BYx*l?q44Ipr}Kp_^_PA;9Di>H{RqNSuav?~%;zNVZ77ClqySfrVGiX3tdvP?Pj z5S;=K>VJuf1raB;MPVkG@66_nrGYuq!VpC&Pnq$YfKi6W0g9aVmAO#Js0v9VBvRVF zcmh+ZSRwiguM-j8Kxg?U^-WWkuGL@8{(!}yh(@53N`m!L1{VCb-&lUk0w{(vNm(~Y z|8G=5=E@I`63Cep&xGK}+P@Bf2_v>rGS*O#!o+5!`+7N&w`_}XC=#B%MYK3Nd_16w ztr@!Y;Jeh&ptS>__W@ZR(XwsC+x;wG$*yH!&#)J!PM6>ASCg6%wb4eIQ9$VQQ^8?R zB5QlK&-ThNHqJ%qv*umHUftwH*yzSyLwn{Th=yYo$N7+TTjp=5&}+u;Wu{y#)5$j! z(u>?ePUN$!2IJW2gp2mHkl&DUj!m+h3(di>FO=6slbN@4SsD|%P2^%w>=7m_#e4Hk zS#vUmE4ub)_h`S5`o2;4RJV{QvI*0WIKZUyBg-KnJ03Vm*#1aafwQUHG!%>Ji?i+T zDR7$807<)SF7RsNqwL|}tt3Tf!+Vul#8MQ(D0x;KAIX21z`+|Bs5;#hab-*QaV}coGD6(i3WN7B~>@G4y!Z1v#&BF9N?W@ z2yxm2WeUA} zWRow!?W4bdc^KLi6_GpmS26$34Tj*AiNsYL7>5Dye!*VTHG}1`VSca)%hzp5Ia+gG?Ak;tiV|F1(!vPiYfd|cSpcGe18NH741wc0Q9kef@aBv3;Ah`2sxo-OB8G7kWNH!>alm zS@Jxv{{nje)x~ZsAEy1y-F?u6cMbph=4~RnSoz$ zCowgvp_clUEg4cA!j9L+`yR6k=#dW5K985d(6Q7;*jMJae7W1YzEiJnZ zcz~{4%@+?ZM-?Iu^V5qT?%skYHUoyF#lfC|M7u7SH}hrfQ|K70M*Zec)o0C#81_Uv zBo(7WhEJ8ma*}o~`tK(A23_MoW3nUSb=J+X&w_+5BgckJn{UdAKjnTH9a49cn9Mni2ezq(Wmh5jX&|9=AeIl%G$R= z%&ZcgifOjSb^T@xoo4v-n8Cn+-XQh=BrPwvxiSm67Sl`vkcftlI6;SHhKL7$P?k4@QR znqv)6mDl%du(mMm+)N+`1fnvtPbZyFg{oAco^hhqEJtI{Bvp@_p57kmeLEFm;=_NY zG$}nP9hX$#_~#hxjS}QEt}~bf;E|@Y7};HNwL|9ByY+QmUE^-0^x;w4-0NVLakRd5SP}voJfdrO3E-b0VN_(^WRi+ z4yACX=w$=R`^w97B9g46k6|<3olM+V+pW^xU*9ye;`ueX#VD=PW#@d_`TD?dvl!?( z&U!AZBO2f%c~yOPyKb@7*O}(18j6Oe+J@^F^W-35HmEW>5OZM@>`2U+t%vo&4z}!D92?ahq-lpr=i}yRi5A_2vtRX( zzR~04-1$aQ`mZ+$D#@lcJT5Ici=Ec8A$xsnPZRxg0iiXcV`i1AiJ1>bx)#*Rc`yd> z)&iJ7BYb@68(##zsC4hz(PVx+-}+rE{fH)-NOFCrM6LQY~nr@I_Yny%e_tYpCU$lcsc zE-_-4u(26g32QNSFUC>!RBC43*kbAL69cDzw;C7`MU34^T___f>>Hh>0Ue7|AYkPZ@N5`b)dX>P6PpRMrTEdkpbNth(>FUO9?Trxt-D?4Cz|)h3xmh6 zc(`o>j$!;jpfnPLyIqMK(t7%9>|YUo7qz38E6eY8|G-OfUZUS*;cczXreOZcd58oY z_}OcE0Q~W6YDRS&(ff=rsa&^%9N}|2)Da>)q0H!TmU-3t@gRrxh|&tzzsHz=s%e|0 zp)#~m7ge?PT97*pdd3R^V;pxQwQKJT#oJX1+`*(|$2~2y$-{Z&g&y~f+vbz(tUW|; zUD$0s-!)r;6pA}cRy~BfOPmzR#?8GbV?LI2$=XyGOjV1Cm=iRD??d-(jTCBE6EESh zT(@@&AI5HWvKLh@7FHC~197?qO?Pj6`am0J;;sClGLE)!=gFTDBOwK6kuBHW{lv7qiNbeUgm= zD$=PZ7kI;d;0Nx#$_q3bBj$*)=TuWtfN!nmfwJ6hiSK?|QjauWY~bxi{GJcja)1wN zApn7T=>LmYz&H-Wjs{_iV8Mcr0<(e8)G{5#Y$suP+FqADsWx=86uQ8b6 z{l^CgRPiclCLGP0RU{D}wCTM>@5HXxt3NTR>qB?Xl3l`nbbN9u)q%ihx5S_-kjtB` z$uQzp2%68vOQ;xjNQW`$L-n5T-o?Ba)p!TYp_d;QCC%nM&p9@y7b{js98-#GdcI3d zev(QXE@tDj?WtYySxHXpijJj?g4;LK`MEu>K-r+%jn>3lM_+;#sP+(?9Io!xKk(mS zro-HDi973ODzi~@-o>D^vue+p$%XGwXC7$2BQvGu=r_HcmMq)Tb`%L7F8_=_>}udr~)}f(q`gF z`?AoS;mvpaK zktq@;SXSlmu94kxLH=pwkL``+pY&F0)n9-70~wGt2Hi!<#PcQy!y@Z*){QUz zu|KX9fh+l{edln3d`;ac0 z?>YR(sBFi%R^3>Y68#BSDP!ZgE*6tkuii1yR-zCmXa`+-r)|jX-W^5Vvdj2#nB47H zJ+rgpKiBZnYVzE?IgkYyL=|WBE=h2OK@k$D-gBYB>-XE^KCmY5R^Ek+yuqnw7g0#3T5w<3jL=Amx_(n> z<1fqlQ3)D-M7VypQNFGhG$L3R@NyOh%b%m#;LBZuCynb7-F!&zEy3Vf43k|8BePL=(E8e&d2`qttsN}p$4Jr z!!e%fU>z6o(U;R*(X|6gj*D_B*vN&QQ>Ya>TbXO}EpM{14bP6Oce$~fUtPO}L{gig z@218ka&|6s-lX)=1;6m(c?o03HB#GR|C3_dkyw`I8bvZ94~II(Xralyd(1iJFbZhZ z+KN>dJJq&von|OzmdoaNHVKtrT&Bi8Zn1mlcZcHbLq4ag*B#&lgXlFo#Gg5Twe3$A zdglMsc3T`KCx+y3{_-%ud&pMHy*XfG3D31r3P`7zSOyaK3BFl3QE+f$Eii49& zRLPy@^1%(G(;!O@rTns{r?c)M-eYH5#<}_5n~-xVnAB|J_fR|5*t!O3h^sYg+`Z`u zVcm2?^IMqdxomzlfL)7*r{yt+Cnq&keLWXAv3O1i`t_myEV~nl<0=}MW`OrJAWMxK zUe}15wVbE5{nVYFcjVGYDvaS39Hf&Y&QQbNoGKke?|~pX!#@F@aj$MU(q&3})i84s zIHiA!wN5=5Q7j(mnE>>H={(u{^l(At>0pYh;+a#Q6Ox{_Mg`(-g>As=9mzxOE=~8r0EBZ%oIkRzKI#?3Q{0X)tXVeQSs&Yw7Ll$jD6r+>v*-u<`l( zH@03;xPL>AY5%WY zW->Jp#Q;r?+uHBxVKijb0dSag>5OT3jf{<0(~F~$mG0eAlDR5e3$0Yk>3W(F8rZQ! za*Ujl^Cn4(CL$x6DJU$HrY3U7Izoz!zepFvYpb$>;2E=w?pX#cqoNpoE7D?=>U;n? zP%6L#k#GZzC0OKRZvbRsrINnu3pTId+w&*$cTE<>|Bx>b`Yho|C~*%p?%)m?VBhCT zvP!P(J4E%POo4m8@dcC6@X{K7;EJwzt~L(y;{rob(wSm-=y9O?P$w5yq{@eL+*qWE z36+^{5Z&+u!_U3dNchNBFNYk)ikg0pA|dY5?|kgN-;-bBoh(Rs%A3WYBrGb(o{mE7 zK1L~t^rq8hJZ*ax;HI%h&|Fxz#S+H^43e~d%C4b*;;s7lR}C@_=D_eK;%%YA5>O+$qEI{ z>{KkLp!Jpn0exgA0B$+VY0_P*NabyMSx>aWyy{O_6kC2S{y>bT;n8cJoAf+i;de2o z+jxu=5H|Ovg~jr_aY&ajt9F$#kP|_a65CYk!rP~A=X!K<&t-Vsb;RI(vW07MC3bb? zUo8n+h^en^2@9~P9$GP&TIzHx?n%+mBHx@>`W98r+eDy1eJ+P=7AM%%ryq3*Q=iLXq*;y$?)>rp?zs!T4HeFN4z_1 zS_gZmWiIgh;5SC(u57fJ0{Ro;>$~*_ylqoOBmo{!eHy6?J-~nnvFjY6)7K!GSpq}M z=K*pLqThf+_attG+SPr4WTF=uSe{uC(jY~J90DVL>m5h{F>O#6R>+4DZ98@nBb#c) z>l=TR{sVX%*!D)bK&fx0ct@OCC>=KExyA0~*DB>pgB;9`8~roI=w+0n)dLqn6%3;b zT}VwMH}>zT)*0jKk6G&(E-HL0U17LPgo|Ynopb<;GduDBG6(uk!9hk5XKt8?gaXJ$ zh#+p@sKEkJLW`@~c46*%oPgmcwxg51DFa3LTlCjHbNJ~aY`l)qFXRZUeoST_p}29+ zcnqm{?0cCH90TyFL*xbplOdU-@H9!A&=8>mlfXoS7fnCPSp|>o@{_iZ>h6FG_2IN5n0xw%T_6L8v%6hE~=&v_#mF*_@Jqvt6 zgLFtTp7#%XIbdj(oM$yO(Pipl25jBe%*cK#doH@V46dQ~iO|H!QI`~r&;`d;#WK`t zt8RZz@CK7ri{^qZwlZw`sy1`8S={+Aq5aJ=3PB6m@2F3Px)_X*#$6&$=w^l9^dmx2 z6FVvcDxWJmk{DZ1Y^^_0sJ_e9NrOrzG0lqmNjbQ>TM(A)co^Rb)c-OP9z?h=9D zUtlaj1yUN%u=R z>cj|w2WE}1c!M?E3oFJ;4oxFBZj76poGbdn=acrYbvoT2?MW+fyfQ`N)Zj8&*VGhd zW1(9HSQRsyI-Bt1yYOr6q{vCt!c*HP<+|j)tr-3+dNP?qFFj_vLu?YPnKddJ zH6NSMSvqJVP_=?BNB=nLXR#EqskT>r$1_YdK5XlFa(kTzR}^S&5N145Ur{$e(Vexc zn?T!5NNmk66?@97p()=_Vf7Yp>afKxe~tv{jTn|M2iSEGG6C!E2TAceA)qg_9f#;l?kVjR0(1?q z(->&F4IvsNQf1Z|LB!ILb;#$~#PvxWQUx5}--| z{<=ZsuX&{X+q}`;;Vor{W#^trPTw$_E+>@lH#-Fo@J8deqKT$ry$*JD&8MyT+=Y2+ zvcx$2a^O_y++l)l&PM1K^Po$eDVqN5Unh{GbHz-(PF)`)QmM{cH6rVA$@o--3A-I!u*Y#4g?0N=mg}MIy zIS@UdN&>gYg>@SRbT(|51@U5i>e@Q;x5}-xI2Wt2_=T#%P)9#yPj{;jV^NB92>sR{ z+9}6Xah{+ueKDnD+3c?%^yy5l>#R z9s6~r2Uq^`{&PQ~H8XgeX?u;7`Y3a;@;0hT2vjdAU_HMPO|E|@Bg`RuNO+KZBR>Le>q)Hv{0f zLt#c>*~}PDR!@ry|8cj)@Rg$%E3*qi?k zWuRntG*>lsLR?8cOK4TKmB<@eLO=`GU-am?f6v~f@?;=QD}U%JP*@~s{9oR z)I*kx+m5m25tf8-vDY0N?dLa^iE`h{d+v)^nzD$;cbaVEHhZ8;qonK#=)BA-aU=|Q zzeznQGh@}%aKG0|9Q+X`D(6)NH+zRW&4aCXdoH4s`wY$8)O;I!U2`swezwM)2DEa6 zYPjRN9;ehxaF&tbpfqBPXIT;(A(+Gct!pdcsLpj>t+YRT9OKWsJ)s91&1VOOn2`d@ zXLdw?wNfnYhV9nr+Lf_b(x?_h=WpG()cPS=rdntN*PWg)c+bloyU)eDc+Nds%!yi6 z%7rMcH~y6Dm8;}be|a|-r=?$|=|XZEP948QZl2-B)zyV8GG7y6kAw8WRAYtsT; zN-@zx*iBJ6B}TDxwKRngfqvymdaFv$$vS;-4s5%3a8*;Zo^~Jbf&^T`7+BR$K@%Mx zI637Q9mgG!?G_n7Kvs#GP97SnbI44={PdKrW{SjGQJ;2T|#FOj1!?+7k+-7fMsHif8qh=hY8b5=FP!QgYLBdFbX-$xCfH-A@)B3*z z$exf07^LSvl}AiOC;CwWPs=YL9Y$`sK=pR{?x1_j7i%Bf*Y`WVr9N-y1%v_lUT2e+ zJsj5p$^(k)3(JYWvn@U;#=1fFf5qZ{MgUhug6kX`-R0a$^l|`0DQr%V-*pkD_=H+X zI$dyYMB-qXYy4i;*(U3@ABdA72EV5xIi;SB1l&^h!6b;Evo}d8`puoWSs*&bkEbmj zqhmXN?q46b9Kx<*Qor-)K4CaN%&^hYyB{e^Jt{GQ+!g2(^GaqL+C!)0jm?~8PCt8G zH1$R0vIY~A^iGZo&&z?=@bj7Ht_$AmxcF)ZmlO42hf%eh_@%JwrOur76nbA@y|Jg_ z9=mVf#m3fBlqN7&;AgwaSGad>%uw9qQH!R8^6RUJV~*?X5>AV<@J`C^IgMphaXYUm zCE-pzn|2Kt@970Yc9#fI%f2R_moh}v&Poyq6sEy6a%FYi-mv(wSu%wg-*8PK;v9-W zTpID0CyJIBQnzYbtyF2&Mh~MJ@9V;mA_l05G?{d!x~RuA*$!mm5+x8#zN^9$;}E|w zMeCNKX(&|5vaZ~W`=9s%J$o0sgZ_jHplx~HfnSJtn}`4WsE8N!!bLi_Z%qG*ZGW~E zQ(91LTa!RJM^><;i&(BxQXOlpj+7T6I!R^wX&Qgo#Cupat zQZ7qX5}`XZwH~7ipEv9!0%}iYQd{5LHg4@4md0JPtH`X2xdvm?8ansc`ie;cXWgUS z6f@NMDHw?V?3@m{H#c>)1{Z}&5jRg|_xunGD5KLMOx->l6W-JdCI9iE;A^KVFjpdn&SBrx!=0OVgwVJpK7aeo^#TRruU z0G#K-^pzQ4@k^)~?n%Y(J(E8t094xkwVJFdE3bN^tRWymv{m;pF@&{Zl)2{e0INe} zlyGTR$fv;~QeJVI{7PTiH=14;Zio!ZOnNVNI^4VM&IkuzsVhsS`uel>=F%CIB+2$U zgn!1gtv>+;e)Y4{i7Fty`%?Gf3cK!1yEk`h0A=n3e6zrX^lO2EcWuF|4xB_Si%{)K z+GNQJ*>l_JE^k+9FYCuwH1^K{ZE zy4V7EZ6x4#s4Q^LYf^>5n^N6D+pW07kl+-3}5lMXJiCpkjJDl#D7%{Cm>d9at%^B9sQTGijR9VGO71h%z8}58I z-T1f^+oi1A&Yg(LWM@-m6n?fnYQvZ;-j#2u*BP|VTEaVw;nU)NG-xs+Y99A#B^VrX z6E+lw2k$Bl1CfzpKk{mov0%;~r;DoBdP za%munpi_p1!txv#SKlI|J?#eIWN2N|-i8=SRNqLrtA;wn^UG2WmF*VE5F&P@qCjlenIBrATF?CT#3?n=c(Mtjk$1vd|XZ%G~E2O8G4&h#e z9S~I%i>jihh9a5F$$E?;iQdNX$0RRk905QFTA%;qi)jk1qpUbDPHO*4JJ=41^ZgBQB>)drW;$9!*P7~w; zt1moz>V8A0v38)RaX>H-zPi-|pqyf?(yuR)+{)^U7v1M63nQ4dw6nFY;`dFuZW6>c z+S4iQWDQF3z2`hsCVj*_r<15T9OmxaVY{TPj%*}hFqtVA0|pQSE29Xk$4nHVkt#ay zb+l&Iqq@zD3&c|aR1M>{3fCI5un&f5osrY!#+&YFMFkuUx?|?7B6b!cL6KUefX2ab zI^pRVKQMjRx=Rad3^T(zWsUD3AdYkJK&2UFW)$8#FOXw(>l zK1@1bp}bX@c-hW+L3=&6(ZwzChsO?E64a4+sT`5SYzot?NnWBJvoB7(c6ck<{p8*J zdI#iTS|*R5KmFRHE#S;^w_R8?NM`GA$>HQGDU$RCIMb?RkgUHK?qbwL4SDElf=hX| z829!U&89+mMCy645e9L_-IV96IM(RIK&6ee-fpAIrZ>w(7lH}DE&K+D+h4Ry403^0 zN>f$&;@CDuj<3;+OG7sE!dRg#P-CU+d4}R7{=$D67&2d9ut)5dEGm&U&g&)w`wLcAM&tE~$Ap`k<6?cys{ znzfu`jU3e1eb+N}Un`Hl;V`OiuJIk|m|(q}n6!oaBwlw4rM?WMyf}wECjbX2)kex1 ztTW>5j|L~`q+#BbQfcIxzo!QSRKkjUA_E=d4bn#cM#k_L2-r*J{NJ%6yJ)Nd6!Pjf zyj|(1o10p3Q%(dG5A|>#x;;#Wj6pB-_Pn^L+nu?bwOoF>q{(M24<*1OFA;HsJJDWo zgf*a~CSetUrYQ(MZi*zFeh0p|pTj~z2`w8al7zSdra6VG3P|FhKcr3$4lU!+$$YX`9WfRt{;$!v;-!r=6MGPkV$Tm+M&ds+19Ix zeCqyIe`5{pRmgreAPV=(%TsBS?*mu^0kn?F{L7J&CFjnDsxq&&0kh)-T%2%m9yVKg z@%NG9dh5$(1tjo`%m=JL`>=2AOV3k|=|zQQ+8NyD30a*59K8+CP1p#oDB-#$Yd&;` zy8E809{L-HAuf-bRC1u8Pqr>=ZOw*UR~|jrc0}yES;4#JqsE61`rA~^C^UX3e*>S| zo8h^QYL9^*)*>!>I>liX7ONew_39j80%soQ=Ux?yU5eAoYUed_8!*YB_epGPcTI5n z1y9>6x1&D9?7hKFVM%uE8y9X&S8((heLdbcs6c`004uDpO>B^F1=d zVFE46TF`z0jmlZ0u-i76IDV$(B)*qhQ0rTM$RX%O`vhoS4#{u&qYj4ny0lLA(;mvr^L8ovGwg|j(ImcKM;&JZ z;F~D%8GC_1e?a}9ruaQhm7V_;%l|cP05Ulpn#F*Sfq@O%W^a*-k znOzK$Cj7asL>ci*no2D|2&YQ;uFwJ^Z4Eh@aQCTd{5wS69gD>j4`1G zTi$^>AtLWKRi3uL@vmEzmHZQvPAR%5fz#Orr%-MkejHZ+PblYQDwZ@NK_|Rjt+GOY zmzC6ij6=R?jk-$rqm4sGq(ogAMO&p!T`Q?_uUfdREtM%L?*qj2#g1s3Gc*_`V(zAqB;gjba=?7DC^&+;q z_*AQRVmFSj7eS2RY8naE%H)O9>|qX26*uH}c0`$){UD(s+Nf+k!K;)>4>^)6Rkt)z zqDAm4mMs-d(|W%7lxSzo9yEU{)Z7&8%WBA@94d2eP6Ux~(D(QJ{(%u%2 z1a~VEYE8v=96B5V5RO6Zw#J)&n5y|ilz66hJ+rqD#=&&asNtgnsxKb|XWo_ZB6ibMLrlJa5ys3kga@_)IT<<2Go0u?iorUEq) zTZ-#&3?R@!0fYeT=2!AngG6mdPsNSu>lzA(h@Of2FB{HKBFmg`c3J^=DeW;PG4)uhJZ9Gq`J?Pew#kq(2@PJ#}c0tYMn9wlB3FVwb?8(7(cO z`Fo^-CWKHapYqe8Kg(kuAoQC7H^4<7rGma!QoIt#{WHnJi}2>Jr_ct(qF2Rj;u-4C zj~MskY!LJ39mmCW!84W(!+P~1oOXOrug0(uT!LF+pFrrSUH5X?w9>VgV!1-J z3tQj(05t3D^!8<6?bfs6JCunmL*Y&WZZ$Jr!2}#iXdh+*;im$OOUE}T-VdZZ+1tz% z9A!eSED>=JzXog8SAr=#4E|6nU9e_i<_TbA&g)unUhOC)404_8i36e>TcycYpw!&j zP%e}1a7%HgExeLs-PClyci)Ql%HNxqq4DX%>fAyF%JP!v=J%LS?<8}<7yyfoIPkt5 zY0IMHm-tyM5kLK?aurwqdeAgk7wxt;TuF#|gf&8Q~axM?RF77bZ`pK2(j zzCO6J+`h8|howI6tB$Aj*`wU5_UVL`MZ78d8lB!jL#%GTHS7W_hI(OuM4_0Y zUaeUNUo;zG_NrcDaWo%FcO50})Nm}s-vUB%zqCZtb@k&rOZ0P#8SR!WNmK@#QG_7i z*d1lXf*xk>l(@6okVQpvT{_6P6a&E8Ok)PjElWBj#&Qw>wx z24sph2ET@0O!aPDt};dmYi^Y(PvMHsbWfMeHhj9RmOREMzB4#C3kg8!_u zn%RCcv&owzR9oBFjdA!D!8ThGCAJDTj@kZrdon$d0^|K@K?kLOJZ=&j)eZrF!@y?h zK1h+A!25v0ROLT)N^Re;=`!7UX%qGU1DNPV%s(>riR9jkaCs^8L_GXn(J`Qr>*exdytN1ER6|q~S%nD*!b(dwNlyi&%vv_Gc)teUjwvBEwH>bnZ z-tRwYX%u}zB3)CvC*mRB`{7z1mkp)Qh|e5Ux3h(3_Il_LsGO z#F%r8>pIWluo6R$)ao2ZIX$6&7Ju@Wi=bun;x|R=!`w1uVR;h;*Gmo5Yue6%wEDXp zV?oQnh!>s@@ITn>@q!e<83D<#wz@G>Owz4W#E7(7>ud%6L4EC&r+M=rC&(9&sjwQz ze2n0(=!L25n_u!Lo}o;$yj75|QuHTXDH$P_=T0QKG~!cU zOZg+w_SxevY5qx?%0V-@yjmRp9_n71l(k#jE-Y_JG{m?_4@ucsP1`S^;D&jlofMV{ z;vuPgp5eCou%KHnIidN4RXze6X9m6{CEI*xo6A(Zby(Z+9|oL%tv$=ffy3l zVo@+c)>O?B7r+?8?P?oxLH6p1S6ikb*Y%M@K%eFY^^7 zg+Zs-tKjSY&Mk674(6=H4dcai{iX`VG2LGgHN{@mz8J98>^&{r|AcKqd5o%xL9q7$*a^UiBP4Sb_=-cV0pRCM&NqIZi zK;C%=_OK3yJhplXY@wbIZmBYoJ+{$|nrMV@pwMCf_a!tic}bpQSeGRI?^F#>{U?Iu z8_d}%uuTeLua1H&X@-&iH8Q(YR-_R;Q zuRPAYpmo<(+h2Es_gld&4;teQ<$&Jgwyd7>GU4LNUr2M|M1e$iSnpCG{6e7dnn~=h zSxZJ{lU2{NpA`E<4xINq^zM{%3Y7qk9*{?N61b_Qil%CX)_MuQzmn4u{j_yZt8mZ1 zmUIYY?N}(Ac2v!UiJU=2E@=n)af1pZQNL!lx_`fRY?1j6dE`u7$ZYK%)pER2SG_Xr z5ZlO)`3-C>zn9e{26lWt>fkIc?3h(ehXV_tLHN{>^faaf*CF>ntN5UQ-?C%zAUJ!s z`WglyfZch++VcEMQSiYz(qX2`Ce>Rkz52TnDnDtUaT-uld+(&kF{GZ=X32QtNdu{xD0*kjJmaEWew2T6o z>nw~9KkDWzDG@!YlSx4_PRKXkJqR+gaImIsywR?VC)8BQ`L}5I%j!&R9q)(4`b_R= zewqTTEjROU#|b)ZWc;aOhxT188iyhD`o!cZIfeoi&v>o;CL2Y1Y|n~gv=b**yY}~p z1k`z)2Tq(DudcMOjT5Rt+q! zR-V;`yGoz6kyOASbUM8f#00L&DcbQ=xgt)L3DG8V+5h@OB`DJ+Mr;dc@|2%z=w!Ne zFT5A3hOCbI6C$OQHhkAOR%G0Qd(BW+J2z!4O@5(ibsrwi$!f7Q3zKsteScn~p!JC3 zmcQvBx8oc-&XQFwpzAA0G91m~{mhR)8OSNOn!-!FIV-W#!mMS3Yx66BeRL6CCq@9= zw^3krFAdS`-oQF zm-?8B-aP|ql=i}}%zJmT90>&@DkIOEaB&p;4e->o;cL~2-0ASe{QSGiQorvs_vBq? zVg#WC*n(KG%bnl~|FmfdJUrik^%ciw0{ayipQ;gHs8JFuqbeOuhRH`c<2dMR-@Bj` z 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%0DsY&N>rvU-or`eF_woe(Luk5TU+6@1r%Pe#3 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 fmt::Result { + unimplemented!() + } + } + + impl Display for MyError { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { + unimplemented!() + } + } + + impl Error for MyError { + fn provide<'a>(&'a self, request: &mut Request<'a>) { + request.provide_ref(&self.0); + } + } +"#; + +fn main() { + match compile_probe() { + Some(status) if status.success() => println!("cargo:rustc-cfg=error_generic_member_access"), + _ => {} + } +} + +fn compile_probe() -> Option { + if env::var_os("RUSTC_STAGE").is_some() { + // We are running inside rustc bootstrap. This is a highly non-standard + // environment with issues such as: + // + // https://github.com/rust-lang/cargo/issues/11138 + // https://github.com/rust-lang/rust/issues/114839 + // + // Let's just not use nightly features here. + return None; + } + + let rustc = env::var_os("RUSTC")?; + let out_dir = env::var_os("OUT_DIR")?; + let probefile = Path::new(&out_dir).join("probe.rs"); + fs::write(&probefile, PROBE).ok()?; + + // Make sure to pick up Cargo rustc configuration. + let mut cmd = if let Some(wrapper) = env::var_os("RUSTC_WRAPPER") { + let mut cmd = Command::new(wrapper); + // The wrapper's first argument is supposed to be the path to rustc. + cmd.arg(rustc); + cmd + } else { + Command::new(rustc) + }; + + cmd.stderr(Stdio::null()) + .arg("--edition=2018") + .arg("--crate-name=xet_error_build") + .arg("--crate-type=lib") + .arg("--emit=metadata") + .arg("--out-dir") + .arg(out_dir) + .arg(probefile); + + if let Some(target) = env::var_os("TARGET") { + cmd.arg("--target").arg(target); + } + + // If Cargo wants to set RUSTFLAGS, use that. + if let Ok(rustflags) = env::var("CARGO_ENCODED_RUSTFLAGS") { + if !rustflags.is_empty() { + for arg in rustflags.split('\x1f') { + cmd.arg(arg); + } + } + } + + cmd.status().ok() +} diff --git a/xet_error/impl/Cargo.toml b/xet_error/impl/Cargo.toml new file mode 100644 index 00000000..fc76bded --- /dev/null +++ b/xet_error/impl/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "xet-error-impl" +version = "1.0.50" +authors = ["David Tolnay "] +description = "Implementation detail of the `thiserror` crate" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/dtolnay/thiserror" +rust-version = "1.56" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.63" +quote = "1.0.29" +syn = "2.0.23" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = ["--generate-link-to-definition"] diff --git a/xet_error/impl/LICENSE-APACHE b/xet_error/impl/LICENSE-APACHE new file mode 120000 index 00000000..965b606f --- /dev/null +++ b/xet_error/impl/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/xet_error/impl/LICENSE-MIT b/xet_error/impl/LICENSE-MIT new file mode 120000 index 00000000..76219eb7 --- /dev/null +++ b/xet_error/impl/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/xet_error/impl/src/ast.rs b/xet_error/impl/src/ast.rs new file mode 100644 index 00000000..674fea70 --- /dev/null +++ b/xet_error/impl/src/ast.rs @@ -0,0 +1,165 @@ +use crate::attr::{self, Attrs}; +use crate::generics::ParamsInScope; +use proc_macro2::Span; +use syn::{ + Data, DataEnum, DataStruct, DeriveInput, Error, Fields, Generics, Ident, Index, Member, Result, + Type, +}; + +pub enum Input<'a> { + Struct(Struct<'a>), + Enum(Enum<'a>), +} + +pub struct Struct<'a> { + pub original: &'a DeriveInput, + pub attrs: Attrs<'a>, + pub ident: Ident, + pub generics: &'a Generics, + pub fields: Vec>, +} + +pub struct Enum<'a> { + pub original: &'a DeriveInput, + pub attrs: Attrs<'a>, + pub ident: Ident, + pub generics: &'a Generics, + pub variants: Vec>, +} + +pub struct Variant<'a> { + pub original: &'a syn::Variant, + pub attrs: Attrs<'a>, + pub ident: Ident, + pub fields: Vec>, +} + +pub struct Field<'a> { + pub original: &'a syn::Field, + pub attrs: Attrs<'a>, + pub member: Member, + pub ty: &'a Type, + pub contains_generic: bool, +} + +impl<'a> Input<'a> { + pub fn from_syn(node: &'a DeriveInput) -> Result { + match &node.data { + Data::Struct(data) => Struct::from_syn(node, data).map(Input::Struct), + Data::Enum(data) => Enum::from_syn(node, data).map(Input::Enum), + Data::Union(_) => Err(Error::new_spanned( + node, + "union as errors are not supported", + )), + } + } +} + +impl<'a> Struct<'a> { + fn from_syn(node: &'a DeriveInput, data: &'a DataStruct) -> Result { + let mut attrs = attr::get(&node.attrs)?; + let scope = ParamsInScope::new(&node.generics); + let span = attrs.span().unwrap_or_else(Span::call_site); + let fields = Field::multiple_from_syn(&data.fields, &scope, span)?; + if let Some(display) = &mut attrs.display { + display.expand_shorthand(&fields); + } + Ok(Struct { + original: node, + attrs, + ident: node.ident.clone(), + generics: &node.generics, + fields, + }) + } +} + +impl<'a> Enum<'a> { + fn from_syn(node: &'a DeriveInput, data: &'a DataEnum) -> Result { + let attrs = attr::get(&node.attrs)?; + let scope = ParamsInScope::new(&node.generics); + let span = attrs.span().unwrap_or_else(Span::call_site); + let variants = data + .variants + .iter() + .map(|node| { + let mut variant = Variant::from_syn(node, &scope, span)?; + if let display @ None = &mut variant.attrs.display { + display.clone_from(&attrs.display); + } + if let Some(display) = &mut variant.attrs.display { + display.expand_shorthand(&variant.fields); + } else if variant.attrs.transparent.is_none() { + variant.attrs.transparent = attrs.transparent; + } + Ok(variant) + }) + .collect::>()?; + Ok(Enum { + original: node, + attrs, + ident: node.ident.clone(), + generics: &node.generics, + variants, + }) + } +} + +impl<'a> Variant<'a> { + fn from_syn(node: &'a syn::Variant, scope: &ParamsInScope<'a>, span: Span) -> Result { + let attrs = attr::get(&node.attrs)?; + let span = attrs.span().unwrap_or(span); + Ok(Variant { + original: node, + attrs, + ident: node.ident.clone(), + fields: Field::multiple_from_syn(&node.fields, scope, span)?, + }) + } +} + +impl<'a> Field<'a> { + fn multiple_from_syn( + fields: &'a Fields, + scope: &ParamsInScope<'a>, + span: Span, + ) -> Result> { + fields + .iter() + .enumerate() + .map(|(i, field)| Field::from_syn(i, field, scope, span)) + .collect() + } + + fn from_syn( + i: usize, + node: &'a syn::Field, + scope: &ParamsInScope<'a>, + span: Span, + ) -> Result { + Ok(Field { + original: node, + attrs: attr::get(&node.attrs)?, + member: node.ident.clone().map(Member::Named).unwrap_or_else(|| { + Member::Unnamed(Index { + index: i as u32, + span, + }) + }), + ty: &node.ty, + contains_generic: scope.intersects(&node.ty), + }) + } +} + +impl Attrs<'_> { + pub fn span(&self) -> Option { + if let Some(display) = &self.display { + Some(display.fmt.span()) + } else if let Some(transparent) = &self.transparent { + Some(transparent.span) + } else { + None + } + } +} diff --git a/xet_error/impl/src/attr.rs b/xet_error/impl/src/attr.rs new file mode 100644 index 00000000..4beb8c96 --- /dev/null +++ b/xet_error/impl/src/attr.rs @@ -0,0 +1,210 @@ +use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree}; +use quote::{format_ident, quote, ToTokens}; +use std::collections::BTreeSet as Set; +use syn::parse::ParseStream; +use syn::{ + braced, bracketed, parenthesized, token, Attribute, Error, Ident, Index, LitInt, LitStr, Meta, + Result, Token, +}; + +pub struct Attrs<'a> { + pub display: Option>, + pub source: Option<&'a Attribute>, + pub backtrace: Option<&'a Attribute>, + pub from: Option<&'a Attribute>, + pub transparent: Option>, +} + +#[derive(Clone)] +pub struct Display<'a> { + pub original: &'a Attribute, + pub fmt: LitStr, + pub args: TokenStream, + pub has_bonus_display: bool, + pub implied_bounds: Set<(usize, Trait)>, +} + +#[derive(Copy, Clone)] +pub struct Transparent<'a> { + pub original: &'a Attribute, + pub span: Span, +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub enum Trait { + Debug, + Display, + Octal, + LowerHex, + UpperHex, + Pointer, + Binary, + LowerExp, + UpperExp, +} + +pub fn get(input: &[Attribute]) -> Result { + let mut attrs = Attrs { + display: None, + source: None, + backtrace: None, + from: None, + transparent: None, + }; + + for attr in input { + if attr.path().is_ident("error") { + parse_error_attribute(&mut attrs, attr)?; + } else if attr.path().is_ident("source") { + attr.meta.require_path_only()?; + if attrs.source.is_some() { + return Err(Error::new_spanned(attr, "duplicate #[source] attribute")); + } + attrs.source = Some(attr); + } else if attr.path().is_ident("backtrace") { + attr.meta.require_path_only()?; + if attrs.backtrace.is_some() { + return Err(Error::new_spanned(attr, "duplicate #[backtrace] attribute")); + } + attrs.backtrace = Some(attr); + } else if attr.path().is_ident("from") { + match attr.meta { + Meta::Path(_) => {} + Meta::List(_) | Meta::NameValue(_) => { + // Assume this is meant for derive_more crate or something. + continue; + } + } + if attrs.from.is_some() { + return Err(Error::new_spanned(attr, "duplicate #[from] attribute")); + } + attrs.from = Some(attr); + } + } + + Ok(attrs) +} + +fn parse_error_attribute<'a>(attrs: &mut Attrs<'a>, attr: &'a Attribute) -> Result<()> { + syn::custom_keyword!(transparent); + + attr.parse_args_with(|input: ParseStream| { + if let Some(kw) = input.parse::>()? { + if attrs.transparent.is_some() { + return Err(Error::new_spanned( + attr, + "duplicate #[error(transparent)] attribute", + )); + } + attrs.transparent = Some(Transparent { + original: attr, + span: kw.span, + }); + return Ok(()); + } + + let display = Display { + original: attr, + fmt: input.parse()?, + args: parse_token_expr(input, false)?, + has_bonus_display: false, + implied_bounds: Set::new(), + }; + if attrs.display.is_some() { + return Err(Error::new_spanned( + attr, + "only one #[error(...)] attribute is allowed", + )); + } + attrs.display = Some(display); + Ok(()) + }) +} + +fn parse_token_expr(input: ParseStream, mut begin_expr: bool) -> Result { + let mut tokens = Vec::new(); + while !input.is_empty() { + if begin_expr && input.peek(Token![.]) { + if input.peek2(Ident) { + input.parse::()?; + begin_expr = false; + continue; + } + if input.peek2(LitInt) { + input.parse::()?; + let int: Index = input.parse()?; + let ident = format_ident!("_{}", int.index, span = int.span); + tokens.push(TokenTree::Ident(ident)); + begin_expr = false; + continue; + } + } + + begin_expr = input.peek(Token![break]) + || input.peek(Token![continue]) + || input.peek(Token![if]) + || input.peek(Token![in]) + || input.peek(Token![match]) + || input.peek(Token![mut]) + || input.peek(Token![return]) + || input.peek(Token![while]) + || input.peek(Token![+]) + || input.peek(Token![&]) + || input.peek(Token![!]) + || input.peek(Token![^]) + || input.peek(Token![,]) + || input.peek(Token![/]) + || input.peek(Token![=]) + || input.peek(Token![>]) + || input.peek(Token![<]) + || input.peek(Token![|]) + || input.peek(Token![%]) + || input.peek(Token![;]) + || input.peek(Token![*]) + || input.peek(Token![-]); + + let token: TokenTree = if input.peek(token::Paren) { + let content; + let delimiter = parenthesized!(content in input); + let nested = parse_token_expr(&content, true)?; + let mut group = Group::new(Delimiter::Parenthesis, nested); + group.set_span(delimiter.span.join()); + TokenTree::Group(group) + } else if input.peek(token::Brace) { + let content; + let delimiter = braced!(content in input); + let nested = parse_token_expr(&content, true)?; + let mut group = Group::new(Delimiter::Brace, nested); + group.set_span(delimiter.span.join()); + TokenTree::Group(group) + } else if input.peek(token::Bracket) { + let content; + let delimiter = bracketed!(content in input); + let nested = parse_token_expr(&content, true)?; + let mut group = Group::new(Delimiter::Bracket, nested); + group.set_span(delimiter.span.join()); + TokenTree::Group(group) + } else { + input.parse()? + }; + tokens.push(token); + } + Ok(TokenStream::from_iter(tokens)) +} + +impl ToTokens for Display<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let fmt = &self.fmt; + let args = &self.args; + tokens.extend(quote! { + ::core::write!(__formatter, #fmt #args) + }); + } +} + +impl ToTokens for Trait { + fn to_tokens(&self, tokens: &mut TokenStream) { + let trait_name = format_ident!("{}", format!("{:?}", self)); + tokens.extend(quote!(::core::fmt::#trait_name)); + } +} diff --git a/xet_error/impl/src/expand.rs b/xet_error/impl/src/expand.rs new file mode 100644 index 00000000..26522eb3 --- /dev/null +++ b/xet_error/impl/src/expand.rs @@ -0,0 +1,551 @@ +use crate::ast::{Enum, Field, Input, Struct}; +use crate::attr::Trait; +use crate::generics::InferredBounds; +use crate::span::MemberSpan; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use std::collections::BTreeSet as Set; +use syn::{ + Data, DeriveInput, GenericArgument, Member, PathArguments, Result, Token, Type, Visibility, +}; + +pub fn derive(node: &DeriveInput) -> Result { + let input = Input::from_syn(node)?; + input.validate()?; + Ok(match input { + Input::Struct(input) => impl_struct(input), + Input::Enum(input) => impl_enum(input), + }) +} + +fn impl_struct(input: Struct) -> TokenStream { + let ty = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let mut error_inferred_bounds = InferredBounds::new(); + + let source_body = if let Some(transparent_attr) = &input.attrs.transparent { + let only_field = &input.fields[0]; + if only_field.contains_generic { + error_inferred_bounds.insert(only_field.ty, quote!(std::error::Error)); + } + let member = &only_field.member; + Some(quote_spanned! {transparent_attr.span=> + std::error::Error::source(self.#member.as_dyn_error()) + }) + } else if let Some(source_field) = input.source_field() { + let source = &source_field.member; + if source_field.contains_generic { + let ty = unoptional_type(source_field.ty); + error_inferred_bounds.insert(ty, quote!(std::error::Error + 'static)); + } + let asref = if type_is_option(source_field.ty) { + Some(quote_spanned!(source.member_span()=> .as_ref()?)) + } else { + None + }; + let dyn_error = quote_spanned! {source_field.source_span()=> + self.#source #asref.as_dyn_error() + }; + Some(quote! { + ::core::option::Option::Some(#dyn_error) + }) + } else { + None + }; + let source_method = source_body.map(|body| { + quote! { + fn source(&self) -> ::core::option::Option<&(dyn std::error::Error + 'static)> { + use xet_error::__private::AsDynError; + #body + } + } + }); + + let provide_method = input.backtrace_field().map(|backtrace_field| { + let request = quote!(request); + let backtrace = &backtrace_field.member; + let body = if let Some(source_field) = input.source_field() { + let source = &source_field.member; + let source_provide = if type_is_option(source_field.ty) { + quote_spanned! {source.member_span()=> + if let ::core::option::Option::Some(source) = &self.#source { + source.xet_error_provide(#request); + } + } + } else { + quote_spanned! {source.member_span()=> + self.#source.xet_error_provide(#request); + } + }; + let self_provide = if source == backtrace { + None + } else if type_is_option(backtrace_field.ty) { + Some(quote! { + if let ::core::option::Option::Some(backtrace) = &self.#backtrace { + #request.provide_ref::(backtrace); + } + }) + } else { + Some(quote! { + #request.provide_ref::(&self.#backtrace); + }) + }; + quote! { + use xet_error::__private::ThiserrorProvide; + #source_provide + #self_provide + } + } else if type_is_option(backtrace_field.ty) { + quote! { + if let ::core::option::Option::Some(backtrace) = &self.#backtrace { + #request.provide_ref::(backtrace); + } + } + } else { + quote! { + #request.provide_ref::(&self.#backtrace); + } + }; + quote! { + fn provide<'_request>(&'_request self, #request: &mut std::error::Request<'_request>) { + #body + } + } + }); + + let mut display_implied_bounds = Set::new(); + let display_body = if input.attrs.transparent.is_some() { + let only_field = &input.fields[0].member; + display_implied_bounds.insert((0, Trait::Display)); + Some(quote! { + ::core::fmt::Display::fmt(&self.#only_field, __formatter) + }) + } else if let Some(display) = &input.attrs.display { + display_implied_bounds.clone_from(&display.implied_bounds); + let use_as_display = use_as_display(display.has_bonus_display); + let pat = fields_pat(&input.fields); + Some(quote! { + #use_as_display + #[allow(unused_variables, deprecated)] + let Self #pat = self; + #display + }) + } else { + None + }; + let display_impl = display_body.map(|body| { + let mut display_inferred_bounds = InferredBounds::new(); + for (field, bound) in display_implied_bounds { + let field = &input.fields[field]; + if field.contains_generic { + display_inferred_bounds.insert(field.ty, bound); + } + } + let display_where_clause = display_inferred_bounds.augment_where_clause(input.generics); + quote! { + #[allow(unused_qualifications)] + impl #impl_generics ::core::fmt::Display for #ty #ty_generics #display_where_clause { + #[allow(clippy::used_underscore_binding)] + fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + #body + } + } + } + }); + + let from_impl = input.from_field().map(|from_field| { + let backtrace_field = input.distinct_backtrace_field(); + let from = unoptional_type(from_field.ty); + let body = from_initializer(from_field, backtrace_field); + quote! { + #[allow(unused_qualifications)] + impl #impl_generics ::core::convert::From<#from> for #ty #ty_generics #where_clause { + #[allow(deprecated)] + fn from(source: #from) -> Self { + xet_error::error_hook(&format!("{source:?}")); + #ty #body + } + } + } + }); + + let error_trait = spanned_error_trait(input.original); + if input.generics.type_params().next().is_some() { + let self_token = ::default(); + error_inferred_bounds.insert(self_token, Trait::Debug); + error_inferred_bounds.insert(self_token, Trait::Display); + } + let error_where_clause = error_inferred_bounds.augment_where_clause(input.generics); + + quote! { + #[allow(unused_qualifications)] + impl #impl_generics #error_trait for #ty #ty_generics #error_where_clause { + #source_method + #provide_method + } + #display_impl + #from_impl + } +} + +fn impl_enum(input: Enum) -> TokenStream { + let ty = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let mut error_inferred_bounds = InferredBounds::new(); + + let source_method = if input.has_source() { + let arms = input.variants.iter().map(|variant| { + let ident = &variant.ident; + if let Some(transparent_attr) = &variant.attrs.transparent { + let only_field = &variant.fields[0]; + if only_field.contains_generic { + error_inferred_bounds.insert(only_field.ty, quote!(std::error::Error)); + } + let member = &only_field.member; + let source = quote_spanned! {transparent_attr.span=> + std::error::Error::source(transparent.as_dyn_error()) + }; + quote! { + #ty::#ident {#member: transparent} => #source, + } + } else if let Some(source_field) = variant.source_field() { + let source = &source_field.member; + if source_field.contains_generic { + let ty = unoptional_type(source_field.ty); + error_inferred_bounds.insert(ty, quote!(std::error::Error + 'static)); + } + let asref = if type_is_option(source_field.ty) { + Some(quote_spanned!(source.member_span()=> .as_ref()?)) + } else { + None + }; + let varsource = quote!(source); + let dyn_error = quote_spanned! {source_field.source_span()=> + #varsource #asref.as_dyn_error() + }; + quote! { + #ty::#ident {#source: #varsource, ..} => ::core::option::Option::Some(#dyn_error), + } + } else { + quote! { + #ty::#ident {..} => ::core::option::Option::None, + } + } + }); + Some(quote! { + fn source(&self) -> ::core::option::Option<&(dyn std::error::Error + 'static)> { + use xet_error::__private::AsDynError; + #[allow(deprecated)] + match self { + #(#arms)* + } + } + }) + } else { + None + }; + + let provide_method = if input.has_backtrace() { + let request = quote!(request); + let arms = input.variants.iter().map(|variant| { + let ident = &variant.ident; + match (variant.backtrace_field(), variant.source_field()) { + (Some(backtrace_field), Some(source_field)) + if backtrace_field.attrs.backtrace.is_none() => + { + let backtrace = &backtrace_field.member; + let source = &source_field.member; + let varsource = quote!(source); + let source_provide = if type_is_option(source_field.ty) { + quote_spanned! {source.member_span()=> + if let ::core::option::Option::Some(source) = #varsource { + source.xet_error_provide(#request); + } + } + } else { + quote_spanned! {source.member_span()=> + #varsource.xet_error_provide(#request); + } + }; + let self_provide = if type_is_option(backtrace_field.ty) { + quote! { + if let ::core::option::Option::Some(backtrace) = backtrace { + #request.provide_ref::(backtrace); + } + } + } else { + quote! { + #request.provide_ref::(backtrace); + } + }; + quote! { + #ty::#ident { + #backtrace: backtrace, + #source: #varsource, + .. + } => { + use xet_error::__private::ThiserrorProvide; + #source_provide + #self_provide + } + } + } + (Some(backtrace_field), Some(source_field)) + if backtrace_field.member == source_field.member => + { + let backtrace = &backtrace_field.member; + let varsource = quote!(source); + let source_provide = if type_is_option(source_field.ty) { + quote_spanned! {backtrace.member_span()=> + if let ::core::option::Option::Some(source) = #varsource { + source.xet_error_provide(#request); + } + } + } else { + quote_spanned! {backtrace.member_span()=> + #varsource.xet_error_provide(#request); + } + }; + quote! { + #ty::#ident {#backtrace: #varsource, ..} => { + use xet_error::__private::ThiserrorProvide; + #source_provide + } + } + } + (Some(backtrace_field), _) => { + let backtrace = &backtrace_field.member; + let body = if type_is_option(backtrace_field.ty) { + quote! { + if let ::core::option::Option::Some(backtrace) = backtrace { + #request.provide_ref::(backtrace); + } + } + } else { + quote! { + #request.provide_ref::(backtrace); + } + }; + quote! { + #ty::#ident {#backtrace: backtrace, ..} => { + #body + } + } + } + (None, _) => quote! { + #ty::#ident {..} => {} + }, + } + }); + Some(quote! { + fn provide<'_request>(&'_request self, #request: &mut std::error::Request<'_request>) { + #[allow(deprecated)] + match self { + #(#arms)* + } + } + }) + } else { + None + }; + + let display_impl = if input.has_display() { + let mut display_inferred_bounds = InferredBounds::new(); + let has_bonus_display = input.variants.iter().any(|v| { + v.attrs + .display + .as_ref() + .map_or(false, |display| display.has_bonus_display) + }); + let use_as_display = use_as_display(has_bonus_display); + let void_deref = if input.variants.is_empty() { + Some(quote!(*)) + } else { + None + }; + let arms = input.variants.iter().map(|variant| { + let mut display_implied_bounds = Set::new(); + let display = match &variant.attrs.display { + Some(display) => { + display_implied_bounds.clone_from(&display.implied_bounds); + display.to_token_stream() + } + None => { + let only_field = match &variant.fields[0].member { + Member::Named(ident) => ident.clone(), + Member::Unnamed(index) => format_ident!("_{}", index), + }; + display_implied_bounds.insert((0, Trait::Display)); + quote!(::core::fmt::Display::fmt(#only_field, __formatter)) + } + }; + for (field, bound) in display_implied_bounds { + let field = &variant.fields[field]; + if field.contains_generic { + display_inferred_bounds.insert(field.ty, bound); + } + } + let ident = &variant.ident; + let pat = fields_pat(&variant.fields); + quote! { + #ty::#ident #pat => #display + } + }); + let arms = arms.collect::>(); + let display_where_clause = display_inferred_bounds.augment_where_clause(input.generics); + Some(quote! { + #[allow(unused_qualifications)] + impl #impl_generics ::core::fmt::Display for #ty #ty_generics #display_where_clause { + fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + #use_as_display + #[allow(unused_variables, deprecated, clippy::used_underscore_binding)] + match #void_deref self { + #(#arms,)* + } + } + } + }) + } else { + None + }; + + let from_impls = input.variants.iter().filter_map(|variant| { + let from_field = variant.from_field()?; + let backtrace_field = variant.distinct_backtrace_field(); + let variant = &variant.ident; + let from = unoptional_type(from_field.ty); + let body = from_initializer(from_field, backtrace_field); + Some(quote! { + #[allow(unused_qualifications)] + impl #impl_generics ::core::convert::From<#from> for #ty #ty_generics #where_clause { + + #[allow(deprecated)] + fn from(source: #from) -> Self { + xet_error::error_hook(&format!("{source:?}")); + #ty::#variant #body + } + } + }) + }); + + let error_trait = spanned_error_trait(input.original); + if input.generics.type_params().next().is_some() { + let self_token = ::default(); + error_inferred_bounds.insert(self_token, Trait::Debug); + error_inferred_bounds.insert(self_token, Trait::Display); + } + let error_where_clause = error_inferred_bounds.augment_where_clause(input.generics); + + quote! { + #[allow(unused_qualifications)] + impl #impl_generics #error_trait for #ty #ty_generics #error_where_clause { + #source_method + #provide_method + } + #display_impl + #(#from_impls)* + } +} + +fn fields_pat(fields: &[Field]) -> TokenStream { + let mut members = fields.iter().map(|field| &field.member).peekable(); + match members.peek() { + Some(Member::Named(_)) => quote!({ #(#members),* }), + Some(Member::Unnamed(_)) => { + let vars = members.map(|member| match member { + Member::Unnamed(member) => format_ident!("_{}", member), + Member::Named(_) => unreachable!(), + }); + quote!((#(#vars),*)) + } + None => quote!({}), + } +} + +fn use_as_display(needs_as_display: bool) -> Option { + if needs_as_display { + Some(quote! { + use xet_error::__private::AsDisplay as _; + }) + } else { + None + } +} + +fn from_initializer(from_field: &Field, backtrace_field: Option<&Field>) -> TokenStream { + let from_member = &from_field.member; + let some_source = if type_is_option(from_field.ty) { + quote!(::core::option::Option::Some(source)) + } else { + quote!(source) + }; + let backtrace = backtrace_field.map(|backtrace_field| { + let backtrace_member = &backtrace_field.member; + if type_is_option(backtrace_field.ty) { + quote! { + #backtrace_member: ::core::option::Option::Some(std::backtrace::Backtrace::capture()), + } + } else { + quote! { + #backtrace_member: ::core::convert::From::from(std::backtrace::Backtrace::capture()), + } + } + }); + quote!({ + #from_member: #some_source, + #backtrace + }) +} + +fn type_is_option(ty: &Type) -> bool { + type_parameter_of_option(ty).is_some() +} + +fn unoptional_type(ty: &Type) -> TokenStream { + let unoptional = type_parameter_of_option(ty).unwrap_or(ty); + quote!(#unoptional) +} + +fn type_parameter_of_option(ty: &Type) -> Option<&Type> { + let path = match ty { + Type::Path(ty) => &ty.path, + _ => return None, + }; + + let last = path.segments.last().unwrap(); + if last.ident != "Option" { + return None; + } + + let bracketed = match &last.arguments { + PathArguments::AngleBracketed(bracketed) => bracketed, + _ => return None, + }; + + if bracketed.args.len() != 1 { + return None; + } + + match &bracketed.args[0] { + GenericArgument::Type(arg) => Some(arg), + _ => None, + } +} + +fn spanned_error_trait(input: &DeriveInput) -> TokenStream { + let vis_span = match &input.vis { + Visibility::Public(vis) => Some(vis.span), + Visibility::Restricted(vis) => Some(vis.pub_token.span), + Visibility::Inherited => None, + }; + let data_span = match &input.data { + Data::Struct(data) => data.struct_token.span, + Data::Enum(data) => data.enum_token.span, + Data::Union(data) => data.union_token.span, + }; + let first_span = vis_span.unwrap_or(data_span); + let last_span = input.ident.span(); + let path = quote_spanned!(first_span=> std::error::); + let error = quote_spanned!(last_span=> Error); + quote!(#path #error) +} diff --git a/xet_error/impl/src/fmt.rs b/xet_error/impl/src/fmt.rs new file mode 100644 index 00000000..807dfb96 --- /dev/null +++ b/xet_error/impl/src/fmt.rs @@ -0,0 +1,170 @@ +use crate::ast::Field; +use crate::attr::{Display, Trait}; +use proc_macro2::TokenTree; +use quote::{format_ident, quote_spanned}; +use std::collections::{BTreeSet as Set, HashMap as Map}; +use syn::ext::IdentExt; +use syn::parse::{ParseStream, Parser}; +use syn::{Ident, Index, LitStr, Member, Result, Token}; + +impl Display<'_> { + // Transform `"error {var}"` to `"error {}", var`. + pub fn expand_shorthand(&mut self, fields: &[Field]) { + let raw_args = self.args.clone(); + let mut named_args = explicit_named_args.parse2(raw_args).unwrap(); + let mut member_index = Map::new(); + for (i, field) in fields.iter().enumerate() { + member_index.insert(&field.member, i); + } + + let span = self.fmt.span(); + let fmt = self.fmt.value(); + let mut read = fmt.as_str(); + let mut out = String::new(); + let mut args = self.args.clone(); + let mut has_bonus_display = false; + let mut implied_bounds = Set::new(); + + let mut has_trailing_comma = false; + if let Some(TokenTree::Punct(punct)) = args.clone().into_iter().last() { + if punct.as_char() == ',' { + has_trailing_comma = true; + } + } + + while let Some(brace) = read.find('{') { + out += &read[..brace + 1]; + read = &read[brace + 1..]; + if read.starts_with('{') { + out.push('{'); + read = &read[1..]; + continue; + } + let next = match read.chars().next() { + Some(next) => next, + None => return, + }; + let member = match next { + '0'..='9' => { + let int = take_int(&mut read); + let member = match int.parse::() { + Ok(index) => Member::Unnamed(Index { index, span }), + Err(_) => return, + }; + if !member_index.contains_key(&member) { + out += ∫ + continue; + } + member + } + 'a'..='z' | 'A'..='Z' | '_' => { + let mut ident = take_ident(&mut read); + ident.set_span(span); + Member::Named(ident) + } + _ => continue, + }; + if let Some(&field) = member_index.get(&member) { + let end_spec = match read.find('}') { + Some(end_spec) => end_spec, + None => return, + }; + let bound = match read[..end_spec].chars().next_back() { + Some('?') => Trait::Debug, + Some('o') => Trait::Octal, + Some('x') => Trait::LowerHex, + Some('X') => Trait::UpperHex, + Some('p') => Trait::Pointer, + Some('b') => Trait::Binary, + Some('e') => Trait::LowerExp, + Some('E') => Trait::UpperExp, + Some(_) | None => Trait::Display, + }; + implied_bounds.insert((field, bound)); + } + let local = match &member { + Member::Unnamed(index) => format_ident!("_{}", index), + Member::Named(ident) => ident.clone(), + }; + let mut formatvar = local.clone(); + if formatvar.to_string().starts_with("r#") { + formatvar = format_ident!("r_{}", formatvar); + } + if formatvar.to_string().starts_with('_') { + // Work around leading underscore being rejected by 1.40 and + // older compilers. https://github.com/rust-lang/rust/pull/66847 + formatvar = format_ident!("field_{}", formatvar); + } + out += &formatvar.to_string(); + if !named_args.insert(formatvar.clone()) { + // Already specified in the format argument list. + continue; + } + if !has_trailing_comma { + args.extend(quote_spanned!(span=> ,)); + } + args.extend(quote_spanned!(span=> #formatvar = #local)); + if read.starts_with('}') && member_index.contains_key(&member) { + has_bonus_display = true; + args.extend(quote_spanned!(span=> .as_display())); + } + has_trailing_comma = false; + } + + out += read; + self.fmt = LitStr::new(&out, self.fmt.span()); + self.args = args; + self.has_bonus_display = has_bonus_display; + self.implied_bounds = implied_bounds; + } +} + +fn explicit_named_args(input: ParseStream) -> Result> { + let mut named_args = Set::new(); + + while !input.is_empty() { + if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) { + input.parse::()?; + let ident = input.call(Ident::parse_any)?; + input.parse::()?; + named_args.insert(ident); + } else { + input.parse::()?; + } + } + + Ok(named_args) +} + +fn take_int(read: &mut &str) -> String { + let mut int = String::new(); + for (i, ch) in read.char_indices() { + match ch { + '0'..='9' => int.push(ch), + _ => { + *read = &read[i..]; + break; + } + } + } + int +} + +fn take_ident(read: &mut &str) -> Ident { + let mut ident = String::new(); + let raw = read.starts_with("r#"); + if raw { + ident.push_str("r#"); + *read = &read[2..]; + } + for (i, ch) in read.char_indices() { + match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch), + _ => { + *read = &read[i..]; + break; + } + } + } + Ident::parse_any.parse_str(&ident).unwrap() +} diff --git a/xet_error/impl/src/generics.rs b/xet_error/impl/src/generics.rs new file mode 100644 index 00000000..95592a73 --- /dev/null +++ b/xet_error/impl/src/generics.rs @@ -0,0 +1,83 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap as Map, BTreeSet as Set}; +use syn::punctuated::Punctuated; +use syn::{parse_quote, GenericArgument, Generics, Ident, PathArguments, Token, Type, WhereClause}; + +pub struct ParamsInScope<'a> { + names: Set<&'a Ident>, +} + +impl<'a> ParamsInScope<'a> { + pub fn new(generics: &'a Generics) -> Self { + ParamsInScope { + names: generics.type_params().map(|param| ¶m.ident).collect(), + } + } + + pub fn intersects(&self, ty: &Type) -> bool { + let mut found = false; + crawl(self, ty, &mut found); + found + } +} + +fn crawl(in_scope: &ParamsInScope, ty: &Type, found: &mut bool) { + if let Type::Path(ty) = ty { + if ty.qself.is_none() { + if let Some(ident) = ty.path.get_ident() { + if in_scope.names.contains(ident) { + *found = true; + } + } + } + for segment in &ty.path.segments { + if let PathArguments::AngleBracketed(arguments) = &segment.arguments { + for arg in &arguments.args { + if let GenericArgument::Type(ty) = arg { + crawl(in_scope, ty, found); + } + } + } + } + } +} + +pub struct InferredBounds { + bounds: Map, Punctuated)>, + order: Vec, +} + +impl InferredBounds { + pub fn new() -> Self { + InferredBounds { + bounds: Map::new(), + order: Vec::new(), + } + } + + #[allow(clippy::type_repetition_in_bounds, clippy::trait_duplication_in_bounds)] // clippy bug: https://github.com/rust-lang/rust-clippy/issues/8771 + pub fn insert(&mut self, ty: impl ToTokens, bound: impl ToTokens) { + let ty = ty.to_token_stream(); + let bound = bound.to_token_stream(); + let entry = self.bounds.entry(ty.to_string()); + if let Entry::Vacant(_) = entry { + self.order.push(ty); + } + let (set, tokens) = entry.or_default(); + if set.insert(bound.to_string()) { + tokens.push(bound); + } + } + + pub fn augment_where_clause(&self, generics: &Generics) -> WhereClause { + let mut generics = generics.clone(); + let where_clause = generics.make_where_clause(); + for ty in &self.order { + let (_set, bounds) = &self.bounds[&ty.to_string()]; + where_clause.predicates.push(parse_quote!(#ty: #bounds)); + } + generics.where_clause.unwrap() + } +} diff --git a/xet_error/impl/src/lib.rs b/xet_error/impl/src/lib.rs new file mode 100644 index 00000000..6afd6763 --- /dev/null +++ b/xet_error/impl/src/lib.rs @@ -0,0 +1,42 @@ +#![allow( + unknown_lints, + renamed_and_removed_lints, + clippy::blocks_in_conditions, + clippy::blocks_in_if_conditions, + clippy::cast_lossless, + clippy::cast_possible_truncation, + clippy::manual_find, + clippy::manual_let_else, + clippy::manual_map, + clippy::map_unwrap_or, + clippy::module_name_repetitions, + clippy::needless_pass_by_value, + clippy::option_if_let_else, + clippy::range_plus_one, + clippy::single_match_else, + clippy::struct_field_names, + clippy::too_many_lines, + clippy::wrong_self_convention +)] + +extern crate proc_macro; + +mod ast; +mod attr; +mod expand; +mod fmt; +mod generics; +mod prop; +mod span; +mod valid; + +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(Error, attributes(backtrace, error, from, source))] +pub fn derive_error(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand::derive(&input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/xet_error/impl/src/prop.rs b/xet_error/impl/src/prop.rs new file mode 100644 index 00000000..2867cd31 --- /dev/null +++ b/xet_error/impl/src/prop.rs @@ -0,0 +1,147 @@ +use crate::ast::{Enum, Field, Struct, Variant}; +use crate::span::MemberSpan; +use proc_macro2::Span; +use syn::{Member, Type}; + +impl Struct<'_> { + pub(crate) fn from_field(&self) -> Option<&Field> { + from_field(&self.fields) + } + + pub(crate) fn source_field(&self) -> Option<&Field> { + source_field(&self.fields) + } + + pub(crate) fn backtrace_field(&self) -> Option<&Field> { + backtrace_field(&self.fields) + } + + pub(crate) fn distinct_backtrace_field(&self) -> Option<&Field> { + let backtrace_field = self.backtrace_field()?; + distinct_backtrace_field(backtrace_field, self.from_field()) + } +} + +impl Enum<'_> { + pub(crate) fn has_source(&self) -> bool { + self.variants + .iter() + .any(|variant| variant.source_field().is_some() || variant.attrs.transparent.is_some()) + } + + pub(crate) fn has_backtrace(&self) -> bool { + self.variants + .iter() + .any(|variant| variant.backtrace_field().is_some()) + } + + pub(crate) fn has_display(&self) -> bool { + self.attrs.display.is_some() + || self.attrs.transparent.is_some() + || self + .variants + .iter() + .any(|variant| variant.attrs.display.is_some()) + || self + .variants + .iter() + .all(|variant| variant.attrs.transparent.is_some()) + } +} + +impl Variant<'_> { + pub(crate) fn from_field(&self) -> Option<&Field> { + from_field(&self.fields) + } + + pub(crate) fn source_field(&self) -> Option<&Field> { + source_field(&self.fields) + } + + pub(crate) fn backtrace_field(&self) -> Option<&Field> { + backtrace_field(&self.fields) + } + + pub(crate) fn distinct_backtrace_field(&self) -> Option<&Field> { + let backtrace_field = self.backtrace_field()?; + distinct_backtrace_field(backtrace_field, self.from_field()) + } +} + +impl Field<'_> { + pub(crate) fn is_backtrace(&self) -> bool { + type_is_backtrace(self.ty) + } + + pub(crate) fn source_span(&self) -> Span { + if let Some(source_attr) = &self.attrs.source { + source_attr.path().get_ident().unwrap().span() + } else if let Some(from_attr) = &self.attrs.from { + from_attr.path().get_ident().unwrap().span() + } else { + self.member.member_span() + } + } +} + +fn from_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { + for field in fields { + if field.attrs.from.is_some() { + return Some(field); + } + } + None +} + +fn source_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { + for field in fields { + if field.attrs.from.is_some() || field.attrs.source.is_some() { + return Some(field); + } + } + for field in fields { + match &field.member { + Member::Named(ident) if ident == "source" => return Some(field), + _ => {} + } + } + None +} + +fn backtrace_field<'a, 'b>(fields: &'a [Field<'b>]) -> Option<&'a Field<'b>> { + for field in fields { + if field.attrs.backtrace.is_some() { + return Some(field); + } + } + for field in fields { + if field.is_backtrace() { + return Some(field); + } + } + None +} + +// The #[backtrace] field, if it is not the same as the #[from] field. +fn distinct_backtrace_field<'a, 'b>( + backtrace_field: &'a Field<'b>, + from_field: Option<&Field>, +) -> Option<&'a Field<'b>> { + if from_field.map_or(false, |from_field| { + from_field.member == backtrace_field.member + }) { + None + } else { + Some(backtrace_field) + } +} + +fn type_is_backtrace(ty: &Type) -> bool { + let path = match ty { + Type::Path(ty) => &ty.path, + _ => return false, + }; + + let last = path.segments.last().unwrap(); + last.ident == "Backtrace" && last.arguments.is_empty() +} diff --git a/xet_error/impl/src/span.rs b/xet_error/impl/src/span.rs new file mode 100644 index 00000000..c1237ddf --- /dev/null +++ b/xet_error/impl/src/span.rs @@ -0,0 +1,15 @@ +use proc_macro2::Span; +use syn::Member; + +pub trait MemberSpan { + fn member_span(&self) -> Span; +} + +impl MemberSpan for Member { + fn member_span(&self) -> Span { + match self { + Member::Named(ident) => ident.span(), + Member::Unnamed(index) => index.span, + } + } +} diff --git a/xet_error/impl/src/valid.rs b/xet_error/impl/src/valid.rs new file mode 100644 index 00000000..cf5b8592 --- /dev/null +++ b/xet_error/impl/src/valid.rs @@ -0,0 +1,237 @@ +use crate::ast::{Enum, Field, Input, Struct, Variant}; +use crate::attr::Attrs; +use quote::ToTokens; +use std::collections::BTreeSet as Set; +use syn::{Error, GenericArgument, Member, PathArguments, Result, Type}; + +impl Input<'_> { + pub(crate) fn validate(&self) -> Result<()> { + match self { + Input::Struct(input) => input.validate(), + Input::Enum(input) => input.validate(), + } + } +} + +impl Struct<'_> { + fn validate(&self) -> Result<()> { + check_non_field_attrs(&self.attrs)?; + if let Some(transparent) = self.attrs.transparent { + if self.fields.len() != 1 { + return Err(Error::new_spanned( + transparent.original, + "#[error(transparent)] requires exactly one field", + )); + } + if let Some(source) = self.fields.iter().find_map(|f| f.attrs.source) { + return Err(Error::new_spanned( + source, + "transparent error struct can't contain #[source]", + )); + } + } + check_field_attrs(&self.fields)?; + for field in &self.fields { + field.validate()?; + } + Ok(()) + } +} + +impl Enum<'_> { + fn validate(&self) -> Result<()> { + check_non_field_attrs(&self.attrs)?; + let has_display = self.has_display(); + for variant in &self.variants { + variant.validate()?; + if has_display && variant.attrs.display.is_none() && variant.attrs.transparent.is_none() + { + return Err(Error::new_spanned( + variant.original, + "missing #[error(\"...\")] display attribute", + )); + } + } + let mut from_types = Set::new(); + for variant in &self.variants { + if let Some(from_field) = variant.from_field() { + let repr = from_field.ty.to_token_stream().to_string(); + if !from_types.insert(repr) { + return Err(Error::new_spanned( + from_field.original, + "cannot derive From because another variant has the same source type", + )); + } + } + } + Ok(()) + } +} + +impl Variant<'_> { + fn validate(&self) -> Result<()> { + check_non_field_attrs(&self.attrs)?; + if self.attrs.transparent.is_some() { + if self.fields.len() != 1 { + return Err(Error::new_spanned( + self.original, + "#[error(transparent)] requires exactly one field", + )); + } + if let Some(source) = self.fields.iter().find_map(|f| f.attrs.source) { + return Err(Error::new_spanned( + source, + "transparent variant can't contain #[source]", + )); + } + } + check_field_attrs(&self.fields)?; + for field in &self.fields { + field.validate()?; + } + Ok(()) + } +} + +impl Field<'_> { + fn validate(&self) -> Result<()> { + if let Some(display) = &self.attrs.display { + return Err(Error::new_spanned( + display.original, + "not expected here; the #[error(...)] attribute belongs on top of a struct or an enum variant", + )); + } + Ok(()) + } +} + +fn check_non_field_attrs(attrs: &Attrs) -> Result<()> { + if let Some(from) = &attrs.from { + return Err(Error::new_spanned( + from, + "not expected here; the #[from] attribute belongs on a specific field", + )); + } + if let Some(source) = &attrs.source { + return Err(Error::new_spanned( + source, + "not expected here; the #[source] attribute belongs on a specific field", + )); + } + if let Some(backtrace) = &attrs.backtrace { + return Err(Error::new_spanned( + backtrace, + "not expected here; the #[backtrace] attribute belongs on a specific field", + )); + } + if let Some(display) = &attrs.display { + if attrs.transparent.is_some() { + return Err(Error::new_spanned( + display.original, + "cannot have both #[error(transparent)] and a display attribute", + )); + } + } + Ok(()) +} + +fn check_field_attrs(fields: &[Field]) -> Result<()> { + let mut from_field = None; + let mut source_field = None; + let mut backtrace_field = None; + let mut has_backtrace = false; + for field in fields { + if let Some(from) = field.attrs.from { + if from_field.is_some() { + return Err(Error::new_spanned(from, "duplicate #[from] attribute")); + } + from_field = Some(field); + } + if let Some(source) = field.attrs.source { + if source_field.is_some() { + return Err(Error::new_spanned(source, "duplicate #[source] attribute")); + } + source_field = Some(field); + } + if let Some(backtrace) = field.attrs.backtrace { + if backtrace_field.is_some() { + return Err(Error::new_spanned( + backtrace, + "duplicate #[backtrace] attribute", + )); + } + backtrace_field = Some(field); + has_backtrace = true; + } + if let Some(transparent) = field.attrs.transparent { + return Err(Error::new_spanned( + transparent.original, + "#[error(transparent)] needs to go outside the enum or struct, not on an individual field", + )); + } + has_backtrace |= field.is_backtrace(); + } + if let (Some(from_field), Some(source_field)) = (from_field, source_field) { + if !same_member(from_field, source_field) { + return Err(Error::new_spanned( + from_field.attrs.from, + "#[from] is only supported on the source field, not any other field", + )); + } + } + if let Some(from_field) = from_field { + let max_expected_fields = match backtrace_field { + Some(backtrace_field) => 1 + !same_member(from_field, backtrace_field) as usize, + None => 1 + has_backtrace as usize, + }; + if fields.len() > max_expected_fields { + return Err(Error::new_spanned( + from_field.attrs.from, + "deriving From requires no fields other than source and backtrace", + )); + } + } + if let Some(source_field) = source_field.or(from_field) { + if contains_non_static_lifetime(source_field.ty) { + return Err(Error::new_spanned( + &source_field.original.ty, + "non-static lifetimes are not allowed in the source of an error, because std::error::Error requires the source is dyn Error + 'static", + )); + } + } + Ok(()) +} + +fn same_member(one: &Field, two: &Field) -> bool { + match (&one.member, &two.member) { + (Member::Named(one), Member::Named(two)) => one == two, + (Member::Unnamed(one), Member::Unnamed(two)) => one.index == two.index, + _ => unreachable!(), + } +} + +fn contains_non_static_lifetime(ty: &Type) -> bool { + match ty { + Type::Path(ty) => { + let bracketed = match &ty.path.segments.last().unwrap().arguments { + PathArguments::AngleBracketed(bracketed) => bracketed, + _ => return false, + }; + for arg in &bracketed.args { + match arg { + GenericArgument::Type(ty) if contains_non_static_lifetime(ty) => return true, + GenericArgument::Lifetime(lifetime) if lifetime.ident != "static" => { + return true + } + _ => {} + } + } + false + } + Type::Reference(ty) => ty + .lifetime + .as_ref() + .map_or(false, |lifetime| lifetime.ident != "static"), + _ => false, // maybe implement later if there are common other cases + } +} diff --git a/xet_error/rust-toolchain.toml b/xet_error/rust-toolchain.toml new file mode 100644 index 00000000..20fe888c --- /dev/null +++ b/xet_error/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +components = ["rust-src"] diff --git a/xet_error/src/aserror.rs b/xet_error/src/aserror.rs new file mode 100644 index 00000000..54fc6f11 --- /dev/null +++ b/xet_error/src/aserror.rs @@ -0,0 +1,50 @@ +use std::error::Error; +use std::panic::UnwindSafe; + +#[doc(hidden)] +pub trait AsDynError<'a>: Sealed { + fn as_dyn_error(&self) -> &(dyn Error + 'a); +} + +impl<'a, T: Error + 'a> AsDynError<'a> for T { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +impl<'a> AsDynError<'a> for dyn Error + 'a { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +impl<'a> AsDynError<'a> for dyn Error + Send + 'a { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +impl<'a> AsDynError<'a> for dyn Error + Send + Sync + 'a { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +impl<'a> AsDynError<'a> for dyn Error + Send + Sync + UnwindSafe + 'a { + #[inline] + fn as_dyn_error(&self) -> &(dyn Error + 'a) { + self + } +} + +#[doc(hidden)] +pub trait Sealed {} +impl<'a, T: Error + 'a> Sealed for T {} +impl<'a> Sealed for dyn Error + 'a {} +impl<'a> Sealed for dyn Error + Send + 'a {} +impl<'a> Sealed for dyn Error + Send + Sync + 'a {} +impl<'a> Sealed for dyn Error + Send + Sync + UnwindSafe + 'a {} diff --git a/xet_error/src/display.rs b/xet_error/src/display.rs new file mode 100644 index 00000000..27098f16 --- /dev/null +++ b/xet_error/src/display.rs @@ -0,0 +1,40 @@ +use std::fmt::Display; +use std::path::{self, Path, PathBuf}; + +#[doc(hidden)] +pub trait AsDisplay<'a> { + // TODO: convert to generic associated type. + // https://github.com/dtolnay/thiserror/pull/253 + type Target: Display; + + fn as_display(&'a self) -> Self::Target; +} + +impl<'a, T> AsDisplay<'a> for &T +where + T: Display + 'a, +{ + type Target = &'a T; + + fn as_display(&'a self) -> Self::Target { + *self + } +} + +impl<'a> AsDisplay<'a> for Path { + type Target = path::Display<'a>; + + #[inline] + fn as_display(&'a self) -> Self::Target { + self.display() + } +} + +impl<'a> AsDisplay<'a> for PathBuf { + type Target = path::Display<'a>; + + #[inline] + fn as_display(&'a self) -> Self::Target { + self.display() + } +} diff --git a/xet_error/src/lib.rs b/xet_error/src/lib.rs new file mode 100644 index 00000000..253e073b --- /dev/null +++ b/xet_error/src/lib.rs @@ -0,0 +1,272 @@ +//! [![github]](https://github.com/dtolnay/thiserror) [![crates-io]](https://crates.io/crates/thiserror) [![docs-rs]](https://docs.rs/thiserror) +//! +//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +//! +//!
+//! +//! This library provides a convenient derive macro for the standard library's +//! [`std::error::Error`] trait. +//! +//! [`std::error::Error`]: https://doc.rust-lang.org/std/error/trait.Error.html +//! +//!
+//! +//! # Example +//! +//! ```rust +//! # use std::io; +//! use xet_error::Error; +//! +//! #[derive(Error, Debug)] +//! pub enum DataStoreError { +//! #[error("data store disconnected")] +//! Disconnect(#[from] io::Error), +//! #[error("the data for key `{0}` is not available")] +//! Redaction(String), +//! #[error("invalid header (expected {expected:?}, found {found:?})")] +//! InvalidHeader { +//! expected: String, +//! found: String, +//! }, +//! #[error("unknown data store error")] +//! Unknown, +//! } +//! ``` +//! +//!
+//! +//! # Details +//! +//! - Thiserror deliberately does not appear in your public API. You get the +//! same thing as if you had written an implementation of `std::error::Error` +//! by hand, and switching from handwritten impls to xet_error or vice versa +//! is not a breaking change. +//! +//! - Errors may be enums, structs with named fields, tuple structs, or unit +//! structs. +//! +//! - A `Display` impl is generated for your error if you provide +//! `#[error("...")]` messages on the struct or each variant of your enum, as +//! shown above in the example. +//! +//! The messages support a shorthand for interpolating fields from the error. +//! +//! - `#[error("{var}")]` ⟶ `write!("{}", self.var)` +//! - `#[error("{0}")]` ⟶ `write!("{}", self.0)` +//! - `#[error("{var:?}")]` ⟶ `write!("{:?}", self.var)` +//! - `#[error("{0:?}")]` ⟶ `write!("{:?}", self.0)` +//! +//! These shorthands can be used together with any additional format args, +//! which may be arbitrary expressions. For example: +//! +//! ```rust +//! # use std::i32; +//! # use xet_error::Error; +//! # +//! #[derive(Error, Debug)] +//! pub enum Error { +//! #[error("invalid rdo_lookahead_frames {0} (expected < {})", i32::MAX)] +//! InvalidLookahead(u32), +//! } +//! ``` +//! +//! If one of the additional expression arguments needs to refer to a field of +//! the struct or enum, then refer to named fields as `.var` and tuple fields +//! as `.0`. +//! +//! ```rust +//! # use xet_error::Error; +//! # +//! # fn first_char(s: &String) -> char { +//! # s.chars().next().unwrap() +//! # } +//! # +//! # #[derive(Debug)] +//! # struct Limits { +//! # lo: usize, +//! # hi: usize, +//! # } +//! # +//! #[derive(Error, Debug)] +//! pub enum Error { +//! #[error("first letter must be lowercase but was {:?}", first_char(.0))] +//! WrongCase(String), +//! #[error("invalid index {idx}, expected at least {} and at most {}", .limits.lo, .limits.hi)] +//! OutOfBounds { idx: usize, limits: Limits }, +//! } +//! ``` +//! +//! - A `From` impl is generated for each variant containing a `#[from]` +//! attribute. +//! +//! Note that the variant must not contain any other fields beyond the source +//! error and possibly a backtrace. A backtrace is captured from within the +//! `From` impl if there is a field for it. +//! +//! ```rust +//! # const IGNORE: &str = stringify! { +//! #[derive(Error, Debug)] +//! pub enum MyError { +//! Io { +//! #[from] +//! source: io::Error, +//! backtrace: Backtrace, +//! }, +//! } +//! # }; +//! ``` +//! +//! - The Error trait's `source()` method is implemented to return whichever +//! field has a `#[source]` attribute or is named `source`, if any. This is +//! for identifying the underlying lower level error that caused your error. +//! +//! The `#[from]` attribute always implies that the same field is `#[source]`, +//! so you don't ever need to specify both attributes. +//! +//! Any error type that implements `std::error::Error` or dereferences to `dyn +//! std::error::Error` will work as a source. +//! +//! ```rust +//! # use std::fmt::{self, Display}; +//! # use xet_error::Error; +//! # +//! #[derive(Error, Debug)] +//! pub struct MyError { +//! msg: String, +//! #[source] // optional if field name is `source` +//! source: anyhow::Error, +//! } +//! # +//! # impl Display for MyError { +//! # fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { +//! # unimplemented!() +//! # } +//! # } +//! ``` +//! +//! - The Error trait's `provide()` method is implemented to provide whichever +//! field has a type named `Backtrace`, if any, as a +//! `std::backtrace::Backtrace`. +//! +//! ```rust +//! # const IGNORE: &str = stringify! { +//! use std::backtrace::Backtrace; +//! +//! #[derive(Error, Debug)] +//! pub struct MyError { +//! msg: String, +//! backtrace: Backtrace, // automatically detected +//! } +//! # }; +//! ``` +//! +//! - If a field is both a source (named `source`, or has `#[source]` or +//! `#[from]` attribute) *and* is marked `#[backtrace]`, then the Error +//! trait's `provide()` method is forwarded to the source's `provide` so that +//! both layers of the error share the same backtrace. +//! +//! ```rust +//! # const IGNORE: &str = stringify! { +//! #[derive(Error, Debug)] +//! pub enum MyError { +//! Io { +//! #[backtrace] +//! source: io::Error, +//! }, +//! } +//! # }; +//! ``` +//! +//! - Errors may use `error(transparent)` to forward the source and Display +//! methods straight through to an underlying error without adding an +//! additional message. This would be appropriate for enums that need an +//! "anything else" variant. +//! +//! ``` +//! # use xet_error::Error; +//! # +//! #[derive(Error, Debug)] +//! pub enum MyError { +//! # /* +//! ... +//! # */ +//! +//! #[error(transparent)] +//! Other(#[from] anyhow::Error), // source and Display delegate to anyhow::Error +//! } +//! ``` +//! +//! Another use case is hiding implementation details of an error +//! representation behind an opaque error type, so that the representation is +//! able to evolve without breaking the crate's public API. +//! +//! ``` +//! # use xet_error::Error; +//! # +//! // PublicError is public, but opaque and easy to keep compatible. +//! #[derive(Error, Debug)] +//! #[error(transparent)] +//! pub struct PublicError(#[from] ErrorRepr); +//! +//! impl PublicError { +//! // Accessors for anything we do want to expose publicly. +//! } +//! +//! // Private and free to change across minor version of the crate. +//! #[derive(Error, Debug)] +//! enum ErrorRepr { +//! # /* +//! ... +//! # */ +//! } +//! ``` +//! +//! - See also the [`anyhow`] library for a convenient single error type to use +//! in application code. +//! +//! [`anyhow`]: https://github.com/dtolnay/anyhow + +#![doc(html_root_url = "https://docs.rs/thiserror/1.0.50")] +#![allow( + clippy::module_name_repetitions, + clippy::needless_lifetimes, + clippy::return_self_not_must_use, + clippy::wildcard_imports +)] +#![cfg_attr(error_generic_member_access, feature(error_generic_member_access))] + +mod aserror; +mod display; +#[cfg(error_generic_member_access)] +mod provide; + +pub use xet_error_impl::*; + +// Not public API. +#[doc(hidden)] +pub mod __private { + #[doc(hidden)] + pub use crate::aserror::AsDynError; + #[doc(hidden)] + pub use crate::display::AsDisplay; + #[cfg(error_generic_member_access)] + #[doc(hidden)] + pub use crate::provide::ThiserrorProvide; +} + +use lazy_static::lazy_static; +lazy_static! { + static ref LOG_EXCEPTION_FUNCTION: std::sync::Mutex = std::sync::Mutex::new(|_| ()); +} + +pub fn enable_exception_logging(logger: fn(&str)) { + *(LOG_EXCEPTION_FUNCTION.lock().unwrap()) = logger; +} + +#[inline(never)] +#[no_mangle] +pub fn error_hook(source: &str) { + (LOG_EXCEPTION_FUNCTION.lock().unwrap())(source); +} diff --git a/xet_error/src/provide.rs b/xet_error/src/provide.rs new file mode 100644 index 00000000..20a26f36 --- /dev/null +++ b/xet_error/src/provide.rs @@ -0,0 +1,20 @@ +use std::error::{Error, Request}; + +#[doc(hidden)] +pub trait ThiserrorProvide: Sealed { + fn xet_error_provide<'a>(&'a self, request: &mut Request<'a>); +} + +impl ThiserrorProvide for T +where + T: Error + ?Sized, +{ + #[inline] + fn xet_error_provide<'a>(&'a self, request: &mut Request<'a>) { + self.provide(request); + } +} + +#[doc(hidden)] +pub trait Sealed {} +impl Sealed for T {} diff --git a/xet_error/tests/compiletest.rs b/xet_error/tests/compiletest.rs new file mode 100644 index 00000000..7974a624 --- /dev/null +++ b/xet_error/tests/compiletest.rs @@ -0,0 +1,7 @@ +#[rustversion::attr(not(nightly), ignore)] +#[cfg_attr(miri, ignore)] +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/xet_error/tests/test_backtrace.rs b/xet_error/tests/test_backtrace.rs new file mode 100644 index 00000000..e8f062c1 --- /dev/null +++ b/xet_error/tests/test_backtrace.rs @@ -0,0 +1,274 @@ +#![cfg_attr(xet_error_nightly_testing, feature(error_generic_member_access))] + +use xet_error::Error; + +#[derive(Error, Debug)] +#[error("...")] +pub struct Inner; + +#[cfg(xet_error_nightly_testing)] +#[derive(Error, Debug)] +#[error("...")] +pub struct InnerBacktrace { + backtrace: std::backtrace::Backtrace, +} + +#[cfg(xet_error_nightly_testing)] +pub mod structs { + use super::{Inner, InnerBacktrace}; + use std::backtrace::Backtrace; + use std::error::{self, Error}; + use std::sync::Arc; + use xet_error::Error; + + #[derive(Error, Debug)] + #[error("...")] + pub struct PlainBacktrace { + backtrace: Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct ExplicitBacktrace { + #[backtrace] + backtrace: Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptBacktrace { + #[backtrace] + backtrace: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct ArcBacktrace { + #[backtrace] + backtrace: Arc, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct BacktraceFrom { + #[from] + source: Inner, + #[backtrace] + backtrace: Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct CombinedBacktraceFrom { + #[from] + #[backtrace] + source: InnerBacktrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptBacktraceFrom { + #[from] + source: Inner, + #[backtrace] + backtrace: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct ArcBacktraceFrom { + #[from] + source: Inner, + #[backtrace] + backtrace: Arc, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct AnyhowBacktrace { + #[backtrace] + source: anyhow::Error, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct BoxDynErrorBacktrace { + #[backtrace] + source: Box, + } + + #[test] + fn test_backtrace() { + let error = PlainBacktrace { + backtrace: Backtrace::capture(), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = ExplicitBacktrace { + backtrace: Backtrace::capture(), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = OptBacktrace { + backtrace: Some(Backtrace::capture()), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = ArcBacktrace { + backtrace: Arc::new(Backtrace::capture()), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = BacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = CombinedBacktraceFrom::from(InnerBacktrace { + backtrace: Backtrace::capture(), + }); + assert!(error::request_ref::(&error).is_some()); + + let error = OptBacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = ArcBacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = AnyhowBacktrace { + source: anyhow::Error::msg("..."), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = BoxDynErrorBacktrace { + source: Box::new(PlainBacktrace { + backtrace: Backtrace::capture(), + }), + }; + assert!(error::request_ref::(&error).is_some()); + } +} + +#[cfg(xet_error_nightly_testing)] +pub mod enums { + use super::{Inner, InnerBacktrace}; + use std::backtrace::Backtrace; + use std::error; + use std::sync::Arc; + use xet_error::Error; + + #[derive(Error, Debug)] + pub enum PlainBacktrace { + #[error("...")] + Test { backtrace: Backtrace }, + } + + #[derive(Error, Debug)] + pub enum ExplicitBacktrace { + #[error("...")] + Test { + #[backtrace] + backtrace: Backtrace, + }, + } + + #[derive(Error, Debug)] + pub enum OptBacktrace { + #[error("...")] + Test { + #[backtrace] + backtrace: Option, + }, + } + + #[derive(Error, Debug)] + pub enum ArcBacktrace { + #[error("...")] + Test { + #[backtrace] + backtrace: Arc, + }, + } + + #[derive(Error, Debug)] + pub enum BacktraceFrom { + #[error("...")] + Test { + #[from] + source: Inner, + #[backtrace] + backtrace: Backtrace, + }, + } + + #[derive(Error, Debug)] + pub enum CombinedBacktraceFrom { + #[error("...")] + Test { + #[from] + #[backtrace] + source: InnerBacktrace, + }, + } + + #[derive(Error, Debug)] + pub enum OptBacktraceFrom { + #[error("...")] + Test { + #[from] + source: Inner, + #[backtrace] + backtrace: Option, + }, + } + + #[derive(Error, Debug)] + pub enum ArcBacktraceFrom { + #[error("...")] + Test { + #[from] + source: Inner, + #[backtrace] + backtrace: Arc, + }, + } + + #[test] + fn test_backtrace() { + let error = PlainBacktrace::Test { + backtrace: Backtrace::capture(), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = ExplicitBacktrace::Test { + backtrace: Backtrace::capture(), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = OptBacktrace::Test { + backtrace: Some(Backtrace::capture()), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = ArcBacktrace::Test { + backtrace: Arc::new(Backtrace::capture()), + }; + assert!(error::request_ref::(&error).is_some()); + + let error = BacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = CombinedBacktraceFrom::from(InnerBacktrace { + backtrace: Backtrace::capture(), + }); + assert!(error::request_ref::(&error).is_some()); + + let error = OptBacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + + let error = ArcBacktraceFrom::from(Inner); + assert!(error::request_ref::(&error).is_some()); + } +} + +#[test] +#[cfg_attr(not(xet_error_nightly_testing), ignore)] +fn test_backtrace() {} diff --git a/xet_error/tests/test_deprecated.rs b/xet_error/tests/test_deprecated.rs new file mode 100644 index 00000000..719b1b82 --- /dev/null +++ b/xet_error/tests/test_deprecated.rs @@ -0,0 +1,10 @@ +#![deny(deprecated, clippy::all, clippy::pedantic)] + +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[deprecated] + #[error("...")] + Deprecated, +} diff --git a/xet_error/tests/test_display.rs b/xet_error/tests/test_display.rs new file mode 100644 index 00000000..d45aa176 --- /dev/null +++ b/xet_error/tests/test_display.rs @@ -0,0 +1,303 @@ +#![allow(clippy::uninlined_format_args)] + +use std::fmt::{self, Display}; +use xet_error::Error; + +fn assert(expected: &str, value: T) { + assert_eq!(expected, value.to_string()); +} + +#[test] +fn test_braced() { + #[derive(Error, Debug)] + #[error("braced error: {msg}")] + struct Error { + msg: String, + } + + let msg = "T".to_owned(); + assert("braced error: T", Error { msg }); +} + +#[test] +fn test_braced_unused() { + #[derive(Error, Debug)] + #[error("braced error")] + struct Error { + extra: usize, + } + + assert("braced error", Error { extra: 0 }); +} + +#[test] +fn test_tuple() { + #[derive(Error, Debug)] + #[error("tuple error: {0}")] + struct Error(usize); + + assert("tuple error: 0", Error(0)); +} + +#[test] +fn test_unit() { + #[derive(Error, Debug)] + #[error("unit error")] + struct Error; + + assert("unit error", Error); +} + +#[test] +fn test_enum() { + #[derive(Error, Debug)] + enum Error { + #[error("braced error: {id}")] + Braced { id: usize }, + #[error("tuple error: {0}")] + Tuple(usize), + #[error("unit error")] + Unit, + } + + assert("braced error: 0", Error::Braced { id: 0 }); + assert("tuple error: 0", Error::Tuple(0)); + assert("unit error", Error::Unit); +} + +#[test] +fn test_constants() { + #[derive(Error, Debug)] + #[error("{MSG}: {id:?} (code {CODE:?})")] + struct Error { + id: &'static str, + } + + const MSG: &str = "failed to do"; + const CODE: usize = 9; + + assert("failed to do: \"\" (code 9)", Error { id: "" }); +} + +#[test] +fn test_inherit() { + #[derive(Error, Debug)] + #[error("{0}")] + enum Error { + Some(&'static str), + #[error("other error")] + Other(&'static str), + } + + assert("some error", Error::Some("some error")); + assert("other error", Error::Other("...")); +} + +#[test] +fn test_brace_escape() { + #[derive(Error, Debug)] + #[error("fn main() {{}}")] + struct Error; + + assert("fn main() {}", Error); +} + +#[test] +fn test_expr() { + #[derive(Error, Debug)] + #[error("1 + 1 = {}", 1 + 1)] + struct Error; + assert("1 + 1 = 2", Error); +} + +#[test] +fn test_nested() { + #[derive(Error, Debug)] + #[error("!bool = {}", not(.0))] + struct Error(bool); + + #[allow(clippy::trivially_copy_pass_by_ref)] + fn not(bool: &bool) -> bool { + !*bool + } + + assert("!bool = false", Error(true)); +} + +#[test] +fn test_match() { + #[derive(Error, Debug)] + #[error("{}: {0}", match .1 { + Some(n) => format!("error occurred with {}", n), + None => "there was an empty error".to_owned(), + })] + struct Error(String, Option); + + assert( + "error occurred with 1: ...", + Error("...".to_owned(), Some(1)), + ); + assert( + "there was an empty error: ...", + Error("...".to_owned(), None), + ); +} + +#[test] +fn test_nested_display() { + // Same behavior as the one in `test_match`, but without String allocations. + #[derive(Error, Debug)] + #[error("{}", { + struct Msg<'a>(&'a String, &'a Option); + impl<'a> Display for Msg<'a> { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self.1 { + Some(n) => write!(formatter, "error occurred with {}", n), + None => write!(formatter, "there was an empty error"), + }?; + write!(formatter, ": {}", self.0) + } + } + Msg(.0, .1) + })] + struct Error(String, Option); + + assert( + "error occurred with 1: ...", + Error("...".to_owned(), Some(1)), + ); + assert( + "there was an empty error: ...", + Error("...".to_owned(), None), + ); +} + +#[test] +fn test_void() { + #[allow(clippy::empty_enum)] + #[derive(Error, Debug)] + #[error("...")] + pub enum Error {} + + let _: Error; +} + +#[test] +fn test_mixed() { + #[derive(Error, Debug)] + #[error("a={a} :: b={} :: c={c} :: d={d}", 1, c = 2, d = 3)] + struct Error { + a: usize, + d: usize, + } + + assert("a=0 :: b=1 :: c=2 :: d=3", Error { a: 0, d: 0 }); +} + +#[test] +fn test_ints() { + #[derive(Error, Debug)] + enum Error { + #[error("error {0}")] + Tuple(usize, usize), + #[error("error {0}", '?')] + Struct { v: usize }, + } + + assert("error 9", Error::Tuple(9, 0)); + assert("error ?", Error::Struct { v: 0 }); +} + +#[test] +fn test_trailing_comma() { + #[derive(Error, Debug)] + #[error( + "error {0}", + )] + #[rustfmt::skip] + struct Error(char); + + assert("error ?", Error('?')); +} + +#[test] +fn test_field() { + #[derive(Debug)] + struct Inner { + data: usize, + } + + #[derive(Error, Debug)] + #[error("{}", .0.data)] + struct Error(Inner); + + assert("0", Error(Inner { data: 0 })); +} + +#[test] +fn test_macro_rules() { + // Regression test for https://github.com/dtolnay/thiserror/issues/86 + + macro_rules! decl_error { + ($variant:ident($value:ident)) => { + #[derive(Debug, Error)] + pub enum Error0 { + #[error("{0:?}")] + $variant($value), + } + + #[derive(Debug, Error)] + #[error("{0:?}")] + pub enum Error1 { + $variant($value), + } + }; + } + + decl_error!(Repro(u8)); + + assert("0", Error0::Repro(0)); + assert("0", Error1::Repro(0)); +} + +#[test] +fn test_raw() { + #[derive(Error, Debug)] + #[error("braced raw error: {r#fn}")] + struct Error { + r#fn: &'static str, + } + + assert("braced raw error: T", Error { r#fn: "T" }); +} + +#[test] +fn test_raw_enum() { + #[derive(Error, Debug)] + enum Error { + #[error("braced raw error: {r#fn}")] + Braced { r#fn: &'static str }, + } + + assert("braced raw error: T", Error::Braced { r#fn: "T" }); +} + +#[test] +fn test_raw_conflict() { + #[derive(Error, Debug)] + enum Error { + #[error("braced raw error: {r#func}, {func}", func = "U")] + Braced { r#func: &'static str }, + } + + assert("braced raw error: T, U", Error::Braced { r#func: "T" }); +} + +#[test] +fn test_keyword() { + #[derive(Error, Debug)] + #[error("error: {type}", type = 1)] + struct Error; + + assert("error: 1", Error); +} diff --git a/xet_error/tests/test_error.rs b/xet_error/tests/test_error.rs new file mode 100644 index 00000000..64b4d205 --- /dev/null +++ b/xet_error/tests/test_error.rs @@ -0,0 +1,56 @@ +#![allow(dead_code)] + +use std::fmt::{self, Display}; +use std::io; +use xet_error::Error; + +macro_rules! unimplemented_display { + ($ty:ty) => { + impl Display for $ty { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { + unimplemented!() + } + } + }; +} + +#[derive(Error, Debug)] +struct BracedError { + msg: String, + pos: usize, +} + +#[derive(Error, Debug)] +struct TupleError(String, usize); + +#[derive(Error, Debug)] +struct UnitError; + +#[derive(Error, Debug)] +struct WithSource { + #[source] + cause: io::Error, +} + +#[derive(Error, Debug)] +struct WithAnyhow { + #[source] + cause: anyhow::Error, +} + +#[derive(Error, Debug)] +enum EnumError { + Braced { + #[source] + cause: io::Error, + }, + Tuple(#[source] io::Error), + Unit, +} + +unimplemented_display!(BracedError); +unimplemented_display!(TupleError); +unimplemented_display!(UnitError); +unimplemented_display!(WithSource); +unimplemented_display!(WithAnyhow); +unimplemented_display!(EnumError); diff --git a/xet_error/tests/test_expr.rs b/xet_error/tests/test_expr.rs new file mode 100644 index 00000000..98859c3c --- /dev/null +++ b/xet_error/tests/test_expr.rs @@ -0,0 +1,92 @@ +#![allow( + clippy::iter_cloned_collect, + clippy::option_if_let_else, + clippy::uninlined_format_args +)] + +use std::fmt::Display; +use xet_error::Error; + +// Some of the elaborate cases from the rcc codebase, which is a C compiler in +// Rust. https://github.com/jyn514/rcc/blob/0.8.0/src/data/error.rs +#[derive(Error, Debug)] +pub enum CompilerError { + #[error("cannot shift {} by {maximum} or more bits (got {current})", if *.is_left { "left" } else { "right" })] + TooManyShiftBits { + is_left: bool, + maximum: u64, + current: u64, + }, + + #[error("#error {}", (.0).iter().copied().collect::>().join(" "))] + User(Vec<&'static str>), + + #[error("overflow while parsing {}integer literal", + if let Some(signed) = .is_signed { + if *signed { "signed "} else { "unsigned "} + } else { + "" + } + )] + IntegerOverflow { is_signed: Option }, + + #[error("overflow while parsing {}integer literal", match .is_signed { + Some(true) => "signed ", + Some(false) => "unsigned ", + None => "", + })] + IntegerOverflow2 { is_signed: Option }, +} + +// Examples drawn from Rustup. +#[derive(Error, Debug)] +pub enum RustupError { + #[error( + "toolchain '{name}' does not contain component {component}{}", + .suggestion + .as_ref() + .map_or_else(String::new, |s| format!("; did you mean '{}'?", s)), + )] + UnknownComponent { + name: String, + component: String, + suggestion: Option, + }, +} + +fn assert(expected: &str, value: T) { + assert_eq!(expected, value.to_string()); +} + +#[test] +fn test_rcc() { + assert( + "cannot shift left by 32 or more bits (got 50)", + CompilerError::TooManyShiftBits { + is_left: true, + maximum: 32, + current: 50, + }, + ); + + assert("#error A B C", CompilerError::User(vec!["A", "B", "C"])); + + assert( + "overflow while parsing signed integer literal", + CompilerError::IntegerOverflow { + is_signed: Some(true), + }, + ); +} + +#[test] +fn test_rustup() { + assert( + "toolchain 'nightly' does not contain component clipy; did you mean 'clippy'?", + RustupError::UnknownComponent { + name: "nightly".to_owned(), + component: "clipy".to_owned(), + suggestion: Some("clippy".to_owned()), + }, + ); +} diff --git a/xet_error/tests/test_from.rs b/xet_error/tests/test_from.rs new file mode 100644 index 00000000..012589f4 --- /dev/null +++ b/xet_error/tests/test_from.rs @@ -0,0 +1,64 @@ +#![allow(clippy::extra_unused_type_parameters)] + +use std::io; +use xet_error::Error; + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorStruct { + #[from] + source: io::Error, +} + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorStructOptional { + #[from] + source: Option, +} + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorTuple(#[from] io::Error); + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorTupleOptional(#[from] Option); + +#[derive(Error, Debug)] +#[error("...")] +pub enum ErrorEnum { + Test { + #[from] + source: io::Error, + }, +} + +#[derive(Error, Debug)] +#[error("...")] +pub enum ErrorEnumOptional { + Test { + #[from] + source: Option, + }, +} + +#[derive(Error, Debug)] +#[error("...")] +pub enum Many { + Any(#[from] anyhow::Error), + Io(#[from] io::Error), +} + +fn assert_impl>() {} + +#[test] +fn test_from() { + assert_impl::(); + assert_impl::(); + assert_impl::(); + assert_impl::(); + assert_impl::(); + assert_impl::(); + assert_impl::(); +} diff --git a/xet_error/tests/test_generics.rs b/xet_error/tests/test_generics.rs new file mode 100644 index 00000000..2bc48bdd --- /dev/null +++ b/xet_error/tests/test_generics.rs @@ -0,0 +1,161 @@ +#![allow(clippy::needless_late_init, clippy::uninlined_format_args)] + +use std::fmt::{self, Debug, Display}; +use xet_error::Error; + +pub struct NoFormat; + +#[derive(Debug)] +pub struct DebugOnly; + +pub struct DisplayOnly; + +impl Display for DisplayOnly { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("display only") + } +} + +#[derive(Debug)] +pub struct DebugAndDisplay; + +impl Display for DebugAndDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("debug and display") + } +} + +// Should expand to: +// +// impl Display for EnumDebugField +// where +// E: Debug; +// +// impl Error for EnumDebugField +// where +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub enum EnumDebugGeneric { + #[error("{0:?}")] + FatalError(E), +} + +// Should expand to: +// +// impl Display for EnumFromGeneric; +// +// impl Error for EnumFromGeneric +// where +// EnumDebugGeneric: Error + 'static, +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub enum EnumFromGeneric { + #[error("enum from generic")] + Source(#[from] EnumDebugGeneric), +} + +// Should expand to: +// +// impl Display +// for EnumCompound +// where +// HasDisplay: Display, +// HasDebug: Debug; +// +// impl Error +// for EnumCompound +// where +// Self: Debug + Display; +// +#[derive(Error)] +pub enum EnumCompound { + #[error("{0} {1:?}")] + DisplayDebug(HasDisplay, HasDebug), + #[error("{0}")] + Display(HasDisplay, HasNeither), + #[error("{1:?}")] + Debug(HasNeither, HasDebug), +} + +impl Debug for EnumCompound { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("EnumCompound") + } +} + +#[test] +fn test_display_enum_compound() { + let mut instance: EnumCompound; + + instance = EnumCompound::DisplayDebug(DisplayOnly, DebugOnly); + assert_eq!(format!("{}", instance), "display only DebugOnly"); + + instance = EnumCompound::Display(DisplayOnly, NoFormat); + assert_eq!(format!("{}", instance), "display only"); + + instance = EnumCompound::Debug(NoFormat, DebugOnly); + assert_eq!(format!("{}", instance), "DebugOnly"); +} + +// Should expand to: +// +// impl Display for EnumTransparentGeneric +// where +// E: Display; +// +// impl Error for EnumTransparentGeneric +// where +// E: Error, +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub enum EnumTransparentGeneric { + #[error(transparent)] + Other(E), +} + +// Should expand to: +// +// impl Display for StructDebugGeneric +// where +// E: Debug; +// +// impl Error for StructDebugGeneric +// where +// Self: Debug + Display; +// +#[derive(Error, Debug)] +#[error("{underlying:?}")] +pub struct StructDebugGeneric { + pub underlying: E, +} + +// Should expand to: +// +// impl Error for StructFromGeneric +// where +// StructDebugGeneric: Error + 'static, +// Self: Debug + Display; +// +#[derive(Error, Debug)] +pub struct StructFromGeneric { + #[from] + pub source: StructDebugGeneric, +} + +// Should expand to: +// +// impl Display for StructTransparentGeneric +// where +// E: Display; +// +// impl Error for StructTransparentGeneric +// where +// E: Error, +// Self: Debug + Display; +// +#[derive(Error, Debug)] +#[error(transparent)] +pub struct StructTransparentGeneric(E); diff --git a/xet_error/tests/test_lints.rs b/xet_error/tests/test_lints.rs new file mode 100644 index 00000000..e29ee446 --- /dev/null +++ b/xet_error/tests/test_lints.rs @@ -0,0 +1,18 @@ +use xet_error::Error; + +pub use std::error::Error; + +#[test] +fn test_unused_qualifications() { + #![deny(unused_qualifications)] + + // Expansion of derive(Error) macro can't know whether something like + // std::error::Error is already imported in the caller's scope so it must + // suppress unused_qualifications. + + #[derive(Debug, Error)] + #[error("...")] + pub struct MyError; + + let _: MyError; +} diff --git a/xet_error/tests/test_option.rs b/xet_error/tests/test_option.rs new file mode 100644 index 00000000..1d64fc9b --- /dev/null +++ b/xet_error/tests/test_option.rs @@ -0,0 +1,105 @@ +#![cfg_attr(xet_error_nightly_testing, feature(error_generic_member_access))] + +#[cfg(xet_error_nightly_testing)] +pub mod structs { + use std::backtrace::Backtrace; + use xet_error::Error; + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptSourceNoBacktrace { + #[source] + source: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptSourceAlwaysBacktrace { + #[source] + source: Option, + backtrace: Backtrace, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct NoSourceOptBacktrace { + #[backtrace] + backtrace: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct AlwaysSourceOptBacktrace { + source: anyhow::Error, + #[backtrace] + backtrace: Option, + } + + #[derive(Error, Debug)] + #[error("...")] + pub struct OptSourceOptBacktrace { + #[source] + source: Option, + #[backtrace] + backtrace: Option, + } +} + +#[cfg(xet_error_nightly_testing)] +pub mod enums { + use std::backtrace::Backtrace; + use xet_error::Error; + + #[derive(Error, Debug)] + pub enum OptSourceNoBacktrace { + #[error("...")] + Test { + #[source] + source: Option, + }, + } + + #[derive(Error, Debug)] + pub enum OptSourceAlwaysBacktrace { + #[error("...")] + Test { + #[source] + source: Option, + backtrace: Backtrace, + }, + } + + #[derive(Error, Debug)] + pub enum NoSourceOptBacktrace { + #[error("...")] + Test { + #[backtrace] + backtrace: Option, + }, + } + + #[derive(Error, Debug)] + pub enum AlwaysSourceOptBacktrace { + #[error("...")] + Test { + source: anyhow::Error, + #[backtrace] + backtrace: Option, + }, + } + + #[derive(Error, Debug)] + pub enum OptSourceOptBacktrace { + #[error("...")] + Test { + #[source] + source: Option, + #[backtrace] + backtrace: Option, + }, + } +} + +#[test] +#[cfg_attr(not(xet_error_nightly_testing), ignore)] +fn test_option() {} diff --git a/xet_error/tests/test_path.rs b/xet_error/tests/test_path.rs new file mode 100644 index 00000000..82112750 --- /dev/null +++ b/xet_error/tests/test_path.rs @@ -0,0 +1,37 @@ +use ref_cast::RefCast; +use std::fmt::Display; +use std::path::{Path, PathBuf}; +use xet_error::Error; + +#[derive(Error, Debug)] +#[error("failed to read '{file}'")] +struct StructPathBuf { + file: PathBuf, +} + +#[derive(Error, Debug, RefCast)] +#[repr(C)] +#[error("failed to read '{file}'")] +struct StructPath { + file: Path, +} + +#[derive(Error, Debug)] +enum EnumPathBuf { + #[error("failed to read '{0}'")] + Read(PathBuf), +} + +fn assert(expected: &str, value: T) { + assert_eq!(expected, value.to_string()); +} + +#[test] +fn test_display() { + let path = Path::new("/thiserror"); + let file = path.to_owned(); + assert("failed to read '/thiserror'", StructPathBuf { file }); + let file = path.to_owned(); + assert("failed to read '/thiserror'", EnumPathBuf::Read(file)); + assert("failed to read '/thiserror'", StructPath::ref_cast(path)); +} diff --git a/xet_error/tests/test_source.rs b/xet_error/tests/test_source.rs new file mode 100644 index 00000000..99158d1b --- /dev/null +++ b/xet_error/tests/test_source.rs @@ -0,0 +1,65 @@ +use std::error::Error as StdError; +use std::io; +use xet_error::Error; + +#[derive(Error, Debug)] +#[error("implicit source")] +pub struct ImplicitSource { + source: io::Error, +} + +#[derive(Error, Debug)] +#[error("explicit source")] +pub struct ExplicitSource { + source: String, + #[source] + io: io::Error, +} + +#[derive(Error, Debug)] +#[error("boxed source")] +pub struct BoxedSource { + #[source] + source: Box, +} + +#[test] +fn test_implicit_source() { + let io = io::Error::new(io::ErrorKind::Other, "oh no!"); + let error = ImplicitSource { source: io }; + error.source().unwrap().downcast_ref::().unwrap(); +} + +#[test] +fn test_explicit_source() { + let io = io::Error::new(io::ErrorKind::Other, "oh no!"); + let error = ExplicitSource { + source: String::new(), + io, + }; + error.source().unwrap().downcast_ref::().unwrap(); +} + +#[test] +fn test_boxed_source() { + let source = Box::new(io::Error::new(io::ErrorKind::Other, "oh no!")); + let error = BoxedSource { source }; + error.source().unwrap().downcast_ref::().unwrap(); +} + +macro_rules! error_from_macro { + ($($variants:tt)*) => { + #[derive(Error)] + #[derive(Debug)] + pub enum MacroSource { + $($variants)* + } + } +} + +// Test that we generate impls with the proper hygiene +#[rustfmt::skip] +error_from_macro! { + #[error("Something")] + Variant(#[from] io::Error) +} diff --git a/xet_error/tests/test_transparent.rs b/xet_error/tests/test_transparent.rs new file mode 100644 index 00000000..30803766 --- /dev/null +++ b/xet_error/tests/test_transparent.rs @@ -0,0 +1,78 @@ +use anyhow::anyhow; +use std::error::Error as _; +use std::io; +use xet_error::Error; + +#[test] +fn test_transparent_struct() { + #[derive(Error, Debug)] + #[error(transparent)] + struct Error(ErrorKind); + + #[derive(Error, Debug)] + enum ErrorKind { + #[error("E0")] + E0, + #[error("E1")] + E1(#[from] io::Error), + } + + let error = Error(ErrorKind::E0); + assert_eq!("E0", error.to_string()); + assert!(error.source().is_none()); + + let io = io::Error::new(io::ErrorKind::Other, "oh no!"); + let error = Error(ErrorKind::from(io)); + assert_eq!("E1", error.to_string()); + error.source().unwrap().downcast_ref::().unwrap(); +} + +#[test] +fn test_transparent_enum() { + #[derive(Error, Debug)] + enum Error { + #[error("this failed")] + This, + #[error(transparent)] + Other(anyhow::Error), + } + + let error = Error::This; + assert_eq!("this failed", error.to_string()); + + let error = Error::Other(anyhow!("inner").context("outer")); + assert_eq!("outer", error.to_string()); + assert_eq!("inner", error.source().unwrap().to_string()); +} + +#[test] +fn test_anyhow() { + #[derive(Error, Debug)] + #[error(transparent)] + struct Any(#[from] anyhow::Error); + + let error = Any::from(anyhow!("inner").context("outer")); + assert_eq!("outer", error.to_string()); + assert_eq!("inner", error.source().unwrap().to_string()); +} + +#[test] +fn test_non_static() { + #[derive(Error, Debug)] + #[error(transparent)] + struct Error<'a> { + inner: ErrorKind<'a>, + } + + #[derive(Error, Debug)] + enum ErrorKind<'a> { + #[error("unexpected token: {:?}", token)] + Unexpected { token: &'a str }, + } + + let error = Error { + inner: ErrorKind::Unexpected { token: "error" }, + }; + assert_eq!("unexpected token: \"error\"", error.to_string()); + assert!(error.source().is_none()); +} diff --git a/xet_error/tests/ui/bad-field-attr.rs b/xet_error/tests/ui/bad-field-attr.rs new file mode 100644 index 00000000..a02992c4 --- /dev/null +++ b/xet_error/tests/ui/bad-field-attr.rs @@ -0,0 +1,7 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error(#[error(transparent)] std::io::Error); + +fn main() {} diff --git a/xet_error/tests/ui/bad-field-attr.stderr b/xet_error/tests/ui/bad-field-attr.stderr new file mode 100644 index 00000000..5fb57441 --- /dev/null +++ b/xet_error/tests/ui/bad-field-attr.stderr @@ -0,0 +1,5 @@ +error: #[error(transparent)] needs to go outside the enum or struct, not on an individual field + --> tests/ui/bad-field-attr.rs:5:18 + | +5 | pub struct Error(#[error(transparent)] std::io::Error); + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/concat-display.rs b/xet_error/tests/ui/concat-display.rs new file mode 100644 index 00000000..b99f58ad --- /dev/null +++ b/xet_error/tests/ui/concat-display.rs @@ -0,0 +1,15 @@ +use xet_error::Error; + +macro_rules! error_type { + ($name:ident, $what:expr) => { + // Use #[error("invalid {}", $what)] instead. + + #[derive(Error, Debug)] + #[error(concat!("invalid ", $what))] + pub struct $name; + }; +} + +error_type!(Error, "foo"); + +fn main() {} diff --git a/xet_error/tests/ui/concat-display.stderr b/xet_error/tests/ui/concat-display.stderr new file mode 100644 index 00000000..dbecd69f --- /dev/null +++ b/xet_error/tests/ui/concat-display.stderr @@ -0,0 +1,10 @@ +error: expected string literal + --> tests/ui/concat-display.rs:8:17 + | +8 | #[error(concat!("invalid ", $what))] + | ^^^^^^ +... +13 | error_type!(Error, "foo"); + | ------------------------- in this macro invocation + | + = note: this error originates in the macro `error_type` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/xet_error/tests/ui/duplicate-enum-source.rs b/xet_error/tests/ui/duplicate-enum-source.rs new file mode 100644 index 00000000..c5e6981c --- /dev/null +++ b/xet_error/tests/ui/duplicate-enum-source.rs @@ -0,0 +1,13 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum ErrorEnum { + Confusing { + #[source] + a: std::io::Error, + #[source] + b: anyhow::Error, + }, +} + +fn main() {} diff --git a/xet_error/tests/ui/duplicate-enum-source.stderr b/xet_error/tests/ui/duplicate-enum-source.stderr new file mode 100644 index 00000000..4a4b2d39 --- /dev/null +++ b/xet_error/tests/ui/duplicate-enum-source.stderr @@ -0,0 +1,5 @@ +error: duplicate #[source] attribute + --> tests/ui/duplicate-enum-source.rs:8:9 + | +8 | #[source] + | ^^^^^^^^^ diff --git a/xet_error/tests/ui/duplicate-fmt.rs b/xet_error/tests/ui/duplicate-fmt.rs new file mode 100644 index 00000000..034967f9 --- /dev/null +++ b/xet_error/tests/ui/duplicate-fmt.rs @@ -0,0 +1,8 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[error("...")] +#[error("...")] +pub struct Error; + +fn main() {} diff --git a/xet_error/tests/ui/duplicate-fmt.stderr b/xet_error/tests/ui/duplicate-fmt.stderr new file mode 100644 index 00000000..532b16bd --- /dev/null +++ b/xet_error/tests/ui/duplicate-fmt.stderr @@ -0,0 +1,5 @@ +error: only one #[error(...)] attribute is allowed + --> tests/ui/duplicate-fmt.rs:5:1 + | +5 | #[error("...")] + | ^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/duplicate-struct-source.rs b/xet_error/tests/ui/duplicate-struct-source.rs new file mode 100644 index 00000000..01d706e6 --- /dev/null +++ b/xet_error/tests/ui/duplicate-struct-source.rs @@ -0,0 +1,11 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +pub struct ErrorStruct { + #[source] + a: std::io::Error, + #[source] + b: anyhow::Error, +} + +fn main() {} diff --git a/xet_error/tests/ui/duplicate-struct-source.stderr b/xet_error/tests/ui/duplicate-struct-source.stderr new file mode 100644 index 00000000..c8de5747 --- /dev/null +++ b/xet_error/tests/ui/duplicate-struct-source.stderr @@ -0,0 +1,5 @@ +error: duplicate #[source] attribute + --> tests/ui/duplicate-struct-source.rs:7:5 + | +7 | #[source] + | ^^^^^^^^^ diff --git a/xet_error/tests/ui/duplicate-transparent.rs b/xet_error/tests/ui/duplicate-transparent.rs new file mode 100644 index 00000000..8fbe57ae --- /dev/null +++ b/xet_error/tests/ui/duplicate-transparent.rs @@ -0,0 +1,8 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +#[error(transparent)] +pub struct Error(anyhow::Error); + +fn main() {} diff --git a/xet_error/tests/ui/duplicate-transparent.stderr b/xet_error/tests/ui/duplicate-transparent.stderr new file mode 100644 index 00000000..a8308790 --- /dev/null +++ b/xet_error/tests/ui/duplicate-transparent.stderr @@ -0,0 +1,5 @@ +error: duplicate #[error(transparent)] attribute + --> tests/ui/duplicate-transparent.rs:5:1 + | +5 | #[error(transparent)] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/from-backtrace-backtrace.rs b/xet_error/tests/ui/from-backtrace-backtrace.rs new file mode 100644 index 00000000..cb6203a5 --- /dev/null +++ b/xet_error/tests/ui/from-backtrace-backtrace.rs @@ -0,0 +1,15 @@ +// https://github.com/dtolnay/thiserror/issues/163 + +use std::backtrace::Backtrace; +use xet_error::Error; + +#[derive(Error, Debug)] +#[error("...")] +pub struct Error( + #[from] + #[backtrace] + std::io::Error, + Backtrace, +); + +fn main() {} diff --git a/xet_error/tests/ui/from-backtrace-backtrace.stderr b/xet_error/tests/ui/from-backtrace-backtrace.stderr new file mode 100644 index 00000000..5c0b9a3b --- /dev/null +++ b/xet_error/tests/ui/from-backtrace-backtrace.stderr @@ -0,0 +1,5 @@ +error: deriving From requires no fields other than source and backtrace + --> tests/ui/from-backtrace-backtrace.rs:9:5 + | +9 | #[from] + | ^^^^^^^ diff --git a/xet_error/tests/ui/from-not-source.rs b/xet_error/tests/ui/from-not-source.rs new file mode 100644 index 00000000..bca0261a --- /dev/null +++ b/xet_error/tests/ui/from-not-source.rs @@ -0,0 +1,11 @@ +use xet_error::Error; + +#[derive(Debug, Error)] +pub struct Error { + #[source] + source: std::io::Error, + #[from] + other: anyhow::Error, +} + +fn main() {} diff --git a/xet_error/tests/ui/from-not-source.stderr b/xet_error/tests/ui/from-not-source.stderr new file mode 100644 index 00000000..97136017 --- /dev/null +++ b/xet_error/tests/ui/from-not-source.stderr @@ -0,0 +1,5 @@ +error: #[from] is only supported on the source field, not any other field + --> tests/ui/from-not-source.rs:7:5 + | +7 | #[from] + | ^^^^^^^ diff --git a/xet_error/tests/ui/lifetime.rs b/xet_error/tests/ui/lifetime.rs new file mode 100644 index 00000000..00946b6c --- /dev/null +++ b/xet_error/tests/ui/lifetime.rs @@ -0,0 +1,24 @@ +use std::fmt::Debug; +use xet_error::Error; + +#[derive(Error, Debug)] +#[error("error")] +struct Error<'a>(#[from] Inner<'a>); + +#[derive(Error, Debug)] +#[error("{0}")] +struct Inner<'a>(&'a str); + +#[derive(Error, Debug)] +enum Enum<'a> { + #[error("error")] + Foo(#[from] Generic<&'a str>), +} + +#[derive(Error, Debug)] +#[error("{0:?}")] +struct Generic(T); + +fn main() -> Result<(), Error<'static>> { + Err(Error(Inner("some text"))) +} diff --git a/xet_error/tests/ui/lifetime.stderr b/xet_error/tests/ui/lifetime.stderr new file mode 100644 index 00000000..8b58136e --- /dev/null +++ b/xet_error/tests/ui/lifetime.stderr @@ -0,0 +1,11 @@ +error: non-static lifetimes are not allowed in the source of an error, because std::error::Error requires the source is dyn Error + 'static + --> tests/ui/lifetime.rs:6:26 + | +6 | struct Error<'a>(#[from] Inner<'a>); + | ^^^^^^^^^ + +error: non-static lifetimes are not allowed in the source of an error, because std::error::Error requires the source is dyn Error + 'static + --> tests/ui/lifetime.rs:15:17 + | +15 | Foo(#[from] Generic<&'a str>), + | ^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/missing-fmt.rs b/xet_error/tests/ui/missing-fmt.rs new file mode 100644 index 00000000..f424ee0f --- /dev/null +++ b/xet_error/tests/ui/missing-fmt.rs @@ -0,0 +1,10 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("...")] + A(usize), + B(usize), +} + +fn main() {} diff --git a/xet_error/tests/ui/missing-fmt.stderr b/xet_error/tests/ui/missing-fmt.stderr new file mode 100644 index 00000000..c0be3735 --- /dev/null +++ b/xet_error/tests/ui/missing-fmt.stderr @@ -0,0 +1,5 @@ +error: missing #[error("...")] display attribute + --> tests/ui/missing-fmt.rs:7:5 + | +7 | B(usize), + | ^^^^^^^^ diff --git a/xet_error/tests/ui/no-display.rs b/xet_error/tests/ui/no-display.rs new file mode 100644 index 00000000..38205157 --- /dev/null +++ b/xet_error/tests/ui/no-display.rs @@ -0,0 +1,12 @@ +use xet_error::Error; + +#[derive(Debug)] +struct NoDisplay; + +#[derive(Error, Debug)] +#[error("thread: {thread}")] +pub struct Error { + thread: NoDisplay, +} + +fn main() {} diff --git a/xet_error/tests/ui/no-display.stderr b/xet_error/tests/ui/no-display.stderr new file mode 100644 index 00000000..0f47c24b --- /dev/null +++ b/xet_error/tests/ui/no-display.stderr @@ -0,0 +1,17 @@ +error[E0599]: the method `as_display` exists for reference `&NoDisplay`, but its trait bounds were not satisfied + --> tests/ui/no-display.rs:7:9 + | +4 | struct NoDisplay; + | ---------------- doesn't satisfy `NoDisplay: std::fmt::Display` +... +7 | #[error("thread: {thread}")] + | ^^^^^^^^^^^^^^^^^^ method cannot be called on `&NoDisplay` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NoDisplay: std::fmt::Display` + which is required by `&NoDisplay: AsDisplay<'_>` +note: the trait `std::fmt::Display` must be implemented + --> $RUST/core/src/fmt/mod.rs + | + | pub trait Display { + | ^^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/source-enum-not-error.rs b/xet_error/tests/ui/source-enum-not-error.rs new file mode 100644 index 00000000..43142ff9 --- /dev/null +++ b/xet_error/tests/ui/source-enum-not-error.rs @@ -0,0 +1,12 @@ +use xet_error::Error; + +#[derive(Debug)] +pub struct NotError; + +#[derive(Error, Debug)] +#[error("...")] +pub enum ErrorEnum { + Broken { source: NotError }, +} + +fn main() {} diff --git a/xet_error/tests/ui/source-enum-not-error.stderr b/xet_error/tests/ui/source-enum-not-error.stderr new file mode 100644 index 00000000..4c44742d --- /dev/null +++ b/xet_error/tests/ui/source-enum-not-error.stderr @@ -0,0 +1,22 @@ +error[E0599]: the method `as_dyn_error` exists for reference `&NotError`, but its trait bounds were not satisfied + --> tests/ui/source-enum-not-error.rs:9:14 + | +4 | pub struct NotError; + | ------------------- + | | + | doesn't satisfy `NotError: AsDynError<'_>` + | doesn't satisfy `NotError: std::error::Error` +... +9 | Broken { source: NotError }, + | ^^^^^^ method cannot be called on `&NotError` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotError: std::error::Error` + which is required by `NotError: AsDynError<'_>` + `&NotError: std::error::Error` + which is required by `&NotError: AsDynError<'_>` +note: the trait `std::error::Error` must be implemented + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/source-enum-unnamed-field-not-error.rs b/xet_error/tests/ui/source-enum-unnamed-field-not-error.rs new file mode 100644 index 00000000..16b37bc7 --- /dev/null +++ b/xet_error/tests/ui/source-enum-unnamed-field-not-error.rs @@ -0,0 +1,12 @@ +use xet_error::Error; + +#[derive(Debug)] +pub struct NotError; + +#[derive(Error, Debug)] +#[error("...")] +pub enum ErrorEnum { + Broken(#[source] NotError), +} + +fn main() {} diff --git a/xet_error/tests/ui/source-enum-unnamed-field-not-error.stderr b/xet_error/tests/ui/source-enum-unnamed-field-not-error.stderr new file mode 100644 index 00000000..da6d225f --- /dev/null +++ b/xet_error/tests/ui/source-enum-unnamed-field-not-error.stderr @@ -0,0 +1,22 @@ +error[E0599]: the method `as_dyn_error` exists for reference `&NotError`, but its trait bounds were not satisfied + --> tests/ui/source-enum-unnamed-field-not-error.rs:9:14 + | +4 | pub struct NotError; + | ------------------- + | | + | doesn't satisfy `NotError: AsDynError<'_>` + | doesn't satisfy `NotError: std::error::Error` +... +9 | Broken(#[source] NotError), + | ^^^^^^ method cannot be called on `&NotError` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotError: std::error::Error` + which is required by `NotError: AsDynError<'_>` + `&NotError: std::error::Error` + which is required by `&NotError: AsDynError<'_>` +note: the trait `std::error::Error` must be implemented + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/source-struct-not-error.rs b/xet_error/tests/ui/source-struct-not-error.rs new file mode 100644 index 00000000..f5378486 --- /dev/null +++ b/xet_error/tests/ui/source-struct-not-error.rs @@ -0,0 +1,12 @@ +use xet_error::Error; + +#[derive(Debug)] +struct NotError; + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorStruct { + source: NotError, +} + +fn main() {} diff --git a/xet_error/tests/ui/source-struct-not-error.stderr b/xet_error/tests/ui/source-struct-not-error.stderr new file mode 100644 index 00000000..b98460fc --- /dev/null +++ b/xet_error/tests/ui/source-struct-not-error.stderr @@ -0,0 +1,21 @@ +error[E0599]: the method `as_dyn_error` exists for struct `NotError`, but its trait bounds were not satisfied + --> tests/ui/source-struct-not-error.rs:9:5 + | +4 | struct NotError; + | --------------- + | | + | method `as_dyn_error` not found for this struct + | doesn't satisfy `NotError: AsDynError<'_>` + | doesn't satisfy `NotError: std::error::Error` +... +9 | source: NotError, + | ^^^^^^ method cannot be called on `NotError` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotError: std::error::Error` + which is required by `NotError: AsDynError<'_>` +note: the trait `std::error::Error` must be implemented + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/source-struct-unnamed-field-not-error.rs b/xet_error/tests/ui/source-struct-unnamed-field-not-error.rs new file mode 100644 index 00000000..ecf0be1d --- /dev/null +++ b/xet_error/tests/ui/source-struct-unnamed-field-not-error.rs @@ -0,0 +1,10 @@ +use xet_error::Error; + +#[derive(Debug)] +struct NotError; + +#[derive(Error, Debug)] +#[error("...")] +pub struct ErrorStruct(#[source] NotError); + +fn main() {} diff --git a/xet_error/tests/ui/source-struct-unnamed-field-not-error.stderr b/xet_error/tests/ui/source-struct-unnamed-field-not-error.stderr new file mode 100644 index 00000000..a23f2682 --- /dev/null +++ b/xet_error/tests/ui/source-struct-unnamed-field-not-error.stderr @@ -0,0 +1,21 @@ +error[E0599]: the method `as_dyn_error` exists for struct `NotError`, but its trait bounds were not satisfied + --> tests/ui/source-struct-unnamed-field-not-error.rs:8:26 + | +4 | struct NotError; + | --------------- + | | + | method `as_dyn_error` not found for this struct + | doesn't satisfy `NotError: AsDynError<'_>` + | doesn't satisfy `NotError: std::error::Error` +... +8 | pub struct ErrorStruct(#[source] NotError); + | ^^^^^^ method cannot be called on `NotError` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotError: std::error::Error` + which is required by `NotError: AsDynError<'_>` +note: the trait `std::error::Error` must be implemented + --> $RUST/core/src/error.rs + | + | pub trait Error: Debug + Display { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/transparent-display.rs b/xet_error/tests/ui/transparent-display.rs new file mode 100644 index 00000000..1e3ae263 --- /dev/null +++ b/xet_error/tests/ui/transparent-display.rs @@ -0,0 +1,8 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +#[error("...")] +pub struct Error(anyhow::Error); + +fn main() {} diff --git a/xet_error/tests/ui/transparent-display.stderr b/xet_error/tests/ui/transparent-display.stderr new file mode 100644 index 00000000..54d958b2 --- /dev/null +++ b/xet_error/tests/ui/transparent-display.stderr @@ -0,0 +1,5 @@ +error: cannot have both #[error(transparent)] and a display attribute + --> tests/ui/transparent-display.rs:5:1 + | +5 | #[error("...")] + | ^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/transparent-enum-many.rs b/xet_error/tests/ui/transparent-enum-many.rs new file mode 100644 index 00000000..7f621ef2 --- /dev/null +++ b/xet_error/tests/ui/transparent-enum-many.rs @@ -0,0 +1,9 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Other(anyhow::Error, String), +} + +fn main() {} diff --git a/xet_error/tests/ui/transparent-enum-many.stderr b/xet_error/tests/ui/transparent-enum-many.stderr new file mode 100644 index 00000000..a9adfa5a --- /dev/null +++ b/xet_error/tests/ui/transparent-enum-many.stderr @@ -0,0 +1,6 @@ +error: #[error(transparent)] requires exactly one field + --> tests/ui/transparent-enum-many.rs:5:5 + | +5 | / #[error(transparent)] +6 | | Other(anyhow::Error, String), + | |________________________________^ diff --git a/xet_error/tests/ui/transparent-enum-not-error.rs b/xet_error/tests/ui/transparent-enum-not-error.rs new file mode 100644 index 00000000..c35456bf --- /dev/null +++ b/xet_error/tests/ui/transparent-enum-not-error.rs @@ -0,0 +1,9 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Other { message: String }, +} + +fn main() {} diff --git a/xet_error/tests/ui/transparent-enum-not-error.stderr b/xet_error/tests/ui/transparent-enum-not-error.stderr new file mode 100644 index 00000000..9be51434 --- /dev/null +++ b/xet_error/tests/ui/transparent-enum-not-error.stderr @@ -0,0 +1,23 @@ +error[E0599]: the method `as_dyn_error` exists for reference `&String`, but its trait bounds were not satisfied + --> tests/ui/transparent-enum-not-error.rs:5:13 + | +5 | #[error(transparent)] + | ^^^^^^^^^^^ method cannot be called on `&String` due to unsatisfied trait bounds + | + ::: $RUST/alloc/src/string.rs + | + | pub struct String { + | ----------------- + | | + | doesn't satisfy `String: AsDynError<'_>` + | doesn't satisfy `String: std::error::Error` + | + = note: the following trait bounds were not satisfied: + `String: std::error::Error` + which is required by `String: AsDynError<'_>` + `&String: std::error::Error` + which is required by `&String: AsDynError<'_>` + `str: Sized` + which is required by `str: AsDynError<'_>` + `str: std::error::Error` + which is required by `str: AsDynError<'_>` diff --git a/xet_error/tests/ui/transparent-enum-source.rs b/xet_error/tests/ui/transparent-enum-source.rs new file mode 100644 index 00000000..8ab61abe --- /dev/null +++ b/xet_error/tests/ui/transparent-enum-source.rs @@ -0,0 +1,9 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Other(#[source] anyhow::Error), +} + +fn main() {} diff --git a/xet_error/tests/ui/transparent-enum-source.stderr b/xet_error/tests/ui/transparent-enum-source.stderr new file mode 100644 index 00000000..ccb90677 --- /dev/null +++ b/xet_error/tests/ui/transparent-enum-source.stderr @@ -0,0 +1,5 @@ +error: transparent variant can't contain #[source] + --> tests/ui/transparent-enum-source.rs:6:11 + | +6 | Other(#[source] anyhow::Error), + | ^^^^^^^^^ diff --git a/xet_error/tests/ui/transparent-enum-unnamed-field-not-error.rs b/xet_error/tests/ui/transparent-enum-unnamed-field-not-error.rs new file mode 100644 index 00000000..c396dbd2 --- /dev/null +++ b/xet_error/tests/ui/transparent-enum-unnamed-field-not-error.rs @@ -0,0 +1,9 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Other(String), +} + +fn main() {} diff --git a/xet_error/tests/ui/transparent-enum-unnamed-field-not-error.stderr b/xet_error/tests/ui/transparent-enum-unnamed-field-not-error.stderr new file mode 100644 index 00000000..3d23c3a0 --- /dev/null +++ b/xet_error/tests/ui/transparent-enum-unnamed-field-not-error.stderr @@ -0,0 +1,23 @@ +error[E0599]: the method `as_dyn_error` exists for reference `&String`, but its trait bounds were not satisfied + --> tests/ui/transparent-enum-unnamed-field-not-error.rs:5:13 + | +5 | #[error(transparent)] + | ^^^^^^^^^^^ method cannot be called on `&String` due to unsatisfied trait bounds + | + ::: $RUST/alloc/src/string.rs + | + | pub struct String { + | ----------------- + | | + | doesn't satisfy `String: AsDynError<'_>` + | doesn't satisfy `String: std::error::Error` + | + = note: the following trait bounds were not satisfied: + `String: std::error::Error` + which is required by `String: AsDynError<'_>` + `&String: std::error::Error` + which is required by `&String: AsDynError<'_>` + `str: Sized` + which is required by `str: AsDynError<'_>` + `str: std::error::Error` + which is required by `str: AsDynError<'_>` diff --git a/xet_error/tests/ui/transparent-struct-many.rs b/xet_error/tests/ui/transparent-struct-many.rs new file mode 100644 index 00000000..eefbb80f --- /dev/null +++ b/xet_error/tests/ui/transparent-struct-many.rs @@ -0,0 +1,10 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error { + inner: anyhow::Error, + what: String, +} + +fn main() {} diff --git a/xet_error/tests/ui/transparent-struct-many.stderr b/xet_error/tests/ui/transparent-struct-many.stderr new file mode 100644 index 00000000..c0e3806e --- /dev/null +++ b/xet_error/tests/ui/transparent-struct-many.stderr @@ -0,0 +1,5 @@ +error: #[error(transparent)] requires exactly one field + --> tests/ui/transparent-struct-many.rs:4:1 + | +4 | #[error(transparent)] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/transparent-struct-not-error.rs b/xet_error/tests/ui/transparent-struct-not-error.rs new file mode 100644 index 00000000..c0fcb6e7 --- /dev/null +++ b/xet_error/tests/ui/transparent-struct-not-error.rs @@ -0,0 +1,9 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error { + message: String, +} + +fn main() {} diff --git a/xet_error/tests/ui/transparent-struct-not-error.stderr b/xet_error/tests/ui/transparent-struct-not-error.stderr new file mode 100644 index 00000000..d67a6944 --- /dev/null +++ b/xet_error/tests/ui/transparent-struct-not-error.stderr @@ -0,0 +1,21 @@ +error[E0599]: the method `as_dyn_error` exists for struct `String`, but its trait bounds were not satisfied + --> tests/ui/transparent-struct-not-error.rs:4:9 + | +4 | #[error(transparent)] + | ^^^^^^^^^^^ method cannot be called on `String` due to unsatisfied trait bounds + | + ::: $RUST/alloc/src/string.rs + | + | pub struct String { + | ----------------- + | | + | doesn't satisfy `String: AsDynError<'_>` + | doesn't satisfy `String: std::error::Error` + | + = note: the following trait bounds were not satisfied: + `String: std::error::Error` + which is required by `String: AsDynError<'_>` + `str: Sized` + which is required by `str: AsDynError<'_>` + `str: std::error::Error` + which is required by `str: AsDynError<'_>` diff --git a/xet_error/tests/ui/transparent-struct-source.rs b/xet_error/tests/ui/transparent-struct-source.rs new file mode 100644 index 00000000..b3b9234c --- /dev/null +++ b/xet_error/tests/ui/transparent-struct-source.rs @@ -0,0 +1,7 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error(#[source] anyhow::Error); + +fn main() {} diff --git a/xet_error/tests/ui/transparent-struct-source.stderr b/xet_error/tests/ui/transparent-struct-source.stderr new file mode 100644 index 00000000..3012ca31 --- /dev/null +++ b/xet_error/tests/ui/transparent-struct-source.stderr @@ -0,0 +1,5 @@ +error: transparent error struct can't contain #[source] + --> tests/ui/transparent-struct-source.rs:5:18 + | +5 | pub struct Error(#[source] anyhow::Error); + | ^^^^^^^^^ diff --git a/xet_error/tests/ui/transparent-struct-unnamed-field-not-error.rs b/xet_error/tests/ui/transparent-struct-unnamed-field-not-error.rs new file mode 100644 index 00000000..175d2111 --- /dev/null +++ b/xet_error/tests/ui/transparent-struct-unnamed-field-not-error.rs @@ -0,0 +1,7 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct Error(String); + +fn main() {} diff --git a/xet_error/tests/ui/transparent-struct-unnamed-field-not-error.stderr b/xet_error/tests/ui/transparent-struct-unnamed-field-not-error.stderr new file mode 100644 index 00000000..f715a151 --- /dev/null +++ b/xet_error/tests/ui/transparent-struct-unnamed-field-not-error.stderr @@ -0,0 +1,21 @@ +error[E0599]: the method `as_dyn_error` exists for struct `String`, but its trait bounds were not satisfied + --> tests/ui/transparent-struct-unnamed-field-not-error.rs:4:9 + | +4 | #[error(transparent)] + | ^^^^^^^^^^^ method cannot be called on `String` due to unsatisfied trait bounds + | + ::: $RUST/alloc/src/string.rs + | + | pub struct String { + | ----------------- + | | + | doesn't satisfy `String: AsDynError<'_>` + | doesn't satisfy `String: std::error::Error` + | + = note: the following trait bounds were not satisfied: + `String: std::error::Error` + which is required by `String: AsDynError<'_>` + `str: Sized` + which is required by `str: AsDynError<'_>` + `str: std::error::Error` + which is required by `str: AsDynError<'_>` diff --git a/xet_error/tests/ui/unexpected-field-fmt.rs b/xet_error/tests/ui/unexpected-field-fmt.rs new file mode 100644 index 00000000..d260e4aa --- /dev/null +++ b/xet_error/tests/ui/unexpected-field-fmt.rs @@ -0,0 +1,11 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +pub enum Error { + What { + #[error("...")] + io: std::io::Error, + }, +} + +fn main() {} diff --git a/xet_error/tests/ui/unexpected-field-fmt.stderr b/xet_error/tests/ui/unexpected-field-fmt.stderr new file mode 100644 index 00000000..bf3c24df --- /dev/null +++ b/xet_error/tests/ui/unexpected-field-fmt.stderr @@ -0,0 +1,5 @@ +error: not expected here; the #[error(...)] attribute belongs on top of a struct or an enum variant + --> tests/ui/unexpected-field-fmt.rs:6:9 + | +6 | #[error("...")] + | ^^^^^^^^^^^^^^^ diff --git a/xet_error/tests/ui/unexpected-struct-source.rs b/xet_error/tests/ui/unexpected-struct-source.rs new file mode 100644 index 00000000..5aba7da4 --- /dev/null +++ b/xet_error/tests/ui/unexpected-struct-source.rs @@ -0,0 +1,7 @@ +use xet_error::Error; + +#[derive(Error, Debug)] +#[source] +pub struct Error; + +fn main() {} diff --git a/xet_error/tests/ui/unexpected-struct-source.stderr b/xet_error/tests/ui/unexpected-struct-source.stderr new file mode 100644 index 00000000..6f15841d --- /dev/null +++ b/xet_error/tests/ui/unexpected-struct-source.stderr @@ -0,0 +1,5 @@ +error: not expected here; the #[source] attribute belongs on a specific field + --> tests/ui/unexpected-struct-source.rs:4:1 + | +4 | #[source] + | ^^^^^^^^^ diff --git a/xet_error/tests/ui/union.rs b/xet_error/tests/ui/union.rs new file mode 100644 index 00000000..bebe4c88 --- /dev/null +++ b/xet_error/tests/ui/union.rs @@ -0,0 +1,9 @@ +use xet_error::Error; + +#[derive(Error)] +pub union U { + msg: &'static str, + num: usize, +} + +fn main() {} diff --git a/xet_error/tests/ui/union.stderr b/xet_error/tests/ui/union.stderr new file mode 100644 index 00000000..3ec4d71c --- /dev/null +++ b/xet_error/tests/ui/union.stderr @@ -0,0 +1,8 @@ +error: union as errors are not supported + --> tests/ui/union.rs:4:1 + | +4 | / pub union U { +5 | | msg: &'static str, +6 | | num: usize, +7 | | } + | |_^